<!-- title: MCP远程传输与OAuth认证部署实战 -->

# Claude Code 远程 MCP 服务器开发：Streamable HTTP 与 OAuth 2.1 生产部署

## 概述

本教程聚焦 MCP（Model Context Protocol）的**远程传输与生产级认证**这一进阶主题。你将学到如何把一个本地 `stdio` MCP 工具升级为可被任何授权 Claude Code 客户端跨网络访问的远程服务，并使用 **Streamable HTTP 传输**、**OAuth 2.1 + PKCE** 认证、**RFC 8707 资源指示符（resource indicator）**以及无服务器平台部署完成上线。适合已经写过本地 MCP 工具、希望将其产品化或对企业/多租户场景开放的工程师。

## 前置条件

- 已完成至少一个本地 `stdio` MCP 服务器（TypeScript 或 Python）
- Node.js 20+ 或 Python 3.11+，安装了官方 MCP SDK（`@modelcontextprotocol/sdk` 或 `mcp`/`fastmcp`）
- Claude Code CLI 已更新至支持远程 MCP 的版本（2025 年 6 月之后均支持）
- 一个 HTTPS 域名（可用 Cloudflare Workers、Vercel 或 AWS Lambda + API Gateway 免费获得）
- 基础的 HTTP 与 OAuth 概念（authorization code、PKCE、token endpoint）

---

## 一、为什么需要把 MCP 服务器搬到远程

本地 `stdio` 传输是 MCP 的入门形态：客户端启动一个子进程，通过标准输入输出交换 JSON-RPC 消息。它简单、零网络成本，但有三条硬限制使其无法支撑生产场景：

| 维度 | `stdio` 本地传输 | Streamable HTTP 远程传输 |
|---|---|---|
| 部署位置 | 调用方本机，每台机器一份 | 云端单实例，全球可达 |
| 凭证安全 | API Key 必须下发到每个客户端 | 凭证留在服务器，客户端只持有短期 token |
| 多租户/计费 | 无法实现 | 可按用户、按调用计费 |
| 状态与会话 | 进程级，重启即丢 | 可挂载 Redis/DB 做有状态会话 |
| 连接模型 | 一对一子进程 | 单端点服务任意并发客户端 |

一句话原则：**只要你的 MCP 工具会接触到付费 API、企业内网数据或需要按用户授权，就必须走远程传输 + OAuth。** Anthropic、Cloudflare、微软等厂商在 2025 年下半年共同确立了以 **Streamable HTTP 取代旧版 SSE** 作为官方远程传输标准，OAuth 2.1 则是强制的认证层。

## 二、传输协议演进：从 stdio 到 Streamable HTTP

早期 MCP 远程方案使用一对 SSE 长连接 + POST 端点，存在连接数膨胀、断线重连复杂、不利于无服务器冷启动等问题。**Streamable HTTP（2025-03 版规范引入，2025-06 版成为推荐）**将其统一为：

- **单一端点**（通常 `/mcp`）：客户端 POST JSON-RPC 请求，服务器可选择返回普通 JSON（对非流式请求）或 `Content-Type: text/event-stream`（对需要流式的请求）。
- **可选会话**：服务器通过响应头 `Mcp-Session-Id` 颁发会话 ID，客户端在后续请求中回传，从而支持有状态交互；无服务器场景可关闭会话走无状态模式。
- **可恢复性**：服务器发送带 ID 的事件，客户端可用 `Last-Event-ID` 头重连续传，天然适配边缘网络抖动。

这套模型让 MCP 服务器本质上就是一个"能说 JSON-RPC 的普通 Web 服务"，因此可以无痛地部署到任何 HTTP 运行时。

```mermaid
flowchart LR
    subgraph Client["Claude Code 客户端"]
        A["/mcp add --transport http"]
    end
    subgraph Edge["边缘/网关层"]
        B["HTTPS 终结<br/>OAuth 2.1 中间件"]
    end
    subgraph Server["MCP 服务器 (Streamable HTTP)"]
        C["/mcp 端点<br/>JSON-RPC over HTTP/SSE"]
        D["Tools / Resources / Prompts"]
    end
    subgraph Back["后端依赖"]
        E["业务 API / DB"]
        F["Token / 计费服务"]
    end
    A -->|POST JSON-RPC<br/>Bearer token| B
    B --> C
    C --> D
    D --> E
    B -.校验 token.-> F
```

