<!-- title: 造远程MCP服务器并部署上线 -->

## 概述

本教程教你构建一个**可被多台机器、整个团队共享**的远程 MCP 服务器。与"装在本机的 stdio 服务器"不同，远程服务器通过 **Streamable HTTP 传输**暴露在网络上，任何授权的 Claude Code 会话都能连。学完你能：用 FastMCP 写一个 Streamable HTTP 服务、加会话管理与认证、部署到 Render/Railway，并在任意一台机器的 Claude Code 里接入它。适合已经写过本地 MCP、想把它产品化分享出去的开发者。

## 前置条件

- 已写过至少一个本地 MCP（stdio 传输），理解 Tool / Resource 概念
- Python 3.11+ 与 `uv`（`pip install uv`），或 Node 20+ 与 TypeScript SDK
- 一个云账号：Render、Railway 或 Cloudflare（任选其一，免费额度即可）
- 一台能 `curl` 的终端，用于手测 JSON-RPC 协议

---

## 一、为什么需要远程 MCP：stdio 到 Streamable HTTP 的演进

本地 MCP 用 **stdio**：Claude Code 启动一个子进程，靠标准输入输出收发 JSON-RPC。它简单、零网络、随用随启，但有一个硬伤——**进程是本机的、私有的**。你写的"公司内部知识库查询"工具，无法让同事直接复用，除非每个人各自 clone 代码、各自配密钥。

2025 年 6 月规范引入 **Streamable HTTP 传输**（取代了旧的 HTTP+SSE 双端点方案），用一个 `/mcp` 端点同时承载请求与流式响应。这让 MCP 从"本机脚本"升级为"网络服务"：

| 维度 | stdio | Streamable HTTP |
|------|-------|-----------------|
| 部署 | 本机子进程 | 任意 HTTP 主机 |
| 共享 | 一人一份 | 一份多人/多客户端连 |
| 认证 | 进程级 | OAuth 2.1 / Bearer |
| 生命周期 | 客户端拉起 | 7×24 常驻 |
| 状态 | 默认有状态 | 可无状态水平扩展 |

据 2026 Q2 统计，约 28% 的 MCP 服务已迁到 Streamable HTTP，且增速最快——企业内部共享工具、SaaS 厂商对外开放 AI 接口，几乎都走这条路。

```mermaid
flowchart LR
    subgraph 本机["本机方案 stdio"]
        A1["Claude Code"] -->|stdin/stdout| A2["本地 MCP 子进程"]
    end
    subgraph 远程["远程方案 Streamable HTTP"]
        B1["工程师甲 Claude Code"] --> H["HTTPS /mcp"]
        B2["工程师乙 Claude Code"] --> H
        B3["CI 机器人"] --> H
        H --> B4["云端 MCP 服务器"]
        B4 --> B5["外部 API/DB"]
    end
```

## 二、用 FastMCP 写第一个 Streamable HTTP 服务

### 2.1 初始化项目

```bash
mkdir remote-mcp && cd remote-mcp
uv init --python 3.11
uv add "mcp[cli]" httpx
```

### 2.2 服务端代码

新建 `server.py`：

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

mcp = FastMCP("infra-probe")  # 服务名，会出现在客户端工具列表里

@mcp.tool()
def ping_site(url: str) -> str:
    """探测一个站点是否可达，返回状态码与耗时(毫秒)。"""
    import time, httpx
    t0 = time.perf_counter()
    try:
        r = httpx.get(url, timeout=8, follow_redirects=True)
        ms = int((time.perf_counter() - t0) * 1000)
        return f"{r.status_code} in {ms}ms"
    except Exception as e:
        return f"ERR {type(e).__name__}: {e}"

@mcp.tool()
def check_ssl(domain: str) -> str:
    """返回域名 TLS 证书剩余有效天数。"""
    import ssl, socket
    ctx = ssl.create_default_context()
    with ctx.wrap_socket(socket.socket(), server_hostname=domain) as s:
        s.settimeout(8)
        s.connect((domain, 443))
        cert = s.getpeercert()
    import datetime
    expire = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
    days = (expire - datetime.datetime.utcnow()).days
    return f"{domain} 证书还剩 {days} 天"

if __name__ == "__main__":
    # 关键：transport 用 streamable-http，监听所有网卡
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, path="/mcp")
```

### 2.3 预期结果

```bash
uv run server.py
# 输出类似:
# Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# MCP server 'infra-probe' running with streamable-http transport
```

端点即 `http://localhost:8000/mcp`。

### 2.4 常见问题

- **`mcp.run()` 报 transport 未知**：SDK 版本太旧，`uv add -U "mcp[cli]"` 升到 1.2+。
- **容器内连不上**：监听地址必须写 `0.0.0.0` 而非 `127.0.0.1`，否则只接受本机回环。

