第 33 课: MCP 协议适配

3 阅读5分钟

课程目标

精读 @langchain/mcp-adapters 包的核心实现:MCP 工具到 LangChain StructuredTool 的转换、MCP 客户端管理、多传输层支持(SSE / stdio / HTTP)、以及生命周期钩子。


33.1 MCP 协议概述

MCP(Model Context Protocol)是一个标准化的 AI 工具通信协议,定义了:

  • Tool — 可调用的函数,有 JSON Schema 描述的输入参数
  • Resource — 可读取的数据源(文件、数据库等)
  • Prompt — 可复用的提示词模板

LangChain.js 通过 @langchain/mcp-adapters 将 MCP Server 暴露的工具转换为 LangChain 的 DynamicStructuredTool,从而无缝接入 Agent 和 Chain 体系。


33.2 包结构总览

源码位置: libs/langchain-mcp-adapters/src/

langchain-mcp-adapters/src/
├── tools.ts        — MCP Tool -> LangChain DynamicStructuredTool 转换
├── client.ts       — MultiServerMCPClient 多服务器客户端管理
├── connection.ts   — ConnectionManager 连接池与传输层
├── hooks.ts        — beforeToolCall / afterToolCall 生命周期钩子
├── types.ts        — 类型定义与配置 schema
├── logging.ts      — 调试日志
└── index.ts        — 统一导出

33.3 tools.ts — MCP 工具转换核心

源码位置: libs/langchain-mcp-adapters/src/tools.ts:1192

loadMcpTools() 是核心入口函数:

export async function loadMcpTools(
  serverName: string,
  client: MCPInstance,
  options?: LoadMcpToolsOptions
): Promise<DynamicStructuredTool[]> {
  // 1. 分页获取所有 MCP 工具
  const mcpTools: MCPTool[] = [];
  let toolsResponse: ListToolsResult | undefined;
  do {
    toolsResponse = await client.listTools({
      ...(toolsResponse?.nextCursor ? { cursor: toolsResponse.nextCursor } : {}),
    });
    mcpTools.push(...(toolsResponse.tools || []));
  } while (toolsResponse.nextCursor);

  // 2. 逐个转换为 LangChain DynamicStructuredTool
  return mcpTools.filter((tool) => !!tool.name).map((tool) => {
    // 2a. 解引用 JSON Schema 中的 $ref/$defs
    const dereferencedSchema = dereferenceJsonSchema(tool.inputSchema);
    // 2b. 简化 JSON Schema(去除 LLM 不支持的 allOf/anyOf/oneOf 等)
    const simplifiedSchema = simplifyJsonSchemaForLLM(dereferencedSchema);

    return new DynamicStructuredTool({
      name: `${toolNamePrefix}${tool.name}`,
      description: tool.description || "",
      schema: simplifiedSchema,
      responseFormat: "content_and_artifact",
      func: async (args, _runManager, config) => {
        return _callTool({ serverName, toolName: tool.name, client, args, config, /*...*/ });
      },
    });
  });
}

转换过程的三个关键步骤

1) JSON Schema 解引用

MCP 工具的 inputSchema 可能使用 $ref + $defs 定义嵌套类型,但大多数 LLM 不理解这种格式:

function dereferenceJsonSchema(schema: JsonSchemaObject): JsonSchemaObject {
  const definitions = schema.$defs ?? schema.definitions ?? {};

  function resolveRefs(obj: JsonSchemaObject, visitedRefs: Set<string> = new Set()): JsonSchemaObject {
    if (obj.$ref) {
      // 将 $ref 替换为实际定义,检测循环引用
      const match = refPath.match(/^#\/\$defs\/(.+)$/);
      if (match && definitions[match[1]]) {
        if (visitedRefs.has(refPath)) return { type: "object" }; // 循环引用保护
        return resolveRefs(definitions[match[1]], newVisitedRefs);
      }
    }
    // 递归处理所有属性
    // ...
  }
  return resolveRefs(schema);
}

2) Schema 简化

function simplifyJsonSchemaForLLM(schema: JsonSchemaObject): JsonSchemaObject {
  // 移除 LLM 不支持的模式:
  // - allOf -> 合并为单个 schema
  // - anyOf/oneOf -> 扁平化为第一个对象变体或合并
  // - if/then/else -> 提取属性
  // - not -> 移除
  // - $schema -> 移除
  // - unevaluatedProperties -> 移除
}

