首页 / 资讯中心 / 文章详情

Cursor智能体开发:命令行界面支持 ACP 的 JSON-RPC 配置与验证

Cursor智能体开发:命令行界面支持 ACP 的 JSON-RPC 配置与验证 ★ FEATURED ARTICLE
1. Cursor 智能体开发命令行界面支持 ACP 的 JSON-RPC 配置与验证如果你正在做编辑器插件、终端工具或者自研 IDE想让 Cursor 的智能体能力跑在自己的客户端里那 ACPAgent Client Protocol就是那条对外的标准通道。简单说ACP 是 Cursor 命令行界面暴露出来的一套 JSON-RPC 2.0 协议你启动agent acp之后它就在 stdin/stdout 上跟你对话你发请求、它回响应模型流式输出、工具授权、待办更新这些事件都会以通知形式推给你。它适合谁适合想构建自定义客户端、把 Cursor Agent 接进 Neovim、Zed、JetBrains 或者自己写的编辑器里的开发者也适合本地调试智能体调用链路的同学。我这次的目标很明确不依赖 Cursor 桌面应用纯命令行侧把 ACP 的 JSON-RPC 通信链路跑通确认一次完整的请求-响应往返。整个过程分三块——先把 ACP 服务端拉起来并完成鉴权再写一个最小客户端发initialize、authenticate、session/new、session/prompt最后看流式通知和stopReason是否正确回来。踩过的坑主要集中在鉴权字段和 stdio 分帧上下面会逐个拆开讲。需要说明的是ACP 面向的是自定义客户端和集成场景。如果你只是日常在终端里用 Cursor直接跑交互式的agent命令就够了不必绕 ACP 这一层。ACP 的价值在于把智能体能力协议化让别的程序能编程式地驱动它。2. TaoToken 前置为 ACP 链路准备可用的模型接入在动手写 ACP 客户端之前得先解决一个现实问题智能体背后要有模型可用。Cursor 命令行界面本身支持通过 API Key 或 Auth Token 完成认证但如果你在做多模型对比、或者想把请求路由到统一的接入层用 TaoToken 这类兼容 OpenAI 协议的服务会更灵活。它的 API 地址是https://taotoken.net/api官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content模型对话入口、Coding Plan、控制台和 API Keys 都能在站内找到。为什么在 ACP 教程里要提这个因为 ACP 的authenticate方法声明的是cursor_login实际使用中你可以通过现有的命令行界面认证方式在启动前预先完成认证比如agent login --api-key或者环境变量CURSOR_API_KEY。当你需要把模型请求指向自建或第三方接入层时可以在根命令行界面命令里传递 endpoint 和 TLS 选项形如agent -e https://api2.cursor.sh acp。这里的 endpoint 就是你要替换的关键字段。我建议的准备工作是这样先在 TaoToken 控制台创建一个 API Key记下 Base URL 和你要用的 Model ID。然后在本地把它写进环境变量避免明文散落在脚本里。你可以这样操作export CURSOR_API_KEYsk-你的TaoToken密钥 export CURSOR_API_ENDPOINThttps://taotoken.net/api注意ACP 模式下团队级 MCP 服务器不受支持但项目级或用户级的.cursor/mcp.json是可以用的。如果你打算在 ACP 会话里挂 MCP 工具提前在项目目录下把.cursor/mcp.json配好启动agent后批准要使用的服务器即可。这一步不做也不影响最小链路验证但做集成时迟早要碰。另外提醒一句鉴权信息不要写进会提交到版本库的文件里。用环境变量或者本地未跟踪的配置文件是更稳妥的做法。下面进入可复制配置环节。3. 可复制配置ACP 端点、鉴权字段与客户端骨架这一节给你可以直接抄的配置片段。先看 ACP 服务端的启动方式再看客户端侧的 JSON-RPC 消息结构最后给一份完整的 Node.js 最小客户端。启动 ACP 服务端最基础的一条命令是agent acp如果你要指定 endpoint 和鉴权可以组合成这样agent --api-key $CURSOR_API_KEY -e $CURSOR_API_ENDPOINT acp传输层是 stdio协议封套是 JSON-RPC 2.0消息分帧按行分隔——每行一条 JSON。客户端把请求和通知写进 stdinCursor 命令行界面把响应和通知写进 stdout日志可能写到 stderr。这个分帧规则很关键后面排障会用到。ACP 会话的典型请求流程是initialize→ 用methodId: cursor_login做authenticate→session/new或session/load→session/prompt→ 处理session/update流式通知 → 通过session/request_permission返回决策 → 可选发送session/cancel。下面是一份可直接运行的 Node.js 最小客户端保存为acp-minimal-client.mjsimport { spawn } from node:child_process; import readline from node:readline; const agent spawn(agent, [acp], { stdio: [pipe, pipe, inherit] }); let nextId 1; const pending new Map(); function send(method, params) { const id nextId; agent.stdin.write(JSON.stringify({ jsonrpc: 2.0, id, method, params }) \n); return new Promise((resolve, reject) pending.set(id, { resolve, reject })); } function respond(id, result) { agent.stdin.write(JSON.stringify({ jsonrpc: 2.0, id, result }) \n); } const rl readline.createInterface({ input: agent.stdout }); rl.on(line, line { const msg JSON.parse(line); if (msg.id (msg.result || msg.error)) { const waiter pending.get(msg.id); if (!waiter) return; pending.delete(msg.id); msg.error ? waiter.reject(msg.error) : waiter.resolve(msg.result); return; } if (msg.method session/update) { const update msg.params?.update; if (update?.sessionUpdate agent_message_chunk update.content?.text) { process.stdout.write(update.content.text); } return; } if (msg.method session/request_permission) { respond(msg.id, { outcome: { outcome: selected, optionId: allow-once } }); } }); const init async () { await send(initialize, { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }, clientInfo: { name: acp-minimal-client, version: 0.1.0 } }); await send(authenticate, { methodId: cursor_login }); const { sessionId } await send(session/new, { cwd: process.cwd(), mcpServers: [] }); const result await send(session/prompt, { sessionId, prompt: [{ type: text, text: Say hello in one sentence. }] }); console.log(\n\n[stopReason${result.stopReason}]); }; init().finally(() { agent.stdin.end(); agent.kill(); });这份骨架里initialize的clientCapabilities把文件读写和终端都关掉了先跑通纯文本链路。等你确认通信没问题再按需打开fs.readTextFile、fs.writeTextFile、terminal这些能力。session/new的mcpServers传空数组表示这次不挂 MCP。如果你用 Neovim 的 avante.nvim配置里provider设为cursor、mode设为agentic、command指向agent可执行文件、auth_method用cursor_login就能通过 ACP 把 Neovim 接到 Cursor 的 agent 上。关键字段是command和args默认安装路径是~/.local/bin/agent装别处就改路径。4. 验证请求一次完整的 JSON-RPC 往返与成功结果配置就绪后跑一次验证。先确认agent在 PATH 里然后执行node acp-minimal-client.mjs预期你会看到模型流式吐出的文本最后打印一行[stopReason...]。如果一切正常stopReason通常是end_turn之类的正常结束原因说明整条链路——启动、鉴权、建会话、发提示、收流式通知、收最终响应——全部走通了。想更细地看协议交互可以在客户端里把收到的每一行原始消息打出来。临时加一行日志rl.on(line, line { console.error([RAW], line); const msg JSON.parse(line); // ... 后续逻辑不变 });这样 stderr 会显示每一条 JSON-RPC 消息你能清楚看到initialize的响应里带了什么、session/new返回的sessionId长什么样、session/update通知的sessionUpdate字段是agent_message_chunk还是别的类型。关于权限请求当工具需要授权时Cursor 会发session/request_permission客户端应返回allow-once、allow-always或reject-once之一。如果客户端不响应权限请求工具执行可能会被阻塞。上面骨架里我直接回了allow-once验证阶段够用生产客户端里你应该把决策交给用户。Cursor 还会发一些扩展方法。阻塞类的有cursor/ask_question向用户提多项选择题和cursor/create_plan请求批准方案你的客户端必须返回 JSON-RPC 响应否则智能体会一直等。通知类的有cursor/update_todos、cursor/task、cursor/generate_image发出后无需回复客户端可以显示但不必响应。验证阶段可以先忽略这些但做完整集成时得处理。一个成功的验证结果应该满足三点stdout 上有模型文本输出最终响应里stopReason字段存在且合理stderr 里没有解析错误或未处理的异常。三点都满足说明命令行侧智能体调用按预期工作。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth实际跑的时候报错基本集中在几个地方。下面按真实错误对照排查。401 Unauthorized。最常见的原因是鉴权没做或做晚了。ACP 要求先authenticate再session/new顺序错了会 401。另外检查CURSOR_API_KEY是否真的注入到了子进程环境里——spawn默认继承父进程环境但如果你手动传了env对象又漏了 key就会 401。用 TaoToken 接入时确认 Base URL 和 Key 是配套的别把 A 家的 Key 配到 B 家的 endpoint 上。local proxy failed。这个通常出现在 endpoint 配置不对或者网络层拦截时。检查-e后面的地址是否可达TLS 选项是否匹配。如果你在客户端里自己拼 endpoint注意别把路径拼重了。这个错误也可能和本地环境变量里的代理设置冲突有关把无关的代理变量清掉再试。reading choices 相关报错。这类错误多半出在cursor/ask_question的响应格式上。请求里questions是数组每个元素有id、prompt、options响应必须是{ outcome: answered, answers: [...] }或{ outcome: skipped }或{ outcome: cancelled }。如果你返回的selectedOptionIds对不上请求里的options[].id客户端解析就会出问题。对照请求里的 id 逐个填别自己造。OAuth 相关报错。ACP 声明的认证方式是cursor_login如果你之前用 OAuth 登录过、token 过期了authenticate会失败。解决办法是先在终端里跑agent login重新完成登录认证或者用--api-key/--auth-token显式传入凭据。用CURSOR_AUTH_TOKEN环境变量也行。注意 token 和 api-key 是两种不同的凭据别混用。消息解析失败 / JSON.parse 抛异常。九成是分帧问题。ACP 按行分隔 JSON每行一条消息。如果你的 stdout 读取没有按行切或者把 stderr 的日志混进了 stdout 解析就会解析失败。确保readline绑的是agent.stdout日志走 stderr。工具执行卡住不动。检查是不是没响应session/request_permission。客户端不响应权限请求工具执行会被阻塞表现就是智能体停在那里不往下走。补上权限响应即可。排查时有个通用手法把原始消息全打到 stderr对照 JSON-RPC 的id看请求和响应是否配对。id对不上说明你的pendingMap 管理有问题通常是并发发送时覆盖了。6. 语义一致 CTA把 ACP 链路接到你的开发流里链路跑通之后下一步就是把它接到真实开发流里。如果你要继续调试接入细节去 TaoToken 的 API Keys 页面拿密钥、对照接入文档配 endpoint地址是https://taotoken.net/api-keys和https://taotoken.net/doc两个入口都在站内。想先验证模型本身是否正常用模型对话页面发一条消息试试https://taotoken.net/chat。如果你打算长期做编码类智能体、把 ACP 客户端当成日常工具Coding Plan 会更合适入口在https://taotoken.net/coding-plan。回到 ACP 本身构建集成的核心步骤就那几步把agent acp作为子进程启动用 JSON-RPC 通过 stdin/stdout 通信处理session/update通知来显示流式响应在工具需要批准时响应session/request_permission可选地实现 Cursor 扩展方法来提供更丰富的用户体验。上面那份 Node.js 客户端就是可运行的参考实现你可以直接拿它当起点逐步加上文件读写、终端能力和 MCP 支持。最后留个实用技巧调试阶段把clientCapabilities里的能力全关掉先保证纯文本链路稳定再一项一项打开。每打开一项就跑一次验证出问题能立刻定位到是哪项能力引入的。这比一上来全开、然后在一堆报错里猜要高效得多。
阅读完成 · 觉得有帮助?
咨询建站