## 三、手测 JSON-RPC：用 curl 验证协议

在远程 MCP 上线前，务必用原始 HTTP 走一遍协议，确认每个报文符合规范，别依赖客户端黑盒。

### 3.1 初始化握手

```bash
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"}}}'
```

**预期结果**：响应头里出现 `Mcp-Session-Id: <一串 UUID>`，响应体返回服务器能力声明。记下这个 session id。

### 3.2 发送 initialized 通知 + 列工具

```bash
# 通知已就绪(无 id, 无返回)
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","method":"notifications/initialized"}'

# 列出工具
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/list"}'
```

应看到 `ping_site` 与 `check_ssl` 两个工具描述。最后用 `tools/call` 调用 `ping_site`，传 `{"name":"ping_site","arguments":{"url":"https://example.com"}}`，拿到 `200 in 312ms`。

```mermaid
sequenceDiagram
    participant C as Client (curl/Claude)
    participant S as Remote MCP /mcp
    C->>S: POST initialize
    S-->>C: 200 + Mcp-Session-Id
    C->>S: POST notifications/initialized (sid)
    C->>S: POST tools/list (sid)
    S-->>C: JSON 或 SSE 流
    C->>S: POST tools/call ping_site (sid)
    S-->>C: "200 in 312ms"
    Note over S: 可选: GET 长连接接收服务端推送
```

### 3.3 常见问题

- **收到 406 Not Acceptable**：少了 `Accept: application/json, text/event-stream` 头，规范要求客户端必须声明它能接受这两种响应。
- **第二次请求 404 session**：忘记带 `Mcp-Session-Id` 头。无状态模式下可不带（见下节），但默认实现要求带。

## 四、会话管理：有状态 vs 无状态

默认 FastMCP 给每个客户端分配 session id，服务器内存里存上下文。这对单实例够用，但**水平扩展时会出问题**——请求落到另一台机器，session 就丢了。

生产部署二选一：

### 4.1 无状态模式（推荐水平扩展）

把每个请求做成自包含，服务器不存 session。在云函数/多副本场景用：

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

mcp = FastMCP("infra-probe",
              stateless_http=True,   # 无状态
              json_response=True)    # 优先返回纯 JSON 而非 SSE
```

无状态模式下，每次 `tools/call` 都是一次独立 HTTP 往返，服务器不返回 `Mcp-Session-Id`，客户端也可不带。代价是不能用需要跨调用记忆的特性（如某些订阅）。

### 4.2 粘性会话（保留有状态）

若必须有状态，部署时给负载均衡器开 **sticky session**，按 `Mcp-Session-Id` 哈希路由到固定副本。

### 4.3 常见问题

- **多副本下偶发 404**：99% 是 session 路由没粘住，改无状态或开 sticky。
- **`json_response` 开了仍返回 SSE**：某次工具调用触发了流式通知（如进度上报），这是正常的——同一个端点可按需在 JSON 与 SSE 间切换。

## 五、加认证：从共享密钥到 OAuth 2.1

把服务挂到公网却不加认证，等于开放一个能被 AI 任意调用的接口，必须上锁。

### 5.1 最小可用：Bearer 共享密钥

团队内部小范围用，先加一层 Bearer token 中间件。FastMCP 可直接拿到 ASGI 应用再包一层：

```python
import secrets
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse

VALID_TOKEN = os.environ["PROBE_TOKEN"]  # 从环境变量读，别硬编码

async def auth_mw(request, call_next):
    auth = request.headers.get("authorization", "")
    if request.url.path == "/mcp" and not auth.startswith(f"Bearer {VALID_TOKEN}"):
        return JSONResponse({"error":"unauthorized"}, status_code=401)
    return await call_next(request)

app = mcp.streamable_http_app()      # 拿到底层 Starlette app
app.middleware("http")(auth_mw)      # 挂认证中间件

