cc-libs/docs/adrs/adr-0019-mcp-over-sandbox-gateway.md
2026-06-15 19:57:45 +02:00

73 lines
3.8 KiB
Markdown

# ADR 0019: MCP Over The Sandbox Gateway
## Status
Accepted (supersedes the transport of [ADR-0017](adr-0017-mcp-remote-lua-execution.md))
## Date
2026-06-15
## Context
The MCP tools (`probe-computers`, `exec-lua`, `write-file`) shipped in `tools/mcp-bridge`
([ADR-0017](adr-0017-mcp-remote-lua-execution.md)) over a dedicated CC-link WebSocket and
the `mcp-computer` / `mcp-computer-server` Lua programs.
Meanwhile `tools/trap-sandbox` introduced a second, richer host↔computer connection: a
single persistent gateway WebSocket per computer (`servers/sandbox` + `apis/libsandbox`),
a hello handshake with a shared secret, a coded error model, and a bidirectional
request/reply (`GatewayRegistry.request` → `gateway_request``computer_response`).
Running both stacks meant two sockets, two protocols, and a second autostart daemon per
computer. The sandbox gateway already had everything MCP needs.
## Decision
Move the MCP features onto the sandbox gateway and retire the dedicated MCP transport.
- **Host (`tools/trap-sandbox`):** a second Fastify listener (`src/modules/mcp`) bound to
`127.0.0.1` (default `MCP_PORT=4445`) speaks the same custom JSON-RPC 2.0 as the old
bridge. Each tool call is a `GatewayRegistry.request(accountId, traposId, type, payload)`
reusing the existing gateway socket. Computers are addressed by **`traposId`**
(`"<id>:<label>"`, the registry's key) rather than a numeric `computerId`;
`probe-computers` lists the connected traposIds.
- **Computer side:** `apis/libsandbox` gains a reactive serving path symmetric to its
request path. In-OS servers call `sandbox.serve(type, handler, { eventloop })`, which
advertises the type to the daemon (`trapos_sandbox_serve`) and reacts to inbound
`trapos_sandbox_request` events, replying via `trapos_sandbox_respond`
(`computer_response`). Unadvertised non-`ping` types still get an immediate
`unknown_type` reply (the daemon tracks advertised types, so there is no timer race).
`ping` stays built-in, so `probe-computers` works against any computer running just the
sandbox daemon.
- **`servers/mcp-computer-server`** is rewritten to be reactive: it owns no socket, serves
`exec-lua` / `write-file` over the gateway via `sandbox.serve`, and is gated on
`sandbox.url` being set. `apis/libmcpcomputer` is slimmed to the protocol-agnostic
execution primitives (`executeLua`, `writeFile`); both move into the `trapos-sandbox`
package (autostart `servers/mcp-computer-server` after `servers/sandbox`). The standalone
`programs/mcp-computer` client is deleted and the MCP entries are removed from
`trapos-sandbox-legacy`.
- A code error or a failed write is reported as a *successful* round-trip carrying an
`ok:false` result payload (preserving captured `output`), matching the old bridge's
semantics; only a dropped connection or timeout is a transport-level failure.
`tools/mcp-bridge` is **not** deleted: its opencode HTTP-over-WS proxy is still used by the
in-game `ai` bridge mode (`apis/libhttpws`, `apis/libai`). Only the MCP server and CC-link
parts are superseded here; the proxy migration is deferred.
## Consequences
- One connection and one protocol per computer; no `mcp-computer.ws-url` setting.
- The exec trust boundary is now the authenticated gateway connection (`sandbox.url` +
`SANDBOX_PASSWORD`) plus the local-only MCP bind, instead of "trust the bridge you run
`mcp-computer` against."
- `apis/libsandbox.serve` / `respond` are reusable for any future computer-side service
invoked from the host, not just MCP.
- Tests cover host-side MCP routing (unit) and the real CraftOS gateway round-trip
(integration), per [ADR-0016](adr-0016-js-tool-verification.md).
## Future Work
- Migrate the opencode HTTP proxy off `mcp-bridge` so the tool can be fully retired.
- Per-computer exec policy if the gateway is ever shared beyond trusted development.