MCP 入门实战:写一个能读本地文件的极简服务

前几天折腾 AI IDE 的时候,一直有个特别烦人的痛点:大模型只能跟你聊代码逻辑,没法直接读我本地的项目文件。每次想让它帮我看个配置、改个脚本,都得手动复制一大段内容粘贴进去,文件长了特别折腾。

直到我看到有人提 MCP,说能让大模型直接调用本地工具。我寻思不就是读个文件嘛,应该不难,索性自己动手写个最简单的文件读取 MCP 服务。结果真上手才发现,坑全在细节里,折腾了小半天才跑通。今天顺着我当时的思路捋一遍,省得后面有人跟我一样走弯路。

先搞懂:MCP 到底在中间干了啥

说实话,最开始我对 MCP 的概念特别模糊,以为又是啥新的微服务框架。翻了半天才搞明白,它全称是 Model Context Protocol,说白了就是一套「统一传话标准」。

你可以这么理解:以前大模型想调用本地工具,每个工具都得单独写对接代码,读文件是一套接口,调数据库又是一套,客户端得挨个适配。有了 MCP 之后,不管你背后是什么工具,都按同一套协议跟大模型客户端说话。客户端不用管你工具内部怎么实现的,只要符合 MCP 协议,就能直接用。

我当时整理了一条完整的调用链路,画出来大概是这样:

换成大白话就是:你跟 AI 说 "帮我看看本地的 server.js 写了啥",大模型一琢磨,这事得调用读文件工具,就让客户端给 MCP 服务发个指令。服务收到指令去读文件,读完把内容传回来,大模型再拿着内容给你解释。整个过程里,MCP 服务就像个专职跑腿的,只负责按规矩收消息、干活、回消息。

动手写:不到 50 行的核心代码

原理搞懂了,写起来其实很快。核心就两个东西:一个是官方的 SDK 帮我们处理协议通信,另一个是 zod 用来做参数校验。

先装依赖

新建个文件夹,初始化 npm,装两个包就行:

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

一个是官方的 MCP SDK,帮我们搞定所有协议层的脏活累活;另一个 zod 是用来定义工具参数的校验规则,保证传进来的参数格式是对的。

服务主体代码

新建个 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';

    // 初始化服务,起个名字,标个版本号
    const server = new McpServer({
      name: 'simple-read-mcp',
      version: '1.0.0'
    });

然后注册我们的核心工具 ------read_file,用来读取本地文件。这里要定义工具的名字、描述,还有参数的 schema:

javascript 复制代码
    server.tool(
      'read_file',
      '读取指定路径的本地文件内容',
      {
        // 用zod定义参数:path是字符串类型,加个描述方便大模型理解
        path: z.string().describe('文件的绝对或相对路径')
      },
      async (args) => {
        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 }]
          };
        }
      }
    );

最后写启动函数,用 stdio 的方式连接服务:

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

    main();

我踩的第一个坑:返回格式不能乱写

说真的,第一次写的时候我根本没在意返回格式。我寻思不就是返回文件内容嘛,直接 return content 不就完了?结果配置完之后,大模型能看到这个工具,调用也成功了,但就是拿不到内容,一直报解析错误。

我翻了半天 SDK 的类型定义才反应过来,MCP 对返回结果有严格的格式要求。不能直接返回字符串,必须是一个带 content 数组的对象,数组里每一项要声明 type 和对应的内容。错误返回也要加上 isError: true,不然大模型分不清是正常结果还是报错。

错误的写法大概长这样,别学我:

javascript 复制代码
    // ❌ 错误写法:直接返回字符串,客户端解析不了
    return content;

客户端配置:怎么让 Trae 认出这个服务

代码写完了,直接 node 运行是没用的,它本身就是个靠标准输入输出干活的程序,得配合支持 MCP 的客户端用。我用的是 Trae,配置起来其实不复杂。

找到 Trae 的 MCP 配置文件,加上这么一段:

json 复制代码
    {
      "mcpServers": {
        "simple-read-mcp": {
          "command": "node",
          "args": [
            "D:/workspace/xjl_ai/ai/mcp/simple-read-mcp/server.js"
          ]
        }
      }
    }

意思就是告诉客户端:我有个叫 simple-read-mcp 的服务,你用 node 命令去跑这个 js 文件,它会通过 stdio 跟你通信。

配置完重启一下客户端,正常的话就能在 MCP 列表里看到我们的服务了,就像这样:

当时看到这个绿色开关亮起来,名字旁边还打了对勾,我还以为稳了。结果一试,工具调用直接失败,报路径找不到。

第二个坑:路径问题比想象中麻烦

我当时测试的时候,写的是相对路径,让它读同目录下的 test.txt,结果一直报 ENOENT,说文件不存在。

