从零搭建文件读取 MCP 服务——让 AI 拥有读文件的能力

1. 开篇:AI 为什么需要读文件?

想象一个场景:你正在用 AI 写代码,想让它帮你重构一个项目。你把代码片段粘贴过去,AI 说"这个函数看起来有点问题,但我需要看一下完整的上下文"。你又粘贴了更多代码,AI 又说"我还需要看一下相关的配置文件"。来来回回粘贴了十几轮,你累得不行,AI 也始终没法获得完整的项目视角。

这不就是"盲人摸象"吗?AI 就像那个摸象的盲人,每次只能摸到一小块,永远没法看到全貌。

有没有一种办法,让 AI 能自己"走过去"看看整个项目?就像给它一双眼睛,让它能自己去读取文件?

答案是:MCP(Model Context Protocol)


2. MCP 是什么?------ 让 AI 突破认知边界

在讲 MCP 之前,先讲一个小故事。

你小时候有没有玩过"传话游戏"?第一个人说一句话,传给第二个人,第二个人再传给第三个人......最后那句话可能已经面目全非。AI 和外部世界的交互,本质上也是一个传话游戏------用户把信息传给 AI,AI 再通过用户去执行操作。

MCP 就是要打破这个传话链。它让 AI 可以直接和工具对话,跳过中间的"传话人"。

MCP 的核心思想

MCP 是一个协议,它定义了 AI 如何调用外部工具。简单来说,它解决了三个问题:

  1. "我能做什么?" ------ 工具向 AI 声明自己的能力(工具名称、描述、参数)
  2. "怎么调用我?" ------ AI 通过标准协议发送调用请求
  3. "结果是什么?" ------ 工具返回结构化的执行结果

MCP 架构概览

arduino 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                         AI / LLM                                │
│  ┌─────────────┐                                                │
│  │   思考模块   │ ← 决定是否调用工具                              │
│  └──────┬──────┘                                                │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                                │
│  │   工具调用   │ ← 生成工具调用请求                              │
│  └──────┬──────┘                                                │
│         │                                                       │
│         ▼                                                       │
├────────┼────────────────────────────────────────────────────────┤
│        │ CC (Client Coordinator)                                │
│        │ ┌──────────────────────────────────────┐               │
│        │ │ 工具发现 → 参数校验 → 协议转换 → 分发  │               │
│        │ └──────────────────────────────────────┘               │
│        │                                                        │
├────────┼────────────────────────────────────────────────────────┤
│        │                                                        │
│        ▼                                                        │
│  ┌─────────────────┐     ┌─────────────────┐     ┌───────────┐  │
│  │  MCP Server 1   │     │  MCP Server 2   │     │    ...    │  │
│  │  (文件读取)      │     │  (网络搜索)      │     │           │  │
│  └────────┬────────┘     └────────┬────────┘     └─────┬─────┘  │
│           │                       │                    │         │
│           ▼                       ▼                    ▼         │
│     ┌─────────┐            ┌─────────┐           ┌─────────┐    │
│     │  文件系统│            │   网络   │           │   其他   │    │
│     └─────────┘            └─────────┘           └─────────┘    │
└─────────────────────────────────────────────────────────────────┘

这个架构的精妙之处在于 解耦

  • AI 不需要知道工具怎么实现 ------ 它只需要知道"有什么工具可用"和"怎么调用"
  • 工具不需要知道 AI 是谁 ------ 它只需要遵循 MCP 协议,就能被任何兼容的 AI 调用
  • CC 作为中间层 ------ 负责协调和路由,让整个系统可扩展

本项目的定位

我们今天要拆解的 simple-read-mcp,就是这个架构中的一个"螺丝钉"------一个负责读取本地文件的 MCP Server。它虽小,但五脏俱全,是理解 MCP 协议的绝佳切入点。


3. 项目骨架解析:package.json 和 mcp.json

在看核心代码之前,先看看项目的基础设施。一个 Node.js 项目就像一栋房子,package.json 是房产证,mcp.json 是门牌。

package.json ------ 项目的身份证

