课程目标
精读 @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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | mcp-adapters/src/tools.ts | loadMcpTools()、Schema 转换、_callTool() |
| P0 | mcp-adapters/src/client.ts | MultiServerMCPClient、getTools()、initializeConnections() |
| P1 | mcp-adapters/src/connection.ts | ConnectionManager、三种传输层初始化 |
| P1 | mcp-adapters/src/hooks.ts | ToolHooks、beforeToolCall/afterToolCall |
| P2 | mcp-adapters/src/types.ts | 配置 Schema(Zod 验证) |
33.10 实战练习
- 基础: 使用
MultiServerMCPClient连接一个 stdio MCP Server,获取并列出所有工具 - 进阶: 将 MCP 工具集成到 ReAct Agent 中,让 Agent 调用 MCP 工具完成任务
- 高阶: 实现
beforeToolCall钩子,为所有工具调用注入审计日志和认证信息
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解 MCP 协议的基本概念:Tool、Resource、Prompt |
| 🔵 中阶 | 能用 MultiServerMCPClient 连接 MCP Server 并获取工具 |
| 🟡 高阶 | 理解 SSE / stdio / HTTP 三种传输层的适用场景和自动降级 |
| 🟠 资深 | 掌握 MCP Schema -> DynamicStructuredTool 的转换细节(解引用、简化) |
| 🔴 架构 | 评估 MCP 在 Agent 生态中的定位:标准化工具协议 vs 私有集成 |
下一课预告
第 34 课深入序列化、缓存与存储系统 — Serializable 基类、动态加载、LLM 缓存和 LangChain Hub。