forked from admin/VolcanicAgenticCore
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.
3.2 KiB
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, reusingToolNaming,McpResultMapperandMcpTool.NormalizeSchema. The stdioMcpExtensionis unchanged. - Storage:
<workspace>/.volcanic/mcp/servers.json, like the A2Aagents.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 byauth(headerstoday) 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 aUiRequestOptions.Secretquestion, which the Telegram UI deletes from the chat; Telegram also deletes a/mcp_addmessage 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
RemoteMcpToolper 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 prefixesmcp__volcanic__. - Lifecycle:
SessionStartreconciles with the file and connects in the background.BeforeAgentStartreconciles 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
ToolResultEventhandler 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_addagain), not live (notifications/tools/list_changedis ignored).