3) 工具调用

async function _callTool({ serverName, toolName, client, args, config, ... }): Promise<ContentBlocksWithArtifacts> {
  // 提取超时配置
  const numericTimeout = config?.metadata?.timeoutMs ?? config?.timeout;

  // 执行 beforeToolCall 钩子
  const modification = await beforeToolCall?.({ name: toolName, args, serverName }, state, config);
  const finalArgs = Object.assign(args, modification?.args || {});

  // 如果钩子返回了 headers,fork 一个新 client
  const finalClient = hasHeaderChanges ? await client.fork(headers) : client;

  // 调用 MCP 工具
  const result = await finalClient.callTool({ name: toolName, arguments: finalArgs });

  // 转换结果为 LangChain ContentBlock
  const [content, artifacts] = await _convertCallToolResult({ serverName, toolName, result, client, ... });

  // 执行 afterToolCall 钩子
  const intercepted = await afterToolCall?.({ name: toolName, args: finalArgs, result: [content, artifacts], serverName }, state, config);
  return intercepted?.result ?? [content, artifacts];
}

33.4 client.ts — 多服务器客户端

源码位置: libs/langchain-mcp-adapters/src/client.ts:124

MultiServerMCPClient 管理到多个 MCP Server 的连接:

export class MultiServerMCPClient {
  #serverNameToTools: Record<string, DynamicStructuredTool[]> = {};
  #clientConnections: ConnectionManager;

  constructor(config: ClientConfig | Record<string, Connection>) {
    // 解析并验证配置(使用 Zod schema)
    const parsedServerConfig = clientConfigSchema.parse(config);
    if (Object.keys(parsedServerConfig.mcpServers).length === 0) {
      throw new MCPClientError("No MCP servers provided");
    }
    this.#clientConnections = new ConnectionManager(parsedServerConfig);
  }

  // 获取所有工具(自动初始化连接)
  async getTools(...servers: string[]): Promise<DynamicStructuredTool[]> {
    await this.initializeConnections();
    return servers.length === 0
      ? this._getAllToolsAsFlatArray()
      : this._getToolsFromServers(servers);
  }

  // 关闭所有连接
  async close(): Promise<void> { /* ... */ }
}

使用示例

import { MultiServerMCPClient } from "@langchain/mcp-adapters";

const client = new MultiServerMCPClient({
  mcpServers: {
    filesystem: {
      transport: "stdio",
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
    },
    weather: {
      transport: "http",
      url: "http://localhost:3001/mcp",
    },
  },
});

// 获取所有服务器的工具
const tools = await client.getTools();

// 只获取特定服务器的工具
const fsTools = await client.getTools("filesystem");

// 清理
await client.close();

33.5 connection.ts — 传输层管理

源码位置: libs/langchain-mcp-adapters/src/connection.ts

支持三种传输层:

传输类型适用场景配置方式
stdio本地进程通信command + args
http (Streamable HTTP)远程 HTTP 服务url
sse旧版 SSE 协议url + transport: "sse"

ConnectionManager 管理连接池:

export class ConnectionManager {
  #connections: Map<ClientKeyObject, Connection> = new Map();

  async createClient(type: "stdio" | "http" | "sse", serverName: string, options: ...): Promise<Client>;
  get(key: string | TransportOptions): Client | undefined;
  has(key: string | TransportOptions): boolean;
  async delete(key?: TransportOptions): Promise<void>;
}

连接自动恢复

  • stdio 连接支持 restart 配置(进程退出时自动重启)
  • SSE/HTTP 连接支持 reconnect 配置(断开时自动重连)
// HTTP 连接的自动 SSE 降级
// 如果 Streamable HTTP 返回 4xx,自动尝试 SSE 协议
if (automaticSSEFallback && code >= 400 && code < 500) {
  await this._initializeSSEConnection(serverName, connection);
}

Client 接口扩展

export interface Client extends MCPClient {
  fork: (headers: Record<string, string>) => Promise<Client>;
}

