手把手教你实现一个 MCP 文件读取服务器:从协议到代码的深度解析

手把手教你实现一个 MCP 文件读取服务器:从协议到代码的深度解析

用 Node.js 和官方 SDK 打造一个让 AI 能直接读你硬盘文件的 MCP 服务

引言:AI 与本地世界的桥梁

大语言模型(LLM)虽然拥有海量知识,但它的"眼睛"和"耳朵"却无法直接触及你的本地文件系统。当你想让 AI 帮你分析一个本地日志、处理一份 Markdown 笔记,或者读取配置文件时,常规的对话方式束手无策。这时候,MCP(Model Context Protocol) 就登场了------它是一个专为 AI 应用设计的开放协议,让 LLM 能够通过标准化的工具调用,安全地与外部世界交互。

本文我将带着你从零实现一个 手写文件处理 MCP 服务器。它只做一件事:接收一个文件路径,返回文件内容。麻雀虽小,但五脏俱全,我们将深入探讨 MCP 的核心概念、开发流程、通信机制,并梳理出清晰的架构图。读完你不仅能跑起这个服务,更能理解 MCP 的设计哲学,为后续构建更复杂的工具打下基础。


一、整体架构:从用户提问到文件读取的完整链路

我们先从一个全局视角,看看当用户对 AI 说"帮我读一下 /tmp/test.txt"时,背后发生了什么。根据 README 中的笔记,整个流程可以抽象为:

arduino 复制代码
用户 prompt → stdioServerTransport → MCP Server 分析 → 选中 fs 工具客户端 → 
StdioTransport 回传 → 大模型生成最终回复

用更通俗的语言拆解:

  1. 用户输入:用户通过某个支持 MCP 的客户端(比如 Claude Desktop、自定义 Chat UI)发送自然语言请求。
  2. 客户端与 Server 的通信 :客户端通过 stdioServerTransport(标准输入输出流)将请求传递给 MCP Server。
  3. Server 解析与工具匹配 :Server 收到请求后,根据内置的工具列表(我们注册的 read_file)判断用户意图,并提取参数(文件路径)。
  4. 工具执行 :Server 调用 read_file 的实际逻辑,通过 Node.js 的 fs/promises 读取本地文件。
  5. 结果返回 :读取到的内容(或错误信息)通过 StdioTransport 原路返回给客户端。
  6. LLM 生成回复:客户端将工具返回的结果交给大模型,模型结合上下文生成最终的自然语言回答。

这一来一回,AI 就"看到"了你的文件。接下来我们将深入实现每一个环节。


二、技术选型:三剑客让 MCP 开发如此丝滑

在动手编码前,先介绍我们依赖的三个核心库,它们各自扮演了什么角色。

1. @modelcontextprotocol/sdk ------ MCP 协议的 Node.js 实现

这是官方提供的 SDK,封装了 MCP 协议的消息格式、请求/响应处理、传输层适配等。我们不再需要手写 JSON-RPC 解析,只需要调用高阶 API 即可快速搭建 Server。

2. zod ------ 声明式数据验证

MCP 要求工具的参数以 JSON Schema 形式声明,以便大模型理解应该传什么字段。手动写 JSON Schema 繁琐且容易出错。zod 允许我们用 TypeScript 风格的声明式语法定义 schema,SDK 内部会自动将其转换为标准的 JSON Schema 并注册到协议中。

3. fs/promises ------ Node.js 原生文件操作

负责实际的文件读取,异步 API 保证非阻塞性能。

这三者组合,让我们在几分钟内就能写出一个生产可用的 MCP 工具。


三、代码实现:逐行解读 MCP 文件读取服务

现在开始写代码。我们创建一个 server.js,按以下顺序构建:

  1. 导入依赖
  2. 实例化 MCP Server
  3. 注册工具(包括 schema 定义和执行函数)
  4. 启动服务器,绑定 stdio 传输

下面逐个模块讲解。

3.1 导入模块

javascript 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from 'fs/promises';
  • McpServer:新版(较新版本)的服务器类,简化了工具注册和事件处理。
  • StdioServerTransport:基于标准输入输出的传输层实现,适合本地命令行场景。
  • z:zod 的核心对象,用于定义 schema。
  • fs/promises:返回 Promise 版本的文件 API。

3.2 实例化 MCP Server

