手写 MCP Server 全栈实战:用 Node.js + Zod 从零构建 AI 文件读取工具

手写 MCP Server 全栈实战:用 Node.js + Zod 从零构建 AI 文件读取工具

上篇文章我们学会了配置 MCP------用官方的 server-filesystem。但如果想自定义工具逻辑呢?本文从零开始,用 @modelcontextprotocol/sdk + zod 手写一个完整的 MCP Server,实现 AI 读取本地文件的能力,深入理解 MCP 协议的通信机制和工具注册流程。


前言

在上一篇文章中,我们通过配置 .mcp.json 使用了官方的 server-filesystem。但如果你的需求不只是读写文件呢?比如想让 AI 读取日志、查询数据库、调用内部 API------就需要自己写 MCP Server

本文将手把手带你:

  1. 理解 MCP Server 的通信原理(stdio 传输)
  2. @modelcontextprotocol/sdk 创建 Server 实例
  3. zod 声明参数 Schema(类型安全校验)
  4. 实现一个完整的文件读取 MCP Server
  5. 理解从 Claude Code Prompt 到 Server 执行的完整链路

一、MCP Server 的工作原理

1.1 整体通信链路

当你(通过 Claude Code)发出一个请求时,数据的流动路径如下:

c 复制代码
┌──────────────────────────────────────────────────────────────┐
│                  MCP 完整通信链路                               │
│                                                               │
│  用户 Prompt                                                  │
│      ↓                                                        │
│  Claude Code(Host)                                           │
│      ↓                                                        │
│  LLM 推理:需要读取文件 → 选择 filesystem Client                │
│      ↓                                                        │
│  StdioServerTransport(stdin/stdout)                           │
│      ↓  stdin(JSON-RPC 消息)                                  │
│  MCP Server(我们的 server.js)                                │
│      ↓  执行 read_file 工具                                   │
│  读取本地文件                                                  │
│      ↓  stdout(JSON-RPC 返回结果)                            │
│  StdioServerTransport                                         │
│      ↓                                                        │
│  Claude Code → LLM → 生成最终回答                              │
└──────────────────────────────────────────────────────────────┘

关键理解:MCP Server 是一个独立的 Node.js 进程,通过标准输入输出(stdin/stdout)与 Host 通信。Host 启动 Server 作为子进程,两者通过 JSON-RPC 消息交换数据。

1.2 Server 的三大职责

职责 说明
声明工具 用 Schema 定义工具名、参数、描述
执行逻辑 接收参数 → 执行操作 → 返回结果
协议通信 通过 stdio/SSE 与 Host 交换消息

二、项目搭建:从 package.json 开始

2.1 初始化项目

bash 复制代码
mkdir simple-read-mcp
cd simple-read-mcp
npm init -y

2.2 安装依赖

bash 复制代码
npm install @modelcontextprotocol/sdk zod

两个核心依赖

依赖 版本 作用
@modelcontextprotocol/sdk ^1.29.0 MCP 官方 SDK,封装协议通信细节
zod ^4.4.3 TypeScript 优先的运行时数据校验库

2.3 完整的 package.json

json 复制代码
{
  "name": "simple-read-mcp",
  "version": "1.0.0",
  "description": "自定义 MCP Server,为 AI 提供文件读取能力",
  "main": "server.js",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^4.4.3"
  }
}

注意"type": "module" 声明了使用 ES Modules(.mjs.js 中可用 import/export)。


三、核心代码:40 行实现一个 MCP Server

3.1 完整代码

server.js

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

// 1. 创建 MCP Server 实例
const server = new McpServer({
  name: 'simple-read-mcp',
  version: '1.0.0'
});

