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

Mac 上 Skill CLI 无法执行的坑:最后其实一条命令就够了

我在做 Amazon skills 的过程中,逐步把本地 CLI 从 Python 脚本切到 Go 二进制。这样做的好处很明显:用户不用装 Python、不用配依赖,解压 skill 后直接运行。但在 macOS 上,我们反复遇到一个看起来很玄的问题:同一个二进制,在 Linux/Windows 上正常,在 Mac 上就是执行不了。 当时遇到的现象 常见报错大概有几类: * 双击或 agent 调用 CLI 时,系统提示文件来自未知开发者,无法打开。 * 终端里执行时提示 Permission denied。 * 已经 chmod +x 了,仍然被 macOS 拦截。 * Apple

By ladydd

当我把全世界人群的基因 PCA 跑出来后,看见了一个倒 L 型

最近我把之前学的一些分子人类学知识,终于真正落地了。 不是停留在看论文、看别人画图、看别人解释“人群结构”这些概念,而是自己把数据处理完,自己跑 PCA,自己把全世界不同人群放到一张图上。 然后那一刻,我真的被击中了。 图上出现了一个非常漂亮的倒 L 型。 一端是非洲,另一端逐渐拉向东亚,中间有中东、欧洲、南亚、欧亚大陆上的各种过渡人群。它不是那种随机散点图,而是有方向、有骨架、有历史感的结构。 我第一眼看到的时候,脑子里直接冒出一句话: 这不像是一张普通统计图,这像是人类迁徙史在二维空间里留下的影子。 当然,后来我也提醒自己,PCA 不能被过度浪漫化。它不是地图,也不是时间轴,更不是“谁从哪里走到哪里”的直接证据。PCA 本质上是把高维基因差异压缩到几个主成分上,用最大方差方向把样本摊开。它可以帮助我们观察人群结构、相似性、分化和混合,但不能单独承担全部历史解释。PCA 在群体遗传学里常用于观察 population structure

By ladydd

吞吐与延迟:一个厨房比喻讲透性能压测

写于 2026-06-26。背景: MCP 服务 跑在 3 台 ClickHouse(每台 16 核 / 64G,1 分片 3 副本)上。 我们花了一整轮做公网压测,把这套系统的极限、天花板和杠杆全摸清了。这篇把"吞吐 / 延迟 / 排队"这三个最容易混的概念讲透,配我们自己的真实实测数据。 一句话结论 我们这套系统的吞吐天花板 ≈ 76 req/s。 往里塞再多并发(100、200、300、500),每秒"做完"的还是大约 76 个,多出来的全在排队。系统不会崩,只会让每个人等得更久。 天花板能不能抬?

By ladydd

四卡 3090 本地模型部署复盘:Ollama 跑通 35B,以及 GPU0 掉卡问题

这次做的是一轮真实的本地模型部署摸底。 目标不是搭一个临时 Demo,而是把一台四卡 3090 GPU 机器接进自己的日常 AI 使用环境:本机跑 Open WebUI,负责账号、会话和前端配置;GPU 机器只负责模型推理。这样以后换模型、换推理框架、重启服务,都尽量不影响本机的使用入口。 最后结论比较清楚:qwen3.5:35b 的 GGUF Q4_K_M 量化版已经通过 Ollama 跑通,本机 Open WebUI 可以接入,热加载后的聊天速度也能用;但 GPU0 存在明显稳定性问题,重启后能短暂恢复,跑过负载后又会掉到 NVML 异常状态。 状态快照 当前能用的部分: * 本机 Open WebUI 已部署,

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