首页 › tutorials › 造远程MCP服务器并部署上线

概述

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

前置条件


一、为什么需要远程 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 接口,几乎都走这条路。

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 初始化项目

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

2.2 服务端代码

新建 server.py:

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 预期结果

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 常见问题

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

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

3.1 初始化握手

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 通知 + 列工具

# 通知已就绪(无 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。

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 常见问题

四、会话管理:有状态 vs 无状态

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

生产部署二选一:

4.1 无状态模式(推荐水平扩展)

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

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 常见问题

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

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

5.1 最小可用:Bearer 共享密钥

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

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 端点发现配置:

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

5.3 常见问题

六、部署到云端

6.1 Render / Railway(Python 进程型)

根目录加 requirements.txt 或用 uv export:

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

再加 render.yaml(Render):

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:

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)

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 常见问题

七、在 Claude Code 里接入远程 MCP

7.1 命令行一键添加

# 带 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 自动加载:

{
  "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

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

架构:

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(合并认证 + 无状态 + 两个工具):

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)

部署与分发:

# 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 有流式通知 正常现象,同端点按需切换

延伸阅读

训练营适用性评估

参考来源

  1. Complete Guide to MCP in 2026 — Architecture & Implementation — 涵盖生产部署、Streamable HTTP 与 stdio 全景
  2. MCP Streamable HTTP Production Guide 2026: stdio vs Streamable HTTP — 67%/28% 传输占比与生产配置要点
  3. Transports — Model Context Protocol 规范 — Streamable HTTP、Mcp-Session-Id、SSE 响应的权威定义
  4. Build a Remote MCP server — Cloudflare Agents docs — Cloudflare 边缘部署 + 内置 OAuth 实战
  5. Deploying a Remote MCP Server: Stateless, Streamable HTTP — 无状态水平扩展与多副本会话处理
  6. Remote MCP Servers: Setup, Hosting & Best Options (2026) — 托管平台选型与最佳实践

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