
1. 项目概述与核心价值最近在折腾AI Agent的开发发现一个挺有意思的项目叫kirill-markin/example-mcp-server。这名字听起来平平无奇但如果你正在研究如何让ChatGPT、Claude这类大模型助手变得更“能干”能直接操作你电脑上的文件、查询数据库甚至控制你的智能家居那这个项目就是一个绝佳的起点。简单来说它提供了一个构建“模型上下文协议Model Context Protocol MCP”服务器的标准范例。MCP是Anthropic提出的一套开放协议你可以把它理解为AI助手和你个人工具、数据之间的“翻译官”和“接线员”。我们平时用ChatGPT的联网搜索或代码解释器功能是固定的。而MCP允许你为AI助手自定义无限的能力扩展。比如我可以通过一个MCP服务器让Claude直接读取我本地项目目录下的文件列表、查看某个文件的内容或者向我的待办事项列表添加一条任务——所有这些操作都不需要我把敏感数据上传到云端而是在本地安全地完成。kirill-markin/example-mcp-server这个仓库就用最清晰的代码展示了如何从零开始构建这样一个“翻译官”。这个项目特别适合两类朋友一是对AI应用开发感兴趣的开发者想了解如何将大模型与私有系统集成二是重度依赖AI辅助编程或写作的极客希望打造一个完全受控于自己、能力强大的私人AI工作流。它不涉及复杂的机器学习算法核心是WebSocket通信和标准化的协议实现只要有基本的Node.js或Python开发经验就能跟着玩起来。接下来我会带你彻底拆解这个示例服务器看看它怎么工作以及如何基于它打造你自己的AI“瑞士军刀”。2. MCP协议核心思想与项目架构解析2.1 为什么需要MCP从“聊天机器人”到“行动代理人”传统的AI对话模型本质是一个基于庞大文本数据训练的“超级预测器”。你问它答信息都在它的训练数据里。但现实世界是动态的我们需要AI能基于“此时此刻”的上下文行动比如“帮我总结一下昨天/work/logs目录下所有日志文件的错误信息”。没有MCP你只能手动找到文件复制粘贴内容给AI。而MCP的目标是让AI自己能去“找到”并“读取”那些文件。MCP协议的核心思想是标准化AI与工具的对话方式。它定义了一套基于JSON-RPC over WebSocket或STDIO的通信规范。在这个体系里AI客户端如Claude Desktop是发出指令的“大脑”。MCP服务器是执行具体操作的“手和脚”它暴露出一些“工具Tools”和“资源Resources”。协议本身规定了“大脑”如何告诉“手脚”要做什么调用工具以及“手脚”如何把结果资源返回给“大脑”。kirill-markin/example-mcp-server项目完美诠释了这个架构。它通常包含以下几个关键部分协议实现层处理与AI客户端的连接、消息解析JSON-RPC和分发。这部分往往使用官方SDK如modelcontextprotocol/sdkfor Node.js来简化。工具Tools注册声明服务器能提供哪些能力。每个工具都有名称、描述、输入参数模式JSON Schema。例如一个“read_file”工具参数是{“path”: “string”}。资源Resources定义声明服务器能提供哪些数据源。资源用URI标识例如file:///home/user/doc.txt。服务器可以告诉客户端“我这里有这些资源可用”客户端可以直接请求资源内容。工具实现逻辑这是你需要编写业务代码的地方。当客户端调用“read_file”工具时服务器端的处理函数被执行它根据传入的path参数读取本地文件并将内容返回。这个示例项目的价值在于它剥离了所有业务复杂性只保留了最骨架、最标准的协议交互代码。你看懂了它就掌握了为任何AI助手赋能的基本方法。2.2 项目文件结构与技术栈选择我们以最常见的Node.js版本为例很多MCP示例服务器使用Node.js因其在构建工具和网络服务方面的成熟生态。打开项目仓库你可能会看到类似如下的结构example-mcp-server/ ├── package.json ├── src/ │ ├── index.ts (或 server.js) # 服务器主入口 │ ├── tools/ # 工具实现目录 │ │ ├── fileTools.ts # 文件操作工具 │ │ └── calculatorTool.ts # 计算器工具示例 │ └── resources/ # 资源定义目录 ├── tsconfig.json (如果是TypeScript项目) └── README.mdpackage.json核心依赖通常会包括modelcontextprotocol/sdk。这是Anthropic官方提供的Node.js SDK封装了服务器和客户端的通信细节让你无需从零实现JSON-RPC和WebSocket管理。src/index.ts这是服务器的心脏。它的工作流程通常是导入SDK创建一个Server实例。定义服务器提供的“能力Capabilities”比如声明支持哪些工具、可以提供哪些资源列表。注册具体的工具处理函数Handler。启动服务器监听来自客户端的连接可能是通过STDIO或WebSocket。src/tools/这里存放着每个具体工具的代码。一个工具模块会导出一个符合Tool接口的对象包含name,description,inputSchema定义参数格式以及最重要的execute函数。execute函数是真正干活的地方它接收客户端传来的参数执行逻辑如读写文件、调用API然后返回结果或错误。TypeScript很多示例项目使用TypeScript因为MCP SDK提供了完善的类型定义能极大提升开发体验避免因参数格式错误导致的通信问题。注意虽然这里以Node.js为例但MCP是语言无关的协议。只要你遵循协议规范用Python、Go、Rust甚至Shell脚本都能编写MCP服务器。kirill-markin/example-mcp-server的核心价值在于展示了协议的通用交互模式这种模式在任何语言中都是相通的。3. 核心工具实现与协议交互细节拆解3.1 一个工具从定义到执行的完整生命周期让我们深入代码看一个最简单的工具是如何工作的。假设我们有一个“获取服务器当前时间”的工具。在src/tools/timeTool.ts中import { Tool } from modelcontextprotocol/sdk/server.js; export const getTimeTool: Tool { name: get_current_time, description: 获取服务器的当前系统时间UTC。, inputSchema: { type: object, properties: {}, // 这个工具不需要输入参数 additionalProperties: false, }, async execute(_arguments: any, _extra: any) { // 这是工具的执行逻辑 const now new Date(); return { content: [ { type: text, text: 当前服务器时间是${now.toISOString()}, }, ], }; }, };拆解说明定义Definitionname和description至关重要。AI客户端如Claude会根据这些描述来决定在什么情境下向用户建议使用这个工具。描述要清晰、具体。输入模式Input Schema使用JSON Schema定义。这里定义了一个空对象表示调用此工具时不需要任何参数。如果需要参数比如一个加法工具properties里就会定义a: { type: number }, b: { type: number }。执行Execute当AI客户端决定调用get_current_time工具时它会发送一个JSON-RPC请求如{method: tools/call, params: {name: get_current_time, arguments: {}}}。服务器收到后SDK会路由到对应工具的execute方法并执行。方法返回的结果必须遵循特定的格式通常包含一个content数组。那么这个工具是如何被客户端知晓的呢这就涉及到MCP的初始化流程。在服务器启动时它会向客户端发送一个initialize响应其中包含服务器的capabilities能力声明。在能力声明中服务器会列出所有可用的工具tools列表。客户端收到后便知道了这个服务器“手上有哪些牌”。3.2 资源Resources的声明与访问工具让AI可以“做事情”而资源让AI可以“读数据”。资源是服务器声明的一些数据URI客户端可以直接请求其内容。这在提供只读数据如系统状态、配置文件模板、知识库片段时非常有用。例如服务器可以声明一个资源file:///example-server/readme对应的URI模板可能是file:///example-server/{path}。在src/index.ts中server.setRequestHandler( McpServerMethods.listResources, async (): PromiseListResourcesResult { return { resources: [ { uri: file:///example-server/readme, name: 服务器使用说明, description: 本示例MCP服务器的简要介绍文档。, mimeType: text/plain, }, // ... 可以列出更多资源 ], }; } ); // 处理客户端对特定资源内容的请求 server.setRequestHandler( McpServerMethods.readResource, async (request: ReadResourceRequest): PromiseReadResourceResult { const { uri } request.params; if (uri file:///example-server/readme) { return { contents: [ { uri: request.params.uri, mimeType: text/plain, text: # 示例MCP服务器\n\n这是一个用于演示Model Context Protocol的示例服务器。它提供了..., }, ], }; } throw new Error(Resource not found); } );资源与工具的区别工具是动词需要客户端主动“调用”并可能产生副作用如写文件。资源是名词是客户端可以“获取”的静态或动态内容。客户端在需要某些背景信息时可以自主决定去读取相关资源无需用户显式触发。在实际项目中你可以用资源来暴露API文档、数据库Schema、常用代码片段库等极大地丰富了AI的上下文知识。3.3 错误处理与状态管理一个健壮的MCP服务器必须妥善处理错误。在工具的execute函数中对于可预见的错误如文件不存在、参数无效应该抛出或返回结构化的错误信息。async execute(arguments: { path: string }) { const { path } arguments; if (!path) { // 返回协议定义的标准错误格式 return { content: [], isError: true, error: 参数错误必须提供文件路径。 }; } try { const content await fs.promises.readFile(path, utf-8); return { content: [{ type: text, text: content }] }; } catch (error: any) { if (error.code ENOENT) { return { content: [], isError: true, error: 文件未找到${path} }; } // 其他未知错误 throw error; // 抛出错误会被SDK捕获并转换为JSON-RPC错误响应 } }状态管理是另一个需要考虑的点。MCP服务器默认是无状态的每次工具调用都是独立的。如果你需要维护会话状态例如一个需要多轮交互的复杂工具通常需要在工具内部或通过外部存储数据库、内存缓存来管理。协议本身不直接提供会话机制这需要开发者自行设计。4. 从示例到实战构建自定义MCP服务器4.1 环境搭建与项目初始化假设我们现在要从零开始基于kirill-markin/example-mcp-server的模式构建一个用于辅助软件开发的MCP服务器它提供“搜索项目代码”、“运行单元测试”、“查看Git状态”等功能。第一步初始化项目mkdir my-dev-mcp-server cd my-dev-mcp-server npm init -y npm install modelcontextprotocol/sdk npm install -D typescript types/node tsx # 初始化TypeScript配置 npx tsc --init第二步创建基础服务器文件src/server.tsimport { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; // 稍后我们会在这里导入工具 // 1. 创建服务器实例 const server new Server( { name: my-dev-mcp-server, version: 0.1.0, }, { capabilities: { // 初始声明为空后续动态添加 tools: {}, }, } ); // 2. 定义工具先占位下一节实现 const tools []; // 我们将把工具对象放在这里 // 3. 注册工具到服务器 for (const tool of tools) { server.registerTool(tool.name, tool.inputSchema, async (request) { return await tool.execute(request.params.arguments, request); }); } // 4. 处理连接错误 server.onerror (error) { console.error([MCP Server Error], error); }; // 5. 启动服务器使用STDIO传输这是Claude Desktop默认的连接方式 async function run() { const transport new StdioServerTransport(); await server.connect(transport); console.error(My Dev MCP Server running on stdio); } run().catch(console.error);关键点解析传输方式TransportStdioServerTransport意味着服务器通过标准输入输出stdio与客户端通信。这是本地集成最安全、最常用的方式数据不经过网络。对于远程或Web应用可以使用WebSocketServerTransport。能力声明在new Server时初始化的capabilities可以留空或部分填写。工具也可以在运行时动态注册但必须在客户端initialize请求完成前准备好。4.2 实现核心开发工具现在我们来实现第一个实用工具search_code用于在指定目录下递归搜索包含特定关键词的代码文件。创建src/tools/searchCodeTool.tsimport { Tool } from modelcontextprotocol/sdk/server.js; import { glob } from glob; // 需要安装 glob 包 import fs from fs/promises; import path from path; export const searchCodeTool: Tool { name: search_code, description: 在指定项目目录中递归搜索包含特定文本的代码文件。返回匹配的文件路径和上下文行。, inputSchema: { type: object, properties: { rootDir: { type: string, description: 要搜索的根目录路径。, }, pattern: { type: string, description: 要搜索的文本模式支持正则表达式。, }, fileExtensions: { type: array, items: { type: string }, description: 要限制的文件扩展名数组如 [.js, .ts, .py]。默认为空搜索所有文件。, default: [], }, maxResults: { type: number, description: 返回的最大结果数量。, default: 10, }, }, required: [rootDir, pattern], additionalProperties: false, }, async execute(args: { rootDir: string; pattern: string; fileExtensions?: string[]; maxResults?: number; }) { const { rootDir, pattern, fileExtensions [], maxResults 10 } args; // 安全检查防止目录遍历攻击 const resolvedPath path.resolve(rootDir); if (!resolvedPath.startsWith(process.cwd())) { // 简单示例限制在当前工作目录下 throw new Error(出于安全考虑只能搜索当前工作目录及其子目录下的文件。); } // 构建glob模式 let globPattern ${rootDir}/**/*; if (fileExtensions.length 0) { globPattern ${rootDir}/**/*.{${fileExtensions.join(,)}}; } const files await glob(globPattern, { nodir: true }); const results: string[] []; const regex new RegExp(pattern, i); // 简单起见不区分大小写 for (const file of files) { if (results.length maxResults) break; try { const content await fs.readFile(file, utf-8); const lines content.split(\n); const matchingLines: string[] []; for (let i 0; i lines.length; i) { if (regex.test(lines[i])) { // 提供上下文显示匹配行及其前后一行 const start Math.max(0, i - 1); const end Math.min(lines.length - 1, i 1); const context lines.slice(start, end 1).join(\n); matchingLines.push(L${start 1}-L${end 1}:\n${context}); break; // 每个文件只取第一个匹配处 } } if (matchingLines.length 0) { const relativePath path.relative(process.cwd(), file); results.push(**文件** ${relativePath}\n${matchingLines.join(\n---\n)}); } } catch (error) { // 忽略无权限读取的文件 console.error(无法读取文件 ${file}:, error); } } if (results.length 0) { return { content: [{ type: text, text: 未在目录 ${rootDir} 下找到匹配模式 ${pattern} 的代码。 }], }; } return { content: [ { type: text, text: 在 ${rootDir} 中找到 ${results.length} 个匹配文件\n\n${results.join(\n\n)}, }, ], }; }, };工具实现要点输入验证与默认值inputSchema中定义了参数的类型、描述、是否必需以及默认值。这是AI客户端理解如何调用工具的依据。安全第一工具将直接操作本地系统必须进行路径解析和访问限制防止恶意参数导致服务器读取或修改敏感系统文件。这里简单限制在了当前工作目录下。用户体验返回的结果格式清晰包含文件相对路径和代码上下文方便AI和用户理解。限制了最大结果数避免一次返回过多数据。错误容忍对无法读取的文件进行捕获和记录而不是让整个工具调用失败。按照同样的模式你可以继续实现run_tests调用npm test或pytest、git_status调用git status -s并解析结果等工具。每个工具都是一个独立的模块最后在src/server.ts中导入并注册。4.3 配置与连接AI客户端以Claude Desktop为例服务器写好了如何让Claude使用它呢这需要通过客户端的配置文件来建立连接。对于Claude Desktop找到Claude Desktop的配置文件夹。macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json编辑或创建claude_desktop_config.json文件添加你的MCP服务器配置。{ mcpServers: { my-dev-server: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/server.js // 指向你编译后的JS文件 ], env: { NODE_ENV: production } } } }保存文件并重启Claude Desktop。配置解析command: 启动服务器的命令这里是node。args: 传递给命令的参数第一个是你的服务器入口文件。必须使用绝对路径。env: 可选的环-境变量。重启后Claude Desktop会在启动时自动运行你配置的MCP服务器。你可以在Claude的聊天界面中通过输入“/”来查看和调用服务器提供的工具。例如输入/search_codeClaude会引导你填写参数然后调用你的服务器并显示结果。重要提示在开发调试阶段建议先通过命令行手动运行服务器确保没有语法错误并能正常处理请求。你可以创建一个简单的测试脚本来模拟客户端调用或者使用MCP SDK自带的测试工具。5. 高级主题性能、安全与最佳实践5.1 性能优化策略当你的MCP服务器工具越来越复杂或者需要处理大量数据时性能就成为关键考量。工具调用的异步与非阻塞确保所有工具的实现都是完全异步的使用async/await。避免在工具处理函数中执行同步的、耗时的CPU密集型操作如大型文件同步读取、复杂计算这会导致服务器阻塞无法响应其他请求。对于耗时操作考虑使用Worker线程或子进程。资源懒加载与缓存对于通过resources暴露的数据如果生成成本高应考虑缓存策略。例如一个“获取系统性能指标”的资源可以每5秒更新一次缓存而不是每次请求都实时执行ps、top等命令。结果分页与流式传输对于可能返回大量数据的工具如全文搜索实现分页机制。MCP协议本身支持在工具响应中返回isComplete标志和分页令牌但需要服务器端实现逻辑。更高级的做法是探索服务器向客户端的推送notifications实现流式结果返回。连接管理与心跳确保服务器能稳定处理客户端连接断开和重连。虽然SDK通常会处理底层连接但你的工具代码应考虑到连接可能在任何时刻中断做好资源清理如关闭临时打开的文件句柄、数据库连接。5.2 安全加固指南MCP服务器运行在本地拥有与启动用户相同的权限。一个设计不当的工具可能成为安全漏洞。输入验证与净化Sanitization这是最重要的防线。对所有来自客户端的输入参数进行严格验证。路径遍历使用path.resolve()解析路径并检查解析后的路径是否在预期的安全目录内白名单机制。绝对不要直接将用户输入的路径传递给fs.readFile。命令注入如果需要执行系统命令如调用git、npm绝对不要使用字符串拼接来构建命令。始终使用数组形式的参数并考虑使用child_process.spawn。// 危险 const command git log ${userInput}; // 如果userInput是 --prettyformat:%H rm -rf / 就完了 // 安全做法 import { spawn } from child_process; const args [log, --prettyformat:%H]; // 固定参数 if (userInput) { // 对userInput进行严格的白名单验证 if (isValidGitOption(userInput)) { args.push(userInput); } } const child spawn(git, args);权限最小化不要以高权限如root/Administrator运行MCP服务器。为它创建一个专用的、权限受限的系统用户。在服务器代码中可以主动降低进程权限Node.js中可用process.setuid。敏感信息处理工具返回的内容可能被AI用于后续对话。确保工具不会意外返回密码、API密钥、个人身份信息等敏感数据。考虑对输出进行过滤或脱敏。审计与日志记录所有工具调用的日志包括调用者、参数、时间、结果状态但注意不要记录敏感参数内容。这有助于事后审查和问题排查。5.3 开发与调试技巧使用TypeScript和严格模式MCP SDK提供了完整的TypeScript类型定义。利用它们可以在编码阶段就发现很多潜在的类型错误和协议兼容性问题。在tsconfig.json中开启strict: true。利用SDK的调试模式一些MCP SDK提供了调试日志。在开发时可以设置环境变量如DEBUGmcp*来打印详细的协议通信日志这对于理解请求/响应流程非常有帮助。模拟客户端进行单元测试为你的工具函数编写单元测试模拟输入参数验证输出是否符合协议规范。对于服务器集成测试可以编写一个简单的测试客户端使用相同的SDK连接到你的服务器并发送请求。逐步增加复杂性从一个最简单的“echo”工具开始确保基础通信正常。然后再逐步添加文件操作、命令执行等更复杂的工具。每增加一个工具都先在Claude Desktop中测试其可用性和安全性。参考官方和社区示例除了kirill-markin/example-mcp-server多研究Anthropic官方提供的其他示例服务器如文件系统服务器、SQLite服务器。社区也有很多优秀的开源MCP服务器项目可以学习它们的架构和实现。6. 常见问题与排查实录在实际开发和集成过程中你肯定会遇到各种问题。以下是我在搭建和调试MCP服务器时踩过的一些坑和解决方案。6.1 连接与配置问题问题1Claude Desktop重启后提示“无法连接到MCP服务器”或没有任何反应。排查步骤检查配置文件路径和语法确保claude_desktop_config.json文件在正确的位置并且是合法的JSON格式。一个多余的逗号就会导致整个配置失效。检查命令路径args中的服务器入口文件路径必须是绝对路径。使用相对路径如./dist/server.js在Claude Desktop的上下文中很可能无法解析。可以用pwd命令获取绝对路径。手动运行测试打开终端切换到项目目录直接运行配置文件中写的命令如node /absolute/path/to/server.js。观察控制台是否有错误输出。常见的错误包括Error: Cannot find module modelcontextprotocol/sdk/server- 依赖未安装在项目目录下运行npm install。语法错误 - 使用tsc --noEmit或node --check检查TypeScript/JavaScript语法。查看Claude Desktop日志Claude Desktop通常会有应用日志里面可能包含更详细的连接错误信息。日志位置因操作系统而异可以在网上搜索“Claude Desktop logs location”找到。问题2服务器启动了但在Claude里看不到工具列表。可能原因初始化响应超时或格式错误客户端在连接后会发送initialize请求服务器必须在短时间内正确响应。检查你的服务器代码确保server.setRequestHandler或server.registerTool在连接建立前就已完成。响应结构必须严格符合协议。工具描述过长或格式不符工具Tool对象的description和inputSchema中的description字段应简洁明了。虽然协议没有严格长度限制但过长的描述可能导致解析问题。确保inputSchema是有效的JSON Schema对象。能力声明缺失在new Server()时或是在initialize请求的处理中返回的capabilities对象里必须包含tools字段即使为空对象{}。6.2 工具调用与执行问题问题3调用工具时AI返回“工具执行错误”或超时。排查步骤查看服务器日志这是最重要的信息源。确保你的服务器将错误和异常打印到控制台console.error。超时通常意味着工具execute函数中有未处理的Promise拒绝或死循环。参数验证失败检查客户端发送的参数是否完全符合你定义的inputSchema。SDK可能会进行初步验证但复杂的自定义验证逻辑失败也会导致错误。在execute函数开头打印传入的arguments进行确认。异步操作未正确处理确保execute函数是async的并且所有异步操作都使用了await。一个未捕获的Promise拒绝会导致整个调用失败。资源权限不足如果工具涉及文件或网络操作检查运行Claude Desktop和MCP服务器的用户是否有相应的读写权限或网络访问权。问题4工具执行成功但返回的结果AI无法理解或格式错误。解决方案MCP协议要求工具返回一个特定格式的对象。最常用的格式是return { content: [{ type: text, // 目前主要支持text类型 text: 你的结果字符串在这里 }] };确保返回的是对象不是字符串。确保content是一个数组。确保数组中的每个元素都有type和text属性。text内容可以是Markdown格式AI能更好地渲染和理解。6.3 进阶调试技巧使用网络版Claude进行远程调试除了Claude Desktop你还可以配置Claude.ai网页版通过SSH隧道连接到你开发机上的MCP服务器使用WebSocket传输。这允许你在功能强大的开发环境中调试而无需频繁重启桌面应用。具体配置涉及SSH隧道和WebSocketServerTransport的使用有一定复杂度但对于复杂服务器开发非常有用。编写集成测试脚本创建一个独立的测试脚本使用StdioClientTransport或WebSocketClientTransport模拟客户端对你的服务器进行一系列自动化调用测试。这能极大提高开发效率。监控资源使用长时间运行的MCP服务器可能会有内存泄漏。使用Node.js内置的--inspect标志启动服务器然后使用Chrome DevTools或node --inspect进行内存快照和性能分析。构建一个稳定、好用的MCP服务器是一个迭代的过程。从最简单的示例出发理解协议的核心交互然后逐步添加符合自己工作流的工具。每当你手动执行一个重复性的任务时都可以思考一下“这个任务能否抽象成一个MCP工具让AI替我做” 久而久之你就会拥有一个高度定制化、无比顺手的AI增强工作环境。kirill-markin/example-mcp-server就是这个旅程的完美第一站它给了你地图和钥匙剩下的路就看你如何探索了。