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)

← ADR-0098 · ADR-0100 →

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)

  1. 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 at GATEWAY_HOST:GATEWAY_PORT, authenticates with GATEWAY_TOKEN from the deployed environment (~/.env secrets 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 own ScopeGuard, 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).
  2. One declarative method table, read by both transports. _build_method_registry is refactored into a module-level table METHODS: tuple[MethodSpec, ...] (name, handler reference by dotted path, mutates: bool, expose: bool, optional params_schema), importable without constructing anything; RPCHandler builds its dict from that table and McpTransport builds 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.
  3. Gated unless marked read-only; streaming and self-referential methods are not tools. mutates defaults to True for 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 in list, get, query, status, info, rules, decisions, current, health, plus ping, each confirmed by reading the handler, not by the name alone. subscribe, publish, cxp.subscribe and the other streaming methods are expose=False (MCP tool calls are request/response); legacy aliases and auth.authenticate are expose=False (the transport authenticates itself). With no params schema the tool's input_schema is {"type":"object"} and the gateway's own validation is the contract, as it is for every other client.

Alternatives rejected

Consequences

Rulings (recorded the same turn, one rule at a time)

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.

Relations of ADR-0099

Rulings cited

None.

Specs cited