# 然后 uvicorn server:app --host 0.0.0.0 --port 8000
```

### 5.2 生产级：OAuth 2.1 + PKCE

MCP 规范规定远程服务器走 **OAuth 2.1**，服务器作为 resource server，可对接外部授权服务器（Auth0、GitHub OAuth、Cloudflare Access）。客户端通过两个 well-known 端点发现配置：

- `/.well-known/oauth-protected-resource`：声明本服务受保护、指向授权服务器
- `/.well-known/oauth-authorization-server`：授权服务器的端点元数据

最省心的做法是**托管在 Cloudflare**：它的 `agents` 框架内置免费 OAuth，`wrangler deploy` 后自动拿到 `*.workers.dev` 域名 + Access 登录页，无需自己写授权码流程。

### 5.3 常见问题

- **token 校验通过但工具报错**：确认中间件只拦 `/mcp`，别误拦 `/.well-known/*`，否则客户端发现不了授权元数据。
- **Claude Code 连接转圈不出登录框**：你的授权服务器没正确实现 PKCE 的 token endpoint，用 Cloudflare Access 这类成熟方案能省很多坑。

## 六、部署到云端

### 6.1 Render / Railway（Python 进程型）

根目录加 `requirements.txt` 或用 `uv export`：

```bash
uv export --format requirements-txt > requirements.txt
```

再加 `render.yaml`（Render）：

```yaml
services:
  - type: web
    name: infra-probe
    runtime: python
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn server:app --host 0.0.0.0 --port $PORT
    envVars:
      - key: PROBE_TOKEN
        sync: false
```

**关键点**：云平台用环境变量 `$PORT` 注入端口，代码里**别写死 8000**。把 `mcp.streamable_http_app()` 暴露为 `app`，让 uvicorn 直接跑 ASGI，比 `mcp.run()` 更可控。

### 6.2 Cloudflare（边缘无服务器）

Cloudflare 的 `agents` 包专门支持远程 MCP，免运维 + 全球边缘 + 内置 OAuth：

```bash
npm create cloudflare@latest infra-probe -- --type=agents
```

在 `src/index.ts` 里用 `McpAgent` 定义工具，`wrangler deploy` 后即得 `https://infra-probe.<你>.workers.dev/sse`（Cloudflare 仍兼容旧 SSE 别名，同时支持 streamable）。

### 6.3 Docker（自托管/K8s）

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY server.py .
ENV PORT=8000
EXPOSE 8000
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
```

### 6.4 常见问题

- **部署后 502**：本地 `127.0.0.1` 监听没改 `0.0.0.0`，云平台的反向代理连不上。
- **冷启动超时**：无服务器函数首次加载 `httpx`/模型较慢，把启动开销大的依赖做成全局单例，别放进工具函数。

## 七、在 Claude Code 里接入远程 MCP

### 7.1 命令行一键添加

```bash
# 带 Bearer token 的私有服务
claude mcp add --transport http infra-probe \
  https://infra-probe.onrender.com/mcp \
  --header "Authorization: Bearer $PROBE_TOKEN"

# Cloudflare 这种已登录态的，可不带 token
claude mcp add --transport http cf-probe https://xxx.workers.dev/mcp
```

### 7.2 团队级配置：.mcp.json

在团队仓库根目录放 `.mcp.json`，所有人 clone 后 Claude Code 自动加载：

```json
{
  "mcpServers": {
    "infra-probe": {
      "type": "http",
      "url": "https://infra-probe.onrender.com/mcp",
      "headers": { "Authorization": "Bearer ${PROBE_TOKEN}" }
    }
  }
}
```

`${PROBE_TOKEN}` 从各人本机环境变量读取，密钥不入仓库。

### 7.3 预期结果

在 Claude Code 里输入"帮我看看 api.acme.com 还活着吗，证书多久过期"，它会自动调用 `ping_site` 和 `check_ssl`，把两个结果合并成一段自然语言报告——而这两个工具的代码只部署在你团队的那一台服务器上。

### 7.4 常见问题

- **`MCP server failed to connect`**：先 `curl -i` 验证端点，再确认 transport 类型填 `http` 而非 `sse`（除非服务端只支持旧 SSE）。
- **工具列表为空**：握手成功了但没收到 `initialized` 通知，通常是鉴权中间件误拦，回看 5.3。

---

## 实战案例：团队共享的"站点巡检"远程 MCP

把上面的零散步骤拼成一个完整闭环。目标：SRE 团队三人共用一个部署在 Render 上的 MCP，每个人在自己的 Claude Code 里直接用自然语言排查线上站点。

**架构**：

```mermaid
flowchart TB
    subgraph 团队["三个工程师的本机"]
        U1["Alice Claude Code"]
        U2["Bob Claude Code"]
        U3["Carol Claude Code"]
    end
    U1 & U2 & U3 -->|"HTTPS + Bearer"| LB["Render 反向代理"]
    LB --> APP["infra-probe MCP\n(streamable-http, stateless)"]
    APP -->|"httpx"| WEB["被探测的线上站点"]
    APP --> E1["PROBE_TOKEN 校验"]
    subgraph 仓库["公司 Git 仓库"]
        CFG[".mcp.json\n指向远程 URL"]
    end
    U1 -.clone.-> CFG
```

**完整 `server.py`**（合并认证 + 无状态 + 两个工具）：

```python
import os, time, ssl, socket, datetime
import httpx
from mcp.server.fastmcp import FastMCP
from starlette.responses import JSONResponse

mcp = FastMCP("infra-probe", stateless_http=True, json_response=True)
TOKEN = os.environ["PROBE_TOKEN"]

@mcp.tool()
def ping_site(url: str) -> str:
    t0 = time.perf_counter()
    try:
        r = httpx.get(url, timeout=8, follow_redirects=True)
        return f"{r.status_code} in {int((time.perf_counter()-t0)*1000)}ms"
    except Exception as e:
        return f"ERR {type(e).__name__}: {e}"

@mcp.tool()
def check_ssl(domain: str) -> str:
    ctx = ssl.create_default_context()
    with ctx.wrap_socket(socket.socket(), server_hostname=domain) as s:
        s.settimeout(8); s.connect((domain, 443))
        expire = datetime.datetime.strptime(
            s.getpeercert()["notAfter"], "%b %d %H:%M:%S %Y %Z")
    return f"{domain} 证书还剩 {(expire-datetime.datetime.utcnow()).days} 天"

app = mcp.streamable_http_app()

@app.middleware("http")
async def guard(request, call_next):
    if request.url.path == "/mcp":
        if request.headers.get("authorization") != f"Bearer {TOKEN}":
            return JSONResponse({"error": "unauthorized"}, status_code=401)
    return await call_next(request)
```

**部署与分发**：

```bash
# 1. Render 上设环境变量 PROBE_TOKEN=$(openssl rand -hex 24)
# 2. 推送代码，Render 自动 uvicorn server:app
# 3. 仓库里提交 .mcp.json，团队成员各自 export PROBE_TOKEN=xxx
# 4. 每人本机: claude mcp add ... 或直接打开 Claude Code 自动发现
```

跑通后，Bob 在 Claude Code 里问"巡检一下支付网关 pay.acme.com"，Alice 加的新工具他立刻能用——**改一处代码，全员即时受益**，这就是远程 MCP 相对 stdio 的核心价值。

---

## 常见问题速查表

| 现象 | 根因 | 解决 |
|------|------|------|
| `curl` 初始化正常，第二次 404 | 未带 `Mcp-Session-Id` | 有状态模式必带头；或切无状态 |
| 部署后只能本机访问 | 监听 `127.0.0.1` | 改 `0.0.0.0`，端口读 `$PORT` |
| 多副本偶发 404 | session 路由丢失 | 无状态化或开 sticky session |
| Claude Code 不弹登录框 | OAuth well-known 被拦 | 中间件放行 `/.well-known/*` |
| 工具调用首次很慢 | 冷启动加载依赖 | 依赖全局单例，减少工具内 import |
| 响应总是 SSE 不返回 JSON | 有流式通知 | 正常现象，同端点按需切换 |

## 延伸阅读

- **MCP 传输规范**（modelcontextprotocol.io）——Streamable HTTP 字段、session resumption、`Last-Event-ID` 的权威定义
- **MCP Inspector**——官方调试器，可视化握手与工具调用，比 curl 更直观
- **Cloudflare Agents 文档**——边缘远程 MCP + 内置 OAuth 的最省心路径
- **OAuth 2.1 for MCP**——授权码 + PKCE、metadata discovery 的完整流程

## 训练营适用性评估

- **适合**：企业内部 AI 工具化训练营、SRE/平台工程训练营、SaaS API 开放能力训练营
- **不适合**：纯入门营（需先掌握本地 MCP）
- **预计时长**：半天到一天。3 小时讲原理 + 写服务，2 小时部署与联调，1 小时团队协作演练
- **前置衔接**：建议排在"本地 MCP（FastMCP 三原语）"之后、"MCP 安全加固/可观测性"之前
- **考核点**：能独立部署一个带认证的远程 MCP，并在另一台机器的 Claude Code 中成功调用

## 参考来源

1. [Complete Guide to MCP in 2026 — Architecture & Implementation](https://dev.to/x4nent/complete-guide-to-mcp-model-context-protocol-in-2026-architecture-implementation-and-4a11) — 涵盖生产部署、Streamable HTTP 与 stdio 全景
2. [MCP Streamable HTTP Production Guide 2026: stdio vs Streamable HTTP](https://baeseokjae.github.io/posts/mcp-production-guide-streamable-http-2026/) — 67%/28% 传输占比与生产配置要点
3. [Transports — Model Context Protocol 规范](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — Streamable HTTP、Mcp-Session-Id、SSE 响应的权威定义
4. [Build a Remote MCP server — Cloudflare Agents docs](https://developers.cloudflare.com/agents/model-context-protocol/guides/remote-mcp-server/) — Cloudflare 边缘部署 + 内置 OAuth 实战
5. [Deploying a Remote MCP Server: Stateless, Streamable HTTP](https://agentscamp.com/guides/mcp/deploy-remote-mcp-server) — 无状态水平扩展与多副本会话处理
6. [Remote MCP Servers: Setup, Hosting & Best Options (2026)](https://apigene.ai/blog/remote-mcp-servers) — 托管平台选型与最佳实践

> [!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)
