forked from 1b-agent/opencadstudio-upstream
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.
This commit is contained in:
1 parent
b8caa5ccac
commit
3c751af337
1 file changed
+98
@@ -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).
|
||||
Reference in new issue
Block a user