## 三、第一步：构建 Streamable HTTP MCP 服务器

以 TypeScript + 官方 SDK 为例。初始化项目：

```bash
mkdir remote-mcp && cd remote-mcp
npm init -y
npm install @modelcontextprotocol/sdk zexpress express
```

创建 `src/server.ts`，关键在于用 `StreamableHTTPServerTransport` 替代本地开发时用的 `stdio` 传输：

```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const app = express();
app.use(express.json());

// 1) 创建 MCP 服务器，声明能力
const server = new McpServer({
  name: "invoice-mcp",
  version: "1.0.0",
});

// 2) 注册一个工具：按用户身份查询发票
server.tool(
  "list-invoices",
  { month: z.string().describe("YYYY-MM 格式的月份") },
  async ({ month }, extra) => {
    // extra.authInfo 由 OAuth 中间件注入（见第四节）
    const user = extra.authInfo?.extra?.sub ?? "anonymous";
    const invoices = await db.query(
      "SELECT * FROM invoices WHERE owner = ? AND month = ?",
      [user, month]
    );
    return {
      content: [{ type: "text", text: JSON.stringify(invoices) }],
    };
  }
);

// 3) 挂载 Streamable HTTP 传输，支持无状态模式（适合无服务器）
app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // 无状态：每次请求独立
  });
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.listen(3000, () => console.log("MCP on :3000/mcp"));
```

**预期结果**：启动后，对 `http://localhost:3000/mcp` 发起一个带 `Accept: application/json, text/event-stream` 的 POST，带上 `{"jsonrpc":"2.0","id":1,"method":"initialize",...}`，应返回 200 与服务器能力声明。

**常见问题**：
- *报错 `No transport found`*：忘记在 `/mcp` 路径实例化 `StreamableHTTPServerTransport`，或者把它和 `stdio` 混用了。
- *请求被 404/挂起*：客户端必须同时声明 `Accept: application/json, text/event-stream`，否则服务器无法决定用哪种响应体。
- *无服务器冷启动超时*：开启 `sessionIdGenerator: undefined` 走无状态模式，避免 Lambda 中维护会话映射。

## 四、第二步：OAuth 2.1 认证集成

MCP 规范要求远程服务器对外暴露 **OAuth 2.1 Protected Resource Metadata**（RFC 9728 衍生）以及兼容 **OAuth 2.0 Authorization Server Metadata**（RFC 8414）。流程要点：

1. **未授权请求返回 401 + metadata 链接**：客户端据此发现授权服务器。
2. **客户端走 Authorization Code + PKCE**：Claude Code 内置 PKCE 生成，自动用浏览器打开登录页。
3. **RFC 8707 资源指示符**：授权请求带 `resource=https://your-mcp.example.com/mcp`，token 只能用于该资源，防止跨服务 token 误用。

```mermaid
sequenceDiagram
    participant CC as Claude Code
    participant MCP as /mcp (Protected Resource)
    participant AS as 授权服务器 (Auth0/Keycloak)
    CC->>MCP: POST /mcp (无 token)
    MCP-->>CC: 401 + resource_metadata URL
    CC->>AS: GET /.well-known/oauth-authorization-server
    AS-->>CC: {authorize_url, token_url, ...}
    CC->>AS: GET /authorize?response_type=code&code_challenge=...&resource=<mcp>
    Note over CC,AS: 浏览器打开登录页，用户授权
    AS-->>CC: 302 回调 + authorization code
    CC->>AS: POST /token (code + code_verifier)
    AS-->>CC: access_token (短期) + refresh_token
    CC->>MCP: POST /mcp (Authorization: Bearer <token>)
    MCP-->>CC: 200 JSON-RPC 响应
```

下面是一个最小化的 Express 中间件，校验 Bearer token 并把用户身份注入 `authInfo`：

