2937 字
15 分钟
ACP、MCP、A2A 如何复用 JSON-RPC:一次通信过程看懂三者区别

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:提供文件读取能力的服务。
NOTE

MCP 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 收到后:

  1. 解析 JSON-RPC 请求。
  2. 找到 tools/call 对应的处理函数。
  3. 根据 name 找到 read_file 工具实现。
  4. 将 arguments 传入工具函数。
  5. 工具函数读取文件。
  6. 把结果封装成 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 知道:

  • 对方是谁。
  • 对方提供什么能力。
  • 应该向哪个地址发送请求。
  • 对方是否支持流式通信。
NOTE

Agent 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 /a2a
Content-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 → 跟踪任务 → 获取结果。

三、把三个协议放在一起看#

把三个过程放在一起,差别就清楚了。

对比MCPACPA2A
通信双方Host/Client ↔ ServerIDE ↔ AgentAgent ↔ Agent
核心目标提供上下文和工具能力连接编程 Agent 与客户端Agent 之间协作
主要操作tools/list、tools/callsession/new、session/promptmessage/send、tasks/get
核心对象Tool、Resource、PromptSession、Prompt、UpdateMessage、Task、Artifact
通信方向双向 RPC双向 RPC通常由 Client 发起任务请求
常见传输stdio、Streamable HTTPstdio 等HTTP(S)、SSE

这里还有一个值得注意的区别:它们虽然都采用 JSON-RPC,但传输层并不完全一样。

协议JSON-RPC 消息怎么传输
MCPstdio 或 HTTP
ACP常见为 stdio
A2AHTTP 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、双向请求和流式通信。

ACP、MCP、A2A 如何复用 JSON-RPC:一次通信过程看懂三者区别
https://blog.xqcherry.top/posts/ai/acp-mcp-a2a-json-rpc/
作者
xqcherry
发布于
2026-11-05
许可协议
CC BY-NC-SA 4.0