Generalizes the v1.14.1 single-server Context7 PoC into a multi-server MCP client registry with per-server graceful degradation. JSON config at /data/mcp.json (bind-mounted alongside AGENTS.md) matches opencode's mcpServers schema shape. Config file missing = no MCP (opt-in by presence). Two transports: Streamable HTTP (remote servers like Context7) and stdio (local subprocess servers like codecontext). Stdio spawns a persistent child via the SDK's StdioClientTransport; shutdown hook closes all transports. Tool prefix generalized from context7_<name> to <serverName>_<toolName> with a toolToServer reverse map for dispatch routing. AGENTS.md tools: field now supports glob patterns (context7_*, !web_*) via matchToolGlob — last-match- wins with ! deny prefix. Replaces exact-match .includes() in stream-phase.ts. refreshToolNames() in agents.ts rebuilds the DEFAULT_TOOLS snapshot after appendMcpTools so agents without explicit tools: lists see MCP tools — reviewer caught that the module-load-time snapshot would permanently exclude late-registered tools. Read-only invariant: readOnlyHint === false rejected at discovery. Result size capped at 5MB. v1.14.1 env vars removed — superseded by config file. Default data/mcp.json ships with Context7 disabled. 363/363 server tests passing. No schema changes, no frontend changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.3 KiB
v1.15.0-mcp-multi — multi-server MCP client + stdio transport + config file
Generalize the v1.14.1 single-server Context7 PoC into a multi-server MCP client. Add stdio transport (for local subprocess MCP servers like codecontext). JSON config file matching opencode's schema shape. Per-agent tool glob patterns in AGENTS.md frontmatter.
Why
v1.14.1 proved the MCP loop works end-to-end but is hardcoded to one server (Context7) via env vars. Real value comes from multiple servers: Context7 for docs, codecontext re-wired as a proper MCP server (stdio), future local tools. The config shape should match opencode's so Sam can copy mcp blocks between the two without translation.
Scope
S1. JSON config file for MCP servers
New file at /data/mcp.json (bind-mounted like AGENTS.md). Env var MCP_CONFIG_PATH points to it (default /data/mcp.json).
Schema (matching opencode's shape):
{
"mcpServers": {
"context7": {
"type": "streamableHttp",
"url": "https://mcp.context7.com/mcp",
"headers": { "X-API-Key": "optional-key" },
"enabled": true
},
"codecontext": {
"type": "stdio",
"command": "/usr/local/bin/codecontext",
"args": ["--mcp"],
"env": { "WORKSPACE": "/opt" },
"enabled": false
}
}
}
Zod-validated at startup. Unknown keys silently ignored (forward-compat). Each server entry has:
type:"streamableHttp"|"stdio"(SSE deferred — Streamable HTTP supersedes it per the MCP spec)url(HTTP) orcommand+args+env(stdio)headers(HTTP, optional) — for API keysenabled(boolean, default true)
S2. Multi-server MCP client
Refactor mcp-client.ts from a singleton to a registry of named MCP clients. On startup:
- Read
/data/mcp.json(or path fromMCP_CONFIG_PATH) - For each enabled server: create a Client + transport, connect, discover tools via
tools/list - Wrap tools with
<server-name>_<tool-name>prefix (generalizes thecontext7_pattern) - Apply read-only invariant guard per-tool (reject
readOnlyHint: false) - Append all MCP tools to
ALL_TOOLSin a singleappendMcpTools()call - Per-server graceful degradation: one server failing doesn't block others
Expose: getMcpServers(): McpServerStatus[] for debug/status endpoint, callTool(prefixedName, args) routed to the correct server by prefix.
S3. Stdio transport
For type: "stdio" servers: spawn a subprocess via child_process.spawn(command, args, {env, stdio: 'pipe'}). Use @modelcontextprotocol/sdk's StdioClientTransport (or implement the NDJSON framing ourselves — the SDK should have it). The subprocess runs for the lifetime of the BooCode server (persistent connection, not per-call spawn).
Child lifecycle:
- Spawn on initialize. If spawn fails, log warn, skip server (graceful degradation).
- On child exit: log error, mark server as unavailable. Do NOT restart automatically (v1.15 keeps it simple; auto-restart is a v2.0 concern).
- On BooCode shutdown (
app.addHook('onClose')): kill child processes.
S4. Per-agent tool glob patterns in AGENTS.md
Currently tools: in AGENTS.md frontmatter is an exact-match whitelist (array of tool names). Extend to support glob patterns via a lightweight matcher:
context7_*— all tools from the context7 serverview_*— all tools starting withview_!web_*— exclude web tools (deny pattern)- Plain names (
grep,view_file) work as before (exact match)
Evaluation order: for each tool in ALL_TOOLS, check if it matches any pattern in the agent's tools: list. A ! prefix means exclude. Last-match-wins.
Parser change in agents.ts: when validating tools:, don't reject unknown names if they contain * (glob patterns can't be validated against the current tool list since MCP tools are discovered at runtime). Exact names are still validated.
S5. Remove v1.14.1 env-var config
Delete MCP_CONTEXT7_URL and MCP_CONTEXT7_API_KEY from config.ts. They're superseded by the JSON config file. The v1.14.1 PoC is throwaway-by-design (proposal said "throwaway-if-needed").
S6. Read-only invariant preserved
BooChat's read-only guarantee stays: every MCP tool with readOnlyHint: false is rejected at discovery. This applies globally, not per-server. Config has no allowWriteTools flag — that's a v2.0 BooCoder concern.
Deferred to v2.0
- Permission ruleset tables (
permissions,agent_permissions,session_permissions). Enterprise pattern that doesn't serve until BooCoder adds write tools. The read-only invariant guard is the BooChat-era defense-in-depth. - OAuth / Dynamic Client Registration. Needs secret storage primitive first.
- SSE transport. Streamable HTTP supersedes it per the MCP spec. SSE is a legacy fallback.
- Per-session MCP toggle. No
session.mcp_enabledcolumn in v1.15. MCP servers are globally configured; agent tool globs are the scoping mechanism. mcp_serversDB table. In-memory registry is sufficient for single-user. DB tracking deferred to v2.0.- codecontext re-wiring to MCP. Separate batch after v1.15 proves stdio transport works.
Non-goals
- No frontend changes. MCP tools surface via the existing tool registry; results render as normal tool-result parts.
- No schema changes. No new DB tables or columns.
- No changes to the inference loop (v1.14.0 outer loop unchanged).
- No changes to
executeToolCalldispatch (transparent via ToolDef.execute).
Hard rules
- No git commit/push. Sam commits.
- Read-only invariant: reject any MCP tool with
readOnlyHint: false. - Graceful degradation: any server down → that server's tools unavailable, rest unaffected.
- Alpha-sort of ALL_TOOLS preserved.
- One new dep only: none (MCP SDK already installed from v1.14.1).
- 348+ existing tests still pass.
Files expected to touch
apps/server/src/services/mcp-client.ts— refactor from singleton to multi-server registry (~200→300 lines)apps/server/src/services/tools.ts— no changes expected (appendMcpTools already works for multiple tools)apps/server/src/config.ts— replace MCP env vars withMCP_CONFIG_PATHapps/server/src/index.ts— startup reads config file, iterates serversapps/server/src/services/agents.ts— glob pattern support intools:whitelistdata/mcp.json— NEW, example config with Context7 (disabled by default, enabled via edit)apps/server/src/services/__tests__/mcp-client.test.ts— update for multi-server, add stdio transport testsapps/server/src/services/__tests__/agents-glob.test.ts— NEW, glob pattern matching tests
Estimate
~350 LoC. The MCP SDK handles both transports; BooCode's job is config parsing, multi-server lifecycle, and glob matching.
Smoke plan
- Create
/data/mcp.jsonwith Context7 enabled. Restart. Confirm tools discovered + logged. - Send a chat asking about library docs. Confirm
context7_*tools called + results rendered. - Disable Context7 in config (
"enabled": false). Restart. Confirm zero MCP tools. - Add a dummy stdio server entry pointing to
/bin/cat(will fail). Confirm graceful degradation: Context7 works, dummy fails with a logged warning. - Add
tools: [context7_*]to the Architect agent in AGENTS.md. Confirm Architect sees only Context7 tools (via AgentPicker or by chatting with Architect selected). - Stop boocode, confirm child processes are killed (no orphans).