首页 › tutorials › MCP服务器上云:Streamable HTTP远程部署实战

概述

本教程聚焦一个尚未被广泛讲透的 MCP 维度:把一个本地 stdio MCP 服务器改造成可远程访问的 Streamable HTTP 服务,并部署到云端、供团队乃至移动端共用。学完你将掌握:stdio 与 Streamable HTTP 的本质差异、单一 /mcp 端点的请求/响应契约、有状态与无状态两种会话模式、Cloudflare Workers 与 Docker 两种部署路径,以及如何在 Claude Code 中用一行 claude mcp add-json 挂载远程服务器并加上认证。适用于已经能写出本地 MCP 工具、想把它升级为「团队共享服务」或「多端可用能力」的开发者。

前置条件


第一步:先看清 stdio 的天花板

绝大多数入门教程的第一台 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 传输的存在意义。

第二步:读懂 Streamable HTTP 的契约

MCP 在 2025-03-26 规范版本中引入 Streamable HTTP,取代了旧的「HTTP + SSE 双端点」设计。它的核心是单一端点 + 内容协商:

理解了这套契约,你就明白为什么它比旧方案优雅:旧的 HTTP+SSE 需要两个端点(一个 POST 收请求、一个 GET 开 SSE 流),还有复杂的会话重连问题;Streamable HTTP 用一个端点 + 协商,把「单次问答」和「长连接流」统一了。

第三步:准备一个可移植的 MCP 服务器

先写一个与传输无关的「纯逻辑」服务器,这样后面切换传输时业务代码一行都不用改。以 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 时要确保包确实能跑起来。

第四步:切换到 Streamable HTTP

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,意味着选择了无状态模式——每个请求自包含、不保留会话,最适合无状态横向扩展。

第五步:用 curl 验证端点契约

部署前先在本地把契约跑通,能省下大量云端调试时间。直接对着 /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"}
    }
  }'

预期结果:

拿到会话 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。

第六步:有状态 vs 无状态——做对选择

这是远程部署最容易被忽略、却决定架构的一步。

选型口诀:工具类、查询类、无副作用的远程服务器 → 优先无状态;需要长会话、订阅推送、多步游标 → 选有状态并配粘性负载均衡。本教程的「代码片段管家」属于前者,所以全程用无状态。

第七步:部署到云端(两条路径)

路径 A:Cloudflare Workers(最快上云,原生无状态)

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 的首选。

路径 B:Docker + 反向代理(可控、可私有部署)

适合需要自带数据库、要部署在自有机房的场景。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。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

关键决策点全部对应前面的步骤:

  1. 选 无状态(第六步)——因为存取查询无需会话连续性,可直接横向扩展。
  2. 部署 Workers 多副本(第七步路径 A),用 Workers KV 当共享存储,省去运维数据库。
  3. 每个团队成员发一个 独立 Bearer Token(第九步),token 内嵌团队 ID 实现租户隔离。
  4. Claude Code 侧每人 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

延伸阅读

参考来源

  1. Connect to external tools with MCP — Claude Code Docs — Claude Code 官方 MCP 配置、传输类型、认证与错误处理
  2. Build a Remote MCP server — Cloudflare Agents docs — Workers 上用 Streamable HTTP 部署远程 MCP 并接认证
  3. Install GitHub MCP Server in Claude Applications — claude mcp add-json 远程 HTTP 注册命令与 scope 的权威范例
  4. How to Deploy a Remote MCP Server — Toolradar Blog — Cloudflare/Vercel/AWS 部署对比,点明 stdio 在移动端/Web 端的局限
  5. MCP Remote HTTP Server Deployment Patterns — jangwook.net — 有状态 vs 无状态部署模式的实战取舍

训练营适用性评估

[!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学通信知识星球海报-扫码加入