<!-- title: MCP服务器远程部署实战 -->
# Claude Code MCP 服务器开发：把本地工具推上 Streamable HTTP

## 概述

本教程带你完成 MCP 服务器从「本地 stdio 子进程」到「远程 Streamable HTTP 服务」的关键一跃。你会搞清三种传输方式的取舍，用 FastMCP 写出同一份代码、靠一个参数切换传输，再用 Docker 容器化部署，最后让团队里任意一台机器上的 Claude Code 通过带鉴权的 HTTP 端点接入。适合已能用 stdio 跑通 MCP、想让工具被多人/CI 共享的后端与平台工程师。

## 前置条件

- 已读过 MCP 三原语（tools/resources/prompts）的基础概念，能跑通一个 stdio 版 MCP 服务器。
- 本机已装 `claude`（Claude Code 2.1.1 及以上，原生支持远程 HTTP MCP）、Node 18+、Python 3.11+、`uv`（`pip install uv` 或官方安装脚本）、Docker。
- 一台能开放端口的远程主机（云主机、内网服务器或本机 `0.0.0.0` 演示均可），以及一个能签发 Bearer Token 的简单机制（演示阶段可手写随机串）。

---

## 一、为什么要把 MCP 推上「远程」

很多教程止步于 stdio：Claude Code 在本机 fork 一个子进程，通过标准输入输出收发 JSON-RPC。这对个人本地工具够用，但有三个硬伤，正是 Streamable HTTP 要解决的：

1. **后端状态无法共享**：stdio 服务器活在单个客户端进程里，工程师 A 的会话和工程师 B 的会话互不可见。一个「团队变更日志」工具，每人存一份本地 JSON，永远对不齐。
2. **机密与连通性下沉不到本机**：内网数据库、K8s 集群、CI 系统，不该也不能铺到每个工程师的笔记本上。你想要的是「一台服务器握着凭证，多个客户端来问」。
3. **CI 与无头场景接入麻烦**：CI runner 想调你的工具，再去 fork 一个本地进程、装一堆依赖，又慢又脆。一个 HTTP 端点对 CI 友好得多。

一句话：**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、负载均衡 | **当前推荐（远程）** |

### 2.1 Streamable HTTP 的核心机制

旧的 HTTP+SSE 需要两个端点（一个 POST 发请求、一个长连 SSE 收推送），状态与连接强绑定，横向扩展痛苦。Streamable HTTP 把它收敛为**单端点**：

- **POST `/mcp`**：客户端发 JSON-RPC 请求与通知。服务器可选择返回普通 `application/json`，或升级为 `text/event-stream`（SSE）做流式响应。
- **GET `/mcp`**：可选，客户端打开一条 SSE 长连，接收服务器主动推送（如长任务的进度通知、资源变更）。
- **DELETE `/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)` —— 查单条详情

### 3.1 项目骨架

```bash
mkdir changelog-mcp && cd changelog-mcp
uv init --no-readme
uv add fastmcp
```

### 3.2 服务器代码 `server.py`

```python
# 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 参数覆盖
```

### 3.3 关键一步：一份代码，两种传输

FastMCP 2.x 的设计精髓就在这里——**代码不写死传输，命令行切**：

```bash
# 方式 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` 头如何下发、后续请求如何回带——这是理解协议最快的办法。

---

## 四、容器化部署

### 4.1 Dockerfile

```dockerfile
# 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"]
```

### 4.2 构建与运行

```bash
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`。

---

## 五、Claude Code 接入远程 MCP

### 5.1 用命令行接入（推荐临时/个人验证）

```bash
# 先对比：本地 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
```

常用管理命令：

```bash
claude mcp list          # 列出所有已配服务器及连接状态
claude mcp get changelog # 看单个服务器详情
claude mcp remove changelog
```

### 5.2 用 `.mcp.json` 接入（推荐团队/项目共享）

在项目根放 `.mcp.json`，提交到仓库，全队 `clone` 即用：

```json
{
  "mcpServers": {
    "changelog": {
      "type": "http",
      "url": "https://changelog.internal.corp/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}
```

`${MCP_TOKEN}` 从环境变量注入，避免凭证入库。首次进入项目，Claude Code 会提示是否信任并连接该 MCP。

### 5.3 验证

