Files
boocode/openspec/changes/v1.15-mcp-multi/tasks.md
indifferentketchup d27a977d59 v1.15.0-mcp-multi: multi-server MCP client + stdio transport + config file + tool globs
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>
2026-05-24 04:08:42 +00:00

4.4 KiB

v1.15.0-mcp-multi tasks

B1 — Backups

  • mcp-client.ts, config.ts, index.ts, agents.ts, mcp-client.test.ts

B2 — MCP config file schema + loader

  • NEW apps/server/src/services/mcp-config.ts (~50 lines)
  • Zod schema for mcp.json: McpServerConfig with type, url/command/args/env, headers, enabled
  • loadMcpConfig(configPath: string, log): McpServerConfig[] — reads JSON, validates, returns enabled servers
  • Graceful: file missing → log info, return empty array (no MCP)
  • Graceful: parse error → log warn with details, return empty array

B3 — Config.ts: replace MCP env vars

  • Remove MCP_CONTEXT7_URL and MCP_CONTEXT7_API_KEY from Zod schema
  • Add MCP_CONFIG_PATH: z.string().optional() (no default — opt-in)

B4 — Refactor mcp-client.ts to multi-server registry

  • Replace module-level singleton with Map<serverName, {client, transport, tools}>
  • initialize(servers: McpServerConfig[], log) — iterate servers, connect each, discover tools, wrap with <serverName>_<toolName> prefix, apply read-only guard
  • Streamable HTTP transport: reuse existing pattern from v1.14.1
  • Stdio transport: use @modelcontextprotocol/sdk's StdioClientTransport (check SDK exports; fallback to child_process.spawn + NDJSON if SDK doesn't expose it)
  • callTool(prefixedName, args) — extract server name from prefix, route to correct client
  • getTools() — return all tools from all servers, flattened
  • getMcpServers() — return status of each server (name, type, toolCount, connected)
  • Per-server graceful degradation: catch per-server errors, log, skip; continue with others
  • shutdown() — kill stdio child processes, close HTTP clients
  • app.addHook('onClose') calls shutdown

B5 — Startup wiring (index.ts)

  • Read config: const mcpConfigPath = config.MCP_CONFIG_PATH ?? '/data/mcp.json'
  • const mcpServers = loadMcpConfig(mcpConfigPath, app.log)
  • await mcpClient.initialize(mcpServers, app.log)
  • appendMcpTools(mcpClient.getTools())
  • Log summary: "mcp: N servers connected, M tools registered"
  • app.addHook('onClose', () => mcpClient.shutdown())

B6 — AGENTS.md glob patterns

  • apps/server/src/services/agents.ts — in tool whitelist validation, skip validation for entries containing * (can't validate against runtime-discovered tools)
  • NEW helper matchToolGlob(toolName: string, patterns: string[]): boolean — supports * wildcard and ! deny prefix, last-match-wins
  • Wire into executeStreamPhase (stream-phase.ts) where agent tools are filtered: replace exact-match .includes() with matchToolGlob()
  • Export matchToolGlob for test access

B7 — Example config file

  • NEW data/mcp.json with Context7 entry (enabled: true, with URL, no API key)
  • Comment in the file noting it's bind-mounted at /data/mcp.json inside the container

B8 — Tests

  • Update mcp-client.test.ts for multi-server wrapping (tools from two servers, prefix routing)
  • Test: server A fails, server B succeeds — only B's tools registered
  • Test: callTool routes to correct server by prefix
  • Test: shutdown kills stdio transports
  • NEW apps/server/src/services/__tests__/mcp-glob.test.ts
  • Test: exact match ("grep" matches "grep")
  • Test: wildcard ("context7_*" matches "context7_query-docs")
  • Test: deny ("!web_*" excludes "web_search")
  • Test: last-match-wins ("" then "!web_" → web tools excluded)
  • Test: empty pattern list → nothing matches (agent gets no tools — same as current behavior for explicit whitelists)

B9 — Verification

  • npx tsc --noEmit -p apps/server — 0 errors
  • pnpm -C apps/server test — all passing
  • pnpm -C apps/web build — green (no web changes)

B10 — Deploy + smoke

  • Create /data/mcp.json on the host with Context7 enabled
  • Update docker-compose bind mount if needed (data/ already mounted)
  • docker compose up --build -d
  • Check logs for multi-server init
  • Live-smoke: Context7 tool call from chat
  • Disable Context7 in config, restart, confirm zero MCP tools

B11 — Docs + tag

  • CHANGELOG.md entry
  • boocode_roadmap.md retrospective bullet on v1.15 section
  • CLAUDE.md — update MCP references
  • Commit, tag v1.15.0-mcp-multi, push, rebuild