如果你最近在用 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 的文档模型上,而不是绕过扩展直接写磁盘。这样编辑器能正确感知变化,用户可以立刻看到改了什么、改在哪里,也能直接按 Ctrl+Z 撤销。
方向 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 猜测位置。
第三类是小的函数重构。我把一段手写的重复逻辑选中,告诉它“抽成一个独立函数”。它会把替换结果直接写进文档,我按 Ctrl+Z 就能一步步回退。编辑器的原生撤销和桥接的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 工具,不要直接写磁盘文件。”就这么一句提示,让整个桥接工具的命中率提升了很多。工具做得再好,也得让使用它的人知道在什么时候该用。这件事看起来是个配置小技巧,实际上是让整个桥接体验成立的关键最后一公里。