
1. 为什么我要手搓一个能读本地文件的 MCP ServerMCP Server 是什么一句话它把「本地文件、数据库、内部 API」这些 LLM 原本够不着的东西包装成一套标准工具接口让模型通过 JSON-RPC 直接调用。能做什么你问一句「帮我看看 config.yaml 里数据库连接怎么配的」模型不再瞎编而是真的去读你磁盘上的文件再回答。适合谁正在做 Agent、想让 LLM 接入本地开发环境、又不想为每个工具单独写适配层的开发者。我试过把一堆脚本硬塞进 prompt 里让模型「假装」能读文件结果路径一换就翻车。后来才明白LLM 本身没有文件系统访问权它只有「意图」。真正干活的是 MCP Server模型输出一个工具调用意图Client 把它转成 JSON-RPC 请求发给 ServerServer 执行fs.readFile再把内容塞回上下文。整条链路里Server 才是那只真正伸进你硬盘的手。这篇要交付的东西很具体一个 100 行左右的 TypeScript MCP Server注册一个read_file工具一份可复制的mcpServers配置片段以及用 TaoToken 统一 Key 接入时 Base URL 该填哪里、怎么用 curl 验证工具列表和文件读取结果。全程本地开发环境不需要任何额外网络条件。先说清楚 MCP 的三块核心能力避免后面看代码发懵。Tools 是 Server 暴露给 Client 调用的函数类似 OpenAPI 的接口LLM 决定何时调用Resources 是 Server 主动暴露的数据内容类似文件或查询结果Prompts 是预设的提示词模板方便 Client 快速构造对话。我们这个文件读取 Server 只用 Tools 就够了——注册一个read_file让 LLM 能调它读本地文件。通信层基于 JSON-RPC 2.0传输方式最常用的是 stdioClient 启动 Server 子进程通过 stdin/stdout 交换消息。这意味着一个关键纪律——所有调试日志必须走 stderr往 stdout 打日志会污染协议消息Client 直接解析失败。这个坑我踩过后面排障章节会细说。2. TaoToken 统一 Key 的前置准备与 Base URL 填写位置在写代码之前先把「模型侧」的接入准备好否则 Server 写完了却没有 Client 能调它。这里用 TaoToken 的统一 Key 方案好处是一个 Key 走通多个模型不用为每个模型单独维护鉴权。你需要先拿到 API Key。打开 https://taotoken.net/api-keys 创建复制那串以sk-开头的字符串。注意这个 Key 只显示一次丢了只能重建。拿到后不要硬编码进代码放进环境变量export TAOTOKEN_API_KEYsk-你的keyBase URL 的填写位置是接入时最容易搞错的地方。TaoToken 的 API 端点是https://taotoken.net/api注意这里不带任何查询参数。很多同学把官网首页地址填进 Base URL结果请求 404 或者返回 HTML因为首页是给人看的API 才是给程序调的。正确做法是Base URL 填https://taotoken.net/api路径部分由各家 SDK 自己拼接比如 OpenAI 兼容接口会自动补/v1/chat/completions。如果你用的是 Claude Code 这类工具配置通常写在~/.claude/settings.json或项目级 settings 里形如{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key } }如果你用的是 Cline、Codex 这类支持自定义 provider 的客户端配置项一般长这样三件套缺一不可{ provider: openai-compatible, baseUrl: https://taotoken.net/api, apiKey: sk-你的key, model: claude-sonnet-4-20250514 }Base URL、Key、Model ID 这三样必须同时正确。只填 Base URL 不填 Key 会 401Key 对了但 Model ID 写错会报 model not foundBase URL 少了/api会连到官网首页。我建议你把这三件套写进一个.env文件代码里用process.env读取既安全又方便切换。还有一点MCP Server 本身不直接调用模型 API它只负责执行工具。真正调模型的是 Client比如 Claude Desktop、你的 Agent 程序。所以 TaoToken 的 Key 是配在 Client 侧的Server 侧不需要。这个分工要理清楚否则你会困惑「Server 里怎么没有 API Key」。3. 100 行代码实现 read_file MCP Server 的可复制配置先初始化项目。Node 18 以上即可我用的是 20mkdir simple-read-mcp cd simple-read-mcp npm init -y npm install modelcontextprotocol/sdk npm install -D typescript types/node npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir dist然后写server.ts。下面这份代码可以直接复制关键点我都加了注释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; import path from path; // 1. 创建 Server 实例声明支持 tools 能力 const server new Server( { name: simple-read-mcp, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 2. 注册工具列表处理器Client 启动时会调用 server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: read_file, description: 读取指定路径的本地文件内容返回纯文本, inputSchema: { type: object, properties: { path: { type: string, description: 文件的绝对路径或相对路径, }, }, required: [path], }, }, ], })); // 3. 注册工具调用处理器LLM 决定调用时触发 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name ! read_file) { throw new Error(未知工具: ${name}); } try { const target path.resolve(String(args?.path ?? )); const content await fs.readFile(target, utf-8); return { content: [{ type: text, text: content }] }; } catch (error) { const msg error instanceof Error ? error.message : String(error); return { isError: true, content: [{ type: text, text: 读取文件失败: ${msg} }], }; } }); // 4. 通过 stdio 启动与 Client 通信 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(simple-read-mcp started, waiting for requests...); } main();编译并运行npx tsc node dist/server.js此时进程会挂起等待 stdin 输入这是正常的——它在等 Client 发 JSON-RPC 消息。console.error那行日志会出现在 stderr不会污染协议。接下来是 Client 侧的配置片段。以 Claude Desktop 为例编辑claude_desktop_config.json{ mcpServers: { simple-read-mcp: { command: node, args: [/absolute/path/to/simple-read-mcp/dist/server.js] } } }如果你用的是 Cline配置写在 Cline 的 MCP 设置里结构一致。注意args里必须是编译后server.js的绝对路径Windows 下反斜杠要转义或改用正斜杠。保存后重启 Client它会在启动时拉起这个子进程并调用tools/list。这里补一句关于 TaoToken 的位置MCP Server 配置里不需要出现 Base URL 和 Key因为 Server 不调模型。TaoToken 的三件套是配在 Client 的模型 provider 里的和 MCP 配置是两个独立的块。很多人把两者混在一个 JSON 里结果 Client 解析报错。4. 用 curl 验证工具列表与文件读取结果Server 写完了怎么确认它真的能工作最直接的办法是用 curl 手动发 JSON-RPC 请求。但 stdio 传输没法直接 curl所以先用官方 inspector 做交互验证npx modelcontextprotocol/inspector node dist/server.js它会启动一个本地 Web 界面你能看到工具列表、手动填参数调用、查看返回。这是调试利器建议每次改完代码都跑一遍。如果你想用 curl 验证「模型侧」的接入是否通那验证的是 TaoToken 的 API而不是 MCP Server。发一个最小请求curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 只回复两个字通了}], max_tokens: 16 }返回里能看到choices[0].message.content就是成功。这一步验证的是 Base URL、Key、Model ID 三件套是否正确。如果返回 401检查 Key返回 404检查 Base URL 是否漏了/api返回 model not found检查 Model ID 拼写。回到 MCP Server 本身用 stdio 手动喂一条 JSON-RPC 也能验证。先发初始化请求echo {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:curl-test,version:1.0}}} | node dist/server.js你会看到 stdout 返回一段 JSON包含serverInfo和capabilities。接着验证工具列表需要连续发两条消息initialize 后跟 initialized 通知再跟 tools/list实际用 inspector 更省事。工具调用请求长这样{ jsonrpc: 2.0, id: 2, method: tools/call, params: { name: read_file, arguments: { path: /tmp/demo.txt } } }如果/tmp/demo.txt存在且内容为hello mcp返回的content[0].text就是hello mcp。这一步跑通说明从协议握手到文件读取的整条链路都活了。5. 本篇常见错误排查401、local proxy failed 与 reading choices排障这块我按真实报错来对。第一个高频错误是401 Unauthorized。出现在 TaoToken 请求里八成是 Key 没读到——检查echo $TAOTOKEN_API_KEY是否有值或者代码里process.env拼写是否一致。出现在 MCP 场景里则可能是你把 Key 配到了 Server 配置块而 Client 根本没读那个字段。第二个是local proxy failed或连接被拒。这通常发生在 Client 启动 Server 子进程时command或args路径写错进程根本没起来。排查方法把command和args拼成一条命令在终端手动跑看是否报Cannot find module。Windows 用户特别注意路径分隔符建议统一用正斜杠。第三个是reading choices或Cannot read properties of undefined (reading choices)。这是解析模型响应时字段不存在根因往往是 Base URL 填成了官网首页返回的是 HTML 而不是 JSON解析自然拿不到choices。把 Base URL 改成https://taotoken.net/api即可。也有可能是 Model ID 写错导致返回错误结构先看原始响应体再下结论。第四个是 Server 启动后 Client 报「无法解析响应」。这几乎一定是往 stdout 打了日志。记住stdout 是协议通道所有console.log都要改成console.error。我当初就是在一个工具函数里留了句console.log(args)排查了半小时。第五个是OAuth相关报错。部分 Client 在检测到自定义 Base URL 时会尝试走 OAuth 流程如果你的接入方式是纯 API Key需要在 Client 设置里显式关闭 OAuth 或选择 API Key 模式。具体开关位置各家不同Claude Code 在 settings 里Cline 在 provider 配置里。对照表帮你快速定位报错最可能原因处理401Key 未读到或配错位置检查环境变量与 Client 配置块local proxy failedServer 子进程启动失败手动跑 commandargs 验证路径reading choicesBase URL 错误返回 HTML改为 https://taotoken.net/api无法解析响应stdout 被日志污染console.log 全改 console.errorOAuth 报错Client 走了 OAuth 流程切换为 API Key 模式6. 把 MCP 接入你的 Agent从验证到长期编码Server 跑通只是第一步真正有价值的是把它接进你的日常编码流。如果你只是偶尔验证一下模型能不能读文件用模型对话页面手动测就够了但如果你想让 Agent 在写代码时随时读项目里的配置文件、日志、schema那就需要一套稳定的长期方案。我的做法是把 MCP Server 和 Coding Plan 配合用MCP 负责「读」Coding Plan 负责「写和推理」。比如让 Agent 读docker-compose.yml后自动补全缺失的环境变量或者读schema.prisma后生成对应的 TypeScript 类型。这种组合下Server 的工具描述要写得足够清楚LLM 才知道什么时候该调read_file。工具描述里最好带上使用场景示例比如「当用户询问某个配置文件的内容时调用」。LLM 是靠这段文字判断调用时机的写得太笼统它就不调写得太宽泛它乱调。我一般会在 description 里加一句「仅用于读取文本文件不要用于二进制文件」。路径安全也要提前想。上面示例允许读任意路径本地开发够用但如果你的 Agent 会处理不可信输入建议加白名单目录限制把path.resolve后的结果和允许的根目录做前缀比对不在范围内直接返回错误。这个改动不到十行但能避免很多麻烦。最后给一个实用技巧把 Server 的启动命令写进项目的package.jsonscripts比如mcp:dev: node dist/server.js配合tsc --watch改完代码自动重编译Client 重启后立刻生效。这样迭代 MCP 工具的体验会顺很多不用每次手动编译。整套流程走下来你会发现 MCP 的门槛比想象中低——核心就是注册工具、处理调用、走 stdio。真正花时间的是把工具描述写准、把错误处理做细。这两件事做好了你的 Agent 才算真正长出了「读本地文件」这只手。