ACP、MCP、A2A 都可以使用 JSON-RPC 2.0 作为消息格式,但真正的区别不在消息格式本身,而在:谁发起请求、请求发给谁、对方如何处理、结果如何返回。
与其先讲协议定义,不如从一次真实的通信过程入手。假设有一个 AI Agent,需要调用工具、连接 IDE,或者把任务交给另一个 Agent。
先看一次工具调用里,JSON-RPC 消息长什么样。
一、先理解 JSON-RPC 在其中扮演什么角色
JSON-RPC 本身只负责定义消息格式和调用规则。
例如,Agent 想调用一个工具,可以发送:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}这条消息表达的是:
jsonrpc:使用 JSON-RPC 2.0。id:请求编号,方便匹配响应。method:要调用的方法。params:调用参数。
对方处理完后返回:
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "read_file", "description": "读取文件" } ] }}这里要注意:
JSON-RPC 并不负责实现 tools/list,也不知道什么是 Agent、工具或文件。
它只规定如何表达一次请求和响应。
真正决定 tools/list 是什么、怎么执行、返回什么数据的是上层协议。
二、一次完整的通信过程
下面用三个场景分别理解。
1. MCP:Agent 调用外部工具
场景:让 AI 读取 NEC8 上的一个文件。
假设系统中有三个角色:
- AI Host:运行 AI 模型的应用,例如一个 Agent 客户端。
- MCP Client:Host 内部负责 MCP 通信的模块。
- MCP Server:提供文件读取能力的服务。
NOTEMCP Client 通常不是一个独立应用,而是 Host 内部的通信组件。
sequenceDiagram participant U as 用户 participant H as AI Host participant C as MCP Client participant S as MCP Server U->>H: 读取文件 H->>C: 模型决定调用 read_file C->>S: tools/call(name 为 read_file,arguments 带 path) S->>S: 执行 read_file S-->>C: 文件内容(JSON-RPC result) C-->>H: 工具执行结果 H->>H: 把结果交给模型继续推理 H-->>U: 展示文件内容下面把其中几个关键步骤拆开。
第一步:建立连接。
MCP Client 首先向 MCP Server 发送 initialize:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "my-agent", "version": "1.0.0" } }}Server 返回支持的协议版本、能力等信息。
之后 Client 发送一个通知:
{ "jsonrpc": "2.0", "method": "notifications/initialized"}双方完成初始化。
第二步:发现工具。
Client 发送:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}Server 返回:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "read_file", "description": "读取指定文件", "inputSchema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } } ] }}这里有一个非常重要的细节:
MCP Server 并没有执行读取文件操作,它只是告诉 Client:我有一个叫 read_file 的工具,以及这个工具需要什么参数。
Client 再把工具定义交给模型。
第三步:真正调用工具。
模型判断需要读取文件,于是 Host 让 MCP Client 发送:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/etc/example/config.yaml" } }}MCP Server 收到后:
- 解析 JSON-RPC 请求。
- 找到
tools/call对应的处理函数。 - 根据
name找到read_file工具实现。 - 将
arguments传入工具函数。 - 工具函数读取文件。
- 把结果封装成 JSON-RPC 响应。
最后返回:
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "server:\n port: 8080" } ], "isError": false }}Host 收到结果,再交给模型继续推理。
所以 MCP 的核心流程是:发现能力 → 模型决定调用 → Client 发送调用请求 → Server 执行 → 返回结果。
2. ACP:IDE 与 Agent 之间通信
场景:在 IDE 中打开一个 AI 编程 Agent,让它帮忙修改代码。
这里的角色变了:
- Client:IDE,例如支持 ACP 的编辑器。
- Agent:真正执行编程任务的 AI Agent。
ACP 和 MCP 最大的区别在于:它不是单纯让 Agent 调用工具,而是让 IDE 和 Agent 之间建立一套双向通信机制。
sequenceDiagram participant I as IDE Client participant A as AI Agent I->>A: initialize(能力协商) I->>A: session/new(创建会话) I->>A: session/prompt(发送用户任务) A-->>I: session/update(通知,推送执行进度) A->>I: fs/write_text_file(反向请求 IDE 写入文件) I-->>A: 返回写入结果 A-->>I: session/prompt 的最终响应这里重点看四个阶段。
第一步:初始化。
IDE 向 Agent 发送:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": 1, "clientCapabilities": { "fs": { "readTextFile": true, "writeTextFile": true } }, "clientInfo": { "name": "my-ide", "version": "1.0.0" } }}Agent 返回自己支持的能力。
例如:
- 是否支持恢复会话。
- 是否支持图片输入。
- 是否支持 MCP。
- 是否支持某些交互模式。
第二步:创建会话。
IDE 发送:
{ "jsonrpc": "2.0", "id": 2, "method": "session/new", "params": { "cwd": "/home/user/project", "mcpServers": [] }}Agent 创建一个独立会话,返回:
{ "jsonrpc": "2.0", "id": 2, "result": { "sessionId": "session-001" }}之后的操作就可以围绕这个 sessionId 展开。
第三步:发送用户任务。
IDE 发送:
{ "jsonrpc": "2.0", "id": 3, "method": "session/prompt", "params": { "sessionId": "session-001", "prompt": [ { "type": "text", "text": "帮我修复登录功能的 Bug" } ] }}Agent 开始执行任务。
注意,这时不一定马上返回最终答案。
Agent 可以主动向 IDE 发送通知:
{ "jsonrpc": "2.0", "method": "session/update", "params": { "sessionId": "session-001", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "正在分析登录模块..." } } }}这是一条 Notification:
- 没有
id。 - 不需要 IDE 返回 JSON-RPC 响应。
- 主要用于推送进度、消息和状态。
第四步:Agent 需要 IDE 配合。
这里有一个容易忽略的设计:
假设 Agent 需要写入文件,但文件操作由 IDE 管理。
Agent 可以反过来向 IDE 发起请求:
{ "jsonrpc": "2.0", "id": 10, "method": "fs/write_text_file", "params": { "sessionId": "session-001", "path": "src/login.ts", "content": "..." }}IDE 执行写入后返回:
{ "jsonrpc": "2.0", "id": 10, "result": {}}这里出现了一个关键现象:
ACP 不只是 Client 调用 Agent,Agent 也可以调用 Client 暴露出来的方法。
这就是 ACP 的双向 RPC 通信。
它让 IDE 和 Agent 各自负责自己掌握的能力。
- IDE 管理编辑器、文件访问、终端等客户端能力。
- Agent 管理模型推理、任务执行、会话状态等 Agent 能力。
3. A2A:Agent 与 Agent 之间通信
场景:有两个不同的 Agent。
- Research Agent:负责资料搜索和研究。
- Coding Agent:负责根据研究结果编写代码。
Research Agent 无法直接完成全部工作,于是它把任务交给 Coding Agent。
这里的角色是:
- A2A Client:任务发起方的 Agent。
- A2A Server:接收任务的 Agent。
- Agent Card:描述 Agent 能力的名片。
sequenceDiagram participant R as Research Agent participant C as Coding Agent Note over R: 读取 Coding Agent 的 Agent Card R->>C: message/send(HTTP POST 到 Agent Card 中的地址) C-->>R: 返回 Task(task-001,状态 working) R->>C: tasks/get(轮询任务状态) C-->>R: Task 状态与 artifacts(最终结果)下面也拆开看。
第一步:发现另一个 Agent。
Research Agent 需要知道 Coding Agent 在哪里,以及它能做什么。
A2A 使用 Agent Card 描述 Agent 的信息。
简化示例:
{ "name": "Coding Agent", "description": "负责软件开发和代码修改", "url": "https://coding.example.com", "skills": [ { "id": "code-generation", "name": "代码生成", "description": "根据需求生成代码" } ], "capabilities": { "streaming": true }}Research Agent 通过 Agent Card 知道:
- 对方是谁。
- 对方提供什么能力。
- 应该向哪个地址发送请求。
- 对方是否支持流式通信。
NOTEAgent Card 是能力描述和服务发现机制,不是 JSON-RPC 请求本身。
第二步:发送任务。
Research Agent 向 Coding Agent 发送:
{ "jsonrpc": "2.0", "id": 1, "method": "message/send", "params": { "message": { "messageId": "msg-001", "role": "user", "parts": [ { "kind": "text", "text": "使用 Go 实现一个 HTTP 文件服务" } ] } }}底层实际发生的事情是:
POST /a2aContent-Type: application/json请求体就是上面的 JSON-RPC 数据。
这里要特别注意:
A2A 的 JSON-RPC 不是直接通过 HTTP 请求方法 message/send 访问的。
它是:
- HTTP 方法:
POST - HTTP 地址:Agent Card 中提供的服务地址
- JSON-RPC method:
message/send
这三个东西属于不同层次。
第三步:接收任务。
Coding Agent 收到请求后,不一定马上完成任务。
它可能创建一个 Task:
{ "jsonrpc": "2.0", "id": 1, "result": { "id": "task-001", "contextId": "context-001", "status": { "state": "working" } }}这代表:
task-001:这次任务的唯一标识。context-001:这次交互所属的上下文。working:Agent 正在执行任务。
Research Agent 可以继续调用:
{ "jsonrpc": "2.0", "id": 2, "method": "tasks/get", "params": { "id": "task-001" }}Coding Agent 返回当前任务状态。
第四步:获取最终结果。
Coding Agent 完成后,Task 状态变为:
{ "jsonrpc": "2.0", "id": 2, "result": { "id": "task-001", "contextId": "context-001", "status": { "state": "completed" }, "artifacts": [ { "artifactId": "artifact-001", "parts": [ { "kind": "text", "text": "HTTP 文件服务已经实现" } ] } ] }}Research Agent 获得结果后,就可以继续自己的工作。
当然,如果任务需要很长时间,也可以使用 message/stream,通过 SSE 持续接收进度,而不必反复轮询 tasks/get。
所以 A2A 的核心流程是:发现 Agent → 查看能力 → 发送任务 → 创建 Task → 跟踪任务 → 获取结果。
三、把三个协议放在一起看
把三个过程放在一起,差别就清楚了。
| 对比 | MCP | ACP | A2A |
|---|---|---|---|
| 通信双方 | Host/Client ↔ Server | IDE ↔ Agent | Agent ↔ Agent |
| 核心目标 | 提供上下文和工具能力 | 连接编程 Agent 与客户端 | Agent 之间协作 |
| 主要操作 | tools/list、tools/call | session/new、session/prompt | message/send、tasks/get |
| 核心对象 | Tool、Resource、Prompt | Session、Prompt、Update | Message、Task、Artifact |
| 通信方向 | 双向 RPC | 双向 RPC | 通常由 Client 发起任务请求 |
| 常见传输 | stdio、Streamable HTTP | stdio 等 | HTTP(S)、SSE |
这里还有一个值得注意的区别:它们虽然都采用 JSON-RPC,但传输层并不完全一样。
| 协议 | JSON-RPC 消息怎么传输 |
|---|---|
| MCP | stdio 或 HTTP |
| ACP | 常见为 stdio |
| A2A | HTTP POST,流式时使用 SSE |
例如:
- stdio:一个进程向另一个进程的标准输入输出发送消息。
- HTTP:通过网络发送请求和响应。
- SSE:服务端可以持续向客户端推送事件。
JSON-RPC 只负责消息格式,不负责决定使用哪种传输方式。
四、真正理解通信:收到 JSON 后,程序内部做了什么?
这是最重要的一层。
假设 MCP Server 收到:
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/tmp/test.txt" } }}它内部并不是让 JSON-RPC 自动执行工具,而是由程序自己实现一个请求分发器(Dispatcher)。
可以把它理解成下面的代码:
func HandleRequest(req Request) Response { switch req.Method { case "tools/list": return listTools(req)
case "tools/call": return callTool(req)
case "resources/list": return listResources(req)
default: return methodNotFound(req) }}而 callTool 内部还会继续分发:
func callTool(req Request) Response { name := req.Params.Name
switch name { case "read_file": result := readFile(req.Params.Arguments) return success(req.ID, result)
default: return errorResponse(req.ID) }}实际实现通常会使用注册表、接口和参数校验,而不一定直接写 switch。
这说明了整个过程:
flowchart TD A["JSON-RPC 请求"] --> B["解析 JSON,提取 method 和 params"] B --> C["Dispatcher:按 method 找到对应处理函数"] C --> D1["tools/list → listTools"] C --> D2["tools/call → callTool(再按工具名分发)"] C --> D3["resources/list → listResources"] D1 --> E["执行实际业务逻辑"] D2 --> E D3 --> E E --> F["封装 result / error,使用原 id 返回响应"]ACP 和 A2A 也是类似的思路,只不过它们的 method 对应的是不同的业务处理函数。
例如:
- ACP 的
session/prompt:进入 Agent 的会话处理逻辑。 - MCP 的
tools/call:进入工具执行逻辑。 - A2A 的
message/send:进入 Agent 的任务处理逻辑。
所以,JSON-RPC 可以理解为统一的调用接口格式,而 ACP、MCP、A2A 定义了这套接口具体有哪些方法,以及每个方法应该做什么。
五、三个通信过程的一句话总结
- MCP:AI Host 决定调用工具,MCP Server 提供并执行工具。
- ACP:IDE Client 提供文件、终端、交互,AI Agent 负责推理、会话和任务。
- A2A:Agent A 发起任务,Agent B 执行任务并返回结果。
下一步值得重点理解的问题是:
为什么这些协议需要 JSON-RPC,而不是直接使用 RESTful API?
理解这个问题之后,就能真正理解为什么这些协议会设计成 tools/call、session/prompt、message/send 这种调用方式,以及为什么需要 Notification、双向请求和流式通信。