Files
Playa/docs/superpowers/specs/2026-10-09-remote-mcp-per-chat-design.md
EugeneTes 891a644590 feat(mcp): remote MCP servers per chat with sealed keys and owner-only commands
Streamable HTTP/SSE servers from the workspace's .volcanic/mcp/servers.json.
Headers are sealed with AES-GCM; the chat and the visible fields are the
associated data, so only /mcp_add|remove|tools change servers. Tools
<server>__<tool> go live in the session. An unreachable server is skipped
and reported in the system prompt. Keys are redacted from tool results, and
Telegram deletes messages that carry one.
2026-10-09 21:11:01 +00:00

3.2 KiB

Remote MCP servers per chat — design

Goal

Let a chat's owner connect their own remote MCP servers (Streamable HTTP, SSE fallback) to that chat's agent. No stdio, no processes. The agent must not be able to add or redirect servers, keys must not reach the model, and an unreachable server must not stop the session.

Decisions

  • Separate extension RemoteMcpExtension (mcp-remote) in the existing Mcp assembly, reusing ToolNaming, McpResultMapper and McpTool.NormalizeSchema. The stdio McpExtension is unchanged.
  • Storage: <workspace>/.volcanic/mcp/servers.json, like the A2A agents.json, but each entry's headers are sealed with AES-256-GCM. The key comes from a host secret via HKDF (MCP_ENCRYPTION_SECRET, else the bot token; both are on the deny-list). The associated data is the chat's session scope plus every visible field (name, url, transport, tools, auth). This makes the seal an authenticity check: the agent can write to the workspace but can't create, redirect or move an entry, so only the commands add servers. The OAuth case is covered by auth (headers today) plus the sealed part.
  • Commands only (/mcp_list, /mcp_add, /mcp_remove, /mcp_tools). Commands never enter the conversation. The key comes inline or as the answer to a UiRequestOptions.Secret question, which the Telegram UI deletes from the chat; Telegram also deletes a /mcp_add message that carries --bearer/--header. The commands are registered even without a secret, so a /mcp_add … --bearer … never falls through to the model.
  • Live tools: the registry allows registration after activation but no removal. One RemoteMcpTool per exposed name is therefore re-bound (description, schema, connection) on reconnect or re-add, and unbound plus deactivated on removal. Names: <server>__<tool>. mcp__ is avoided because the Claude Code bridge already prefixes mcp__volcanic__.
  • Lifecycle: SessionStart reconciles with the file and connects in the background. BeforeAgentStart reconciles again (this picks up a deleted file), waits for connects in flight (each ≤ ConnectTimeout, 15 s), retries failed servers with backoff (1 min doubling to 15 min), and writes a "Remote MCP servers" prompt section. That section names unavailable servers and why, which is how the agent learns about an error. A transport failure during a call drops the client, and the next call reconnects. Calls are not retried.
  • Redaction: a ToolResultEvent handler replaces header values (and the credential after a scheme) in every tool result of the chat. Errors and command replies go through the same redactor.

Limits

  • Only header auth; no OAuth flow yet.
  • The agent can delete servers.json (dropping servers) or restore an older copy of it that it saved earlier (bringing back a removed server). It can't forge or alter an entry.
  • The key is visible in Telegram until the bot deletes the message (a moment), and Telegram keeps it on its servers until then.
  • A changed tool list on the server is picked up on reconnect (a new session after a restart, or /mcp_add again), not live (notifications/tools/list_changed is ignored).