---
title: "MCP 传输层：stdio 与 Streamable HTTP"
wiki: mcp
category: "协议规范"
slug: mcp-transport
url: https://learnagent.wiki/mcp/cards/mcp-transport
tags: ["MCP", "Transport", "stdio", "HTTP", "SSE", "传输"]
last_updated: 2026-04-11
reading_time: 12
---

> 在前面两张卡片里我们已经知道：MCP 的数据层用 JSON-RPC 2.0 定义了"消息长什么样"，而传输层要解决的是另一个问题——**这些 JSON 消息怎么从 Client 送到 Server、再送回来**。

# MCP 传输层：stdio 与 Streamable HTTP

## 基础概念

在前面两张卡片里我们已经知道：MCP 的数据层用 JSON-RPC 2.0 定义了"消息长什么样"，而传输层要解决的是另一个问题——**这些 JSON 消息怎么从 Client 送到 Server、再送回来**。

MCP 目前定义了两种标准传输方式：

| 传输方式 | 一句话 | 适用场景 |
|---------|--------|----------|
| **stdio** | Client 启动一个子进程，通过标准输入/输出管道交换 JSON 消息 | 本地 Server（Filesystem、Git、SQLite 等跑在你电脑上的程序） |
| **Streamable HTTP** | Client 通过 HTTP POST 发送消息，Server 可以用 SSE 流式返回 | 远程 Server（Sentry、Cloudflare 等跑在云端的服务） |

用一个类比：**stdio 像面对面说话**——你和对方在同一个房间里，声音直接传过去，不需要电话线、不需要网络。**Streamable HTTP 像打电话**——对方在远方，你需要拨号、等接通、有可能断线重连，但好处是距离不是问题。

两种传输方式之上的 JSON-RPC 消息格式是**完全一样的**。换句话说，同一个 Server 的业务代码不用改一行，只要切换启动方式就能从本地切到远程。

### stdio 工作原理

stdio 是最简单、最常用的传输方式。流程如下：

```mermaid
sequenceDiagram
    participant Host as Host（Claude Desktop）
    participant Server as Server 子进程

    Host->>+Server: 启动子进程（spawn）
    Note right of Server: 进程 stdin/stdout<br/>就是通信管道
    
    loop 消息交换
        Host->>Server: 写入 stdin（JSON-RPC 请求）
        Server->>Host: 写入 stdout（JSON-RPC 响应）
        Server--)Host: stderr 可选日志输出
    end
    
    Host->>Server: 关闭 stdin
    deactivate Server
    Note right of Server: 子进程退出
```

关键规则：

- **消息用换行符分隔**，每条消息占一行，消息内部**不能有换行符**
- Server 的 **stdout 只能写合法的 JSON-RPC 消息**，不能混入 `print("debug")` 之类的调试信息
- Server 可以把日志写到 **stderr**，Client 可以选择捕获、转发或忽略
- Client 关闭 stdin 就意味着通信结束，Server 应该收到信号后退出

一个真实的 stdio 消息交换看起来是这样的（每行一条 JSON）：

```text
→ stdin:  {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18",...}}
→ stdin:  {"jsonrpc":"2.0","method":"notifications/initialized"}
→ stdin:  {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← stdout: {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
```

### Streamable HTTP 工作原理

Streamable HTTP 是 MCP 2025 年引入的新传输方式（替代了旧版的 HTTP+SSE transport）。它用一个**单一的 HTTP 端点**（比如 `https://example.com/mcp`）同时处理 POST 和 GET 请求：

