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>
131 lines
7.3 KiB
Markdown
131 lines
7.3 KiB
Markdown
# 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 `<server-name>_<tool-name>` 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).
|