MCP Solver

repository·main·Indexed 41 days ago

https://github.com/szeider/mcp-solver

An MCP server and CLI tool for LLM-driven constraint solving (SAT, MaxSAT, SMT, CP, ASP). Version 4.1.0 utilizes a persistent IPython kernel to allow LLM hosts to write, execute, and verify solver programs. It includes mcp-minion, a minimal ReAct agent for batch runs, and a benchmark harness (mcp-solver-bench) to validate problems against ground truth.

Tokens
33K
Snippets
91
Records
128
Agent score
52%

What's inside MCP Solver

  1. Avoid grounding explosion with aggregate decomposition

    main

    Writing a single, large arithmetic constraint involving many independent variables causes a 'grounding explosion' because the grounder must instantiate the rule for every possible combination of values.

    The Principle: Aggregate, Don't Enumerate. Use auxiliary predicates and aggregates (#sum, #count, etc.) to compute intermediate results, then write simpler constraints over those results.

    % ANTI-PATTERN: Monolithic arithmetic (10,000 ground rules for 4 vars in 1..10)
    :- assign(a, N1), assign(b, N2), assign(c, N3), assign(d, N4), N1 + N2 != N3 + N4.
    
    % GOOD PATTERN: Decompose with #sum (highly optimized)
    sum_ab(S) :- S = #sum { N,V : assign(V,N), (V=a; V=b) }.
    sum_cd(S) :- S = #sum { N,V : assign(V,N), (V=c; V=d) }.
    :- sum_ab(S1), sum_cd(S2), S1 != S2.
  2. Use the Eliminative Mindset for Constraints

    main

    ASP constraints (:-) work by elimination, not assertion. Instead of stating what is valid, you must state what is invalid. Think: "What makes a solution invalid?" then write :- invalid_condition.

    Common Constraint Patterns

    GoalLogicASP Pattern
    Enforce $X \ge Y$Forbid $X < Y$:- val(X), limit(Y), X < Y.
    Enforce $X \neq \text{value}$Forbid $X = \text{value}$:- status(X, "invalid").
    At least one selectionForbid having none:- not selected(_).
    Mutual ExclusionForbid both being true:- has(X, hot), has(X, cold).
  3. Implement Multi-Objective Optimization

    main

    When using multiple #minimize or #maximize statements, Clingo optimizes them level-by-level in the order they are defined. This allows you to handle complex trade-offs by setting priorities.

    Optimization Order Example:

    % 1. Highest priority: Maximize AI performance
    #maximize { Score*3 : ai_performance(Score) }.
    
    % 2. Second priority: Maximize gaming score
    #maximize { Score*2 : gaming_score(Score) }.
    
    % 3. Third priority: Maximize user preference
    #maximize { Score : user_preference(Score) }.
    
    % 4. Lowest priority: Minimize cost
    #minimize { Cost : total_cost(Cost) }.
    % Clingo will first satisfy the highest priority level before moving to the next
    #maximize { Score*3 : ai_performance(Score) }.
    #minimize { Cost : total_cost(Cost) }.
  4. Compare v3 and v4 architectures

    main

    The project has evolved from v3 (model-editing via tools) to v4 (programmatic solving via IPython kernel).

    Featurev3 (SAT 2025)v4 (CP-Agent / ASP-Bench)
    Who does the workHost chat LLM via MCP toolsHost LLM (or mcp-minion) as programmer
    Unit of workModel items (add_item, etc.)Complete Python solver program
    ExecutionInside the MCP serverIn a persistent IPython kernel (ipython_mcp)
    VerificationManual by host LLMBuilt-in: run $\rightarrow$ check $\rightarrow$ submit_code
    ArtifactTransient model stateVerified, re-executable program
    Host interfaceFine-grained editing toolsSolving kernel: select_backend, python_exec, submit_code
    BackendsMiniZinc, PySAT, MaxSAT, Z3, ASPPySAT, MaxSAT, Z3, CPMpy, Clingo, DIDP
  5. Model multi-state sequences (Constrain Runs, Not Gaps)

    main

    In multi-state problems (like colored nonograms or multi-skill scheduling), cells/slots of one state can act as separators for runs of another state.

    The Anti-Pattern: Requiring every non-empty cell to belong to a run. This over-constrains the model and causes false UNSAT results because it forbids the 'separator' cells.

    The Correct Pattern: Focus ONLY on enforcing the properties of the runs themselves (position, length, color) and let the solver determine the separators naturally.

    1. Generate Run Positions: Use choice rules to pick starting positions for runs.
    2. Enforce Run Consistency: Constrain cells within the run to match the required color/state.
    3. Enforce Ordering: Ensure runs appear in the correct sequence.
    4. Enforce Separation for Identical States: Only require gaps between runs of the same color/state. Different states/colors are allowed to be adjacent.

    Multi-Color Nonogram Pattern:

    % 1. Define possible starting positions
    { row_run_pos(R, RunIdx, StartCol) : possible_start(StartCol) } 1 :- row_clue(R, RunIdx, _, _).
    
    % 2. Cells within run must match run's color
    :- row_run_pos(R, RunIdx, Start), row_clue(R, RunIdx, ReqColor, Len), 
       col(C), C >= Start, C < Start + Len, 
       cell(R, C, ActualColor), ActualColor != ReqColor.
    
    % 3. Runs must appear in order
    :- row_run_pos(R, I1, C1), row_run_pos(R, I2, C2), 
       row_clue(R, I1, _, L1), I2 = I1 + 1, C2 < C1 + L1.
    
    % 4. Runs of SAME color must be separated
    :- row_run_pos(R, I1, C1), row_run_pos(R, I2, C2), 
       row_clue(R, I1, SameColor, L1), row_clue(R, I2, SameColor, _), 
       I1 < I2, C2 < C1 + L1 + 1.
    % Runs of SAME color must be separated (different colors can touch)
    :- row_run_pos(R, I1, C1), row_run_pos(R, I2, C2), 
       row_clue(R, I1, SameColor, L1), row_clue(R, I2, SameColor, _), 
       I1 < I2, C2 < C1 + L1 + 1.
  6. Ensure Variable Safety and Grounding

    main

    Every variable in an ASP rule MUST be grounded. A variable is grounded if it appears in at least one positive, non-aggregate, non-arithmetic literal in the rule's body. This tells the solver which domain of values to check.

    Grounding with Negation (not)

    Any variable used inside a not literal must be grounded by a positive literal elsewhere in the same rule body.

    • Incorrect: unassigned(T) :- not assigned(T, W). (Variable W is unsafe because the solver doesn't know which workers to check).
    • Correct: unassigned(T) :- task(T), not assigned(T, _). (task(T) grounds T, and _ is used for the unknown worker).

    Grounding in Choice Rules

    Variables in choice rules {...} must be grounded either in the choice domain or in the rule body.

    • Incorrect: {chosen(X, Var1, Var2)} :- domain(X). (Var1 and Var2 are unsafe).
    • Correct (Option 1 - Choice Domain): {chosen(X, Var1, Var2) : property1(Var1), property2(Var2)} :- domain(X).
    • Correct (Option 2 - Rule Body): {chosen(X, Var1, Var2)} :- domain(X), property1(Var1), property2(Var2).
    % SAFE: Variable S is grounded by shelf(S)
    :- item(I), shelf(S), not on_shelf(I, S).
    
    % SAFE: Grounding in choice domain
    {assign(Task, Worker, Priority) : worker(Worker), priority(Priority)} :- task(Task).
  7. Enforce State Exclusivity for Fluents

    main

    A fluent is a property that changes over time. To prevent logical bugs, you must enforce mutual exclusivity: a single entity cannot occupy two different states or locations at the same time.

    Common Patterns

    1. Prevent an item from being in two places at once: :- item_at(I,X1,Y1,T), item_at(I,X2,Y2,T), (X1,Y1) != (X2,Y2).

    2. Prevent an item from being both on a grid and being carried: :- item_at(I,X,Y,T), carrying(_,I,T).

    % Enforce that an item cannot be in two different locations at time T
    :- item_at(I,X1,Y1,T), item_at(I,X2,Y2,T), (X1,Y1) != (X2,Y2).
  8. Distinguish between Hard and Weak Constraints

    main

    Choosing between integrity constraints and weak constraints determines whether a condition is a requirement or a preference.

    Integrity Constraints (:-)

    Definition: These define what is IMPOSSIBLE. They eliminate invalid answer sets entirely.

    • Example: :- overlapping_tasks(T1,T2). (No two tasks can overlap).
    • Example: :- budget_exceeded. (The budget cannot be exceeded).

    Weak Constraints (:~)

    Definition: These define what is UNDESIRABLE. They guide optimization without eliminating solutions.

    • Example: :~ delayed(Task). [1@2] (Prefer tasks that are not delayed; priority 2).
    • Example: :~ cost(C). [C@1] (Minimize cost; priority 1).
    % Hard constraint: eliminates solutions where budget is exceeded
    :- budget_exceeded.
    
    % Weak constraint: penalizes cost to guide optimization
    :~ cost(C). [C@1]
  9. Extract solutions safely from Clingo models

    main

    The clingo.Model object passed to the on_model callback is only valid during the execution of that callback.

    CRITICAL: Do not store the Model object itself in a global variable or any structure that outlives the callback. Accessing a Model object after ctl.solve() has returned will cause a segmentation fault (exit code 139) because the underlying memory has been freed.

    Correct Pattern: Inside the on_model callback, iterate through the symbols and extract the necessary data into plain Python types (strings, integers, tuples, or dictionaries).

    # WRONG - causes segmentation fault
    saved = None
    def on_model(m):
        global saved
        saved = m
    ctl.solve(on_model=on_model)
    atoms = saved.symbols(atoms=True) # SIGSEGV
    
    # CORRECT - extract data into plain Python objects
    result = []
    def on_model(m):
        for a in m.symbols(atoms=True):
            if a.match("assigned", 2):
                # Convert to plain tuple of strings
                result.append((str(a.arguments[0]), str(a.arguments[1])))
    ctl.solve(on_model=on_model)
    # 'result' is safe to use here
  10. Follow ASP syntax rules for Constants and Variables

    main

    In Answer Set Programming (ASP), distinguishing between constants and variables is critical to avoid 'unsafe variable' errors.

    • Constants (Symbols): Must start with a lowercase letter, be a number, or be enclosed in double quotes "". Use double quotes for any input data that contains uppercase letters.

      • item(apple). (lowercase)
      • cost(100). (number)
      • amino_acid(1, "H"). (quoted string)
    • Variables: Must start with an uppercase letter or an underscore _.

      • has_color(Item, Color) :- ...
      • cost(X, C) :- ...

    Common Mistakes:

    • amino_acid(1, H). $\rightarrow$ Incorrect (H is treated as an undefined variable).
    • amino_acid(1, "H"). $\rightarrow$ Correct.
    • player(P1). $\rightarrow$ Incorrect (P1 is an uppercase variable).
    • player(p1). or player("P1"). $\rightarrow$ Correct.
    % Correct usage of constants and variables
    item(apple).
    cost(100).
    amino_acid(1, "H").
    
    % Correct usage of variables in rules
    has_color(Item, Color) :- item(Item), color(Color).
  11. Understand the v4 architecture (IPython-kernel approach)

    main

    The current architecture (v4) follows an IPython-kernel approach where the host LLM (or mcp-minion for batch runs) acts as a solver programmer. Instead of using fine-grained model-editing tools, the agent works within a persistent IPython kernel (ipython_mcp) to iteratively write, execute, and verify complete, runnable Python solver programs.

    Key differences from v3:

    • Unit of work: A complete, runnable Python solver program (instead of individual model items).
    • Execution: Occurs in a persistent IPython kernel.
    • Verification: Built into the loop (run $\rightarrow$ check $\rightarrow$ submit_code).
    • Artifact: A verified, re-executable program.
    • Host interface: Uses a solving kernel with tools like select_backend, python_exec, and submit_code.
  12. Use canonical ordering for symmetric relations

    main

    When counting relationships between pairs (e.g., connections, edges, paths), symmetric definitions can lead to double-counting. This makes cardinality constraints (#count) impossible to satisfy because a single relationship is represented by multiple atoms.

    The Solution: Canonical Ordering. Enforce an ordering constraint (typically using <) to ensure each pair is represented by exactly one atom.

    % INCORRECT: Double-counts one-way paths
    oneway(R1, R2) :- conn(R1, R2, _), not conn(R2, R1, _), R2 != "Goal".
    oneway(R1, R2) :- conn(R2, R1, _), not conn(R1, R2, _), R2 != "Goal".
    
    % CORRECT: Canonical counting with ordering
    oneway_pair(R1, R2) :- R1 < R2,
        ( (conn(R1, R2, _), not conn(R2, R1, _)) ;
          (conn(R2, R1, _), not conn(R1, R2, _)) ).
    
    % Now the constraint works correctly
    :- #count { R1, R2 : oneway_pair(R1, R2) } != 1.