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

传统 SaaS 转向 AI 时代,我目前的一点理解:先把数据能力变成 Agent 可调用的基础设施

最近我一直在思考一个问题:传统 SaaS 到底应该怎么转向 AI? 一开始很容易想到的方向是:给原来的系统加一个 AI 助手。 比如在页面右下角放一个聊天框,让用户可以问数据、生成报告、总结内容、解释指标。这个当然有价值,但我现在越来越觉得,这只是比较表层的一种转型。 真正的变化,可能不是“在 SaaS 里面加 AI”,而是 SaaS 本身的能力形态发生变化。 过去的 SaaS,核心是给人使用。 人登录系统,看页面、点按钮、筛选数据、导出报表、判断问题,然后再去做决策。数据库是给 Web 页面供数的,后端 API 是给前端页面服务的,整个产品的中心是“人如何操作软件”。 但 AI 时代,尤其是 Agent 逐渐发展之后,

By ladydd

对 Python 应用场景的一次重新思考:FastAPI、协程、线程、数据库与任务系统边界

最近在重新设计一个任务系统时,我顺便把自己对 Python,尤其是 CPython 应用场景的理解重新梳理了一遍。 这次讨论的背景是一个典型的异步任务服务: 上游提交任务 API 立即返回 task_id 后台 worker 慢慢执行 用户通过 task_id 查询任务状态 任务主要是 LLM 调用、图片下载、外部 HTTP 请求这类 I/O 型工作。 一开始关注的是队列、Redis、PostgreSQL、worker 并发控制这些问题。但聊到后面,其实更核心的问题变成了: Python 到底应该放在什么位置? 哪些并发适合 Python? 哪些并发不要硬塞给 Python? FastAPI、协程、线程、数据库之间应该怎么分工? 这篇文章就是这次思考的整理。 一、我不想抛弃 Python,

By ladydd

Go 和 Python 的并发模型对比:进程、线程、协程、并发和并行到底怎么理解?

最近我在写 worker 任务系统的时候,重新理解了一遍 Python 和 Go 的并发差异。 以前写 Python,多 worker 经常要考虑: 多进程怎么管理? 日志会不会串? 一个 worker 崩了怎么办? 怎么吃满多核心? 后来换成 Go,发现一个进程里开多个 goroutine worker 就很自然: go worker(1) go worker(2) go worker(3) go worker(4) 日志也好管,状态也好管,而且单进程还能利用多个 CPU 核心。 一开始很容易误会成: Python 不行,Go 行 但更准确的理解应该是: Python 和

By ladydd

Python 进程和 Go 进程的区别:为什么 Go 单进程多 worker 用起来更爽?

最近我在做 worker 任务系统的时候,突然意识到一个很关键的问题: 以前写 Python,多 worker 的时候经常要小心日志串、文件切割乱、时间不好管理。 但是换成 Go 以后,一个进程里开多个 goroutine worker,反而可以比较自然地写到同一个日志文件里。 一开始我以为这是“Python 和 Go 写日志能力不一样”,后来想明白了,核心不是日志本身,而是: Python 常见 worker 模型:多进程 Go 常见 worker 模型:单进程 + 多 goroutine 这背后其实是两个语言在并发模型上的巨大差异。 一、进程、线程、goroutine 先分清楚 先把几个概念捋一下。 进程:操作系统分配资源的单位 线程:CPU 调度执行的基本单位

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