首页 › tutorials › MCP远程服务器:OAuth到云端部署全实战

MCP远程服务器:OAuth认证到云端部署全实战

概述

本教程面向已掌握MCP本地服务器开发的工程师,系统讲解如何将MCP服务器从本地stdio推向远程生产环境。你将学到Streamable HTTP传输协议、OAuth 2.1+PKCE认证流程、动态客户端注册(DCR),以及如何将带认证的MCP服务器部署到Cloudflare Workers并通过Claude Code连接。适合有一定TypeScript和OAuth基础的开发者,预计学习时长4-6小时。

前置条件


一、为什么需要远程MCP服务器

1.1 传输协议演进

MCP协议经历了三代传输协议的演进,理解这个演进对选择正确方案至关重要:

timeline
    title MCP传输协议演进
    2024-11 : stdio传输\n本地进程间通信\nstdin/stdout管道
    2024-11 : HTTP+SSE传输\n远程HTTP连接\n服务端推送事件
    2025-03 : Streamable HTTP\n统一HTTP端点\n可选SSE升级\n(旧SSE标记废弃)
    2025-06 : Streamable HTTP\n成为唯一推荐远程传输\nSSE正式移除
传输方式 适用场景 认证要求 连接模式
stdio 本地CLI工具 无(进程级隔离) 双向管道
HTTP+SSE(已废弃) 早期远程服务器 可选 双通道
Streamable HTTP 生产远程服务器 OAuth 2.1(强制) 单端点多模式

1.2 stdio的局限性

本地stdio传输虽然简单,但存在天然限制:

1.3 Streamable HTTP的核心优势

Streamable HTTP在2025-06-18规范中成为唯一推荐的远程传输协议:


二、Streamable HTTP协议深度解析

2.1 通信模型

Streamable HTTP的核心设计是:所有请求都是 POST 到同一个端点,响应格式由服务端决定。

sequenceDiagram
    participant C as Claude Code (MCP Client)
    participant S as Remote MCP Server

    C->>S: POST /mcp<br/>Content-Type: application/json<br/>Accept: application/json, text/event-stream
    Note over C,S: 初始化请求 (initialize)

    S-->>C: 200 OK<br/>Content-Type: application/json<br/>Mcp-Session-Id: sess_abc123
    Note over C,S: 返回服务端能力

    C->>S: POST /mcp<br/>Mcp-Session-Id: sess_abc123<br/>Content-Type: application/json
    Note over C,S: 调用工具 (tools/call)

    S-->>C: 200 OK<br/>Content-Type: text/event-stream
    Note over C,S: SSE流式返回进度和结果

    C->>S: DELETE /mcp<br/>Mcp-Session-Id: sess_abc123
    Note over C,S: 关闭会话

    S-->>C: 200 OK

2.2 三种响应模式

Streamable HTTP定义了三种合法的响应模式:

模式一:直接JSON响应(适用于快速工具调用)

客户端 → POST /mcp
服务端 → 200 OK, Content-Type: application/json
         {"jsonrpc":"2.0","id":1,"result":{...}}

模式二:SSE流式响应(适用于长时间运行的工具)

客户端 → POST /mcp
服务端 → 200 OK, Content-Type: text/event-stream
         event: message
         data: {"jsonrpc":"2.0","method":"notifications/progress","params":{...}}

         event: message
         data: {"jsonrpc":"2.0","id":1,"result":{...}}

模式三:GET监听(可选,服务端主动推送)

客户端 → GET /mcp
服务端 → 200 OK, Content-Type: text/event-stream
         (保持连接,推送服务端发起的通知)

2.3 关键HTTP头

头名称 方向 作用
Mcp-Session-Id 双向 会话标识,初始化后必须携带
Accept 客户端→服务端 必须包含 application/json 和 text/event-stream
Content-Type 双向 application/json 或 text/event-stream

三、实战:构建Streamable HTTP服务器

3.1 项目初始化

# 创建项目
mkdir mcp-remote-server && cd mcp-remote-server
npm init -y

# 安装MCP SDK和相关依赖
npm install @modelcontextprotocol/sdk zexpress
npm install -D typescript @types/node tsx

# 初始化TypeScript
npx tsc --init

配置 tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

3.2 最小化Streamable HTTP服务器

创建 src/server.ts:

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

// 创建MCP服务器实例
const server = new McpServer({
  name: "remote-weather-server",
  version: "1.0.0",
});