```typescript
import jwt from "jsonwebtoken";
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URL!));

async function oauthMiddleware(req, res, next) {
  // 1) 暴露 Protected Resource Metadata（规范要求）
  if (req.path === "/.well-known/oauth-protected-resource") {
    return res.json({
      resource: "https://mcp.example.com/mcp",
      authorization_servers: ["https://auth.example.com"],
      bearer_methods_supported: ["header"],
    });
  }
  // 2) 校验 Bearer token
  const auth = req.headers.authorization || "";
  if (!auth.startsWith("Bearer ")) {
    return res
      .status(401)
      .header("WWW-Authenticate", `Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"`)
      .json({ error: "missing_token" });
  }
  try {
    const { payload } = await jwtVerify(auth.slice(7), JWKS, {
      issuer: "https://auth.example.com",
      audience: "https://mcp.example.com/mcp", // RFC 8707 resource
    });
    req.authInfo = {
      token: auth.slice(7),
      clientId: payload.client_id,
      scopes: (payload.scope || "").split(" "),
      extra: { sub: payload.sub, plan: payload.plan },
    };
    next();
  } catch (e) {
    return res.status(401).json({ error: "invalid_token" });
  }
}

app.use(oauthMiddleware); // 必须挂在 /mcp 之前
```

随后在工具实现里通过 `extra.authInfo.extra.sub` 拿到用户 ID、用 `extra.authInfo.extra.plan` 做套餐限流。

**常见问题**：
- *浏览器登录后无限重定向*：回调地址在授权服务器注册的 redirect_uri 与 Claude Code 实际使用的 `http://localhost:<port>/callback` 不一致，需在 Auth0/Keycloak 里登记本地回调（Claude Code 用本地 loopback 接收 code）。
- *token 校验通过但 scope 不足*：用 `extra.authInfo.scopes.includes("invoices:read")` 做细粒度判断，未命中则返回 `jsonrpc` 的 error 对象（code `-32603`）。
- *resource indicator 报 invalid_target*：授权服务器的客户端配置没有启用 RFC 8707；在 Auth0 中需在 API 设置打开 `Allow Skipping Consent` 之外还要允许 `resource` 参数。

## 五、第三步：部署到无服务器平台

Streamable HTTP 的无状态模式天然适配 FaaS。以 **Cloudflare Workers** 为例（最省心，自带边缘 HTTPS 与零冷启动）：

```bash
npm create cloudflare@latest remote-mcp -- --type=hello-world
cd remote-mcp
npm install @modelcontextprotocol/sdk workers-oauth-provider
```

`src/index.ts`：

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { MyWorkerAuth } from "./auth";

const authProvider = new MyWorkerAuth();

export default {
  async fetch(req: Request, env: Env) {
    const url = new URL(req.url);
    if (url.pathname === "/.well-known/oauth-protected-resource") {
      return Response.json(authProvider.resourceMetadata());
    }
    if (url.pathname === "/authorize" || url.pathname === "/token") {
      return authProvider.handle(req, env); // 内置授权码 + token 端点
    }
    if (url.pathname === "/mcp") {
      const token = req.headers.get("authorization")?.slice(7);
      const identity = await authProvider.verify(token, env);
      if (!identity) return new Response("unauthorized", { status: 401 });

      const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
      const server = new McpServer({ name: "invoice-mcp", version: "1.0.0" });
      // 注入身份到每个工具
      registerTools(server, identity);
      await server.connect(transport);
      return transport.handleRequest(req, new Response());
    }
    return new Response("not found", { status: 404 });
  },
};
```

部署与绑定自定义域：

```bash
npx wrangler deploy
npx wrangler domains attach mcp.example.com
```

在 AWS Lambda + API Gateway 上同理：把 `app`（Express/Fastify）用 `serverless-http` 包一层，开启 HTTP API 的 `payload format v2`，并确保响应里保留 `Mcp-Session-Id` 头（Lambda 默认会透传自定义头，但 API Gateway 需在"响应头"里放行）。

**常见问题**：
- *Workers 报 "subrequest limit"*：工具实现里对后端 API 的调用要在 50 个子请求配额内；可改用批量接口或在 Workers 内做缓存。
- *Vercel Edge 函数 30 秒超时*：长耗时工具拆成"启动 + 轮询"两个工具，避免单次请求超时。
- *回调地址在无服务器下无法接收*：Claude Code 的 PKCE 回调始终落在本地 loopback，与服务器部署位置无关；服务器侧只需实现 `/authorize`、`/token` 两个端点。

## 六、第四步：从 Claude Code 客户端连接

远程服务器就绪后，在 Claude Code 里注册：

```bash
# 方式一：HTTP 传输（无认证或自定义头）
claude mcp add --transport http invoice https://mcp.example.com/mcp

