MCP协议实战指南:从零构建AI Agent可插拔工具与资源服务器
1. 项目概述:为什么我们需要深入理解 MCP?
如果你最近在折腾 AI 应用开发,特别是想把不同的工具、数据源和模型能力“粘合”起来,构建一个更智能的 Agent(智能体),那你大概率已经听过 MCP 这个词了。它不是什么新出的服务器型号,也不是某个加密协议,而是Model Context Protocol的缩写,一个由 Anthropic 公司牵头推出的开放协议。简单来说,MCP 想解决一个很实际的问题:如何让 AI 应用(比如 Claude Desktop、Cursor 里的 AI 助手)安全、标准化地访问外部工具和数据,而无需开发者每次都去写一堆定制化的、脆硬的集成代码。
我最初接触 MCP 是因为想给团队内部的 AI 助手接入公司内部的 Jira 看板和 Confluence 文档库。传统做法要么是调用不稳定的 API 包装层,要么就是给 AI 一个权限过高的账号,安全和稳定性都让人头疼。MCP 的出现,相当于定义了一套“插座”和“插头”的标准。任何符合 MCP 标准的工具(称为MCP Server)都可以被任何支持 MCP 的 AI 应用(称为MCP Client,比如 Claude Desktop)即插即用。这极大地降低了集成成本,也让 AI 的能力边界得以灵活扩展。
所以,这个“完整指南”的目标很明确:我们不只停留在“知道 MCP 是什么”,而是要彻底吃透它。从协议的核心思想拆解开始,到自己动手搭建一个能提供特定服务(比如查询天气、管理待办事项)的 MCP Server,最后再探讨如何将这种能力集成到一个自主运行的 AI Agent 系统中。整个过程,我会结合我踩过的坑和实战心得,让你不仅能复现,更能理解背后的设计哲学和最佳实践。
2. MCP 协议深度解析:不只是 API,更是会话模型
很多人会把 MCP 简单地理解成另一种 RPC(远程过程调用)协议,比如 gRPC 或 JSON-RPC 的变种。这其实低估了它的价值。MCP 的核心创新在于它采用了一种资源(Resources)与工具(Tools)为中心的声明式模型,并且设计为围绕SSE(Server-Sent Events)的双向通信,这更贴合 AI 与外部世界交互的异步、流式特性。
2.1 核心概念:资源、工具与提示词模板
要理解 MCP,必须搞清楚三个核心概念,它们构成了 Server 向 Client 暴露能力的全部内容。
资源(Resources):你可以把它想象成“只读的数据源”。一个资源有一个唯一的
uri(如file:///path/to/doc.md或jira://issue/PROJ-123),一个mimeType描述其内容格式,以及一个name和description供 AI 理解。Client 可以通过read_resource请求来获取资源的内容。例如,一个“今日头条新闻”资源,其内容就是最新的新闻列表文本。资源的关键在于它是静态或快照式的,AI 读取它,但不直接修改它。工具(Tools):工具代表了“可执行的动作”。每个工具有一个
name、description和定义输入参数的inputSchema(基于 JSON Schema)。Client 通过call_tool请求来调用它。例如,“创建 Jira 工单”就是一个工具,它需要输入project,summary,description等参数。工具是 AI 与外界交互,产生“副作用”(如创建记录、发送消息)的主要方式。提示词模板(Prompts):这是一个非常实用的设计。它允许 Server 预定义一些复杂的、多步骤的提示词框架。Client 可以获取这些模板(
get_prompt),并传入参数来实例化成一个完整的提示词,直接用于与 AI 模型的对话。比如,一个“代码审查”提示词模板,可以接受code和language参数,生成一个结构化的审查请求。这相当于把最佳实践“固化”下来,在 Client 侧复用。
为什么这样设计?传统 API 集成需要 AI 模型去理解复杂的 API 文档和参数结构。而 MCP 通过description和结构化的inputSchema,将这些信息以一种对 AI 更友好的方式暴露出来。AI 只需要根据自然语言描述选择正确的工具或资源,并生成符合 Schema 的参数即可,大大降低了幻觉和调用错误。
2.2 通信模型:基于 SSE 的会话流
MCP 没有采用传统的 HTTP 请求-响应模式,而是基于Server-Sent Events (SSE)。这是一个关键区别。在 SSE 模型中,Client 与 Server 建立一条长期连接,通信以 Server 向 Client 推送事件流(text/event-stream)的形式进行。
连接建立后,流程通常是这样的:
- 初始化(Initialization):Client 发送
initialize请求,Server 回复其支持的协议版本、能力(如支持哪些资源、工具)以及一个唯一的serverId。 - 列表(Listing):Client 随后会发送
list_系列的请求,如list_resources、list_tools、list_prompts,来获取 Server 提供的所有能力的元数据。 - 会话(Session):此后,Client 可以在整个会话中,随时发送
read_resource、call_tool、get_prompt等请求。Server 处理请求并返回结果。 - 通知(Notifications):这是 SSE 的优势所在。Server 可以主动向 Client 推送
notifications,例如,当一个被监听的资源(如日志文件)内容发生变化时,Server 可以主动推送resources/updated事件,Client 从而可以及时获取最新内容,实现近乎实时的数据同步。
实战心得:选择 SSE 而非 WebSocket刚开始我疑惑为什么不用更全双工的 WebSocket。实践后发现,对于 MCP 这种以 Server 向 Client 推送状态变化为主、Client 发起操作请求为辅的场景,SSE 更简单轻量。它基于 HTTP,兼容性更好,内置了重连机制,并且大多数编程语言都有成熟库。我们只需要关心事件格式(MCP 定义的标准 JSON-RPC 消息),而不必处理底层的帧协议。
2.3 协议格式与安全考量
MCP 的消息格式遵循 JSON-RPC 2.0 规范,每个消息包含jsonrpc,id,method,params等标准字段。这保证了协议的广泛兼容性和可调试性。
安全是 MCP 设计中的重中之重,主要体现在:
- 无默认网络传输:MCP 规范本身不规定传输层。Server 和 Client 通常通过stdio(标准输入输出)或SSH进行通信。这意味着 Server 默认只运行在本地或受信任的远程主机上,极大地缩小了攻击面。你几乎不会看到一个 MCP Server 默认监听一个 TCP 端口。
- 显式权限模型:Client(如 Claude Desktop)在首次连接一个 Server 时,会向用户清晰展示这个 Server 提供了哪些资源、工具和提示词,并请求用户授权。用户可以看到“这个 Server 想访问你的文件系统”或“拥有发送邮件的权限”,从而做出明确选择。这比直接给 AI 一个万能密钥要安全得多。
- 上下文隔离:每个工具调用、资源读取都在独立的请求中完成,Server 可以实现严格的参数校验和操作审计。
注意:正因为 MCP Server 通常通过 stdio 运行,你在开发时可能会遇到进程管理的问题。例如,如果 Server 崩溃,需要 Client 有能力重启它。在集成到 Agent 系统时,需要设计稳健的子进程管理机制。
3. 动手构建你的第一个 MCP Server
理解了协议,最好的巩固方式就是动手写一个。我们以构建一个“待办事项(Todo)管理” MCP Server 为例。它将提供一个资源(列出所有待办项)和两个工具(添加待办项、标记完成)。我们将使用官方推荐的TypeScript SDK来开发,这是目前最成熟、文档最全的方案。
3.1 环境准备与项目初始化
首先,确保你的环境有 Node.js(建议 18+ 版本)和 npm。
# 创建一个新目录并初始化项目 mkdir mcp-server-todo cd mcp-server-todo npm init -y # 安装 MCP TypeScript SDK 和必要的类型定义 npm install @modelcontextprotocol/sdk npm install --save-dev typescript @types/node tsx # 初始化 TypeScript 配置 npx tsc --init修改生成的tsconfig.json,确保设置合适的编译选项,例如"module": "ESNext","target": "ES2022", 和"outDir": "./dist"。
3.2 核心 Server 类实现
接下来,创建src/server.ts文件,开始编写 Server 逻辑。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // 一个简单的内存存储,实际项目中可替换为数据库 interface TodoItem { id: number; title: string; completed: boolean; } class TodoMcpServer { private server: Server; private todos: TodoItem[]; private nextId: number; constructor() { this.server = new Server( { name: 'todo-mcp-server', version: '1.0.0', }, { capabilities: { resources: {}, // 声明我们支持资源 tools: {}, // 声明我们支持工具 }, } ); this.todos = []; this.nextId = 1; this.setupRequestHandlers(); this.setupErrorHandlers(); } private setupRequestHandlers() { // 1. 处理列出所有资源的请求 this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: 'todo:///items', mimeType: 'application/json', name: '所有待办事项', description: '获取当前所有的待办事项列表,包括已完成和未完成的。', }, ], }; }); // 2. 处理读取特定资源的请求 this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (request.params.uri === 'todo:///items') { return { contents: [ { uri: request.params.uri, mimeType: 'application/json', // 将待办列表以 JSON 字符串形式返回 text: JSON.stringify(this.todos, null, 2), }, ], }; } throw new Error(`Resource not found: ${request.params.uri}`); }); // 3. 处理列出所有工具的请求 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'add_todo', description: '添加一个新的待办事项。', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '待办事项的标题', }, }, required: ['title'], }, }, { name: 'complete_todo', description: '根据 ID 标记一个待办事项为已完成。', inputSchema: { type: 'object', properties: { id: { type: 'number', description: '要标记为完成的待办事项的 ID', }, }, required: ['id'], }, }, ], }; }); // 4. 处理调用工具的请求 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'add_todo': { const title = args?.title; if (typeof title !== 'string' || !title.trim()) { throw new Error('Title is required and must be a non-empty string.'); } const newTodo: TodoItem = { id: this.nextId++, title: title.trim(), completed: false, }; this.todos.push(newTodo); return { content: [ { type: 'text', text: `待办事项添加成功!ID: ${newTodo.id}, 标题: "${newTodo.title}"`, }, ], }; } case 'complete_todo': { const id = args?.id; if (typeof id !== 'number') { throw new Error('ID is required and must be a number.'); } const todo = this.todos.find((t) => t.id === id); if (!todo) { throw new Error(`未找到 ID 为 ${id} 的待办事项。`); } if (todo.completed) { return { content: [ { type: 'text', text: `待办事项 ID: ${id} 已经是完成状态。`, }, ], }; } todo.completed = true; return { content: [ { type: 'text', text: `成功将待办事项 ID: ${id} 标记为完成。`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } }); } private setupErrorHandlers() { this.server.onerror = (error) => { console.error('[Server Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Todo MCP Server running on stdio...'); } } // 启动服务器 const server = new TodoMcpServer(); server.run().catch(console.error);代码解析与注意事项:
- 能力声明:在
Server构造函数的capabilities中,我们明确声明了此 Server 支持resources和tools。这符合 MCP 的声明式哲学,Client 在初始化时就能知道你能做什么。 - URI 设计:我们为资源定义了一个 URI
todo:///items。todo是自定义的 scheme,///items是路径。这是一种常见的模式,用于标识资源的类型和位置。 - 输入验证:在
call_tool处理中,我们对参数进行了严格的类型和有效性检查。这是必须的,因为 AI 生成的参数可能不准确,健全的校验能防止 Server 崩溃或产生不可预期的行为。 - 错误处理:我们使用
throw new Error()来返回错误。MCP SDK 会将其捕获并格式化为标准的 JSON-RPC 错误响应返回给 Client。同时,我们也监听了进程信号,以便优雅关闭。
3.3 编译、运行与在 Claude Desktop 中测试
首先,在package.json中添加启动脚本:
{ "scripts": { "build": "tsc", "start": "node dist/server.js", "dev": "tsx watch src/server.ts" } }使用npm run dev可以在开发模式下运行(依赖tsx)。但为了在 Claude Desktop 中测试,我们需要一个稳定的可执行文件。
- 编译:运行
npm run build,生成dist/server.js。 - 配置 Claude Desktop:
- 找到 Claude Desktop 的配置文件夹。在 macOS 上通常是
~/Library/Application Support/Claude/claude_desktop_config.json,在 Windows 上是%APPDATA%\Claude\claude_desktop_config.json。 - 编辑这个 JSON 文件,添加我们的 MCP Server 配置:
- 找到 Claude Desktop 的配置文件夹。在 macOS 上通常是
{ "mcpServers": { "todo": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/YOUR/mcp-server-todo/dist/server.js"] } } }重要提示:必须使用绝对路径。
node命令也需要在系统 PATH 中,或者你也可以直接指向 node 的绝对路径(如"/usr/local/bin/node")。
- 重启 Claude Desktop:保存配置并完全重启 Claude Desktop 应用。
- 测试:重启后,当你新建一个对话时,Claude 应该会提示“已连接至待办事项服务器”。你可以尝试对它说:“帮我看看现在的待办事项有哪些?”(触发
read_resource),或者说:“添加一个待办事项:写 MCP 博客”(触发call_tool“add_todo”)。你可以在对话中看到 Claude 调用工具的过程和结果。
踩坑记录:权限与路径第一次配置时,最常见的问题是路径错误或权限不足。确保:
command中的node在 Claude Desktop 的运行环境中可访问。有时 GUI 应用的环境变量与终端不同,使用绝对路径最保险。- 你的脚本文件有可执行权限(在 Unix 系统上)。
- 如果看到连接失败,可以查看 Claude Desktop 的日志文件(位置在配置文件夹内)来获取详细的错误信息。
4. 进阶:构建一个实用的“天气查询” MCP Server
内存待办事项只是个玩具。我们再来构建一个更有实用价值的 Server:天气查询。这个例子将展示如何集成第三方 API,并处理更复杂的参数和错误。
4.1 设计资源、工具与选择 API
- 资源:我们可以提供一个
weather://current/{city}资源,返回指定城市的当前天气快照(JSON 格式)。 - 工具:提供一个
get_weather工具,参数为city(城市名)和可选的units(单位制,如metric或imperial)。 - API 选择:我们将使用 OpenWeatherMap 的免费 API。你需要去其官网注册一个免费账户,获取 API Key。
4.2 集成第三方 API 与错误处理
创建src/weather-server.ts。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; // 需要安装: npm install node-fetch interface WeatherData { city: string; temperature: number; feels_like: number; humidity: number; description: string; icon: string; timestamp: number; } class WeatherMcpServer { private server: Server; private apiKey: string; constructor(apiKey: string) { if (!apiKey) { throw new Error('OpenWeatherMap API key is required.'); } this.apiKey = apiKey; this.server = new Server( { name: 'weather-mcp-server', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, }, } ); this.setupHandlers(); } private setupHandlers() { // 列出资源 this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'weather://current/*', // 使用通配符表示需要具体城市 mimeType: 'application/json', name: '当前天气', description: '获取指定城市的当前天气信息。URI 格式: weather://current/{城市名},例如 weather://current/Beijing', }, ], })); // 读取资源 - 实现从 URI 解析城市并调用 API this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const match = uri.match(/^weather:\/\/current\/(.+)$/); if (!match) { throw new Error(`Invalid weather resource URI. Expected format: weather://current/{city}`); } const city = decodeURIComponent(match[1]); const weather = await this.fetchWeather(city); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(weather, null, 2), }], }; }); // 列出工具 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_weather', description: '查询指定城市的当前天气。', inputSchema: { type: 'object', properties: { city: { type: 'string', description: '城市名称,例如 "London" 或 "北京"。支持中文和英文。', }, units: { type: 'string', description: '单位制。可选值: "metric"(摄氏度), "imperial"(华氏度)。默认为 "metric"。', enum: ['metric', 'imperial'], default: 'metric', }, }, required: ['city'], }, }, ], })); // 调用工具 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'get_weather') { throw new Error(`Unknown tool: ${request.params.name}`); } const args = request.params.arguments as { city: string; units?: string }; if (!args || !args.city) { throw new Error('City parameter is required.'); } const city = args.city; const units = args.units || 'metric'; try { const weather = await this.fetchWeather(city, units); const unitSymbol = units === 'metric' ? '°C' : '°F'; return { content: [{ type: 'text', text: `**${weather.city}** 当前天气: - 温度: ${weather.temperature} ${unitSymbol} (体感 ${weather.feels_like} ${unitSymbol}) - 湿度: ${weather.humidity}% - 状况: ${weather.description} - 更新时间: ${new Date(weather.timestamp).toLocaleString()} `, }], }; } catch (error: any) { // 将 API 错误转化为用户友好的信息 let errorMessage = '获取天气信息失败。'; if (error.message.includes('404')) { errorMessage = `未找到城市 "${city}",请检查名称是否正确。`; } else if (error.message.includes('401')) { errorMessage = 'API 密钥无效,请检查服务器配置。'; } else if (error.message.includes('429')) { errorMessage = 'API 调用频率超限,请稍后再试。'; } throw new Error(errorMessage); } }); } private async fetchWeather(city: string, units: string = 'metric'): Promise<WeatherData> { const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${this.apiKey}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data: any = await response.json(); return { city: data.name, temperature: data.main.temp, feels_like: data.main.feels_like, humidity: data.main.humidity, description: data.weather[0].description, icon: data.weather[0].icon, timestamp: data.dt * 1000, }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Weather MCP Server running...'); } } // 从环境变量获取 API Key const apiKey = process.env.OPENWEATHER_API_KEY; if (!apiKey) { console.error('错误:请设置 OPENWEATHER_API_KEY 环境变量。'); process.exit(1); } const server = new WeatherMcpServer(apiKey); server.run().catch(console.error);关键点与避坑指南:
- 环境变量管理:API Key 等敏感信息绝不能硬编码在代码中。我们通过
process.env从环境变量读取。在 Claude Desktop 配置中,你需要确保这个环境变量被设置。一种更安全的方式是使用args传递一个配置文件的路径。 - URI 模式匹配:在
read_resource处理器中,我们使用正则表达式从 URI 中提取城市参数。这使得资源 URI 是动态的、可寻址的。 - 友好的错误处理:在
call_tool中,我们捕获了fetchWeather可能抛出的错误(如网络错误、API 错误),并将其转换为对最终用户(或 AI)更友好的自然语言描述。这是提升用户体验的关键。 - 资源与工具的互补:注意,我们既提供了
weather://current/{city}资源,也提供了get_weather工具。它们的区别在于:- 资源:更适合被 AI 以“读取数据”的方式消费,返回的是原始结构化数据(JSON),AI 可以进一步解析和推理。
- 工具:更适合完成一个具体的“任务”,返回的是格式化好的、面向人类阅读的自然语言文本摘要。 在实际设计中,你可以根据使用场景决定暴露为资源、工具,或两者都提供。
5. 将 MCP Server 能力集成到自主 AI Agent 中
到目前为止,我们的 MCP Server 都是被 Claude Desktop 这样的“通用 Client”调用。但在更复杂的自动化场景中,我们可能需要构建一个自主运行的AI Agent,它能主动规划、决策并调用 MCP Server 提供的工具。这里,我们以使用LangChain JS/TS框架为例,展示如何集成。
5.1 架构设计:Agent 作为 MCP Client
在这个架构中,你的 AI Agent 系统将扮演 MCP Client 的角色。它需要:
- 动态发现与管理 MCP Server:启动或连接到一个或多个 MCP Server。
- 获取工具列表:通过 MCP 协议获取每个 Server 暴露的工具及其 Schema。
- 将工具“翻译”给 Agent 框架:将 MCP 工具描述转化为 LangChain 等框架能理解的
Tool对象。 - 供 Agent 调用:Agent 根据任务规划,选择并调用合适的工具。
5.2 使用 LangChain 集成 MCP 工具
首先,安装 LangChain 相关包:
npm install @langchain/core langchain @langchain/openai假设我们已经有一个运行在 stdio 上的 Todo MCP Server。我们需要创建一个 LangChain Tool 来封装对它的调用。
// src/langchain-mcp-adapter.ts import { DynamicStructuredTool } from "@langchain/core/tools"; import { z } from "zod"; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn } from 'child_process'; import path from 'path'; /** * 一个通用的 MCP 工具适配器类 * 负责启动 MCP Server 进程,连接 Client,并将 MCP 工具转换为 LangChain Tool。 */ export class MCPToolAdapter { private client: Client; private serverProcess; constructor(serverScriptPath: string) { // 1. 启动 MCP Server 子进程 this.serverProcess = spawn('node', [serverScriptPath], { stdio: ['pipe', 'pipe', 'inherit'], // 将 Server 的 stderr 继承到当前进程,便于调试 }); // 2. 创建 MCP Client 并连接到 Server 的 stdio this.client = new Client( { name: 'langchain-agent-mcp-client', version: '1.0.0', }, { capabilities: {}, // Client 的能力声明,这里为空 } ); const transport = new StdioClientTransport(this.serverProcess); this.client.connect(transport).catch(console.error); } /** * 获取所有 MCP 工具并转换为 LangChain Tool 数组 */ async getTools(): Promise<DynamicStructuredTool[]> { await this.client.initialize(); // 等待初始化完成 const { tools } = await this.client.listTools(); // 获取工具列表 const langchainTools: DynamicStructuredTool[] = []; for (const mcpTool of tools) { // 根据 MCP Tool 的 inputSchema 构建 Zod Schema const zodSchema = this.convertJsonSchemaToZod(mcpTool.inputSchema); const tool = new DynamicStructuredTool({ name: mcpTool.name, description: mcpTool.description, schema: zodSchema, func: async (args) => { // 调用 MCP 工具 const result = await this.client.callTool({ name: mcpTool.name, arguments: args, }); // 将结果转换为字符串返回给 Agent // 注意:call_tool 返回的 content 是数组,我们需要提取文本 const textContent = result.content?.find(c => c.type === 'text')?.text; return textContent || JSON.stringify(result.content); }, }); langchainTools.push(tool); } return langchainTools; } // 一个简单的 JSON Schema 到 Zod 的转换器(简化版,仅处理基本类型) private convertJsonSchemaToZod(schema: any): z.ZodType<any, any, any> { if (schema.type === 'object') { const shape: any = {}; for (const [key, prop] of Object.entries(schema.properties || {})) { shape[key] = this.convertJsonSchemaToZod(prop); } return z.object(shape); } else if (schema.type === 'string') { return z.string(); } else if (schema.type === 'number') { return z.number(); } else if (schema.type === 'boolean') { return z.boolean(); } else if (schema.type === 'array') { return z.array(this.convertJsonSchemaToZod(schema.items)); } // 默认返回 any return z.any(); } async cleanup() { await this.client.close(); this.serverProcess.kill(); } }代码解析:
- 进程管理:
MCPToolAdapter在构造函数中启动了 MCP Server 的子进程。这是关键一步,因为 MCP 通信依赖于 stdio。你需要确保 Server 脚本路径正确。 - 协议连接:使用
StdioClientTransport将 Client 连接到子进程的 stdin/stdout。 - 动态工具创建:
getTools方法在运行时查询 Server 有哪些工具,并根据其inputSchema动态创建 LangChain 的DynamicStructuredTool。这使得我们的 Agent 能够适配任何符合 MCP 协议的 Server,无需为每个 Server 硬编码工具。 - Schema 转换:
convertJsonSchemaToZod是一个简化版的转换函数,将 MCP 工具使用的 JSON Schema 转换为 LangChain 所需的 Zod Schema。在实际生产中,你可能需要一个更完善的转换库来处理所有 JSON Schema 特性。
5.3 构建一个简单的任务执行 Agent
现在,我们可以使用这些工具来构建一个 Agent。
// src/agent.ts import { ChatOpenAI } from "@langchain/openai"; import { AgentExecutor, createReactAgent } from "langchain/agents"; import { MCPToolAdapter } from "./langchain-mcp-adapter.js"; import * as dotenv from 'dotenv'; dotenv.config(); async function main() { // 1. 初始化 LLM const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", // 或 gpt-4-turbo temperature: 0, openAIApiKey: process.env.OPENAI_API_KEY, }); // 2. 创建 MCP 工具适配器并获取工具 const todoServerPath = path.resolve(process.cwd(), 'dist', 'todo-server.js'); const adapter = new MCPToolAdapter(todoServerPath); const tools = await adapter.getTools(); console.log(`Loaded ${tools.length} MCP tools:`, tools.map(t => t.name)); // 3. 创建 ReAct Agent const agent = createReactAgent({ llm, tools, }); // 4. 创建执行器 const agentExecutor = new AgentExecutor({ agent, tools, verbose: true, // 打印详细执行过程 }); // 5. 运行一个任务 try { const result = await agentExecutor.invoke({ input: "请先帮我添加一个待办事项:'购买 groceries'。然后,再添加一个:'阅读 MCP 文档'。最后,列出所有现有的待办事项给我看看。", }); console.log("\n--- Agent 执行结果 ---"); console.log(result.output); } catch (error) { console.error("Agent 执行出错:", error); } finally { // 6. 清理资源 await adapter.cleanup(); } } main();运行这个 Agent,你会看到 LangChain Agent 的思考过程(由于verbose: true):
- 思考:识别出需要调用
add_todo工具两次。 - 行动:调用
add_todo工具,并传入正确的参数。 - 观察:接收工具返回的成功信息。
- 再思考:识别出需要调用“列出待办事项”的功能。注意,我们的 Todo Server 只提供了
todo:///items资源,没有对应的“列出”工具。这时,一个更智能的 Agent 需要能够理解“列出所有待办事项”这个用户指令,对应于“读取todo:///items资源”。这需要更高级的 Agent 规划能力,或者我们可以在 Server 端额外暴露一个list_todos工具来简化操作。
集成中的挑战与心得:
- 工具与资源的映射:Agent 框架通常围绕“工具调用”设计。如果你的 MCP Server 主要提供资源,可能需要额外包装一层,创建一个“读取某资源”的工具,或者教会 Agent 理解资源 URI 的概念。更成熟的做法是使用支持 MCP 原生集成的 Agent 框架(正在涌现中)。
- 错误传播:MCP Server 的错误需要被妥善捕获并转换为 Agent 能理解的格式,以便其进行重试或调整策略。
- 性能与生命周期:频繁地启动/关闭 MCP Server 进程开销很大。在生产环境中,通常采用Server 常驻 + Client 连接池的模式。我们的适配器类需要改进为支持连接到一个已运行的 Server(例如通过 TCP Socket,如果 Server 支持的话)。
6. 生产环境部署与优化考量
当你开发完一个有用的 MCP Server,并成功集成到 Agent 后,就需要考虑如何将它部署到生产环境,供团队或更广泛的用户使用。
6.1 部署模式:从 Stdio 到 Socket
开发时我们使用 stdio,部署时则有更多选择:
- 进程托管(推荐用于桌面集成):对于 Claude Desktop 这类场景,由桌面应用管理 Server 进程的生命周期是最简单的。你只需要提供一个可执行文件或脚本。
- 常驻服务(Socket):对于 Agent 后端服务,你可能希望 MCP Server 作为一个常驻进程运行,监听一个 Unix Domain Socket 或 TCP 端口。这样,多个 Agent 实例可以连接同一个 Server,共享状态(如共享的待办列表)。MCP SDK 目前对 Socket 传输的支持还在完善中,但你可以基于其底层协议自己实现
ServerTransport和ClientTransport。 - HTTP 桥接:一个更通用的模式是,开发一个轻量的 HTTP 服务,它内部启动 MCP Server 并通过 stdio 与之通信,然后将 MCP 的资源/工具暴露为 RESTful API。这样,任何能发送 HTTP 请求的客户端都能使用,而不仅仅是支持 MCP 的 AI 应用。
6.2 安全性强化
- 权限细分:在 Server 实现中,应根据调用者的上下文(如果协议扩展支持)进行更细粒度的权限检查。例如,一个“文件管理” Server 可以限制工具只能访问特定目录。
- 输入消毒与限流:对所有来自 Client 的输入进行严格的消毒,防止注入攻击。对工具调用进行限流,防止滥用。
- 审计日志:记录所有的工具调用和资源访问请求,用于安全审计和问题排查。
6.3 性能与可观测性
- 连接池:如果采用 Socket 模式,为 MCP Client 实现连接池,避免频繁建立连接的开销。
- 超时与重试:为工具调用设置合理的超时,并实现重试机制以应对临时性故障。
- 指标暴露:在 Server 中集成监控,暴露如请求数、耗时、错误率等指标,方便接入 Prometheus 等监控系统。
构建和集成 MCP Server 的旅程,本质上是在为 AI 构建一套标准化、可扩展的“手”和“眼”。它剥离了集成中的复杂性,让开发者能更专注于工具本身的价值。从理解协议规范,到亲手实现 Server,再到将其融入自主 Agent 的决策循环,每一步都让我对如何设计 AI 可用的接口有了更深的理解。最大的体会是,良好的协议设计能极大降低生态的参与门槛,而围绕 MCP 正在形成的工具生态,很可能成为下一代 AI 应用的基础设施。如果你正在构建复杂的 AI 应用,现在投入时间学习 MCP,会是一个非常值得的前期投资。