// 注册一个天气查询工具
server.tool(
  "get_weather",
  "获取指定城市的天气信息",
  { city: z.string().describe("城市名称,如 Beijing, Tokyo") },
  async ({ city }) => {
    // 模拟天气API调用
    const weatherData: Record<string, { temp: string; condition: string }> = {
      Beijing: { temp: "22°C", condition: "晴天" },
      Tokyo: { temp: "18°C", condition: "多云" },
      "New York": { temp: "15°C", condition: "小雨" },
    };
    const data = weatherData[city] || { temp: "20°C", condition: "未知" };

    return {
      content: [
        {
          type: "text" as const,
          text: `${city}天气:${data.condition},温度 ${data.temp}`,
        },
      ],
    };
  }
);

// 注册一个带进度的工具(演示SSE流式响应)
server.tool(
  "analyze_data",
  "分析数据集(模拟长时间任务)",
  {
    dataset: z.string().describe("数据集名称"),
    rows: z.number().describe("数据行数"),
  },
  async ({ dataset, rows }, { sendNotification }) => {
    const steps = ["加载", "清洗", "统计", "建模", "报告"];
    for (let i = 0; i < steps.length; i++) {
      // 发送进度通知
      await sendNotification({
        method: "notifications/progress",
        params: {
          progress: ((i + 1) / steps.length) * 100,
          total: 100,
        },
      });
      // 模拟处理时间
      await new Promise((r) => setTimeout(r, 500));
    }

    return {
      content: [
        {
          type: "text" as const,
          text: `数据集 "${dataset}" (${rows}行) 分析完成:\n` +
            `- 有效数据率: 94.2%\n` +
            `- 异常值: ${Math.floor(rows * 0.03)} 个\n` +
            `- 平均值: 42.7\n` +
            `- 标准差: 12.3`,
        },
      ],
    };
  }
);

// 创建Streamable HTTP传输层
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),
});

// 连接服务器到传输层
await server.connect(transport);

// 创建HTTP服务器
const httpServer = http.createServer(async (req, res) => {
  // CORS头(开发环境)
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "*");

  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  // 只处理 /mcp 端点
  if (req.url?.startsWith("/mcp")) {
    await transport.handleRequest(req, res);
    return;
  }

  res.writeHead(404);
  res.end("Not Found");
});

const PORT = 3000;
httpServer.listen(PORT, () => {
  console.log(`MCP Remote Server running on http://localhost:${PORT}/mcp`);
});

3.3 本地测试

# 启动服务器
npx tsx src/server.ts

用curl测试初始化握手:

# 发送initialize请求
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "capabilities": {},
      "clientInfo": {"name": "test-client", "version": "1.0.0"}
    }
  }'

预期响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "remote-weather-server", "version": "1.0.0" }
  }
}

注意响应头中包含 Mcp-Session-Id,后续请求需要携带。

3.4 在Claude Code中连接本地远程服务器

编辑 ~/.claude/settings.json 或项目级 .claude/settings.json:

{
  "mcpServers": {
    "weather": {
      "type": "http",
      "url": "http://localhost:3000/mcp"
    }
  }
}

重启Claude Code,即可使用天气查询工具。


四、OAuth 2.1认证:从零到完整流程

4.1 为什么远程MCP必须认证

MCP规范要求:所有远程MCP服务器必须实现OAuth 2.1认证。原因如下:

4.2 OAuth 2.1 + PKCE流程

MCP使用OAuth 2.1 Authorization Code + PKCE(Proof Key for Code Exchange)流程:

sequenceDiagram
    participant CC as Claude Code
    participant MS as MCP Server
    participant AS as Auth Server

    Note over CC,AS: 第一步:发现(Discovery)
    CC->>MS: GET /.well-known/oauth-protected-resource
    MS-->>CC: {resource, authorization_servers}
    CC->>AS: GET /.well-known/oauth-authorization-server
    AS-->>CC: {issuer, token_endpoint, registration_endpoint, ...}

    Note over CC,AS: 第二步:动态客户端注册(DCR)
    CC->>AS: POST /register<br/>{client_name, redirect_uris, grant_types}
    AS-->>CC: {client_id, client_secret}

    Note over CC,AS: 第三步:授权码流程 + PKCE
    CC->>CC: 生成 code_verifier + code_challenge
    CC->>AS: GET /authorize?client_id=...&code_challenge=...&redirect_uri=...
    AS-->>CC: 302 → 用户登录页面
    Note over AS: 用户在浏览器中授权
    AS-->>CC: 302 → redirect_uri?code=AUTH_CODE

    Note over CC,AS: 第四步:令牌交换
    CC->>AS: POST /token<br/>grant_type=authorization_code<br/>code=AUTH_CODE&code_verifier=...
    AS-->>CC: {access_token, token_type, expires_in}

    Note over CC,AS: 第五步:访问MCP
    CC->>MS: POST /mcp<br/>Authorization: Bearer <access_token>
    MS-->>CC: 200 OK + MCP响应

4.3 关键概念解析

