从 MCP 到 JSON-RPC,再到 Streamable HTTP:理清 Agent 工具调用里的协议关系

做 MCP 相关项目时,经常会遇到几个词:

MCP
JSON-RPC
Transport
stdio
SSE
Streamable HTTP

这些词经常一起出现,如果不把层次拆开,很容易混在一起。

这篇文章尝试从工程开发的角度,把它们之间的关系梳理清楚。重点不是翻译标准文档,而是建立一套比较稳定的理解模型。


一、先分清三层:MCP、JSON-RPC、Transport

可以先记住这句话:

MCP 负责:Agent 能调用什么能力
JSON-RPC 负责:调用消息长什么样
Transport 负责:消息怎么传过去

也就是:

MCP = 协议语义层
JSON-RPC = 消息格式层
Transport = 传输层

更直观一点:

MCP:
我要调用工具、读取资源、获取 prompt

JSON-RPC:
method 是什么,params 是什么,result 怎么返回

Transport:
这坨 JSON 是通过 stdio、HTTP、SSE 还是别的方式传过去

只要把这三层拆开,后面很多概念都会清晰很多。


二、RPC 是一种调用思想,不是某个具体协议

RPC 是 Remote Procedure Call,也就是远程过程调用。

它首先是一种编程思想:

像调用本地函数一样调用远程服务

比如本地函数调用是:

result = get_user(123)

RPC 想提供的体验是:

result = remote_service.get_user(123)

虽然底层可能发生了网络通信、序列化、反序列化、错误处理,但调用者的心智模型是:

我在调用一个函数

所以 RPC 本身不是某一种具体通信协议,而是一类设计思想。


三、JSON-RPC 是 RPC 思想的一种轻量实现

理解 RPC 之后,再看 JSON-RPC 就比较自然了。

JSON-RPC 本质是:

用 JSON 表达 RPC 调用的一种协议规范

一次调用大概长这样:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {
      "keyword": "example"
    }
  },
  "id": 1
}

返回大概长这样:

{
  "jsonrpc": "2.0",
  "result": {
    "content": "result..."
  },
  "id": 1
}

几个核心字段:

method:我要调用什么方法
params:调用参数是什么
result:返回结果是什么
error:如果失败,错误是什么
id:请求和响应如何对应

JSON-RPC 专注解决的事情其实很明确:

怎么表达一次方法调用
怎么表达调用结果
怎么表达调用错误
怎么把响应和请求对应起来

它不负责这些事情:

怎么鉴权
怎么限流
怎么做用户体系
怎么做日志
怎么部署
怎么加 HTTPS
怎么做流式传输

这些通常交给外层系统。

所以 JSON-RPC 不是“完整后端框架”,它更像是一个轻量、克制的调用消息规范。


四、JSON-RPC 和 gRPC 的区别

JSON-RPC 和 gRPC 都和 RPC 有关系,但它们的重量级不同。

可以这样理解:

RPC = 思想
JSON-RPC = 轻量协议规范
gRPC = 工业级 RPC 框架

JSON-RPC 主要规定 JSON 消息格式:

method
params
result
error
id

而 gRPC 通常包含:

protobuf
HTTP/2
代码生成
强类型接口
streaming
deadline
metadata
服务端和客户端 stub

所以 gRPC 不只是“一个协议”那么简单,它更像是一整套 RPC 工程体系。

一个简单类比:

JSON-RPC = 给你一把螺丝刀,你自己决定怎么组装

gRPC = 给你一条标准化生产线,但你要按它的规矩来

如果是微服务之间的高性能通信,gRPC 很合适。

如果是 Agent 调用工具,MCP 这种场景选择 JSON-RPC 就很合理。

因为 Agent 工具调用更需要:

简单
通用
跨语言
容易调试
容易通过 stdio 或 HTTP 承载

而不是一开始就引入一整套强类型、代码生成、HTTP/2 的微服务体系。


五、MCP 为什么使用 JSON-RPC?

