Files
OneCAD-Draft/docs/automation/DEVELOPING.md
T
Karim Jerbi 3c751af337 docs(mcp): developing guide for bridge contributors
How to iterate the bridge: fast builds, digest pin, session model, discovery, launch serialization, cancellation, error routing, safety model, rebuild swaps, conformance gates. No diary entries, no harness names, no build-specific data.
2026-10-04 17:02:24 +01:00

4.8 KiB

Developing the MCP bridge

Operator-facing setup lives in README.md. This note is for contributors iterating on the bridge itself: the stdio JSON-RPC layer in src/mcp.rs and the operation validators in src/mcp_ops.rs.

Fast iteration: no version bumps needed

  • Run the bridge straight from a local build: target/debug/OpenCADStudio --mcp for speed, target/release/OpenCADStudio --mcp when close to shipping. Both report the same handshake; only the build_profile field differs.
  • Tool-surface changes need no version-number change. serverInfo carries a content tool_schema digest plus the build_rev revision hash, so staleness is observable without a release.
  • If you change any tool schema, the tool_schema_digest_is_pinned test fails: review the diff, then update TOOL_SCHEMA_DIGEST deliberately (never blindly).

Sessions are per client connection

Clients bind the tool surface once when they connect, so after every rebuild, restart the client session (or reconnect its MCP servers) to pick up the new schema. The bridge helps: it watches its own executable and, once it detects a rebuild, finishes the in-flight request and exits, so the next call spawns a fresh bridge. No in-flight call ever fails because of this; at worst the following call pays one respawn.

Session discovery

ocs_sessions reads GUI descriptor files and verifies each one is alive before probing it: process-liveness checks come first (cached snapshots where the OS has no process table to read), dead descriptors are deleted, and every remaining TCP probe uses a bounded timeout. Any step that cannot run fails open toward treating the session as alive, so a broken check can slow discovery but never wedge it.

Concurrent launches are serialized with automation/starting.lock ({"pid":..,"started":unix_secs}, 60s TTL): the first launch_if_none: true caller claims it and spawns the GUI, later callers with a fresh live claim wait for the same descriptor instead of opening another window. Stale claims and claims from dead pids are reclaimed; an unusable directory fails open so a broken lock can never wedge launching. Covered by startup_lock_serializes_concurrent_launches (claim, wait, stale-reclaim, dead-pid-reclaim, release).

Cancellation

run() reads stdin on a feeder thread into a channel (CancelPump); the main loop stays the only stdout writer. Wait loops harvest the channel in short quanta instead of sleeping blind. A notifications/cancelled naming the in-flight request dismisses the GUI prompt with a second {"op":"cancel"} exchange, drops the late result, and sends nothing back. Cancels arriving between polls resolve through a pending-operation map; re-polls for dismissed operations answer cancelled without touching the GUI. Interactive picks (user_select, getpoint) have a ten-minute single-call ceiling ending in an explicit timeout error; everything else keeps the one-minute clamp.

Error routing

Tool-call failures are classified at the source (CallError): tool-domain failures (bad arguments, failed operations) carry model-actionable guidance and stay isError results; bridge and GUI infrastructure failures (no live session, dead transport) become -32603; cancelled requests get no response. Resource-not-found follows the negotiated era (-32602 modern, -32002 legacy). Error results omit structuredContent: strict clients validate it against outputSchema, which only describes success.

Safety model

Never assume the client prompts before destructive calls. ocs_execute is marked destructiveHint: true, but confirmation behavior is client-defined and unobservable from here — server-side validation is the only safety net. Validate every argument before touching the drawing, keep destructive and read-only paths in separate ops, and prefer reversible effects.

Rebuilding while the bridge runs

If the linker refuses to replace the binary while a bridge process runs from it, move the running binary aside under a new name and rebuild. The live bridge keeps serving from the moved file; restart the client afterwards so it respawns from the fresh binary, and delete the moved file once nothing runs from it anymore.

Conformance gates

These tests hold the spec line; keep them green and extend them with every protocol change: tool_schema_digest_is_pinned, initialize_is_spec_pure_with_experimental_build, malformed_requests_get_jsonrpc_errors, legacy_list_results_carry_sep2549_ttl, resource_not_found_code_follows_era, non_object_input_is_invalid_request, unknown_tool_is_a_protocol_error_with_names, cancel_matching_and_wait_deadlines. For end-to-end proof, run the official MCP Inspector CLI against a local build (headless-safe methods only: initialize, tools/list, resources/list, resources/read — anything touching the drawing needs a live desktop session and stays in unit tests).