Protected Resource Metadata(RFC 9728)

MCP服务器通过 .well-known/oauth-protected-resource 声明自己是受保护资源:

{
  "resource": "https://my-mcp-server.example.com/mcp",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "bearer_methods_supported": ["header"]
}

Dynamic Client Registration(RFC 7591)

MCP客户端不需要预注册。发现DCR端点后,客户端动态注册自己:

// POST /register
{
  "client_name": "Claude Code",
  "redirect_uris": ["http://localhost:8931/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none"
}

// 响应
{
  "client_id": "generated-client-id-xxx",
  "client_id_issued_at": 1716854400,
  "grant_types": ["authorization_code"],
  "redirect_uris": ["http://localhost:8931/callback"]
}

PKCE流程

1. 生成 code_verifier: 随机字符串 (43-128字符)
2. 计算 code_challenge: BASE64URL(SHA256(code_verifier))
3. 授权请求携带 code_challenge
4. 令牌请求携带 code_verifier
5. Auth Server 验证 SHA256(code_verifier) == code_challenge

4.4 实现带认证的MCP服务器

创建 src/auth-server.ts,在原有基础上添加OAuth中间件:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import http from "http";
import crypto from "crypto";

// ============ 简化版OAuth Provider(演示用) ============
// 生产环境请使用 Auth0、Clerk、或自建 IdP

interface OAuthClient {
  client_id: string;
  client_secret?: string;
  redirect_uris: string[];
  grant_types: string[];
}

interface AuthCode {
  code: string;
  client_id: string;
  redirect_uri: string;
  code_challenge: string;
  code_challenge_method: string;
  expires_at: number;
  scope: string;
}

interface Token {
  access_token: string;
  token_type: string;
  expires_in: number;
  scope: string;
}

// 内存存储(生产环境请用数据库)
const clients = new Map<string, OAuthClient>();
const authCodes = new Map<string, AuthCode>();
const tokens = new Map<string, Token>();

const ISSUER = "https://my-mcp.example.com";

// ============ OAuth端点 ============

function handleOAuthDiscovery(req: http.IncomingMessage, res: http.ServerResponse) {
  // RFC 9728: Protected Resource Metadata
  if (req.url === "/.well-known/oauth-protected-resource") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      resource: `${ISSUER}/mcp`,
      authorization_servers: [ISSUER],
      scopes_supported: ["mcp:tools", "mcp:resources"],
      bearer_methods_supported: ["header"],
    }));
    return true;
  }

  // RFC 8414: Authorization Server Metadata
  if (req.url === "/.well-known/oauth-authorization-server") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      issuer: ISSUER,
      authorization_endpoint: `${ISSUER}/authorize`,
      token_endpoint: `${ISSUER}/token`,
      registration_endpoint: `${ISSUER}/register`,
      response_types_supported: ["code"],
      code_challenge_methods_supported: ["S256"],
      grant_types_supported: ["authorization_code"],
      token_endpoint_auth_methods_supported: ["none"],
    }));
    return true;
  }

  return false;
}

// 动态客户端注册
async function handleRegister(req: http.IncomingMessage, res: http.ServerResponse) {
  if (req.url !== "/register" || req.method !== "POST") return false;

  const body = await readBody(req);
  const client_id = `client_${crypto.randomUUID()}`;
  const client: OAuthClient = {
    client_id,
    redirect_uris: body.redirect_uris || [],
    grant_types: body.grant_types || ["authorization_code"],
  };
  clients.set(client_id, client);

  res.writeHead(201, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ client_id, ...client }));
  return true;
}

// 授权端点(生产环境中这是用户登录页面)
function handleAuthorize(req: http.IncomingMessage, res: http.ServerResponse) {
  if (!req.url?.startsWith("/authorize") || req.method !== "GET") return false;

  const params = new URL(req.url, ISSUER).searchParams;
  const client_id = params.get("client_id")!;
  const redirect_uri = params.get("redirect_uri")!;
  const code_challenge = params.get("code_challenge")!;
  const code_challenge_method = params.get("code_challenge_method")!;

  if (!clients.has(client_id)) {
    res.writeHead(400);
    res.end("Unknown client");
    return true;
  }

  // 自动授权(演示用,生产环境需要用户确认)
  const code = crypto.randomBytes(32).toString("base64url");
  authCodes.set(code, {
    code,
    client_id,
    redirect_uri,
    code_challenge,
    code_challenge_method,
    expires_at: Date.now() + 60000,
    scope: params.get("scope") || "mcp:tools",
  });

  const callbackUrl = `${redirect_uri}?code=${code}&state=${params.get("state")}`;
  res.writeHead(302, { Location: callbackUrl });
  res.end();
  return true;
}

