本教程带你完成 MCP 服务器从「本地 stdio 子进程」到「远程 Streamable HTTP 服务」的关键一跃。你会搞清三种传输方式的取舍,用 FastMCP 写出同一份代码、靠一个参数切换传输,再用 Docker 容器化部署,最后让团队里任意一台机器上的 Claude Code 通过带鉴权的 HTTP 端点接入。适合已能用 stdio 跑通 MCP、想让工具被多人/CI 共享的后端与平台工程师。
claude(Claude Code 2.1.1 及以上,原生支持远程 HTTP MCP)、Node 18+、Python 3.11+、uv(pip install uv 或官方安装脚本)、Docker。0.0.0.0 演示均可),以及一个能签发 Bearer Token 的简单机制(演示阶段可手写随机串)。很多教程止步于 stdio:Claude Code 在本机 fork 一个子进程,通过标准输入输出收发 JSON-RPC。这对个人本地工具够用,但有三个硬伤,正是 Streamable HTTP 要解决的:
一句话:stdio 是「我自己的工具」,远程 HTTP 是「团队/平台的工具」。
MCP 规范(截至 2025-06-18 修订版)定义了三类传输:
| 传输方式 | 规范版本 | 拓扑 | 会话 | 典型场景 | 状态 |
|---|---|---|---|---|---|
| stdio | 最早 | 客户端 fork 子进程 | 进程内 | 本地个人工具、文件操作 | 当前推荐(本地) |
| HTTP+SSE | 2024-11-05 | 双端点(POST + SSE GET) | 有状态 | 早期远程服务器 | 已废弃(2025-03-26 起) |
| Streamable HTTP | 2025-03-26 | 单端点 /mcp,POST+GET+DELETE |
可有可无 | 远程、Serverless、负载均衡 | 当前推荐(远程) |
旧的 HTTP+SSE 需要两个端点(一个 POST 发请求、一个长连 SSE 收推送),状态与连接强绑定,横向扩展痛苦。Streamable HTTP 把它收敛为单端点:
/mcp:客户端发 JSON-RPC 请求与通知。服务器可选择返回普通 application/json,或升级为 text/event-stream(SSE)做流式响应。/mcp:可选,客户端打开一条 SSE 长连,接收服务器主动推送(如长任务的进度通知、资源变更)。/mcp:显式结束会话。Mcp-Session-Id 头:服务器在 initialize 时下发,客户端后续请求回带。不传这个头就是无状态模式——这恰恰是 Serverless、负载均衡最想要的特性:任何副本都能处理任意请求。安全上规范要求:远程服务必须做 Origin 校验(防 DNS rebinding),必须上鉴权(OAuth 2.1 Bearer 是事实标准);本地调试时务必只绑 127.0.0.1 而非 0.0.0.0。
目标:写一个「团队变更日志」MCP 服务器,背后是一份共享的 SQLite。所有人接同一个远程端点,看到的就是同一份数据——这是 stdio 永远做不到的。
工具集:
list_changes(days) —— 列出近 N 天变更add_change(title, author, detail?) —— 新增一条(会立刻对所有人可见)get_change(change_id) —— 查单条详情mkdir changelog-mcp && cd changelog-mcp
uv init --no-readme
uv add fastmcp
server.py# server.py —— 同一份代码,传输方式由命令行参数决定
import sqlite3, time, uuid
from contextlib import closing
from fastmcp import FastMCP
DB = "/data/changelog.db" # 容器内持久化路径;本机调试可改 ./changelog.db
def _conn():
c = sqlite3.connect(DB)
c.execute("""CREATE TABLE IF NOT EXISTS changes(
id TEXT PRIMARY KEY, ts INTEGER, title TEXT, author TEXT, detail TEXT)""")
return c
mcp = FastMCP("changelog")
@mcp.tool()
def add_change(title: str, author: str, detail: str = "") -> str:
"""新增一条团队变更记录。title 简述、author 提交人、detail 可选正文。"""
cid = uuid.uuid4().hex[:8]
with closing(_conn()) as c:
c.execute("INSERT INTO changes VALUES(?,?,?,?,?)",
(cid, int(time.time()), title, author, detail))
c.commit()
return f"已记录 {cid}: {title}({author})"
@mcp.tool()
def list_changes(days: int = 7) -> str:
"""列出近 days 天内的变更,默认 7 天。"""
cutoff = int(time.time()) - days * 86400
with closing(_conn()) as c:
rows = c.execute(
"SELECT id, ts, title, author FROM changes WHERE ts>=? ORDER BY ts DESC",
(cutoff,)).fetchall()
if not rows:
return f"近 {days} 天无变更记录。"
return "\n".join(f"[{r[0]}] {time.strftime('%m-%d', time.localtime(r[1]))} {r[2]} @{r[3]}" for r in rows)
@mcp.tool()
def get_change(change_id: str) -> str:
"""按 id 查单条变更详情。"""
with closing(_conn()) as c:
r = c.execute("SELECT id, ts, title, author, detail FROM changes WHERE id=?",
(change_id,)).fetchone()
if not r:
return f"找不到 id={change_id}"
return f"[{r[0]}] {r[2]}\n提交人: {r[3]}\n时间: {time.strftime('%Y-%m-%d %H:%M', time.localtime(r[1]))}\n详情: {r[4] or '(无)'}"
if __name__ == "__main__":
mcp.run() # 默认 stdio;传输由 CLI 参数覆盖
FastMCP 2.x 的设计精髓就在这里——代码不写死传输,命令行切:
# 方式 A:stdio(本机自用,与 stdio 教程里的写法一致)
fastmcp run server.py
# 方式 B:Streamable HTTP(远程部署,团队共享)
fastmcp run server.py \
--transport streamable-http \
--host 0.0.0.0 --port 8000
预期结果:方式 B 启动后日志会出现类似
FastMCP streamable-http server running on http://0.0.0.0:8000/mcp
访问端点是 http://<host>:8000/mcp(注意带 /mcp 路径,这是 Streamable HTTP 的单端点)。
也可用
fastmcp dev server.py启动并自动打开 MCP Inspector,在浏览器里手动 POST 一条initialize请求,直观看到Mcp-Session-Id头如何下发、后续请求如何回带——这是理解协议最快的办法。
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock* ./
COPY server.py ./
RUN uv pip install --system --no-cache .
# 持久化目录(数据库落盘处)
RUN mkdir -p /data
EXPOSE 8000
CMD ["fastmcp", "run", "server.py", \
"--transport", "streamable-http", \
"--host", "0.0.0.0", "--port", "8000"]
docker build -t changelog-mcp:latest .
# 仅本机访问 → 绑 127.0.0.1;团队访问 → 0.0.0.0 + 反向代理上 TLS
docker run -d --name changelog \
-p 8000:8000 \
-v $(pwd)/data:/data \
changelog-mcp:latest
# 健康探活:发一条 initialize
curl -sS -X POST http://localhost:8000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
预期结果:响应头里能看到 Mcp-Session-Id: <uuid>,JSON 体里 result.capabilities 列出服务器支持的能力。拿到这个响应,说明传输层已通。
生产环境务必在前面加一层 nginx/Caddy 做 TLS 终结与 Origin 校验,再签发长期 Bearer Token,而不是裸跑 0.0.0.0:8000。
# 先对比:本地 stdio 接法
claude mcp add --transport stdio changelog-local -- uv run server.py
# 远程 HTTP 接法:注意选项必须在名字和 URL 之前
export MCP_TOKEN="$(openssl rand -hex 24)" # 与服务器侧约定
claude mcp add \
--transport http \
--scope user \
--header "Authorization: Bearer ${MCP_TOKEN}" \
changelog https://changelog.internal.corp/mcp
常用管理命令:
claude mcp list # 列出所有已配服务器及连接状态
claude mcp get changelog # 看单个服务器详情
claude mcp remove changelog
.mcp.json 接入(推荐团队/项目共享)在项目根放 .mcp.json,提交到仓库,全队 clone 即用:
{
"mcpServers": {
"changelog": {
"type": "http",
"url": "https://changelog.internal.corp/mcp",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}
${MCP_TOKEN} 从环境变量注入,避免凭证入库。首次进入项目,Claude Code 会提示是否信任并连接该 MCP。
在 Claude Code 里输入 /mcp,应看到 changelog 状态为 connected,并列出 add_change / list_changes / get_change 三个工具。然后直接对话测试:
「帮我看看近 3 天团队都改了什么」
Claude 会调用 list_changes(days=3),返回共享数据库里的真实数据——工程师 A 刚提交的记录,工程师 B 此刻也能看到。这就是远程化的全部价值。
401 Unauthorized / Token 没带上:检查 --header 是否在位置参数之前;.mcp.json 里 headers 键名是否正确(不是 header);${MCP_TOKEN} 是否真的在启动 Claude 的 shell 里 export 过。403 / Origin 被拒(DNS rebinding 防护):远程服务器默认校验 Origin/Host 头。浏览器调试时会被拦,需在服务器侧显式放行可信 Origin;生产环境务必保留校验。Mcp-Session-Id),LB 必须开 sticky session;或干脆让服务器走无状态模式(不返回/不校验 session id),任意副本可处理任意请求——这才是 Serverless 友好的姿势。curl 探活收到 SSE 流而非 JSON:正常。服务器对 initialize/tools/call 可能返回流式响应。加 Accept: application/json, text/event-stream 头即可,客户端 SDK 会自动处理。/data 没挂卷(-v ./data:/data),容器重启数据归零。生产建议换成 Postgres 等外部库。flowchart LR
subgraph Dev["工程师 A 本机"]
CC1[Claude Code]
end
subgraph CI["CI Runner"]
CC2[claude -p 无头]
end
subgraph Dev2["工程师 B 本机"]
CC3[Claude Code]
end
CC1 -->|"HTTPS + Bearer"| Proxy
CC2 -->|"HTTPS + Bearer"| Proxy
CC3 -->|"HTTPS + Bearer"| Proxy
Proxy["反向代理 nginx/Caddy\nTLS 终结 + Origin 校验"]
Proxy --> LB["负载均衡\n无状态可水平扩"]
LB --> S1["FastMCP 容器 1\n/mcp"]
LB --> S2["FastMCP 容器 2\n/mcp"]
S1 --> DB[("共享 PostgreSQL\n团队变更日志")]
S2 --> DB
Streamable HTTP 握手与一次工具调用的时序:
sequenceDiagram
participant CC as Claude Code
participant S as FastMCP /mcp
CC->>S: POST initialize (protocolVersion, capabilities)
S-->>CC: 200 + Mcp-Session-Id: abc123 (可选)
CC->>S: POST notifications/initialized
CC->>S: POST tools/list
S-->>CC: [add_change, list_changes, get_change]
CC->>S: POST tools/call add_change(...)
S-->>CC: text/event-stream (流式结果) 或 application/json
Note over CC,S: 长任务时: GET /mcp 开 SSE 收进度推送
modelcontextprotocol.io/specification/2025-06-18/basic/transports,三种传输的权威定义。gofastmcp.com/deployment/http,streamable-http 与 sse 两种 HTTP 部署的取舍与参数。code.claude.com/docs/en/mcp,claude mcp add 全部 scope/transport/header 选项。modelcontextprotocol.io/tools,浏览器里调试协议、可视化 session id 流转。claude -p 调 add_change 自动登记发布日志。claude mcp add 的 transport/scope/header 全部选项与 .mcp.json 配置streamable-http 部署参数、有状态与无状态取舍[!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 等场景全覆盖。
