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

作者:To_OC日期:2026/7/11

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

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

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

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

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

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

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

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

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

先装依赖

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

1npm install @modelcontextprotocol/sdk zod
2

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

服务主体代码

新建个 server.js,一步步来写。

首先引入依赖,创建服务实例:

1    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3    import { z } from "zod";
4    import fs from 'fs/promises';
5
6    // 初始化服务,起个名字,标个版本号
7    const server = new McpServer({
8      name: 'simple-read-mcp',
9      version: '1.0.0'
10    });
11

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

1    server.tool(
2      'read_file',
3      '读取指定路径的本地文件内容',
4      {
5        // 用zod定义参数:path是字符串类型,加个描述方便大模型理解
6        path: z.string().describe('文件的绝对或相对路径')
7      },
8      async (args) => {
9        try {
10          const content = await fs.readFile(args.path, 'utf-8');
11          // 注意这个返回格式,坑了我快半小时
12          return {
13            content: [{ type: 'text', text: content }]
14          };
15        } catch (err) {
16          return {
17            isError: true,
18            content: [{ type: 'text', text: err.message }]
19          };
20        }
21      }
22    );
23

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

1    async function main() {
2      const transport = new StdioServerTransport();
3      await server.connect(transport);
4    }
5
6    main();
7

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

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

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

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

1    //  错误写法:直接返回字符串,客户端解析不了
2    return content;
3

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

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

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

1    {
2      "mcpServers": {
3        "simple-read-mcp": {
4          "command": "node",
5          "args": [
6            "D:/workspace/xjl_ai/ai/mcp/simple-read-mcp/server.js"
7          ]
8        }
9      }
10    }
11

意思就是告诉客户端:我有个叫 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,跑不通或者有别的理解,欢迎留个言,我踩过的坑说不定能帮你省点时间。


MCP 入门实战:写一个能读本地文件的极简服务》 是转载文章,点击查看原文


相关推荐


Gson → kotlinx.serialization
plainGeek2026/7/3

Gson → kotlinx.serialization 老写法(Java + Gson) Gson gson = new Gson(); // 序列化 Item item = new Item(1, "商品", 9.99); String json = gson.toJson(item); // 反序列化 Item parsed = gson.fromJson(json, Item.class); List<Item> list = gson.fromJson(jsonArray,


图解 MongoDB 12|索引与查询优化地图:一条主线,三个判断轴
十三Tech2026/6/25

到这里,索引与查询优化这个阶段就讲完了。从第 04 篇的索引模型,到第 11 篇的慢查询排查闭环,中间穿过了索引类型、ESR 原则、explain、覆盖查询。这些不是孤立的知识点,而是一条连贯的主线——每一步都在回答「怎么让查询又快又省」。 这一篇是阶段的收束,不引入新机制,而是把前面讲过的东西收成一张地图和三个判断轴,方便你在实际工作中快速调用。后面进入存储引擎与内存阶段(13–17)时,会从「查询怎么用索引」下沉到「索引和数据怎么在内存里」。 一条主线 这条主线有六个节点,对应这个阶段的六


Vue集成uuid生成唯一标识实践指南
独泪了无痕2026/6/16

一、核心基础 1.1 UUID 是什么   UUID(通用唯一标识符,Universally Unique Identifier) 是一个 128 位用于标识信息的唯一标识符,通常以 32 个十六进制的字符串形式呈现,具有全球唯一性(理论上重复概率可忽略),非常适合用于标识网络中的资源、数据记录或其他任何需要唯一标识的实体。 UUID 生成器:devtool.tech/uuid 1.2 uuid.js 库概述   uuid.js 是用于生成 UUID 的 JavaScript 库,解决


Agent 系列(16):工具链设计——让 LLM 用对工具的五个原则
冬奇Lab2026/6/9

工具文档是写给 LLM 的,不是写给人的 你有没有写过这样的工具文档: @lc_tool def get_data(query: str) -> str: """Get data.""" ... 这对人类来说是糟糕的文档,对 LLM 来说更糟——它不知道这个工具做什么、什么时候调它、传什么参数。 工具设计有三条核心维度:描述质量(LLM 选不选你)、错误处理(出错时崩不崩)、粒度设计(参数好不好提取)。本文用实验数据说话。 Demo 1:描述质量——真正影响工具选择的条件 对