在 Claude Code 里输入 `/mcp`，应看到 `changelog` 状态为 `connected`，并列出 `add_change / list_changes / get_change` 三个工具。然后直接对话测试：

> 「帮我看看近 3 天团队都改了什么」

Claude 会调用 `list_changes(days=3)`，返回共享数据库里的真实数据——工程师 A 刚提交的记录，工程师 B 此刻也能看到。这就是远程化的全部价值。

---

## 六、常见问题与排查

1. **`401 Unauthorized` / Token 没带上**：检查 `--header` 是否在位置参数之前；`.mcp.json` 里 `headers` 键名是否正确（不是 `header`）；`${MCP_TOKEN}` 是否真的在启动 Claude 的 shell 里 export 过。
2. **`403` / Origin 被拒（DNS rebinding 防护）**：远程服务器默认校验 Origin/Host 头。浏览器调试时会被拦，需在服务器侧显式放行可信 Origin；生产环境务必保留校验。
3. **负载均衡下会话丢失**：若用了有状态会话（`Mcp-Session-Id`），LB 必须开 **sticky session**；或干脆让服务器走**无状态模式**（不返回/不校验 session id），任意副本可处理任意请求——这才是 Serverless 友好的姿势。
4. **`curl` 探活收到 SSE 流而非 JSON**：正常。服务器对 `initialize`/`tools/call` 可能返回流式响应。加 `Accept: application/json, text/event-stream` 头即可，客户端 SDK 会自动处理。
5. **容器里数据库「丢」了**：SQLite 路径 `/data` 没挂卷（`-v ./data:/data`），容器重启数据归零。生产建议换成 Postgres 等外部库。
6. **看到 SSE 相关 deprecation 警告**：说明你还在用旧的 HTTP+SSE 端点（双端点）。迁到单端点 Streamable HTTP 即可消除。

---

## 七、工作流与架构总览

```mermaid
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 握手与一次工具调用的时序：

```mermaid
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 收进度推送
```

---

## 八、延伸阅读

- **MCP 规范 - Transports**：`modelcontextprotocol.io/specification/2025-06-18/basic/transports`，三种传输的权威定义。
- **FastMCP 部署文档**：`gofastmcp.com/deployment/http`，`streamable-http` 与 `sse` 两种 HTTP 部署的取舍与参数。
- **Claude Code MCP 接入文档**：`code.claude.com/docs/en/mcp`，`claude mcp add` 全部 scope/transport/header 选项。
- **MCP Inspector**：`modelcontextprotocol.io/tools`，浏览器里调试协议、可视化 session id 流转。

---

## 九、训练营适用性评估

- **适合**：企业内训「AI 平台工程」方向 2～3 天进阶营；要求学员已能独立写出 stdio MCP。重点训练「同一份代码、传输可切换」的抽象思维，以及远程化带来的鉴权、状态、扩展性权衡。
- **预计时长**：讲师演示 1.5 小时 + 学员动手 3 小时（写代码 1h、Docker 化 1h、Claude Code 双端验证与排查 1h）。
- **不适合**：零基础营——需先补 MCP 三原语与 stdio 基础；可与「FastMCP 多工具实战」「MCP 三原语挂载」两期作为前置序列。
- **可拓展任务**：给服务器补 OAuth 2.1 真鉴权（对接 Auth0）；把 SQLite 换 Postgres 并验证多副本无状态；在 CI 里用 `claude -p` 调 `add_change` 自动登记发布日志。

## 参考来源

1. [Transports — Model Context Protocol 官方规范](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — stdio / Streamable HTTP / 旧版 SSE 三种传输的权威定义与会话机制
2. [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp) — `claude mcp add` 的 transport/scope/header 全部选项与 `.mcp.json` 配置
3. [FastMCP — HTTP Deployment](https://gofastmcp.com/deployment/http) — `streamable-http` 部署参数、有状态与无状态取舍
4. [Claude Code Gains Support for Remote MCP Servers over Streamable HTTP — InfoQ](https://www.infoq.com/news/2025/06/anthropic-claude-remote-mcp/) — 远程 MCP 支持的工程背景与发布说明
5. [MCP Transport: Stdio vs Streamable HTTP — TrueFoundry](https://www.truefoundry.com/blog/mcp-stdio-vs-streamable-http-enterprise) — 企业级延迟、负载均衡、扩展性对比

> [!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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
