# 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): ```json { "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) or `command` + `args` + `env` (stdio) - `headers` (HTTP, optional) — for API keys - `enabled` (boolean, default true) ### S2. Multi-server MCP client Refactor `mcp-client.ts` from a singleton to a registry of named MCP clients. On startup: 1. Read `/data/mcp.json` (or path from `MCP_CONFIG_PATH`) 2. For each enabled server: create a Client + transport, connect, discover tools via `tools/list` 3. Wrap tools with `_` prefix (generalizes the `context7_` pattern) 4. Apply read-only invariant guard per-tool (reject `readOnlyHint: false`) 5. Append all MCP tools to `ALL_TOOLS` in a single `appendMcpTools()` call 6. 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 server - `view_*` — all tools starting with `view_` - `!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_enabled` column in v1.15. MCP servers are globally configured; agent tool globs are the scoping mechanism. - **`mcp_servers` DB 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 `executeToolCall` dispatch (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 with `MCP_CONFIG_PATH` - `apps/server/src/index.ts` — startup reads config file, iterates servers - `apps/server/src/services/agents.ts` — glob pattern support in `tools:` whitelist - `data/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 tests - `apps/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 1. Create `/data/mcp.json` with Context7 enabled. Restart. Confirm tools discovered + logged. 2. Send a chat asking about library docs. Confirm `context7_*` tools called + results rendered. 3. Disable Context7 in config (`"enabled": false`). Restart. Confirm zero MCP tools. 4. Add a dummy stdio server entry pointing to `/bin/cat` (will fail). Confirm graceful degradation: Context7 works, dummy fails with a logged warning. 5. Add `tools: [context7_*]` to the Architect agent in AGENTS.md. Confirm Architect sees only Context7 tools (via AgentPicker or by chatting with Architect selected). 6. Stop boocode, confirm child processes are killed (no orphans).