// 令牌端点
async function handleToken(req: http.IncomingMessage, res: http.ServerResponse) {
  if (req.url !== "/token" || req.method !== "POST") return false;

  const body = await readBody(req);
  const authCode = authCodes.get(body.code);

  if (!authCode || authCode.expires_at < Date.now()) {
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "invalid_grant" }));
    return true;
  }

  // PKCE验证
  const expectedChallenge = crypto
    .createHash("sha256")
    .update(body.code_verifier)
    .digest("base64url");

  if (expectedChallenge !== authCode.code_challenge) {
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE验证失败" }));
    return true;
  }

  // 颁发令牌
  const access_token = `tk_${crypto.randomBytes(32).toString("base64url")}`;
  tokens.set(access_token, {
    access_token,
    token_type: "Bearer",
    expires_in: 3600,
    scope: authCode.scope,
  });

  authCodes.delete(body.code);

  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({
    access_token,
    token_type: "Bearer",
    expires_in: 3600,
    scope: authCode.scope,
  }));
  return true;
}

// ============ MCP工具 + 认证中间件 ============

const VALID_TOKENS = new Set(tokens.keys());

function validateToken(req: http.IncomingMessage): boolean {
  const auth = req.headers.authorization;
  if (!auth?.startsWith("Bearer ")) return false;
  const token = auth.slice(7);
  return tokens.has(token);
}

const mcpServer = new McpServer({
  name: "secure-weather-server",
  version: "1.0.0",
});

mcpServer.tool(
  "get_weather",
  "获取城市天气",
  { city: z.string() },
  async ({ city }) => {
    return {
      content: [{ type: "text" as const, text: `${city}: 晴, 22°C` }],
    };
  }
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),
});

await mcpServer.connect(transport);

// ============ HTTP路由总入口 ============

const httpServer = http.createServer(async (req, res) => {
  // CORS
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "*");
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  // OAuth Discovery
  if (handleOAuthDiscovery(req, res)) return;

  // OAuth Endpoints
  if (await handleRegister(req, res)) return;
  if (handleAuthorize(req, res)) return;
  if (await handleToken(req, res)) return;

  // MCP端点 - 需要认证
  if (req.url?.startsWith("/mcp")) {
    if (!validateToken(req)) {
      res.writeHead(401, {
        "Content-Type": "application/json",
        "WWW-Authenticate": `Bearer resource_metadata="${ISSUER}/.well-known/oauth-protected-resource"`,
      });
      res.end(JSON.stringify({ error: "unauthorized" }));
      return;
    }
    await transport.handleRequest(req, res);
    return;
  }

  res.writeHead(404);
  res.end("Not Found");
});

httpServer.listen(3000, () => {
  console.log(`Secure MCP Server: http://localhost:3000/mcp`);
  console.log(`OAuth Discovery: http://localhost:3000/.well-known/oauth-protected-resource`);
});

辅助函数:

function readBody(req: http.IncomingMessage): Promise<any> {
  return new Promise((resolve) => {
    let data = "";
    req.on("data", (chunk) => (data += chunk));
    req.on("end", () => {
      try { resolve(JSON.parse(data)); }
      catch { resolve(Object.fromEntries(new URLSearchParams(data))); }
    });
  });
}

4.5 常见问题与排查

问题 原因 解决方案
401 Unauthorized 缺少或无效的Bearer令牌 检查Authorization头是否正确
invalid_grant PKCE验证失败 确认code_verifier与code_challenge匹配
Discovery失败 .well-known 端点返回404 检查路由注册顺序
令牌过期 expires_in 已过 实现refresh_token或重新授权
回调URL不匹配 redirect_uri未注册 DCR时确保注册正确的callback URL

五、部署到Cloudflare Workers

5.1 为什么选择Cloudflare Workers

Cloudflare Workers是目前部署远程MCP服务器的最佳平台:

5.2 项目结构调整

mcp-remote-server/
├── src/
│   ├── index.ts          # Workers入口
│   ├── mcp-tools.ts      # MCP工具定义
│   ├── auth/
│   │   ├── oauth.ts      # OAuth Provider逻辑
│   │   └── middleware.ts  # 认证中间件
│   └── utils.ts          # 辅助函数
├── wrangler.toml         # Cloudflare配置
├── package.json
└── tsconfig.json

5.3 Cloudflare Workers实现

安装Workers依赖:

npm install @cloudflare/workers-types
npm install -D wrangler

配置 wrangler.toml:

name = "mcp-weather-server"
main = "src/index.ts"
compatibility_date = "2025-12-01"

[vars]
ISSUER = "https://mcp-weather-server.<your-subdomain>.workers.dev"

# D1数据库(存储OAuth客户端和令牌)
[[d1_databases]]
binding = "DB"
database_name = "mcp-auth"
database_id = "<your-database-id>"

