JSON-RPC / MCP 为什么看起来很像“固定一个 POST 接口”?
理解 MCP、JSON-RPC、Streamable HTTP 的过程中,有一个很自然的联想:
这不就是固定一个 POST 接口,然后传入不同 JSON,根据请求体里的字段返回不同响应吗?
这个直觉是对的。
尤其是当 MCP 走 Streamable HTTP 的时候,表面上看确实很像:
POST /mcp
然后请求体里传:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 1
}
服务端根据 method 和 params 判断要做什么,再返回结果。
从后端实现的角度看,这确实很像以前常见的:
固定一个 POST 接口
请求体里传 action / type / cmd
后端根据这个字段分发不同逻辑
但它们又不完全一样。
这篇文章就围绕这个直觉,把普通 POST JSON 接口、JSON-RPC、MCP 之间的关系讲清楚。
一、普通 POST JSON 接口是什么样?
在很多后端项目里,我们都会写这样的接口:
POST /api/action
Content-Type: application/json
请求体:
{
"action": "search",
"data": {
"keyword": "abc"
}
}
服务端逻辑可能是:
def handle_request(body):
action = body.get("action")
if action == "search":
return do_search(body["data"])
if action == "detail":
return do_detail(body["data"])
if action == "export":
return do_export(body["data"])
return {
"success": false,
"message": "unknown action"
}
这种模式非常常见。
它的核心是:
一个固定入口
多个业务动作
通过 JSON body 里的字段决定执行哪个逻辑
也就是说,业务语义不一定写在 URL 里,而是写在请求体里。
传统 REST 更常见的是:
GET /users/123
POST /orders
POST /search
URL 本身承载业务语义。
而这种固定 POST 接口更像:
POST /api/action
真正的业务动作藏在 body 里的 action 字段。
二、JSON-RPC 看起来为什么像这种 POST 接口?
JSON-RPC 的一次请求长这样:
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 1
}
如果它跑在 HTTP 上,可能就是:
POST /rpc
Content-Type: application/json
请求体就是上面那段 JSON。
服务端看到:
method = search
params = { keyword: "abc" }
然后执行:
search(keyword="abc")
所以从实现上看,它和这个模式确实很像:
{
"action": "search",
"data": {
"keyword": "abc"
}
}
只不过 JSON-RPC 把字段标准化了。
普通 POST JSON 可能叫:
action
type
cmd
event
operation
参数可能叫:
data
payload
args
body
input
返回可能叫:
success
code
message
data
而 JSON-RPC 统一规定:
method:要调用的方法
params:调用参数
result:成功结果
error:失败信息
id:请求响应对应编号
jsonrpc:协议版本
所以可以这样理解:
普通 POST JSON:
你自己发明一套 action / data / result / error。
JSON-RPC:
大家约定统一使用 method / params / result / error / id。
这就是 JSON-RPC 的价值之一。
它不是创造了一个前所未有的新东西,而是把一种常见的调用模式标准化了。
三、JSON-RPC 的标准请求格式
JSON-RPC 2.0 的请求一般长这样:
{
"jsonrpc": "2.0",
"method": "add",
"params": {
"a": 1,
"b": 2
},
"id": 1
}
字段含义:
jsonrpc:固定为 "2.0",表示 JSON-RPC 协议版本
method:要调用的方法名
params:调用参数
id:请求编号,用于匹配响应
服务端可以理解为:
调用 add 方法,参数是 a=1, b=2,这个请求编号是 1。
如果成功,响应:
{
"jsonrpc": "2.0",
"result": 3,
"id": 1
}
如果失败,响应:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"reason": "missing field: a"
}
},
"id": 1
}
核心就是:
成功返回 result
失败返回 error
响应带回同一个 id
四、它和普通 POST JSON 最大的区别:标准化
假设没有 JSON-RPC,每家公司、每个项目都可以设计自己的 POST body。
项目 A 可能这样:
{
"action": "search",
"data": {
"keyword": "abc"
}
}
项目 B 可能这样:
{
"type": "search",
"payload": {
"keyword": "abc"
}
}
项目 C 可能这样:
{
"cmd": "search",
"args": {
"keyword": "abc"
}
}
项目 D 可能这样:
{
"operation": "search",
"input": {
"keyword": "abc"
}
}
这些都能工作。
但是客户端每接一个服务,都要重新理解一套格式。
JSON-RPC 的好处是:
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 1
}
统一了。
成功响应也统一:
{
"jsonrpc": "2.0",
"result": {
"items": []
},
"id": 1
}
失败响应也统一:
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": 1
}
这就是协议的意义。
协议不一定是发明了一个完全陌生的能力,而是把大家原来各写各的东西,整理成一套共同约定。
五、id 是 JSON-RPC 很重要的一点
普通 POST API 里,请求和响应天然是一一对应的:
一个 HTTP 请求
一个 HTTP 响应
所以很多时候不需要额外的 id。
但 JSON-RPC 更通用。
它不只可以跑在 HTTP 上,也可以跑在:
stdio
WebSocket
TCP
长连接
批量请求
流式场景
这时候 id 就很重要。
比如客户端发:
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 100
}
服务端返回:
{
"jsonrpc": "2.0",
"result": {
"items": []
},
"id": 100
}
客户端看到 id = 100,就知道:
这个响应对应刚才 id=100 的那个请求。
如果一次发多个请求,就更明显了。
[
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 1
},
{
"jsonrpc": "2.0",
"method": "detail",
"params": {
"id": "item_123"
},
"id": 2
}
]
服务端可能返回:
[
{
"jsonrpc": "2.0",
"result": {
"title": "detail result"
},
"id": 2
},
{
"jsonrpc": "2.0",
"result": {
"items": []
},
"id": 1
}
]
注意,响应顺序不一定和请求顺序一样。
客户端靠 id 匹配。
所以 id 不是多余的字段,它是 JSON-RPC 作为通用 RPC 消息协议的重要设计。
六、JSON-RPC 支持 notification
JSON-RPC 还有一个普通 POST JSON 接口里不一定自然存在的概念:notification。
普通请求有 id:
{
"jsonrpc": "2.0",
"method": "add",
"params": {
"a": 1,
"b": 2
},
"id": 1
}
有 id,意味着服务端应该返回响应。
但如果没有 id:
{
"jsonrpc": "2.0",
"method": "log",
"params": {
"message": "hello"
}
}
这就是 notification。
它的含义是:
我只是通知你一件事,不需要你回复。
在 MCP 里,通知类消息很常见,比如:
进度通知
日志通知
状态变化通知
取消任务通知
比如:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progress": 0.5,
"message": "processing..."
}
}
这种消息不一定需要对方返回结果。
这比简单的 POST /api/action 更像一套完整的消息通信规范。
七、JSON-RPC 的错误结构也是标准化的
普通 POST API 的错误返回经常各不相同。
有人喜欢这样:
{
"success": false,
"message": "参数错误"
}
有人喜欢这样:
{
"code": 40001,
"msg": "invalid param"
}
有人喜欢这样:
{
"error": "missing keyword"
}
JSON-RPC 规定了统一错误结构:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"reason": "missing keyword"
}
},
"id": 1
}
常见标准错误码包括:
-32700 Parse error
JSON 解析失败
-32600 Invalid Request
请求格式不合法
-32601 Method not found
方法不存在
-32602 Invalid params
参数不合法
-32603 Internal error
服务端内部错误
这样客户端就可以统一处理错误。
例如:
-32601:调用的方法不存在
-32602:参数不合法
-32603:服务端内部错误
这比每个项目自己发明一套错误格式更稳定。
八、JSON-RPC 不关心 HTTP,它只关心消息格式
这是一个很关键的点。
虽然我们经常在 HTTP 里看到 JSON-RPC,比如:
POST /rpc
或者 MCP 里的:
POST /mcp
但 JSON-RPC 本身并不绑定 HTTP。
同一条 JSON-RPC 消息:
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
可以通过不同方式传输:
HTTP POST
stdio
WebSocket
TCP
SSE stream
这就是 Transport 的概念。
JSON-RPC 负责:
消息长什么样
Transport 负责:
消息怎么传过去
所以当 MCP 用 stdio 时,本质是:
JSON-RPC 消息通过 stdin/stdout 传输
当 MCP 用 Streamable HTTP 时,本质是:
JSON-RPC 消息通过 HTTP 传输
底层通道变了,但消息格式还是 JSON-RPC。
九、MCP 和普通 POST 接口的相似点
当 MCP 走 Streamable HTTP 的时候,它确实很像固定 POST 接口。
例如:
POST /mcp
Content-Type: application/json
body:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 1
}
从服务端实现看,就是:
def handle_mcp(body):
method = body["method"]
if method == "tools/list":
return list_tools()
if method == "tools/call":
tool_name = body["params"]["name"]
arguments = body["params"]["arguments"]
return call_tool(tool_name, arguments)
return method_not_found()
这和普通 POST JSON 的分发非常像:
def handle_api(body):
action = body["action"]
if action == "search":
return search(body["data"])
if action == "detail":
return detail(body["data"])
return unknown_action()
所以从“服务器根据 body 字段分发逻辑”这个角度看,它们确实是一类东西。
十、MCP 和普通 POST 接口的关键区别
但是 MCP 不只是“一个 POST 接口 + 一个 action 字段”。
MCP 在 JSON-RPC 之上又约定了一套 Agent 工具调用语义。
比如:
initialize
tools/list
tools/call
resources/list
resources/read
prompts/list
其中最关键的是工具发现。
普通 POST 接口里,如果有这些 action:
search
detail
export
客户端通常要看文档才知道:
有哪些 action
每个 action 要传什么参数
每个 action 返回什么结构
但 MCP 里,Agent 可以先调用:
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
服务端返回工具列表。
里面会有:
工具名称
工具描述
参数 schema
然后 Agent 再调用:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 2
}
这就比普通 POST action 模式更适合 Agent。
因为 Agent 不只是需要调用接口,它还需要理解:
你有哪些能力
每个能力是干什么的
每个能力需要什么参数
我该在什么时候调用哪个工具
MCP 的价值就在这里。
十一、传统 REST、普通 POST JSON、JSON-RPC、MCP 的关系
可以把这几种方式放在一起比较。
1. REST API
GET /users/123
POST /orders
POST /search
特点:
URL 承载业务语义
HTTP method 表达操作类型
适合资源建模
比如:
GET /users/123
意思是获取用户 123。
2. 普通 POST JSON
POST /api/action
body:
{
"action": "search",
"data": {
"keyword": "abc"
}
}
特点:
固定入口
body 里的 action/type/cmd 承载业务语义
格式由项目自己约定
3. JSON-RPC
POST /rpc
body:
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 1
}
特点:
固定入口
method 承载调用语义
params 承载参数
result/error 标准化
id 用于请求响应匹配
4. MCP over Streamable HTTP
POST /mcp
body:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 1
}
特点:
固定 /mcp 入口
JSON-RPC 承载消息格式
MCP 规定 tools/list、tools/call 等语义
适合 Agent 发现和调用工具
所以它们不是完全陌生的东西。
更像是一层层标准化:
普通 POST JSON:
自己约定 action/data
JSON-RPC:
标准化 method/params/result/error/id
MCP:
在 JSON-RPC 上标准化 Agent 工具调用语义
十二、为什么 Agent 场景更适合 MCP,而不是普通 POST 接口?
如果只是人类工程师调用接口,普通 POST 接口也可以工作。
比如你告诉同事:
POST /api/action
action=search
data.keyword=xxx
同事看文档就能用。
但 Agent 不一样。
Agent 更需要机器可读的能力描述。
它需要知道:
有哪些工具?
工具叫什么?
工具是干什么的?
工具需要哪些参数?
参数类型是什么?
哪些参数必填?
调用之后返回什么?
MCP 的 tools/list 就是为这件事服务的。
普通 POST JSON 接口一般没有统一的工具发现机制。
你当然也可以自己设计一个:
GET /api/actions
返回所有 action 的描述。
但是如果每个项目都自己设计一套,Agent 就很难通用。
MCP 的价值是:
大家都用同一套工具发现和工具调用语义
这就让 Agent 更容易接入不同工具。
十三、JSON-RPC / MCP 不是更神秘,而是更标准
所以不要把 JSON-RPC 或 MCP 想得太玄。
从实现上看,它们确实可以很朴素:
收到 JSON
看 method
分发函数
返回 result 或 error
但是它们的价值在于:
统一字段
统一错误
统一请求响应对应
统一工具发现
统一工具调用语义
换句话说:
能不能这么写,不是重点。
大家都按同一套方式这么写,才是重点。
这就是协议的价值。
普通 POST JSON 接口当然可以完成业务。
但 MCP + JSON-RPC 的意义在于,让 Agent 和工具之间有一套共同语言。
十四、用代码模拟一下差异
普通 POST action 风格
请求:
{
"action": "search",
"data": {
"keyword": "abc"
}
}
服务端:
def handle_action_api(body):
action = body.get("action")
data = body.get("data", {})
if action == "search":
result = search(data["keyword"])
return {
"success": True,
"data": result
}
if action == "detail":
result = detail(data["id"])
return {
"success": True,
"data": result
}
return {
"success": False,
"message": "unknown action"
}
这完全能用。
但这是项目内部自定义格式。
JSON-RPC 风格
请求:
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"keyword": "abc"
},
"id": 1
}
服务端:
def handle_jsonrpc(body):
request_id = body.get("id")
if body.get("jsonrpc") != "2.0":
return {
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request"
},
"id": request_id
}
method = body.get("method")
params = body.get("params", {})
try:
if method == "search":
result = search(params["keyword"])
elif method == "detail":
result = detail(params["id"])
else:
return {
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": request_id
}
return {
"jsonrpc": "2.0",
"result": result,
"id": request_id
}
except Exception as e:
return {
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": str(e)
},
"id": request_id
}
你会发现,逻辑差不多。
但是 JSON-RPC 把请求结构、响应结构、错误结构、id 对应方式统一了。
MCP 风格
MCP 会更进一步。
客户端不是直接调用:
{
"method": "search"
}
而是:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 1
}
服务端逻辑:
def handle_mcp(body):
method = body.get("method")
params = body.get("params", {})
request_id = body.get("id")
if method == "tools/list":
return {
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "search",
"description": "Search something by keyword",
"inputSchema": {
"type": "object",
"properties": {
"keyword": {
"type": "string"
}
},
"required": ["keyword"]
}
}
]
},
"id": request_id
}
if method == "tools/call":
tool_name = params["name"]
arguments = params.get("arguments", {})
if tool_name == "search":
result = search(arguments["keyword"])
return {
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": result
}
]
},
"id": request_id
}
return {
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": request_id
}
这时,MCP 不只是“调用 search”,还提供了:
工具列表
工具描述
参数 schema
统一调用入口
统一返回结构
这就是它比普通 POST 接口更适合 Agent 的地方。
十五、再回到 Streamable HTTP
当 MCP 使用 Streamable HTTP 时:
HTTP 是传输层
JSON-RPC 是消息格式
MCP 是工具调用语义
请求可能是:
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
body:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"keyword": "abc"
}
},
"id": 1
}
如果是普通短任务,服务端可以一次性返回 JSON:
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "result..."
}
]
},
"id": 1
}
如果是长任务,服务端可以用 SSE 一段一段返回事件。
所以 Streamable HTTP 和普通 POST 接口也有相似之处:
都是 HTTP
都可以 POST JSON
都可以固定一个入口
区别是:
普通 POST 接口:业务格式自己定义
Streamable HTTP MCP:按照 MCP + JSON-RPC 的规范传输和响应
十六、最终理解
把这个问题说透,其实可以总结成一句话:
JSON-RPC / MCP over HTTP 的工程形态,确实很像“固定一个 POST 接口,然后根据 JSON body 分发逻辑”。
但它比普通自定义 POST 接口多了几层标准化:
JSON-RPC 标准化:
method
params
result
error
id
MCP 标准化:
initialize
tools/list
tools/call
resources/list
resources/read
prompts/list
工具描述
参数 schema
调用结果格式
Streamable HTTP 标准化:
通过 HTTP 承载 MCP JSON-RPC 消息
普通 JSON 返回或需要时流式返回
所以最终关系是:
普通 POST JSON:
固定接口 + 自定义 body
JSON-RPC:
固定接口 + 标准 RPC body
MCP:
JSON-RPC + Agent 工具调用语义
Streamable HTTP MCP:
HTTP Transport + JSON-RPC + MCP 语义
十七、最后一句总结
如果只是从服务器代码实现看:
MCP over HTTP 确实像一个固定 POST 接口。
但从协议和生态看:
它不是随便约定一个 action 字段。
它是把 Agent 调工具这件事标准化了。
也就是说:
普通 POST JSON 解决的是“我这个项目怎么调”。
JSON-RPC 解决的是“远程方法调用消息怎么统一表达”。
MCP 解决的是“Agent 怎么统一发现和调用工具”。
这就是它们最核心的区别。
陕公网安备61011302002223号