# 方式二：需要 OAuth 时，Claude Code 会自动触发浏览器登录
claude mcp add --transport http invoice https://mcp.example.com/mcp --scope user
```

首次调用 `invoice` 工具时，Claude Code 检测到 401 + resource metadata，自动打开浏览器完成 PKCE 授权，token 以加密形式存在本地钥匙串（macOS Keychain / Windows Credential Manager），并在过期前静默刷新。

验证连接：

```bash
claude mcp list                 # 应显示 invoice (http) ✓ connected
claude mcp get invoice          # 查看工具列表与 token 状态
```

在会话里输入 *"列出我本月发票"*，Claude Code 会调用 `list-invoices`，自动在请求头附上 Bearer token，并以你的 `sub` 作为查询条件——这就是远程 MCP 的核心价值：**能力在云端，身份在本地，凭证永不离开服务器。**

## 七、第五步：生产化——多租户隔离与计费

将 MCP 工具变成对外产品时，三个能力必须就位：

1. **按租户数据隔离**：所有 SQL/查询在 WHERE 条件注入 `extra.authInfo.extra.sub`，并在 DB 层加行级安全策略（PostgreSQL RLS）兜底，防止工具实现漏写条件。
2. **配额与计费**：在 OAuth 中间件后加一层计数中间件，按 `tool_name` 维度写入计量表；超额时返回 JSON-RPC error code `-32001`（自定义资源耗尽），Claude Code 会把错误信息透传给用户。
3. **审计日志**：把每次 `{sub, tool, args, latency, status}` 结构化写入对象存储，便于合规与问题回溯。

计费中间件示例：

```typescript
async function meteringMiddleware(req, res, next) {
  const start = Date.now();
  res.on("finish", async () => {
    const toolName = req.__mcp_tool; // 由传输层钩子填充
    await env.METERING.put(
      `${req.authInfo.extra.sub}:${Date.now()}`,
      JSON.stringify({ tool: toolName, ms: Date.now() - start, status: res.statusCode })
    );
  });
  if (await isOverQuota(req.authInfo.extra.sub)) {
    return res.status(200).json({
      jsonrpc: "2.0", id: req.body.id,
      error: { code: -32001, message: "monthly quota exceeded" },
    });
  }
  next();
}
```

## 实战案例：一个可运行的"订阅制"远程 MCP 服务器

把以上步骤串联成一个最小可运行项目 `invoice-mcp`，它对外提供两个工具：

- `list-invoices(month)`：列出当前登录用户的发票
- `download-invoice(id)`：返回某张发票的 PDF 下载链接

项目结构：

```
invoice-mcp/
├── src/
│   ├── server.ts        # MCP server + Streamable HTTP
│   ├── auth.ts          # OAuth 2.1 中间件 (JWKS 校验)
│   ├── metering.ts      # 计费 / 配额
│   └── tools.ts         # 两个业务工具
├── wrangler.toml        # Cloudflare Workers 配置
└── .well-known/
    └── oauth-protected-resource.json
```

端到端验证脚本（不依赖 Claude Code，用 `mcp-cli` 直连）：

```bash
# 1) 部署
npx wrangler deploy
# 2) 用 mcp-cli 走完整 OAuth
npx @modelcontextprotocol/cli@latest \
  --transport http https://mcp.example.com/mcp \
  --tool list-invoices --arg month=2026-06
