👍 手写一个 MCP 文件读取服务:原来 AI 能"读"我的文件,全靠这个协议

写在前面:之前学了 MCP 的理论------它是 AI 界的 USB-C。今天老师带我们"手写"了一个 MCP Server!当我自己写代码,在 Claude Code 里注册一个"读文件"的工具,然后跟 Claude 说"帮我读一下桌面的 readme.md",它真的去读了文件,然后把内容告诉我------那一刻我整个人都惊了。原来 Cursor 和 Claude Code 能"看"我的文件,背后就是这样一个自定义的 MCP Server。


一、MCP Server 开发前的准备

1.1 需要用到的库

package.json

json 复制代码
{
    "type": "module",
    "dependencies": {
        "zod": "^4.4.3",
        "@modelcontextprotocol/sdk": "^1.29.0"
    }
}

两个依赖:

作用 比喻
@modelcontextprotocol/sdk MCP 官方 SDK,处理通信协议 电话基站,负责信号的收发
zod 数据验证,确保参数格式正确 门禁系统,检查来访者身份

zod 是用来做数据验证(Schema 校验)的。 比如你要接收一个 path 参数,zod 可以确保它必须是字符串,不能是数字或 null。

老师说:

"zod------数据验证 Schema。@modelcontextprotocol/sdk------协议的 SDK,通信部分。"

1.2 开发流程总览

老师说:

"CC(Claude Code)Prompt → LLM → 分析 → 选中 fs client → StdioServerTransport → stdio → Server → 返回执行 → stdout → StdioServerTransport → CC → LLM → Generate。"

整个流程是这样的:

arduino 复制代码
你问:"帮我读一下桌面的 readme.md"
  ↓
Claude Code 收到指令,分析需要调用 read-file 工具
  ↓
Claude Code 通过 MCP Client → 经过 StdioServerTransport(标准输入输出通道)
  ↓
MCP Server 收到请求,执行 fs.readFile
  ↓
读取结果通过 StdioServerTransport 传回
  ↓
Claude Code 拿到结果,LLM 生成回答
  ↓
"桌面的 readme.md 内容是......"

StdioServerTransport(标准输入输出传输) 是本地通信的通道。MCP Server 通过 stdio(标准输入输出)和 Claude Code 通信。


二、手写 MCP Server:从零到一

2.1 实例化 Server

javascript 复制代码
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: {} } }
);

new Server() 创建了一个 MCP 服务器实例:

  • name: 'simple-read-mcp':服务器名字,告诉 Claude Code "我叫 simple-read-mcp"。
  • version: '1.0.0':版本号。
  • capabilities: { tools: {} }:声明这个 MCP 提供"工具"能力。

这就像你开了一家小店,挂了个招牌------"simple-read-mcp,专治各种读文件需求"。

2.2 注册工具列表(ListToolsRequestSchema)

javascript 复制代码
// 处理 agent 请求------列出所有工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [
        {
            name: 'read-file',
            description: '读取指定路径的文本本地文件内容',
            inputSchema: {
                type: 'object',
                properties: {
                    path: {
                        type: 'string',
                        description: '文件的绝对或相对路径',
                    },
                },
                required: ['path']
            }
        }
    ]
}));

这段代码在做什么?

老师说:

"ListToolRequestSchema------MCP 事件之一,列出所有工具。"

当 Claude Code 连接到这个 MCP Server 时,它会先问:"你有什么工具?"

这个 Server 回答:"我只有一个工具------read-file,功能是读取本地文件,需要一个 path 参数。"

inputSchema 就是工具的"说明书":

  • name: 'read-file':工具的名字。
  • description: '读取指定路径的文本本地文件内容':告诉 LLM 这个工具能干什么。
  • inputSchema:告诉 LLM 调用时需要传什么参数。

2.3 执行工具(CallToolRequestSchema)

javascript 复制代码
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 (err) {
            return {
                isError: true,
                content: [
                    { type: 'text', text: `读取文件失败: ${err.message}` }
                ]
            }
        }
    }

    throw new Error(`未知工具: ${name}`);
});

这段代码才是真正的"干活"部分:

  1. 接收指令:LLM 说"我要调用 read-file 工具,参数是 path='./readme.md'"。
  2. 执行fs.readFile(args.path, 'utf-8')------真正的文件读取操作。
  3. 返回结果:把文件内容包装成 MCP 协议要求的格式返回。
  4. 错误处理 :如果文件不存在或读取失败,返回 isError: true 和错误信息。

