MCP协议详解:从零构建大模型工具扩展接口

📅 2026/8/27 23:35:01
MCP协议详解:从零构建大模型工具扩展接口
1. 项目概述为什么我们需要一个“模型能力”的接口标准最近在折腾大模型应用开发的朋友估计都绕不开一个词MCP。全称是Model Context Protocol直译过来是“模型上下文协议”。乍一听可能有点云里雾里但如果你尝试过把不同来源的工具、数据接入到同一个AI助手比如Claude Desktop、Cursor IDE里的AI功能里你就会立刻明白它的价值。简单来说MCP就是一个为AI模型特别是大语言模型定义如何与外部工具、数据源进行安全、标准化交互的协议。想象一下这个场景你希望你的AI编程助手不仅能写代码还能帮你查数据库、调用公司的内部API、读取你本地特定格式的日志文件甚至控制你的智能家居。如果没有一个统一的标准每个工具、每个数据源的接入方式都千奇百怪——有的用HTTP API有的用gRPC有的甚至需要你写一堆胶水代码和复杂的提示词Prompt来“教”模型怎么用。这不仅开发效率低下更带来了巨大的安全风险和不可控性。MCP要解决的就是这个“最后一公里”的标准化问题。它为模型的能力扩展定义了一套“插座”和“插头”的规范让任何符合标准的“能力”我们称之为Server都能即插即用地被任何支持MCP的“模型客户端”我们称之为Client所使用。这不仅仅是技术上的便利。从行业角度看MCP正在成为AI应用架构中一个潜在的基础层。它分离了“模型推理”和“工具使用”让模型专注于它擅长的理解和生成而将具体的执行交给专业化、受控的外部服务。对于开发者而言这意味着你可以专注于编写实现特定功能的MCP Server而无需关心最终用户用的是Claude、GPT还是其他什么模型。对于用户而言这意味着你可以像安装插件一样为你喜欢的AI助手灵活增添能力构建真正属于你的、功能强大的数字副驾。2. MCP核心设计思路与架构拆解MCP的设计哲学非常清晰标准化、松耦合、安全性优先。它不是另一个RPC框架而是一套专门为“模型调用工具”这个场景设计的通信契约。2.1 核心角色Client, Server 与 Transport整个协议围绕着三个核心角色运转理解它们的关系是理解MCP的关键。MCP Server能力提供方这是实际干活的部分。一个MCP Server对外暴露一组定义好的“能力”比如“读取文件系统”、“执行SQL查询”、“调用天气API”。它不关心谁在调用它只负责接收标准化的请求执行操作并返回标准化的结果。你可以把它想象成一个微服务但接口是专门为AI模型定制的。MCP Client模型/应用方这是发起请求的一方。通常是一个大模型应用或平台比如Claude Desktop、Cursor或者你自己写的AI应用。Client的角色是集成MCP协议发现可用的Server并在需要时代表模型向Server发起请求。Client的核心职责还包括管理模型与Server之间的会话上下文。Transport传输层这是连接Client和Server的桥梁。MCP设计上支持多种传输方式目前最常见的是stdio标准输入输出和SSEServer-Sent Events。Stdio模式通常用于本地集成比如一个本地的Python脚本作为Server而SSE则更适合网络远程调用。传输层是透明的协议本身的消息格式是统一的。这种架构带来的最大好处是解耦。工具开发者Server方和应用开发者Client方可以独立工作只要他们都遵守MCP协议。一个工具可以被任何支持MCP的AI应用使用反之亦然。2.2 协议核心资源Resources与工具ToolsMCP定义了两类核心实体模型通过它们与外界交互资源Resources代表可供模型读取的静态或动态数据。例如一个数据库表、一个API的文档、一个文件夹的文件列表甚至是一个实时更新的股票价格流。资源有唯一的URI如file:///path/to/doc或db://sales/customers和MIME类型。Client可以通过“列出资源”和“读取资源”来获取这些信息并将其作为上下文提供给模型。这解决了“模型如何知道外部有什么数据可用”的问题。工具Tools代表可供模型调用的函数或操作。这是模型主动影响外界的接口。每个工具都有名称、描述、以及严格定义的输入参数JSON Schema。例如“执行SQL”工具可能接受一个query字符串参数“发送邮件”工具可能需要to,subject,body等参数。当模型决定使用一个工具时Client会代表它调用对应的Server工具并将执行结果返回给模型。一个关键设计在于模型并不直接“看到”Server。Client会将自己连接的所有Server提供的资源和工具以一种统一、规范的方式“呈现”给模型。模型只知道“有一些可用的资源和工具”而无需知晓它们背后来自哪个Server、如何实现。这极大地简化了模型的认知负担也提升了安全性。2.3 会话Session与上下文管理MCP协议是有状态的基于会话。一个会话始于Client和Server的握手initialize请求包含了Server宣告其能力list_resources,list_tools以及后续一系列的“读取资源”和“调用工具”交互。所有与某个Server的通信都在同一个会话上下文中进行这允许Server维护一些临时状态比如数据库连接、用户认证令牌等。注意虽然MCP Server可以维护会话状态但协议鼓励将其设计为无状态或轻状态。最佳实践是将状态保存在外部如数据库、缓存而Server本身是可随时重启或替换的。这符合云原生和微服务的设计理念。3. 从零实现一个MCP Server以“待办事项管理器”为例理论讲得再多不如动手实现一个。我们来实现一个简单的“待办事项Todo List管理器”MCP Server。它将提供两个能力1) 列出所有待办事项作为资源2) 添加新的待办事项作为工具。我们将使用官方推荐的TypeScript/JavaScript SDK(modelcontextprotocol/sdk) 来开发这是目前最成熟和活跃的实现。3.1 环境准备与项目初始化首先确保你的环境有Node.js建议18以上版本和npm。# 创建一个新目录并初始化项目 mkdir mcp-todo-server cd mcp-todo-server npm init -y # 安装MCP SDK和TypeScript用于类型安全非必须但强烈推荐 npm install modelcontextprotocol/sdk npm install -D typescript types/node tsx npx tsc --init编辑package.json添加一个启动脚本{ scripts: { start: tsx server.ts } }3.2 构建Server核心逻辑创建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; // 1. 初始化Server实例 const server new Server( { name: todo-list-server, version: 0.1.0, }, { capabilities: { resources: {}, // 声明我们支持资源相关操作 tools: {}, // 声明我们支持工具相关操作 }, } ); // 2. 模拟一个内存中的待办事项存储 let todoItems: Array{id: string, title: string, completed: boolean} [ { id: 1, title: 学习MCP协议, completed: true }, { id: 2, title: 编写示例Server, completed: false }, { id: 3, title: 测试集成, completed: false }, ]; // 3. 实现“列出资源”处理器 server.setRequestHandler(ListResourcesRequestSchema, async () { // 我们将整个待办事项列表作为一个资源提供 return { resources: [ { uri: todo://list/all, // 自定义的URI scheme用于标识资源 mimeType: application/json, name: 所有待办事项, description: 当前所有的待办事项列表, }, ], }; }); // 4. 实现“读取资源”处理器 server.setRequestHandler(ReadResourceRequestSchema, async (request) { if (request.params.uri todo://list/all) { // 返回待办事项列表的JSON字符串 return { contents: [ { uri: request.params.uri, mimeType: application/json, text: JSON.stringify(todoItems, null, 2), // 美化输出方便阅读 }, ], }; } // 如果请求了未知资源抛出错误 throw new Error(Resource not found: ${request.params.uri}); }); // 5. 定义“添加待办事项”工具 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: add_todo_item, description: 添加一个新的待办事项, inputSchema: { type: object, properties: { title: { type: string, description: 待办事项的标题, }, }, required: [title], }, }, ], }; }); // 6. 实现“调用工具”处理器 server.setRequestHandler(CallToolRequestSchema, async (request) { if (request.params.name add_todo_item) { const title request.params.arguments?.title; if (typeof title ! string || !title.trim()) { throw new Error(参数“title”是必须的字符串且不能为空); } // 创建新待办事项 const newItem { id: todo_${Date.now()}, title: title.trim(), completed: false, }; todoItems.push(newItem); // 返回执行结果 return { content: [ { type: text, text: 已成功添加待办事项“${newItem.title}”。当前共有 ${todoItems.length} 项待办。, }, // 也可以选择性地返回更新后的列表作为结构化数据 { type: object, object: { added: newItem, totalCount: todoItems.length, }, }, ], }; } throw new Error(Unknown tool: ${request.params.name}); }); // 7. 启动Server使用stdio传输 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Todo Server is running on stdio...); } main().catch((error) { console.error(Server fatal error:, error); process.exit(1); });3.3 关键代码解析与注意事项URI设计我们使用了todo://list/all这个自定义URI。在实际项目中URI的设计应具有层次性和意义能清晰表达资源的类型和标识。例如github://owner/repo/issues/open可能表示GitHub上某个仓库的开放issue列表。工具输入模式inputSchema这是安全性和可用性的关键。我们使用JSON Schema严格定义了add_todo_item工具只接受一个名为title的字符串参数且必填。这确保了模型或任何调用者必须提供格式正确的数据Server端可以进行验证避免了任意参数注入的风险。结果返回格式CallToolRequest的返回结果content字段是一个数组支持多种类型text,image,object等。我们同时返回了易于人类阅读的文本和结构化的对象数据。结构化数据object类型对于Client进行后续处理非常有用。错误处理对于未知的URI或工具我们抛出了Error。MCP SDK会将其转换为标准的错误响应。在生产环境中你需要更精细的错误分类和用户友好的错误信息。状态管理本例使用内存数组存储数据这意味着Server重启后数据会丢失。在生产环境中你必须使用外部持久化存储如数据库、文件系统或云存储。Server实例本身应该是无状态的。4. 在Claude Desktop中集成与测试你的MCP Server编写完Server后最关键的一步是让它被AI客户端使用。我们以Anthropic官方出品的Claude Desktop为例。4.1 配置Claude DesktopClaude Desktop允许通过一个JSON配置文件来添加MCP Server。配置文件的位置通常如下macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json如果文件不存在就创建一个。其基本结构如下{ mcpServers: { todo-list: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/mcp-todo-server/server.ts ], env: { NODE_ENV: development } } } }重要提示command必须是能在系统PATH中找到的命令。这里我们用node来执行TypeScript文件前提是你安装了tsx或ts-node并能直接运行.ts文件。更稳妥的方式是先将TypeScript编译成JavaScript然后指向.js文件。args数组中的第一个元素是脚本的绝对路径。相对路径很可能导致启动失败。修改配置后必须完全重启Claude Desktop应用退出后重新启动配置才会生效。4.2 更稳健的部署方式使用可执行脚本直接运行.ts文件在开发时方便但在生产配置中容易出问题。建议创建一个启动脚本。编译TypeScript在package.json中添加构建脚本。scripts: { build: tsc, start: node build/server.js }运行npm run build生成build/server.js。创建Shell脚本Unix/macOS/Linux创建run_server.sh#!/bin/bash # 进入脚本所在目录 cd $(dirname $0) # 执行编译后的JS文件 node build/server.js赋予执行权限chmod x run_server.sh更新Claude配置{ mcpServers: { todo-list: { command: /ABSOLUTE/PATH/TO/mcp-todo-server/run_server.sh } } }或者如果你全局安装了tsx也可以配置为command: tsx, args: [/path/to/server.ts]。4.3 在Claude中验证与使用重启Claude Desktop后打开聊天界面。如果Server配置成功且无错误启动Claude会在后台与其建立连接。你可以通过以下方式验证直接询问尝试问Claude“你现在有什么额外的工具或能力吗”或者“你能看到我的待办事项列表吗”。一个正确集成的Claude可能会回答“我连接了一个待办事项管理器可以帮你查看列表或添加新事项。”使用工具你可以说“请使用添加待办事项工具帮我记下‘给MCP Server添加删除功能’。” Claude应该会理解你的意图调用add_todo_item工具并返回操作结果。查看资源你可以说“让我看看当前的待办事项列表。” Claude会调用read_resource来获取todo://list/all的内容并将其作为上下文信息呈现给你。实操心得在开发调试阶段务必查看Claude Desktop的日志。在macOS上你可以通过运行Console.app控制台在左侧选择你的设备然后搜索“Claude”来查看其详细的输出日志。Server通过stdio输出的错误信息比如console.error会在这里显示这是排查连接失败、协议错误或代码bug的最重要途径。5. 高级主题安全、性能与生产级实践一个玩具级的Server和能上生产环境的Server之间有巨大差距。以下是几个关键考量点。5.1 安全性设计MCP Server本质上是授予AI模型一个执行权限安全至关重要。权限最小化你的Server暴露的工具和资源应该遵循最小权限原则。例如一个文件浏览Server不应该提供“删除文件”或“执行任意命令”的工具除非绝对必要。输入验证与净化即使在inputSchema中定义了类型在工具实现内部也必须再次进行严格的验证和净化。特别是对于涉及文件路径、SQL语句、系统命令的参数要防止目录遍历、SQL注入、命令注入等攻击。// 反例危险的文件读取 const filePath args.path; // 用户可控 fs.readFileSync(filePath); // 如果path是../../../etc/passwd则造成安全漏洞 // 正例安全限制 const SAFE_BASE_DIR /home/user/data; const userPath args.path; const resolvedPath path.resolve(SAFE_BASE_DIR, userPath); if (!resolvedPath.startsWith(SAFE_BASE_DIR)) { throw new Error(访问路径越界); } fs.readFileSync(resolvedPath);认证与授权网络Server如果你运行的是SSE模式的网络Server必须实现认证。可以在Client配置中添加API密钥并在Server启动时验证。MCP协议本身不规定认证方式这需要你在传输层之上自己实现。沙箱化执行对于执行代码、访问敏感数据的工具考虑在沙箱环境如Docker容器、VM、安全的子进程中运行以隔离潜在风险。5.2 性能与可观测性异步与非阻塞确保你的工具处理函数是异步的async并且不会阻塞事件循环。对于可能耗时的操作如网络请求、大文件处理要使用setTimeout、Promise或工作线程来避免阻塞。资源列表的优化list_resources可能在每次会话初始化时都被调用。如果资源列表很大或生成成本高考虑实现分页、缓存或只返回一个摘要在read_resource时再懒加载详细信息。日志与监控集成成熟的日志库如Winston、Pino记录Server的生命周期事件、工具调用参数、结果、耗时和错误。这对于调试和运营至关重要。可以考虑将指标如调用次数、延迟导出到Prometheus等监控系统。健康检查为你的网络Server实现一个独立的/health端点供容器编排器如Kubernetes进行存活性和就绪性探测。5.3 构建复杂的生产级Server数据库与外部API集成让我们扩展之前的Todo Server将其连接到真实的数据库并添加一个调用外部API的工具。集成数据库以SQLite为例npm install better-sqlite3import Database from better-sqlite3; const db new Database(todos.db); // 初始化表 db.exec( CREATE TABLE IF NOT EXISTS todos ( id TEXT PRIMARY KEY, title TEXT NOT NULL, completed BOOLEAN DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ) ); // 修改工具实现使用数据库操作 server.setRequestHandler(CallToolRequestSchema, async (request) { if (request.params.name add_todo_item) { const title request.params.arguments?.title; // ... 验证 ... const id todo_${Date.now()}; const stmt db.prepare(INSERT INTO todos (id, title) VALUES (?, ?)); stmt.run(id, title); // ... 返回结果 ... } });集成外部API以天气查询为例server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ // ... 原有的add_todo_item ... { name: get_weather, description: 获取指定城市的当前天气, inputSchema: { type: object, properties: { city: { type: string, description: 城市名称如“北京” }, units: { type: string, enum: [metric, imperial], description: 单位制metric为摄氏度imperial为华氏度, default: metric } }, required: [city], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) { if (request.params.name get_weather) { const { city, units metric } request.params.arguments as any; // 使用一个假设的天气API实际中请替换为真实的API和密钥 const apiKey process.env.WEATHER_API_KEY; if (!apiKey) { throw new Error(天气服务未配置API密钥); } const url https://api.weatherapi.com/v1/current.json?key${apiKey}q${encodeURIComponent(city)}; const response await fetch(url); if (!response.ok) { throw new Error(天气API请求失败: ${response.statusText}); } const data await response.json(); // 提取并格式化所需信息 const temp data.current.temp_c; const condition data.current.condition.text; return { content: [{ type: text, text: 城市 ${city} 的当前天气${condition}温度 ${temp}°C。 }] }; } // ... 处理其他工具 ... });注意事项API密钥等敏感信息必须通过环境变量process.env传入绝不可硬编码在代码中。对于网络请求必须添加超时和重试逻辑并妥善处理各种错误情况网络异常、API限流、返回数据格式不符等。6. 常见问题排查与调试技巧实录在实际开发和集成中你一定会遇到各种问题。以下是我踩过坑后总结的排查清单。6.1 Server启动失败或连接不上现象可能原因排查步骤Claude启动后无新功能配置文件路径错误、JSON格式错误、命令执行失败1. 检查配置文件路径和名称是否正确。2. 使用jsonlint或在线工具验证JSON格式。3. 在终端手动执行配置中的command和args看能否成功运行脚本。4. 查看Claude Desktop日志控制台寻找Server进程的启动错误输出。连接短暂建立后断开Server代码存在未捕获的异常导致进程崩溃1. 在Server代码开头添加process.on(uncaughtException, ...)和process.on(unhandledRejection, ...)全局错误处理器记录错误。2. 确保所有异步操作都有.catch处理或放在try...catch中。3. 检查工具/资源处理函数中是否有同步抛出异常的情况。提示“无法初始化”或“协议错误”Server没有正确实现MCP协议握手1. 确认你使用的SDK版本与Client兼容。2. 检查Server初始化时的capabilities配置是否正确声明了你实现的功能如resources: {}, tools: {}。3. 确保setRequestHandler为必要的请求类型ListResourcesRequest等设置了处理器。6.2 工具或资源不可见现象可能原因排查步骤Claude感知不到新工具list_tools返回格式错误或工具定义不符合Schema1. 在list_tools处理器中使用console.log打印返回的tools数组确保结构正确。2. 检查inputSchema是否符合JSON Schema规范特别是type,properties,required字段。3. 重启Claude有时Client会缓存之前的能力列表。读取资源返回错误或空内容URI不匹配或read_resource处理器逻辑错误1. 确认在list_resources中返回的uri与read_resource请求中的uri完全一致包括大小写和协议头。2. 在read_resource处理器中打印request.params.uri验证是否被正确调用。3. 确保返回的contents数组结构正确且text或data字段存在。6.3 工具调用失败或结果异常现象可能原因排查步骤调用工具时报“参数无效”模型传递的参数与inputSchema不匹配1. 在call_tool处理器中打印request.params.arguments查看模型实际传递了什么。2. 对比你的inputSchema检查是否有未设置default值的可选参数被模型忽略了或者参数类型不匹配。3.模型有时会“脑补”参数即使Schema要求是数字模型也可能传字符串。在工具实现内部做类型转换和验证。工具执行成功但Claude不理解结果返回的content格式不利于模型理解1. 优先返回type: text的、自然语言描述的结果这是模型最容易处理的。2. 结构化数据object可以作为补充但不要完全依赖它。模型可能无法正确解析复杂的嵌套对象。3. 在返回文本中清晰说明操作的结果和状态变化。工具执行超时或无响应工具函数执行了长时间阻塞操作1. 将耗时操作如网络请求、大文件I/O包装在Promise中并设置超时。2. 考虑将长时间任务改为异步通知机制工具立即返回一个“任务已提交”的响应然后通过其他方式如另一个资源传递结果。6.4 调试技巧进阶独立测试Server不要总依赖Claude来测试。可以写一个简单的测试Client脚本使用StdioTransport连接你的Server手动发送协议请求观察响应。这能帮你快速定位是协议问题还是Claude集成问题。启用SDK调试日志许多MCP SDK支持设置环境变量来输出详细的调试日志例如NODE_DEBUGmcp*。这能让你看到每一帧协议消息的发送和接收。模拟慢速或异常网络对于网络Server使用工具如tc命令限速、toxiproxy模拟网络故障来测试Client的重连和容错机制是否健全。版本兼容性密切关注MCP协议版本和SDK的更新。不同版本间可能有细微的破坏性变更。在package.json中固定SDK的版本号避免自动升级导致生产环境故障。7. MCP生态展望与个人实践建议MCP协议虽然年轻但其背后体现的“标准化模型能力接口”的思想正在被越来越广泛的社区和厂商接受。除了Anthropic的Claude其他平台如Cursor、Windsurf等IDE也在积极集成MCP。开源社区也涌现了大量优秀的MCP Server从连接GitHub、Jira到控制智能家居、查询区块链数据几乎无所不包。对于个人开发者和团队我的建议是从解决自己的痛点开始最好的MCP Server往往源于自身的需求。你是否经常需要让AI助手查询某个内部系统状态处理特定格式的本地文件把它封装成一个MCP Server你立刻就能在Claude里使用它。设计清晰、专注的接口一个Server最好只做一件事并把它做好。避免创建“瑞士军刀”式的巨型Server。工具和资源的命名、描述要清晰、无歧义这直接影响了模型能否正确理解和使用它们。积极参与社区MCP的官方仓库和社区论坛是学习的最佳场所。看看别人是怎么设计URI的如何处理错误有什么安全最佳实践。遇到问题时去那里提问或搜索很可能已经有人解决了。将安全性置于首位在将任何Server连接到存有敏感数据或拥有执行权限的AI助手之前反复审查其代码。思考“如果这个工具被恶意提示词滥用最坏的结果是什么” 并实施相应的防护措施。我个人在将多个内部系统项目管理系统、监控仪表盘、部署工具通过MCP暴露给Claude后日常工作流发生了显著变化。很多原本需要切换多个浏览器标签、登录不同后台才能完成的信息查询和简单操作现在只需要在Claude里用自然语言说一句就能完成。这种“能力即插即用”的体验正是MCP协议带来的最直观价值。它不是一个遥不可及的技术概念而是一个能立刻提升你与AI协作效率的实用工具。开始构建你的第一个Server你会发现为模型扩展能力比想象中要简单得多。