
实战派从零实现一个文件读取 MCP搞懂协议、通信与 Agent 集成为什么你需要关心 MCP我们正处在 AI Agent 爆发的前夜。LLM 再强大如果它只能回答你“脑内”的知识那它就是个昂贵的百科全书。真正的 Agent 必须能调用工具、读取外部数据、执行具体操作。但问题来了每个工具都有自己的 API 格式、鉴权方式、输入输出规范。如果每个 Agent 都要为每个工具单独写适配代码那将是无尽的重复劳动。于是MCPModel Context Protocol应运而生——它定义了一套标准化的协议让 LLM 应用Client和工具提供者Server之间能够即插即用。MCP 就像 AI 世界的 USB-C 接口——统一了连接标准让工具和智能体可以自由组合。今天我们就从零开始手写一个文件读取 MCP Server让你深刻理解 MCP 的工作流程并能快速将其落地到自己的 Agent 项目中。MCP 核心三板斧在动手之前我们先快速过一遍 MCP 的基本概念理论点到即止实战才是王道。MCP 基于JSON-RPC 2.0通信定义了三种核心能力Tools工具Server 提供给 Client 可调用的函数类似 OpenAPI 的接口。Resources资源Server 暴露给 Client 的数据内容类似文件或数据库查询结果。Prompts提示词模板Server 提供的预设 Prompt方便 Client 快速构造对话。我们的文件读取 Server 只需要用到Tools——提供一个read_file工具让 LLM 可以调用它来读取本地文件内容。实战从零搭建文件读取 MCP Server1. 初始化项目mkdir simple-read-mcp cd simple-read-mcp npm init -y npm install modelcontextprotocol/sdk npm install -D types/node typescript2. 编写 Server 骨架直接上完整代码关键点已注释你甚至可以复制粘贴直接跑起来。// server.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema } from modelcontextprotocol/sdk/types.js; import fs from fs/promises; // 1. 创建 Server 实例定义名称和版本 const server new Server( { name: simple-read-mcp, version: 1.0.0 }, { capabilities: { tools: {} } } // 声明我们支持 tools 能力 ); // 2. 处理工具列表请求Client 启动时会调用 server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: read_file, description: 读取指定路径的本地文件内容, inputSchema: { type: object, properties: { path: { type: string, description: 文件的绝对路径或相对路径 } }, required: [path] } } ] })); // 3. 处理工具调用请求LLM 决定调用工具时触发 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name read_file) { try { const content await fs.readFile(args.path, utf-8); // 返回内容作为上下文给 LLM return { content: [ { type: text, text: content } ] }; } catch (error) { return { isError: true, content: [ { type: text, text: 读取文件失败: ${error.message} } ] }; } } throw new Error(未知工具: ${name}); }); // 4. 启动 Server通过标准输入输出stdio与 Client 通信 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Server started and waiting for requests...); } main(); 代码逐段深度解析光看代码还不够我们要把每一块的“为什么”讲透。下面我们把server.ts拆成四个核心部分逐一剖析。第一块导入与实例化import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema } from modelcontextprotocol/sdk/types.js; import fs from fs/promises; const server new Server( { name: simple-read-mcp, version: 1.0.0 }, { capabilities: { tools: {} } } );Server类这是 SDK 提供的服务端骨架封装了 JSON‑RPC 消息的解析、路由和生命周期管理。你不需要自己处理底层协议细节只需注册对应的处理器。第一个参数serverInfo包含服务名称和版本会暴露给 Client 用于标识和日志记录。第二个参数serverOptionscapabilities用于声明该 Server 支持哪些 MCP 能力。这里我们只写了tools: {}表示支持工具调用能力工具列表后续通过 Handler 动态提供。如果将来还需要提供 Resources 或 Prompts也要在这里声明例如resources: {}。StdioServerTransport这是通信传输层的实现通过标准输入输出stdin/stdout与父进程Client交换 JSON‑RPC 消息。它是当前最通用的方式适合与 Claude Desktop、命令行 Agent 等集成。你也可以选择SSEServerTransport用于 HTTP/SSE 场景但 stdio 最简单直接。fs/promises使用 Promise 版本的 fs API方便在 async 函数中await。第二块注册工具列表处理器ListToolsserver.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: read_file, description: 读取指定路径的本地文件内容, inputSchema: { type: object, properties: { path: { type: string, description: 文件的绝对路径或相对路径 } }, required: [path] } } ] }));setRequestHandlerServer 注册请求处理器的方法。第一个参数是RequestSchema这里使用ListToolsRequestSchema它对应 MCP 协议中的tools/list请求——即 Client 询问“你有哪些可用工具”时触发。返回值必须是一个包含tools数组的对象。每个工具对象包含name工具的唯一标识符LLM 在调用时会引用它。description描述工具的用途LLM 会根据这段文字判断何时调用该工具所以一定要写清楚最好包含使用场景示例。inputSchema工具参数的 JSON Schema遵循 JSON Schema Draft‑07。LLM 会依据此 Schema 生成符合格式的参数对象。这里我们定义path为必填的字符串。注意这个处理器仅在 Client 初次连接或主动刷新工具列表时调用不会在每次工具调用时重复执行。返回的工具列表会被 Client 缓存并传递给 LLM 作为可用工具的上下文。第三块注册工具调用处理器CallToolserver.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name read_file) { try { const content await fs.readFile(args.path, utf-8); return { content: [ { type: text, text: content } ] }; } catch (error) { return { isError: true, content: [ { type: text, text: 读取文件失败: ${error.message} } ] }; } } throw new Error(未知工具: ${name}); });CallToolRequestSchema对应协议中的tools/call请求当 LLM 决定调用某个工具时Client 会向 Server 发送此请求。解构参数request.params中包含name工具名和arguments调用参数。注意这里使用了arguments: args的重命名避免与 JavaScript 关键字冲突。工具路由我们通过if (name read_file)来判断要执行哪个工具实际项目中建议用 Map 或 switch 做路由。执行与返回成功时返回一个content数组其中包含type: text的文本内容。这个内容会成为 LLM 的上下文LLM 会根据它来生成最终回答。失败时返回isError: true并附带错误信息。这样 Client 可以明确知道工具执行失败LLM 也会收到错误提示并告知用户。重要如果收到未知工具名应该抛出一个错误让 Server 自动返回 JSON‑RPC 错误响应而不是默默忽略。第四块启动 Server 与 stdio 传输async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Server started and waiting for requests...); } main();StdioServerTransport实例化后会监听进程的stdin并将输出写入stdout。所有 JSON‑RPC 消息都通过这个管道传输。server.connect(transport)将 Server 与传输层绑定启动消息循环。此时 Server 开始接收并处理来自 Client 的请求。console.error这里使用stderr输出日志因为stdout被用于协议通信如果往stdout打日志会干扰协议消息。所有调试信息都应该输出到stderr这也是 MCP 官方推荐的做法。通信流程拆解从用户提问到拿到文件内容很多同学会好奇这个 MCP 到底是怎么和 LLM 配合的笔记里画了个流程图我用文字再给你讲透用户提问比如“请帮我读取/home/user/data.txt的内容”。LLM 分析LLM 意识到需要调用工具它会从 Server 提供的tools列表中找到read_file。Client 发起调用Client比如 Claude Desktop、自定义 Agent通过StdioServerTransport将调用请求写入子进程的stdin。Server 处理我们的 Server 监听到stdin的 JSON-RPC 请求解析后执行fs.readFile结果返回至stdout。Client 接收Client 读取stdout将结果文件内容拼接回 LLM 的上下文。LLM 生成最终答案LLM 根据文件内容给出用户想要的回答。金句MCP 的 stdio 传输就像一根无形的管道把 LLM 的“意图”和操作系统的“文件系统”连接了起来。细节打磨让 Server 更健壮、更安全1. 参数验证引入 Zod虽然示例中直接使用了args.path但生产环境下我们必须校验参数类型和存在性。modelcontextprotocol/sdk并不强制验证我们可以引入zod来增强npm install zod然后在处理函数中加入验证import { z } from zod; const ReadFileSchema z.object({ path: z.string().min(1, 路径不能为空) }); // 在 CallTool 中 try { const { path } ReadFileSchema.parse(args); const content await fs.readFile(path, utf-8); // ... } catch (error) { // 参数验证失败或文件读取失败 return { isError: true, content: [...] }; }2. 错误处理的粒度我们已经捕获了readFile的错误但还可以区分“文件不存在”、“权限不足”、“路径非法”等情况返回更友好的错误信息帮助 LLM 更好地向用户解释。3. 路径安全限制直接允许任意路径存在安全隐患。你可以在生产环境中增加白名单目录限制防止读取敏感系统文件。如何验证你的 MCP 是否能正常工作官方提供了mcp-inspector工具可以交互式测试你的 Servernpx modelcontextprotocol/inspector node dist/server.js它会启动一个 Web 界面让你可以手动列出工具、调用工具并查看返回结果是调试的利器。扩展思考如何将这个 MCP 集成到你的 Agent在 Trae 中配置找到 Trae 的mcp配置文件通常在设置里找在mcp中加入我们的配置json{ mcpServers: { simple-read-mcp: { command: node, args: [ E:\workspace\lgl_ai\ai\mcp\simple-read-mcp\server.js ] } } }保存后LLM 就能直接调用read_file读取你本地的文件了。注意路径要替换为你实际编译后的server.js绝对路径如果你用 TypeScript需先编译成 JS。你会发现一旦你掌握 MCP 的套路为你的 Agent 增加新能力就像“插拔 U 盘”一样简单。你会发现一旦你掌握 MCP 的套路为你的 Agent 增加新能力就像“插拔 U 盘”一样简单。总结与展望今天我们手写了一个文件读取 MCP Server收获了MCP 的核心概念Tools、Resources、PromptsServer 的编写流程创建实例、注册工具列表、处理调用、启动 stdio 传输通信机制stdin/stdout 管道JSON-RPC 协议增强安全性参数验证与错误处理MCP 的生态正在飞速发展除了文件读取你还可以写数据库查询 MCP、API 调用 MCP、代码执行 MCP……只要你的想象力够丰富LLM 的能力边界就能无限扩展。最后一句不会写 MCP 的 Agent 工程师不是合格的掘金作者。赶紧动手试试吧把本地文件变成 LLM 的“外挂大脑”