创建 src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

interface Env {
  DB: D1Database;
  ISSUER: string;
}

// ============ MCP工具定义 ============

function createMcpServer() {
  const server = new McpServer({
    name: "cloudflare-weather-server",
    version: "1.0.0",
  });

  server.tool(
    "get_weather",
    "获取城市天气信息",
    { city: z.string().describe("城市名称") },
    async ({ city }) => {
      // 在Workers中可以调用外部API
      const resp = await fetch(
        `https://api.open-meteo.com/v1/forecast?` +
        `latitude=39.9&longitude=116.4&current_weather=true`
      );
      const data = await resp.json() as any;
      return {
        content: [{
          type: "text" as const,
          text: `${city}天气:温度 ${data.current_weather?.temperature ?? "N/A"}°C,` +
            `风速 ${data.current_weather?.windspeed ?? "N/A"} km/h`,
        }],
      };
    }
  );

  server.tool(
    "list_cities",
    "列出支持查询的城市",
    {},
    async () => {
      return {
        content: [{
          type: "text" as const,
          text: "支持城市:Beijing (39.9°N, 116.4°E), Tokyo (35.7°N, 139.7°E), " +
            "New York (40.7°N, -74.0°E), London (51.5°N, -0.1°E)",
        }],
      };
    }
  );

  return server;
}

// ============ OAuth中间件 ============

async function validateBearerToken(request: Request, db: D1Database): Promise<boolean> {
  const auth = request.headers.get("Authorization");
  if (!auth?.startsWith("Bearer ")) return false;

  const token = auth.slice(7);
  const result = await db.prepare(
    "SELECT * FROM tokens WHERE access_token = ? AND expires_at > ?"
  ).bind(token, Date.now()).first();

  return !!result;
}

// ============ Workers入口 ============

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // CORS预检
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 204,
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
          "Access-Control-Allow-Headers": "*",
        },
      });
    }

    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "*",
    };

    // OAuth Discovery
    if (url.pathname === "/.well-known/oauth-protected-resource") {
      return new Response(JSON.stringify({
        resource: `${env.ISSUER}/mcp`,
        authorization_servers: [env.ISSUER],
        scopes_supported: ["mcp:tools"],
        bearer_methods_supported: ["header"],
      }), {
        headers: { "Content-Type": "application/json", ...corsHeaders },
      });
    }

    if (url.pathname === "/.well-known/oauth-authorization-server") {
      return new Response(JSON.stringify({
        issuer: env.ISSUER,
        authorization_endpoint: `${env.ISSUER}/authorize`,
        token_endpoint: `${env.ISSUER}/token`,
        registration_endpoint: `${env.ISSUER}/register`,
        response_types_supported: ["code"],
        code_challenge_methods_supported: ["S256"],
        grant_types_supported: ["authorization_code"],
        token_endpoint_auth_methods_supported: ["none"],
      }), {
        headers: { "Content-Type": "application/json", ...corsHeaders },
      });
    }

    // 动态客户端注册
    if (url.pathname === "/register" && request.method === "POST") {
      const body = await request.json() as any;
      const clientId = crypto.randomUUID();

      await env.DB.prepare(
        "INSERT INTO oauth_clients (client_id, redirect_uris, grant_types) VALUES (?, ?, ?)"
      ).bind(clientId, JSON.stringify(body.redirect_uris), JSON.stringify(body.grant_types)).run();

      return new Response(JSON.stringify({
        client_id: clientId,
        redirect_uris: body.redirect_uris,
        grant_types: body.grant_types,
      }), { status: 201, headers: { "Content-Type": "application/json", ...corsHeaders } });
    }

    // MCP端点 - 需要认证
    if (url.pathname === "/mcp") {
      const isAuthed = await validateBearerToken(request, env.DB);

      if (!isAuthed) {
        return new Response(JSON.stringify({ error: "unauthorized" }), {
          status: 401,
          headers: {
            "Content-Type": "application/json",
            "WWW-Authenticate": `Bearer resource_metadata="${env.ISSUER}/.well-known/oauth-protected-resource"`,
            ...corsHeaders,
          },
        });
      }

      // 创建MCP服务器并处理请求
      const server = createMcpServer();
      // 使用Cloudflare Agents SDK的标准模式处理Streamable HTTP
      const { StreamableHTTPServerTransport } = await import(
        "@modelcontextprotocol/sdk/server/streamableHttp.js"
      );
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => crypto.randomUUID(),
      });

      await server.connect(transport);

      // 转换Request为Node-like格式再传给transport
      // 注意:实际部署建议使用 @cloudflare/agents 的 McpAgent 类
      return transport.handleRequest(request);
    }

    return new Response("Not Found", { status: 404, headers: corsHeaders });
  },
};

