Home / Projects / Orchestrata Ultimate / Decisions / ADR index / ADR-0099
ADR-0099: The gateway's MCP transport is a WebSocket JSON-RPC client of the running gateway, over one declarative method table that both transports read, and every method is approval-gated unless marked read-only
| field | value |
|---|---|
| Status | Accepted — Accepted (ruling, Mohamed, 2026-10-13, one rule at a time, verbatim each time: "agreed, next" at 17:58, 18:03 and 18:21) |
| Date | 2026-10-13 |
| Register | factory (docs/adr, 4-digit ids) |
| Source | docs/adr/0099-the-gateway-mcp-transport-is-a-websocket-json-rpc-client-of-the-running-gateway-over-one-declarative-method-table-and-every-method-is-gated-unless-marked-read-only.md — permalink at 7fcf7c79f (unchanged on master since that commit) |
| Wiki register entry | ADR-0099 (wiki status: Accepted) |
The record
Status: Accepted (ruling, Mohamed, 2026-10-13, one rule at a time, verbatim each time: "agreed, next" at 17:58, 18:03 and 18:21) · Date: 2026-10-13 · Executes ADR-0038 rule 4 (orchestrata gets the same adapter) where the brief's premise met the code; three forks reported by the builder on 2026-10-13 17:31, none settled by ADR-0038
Context
The ADR-0038 brief (plans/briefs/2026-10-13-mcp-gateway-transport-and-deployed-registration.md, master 4b772201d) assumed the gateway exposes its verbs over HTTP and carries a mutation marker per verb. The builder read the code before writing any and stopped:
| Premise in the brief | What the code says (gateway/http_surface.py, gateway/rpc.py, gateway/handlers/) |
|---|---|
| calls run "over the existing HTTP surface" | HTTP has /health, /api/v1/health, /ready, /metrics, the postman health router; CORS allows GET and OPTIONS; no RPC route |
| a handler table exists as data | RPCHandler.dispatch(method, connection_id, params) reaches a dict built by _build_method_registry(...) on a constructed handler that needs a ConnectionManager, PubSub, ring buffer and audit writer; not importable without building them |
| handlers say whether they mutate | no mutates/read-only marker anywhere; about 80 methods (gateway.*, cxp.*, volk.*, session.*, governance.*, knowledge.*, projects.*, orchestrator.intent, tui.*, state.*, auth.authenticate, fireflies.*, memory.*, tests.run, metrics.current, workflow.*, config.*), legacy aliases (ping, subscribe, publish), fallback lambdas for absent handlers |
| per-method input schema | handlers take (connection_id, params: dict); no schema |
Dispatch is reachable only through the WebSocket JSON-RPC server, with per-connection state (ScopeGuard, auth.authenticate, an admin token for volk.register/volk.deregister). Configuration names GATEWAY_HOST, GATEWAY_PORT, GATEWAY_TOKEN.
Decision (proposed; three rules, one per fork)
- Transport: a WebSocket JSON-RPC client of the running gateway.
orchestrata-mcp(stdio, launched by Claude from/opt/orchestrata/bin) opens one WebSocket to the deployed gateway atGATEWAY_HOST:GATEWAY_PORT, authenticates withGATEWAY_TOKENfrom the deployed environment (~/.envsecrets layer, never a flag, never written to the ledger), and forwards each tool call as the JSON-RPC method it names. Every call therefore passes the gateway's ownScopeGuard, auth and audit exactly as the TUI's do. No new HTTP RPC route (a second dispatch surface with its own CORS and auth story), no in-process dispatch (bypasses auth and audit and runs code that is not the deployed gateway). - One declarative method table, read by both transports.
_build_method_registryis refactored into a module-level tableMETHODS: tuple[MethodSpec, ...](name, handler reference by dotted path,mutates: bool,expose: bool, optionalparams_schema), importable without constructing anything;RPCHandlerbuilds its dict from that table andMcpTransportbuilds its tool list from the same table (ADR-0038 rule 1, "one registry, two transports"). A method added to the table appears in both; a method added anywhere else is a test failure. - Gated unless marked read-only; streaming and self-referential methods are not tools.
mutatesdefaults toTruefor any entry that does not say otherwise, so an unmarked method is approval-gated (fails closed). The same change marks the read-only set explicitly: methods whose names end inlist,get,query,status,info,rules,decisions,current,health, plusping, each confirmed by reading the handler, not by the name alone.subscribe,publish,cxp.subscribeand the other streaming methods areexpose=False(MCP tool calls are request/response); legacy aliases andauth.authenticateareexpose=False(the transport authenticates itself). With no params schema the tool'sinput_schemais{"type":"object"}and the gateway's own validation is the contract, as it is for every other client.
Alternatives rejected
- New HTTP RPC route on the gateway. Doubles the dispatch surface and needs its own auth and CORS decisions; the WebSocket path already has both.
- Construct an
RPCHandlerat transport start to read its keys. Builds the gateway's infrastructure for a list of names and runs side effects in a client process. - Name-convention mutation rule only.
tests.runandworkflow.*read as neutral names and mutate; silent misclassification is exactly what the approval gate exists to prevent. Names are a starting list; the marker is the rule. - Gate everything. Satisfies safety and breaks ADR-0038 rule 2's read-only parity: every graph query would need a token.
Consequences
- The brief gains rules 1 to 3 and its FILES list gains
gateway/method_table.py(the table andMethodSpec) and therpc.pyrefactor; themcpSDK is added to the lock (same dependency as codetools, confirmed 2026-10-13). - The read-only marking is reviewed per handler in the same commit; the report lists every method with its
mutatesandexposevalues, which is the first complete inventory of the gateway's surface. - Spec 014 gains REQ ids for the table, the transport, the gate and the exposure rule; the BDD scenarios run against a real gateway on a test port over a real WebSocket.
Rulings (recorded the same turn, one rule at a time)
- Rule 1 (transport: WebSocket JSON-RPC client of the running gateway): Accepted — Mohamed, 2026-10-13 17:58, verbatim: "agreed, next".
- Rule 2 (one declarative method table read by both transports): Accepted — Mohamed, 2026-10-13 18:03, verbatim: "agreed, next".
- Rule 3 (gated unless marked read-only; streaming, aliases and auth not exposed): Accepted — Mohamed, 2026-10-13 18:21, verbatim: "agreed, next". With all three rules ruled the ADR is Accepted.
Provenance
Builder stop report of 2026-10-13 17:31 (worktree mcp-gateway, no edits, uv sync --frozen only), quoted in Context. Drafted by the coordinating session the same turn; the recommendation is rules 1 to 3 as written. Awaiting Mohamed's ruling, one rule at a time.
Related ADRs
Rulings cited
None.