/** * Minimal MCP (Model Context Protocol) stdio client. * * Each `McpClient` instance owns one child process running an MCP server * (e.g. `npx @modelcontextprotocol/server-filesystem /some/path`). It speaks * JSON-RPC 2.0 over stdio per the MCP spec, performs the * initialize → tools/list handshake, and exposes a `callTool` method that * agent tool dispatch routes through. * * Scope of this MVP: * - initialize + tools/list discovery * - tools/call forwarding * - stop() kills the process and rejects in-flight requests * * NOT covered yet (defer to a future iteration): * - resources / prompts / sampling MCP primitives * - capability negotiation beyond "we want tools" * - server-initiated requests (we ignore them) * - reconnect on crash (process exit is fatal for that client) */ import type { McpServer } from '../acp/protocol.js'; export interface McpTool { name: string; description?: string; inputSchema?: Record; } export interface McpResource { uri: string; name?: string; description?: string; mimeType?: string; } export interface McpResourceContent { uri: string; mimeType?: string; /** Text payload — set when the server returns a text resource. */ text?: string; /** Base64 blob payload — set when the server returns binary. */ blob?: string; } export interface McpPrompt { name: string; description?: string; /** Argument metadata if the prompt is parameterised. */ arguments?: { name: string; description?: string; required?: boolean; }[]; } export interface McpPromptMessage { role: 'user' | 'assistant' | 'system'; content: { type: string; text?: string; [k: string]: unknown; }; } /** * Server-initiated `sampling/createMessage` request payload. MCP servers * that opt into the `sampling` capability send this to ask the host LLM * (Codeep, in our case) to generate a completion on their behalf. */ export interface SamplingCreateMessageParams { messages: { role: 'user' | 'assistant'; content: { type: 'text'; text: string; } | { type: 'image'; data: string; mimeType: string; }; }[]; modelPreferences?: { hints?: { name?: string; }[]; costPriority?: number; speedPriority?: number; intelligencePriority?: number; }; systemPrompt?: string; includeContext?: 'none' | 'thisServer' | 'allServers'; temperature?: number; maxTokens?: number; stopSequences?: string[]; metadata?: Record; } export interface SamplingCreateMessageResult { role: 'assistant'; content: { type: 'text'; text: string; }; model: string; stopReason?: 'endTurn' | 'stopSequence' | 'maxTokens'; } export declare class McpClient { readonly server: McpServer; readonly clientOpts: { workspaceRoot?: string; onSamplingRequest?: (params: SamplingCreateMessageParams) => Promise; }; /** Stdio transport state. Null when running over HTTP (or before start). */ private child; /** HTTP transport state. Null when running over stdio. */ private http; private pending; private buffer; private stopped; private toolsCache; /** True when this client is configured for the Streamable HTTP transport. */ private get isHttp(); /** * Rolling-window record of recent crash times (ms epoch). Used by the * auto-reconnect logic: too many crashes in a short window → give up * instead of spinning indefinitely on a broken server. */ private crashTimestamps; /** Reconnect tuning — generous defaults, configurable via env if needed. */ private readonly MAX_RESTARTS; private readonly RESTART_WINDOW_MS; /** Has the agent loop been notified that this server is fully gone? */ private gaveUp; /** * Optional callback fired after a successful auto-restart. The registry * uses this to drop its tools cache so the next `listTools()` re-queries * (the server may expose a different tool set after restart). */ onRestart?: () => void; /** * Optional callback fired when the client gives up after exceeding the * restart budget. The registry uses this to surface a visible "MCP * server died" error in /mcp. */ onGaveUp?: (reason: string) => void; /** * Optional callback fired when the server sends a `notifications/*` * indicating its catalog changed (tools, resources, prompts). The * registry forwards this up so the agent loop can re-fetch on the next * iteration. */ onCatalogChanged?: (kind: 'tools' | 'resources' | 'prompts') => void; /** * @param server MCP server config (command, args, env, name). * @param opts Optional client metadata. * - `workspaceRoot` exposed to the server as a root via * the `roots` capability so filesystem-style servers * can scope their reads. * - `onSamplingRequest` makes the client advertise the * `sampling` capability and routes server-initiated * `sampling/createMessage` to the host LLM. */ constructor(server: McpServer, clientOpts?: { workspaceRoot?: string; onSamplingRequest?: (params: SamplingCreateMessageParams) => Promise; }); /** Open the transport and perform the MCP handshake. */ start(opts?: { initTimeoutMs?: number; }): Promise; /** Discover tools the server exposes. Cached on first call. */ listTools(): Promise; /** * Discover resources the server exposes. Not all servers implement * resources/list — those return a `-32601 Method not found`, which we * surface as an empty array (callers can treat absence and emptiness * the same way). */ listResources(): Promise; /** Read one resource by URI. */ readResource(uri: string): Promise; /** Discover prompt templates the server exposes (optional capability). */ listPrompts(): Promise; /** Materialise a prompt template into its message sequence. */ getPrompt(name: string, args?: Record): Promise<{ description?: string; messages: McpPromptMessage[]; }>; /** Invoke a tool on this server. */ callTool(name: string, args: Record, opts?: { timeoutMs?: number; }): Promise; /** * Attempt to spawn a fresh child process after a crash. Tries up to * MAX_RESTARTS times within RESTART_WINDOW_MS, then gives up. After a * successful restart, `toolsCache` is cleared so the next listTools() * re-queries — the server may legitimately expose different tools after * a code reload. */ private attemptRestart; /** Wire up data/exit/error listeners on the current child. Used by start() and attemptRestart(). */ private attachChildHandlers; /** Tear down the transport (stdio child or HTTP stream) and reject pending requests. */ stop(): Promise; private handleStdout; /** * Handle a request from the MCP server (server-initiated JSON-RPC). * Currently handled methods: * - `roots/list` — return the workspace folder if provided * - `sampling/createMessage` — delegate to the host LLM callback if * one was wired into the constructor; otherwise -32601 (so a * server that asks without us advertising the capability gets a * clear "no" instead of a hang). * * Anything else replies with `-32601 Method not found` per JSON-RPC spec. */ private handleServerRequest; /** Serialise and send a JSON-RPC response over whichever transport is active. */ private writeResponse; /** * Single send path used by request/notify/writeResponse. Stdio just * pipes the serialised frame + newline. HTTP POSTs the JSON body; the * response (or any later SSE event) re-enters via `dispatchFrame`. * Errors on the HTTP path reject pending request promises so the * agent doesn't hang waiting on a frame that'll never come. */ private writeFrame; /** * Common entry point for every incoming JSON-RPC frame, regardless of * transport. Stdio's `handleStdout` parses lines and forwards each * here; the HTTP transport calls this directly from its `onFrame`. */ private dispatchFrame; private request; private notify; }