Repository navigation
Increment 37: rasputin mcp, the commands served over the Model Context Protocol - #236
Merged
Merged
Conversation
…hitect) Design docs/increments/37-mcp-server.md and ROADMAP row 37, on b4a81c0. Ola's rulings: the official `mcp` Python SDK (2026-10-12); the server is the first step of "intent-driven triangulation" (2026-10-11). What is designed: `rasputin mcp` serves over stdio with the SDK's low-level Server; one tool per Typer command, the JSON schema derived from the command's click parameters (one source of truth; `additionalProperties: false`); arguments validated by the derived Pydantic model before any child runs; each call a child `python -m tin_engine <command>` with TYPER_USE_RICH=0, killed on cancel; the exit code, output paths, run record and --stats returned as data, a refusal as an is_error result with the CLI's plain words and exit code; progress per stderr line plus a 5 s heartbeat over stdio; a jobs cap, default 1; no Tasks (the SDK has none server-side), no listed resources (resource links instead); the `mcp` extra in pyproject; a 3-line tin_engine/__main__.py; about 250 counted lines. Measured by docs/increments/37-probes/mcp_probe.py (output beside it): mcp 2.3.0 on Python 3.14.7; schemas for all 8 commands 16,575 bytes, mesh 34 properties; a tool call costs 0.25 s over the command (the child's import of tin_engine.cli); the 1 m benchmark mesh 0.72 s through the tool with the 38-key record returned; a cancelled child dead within 0.5 s in process and over stdio; unknown and mistyped arguments refused before a child runs; two concurrent calls run side by side. Two SDK facts the docs do not state, found by the probe: the server validates no tool argument itself, and the in-process transport carries no progress token. Prior art: MCP specification 2026-07-28 (tools, progress, cancellation, the Tasks extension), the SDK's v2 docs, gdal-mcp and the QGIS MCP plugins (hand-written tool signatures, FastMCP/Rasterio); legacy grep: nothing. Speed of existing commands: expected unchanged (a new entry point; no refine or mesh code; no @Perf run due). The probe carries a file-level `ruff: noqa: E501, ASYNC240` with its reason; `ruff check docs/increments/37-probes/` passes; `check_citations.py` resolves every citation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…architect) Blocker 1, the killed-run claim. Section 6.5 and the degeneracy row no longer say every output goes through `.part` and a rename. That holds for the GeoPackage (`_write_geopackage`, cli.py@b4a81c0d:1250-1289, the `os.replace` at :1289); `.vtk` and `.ply` are one `target.write_bytes` (:1241) and `_writing` (:2121) only maps OSError to a refusal. The design now states that a mesh call cancelled during that write can leave a truncated file under the asked name, and that the server does not guard against it: it cannot tell a truncated file from a complete one or from the caller's older file, so deleting on cancel would destroy the last two. It documents it instead (one more line in the server's `instructions`) and leaves the remedy, every output through `.part`, to cli.py (in Ola's ideas file, not this increment). Blocker 2, the temporary record/stats path under `out_parent`. The sidecar goes beside `out`: `Path(out).parent / f".{name}.record-{token}.json"` (and `.stats-{token}.md`), token from `secrets.token_hex(4)`, the name chosen by the server and the file created by the child, removed in the handler's `finally`. Checked against `_destination` (cli.py@b4a81c0d:420-447) check by check: not a symlink (:438); inside `out_parent` exactly when `out` is, since they share a directory and `out` is not a symlink (:441-444); the directory exists because `--out` at :1180 was accepted first or refused first (:445); the random name collides with no mesh, report or record path (:1343, :1360). T4 pins the argv the fake child receives, T5 that no sidecar survives a refusal, T6 the real mesh with `out_parent` set to the mesh's directory (succeeds) and to a sibling (refused naming --out). Blocker 3, the progress value. Every notification of a call, stderr line, heartbeat or waiting heartbeat, carries `progress` = seconds elapsed since the handler began (`time.monotonic()`, rounded to milliseconds, `total` omitted); one sender per call sends `max(elapsed, last + 0.001)`, so two lines in one millisecond still increase. The clock starts before the semaphore wait, so waiting and running notifications form one sequence. T9 asserts strict increase over lines and heartbeats in the order received, each value within 0.05 s of wall time, and the waiting-then-running sequence under jobs 1. The probe sent the line count (37-probes/mcp_probe.py:186-188); its measured output, the tick count and text, does not depend on the value, so the probe was not rerun. Prose: "1 of 81" is 2 (`snap_spacing` on draw at :478 and mesh at :676); the timeout-reset quotation moved to the Cancellation bullet with the specification's own words from its Timeouts section, and the Progress bullet now quotes "MUST increase with each notification" (both pages fetched today); the `python` CI job at main.yaml:261 is added to the three install lines that gain the `mcp` extra; the geo-mcp-servers list says "100 servers tracked" today. Suggestions taken: a Boundaries bullet says the server process writes nothing but JSON-RPC to its own stdout; T11 also drops `tin_engine.mcp.server` from `sys.modules`. Estimate: server.py about 150 (was 140) for the sidecar names and the sender; total about 260, worst case about 360, one PR. Also: two short paths in the round 1 Review line (`cli.py@b4a81c0:442`, `main.yaml:261`) are expanded to full paths so `tools/check_citations.py` resolves them; the gate was red on the branch (exit 1) and is green now. Status line: designed, review round 1 fixes in; next round 2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-out case applied as specified, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…iew-approved, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ot read it as a cite, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cel, jobs-cap, stdio and extra-absent tests (@tester) Failing tests for increment 37, an MCP server over the rasputin commands (docs/increments/37-mcp-server.md, section 9, T1 to T12), written before any production code. 62 tests red, 61 pass (the unchanged layering rows): .venv/bin/python -m pytest tests/python/test_mcp_schema.py \ tests/python/test_mcp_server.py tests/python/test_mcp_stdio.py \ tests/python/test_mcp_cli.py tests/python/test_layering.py -o addopts="" -> 62 failed, 61 passed Every failure is a missing module, attribute or subcommand, none a typo: 51 on `ModuleNotFoundError: No module named 'tin_engine.mcp'` (schema, server and the layering rows), 2 on `tin_engine.__main__` (the layering row and `test_no_module_imports_by_name`), 5 stdio tests on the SDK's `MCPError: Connection closed` because the spawned server dies at import (`python -m tin_engine mcp`: "No module named tin_engine.__main__"; the launcher: `tin_engine.mcp`), 2 on `rasputin mcp` not being a subcommand (`invoke` refuses the name; `--help` does not list it), 1 on the layering table naming modules not on disk. Files, all under tests/python/: - test_mcp_schema.py: T1 to T3. A toy Typer app with one parameter per row of section 5's type table (argument, int, IntRange, float, flag pair, 4-tuple, path-and-number tuple, list, Choice, nullable, Path) and the server's own `mcp` command; the derived schema, required, extra forbidden, defaults, descriptions and refusals; the argv round trip (only set keys, `--ascii` for false, tuples expand, lists repeat, the command's parameter order); the real app's eight tools in the CLI's order with the probe's parameter counts, each property set re-derived from the click parameters at test time, section 3's five `mesh` snapshots, `fetch.connections`, the list under 20,000 bytes, annotations, `RunResult` as every tool's output schema. - test_mcp_server.py: T4 to T6, T8 and T10 in process through the SDK's `Client(server)`. Validation before the child with a fake child that records its argv; `--out-parent` passed through, the record sidecar `.a.vtk.record-<8 hex>.json` beside `out`, `stats: "-"` as a stats sidecar, a caller's `record_path` kept; refusal mapping for exit 2, exit 1 and a signal, with stderr in full; no sidecar left after a refusal, whether or not the child created it; the real `mesh` on the micro tile (12 triangles, record in `run_record.WORDING`'s order, one `.vtk` in `outputs` and as a `file://` resource link, directory otherwise empty), with `out_parent` on the mesh's directory, on a sibling (refused naming `--out`), a relative `out` under chdir, and `stats: "-"` returned as data; the Kartverket tile at 1 m under `needs_codecs`; a client timeout kills the child within 1 s; a jobs cap of 1 serialises two 0.5 s children and 2 runs them side by side. - test_mcp_stdio.py: T7 to T9. `python -m tin_engine mcp` through `StdioServerParameters` lists the tools and runs `version`; the cancel over stdio; progress: one notification per stderr line with the line as message, heartbeats `running, N s` at a 0.1 s constant around a line, the queued call's `waiting for a running job, N s` then `running` on one strictly increasing clock; every value is bounded by the child's sleeps below and the client's own elapsed time above, with `total` absent. - test_mcp_cli.py: T11, not skipped without the SDK. `rasputin mcp` with `mcp` and `mcp_types` planted as None in sys.modules and the server modules dropped exits 2 with the install line; `--help` lists `mcp`. - test_layering.py: T12, the section 6.8 rows (`cli` gains `mcp.server`; `__main__` -> `cli`; `mcp`, `mcp.schema` empty; `mcp.server` -> `mcp.schema tin_engine`). - mcp_driver.py, mcp_fake_child.py, mcp_stdio_launcher.py: shared support. The fake child is a Python script the server runs in place of `python -m tin_engine`, driven by a JSON spec (argv file, pid file, stderr lines with delays, sleep, signal, exit code, interval log); the launcher runs `build_server` on the real app over the SDK's `stdio_server` with the fake child and a heartbeat from a config file. Pinned beyond the design (for @architect): `derive(app)` drops the command named `mcp`; `DerivedCommand.tool_name`, `.model`, `.argv()`, `.tool()`; `argv[0]` is the hyphenated CLI command name; argv follows the command's parameter order; an Enum default is its value and a Path default its string in the schema; `build_server(app, *, jobs=1, child=...)` with `child` the argv prefix; `HEARTBEAT_SECONDS` a module constant; an error result's `structured_content` carries `stderr`; the text block of a success is the structured content's JSON verbatim; `tin_engine.__main__` must import without running the app (the layering test imports it), so it needs an `if __name__ == "__main__"` guard beyond the design's three lines. Departures from section 9, named: the Kartverket count is asserted in a window (410,000 to 520,000 around the measured 464,290) and the tolerance from the record, as test_cli_mesh_refine does, since the triangle count is not a record field; the progress value's "within 0.05 s of wall time" is replaced by exact two-sided bounds that cannot flake and still refute a count. The SDK is not yet a dependency of the tree: the three SDK-using files `pytest.importorskip("mcp")` at the top, so a CI job without the extra skips them and runs T11 and T12. ruff check and ruff format --check clean on the tree; the SDK's client calls the suite relies on (two concurrent calls on one client, a read timeout raising MCPError, the progress callback's signature) were checked against the design's probe server in docs/increments/37-probes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…otocol (@developer) The green step of increment 37 (docs/increments/37-mcp-server.md, sections 4 to 7) against @tester's red suite ee5b5d5: all 62 red tests pass, the rest of the tree unchanged in behaviour. .venv/bin/python -m pytest tests/python/ -> 7292 passed, 17 skipped, coverage 98.43% (mcp/schema.py 100%, mcp/server.py 91%); the three test_ci_changes.py failures of that run were from a workflow edit since reverted (below); rerun green. T6 on the Kartverket tile: 464,290 triangles, 0.77 s (probe: 464,290, 0.72 s). New, src_python/tin_engine/mcp/: - schema.py: `derive(app)` reads each Typer command's click parameters into a Pydantic model (extra forbidden) and a `DerivedCommand` with `tool_name` (`-` as `_`, the `mcp` command dropped), `model`, `tool()` (input schema without `title`, `RunResult` as output schema, section 5's annotations) and `argv()` (only the keys the caller set, in the command's parameter order; a false flag is its second name; tuples expand; lists repeat the option; an argument is positional). Section 5's type table: IntRange bounds, Choice as enum, tuple types as fixed-length arrays, Enum defaults as their value, Path defaults as strings. - server.py: `build_server(app, *, jobs=1, child=CHILD)` on the SDK's low-level `Server`; a child `[sys.executable, "-m", "tin_engine", *argv]` per call with TYPER_USE_RICH=0, NO_COLOR=1, TERM=dumb, stdin from /dev/null, no `cwd=`; stdout and stderr lines as data; exit 2 or 1 as `is_error` with the CLI's `Error:` words and the whole stderr; a signal as `killed by signal N (NAME)`; the record and `stats -` sidecars beside `out` as `.{name}.record-{8 hex}.json` and `.{name}.stats-{8 hex}.md`, read back and unlinked in a shielded `finally`; `outputs` the stdout lines that exist after the run less the sidecars, each also a `file://` resource link with the suffix's MIME type; progress = elapsed seconds, strictly increasing under one lock per call, a line per stderr line and a heartbeat every `HEARTBEAT_SECONDS` (module constant, read at each tick) saying `running, N s` or `waiting for a running job, N s`; an `anyio.Semaphore(jobs)` cap; cancellation kills the child and waits for it under a shield; an unknown tool is `MCPError(-32602)`; any exception of the server's own is an error result in words. `serve()` runs it over the SDK's `stdio_server`. Nothing prints. - __init__.py: a docstring. - src_python/tin_engine/__main__.py: `python -m tin_engine`, with the `if __name__ == "__main__"` guard the layering test needs; `sys.argv[0]` renamed so the record's `command` reads `rasputin mesh ...`. - src_python/tin_engine/cli.py: the `mcp` command with `--jobs`, a lazy import of `tin_engine.mcp.server`, and on ImportError exit 2 with `Error: the mcp extra is not installed; run pip install "rasputin[mcp]"`. - pyproject.toml: the `mcp` extra (`mcp>=2.3`), and `dev` pulls it. Beyond the design's file list, needed by a test and named here: - cli.py `_destination` gains `hint` (default `--out`), passed as `param_hint` to its three refusals; the four non-`--out` callers pass `--out-edges`, `--stats`, `--record`, `--out-dir`. The design (6.4, T6) says a mesh whose `out` is outside `out_parent` is "refused naming --out"; at b4a81c0 the refusal read `Invalid value: X is outside the permitted parent Y` and named no option, so T6's test_out_outside_out_parent_is_refused_naming_out_not_the_sidecar was red on the CLI's wording. It now reads `Invalid value for --out: ...`. The one existing test on that wording (test_cli_mesh_stats.py:393) checks the phrase, which is kept. - `RunResult` lives in schema.py, not server.py as section 5 sites it: `DerivedCommand.tool()` declares it as the output schema and the layering rows forbid schema importing server; server re-exports it. - The design's 6.1 edit to .github/workflows/main.yaml (adding `mcp` to the four install lines) is not made: tests/python/test_ci_changes.py pins those lines verbatim (three tests went red when tried). The `dev` extra pulls `mcp>=2.3` instead, so CI's `.[dev]` installs run the suite. Gates: ruff check, ruff format --check, mypy (72 files, strict), check_prohibited_deps.py (and it fails naming rasterio when planted in the mcp extra), check_detria_boundary.py. No C++ touched. Count: python3 tools/count_loc.py b4a81c0 HEAD, reported in the handback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…with the Status line and ROADMAP row, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ons pinned, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… imports made plain (@tester) Blocker 2 of code review round 1 (docs/increments/37-mcp-server.md, Review section): tests/python/test_mcp_server.py gains two cases in TestRefusalMapping, run through mcp_fake_child. - test_progress_lines_before_the_error_line_stay_out_of_error: stderr is two fetch-style progress lines then "Error: no route to the catalogue", exit 1. RED against dbc77f3 on `assert (body["exit_code"], body["error"]) == (1, "exit 1: no route to the catalogue")`: today `error` is "exit 1: 1,000 of 2,000 bytes 2,000 of 2,000 bytes Error: no route to the catalogue", because src_python/tin_engine/mcp/server.py `_refusal` joins every non-Usage line. The case also asserts `stderr` keeps all three lines (section 6: "stderr kept in full"). - test_without_an_error_line_every_line_but_the_usage_reminder_is_kept: stderr has Usage:, Try, a blank, a progress line and "Aborted!", no Error: line; expects "exit 1: 1,000 of 2,000 bytes Aborted!". GREEN against dbc77f3: the fallback the reviewer named is today's behaviour, so this case cannot be red; it pins the fix against returning nothing when no Error: line exists. Blocker 4: the red-step prose. Nothing needs the lazy imports now that the module exists, so they are plain imports: tests/python/mcp_driver.py loses schema_module()/server_module() and the "before the increment is built" sentence; tests/python/mcp_stdio_launcher.py imports `app` and the server module at the top; test_mcp_schema.py and test_mcp_server.py import `tin_engine.mcp.schema` / `.server` directly (aliased mcp_schema / mcp_server, since `schema` and `server` are local names in those suites). Checked: the four MCP suites in the worktree venv (mcp 2.3.0): 1 failed (the red case above), 56 passed; `ruff check tests/` and `ruff format --check tests/` clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ror: line's words (@developer) `_refusal` in src_python/tin_engine/mcp/server.py joined every stderr line but the Usage:/Try reminder into the result's `error` field, so a `fetch` that printed progress before failing read "exit 1: 1,000 of 2,000 bytes 2,000 of 2,000 bytes Error: no route to the catalogue". Section 6 of docs/increments/37-mcp-server.md says `error` carries the `Error:` line(s)' words. Now, when any `Error:` line exists, `error` is those lines' words (each `Error: ` prefix removed, joined by a space); otherwise the fallback stays as before: every line but the reminder, so a child dying with a traceback or `Aborted!` still has its words in `error`. `stderr` keeps every line in both cases. Green: tests/python/test_mcp_server.py TestRefusalMapping::test_progress_lines_before_the_error_line_stay_out_of_error (red at 783c336); the companion test_without_an_error_line_every_line_but_the_usage_reminder_is_kept stays green. The four MCP suites: 57 passed. mypy, ruff check, ruff format --check, check_prohibited_deps.py, check_detria_boundary.py and check_citations.py all pass. No C++ touched; no test file touched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion (@architect) Design section 6.8 (and the file list at section 10) said README.md gains a short "Connect an agent" section with the Claude Desktop and Codex client forms; the built branch left README unchanged and section 12 said nothing. This commit writes the section and records it. Where: README.md, between "Output and viewing" and "How it is built", so the usage sections (example, output, agent) stay together ahead of the internals. Human reader first, as Ola ruled for the README on 2026-10-11. 243 words of prose, 279 with the code blocks (awk over the section). What it says, and where each claim was checked at 730162f: - pip install "rasputin[mcp]"; the dev extra pulls it: pyproject.toml, [project.optional-dependencies]. - rasputin mcp over stdin and stdout, --jobs N, python -m tin_engine mcp as the alternative: cli.py's mcp command, __main__.py, and `python -m tin_engine mcp --help` in the worktree venv. - Claude Desktop: mcpServers / command / args in claude_desktop_config.json under ~/Library/Application Support/Claude/ on macOS: modelcontextprotocol.io/docs/develop/build-server. Codex: [mcp_servers.<name>] with command and args in ~/.codex/config.toml: learn.chatgpt.com/docs/extend/mcp?surface=cli. - One tool per command, underscores, options as the input schema with help text: schema.derive() run in the venv lists version, draw, mesh, palette, fetch, fetch_stations, catchment, station_catchments; mesh has 34 properties with descriptions, additionalProperties false. - The result (exit code, stdout, stderr, file:// resource links, record and stats as data), refusals as errors with the Error: line's words, the 5 s heartbeat, every stderr line as a progress notice, kill on cancel, one job at a time: server.py (RunResult, _call, _refusal, HEARTBEAT_SECONDS, _run_child's finally, the semaphore). - The caveat: a cancelled .vtk/.ply mesh may be truncated, a .gpkg goes through .part and a rename: design 6.5 and server.INSTRUCTIONS. The four MCP suites pass in the worktree venv (57 passed, 13 s; Kartverket through the tool 464,290 triangles, 0.80 s). check_citations exits 0. Section 12 gains one paragraph saying the section landed and how it was checked (the reviewer's lesson from round 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r Rich (@developer)
Typer renders help through Rich by default, and Rich reads "[mcp]" as a
markup tag and drops it, so `python -m tin_engine mcp --help` showed
`pip install "rasputin"`. Click's plain formatter (TYPER_USE_RICH=0)
prints the text verbatim, so a backslash in the docstring would have
shown there. The install line is now one constant, INSTALL_MCP, shared
by the help text and the refusal line, and the help text escapes the
bracket exactly when the app renders through Rich: Typer's format_help
takes the plain path when `rich_markup_mode is None`, which is the
default without Rich (typer/core.py 28-32 in 0.27.3). The help moves
from the docstring to `help=` so it can carry the escaped copy; the
docstring stays one line.
Rendered after the fix.
Default (Rich):
Usage: python -m tin_engine.rasputin mcp [OPTIONS]
Serve the commands to an agent over the Model Context Protocol, on stdin and
stdout.
One tool per command, each call run as ``python -m tin_engine`` (increment
37). Needs the ``mcp`` extra: ``pip install "rasputin[mcp]"``.
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --jobs <int range> [x>=1] Tool calls run at once; one beyond this │
│ waits. │
│ [default: 1] │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
TYPER_USE_RICH=0:
Usage: python -m tin_engine.rasputin mcp [OPTIONS]
Serve the commands to an agent over the Model Context Protocol, on stdin and
stdout.
One tool per command, each call run as ``python -m tin_engine`` (increment
37). Needs the ``mcp`` extra: ``pip install "rasputin[mcp]"``.
Options:
--jobs <int range> Tool calls run at once; one beyond this waits.
[default: 1; x>=1]
--help Show this message and exit.
The refusal without the extra still prints
`Error: the mcp extra is not installed; run pip install "rasputin[mcp]"`,
exit 2. The server's tool list excludes `mcp` (schema.py SERVER_COMMAND),
so the escaped copy never reaches a tool description. Checked: ruff
check, ruff format --check, mypy, tests/python/test_mcp_*.py (57 passed)
in the worktree venv.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…p-server (@developer) Master moved to c317c7e: #233 the ruling record, #234 a retrospective, #235 increment 26b (land-cover fractions per triangle, with new C++ in include/terrain/land_cover/overlap.hpp and bindings/core.cpp). Two files conflicted, both resolved by hand; the h18 ruling allows the test-file edit during a master merge. ROADMAP.md: the hunk held master's row 36 (now "merged (#231)") against this branch's row 36 plus its new row 37, placed right after it. This branch never edited row 36 since the merge base (b4a81c0), so master's row 36 is kept and row 37 follows it as this branch wrote it. Rows 26 and 35 changed only on master and merged on their own. tests/python/test_layering.py: both sides extended the `cli` row of the L5 layer. The merged row lists both `land_cover_fractions` (26b) and `mcp.server` (37), checked against cli.py as merged (`from tin_engine.land_cover_fractions import assign ...` at module level, `from tin_engine.mcp import server` inside the `mcp` command); this branch's rows for `__main__`, `mcp`, `mcp.schema` and `mcp.server` stay. tests/python/test_layering.py: 69 passed after the resolution. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-parameters check (@tester) Master merge 030fd32 brought increment 26b, whose `mesh` gains `--land-cover` and `--land-cover-year`: 34 -> 36 parameters, 83 across the eight tools. The pinned PARAMETER_COUNTS in tests/python/test_mcp_schema.py went red while the same test's second check, that each tool's schema properties equal its command's click parameters, stayed green for all eight tools. Choice: the count is dropped, not updated. Set equality per tool implies the count, so the pin added no coverage of the schema derivation; what it did detect is a CLI option added elsewhere, which is not this suite's subject, and it would break on every master merge that adds one. The eight tool names stay pinned as TOOL_NAMES, so losing a command or leaking the server's own `mcp` command still fails. The test is renamed test_no_drift_from_the_click_parameters; the file still collects 26 tests, as the increment file's section 12 cites. Checked: the four MCP suites (test_mcp_schema, test_mcp_server, test_mcp_stdio, test_mcp_cli) 57 passed in the worktree venv (mcp 2.3.0, _core from build-wt); ruff check and ruff format --check over tests/ clean; a probe wrapping derive() to drop `land_cover` from the mesh schema turns the drift test red naming `mesh`, so the surviving check can fail. The design's "81" and T3's listed counts (docs/increments/37-mcp-server.md:32, :156, :245, :328-331, :582) are the main session's to update. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ext escape recorded; the design's 81 pinned to its commit, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ADMAP row, by the main session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Increment 37,
docs/increments/37-mcp-server.md(section 12 is the as-built record). 328 net production lines bypython3 tools/count_loc.py origin/master HEADagainst the design's 260 and worst case 360; under 700.rasputin mcp(andpython -m tin_engine mcp) serves every rasputin command as an MCP tool over stdio, on the officialmcpPython SDK (Ola's ruling 2026-10-12), installed as the optional extrarasputin[mcp], which thedevextra pulls so CI runs the suite.file://links to the outputs, the run record and--statsas data; refusals come back as errors carrying the CLI's ownError:words; progress notifications per stderr line with a 5 s heartbeat; cancellation kills the child; a jobs cap.README.mdgains "Connect an agent" (Claude Desktop and Codex configuration blocks). The CLI's path refusals now name the option (Invalid value for --out: ...).Evidence: design
b990e38capproved after three review rounds; redee5b5d52(62 tests), green851592ba; code review rounds 1 to 3 recorded, round 3 APPROVED; masterc317c7e4merged in as030fd32b. No refine or mesh code touched, so no@perfacceptance by the rule; no mutant-critical suite, so no mutation round.🤖 Generated with Claude Code