1. 项目概述:从零构建你的第一个MCP服务器
最近在AI应用开发圈里,MCP(Model Context Protocol)这个词的热度越来越高。如果你正在尝试将大型语言模型(LLM)的能力集成到自己的应用中,或者想让你的AI助手能更灵活地调用外部工具和数据,那么理解并实践MCP几乎是绕不开的一步。vivy-yi/mcp-tutorial这个项目,就是一个非常棒的入门实践指南。它不是一个简单的概念介绍,而是一个手把手教你从零开始,搭建一个具备实际功能的MCP服务器的实战教程。
简单来说,MCP定义了一套标准协议,让AI模型(比如Claude、GPT)能够以一种结构化的方式发现、描述和调用外部工具(Tools)或资源(Resources)。你可以把它想象成给AI模型装上了一套标准化的“插件系统”。通过MCP,模型不再仅仅依赖于预训练的知识,而是可以实时查询数据库、调用API、读取文件,甚至操作本地系统,极大地扩展了其能力边界和应用场景。这个教程的核心价值在于,它用一个具体的例子——构建一个“待办事项(Todo)管理器”的MCP服务器——将协议规范、SDK使用、服务部署和客户端调试的全流程串联了起来,让你在动手的过程中真正吃透MCP的工作原理。
无论你是前端开发者想为你的应用增加AI智能体,还是后端工程师希望构建可被AI调用的服务化能力,亦或是AI产品经理需要理解技术实现的边界,这个教程都能提供一个扎实的起点。它不要求你事先精通MCP的所有细节,但需要你具备基本的Node.js和JavaScript知识,以及一台可以运行命令的电脑。接下来,我们就深入这个项目,拆解每一个关键环节,并补充大量实战中才会遇到的细节和避坑指南。
2. 核心概念与MCP协议深度解析
在动手写代码之前,我们必须先搞清楚MCP到底是什么,以及它试图解决什么问题。这能帮助我们在后续开发中做出更合理的设计决策。
2.1 MCP要解决的核心痛点
在没有MCP这类协议之前,让AI模型使用外部工具通常有两种方式:一是通过特定的提示词工程(Prompt Engineering)在对话中“教”模型如何使用某个API,这种方式脆弱且难以维护;二是使用像LangChain这样的框架,它封装了很多工具链,但往往和框架本身绑定较深,不够通用。MCP的出现,就是为了制定一个与具体模型、具体框架无关的“通用插座”标准。
它的核心思想是服务化和声明式。服务化意味着工具能力由独立的服务器提供,与AI客户端解耦;声明式意味着服务器不需要告诉模型“怎么用”,只需要告诉模型“我有什么”(工具列表)和“每个工具是什么”(工具描述)。模型根据这些声明信息,自主决定在何时、为何种目的调用哪个工具。这极大地提升了灵活性和可组合性。
2.2 MCP协议的三驾马车:工具、资源和提示词
MCP协议主要围绕三个核心概念来组织能力,理解它们是你设计服务器的基石。
1. 工具(Tools)这是最常用、最核心的概念。一个工具就是一个可以被模型调用的函数。每个工具必须包含:
name: 唯一标识符,通常使用蛇形命名法,如create_todo。description: 对工具功能的自然语言描述。这部分至关重要,因为模型完全依赖这个描述来决定是否以及如何调用它。描述应清晰、无歧义,并说明输入参数。inputSchema: 定义工具接受的参数,使用JSON Schema格式。这相当于给模型的“参数说明书”。
例如,一个删除待办事项的工具描述可能是:“根据待办事项的ID删除一个指定的待办事项。需要提供id参数。” 对应的inputSchema会定义id字段为必填的字符串类型。
2. 资源(Resources)资源代表模型可以读取的静态或动态数据内容,比如一个文件、一个数据库查询的视图,或者一个API的响应。资源也通过URI和元数据(MIME类型、描述)来声明。模型可以请求“读取”某个资源的内容。在待办事项示例中,你可以把整个待办事项列表暴露为一个资源todo://list,模型就可以直接读取列表内容,而不必通过工具调用。
3. 提示词(Prompts)提示词是预定义的、参数化的文本模板。模型可以请求获取这些提示词,用于引导对话或生成特定内容。例如,你可以定义一个“总结待办事项”的提示词模板,它接受一个日期参数,返回类似“请总结用户在{{date}}这一天的待办事项完成情况”的文本。这允许服务器端对提示词进行集中管理和优化。
注意:在初学阶段,建议先从“工具”入手,因为它的使用模式最直接,也最能体现MCP的价值。资源和提示词可以在后续迭代中逐步加入。
2.3 通信方式:Stdio vs. SSE
MCP服务器与客户端(AI应用)之间如何通信?协议支持两种主要方式,教程中用的是第一种,但了解第二种对后续部署很重要。
Stdio(标准输入/输出)这是开发调试时最常用的方式。服务器作为一个命令行进程启动,通过标准输入(stdin)接收JSON-RPC请求,通过标准输出(stdout)发送JSON-RPC响应。这种方式简单、直接,无需网络配置,非常适合本地开发和与mcp命令行工具进行集成测试。教程中我们主要使用这种方式。
SSE(Server-Sent Events)这是一种基于HTTP的单向通信协议(服务器向客户端推送),MCP通常结合SSE和HTTP POST来实现双向通信。服务器作为一个HTTP服务运行。这种方式更适合生产环境部署,客户端可以通过网络远程连接服务器,实现能力的远程调用。当你需要将MCP服务器部署到云端供多个AI应用使用时,就需要采用SSE模式。
3. 环境准备与项目初始化实操
理论铺垫完毕,现在让我们打开终端,开始动手。这里我会补充大量教程之外的细节,确保你的环境一次配好,少走弯路。
3.1 开发环境搭建要点
首先,确保你的系统已经安装了Node.js(版本18或以上,推荐最新的LTS版本)和npm。你可以通过node --version和npm --version来检查。
接下来,我们需要一个趁手的代码编辑器。VS Code是绝大多数Node.js开发者的选择,我强烈建议安装以下几个扩展,能极大提升开发效率:
- ESLint: 代码质量检查。
- Prettier: 代码自动格式化。建议配置保存时自动格式化。
- JSON Schema Store: 自动为
package.json等文件提供智能提示。 - Thunder Client或REST Client: 用于后续测试HTTP端点(如果你扩展了SSE模式)。
创建一个全新的项目目录,并初始化项目:
mkdir my-mcp-todo-server cd my-mcp-todo-server npm init -y执行npm init -y会快速生成一个默认的package.json文件。我建议你立刻打开这个文件,做两处关键修改:
- 在
"scripts"部分,预先添加我们后续要用的命令,比如"start": "node index.js","dev": "nodemon index.js"。 - 在末尾添加
"type": "module"。这行代码非常重要,它告诉Node.js这个项目使用ES模块(ESM)规范,而不是旧的CommonJS(CJS)规范。MCP的官方SDK和现代JavaScript生态更倾向于ESM,使用它能避免后续很多模块导入导出的报错问题。
3.2 关键依赖安装与选型解析
教程会引导你安装@modelcontextprotocol/sdk。这是构建MCP服务器的核心SDK,由协议的主要推动者Anthropic官方维护,保证了与协议版本的同步和兼容性。
执行安装命令:
npm install @modelcontextprotocol/sdk除了核心SDK,在实际开发中,我们通常会引入一些辅助依赖来让开发更顺畅。我建议你一并安装它们:
npm install -D nodemon- nodemon:这是一个开发工具。它会在你修改代码后自动重启Node.js服务,无需你手动停止再启动。我们将它配置在
npm run dev命令中,用于开发阶段的热重载。
现在,你的package.json的dependencies和devDependencies应该看起来类似这样:
{ "name": "my-mcp-todo-server", "type": "module", "scripts": { "start": "node index.js", "dev": "nodemon index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0" }, "devDependencies": { "nodemon": "^3.0.0" } }实操心得:在Node.js项目中,明确模块系统(ESM vs CJS)是第一步,也是最容易踩坑的地方。如果你在后续导入模块时遇到
Cannot use import statement outside a module这类错误,99%的原因就是package.json里缺少"type": "module",或者你的文件扩展名不是.js而是.cjs。统一使用ESM能让你的代码更现代,也更容易与新的库集成。
4. 构建Todo MCP服务器的核心实现
让我们进入最核心的环节:编写服务器代码。我们将按照功能模块,一步步构建出一个完整的、具备增删改查能力的待办事项MCP服务器。
4.1 服务器骨架与连接初始化
首先,在项目根目录创建入口文件index.js。我们将从这里开始构建。
第一步是导入SDK并创建一个服务器实例。SDK提供了Server类,它是我们所有工作的基础。
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; // 1. 创建Server实例 const server = new Server( { name: 'todo-list-server', // 你的服务器名称 version: '0.1.0', // 版本号 }, { capabilities: { // 声明服务器支持的能力 tools: {}, // 我们支持提供工具 // 后续可以在这里添加 resources: {}, prompts: {} }, } );这里有几个关键点:
name和version:这是服务器的身份标识,客户端会看到这些信息。取一个清晰的名字很重要。capabilities:这是一个声明对象,告诉客户端“我有哪些本事”。目前我们只声明了tools,表示本服务器提供工具。这是一个空对象{},具体的工具列表会在后面动态添加。
接下来,我们需要设置传输层(Transport)。对于Stdio模式,SDK提供了开箱即用的StdioServerTransport。
// 2. 创建传输层并连接 const transport = new StdioServerTransport(); await server.connect(transport); console.error('Todo MCP 服务器已通过 stdio 启动'); // 使用 console.error 输出日志,避免干扰 JSON-RPC 通信await server.connect(transport)这行代码建立了服务器与传输通道的连接。特别注意:我们使用console.error来输出日志,因为MCP协议使用标准输出(stdout)进行JSON-RPC通信,任何意外的console.log输出都会破坏JSON格式,导致客户端解析失败。将日志重定向到标准错误(stderr)是MCP开发中的一个重要实践。
4.2 数据层的设计与内存存储
在实现工具之前,我们需要一个地方来存储待办事项。为了简化教程,我们使用内存存储,即一个JavaScript数组。在生产环境中,你需要将其替换为数据库(如SQLite、PostgreSQL等)。
我们在文件顶部,创建服务器实例之后,定义存储结构:
// 内存中的待办事项存储 let todos = []; let nextId = 1; // 用于生成自增ID // 待办事项的数据结构 // { // id: string, // 唯一标识 // title: string, // 事项标题 // completed: boolean, // 是否完成 // createdAt: Date // 创建时间 // }这里定义了一个todos数组和一个nextId计数器。每个待办事项对象包含id,title,completed,createdAt四个字段。使用自增ID虽然简单,但在分布式环境下会有问题,这里仅用于演示。
4.3 工具(Tools)的完整实现与注册
这是MCP服务器的灵魂。我们将实现四个核心工具:列出所有待办事项、创建新事项、切换完成状态、删除事项。
1. 工具:list_todos- 列出所有待办事项这个工具最简单,它不需要任何参数,直接返回内存中的todos数组。
server.setRequestHandler('tools/list', async () => { // 直接返回存储的待办事项列表 return { tools: [ { name: 'list_todos', description: '获取所有的待办事项列表。', inputSchema: { type: 'object', properties: {}, // 无输入参数 }, }, // 其他工具会在这里依次添加 ], }; });server.setRequestHandler用于处理客户端发来的特定请求。'tools/list'是请求类型,表示客户端在询问“你有什么工具?”。当这个请求到来时,我们返回一个包含所有工具定义的数组。注意,每个工具定义都严格遵循之前提到的结构:name,description,inputSchema。
2. 工具:create_todo- 创建新的待办事项这个工具需要接收一个参数title(事项标题)。
// 在 tools/list 返回的数组中,添加 create_todo 工具定义 { name: 'create_todo', description: '创建一个新的待办事项。需要提供事项的标题(title)。', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '待办事项的标题', }, }, required: ['title'], // 标记 title 为必填项 }, }定义好工具后,我们还需要处理它的调用。这是通过server.setRequestHandler('tools/call', ...)来实现的。
server.setRequestHandler('tools/call', async (request) => { const { name, arguments: args } = request.params; if (name === 'create_todo') { const { title } = args; if (!title || title.trim() === '') { throw new Error('标题不能为空'); } const newTodo = { id: String(nextId++), // 生成ID并转换为字符串 title: title.trim(), completed: false, createdAt: new Date(), }; todos.push(newTodo); // 返回调用结果。content 数组中的 text 内容会被模型看到。 return { content: [ { type: 'text', text: `已成功创建待办事项:“${newTodo.title}”(ID: ${newTodo.id})`, }, ], }; } // ... 其他工具的处理逻辑 });当客户端调用create_todo工具时,请求会到达这里。我们从request.params中解析出工具名和参数。进行简单的验证(标题非空)后,构造一个新的待办事项对象,存入todos数组,并返回一个成功的消息。返回的content字段是一个数组,其中text里的内容就是AI模型或用户最终会看到的工具执行结果。
3. 工具:toggle_todo- 切换待办事项的完成状态这个工具需要接收一个参数id,用于定位要操作的事项。
// 工具定义 { name: 'toggle_todo', description: '切换指定待办事项的完成状态(标记为完成或未完成)。需要提供待办事项的ID。', inputSchema: { type: 'object', properties: { id: { type: 'string', description: '待办事项的唯一ID', }, }, required: ['id'], }, } // 在 tools/call 处理逻辑中添加 if (name === 'toggle_todo') { const { id } = args; const todo = todos.find(t => t.id === id); if (!todo) { throw new Error(`未找到ID为 ${id} 的待办事项`); } todo.completed = !todo.completed; const status = todo.completed ? '已完成' : '未完成'; return { content: [ { type: 'text', text: `待办事项“${todo.title}”的状态已切换为:${status}`, }, ], }; }这里演示了基本的错误处理:如果根据提供的id找不到对应的待办事项,就抛出一个错误。错误信息会被MCP框架捕获并返回给客户端。
4. 工具:delete_todo- 删除待办事项实现与toggle_todo类似,但操作是从数组中移除元素。
// 工具定义 { name: 'delete_todo', description: '根据ID删除一个指定的待办事项。', inputSchema: { type: 'object', properties: { id: { type: 'string', description: '待办事项的唯一ID', }, }, required: ['id'], }, } // 在 tools/call 处理逻辑中添加 if (name === 'delete_todo') { const { id } = args; const initialLength = todos.length; todos = todos.filter(t => t.id !== id); // 过滤掉指定id的项 if (todos.length === initialLength) { throw new Error(`未找到ID为 ${id} 的待办事项,删除失败`); } return { content: [ { type: 'text', text: `已删除ID为 ${id} 的待办事项。`, }, ], }; }这里使用了数组的filter方法来删除元素,并通过比较删除前后的数组长度来判断是否成功找到了目标项。
4.4 工具列表的集中管理
随着工具增多,将工具定义集中管理是一个好习惯。你可以创建一个单独的tools.js文件,导出一个包含所有工具定义的数组,然后在index.js中导入。这样tools/list的处理函数就会变得非常简洁:
// tools.js export const toolDefinitions = [ { name: 'list_todos', ... }, { name: 'create_todo', ... }, // ... ]; // index.js import { toolDefinitions } from './tools.js'; server.setRequestHandler('tools/list', async () => ({ tools: toolDefinitions, }));5. 本地测试、调试与集成
代码写完了,但它到底能不能用?我们需要进行测试。MCP生态提供了强大的命令行工具来帮助我们。
5.1 安装MCP CLI并运行服务器
首先,你需要全局安装MCP命令行工具(假设你使用npm):
npm install -g @modelcontextprotocol/cli安装完成后,你可以使用mcp命令。
运行你的服务器有两种方式:
- 直接运行Node脚本:
node index.js。此时服务器会启动并等待来自stdin的输入。 - 使用MCP CLI进行测试:这是更推荐的方式,因为它提供了丰富的内省和调试功能。
mcp dev index.jsmcp dev命令会启动你的服务器,并进入一个交互式REPL(读取-求值-打印循环)环境。在这个环境里,你可以直接输入MCP协议命令来测试服务器。
5.2 使用MCP REPL进行手动测试
在mcp dev启动的REPL环境中,你可以尝试以下命令:
list_tools: 列出服务器提供的所有工具。你应该能看到我们定义的四个工具及其描述。call_tool list_todos: 调用list_todos工具。由于初始列表为空,可能会返回空数组。call_tool create_todo '{"title": "学习MCP协议"}': 调用create_todo工具,并传入JSON格式的参数。注意,参数必须是一个JSON字符串。- 再次调用
list_tools和call_tool list_todos,查看工具列表和新增的待办事项。
这个过程能让你直观地验证工具定义是否正确,以及工具调用逻辑是否正常工作。如果任何一步出错,REPL会打印出详细的错误信息,帮助你定位问题。
5.3 与AI客户端(如Claude Desktop)集成测试
真正的考验是与一个真正的AI客户端集成。Anthropic推出的Claude Desktop应用内置了MCP客户端支持,是绝佳的测试平台。
配置Claude Desktop:
- 打开Claude Desktop应用。
- 进入设置(Settings)。
- 找到“开发者设置”(Developer Settings)或“MCP服务器”配置部分。
- 点击“添加MCP服务器”(Add MCP Server)。
- 在配置中,你需要提供:
- 名称:例如 “My Todo Server”。
- 命令:这里要填写启动你服务器的完整命令。由于Claude Desktop会在它自己的环境下启动一个子进程,你需要提供Node.js解释器的绝对路径和你脚本的绝对路径。
- 在macOS/Linux上,可以通过
which node命令找到node路径。 - 一个典型的配置可能是:
/usr/local/bin/node /Users/yourname/projects/my-mcp-todo-server/index.js
- 在macOS/Linux上,可以通过
- 参数:通常留空。
保存配置并重启Claude Desktop。如果配置正确,你在与Claude对话时,它就能“发现”你提供的工具。你可以尝试对Claude说:“帮我创建一个待办事项,内容是‘买牛奶’。” Claude应该会理解你的意图,并调用create_todo工具。你可以继续测试:“列出我所有的待办事项”、“把第一个事项标记为完成”、“删除第二个事项”。
避坑指南:与Claude Desktop集成是新手最容易失败的一步。90%的问题出在“命令”配置上。确保:
- Node路径绝对正确。
- 脚本文件路径绝对正确,并且该文件有可执行权限。
- 你的服务器代码没有立即退出的情况(比如忘了
await server.connect或者进程过早结束)。服务器必须是一个持续运行的进程,等待stdin输入。- 检查Claude Desktop的日志(通常可以在设置中找到日志文件位置),里面会有更详细的错误信息。
6. 生产化进阶与扩展思路
一个能在本地运行的内存版服务器只是个开始。要让它在实际项目中发挥作用,我们还需要考虑更多。
6.1 数据持久化:从内存到数据库
内存存储的数据在服务器重启后会全部丢失。生产环境必须使用数据库。以轻量级的SQLite为例:
- 安装依赖:
npm install better-sqlite3 - 初始化数据库和表:
import Database from 'better-sqlite3'; const db = new Database('todos.db'); // 创建表 db.exec(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, completed BOOLEAN DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP ) `);- 重写数据操作逻辑:将原来操作
todos数组的代码,改为执行SQL语句。例如,create_todo工具的核心逻辑变为:
const stmt = db.prepare('INSERT INTO todos (title) VALUES (?)'); const info = stmt.run(title); // info.lastInsertRowid 可以获取插入的IDlist_todos工具则变为db.prepare('SELECT * FROM todos').all()。
6.2 支持SSE传输模式
要让服务器能被远程调用,需要实现SSE传输。这通常意味着要创建一个HTTP服务器。你可以使用Express.js等框架来简化。
- 安装Express:
npm install express - 创建HTTP服务器并处理MCP SSE连接:
import express from 'express'; import { SseServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; const app = express(); const PORT = process.env.PORT || 3000; // 为 /sse 端点提供SSE连接 app.get('/sse', async (req, res) => { const transport = new SseServerTransport('/message', res); await server.connect(transport); // 连接会一直保持,直到客户端断开 }); // 用于接收客户端消息的端点 app.post('/message', express.text(), (req, res) => { // 这里需要将接收到的消息转发给 transport // 具体实现依赖于SDK的SseServerTransport如何与Express集成 // 通常SDK会提供相应的适配器或示例 res.sendStatus(200); }); app.listen(PORT, () => { console.error(`MCP SSE 服务器运行在 http://localhost:${PORT}`); });实现完整的SSE服务器需要仔细阅读SDK中关于SseServerTransport的文档和示例,因为它涉及双向的事件流通信。核心思想是:/sse端点建立单向的服务器到客户端事件流,而/message端点用于接收客户端发来的请求。
6.3 错误处理、日志与监控强化
一个健壮的生产服务器必须有完善的错误处理。
- 全局错误捕获:在
tools/call处理函数外层使用try...catch,确保任何工具抛出的错误都能被捕获,并以MCP协议规定的错误格式返回给客户端,而不是导致整个服务器崩溃。 - 结构化日志:使用
winston或pino等日志库替代console.error,将日志按级别(info, warn, error)输出到文件或日志服务,方便排查问题。 - 输入验证:对工具参数进行更严格的验证,比如
title的长度限制、id的格式校验等。 - 性能监控:可以考虑为每个工具调用添加简单的耗时统计,记录到日志中,用于性能分析。
6.4 扩展更多MCP能力:资源与提示词
在工具之外,你可以探索MCP的另外两大能力。
- 暴露资源:例如,你可以将“本周已完成事项”作为一个资源
todo://resources/completed_this_week暴露出来。当模型需要总结本周工作时,它可以直接“读取”这个资源,而不是通过一系列工具调用来计算。 - 提供提示词模板:你可以定义一个提示词
summarize_todos,其模板是:“用户今天的待办事项情况如下:{{todos}}。请生成一份简短的工作汇报。” 当模型需要执行总结任务时,它可以请求这个提示词,并传入具体的todos数据,获得一个结构化的任务指令。
7. 常见问题排查与调试技巧实录
在实际开发中,你一定会遇到各种问题。下面是我在多次实践中总结的一些典型问题及其解决方法。
7.1 服务器启动失败或立即退出
症状:运行node index.js或mcp dev后,进程立刻结束,没有等待输入。排查:
- 检查异步连接:确保你使用了
await server.connect(transport)而不是server.connect(transport)。缺少await会导致连接过程还没完成,后续代码(可能没有了)执行完,进程就退出了。 - 检查未处理的Promise拒绝:在文件末尾添加以下代码,捕获可能被忽略的异步错误。
process.on('unhandledRejection', (reason, promise) => { console.error('未处理的Promise拒绝:', reason); process.exit(1); }); - 使用调试模式:在启动命令前加
NODE_DEBUG=mcp*环境变量,可以输出MCP SDK内部的详细日志。NODE_DEBUG=mcp* node index.js
7.2 客户端(如Claude Desktop)无法发现工具
症状:在Claude Desktop中配置了服务器,但对话时Claude表示没有可用工具。排查:
- 验证服务器独立运行是否正常:首先在终端用
mcp dev index.js测试,使用list_tools命令,确认服务器本身能正确返回工具列表。如果这里就失败,问题在服务器代码。 - 检查Claude Desktop配置:这是最常见的问题源。务必确认“命令”字段指向的Node和脚本路径绝对正确。一个验证方法是,把配置中的命令复制到终端里直接执行,看是否能启动一个持续运行的进程。
- 查看客户端日志:Claude Desktop通常有应用日志。在macOS上,日志可能在
~/Library/Logs/Claude/或通过Console.app查看。在Windows上,可能在使用者目录的AppData相关路径下。日志中会有连接服务器失败或协议握手失败的具体原因。 - 检查协议版本兼容性:确保你使用的
@modelcontextprotocol/sdk版本与Claude Desktop内置客户端支持的MCP协议版本兼容。通常使用最新的SDK版本问题不大。
7.3 工具调用失败,返回参数错误
症状:在REPL或Claude中调用工具,收到“Invalid params”或类似错误。排查:
- 核对
inputSchema:仔细检查工具定义中的inputSchema。required字段是否包含了所有必填参数?properties中定义的参数类型(type)是否与处理函数中期望的一致?例如,定义是string,但代码里当成了number使用。 - 检查参数传递格式:在REPL中调用时,参数必须是一个JSON字符串。
call_tool create_todo '{"title": "test"}'是正确的,而call_tool create_todo "test"或call_tool create_todo title=test是错误的。 - 在代码中添加详细日志:在
tools/call处理函数的最开始,打印request.params,看看客户端实际发送过来的参数到底是什么。server.setRequestHandler('tools/call', async (request) => { console.error('[DEBUG] 调用参数:', JSON.stringify(request.params)); // ... 原有逻辑 });
7.4 性能问题与内存泄漏
症状:服务器运行一段时间后变慢或崩溃。排查:
- 内存存储限制:如果一直使用内存数组,待办事项数量无限增长最终会导致内存耗尽。务必实现数据持久化或设置存储上限。
- 避免阻塞操作:在工具处理函数中,如果执行了同步的、耗时的操作(如大文件读取、复杂的同步计算),会阻塞整个服务器,导致其他请求排队。确保所有I/O操作都是异步的(使用async/await)。
- 检查循环引用:如果你在工具处理函数中不小心引用了会导致循环引用的对象,并且没有正确释放,可能会引起内存泄漏。使用Node.js的
--inspect标志启动服务器,利用Chrome DevTools的Memory面板进行快照对比分析。
7.5 安全考量
当你的MCP服务器开始暴露给网络时,安全就变得重要。
- 输入净化:永远不要相信客户端传入的参数。对所有的字符串参数进行净化,防止注入攻击(无论是SQL注入还是命令注入)。例如,即使参数用于文件名,也要严格限制字符集。
- 身份验证与授权:SSE服务器通常需要暴露在网络上。考虑为你的MCP服务器添加简单的API密钥认证。可以在HTTP请求头中检查一个预共享的密钥。
- 权限控制:不是所有工具都应该被所有调用者使用。如果你的服务器有
delete_database这样的危险工具,考虑实现基于调用来源的权限控制(但这通常需要客户端支持传递身份信息,目前MCP协议标准对此的支持还在演进中)。 - 速率限制:为你的服务器接口添加速率限制,防止恶意调用或意外循环调用导致服务过载。
构建MCP服务器的过程,本质上是在为AI模型设计一套可编程的“手”和“眼”。从最初的概念理解,到第一个工具的成功调用,再到最终部署为一个健壮的远程服务,每一步都加深了你对AI应用架构的理解。这个“待办事项管理器”的示例虽然简单,但它所蕴含的协议交互模式、工具设计理念和系统架构思想,是构建任何复杂AI赋能应用的基础。当你掌握了这些,你就可以尝试将MCP连接到你的数据库、内部API、云服务甚至物联网设备,真正释放出AI模型与真实世界交互的潜力。