agentic-mcp-solver 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentic_mcp_solver-4.2.0.dist-info/METADATA +318 -0
- agentic_mcp_solver-4.2.0.dist-info/RECORD +22 -0
- agentic_mcp_solver-4.2.0.dist-info/WHEEL +4 -0
- agentic_mcp_solver-4.2.0.dist-info/entry_points.txt +5 -0
- agentic_mcp_solver-4.2.0.dist-info/licenses/LICENSE +21 -0
- mcp_solver/__init__.py +20 -0
- mcp_solver/agent/__init__.py +1 -0
- mcp_solver/agent/cli.py +496 -0
- mcp_solver/agent/harness.py +258 -0
- mcp_solver/agent/mcp_server.py +508 -0
- mcp_solver/agent/runfolder.py +159 -0
- mcp_solver/helpers/__init__.py +7 -0
- mcp_solver/helpers/maxsat.py +79 -0
- mcp_solver/helpers/pysat.py +346 -0
- mcp_solver/helpers/z3.py +213 -0
- mcp_solver/templates/__init__.py +22 -0
- mcp_solver/templates/clingo.md +1305 -0
- mcp_solver/templates/cpmpy.md +124 -0
- mcp_solver/templates/didp.md +270 -0
- mcp_solver/templates/maxsat.md +192 -0
- mcp_solver/templates/pysat.md +186 -0
- mcp_solver/templates/z3.md +165 -0
mcp_solver/agent/cli.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""mcp-solver command-line interface (v4).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
mcp-solver <solver> [task ...] [options]
|
|
5
|
+
|
|
6
|
+
Runs an mcp-minion agent that writes a solver program for a constraint
|
|
7
|
+
problem, working against the agentic-python-coder ``ipython_mcp`` kernel
|
|
8
|
+
server over stdio. The agent must submit its final program via the server's
|
|
9
|
+
``submit_code`` tool; the CLI persists that submission to
|
|
10
|
+
``<basename>_code.py``, re-executes it in a fresh ``uv`` kernel, and prints
|
|
11
|
+
its solution (JSON) to stdout.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import mcp_solver
|
|
26
|
+
from mcp_solver.templates import SOLVERS, get_template
|
|
27
|
+
|
|
28
|
+
# Re-execute the submitted program under the same interpreter version that
|
|
29
|
+
# ran the agent's kernel, so in-kernel verification carries over.
|
|
30
|
+
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
31
|
+
|
|
32
|
+
# Solver libraries injected into the solve-time kernel, per backend.
|
|
33
|
+
# pypblib backs pysat.pb.PBEnc (pseudo-Boolean encodings — budgets,
|
|
34
|
+
# capacities); without it PBEnc raises at import time.
|
|
35
|
+
SOLVER_PACKAGES: dict[str, list[str]] = {
|
|
36
|
+
"pysat": ["python-sat", "pypblib"],
|
|
37
|
+
"maxsat": ["python-sat", "pypblib"],
|
|
38
|
+
"z3": ["z3-solver"],
|
|
39
|
+
"cpmpy": ["cpmpy"],
|
|
40
|
+
"clingo": ["clingo"],
|
|
41
|
+
# didppy is bounded: the didp template documents the 0.10.x API, which
|
|
42
|
+
# was verified empirically; lift the cap only after re-testing.
|
|
43
|
+
"didp": ["didppy>=0.11.1,<0.12"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _local_checkout() -> str | None:
|
|
48
|
+
"""Return the repo root when mcp_solver runs from a source checkout.
|
|
49
|
+
|
|
50
|
+
An editable install resolves ``mcp_solver`` to ``<root>/src/mcp_solver``;
|
|
51
|
+
a regular install resolves it inside site-packages, where the checkout
|
|
52
|
+
markers are absent.
|
|
53
|
+
"""
|
|
54
|
+
root = Path(mcp_solver.__file__).resolve().parents[2]
|
|
55
|
+
if (root / "pyproject.toml").is_file() and (root / "src" / "mcp_solver").is_dir():
|
|
56
|
+
return str(root)
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_unpublishable(version: str) -> bool:
|
|
61
|
+
"""Return True when *version* looks like a pre-release/dev build.
|
|
62
|
+
|
|
63
|
+
Published releases are plain numeric (e.g. ``4.0.0``). Anything else —
|
|
64
|
+
an alphabetic marker (``a``/``b``/``rc``/``dev`` per PEP 440) or a local
|
|
65
|
+
segment (``4.0.0+1``) — is not on PyPI, so a PyPI pin to it would fail.
|
|
66
|
+
"""
|
|
67
|
+
return re.fullmatch(r"\d+(\.\d+)*", version) is None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_dev_path(
|
|
71
|
+
args: argparse.Namespace, parser: argparse.ArgumentParser
|
|
72
|
+
) -> str | None:
|
|
73
|
+
"""Return the effective dev-mode path (a source checkout root) or None.
|
|
74
|
+
|
|
75
|
+
Dev mode means "everything Python-related comes from the local repo":
|
|
76
|
+
the injected helper library and the solver templates. Resolution order:
|
|
77
|
+
|
|
78
|
+
1. ``--no-dev`` → None (published behavior), overrides everything.
|
|
79
|
+
2. ``--dev PATH`` → that path.
|
|
80
|
+
3. ``--dev`` (no path) → auto-detected checkout, or a parser error.
|
|
81
|
+
4. ``$MCP_SOLVER_DEV`` set to ``1``/``true``/``auto`` → auto-detect
|
|
82
|
+
(error if none); any other non-empty value → treat as a path.
|
|
83
|
+
5. Nothing set → auto-default to a detected checkout (may be None).
|
|
84
|
+
"""
|
|
85
|
+
if args.no_dev:
|
|
86
|
+
return None
|
|
87
|
+
if args.dev is not None:
|
|
88
|
+
if args.dev == "auto":
|
|
89
|
+
checkout = _local_checkout()
|
|
90
|
+
if checkout is None:
|
|
91
|
+
parser.error("no source checkout detected; pass --dev PATH")
|
|
92
|
+
return checkout
|
|
93
|
+
return args.dev
|
|
94
|
+
env = os.environ.get("MCP_SOLVER_DEV")
|
|
95
|
+
if env:
|
|
96
|
+
if env.lower() in ("1", "true", "auto"):
|
|
97
|
+
checkout = _local_checkout()
|
|
98
|
+
if checkout is None:
|
|
99
|
+
parser.error("no source checkout detected; pass --dev PATH")
|
|
100
|
+
return checkout
|
|
101
|
+
return env
|
|
102
|
+
return _local_checkout()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def helpers_package(dev_path: str | None = None) -> str:
|
|
106
|
+
"""Return the ``--with`` spec for the mcp-solver helper library.
|
|
107
|
+
|
|
108
|
+
In dev mode (*dev_path* given) the helpers come from that local
|
|
109
|
+
checkout; otherwise from a PyPI pin to the installed version.
|
|
110
|
+
"""
|
|
111
|
+
if dev_path:
|
|
112
|
+
return dev_path
|
|
113
|
+
return f"{mcp_solver.DIST_NAME}=={mcp_solver.__version__}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_with_packages(solver: str, dev_path: str | None = None) -> list[str]:
|
|
117
|
+
"""Assemble the ``--with`` package set for a solver run.
|
|
118
|
+
|
|
119
|
+
The solver library comes first, then the mcp-solver helpers. Only the
|
|
120
|
+
pysat/maxsat/z3 templates reference the helpers, but they are injected
|
|
121
|
+
for every backend for uniformity.
|
|
122
|
+
"""
|
|
123
|
+
return [*SOLVER_PACKAGES[solver], helpers_package(dev_path)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _model_params(config: dict[str, Any]) -> dict[str, Any]:
|
|
127
|
+
"""Translate a model-alias config dict into OpenRouter API parameters.
|
|
128
|
+
|
|
129
|
+
Mirrors the engine's rules: models flagged ``no_sampling_params`` accept
|
|
130
|
+
only ``max_tokens``; otherwise the standard sampling knobs pass through.
|
|
131
|
+
(``top_k``/``provider``/``reasoning`` need ``extra_body``, which the
|
|
132
|
+
minion agent reserves, so they are not forwarded.)
|
|
133
|
+
"""
|
|
134
|
+
params: dict[str, Any] = {}
|
|
135
|
+
if config.get("no_sampling_params"):
|
|
136
|
+
if "max_tokens" in config:
|
|
137
|
+
params["max_tokens"] = config["max_tokens"]
|
|
138
|
+
return params
|
|
139
|
+
for key in (
|
|
140
|
+
"temperature",
|
|
141
|
+
"max_tokens",
|
|
142
|
+
"top_p",
|
|
143
|
+
"frequency_penalty",
|
|
144
|
+
"presence_penalty",
|
|
145
|
+
):
|
|
146
|
+
if key in config:
|
|
147
|
+
params[key] = config[key]
|
|
148
|
+
return params
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_model(name: str) -> tuple[str, dict[str, Any]]:
|
|
152
|
+
"""Resolve a model name to ``(openrouter_id, api_params)``.
|
|
153
|
+
|
|
154
|
+
A name containing ``/`` is already an OpenRouter model ID and passes
|
|
155
|
+
through unchanged. Anything else is an alias looked up in the engine's
|
|
156
|
+
bundled ``models/<name>.json`` (e.g. ``gpt56terra``).
|
|
157
|
+
|
|
158
|
+
Raises ValueError for an unknown alias.
|
|
159
|
+
"""
|
|
160
|
+
if "/" in name:
|
|
161
|
+
return name, {}
|
|
162
|
+
from importlib.resources import files
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
text = (files("agentic_python_coder") / "models" / f"{name}.json").read_text(
|
|
166
|
+
encoding="utf-8"
|
|
167
|
+
)
|
|
168
|
+
except (ModuleNotFoundError, FileNotFoundError) as exc:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"unknown model alias {name!r}; pass a full OpenRouter ID"
|
|
171
|
+
" (e.g. openai/gpt-5.6-terra) or install the engine for aliases"
|
|
172
|
+
) from exc
|
|
173
|
+
config = json.loads(text)
|
|
174
|
+
return config["path"], _model_params(config)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def find_api_key() -> str | None:
|
|
178
|
+
"""Return the OpenRouter API key, or None.
|
|
179
|
+
|
|
180
|
+
Checked in order: the environment, then the dotenv files used by the
|
|
181
|
+
engine (``~/.config/coder/.env``) and by mcp-minion (``~/.mcp-minion``).
|
|
182
|
+
"""
|
|
183
|
+
key = os.environ.get("OPENROUTER_API_KEY")
|
|
184
|
+
if key:
|
|
185
|
+
return key
|
|
186
|
+
from dotenv import dotenv_values
|
|
187
|
+
|
|
188
|
+
for dotfile in (
|
|
189
|
+
Path.home() / ".config" / "coder" / ".env",
|
|
190
|
+
Path.home() / ".mcp-minion",
|
|
191
|
+
):
|
|
192
|
+
if dotfile.is_file():
|
|
193
|
+
key = dotenv_values(dotfile).get("OPENROUTER_API_KEY")
|
|
194
|
+
if key:
|
|
195
|
+
return key
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
200
|
+
"""Construct the argument parser."""
|
|
201
|
+
parser = argparse.ArgumentParser(
|
|
202
|
+
prog="mcp-solver",
|
|
203
|
+
description="Solve a constraint problem with an LLM coding agent.",
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"solver",
|
|
207
|
+
choices=SOLVERS,
|
|
208
|
+
help="constraint solver backend",
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"task",
|
|
212
|
+
nargs="*",
|
|
213
|
+
help="task text (words are joined with spaces)",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--problem",
|
|
217
|
+
metavar="FILE",
|
|
218
|
+
help="read the task from a markdown file instead of positional text",
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--model",
|
|
222
|
+
default="gpt56terra",
|
|
223
|
+
help="agent model name (default: %(default)s)",
|
|
224
|
+
)
|
|
225
|
+
parser.add_argument(
|
|
226
|
+
"--workdir",
|
|
227
|
+
metavar="DIR",
|
|
228
|
+
default=None,
|
|
229
|
+
help="working directory for the run (default: current directory)",
|
|
230
|
+
)
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
"--step-limit",
|
|
233
|
+
type=int,
|
|
234
|
+
default=30,
|
|
235
|
+
metavar="N",
|
|
236
|
+
help="maximum agent steps before stopping (default: %(default)s)",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"-q",
|
|
240
|
+
"--quiet",
|
|
241
|
+
action="store_true",
|
|
242
|
+
help="suppress engine progress output",
|
|
243
|
+
)
|
|
244
|
+
dev_group = parser.add_mutually_exclusive_group()
|
|
245
|
+
dev_group.add_argument(
|
|
246
|
+
"--dev",
|
|
247
|
+
nargs="?",
|
|
248
|
+
const="auto",
|
|
249
|
+
default=None,
|
|
250
|
+
metavar="PATH",
|
|
251
|
+
help="dev mode: take helpers and templates from a local checkout"
|
|
252
|
+
" (bare --dev auto-detects a source checkout; auto-on inside one)",
|
|
253
|
+
)
|
|
254
|
+
dev_group.add_argument(
|
|
255
|
+
"--no-dev",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="disable dev mode; use the published (PyPI-pinned) helpers",
|
|
258
|
+
)
|
|
259
|
+
parser.add_argument(
|
|
260
|
+
"--stats-json",
|
|
261
|
+
metavar="FILE",
|
|
262
|
+
default=None,
|
|
263
|
+
help="write the engine run statistics to FILE as JSON",
|
|
264
|
+
)
|
|
265
|
+
return parser
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def resolve_task(
|
|
269
|
+
args: argparse.Namespace, parser: argparse.ArgumentParser
|
|
270
|
+
) -> tuple[str, str]:
|
|
271
|
+
"""Return (task_text, task_basename) enforcing task/problem exclusivity.
|
|
272
|
+
|
|
273
|
+
Exactly one of positional task text or ``--problem`` must be given.
|
|
274
|
+
"""
|
|
275
|
+
task_text = " ".join(args.task).strip()
|
|
276
|
+
if args.problem and task_text:
|
|
277
|
+
parser.error("provide task text OR --problem, not both")
|
|
278
|
+
if not args.problem and not task_text:
|
|
279
|
+
parser.error("provide task text or --problem <file.md>")
|
|
280
|
+
if args.problem:
|
|
281
|
+
problem_path = Path(args.problem)
|
|
282
|
+
try:
|
|
283
|
+
return problem_path.read_text(encoding="utf-8"), problem_path.stem
|
|
284
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
285
|
+
parser.error(f"cannot read problem file {args.problem}: {exc}")
|
|
286
|
+
return task_text, "task"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def build_stats(result: Any, elapsed: float, model_id: str) -> dict:
|
|
290
|
+
"""Summarise an AgentResult as the stats dict consumed by the harness.
|
|
291
|
+
|
|
292
|
+
Keeps the key shape of the old engine stats (``token_consumption``,
|
|
293
|
+
``tool_usage``, ``execution_time_seconds``, ``step_limit_reached``) so
|
|
294
|
+
downstream consumers need no changes.
|
|
295
|
+
"""
|
|
296
|
+
tool_usage: dict[str, int] = {}
|
|
297
|
+
for step in result.steps:
|
|
298
|
+
for call in step.get("tool_calls") or []:
|
|
299
|
+
name = call.get("name", "?")
|
|
300
|
+
tool_usage[name] = tool_usage.get(name, 0) + 1
|
|
301
|
+
return {
|
|
302
|
+
"token_consumption": {
|
|
303
|
+
"input_tokens": result.input_tokens,
|
|
304
|
+
"output_tokens": result.output_tokens,
|
|
305
|
+
"total_tokens": result.input_tokens + result.output_tokens,
|
|
306
|
+
"cost_usd": round(getattr(result, "cost", 0.0), 6),
|
|
307
|
+
},
|
|
308
|
+
"tool_usage": tool_usage,
|
|
309
|
+
"execution_time_seconds": elapsed,
|
|
310
|
+
"steps": len(result.steps),
|
|
311
|
+
"tool_calls_made": result.tool_calls_made,
|
|
312
|
+
"step_limit_reached": result.max_steps_reached,
|
|
313
|
+
"model": model_id,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _progress(step_info: dict) -> None:
|
|
318
|
+
"""Print a one-line progress note for a completed agent step."""
|
|
319
|
+
calls = step_info.get("tool_calls") or []
|
|
320
|
+
detail = ", ".join(c.get("name", "?") for c in calls) if calls else "final answer"
|
|
321
|
+
print(f"mcp-solver: step {step_info['step']}: {detail}", file=sys.stderr)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def solve(
|
|
325
|
+
task: str,
|
|
326
|
+
api_key: str,
|
|
327
|
+
config: Any,
|
|
328
|
+
logger: Any,
|
|
329
|
+
quiet: bool,
|
|
330
|
+
) -> Any:
|
|
331
|
+
"""Run one solve: minion agent over the engine's ipython_mcp server."""
|
|
332
|
+
from mcp_minion import Agent, MCPManager
|
|
333
|
+
|
|
334
|
+
servers = {
|
|
335
|
+
"ipython": {
|
|
336
|
+
"command": sys.executable,
|
|
337
|
+
"args": ["-m", "agentic_python_coder.mcp_server"],
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
manager = MCPManager(servers)
|
|
341
|
+
await manager.__aenter__()
|
|
342
|
+
try:
|
|
343
|
+
if not manager.has_tool("submit_code"):
|
|
344
|
+
raise RuntimeError(
|
|
345
|
+
"the ipython_mcp server does not provide submit_code;"
|
|
346
|
+
" upgrade agentic-python-coder to >=3.4.0"
|
|
347
|
+
)
|
|
348
|
+
agent = Agent(
|
|
349
|
+
api_key=api_key,
|
|
350
|
+
tools=None,
|
|
351
|
+
config=config,
|
|
352
|
+
logger=logger,
|
|
353
|
+
mcp_manager=manager,
|
|
354
|
+
on_step=None if quiet else _progress,
|
|
355
|
+
)
|
|
356
|
+
return await agent.run_async(task)
|
|
357
|
+
finally:
|
|
358
|
+
await manager.__aexit__(None, None, None)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def print_stats(stats: dict) -> None:
|
|
362
|
+
"""Print a compact one-line summary of the solve run to stderr."""
|
|
363
|
+
tokens = stats.get("token_consumption", {}).get("total_tokens", 0)
|
|
364
|
+
cost = stats.get("token_consumption", {}).get("cost_usd", 0)
|
|
365
|
+
exec_calls = stats.get("tool_usage", {}).get("python_exec", 0)
|
|
366
|
+
elapsed = stats.get("execution_time_seconds", 0)
|
|
367
|
+
cost_part = f", ${cost:.4f}" if cost else ""
|
|
368
|
+
print(
|
|
369
|
+
f"mcp-solver: {tokens:,} tokens{cost_part}, {exec_calls} exec calls,"
|
|
370
|
+
f" {elapsed:.1f}s",
|
|
371
|
+
file=sys.stderr,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def run_program(program: Path, with_packages: list[str]) -> int:
|
|
376
|
+
"""Execute the saved program in a uv kernel; return its exit code.
|
|
377
|
+
|
|
378
|
+
The program's stdout (the solution JSON) is inherited so it becomes
|
|
379
|
+
this command's stdout.
|
|
380
|
+
"""
|
|
381
|
+
cmd = ["uv", "run", "--no-project", "--python", PYTHON_VERSION]
|
|
382
|
+
for pkg in with_packages:
|
|
383
|
+
cmd += ["--with", pkg]
|
|
384
|
+
cmd += ["python", str(program)]
|
|
385
|
+
try:
|
|
386
|
+
return subprocess.run(cmd).returncode
|
|
387
|
+
except FileNotFoundError:
|
|
388
|
+
print(
|
|
389
|
+
"mcp-solver: 'uv' not found on PATH; it is required to run the"
|
|
390
|
+
" submitted program (https://docs.astral.sh/uv/).",
|
|
391
|
+
file=sys.stderr,
|
|
392
|
+
)
|
|
393
|
+
return 127
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main(argv: list[str] | None = None) -> int:
|
|
397
|
+
parser = build_parser()
|
|
398
|
+
args = parser.parse_args(argv)
|
|
399
|
+
task, task_basename = resolve_task(args, parser)
|
|
400
|
+
dev_path = resolve_dev_path(args, parser)
|
|
401
|
+
|
|
402
|
+
if dev_path is None and _is_unpublishable(mcp_solver.__version__):
|
|
403
|
+
print(
|
|
404
|
+
f"mcp-solver: installed version {mcp_solver.__version__} is not a"
|
|
405
|
+
" published release and dev mode is off.\n"
|
|
406
|
+
"Run from a source checkout, or pass --dev PATH (or --dev in a"
|
|
407
|
+
" checkout) to take helpers and templates from the local repo.",
|
|
408
|
+
file=sys.stderr,
|
|
409
|
+
)
|
|
410
|
+
return 2
|
|
411
|
+
|
|
412
|
+
with_packages = build_with_packages(args.solver, dev_path)
|
|
413
|
+
if dev_path is not None:
|
|
414
|
+
print(
|
|
415
|
+
f"mcp-solver: dev mode — helpers and templates from {dev_path}",
|
|
416
|
+
file=sys.stderr,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
import agentic_python_coder # noqa: F401 (kernel server + model aliases)
|
|
421
|
+
import mcp_minion # noqa: F401
|
|
422
|
+
except ImportError:
|
|
423
|
+
print(
|
|
424
|
+
"mcp-solver: the agent layer is not installed"
|
|
425
|
+
" (needs agentic-python-coder and mcp-minion).\n"
|
|
426
|
+
f"Install it with: uv pip install '{mcp_solver.DIST_NAME}[agent]'"
|
|
427
|
+
" (from a checkout: uv pip install -e '.[agent]')",
|
|
428
|
+
file=sys.stderr,
|
|
429
|
+
)
|
|
430
|
+
return 1
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
model_id, api_params = resolve_model(args.model)
|
|
434
|
+
except ValueError as exc:
|
|
435
|
+
parser.error(str(exc))
|
|
436
|
+
|
|
437
|
+
api_key = find_api_key()
|
|
438
|
+
if api_key is None:
|
|
439
|
+
print(
|
|
440
|
+
"mcp-solver: no OpenRouter API key found.\n"
|
|
441
|
+
"Set OPENROUTER_API_KEY or put it in ~/.config/coder/.env"
|
|
442
|
+
" (see INSTALL.md).",
|
|
443
|
+
file=sys.stderr,
|
|
444
|
+
)
|
|
445
|
+
return 1
|
|
446
|
+
|
|
447
|
+
workdir = Path(args.workdir or os.getcwd()).resolve()
|
|
448
|
+
project_prompt = get_template(args.solver, root=dev_path)
|
|
449
|
+
|
|
450
|
+
from mcp_minion import AgentConfig, RunLogger, extract_last_submission
|
|
451
|
+
|
|
452
|
+
config = AgentConfig(
|
|
453
|
+
model=model_id,
|
|
454
|
+
max_steps=args.step_limit,
|
|
455
|
+
api_params=api_params,
|
|
456
|
+
system_prompt=project_prompt,
|
|
457
|
+
packages=with_packages,
|
|
458
|
+
)
|
|
459
|
+
logger = RunLogger(log_dir=workdir)
|
|
460
|
+
|
|
461
|
+
start = time.monotonic()
|
|
462
|
+
try:
|
|
463
|
+
result = asyncio.run(solve(task, api_key, config, logger, args.quiet))
|
|
464
|
+
except RuntimeError as exc:
|
|
465
|
+
print(f"mcp-solver: {exc}", file=sys.stderr)
|
|
466
|
+
return 1
|
|
467
|
+
elapsed = time.monotonic() - start
|
|
468
|
+
|
|
469
|
+
stats = build_stats(result, elapsed, model_id)
|
|
470
|
+
print_stats(stats)
|
|
471
|
+
if args.stats_json:
|
|
472
|
+
try:
|
|
473
|
+
Path(args.stats_json).write_text(json.dumps(stats, default=str))
|
|
474
|
+
except OSError as exc:
|
|
475
|
+
print(
|
|
476
|
+
f"mcp-solver: could not write stats to {args.stats_json}: {exc}",
|
|
477
|
+
file=sys.stderr,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
code = extract_last_submission(result.steps)
|
|
481
|
+
if code is None:
|
|
482
|
+
print(
|
|
483
|
+
"mcp-solver: the run submitted no final program"
|
|
484
|
+
" (submit_code was never called successfully).",
|
|
485
|
+
file=sys.stderr,
|
|
486
|
+
)
|
|
487
|
+
return 3
|
|
488
|
+
program = workdir / f"{task_basename}_code.py"
|
|
489
|
+
program.write_text(code, encoding="utf-8")
|
|
490
|
+
|
|
491
|
+
print(f"mcp-solver: running {program}", file=sys.stderr)
|
|
492
|
+
return run_program(program, with_packages)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
sys.exit(main())
|