
么都选了它
一、为什么是 JSON-RPC
一个 2005 年的老协议
JSON-RPC 的第一版诞生在 2005 年,比 REST 这个词还早几年。它的设计目标极其简单:让两个程序通过 JSON 互相调用对方的方法。
一个 JSON-RPC 消息就是一个 JSON 对象。只有三种类型:
Request(请求)------我要调你一个方法,等你回复:
json
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "add",
"params": {"a": 1, "b": 2}
}
Response(响应)------回复一个 id 对应的请求,要么 result 要么 error:
json
{
"jsonrpc": "2.0",
"id": "req-001",
"result": 3
}
json
{
"jsonrpc": "2.0",
"id": "req-001",
"error": {"code": -32602, "message": "Invalid params"}
}
Notification(通知)------单向推送,不带 id,不期望回复:
json
{
"jsonrpc": "2.0",
"method": "log",
"params": {"level": "info", "msg": "hello"}
}
整个协议就这么多。 没有 schema、没有版本协商、没有连接握手、没有 content-type。一个信封格式 + 三种消息类型 + 一组约定的错误码,完事。

跟 REST、gRPC 比一比
JSON-RPC 2.0 跟另外两个主流协议(REST、gRPC)的定位不一样:
| 协议 | 抽象层级 | 传输 | 双向支持 | 典型场景 |
|---|---|---|---|---|
| REST | 资源(HTTP verb + URL) | HTTP | 单向请求-响应 | Web API |
| gRPC | 方法(Protobuf schema) | HTTP/2 | 支持流式 | 微服务 |
| JSON-RPC 2.0 | 方法(JSON) | 任意 | 原生双向 | 长连接对等通信 |
核心区别在"抽象"和"双向"两件事上。
REST 的抽象是"资源"------一切都是 GET/POST/PUT/DELETE 某个 URL。这套抽象对 Web 非常自然,但对"我调你一个方法、你调我一个方法"的场景很别扭。你见过哪个 RPC 调用要写成 POST /sessions/abc/prompts 还要关心 HTTP status code 的?
gRPC 的抽象是"方法",更接近 RPC 本质。但它强依赖 Protobuf schema 和 HTTP/2,重------你要先写 .proto 文件、生成代码、装依赖,才能跑起来。
JSON-RPC 2.0 的抽象也是"方法"------直接传 method 名和 params,没有 schema 约束。传输完全不管 ------stdio、TCP、WebSocket、HTTP、进程管道都行。双向原生支持------Notification 就是单向推送,Request/Response 是对称的请求-响应。
这就是它在 AI Agent 圈翻红的根本原因:Agent 跟宿主之间的通信,恰好是"长连接 + 双向 + 方法抽象 + 简单"------这四件事 JSON-RPC 一次都满足了。
跟 WebSocket 的区别
很多人会把 JSON-RPC 跟 WebSocket 搞混。它们不是一回事:
- WebSocket 是传输层协议------它管的是"如何在 TCP 之上做双向字节流"
- JSON-RPC 是应用层协议------它管的是"消息长什么样"
两者经常一起用:WebSocket 提供双向管道,JSON-RPC 提供消息格式。但它们也可以分开:JSON-RPC 能跑在 stdio、TCP、WebSocket 上;WebSocket 也能承载别的应用协议。
类比一下:TCP 是公路,HTTP 是跑在上面的卡车。JSON-RPC 和 WebSocket 的关系也差不多------WebSocket 是路,JSON-RPC 是卡车,可以换路也可以换车。
二、为什么 Agent 圈选了它
把上面那张协议对比表再看一遍,你会发现 REST 和 gRPC 在 AI Agent 场景下有几个硬伤:
硬伤一:REST 没法"反向调"
在 ACP 的场景下,Agent 想反向调 Client 的文件系统、终端、鉴权。REST 做不到------HTTP 天然是"客户端发起、服务端响应",服务端要推消息得靠 SSE 或长轮询,而且只能单向。Agent 想用 REST 反向调 Client,得让 Client 再开一个反向的 REST 服务,复杂度爆炸。
JSON-RPC 双向对称:同一根管子里,谁都可以发 Request,谁都可以发 Notification。Agent 反向调 Client 的工具,协议层天然支持。
硬伤二:gRPC 太重
gRPC 要 Protobuf schema、要编译生成代码、要 HTTP/2、要一堆依赖。Agent 这个领域的特点是方法经常变、协议在演进------你让用户每次更新 Agent 都要重新生成 Protobuf 代码?太反人类。
JSON-RPC 没有 schema 约束,传 method 名和 params 就完事。加新方法、改参数都不影响协议本身。Agent 协议演进快,就需要这种轻量级。
硬伤三:Agent 通信是"长连接 + 流式"
REST 的请求-响应模型是短连接思维。Agent 跟宿主的通信是长连接------IDE 启动时 spawn Agent,整个 IDE 生命周期里一直用这根管子交换消息。而且 Agent 的输出是流式的(LLM 一个字一个字吐),需要一种"单向推送 + 不期望回复"的消息类型。
JSON-RPC 的 Notification 就是为这种场景设计的。MCP 和 ACP 都大量使用 Notification 来流式推送 Agent 的思考过程、工具调用、输出片段。
一张图看清楚选择