```mermaid
sequenceDiagram
    participant Client
    participant Server as Server（远程）

    Note over Client,Server: 初始化
    Client->>+Server: POST /mcp（InitializeRequest）
    Server->>-Client: 200 OK + InitializeResponse<br/>Mcp-Session-Id: abc123

    Note over Client,Server: Client 发送请求
    Client->>+Server: POST /mcp（tools/call）<br/>Mcp-Session-Id: abc123
    
    alt 简单响应
        Server->>Client: 200 OK（application/json）
    else 流式响应
        Server--)Client: SSE 流（text/event-stream）
        Server--)Client: SSE: 中间通知...
        Server--)Client: SSE: 最终响应
    end
    deactivate Server

    Note over Client,Server: Server 主动推送
    Client->>+Server: GET /mcp<br/>Mcp-Session-Id: abc123
    loop SSE 流
        Server--)Client: SSE: 通知/请求
    end
    deactivate Server
```

关键机制：

- **POST 发消息**：Client 的每条 JSON-RPC 消息都用 HTTP POST 发送，请求头需要声明接受 `application/json` 和 `text/event-stream` 两种响应格式
- **SSE 流式返回**：Server 可以选择返回单条 JSON（简单场景），或者开启一个 SSE 流来发送多条消息（需要流式输出或中间通知时）
- **GET 监听**：Client 可以通过 GET 请求打开一个 SSE 流，让 Server 能主动推送通知（不需要 Client 先发请求）
- **Session 管理**：Server 在初始化时返回一个 `Mcp-Session-Id`，后续所有请求都需要带上这个 ID
- **断线恢复**：SSE 事件可以带 `id` 字段，断线后 Client 用 `Last-Event-Id` 请求头恢复，Server 从断点续传

### 旧版 HTTP+SSE（已弃用）

2024-11-05 版本的 MCP 协议用的是另一种传输方式：Server 先通过一个 SSE 端点返回一个 POST URL，Client 再往那个 URL 发消息。这种方式已经被 Streamable HTTP 替代，但为了向后兼容：

- **新 Server 想支持老 Client**：同时保留旧的 SSE + POST 端点和新的统一 MCP 端点
- **新 Client 想连老 Server**：先尝试 POST 到 Server URL，如果返回 4xx 错误，再退回到 GET 模式走旧流程

### 安全与鉴权

stdio 不需要鉴权（因为 Server 就是 Host 启动的本地子进程，天然可信）。Streamable HTTP 面对的是公网环境，所以需要鉴权：

| 鉴权方式 | 说明 |
|---------|------|
| **OAuth 2.1** | MCP 推荐的标准方式，适合企业级远程 Server |
| **Bearer Token** | 在 HTTP `Authorization` 头里传 token |
| **API Key** | 作为查询参数或自定义请求头传递 |

此外，远程 Server 还需要注意：
- **验证 `Origin` 请求头**，防止 DNS 重绑定攻击
- **绑定 localhost（127.0.0.1）**而非 0.0.0.0，避免本地 Server 被局域网内其他设备访问

## 基础用法

### stdio 配置示例（最常见）

在 Claude Desktop 的 `claude_desktop_config.json` 里配一个本地 Server，用的就是 stdio：

```json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/Desktop"]
    }
  }
}
```

Host 看到 `command` + `args` 就知道：启动这个命令作为子进程，通过 stdin/stdout 通信。不需要写 `transport: "stdio"`，因为**有 `command` 字段的默认就是 stdio**。

### Streamable HTTP 配置示例

连接一个远程 Server，比如某个部署在云端的 MCP 服务：

```json
{
  "mcpServers": {
    "remote-service": {
      "url": "https://mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer your-api-token-here"
      }
    }
  }
}
```

Host 看到 `url` 字段就知道：这是一个远程 Server，通过 Streamable HTTP 通信。`headers` 用于传鉴权信息。

### 用 Python SDK 写一个同时支持两种 transport 的 Server

```python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def hello(name: str) -> str:
    """Say hello"""
    return f"Hello, {name}!"

# stdio 模式启动（本地开发）
# 命令行运行：python server.py
if __name__ == "__main__":
    mcp.run()  # 默认 stdio

# 如果要切换成 HTTP 模式（远程部署），只需改启动方式：
# mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
```

注意看：**Tool 的代码一行都不用改**，只是启动方式从 `mcp.run()` 变成 `mcp.run(transport="streamable-http")`。这就是数据层和传输层分离的好处。

