repo_description: Build your own Claude Code from scratch.  🔍 Claude Code 开源了 50 万行代码，读不动？用 ~4000 行 TypeScript / Python 从零复现核心架构，11 章分步教程带你理解 coding agent 精髓
repo_topics: ai ai-agent anthropic build-from-scratch claude claude-code coding-agent llm tutorial typescript
code: /**
   * MCP Client — connects to stdio-based MCP servers, discovers and forwards tool calls.
   * Uses raw JSON-RPC over stdio (no SDK dependency for simplicity).
   *
   * Config is read from .claude/settings.json and ~/.claude/settings.json:
   *   { "mcpServers": { "name": { "command": "...", "args": [...], "env": {...} } } }
   *
   * Each MCP tool is exposed with a "mcp__serverName__toolName" prefix to avoid conflicts.
   */
  
  import { spawn, type ChildProcess } from "child_process";
  import { readFileSync, existsSync } from "fs";
  import { join } from "path";
  import { homedir } from "os";
  import { createInterface, type Interface } from "readline";
  
  // ─── Types ──────────────────────────────────────────────────
  
  interface McpServerConfig {
    command: string;
    args?: string[];
    env?: Record<string, string>;
  }
  
  interface McpToolInfo {
    name: string;
    description?: string;
    inputSchema?: any;
    serverName: string;
  }
  
  // ─── Single MCP connection (one per server) ─────────────────
  
  class McpConnection {
    private process: ChildProcess | null = null;
    private nextId = 1;
    private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
    private rl: Interface | null = null;
  
    constructor(private serverName: string, private config: McpServerConfig) {}
  
    /** Spawn the server process and wire up JSON-RPC over stdio. */
    async connect(): Promise<void> {
      const env = { ...process.env, ...(this.config.env || {}) };
      this.process = spawn(this.config.command, this.config.args || [], {
        stdio: ["pipe", "pipe", "pipe"],
        env,
      });
  
      // Parse newline-delimited JSON-RPC from stdout
      this.rl = createInterface({ input: this.process.stdout! });
      this.rl.on("line", (line: string) => {
        try {
          const msg = JSON.parse(line);
          if (msg.id !== undefined && this.pending.has(msg.id)) {
            const { resolve, reject } = this.pending.get(msg.id)!;
            this.pending.delete(msg.id);
            if (msg.error) {
              reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
            } else {
              resolve(msg.result);
            }
          }
        } catch {
          // ignore non-JSON lines (e.g. server logs)
        }
      });
  
      // Surface stderr as warnings (don't crash)
      this.process.stderr?.on("data", () => {});
  
      this.process.on("error", (err) => {
        console.error(`[mcp:${this.serverName}] process error: ${err.message}`);
      });
  
      this.process.on("exit", (code) => {
        // Reject all pending requests
        for (const [, { reject }] of this.pending) {
          reject(new Error(`MCP server '${this.serverName}' exited with code ${code}`));
        }
        this.pending.clear();
      });
    }
  
    /** Send a JSON-RPC request and wait for the response. */
    private sendRequest(method: string, params: any = {}): Promise<any> {
      return new Promise((resolve, reject) => {
        if (!this.process?.stdin?.writable) {
          return reject(new Error(`MCP server '${this.serverName}' is not connected`));
        }
        const id = this.nextId++;
        this.pending.set(id, { resolve, reject });
        const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
        this.process.stdin.write(msg);
      });
    }
  
    /** Send a JSON-RPC notification (no id, no response expected). */
    private sendNotification(method: string, params: any = {}): void {
      if (!this.process?.stdin?.writable) return;
      const msg = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
      this.process.stdin.write(msg);
    }
  
    /** Perform MCP initialize handshake. */
    async initialize(): Promise<void> {
      await this.sendRequest("initialize", {
        protocolVersion: "2024-11-05",
        capabilities: {},
        clientInfo: { name: "mini-claude", version: "1.0.0" },
      });
      this.sendNotification("notifications/initialized");
    }
  
    /** Discover available tools from this server. */
    async listTools(): Promise<McpToolInfo[]> {
      const result = await this.sendRequest("tools/list");
      if (!result?.tools || !Array.isArray(result.tools)) return [];
      return result.tools.map((t: any) => ({
        name: t.name,
        description: t.description || "",
        inputSchema: t.inputSchema,
        serverName: this.serverName,
      }));
    }
  
    /** Call a tool and return the text result. */
    async callTool(name: string, args: any): Promise<string> {
      const result = await this.sendRequest("tools/call", { name, arguments: args });
      // MCP returns { content: [{ type: "text", text: "..." }, ...] }
      if (result?.content && Array.isArray(result.content)) {
        return result.content
          .filter((c: any) => c.type === "text")
          .map((c: any) => c.text)
          .join("\n");
      }
      return JSON.stringify(result);
    }
  
    /** Kill the server process. */
    close(): void {
      this.rl?.close();
      this.process?.kill();
      this.process = null;
    }
  }
  
  // ─── MCP Manager (manages all connections) ──────────────────
  
  export class McpManager {
    private connections = new Map<string, McpConnection>();
    private tools: McpToolInfo[] = [];
    private connected = false;
  
    /**
     * Read settings files, connect to all configured MCP servers,
     * and discover their tools. Safe to call multiple times (no-op after first).
     */
    async loadAndConnect(): Promise<void> {
      if (this.connected) return;
      this.connected = true;
  
      const configs = this.loadConfigs();
      if (Object.keys(configs).length === 0) return;
  
      const TIMEOUT_MS = 15_000;
  
      for (const [name, config] of Object.entries(configs)) {
        const conn = new McpConnection(name, config);
        try {
          await conn.connect();
          await Promise.race([
            conn.initialize(),
            new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), TIMEOUT_MS)),
          ]);
          const serverTools = await Promise.race([
            conn.listTools(),
            new Promise<McpToolInfo[]>((_, rej) => setTimeout(() => rej(new Error("timeout")), TIMEOUT_MS)),
          ]);
          this.connections.set(name, conn);
          this.tools.push(...serverTools);
          console.error(`[mcp] Connected to '${name}' — ${serverTools.length} tools`);
        } catch (err: any) {
          console.error(`[mcp] Failed to connect to '${name}': ${err.message}`);
          conn.close();
        }
      }
    }
  
    /**
     * Return tool definitions in Anthropic API format, with mcp__server__tool prefix.
     */
    getToolDefinitions(): Array<{ name: string; description: string; input_schema: any }> {
      return this.tools.map((t) => ({
        name: `mcp__${t.serverName}__${t.name}`,
        description: t.description || `MCP tool ${t.name} from ${t.serverName}`,
        input_schema: t.inputSchema || { type: "object", properties: {} },
      }));
    }
  
    /** Check if a tool name is an MCP-prefixed tool. */
    isMcpTool(name: string): boolean {
      return name.startsWith("mcp__");
    }
  
    /** Route a prefixed tool call to the correct server. */
    async callTool(prefixedName: string, args: any): Promise<string> {
      // mcp__serverName__toolName → serverName, toolName
      const parts = prefixedName.split("__");
      if (parts.length < 3) throw new Error(`Invalid MCP tool name: ${prefixedName}`);
      const serverName = parts[1];
      const toolName = parts.slice(2).join("__"); // tool name might contain __
      const conn = this.connections.get(serverName);
      if (!conn) throw new Error(`MCP server '${serverName}' not connected`);
      return conn.callTool(toolName, args);
    }
  
    /** Disconnect all servers. */
    async disconnectAll(): Promise<void> {
      for (const [, conn] of this.connections) {
        conn.close();
      }
      this.connections.clear();
      this.tools = [];
      this.connected = false;
    }
  
    // ─── Private: config loading ──────────────────────────────
  
    private loadConfigs(): Record<string, McpServerConfig> {
      const merged: Record<string, McpServerConfig> = {};
  
      // 1. Global: ~/.claude/settings.json
      const globalPath = join(homedir(), ".claude", "settings.json");
      this.mergeConfigFile(globalPath, merged);
  
      // 2. Project: .claude/settings.json (cwd)
      const projectPath = join(process.cwd(), ".claude", "settings.json");
      this.mergeConfigFile(projectPath, merged);
  
      // 3. Also check .mcp.json (Claude Code convention)
      const mcpJsonPath = join(process.cwd(), ".mcp.json");
      this.mergeConfigFile(mcpJsonPath, merged);
  
      return merged;
    }
  
    private mergeConfigFile(filePath: string, target: Record<string, McpServerConfig>): void {
      if (!existsSync(filePath)) return;
      try {
        const raw = JSON.parse(readFileSync(filePath, "utf-8"));
        const servers = raw.mcpServers || raw;
        for (const [name, config] of Object.entries(servers)) {
          if (this.isValidConfig(config)) {
            target[name] = config as McpServerConfig;
          }
        }
      } catch {
        // Silently skip malformed config files
      }
    }
  
    private isValidConfig(config: any): boolean {
      return config && typeof config === "object" && typeof config.command === "string";
    }
  }
  