本教程面向已掌握MCP本地服务器开发的工程师,系统讲解如何将MCP服务器从本地stdio推向远程生产环境。你将学到Streamable HTTP传输协议、OAuth 2.1+PKCE认证流程、动态客户端注册(DCR),以及如何将带认证的MCP服务器部署到Cloudflare Workers并通过Claude Code连接。适合有一定TypeScript和OAuth基础的开发者,预计学习时长4-6小时。
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(强制) | 单端点多模式 |
本地stdio传输虽然简单,但存在天然限制:
Streamable HTTP在2025-06-18规范中成为唯一推荐的远程传输协议:
/mcp 或 / 端点Mcp-Session-Id 头实现状态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
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
(保持连接,推送服务端发起的通知)
| 头名称 | 方向 | 作用 |
|---|---|---|
Mcp-Session-Id |
双向 | 会话标识,初始化后必须携带 |
Accept |
客户端→服务端 | 必须包含 application/json 和 text/event-stream |
Content-Type |
双向 | application/json 或 text/event-stream |
# 创建项目
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/**/*"]
}
创建 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`);
});
# 启动服务器
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,后续请求需要携带。
编辑 ~/.claude/settings.json 或项目级 .claude/settings.json:
{
"mcpServers": {
"weather": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
}
重启Claude Code,即可使用天气查询工具。
MCP规范要求:所有远程MCP服务器必须实现OAuth 2.1认证。原因如下:
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响应
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
创建 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))); }
});
});
}
| 问题 | 原因 | 解决方案 |
|---|---|---|
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是目前部署远程MCP服务器的最佳平台:
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
安装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¤t_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 });
},
};
更简洁的方式是使用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¤t_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"
# 登录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/settings.json:
{
"mcpServers": {
"weather": {
"type": "http",
"url": "https://mcp-weather-agent.<your-subdomain>.workers.dev/mcp"
}
}
}
Claude Code会自动执行OAuth流程:
/mcp → 收到 401 + WWW-Authenticate 头.well-known/oauth-protected-resource.well-known/oauth-authorization-server对于内部工具或开发环境,可以使用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;
}
{
"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"
}
}
}
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
// 使用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; // 继续处理
}
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();
}
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服务器,提供文档搜索、摘要生成和知识问答三个工具。
// 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,
};
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"
# 创建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认证流程是什么?
# 使用wrangler dev本地运行(支持热重载)
npx wrangler dev --remote
# 使用MCP Inspector调试
npx @modelcontextprotocol/inspector http://localhost:8787/mcp
MCP Inspector是一个Web界面工具,可以: - 可视化查看服务器能力(tools、resources、prompts) - 逐个测试工具调用 - 查看完整的JSON-RPC请求/响应 - 模拟OAuth流程
# 查看MCP服务器连接状态
claude mcp list
# 查看特定服务器的日志
claude mcp logs weather
# 测试工具调用
claude mcp call weather get_weather '{"city": "Beijing"}'
| 问题 | 排查步骤 |
|---|---|
| 502 Bad Gateway | 检查Workers日志 npx wrangler tail |
| OAuth Discovery失败 | 验证 .well-known 返回正确的JSON |
| Token验证失败 | 检查D1数据库中令牌是否正确存储 |
| CORS错误 | 确认OPTIONS请求正确处理 |
| 工具列表为空 | 确认 server.tool() 在 init() 中调用 |
| 方案 | 适用场景 | 安全级别 | 实现复杂度 |
|---|---|---|---|
| API Key | 内部工具、开发环境 | 中 | 低 |
| OAuth 2.1 + PKCE | 公开服务、多租户 | 高 | 高 |
| mTLS | 企业内部、服务间 | 最高 | 最高 |
| JWT Bearer | 微服务、已知客户端 | 高 | 中 |
| 平台 | 冷启动 | 免费额度 | MCP支持 | 推荐度 |
|---|---|---|---|---|
| Cloudflare Workers | < 5ms | 10万req/天 | 官方SDK | ★★★★★ |
| Vercel | ~250ms | 10万req/月 | 需适配 | ★★★☆☆ |
| AWS Lambda | ~100ms | 100万req/月 | 需适配 | ★★★☆☆ |
| Railway/Fly.io | ~500ms | 有限 | 需适配 | ★★☆☆☆ |
训练营适用性评估:本教程适合全栈进阶训练营和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 等场景全覆盖。
