使用mcp-maker快速构建AI工具调用服务器:从协议原理到工程实践
1. 项目概述与核心价值最近在折腾AI应用开发特别是想给大语言模型LLM装上更强大的“手脚”让它能直接操作我电脑上的各种软件和工具。这听起来很酷对吧但实际操作起来你会发现一个核心痛点如何让AI安全、高效地理解并调用外部工具这就是我接触到MCPModel Context Protocol协议的原因。简单来说MCP是一个标准化的协议它定义了AI模型客户端如何与外部工具服务器进行通信。你可以把它想象成AI世界的“USB接口”标准有了它不同的AI模型就能即插即用地调用各种各样的工具而工具开发者也不用为每个模型都写一遍适配代码。今天要聊的这个项目MrAliHasan/mcp-maker就是一个专门用来快速创建MCP服务器的工具包。它的核心价值就是让你能像搭积木一样把你想让AI操作的功能比如读取文件、查询数据库、控制智能家居封装成一个标准的MCP服务器。这样一来无论是Claude Desktop、Cursor还是其他支持MCP的AI应用都能无缝调用你自定义的工具。我花了几天时间深度使用和拆解了这个项目发现它确实极大地简化了MCP服务器的开发流程把原本需要理解协议细节、处理网络通信、管理资源生命周期的复杂工作封装成了几个简单的函数调用和配置。对于想为AI构建专属“工具箱”的开发者来说这无疑是一把利器。2. MCP协议与Maker工具包的核心设计解析在深入代码之前我们得先搞清楚MCP协议到底在解决什么问题以及mcp-maker是如何基于此进行设计的。传统的AI工具调用往往是“硬编码”或者通过特定的插件系统。比如你想让ChatGPT帮你总结一个PDF可能需要一个专门的“PDF总结插件”。这种方式的问题在于耦合性太高工具和AI模型绑定死了扩展性和复用性都很差。MCP协议的出现就是为了解耦。它定义了一套基于JSON-RPC的通信规范服务器工具提供方向客户端AI模型宣告自己有哪些“能力”工具以及这些工具的输入参数格式。当AI需要使用时就按照约定好的格式发送请求服务器执行后返回结果。那么mcp-maker的设计聪明在哪里呢它没有重新发明轮子而是基于官方的modelcontextprotocol/sdkNode.js版进行了高层抽象。它的核心设计思想是“声明式配置”和“约定优于配置”。你不需要从零开始写一个HTTP服务器、处理WebSocket连接、解析MCP消息。你只需要关注两件事第一我的工具要做什么业务逻辑第二我的工具需要什么参数。mcp-maker帮你处理了所有协议层的脏活累活。它的架构可以简单理解为三层协议适配层由mcp-maker内部实现负责与MCP客户端建立连接、收发符合协议的消息、管理会话状态。工具注册与管理层这是你主要交互的部分。你通过调用maker对象的方法来注册你的工具函数并声明工具的名称、描述和参数模式schema。业务逻辑层这就是你写的JavaScript/TypeScript函数实现具体的工具功能比如读写文件、调用API等。这种设计带来的最大好处是开发效率的飞跃。原本可能需要几百行代码才能搭建的一个基础MCP服务器现在可能只需要几十行。更重要的是它降低了心智负担开发者可以更专注于工具本身的逻辑而不是协议的细枝末节。3. 从零开始环境准备与第一个MCP服务器理论说得再多不如动手做一遍。我们来一步步搭建环境并创建第一个“Hello World”级别的MCP服务器。这个服务器将提供一个简单的工具让AI可以查询当前时间。3.1 初始化项目与安装依赖首先确保你的系统已经安装了Node.js建议版本18或以上和npm。然后创建一个新的项目目录并初始化。mkdir my-first-mcp-server cd my-first-mcp-server npm init -y接下来安装核心依赖。mcp-maker是我们要用的主包同时我们也会安装官方SDK和TypeScript相关依赖如果你想用TS的话推荐使用可以获得更好的类型提示。npm install modelcontextprotocol/sdk mcp-maker npm install -D typescript types/node tsx然后初始化TypeScript配置。npx tsc --init在生成的tsconfig.json中确保target是ES2022或更高module是commonjs或NodeNext并且outDir设置为./dist。3.2 编写核心服务器代码现在创建我们的主文件src/server.ts。我们将在这里定义我们的工具和启动服务器。import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; // 用于定义参数schema需要额外安装 npm install zod import { makeMcpServer } from mcp-maker; // 1. 使用 mcp-maker 创建服务器实例 // makeMcpServer 内部已经帮我们创建了 McpServer 实例并做了一些默认配置 const { server, maker } makeMcpServer({ name: my-first-toolbox, version: 0.1.0, }); // 2. 使用 maker 注册工具 // 第一个参数是工具的唯一名称第二个参数是工具的描述AI会看到这个描述来决定是否使用该工具 // 第三个参数是参数schema这里我们定义一个可选参数 format表示时间格式 maker.addTool( get_current_time, 获取当前的系统时间。可以指定格式例如 YYYY-MM-DD 或 HH:mm:ss。, { format: z.string().optional().describe(时间格式字符串默认为完整ISO格式), }, // 第四个参数是工具的执行函数其参数就是上面schema定义的对象 async ({ format }) { const now new Date(); let result: string; if (format YYYY-MM-DD) { result now.toISOString().split(T)[0]; // 取日期部分 } else if (format HH:mm:ss) { const timePart now.toISOString().split(T)[1].split(.)[0]; result timePart; // 取时间部分 } else { result now.toISOString(); // 默认返回完整ISO字符串 } // 返回的内容会被包装成MCP协议规定的格式返回给AI客户端 return { content: [ { type: text, text: 当前时间是${result}, }, ], }; } ); // 3. 注册另一个工具简单的计算器 maker.addTool( calculate, 执行简单的数学运算。支持加()、减(-)、乘(*)、除(/)。, { a: z.number().describe(第一个数字), b: z.number().describe(第二个数字), op: z.enum([, -, *, /]).describe(运算符), }, async ({ a, b, op }) { let result: number; switch (op) { case : result a b; break; case -: result a - b; break; case *: result a * b; break; case /: if (b 0) { return { content: [{ type: text, text: 错误除数不能为零。 }], }; } result a / b; break; default: return { content: [{ type: text, text: 不支持的运算符: ${op} }], }; } return { content: [ { type: text, text: 计算结果${a} ${op} ${b} ${result}, }, ], }; } ); // 4. 启动服务器使用标准输入输出作为传输层 // 这是MCP服务器最常见的运行方式由父进程如AI客户端通过stdio启动 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP服务器已启动正在等待连接...); } main().catch((error) { console.error(服务器启动失败:, error); process.exit(1); });注意MCP服务器通常通过stdio标准输入/输出与客户端通信而不是监听一个HTTP端口。这意味着你的服务器需要被AI客户端作为一个子进程启动。代码中console.error用于输出日志因为stdout需要用于协议通信。3.3 构建与运行测试编写完代码后我们需要编译TypeScript并创建一个可以直接运行的脚本。首先在package.json中添加启动脚本。{ name: my-first-mcp-server, version: 0.1.0, type: module, scripts: { build: tsc, start: node dist/server.js }, dependencies: { modelcontextprotocol/sdk: ^0.5.0, mcp-maker: ^0.2.1, zod: ^3.22.4 }, devDependencies: { types/node: ^20.11.24, typescript: ^5.3.3 } }运行构建和启动命令npm run build npm start如果一切正常你会看到“MCP服务器已启动正在等待连接...”的输出并且进程不会退出而是在等待来自stdin的输入。这时你的MCP服务器就已经在运行了。要测试它你需要一个MCP客户端。最方便的是使用Claude Desktop并在其配置中添加你的服务器。4. 高级功能与核心环节实现一个基础的服务器跑起来后我们会发现很多实际需求比如工具需要访问文件系统、调用网络资源、或者需要有状态比如记住用户的上一次操作。mcp-maker和MCP协议本身提供了一些高级机制来处理这些场景。4.1 资源Resources与提示Prompts的注册MCP协议不仅支持工具Tools还支持资源Resources和提示Prompts。资源可以理解为AI可读取的“数据源”比如一个文件、一个数据库查询的只读视图。提示则是预定义的文本模板AI可以用来引导对话或生成内容。mcp-maker同样简化了这两者的注册。下面我们看一个例子注册一个资源读取项目下的README文件和一个提示代码审查模板。// 在 server.ts 中注册工具之后添加资源和提示 import fs from fs/promises; import path from path; import { fileURLToPath } from url; const __dirname path.dirname(fileURLToPath(import.meta.url)); // ... 之前注册工具的代码 ... // 注册一个资源项目根目录的 README.md 文件 maker.addResource( readme_file, // URI模板客户端会通过类似 file:///readme 的URI来访问 file:///readme, async (uri) { // 当客户端请求这个资源时这个函数被调用 try { const filePath path.join(__dirname, .., README.md); const content await fs.readFile(filePath, utf-8); return { contents: [ { uri: uri.href, mimeType: text/markdown, text: content, }, ], }; } catch (error) { // 如果文件不存在返回一个错误信息资源 return { contents: [ { uri: uri.href, mimeType: text/plain, text: 无法读取README文件: ${error}, }, ], }; } } ); // 注册一个提示Prompt maker.addPrompt( code_review, 一个用于代码审查的提示模板它会请求AI分析给定代码的潜在问题。, { code: z.string().describe(需要审查的代码片段), language: z.string().optional().describe(编程语言例如 javascript, python), }, async ({ code, language }) { // 构建一个提示消息数组这将成为AI对话的初始上下文 const messages [ { role: user as const, content: { type: text, text: 请对以下${language || 代码}进行审查指出潜在的错误、代码风格问题、性能隐患和安全漏洞。请分点列出。\n\n代码\n\\\${language || }\n${code}\n\\\, }, }, ]; return { messages }; } ); // ... 之后的启动代码 ...这里的关键点资源和工具不同它们通常是被“读取”的而不是被“调用”的。AI客户端可以通过list_resources和read_resource来发现和获取资源内容。而提示Prompt则更像一个对话的快捷方式当用户调用这个提示时AI客户端会直接将messages插入到对话上下文中从而引导AI进入特定的角色或任务。4.2 状态管理与上下文保持MCP服务器默认是无状态的每次工具调用都是独立的。但有些工具需要记住一些信息比如一个简单的待办事项列表。MCP协议支持通过session会话来保持状态。mcp-maker通过maker实例的上下文可以相对方便地在同一次服务器运行的生命周期内共享数据但要注意如果服务器进程重启内存中的状态就会丢失。对于需要持久化或更复杂状态管理的场景你需要自己引入数据库或文件存储。下面是一个在内存中维护待办事项的例子演示如何让多个工具共享状态。// 定义一个简单的内存存储 interface TodoItem { id: number; task: string; completed: boolean; } class TodoStore { private todos: TodoItem[] []; private nextId 1; add(task: string): TodoItem { const item { id: this.nextId, task, completed: false }; this.todos.push(item); return item; } list(): TodoItem[] { return [...this.todos]; } complete(id: number): boolean { const item this.todos.find(t t.id id); if (item) { item.completed true; return true; } return false; } } // 创建全局存储实例在实际应用中需要考虑并发安全这里仅为示例 const todoStore new TodoStore(); // 注册添加待办事项的工具 maker.addTool( todo_add, 添加一个新的待办事项。, { task: z.string().describe(待办事项的描述), }, async ({ task }) { const item todoStore.add(task); return { content: [{ type: text, text: 已添加待办事项 [#${item.id}]${task}, }], }; } ); // 注册列出所有待办事项的工具 maker.addTool( todo_list, 列出所有的待办事项。, {}, async () { const todos todoStore.list(); if (todos.length 0) { return { content: [{ type: text, text: 当前没有待办事项。, }], }; } const listText todos.map(t [${t.completed ? x : }] #${t.id}: ${t.task}).join(\n); return { content: [{ type: text, text: 当前待办事项\n${listText}, }], }; } ); // 注册完成待办事项的工具 maker.addTool( todo_complete, 标记一个待办事项为已完成。, { id: z.number().describe(待办事项的ID), }, async ({ id }) { const success todoStore.complete(id); return { content: [{ type: text, text: success ? 待办事项 #${id} 已完成。 : 未找到ID为 ${id} 的待办事项。, }], }; } );通过这种方式todo_add、todo_list和todo_complete这三个工具就能操作同一个内存中的数据存储。当你在AI客户端如Claude中依次调用它们时就能实现一个简单的交互式待办列表管理。4.3 错误处理与日志输出健壮的工具必须要有良好的错误处理。在工具函数中你应该使用try...catch来捕获可能的异常并返回友好的错误信息而不是让整个服务器崩溃。MCP协议允许在返回结果中包含错误信息但更常见的做法是在工具函数内部处理并返回一个包含错误文本的content。maker.addTool( read_file, 读取指定路径的文件内容。, { filepath: z.string().describe(文件的路径), }, async ({ filepath }) { try { // 安全警告在实际生产中必须严格验证和限制 filepath防止路径遍历攻击 const absolutePath path.resolve(process.cwd(), filepath); // 可以在这里添加路径安全检查逻辑... const content await fs.readFile(absolutePath, utf-8); return { content: [{ type: text, text: 文件【${filepath}】的内容\n\\\\n${content}\n\\\, }], }; } catch (error: any) { // 返回结构化的错误信息而不是抛出异常 return { content: [{ type: text, text: 读取文件失败${error.message}, }], }; } } );对于服务器本身的运行日志如前所述请使用console.error或console.warn避免使用console.log因为stdout被用于协议通信。你也可以集成像winston或pino这样的专业日志库将日志输出到文件或标准错误。5. 与AI客户端集成以Claude Desktop为例开发好的MCP服务器最终是要被AI客户端使用的。目前对MCP支持最完善、体验最好的客户端之一是Anthropic的Claude Desktop应用。下面介绍如何将我们自建的服务器配置到Claude Desktop中。5.1 创建客户端配置文件Claude Desktop允许通过JSON配置文件来添加自定义的MCP服务器。配置文件的位置通常如下macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json如果文件不存在就创建一个。配置文件的基本结构如下{ mcpServers: { my-first-toolbox: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/server.js ], env: { NODE_ENV: development } } } }关键配置解析my-first-toolbox这是你给这个服务器起的名字会在Claude的界面中显示。command启动服务器的命令。因为我们是用Node.js运行的所以是node。args命令的参数。最重要的一点这里必须提供编译后的JavaScript文件server.js的绝对路径。不能是TypeScript源文件也不能是相对路径除非你能确保Claude启动时的工作目录正确但这很不可靠。使用path.resolve或直接写绝对路径是最稳妥的。env可选设置服务器进程的环境变量。5.2 配置生效与测试保存配置文件。完全退出Claude Desktop应用不仅仅是关闭窗口要从任务栏/程序坞退出。重新启动Claude Desktop。在Claude的聊天界面中你应该能看到一个新的“工具”图标通常是个螺丝刀或魔杖形状。点击它如果配置正确你会看到你注册的工具列表例如“get_current_time”、“calculate”、“todo_add”等。现在你就可以在聊天中直接使用了。例如输入“请用我的工具获取当前时间”Claude可能会自动调用get_current_time工具并返回结果。或者你可以更直接地说“调用calculate工具计算123乘以456”。实操心得在配置args路径时我强烈建议在服务器启动脚本server.ts的开头用console.error打印出__dirname和要加载的文件路径。这能帮你快速定位路径问题这是集成过程中最常见的坑。5.3 调试技巧当服务器没有正常工作时可以按以下步骤排查检查Claude配置确认JSON格式正确路径无误。可以尝试在终端中直接用配置的命令和参数启动服务器看是否能正常运行。查看客户端日志Claude Desktop通常会有应用日志。在macOS上可以通过Console.app查看在Linux上可能输出到~/.config/Claude/logs。日志中会包含加载MCP服务器失败的具体原因。服务器端日志我们在代码中使用的console.error信息会输出到服务器的stderr。当Claude启动服务器时这些日志可能会被重定向到某个地方或者被丢弃。一个更可靠的调试方法是在开发时暂时修改服务器代码让其不仅通过stdio运行也同时在一个简单的HTTP端口上提供调试信息但这需要修改传输层比较麻烦。更简单的方法是使用文件日志将console.error重定向到一个文件。使用MCP InspectorMCP生态系统提供了一个名为modelcontextprotocol/inspector的工具它可以作为一个中间人记录客户端和服务器之间的所有通信。这对于深度调试协议层面的问题非常有用。你可以通过NPM全局安装它然后修改Claude的配置让command指向mcp-inspector并把你服务器的命令作为参数传给inspector。6. 生产环境部署与性能优化考量当我们想把一个MCP服务器用于生产环境或者提供给团队其他成员使用时就需要考虑更多工程化的问题。6.1 项目结构与代码组织对于一个包含多个复杂工具的服务器把所有代码都写在server.ts里会变得难以维护。推荐按功能模块进行拆分。src/ ├── index.ts // 主入口创建服务器、注册模块 ├── tools/ │ ├── index.ts // 聚合所有工具模块 │ ├── systemTools.ts // 系统类工具时间、文件 │ ├── calculator.ts // 计算器工具 │ └── todoManager.ts // 待办事项工具 ├── resources/ │ └── fileResource.ts // 文件资源相关 ├── prompts/ │ └── codeReviewPrompt.ts // 提示模板 └── utils/ └── logger.ts // 日志工具在每个工具模块中导出工具的定义函数。// src/tools/systemTools.ts import { z } from zod; import { ToolFunction } from mcp-maker; // 假设mcp-maker导出这个类型 export const getCurrentTimeTool: ToolFunction { name: get_current_time, description: 获取当前的系统时间。可以指定格式例如 YYYY-MM-DD 或 HH:mm:ss。, schema: { format: z.string().optional().describe(时间格式字符串默认为完整ISO格式), }, handler: async ({ format }) { // ... 处理逻辑 ... }, }; // src/tools/index.ts import { getCurrentTimeTool } from ./systemTools.js; import { calculateTool } from ./calculator.js; // ... 导入其他工具 export const allTools [ getCurrentTimeTool, calculateTool, // ... ];在主入口文件中遍历allTools数组用maker.addTool逐一注册。这样结构清晰易于扩展和维护。6.2 安全性加固MCP服务器赋予了AI直接操作你系统资源的能力因此安全是重中之重。输入验证与净化Zod Schema是第一步但还不够。对于文件路径、URL、系统命令等参数必须进行严格的验证和限制。例如对于read_file工具应该将路径限制在某个安全目录沙箱内并检查是否包含..等路径遍历字符。import path from path; const SAFE_BASE_DIR path.resolve(process.cwd(), ./data); const userPath path.resolve(SAFE_BASE_DIR, inputPath); if (!userPath.startsWith(SAFE_BASE_DIR)) { throw new Error(访问路径超出允许范围); }权限控制不是所有工具都应该对所有用户开放。虽然MCP协议本身没有内置的用户认证但你可以在服务器层面实现。例如通过环境变量传递一个API密钥或者在工具函数中检查调用上下文如果未来协议支持。目前更常见的做法是将不同安全级别的工具拆分成不同的服务器并为高权限服务器配置更严格的启动控制比如只允许本地连接。速率限制防止被恶意或意外地频繁调用导致资源耗尽。可以在工具函数外层添加一个简单的计数器或使用rate-limiter-flexible这样的库。错误信息模糊化在生产环境中返回给客户端的错误信息不应包含系统内部细节如堆栈跟踪、绝对路径以免泄露敏感信息。6.3 性能与可观测性异步与并发确保你的工具函数是异步的async并且处理好并发。避免使用阻塞操作。对于耗时的操作如调用外部API考虑设置超时。健康检查虽然MCP协议没有标准的健康检查端点但你可以注册一个简单的ping工具或者通过进程信号来管理。日志与监控集成结构化的日志系统如Pino将工具调用记录、参数脱敏后、执行时间、成功/失败状态记录到日志文件或日志收集系统如Loki、ELK。这对于问题排查和用量分析至关重要。进程管理对于生产环境不建议直接用node命令运行。应该使用进程管理器如pm2或systemd来保证进程崩溃后自动重启并管理日志轮转。# 使用PM2的例子 pm2 start dist/server.js --name mcp-my-toolbox --log-date-format YYYY-MM-DD HH:mm:ss --output /var/log/mcp-server.log --error /var/log/mcp-server-error.log7. 常见问题与排查技巧实录在实际开发和集成过程中我踩过不少坑。这里把一些典型问题和解决方法记录下来希望能帮你节省时间。7.1 服务器启动失败或连接立即断开症状Claude中工具列表不显示或者显示后很快消失。查看Claude日志或服务器输出可能有连接错误。排查步骤检查路径和命令这是最常见的问题。确保Claude配置中的command和args能在终端中直接运行成功。特别是args里的JS文件路径必须是绝对路径。检查Node版本确保服务器运行的Node版本与开发环境一致并且符合mcp-maker和SDK的要求。检查端口冲突虽然Stdio传输不用端口但如果你自定义了其他传输方式如HTTP检查端口是否被占用。查看详细日志在服务器启动代码的最外层添加一个全局的uncaughtException和unhandledRejection处理器将错误打印到stderr。process.on(uncaughtException, (err) { console.error(未捕获的异常:, err); }); process.on(unhandledRejection, (reason, promise) { console.error(未处理的Promise拒绝:, reason); });7.2 工具列表不显示或显示不全症状Claude中能看到服务器但点开工具列表是空的或者缺少某些工具。排查步骤检查工具注册顺序确保所有maker.addTool的调用都在server.connect(transport)之前完成。一旦连接建立再注册工具可能不会被客户端感知到。检查Schema定义Zod Schema定义错误可能导致工具注册失败。尝试使用最简单的Schema如{}空对象测试工具是否能出现。检查函数异常工具注册函数本身如果抛出同步异常可能导致整个注册过程失败。确保注册代码是健壮的。7.3 工具调用无响应或返回错误症状能调用工具但一直等待或者返回一个模糊的错误。排查步骤工具函数内部错误在工具函数内部添加详细的try-catch并在catch块中用console.error打印错误堆栈。这些日志可能输出到Claude的某个地方或者你需要用文件日志来捕获。异步操作未完成确保工具函数返回一个Promise并且所有异步操作都正确await了。如果函数内部有未处理的Promise拒绝调用可能会挂起。返回值格式错误MCP协议对工具返回值的格式有严格要求。必须返回一个包含content数组的对象content里的每个元素要有type和text或image等属性。使用mcp-maker的maker.addTool一般会帮你处理好格式但如果你直接操作底层的McpServer就需要特别注意。7.4 性能问题工具调用缓慢症状调用一个简单的工具也需要好几秒才有响应。排查步骤分析工具逻辑是否是工具函数本身执行慢例如是否在频繁读写大文件、调用慢速的网络API考虑添加缓存或优化算法。检查I/O操作避免在工具函数中进行同步的、阻塞的I/O操作。始终使用异步APIfs.promises。冷启动延迟如果服务器是每次调用时才启动某些客户端配置可能如此那么Node应用的启动时间会成为开销。考虑使用长运行的服务器进程。7.5 与特定客户端兼容性问题症状在Claude Desktop工作正常但在其他MCP客户端如某些代码编辑器的插件中异常。排查步骤协议版本检查客户端和服务器使用的MCP SDK版本是否兼容。mcp-maker和底层SDK都在快速迭代版本差异可能导致问题。传输层差异虽然Stdio是标准方式但不同客户端的启动和环境变量设置可能略有不同。确保你的服务器对stdin/stdout的依赖是纯粹的不要假设存在tty。功能支持不是所有客户端都完全支持MCP协议的所有功能如资源、提示。如果你的服务器依赖这些高级功能在不支持的客户端上可能表现异常。可以在服务器代码中做特性检测尽管协议层面支持有限或者提供降级方案。经过这一番从原理到实践从开发到部署的折腾我深刻感受到mcp-maker这个工具包带来的效率提升。它把构建AI智能体“手和脚”的门槛降得非常低。你现在完全可以将公司内部的API、数据库查询、运维脚本甚至复杂的业务工作流封装成MCP工具让你熟悉的AI助手瞬间变成一个精通你业务的全能助手。下一步我计划探索如何将更多动态资源如实时数据仪表盘和更复杂的提示链Prompt Chaining集成进来让这个“工具箱”变得更加强大和智能。