5.4 使用Cloudflare Agents SDK(推荐方式)

更简洁的方式是使用Cloudflare官方的Agents SDK:

npm install agents

创建 src/agent.ts:

import { McpAgent } from "agents/mcp";
import { z } from "zod";

export class MyMCPAgent extends McpAgent {
  async init() {
    // 注册工具
    this.server.tool(
      "get_weather",
      "获取城市天气",
      { city: z.string() },
      async ({ city }) => {
        const resp = await fetch(
          `https://api.open-meteo.com/v1/forecast?latitude=39.9&longitude=116.4&current_weather=true`
        );
        const data = await resp.json() as any;
        return {
          content: [{
            type: "text" as const,
            text: `${city}: ${data.current_weather?.temperature}°C`,
          }],
        };
      }
    );

    // 注册资源
    this.server.resource(
      "cities-config",
      "mcp://config/cities",
      async (uri) => ({
        contents: [{
          uri: uri.href,
          text: JSON.stringify({
            supported: ["Beijing", "Tokyo", "New York", "London"],
            default: "Beijing",
          }),
        }],
      })
    );
  }
}

export default {
  fetch: MyMCPAgent.serve("/mcp").fetch,
};

对应的 wrangler.toml:

name = "mcp-weather-agent"
main = "src/agent.ts"
compatibility_date = "2025-12-01"

[vars]
ISSUER = "https://mcp-weather-agent.<your-subdomain>.workers.dev"

5.5 部署

# 登录Cloudflare
npx wrangler login

# 创建D1数据库
npx wrangler d1 create mcp-auth

# 初始化数据库表
npx wrangler d1 execute mcp-auth --remote --command "
CREATE TABLE IF NOT EXISTS oauth_clients (
  client_id TEXT PRIMARY KEY,
  redirect_uris TEXT,
  grant_types TEXT,
  created_at INTEGER DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS tokens (
  access_token TEXT PRIMARY KEY,
  client_id TEXT,
  scope TEXT,
  expires_at INTEGER
);
"

# 部署
npx wrangler deploy

部署成功后你会看到:

Published mcp-weather-agent
  https://mcp-weather-agent.<your-subdomain>.workers.dev

六、Claude Code连接远程MCP服务器

6.1 配置远程MCP服务器

编辑 ~/.claude/settings.json:

{
  "mcpServers": {
    "weather": {
      "type": "http",
      "url": "https://mcp-weather-agent.<your-subdomain>.workers.dev/mcp"
    }
  }
}

Claude Code会自动执行OAuth流程:

  1. 请求 /mcp → 收到 401 + WWW-Authenticate 头
  2. 发现 .well-known/oauth-protected-resource
  3. 发现 .well-known/oauth-authorization-server
  4. 动态注册客户端(DCR)
  5. 打开浏览器进行用户授权
  6. 获取access_token
  7. 带Bearer token调用MCP工具

6.2 使用API Key的简化方案

对于内部工具或开发环境,可以使用API Key代替完整OAuth:

{
  "mcpServers": {
    "weather-dev": {
      "type": "http",
      "url": "https://mcp-weather-agent.<your-subdomain>.workers.dev/mcp",
      "headers": {
        "Authorization": "Bearer sk-dev-token-xxx"
      }
    }
  }
}

服务端验证API Key的实现:

// 简化版认证中间件
function validateApiKey(request: Request, env: Env): boolean {
  const auth = request.headers.get("Authorization");
  if (!auth?.startsWith("Bearer ")) return false;
  const token = auth.slice(7);

  // 方案一:硬编码(仅开发)
  if (token === env.API_KEY) return true;

  // 方案二:KV存储查找
  // const exists = await env.KV.get(`apikey:${token}`);
  // return !!exists;

  return false;
}

6.3 多环境配置

{
  "mcpServers": {
    "weather-local": {
      "type": "http",
      "url": "http://localhost:3000/mcp"
    },
    "weather-staging": {
      "type": "http",
      "url": "https://staging-mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer sk-staging-xxx"
      }
    },
    "weather-prod": {
      "type": "http",
      "url": "https://mcp.example.com/mcp"
    }
  }
}

七、生产环境最佳实践

7.1 架构总览

