首页 › tutorials › 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 工具、希望将其产品化或对企业/多租户场景开放的工程师。

前置条件


一、为什么需要把 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 服务器本质上就是一个"能说 JSON-RPC 的普通 Web 服务",因此可以无痛地部署到任何 HTTP 运行时。

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 为例。初始化项目:

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

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

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 误用。
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:

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 与零冷启动):

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

src/index.ts:

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 });
  },
};

部署与绑定自定义域:

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 里注册:

# 方式一: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),并在过期前静默刷新。

验证连接:

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} 结构化写入对象存储,便于合规与问题回溯。

计费中间件示例:

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,它对外提供两个工具:

项目结构:

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 直连):

# 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

总体架构

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

延伸阅读

训练营适用性评估

参考来源

  1. MCP Server in TypeScript: OAuth 2.1 + Streamable HTTP (2026) — 生产级 TS 服务器与 RFC 8707 资源指示符的端到端实现
  2. MCP OAuth 2.1 Authentication: Complete Developer Guide 2026 — PKCE 流程、metadata 发现与 token 刷新的逐步讲解
  3. How to Deploy a Remote MCP Server — Toolradar Blog — Cloudflare Workers / Vercel / AWS 三种平台部署对比
  4. Building a Secure MCP Server with OAuth 2.1 and Azure AD — Microsoft DevBlogs — 企业级 On-Behalf-Of 流程与 Azure 容器应用部署
  5. Complete Guide to MCP in 2026 — Architecture & Implementation — 协议架构、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学通信知识星球海报-扫码加入