首页 › tutorials › MCP三种传输模式横评与实战选型

MCP三种传输模式横评与实战选型:从stdio到Streamable HTTP

概述

本教程面向已经写过至少一个 MCP(Model Context Protocol)服务器、却卡在"该用哪种传输方式"这一步的开发者。学完之后你将能够:清晰区分 stdio、HTTP+SSE(旧版)与 Streamable HTTP(2025-03-26 规范引入)三种传输模式的工作原理与适用场景;用同一份 Python/TypeScript 服务器代码在三种模式之间无缝切换;针对本地工具、团队共享、公网生产三类需求做出正确的传输选型。适用人群:MCP 工具开发者、想把内部系统暴露给 Claude Code/Cursor 的平台工程师。

前置条件


一、为什么传输层是 MCP 工程化的第一道分水岭

很多人写完第一个 MCP 工具后就停下来,以为协议不过如此。但当你想把工具从"只给自己用"推进到"团队共享""部署上云""对接 Cursor 和 Claude Code 双客户端"时,第一个撞墙的问题不是工具逻辑,而是传输模式。

MCP 协议本身定义的是客户端与服务器之间的 JSON-RPC 2.0 消息集合(initialize、tools/list、tools/call 等),它刻意把传输层抽象掉了。也就是说,同一套业务逻辑(你的工具、资源、提示词)可以跑在完全不同的"管道"上,而协议握手代码一行都不用改。这正是传输层的价值,也是它的陷阱——选错管道,轻则延迟翻倍,重则在生产环境频繁掉线、无法水平扩展。

协议当前规范明确支持两种官方传输模式,外加一种正在被淘汰的旧模式:

  1. stdio——本地子进程,标准输入输出管道。
  2. Streamable HTTP——2025-03-26 规范引入,单一 HTTP 端点,是当前远程部署的推荐方式。
  3. HTTP+SSE(旧版)——2024 年初版的远程方案,依赖长连接双端点,已被 Streamable HTTP 取代,规范标注为 deprecated。

下面逐一拆解。

二、stdio:最简单也最被低估的本地管道

工作原理

stdio 模式下,MCP 客户端(如 Claude Code)把你的服务器当一个子进程拉起,通过进程的 stdin 写入请求、从 stdout 读取响应。消息采用换行分隔的 JSON-RPC(newline-delimited JSON):每条消息独占一行,以 \n 结尾。

客户端进程 ──stdin──▶ MCP服务器子进程
          ◀──stdout─

一个致命的初学者坑:stdout 被协议独占。你在代码里写的任何 print()、日志输出都会污染 JSON-RPC 流,导致客户端解析失败、工具"莫名消失"。所有调试日志必须走 stderr。

代码示例(Python FastMCP)

# 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 返回结果,整个过程零网络开销,单次调用框架开销通常在亚毫秒级。

常见问题

三、HTTP+SSE(旧版):理解它是为了知道为什么被淘汰

2024 年 MCP 初版里,远程服务器用的是 HTTP + Server-Sent Events 双端点方案:

这套方案的问题很直接:

  1. 两条连接,状态绑定在 SSE 长连接上,断了就要重新握手、重建会话。
  2. 长连接对基础设施不友好:serverless 平台会掐断、负载均衡器需要 sticky session、CDN/反代常常 buffer 事件流导致"卡住"。
  3. 水平扩展困难,因为会话状态黏在单条连接上。

正因如此,规范在 2025-03-26 版本里用 Streamable HTTP 替换了它。旧代码里的 transport="sse" 在新 SDK 中仍然可用以保持兼容,但新项目不要再用。本文提及它,主要是为了你在阅读老博客、老仓库时能识别并果断迁移。

四、Streamable HTTP:当前远程部署的标准答案

核心设计

Streamable HTTP 把双端点压缩成单一端点(通常 /mcp),用普通 HTTP 语义承载 JSON-RPC:

它的本质优势是回归了无状态、短连接的 HTTP 模型:每个请求自包含、可独立重试、可被任意负载均衡器分发、可在 serverless 上跑。需要流式时再"升级"为 SSE,不需要时就是普通 REST。这种弹性让它成为云原生部署的首选。

代码示例(Python FastMCP,从 stdio 切换只需一行)

# 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。这是配置层面最直观的差异。

TypeScript SDK 版本

// 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 即进入无状态模式。

常见问题

五、三种模式横评一览

维度 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。

八、延伸阅读

九、训练营适用性评估


参考来源

  1. Transports — Model Context Protocol 官方规范 — 协议级定义 JSON-RPC 编码、stdio 与 Streamable HTTP 传输要求,Mcp-Session-Id 会话语义
  2. Understanding MCP Server Transports: STDIO, SSE, and HTTP Streamable — 三种传输模式原理拆解与代码示例
  3. MCP Transports Compared: stdio vs SSE vs Streamable HTTP (2026) — 针对各客户端(Claude/Cursor/VS Code)的传输选型对比
  4. MCP Streamable HTTP Production Guide 2026: stdio vs Streamable HTTP — 生产部署视角的延迟、扩展性与容器化实践
  5. MCP Transport: Stdio vs Streamable HTTP — Architecture, Latency — 企业级部署的架构权衡与延迟分析

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入