```

预期输出是一段 JSON，内含属于当前授权用户的发票数组；换一个用户授权后，结果集随之改变——这证明身份隔离生效。

## 常见问题速查表

| 症状 | 根因 | 解决 |
|---|---|---|
| 客户端一直卡在 "Discovering auth server" | metadata 端点返回的 `authorization_servers` 与实际 issuer 不一致 | 校对 `/.well-known/oauth-protected-resource` 的域名 |
| token 有效但调用返回 invalid_target | 未传 RFC 8707 `resource` 参数 | 在 Auth0/Keycloak 客户端启用 resource indicator |
| Workers 下偶发 502 | 长连接被 FaaS 砍断 | 关闭会话（`sessionIdGenerator: undefined`），改无状态 |
| 工具返回别人数据 | 缺少租户隔离 WHERE | 数据库层加 RLS，工具层强制注入 `sub` |

## 总体架构

```mermaid
flowchart TB
    User["最终用户"]
    CC["Claude Code<br/>(PKCE + 本地 token 存储)"]
    subgraph Cloud["云端 (mcp.example.com)"]
        GW["Cloudflare Workers<br/>HTTPS + 边缘"]
        AS["/authorize /token<br/>OAuth 2.1"]
        MWP["/.well-known/<br/>resource metadata"]
        MCP["/mcp Streamable HTTP"]
        Tools["list-invoices<br/>download-invoice"]
        Auth["JWKS 校验中间件"]
        Meter["计费 + 配额"]
    end
    DB[("PostgreSQL + RLS<br/>租户隔离")]
    Usage[("计量表 / 对象存储")]

    User --> CC
    CC -->|1 探测| MWP
    CC -->|2 PKCE 授权| AS
    CC -->|3 Bearer 调用| GW
    GW --> Auth --> Meter --> MCP --> Tools --> DB
    Meter --> Usage
```

## 延伸阅读

- **MCP 官方规范**（modelcontextprotocol.io）——传输与授权章节，权威定义 Streamable HTTP 与 OAuth 2.1 要求
- **Anthropic Engineering 博客**——Remote MCP Servers 与 OAuth 设计动机
- **Cloudflare Workers MCP 模板**（github.com/cloudflare/workers-mcp）——官方无服务器远程 MCP 脚手架，含完整授权端点实现
- **NapthaAI/http-oauth-mcp-server**——一个可直接参考的远程 MCP + OAuth 参考实现仓库
- **RFC 8707 / RFC 9728 / RFC 8414**——resource indicator、protected resource metadata、authorization server metadata 三份标准，是理解 MCP 远程认证的理论底座

## 训练营适用性评估

- **适合类型**：面向 3–5 年经验后端工程师的"AI 工具化 / SaaS 集成"进阶训练营，或企业内部"把内部系统安全暴露给 AI 助手"的实战研讨班。
- **前置课程**：建议先完成本地 `stdio` MCP 工具开发、OAuth 基础原理两门课程。
- **教学时长**：建议 1 天（6 学时）。上午 2 学时讲传输协议演进与 OAuth 流程，2 学时动手搭服务器 + 认证；下午 2 学时无服务器部署与联调，2 学时多租户/计费实战与结业 demo。
- **难度评级**：中高。主要难点在 OAuth metadata 发现链路、PKCE 回调在无服务器下的处理，以及资源指示符的配置细节。
- **可交付成果**：每位学员完成一个部署在公网、带 OAuth 登录、按用户隔离数据的远程 MCP 服务器，并能从自己的 Claude Code 客户端成功调用。

## 参考来源

1. [MCP Server in TypeScript: OAuth 2.1 + Streamable HTTP (2026)](https://nerdleveltech.com/mcp-server-typescript-oauth-streamable-http-production-tutorial) — 生产级 TS 服务器与 RFC 8707 资源指示符的端到端实现
2. [MCP OAuth 2.1 Authentication: Complete Developer Guide 2026](https://baeseokjae.github.io/posts/mcp-oauth-authentication-guide-2026/) — PKCE 流程、metadata 发现与 token 刷新的逐步讲解
3. [How to Deploy a Remote MCP Server — Toolradar Blog](https://toolradar.com/blog/deploy-remote-mcp-server) — Cloudflare Workers / Vercel / AWS 三种平台部署对比
4. [Building a Secure MCP Server with OAuth 2.1 and Azure AD — Microsoft DevBlogs](https://devblogs.microsoft.com/ise/aca-secure-mcp-server-oauth21-azure-ad/) — 企业级 On-Behalf-Of 流程与 Azure 容器应用部署
5. [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 与生产部署的综合参考

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