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.
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 --mcpfor speed,target/release/OpenCADStudio --mcpwhen close to shipping. Both report the same handshake; only thebuild_profilefield differs. - Tool-surface changes need no version-number change.
serverInfocarries a contenttool_schemadigest plus thebuild_revrevision hash, so staleness is observable without a release. - If you change any tool schema, the
tool_schema_digest_is_pinnedtest fails: review the diff, then updateTOOL_SCHEMA_DIGESTdeliberately (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).