从 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

三台机器部署 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号