## 同类工具对比

| 维度 | MCP stdio | MCP Streamable HTTP | gRPC | REST API | WebSocket |
|------|-----------|---------------------|------|----------|-----------|
| 通信方向 | 双向（stdin/stdout） | 双向（POST + SSE） | 双向 | 单向（请求-响应） | 双向 |
| 网络需求 | 无（本地管道） | 需要 HTTP | 需要 HTTP/2 | 需要 HTTP | 需要 TCP |
| 流式支持 | 逐行 JSON | SSE 流 | 原生流 | 不支持 | 原生流 |
| 鉴权 | 不需要 | OAuth / Bearer / API Key | mTLS / Token | 任意 | 任意 |
| 协议开销 | 极低 | HTTP 头开销 | Protobuf 编码开销 | HTTP 头开销 | 帧头开销 |
| 适合场景 | 本地工具 | 远程服务 | 微服务间通信 | 通用 API | 实时通信 |

核心区别：

- **stdio**：专为"Host 和 Server 在同一台机器"设计，零网络开销、零鉴权负担，是本地 MCP 的最佳选择
- **Streamable HTTP**：复用 HTTP 生态（代理、CDN、鉴权），是远程 MCP 的标准方案
- **gRPC / WebSocket**：MCP 目前没有官方支持，但协议是传输无关的，社区可以自行实现

## 常见误区

| 误区 | 准确理解 |
|------|----------|
| 以为 stdio 只能传文本 | stdio 传的是 JSON-RPC 消息（UTF-8 编码的 JSON），工具返回的图片/音频数据用 base64 编码后也能通过 JSON 在 stdio 里传输 |
| 以为 Streamable HTTP 就是 REST API | 不是。REST 是请求-响应模式，而 Streamable HTTP 支持 SSE 流式推送和 GET 长连接，Server 可以主动给 Client 发通知。更接近"HTTP 上的双向消息通道" |
| 以为用了 HTTP transport 就不需要初始化握手了 | 不管用什么 transport，MCP 都需要先完成 `initialize` → `initialized` 的生命周期握手。HTTP transport 的 `Mcp-Session-Id` 就是握手后分配的 |
| 以为 Server 的 `print()` 可以随便用 | 在 stdio 模式下，stdout 是消息管道！如果 Server 里有 `print("debug")`，这条文本会混入 JSON 消息流，直接搞坏协议。调试信息**必须写到 stderr** |
| 以为旧的 HTTP+SSE transport 还能用 | 新版 MCP（2025-03-26 起）官方已用 Streamable HTTP 替代旧版。旧版仍可向后兼容，但新 Server 应该只实现 Streamable HTTP |

## 优劣势分析

| 优势 | 劣势 |
|------|------|
| **stdio 极简**：不需要网络、不需要鉴权、不需要端口，Host 启动子进程就完事，调试只需看 stdin/stdout | **stdio 无法远程**：Host 和 Server 必须在同一台机器上，不能跨网络调用 |
| **Streamable HTTP 全能**：支持流式、断线恢复、Session 管理、OAuth 鉴权，企业级特性齐全 | **Streamable HTTP 复杂度高**：要处理 Session ID、SSE 流管理、断线重连、Origin 验证等，实现成本远高于 stdio |
| **传输无关的数据层**：换 transport 不需要改业务代码，一份 Server 逻辑同时适配本地和远程 | **没有官方的 WebSocket/gRPC transport**：对于需要低延迟双向实时通信的场景，社区实现还不成熟 |
| **向后兼容设计**：新 Client 有标准的降级流程来连接旧版 HTTP+SSE Server | **stdio 的 print 陷阱**：新手在 Server 里写了 `print()` 导致协议被污染，这是最常见的调试痛点 |

## 思考题

<details>
<summary>初级：为什么 stdio 模式下 Server 不能用 print() 输出调试信息？</summary>

**参考答案：**