json 复制代码
{
  "name": "simple-read-mcp",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^4.4.3",
    "zod-to-json-schema": "^3.25.2"
  }
}

这里有三个关键依赖:

  1. @modelcontextprotocol/sdk ------ MCP 协议的官方 SDK,提供了 McpServerStdioServerTransport
  2. zod ------ 新一代数据验证库,用来定义工具参数的校验规则
  3. zod-to-json-schema ------ 把 zod schema 自动转换成 JSON Schema,供 AI 理解参数结构

注意 "type": "module" 这一行,它告诉 Node.js 使用 ESM 模块系统,所以我们能使用 import 语法。

mcp.json ------ 给客户端的"使用说明书"

json 复制代码
{
    "mcpServers":{
        "simple-read-mcp":{
            "version":"1.0.0",
            "description":"一个简单的读取文件的工具",
            "command":"node",
            "args":["C:\\Users\\DMYX\\Desktop\\workspace\\lesson_zp\\ai\\mcp\\simple-read-mcp\\server.js"]
        }
    }
}

这个文件是给 CC(Client Coordinator) 看的,它告诉 CC:

  • 服务名称:simple-read-mcp
  • 版本:1.0.0
  • 描述:一个简单的读取文件的工具(AI 通过这个描述来理解工具用途)
  • 启动方式:用 node 命令执行 server.js

为什么需要这个文件?

想象一下,你电脑上可能装了很多 MCP 服务------文件读取、网络搜索、数据库查询......CC 需要知道它们在哪里、怎么启动、能做什么。mcp.json 就是这个"服务注册表"。


4. 核心实现:server.js 逐行拆解

终于到了最核心的部分。这 40 行代码,浓缩了一个完整 MCP 服务的所有精华。让我们逐行剖析。

4.1 导入依赖(第 1-4 行)

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';

这四个 import 各司其职:

模块 职责
McpServer MCP 服务的核心类,负责注册工具和处理请求
StdioServerTransport 标准输入输出传输层,负责进程间通信
z zod 验证库,定义参数校验规则
fs/promises Node.js 文件系统的 Promise API,异步读取文件

为什么选 Stdio?

进程间通信有很多方式:网络 socket、命名管道、共享内存......为什么选最简单的标准输入输出?

  • 零配置:不需要端口号、不需要 IP 地址
  • 跨平台:Windows 和 Unix 都支持
  • 安全:不会暴露在网络上,只有父进程能访问

4.2 创建服务器实例(第 7-10 行)

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

这一步很简单,但背后有讲究:

  • nameversion 必须和 mcp.json 中的定义一致,否则 CC 无法匹配
  • 这是服务的"身份标识",AI 通过这些信息来识别和选择工具

4.3 注册工具(第 13-32 行)------ 核心中的核心

