基于MCP协议与Node.js构建AI工具服务器:从原理到实践
1. 项目概述一个为AI应用量身定制的MCP模板最近在折腾AI应用开发特别是想给大语言模型LLM接上各种外部工具和API时发现了一个绕不开的“中间件”——Model Context Protocol也就是MCP。简单来说MCP就像是一个标准化的“插座”和“插头”规范它定义了AI模型比如Claude、ChatGPT如何安全、高效地调用外部服务器比如数据库、天气API、文件系统上的功能。而adamwulf/mcp-template这个项目就是一个为快速构建这类MCP服务器也就是“插座”本身而生的脚手架模板。想象一下你有一个很棒的想法让AI助手帮你管理日历、分析GitHub仓库数据或者控制智能家居。要实现这些你需要写一个服务端程序暴露一些API然后还要写一堆胶水代码让AI模型理解并调用这些API。这个过程繁琐且容易出错。mcp-template的价值就在于它把MCP协议实现的底层细节比如SSE通信、协议格式封装、错误处理都打包好了你只需要像填空一样专注于实现你的核心业务逻辑——“我这个服务器到底能干什么”。这个模板由开发者Adam Wulf维护它基于Node.js和TypeScript提供了开箱即用的项目结构、类型安全、热重载、以及完善的测试和打包配置。无论你是想为内部工具快速搭建一个AI可用的接口还是想开发一个面向社区的通用MCP工具这个模板都能让你跳过从零开始的痛苦阶段直接进入“创造”环节。接下来我就结合自己用它开发一个小型“待办事项管理MCP服务器”的经历拆解一下它的核心设计、使用方法和那些官方文档里不会写的实战技巧。2. 核心架构与设计思路拆解2.1 为什么是MCP协议层解决了什么问题在深入模板之前有必要先理解MCP协议本身要解决的核心痛点。在没有统一协议之前每个AI应用平台如Claude Desktop、Cursor如果想集成第三方工具都需要开发者针对其特定的插件SDK进行适配。这导致了几个问题1. 开发碎片化为一个功能写Claude插件、写Cursor插件、写ChatGPT的GPTs Action本质是重复劳动。2. 安全与权限控制复杂每个平台都有自己的授权和资源访问模型难以统一管理。3. 工具发现与组合困难工具之间难以相互调用和组合成工作流。MCP的出现旨在将工具提供者Server和工具消费者Client通常是AI应用解耦。它定义了一套基于JSON-RPC over SSEServer-Sent Events或STDIO的通信标准。Server只需要声明自己提供了哪些“工具”Tools和“资源”ResourcesClient就能动态发现并调用它们。adamwulf/mcp-template正是为快速创建这样一个标准化的Server而设计的。2.2 模板的目录结构与职责划分克隆adamwulf/mcp-template仓库后你会看到一个非常清晰且现代的项目结构这本身就体现了良好的工程实践。mcp-template/ ├── src/ │ ├── index.ts # 服务器主入口MCP服务器初始化与配置 │ ├── server.ts # MCP服务器核心逻辑工具和资源的注册中心 │ ├── tools/ # 工具Tools实现目录 │ │ └── exampleTool.ts # 示例工具 │ ├── resources/ # 资源Resources实现目录 │ │ └── exampleResource.ts # 示例资源 │ └── types.ts # 项目相关的TypeScript类型定义 ├── scripts/ │ └── develop.mjs # 开发脚本用于启动带热重载的开发服务器 ├── tests/ │ └── server.test.ts # 服务器功能测试 ├── package.json ├── tsconfig.json └── README.md核心文件解读src/index.ts这是应用的起点。它通常非常简洁主要工作是调用src/server.ts中导出的函数来创建MCP服务器实例并启动它。在这里你可以注入一些配置比如服务器监听的端口、启用的工具列表等。src/server.ts这是大脑。它利用modelcontextprotocol/sdk这个官方SDK创建一个Server实例。你的主要工作就是在这个文件里通过server.setRequestHandler()方法来注册对各种MCP请求如tools/list,tools/call,resources/list等的处理函数。模板已经搭好了骨架你只需要在对应的地方“填空”。src/tools/和src/resources/这是你要填充血肉的地方。每个工具或资源都是一个独立的模块遵循特定的接口定义。这种模块化设计让你可以轻松地增删功能。实操心得一开始我试图把所有工具逻辑都塞进server.ts很快就变得难以维护。后来严格遵循模板的模块化设计一个功能一个文件不仅代码清晰而且单元测试写起来也特别方便。这是模板带来的第一个也是最重要的好习惯。2.3 类型安全与开发体验优化模板全面采用TypeScript并预先配置了严格的tsconfig.json。这意味着你在实现工具时能获得完善的类型提示和编译时检查。例如当你定义一个工具时你需要指定它的name、description和inputSchema。TypeScript会确保inputSchema符合JSON Schema类型并在你调用工具处理函数时自动推断出参数args的类型。package.json里预置的脚本也是一大亮点npm run dev启动开发服务器并监视文件变化实现热重载。你修改src/tools/下的代码后几乎秒级就能生效无需手动重启。npm run build将TypeScript代码编译、打包成单一的、优化过的JavaScript文件输出到dist/目录便于部署。npm test运行Jest测试套件。模板自带了一个简单的测试示例引导你如何对MCP服务器进行集成测试。这种开箱即用的配置把项目搭建、开发、构建、测试的流水线都准备好了让你能100%聚焦于业务逻辑开发。3. 从零实现一个“待办事项”MCP工具理论说得再多不如动手做一个。假设我们要创建一个简单的“待办事项管理器”MCP服务器让AI助手可以帮我们查看、添加、完成待办事项。3.1 环境准备与项目初始化首先确保你的系统安装了Node.js建议18.x或更高版本和npm。然后你可以直接使用这个模板作为起点。# 使用degit、git clone或直接下载模板仓库 npx degit adamwulf/mcp-template my-todo-mcp-server cd my-todo-mcp-server npm install安装完成后运行npm run dev你应该能看到服务器成功启动的日志通常监听在3000端口。此时一个遵循MCP协议的服务器就已经在运行了虽然它现在还只有模板自带的示例工具。3.2 定义数据模型与内存存储为了简化我们不引入外部数据库先用一个内存中的数组来存储待办事项。在src/目录下创建一个store.ts文件。// src/store.ts export interface TodoItem { id: string; title: string; description?: string; completed: boolean; createdAt: Date; } // 简单的内存存储 class TodoStore { private todos: TodoItem[] []; private idCounter 1; getAll(): TodoItem[] { return [...this.todos]; // 返回副本 } getById(id: string): TodoItem | undefined { return this.todos.find(todo todo.id id); } add(title: string, description?: string): TodoItem { const newTodo: TodoItem { id: todo_${this.idCounter}, title, description, completed: false, createdAt: new Date() }; this.todos.push(newTodo); return newTodo; } toggleComplete(id: string): TodoItem | undefined { const todo this.todos.find(t t.id id); if (todo) { todo.completed !todo.completed; } return todo; } delete(id: string): boolean { const initialLength this.todos.length; this.todos this.todos.filter(t t.id ! id); return this.todos.length initialLength; } } export const todoStore new TodoStore();这个存储类提供了基本的CRUD操作。在实际项目中你可以轻松地将它替换为SQLite、PostgreSQL或任何其他数据库客户端。3.3 实现第一个工具list_todos现在我们来创建第一个MCP工具列出所有待办事项。在src/tools/目录下创建listTodos.ts。// src/tools/listTodos.ts import { Tool } from modelcontextprotocol/sdk/server.js; import { todoStore } from ../store.js; /** * 列出所有待办事项的工具 */ export const listTodosTool: Tool { name: list_todos, // 工具的唯一标识符AI将通过这个名字调用它 description: 获取所有的待办事项列表。可以按完成状态过滤。, inputSchema: { type: object, properties: { filter: { type: string, enum: [all, active, completed], description: 过滤条件all全部active未完成completed已完成。默认为all。 } }, additionalProperties: false // 禁止传入未定义的参数增强安全性 } }; /** * 处理list_tools调用的函数 */ export async function handleListTodos(args: { filter?: all | active | completed }) { const { filter all } args; let todos todoStore.getAll(); if (filter active) { todos todos.filter(t !t.completed); } else if (filter completed) { todos todos.filter(t t.completed); } if (todos.length 0) { return { content: [{ type: text, text: 当前没有${filter all ? : filter}待办事项。 }] }; } // 将待办事项格式化为易读的文本 const todoListText todos.map(todo - [${todo.completed ? x : }] ${todo.title} (ID: ${todo.id})${todo.description ? \n 描述${todo.description} : } ).join(\n); return { content: [{ type: text, text: 当前待办事项${filter}\n${todoListText} }] }; }关键点解析inputSchema这是工具的灵魂。它用JSON Schema精确描述了AI调用此工具时需要或可以提供的参数。这里我们定义了一个可选参数filter并限制了它的可选值。清晰的Schema能让AI更准确地理解如何使用你的工具。返回值格式MCP要求工具返回特定格式。content数组中的每个元素可以是text或image类型。我们返回纯文本并进行了友好的格式化。错误处理在函数内部我们处理了“空列表”的情况返回了友好的提示而不是一个空数组或错误。这能提升AI与用户交互的体验。3.4 实现更多工具并注册到服务器按照同样的模式我们可以实现add_todo、complete_todo等工具。创建src/tools/addTodo.ts和src/tools/completeTodo.ts。// src/tools/addTodo.ts import { Tool } from modelcontextprotocol/sdk/server.js; import { todoStore } from ../store.js; export const addTodoTool: Tool { name: add_todo, description: 添加一个新的待办事项。, inputSchema: { type: object, properties: { title: { type: string, description: 待办事项的标题必填 }, description: { type: string, description: 待办事项的详细描述可选 } }, required: [title], // 标记title为必填参数 additionalProperties: false } }; export async function handleAddTodo(args: { title: string; description?: string }) { const newTodo todoStore.add(args.title, args.description); return { content: [{ type: text, text: ✅ 已成功添加待办事项“${newTodo.title}”ID: ${newTodo.id} }] }; }// src/tools/completeTodo.ts import { Tool } from modelcontextprotocol/sdk/server.js; import { todoStore } from ../store.js; export const completeTodoTool: Tool { name: complete_todo, description: 标记一个待办事项为完成或未完成切换状态。, inputSchema: { type: object, properties: { todo_id: { type: string, description: 待办事项的ID可通过list_todos获取 } }, required: [todo_id], additionalProperties: false } }; export async function handleCompleteTodo(args: { todo_id: string }) { const updatedTodo todoStore.toggleComplete(args.todo_id); if (!updatedTodo) { return { content: [{ type: text, text: ❌ 未找到ID为“${args.todo_id}”的待办事项。请检查ID是否正确。 }], isError: true // 明确告知Client这是一个错误 }; } const status updatedTodo.completed ? 已完成 : 未完成; return { content: [{ type: text, text: ✅ 已将待办事项“${updatedTodo.title}”标记为${status}。 }] }; }现在我们需要在src/server.ts中将这些工具注册到MCP服务器。找到tools相关的处理函数通常在server.setRequestHandler()里修改如下// 在 src/server.ts 中示例位置 import { listTodosTool, handleListTodos } from ./tools/listTodos.js; import { addTodoTool, handleAddTodo } from ./tools/addTodo.js; import { completeTodoTool, handleCompleteTodo } from ./tools/completeTodo.js; // ... 在server.setRequestHandler内部 ... server.setRequestHandler(McpServerRequestMethods.ToolsList, async () { return { tools: [listTodosTool, addTodoTool, completeTodoTool] // 注册所有工具定义 }; }); server.setRequestHandler(McpServerRequestMethods.ToolsCall, async (request) { const { name, arguments: args } request.params; // 根据工具名称路由到对应的处理函数 switch (name) { case listTodosTool.name: return await handleListTodos(args as any); case addTodoTool.name: return await handleAddTodo(args as any); case completeTodoTool.name: return await handleCompleteTodo(args as any); default: throw new Error(未知的工具${name}); } });注意事项工具名称name是AI识别和调用工具的唯一标识建议使用snake_case命名法并保持清晰、无歧义。在ToolsCall处理器中switch路由是一种简单直接的方式。当工具数量增多时可以考虑使用一个Map来管理以提高可维护性。3.5 配置与连接AI客户端服务器写好了如何让Claude或Cursor这样的AI客户端使用它呢这需要通过客户端的配置来实现。以Claude Desktop为例你需要找到其配置文件通常在~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS。你需要添加一个MCP服务器配置指定其名称、命令和参数。因为我们用Node.js开发所以命令是node参数是我们的构建产物路径。{ mcpServers: { my-todo-server: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/mcp-template/dist/index.js ], env: { NODE_ENV: production } } } }关键点绝对路径args中的路径必须是绝对路径。在开发时你可以先运行npm run build然后将生成的dist/index.js的绝对路径填在这里。开发模式连接如果你在开发调试不想每次修改都构建可以配置为直接运行npm run dev对应的命令通常是一个启动开发服务器的脚本。但要注意Claude Desktop可能会因为进程不退出的脚本而报错更稳妥的方式还是连接构建后的产物。重启客户端修改配置文件后必须完全重启Claude Desktop客户端配置才会生效。重启后在Claude的聊天界面你应该能看到一个类似“已连接服务器”的提示或者你可以直接问“你能使用待办事项工具吗”AI应该会回答它发现了list_todos等工具并可以开始调用它们了。4. 高级功能与生产环境考量4.1 实现资源Resources提供除了工具MCP另一个核心概念是“资源”。资源代表一些可读的、结构化的数据块AI可以“读取”它们来获取上下文。例如我们可以提供一个todo://资源让AI直接读取某个特定待办事项的详细信息。在src/resources/下创建todoResource.ts// src/resources/todoResource.ts import { Resource } from modelcontextprotocol/sdk/server.js; import { todoStore } from ../store.js; /** * 声明此服务器提供的资源模板。 * URI格式类似todo://todo_{id} */ export const todoResources: Resource[] [ { uri: todo://todo_, // 这是一个模板实际URI会包含具体ID name: 待办事项详情, description: 通过ID获取特定待办事项的完整详细信息。, mimeType: application/json // 我们以JSON格式返回数据 } ]; /** * 处理读取资源的请求 */ export async function handleReadResource(uri: string) { // 从URI中解析出ID例如从 todo://todo_123 中提取 123 const id uri.replace(todo://todo_, ); const todo todoStore.getById(todo_${id}); // 注意匹配我们存储的ID格式 if (!todo) { throw new Error(未找到URI为 ${uri} 的资源); } return { contents: [{ uri, mimeType: application/json, // 资源内容这里我们返回结构化的JSON text: JSON.stringify({ id: todo.id, title: todo.title, description: todo.description, completed: todo.completed, createdAt: todo.createdAt.toISOString() }, null, 2) // 美化JSON输出 }] }; }然后在src/server.ts中注册资源相关的处理器ResourcesList和ResourcesRead。这样当AI需要深入了解某个待办事项时可以直接引用todo://todo_1这样的URIMCP客户端会自动向服务器请求该资源的内容并将其作为上下文提供给AI模型。4.2 错误处理与日志记录生产级应用必须有健壮的错误处理和清晰的日志。模板本身没有强制规定但你应该自己加上。全局错误捕获在src/index.ts的服务器启动逻辑中用try-catch包裹并记录致命错误。工具调用错误在每个工具处理函数中都应进行充分的参数校验和业务逻辑错误处理。如completeTodo中如果ID不存在我们返回了isError: true的信息。更复杂的场景可能需要定义不同的错误类型。结构化日志使用如pino或winston这样的日志库替代console.log。在开发时输出到控制台在生产环境输出到文件或日志服务。记录关键事件如工具调用参数、结果、错误详情等这对调试和监控至关重要。// 示例简单的日志工具 import pino from pino; const logger pino({ level: process.env.LOG_LEVEL || info }); // 在工具处理函数中使用 export async function handleAddTodo(args: { title: string; description?: string }) { logger.info({ tool: add_todo, args }, 开始添加待办事项); try { // ... 业务逻辑 ... logger.info({ tool: add_todo, newTodoId: newTodo.id }, 待办事项添加成功); return { /* ... */ }; } catch (error) { logger.error({ tool: add_todo, args, error }, 添加待办事项失败); return { content: [{ type: text, text: 添加失败请稍后重试。 }], isError: true }; } }4.3 安全性考虑MCP服务器可能暴露给AI访问内部或外部系统安全至关重要。输入验证Input Validation这是第一道防线。inputSchema是强大的验证工具务必充分利用required、type、enum、pattern正则表达式等属性严格限制输入格式和范围。永远不要相信来自客户端的输入。权限控制Permission ScopingMCP协议本身支持权限提示。在工具定义中你可以通过inputSchema的description字段暗示所需权限但更精细的控制需要在服务器内部实现。例如一个“删除文件”的工具在实现时应检查调用上下文如果服务器能获取到的话或要求额外的认证令牌。访问速率限制Rate Limiting防止滥用。可以使用express-rate-limit等中间件或在你的工具处理逻辑中基于来源IP或会话ID进行简单的调用次数限制。敏感信息不落地不要在日志、错误信息中泄露敏感数据如API密钥、文件路径全文。使用环境变量来管理配置。5. 调试、测试与部署实战5.1 使用MCP Inspector进行调试手动在AI客户端里测试工具调用效率很低。官方提供了modelcontextprotocol/sdk包中的inspector它是一个强大的命令行调试工具。首先确保你的服务器正在运行npm run dev。然后在另一个终端运行npx modelcontextprotocol/sdk inspector http://localhost:3000这会打开一个基于文本的用户界面。在这里你可以列出服务器提供的所有工具和资源。手动调用任何工具并填写参数。实时查看原始的MCP请求和响应。模拟资源读取请求。这是开发过程中不可或缺的调试利器能让你在集成到AI客户端之前就验证工具的逻辑和返回格式是否正确。5.2 编写自动化测试模板使用Jest作为测试框架。为你的工具编写测试能极大提升代码可靠性。例如为handleAddTodo编写测试// tests/tools/addTodo.test.ts import { handleAddTodo } from ../../src/tools/addTodo.js; import { todoStore } from ../../src/store.js; // 在每个测试前清空存储避免测试间相互影响 beforeEach(() { // 假设todoStore有重置方法或者我们直接替换引用根据实现调整 // 这里是一种简单粗暴的方式实际项目中store可能需要提供reset方法 (todoStore as any).todos []; }); describe(handleAddTodo, () { it(应该成功添加一个待办事项, async () { const args { title: 测试待办 }; const result await handleAddTodo(args); expect(result.content[0].text).toContain(已成功添加); expect(result.content[0].text).toContain(测试待办); // 也可以断言store中确实多了一条数据 expect(todoStore.getAll()).toHaveLength(1); expect(todoStore.getAll()[0].title).toBe(测试待办); }); it(添加带描述的待办事项, async () { const args { title: 测试, description: 这是一个描述 }; await handleAddTodo(args); const addedTodo todoStore.getAll()[0]; expect(addedTodo.description).toBe(这是一个描述); }); it(缺少标题时应抛出错误或返回错误信息, async () { // 注意由于我们的inputSchema定义了title为requiredMCP客户端会在调用前就校验。 // 因此handleAddTodo可能永远不会收到缺少title的参数。 // 这个测试案例说明了Schema验证的重要性它把基础校验前置了。 // 我们可以测试其他业务逻辑错误。 }); });运行npm test来执行测试。良好的测试覆盖率是代码质量的保证。5.3 打包与部署当开发完成并通过测试后就需要部署了。构建优化运行npm run build。模板通常配置了esbuild或tsup将代码打包成单个、优化过的JS文件。检查dist/index.js是否生成。环境变量将配置如数据库连接字符串、API密钥通过环境变量传入。在代码中使用process.env.YOUR_VAR读取。可以使用dotenv包在开发时加载.env文件。进程管理在生产环境不要直接用node dist/index.js运行。使用进程管理器如pm2或systemd来保证应用崩溃后自动重启并管理日志。# 使用PM2的例子 npm install -g pm2 pm2 start dist/index.js --name my-mcp-server pm2 save pm2 startup # 设置开机自启反向代理与HTTPS如果你的服务器需要从公网访问虽然MCP通常用于本地或内网建议使用Nginx或Caddy作为反向代理并配置HTTPS证书如Let‘s Encrypt以加密通信数据。6. 常见问题与排查技巧实录在实际使用mcp-template和开发MCP服务器的过程中我踩过不少坑。这里总结一份速查表希望能帮你节省时间。问题现象可能原因排查步骤与解决方案Claude Desktop提示“无法连接服务器”或“服务器未响应”。1. 配置文件路径错误。2. 服务器进程未启动或崩溃。3. 端口冲突。1.检查配置文件确认args中的Node路径和JS文件绝对路径100%正确。路径中不要有~要用完整路径。2.检查服务器日志在终端手动运行node dist/index.js看是否有错误输出。确保代码无语法错误依赖已安装。3.检查端口确认服务器监听的端口默认3000未被其他程序占用。可以在配置中更换端口试试。AI客户端能发现工具但调用时失败返回“工具执行错误”。1. 工具处理函数内部抛出未捕获的异常。2. 返回格式不符合MCP协议。3. 参数类型不匹配。1.查看服务器日志这是最重要的信息源。工具调用时的任何未捕获错误都会在服务器终端打印出来。2.使用MCP Inspector用Inspector手动调用工具对比成功和失败的请求/响应检查返回的JSON结构是否正确是否包含必需的字段如content。3.校验输入Schema确保inputSchema定义准确特别是required字段和type。AI客户端会依据Schema传参如果Schema定义模糊AI可能传错类型。修改代码后重启Claude依然看不到新工具。1. 客户端缓存了旧的工具列表。2. 服务器未正确注册新工具。3. 构建未更新。1.彻底重启客户端完全退出Claude Desktop包括任务栏/托盘图标再重新打开。2.检查注册逻辑确认新工具的定义和处理函数已正确导入并在ToolsList和ToolsCall中注册。3.重新构建如果连接的是构建产物dist/index.js确保在修改代码后运行了npm run build。开发模式下则确保热重载生效。工具调用成功但AI不理解返回的内容。返回的文本内容对AI不友好。优化返回文本AI尤其是GPT/Claude这类模型对自然语言的理解最好。返回的text字段应该是完整、通顺的句子或段落而不是零散的数据。例如与其返回{“status“ “ok”}不如返回“操作已成功完成。文件已保存至/path/to/file。”。清晰的文本能极大提升AI后续回答的质量。服务器在Docker容器中运行客户端无法连接。网络配置问题客户端宿主机无法访问容器内的服务。1.确保端口映射运行Docker时使用-p 3000:3000将容器端口映射到宿主机。2.检查容器内监听地址在服务器代码中确保监听的是0.0.0.0而不是127.0.0.1localhost。0.0.0.0表示监听所有网络接口。最后一点个人体会MCP生态还在快速发展中adamwulf/mcp-template是一个极佳的起点但它不是银弹。理解MCP协议的核心思想——标准化工具调用接口——比熟练使用某个模板更重要。当你需要实现更复杂的功能如工具调用链、动态资源发现、用户认证时你可能需要深入研究官方SDK的文档。这个模板帮你解决了80%的基建问题剩下的20%的定制化和深度优化才是体现你项目独特价值的地方。从这个小巧的待办事项服务器开始尝试去连接更真实的世界吧比如你的邮件系统、项目管理工具或者物联网设备你会发现为AI赋予“动手能力”是一件充满成就感的事。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2599424.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!