本教程聚焦一个尚未被广泛讲透的 MCP 维度:把一个本地 stdio MCP 服务器改造成可远程访问的 Streamable HTTP 服务,并部署到云端、供团队乃至移动端共用。学完你将掌握:stdio 与 Streamable HTTP 的本质差异、单一 /mcp 端点的请求/响应契约、有状态与无状态两种会话模式、Cloudflare Workers 与 Docker 两种部署路径,以及如何在 Claude Code 中用一行 claude mcp add-json 挂载远程服务器并加上认证。适用于已经能写出本地 MCP 工具、想把它升级为「团队共享服务」或「多端可用能力」的开发者。
@modelcontextprotocol/sdk 或 mcp(FastMCP)add-json 命令格式)绝大多数入门教程的第一台 MCP 服务器都是 stdio 形态:claude mcp add xxx -- npx my-server,宿主 spawn 一个子进程,双方用 stdin/stdout 收发换行分隔的 JSON-RPC 消息。这对「本机开发工具」足够,但一旦你要把它变成一个产品级能力,三堵墙立刻出现:
| 维度 | stdio(本地子进程) | Streamable HTTP(远程服务) |
|---|---|---|
| 客户端形态 | 仅限能 spawn 子进程的桌面/CLI | CLI、Web、移动端、CI 皆可 |
| 多用户共享 | 每人各自 npx 安装一份 |
一处部署,N 端连接 |
| 密钥归属 | 凭据散落在每台机器 | 集中托管在服务端 |
| 状态与扩展 | 单进程、难水平扩展 | 可无状态横向扩展、可加负载均衡 |
| 网络可达性 | 仅本机 | 任意网络位置,可做监控/审计 |
结论很直接:当一个工具要被团队、被其他端、被线上 Agent 调用时,它必须从「进程」变成「服务」。这正是 Streamable HTTP 传输的存在意义。
MCP 在 2025-03-26 规范版本中引入 Streamable HTTP,取代了旧的「HTTP + SSE 双端点」设计。它的核心是单一端点 + 内容协商:
/mcp。POST /mcp,Content-Type: application/json,请求体是一条标准 JSON-RPC 2.0 消息;同时声明 Accept: application/json, text/event-stream,表示「我能收单条 JSON,也能收 SSE 流」。Content-Type: application/json 直接返回;Content-Type: text/event-stream,按 SSE 帧推送,每帧 event: message + data: {JSON-RPC}。Mcp-Session-Id 响应头;客户端在后续请求里把它原样带回。这就是「有状态」模式的会话句柄。GET /mcp(带 Accept: text/event-stream)打开一条长连接,接收服务器主动推送的通知/请求。DELETE /mcp(带会话头)即终止会话。理解了这套契约,你就明白为什么它比旧方案优雅:旧的 HTTP+SSE 需要两个端点(一个 POST 收请求、一个 GET 开 SSE 流),还有复杂的会话重连问题;Streamable HTTP 用一个端点 + 协商,把「单次问答」和「长连接流」统一了。
先写一个与传输无关的「纯逻辑」服务器,这样后面切换传输时业务代码一行都不用改。以 Python FastMCP 为例:
# server.py —— 业务逻辑与传输解耦
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("snippet-vault")
@mcp.tool()
def add_snippet(title: str, language: str, code: str) -> str:
"""保存一段代码片段,返回它的 ID。"""
# 这里省略持久化逻辑,返回伪 ID
sid = f"{language}-{abs(hash(title)) % 100000}"
return f"saved {sid}"
@mcp.tool()
def search_snippet(keyword: str) -> str:
"""按关键词检索代码片段。"""
return f"hits for '{keyword}': (示例)"
if __name__ == "__main__":
# 默认 stdio:本地开发期用
mcp.run()
本地验证(stdio 阶段):
# 在项目根目录
claude mcp add vault-local -- uv run server.py
claude mcp list # 应能看到 vault-local ✔ Connected
预期结果:claude mcp list 显示该服务器 Connected;在会话里问「帮我存一段 hello world」,模型会调用 add_snippet。常见问题:若显示 Failed to connect,多半是依赖未安装或启动命令路径不对——用 uv run 或 npx 时要确保包确实能跑起来。
FastMCP 让这一步几乎是「改一行」。把入口从 mcp.run() 换成显式指定传输:
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="0.0.0.0", # 上云必备:监听所有网卡
port=8000,
)
启动后,FastMCP 默认把端点挂在 /mcp。TypeScript SDK 的等价写法稍多一些显式装配,但表达力一致:
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: "snippet-vault", version: "1.0.0" });
// 注册 tool(...) 略
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.listen(8000);
注意上面把 sessionIdGenerator 设为 undefined,意味着选择了无状态模式——每个请求自包含、不保留会话,最适合无状态横向扩展。
部署前先在本地把契约跑通,能省下大量云端调试时间。直接对着 /mcp 发一次 initialize:
curl -i -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":"curl","version":"1.0"}
}
}'
预期结果:
200;Content-Type: application/json(单条响应)或 text/event-stream(流式);{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"...","serverInfo":{...},"capabilities":{"tools":{}}}};Mcp-Session-Id: <随机串>。拿到会话 ID 后,发 tools/call 时要带上它:
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <上一步拿到的ID>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"add_snippet",
"arguments":{"title":"hi","language":"python","code":"print(1)"}}}'
常见问题:若返回 406 Not Acceptable,说明 Accept 头没声明 text/event-stream,而服务器想用流式回包——补上即可;若返回 400 session not found,说明你漏带或带错了 Mcp-Session-Id。
这是远程部署最容易被忽略、却决定架构的一步。
Mcp-Session-Id 维护上下文(订阅列表、游标、缓存)。优点是能支持 Resources 订阅、分页游标等需要连续性的能力;代价是实例与会话绑定,负载均衡要做粘性路由(sticky session),水平扩展受限。选型口诀:工具类、查询类、无副作用的远程服务器 → 优先无状态;需要长会话、订阅推送、多步游标 → 选有状态并配粘性负载均衡。本教程的「代码片段管家」属于前者,所以全程用无状态。
Cloudflare 官方提供了 agents 模板,内置 Streamable HTTP 传输:
pnpm create cloudflare@latest snippet-vault \
--template=agents
模板会生成一个继承 McpAgent 的入口:
// src/index.ts
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export class Vault extends McpAgent<Env> {
server = new McpServer({ name: "snippet-vault", version: "1.0.0" });
async init() {
this.server.tool("add_snippet", { title: z.string(), language: z.string(), code: z.string() },
async ({ title, language, code }) => ({ content: [{ type: "text", text: `saved ${language}-${title}` }] }));
}
}
export default {
fetch: Vault.mount("/mcp", { corsOptions: { origin: "*" } }).fetch,
};
部署:
npx wrangler deploy
# 产物:https://snippet-vault.<your-subdomain>.workers.dev/mcp
Workers 天然无状态、全球边缘分发,是「纯工具类」远程 MCP 的首选。
适合需要自带数据库、要部署在自有机房的场景。Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen
COPY server.py .
EXPOSE 8000
CMD ["uv","run","python","server.py"]
docker-compose.yml 用 Caddy 做 TLS 反代,把 https://vault.internal/mcp 指向容器:
services:
vault:
build: .
caddy:
image: caddy:2
ports: ["443:443"]
command: caddy reverse-proxy --from vault.internal --to vault:8000
docker compose up -d
常见问题:0.0.0.0 监听一定要设——只听 127.0.0.1 的话,容器外完全不可达;反向代理必须保留 Mcp-Session-Id 透传,Caddy/Nginx 默认会透传未知头,但若你自定义了头过滤就要显式放行。
部署成功后,回到 Claude Code。Claude Code 2.1.1+ 用 add-json 注册 HTTP 传输(旧版用 --transport http)。官方 GitHub MCP Server 安装指南给出的正是这种格式:
claude mcp add-json vault \
'{"type":"http","url":"https://snippet-vault.xxx.workers.dev/mcp"}'
若服务器要鉴权,把凭据放进 headers:
claude mcp add-json vault \
'{"type":"http","url":"https://vault.example.com/mcp","headers":{"Authorization":"Bearer '"$VAULT_TOKEN"'"}}'
--scope 控制配置可见范围:local(仅你本项目,默认)、project(写入 .mcp.json 供全队共享)、user(你的所有项目)。验证:
claude mcp list # 看到 vault ✔ Connected
claude mcp get vault # 查看完整配置
预期结果:列表里 vault 显示已连接;在任意会话里说「存一段快速排序」,模型即可远程调用。常见问题:连接失败时优先查三处——URL 是否带了 /mcp 后缀、TLS 证书是否有效、Accept 头协商是否被中间代理改写。
公网上的 /mcp 必须有访问控制,最简方案是静态 Bearer Token(如上一步的 Authorization 头)。但更规范、更适合多用户的是 OAuth 2.1(Authorization Code + PKCE)。MCP 规范定义了标准化的授权元数据发现流程:客户端访问 /.well-known/oauth-protected-resource,拿到授权服务器地址后走 PKCE 流程拿 token,再以 Bearer 形式带在每个 /mcp 请求上。
在 SaaS 场景里,这层还能天然挂上多租户隔离与计费计量——每个 token 对应一个租户,按 tools/call 次数计费。生产化落地可参考社区已有的 streamable HTTP + OAuth 参考实现,无需从零造轮子。
把上面九步串成一个完整闭环。团队 5 人共用一套代码片段库,要求:Web、移动端、Claude Code 都能存取;存取带身份;按团队隔离。
落地架构:
flowchart LR
subgraph 客户端
A[Claude Code CLI]
B[团队 Web 应用]
C[移动端]
end
A & B & C -->|HTTPS POST /mcp<br/>Bearer Token| GW[(Caddy / Workers<br/>TLS + 鉴权)]
GW --> S1[无状态 MCP 实例 1]
GW --> S2[无状态 MCP 实例 2]
S1 & S2 -->|读写| DB[(共享片段库<br/>D1/KV)]
S1 -.进度/通知.-> A
关键决策点全部对应前面的步骤:
claude mcp add-json vault ... --scope user 一次性配置(第八步)。最终验证流程:成员在 Claude Code 里说「把这段快速排序存进团队库」,模型远程调用 add_snippet;另一位成员在 Web 端用 search_snippet("排序") 即刻检索到——一处部署、多端共用,这正是 stdio 做不到的事。
sequenceDiagram
participant CC as Claude Code
participant SRV as 远程 MCP /mcp
participant DB as 片段库
CC->>SRV: POST initialize (Accept: json, sse)
SRV-->>CC: 200 result{serverInfo, capabilities}
Note over SRV: 无状态:不发 Mcp-Session-Id
CC->>SRV: POST tools/call add_snippet (+ Bearer)
SRV->>DB: 写入片段
SRV-->>CC: 200 单条 JSON 结果
CC->>SRV: POST tools/call search_snippet (+ Bearer)
SRV-->>CC: 200 SSE 流式分页结果
| 现象 | 根因 | 处理 |
|---|---|---|
claude mcp list 显示 Failed to connect |
URL 漏 /mcp、证书无效、Accept 头被代理改写 |
逐步用 curl 验证端点契约 |
| curl 返回 406 | 没声明 text/event-stream 而服务器要流式回包 |
补 Accept 头 |
| curl 返回 400 session not found | 有状态模式下漏带/带错 Mcp-Session-Id |
回放初始化拿到的会话头 |
| 容器外不可达 | 只监听 127.0.0.1 |
改成 host="0.0.0.0" |
| 横向扩展后会话丢失 | 有状态实例没做粘性路由 | 改无状态,或 LB 配 sticky |
| 公网被刷接口 | 没有认证 | 至少上 Bearer Token,正式环境上 OAuth 2.1 |
modelcontextprotocol.io/specification)——协议层的权威契约定义add-json、scope、tool search、认证与错误处理的官方说明NapthaAI/http-oauth-mcp-server——Streamable HTTP + OAuth 可直接读源码claude mcp add-json 远程 HTTP 注册命令与 scope 的权威范例[!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 等场景全覆盖。