三、AI Agent 圈里的 JSON-RPC 应用
讲完为什么,看具体。下面这几个 Agent 圈的主流协议都选了 JSON-RPC 2.0:
3.1 LSP(Language Server Protocol)
LSP 是 JSON-RPC 最成功的应用,没有之一。
2016 年微软为 VS Code 推出 LSP,目标是让"编辑器"和"语言服务器"解耦------任何一个编辑器只要支持 LSP,就能接入任何语言服务器(Java、Go、Rust、TS......)。
LSP 的通信就是用 JSON-RPC 2.0 over stdio(或者 TCP)。典型的 LSP 消息:
json
// 编辑器 → 语言服务器:打开文件
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///project/main.ts",
"languageId": "typescript",
"version": 1,
"text": "..."
}
}
}
// 语言服务器 → 编辑器:推送诊断(Notification,不期望回复)
{
"jsonrpc": "2.0",
"method": "textDocument/publishDiagnostics",
"params": {
"uri": "file:///project/main.ts",
"diagnostics": [{"range": {...}, "message": "Unused variable"}]
}
}
LSP 的成功让后来的协议设计者都把它当模板------ACP 基本是 LSP 在 AI Agent 领域的翻版。
3.2 MCP(Anthropic 的 Model Context Protocol)
MCP 是 Agent-to-Tool 的协议------Agent 通过 MCP 接入外部工具(数据库、搜索、日历、文件系统......)。
MCP 的通信也是 JSON-RPC 2.0:
json
// Agent → MCP Server:列出可用工具
{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/list",
"params": {}
}
// MCP Server → Agent:调用工具结果
{
"jsonrpc": "2.0",
"id": "call-1",
"result": {
"tools": [
{"name": "sqlite_query", "description": "...", "inputSchema": {...}}
]
}
}
// Agent → MCP Server:调用工具
{
"jsonrpc": "2.0",
"id": "call-2",
"method": "tools/call",
"params": {
"name": "sqlite_query",
"arguments": {"sql": "SELECT * FROM users"}
}
}
MCP 默认跑 stdio,也能跑 HTTP + SSE(HTTP 用于请求-响应,SSE 用于服务端推送)。
3.3 ACP(Zed 的 Agent Client Protocol)
ACP 是 IDE-to-Agent 的协议------IDE 通过 ACP 调用 AI Agent。ACP 跟 LSP 的设计哲学一模一样:
json
// IDE → Agent:新建会话
{
"jsonrpc": "2.0",
"id": "1",
"method": "session/new",
"params": {"cwd": "/project"}
}
// Agent → IDE:流式推送思考过程(Notification)
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "abc",
"update": {"sessionUpdate": "agent_thought_chunk", "thought": "Analyzing..."}
}
}
// Agent → IDE:反向调文件工具(Request,期望回复)
{
"jsonrpc": "2.0",
"id": "agent-call-1",
"method": "fs/read_text_file",
"params": {"path": "/project/main.py"}
}
关键的反向调用场景 :Agent 要读 IDE 上的文件,反向发一个 Request 给 IDE,IDE 处理后回复 Response。同一根 stdio 管子里既有 IDE 发出去的 Request,也有 Agent 反向发过来的 Request。这就是双向对称。
3.4 Codex app-server(OpenAI 的 Agent Harness)
Codex 的 app-server 是 ACP 类协议的另一个实现(方法名不同,但模式完全一致)。它跑在 stdio 上,用 JSON-RPC 2.0 信封,双向通信:
json
// IDE → Codex app-server:开启线程
{
"jsonrpc": "2.0",
"id": "init-1",
"method": "thread/start",
"params": {"cwd": "/project"}
}
// Codex app-server → IDE:流式推送输出(Notification)
{
"jsonrpc": "2.0",
"method": "item/agentMessage/delta",
"params": {"threadId": "abc", "delta": "Let me check the file..."}
}
// Codex app-server → IDE:反向调工具(Request,需要 IDE 回复)
{
"jsonrpc": "2.0",
"id": "codex-tool-1",
"method": "fs/readFile",
"params": {"path": "/project/main.py"}
}
3.5 其他领域的经典应用
不止 AI 圈。JSON-RPC 在其他领域也是事实标准:
- 以太坊节点 API :所有以太坊客户端都暴露 JSON-RPC 端口(默认 8545),MetaMask、Web3.js、Ethers.js 都用 JSON-RPC 跟节点通信。
eth_sendTransaction、eth_getBalance这些方法全是 JSON-RPC - 比特币核心 RPC :Bitcoin Core 用 JSON-RPC over HTTP 暴露
getblock、sendtoaddress等方法 - Minecraft RCON:Minecraft 服务器的远程管理用 JSON-RPC 风格的协议
- Kodi、XBMC 媒体中心:用 JSON-RPC 暴露播放控制 API

