本教程面向已经写过至少一个 MCP(Model Context Protocol)服务器、却卡在"该用哪种传输方式"这一步的开发者。学完之后你将能够:清晰区分 stdio、HTTP+SSE(旧版)与 Streamable HTTP(2025-03-26 规范引入)三种传输模式的工作原理与适用场景;用同一份 Python/TypeScript 服务器代码在三种模式之间无缝切换;针对本地工具、团队共享、公网生产三类需求做出正确的传输选型。适用人群:MCP 工具开发者、想把内部系统暴露给 Claude Code/Cursor 的平台工程师。
FastMCP(Python)或 @modelcontextprotocol/sdk(TypeScript)跑通一个最简单的 echo 工具。npx 可执行。.mcp.json 并重启 MCP 服务器。很多人写完第一个 MCP 工具后就停下来,以为协议不过如此。但当你想把工具从"只给自己用"推进到"团队共享""部署上云""对接 Cursor 和 Claude Code 双客户端"时,第一个撞墙的问题不是工具逻辑,而是传输模式。
MCP 协议本身定义的是客户端与服务器之间的 JSON-RPC 2.0 消息集合(initialize、tools/list、tools/call 等),它刻意把传输层抽象掉了。也就是说,同一套业务逻辑(你的工具、资源、提示词)可以跑在完全不同的"管道"上,而协议握手代码一行都不用改。这正是传输层的价值,也是它的陷阱——选错管道,轻则延迟翻倍,重则在生产环境频繁掉线、无法水平扩展。
协议当前规范明确支持两种官方传输模式,外加一种正在被淘汰的旧模式:
下面逐一拆解。
stdio 模式下,MCP 客户端(如 Claude Code)把你的服务器当一个子进程拉起,通过进程的 stdin 写入请求、从 stdout 读取响应。消息采用换行分隔的 JSON-RPC(newline-delimited JSON):每条消息独占一行,以 \n 结尾。
客户端进程 ──stdin──▶ MCP服务器子进程
◀──stdout─
一个致命的初学者坑:stdout 被协议独占。你在代码里写的任何 print()、日志输出都会污染 JSON-RPC 流,导致客户端解析失败、工具"莫名消失"。所有调试日志必须走 stderr。
# server.py —— stdio 模式(默认)
from mcp.server.fastmcp import FastMCP
import sys
mcp = FastMCP("weather")
@mcp.tool()
def get_temp(city: str) -> str:
"""查询城市温度(模拟)"""
sys.stderr.write(f"[debug] query {city}\n") # 日志走 stderr
return f"{city}: 23°C"
if __name__ == "__main__":
mcp.run() # 不传 transport,默认就是 stdio
.mcp.json 配置:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["server.py"]
}
}
}
Claude Code 启动时会 fork 这个进程,握手成功后 tools/list 返回 get_temp。调用时 Claude 自动把参数序列化为 JSON-RPC 写进 stdin,服务器在 stdout 返回结果,整个过程零网络开销,单次调用框架开销通常在亚毫秒级。
print() 污染了 stdout。把所有输出改成 sys.stderr 或日志库。python.exe 全名,避免 PATH 问题;注意 command 不要写成相对路径的 shell 脚本而不带解释器。uv run 或预热导入来缓解。2024 年 MCP 初版里,远程服务器用的是 HTTP + Server-Sent Events 双端点方案:
GET /sse 建立一条长期保持的 SSE 连接,服务器通过它推送所有消息。POST /messages 发送请求。这套方案的问题很直接:
正因如此,规范在 2025-03-26 版本里用 Streamable HTTP 替换了它。旧代码里的 transport="sse" 在新 SDK 中仍然可用以保持兼容,但新项目不要再用。本文提及它,主要是为了你在阅读老博客、老仓库时能识别并果断迁移。
Streamable HTTP 把双端点压缩成单一端点(通常 /mcp),用普通 HTTP 语义承载 JSON-RPC:
POST /mcp:客户端→服务器的所有请求都从这里走。响应可以是普通 JSON,也可以升级为 SSE 流(当服务器需要中途推送日志、进度时)。GET /mcp:可选,打开一条 SSE 流接收服务器主动推送的通知(例如长任务的进度)。Mcp-Session-Id 响应头:初始化时服务器返回,客户端后续请求带上它,即可维持有状态会话;不带则进入无状态模式。Accept: application/json, text/event-stream:客户端声明自己两种响应格式都能处理。它的本质优势是回归了无状态、短连接的 HTTP 模型:每个请求自包含、可独立重试、可被任意负载均衡器分发、可在 serverless 上跑。需要流式时再"升级"为 SSE,不需要时就是普通 REST。这种弹性让它成为云原生部署的首选。
# server_http.py —— Streamable HTTP 模式
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_temp(city: str) -> str:
"""查询城市温度(模拟)"""
return f"{city}: 23°C"
if __name__ == "__main__":
# 与 stdio 版本唯一的区别:指定 transport 与监听参数
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8000,
# FastMCP 默认挂在 /mcp 路径
)
运行:
pip install "mcp[cli]"
python server_http.py
# 控制台输出:Uvicorn running on http://0.0.0.0:8000
.mcp.json 配置(Claude Code 连接远程):
{
"mcpServers": {
"weather-remote": {
"url": "http://localhost:8000/mcp"
}
}
}
注意:连 stdio 用的是 command/args;连 Streamable HTTP 用的是 url。这是配置层面最直观的差异。
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
const server = new McpServer({ name: "weather", version: "1.0.0" });
// 注册工具(省略 schema 细节)
server.tool("get_temp", { city: z.string() }, async ({ city }) => ({
content: [{ type: "text", text: `${city}: 23°C` }],
}));
// 每个会话一个 transport 实例(有状态)
const sessions = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && sessions.has(sessionId)) {
transport = sessions.get(sessionId)!;
} else {
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
transport.onsessioninitialized = (id) => sessions.set(id, transport);
await server.connect(transport);
}
await transport.handleRequest(req, res);
});
app.listen(8000);
TypeScript 版本更能看清"会话与 transport 实例绑定"的细节:sessionIdGenerator 决定是否启用有状态模式,返回 undefined 即进入无状态模式。
proxy_buffering off; 并设置合理的 proxy_read_timeout,否则进度流永远到不了客户端。| 维度 | stdio | HTTP+SSE(旧) | Streamable HTTP(推荐) |
|---|---|---|---|
| 连接模型 | 本地进程管道 | 双端点+长连接 | 单端点+短连接,可升级流 |
| 状态 | 进程内状态 | 黏连在 SSE 连接 | 可有状态(Mcp-Session-Id)可无状态 |
| 部署位置 | 仅本地 | 需公网可达端点 | 需公网可达端点 |
| 延迟 | 最低(亚 ms 框架开销) | 中(长连接,但有双端点握手) | 略高(每次 HTTP RTT,本地约 1-5ms) |
| 水平扩展 | 不适用(单机) | 差(sticky session) | 好(无状态请求可任意分发) |
| serverless 友好 | 否 | 否 | 是 |
| 多客户端共享 | 否(各自拉进程) | 是 | 是 |
| 认证需求 | 无(本地信任) | 需自建 | 需 OAuth2.1/网关 |
| 规范状态 | 官方 | deprecated | 官方推荐 |
需要强调的延迟观感:stdio 因为没有 TCP/TLS 握手,工具调用的稳态延迟确实最低;但 Streamable HTTP 在启用 HTTP keep-alive 后,单次工具调用的网络开销可压到很低,而它换来的可共享、可扩展、可上云收益,对绝大多数非本地场景是值得的。
目标:同一份 get_temp 业务代码,既能在自己机器上以 stdio 秒起,又能部署到团队服务器供所有人通过 Claude Code 调用。
第 1 步:抽出业务逻辑为纯函数,传输层只做薄壳。
# logic.py
def get_temp(city: str) -> str:
return f"{city}: 23°C"
第 2 步:用环境变量切换传输,避免维护两份入口。
# server.py
import os
from mcp.server.fastmcp import FastMCP
from logic import get_temp
mcp = FastMCP("weather")
mcp.tool()(get_temp)
if __name__ == "__main__":
mode = os.getenv("MCP_TRANSPORT", "stdio")
if mode == "stdio":
mcp.run()
elif mode == "streamable-http":
mcp.run(transport="streamable-http",
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "8000")))
第 3 步:本地自用——python server.py,配 command。
第 4 步:团队部署——容器化后设 MCP_TRANSPORT=streamable-http,配 url。
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install "mcp[cli]"
ENV MCP_TRANSPORT=streamable-http
EXPOSE 8000
CMD ["python", "server.py"]
第 5 步:用 MCP Inspector 验证远程端点(不依赖 Claude Code,便于 CI 调试)。
npx @modelcontextprotocol/inspector
# 在打开的网页里选 "Streamable HTTP",填 http://localhost:8000/mcp,点 Connect
# 若 tools/list 能列出 get_temp,说明远程服务就绪
这一步是工程化的关键:Inspector 让你脱离具体客户端,直接在协议层验证传输是否通,是排查"Claude Code 里看不到工具"问题的利器。
下图展示三种传输的连接拓扑差异:
flowchart LR
subgraph Local["本地场景:stdio"]
A1[Claude Code] -->|fork stdin/stdout| S1[服务器子进程]
end
subgraph Legacy["旧远程:HTTP+SSE(已废弃)"]
A2[客户端] -.GET /sse 长连接.-> S2[服务器]
A2 -->|POST /messages| S2
end
subgraph Modern["现代远程:Streamable HTTP"]
A3[Claude Code/Cursor] -->|POST /mcp\nMcp-Session-Id| S3[服务器集群]
S3 -.可选 GET /mcp\nSSE 进度流.-> A3
LB[负载均衡] --> S3
end
style Modern fill:#e8f5e9
style Legacy fill:#ffebee
选型决策树:
flowchart TD
Q1{只给自己用\n且在本机?}
Q1 -->|是| STDIO[选 stdio]
Q1 -->|否| Q2{需要多人/多客户端共享?}
Q2 -->|否| STDIO
Q2 -->|是| Q3{部署在 serverless\n或需水平扩展?}
Q3 -->|是| STREAM[选 Streamable HTTP\n+ OAuth2.1]
Q3 -->|否| Q4{现有老代码是 SSE?}
Q4 -->|是| MIG[迁移到 Streamable HTTP]
Q4 -->|否| STREAM
style STREAM fill:#e8f5e9
style STDIO fill:#e3f2fd
style MIG fill:#fff3e0
一句话总结:本地用 stdio,远程用 Streamable HTTP,任何新代码都别再碰旧版 SSE。
Mcp-Session-Id、Accept 头、流式升级规则)。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