javascript 复制代码
const server = new McpServer({
  name: 'simple-read-mcp',
  version: '1.0.0'
});

创建 Server 实例时需要提供 nameversion,这些信息会在协议握手阶段暴露给客户端,方便识别。

3.3 注册工具 read_file

这是整个服务的核心,我们通过 server.tool() 方法注册一个工具。

javascript 复制代码
server.tool(
  "read_file",                           // 工具名称,大模型通过这个名字调用
  "读取指定路径的本地文件内容",          // 描述,帮助大模型理解工具用途
  {
    path: z.string().describe("文件的绝对或相对路径")  // 参数 schema
  },
  async ({ path }) => {                  // 工具的执行函数
    // ... 实现
  }
);
细节剖析:
  • 工具名称必须具有语义性,大模型会根据用户意图匹配。
  • 描述很重要,它会被包含在系统提示中,影响模型是否选择该工具。
  • 参数 schema :我们使用 z.object 的简写形式(直接传入对象),内部每个字段都调用 .describe() 添加说明,这些说明最终会生成 JSON Schema 的 description 字段,指导大模型如何填充参数。
  • 执行函数是一个异步函数,接收一个对象(参数已根据 schema 解析并验证通过)。我们在这里实现真正的业务逻辑。

3.4 执行函数:读取文件并返回结果

javascript 复制代码
async ({ path }) => {
  try {
    const content = await fs.readFile(path, 'utf-8');
    return {
      content: [{ type: "text", text: content }]
    };
  } catch (err) {
    return {
      isError: true,
      content: [{ type: "text", text: `读取文件失败:${err.message}` }]
    };
  }
}

这里需要注意 MCP 的返回格式规范:

  • 成功时,返回一个对象,包含 content 数组,每个元素是一个 { type: "text", text: ... } 结构。这是 MCP 标准的内容块格式,可以支持多种类型(图片、嵌入等),但这里我们只用纯文本。
  • 失败时,除了 content 外,还需设置 isError: true,明确告诉客户端这是一个错误响应,大模型会据此向用户解释失败原因。

错误处理我们包裹了 try/catch,捕获任何文件读取异常(如路径不存在、无权限等),并返回友好提示。

3.5 启动服务器并绑定传输层

javascript 复制代码
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP read_file 服务已启动(stdio模式)");
}

main().catch(console.error);
  • StdioServerTransport 实例化后,会监听 process.stdinprocess.stdout,所有与客户端的通信都将通过这两个流进行。
  • server.connect(transport) 将 Server 与传输层绑定,随后 Server 开始处理入站请求。
  • 注意我们使用 console.error 输出启动日志,因为 console.log 会向 stdout 输出,而 stdout 已经被传输层占用,任何额外的输出都会破坏协议消息。因此所有调试日志都应使用 stderr

四、MCP 协议与 stdio 传输:藏在背后的通信细节

很多同学会好奇,这个服务启动后是怎么和客户端对话的?其实 MCP 协议基于 JSON-RPC 2.0 消息格式,通过 stdio 通道收发。客户端(比如 Claude Desktop)会以子进程方式启动我们的 Node.js 脚本,然后向它的 stdin 写入 JSON 请求,我们的 Server 从 stdin 读取并解析,处理完毕后将响应 JSON 写入 stdout,客户端再从 stdout 读取。

整个握手过程包括:

  1. 初始化 :客户端发送 initialize 请求,Server 返回自身能力(包括支持的工具列表)。
  2. 工具列表请求 :客户端发送 tools/list,Server 返回我们注册的 read_file 的完整 JSON Schema。
  3. 工具调用 :当用户触发调用,客户端发送 tools/call,包含工具名和参数,Server 执行并返回结果。

而我们的代码之所以如此简洁,是因为 McpServerStdioServerTransport 帮我们自动处理了所有这些 JSON-RPC 的序列化/反序列化、事件分发和错误处理。server.tool() 方法内部自动注册了 ListToolsRequestSchemaCallToolRequestSchema 的事件监听器,我们只需专注于业务逻辑。


五、运行与测试:让 AI 真正读到你的文件

5.1 启动服务

在终端执行:

bash 复制代码
node server.js

你会看到 stderr 输出启动信息。此时服务处于等待输入状态。

5.2 与客户端集成