javascript 复制代码
server.tool(
  "read_file",
  "读取指定路径的本地文件内容",
  {
    path: z.string().describe("文件的绝对或相对路径")
  },
  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}` }]
      };
    }
  }
);

这是整个文件最关键的部分,server.tool() 方法接收四个参数:

参数一:工具名称("read_file"

这是工具的唯一标识符,AI 通过这个名称来调用工具。命名建议使用下划线分隔的小写字母,语义清晰。

参数二:工具描述("读取指定路径的本地文件内容"

这是给 AI 看的"使用说明",AI 根据这个描述来判断什么时候该调用这个工具。描述的质量直接影响 AI 的调用决策,写得越好,AI 越知道什么时候该用。

参数三:参数 Schema({ path: z.string().describe(...) }

这是新版 SDK 的一大亮点------直接用 zod schema 定义参数,不需要手动写 JSON Schema。

javascript 复制代码
{
  path: z.string().describe("文件的绝对或相对路径")
}

z.string() 表示 path 必须是字符串类型。.describe() 方法给参数添加描述,AI 通过这个描述来理解参数的含义。

zod 是怎么工作的?

当 AI 调用工具时,传入的参数会经过 zod 的校验:

  • 如果 path 不是字符串,会自动返回错误
  • 如果缺少 path 参数,会自动返回错误
  • 校验通过后,参数才会传递给执行函数

参数四:执行函数(async ({ path }) => { ... }

这是工具的实际执行逻辑,接收校验后的参数,返回执行结果。

执行流程解析:

scss 复制代码
AI 请求 → CC 校验参数 → server.tool() 接收 → 执行函数调用
              │                                    │
              ▼                                    ▼
        zod 验证失败                        fs.readFile()
              │                                    │
              ▼                                    ▼
        返回错误信息                         返回文件内容
                                                │
                                                ▼
                                          CC 返回给 AI

返回值格式:

成功时返回:

javascript 复制代码
{
  content: [{ type: "text", text: content }]
}

失败时返回:

javascript 复制代码
{
  isError: true,
  content: [{ type: "text", text: `读取文件失败:${err.message}` }]
}

为什么用 content: [{ type: "text", text: ... }] 这种格式?

这是 MCP 协议定义的统一返回格式,支持多种内容类型:

类型 用途 示例
"text" 文本内容 文件内容、错误信息
"image" 图片内容 图片的 base64 编码
"file" 文件引用 文件路径和元信息

这种设计让工具可以返回多种类型的结果,AI 可以根据类型进行不同的处理。

4.4 启动服务(第 34-40 行)

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

main().catch(console.error);

这部分代码启动服务并等待请求:

  1. 创建 StdioServerTransport 实例 ------ 建立标准输入输出通道
  2. server.connect(transport) ------ 让服务器绑定到这个通道上
  3. console.error 输出启动信息 ------ 为什么不用 console.log 因为 console.log 的输出会被 CC 当作协议数据读取,所以日志必须写到 stderr

完整数据流图:

lua 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         用户提问                                     │
│                         "帮我读一下 package.json"                    │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                           AI / LLM                                  │
│  分析问题 → 决定调用 read_file 工具 → 生成调用请求                     │
│  { name: "read_file", arguments: { path: "package.json" } }         │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                             CC                                       │
│  读取 mcp.json → 找到 simple-read-mcp → 启动进程 → 发送请求到 stdin   │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       simple-read-mcp                                │
│  StdioServerTransport 接收请求 → McpServer 处理                      │
│  → 执行 read_file 函数 → fs.readFile("package.json")                │
│  → 返回结果到 stdout                                                │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                             CC                                       │
│  接收 stdout 输出 → 解析结果 → 返回给 AI                              │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                           AI / LLM                                  │
│  接收工具返回结果 → 整合到回答中 → 返回给用户                          │
└─────────────────────────────────────────────────────────────────────┘

5. 安全性思考:一个危险的玩具

现在我们已经拆解完代码了,但是有一个问题必须严肃讨论:这个工具安全吗?

当前实现的安全隐患

让我们仔细看看这个工具的实现:

javascript 复制代码
async ({ path }) => {
  const content = await fs.readFile(path, 'utf-8');
  return { content: [{ type: "text", text: content }] };
};

这里没有任何路径检查,用户(或者说 AI)可以读取系统上的任意文件:

  • read_file("/etc/passwd") ------ 读取系统用户信息
  • read_file("C:\\Windows\\System32\\config\\SAM") ------ 读取 Windows 密码哈希
  • read_file("~/.ssh/id_rsa") ------ 读取 SSH 私钥

这是一个巨大的安全漏洞! 如果这个工具被恶意使用,后果不堪设想。

如何修复?

一个生产级的文件读取工具应该至少包含以下安全措施:

方案一:路径白名单

只允许读取特定目录下的文件:

javascript 复制代码
const ALLOWED_DIRS = [
  process.cwd(),
  '/path/to/project'
];

async ({ path }) => {
  const resolvedPath = path.resolve(path);
  const isAllowed = ALLOWED_DIRS.some(dir => 
    resolvedPath.startsWith(path.resolve(dir))
  );
  
  if (!isAllowed) {
    return {
      isError: true,
      content: [{ type: "text", text: `访问被拒绝:${path}` }]
    };
  }
  
  // ... 后续逻辑
};

方案二:文件大小限制

防止读取超大文件导致内存溢出:

javascript 复制代码
const MAX_FILE_SIZE = 1024 * 1024; // 1MB

async ({ path }) => {
  const stat = await fs.stat(path);
  if (stat.size > MAX_FILE_SIZE) {
    return {
      isError: true,
      content: [{ type: "text", text: `文件过大:${stat.size} bytes` }]
    };
  }
  
  // ... 后续逻辑
};

方案三:文件类型限制

只允许读取特定类型的文件:

javascript 复制代码
const ALLOWED_EXTENSIONS = ['.txt', '.md', '.json', '.js', '.ts'];

async ({ path }) => {
  const ext = path.extname(path);
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
    return {
      isError: true,
      content: [{ type: "text", text: `不支持的文件类型:${ext}` }]
    };
  }
  
  // ... 后续逻辑
};

为什么这个项目没有安全措施?

这个项目是一个教学示例,它的目的是展示 MCP 的核心概念,而不是提供一个生产级的工具。在实际使用中,你必须根据自己的需求添加安全防护。


6. 进阶方向:从玩具到生产

如果你想把这个简单的示例变成一个真正可用的工具,可以考虑以下改进方向:

6.1 添加更多工具

一个文件读取服务不应该只有 read_file

  • list_files ------ 列出目录内容
  • write_file ------ 写入文件(需要更严格的安全控制)
  • read_binary_file ------ 读取二进制文件
  • get_file_info ------ 获取文件元信息

6.2 添加流式支持

对于大文件,可以返回流式结果,而不是一次性读取整个文件:

javascript 复制代码
async ({ path }) => {
  const stream = fs.createReadStream(path);
  return {
    content: [{ type: "stream", stream }]
  };
};

6.3 添加缓存机制

对于频繁读取的文件,可以添加缓存,减少磁盘 I/O:

javascript 复制代码
const cache = new Map();
const CACHE_TTL = 60 * 1000; // 1分钟

async ({ path }) => {
  const now = Date.now();
  const cached = cache.get(path);
  
  if (cached && now - cached.timestamp < CACHE_TTL) {
    return { content: [{ type: "text", text: cached.content }] };
  }
  
  const content = await fs.readFile(path, 'utf-8');
  cache.set(path, { content, timestamp: now });
  
  return { content: [{ type: "text", text: content }] };
};

结尾:AI 的第三只手

回到开头那个"盲人摸象"的故事。MCP 就像是给盲人装上了第三只手------不仅能摸,还能"拿"、能"看"、能"操作"。

今天我们用 40 行代码搭建了一个简单的文件读取工具,这只是 MCP 能力的冰山一角。想象一下:

  • 一个能查询数据库的 MCP 服务
  • 一个能调用 API 的 MCP 服务
  • 一个能控制智能家居的 MCP 服务
  • 一个能操作 Excel 的 MCP 服务

这些服务组合在一起,就能让 AI 拥有无限的能力。唯一的限制就是你的想象力。

最后,留给你一个问题:如果你要为 AI 打造一个"超级工具箱",你会添加哪些工具?为什么?

相关推荐
西瓜橙1 小时前
别再让 AI 一把梭:我用 Loop Engineering 把 AI编程 拽进可验证闭环
人工智能
艾莉丝努力练剑1 小时前
OpenCode AI 编程:Ubuntu 24.04 环境安装与使用指南
linux·服务器·网络·人工智能·tcp/ip·ubuntu
华山令狐虫2 小时前
DBAPI AI 写 SQL:支持动态 SQL 与参数占位符,自然语言一键生成
数据库·人工智能·sql·dbapi
一次旅行2 小时前
DeepSeek-V4 原厂直供模型即将登陆腾讯云!峰谷定价详解 + 实战调用指南
人工智能·腾讯云·ai编程
阿里云大数据AI技术2 小时前
DataWorks Data Agent的演进与工程化实践
人工智能·agent
HERR_QQ2 小时前
强化学习的数学原理 学习笔记
人工智能·笔记·学习·自动驾驶
IvorySQL2 小时前
PG 技术日报|2026-07-04
数据库·人工智能·postgresql·开源
程序员cxuan2 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员
shushangyun_2 小时前
2026年AI知识库系统选型指南
人工智能