fork() 方法允许在运行时创建带有不同 headers 的客户端副本,这在钩子需要动态注入认证头时非常有用。


33.6 hooks.ts — 生命周期钩子

源码位置: libs/langchain-mcp-adapters/src/hooks.ts

两个核心钩子:

export type ToolHooks = {
  // 工具调用前 — 可修改参数和 headers
  beforeToolCall?: (
    request: ToolCallRequest,    // { name, args, serverName }
    state: State,                // LangGraph 状态(如有)
    config: RunnableConfig
  ) => Promise<ToolCallModification | void>;

  // 工具调用后 — 可修改结果
  afterToolCall?: (
    result: ToolCallResult,      // { name, args, result, serverName }
    state: State,
    config: RunnableConfig
  ) => Promise<{ result: ToolResult } | void>;
};

// beforeToolCall 可返回的修改
type ToolCallModification = {
  headers?: Record<string, string>;  // 注入额外 HTTP headers
  args?: unknown;                     // 修改工具参数
};

使用场景

const client = new MultiServerMCPClient({
  mcpServers: { /* ... */ },
  beforeToolCall: async (request, state, config) => {
    // 注入认证 token
    return {
      headers: { "Authorization": `Bearer ${getToken()}` },
      args: { ...request.args, userId: getCurrentUser() },
    };
  },
  afterToolCall: async (result, state, config) => {
    // 日志记录
    console.log(`Tool ${result.name} returned:`, result.result);
    // 可修改结果或返回 void 保持原样
  },
});

33.7 与 Agent 集成

MCP 工具接入 LangChain Agent 的完整流程:

import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { createReactAgent } from "langchain/agents";

const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    calculator: {
      transport: "stdio",
      command: "npx",
      args: ["-y", "mcp-server-calculator"],
    },
  },
});

const tools = await mcpClient.getTools();

// MCP 工具直接作为 Agent 的工具使用
const agent = createReactAgent({
  llm: model,
  tools,  // DynamicStructuredTool[] — 与原生 LangChain 工具完全兼容
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "计算 (15 + 27) * 3" }],
});

// 清理连接
await mcpClient.close();

33.8 资源管理

MultiServerMCPClient 还支持 MCP 的 Resource 能力:

// 列出服务器上的资源
const resources = await mcpClient.listResources("filesystem");
// { filesystem: [{ uri: "file:///tmp/data.txt", name: "data.txt", ... }] }

// 读取资源内容
const content = await mcpClient.readResource("filesystem", "file:///tmp/data.txt");
// [{ uri: "...", text: "文件内容...", mimeType: "text/plain" }]

// 列出资源模板(参数化 URI)
const templates = await mcpClient.listResourceTemplates("filesystem");

33.9 源码精读路线

优先级文件关注点
P0mcp-adapters/src/tools.tsloadMcpTools()、Schema 转换、_callTool()
P0mcp-adapters/src/client.tsMultiServerMCPClientgetTools()initializeConnections()
P1mcp-adapters/src/connection.tsConnectionManager、三种传输层初始化
P1mcp-adapters/src/hooks.tsToolHooks、beforeToolCall/afterToolCall
P2mcp-adapters/src/types.ts配置 Schema(Zod 验证)

33.10 实战练习

  1. 基础: 使用 MultiServerMCPClient 连接一个 stdio MCP Server,获取并列出所有工具
  2. 进阶: 将 MCP 工具集成到 ReAct Agent 中,让 Agent 调用 MCP 工具完成任务
  3. 高阶: 实现 beforeToolCall 钩子,为所有工具调用注入审计日志和认证信息

本课收获总结

级别你应该掌握的
🟢 基础理解 MCP 协议的基本概念:Tool、Resource、Prompt
🔵 中阶能用 MultiServerMCPClient 连接 MCP Server 并获取工具
🟡 高阶理解 SSE / stdio / HTTP 三种传输层的适用场景和自动降级
🟠 资深掌握 MCP Schema -> DynamicStructuredTool 的转换细节(解引用、简化)
🔴 架构评估 MCP 在 Agent 生态中的定位:标准化工具协议 vs 私有集成

下一课预告

第 34 课深入序列化、缓存与存储系统 — Serializable 基类、动态加载、LLM 缓存和 LangChain Hub。