因为在 stdio 传输中，Server 进程的 **stdout 是和 Client 通信的管道**。Client 会把 stdout 上收到的每一行都当作 JSON-RPC 消息来解析。如果 Server 代码里有一句 `print("debug info")`，这段文本会被 Client 当成 JSON 来解析，直接报错——因为 `"debug info"` 根本不是合法的 JSON-RPC 消息。

正确做法：调试信息写到 **stderr**（`import sys; print("debug", file=sys.stderr)`），或者用 MCP 的 Logging 原语发日志给 Client。Python SDK 的 FastMCP 框架会自动帮你把 Python 的 `logging` 模块重定向到 stderr，所以用 `logging.debug()` 是安全的。

</details>

<details>
<summary>中级：一个 Server 想同时支持本地调试（stdio）和线上部署（HTTP），代码架构上应该怎么设计？</summary>

**参考答案：**

MCP 的分层设计天然支持这个需求。关键思路是把业务逻辑（注册 Tool / Resource / Prompt）和传输层启动方式分开。

以 Python SDK 为例：

```python
from mcp.server.fastmcp import FastMCP
import sys

mcp = FastMCP("my-server")

# 业务逻辑——不关心传输方式
@mcp.tool()
def my_tool(input: str) -> str:
    return f"processed: {input}"

# 启动方式——根据参数切换 transport
if __name__ == "__main__":
    if "--http" in sys.argv:
        mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
    else:
        mcp.run()  # 默认 stdio
```

本地开发时 `python server.py` 走 stdio，部署时 `python server.py --http` 走 HTTP。Tool 注册代码一行不改。

更成熟的做法是用环境变量（`MCP_TRANSPORT=http`）或者配置文件来切换，然后在 CI/CD 里根据环境自动选择。Docker 镜像里通常默认启动 HTTP 模式。

</details>

<details>
<summary>进阶：Streamable HTTP 的"断线恢复"（Resumability）机制是怎么工作的？为什么 stdio 不需要这个机制？</summary>

**参考答案：**

Streamable HTTP 的断线恢复基于 SSE 规范的 `id` 和 `Last-Event-ID` 机制：

1. Server 在 SSE 流的每个事件上附加一个唯一的 `id`（在当前 Session 内全局唯一）
2. 如果 Client 和 Server 之间的 HTTP 连接断开（网络波动、超时等）
3. Client 重新发起 GET 请求时，在 `Last-Event-ID` 请求头里带上最后收到的事件 ID
4. Server 从那个 ID 之后开始重发消息，Client 不会丢失断线期间 Server 发的数据

这个机制本质上是在 SSE 流上实现了一个**游标（cursor）**，让流可以从任意点恢复。

stdio 不需要这个机制，因为 stdio 是**进程内管道**，两端始终在同一台机器的同一个操作系统里。管道要么存在（进程活着），要么不存在（进程退了），不存在"断一下再重连"的中间状态。如果子进程崩了，Host 会重新 spawn 一个新进程，从头开始建立连接，不存在"从上次断点续传"的需求。

</details>

## 参考资料

1. 官方传输层文档：<https://modelcontextprotocol.io/docs/concepts/transports>（查询日期 2026-04-11）
2. 官方架构文档（传输层概述）：<https://modelcontextprotocol.io/docs/concepts/architecture>
3. MCP 协议规范 - Streamable HTTP Transport：<https://modelcontextprotocol.io/specification/2025-06-18/basic/transports>
4. MCP 协议规范 - 生命周期管理：<https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle>
5. 旧版 HTTP+SSE Transport 规范（已弃用）：<https://modelcontextprotocol.io/specification/2024-11-05/basic/transports>
6. SSE 标准（W3C）：<https://html.spec.whatwg.org/multipage/server-sent-events.html>

---
*Source: https://learnagent.wiki/mcp/cards/mcp-transport*
*Markdown mirror of https://learnagent.wiki, served as text/markdown for LLM ingestion.*