实战解析:如何用自然语言驱动混沌工程?Blade AI Agent 实现故障演练全链路自动化
阿里云云原生2026/6/1

作者:林曜、穹谷 混沌工程为什么难落地? 每个 SRE 团队都知道混沌工程的价值——在可控条件下主动注入故障,验证系统韧性,防患于未然。 但现实是,绝大多数团队的故障演练停留在“年度任务”而非“日常习惯”。原因很简单: 门槛太高,流程太碎。 一次完整演练五步:定位目标 → 拼装命令 → 确认安全 → 验证效果 → 善后清理。每一步都要查文档、写参数、跑命令。即使是经验丰富的工程师,单次演练也需要 20-30 分钟。而任何一步遗漏(忘了验证、忘了清理),后果都可能比不演练更糟。 Blade AI


策略周度复盘 | 2026年wk19
0xAI2026/5/12

本文观点仅供参考,不构成任何投资建议。投资有风险,入市需谨慎。 一、本周大盘走势 本周从周三开始开盘,只有3个交易日(5月6日-8日),但是整个大A还是实现了开门红。到周五收盘为止,整个大盘走势稳扎稳打,虽然有大涨,不过回调也比较有限,仍然维持着比较强势的多头态势。再加上外围美股市场AI科技大行其道,一片”涨声“,所以下周开盘,大概率还会延续本周的涨势。手上有票的朋友不必慌张,可以继续持股等着更大的涨幅。 接下来,还是老规矩,我们以真实数据说话,一图胜千言。本周三大股指本周仍然是以创业板为主,


🚀 2026 年 4 月 GitHub 十大热门项目排行榜 🔥
一点一木2026/5/2

欢迎来到 2026 年 4 月 GitHub 热门开源项目排行榜!本月榜单横跨 成长型通用智能体、Claude Code 技能与记忆、文档—Markdown 数据管线、Token 经济学 CLI、多智能体协作平台、Harness / 工作流治理、Agent-Native 教育 与 金融时序基础模型 等方向。这些项目共同指向:把编码智能体从「单次对话」推进到「可协作、可度量、可沉淀」;它们几乎全部围绕「更稳的 Harness、更省的 Token、更真的垂直数据」展开,不再是概念验证,而是可以立刻嵌


数据仓库是什么?怎么搭建数据仓库?
isNotNullX2026/4/23

我们每天都在跟数据打交道,但提到数据仓库这个词,大多数人的第一反应还是——听说过,但说不清到底是什么。 有人觉得它就是存数据的地方; 有人觉得它和数据库差不多; 也有不少人以为,只有大厂、只有数据团队才需要数据仓库。 实际上,只要企业存在多个业务系统、多个部门协同、多个分析口径,数据仓库几乎就会成为绕不开的一步。 这篇文章,我们就把数据仓库这件事彻底讲清楚: 数据仓库到底是什么?企业为什么需要它?怎么搭建?又能给企业带来什么价值? 开始之前,我整理了一份数据仓库建设解决方案,里面涵盖了从


C语言-----扫雷游戏
2026/4/14

扫雷游戏的功能说明 : • 使⽤控制台实现经典的扫雷游戏 • 游戏可以通过菜单实现继续玩或者退出游戏 • 扫雷的棋盘是9*9的格⼦ • 默认随机布置10个雷 • 可以排查雷: ◦ 如果位置不是雷,就显⽰周围有⼏个雷 ◦ 如果位置是雷,就炸死游戏结束 ◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束 test.c //⽂件中写游戏的测试逻辑 game.c //⽂件中写游戏中函数的实现等 game.h //⽂件中写游戏需要的数据类型和函数声明等 逻辑开始: 一、菜单 输入1进入游戏,输入


UniApp 页面跳转完全指南:5 种路由方式详解与实战对比
编程随想_Code2026/4/6

前言 在 UniApp 开发中,页面间的跳转是最常见的操作之一。UniApp 提供了 5 种路由 API,分别对应不同的跳转场景。选错跳转方式轻则体验变差,重则出现"无法返回""Tab 页跳转失败"等让人头疼的 bug。 本文将逐一讲解 5 种跳转方式的原理、适用场景与代码示例,并附上横向对比表,方便日常查阅。 一、uni.navigateTo — 保留式跳转 最常用的跳转方式。跳转到新页面时,当前页面并不会被销毁,而是压入页面栈(page stack),用户可以通过左滑或返回按钮回到

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读