news 2026/7/6 13:25:41

用 100 行代码手搓一个 MCP Server,让 LLM 直接读你本地文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 100 行代码手搓一个 MCP Server,让 LLM 直接读你本地文件

实战派:从零实现一个文件读取 MCP,搞懂协议、通信与 Agent 集成


为什么你需要关心 MCP?

我们正处在 AI Agent 爆发的前夜。LLM 再强大,如果它只能回答你“脑内”的知识,那它就是个昂贵的百科全书。真正的 Agent 必须能调用工具读取外部数据执行具体操作

但问题来了:每个工具都有自己的 API 格式、鉴权方式、输入输出规范。如果每个 Agent 都要为每个工具单独写适配代码,那将是无尽的重复劳动。于是,MCP(Model Context Protocol)应运而生——它定义了一套标准化的协议,让 LLM 应用(Client)和工具提供者(Server)之间能够即插即用

MCP 就像 AI 世界的 USB-C 接口——统一了连接标准,让工具和智能体可以自由组合。

今天,我们就从零开始,手写一个文件读取 MCP Server,让你深刻理解 MCP 的工作流程,并能快速将其落地到自己的 Agent 项目中。


MCP 核心三板斧

在动手之前,我们先快速过一遍 MCP 的基本概念(理论点到即止,实战才是王道)。

MCP 基于JSON-RPC 2.0通信,定义了三种核心能力:

  1. Tools(工具):Server 提供给 Client 可调用的函数,类似 OpenAPI 的接口。
  2. Resources(资源):Server 暴露给 Client 的数据内容,类似文件或数据库查询结果。
  3. Prompts(提示词模板):Server 提供的预设 Prompt,方便 Client 快速构造对话。

我们的文件读取 Server 只需要用到Tools——提供一个read_file工具,让 LLM 可以调用它来读取本地文件内容。


实战:从零搭建文件读取 MCP Server

1. 初始化项目

mkdir simple-read-mcp cd simple-read-mcp npm init -y npm install @modelcontextprotocol/sdk npm install -D @types/node typescript

2. 编写 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

第二块:注册工具列表处理器(ListTools

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'read_file', description: '读取指定路径的本地文件内容', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '文件的绝对路径或相对路径' } }, required: ['path'] } } ] }));
  • setRequestHandler:Server 注册请求处理器的方法。第一个参数是RequestSchema,这里使用ListToolsRequestSchema,它对应 MCP 协议中的tools/list请求——即 Client 询问“你有哪些可用工具”时触发。

  • 返回值:必须是一个包含tools数组的对象。每个工具对象包含:

    • name:工具的唯一标识符,LLM 在调用时会引用它。
    • description:描述工具的用途,LLM 会根据这段文字判断何时调用该工具,所以一定要写清楚(最好包含使用场景示例)。
    • inputSchema:工具参数的 JSON Schema(遵循 JSON Schema Draft‑07)。LLM 会依据此 Schema 生成符合格式的参数对象。这里我们定义path为必填的字符串。
  • 注意:这个处理器仅在 Client 初次连接或主动刷新工具列表时调用,不会在每次工具调用时重复执行。返回的工具列表会被 Client 缓存,并传递给 LLM 作为可用工具的上下文。


第三块:注册工具调用处理器(CallTool

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'); 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 配合的?笔记里画了个流程图,我用文字再给你讲透:

  1. 用户提问:比如“请帮我读取/home/user/data.txt的内容”。
  2. LLM 分析:LLM 意识到需要调用工具,它会从 Server 提供的tools列表中找到read_file
  3. Client 发起调用:Client(比如 Claude Desktop、自定义 Agent)通过StdioServerTransport将调用请求写入子进程的stdin
  4. Server 处理:我们的 Server 监听到stdin的 JSON-RPC 请求,解析后执行fs.readFile,结果返回至stdout
  5. Client 接收:Client 读取stdout,将结果(文件内容)拼接回 LLM 的上下文。
  6. 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工具,可以交互式测试你的 Server:

npx @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、Prompts
  • Server 的编写流程:创建实例、注册工具列表、处理调用、启动 stdio 传输
  • 通信机制:stdin/stdout 管道,JSON-RPC 协议
  • 增强安全性:参数验证与错误处理

MCP 的生态正在飞速发展,除了文件读取,你还可以写数据库查询 MCPAPI 调用 MCP代码执行 MCP……只要你的想象力够丰富,LLM 的能力边界就能无限扩展。

最后一句:不会写 MCP 的 Agent 工程师,不是合格的掘金作者。赶紧动手试试吧,把本地文件变成 LLM 的“外挂大脑”!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 13:20:39

小米 NAS 火了:它抢的不是群晖,是你家的数据入口

我看到小米这台 Xiaomi 智能存储 的时候,第一反应不是:“小米终于也做 NAS 了。”我第一反应是:NAS 这个东西,可能真的要从极客圈走进普通家庭了。这两个判断不一样。如果只是 “小米做 NAS”,那就是一个数码新品。大家…

作者头像 李华
网站建设 2026/7/6 13:19:48

《从 300 行到 30 万行代码,我学到的 5 个重构教训》

一、背景 三年前,我加入了一家创业公司,接手了一个「看起来还不错」的项目。 代码量:300 行。 功能:能用。 架构:没有。 反正能跑就行 —— 前同事的原话。 三年后,这个项目变成了 30 万行,团队…

作者头像 李华
网站建设 2026/7/6 13:19:19

大模型工具调用安全:从开放 API 到可控 Agent 的权限治理

大模型工具调用(Tool Use / Function Calling)让 LLM 从“会说话”进化为“能做事”。Agent 可以调用搜索引擎、数据库、代码执行器、邮件系统,甚至直接操作企业 API。但能力越强,风险越大。一个不加以约束的 Agent 可能会泄露敏感…

作者头像 李华
网站建设 2026/7/6 13:19:19

MediaCrawler技术解析:免逆向设计的多平台数据采集实战指南

MediaCrawler技术解析:免逆向设计的多平台数据采集实战指南 【免费下载链接】MediaCrawler-new 项目地址: https://gitcode.com/GitHub_Trending/me/MediaCrawler-new 面对新媒体平台日益复杂的反爬机制和加密算法,传统爬虫技术往往陷入逆向工程…

作者头像 李华