多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MCPCat:为AI编程助手按需读取代码库的轻量级MCP服务器

MCPCat:为AI编程助手按需读取代码库的轻量级MCP服务器 1. 为什么编写 MCPCat从一条高频指令说起最近在改造本地开发工作流时我一直在纠结一个问题AI 编程助手明明已经能写代码了为什么每次让它改一个跨文件的需求总是答非所问反复排查之后发现大多数时候不是模型能力不够而是我对模型说的话根本不够具体。比如用户侧只来了一句“帮我梳理一下订单模块的调用关系”如果直接把整个项目压缩成几个 Markdown 文件丢给助手它会读到大量无关的页面、样式和测试代码真正关键的接口定义反而淹没在噪声里。后来我试着把项目里的关键文件手动摘出来再逐段贴给模型效果确实好了不少但代价是每次都要花很多时间整理上下文。更麻烦的是这种手工作业没法保持连续——模型刚改完 A 文件马上又要看 B 文件我总不能每次都全量重新贴一遍。这个痛点越用越明显于是我开始研究有没有一种方式能让 AI 助手自己按需读取项目里的文件同时又不把整个仓库都灌进对话里。这就是我写 MCPCat 的直接动机它本质上是一个跑在本地、专门为编码场景设计的轻量级 MCPModel Context Protocol服务器。MCPCat 暴露给 AI 客户端的不是“我想让你看整个项目”而是一套结构化的文件查询能力列出目录树、读取指定文件、搜索符号引用、查看最近修改的片段。AI 助手拿到的是一支“受控的遥控器”而不是一车皮未经整理的代码快照。无论你用哪个支持 MCP 的编程客户端只要能注册外部工具都可以通过 MCPCat 获得这种按需访问代码库的能力。这篇文章会从设计思路、搭建步骤、实际调优和踩坑记录四个维度来讲 MCPCat。我不会只贴一个跑通的 Demo而是把“为什么这样设计”也一并说清楚希望它能成为一个可复用的本地开发基础设施而不仅仅是你跟随教程敲出来的又一个玩具项目。2. 核心设计给 LLM 一套“可审计的文件上下文”MCPCat 的第一版很好写无非就是暴露几个 file read 和 list directory 的工具。但真正把它放进日常开发流程之后我才发现设计目标远没有这么简单。关键在于你给大模型一个工具时它对你的项目一无所知它会把工具当成一种通用的“读文件能力”而不会主动考虑 token 成本、敏感文件、路径深度等问题。所以 MCPCat 在设计上必须自带三层约束。2.1 结构化令牌预算防止一次上下文爆炸很多人在给 AI 助手接入文件工具时第一个想法就是“直接提供整个项目文件树”看起来信息量很大实际效果却很差。因为文件树里每个路径都占 token一个中型项目动辄几千个文件光树状结构就能吃掉几万 token真正有用的信息却被挤到上下文边缘。MCPCat 的做法是强制走两层查询第一层返回精简的项目目录骨架只包含目录名、文件名、文件大小和最后修改时间不读取任何文件内容第二层才按用户指定的路径读取具体文件内容。这样模型每次只会加载它真正关心的那几份文件而不是把所有文件都先轮一遍。工具层面我会设置一个最大文件大小默认超过 512KB 的文件只返回前 256 行和一个截断标记如果模型确实需要看后面的代码它会再发一次范围读取请求而不是一次性把大文件整个拉进来。这个“两阶段读取”设计参考了操作系统里的页表理念先看目录找地址再按页加载内容。对 AI 系统最重要的价值在于它让每次工具调用的信息增益是可计算的——模型每花一笔 token都能确切知道这笔 token 换来了什么内容而不是无差别地把所有东西都塞进上下文。2.2 代码仓库索引构建符号引用与调用链仅仅能按路径读文件还是不够的真实开发中最高频的需求是“这个函数在哪里定义”和“谁调用了这个函数”。MCPCat 内置了一个轻量级的索引模块启动时扫描仓库根目录下所有源代码文件用正则提取函数定义、类定义、导出声明建立一份符号表。这份符号表会持久化到本地缓存目录增量变更时只需要重新扫描发生变化的文件即可。索引模块不追求与 IDE 同等强度的静态分析精度它的设计目标是帮助模型快速定位候选文件。比如模型想知道某个函数实现了什么逻辑它会先通过 MCPCat 查询符号“paymentService.refund”得到两个候选位置定义处和调用处。然后它再分别读取这两个位置的上下文代码完成精准分析。这种“索引粗筛 按需精读”的方式比让模型自己去 grep 整个仓库要高效得多因为 grep 每次都是全量扫描而索引查询是在固化数据上做的 O(1) 操作。2.3 安全边界与权限控制可审计的本地代理本地工具还有一个很容易被忽略的问题AI 客户端并不是一个严格的沙箱环境。如果你不加限制地暴露“执行任意 shell 命令”或“读任意文件”的能力一旦模型被诱导生成了恶意工具调用后果会非常严重。MCPCat 在设计上把能力边界框得很死只能读文件不能写文件只能在配置的根目录内访问不能越权读取系统路径默认忽略 .git、node_modules、dist、venv 这些运行时目录防止模型把垃圾文件也当成业务代码来分析。另外我会在每次工具调用时记录访问日志这样你可以随时回看模型到底读了哪些文件、在什么时间读取的做到全链路可审计。这个设计可能不会在演示视频里显眼但真正生产环境里跑久了你就会发现它才是安全感的来源。3. 端到端搭建步骤从零到接入 Coding Agent下面这部分是完整的实操流程我会按自己实际搭环境的顺序来写。我的运行环境是一个普通的 macOS 笔记本Node.js 版本 20Python 3.11不过这里的步骤本身是跨平台的Windows 和 Linux 上也就是命令差异。3.1 初始化项目与安装依赖我采用 TypeScript 来写 MCPCat一方面是 MCP SDK 对 TypeScript 支持比较成熟另一方面是类型系统在定义工具输入输出时能少踩不少坑。先把目录建好然后初始化包管理mkdir mcpcat cd mcpcat npm init -y npm install modelcontextprotocol/sdk types/node typescript tsx这里有个实用细节安装tsx而不是直接用node跑 TS 文件可以省掉每次改动代码都要重新编译的步骤开发期会很舒服。tsx本质上就是一个零配置的 TypeScript 执行器运行tsx src/index.ts就能直接启动服务。3.2 编写核心服务器逻辑入口文件我建议拆成三段逻辑参数解析、仓库扫描、工具注册。下面这段是一个精简但能完整跑起来的最小实现你可以直接抄来当骨架import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import fs from fs/promises; import path from path; const server new McpServer({ name: mcpcat, version: 0.1.0, }); // 用异步遍历收集目录结构忽略常见依赖目录 async function walkDir(dir: string, depth 0, maxDepth 4) { if (depth maxDepth) return []; const entries await fs.readdir(dir, { withFileTypes: true }); const list: { name: string; type: string; path: string }[] []; for (const entry of entries) { const fullPath path.join(dir, entry.name); if (entry.name.startsWith(.) || [node_modules, dist, build, vendor].includes(entry.name)) { continue; } if (entry.isDirectory()) { list.push({ name: entry.name, type: dir, path: fullPath }); list.push(...await walkDir(fullPath, depth 1, maxDepth)); } else { list.push({ name: entry.name, type: file, path: fullPath }); } } return list; } server.registerTool(list_tree, { limit: { type: number, description: 最大返回数量 } }, async ({ limit 300 }) { const root process.env.MCPCAT_ROOT || process.cwd(); const tree await walkDir(root); const sliced tree.slice(0, limit); const lines sliced.map(f ${f.type dir ? DIR : FILE} ${f.path}); return { content: [{ type: text, text: lines.join(\n) || (empty) }] }; }); server.registerTool(read_file, { path: { type: string, description: 绝对路径或相对于根目录的路径 } }, async ({ path: rawPath }) { const root process.env.MCPCAT_ROOT || process.cwd(); const resolved path.isAbsolute(rawPath) ? path.normalize(rawPath) : path.resolve(root, rawPath); if (!resolved.startsWith(root)) { throw new Error(Path outside allowed root); } const content await fs.readFile(resolved, utf-8); return { content: [{ type: text, text: content.slice(0, 20000) }] }; }); const transport new StdioServerTransport(); await server.connect(transport);这段代码已经包含了最核心的两个工具list_tree和read_file。MCPCAT_ROOT环境变量用来指定可访问的项目根目录所有路径解析都会先做一次前缀校验防止模型穿越根目录读取任意文件。3.3 在客户端中注册 MCPCat服务器代码写完之后剩下的问题就是怎么能让 AI 客户端调用它。绝大多数支持 MCP 的编程客户端都在设置里提供了“编辑 MCP 服务器配置”的入口配置格式如下{ mcpServers: { mcpcat: { command: node, args: [/absolute/path/to/mcpcat/dist/index.js], env: { MCPCAT_ROOT: /absolute/path/to/your/project } } } }需要注意一点这里传的是dist/index.js也就是编译后的产物而不是src/index.ts。因为客户端在拉起来 MCP 进程时用的是普通 Node 进程它不认 TypeScript。为了避免每次改完代码都手动跑tsc我会在 package.json 里加一个prestart钩子scripts: { build: tsc, prestart: npm run build, start: node dist/index.js }配置完成后重启客户端再进入对话界面输入“列出当前项目目录结构”或者“读取 src/services/payment.ts 文件”正常的话就能看到模型自动调用了 MCPCat 提供的工具。第一次跑通时我建议你在客户端里查看一下工具调用日志确认list_tree和read_file确实是被模型实际调起来而不是模型又在凭感觉瞎编。4. 实际使用与性能调优一次真实的上下文瘦身MCPCat 接入之后我做的第一件事是用一个线上项目来验证效果。那个项目大概有 300 多个 TypeScript 文件之前如果用“把整个项目塞给模型”的方案每次对话光是加载文件树就要消耗超过 4 万个 token而且模型的注意力被分散得厉害。用 MCPCat 之后模型先调用list_tree拿到目录骨架发现关键代码集中在src/modules/order目录下然后只读取了 4 个核心文件总 token 数不到原来的十分之一。4.1 让模型学会“先看目录再读文件”不过这里有个隐含技巧并不是所有模型都会主动使用工具来探索目录。有些模型拿到工具列表后会直接猜测一个路径然后调用read_file去读如果路径猜错了就报错再继续猜体验很糟糕。我的解决办法是在 MCPCat 的工具描述里写清楚使用策略第一个工具 description 写明“第一步请调用 list_tree 了解项目结构再使用 read_file 读取具体文件”第二个工具 description 写明“不要猜测路径优先使用 list_tree 得到的相对路径”。这一行描述的价值非常大它本质上是在给模型注入“使用规范和最佳实践”。模型遵循工具描述的概率远高于遵循系统上下文的概率所以你在定义工具时千万不要把 description 写得太简短那等于浪费了一次告诉模型“该怎么用”的机会。4.2 并发请求与资源占用MCP 服务器启动后是常驻进程同一个会话里的多次工具调用会复用一个进程这也是我推荐使用 Stdio 传输而不是 SSE 的原因。Stdio 模式直接通过标准输入输出做消息传递没有网络层开销也不存在端口冲突问题。但因为是常驻进程你还是要留个心眼如果你的项目非常大首次启动时 MCPCat 会做一次全量目录扫描这个扫描是异步遍历在机械硬盘上可能要花几百毫秒到几秒不等期间如果有工具请求进来可能会短暂阻塞。我给扫描逻辑加了缓存扫描结果保存在内存中 10 秒避免模型在连续调用时反复刷新的问题。如果后续需要支持多项目并行可以考虑把目录索引持久化到 SQLite不过我目前的使用场景还没到那个量级。4.3 实测数据token 消耗对照我把同一段需求分别用“全量文件树注入”和“MCPCat 按需读取”两种方式跑得到一份对比数据虽然样本很小但趋势很明确方案加载文件数消耗 token模型回答准确率主观评估全量注入目录骨架800约 43000一般容易答非所问MCPCat 按需读取8约 5200高能定位到具体代码行这个差距的来源并不神秘全量注入虽然给了模型全部信息但模型的注意力机制很难在几千个无关文件里精准聚焦按需读取则每次都只给当前任务直接相关的上下文信噪比显著更高。5. 几个绕不开的坑与对策MCPCat 从原型到稳定跑在我日常开发环境里大约用了一周时间。这一周里我踩过的坑比前面所有设计加起来都更有教育意义下面挑三个最典型的展开讲希望能帮你少走弯路。5.1 路径规范化Windows 与符号链接的教训第一版read_file我图省事直接path.join(root, rawPath)就拼路径了。结果在 Windows 上跑的时候模型传入的是src\services\order.ts这种反斜杠路径而path.join在 Windows 上能正常处理在 Linux 上则把反斜杠当成普通字符导致文件永远读不到。后来统一改成path.normalize(path.join(root, rawPath))并在服务启动时判断是否需要把反斜杠替换成正斜杠。更隐蔽的一个问题是符号链接。假设你的项目根目录下有个data - /Users/xxx/data的软链接我的路径前缀校验resolved.startsWith(root)根本拦不住它因为realpath解析之后路径完全跑到了别的目录。修复方法是在校验前先调用fs.realpath拿到真实路径再做前缀判断同时用fs.stat检查文件类型如果发现符号链接就明确拒绝。安全功能不能靠“应该不会有人这么配”来糊弄攻击面往往就藏在那些你没想到的地方。5.2 忽略规则过强导致“只见树木不见森林”另一个让 MCPCat 显得很蠢的坑是忽略规则。我最初把.git、node_modules、dist、build全部堵死理由很充分——这些目录不该给模型看。但实际跑起来发现模型在分析某个构建报错时恰恰需要看dist目录下的产物来对比源码和编译结果的差异分析依赖版本问题时又需要看node_modules里某个具体包的源码来确认行为。一刀切忽略规则让模型在部分场景下变成了瞎子。最后我的折中方案是默认忽略规则不变但增加一个allow_ignore_override参数当模型传入一个被忽略的路径时返回的不是“文件不存在”而是“该路径在忽略列表内若确需访问请追加参数 allow_overridestrue 并注意安全审计”。审计日志会特别标记这类越权访问。这样既避免模型被垃圾信息淹没又保留了关键场景的逃生通道。5.3 长上下文场景下的分页设计还有一个容易忽视的问题当模型需要浏览一个非常大的目录时单次list_tree返回结果可能非常长。我设置上限 300 条记录但即便如此某些 monorepo 仓库的顶层目录一展开就超过这个数。模型看到被截断的结果后往往会误以为整个项目就这些路径导致后续分析完全偏离。解决思路是把目录扫描结果压缩成一种类似“文件系统的统计摘要”的格式而不是纯路径列表第一层只列根目录下的子目录名和每个目录的文件数量让模型先判断业务模块分布第二层再展开具体目录。这种摘要结构更像 IDE 项目窗口的展示方式模型理解起来也更符合直觉。你可以在 MCPCat 的list_tree工具里自行按层级调整返回格式个人经验是“先统计概览、后明细展开”比“一律拍平输出”对模型的决策帮助更大。6. 后续扩展从文件读取到可执行的工作流MCPCat 目前已经能稳定支持我日常 80% 的编码辅助需求但我并不打算就此停手。接下来的扩展方向有三个都已经列进了计划里。第一是支持“区块级读取”。很多时候模型需要看的并不是整个文件而是某个函数体或某个导出块。现在read_file读的是整份文件大文件还是会带来不必要的 token 消耗。下一步我会让 MCPCat 基于之前的符号索引提供read_scope工具允许模型按函数名直接读取该作用域范围内的代码进一步把读取粒度细化。第二是加入“同类文件聚合查询”。比如模型想知道项目中所有service文件暴露了哪些方法目前需要逐个 read 才能汇总更聪明的做法是让 MCPCat 提供正则扫描接口按模式搜索所有匹配文件并返回精炼摘要。虽然 MCP 标准里也包含mcp.resource这类资源协议但我更倾向于在工具层面自己控制摘要逻辑调试起来更直接。第三个方向是“安全写文件能力”。现在 MCPCat 是只读代理模型改完代码之后还是需要我自己手动把模型给的改动复制到文件里。后续我会考虑增加带diff预览的write_file工具并强制要求写入前生成 diff 片段用户确认后才落盘。这个功能涉及权限边界和事务性写入比读文件的设计复杂一个量级我还在谨慎打磨。说到底MCPCat 这类工具的价值不在“给 AI 更多能力”而在于“给 AI 按需拆解上下文的能力”。模型本身的推理能力在快速进步但本地文件世界仍然需要一套结构化的访问协议把它接进来。我写 MCPCat 的这几周里最深的体会是工具链的复杂度和上下文管理的精细程度很多时候才是决定 AI 编程体验上限的因素。如果你也经常觉得 AI 助手“代码水平忽高忽低”不妨先审视一下它看到的内容到底是不是它当前任务真正需要的。
返回列表