这个协议 20 年来反复被重新发现,是因为它解决了一类问题------"长连接 + 双向 + 方法抽象 + 简单"------而且解决得很彻底。
四、写一个最小的 JSON-RPC 实现
概念讲够了,看代码。下面用 Node.js 手写一个最小的 JSON-RPC 端点,跑在 stdio 上。这个实现不到 100 行,跟 dsh、Codex、MCP 用的核心抽象是一样的。
4.1 Transport 抽象
先抽象传输层------任何能"发一条消息 + 收一条消息"的东西都能塞进去:
typescript
// transport.ts
export type JsonRpcMessage =
| { jsonrpc: '2.0'; id: string | number; method: string; params?: unknown } // Request
| { jsonrpc: '2.0'; id: string | number; result?: unknown; error?: { code: number; message: string } } // Response
| { jsonrpc: '2.0'; method: string; params?: unknown } // Notification
export interface Transport {
send(msg: JsonRpcMessage): void
onMessage(cb: (msg: JsonRpcMessage) => void): void
close(): void
}
4.2 stdio 传输实现
typescript
// stdio-transport.ts
import { createInterface } from 'node:readline'
import type { Transport, JsonRpcMessage } from './transport.js'
export function stdioTransport(): Transport {
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity })
const callbacks: ((msg: JsonRpcMessage) => void)[] = []
rl.on('line', (line) => {
if (!line.trim()) return
try {
callbacks.forEach(cb => cb(JSON.parse(line)))
} catch (e) {
console.error('parse error:', line)
}
})
return {
send(msg) { process.stdout.write(JSON.stringify(msg) + '\n') },
onMessage(cb) { callbacks.push(cb) },
close() { rl.close(); process.exit(0) },
}
}
4.3 RPC 端点(双向对称)
这是最关键的部分------同一个端点既能"接收调用"也能"发起调用",这就是双向对称:
typescript
// endpoint.ts
import type { Transport, JsonRpcMessage } from './transport.js'
export class RpcEndpoint {
private handlers = new Map<string, (p: any) => any>()
private pending = new Map<string | number, { resolve: (v: any) => void; reject: (e: Error) => void }>()
private nextId = 1
constructor(private transport: Transport) {
transport.onMessage(m => this.route(m))
}
// 注册本端方法(对端可调)
register(method: string, handler: (params: any) => any) {
this.handlers.set(method, handler)
}
// 主动调对端方法(双向的关键)
async call(method: string, params?: any): Promise<any> {
const id = this.nextId++
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject })
this.transport.send({ jsonrpc: '2.0', id, method, params })
})
}
// 单向通知(不期望回复)
notify(method: string, params?: any) {
this.transport.send({ jsonrpc: '2.0', method, params })
}
private route(msg: JsonRpcMessage) {
if ('method' in msg && 'id' in msg) {
// Request:对端调我的方法
const handler = this.handlers.get(msg.method)
if (!handler) {
this.transport.send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'method not found' } })
return
}
Promise.resolve(handler(msg.params))
.then(result => this.transport.send({ jsonrpc: '2.0', id: msg.id, result }))
.catch(e => this.transport.send({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: e.message } }))
} else if ('id' in msg) {
// Response:对端回复我之前发的 Request
const p = this.pending.get(msg.id)
if (!p) return
this.pending.delete(msg.id)
if ('error' in msg && msg.error) p.reject(new Error(msg.error.message))
else p.resolve(msg.result)
} else if ('method' in msg) {
// Notification:单向通知,不回复
const handler = this.handlers.get(msg.method)
if (handler) Promise.resolve(handler(msg.params)).catch(() => {})
}
}
}
4.4 跑一遍:双向通信
typescript
// demo.ts
import { RpcEndpoint } from './endpoint.js'
import { stdioTransport } from './stdio-transport.js'
const endpoint = new RpcEndpoint(stdioTransport())
// 注册本端方法:对端可以调 add
endpoint.register('add', (p: any) => p.a + p.b)
// 主动调对端的 greet 方法
endpoint.call('greet', { name: 'Alice' }).then(console.log)
// 发一个通知(不期望回复)
endpoint.notify('log', { level: 'info', msg: 'started' })
这 100 行代码就是 ACP、MCP、Codex app-server、LSP 的核心抽象。 它们的差异只是在上面"挂什么方法"和"用什么传输"------底下都是这同一个双向 RpcEndpoint。
五、工程上的几个细节
把协议讲清楚了,再聊几个工程上容易踩的坑。
5.1 id 怎么生成
协议没说 id 必须是字符串还是数字。实践中常见三种:
- 自增整数(简单,但连接复用时有碰撞风险)
- UUID(安全但长)
- 计数器 + 前缀(比如
client-1、agent-1)
关键约束:同一个连接里 id 必须唯一。Response 用 id 找回对应的 Request,id 碰撞会乱。
5.2 Notification 没有 id,不能回复
如果你收到一个没有 id 的消息,它是 Notification------绝对不能回复(即使回一个 error 也不行)。这是协议明确规定的。
很多新手实现会"顺手"给 Notification 也回复一下,结果对端收到一个"孤儿 Response",匹配不上任何 pending Request,状态机就乱了。
5.3 错误码是约定的
协议规定了一组标准错误码:
| 错误码 | 含义 |
|---|---|
| -32700 | Parse error(JSON 解析失败) |
| -32600 | Invalid Request(信封格式错) |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| -32000 到 -32099 | 服务器自定义错误 |
ACP 和 MCP 都在这些约定之上扩展了自己的业务错误码 (比如 ACP 的 session not found、MCP 的 tool rejected)。
5.4 长连接 + 背压
JSON-RPC 是异步的------你可以同时发出 100 个 Request,每个 id 不同,Response 乱序回来。端点层要用 Map<id, Promise> 维护 pending。
但如果对端处理不过来,pending 堆积会爆内存 ------这叫"背压"(backpressure)。Codex app-server 就遇到过这个问题,饱和时返回 -32001 Server overloaded; retry later 让客户端退避。
5.5 长连接断了怎么办
JSON-RPC 本身不管连接生命周期------它没有心跳、没有重连。这些都得在传输层处理:
- stdio:子进程死了就是死了,父进程重新 spawn
- WebSocket:库通常自带 ping/pong 心跳
- TCP:自己实现心跳(每隔 N 秒发个空消息),断了自动重连
这也是为什么 JSON-RPC "简单"的另一面------它不管的事你得自己管。
写在最后
如果这一篇只记住一件事,我希望是这句:
JSON-RPC 2.0 = JSON 信封 + 三种消息类型(Request / Response / Notification)+ 传输无关。
简单到能写在 100 行代码里,强大到能撑起 LSP、ACP、MCP、Codex、以太坊这一整片生态。
2005 年发明,2016 年靠 LSP 翻红,2024-2025 年靠 MCP、ACP、Codex 再次翻红。一个协议能反复被"重新发现",说明它解决的是真正基础的问题------"两个程序通过一根双向管子互相调用方法",这件事在分布式系统、IDE、AI Agent 三个时代都是核心需求。
回到我们的场景
现在回头看开篇那个 Agent 场景:
- 沙箱里的 Agent 想调网关的
tool/call、llm/complete、billing/query------需要反向 RPC - 网关想把 Agent 的流式输出推给前端------需要 Notification 单向推送
- 一次会话里 Agent 和网关会互相发几十上百个调用------需要长连接
JSON-RPC 2.0 把这三件事一次都满足了。 这就是为什么下一篇文章设计那套架构时,网关 ↔ 沙箱之间选了 JSON-RPC over WebSocket------协议层天然契合,不用发明新东西。
相关参考资料
JSON-RPC 2.0 官方
AI Agent 圈的 JSON-RPC 应用
- ACP(Zed):zed.dev/acp
- MCP(Anthropic):modelcontextprotocol.io/
- Codex app-server:github.com/openai/code...
其他经典应用
- LSP(Language Server Protocol):microsoft.github.io/language-se...
- 以太坊 JSON-RPC:ethereum.org/en/develope...
- Bitcoin Core RPC:developer.bitcoin.org/reference/r...
Node.js 实现参考
jayson(最流行的 Node JSON-RPC 库):github.com/tedeh/jayso...- 本文配套的最小实现见上文代码