从VS Code与Claude Code的割裂到无缝协作:MCP桥接实战解析
如果你最近在用 VS Code 配合 Claude Code 写代码应该能立刻理解我描述的这个画面终端里那个 AI 助手吭哧吭哧地改文件改成什么样你看不见改完还要对着 diff 到处找“它动了哪里”。而你在编辑器里手动改完代码、调完参数终端那头也完全不知道。两套工具明明都装在同一台机器上却像隔着雾在合作。这个项目的想法就是从这里冒出来的——给 Claude Code 和 VS Code 之间搭一座真正的桥让它能直接打开我眼前的文件、跳转到指定行、读取我选区里的代码、把我正在编辑的最新内容同步给它。做完之后我把它挂到了社区里标题就是 Show HN: VS Code Bridge for Claude Code。这篇博文不是宣传稿是我把这个桥从零搭起来的过程中完整的设计、实现、踩坑和取舍记录。想自己接 Claude Code 和编辑器能力的同学可以照着这条路复刻。1. 我在终端旁边装了个后视镜为什么需要这座桥1.1 割裂的日常编辑器里的光标和终端里的对话互不相认先说一个我很长时间里每天都在重复的动作。我在 VS Code 里打开某个文件翻到第 120 行看到一个函数的边界有问题想让 Claude Code 帮我改一下。于是我复制文件路径切到终端把路径贴给它再用文字描述“第 120 行”是个什么地方。它改完之后屏幕上出现一大段 diff编辑器这边要么静默刷新要么弹一个“文件已更改”的提示。我想让它跳到我改过的那一行做不到只能自己在 diff 里数行号。反过来也麻烦。我在编辑器里手动调了一个配置参数想把新值告诉 Claude Code最省事的办法是把选中的那几行代码复制粘贴进终端。一次两次无所谓每天重复几十次就是一笔很大的时间开销。而且这种文字搬运很容易出错——贴错了行、漏了闭合括号Claude Code 基于错误上下文给的建议就完全跑偏。我不是说终端工作流不好CLI 的高效率是它能在开发者工具里活下来的核心原因。问题出在信息不对称Claude Code 能直接写磁盘文件但它看不到编辑器里的“缓冲区”VS Code 能显示文件内容但它感受不到终端里正在进行的对话。两边各有一套事实而且这两套事实会随着时间推移越来越不一致。1.2 桥要解决的三件事文件可见、改动可回传、操作可追踪所以我把需求收敛成了三个方向想清楚之后整个项目就好做了。方向 A 是让 Claude Code 能“走进编辑器”调用工具打开某个文件、跳转到指定行、获取当前选中区间的代码。它不再靠着用户复制粘贴来感知上下文而是自己拉取。方向 B 是让 Claude Code 的改动“出现在编辑器里”通过工具把编辑结果直接落到 VS Code 的文档模型上而不是绕过扩展直接写磁盘。这样编辑器能正确感知变化用户可以立刻看到改了什么、改在哪里也能直接按 CtrlZ 撤销。方向 C 是让操作过程“可追踪”每次桥接调用都在扩展侧留下完整日志工具名、参数、结果、耗时全都能查。调试的时候不是靠猜而是能复现完整链路。我给这三个方向起了个比喻叫“后视镜”——终端里那个代码代理需要看见自己刚刚做过什么、现在处于什么状态。这不是一个炫酷的功能但它是让 AI 协作变得舒服的基础设施。2. 桥的主体设计一座住在 VS Code 扩展里的 MCP 服务2.1 选型为什么把 MCP server 直接塞进扩展进程而不是另开 HTTP 服务动手之前我其实列过三种方案各有各的取舍。第一种方案是让 VS Code 扩展起一个本地 HTTP 服务监听某个随机端口同时写一个独立的 MCP 包装进程通过 HTTP 把工具调用转发给扩展。这种方式最灵活但问题也不少要管端口生命周期要在扩展关闭时优雅退出还有可能被本机防火墙拦截。更重要的是扩展和 MCP server 不在同一个进程里很多状态比如当前活跃编辑器只能通过序列化传来传去。第二种方案是外部进程轮询文件变化把变更包装成 VS Code 的 URI 命令发送出去。这个方案的延迟不可控而且本质上还是绕过编辑器在做事情。最后我选了第三种把 MCP server 直接嵌进 VS Code 扩展进程里。Claude Code 启动一个子进程子进程的标准输入输出就是 MCP 的传输通道这个子进程内部和扩展的 activation 逻辑共享同一个上下文。选它的理由很朴素。第一不占端口不受防火墙和代理影响第二生命周期天然跟随 VS Code扩展没起来桥就不存在不会留下僵尸进程第三进程内调用意味着扩展可以直接操作vscode.window.activeTextEditor、workspace.applyEdit这些 API不需要任何中间协议转换。MCP 本身是 JSON-RPC 2.0 的沟通语言Claude Code 启动时通过 stdio 和我们暴露的 server 对话server 这边注册工具列表等待被调用。对 Claude Code 来说这个桥就是一堆普通的外部工具对 VS Code 来说这些工具背后是完完整整的编辑器能力。2.2 协议主干四类工具搞定日常协作v0.1 阶段我只暴露了五个工具覆盖我眼中“终端 AI 与编辑器协作”最核心的动作。第一个是open_file接收路径和可选行号调用vscode.window.showTextDocument打开文件并滚动到指定行。第二个是read_active_buffer读取当前活跃文档的全文或者通过参数截取某个范围。第三个是get_selection返回编辑器当前选中区域的文本和行列坐标。第四个是apply_edits接收一组“从第几行第几个字符替换到第几行第几个字符”的结构然后把改动批量落到当前文档。第五个是run_terminal在集成终端里执行命令这个工具我后来差点砍掉原因后面细说。工具描述一定要写清楚因为 Claude Code 会依据描述来决定何时调用。比如apply_edits的描述里我明确写了“替换目标是当前 VS Code 文档缓冲区而不是磁盘文件”这能引导它优先走桥而不是自己直接写文件。2.3 双向同步不只让 Claude Code 说话也让 VS Code 说话桥的一个隐含问题在于MCP 语义是“Agent 调用工具”本质上只有一条主动链路。那编辑器里的最新内容怎么让 Agent 知道我的做法不是做被动推送而是让 Agent 在需要时主动调用read_active_buffer拉取。这样设计的好处是简单可靠。编辑器的内容变化非常高频如果每次按键都往终端推会淹没 Agent 的上下文。把主动权交给 Agent它只会在准备生成代码之前抓取一次当前状态既保证了信息新鲜度又不产生无谓的流量。后来我在CLAUDE.md里加了一条指令要求它在修改代码前先调用read_active_buffer确认当前内容。这个不起眼的约定把 Agent 的误改率降了一大截。3. 从零搭桥项目初始化到第一个工具调用的完整记录3.1 项目骨架一个最简 VS Code 扩展需要哪些文件说起来这套代码并不复杂传统 VS Code 扩展开发里该有的东西一样不少。package.json里要先声明main入口、activationEvents和contributes.commands。我一开始为了省事只写了onStartupFinished作为激活事件结果扩展在 VS Code 启动后自动加载但终端里的 MCP server 进程可能已经提前启动两边出现竞态。后来我改成同时监听一个自定义命令并保留了onStartupFinished保证扩展任何时候收到调用请求进程都已经就绪。仓库结构上我分成了三个文件extension.ts负责扩展激活和编辑器 API 调用server.ts负责 MCP server 的注册和工具路由protocol.ts专门放工具参数和返回值的类型定义。协议类型单独放的好处是后面要加工具、加字段时不会在编辑器逻辑里到处找类型定义。3.2 注册 MCP server把桥接进 Claude Code 的两种方式Claude Code 对 MCP server 有两种注册方式。一种是当前项目级别一种是用户级别。我强烈建议先做项目级因为桥接工具本身就是跟着项目走的。在项目根目录写一个.mcp.json文件格式长这样{ mcpServers: { bridge: { command: node, args: [/absolute/path/to/dist/server.js], env: { NODE_OPTIONS: --enable-source-maps } } } }command和args的路径建议都写绝对路径。Claude Code 的工作目录不一定等于项目根目录写相对路径很容易出现“明明文件在那就是找不到 server”的诡异问题。写完这个文件重启 VS Code 里的集成终端让 Claude Code 重新加载配置。如果只想在个人环境里全局使用也可以跑一条命令把 server 注册到用户级效果一样只是少了.mcp.json这个可见入口不方便项目团队共享配置。我最终提交到仓库里的是.mcp.json方案这样任何人 clone 下来都能直接复用。3.3 跑通第一个工具调用从“list”到“open file”配置完之后第一件事永远是确认桥是否真的连通。在终端里运行claude mcp list如果看到bridge后面跟着 connected说明进程层面的连接成功了。接下来我会做一个最小的冒烟测试打开一个文件然后对 Claude Code 说“打开当前项目里的 src/index.ts跳到第 42 行”。正常情况下编辑器窗口会立刻把src/index.ts拉起来并滚动到对应位置。这一步成功就代表整条链路已经通了。我当时卡了很久的一个细节是MCP server 在dist/server.js里启动后Claude Code 只是把它当成外部进程并不知道这个进程和 VS Code 扩展有关联。能关联起来的关键在于 server 启动时需要拿到扩展上下文。我的做法是让扩展在激活时把 server 作为子进程拉起来并在启动命令里通过环境变量传一个扩展侧的本地 IPC 句柄。听起来绕实际就是十几行代码的事。4. 最容易翻车的三处细节buffer 同步、选区映射和终端冲突4.1 磁盘文件变了VS Code 里的内容还是旧的这是桥接工具必须面对的模型冲突Claude Code 可以直接写磁盘VS Code 有自己的一套文档缓冲区模型。两套模型不一致时谁该说了算我最初的apply_edits实现是让 Claude Code 把所有编辑结果作为新文件内容整体提交扩展这边直接用WorkspaceEdit替换全文。这种方案在文件小的时候没问题可一旦文件超过几百行编辑器会出现明显的闪烁而且撤销历史会很难看——所有改动变成一步超大撤销。后来我把策略改成“增量替换”。Claude Code 提交的不是全量文件而是一组精确的 Range 替换从某行某字符替换到某行某字符替换成什么内容。扩展收到之后先读当前文档 buffer确认每个 Range 都合法再逐个applyEdit。这个策略有几个好处撤销历史按块保留编辑器不会闪烁而且如果某处 Range 已经过期比如用户手动删过代码扩展可以拒绝那一块并明确报错而不是整个文件被覆盖。实现上要注意一个顺序问题多个编辑块要按从后往前排的顺序执行不然前面的替换会改变后续块的行号坐标。这个坑我在实测里踩过一次Claude Code 一口气提交了五个编辑块前两个成功、第三个开始全部错位。加一个简单的按位置逆序排序就能彻底解决。4.2 Selection 的 Range 坐标系从行列到字节的坑跨工具协作时最隐蔽的问题往往是坐标系的定义。VS Code 的Position是{ line, character }其中 line 从 0 开始character 是 UTF-16 的 code unit 索引而不是字节索引。为什么这个细节要命因为中文、emoji 这类字符在 UTF-16 里可能占两个 code unit。如果 Claude Code 把一个中文注释当做一个字符去数坐标实际传给扩展的行列位置可能偏差很大中英文混排的文件尤其容易炸。我的对策是不让它手算坐标。工具协议里传的是“第几行第几个 code unit”扩展内部用document.offsetAt(position)来转换成绝对偏移量再传给底层 API。在描述里我明确写了“character 是 0-based 的 UTF-16 code unit 索引不是字节数”。这个细节看着小但它决定了工具在处理非纯 ASCII 代码库时的可靠性。4.3 在集成终端里跑命令如何避免“自杀式”调用run_terminal是我最早做进去的几个工具之一因为总觉得“既然都桥接了顺手跑个命令不是理所当然吗”。实际用了两天我就后悔了。最大的问题不是技术而是语义混乱。Claude Code 自己就运行在 VS Code 的集成终端里如果它调用run_terminal再启动另一个 Claude Code 会话就会形成嵌套。更常见的情况是用户让 Agent“帮你跑一下测试”Agent 调用工具在终端执行npm run test但输出又在另一个终端标签页里用户根本不知道结果已经出来了还得手动切过去看。我试过给run_terminal加黑名单把 claude、node、npm run 之类的命令都拦住也试过强制弹确认框。最后发现这些都是在给一个天生模糊的工具打补丁。v0.2 里我把run_terminal从默认工具列表里移除了只在显式配置时启用。砍掉一个看似有用的功能并不丢人保留它反而会让整个桥的概念变得混乱。5. 实测体验与项目取舍哪些场景真能用哪些方向果断砍掉5.1 顺手场景改样式、加注释、重构小函数我把这个桥在一个大约三万行代码的 TypeScript 服务端项目里用了两周每天固定处理几类任务。第一类是高亮配置类修改。比如某个颜色变量要从#1890ff改成#1677ff我只需要在编辑器里选中那一段然后问 Claude Code“把选中区域的配色统一换成一号蓝”。它的流程会变成调用get_selection拿到选中代码调用apply_edits完成替换然后编辑器自动滚动到改动处。整条链路丝滑我不用复制粘贴任何东西。第二类是注释补全。选中一个复杂函数让它给关键分支补注释。它先read_active_buffer拿全函数再生成注释最后批量替换。最舒服的是改完能直接在编辑器里看到结果而不是去看终端里的 diff 猜测位置。第三类是小的函数重构。我把一段手写的重复逻辑选中告诉它“抽成一个独立函数”。它会把替换结果直接写进文档我按 CtrlZ 就能一步步回退。编辑器的原生撤销和桥接的applyEdit配合得天衣无缝这一点是直接写磁盘的方案无论如何都做不到的。5.2 体感一般的场景多文件大改动、上下文过长有顺手的场景自然也有不太行的。多文件改动是我觉得价值最低的方向。当 Claude Code 需要同时改五六个文件时read_active_buffer会把每一个文件的内容都塞进上下文Agent 的注意力会被稀释它开始频繁调用工具确认状态反而比纯终端模式更慢。桥接工具更适合精准小改不适合整库重构。另外一个问题是当前文件特别长时的效率。一个一千多行的文件read_active_buffer返回全量文本Token 消耗立刻上去了。后来我加了范围参数允许它只读当前选区或指定行区间情况改善了不少。这个思路也提醒我任何桥工具都要尽量把“上下文裁剪”的责任放到 Agent 测不能每次都无脑传完整文件。5.3 发布后的反馈大家关心的是“可感知”不是“可控制”把项目挂到社区之后收到的反馈里有一批特别典型的提问。有人问“能不能让它自动打开终端跑命令”有人问“能不能加一个可视化 dashboard 展示所有文件改动”。但我注意到真正高频被讨论的需求其实是“改完我能不能直接看到、能不能直接撤销”。这个反馈和我自己的体感完全一致。大家用桥接工具要的不是一个更复杂的控制台而是让 AI 的每步操作都落在自己熟悉的编辑器上下文里。人眼能盯住的东西是有限的桥接工具的第一价值是降低认知成本而不是增加控制面。基于这个判断v0.2 的路线图里我砍掉了三样东西多 workspace 支持、守护进程持久化、以及可视化 diff 面板。同时把apply_edits的错误提示做得更细每个编辑块失败时都会说明具体原因和当前文件的实际行数而不是笼统报一个“编辑失败”。6. 最后再给想复刻的同学一份避坑清单6.1 扩展没激活桥就静默断连MCP 连接成功但工具调用全失败八成是扩展没真正激活。表现很迷惑claude mcp list显示 connected因为子进程确实起来了但子进程内部等待的扩展上下文还没就绪任何工具调用都会超时。排查方式不算复杂。在 VS Code 的开发者工具里打开“运行扩展”日志看看有没有 pending activation 的警告。我最终是把激活事件改成了onStartupFinished加自定义命令的组合并且在扩展activate函数的第一行打日志确认每个调用发生前激活已经完成。这种日志在调试期看着多余真出问题时能少掉一半排查时间。6.2 .mcp.json 的字段和路径别写错这个坑我犯过一次写的是相对路径./dist/server.js结果在某个工作目录下 Claude Code 就是找不到进程。后来统一用绝对路径世界安静了。另外env字段值得专门研究一下尤其是NODE_OPTIONS或DEBUG环境变量。调试期我会在env里加上DEBUG: mcp:*这样 server 进程会输出 MCP 协议的原始日志工具调用过程中发生了什么一目了然。上线前记得把这个字段删掉否则日志量会大到让人崩溃。6.3 给每次工具调用加 uuid你会感谢这个决定桥接工具调试的难点是Claude Code 里的一句话可能触发好几个工具调用而扩展这边收到的是并发的请求。没有标识时很难把“Agent 请求”和“扩展处理结果”对应起来。我的做法是在 server 收到每个工具请求时生成一个requestId随请求传给扩展侧日志里全程带上这个 ID。排查问题时只要在 Claude Code 这边看到它调了哪个工具再到扩展日志里按 ID 搜几秒钟就能定位问题。这个习惯很基础但它让所有并发问题都变得可见。6.4 v0.1 该做什么不该做什么如果让我重新来一遍v0.1 我会只做四件事open_file、read_active_buffer、get_selection、apply_edits。这四个工具构成的闭环已经涵盖了最核心的协作场景代码量也不大。多终端支持、文件监听、自动刷新、嵌套会话控制这些看起来都很自然但每一个都会引入新的复杂性。工具不在于多而在于每个工具是否可靠。早期版本把链路做窄做稳用户才有信心把你的桥放进日常开发流里而不是因为一两个边缘功能反复出 bug 而放弃。最后再分享一个实际的心得。我在CLAUDE.md里加了一段话大意是“修改代码时优先使用 bridge 工具不要直接写磁盘文件。”就这么一句提示让整个桥接工具的命中率提升了很多。工具做得再好也得让使用它的人知道在什么时候该用。这件事看起来是个配置小技巧实际上是让整个桥接体验成立的关键最后一公里。