编码母题 / P1 / 检索与 Agent 协议 / 02

MCP工具适配器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度岗位专项难度中等预计完成30–40 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

把 MCP Client 的工具描述和调用适配成内部 Registry 能理解的接口;初始化、调用、错误映射和关闭都有明确状态边界。

输入输出示例

关闭后再调用工具应失败;MCP isError 结果应映射成内部错误。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:MCP工具适配器
 * 要求:把 MCP Client 的工具描述和调用适配成内部 Registry 能理解的接口;
 * 初始化、调用、错误映射和关闭都有明确状态边界。
 * 示例:关闭后再调用工具应失败;MCP isError 结果应映射成内部错误。
 */
class MCPToolAdapter {
  constructor(client) {}
  async initialize() {}
  list() {}
  get(name) {}
  async call(name, arguments_, signal) {}
  async close() {}
}

(async () => {
  const calls = [];
  let closed = 0;
  const client = {
    listTools: async () => ({
      tools: [
        {
          name: "search",
          description: "搜索",
          inputSchema: { type: "object" },
        },
      ],
    }),
    callTool: async (request) => {
      calls.push(request);
      return { content: [{ type: "text", text: "ok" }] };
    },
    close: async () => {
      closed += 1;
    },
  };
  const adapter = await new MCPToolAdapter(client).initialize();
  console.log(adapter.list()[0].name); // 预期:"search"
  const result = await adapter.get("search").execute({ q: "x" });
  console.log(result.content[0].text); // 预期:"ok"
  console.log(calls[0]); // 预期:{ name: "search", arguments: { q: "x" } }
})().catch(console.error);

解题思路

展开思路、不变量与常见边界

关键切入点:协议适配器隔离外部 Transport 与内部 Runtime,状态机限制未初始化或已关闭调用。

核心模型:协议适配层隔离外部 Transport 与内部 Runtime,不能让业务代码依赖 MCP 响应细节。不变量:初始化完成前不可调用;工具名冲突立即失败;关闭后不再 发请求;外部错误转换但保留可观测 cause。生产环境还需重连、能力变化通知、 Schema 验证、权限白名单和内容大小限制。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:协议适配器隔离外部 Transport 与内部 Runtime,状态机限制未初始化或已关闭调用。
class MCPToolAdapter {
  constructor(client) {
    this.client = client;
    this.tools = new Map();
    this.state = "new";
  }
  async initialize() {
    if (this.state !== "new") throw new Error("adapter already initialized");
    const response = await this.client.listTools();
    for (const tool of response.tools || []) {
      if (!tool?.name) throw new Error("invalid MCP tool");
      if (this.tools.has(tool.name))
        throw new Error(`duplicate MCP tool: ${tool.name}`);
      this.tools.set(tool.name, {
        name: tool.name,
        description: tool.description,
        schema: tool.inputSchema,
        execute: (arguments_, context = {}) =>
          this.call(tool.name, arguments_, context.signal),
      });
    }
    this.state = "ready";
    return this;
  }
  list() {
    if (this.state !== "ready") throw new Error("adapter not ready");
    return [...this.tools.values()].map(
      ({ execute, ...description }) => description,
    );
  }
  get(name) {
    if (this.state !== "ready") throw new Error("adapter not ready");
    return this.tools.get(name);
  }
  async call(name, arguments_, signal) {
    if (this.state !== "ready") throw new Error("adapter not ready");
    if (signal?.aborted) throw signal.reason || new Error("aborted");
    const response = await this.client.callTool(
      { name, arguments: arguments_ },
      { signal },
    );
    if (response.isError) {
      const error = new Error("MCP tool execution failed");
      error.code = "MCP_TOOL_ERROR";
      error.content = response.content;
      throw error;
    }
    return {
      content: response.content || [],
      structuredContent: response.structuredContent,
    };
  }
  async close() {
    if (this.state === "closed") return;
    this.state = "closed";
    this.tools.clear();
    await this.client.close?.();
  }
}

// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
  const calls = [];
  let closed = 0;
  const client = {
    listTools: async () => ({
      tools: [
        {
          name: "search",
          description: "搜索",
          inputSchema: { type: "object" },
        },
      ],
    }),
    callTool: async (request) => {
      calls.push(request);
      return { content: [{ type: "text", text: "ok" }] };
    },
    close: async () => {
      closed += 1;
    },
  };
  const adapter = await new MCPToolAdapter(client).initialize();
  console.log(adapter.list()[0].name); // 预期:"search"
  const result = await adapter.get("search").execute({ q: "x" });
  console.log(result.content[0].text); // 预期:"ok"
  console.log(calls[0]); // 预期:{ name: "search", arguments: { q: "x" } }
  await adapter.close();
  await adapter.close();
  console.log(closed); // 预期:1
  console.log((() => { try { (() => adapter.list())(); return false; } catch { return true; } })()); // 预期:true(抛出异常)


})().catch(console.error);

复杂度分析

时间复杂度:初始化 O(T),调用 O(1) 适配开销。初始化注册 T 个工具;单次调用只检查状态并转发。

空间复杂度:O(T)。Map 保留 T 个工具适配描述;外部传输缓冲不计。

下载原始 JS 文件