v1.14.1-mcp-poc: single-server MCP client against Context7

Validates the MCP-client loop end-to-end against one real MCP server before
the full v1.15 port. New services/mcp-client.ts wraps @modelcontextprotocol/sdk
v1.29.0 with Streamable HTTP transport. On startup (when MCP_CONTEXT7_URL is
set), connects to Context7, discovers tools via tools/list, wraps each as a
ToolDef prefixed context7_<name>, and appends to ALL_TOOLS via appendMcpTools.

Read-only invariant guard rejects any tool with readOnlyHint: false. Tool
dispatch is transparent — executeToolCall routes MCP calls through the ToolDef
execute wrapper, which strips the prefix before calling the MCP server. Result
size capped at 5MB with truncation. Graceful degradation: server down at
startup → zero tools; server down mid-session → error result, model
self-corrects.

Adversarial review caught that a Zod .default() on the URL config made MCP
always-on instead of opt-in — fixed by removing the default. MCP_CONTEXT7_URL
must be explicitly set to enable.

ALL_TOOLS changed from ReadonlyArray to mutable to support late-registration.
appendMcpTools re-sorts and rebuilds TOOLS_BY_NAME after append.

348/348 server tests passing (16 new mcp-client tests). No schema changes,
no frontend changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 21:58:09 +00:00
parent f4a97808ad
commit 5692e99a5d
11 changed files with 612 additions and 6 deletions

View File

@@ -651,7 +651,9 @@ export const askUserInput: ToolDef<AskUserInputInputT> = {
// of the system prompt, so any order drift would invalidate every cached
// turn. Single source of truth for ordering lives here — toolJsonSchemas()
// and TOOLS_BY_NAME inherit it.
export const ALL_TOOLS: ReadonlyArray<ToolDef<unknown>> = [
// v1.14.1-mcp-poc: changed from ReadonlyArray to let-bound mutable array
// so appendMcpTools() can push MCP-discovered tools at startup.
export let ALL_TOOLS: ToolDef<unknown>[] = [
viewFile as ToolDef<unknown>,
viewTruncatedOutput as ToolDef<unknown>,
listDir as ToolDef<unknown>,
@@ -725,10 +727,23 @@ export const READ_ONLY_TOOL_NAMES = [
'request_read_access',
] as const;
export const TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntries(
export let TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntries(
ALL_TOOLS.map((t) => [t.name, t])
);
// v1.14.1-mcp-poc: append MCP-discovered tools at startup. Called once
// from index.ts after mcpClient.initialize(). Re-sorts ALL_TOOLS and
// rebuilds TOOLS_BY_NAME. READ_ONLY_TOOL_NAMES is not rebuilt because
// it's a const tuple used only for budget-tier checks; MCP tools are
// individually checked via their category at budget resolution time —
// they are all read_only by construction (the read-only guard in
// mcp-client.ts rejects any tool with readOnlyHint: false).
export function appendMcpTools(mcpTools: ToolDef<unknown>[]): void {
if (mcpTools.length === 0) return;
ALL_TOOLS = [...ALL_TOOLS, ...mcpTools].sort((a, b) => a.name.localeCompare(b.name));
TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map((t) => [t.name, t]));
}
// v1.13.15-tools: tiered tool loading. BOOCODE_TOOLS env var (`core` |
// `standard` | `all`) filters the agent's tool whitelist before LLM dispatch.
// Daily-driver token win on qwen3.6-35b-a3b — the 35B-A3B MoE benefits from