diff --git a/docs/automation/DEVELOPING.md b/docs/automation/DEVELOPING.md new file mode 100644 index 000000000..dae5f5da8 --- /dev/null +++ b/docs/automation/DEVELOPING.md @@ -0,0 +1,98 @@ +# Developing the MCP bridge + +Operator-facing setup lives in [README.md](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).