// 2. 注册工具:内置 zod schema 校验
server.tool(
  "read_file",                          // 工具名称
  "读取指定路径的本地文件内容",            // 工具描述(LLM 会看到)
  {
    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}` }]
      };
    }
  }
);

// 3. 启动服务(stdio 传输模式)
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP read_file 服务已启动(stdio模式)");
}

main().catch(console.error);

3.2 代码逐行解析

第一步:创建 Server 实例

javascript 复制代码
const server = new McpServer({
  name: 'simple-read-mcp',  // Server 名称(在 Host 中标识)
  version: '1.0.0'           // 版本号
});

McpServer 是 MCP SDK 提供的高层封装,内部处理了 JSON-RPC 消息的解析和序列化,让开发者只需关注工具注册和业务逻辑。

第二步:注册工具

javascript 复制代码
server.tool(
  "read_file",                            // 参数1:工具名
  "读取指定路径的本地文件内容",               // 参数2:工具描述(LLM 看到这个来决策)
  {                                        // 参数3:参数 Schema
    path: z.string().describe("文件的绝对或相对路径")
  },
  async ({ path }) => { ... }              // 参数4:执行函数
);

server.tool() 方法接收 4 个参数:

参数 说明 类比
工具名 LLM 调用时的标识符 函数名
描述 LLM 看到后决定是否调用 函数文档
Schema 参数的类型校验 函数参数类型
执行函数 接收校验后的参数,执行操作 函数体

第三步:标准返回格式

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

MCP 协议规定工具返回必须是 content 数组,每个元素有 typetext(或 image)。

第四步:错误处理

javascript 复制代码
return {
  isError: true,  // 告诉 LLM:这是一个错误结果
  content: [{ type: "text", text: `读取文件失败:${err.message}` }]
};

isError: true 让 LLM 知道操作失败了,可以选择重试或换方案。

第五步:启动 stdio 传输

javascript 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

StdioServerTransport 通过 stdin 接收 Host 的请求,通过 stdout 返回结果。注意日志要用 console.error 而非 console.log,因为 stdout 已被协议占用。


四、Zod:参数校验的安全网

4.1 为什么需要 Zod?

MCP Server 接收 LLM 传来的参数,但 LLM 的输出是概率性的,可能传入错误类型的参数。Zod 在运行时对参数进行校验,防止意外错误。

javascript 复制代码
// z.string() 确保参数是字符串
// .describe() 提供描述(MCP SDK 会将其转换为 JSON Schema 发给 LLM)
{
  path: z.string().describe("文件的绝对或相对路径")
}

4.2 Zod 的常见用法

javascript 复制代码
import { z } from "zod";

// 字符串
z.string()
z.string().min(1)           // 非空
z.string().describe("描述")

// 数字
z.number()
z.number().int().positive()  // 正整数

// 布尔
z.boolean()

// 枚举
z.enum(["read", "write", "delete"])

// 对象
z.object({
  path: z.string(),
  encoding: z.enum(["utf-8", "gbk"]).optional(),
  lineCount: z.number().int().optional()
})

// 数组
z.array(z.string())

4.3 Zod vs 手写 JSON Schema

css 复制代码
旧版 MCP SDK(手写 JSON Schema):
{
  type: "object",
  properties: {
    path: { type: "string", description: "文件路径" }
  },
  required: ["path"]
}

新版 MCP SDK + Zod:
{
  path: z.string().describe("文件路径")
}

新版 SDK 内部自动将 Zod Schema 转换为 JSON Schema,代码更简洁、类型更安全。


五、接入 Claude Code:配置 .mcp.json

5.1 配置文件

在项目根目录创建 .mcp.json

json 复制代码
{
  "mcpServers": {
    "simple-read": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/simple-read-mcp/server.js"]
    }
  }
}

关键区别 :之前用的是 npx @modelcontextprotocol/server-filesystem(官方 Server),现在用的是 node server.js我们自己写的 Server)。

5.2 参数对比

配置项 官方 Server 自定义 Server
command npx node
args ["@modelcontextprotocol/server-filesystem", "/path"] ["/path/to/server.js"]
可定制性 固定功能 完全自由

5.3 在 Claude Code 中使用

arduino 复制代码
⟩ 使用 simple-read 工具读取 server.js 的内容

Claude Code 会:

  1. 分析请求 → 选择 simple-read Client
  2. 生成调用参数 {"path": "server.js"}
  3. 通过 stdin 发送 JSON-RPC 消息给我们的 Server
  4. Server 读取文件 → 通过 stdout 返回内容
  5. LLM 基于文件内容生成回答

六、项目结构全景

perl 复制代码
simple-read-mcp/
├── package.json          # 项目配置 + 依赖声明
├── server.js             # MCP Server 核心代码(40行)
├── .mcp.json             # Claude Code 接入配置
└── node_modules/
    ├── @modelcontextprotocol/sdk/   # MCP 协议 SDK
    └── zod/                        # 参数校验库

代码量统计:整个 MCP Server 只需 40 行代码------MCP SDK 的抽象做得非常干净。


七、从简单到进阶:扩展你的 MCP Server

7.1 添加更多工具

javascript 复制代码
// 工具1:读取文件
server.tool("read_file", "读取文件", { path: z.string() }, async ({ path }) => {
  const content = await fs.readFile(path, 'utf-8');
  return { content: [{ type: "text", text: content }] };
});

// 工具2:列出目录
server.tool("list_dir", "列出目录内容", { path: z.string() }, async ({ path }) => {
  const files = await fs.readdir(path);
  return { content: [{ type: "text", text: files.join('\n') }] };
});

// 工具3:获取文件信息
server.tool("file_info", "获取文件元信息",
  { path: z.string() },
  async ({ path }) => {
    const stat = await fs.stat(path);
    return {
      content: [{ type: "text", text: JSON.stringify({
        size: stat.size,
        created: stat.birthtime,
        modified: stat.mtime,
        isFile: stat.isFile(),
        isDir: stat.isDirectory()
      }, null, 2) }]
    };
  }
);

7.2 添加资源(Resources)

MCP 不仅支持工具(Tool),还支持资源(Resource)------为 LLM 提供上下文数据:

javascript 复制代码
server.resource(
  "config",           // 资源名
  "file:///config.json",  // URI
  async (uri) => {
    const config = await fs.readFile('./config.json', 'utf-8');
    return {
      contents: [{ uri, mimeType: "application/json", text: config }]
    };
  }
);

7.3 进阶方向

方向 说明
数据库工具 pg / mysql2 实现查询工具
API 代理 封装内部 API 调用,让 AI 访问私有服务
日志分析 读取并分析应用日志,生成诊断报告
多工具组合 一个 Server 注册多个工具,提供完整能力

知识树

typescript 复制代码
手写 MCP Server 全栈实战
├── 工作原理
│   ├── 完整通信链路(Host → stdin → Server → stdout → Host)
│   └── Server 三大职责(声明工具 / 执行逻辑 / 协议通信)
├── 项目搭建
│   ├── package.json(ESM + SDK + Zod)
│   └── npm install @modelcontextprotocol/sdk zod
├── 核心代码(40行)
│   ├── McpServer 实例化
│   ├── server.tool() 注册工具
│   │   ├── 工具名 + 描述
│   │   ├── Zod Schema 参数校验
│   │   └── 异步执行函数 + 标准返回格式
│   ├── StdioServerTransport 启动
│   └── console.error(不要用 console.log)
├── Zod 参数校验
│   ├── string / number / boolean / enum
│   ├── object / array
│   └── Zod vs 手写 JSON Schema
├── 接入 Claude Code
│   ├── .mcp.json 配置(command: node)
│   └── 自然语言调用工具
├── 安全边界
│   └── 路径白名单(防止越界访问)
└── 进阶方向
    ├── 多工具注册
    ├── Resources 资源
    ├── 数据库 / API 工具
    └── 日志分析

结语

手写一个 MCP Server 只需要 40 行代码------这得益于 @modelcontextprotocol/sdk 的高层封装和 zod 的简洁语法。

理解 MCP Server 的本质后,你会发现它的架构非常优雅:

  1. McpServer 负责协议通信(你不需要了解 JSON-RPC 细节)
  2. server.tool() 负责工具注册(名称 + 描述 + Schema + 执行函数)
  3. StdioServerTransport 负责传输(stdin/stdout)
  4. zod 负责安全(运行时参数校验)

从使用官方 Server 到手写自定义 Server,你完成了从"消费者"到"创造者"的跨越。现在你可以为 AI 构建任何能力------读取数据库、调用内部 API、分析日志、甚至远程控制设备。

MCP 让每个开发者都成为 AI 能力的构建者。40 行代码,打开了一个无限可能的世界。


参考与拓展阅读:

  • @modelcontextprotocol/sdk 官方文档
  • zod v4 官方文档
  • MCP 协议规范(Model Context Protocol Specification)
  • 《MCP 协议深度解析》------ AI 界的 USB-C 实战指南
  • 《Harness Engineering 深度解析》------ MCP 是 Harness 工具层的核心

如果本文帮你理解了手写 MCP Server 的完整流程,欢迎点赞 + 收藏。有什么想扩展的 MCP Server 能力,欢迎在评论区交流讨论 👇

#MCP #Node.js #Zod #ClaudeCode #AI工程化 #掘金技术社区

相关推荐
至乐活着4 小时前
让Claude能力飞升:用MCP协议构建自定义工具实战
claude·mcp·model context protocol·自定义工具·ai扩展
HjhIron7 小时前
从 RAG 乱象到统一标准:MCP 凭什么成为 Agentic AI 的底座?
ai编程·mcp
Darling噜啦啦7 小时前
MCP 协议深度解析:AI 界的 USB-C,彻底终结工具接入的混乱时代
mcp
CV-deeplearning19 小时前
万物皆可 Markdown!开源 MCP 服务器 Markdownify,10 种格式一键转换
开源·markdown·格式转换·ai工具·mcp·markdownify
liulei3369dot1 天前
Claw Agent MCP 接入全记录:从卡死两天到手机随身调用
claude·mcp
FogLetter1 天前
远程连接MCP:当AI的“手”不再受限于本地
aigc·openai·mcp
love530love1 天前
WorkBuddy + 本地 ComfyUI Wan2.1 文生视频实战:从连续报错到成功出片的完整踩坑记录
人工智能·windows·python·音视频·devops·comfyui·mcp
玉宇夕落1 天前
mcp的学习
mcp
ServBay2 天前
不会写代码也能建站?AI 时代,非技术创始人如何从零搭建自己的 Web 项目
后端·mcp