graph TB
    subgraph "客户端层"
        CC[Claude Code CLI]
        CW[Claude Web]
        CA[第三方Agent]
    end

    subgraph "Cloudflare边缘"
        CF[Workers<br/>Streamable HTTP]
        CF -.->|认证失败| AUTH[OAuth Provider<br/>Auth0/Clerk]
    end

    subgraph "存储层"
        D1[(D1 Database<br/>客户端+令牌)]
        KV[(KV Store<br/>API Keys)]
        R2[(R2 Storage<br/>资源文件)]
    end

    subgraph "后端服务"
        API[外部API<br/>Open-Meteo]
        DB[(业务数据库)]
    end

    CC -->|Bearer Token| CF
    CW -->|Bearer Token| CF
    CA -->|API Key| CF

    CF -->|validate| D1
    CF -->|validate| KV
    CF -->|fetch data| API
    CF -->|resources| R2
    AUTH -->|issue token| D1

7.2 限流保护

// 使用Cloudflare Rate Limiting或手动实现
async function rateLimit(request: Request, env: Env): Promise<Response | null> {
  const ip = request.headers.get("CF-Connecting-IP") || "unknown";
  const key = `ratelimit:${ip}:${Math.floor(Date.now() / 60000)}`;

  const count = await env.KV.get(key);
  const current = parseInt(count || "0");

  if (current > 100) {
    return new Response(JSON.stringify({ error: "rate_limited" }), {
      status: 429,
      headers: { "Retry-After": "60", "Content-Type": "application/json" },
    });
  }

  await env.KV.put(key, String(current + 1), { expirationTtl: 120 });
  return null; // 继续处理
}

7.3 审计日志

async function auditLog(env: Env, event: {
  method: string;
  tool?: string;
  client_ip: string;
  token_prefix: string;
  timestamp: number;
}) {
  await env.DB.prepare(
    "INSERT INTO audit_logs (method, tool, client_ip, token_prefix, timestamp) VALUES (?, ?, ?, ?, ?)"
  ).bind(event.method, event.tool || null, event.client_ip, event.token_prefix, event.timestamp).run();
}

7.4 健康检查

if (url.pathname === "/health") {
  const dbCheck = await env.DB.prepare("SELECT 1").first();
  return new Response(JSON.stringify({
    status: "ok",
    version: "1.0.0",
    db: dbCheck ? "connected" : "error",
    timestamp: Date.now(),
  }), {
    headers: { "Content-Type": "application/json" },
  });
}

八、完整实战案例:企业知识库MCP服务器

将前面所学整合为一个完整的可部署项目——企业知识库MCP服务器,提供文档搜索、摘要生成和知识问答三个工具。

8.1 核心代码

// src/knowledge-server.ts
import { McpAgent } from "agents/mcp";
import { z } from "zod";

interface Env {
  DB: D1Database;
  AI: Ai; // Cloudflare Workers AI
  VECTORIZE: VectorizeIndex;
  ISSUER: string;
}

export class KnowledgeMCP extends McpAgent {
  env!: Env;

  async init() {
    // 工具1:语义搜索文档
    this.server.tool(
      "search_docs",
      "在企业知识库中语义搜索文档",
      {
        query: z.string().describe("搜索查询,支持自然语言"),
        top_k: z.number().default(5).describe("返回结果数量"),
      },
      async ({ query, top_k }) => {
        // 使用Workers AI生成embedding
        const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
          text: [query],
        });

        // 向量搜索
        const results = await this.env.VECTORIZE.query(
          embedding.data[0],
          { topK: top_k }
        );

        const formatted = results.matches.map((m, i) =>
          `${i + 1}. [score: ${m.score?.toFixed(3)}] ${m.metadata?.title}\n   ${m.metadata?.snippet}`
        ).join("\n\n");

        return {
          content: [{
            type: "text" as const,
            text: `搜索 "${query}" 找到 ${results.count} 个结果:\n\n${formatted}`,
          }],
        };
      }
    );

    // 工具2:文档摘要
    this.server.tool(
      "summarize_doc",
      "生成文档的AI摘要",
      { doc_id: z.string().describe("文档ID") },
      async ({ doc_id }) => {
        const doc = await this.env.DB.prepare(
          "SELECT title, content FROM documents WHERE id = ?"
        ).bind(doc_id).first() as any;

        if (!doc) {
          return {
            content: [{ type: "text" as const, text: `文档 ${doc_id} 不存在` }],
            isError: true,
          };
        }

        const summary = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
          messages: [
            { role: "system", content: "用中文生成文档摘要,不超过200字。" },
            { role: "user", content: doc.content },
          ],
        });

        return {
          content: [{
            type: "text" as const,
            text: `**${doc.title}** 摘要:\n\n${(summary as any).response}`,
          }],
        };
      }
    );

    // 工具3:知识问答(RAG)
    this.server.tool(
      "ask_question",
      "基于知识库回答问题",
      {
        question: z.string().describe("用户问题"),
        context_count: z.number().default(3).describe("参考文档数"),
      },
      async ({ question, context_count }) => {
        // 检索相关文档
        const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
          text: [question],
        });
        const results = await this.env.VECTORIZE.query(
          embedding.data[0],
          { topK: context_count }
        );

        // 构建上下文
        const context = results.matches.map(
          (m) => m.metadata?.content || ""
        ).join("\n---\n");

        // RAG问答
        const answer = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
          messages: [
            {
              role: "system",
              content: `基于以下知识库内容回答用户问题。如果无法回答,请说明。\n\n知识库:\n${context}`,
            },
            { role: "user", content: question },
          ],
        });

        return {
          content: [{
            type: "text" as const,
            text: (answer as any).response,
          }],
        };
      }
    );
  }
}