我愣了半天,明明文件就在那,怎么会找不到?后来才想明白:MCP 服务的工作目录,不是你脚本所在的目录,而是客户端的启动目录。用相对路径的话,它会从客户端的工作目录开始找,自然找不到文件。

解决办法也简单,要么参数一律传绝对路径,要么在代码里把相对路径转成绝对路径。我图省事,直接在参数描述里备注了建议用绝对路径,反正大模型一般也会传完整路径。

提醒如果你的 MCP 服务要处理路径,尽量统一用绝对路径。相对路径的工作目录基准很容易出问题,排查起来特别头疼。

再挖一层:stdio 通信到底是怎么跑的

跑通之后我就好奇,为啥 MCP 默认要用 stdio 通信?起个 HTTP 服务不是更熟悉吗?

后来看了下 StdioServerTransport 的源码,其实特别简单:它就是监听了进程的 stdin,收到消息就按 MCP 协议解析,然后转给服务逻辑;要返回结果的时候,就把消息序列化写到 stdout 里。

说白了,这个 MCP 服务本质上就是个普通的命令行程序。客户端启动它,然后俩人通过命令行的标准输入输出传话,不用占端口,不用跨域,本地跑起来特别轻量。

这么设计的好处也很明显:

  • 跨平台,Windows、Mac、Linux 都能用,标准输入输出是系统自带的
  • 安全,只能本地调用,不会暴露到网络上
  • 简单,开发者不用管网络通信那些事,专心写工具逻辑就行

当然也有局限,就是只能本地用,没法远程调用。不过对于本地工具来说,完全够用了。

还有几个踩过的小坑

除了上面两个大坑,还有几个细节我也踩了,顺手提一嘴。

第一个是配置文件的 JSON 格式,多一个逗号少一个括号都不行。我一开始复制配置的时候,末尾多了个逗号,服务死活加载不出来,控制台也没个明确报错,愣找了十分钟才发现。

第二个是参数描述一定要写清楚。zod 里的 describe 不是写着玩的,大模型就是靠这段描述来理解这个参数该传啥。你写得越清楚,大模型调用的时候越不容易传错参数。

第三个是别在代码里随便 console.log。因为服务是靠 stdout 传消息的,你随便打印的东西会混进协议消息里,导致客户端解析失败。要调试的话,建议写到日志文件里,或者用 stderr 输出。

最后说两句

整个搞下来,核心代码其实不到 50 行,但前前后后踩坑花了不少时间。回头看,最关键的其实就三点:

第一,MCP 不是什么复杂的新技术,它就是一套统一的调用协议,让大模型客户端和各种工具能按同一种规矩说话。

第二,最简单的 MCP 服务,就是注册工具 + stdio 连接,不用搞服务端框架,不用写接口,比写个接口还简单。

第三,返回格式、路径、控制台输出这些细节最容易踩坑,官方文档写得又比较散,新手很容易在这上面卡很久。

当然了,我这个只是最极简的 demo,真要用到生产环境还差得远。比如安全问题,随便让大模型读本地文件风险很高,最好加个路径白名单,只允许读指定目录下的文件;还有权限控制、异常处理这些,都得补全。

但作为入门理解 MCP 的工作原理,这个小 demo 足够了。我一开始把它想得特别高大上,真动手拆解开才发现,核心逻辑朴素得很。

如果你也在折腾 MCP,跑不通或者有别的理解,欢迎留个言,我踩过的坑说不定能帮你省点时间。

相关推荐
俊哥V25 分钟前
AI 今日研究简报 · 2026-09-09
人工智能·ai
张小姐的猫31 分钟前
【AI大模型接入SDK】 —— Gemini接入封装
android·数据结构·数据库·c++·人工智能·python
时空节拍AI数字人37 分钟前
AI 数字人为什么需要“3D”?2D 不够用吗
人工智能·网络协议·tcp/ip·3d·信息可视化
米小虾41 分钟前
4-bit 量化"几乎无损"?把它放进 Agent 循环里再试一次
人工智能·agent
陈皮糖..43 分钟前
从零搭建一个简易 AI 运维问答机器人(RAG + LangChain + Streamlit)
运维·人工智能·ai·langchain·机器人
jimmyleeee1 小时前
大模型安全之五:LLM输出安全
人工智能·安全
HIT_Weston1 小时前
214、【AI】【模型部署】阿里云 PAI:从开发到部署的一站式平台
人工智能·模型部署
制造业的搬运工1 小时前
AI服务器背板与传统背板差异:三大设计升级解析
运维·服务器·人工智能·科技·制造·pcb工艺
hughnz1 小时前
石油工程的端到端数字化转型:演化还是革命
大数据·人工智能·科技
xiao5kou4chang6kai42 小时前
AI-XGBoost机器学习与生态—植被与土地利用识别、土壤碳氮空间预测、生物多样性驱动机制、土壤微生物功能预测、生态退化与风险识
人工智能·机器学习·生态·xgboost·地学