返回的格式:

javascript 复制代码
{
    content: [
        { type: 'text', text: '文件的完整内容......' }
    ]
}

2.4 启动 Server

javascript 复制代码
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
}

main();

StdioServerTransport 是本地通信的"隧道":

  • Server 通过标准输入(stdin)监听 Claude Code 发来的指令。
  • 执行完工具后,通过标准输出(stdout)把结果传回去。

老师说:

"连接本地 transport,打通连接隧道。"

这就像你和 Claude Code 之间牵了一根电话线------它通过这根线把指令传给你,你把结果通过这根线传回去。


三、完整流程演示

当你配置好这个 MCP Server 后,在 Claude Code 里输入:

复制代码
帮我读取桌面上叫 readme.md 的文件

会发生什么?

步骤 谁在做 发生了什么
1 输入指令
2 LLM 分析指令 → 我需要调用 read-file 工具
3 MCP Client 发送 tool_call 请求给 Server
4 Stdio 标准输入通道传输数据
5 你的 Server 执行 fs.readFile('~/Desktop/readme.md')
6 Stdio 标准输出通道传回结果
7 MCP Client 把文件内容传给 LLM
8 LLM 读取内容 → 生成回答
9 看到文件内容

核心:第 5 步是你自己写的代码在执行! 不是 Claude 在读取文件,是你写的 Server 在读取文件。


四、MCP 的错误处理

看代码中的错误处理:

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

如果文件存在: 返回文件内容,LLM 帮你分析。

如果文件不存在: 返回错误信息,LLM 会告诉你"这个文件读取失败了"。


五、总结:手写 MCP = 给你的 AI 装一个"自定义器官"

知识点 说明
MCP SDK @modelcontextprotocol/sdk,处理通信协议
zod 数据验证 Schema
StdioServerTransport 标准输入输出传输通道
ListToolsRequestSchema 列出可用工具
CallToolRequestSchema 调用指定工具
new Server() 创建 MCP 服务器实例
fs.readFile 真正执行文件操作
错误处理 文件不存在或读写失败时返回 isError

手写 MCP Server 的本质:把一段 JavaScript 函数,变成 AI 能理解和调用的"工具"。

你说的每一句话,都经过了"你的代码 → Claude Code → LLM → 你的 Server → 你的代码"的完整链路。AI 看起来能"读文件",实际上是你的代码在执行,AI 只是负责"决定什么时候调用"。


写在最后

今天最大的收获,是亲手写了一个 MCP Server。以前觉得 Cursor 能"看"我的文件很神奇,现在知道了------那不是我 AI 的能力,是有人写了一个 MCP Server,把文件操作暴露给了 AI。手写 MCP 就是给 AI 装"自定义器官"------你想让 AI 能做什么,就自己写一个 MCP Server。

下次面试官问你:"MCP Server 是怎么工作的?"

你可以淡定地说:

"MCP Server 通过 @modelcontextprotocol/sdk 实现。它注册了两个核心事件处理------ListToolsRequestSchema 用于向客户端声明可用的工具列表和参数 Schema,CallToolRequestSchema 用于接收客户端发来的工具调用请求并真正执行操作。通过 StdioServerTransport 建立本地通信隧道。LLM 负责决定何时调用工具,MCP Server 负责真正执行,两者通过标准协议解耦。这种架构让 AI 可以标准化地调用外部能力。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
饮茶三千1 小时前
基于 WangEditor 的富文本编辑器定制实践
前端·vue.js·设计模式
kyle~1 小时前
前端 --- Toast 轻量级弱反馈提示组件
前端
玉宇夕落1 小时前
Vue3 + 流式输出学习
前端
努力成为AK大王1 小时前
CSS 入门完整笔记
前端·css
微笑挖矿2 小时前
大模型流式渲染:为什么"逐字显示"远比你想的复杂
前端
大流星2 小时前
LangChainJS之Chain链(四)
javascript·langchain
weedsfly2 小时前
前端开发中的装饰器模式——给函数和组件“加壳”的艺术
前端·javascript·面试
zhedream2 小时前
那个从没改过的页面,为什么突然坏了?
前端·vue.js
工业HMI实战笔记2 小时前
HMI性能优化技巧:告别卡顿,打造流畅体验
前端·ui·性能优化·自动化·交互