export default {
  fetch: KnowledgeMCP.serve("/mcp").fetch,
};

8.2 完整wrangler.toml

name = "knowledge-mcp"
main = "src/knowledge-server.ts"
compatibility_date = "2025-12-01"

[vars]
ISSUER = "https://knowledge-mcp.<your-subdomain>.workers.dev"

[[d1_databases]]
binding = "DB"
database_name = "knowledge-db"
database_id = "<your-database-id>"

[[vectorize]]
binding = "VECTORIZE"
index_name = "knowledge-index"

[ai]
binding = "AI"

8.3 部署并连接

# 创建Vectorize索引
npx wrangler vectorize create knowledge-index --dimensions 768 --metric cosine

# 部署
npx wrangler deploy

# 在Claude Code中配置
# ~/.claude/settings.json
# {
#   "mcpServers": {
#     "knowledge": {
#       "type": "http",
#       "url": "https://knowledge-mcp.<your-subdomain>.workers.dev/mcp"
#     }
#   }
# }

部署完成后在Claude Code中直接使用:

> 搜索关于"API认证"的文档
> 总结文档 doc_001
> 基于知识库回答:我们的API认证流程是什么?

九、调试与排错指南

9.1 本地开发调试

# 使用wrangler dev本地运行(支持热重载)
npx wrangler dev --remote

# 使用MCP Inspector调试
npx @modelcontextprotocol/inspector http://localhost:8787/mcp

MCP Inspector是一个Web界面工具,可以: - 可视化查看服务器能力(tools、resources、prompts) - 逐个测试工具调用 - 查看完整的JSON-RPC请求/响应 - 模拟OAuth流程

9.2 Claude Code中的调试

# 查看MCP服务器连接状态
claude mcp list

# 查看特定服务器的日志
claude mcp logs weather

# 测试工具调用
claude mcp call weather get_weather '{"city": "Beijing"}'

9.3 常见部署问题

问题 排查步骤
502 Bad Gateway 检查Workers日志 npx wrangler tail
OAuth Discovery失败 验证 .well-known 返回正确的JSON
Token验证失败 检查D1数据库中令牌是否正确存储
CORS错误 确认OPTIONS请求正确处理
工具列表为空 确认 server.tool() 在 init() 中调用

十、架构决策对比

10.1 认证方案对比

方案 适用场景 安全级别 实现复杂度
API Key 内部工具、开发环境 中 低
OAuth 2.1 + PKCE 公开服务、多租户 高 高
mTLS 企业内部、服务间 最高 最高
JWT Bearer 微服务、已知客户端 高 中

10.2 部署平台对比

平台 冷启动 免费额度 MCP支持 推荐度
Cloudflare Workers < 5ms 10万req/天 官方SDK ★★★★★
Vercel ~250ms 10万req/月 需适配 ★★★☆☆
AWS Lambda ~100ms 100万req/月 需适配 ★★★☆☆
Railway/Fly.io ~500ms 有限 需适配 ★★☆☆☆

延伸阅读

参考来源

  1. Complete Guide to MCP in 2026 — MCP 2026架构、Streamable HTTP和OAuth全景指南
  2. MCP 2025-03-26 Spec Breakdown: OAuth Authentication, Remote Connections — MCP规范OAuth和远程连接深度解析
  3. Building a Secure MCP Server with OAuth 2.1 and Azure AD — 微软ISE团队的生产级OAuth 2.1实现
  4. Deploy a Remote MCP Server on Cloudflare — Cloudflare Workers远程MCP部署8分钟教程
  5. Why MCP Deprecated SSE and Went with Streamable HTTP — SSE到Streamable HTTP的迁移原因分析

训练营适用性评估:本教程适合全栈进阶训练营和AI工程实战营,学员需具备TypeScript基础和OAuth概念知识。建议教学时长为 6-8小时(含动手实践),可分为两个半天:第一天完成Streamable HTTP服务器和OAuth认证(步骤1-4),第二天完成Cloudflare部署和知识库项目(步骤5-8)。如果学员MCP基础较弱,建议先完成"MCP Server TypeScript全实战:三原语到发布"教程作为前置。

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