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
}

服务端根据 methodparams 判断要做什么,再返回结果。

从后端实现的角度看,这确实很像以前常见的:

固定一个 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 怎么统一发现和调用工具”。

这就是它们最核心的区别。

Read more

三台机器部署 ClickHouse 高可用集群实战记录

本文是一份可发布版部署记录。真实 IP、域名、账号、密码、下载链接、业务目录名、机器唯一标识等敏感信息已经替换为占位符。命令中的 <...> 需要按自己的环境替换。 目标与拓扑 这次目标是用三台数据节点部署一套 ClickHouse 高可用集群,拓扑采用: 1 shard x 3 replicas 含义是:集群只有一个逻辑分片,三台机器都保存同一份数据的完整副本。任意一台数据节点宕机时,只要 ClickHouse Keeper 仍然有多数派,剩余节点仍可继续提供读写服务。 规划节点如下: 主机名示例地址角色ch-01<ch-01-ip>ClickHouse Server + ClickHouse Keeperch-02<ch-02-ip>ClickHouse Server + ClickHouse Keeperch-03<ch-03-ip&

By ladydd

折腾记(二):接入火山引擎实时语音 API,家庭语音助手体验直接拉满

接上篇 上一篇用全开源组件(Whisper + Hermes + Edge-TTS)搭了个语音助手,能跑,但体验就是"能用"二字: * 中文识别只有 70 分,方言基本歇菜 * 英文唤醒词"Alexa"喊着别扭 * 说完到回复要等 4-8 秒 * 它说话的时候你插不了嘴 这些问题靠堆开源组件很难根治。于是我去试了火山引擎(字节跳动)的语音服务,结果直接换了条路。 这篇分两段:先讲怎么用火山引擎的 ASR/TTS 替换掉开源组件(小改),再讲怎么上端到端实时语音模型(大改)。 第一段:先把 ASR 和 TTS 换成火山引擎 为什么换 我用豆包输入法的时候发现它语音识别准得离谱。一查,豆包用的就是字节自家的火山引擎 Seed-ASR。开通后有免费额度(

By ladydd

折腾记(一):用全开源组件给家里搭一个语音助手,对接自己的 Hermes Agent

起因 事情是从一块 ESP32-S3 开发板开始的。 我手上有一块 Seeed Studio XIAO ESP32-S3 Sense,带摄像头和麦克风。最初的想法很美好:用这块板子做一个无线语音终端,对着它说话,连到我服务器上跑的 Hermes Agent(一个自托管的 AI agent),让它回答我。 但折腾到一半我突然意识到一件事:我的麦克风、音响、服务器全在家里,为什么要绕一圈用 ESP32?直接把麦克风和音响插到服务器上不就行了? ESP32 那条路(做无线拾音终端)当然也有价值,但那是"为了学嵌入式而学",不是解决问题的最短路径。于是这个项目就从"嵌入式项目"变成了"在服务器上拼一个语音助手"。这篇就记录后者。 教训零:先想清楚你要解决的是什么问题。很多时候最优解比你最初设想的简单得多。 目标

By ladydd

Kiro 的三种代理设置方法:本地、服务端、Remote

作为kiro的骨灰级用户,这篇是我自己折腾 Kiro / Kiro Remote / Ubuntu Server 代理问题后的复盘。 核心不是“怎么配一个代理”,而是先判断:到底是谁在访问外网? 谁访问外网,代理就要配给谁。 0. 先说结论 Kiro 相关代理大概分三类: 场景真正访问外网的进程在哪里代理应该配在哪里本地 KiroWindows / Mac 本机本机 Clash / Proxifier / 系统代理服务端 Kiro / CLIUbuntu Server 上的 shell、CLI、node、kiro 进程Ubuntu 的环境变量,比如 HTTP_PROXY / HTTPS_PROXYKiro Remote远程 Ubuntu 上的 ~/.kiro-server 和 extensionHost远程 Ubuntu 的 Kiro Server

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号