如果你使用的是 Claude Desktop,需要在配置文件中添加该 MCP 服务(具体可参考官方文档)。如果是自定义客户端,则可以用子进程启动并模拟 JSON-RPC 请求。

5.3 测试示例

假设我们有一个 test.txt 文件内容为 "Hello MCP!",在客户端输入:

请读取当前目录下的 test.txt 文件

大模型会解析意图,构造工具调用,Server 返回文件内容,最终模型回复:

文件内容为:Hello MCP!


六、深度思考:为什么这样设计?我们能学到什么?

6.1 为什么使用 zod 而不是直接写 JSON Schema?

zod 不仅提供了简洁的声明语法,还自带类型推断能力,在 TypeScript 项目中可获得完整的类型提示。同时,它保证了参数在进入执行函数前已经通过验证,避免在业务代码里做重复的类型检查。

6.2 MCP 的标准化价值

如果没有 MCP,每个 AI 应用都需要自定义插件接口,对开发者、使用者都不友好。MCP 统一了工具的描述格式、调用方式和返回格式,让 AI 模型可以无缝适配不同服务。我们实现的这个简单文件读服务,也可以被任何支持 MCP 的客户端消费,真正实现"一次编写,到处集成"。

6.3 错误处理的思考

我们返回了 isError: true,但并未抛出异常。这是因为 MCP 协议规定工具调用应当总是返回一个有效的响应对象,即使发生错误,也要以错误内容块的形式返回,而不是直接让进程崩溃或抛出未捕获异常。这样客户端(尤其是 LLM)能够优雅地处理错误并反馈给用户。

6.4 扩展方向

当然,真正的生产级文件处理 MCP 还需要考虑:

  • 安全限制(禁止读取系统敏感文件)
  • 支持二进制文件(如返回 Base64)
  • 流式读取大文件
  • 更丰富的参数(编码、偏移量等)

但这些都只是在当前框架上添加更多逻辑,核心架构不变。


七、流程图:一次完整的请求处理路径

为了让理解更直观,我用文字描述一下完整的时序流程(配合上文提到的思路图):

scss 复制代码
[用户] -> (输入自然语言) 
  -> [客户端] -> (构造 JSON-RPC 请求)
    -> [stdin] -> [MCP Server] 
      -> 解析请求 -> 匹配工具 read_file 
        -> 执行 fs.readFile 
          <- 返回结果 (content 或 error)
      <- 封装为 JSON-RPC 响应
    <- [stdout] 
  <- [客户端] 解析响应
<- [LLM] 生成最终回答 -> 展示给用户

每个箭头都代表一次数据流动,而我们的 server.js 就是图中"MCP Server"这个方块的完整实现。


结语:从文件读取开始,迈向更广阔的 MCP 世界

通过这个只有几十行代码的项目,我们不仅掌握了一个实用工具的开发,更深刻理解了 MCP 协议的核心思想:标准化工具接口,让 AI 与外部能力解耦。未来你可以为这个 Server 添加更多工具,比如写入文件、列表目录、甚至调用系统命令,但注册方式、参数验证、错误返回都遵循着相同的模式。

MCP 的潜力远不止文件操作,数据库查询、API 调用、设备控制......一切皆可接入。希望这篇文章能为你打开一扇门,让你在自己的 AI 项目中灵活运用 MCP,真正释放大模型的行动力。

现在,不妨亲手运行一下这个服务,感受 AI 读取你硬盘文件时的奇妙瞬间吧!


如果你在实现过程中遇到任何问题,欢迎在评论区留言交流。如果觉得有用,别忘了点赞收藏,我们下期再见!

相关推荐
用户2136610035721 小时前
Vue商品详情与放大镜组件
前端·javascript
matlab代码1 小时前
基于CNN卷积神经网络手写汉字识别系统 (GUI界面)【源码38期】
人工智能·神经网络·cnn·汉字识别
半个落月1 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
用户938515635071 小时前
RAG 实战:从零搭建语义搜索系统,彻底告别关键词匹配的尴尬
javascript·人工智能
李明卫杭州1 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js
EMA1 小时前
Rag中Query改写的实践方案总结
人工智能
阿部多瑞 ABU1 小时前
论“轻小说”之异化
人工智能
墨染天姬1 小时前
【AI】opencode 使用手册
人工智能
李明卫杭州1 小时前
使用 computed 处理 v-model 复杂数据结构
前端·javascript·vue.js