从 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

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号