<!-- title: MCP远程服务器：OAuth到云端部署全实战 -->

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

## 概述

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

## 前置条件

- Node.js 20+ 和 npm/pnpm
- Claude Code CLI 已安装并配置
- Cloudflare 账户（免费版即可）
- 基本了解 OAuth 2.0 流程
- 熟悉 TypeScript 和 JSON-RPC

---

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

### 1.1 传输协议演进

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

```mermaid
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传输虽然简单，但存在天然限制：

- **无法共享**：团队成员无法复用同一个MCP服务器
- **无法扩展**：每个客户端需要独立进程
- **无认证**：依赖操作系统进程隔离
- **无监控**：难以观测工具调用行为

### 1.3 Streamable HTTP的核心优势

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

- **单端点架构**：只需一个 `/mcp` 或 `/` 端点
- **灵活响应模式**：支持普通JSON响应和SSE流式响应
- **会话管理**：通过 `Mcp-Session-Id` 头实现状态
- **原生HTTP语义**：复用CDN、负载均衡、中间件生态

---

## 二、Streamable HTTP协议深度解析

### 2.1 通信模型

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

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

```bash
# 创建项目
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`：

```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`：

```typescript
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 本地测试

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

用curl测试初始化握手：

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

预期响应：

```json
{
  "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`：

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

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

---

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

### 4.1 为什么远程MCP必须认证

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

- **工具调用风险**：MCP工具可以执行代码、读写文件、访问API
- **资源暴露**：Resource和Prompt可能包含敏感信息
- **合规要求**：企业环境需要审计每一次工具调用

### 4.2 OAuth 2.1 + PKCE流程

MCP使用OAuth 2.1 Authorization Code + PKCE（Proof Key for Code Exchange）流程：

```mermaid
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` 声明自己是受保护资源：

```json
{
  "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端点后，客户端动态注册自己：

```json
// 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中间件：

```typescript
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`);
});
```

辅助函数：

```typescript
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服务器的最佳平台：

- **全球边缘部署**：300+数据中心，延迟极低
- **零冷启动**：V8隔离技术，启动时间 < 5ms
- **免费额度充足**：10万次请求/天
- **原生支持OAuth**：Workers + D1可存储令牌
- **官方MCP支持**：Cloudflare Agents SDK内置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依赖：

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

配置 `wrangler.toml`：

```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`：

```typescript
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：

```bash
npm install agents
```

创建 `src/agent.ts`：

```typescript
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`：

```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 部署

```bash
# 登录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`：

```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：

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

服务端验证API Key的实现：

```typescript
// 简化版认证中间件
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 多环境配置

```json
{
  "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 架构总览

```mermaid
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 限流保护

```typescript
// 使用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 审计日志

```typescript
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 健康检查

```typescript
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 核心代码

```typescript
// 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

```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 部署并连接

```bash
# 创建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 本地开发调试

```bash
# 使用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中的调试

```bash
# 查看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 | 有限 | 需适配 | ★★☆☆☆ |

---

## 延伸阅读

- [MCP Specification 2025-06-18 - Transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) — Streamable HTTP传输协议完整规范
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) — OAuth 2.1认证规范
- [Cloudflare Agents - Remote MCP Server](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) — Cloudflare官方MCP部署指南
- [MCP TypeScript SDK - Streamable HTTP](https://github.com/modelcontextprotocol/typescript-sdk) — 官方TypeScript SDK及Streamable HTTP实现
- [Microsoft MCP for Beginners](https://github.com/microsoft/mcp-for-beginners) — 微软官方MCP入门教程

## 参考来源

1. [Complete Guide to MCP in 2026](https://dev.to/x4nent/complete-guide-to-mcp-model-context-protocol-in-2026-architecture-implementation-and-4a11) — MCP 2026架构、Streamable HTTP和OAuth全景指南
2. [MCP 2025-03-26 Spec Breakdown: OAuth Authentication, Remote Connections](https://qubittool.com/blog/mcp-2025-spec-oauth-remote-agent-skills) — MCP规范OAuth和远程连接深度解析
3. [Building a Secure MCP Server with OAuth 2.1 and Azure AD](https://devblogs.microsoft.com/ise/aca-secure-mcp-server-oauth21-azure-ad/) — 微软ISE团队的生产级OAuth 2.1实现
4. [Deploy a Remote MCP Server on Cloudflare](https://aidevdayindia.org/blogs/mcp-model-context-protocol-server-guide/how-to-deploy-remote-mcp-server-cloudflare.html) — Cloudflare Workers远程MCP部署8分钟教程
5. [Why MCP Deprecated SSE and Went with Streamable HTTP](https://blog.fka.dev/blog/2025-06-06-why-mcp-deprecated-sse-and-go-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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