MCP 的目标不是做传统 REST API,也不是做高性能微服务通信。

它的目标是:

让 Agent 能发现工具、调用工具、读取资源、使用 prompt

MCP 里面会有类似这些方法:

initialize
tools/list
tools/call
resources/list
resources/read
prompts/list

这些方法本质上很像远程方法调用。

例如:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "feyu_search",
    "arguments": {
      "keyword": "xxx"
    }
  },
  "id": 1
}

翻译成人话就是:

调用工具 feyu_search,参数是 keyword=xxx

所以 MCP 使用 JSON-RPC 是比较自然的选择。

可以这么理解:

MCP 定义 Agent 能干什么
JSON-RPC 定义这些调用消息怎么表达

MCP 不是 REST。

MCP 不是 SSE。

MCP 也不是 HTTP 本身。

MCP 更像是:

面向 Agent 的工具调用协议

而 JSON-RPC 是它底层使用的消息表达方式。


六、Transport 是什么?

理解 JSON-RPC 之后,下一层就是 Transport。

Transport 可以理解成:

JSON-RPC 消息怎么从 A 到 B

JSON-RPC 只规定消息内容,不规定这坨 JSON 怎么传。

它可以通过很多方式传:

stdio
HTTP
HTTP + SSE
WebSocket
TCP
SSH

所以 Transport 负责的是:

传输通道
连接方式
消息怎么送达
是否支持流式
是否适合远程服务
是否适合多客户端

一句话:

JSON-RPC 是包裹内容
Transport 是送包裹的路

或者:

JSON-RPC 负责说什么
Transport 负责怎么说过去

七、stdio:适合本地使用的 Transport

MCP 里常见的 transport 之一是 stdio。

stdio 的本质是:

客户端启动一个本地进程
然后通过 stdin/stdout 和这个进程交换 JSON-RPC 消息

例如:

Claude / Codex
    ↓ 启动
python mcp_server.py
    ↓
stdin/stdout 交换 JSON-RPC

这里是本机进程之间的通信。

stdio 很适合:

本地开发
本地工具
单用户使用
Claude Desktop 这类本地客户端
私有插件

但它不适合:

公网服务
局域网服务
多用户访问
统一网关
用户鉴权
限流
租户隔离

因为 stdio 本身不是网络协议。

它默认不能跨机器,也不能直接跨局域网。

当然,技术上可以通过 SSH 做桥接,例如:

ssh user@server "python mcp_server.py"

这样看起来像远程 stdio,但本质是 SSH 帮你把远程进程的 stdio 转发回来了。

这不是 stdio 本身具备网络能力,而是 SSH 做了中间桥接。

所以工程上可以这样记:

stdio = 本地进程管道
不是网络服务

如果只是本地使用,stdio 很合适。

如果要做远程服务,就应该考虑 HTTP 类型的 transport。


八、Streamable HTTP 是什么?

Streamable HTTP 这个名字容易让人第一眼理解成:

Streamable HTTP = 流式 HTTP = SSE

但这个理解不够准确。

更准确地说:

Streamable HTTP = MCP 的 HTTP Transport

它的核心不是“必须流式”,而是:

用 HTTP 来承载 MCP 的 JSON-RPC 消息

也就是说,Agent 可以通过:

POST /mcp

把 JSON-RPC 消息发给 MCP server。

例如:

POST /mcp
Content-Type: application/json

body 是:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "feyu_search",
    "arguments": {
      "keyword": "xxx"
    }
  },
  "id": 1
}

这个时候 HTTP 是运输层。

真正的业务含义不在 URL 里,而在 JSON-RPC 的 method 和 params 里。


九、Streamable HTTP 和传统 HTTP API 的区别

传统 HTTP API 通常是这样设计的:

GET  /users/123
POST /orders
POST /api/search
POST /api/feyu/detail

也就是:

URL 本身承载业务语义

而 Streamable HTTP MCP 更像这样:

POST /mcp

所有工具调用都走这个统一入口。

真正要干什么,写在 JSON-RPC 里面:

{
  "method": "tools/call",
  "params": {
    "name": "feyu_search",
    "arguments": {
      "keyword": "xxx"
    }
  }
}

所以区别是:

传统 HTTP API:
每个 URL 是一个功能

Streamable HTTP MCP:
/mcp 是统一入口,功能由 JSON-RPC method 和 tool name 决定

这也是它适合 Agent 的地方。

Agent 不需要知道一堆 REST API 地址。

Agent 只需要知道:

连接 /mcp
调用 tools/list 看有哪些工具
调用 tools/call 执行某个工具

这比传统 REST 更适合 Agent 自动发现和调用能力。


十、Streamable HTTP 不一定是流式返回

Streamable HTTP 这个名字里的 “Streamable” 容易产生误解。

它不是说每次返回都必须是流式的。

更准确是:

它支持流式,但不强制流式

它可以有两种返回方式。

第一种是普通 JSON response:

客户端发请求
服务端处理完
一次性返回结果

比如普通查询工具:

Agent -> POST /mcp -> tools/call -> 后端调用 API -> 一次性返回结果

这种情况下不需要 SSE。

第二种是 SSE stream response:

客户端发请求
服务端不马上关闭连接
一边处理一边返回事件

比如返回进度:

10%
30%
60%
完成

这种更适合长任务。

所以可以这样记:

Streamable HTTP = MCP 的 HTTP 传输方案
SSE = 需要流式返回时可以使用的一种方式

不是:

Streamable HTTP = SSE

而是:

Streamable HTTP 可以选择用 SSE

十一、什么时候需要 SSE?

不是所有 MCP 工具都需要 SSE。

普通查询类工具通常是:

收到请求
调用第三方 API
processor 整理结果
一次性返回

这种用普通 JSON response 就够了。

真正需要 SSE 的场景通常是:

长任务
进度通知
任务排队
大结果分批返回
服务端持续通知客户端
复杂 agent plan 执行
视频生成
批量导出

比如视频生成工具,可能需要返回:

任务已提交
正在生成关键帧
正在生成视频
正在合成音频
已完成

这种才适合流式。

所以第一版远程 MCP server 完全可以先做:

Streamable HTTP + 普通 JSON response

不用一开始就把 SSE 搞复杂。


十二、老 HTTP+SSE 和新 Streamable HTTP 不一样

这里也值得单独区分。

以前 MCP 有一种老的 HTTP+SSE transport,大概是:

GET  /sse
POST /message

也就是:

/sse     用来接收服务端事件
/message 用来发送客户端消息

这是旧方案。

新的 Streamable HTTP 更统一:

POST /mcp
GET  /mcp

核心是统一成 MCP endpoint。

可以这样理解:

老 HTTP+SSE:
两个入口,偏旧方案

新 Streamable HTTP:
统一 /mcp endpoint,更适合网关、鉴权、云部署

所以新项目更应该面向:

Streamable HTTP MCP

而不是继续围绕旧的 HTTP+SSE transport 设计。


十三、Streamable HTTP 的真正价值不只是流式

Streamable HTTP 的价值不只是“可以流式返回”。

它还有几个更重要的工程价值。

第一,统一入口:

所有 MCP 调用都走 /mcp

第二,适合 Agent:

Agent 通过 tools/list 发现工具
通过 tools/call 调用工具

第三,可以普通返回,也可以流式返回:

短任务普通 JSON
长任务 SSE stream

第四,适合远程服务化:

HTTPS
鉴权
API key
OAuth
网关
限流
日志
租户隔离
部署到云上

所以它不是普通 HTTP API 的简单换皮。

它是:

MCP 在远程网络环境下的一种标准传输方式

十四、FastMCP 做到了哪一层?

如果项目使用的是 FastMCP,也可以用这套分层来理解它。

FastMCP 不只是一个 JSON-RPC 库。

更准确地说:

FastMCP 是 MCP server 框架

它帮开发者处理了好几层:

Python 函数
   ↓
FastMCP tool/resource/prompt 封装
   ↓
MCP 协议语义
   ↓
JSON-RPC 消息处理
   ↓
Transport 层:stdio / Streamable HTTP 等

例如:

from fastmcp import FastMCP

mcp = FastMCP("MyServer")

@mcp.tool
def search(keyword: str) -> str:
    return "result"

mcp.run()

开发者主要关心的是:

search 这个工具怎么实现

至于下面这些:

tools/list 怎么响应
tools/call 怎么解析
JSON-RPC id 怎么匹配
result/error 怎么包装
stdio 怎么收发
HTTP /mcp 怎么暴露
SSE 怎么处理

FastMCP 已经封装了很多。

如果是本地 stdio:

mcp.run(transport="stdio")

如果是远程 HTTP MCP server:

mcp.run(
    transport="streamable-http",
    host="0.0.0.0",
    port=8000,
)

所以 FastMCP 的位置可以理解为:

帮开发者把 Python 函数变成 MCP server
并且支持不同 transport

但它不是完整的生产级 SaaS 网关。

下面这些工程能力通常还需要自己补:

HTTPS
鉴权
用户体系
租户隔离
限流
日志审计
监控
部署
反向代理

十五、完整心智模型

可以把 MCP 相关调用理解成这样:

Agent
  ↓
Transport
  ↓
JSON-RPC
  ↓
MCP methods
  ↓
Tools / Resources / Prompts
  ↓
真实业务逻辑

或者更直观一点:

MCP:
定义 Agent 能调用哪些能力

JSON-RPC:
定义调用消息怎么表示

Transport:
定义消息怎么传过去

FastMCP:
帮开发者把 Python 函数包装成 MCP server,并处理协议和传输细节

几种常见 transport 可以这样区分:

stdio:
本地开发、本地工具、单用户

Streamable HTTP:
远程 Agent、多用户、服务化、公网或局域网访问

SSE:
Streamable HTTP 里需要流式返回时再用

十六、项目里应该怎么选?

如果只是本地开发、本地 Claude/Codex 调工具:

stdio 就够了

如果要做成远程 MCP 服务,让不同 Agent 或不同用户访问:

Streamable HTTP

如果工具调用是普通查询:

Streamable HTTP + 普通 JSON response

如果工具调用是长任务,需要进度通知:

Streamable HTTP + SSE

所以一个比较稳的工程路线是:

先把 FastMCP 的 stdio 工具跑通
再切到 Streamable HTTP
先用普通 JSON 返回
后面确实需要长任务进度,再考虑 SSE

这样可以避免一开始就把所有复杂度都堆上去。


十七、总结

最后可以用这几句话收束:

RPC 是思想,不是具体协议。

JSON-RPC 是 RPC 的轻量实现,专注于 method、params、result、error。

MCP 使用 JSON-RPC 来表达 Agent 对工具、资源、prompt 的调用。

Transport 决定 JSON-RPC 消息怎么传输。

stdio 是本地进程管道,适合本地单用户。

Streamable HTTP 是 MCP 的远程 HTTP transport,适合服务化和 Agent 远程连接。

Streamable HTTP 不等于 SSE,它只是可以使用 SSE 做流式返回。

FastMCP 不只是 JSON-RPC 层,它也支持 transport 层,比如 stdio 和 Streamable HTTP。

最重要的一句话是:

MCP 不是 HTTP,JSON-RPC 不是 Transport,SSE 也不是 MCP。

它们的关系应该是:

MCP 语义
  ↓
JSON-RPC 消息
  ↓
Transport 传输
  ↓
stdio / Streamable HTTP / SSE

理解这套分层之后,MCP 相关概念就会清晰很多。

后续真正落地时,可以按这个路线推进:

用 FastMCP 写工具
本地用 stdio 调试
远程用 Streamable HTTP 暴露 /mcp
需要长任务时再加 SSE
生产环境补鉴权、限流、日志、网关和租户隔离

这样既不会过早复杂化,也能为后续服务化留好空间。

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号