Skip to content

Increment 37: rasputin mcp, the commands served over the Model Context Protocol - #236

Merged
skavhaug merged 20 commits into
masterfrom
worktree-mcp-server
Oct 12, 2026
Merged

skavhaug merged 20 commits into
masterfrom
worktree-mcp-server

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 37, docs/increments/37-mcp-server.md (section 12 is the as-built record). 328 net production lines by python3 tools/count_loc.py origin/master HEAD against the design's 260 and worst case 360; under 700.

  • rasputin mcp (and python -m tin_engine mcp) serves every rasputin command as an MCP tool over stdio, on the official mcp Python SDK (Ola's ruling 2026-10-12), installed as the optional extra rasputin[mcp], which the dev extra pulls so CI runs the suite.
  • One tool per command; the JSON schema is derived from the Typer command's own parameters, so schema and CLI cannot drift (a test pins the equality).
  • Each call runs the command as a child process: exit code, stdout and stderr lines, file:// links to the outputs, the run record and --stats as data; refusals come back as errors carrying the CLI's own Error: words; progress notifications per stderr line with a 5 s heartbeat; cancellation kills the child; a jobs cap.
  • README.md gains "Connect an agent" (Claude Desktop and Codex configuration blocks). The CLI's path refusals now name the option (Invalid value for --out: ...).

Evidence: design b990e38c approved after three review rounds; red ee5b5d52 (62 tests), green 851592ba; code review rounds 1 to 3 recorded, round 3 APPROVED; master c317c7e4 merged in as 030fd32b. No refine or mesh code touched, so no @perf acceptance by the rule; no mutant-critical suite, so no mutation round.

🤖 Generated with Claude Code

skavhaug and others added 20 commits October 12, 2026 10:34
…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>
@skavhaug
skavhaug enabled auto-merge October 12, 2026 14:38
@skavhaug
skavhaug added this pull request to the merge queue Oct 12, 2026
Merged via the queue into master with commit 5268e5b Oct 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant