本文档基于对 VS Code 扩展的逆向工程,解释了 Claude Code IDE 集成背后的协议和架构。使用本指南可以构建自己的集成或理解官方实现的工作原理。
Claude Code 扩展在你的 IDE 中创建 WebSocket 服务器,Claude 连接到该服务器。它们使用 MCP (Model Context Protocol) 的 WebSocket 变体,这是 Claude 独有的。IDE 写入一个包含连接信息的锁文件,设置一些环境变量,Claude 启动时自动连接。
当你从 IDE 启动 Claude Code 时,发生以下过程:
扩展启动一个监听来自 Claude 连接的 WebSocket 服务器,端口随机选择(10000-65535)。
IDE 将发现文件写入 ~/.claude/ide/[port].lock:
{
"pid": 12345, // IDE 进程 ID
"workspaceFolders": ["/path/to/project"], // 打开的文件夹
"ideName": "VS Code", // 或 "Neovim"、"IntelliJ" 等
"transport": "ws", // WebSocket 传输
"authToken": "550e8400-e29b-41d4-a716-446655440000" // 随机 UUID 用于认证
}启动 Claude 时,IDE 设置:
CLAUDE_CODE_SSE_PORT: WebSocket 服务器端口ENABLE_IDE_INTEGRATION: 设置为 "true"
Claude 读取锁文件,从环境变量中找到匹配的端口,然后连接到 WebSocket 服务器。
当 Claude 连接到 IDE 的 WebSocket 服务器时,必须使用锁文件中的令牌进行认证。认证通过自定义 WebSocket 头部进行:
x-claude-code-ide-authorization: 550e8400-e29b-41d4-a716-446655440000
IDE 根据锁文件中的 authToken 值验证此头部。如果令牌不匹配,连接被拒绝。
通信使用 WebSocket 和 JSON-RPC 2.0 消息:
{
"jsonrpc": "2.0",
"method": "method_name",
"params": {
/* 参数 */
},
"id": "unique-id" // 用于期望响应的请求
}该协议基于 MCP (Model Context Protocol) 规范 2025-03-26,但使用 WebSocket 传输而不是 stdio/HTTP。
这些是 IDE 发送的通知,用于保持 Claude 的信息同步:
当用户的选择发生变化时发送:
{
"jsonrpc": "2.0",
"method": "selection_changed",
"params": {
"text": "选中的文本内容",
"filePath": "/absolute/path/to/file.js",
"fileUrl": "file:///absolute/path/to/file.js",
"selection": {
"start": { "line": 10, "character": 5 },
"end": { "line": 15, "character": 20 },
"isEmpty": false
}
}
}当用户显式地将选择作为上下文发送时:
{
"jsonrpc": "2.0",
"method": "at_mentioned",
"params": {
"filePath": "/path/to/file",
"lineStart": 10,
"lineEnd": 20
}
}根据 MCP 规范,Claude 应该能够调用工具,但当前的实现主要是单向的(IDE → Claude)。
{
"jsonrpc": "2.0",
"id": "request-123",
"method": "tools/call",
"params": {
"name": "openFile",
"arguments": {
"filePath": "/path/to/file.js"
}
}
}{
"jsonrpc": "2.0",
"id": "request-123",
"result": {
"content": [{ "type": "text", "text": "文件成功打开" }]
}
}VS Code 扩展注册了 12 个 Claude 可以调用的工具。完整规范如下:
描述:在编辑器中打开文件并可选择性地选择一段文本
输入:
{
"filePath": "/path/to/file.js",
"preview": false,
"startText": "function hello",
"endText": "}",
"selectToEndOfLine": false,
"makeFrontmost": true
}filePath(string, 必需): 要打开的文件路径preview(boolean, 默认: false): 是否以预览模式打开startText(string, 可选): 查找选择起始位置的文本模式endText(string, 可选): 查找选择结束位置的文本模式selectToEndOfLine(boolean, 默认: false): 将选择扩展到行尾makeFrontmost(boolean, 默认: true): 使文件成为活动编辑器标签
输出:当 makeFrontmost=true 时,返回简单消息:
{
"content": [
{
"type": "text",
"text": "Opened file: /path/to/file.js"
}
]
}当 makeFrontmost=false 时,返回详细 JSON:
{
"content": [
{
"type": "text",
"text": "{\"success\": true, \"filePath\": \"/absolute/path/to/file.js\", \"languageId\": \"javascript\", \"lineCount\": 42}"
}
]
}描述:为文件打开 git diff(阻塞操作)
输入:
{
"old_file_path": "/path/to/original.js",
"new_file_path": "/path/to/modified.js",
"new_file_contents": "// 修改后的内容...",
"tab_name": "建议的更改"
}old_file_path(string): 原始文件路径new_file_path(string): 新文件路径new_file_contents(string): 新文件的内容tab_name(string): diff 视图的标签名称
输出:返回 MCP 格式的响应:
{
"content": [
{
"type": "text",
"text": "FILE_SAVED"
}
]
}或
{
"content": [
{
"type": "text",
"text": "DIFF_REJECTED"
}
]
}取决于用户是保存还是拒绝 diff。
描述:获取活动编辑器中的当前文本选择
输入:无
输出:返回 JSON 字符串化的选择数据:
{
"content": [
{
"type": "text",
"text": "{\"success\": true, \"text\": \"选中的内容\", \"filePath\": \"/path/to/file\", \"selection\": {\"start\": {\"line\": 0, \"character\": 0}, \"end\": {\"line\": 0, \"character\": 10}}}"
}
]
}或当没有活动编辑器时:
{
"content": [
{
"type": "text",
"text": "{\"success\": false, \"message\": \"未找到活动编辑器\"}"
}
]
}描述:获取最近的文本选择(即使不在活动编辑器中)
输入:无
输出:JSON 字符串化的选择数据或 {success: false, message: "无可用选择"}
描述:获取当前打开的编辑器的信息
输入:无
输出:返回 JSON 字符串化的打开标签数组:
{
"content": [
{
"type": "text",
"text": "{\"tabs\": [{\"uri\": \"file:///path/to/file\", \"isActive\": true, \"label\": \"filename.ext\", \"languageId\": \"javascript\", \"isDirty\": false}]}"
}
]
}描述:获取 IDE 中当前打开的所有工作区文件夹
输入:无
输出:返回 JSON 字符串化的工作区信息:
{
"content": [
{
"type": "text",
"text": "{\"success\": true, \"folders\": [{\"name\": \"project-name\", \"uri\": \"file:///path/to/workspace\", \"path\": \"/path/to/workspace\"}], \"rootPath\": \"/path/to/workspace\"}"
}
]
}描述:从 VS Code 获取语言诊断信息
输入:
{
"uri": "file:///path/to/file.js"
}uri(string, 可选): 要获取诊断信息的文件 URI。如果未提供,则获取所有文件的诊断信息。
输出:返回 JSON 字符串化的每个文件的诊断数组:
{
"content": [
{
"type": "text",
"text": "[{\"uri\": \"file:///path/to/file\", \"diagnostics\": [{\"message\": \"错误消息\", \"severity\": \"Error\", \"range\": {\"start\": {\"line\": 0, \"character\": 0}}, \"source\": \"typescript\"}]}]"
}
]
}描述:检查文档是否有未保存的更改(是否为脏状态)
输入:
{
"filePath": "/path/to/file.js"
}filePath(string, 必需): 要检查的文件路径
输出:返回文档脏状态:
{
"content": [
{
"type": "text",
"text": "{\"success\": true, \"filePath\": \"/path/to/file.js\", \"isDirty\": true, \"isUntitled\": false}"
}
]
}或当文档未打开时:
{
"content": [
{
"type": "text",
"text": "{\"success\": false, \"message\": \"文档未打开: /path/to/file.js\"}"
}
]
}描述:保存有未保存更改的文档
输入:
{
"filePath": "/path/to/file.js"
}filePath(string, 必需): 要保存的文件路径
输出:返回保存操作结果:
{
"content": [
{
"type": "text",
"text": "{\"success\": true, \"filePath\": \"/path/to/file.js\", \"saved\": true, \"message\": \"文档保存成功\"}"
}
]
}或当文档未打开时:
{
"content": [
{
"type": "text",
"text": "{\"success\": false, \"message\": \"文档未打开: /path/to/file.js\"}"
}
]
}描述:按名称关闭标签
输入:
{
"tab_name": "filename.js"
}tab_name(string, 必需): 要关闭的标签名称
输出:返回 {content: [{type: "text", text: "TAB_CLOSED"}]}
描述:关闭编辑器中的所有 diff 标签
输入:无
输出:返回 {content: [{type: "text", text: "CLOSED_${count}_DIFF_TABS"}]}
描述:在当前笔记本文件的 Jupyter 内核中执行 Python 代码
输入:
{
"code": "print('Hello, World!')"
}code(string, 必需): 要在内核上执行的代码
输出:返回混合内容类型的执行结果:
{
"content": [
{
"type": "text",
"text": "Hello, World!"
},
{
"type": "image",
"data": "base64_encoded_image_data",
"mimeType": "image/png"
}
]
}注意事项:
- 所有执行的代码将在调用之间持久化,除非重启内核
- 除非明确要求,否则避免声明变量或修改内核状态
- 仅在使用 Jupyter 笔记本时可用
- 可以返回多种内容类型,包括文本输出和图像
- 大多数工具遵循驼峰命名法,除了
close_tab(使用蛇形命名法) openDiff工具是阻塞的,会等待用户交互- 工具返回带有内容数组的 MCP 格式响应
- 所有模式在 VS Code 扩展中使用 Zod 验证
- 选择相关的工具使用当前编辑器状态
最小可行实现如下:
-- 仅监听 localhost(重要!)
local server = create_websocket_server("127.0.0.1", random_port)-- ~/.claude/ide/[port].lock
local auth_token = generate_uuid() -- 生成随机 UUID
local lock_data = {
pid = vim.fn.getpid(),
workspaceFolders = { vim.fn.getcwd() },
ideName = "YourEditor",
transport = "ws",
authToken = auth_token
}
write_json(lock_path, lock_data)export CLAUDE_CODE_SSE_PORT=12345
export ENABLE_IDE_INTEGRATION=true
claude # Claude 现在会连接!-- 在 WebSocket 握手时验证认证
function validate_auth(headers)
local auth_header = headers["x-claude-code-ide-authorization"]
return auth_header == auth_token
end
-- 发送选择更新
send_message({
jsonrpc = "2.0",
method = "selection_changed",
params = { ... }
})
-- 实现工具(如果需要)
register_tool("openFile", function(params)
-- 打开文件逻辑
return { content = {{ type = "text", text = "完成" }} }
end)始终仅绑定到 localhost (127.0.0.1)! 这确保 WebSocket 服务器不会暴露到网络。
有了这些协议知识,你可以:
- 为任何编辑器构建集成
- 创建连接到现有 IDE 扩展的代理
- 使用自定义工具扩展协议
- 在不同的 AI 助手和 IDE 之间构建桥梁
WebSocket MCP 变体目前是 Claude 特有的,但这些概念可以适配到其他 AI 编码助手。