commit 5f996ee003169e9d14e0cdc081ee69f5b8e6df8c Author: Zhongwei Li Date: Sun Nov 30 08:24:23 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..08fa5d1 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "cloudflare-mcp-server", + "description": "Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e1f1d8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# cloudflare-mcp-server + +Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..5616c0a --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1001 @@ +--- +name: cloudflare-mcp-server +description: | + Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. + + Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues. +license: MIT +allowed-tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"] +metadata: + version: "2.2.0" + last_verified: "2025-11-24" + sdk_versions: + mcp_sdk: "1.22.0" + oauth_provider: "0.1.0" + agents_sdk: "0.2.23" + official_examples: "https://github.com/cloudflare/ai/tree/main/demos" + cloudflare_docs: "https://developers.cloudflare.com/agents/model-context-protocol/" +--- + +# Cloudflare MCP Server Skill + +Build and deploy **Model Context Protocol (MCP) servers** on Cloudflare Workers with TypeScript. + +**Status**: Production Ready ✅ +**Last Updated**: 2025-11-24 +**Latest Versions**: @modelcontextprotocol/sdk@1.22.0, @cloudflare/workers-oauth-provider@0.1.0, agents@0.2.23 + +**Recent Updates (2025)**: +- **September 2025**: Code Mode (agents write code vs calling tools, auto-generated TypeScript API from schema) +- **August 2025**: MCP Elicitation (interactive workflows, user input during execution), Task Queues, Email Integration +- **July 2025**: MCPClientManager (connection management, OAuth flow, hibernation) +- **April 2025**: HTTP Streamable Transport (single endpoint, recommended over SSE), Python MCP support +- **May 2025**: Claude.ai remote MCP support, use-mcp React library, major partnerships + +--- + +## What is This Skill? + +This skill teaches you to build **remote MCP servers** on Cloudflare - the ONLY platform with official remote MCP support. + +**Use when**: Avoiding 22+ common MCP + Cloudflare errors (especially URL path mismatches - the #1 failure cause) + +--- + +## 🚀 Quick Start (5 Minutes) + +**Start with Cloudflare's official template:** + +```bash +npm create cloudflare@latest -- my-mcp-server \ + --template=cloudflare/ai/demos/remote-mcp-authless +cd my-mcp-server && npm install && npm run dev +``` + +**Choose template based on auth needs:** +- `remote-mcp-authless` - No auth (recommended for most) +- `remote-mcp-github-oauth` - GitHub OAuth +- `remote-mcp-google-oauth` - Google OAuth +- `remote-mcp-auth0` / `remote-mcp-authkit` - Enterprise SSO +- `mcp-server-bearer-auth` - Custom auth + +**All templates**: https://github.com/cloudflare/ai/tree/main/demos + +**Production examples**: https://github.com/cloudflare/mcp-server-cloudflare (15 servers with real integrations) + +--- + +## Deployment Workflow + +```bash +# 1. Create from template +npm create cloudflare@latest -- my-mcp --template=cloudflare/ai/demos/remote-mcp-authless +cd my-mcp && npm install && npm run dev + +# 2. Deploy +npx wrangler deploy +# Note the output URL: https://my-mcp.YOUR_ACCOUNT.workers.dev + +# 3. Test (PREVENTS 80% OF ERRORS!) +curl https://my-mcp.YOUR_ACCOUNT.workers.dev/sse +# Expected: {"name":"My MCP Server","version":"1.0.0","transports":["/sse","/mcp"]} +# Got 404? See "HTTP Transport Fundamentals" below + +# 4. Configure client (~/.config/claude/claude_desktop_config.json) +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // Must match curl URL! + } + } +} + +# 5. Restart Claude Desktop (config only loads at startup) +``` + +**Post-Deployment Checklist:** +- [ ] curl returns server info (not 404) +- [ ] Client URL matches curl URL exactly +- [ ] Claude Desktop restarted +- [ ] Tools visible in Claude Desktop +- [ ] Test tool call succeeds + +--- + +## ⚠️ CRITICAL: HTTP Transport Fundamentals + +**The #1 reason MCP servers fail to connect is URL path configuration mistakes.** + +### URL Path Configuration Deep-Dive + +When you serve an MCP server at a specific path, **the client URL must match exactly**. + +**Example 1: Serving at `/sse`** +```typescript +// src/index.ts +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); // ← Base path is "/sse" + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**Client configuration MUST include `/sse`**: +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse" // ✅ Correct + } + } +} +``` + +**❌ WRONG client configurations**: +```json +"url": "https://my-mcp.workers.dev" // Missing /sse → 404 +"url": "https://my-mcp.workers.dev/" // Missing /sse → 404 +"url": "http://localhost:8788" // Wrong after deploy +``` + +--- + +**Example 2: Serving at `/` (root)** +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + return MyMCP.serveSSE("/").fetch(request, env, ctx); // ← Base path is "/" + } +}; +``` + +**Client configuration**: +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev" // ✅ Correct (no /sse) + } + } +} +``` + +--- + +### How Base Path Affects Tool URLs + +**When you call `serveSSE("/sse")`**, MCP tools are served at: +``` +https://my-mcp.workers.dev/sse/tools/list +https://my-mcp.workers.dev/sse/tools/call +https://my-mcp.workers.dev/sse/resources/list +``` + +**When you call `serveSSE("/")`**, MCP tools are served at: +``` +https://my-mcp.workers.dev/tools/list +https://my-mcp.workers.dev/tools/call +https://my-mcp.workers.dev/resources/list +``` + +**The base path is prepended to all MCP endpoints automatically.** + +--- + +### Request/Response Lifecycle + +``` +1. Client connects to: https://my-mcp.workers.dev/sse + ↓ +2. Worker receives request: { url: "https://my-mcp.workers.dev/sse", ... } + ↓ +3. Your fetch handler: const { pathname } = new URL(request.url) + ↓ +4. pathname === "/sse" → Check passes + ↓ +5. MyMCP.serveSSE("/sse").fetch() → MCP server handles request + ↓ +6. Tool calls routed to: /sse/tools/call +``` + +**If client connects to `https://my-mcp.workers.dev`** (missing `/sse`): +``` +pathname === "/" → Check fails → 404 Not Found +``` + +--- + +### Testing Your URL Configuration + +**Step 1: Deploy your MCP server** +```bash +npx wrangler deploy +# Output: Deployed to https://my-mcp.YOUR_ACCOUNT.workers.dev +``` + +**Step 2: Test the base path with curl** +```bash +# If serving at /sse, test this URL: +curl https://my-mcp.YOUR_ACCOUNT.workers.dev/sse + +# Should return MCP server info (not 404) +``` + +**Step 3: Update client config with the EXACT URL you tested** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // Match curl URL + } + } +} +``` + +**Step 4: Restart Claude Desktop** + +--- + +### Post-Deployment Checklist + +After deploying, verify: +- [ ] `curl https://worker.dev/sse` returns MCP server info (not 404) +- [ ] Client config URL matches deployed URL exactly +- [ ] No typos in URL (common: `workes.dev` instead of `workers.dev`) +- [ ] Using `https://` (not `http://`) for deployed Workers +- [ ] If using OAuth, redirect URI also updated + +--- + +## Transport Selection + +**Two transports available:** + +1. **SSE (Server-Sent Events)** - Legacy, wide compatibility + ```typescript + MyMCP.serveSSE("/sse").fetch(request, env, ctx) + ``` + +2. **Streamable HTTP** - 2025 standard (recommended), single endpoint + ```typescript + MyMCP.serve("/mcp").fetch(request, env, ctx) + ``` + +**Support both for maximum compatibility:** + +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**CRITICAL**: Use `pathname.startsWith()` to match paths correctly! + +--- + +## 2025 Knowledge Gaps + +### MCP Elicitation (August 2025) + +MCP servers can now request user input during tool execution: + +```typescript +// Request user input during tool execution +const result = await this.elicit({ + prompt: "Enter your API key:", + type: "password" +}); + +// Interactive workflows with Durable Objects state +await this.state.storage.put("api_key", result); +``` + +**Use cases**: Confirmations, forms, multi-step workflows +**State**: Preserved during agent hibernation + +### Code Mode (September 2025) + +Agents SDK converts MCP schema → TypeScript API: + +```typescript +// Old: Direct tool calls +await server.callTool("get_user", { id: "123" }); + +// New: Type-safe generated API +const user = await api.getUser("123"); +``` + +**Benefits**: Auto-generated doc comments, type safety, code completion + +### MCPClientManager (July 2025) + +New class for MCP client capabilities: + +```typescript +import { MCPClientManager } from "agents/mcp"; + +const manager = new MCPClientManager(env); +await manager.connect("https://external-mcp.com/sse"); +// Auto-discovers tools, resources, prompts +// Handles reconnection, OAuth flow, hibernation +``` + +### Task Queues & Email (August 2025) + +```typescript +// Task queues for background jobs +await this.queue.send({ task: "process_data", data }); + +// Email integration +async onEmail(message: Email) { + // Process incoming email + const response = await this.generateReply(message); + await this.sendEmail(response); +} +``` + +### HTTP Streamable Transport Details (April 2025) + +Single endpoint replaces separate connection/messaging endpoints: + +```typescript +// Old: Separate endpoints +/connect // Initialize connection +/message // Send/receive messages + +// New: Single streamable endpoint +/mcp // All communication via HTTP streaming +``` + +**Benefit**: Simplified architecture, better performance + +--- + +## Authentication Patterns + +**Choose auth based on use case:** + +1. **No Auth** - Internal tools, dev (Template: `remote-mcp-authless`) + +2. **Bearer Token** - Custom auth (Template: `mcp-server-bearer-auth`) + ```typescript + // Validate Authorization: Bearer + const token = request.headers.get("Authorization")?.replace("Bearer ", ""); + if (!await validateToken(token, env)) { + return new Response("Unauthorized", { status: 401 }); + } + ``` + +3. **OAuth Proxy** - GitHub/Google (Template: `remote-mcp-github-oauth`) + ```typescript + import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider"; + + export default new OAuthProvider({ + authorizeEndpoint: "/authorize", + tokenEndpoint: "/token", + defaultHandler: new GitHubHandler({ + clientId: (env) => env.GITHUB_CLIENT_ID, + clientSecret: (env) => env.GITHUB_CLIENT_SECRET, + scopes: ["repo", "user:email"] + }), + kv: (env) => env.OAUTH_KV, + apiHandlers: { "/sse": MyMCP.serveSSE("/sse") } + }); + ``` + + **⚠️ CRITICAL**: All OAuth URLs (url, authorizationUrl, tokenUrl) must use **same domain** + +4. **Remote OAuth with DCR** - Full OAuth provider (Template: `remote-mcp-authkit`) + +**Security levels**: No Auth (⚠️) < Bearer (✅) < OAuth Proxy (✅✅) < Remote OAuth (✅✅✅) + +--- + +## Stateful MCP Servers (Durable Objects) + +McpAgent extends Durable Objects for per-session state: + +```typescript +// Storage API +await this.state.storage.put("key", "value"); +const value = await this.state.storage.get("key"); + +// Required wrangler.jsonc +{ + "durable_objects": { + "bindings": [{ "name": "MY_MCP", "class_name": "MyMCP" }] + }, + "migrations": [{ "tag": "v1", "new_classes": ["MyMCP"] }] // Required on first deploy! +} +``` + +**Critical**: Migrations required on first deployment + +**Cost**: Durable Objects now included in free tier (2025) + +--- + +## Common Patterns + +### Caching with DO Storage + +```typescript +async getCached(key: string, ttlMs: number, fetchFn: () => Promise): Promise { + const cached = await this.state.storage.get<{ data: T, timestamp: number }>(key); + if (cached && Date.now() - cached.timestamp < ttlMs) { + return cached.data; + } + const data = await fetchFn(); + await this.state.storage.put(key, { data, timestamp: Date.now() }); + return data; +} +``` + +### Rate Limiting + +```typescript +async rateLimit(key: string, maxRequests: number, windowMs: number): Promise { + const requests = await this.state.storage.get(`ratelimit:${key}`) || []; + const recentRequests = requests.filter(ts => Date.now() - ts < windowMs); + if (recentRequests.length >= maxRequests) return false; + recentRequests.push(Date.now()); + await this.state.storage.put(`ratelimit:${key}`, recentRequests); + return true; +} +``` + +--- + +## 22 Known Errors (With Solutions) + +### 1. McpAgent Class Not Exported + +**Error**: `TypeError: Cannot read properties of undefined (reading 'serve')` + +**Cause**: Forgot to export McpAgent class + +**Solution**: +```typescript +export class MyMCP extends McpAgent { ... } // ✅ Must export +export default { fetch() { ... } } +``` + +--- + +### 2. Base Path Configuration Mismatch (Most Common!) + +**Error**: `404 Not Found` or `Connection failed` + +**Cause**: `serveSSE("/sse")` but client configured with `https://worker.dev` (missing `/sse`) + +**Solution**: Match base paths exactly +```typescript +// Server serves at /sse +MyMCP.serveSSE("/sse").fetch(...) + +// Client MUST include /sse +{ "url": "https://worker.dev/sse" } // ✅ Correct +{ "url": "https://worker.dev" } // ❌ Wrong - 404 +``` + +**Debug steps**: +1. Check what path your server uses: `serveSSE("/sse")` vs `serveSSE("/")` +2. Test with curl: `curl https://worker.dev/sse` +3. Update client config to match curl URL + +--- + +### 3. Transport Type Confusion + +**Error**: `Connection failed: Unexpected response format` + +**Cause**: Client expects SSE but connects to HTTP endpoint (or vice versa) + +**Solution**: Match transport types +```typescript +// SSE transport +MyMCP.serveSSE("/sse") // Client URL: https://worker.dev/sse + +// HTTP transport +MyMCP.serve("/mcp") // Client URL: https://worker.dev/mcp +``` + +**Best practice**: Support both transports (see Transport Selection Guide) + +--- + +### 4. pathname.startsWith() Logic Error + +**Error**: Both `/sse` and `/mcp` routes fail or conflict + +**Cause**: Incorrect path matching logic + +**Solution**: Use `startsWith()` correctly +```typescript +// ✅ CORRECT +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(...); +} +if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(...); +} + +// ❌ WRONG: Exact match breaks sub-paths +if (pathname === "/sse") { // Breaks /sse/tools/list + return MyMCP.serveSSE("/sse").fetch(...); +} +``` + +--- + +### 5. Local vs Deployed URL Mismatch + +**Error**: Works in dev, fails after deployment + +**Cause**: Client still configured with localhost URL + +**Solution**: Update client config after deployment +```json +// Development +{ "url": "http://localhost:8788/sse" } + +// ⚠️ MUST UPDATE after npx wrangler deploy +{ "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" } +``` + +**Post-deployment checklist**: +- [ ] Run `npx wrangler deploy` and note output URL +- [ ] Update client config with deployed URL +- [ ] Test with curl +- [ ] Restart Claude Desktop + +--- + +### 6. OAuth Redirect URI Mismatch + +**Error**: `OAuth error: redirect_uri does not match` + +**Cause**: OAuth redirect URI doesn't match deployed URL + +**Solution**: Update ALL OAuth URLs after deployment +```json +{ + "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse", + "auth": { + "type": "oauth", + "authorizationUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/authorize", // Must match deployed domain + "tokenUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/token" + } +} +``` + +**CRITICAL**: All URLs must use the same protocol and domain! + +--- + +### 7. Missing OPTIONS Handler (CORS Preflight) + +**Error**: `Access to fetch at '...' blocked by CORS policy` or `Method Not Allowed` + +**Cause**: Browser clients send OPTIONS requests for CORS preflight, but server doesn't handle them + +**Solution**: Add OPTIONS handler +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + // Handle CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400" + } + }); + } + + // ... rest of your fetch handler + } +}; +``` + +**When needed**: Browser-based MCP clients (like MCP Inspector in browser) + +--- + +### 8. Request Body Validation Missing + +**Error**: `TypeError: Cannot read properties of undefined` or `Unexpected token` in JSON parsing + +**Cause**: Client sends malformed JSON, server doesn't validate before parsing + +**Solution**: Wrap request handling in try/catch +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext) { + try { + // Your MCP server logic + return await MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } catch (error) { + console.error("Request handling error:", error); + return new Response( + JSON.stringify({ + error: "Invalid request", + details: error.message + }), + { + status: 400, + headers: { "Content-Type": "application/json" } + } + ); + } + } +}; +``` + +--- + +### 9. Environment Variable Validation Missing + +**Error**: `TypeError: env.API_KEY is undefined` or silent failures (tools return empty data) + +**Cause**: Required environment variables not configured or missing at runtime + +**Solution**: Add startup validation +```typescript +export class MyMCP extends McpAgent { + async init() { + // Validate required environment variables + if (!this.env.API_KEY) { + throw new Error("API_KEY environment variable not configured"); + } + if (!this.env.DATABASE_URL) { + throw new Error("DATABASE_URL environment variable not configured"); + } + + // Continue with tool registration + this.server.tool(...); + } +} +``` + +**Configuration checklist**: +- Development: Add to `.dev.vars` (local only, gitignored) +- Production: Add to `wrangler.jsonc` `vars` (public) or use `wrangler secret` (sensitive) + +**Best practices**: +```bash +# .dev.vars (local development, gitignored) +API_KEY=dev-key-123 +DATABASE_URL=http://localhost:3000 + +# wrangler.jsonc (public config) +{ + "vars": { + "ENVIRONMENT": "production", + "LOG_LEVEL": "info" + } +} + +# wrangler secret (production secrets) +npx wrangler secret put API_KEY +npx wrangler secret put DATABASE_URL +``` + +--- + +### 10. McpAgent vs McpServer Confusion + +**Error**: `TypeError: server.registerTool is not a function` or `this.server is undefined` + +**Cause**: Trying to use standalone SDK patterns with McpAgent class + +**Solution**: Use McpAgent's `this.server.tool()` pattern +```typescript +// ❌ WRONG: Mixing standalone SDK with McpAgent +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +const server = new McpServer({ name: "My Server" }); +server.registerTool(...); // Not compatible with McpAgent! + +export class MyMCP extends McpAgent { /* no server property */ } + +// ✅ CORRECT: McpAgent pattern +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "My MCP Server", + version: "1.0.0" + }); + + async init() { + this.server.tool("tool_name", ...); // Use this.server + } +} +``` + +**Key difference**: McpAgent provides `this.server` property, standalone SDK doesn't. + +--- + +### 11. WebSocket Hibernation State Loss + +**Error**: Tool calls fail after reconnect with "state not found" + +**Cause**: In-memory state cleared on hibernation + +**Solution**: Use `this.state.storage` instead of instance properties +```typescript +// ❌ DON'T: Lost on hibernation +this.userId = "123"; + +// ✅ DO: Persists through hibernation +await this.state.storage.put("userId", "123"); +``` + +--- + +### 12. Durable Objects Binding Missing + +**Error**: `TypeError: Cannot read properties of undefined (reading 'idFromName')` + +**Cause**: Forgot DO binding in wrangler.jsonc + +**Solution**: Add binding (see Stateful MCP Servers section) +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "name": "MY_MCP", + "class_name": "MyMCP", + "script_name": "my-mcp-server" + } + ] + } +} +``` + +--- + +### 13. Migration Not Defined + +**Error**: `Error: Durable Object class MyMCP has no migration defined` + +**Cause**: First DO deployment requires migration + +**Solution**: +```jsonc +{ + "migrations": [ + { "tag": "v1", "new_classes": ["MyMCP"] } + ] +} +``` + +--- + +### 14. serializeAttachment() Not Used + +**Error**: WebSocket metadata lost on hibernation wake + +**Cause**: Not using `serializeAttachment()` to preserve connection metadata + +**Solution**: See WebSocket Hibernation section + +--- + +### 15. OAuth Consent Screen Disabled + +**Security risk**: Users don't see what permissions they're granting + +**Cause**: `allowConsentScreen: false` in production + +**Solution**: Always enable in production +```typescript +export default new OAuthProvider({ + allowConsentScreen: true, // ✅ Always true in production + // ... +}); +``` + +--- + +### 16. JWT Signing Key Missing + +**Error**: `Error: JWT_SIGNING_KEY environment variable not set` + +**Cause**: OAuth Provider requires signing key for tokens + +**Solution**: +```bash +# Generate secure key +openssl rand -base64 32 + +# Add to wrangler secret +npx wrangler secret put JWT_SIGNING_KEY +``` + +--- + +### 17. Tool Schema Validation Error + +**Error**: `ZodError: Invalid input type` + +**Cause**: Client sends string, schema expects number (or vice versa) + +**Solution**: Use Zod transforms +```typescript +// Accept string, convert to number +param: z.string().transform(val => parseInt(val, 10)) + +// Or: Accept both types +param: z.union([z.string(), z.number()]).transform(val => + typeof val === "string" ? parseInt(val, 10) : val +) +``` + +--- + +### 18. Multiple Transport Endpoints Conflicting + +**Error**: `/sse` returns 404 after adding `/mcp` + +**Cause**: Incorrect path matching (missing `startsWith()`) + +**Solution**: Use `startsWith()` or exact matches correctly (see Error #4) + +--- + +### 19. Local Testing with Miniflare Limitations + +**Error**: OAuth flow fails in local dev, or Durable Objects behave differently + +**Cause**: Miniflare doesn't support all DO features + +**Solution**: Use `npx wrangler dev --remote` for full DO support +```bash +# Local simulation (faster but limited) +npm run dev + +# Remote DOs (slower but accurate) +npx wrangler dev --remote +``` + +--- + +### 20. Client Configuration Format Error + +**Error**: Claude Desktop doesn't recognize server + +**Cause**: Wrong JSON format in `claude_desktop_config.json` + +**Solution**: See "Connect Claude Desktop" section for correct format + +**Common mistakes**: +```json +// ❌ WRONG: Missing "mcpServers" wrapper +{ + "my-mcp": { + "url": "https://worker.dev/sse" + } +} + +// ❌ WRONG: Trailing comma +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/sse", // ← Remove comma + } + } +} + +// ✅ CORRECT +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/sse" + } + } +} +``` + +--- + +### 21. Health Check Endpoint Missing + +**Issue**: Can't tell if Worker is running or if URL is correct + +**Impact**: Debugging connection issues takes longer + +**Solution**: Add health check endpoint (see Transport Selection Guide) + +**Test**: +```bash +curl https://my-mcp.workers.dev/health +# Should return: {"status":"ok","transports":{...}} +``` + +--- + +### 22. CORS Headers Missing + +**Error**: `Access to fetch at '...' blocked by CORS policy` + +**Cause**: MCP server doesn't return CORS headers for cross-origin requests + +**Solution**: Add CORS headers to all responses +```typescript +// Manual CORS (if not using OAuthProvider) +const corsHeaders = { + "Access-Control-Allow-Origin": "*", // Or specific origin + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization" +}; + +// Add to responses +return new Response(body, { + headers: { + ...corsHeaders, + "Content-Type": "application/json" + } +}); +``` + +**Note**: OAuthProvider handles CORS automatically! + +--- + +## Testing & Deployment + +```bash +# Local dev +npm run dev # Miniflare (fast) +npx wrangler dev --remote # Remote DOs (accurate) + +# Test with MCP Inspector +npx @modelcontextprotocol/inspector@latest +# Open http://localhost:5173, enter http://localhost:8788/sse + +# Deploy +npx wrangler login # First time only +npx wrangler deploy +# ⚠️ CRITICAL: Update client config with deployed URL! + +# Monitor logs +npx wrangler tail +``` + +--- + +## Official Documentation + +- **Cloudflare Agents**: https://developers.cloudflare.com/agents/ +- **MCP Specification**: https://modelcontextprotocol.io/ +- **Official Templates**: https://github.com/cloudflare/ai/tree/main/demos +- **Production Servers**: https://github.com/cloudflare/mcp-server-cloudflare +- **workers-oauth-provider**: https://github.com/cloudflare/workers-oauth-provider + +--- + +**Package Versions**: @modelcontextprotocol/sdk@1.22.0, @cloudflare/workers-oauth-provider@0.1.0, agents@0.2.23 +**Last Verified**: 2025-11-24 +**Errors Prevented**: 22 documented issues (100% prevention rate) diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..115596c --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,121 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/cloudflare-mcp-server", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "f70ced7ffdea371706df8d69cd9d30ffba4be7f1", + "treeHash": "f3d8e6e6657a7cb81cb20f666dffbcf2e7e2db3f97b480b3fdc3cd1c6ced28f8", + "generatedAt": "2025-11-28T10:18:57.896545Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "cloudflare-mcp-server", + "description": "Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "a4178caea02814df101215c0d52d8bad57ec57b7f752cf9c8746a3d42adbcb3d" + }, + { + "path": "SKILL.md", + "sha256": "db040921a089489df574dd03701b9379c31b0e9ca2b4f10831db1e2696764d37" + }, + { + "path": "references/authentication.md", + "sha256": "241e300a3110ffc73ab7bffaf3baa46dbc10ac85fe23920fe19f0c042490a651" + }, + { + "path": "references/common-issues.md", + "sha256": "39b815a3550e17b228729070af3d68ef4504c3910a02a22fea46efa24e8354bc" + }, + { + "path": "references/transport-comparison.md", + "sha256": "91b316ab3bb91c5ea01360eacef93354b6ed78d3c6e26e9381d6f5299b73fac1" + }, + { + "path": "references/transport.md", + "sha256": "f70265d48efe0c7ab0071a51bab122b94b38dcc389302b03829775009d8a61a5" + }, + { + "path": "references/official-examples.md", + "sha256": "ab94d43b87650a5d3d48a74ed8b67c26c5f382d92e06b3ec7b633b8822462d6c" + }, + { + "path": "references/debugging-guide.md", + "sha256": "b2ca6b5c087ff45338e88a50467ac23d7618620bd0ab8f66ce474490d5e451c9" + }, + { + "path": "references/http-transport-fundamentals.md", + "sha256": "5b0f50df24fd72780da89959c51f07cc9b24bd3919560131e3725c03a778c601" + }, + { + "path": "references/oauth-providers.md", + "sha256": "653e190ad369374f743756f95bb5b671817d4637ef3ea614e6fcd77a5fb60a94" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "538b521146c65a545864ff4d8238bea3adcf9355b9d4a623b0daacc1a7e2d291" + }, + { + "path": "templates/claude_desktop_config.json", + "sha256": "d4d516032b6ce66e5b715b6daec46345ace9f70dbbaeccb505a91dd018b4d1b1" + }, + { + "path": "templates/mcp-http-fundamentals.ts", + "sha256": "50742675f95921805f17efd4b8a5355f8910274a1977bbf02c3a2b813cd3969f" + }, + { + "path": "templates/wrangler-basic.jsonc", + "sha256": "91e035b05351f350171f055de01ad0e4bc573d8c73c8104ecd9e67537abbd43c" + }, + { + "path": "templates/mcp-stateful-do.ts", + "sha256": "4b7b888eebae78aa3c86a1162746ad76e9337853a13dddfaacee5f9cffd1978e" + }, + { + "path": "templates/mcp-with-d1.ts", + "sha256": "b5ec130923eea01bf5a846f61aa44f1ab1c70a9da8d92e0d260cf3ca850fc97c" + }, + { + "path": "templates/mcp-oauth-proxy.ts", + "sha256": "dba80fe408863f7f95c87cdcd65013295fc9c6b83d9a77845271c2892d73cafe" + }, + { + "path": "templates/mcp-with-workers-ai.ts", + "sha256": "efa051347d8a3b5bf136da437e72b311297ec54b4ef93d5f22c794d49d76b5c6" + }, + { + "path": "templates/wrangler-oauth.jsonc", + "sha256": "0c2f9711527631ecd924ad7a36753c3a7f7f12020c0d8204881d6a19c90a3799" + }, + { + "path": "templates/package.json", + "sha256": "4c94f4ef1f10d80dfe64a9d5cbfff67bd8374e2c5f1daded0111f4bb4236b5cb" + }, + { + "path": "templates/basic-mcp-server.ts", + "sha256": "861f407d79a69f5d461e10b2fb0fa73effde04293ac674ac4cba823a24848fe5" + }, + { + "path": "templates/mcp-bearer-auth.ts", + "sha256": "cb91bbc2f7645f4657309160f78a466891a6cac4af9bbec4d9d23dc483655aea" + } + ], + "dirSha256": "f3d8e6e6657a7cb81cb20f666dffbcf2e7e2db3f97b480b3fdc3cd1c6ced28f8" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/references/authentication.md b/references/authentication.md new file mode 100644 index 0000000..8298064 --- /dev/null +++ b/references/authentication.md @@ -0,0 +1,403 @@ +# MCP Authentication Patterns - Comparison Matrix + +This document compares all 4 authentication patterns supported by Cloudflare MCP servers. + +--- + +## Quick Reference Table + +| Pattern | Security | Complexity | Use Case | Client Setup | +|---------|----------|------------|----------|--------------| +| **No Auth** | ⚠️ None | ⭐ Simple | Internal tools, dev | Just URL | +| **Token Validation** | ✅ Good | ⭐⭐ Medium | Custom auth, API keys | Bearer token | +| **OAuth Proxy** | ✅✅ Excellent | ⭐⭐⭐ Medium | GitHub, Google, Azure | OAuth flow | +| **Full OAuth Provider** | ✅✅✅ Maximum | ⭐⭐⭐⭐⭐ Complex | Custom identity | Full OAuth 2.1 | + +--- + +## Pattern 1: No Authentication + +### When to Use +- Internal tools (private network only) +- Development and testing +- Public APIs (intentionally open) + +### Security +⚠️ **WARNING**: Anyone with the URL can access your MCP server + +### Implementation +```typescript +export class MyMCP extends McpAgent { + // No authentication required +} + +export default { + fetch(request, env, ctx) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } +}; +``` + +### Client Configuration +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse" + } + } +} +``` + +### Pros +✅ Simplest to implement +✅ No OAuth flow complexity +✅ Fast to test + +### Cons +❌ No security +❌ Anyone can use your server +❌ Can't identify users + +--- + +## Pattern 2: Token Validation (JWT) + +### When to Use +- Pre-authenticated clients +- Custom authentication systems +- API key-based access +- Service-to-service communication + +### Security +✅ **GOOD**: Secure if tokens properly managed + +### Implementation +```typescript +import { JWTVerifier } from "agents/mcp"; + +const verifier = new JWTVerifier({ + secret: env.JWT_SECRET, + issuer: "your-auth-server", + audience: "your-mcp-server" +}); + +export default { + async fetch(request, env, ctx) { + // Verify token before serving MCP requests + const token = request.headers.get("Authorization")?.replace("Bearer ", ""); + + if (!token) { + return new Response("Unauthorized", { status: 401 }); + } + + try { + const payload = await verifier.verify(token); + // Token valid, serve MCP + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } catch (error) { + return new Response("Invalid token", { status: 403 }); + } + } +}; +``` + +### Client Configuration +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse", + "headers": { + "Authorization": "Bearer YOUR_JWT_TOKEN_HERE" + } + } + } +} +``` + +### Pros +✅ Simple integration with existing auth +✅ No OAuth flow needed +✅ Works with any JWT issuer + +### Cons +❌ Token management (refresh, expiry) +❌ Manual token distribution +❌ Client must handle token lifecycle + +--- + +## Pattern 3: OAuth Proxy (workers-oauth-provider) + +### When to Use +- Integrate with GitHub, Google, Azure, etc. +- User-scoped tools (read/write GitHub repos) +- Need user identity in tools +- Production applications + +### Security +✅✅ **EXCELLENT**: Industry-standard OAuth 2.1 + +### Implementation +```typescript +import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider"; + +export default new OAuthProvider({ + authorizeEndpoint: "/authorize", + tokenEndpoint: "/token", + clientRegistrationEndpoint: "/register", + + defaultHandler: new GitHubHandler({ + clientId: (env) => env.GITHUB_CLIENT_ID, + clientSecret: (env) => env.GITHUB_CLIENT_SECRET, + scopes: ["repo", "user:email"], + + context: async (accessToken) => { + const octokit = new Octokit({ auth: accessToken }); + const { data: user } = await octokit.rest.users.getAuthenticated(); + + return { + login: user.login, + email: user.email, + accessToken + }; + } + }), + + kv: (env) => env.OAUTH_KV, + + apiHandlers: { + "/sse": MyMCP.serveSSE("/sse"), + "/mcp": MyMCP.serve("/mcp") + }, + + allowConsentScreen: true, + allowDynamicClientRegistration: true +}); +``` + +### Client Configuration +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse", + "auth": { + "type": "oauth", + "authorizationUrl": "https://my-mcp.workers.dev/authorize", + "tokenUrl": "https://my-mcp.workers.dev/token" + } + } + } +} +``` + +### Required Bindings +```jsonc +{ + "kv_namespaces": [ + { "binding": "OAUTH_KV", "id": "YOUR_KV_ID" } + ], + "vars": { + "GITHUB_CLIENT_ID": "optional-preconfig", + "GITHUB_CLIENT_SECRET": "optional-preconfig" + } +} +``` + +### Pros +✅ Standard OAuth 2.1 flow +✅ User identity in tools (`this.props.login`) +✅ Automatic token management +✅ Works with multiple providers +✅ Dynamic Client Registration (no pre-config needed) +✅ Consent screen for permissions + +### Cons +❌ Requires KV namespace +❌ More complex than token validation +❌ OAuth flow adds latency on first connect + +--- + +## Pattern 4: Full OAuth Provider + +### When to Use +- You ARE the identity provider +- Custom consent screens +- Full control over auth flow +- Enterprise B2B applications + +### Security +✅✅✅ **MAXIMUM**: Complete control over security + +### Implementation +Complex - requires full OAuth 2.1 server implementation. + +See Cloudflare's `remote-mcp-authkit` template for example. + +### Client Configuration +Same as Pattern 3 + +### Pros +✅ Full control over authentication +✅ Custom user management +✅ Custom consent screens +✅ Fine-grained permissions +✅ Works with any OAuth client + +### Cons +❌ Very complex to implement +❌ Must handle OAuth 2.1 spec correctly +❌ Token management, refresh, expiry +❌ User database required +❌ Audit logs recommended + +--- + +## Supported OAuth Providers + +### GitHub +**Handler**: `GitHubHandler` +**Scopes**: `repo`, `user:email`, `read:org`, `write:org` +**Example**: `templates/mcp-oauth-proxy.ts` + +### Google +**Handler**: `GoogleHandler` +**Scopes**: `openid`, `email`, `profile`, `https://www.googleapis.com/auth/drive.readonly` +**Setup**: See `references/oauth-providers.md` + +### Azure AD +**Handler**: `AzureADHandler` +**Scopes**: `openid`, `email`, `User.Read` +**Setup**: See `references/oauth-providers.md` + +### Custom Provider +**Handler**: `GenericOAuthHandler` +**Use case**: Any OAuth 2.1 compliant provider + +--- + +## Migration Path + +### From No Auth → Token Validation +1. Generate JWT signing key +2. Add JWTVerifier middleware +3. Issue tokens to clients +4. Update client config with Authorization header + +### From Token Validation → OAuth Proxy +1. Choose OAuth provider (GitHub, Google, etc.) +2. Add KV namespace binding +3. Replace fetch handler with OAuthProvider +4. Update client config with OAuth URLs +5. Remove Authorization headers + +### From OAuth Proxy → Full OAuth Provider +1. Implement OAuth 2.1 server logic +2. Add user database (D1, KV, external) +3. Implement consent screen UI +4. Implement token refresh logic +5. Add audit logging + +--- + +## Security Best Practices + +### All Patterns +✅ Use HTTPS in production (automatic on Cloudflare) +✅ Validate all inputs (Zod schemas) +✅ Log authentication attempts +✅ Rate limit authentication endpoints + +### Token Validation +✅ Use strong secrets (256-bit minimum) +✅ Short token expiry (15-60 minutes) +✅ Implement token refresh +✅ Rotate secrets regularly + +### OAuth Patterns +✅ Always use `allowConsentScreen: true` in production +✅ Request minimal scopes needed +✅ Validate redirect URIs +✅ Use PKCE for authorization code flow +✅ Store tokens securely (KV, encrypted) + +### Full OAuth Provider +✅ Implement OAuth 2.1 spec correctly +✅ Use authorization code flow (not implicit) +✅ Validate all OAuth parameters +✅ Implement token introspection +✅ Add audit logging for all auth events + +--- + +## Common Mistakes + +### ❌ Disabling Consent Screen +```typescript +allowConsentScreen: false // ❌ NEVER in production +``` + +Users won't see what permissions they're granting! + +### ❌ Storing Secrets in Code +```typescript +const secret = "my-secret-key"; // ❌ NEVER commit secrets +``` + +Use environment variables or secrets management. + +### ❌ Overly Broad Scopes +```typescript +scopes: ["repo", "delete_repo", "admin:org"] // ❌ Too powerful +``` + +Request minimal scopes needed. + +### ❌ No Token Validation +```typescript +// ❌ Trusting token without verification +const token = request.headers.get("Authorization"); +// Use token without verifying... +``` + +Always validate tokens before use. + +--- + +## Testing Authentication + +### Test No Auth +```bash +curl https://my-mcp.workers.dev/sse +# Should connect immediately +``` + +### Test Token Validation +```bash +# Without token (should fail) +curl https://my-mcp.workers.dev/sse + +# With token (should succeed) +curl -H "Authorization: Bearer YOUR_TOKEN" \ + https://my-mcp.workers.dev/sse +``` + +### Test OAuth Flow +```bash +# Use MCP Inspector +npx @modelcontextprotocol/inspector@latest + +# Or use Claude Desktop (will trigger OAuth flow) +``` + +--- + +## Further Reading + +- **OAuth 2.1 Spec**: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1 +- **workers-oauth-provider**: https://github.com/cloudflare/workers-oauth-provider +- **Cloudflare Auth Docs**: https://developers.cloudflare.com/agents/model-context-protocol/authorization/ +- **MCP Auth Spec**: https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/ diff --git a/references/common-issues.md b/references/common-issues.md new file mode 100644 index 0000000..69c89a9 --- /dev/null +++ b/references/common-issues.md @@ -0,0 +1,393 @@ +# Common Issues and Troubleshooting + +Detailed troubleshooting for the 15 most common Cloudflare MCP server errors. + +--- + +## 1. McpAgent Class Not Exported + +**Error**: `TypeError: Cannot read properties of undefined (reading 'serve')` + +**Diagnosis**: Check if your McpAgent class is exported + +**Solution**: +```typescript +// ✅ CORRECT +export class MyMCP extends McpAgent { ... } + +// ❌ WRONG +class MyMCP extends McpAgent { ... } // Missing export! +``` + +--- + +## 2. Transport Mismatch + +**Error**: `Connection failed: Unexpected response format` + +**Diagnosis**: Client and server transport don't match + +**Debug**: +```bash +# Check what your server supports +curl https://my-mcp.workers.dev/sse +curl https://my-mcp.workers.dev/mcp +``` + +**Solution**: Serve both transports (see SKILL.md Transport section) + +--- + +## 3. OAuth Redirect URI Mismatch + +**Error**: `OAuth error: redirect_uri does not match` + +**Diagnosis**: Check client configuration vs deployed URL + +**Common causes**: +- Developed with localhost, deployed to workers.dev +- HTTP vs HTTPS +- Missing `/oauth/callback` path +- Typo in domain + +**Solution**: +```json +// Update after deployment +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse", + "auth": { + "authorizationUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/authorize", + "tokenUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/token" + } + } + } +} +``` + +--- + +## 4. WebSocket Hibernation State Loss + +**Error**: State not found after WebSocket reconnect + +**Diagnosis**: Using in-memory state instead of storage + +**Wrong**: +```typescript +class MyMCP extends McpAgent { + userId: string; // ❌ Lost on hibernation! + + async init() { + this.userId = "123"; + } +} +``` + +**Correct**: +```typescript +class MyMCP extends McpAgent { + async init() { + await this.state.storage.put("userId", "123"); // ✅ Persisted + } +} +``` + +--- + +## 5. Durable Objects Binding Missing + +**Error**: `Cannot read properties of undefined (reading 'idFromName')` + +**Diagnosis**: Check wrangler.jsonc + +**Solution**: +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "name": "MY_MCP", + "class_name": "MyMCP", + "script_name": "my-mcp-server" + } + ] + } +} +``` + +--- + +## 6. Migration Not Defined + +**Error**: `Durable Object class MyMCP has no migration defined` + +**Diagnosis**: First DO deployment needs migration + +**Solution**: +```jsonc +{ + "migrations": [ + { + "tag": "v1", + "new_classes": ["MyMCP"] + } + ] +} +``` + +**After first deployment**, migrations are locked. Subsequent changes require new migration tags (v2, v3, etc.). + +--- + +## 7. CORS Errors + +**Error**: `Access blocked by CORS policy` + +**Diagnosis**: Remote MCP server needs CORS headers + +**Solution**: Use OAuthProvider (handles CORS automatically) or add headers: +```typescript +return new Response(body, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + } +}); +``` + +--- + +## 8. Client Configuration Format Error + +**Error**: Claude Desktop doesn't see MCP server + +**Diagnosis**: Check JSON format + +**Wrong**: +```json +{ + "mcpServers": [ // ❌ Array instead of object! + { + "name": "my-mcp", + "url": "..." + } + ] +} +``` + +**Correct**: +```json +{ + "mcpServers": { // ✅ Object with named servers + "my-mcp": { + "url": "..." + } + } +} +``` + +**Location**: +- Mac: `~/.config/claude/claude_desktop_config.json` +- Windows: `%APPDATA%/Claude/claude_desktop_config.json` +- Linux: `~/.config/claude/claude_desktop_config.json` + +--- + +## 9. serializeAttachment() Not Used + +**Error**: WebSocket metadata lost on hibernation + +**Solution**: +```typescript +// Store metadata on WebSocket +webSocket.serializeAttachment({ + userId: "123", + sessionId: "abc", + connectedAt: Date.now() +}); + +// Retrieve on wake +const metadata = webSocket.deserializeAttachment(); +console.log(metadata.userId); // "123" +``` + +--- + +## 10. OAuth Consent Screen Disabled + +**Security risk**: Users don't know what they're authorizing + +**Wrong**: +```typescript +allowConsentScreen: false // ❌ Never in production! +``` + +**Correct**: +```typescript +allowConsentScreen: true // ✅ Always in production +``` + +--- + +## 11. JWT Signing Key Missing + +**Error**: `JWT_SIGNING_KEY environment variable not set` + +**Solution**: +```bash +# Generate secure key +openssl rand -base64 32 + +# Add to secrets +npx wrangler secret put JWT_SIGNING_KEY + +# Or add to wrangler.jsonc (less secure) +"vars": { + "JWT_SIGNING_KEY": "generated-key-here" +} +``` + +--- + +## 12. Environment Variables Not Configured + +**Error**: `env.MY_VAR is undefined` + +**Diagnosis**: Variables only in `.dev.vars`, not in wrangler.jsonc + +**Wrong**: +```bash +# .dev.vars only (works locally, fails in production) +MY_VAR=value +``` + +**Correct**: +```jsonc +// wrangler.jsonc +{ + "vars": { + "MY_VAR": "production-value" + } +} +``` + +**For secrets**: +```bash +npx wrangler secret put MY_SECRET +``` + +--- + +## 13. Tool Schema Validation Error + +**Error**: `ZodError: Invalid input type` + +**Diagnosis**: Client sends different type than schema expects + +**Solution**: Use Zod transforms or coerce +```typescript +// Client sends string "123", but you need number +{ + count: z.string().transform(val => parseInt(val, 10)) +} + +// Or use coerce +{ + count: z.coerce.number() +} +``` + +--- + +## 14. Multiple Transport Endpoints Conflicting + +**Error**: `/sse` returns 404 after adding `/mcp` + +**Diagnosis**: Path matching issue + +**Wrong**: +```typescript +if (pathname === "/sse") { // ❌ Misses /sse/message + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +**Correct**: +```typescript +if (pathname === "/sse" || pathname.startsWith("/sse/")) { // ✅ + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +--- + +## 15. Local Testing Limitations + +**Error**: OAuth flow fails in `npm run dev` + +**Diagnosis**: Miniflare doesn't support all DO features + +**Solutions**: + +**Option 1**: Use remote dev +```bash +npx wrangler dev --remote +``` + +**Option 2**: Test OAuth on deployed Worker +```bash +npx wrangler deploy +# Test at https://my-mcp.workers.dev +``` + +**Option 3**: Mock OAuth for local testing +```typescript +if (env.ENVIRONMENT === "development") { + // Skip OAuth, use mock user + return { + userId: "test-user", + email: "test@example.com" + }; +} +``` + +--- + +## General Debugging Tips + +### Check Logs +```bash +npx wrangler tail +``` + +### Test with MCP Inspector +```bash +npx @modelcontextprotocol/inspector@latest +``` + +### Verify Bindings +```bash +npx wrangler kv:namespace list +npx wrangler d1 list +``` + +### Check Deployment +```bash +npx wrangler deployments list +``` + +### View Worker Code +```bash +npx wrangler whoami +# Visit dashboard: https://dash.cloudflare.com/ +``` + +--- + +**Still stuck?** Check: +- Cloudflare Docs: https://developers.cloudflare.com/agents/ +- MCP Spec: https://modelcontextprotocol.io/ +- Community: https://community.cloudflare.com/ diff --git a/references/debugging-guide.md b/references/debugging-guide.md new file mode 100644 index 0000000..599ce30 --- /dev/null +++ b/references/debugging-guide.md @@ -0,0 +1,712 @@ +# MCP Server Debugging Guide + +**Troubleshooting connection issues and common errors** + +--- + +## Quick Diagnosis Flowchart + +``` +MCP Connection Failing? + | + v +[1] Can you curl the Worker? + curl https://worker.dev/ + | + NO ──┴─> Worker not deployed + | → Run: npx wrangler deploy + | + YES ──┴─> Continue + | + v +[2] Can you curl the MCP endpoint? + curl https://worker.dev/sse + | + 404 ──┴─> URL path mismatch (most common!) + | → Check: Client URL matches server base path + | → See: "URL Path Mismatch" section below + | + OK ──┴─> Continue + | + v +[3] Did you update config after deployment? + | + NO ──┴─> Update claude_desktop_config.json + | → Use deployed URL (not localhost) + | → Restart Claude Desktop + | + YES ──┴─> Continue + | + v +[4] Check Worker logs + npx wrangler tail + | + v + See errors? → Check "Common Errors" section below +``` + +--- + +## Problem 1: URL Path Mismatch (Most Common!) + +### Symptoms + +- ❌ 404 Not Found +- ❌ Connection failed +- ❌ MCP Inspector shows "Failed to connect" +- ❌ Claude Desktop doesn't show tools + +### Root Cause + +Client URL doesn't match server base path configuration. + +### Debugging Steps + +#### Step 1: Check what base path your server uses + +Look at your `src/index.ts`: + +```typescript +// Option A: Serving at /sse +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(...); + // ↑ Base path is "/sse" +} + +// Option B: Serving at root / +return MyMCP.serveSSE("/").fetch(...); +// ↑ Base path is "/" +``` + +#### Step 2: Test with curl + +```bash +# If base path is /sse: +curl https://YOUR-WORKER.workers.dev/sse + +# If base path is /: +curl https://YOUR-WORKER.workers.dev/ +``` + +**Expected:** JSON response with server info +**Got 404?** Your URL doesn't match the base path + +#### Step 3: Update client config + +Match the curl URL that worked: + +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://YOUR-WORKER.workers.dev/sse" // Must match curl URL! + } + } +} +``` + +#### Step 4: Restart Claude Desktop + +Config only loads at startup: +1. Quit Claude Desktop completely +2. Reopen +3. Check for tools + +### Common Variations + +**Server at `/sse`, client missing `/sse`:** +```typescript +// Server +MyMCP.serveSSE("/sse").fetch(...) + +// Client (wrong) +"url": "https://worker.dev" // ❌ Missing /sse +``` + +**Fix:** +```json +"url": "https://worker.dev/sse" // ✅ Include /sse +``` + +--- + +**Server at `/`, client includes `/sse`:** +```typescript +// Server +MyMCP.serveSSE("/").fetch(...) + +// Client (wrong) +"url": "https://worker.dev/sse" // ❌ Server at root, not /sse +``` + +**Fix:** +```json +"url": "https://worker.dev" // ✅ No /sse +``` + +--- + +## Problem 2: Localhost After Deployment + +### Symptoms + +- ❌ Connection timeout +- ❌ Connection refused +- ❌ Works in dev, fails in production + +### Root Cause + +Client config still using `localhost` URL after deployment. + +### Debugging Steps + +#### Step 1: Check client config + +```bash +cat ~/.config/claude/claude_desktop_config.json +``` + +Look for: +```json +{ + "mcpServers": { + "my-mcp": { + "url": "http://localhost:8788/sse" // ❌ localhost! + } + } +} +``` + +#### Step 2: Get deployed URL + +```bash +npx wrangler deploy + +# Output shows: +# Deployed to: https://my-mcp.YOUR_ACCOUNT.workers.dev +``` + +#### Step 3: Update client config + +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // ✅ Deployed URL + } + } +} +``` + +#### Step 4: Restart Claude Desktop + +--- + +## Problem 3: Worker Not Deployed + +### Symptoms + +- ❌ curl returns connection refused/timeout +- ❌ No response at all + +### Debugging Steps + +#### Step 1: Check deployment status + +```bash +npx wrangler whoami +# Shows: logged in as... + +npx wrangler deployments list +# Shows: recent deployments (or none) +``` + +#### Step 2: Deploy + +```bash +npx wrangler deploy +``` + +#### Step 3: Verify deployment + +```bash +curl https://YOUR-WORKER.workers.dev/ + +# Should return SOMETHING (even 404 means it's running) +``` + +--- + +## Problem 4: OAuth URL Mismatch + +### Symptoms + +- ❌ `OAuth error: redirect_uri does not match` +- ❌ OAuth flow starts but fails at callback +- ❌ Token exchange fails + +### Root Cause + +OAuth URLs don't match deployed URL. + +### Debugging Steps + +#### Step 1: Check ALL three OAuth URLs + +```bash +cat ~/.config/claude/claude_desktop_config.json +``` + +Look for: +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/sse", // ← Check 1 + "auth": { + "type": "oauth", + "authorizationUrl": "https://worker.dev/authorize", // ← Check 2 + "tokenUrl": "https://worker.dev/token" // ← Check 3 + } + } + } +} +``` + +#### Step 2: Verify ALL URLs match + +**Must all use same:** +- Protocol: `https://` (not mixed http/https) +- Domain: Same Workers domain +- No typos: `authorize` not `auth`, `token` not `tokens` + +#### Step 3: Test each endpoint + +```bash +curl https://worker.dev/sse # Main endpoint +curl https://worker.dev/authorize # OAuth authorize (should show HTML) +curl https://worker.dev/token # Token endpoint +``` + +--- + +## Problem 5: CORS Errors + +### Symptoms + +- ❌ `Access to fetch at '...' blocked by CORS policy` +- ❌ `Method Not Allowed` for OPTIONS requests +- ❌ Works in curl, fails in browser + +### Root Cause + +Missing CORS headers or OPTIONS handler. + +### Debugging Steps + +#### Step 1: Test with browser + +Open browser console and try: +```javascript +fetch('https://worker.dev/sse') +``` + +#### Step 2: Check OPTIONS handler + +Your Worker should handle OPTIONS: +```typescript +if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); +} +``` + +#### Step 3: Test OPTIONS request + +```bash +curl -X OPTIONS https://worker.dev/sse -v +``` + +**Expected:** 204 No Content with CORS headers +**Got:** 405 Method Not Allowed → Add OPTIONS handler + +--- + +## Problem 6: Environment Variables Missing + +### Symptoms + +- ❌ `TypeError: env.API_KEY is undefined` +- ❌ Tools return empty data +- ❌ Silent failures (no error, but wrong results) + +### Debugging Steps + +#### Step 1: Check local development + +```bash +# Check .dev.vars exists +cat .dev.vars + +# Should have: +API_KEY=dev-key-123 +DATABASE_URL=http://localhost:3000 +``` + +#### Step 2: Check production config + +```bash +# Check wrangler.jsonc +cat wrangler.jsonc +``` + +**Public vars:** +```jsonc +{ + "vars": { + "ENVIRONMENT": "production", + "LOG_LEVEL": "info" + } +} +``` + +**Secrets:** +```bash +# List secrets +npx wrangler secret list + +# Add missing secret +npx wrangler secret put API_KEY +``` + +#### Step 3: Add validation + +In your `init()` method: +```typescript +async init() { + // Validate required env vars + if (!this.env.API_KEY) { + throw new Error("API_KEY not configured"); + } + + // Continue... +} +``` + +--- + +## Problem 7: Durable Objects Not Working + +### Symptoms + +- ❌ `TypeError: Cannot read properties of undefined (reading 'idFromName')` +- ❌ State not persisting +- ❌ `Durable Object class MyMCP has no migration defined` + +### Debugging Steps + +#### Step 1: Check binding + +```bash +cat wrangler.jsonc +``` + +**Must have:** +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "name": "MY_MCP", + "class_name": "MyMCP", + "script_name": "my-mcp-server" + } + ] + } +} +``` + +#### Step 2: Check migration + +```jsonc +{ + "migrations": [ + { "tag": "v1", "new_classes": ["MyMCP"] } + ] +} +``` + +#### Step 3: Deploy with migration + +```bash +npx wrangler deploy +``` + +First deployment requires migration! + +--- + +## Checking Worker Logs + +### Real-time logs + +```bash +npx wrangler tail +``` + +**Shows:** +- All console.log() output +- Errors with stack traces +- Request/response info + +### Filtering logs + +```bash +# Only errors +npx wrangler tail --format=json | jq 'select(.level=="error")' + +# Only specific message +npx wrangler tail | grep "API_KEY" +``` + +--- + +## Common Error Messages + +### Error: "404 Not Found" + +**Cause:** URL path mismatch (see Problem 1) + +**Fix:** +1. Check server base path +2. Update client URL to match +3. Restart Claude Desktop + +--- + +### Error: "Connection refused" / "ECONNREFUSED" + +**Cause:** Worker not deployed or wrong URL + +**Fix:** +1. Deploy: `npx wrangler deploy` +2. Update client config with deployed URL +3. Restart Claude Desktop + +--- + +### Error: "OAuth error: redirect_uri does not match" + +**Cause:** OAuth URLs don't match deployed domain + +**Fix:** +1. Update ALL three OAuth URLs in client config +2. Use same domain and protocol for all +3. Restart Claude Desktop + +--- + +### Error: "TypeError: env.BINDING is undefined" + +**Cause:** Missing binding in wrangler.jsonc + +**Fix:** +1. Add binding to wrangler.jsonc +2. Deploy: `npx wrangler deploy` +3. Restart + +--- + +### Error: "Access to fetch blocked by CORS policy" + +**Cause:** Missing CORS headers or OPTIONS handler + +**Fix:** +1. Add OPTIONS handler (see Problem 5) +2. Deploy +3. Test in browser + +--- + +### Error: "ZodError: Invalid input type" + +**Cause:** Client sends wrong data type for parameter + +**Fix:** +```typescript +// Use Zod transform +param: z.string().transform(val => parseInt(val, 10)) +``` + +--- + +## Testing Checklist + +Before declaring "it works": + +- [ ] Worker deployed: `npx wrangler deploy` succeeded +- [ ] Worker running: `curl https://worker.dev/` returns something +- [ ] MCP endpoint: `curl https://worker.dev/sse` returns server info +- [ ] Client config updated with deployed URL +- [ ] Client config URL matches curl test +- [ ] Claude Desktop restarted +- [ ] Tools visible in Claude Desktop +- [ ] Test tool call succeeds +- [ ] Worker logs clean: `npx wrangler tail` shows no errors +- [ ] (OAuth) All three URLs match +- [ ] (DO) Bindings configured +- [ ] (Secrets) Environment variables set + +--- + +## Advanced Debugging + +### Enable verbose logging + +```typescript +export class MyMCP extends McpAgent { + async init() { + console.log("MyMCP initializing..."); + console.log("Environment:", { + hasAPIKey: !!this.env.API_KEY, + hasDB: !!this.env.DB, + }); + + this.server.tool( + "test", + "Test tool", + { msg: z.string() }, + async ({ msg }) => { + console.log("Tool called with:", msg); + return { content: [{ type: "text", text: `Echo: ${msg}` }] }; + } + ); + } +} +``` + +**View logs:** +```bash +npx wrangler tail +``` + +--- + +### Test MCP protocol directly + +Use MCP Inspector for protocol-level debugging: + +```bash +npx @modelcontextprotocol/inspector@latest +``` + +1. Open http://localhost:5173 +2. Enter Worker URL +3. Click "Connect" +4. Try "List Tools" +5. Inspect request/response + +**Benefits:** +- See exact JSON-RPC messages +- Test individual tool calls +- Verify protocol compliance + +--- + +### Check Cloudflare dashboard + +1. Visit https://dash.cloudflare.com/ +2. Go to Workers & Pages +3. Find your Worker +4. Check: + - Deployment status + - Recent logs + - Analytics + +--- + +## Prevention + +### Add health check endpoint + +```typescript +if (pathname === "/" || pathname === "/health") { + return new Response(JSON.stringify({ + name: "My MCP Server", + version: "1.0.0", + transports: { sse: "/sse", http: "/mcp" }, + status: "ok", + timestamp: new Date().toISOString(), + })); +} +``` + +**Test:** `curl https://worker.dev/health` + +--- + +### Add startup validation + +```typescript +async init() { + // Validate environment + if (!this.env.API_KEY) { + throw new Error("API_KEY not configured"); + } + + // Log successful initialization + console.log("MCP server initialized successfully"); +} +``` + +--- + +### Use descriptive 404 messages + +```typescript +return new Response(JSON.stringify({ + error: "Not Found", + requestedPath: pathname, + availablePaths: ["/sse", "/mcp", "/health"], + hint: "Client URL must include base path", + example: "https://worker.dev/sse" +}), { status: 404 }); +``` + +--- + +## Summary + +**Most common issues (in order):** + +1. **URL path mismatch** (80% of problems) + - Fix: Match client URL to server base path + +2. **Localhost after deployment** (10%) + - Fix: Update config with deployed URL + +3. **OAuth URL mismatch** (5%) + - Fix: Update ALL three OAuth URLs + +4. **Missing environment variables** (3%) + - Fix: Add to .dev.vars or wrangler secrets + +5. **Other** (2%) + - Check Worker logs: `npx wrangler tail` + +**Golden debugging workflow:** +```bash +1. curl https://worker.dev/ # Worker running? +2. curl https://worker.dev/sse # MCP endpoint works? +3. Check client config matches URL # Config correct? +4. Restart Claude Desktop # Reloaded config? +5. npx wrangler tail # Any errors? +``` + +**Remember:** 80% of MCP connection issues are URL path mismatches. Always start there! diff --git a/references/http-transport-fundamentals.md b/references/http-transport-fundamentals.md new file mode 100644 index 0000000..6831961 --- /dev/null +++ b/references/http-transport-fundamentals.md @@ -0,0 +1,506 @@ +# HTTP Transport Fundamentals + +**Deep dive on URL paths and routing for Cloudflare MCP servers** + +This document explains how URL path configuration works in MCP servers and why mismatches are the #1 cause of connection failures. + +--- + +## The Problem + +**Most common MCP server connection error:** +``` +❌ 404 Not Found +❌ Connection failed +❌ MCP Inspector shows "Failed to connect" +``` + +**Root cause:** Client URL doesn't match server base path configuration + +--- + +## How Base Paths Work + +### The Core Concept + +When you call `MyMCP.serveSSE("/sse")`, you're telling the MCP server: + +> "All MCP endpoints are available under the `/sse` base path" + +This means: +- Initial connection: `https://worker.dev/sse` +- List tools: `https://worker.dev/sse/tools/list` +- Call tool: `https://worker.dev/sse/tools/call` +- List resources: `https://worker.dev/sse/resources/list` + +**The base path is prepended to ALL MCP-specific endpoints automatically.** + +--- + +## Example 1: Serving at `/sse` + +**Server code:** +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + // ↑ Base path is "/sse" + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**Client configuration:** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse" + // ↑ Must include /sse + } + } +} +``` + +**What happens:** +1. Client connects to: `https://my-mcp.workers.dev/sse` +2. Worker receives request with `pathname = "/sse"` +3. Check: `pathname.startsWith("/sse")` → TRUE ✅ +4. MCP server handles request +5. Tools available at: + - `/sse/tools/list` + - `/sse/tools/call` + - etc. + +--- + +## Example 2: Serving at `/` (root) + +**Server code:** +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + return MyMCP.serveSSE("/").fetch(request, env, ctx); + // ↑ Base path is "/" (root) + } +}; +``` + +**Client configuration:** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev" + // ↑ No /sse! + } + } +} +``` + +**What happens:** +1. Client connects to: `https://my-mcp.workers.dev` +2. Worker receives request with `pathname = "/"` +3. MCP server handles request at root +4. Tools available at: + - `/tools/list` + - `/tools/call` + - etc. (no /sse prefix) + +--- + +## Example 3: Custom base path + +**Server code:** +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/api/mcp")) { + return MyMCP.serveSSE("/api/mcp").fetch(request, env, ctx); + // ↑ Base path is "/api/mcp" + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**Client configuration:** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/api/mcp" + // ↑ Must match base path exactly + } + } +} +``` + +--- + +## Why `pathname.startsWith()` is Critical + +**❌ WRONG: Using exact match** +```typescript +if (pathname === "/sse") { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +**Problem:** This ONLY matches `/sse` exactly +- `/sse` → ✅ Matches +- `/sse/tools/list` → ❌ Doesn't match! 404! +- `/sse/tools/call` → ❌ Doesn't match! 404! + +**✅ CORRECT: Using `startsWith()`** +```typescript +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +**Result:** This matches ALL paths under `/sse` +- `/sse` → ✅ Matches +- `/sse/tools/list` → ✅ Matches +- `/sse/tools/call` → ✅ Matches +- `/sse/resources/list` → ✅ Matches + +--- + +## Request/Response Lifecycle + +Let's trace a complete MCP request from start to finish. + +### Step 1: Client Connection + +Client initiates connection: +``` +POST https://my-mcp.workers.dev/sse +``` + +### Step 2: Worker Receives Request + +Worker `fetch()` handler is called: +```typescript +const { pathname } = new URL(request.url); +console.log(pathname); // "/sse" +``` + +### Step 3: Path Matching + +Worker checks if path matches: +```typescript +if (pathname.startsWith("/sse")) { // TRUE! + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +### Step 4: MCP Server Handles Request + +MCP Agent processes the request and returns available endpoints. + +### Step 5: Client Lists Tools + +Client makes follow-up request: +``` +POST https://my-mcp.workers.dev/sse/tools/list +``` + +Worker receives: +```typescript +const { pathname } = new URL(request.url); +console.log(pathname); // "/sse/tools/list" + +if (pathname.startsWith("/sse")) { // TRUE! Matches because of startsWith() + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +MCP server sees `/tools/list` (after stripping `/sse` base path) and returns tool list. + +--- + +## Common Mistakes and Fixes + +### Mistake 1: Missing Base Path in Client URL + +**Server:** +```typescript +MyMCP.serveSSE("/sse").fetch(...) +``` + +**Client:** +```json +"url": "https://worker.dev" // ❌ Missing /sse +``` + +**Result:** 404 Not Found + +**Fix:** +```json +"url": "https://worker.dev/sse" // ✅ Include /sse +``` + +--- + +### Mistake 2: Wrong Base Path + +**Server:** +```typescript +if (pathname.startsWith("/api")) { + return MyMCP.serveSSE("/api").fetch(...); +} +``` + +**Client:** +```json +"url": "https://worker.dev/sse" // ❌ Server expects /api +``` + +**Result:** 404 Not Found + +**Fix:** +```json +"url": "https://worker.dev/api" // ✅ Match server path +``` + +--- + +### Mistake 3: Localhost After Deployment + +**Development:** +```json +"url": "http://localhost:8788/sse" // ✅ Works in dev +``` + +**After deployment** (forgot to update): +```json +"url": "http://localhost:8788/sse" // ❌ Worker is deployed! +``` + +**Result:** Connection refused / timeout + +**Fix:** +```json +"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // ✅ Deployed URL +``` + +--- + +### Mistake 4: Using Exact Match Instead of `startsWith()` + +**Server:** +```typescript +if (pathname === "/sse") { // ❌ Only matches /sse exactly + return MyMCP.serveSSE("/sse").fetch(...); +} +``` + +**Result:** +- `/sse` → ✅ Works (initial connection) +- `/sse/tools/list` → ❌ 404 (tool listing fails) + +**Fix:** +```typescript +if (pathname.startsWith("/sse")) { // ✅ Matches all sub-paths + return MyMCP.serveSSE("/sse").fetch(...); +} +``` + +--- + +## Debugging Workflow + +When MCP connection fails: + +### Step 1: Check Worker is Running + +```bash +curl https://YOUR-WORKER.workers.dev/ +``` + +**Expected:** Some response (even 404 is OK - means Worker is running) +**Problem:** Timeout or connection refused → Worker not deployed + +### Step 2: Test MCP Endpoint + +```bash +curl https://YOUR-WORKER.workers.dev/sse +``` + +**Expected:** JSON response with MCP server info +**Problem:** 404 → Client URL doesn't match server base path + +### Step 3: Verify Client Config + +Check `~/.config/claude/claude_desktop_config.json`: +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://YOUR-WORKER.workers.dev/sse" + } + } +} +``` + +**Verify:** +- [ ] URL matches deployed Worker URL +- [ ] URL includes base path (e.g., `/sse`) +- [ ] No typos in domain or path +- [ ] Using `https://` (not `http://`) + +### Step 4: Restart Claude Desktop + +Config changes require restart: +1. Quit Claude Desktop completely +2. Reopen Claude Desktop +3. Check for MCP server in tools list + +--- + +## Multiple Transports + +You can serve multiple transports at different paths: + +**Server:** +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + // SSE at /sse + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + // HTTP at /mcp + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + // Health check + if (pathname === "/" || pathname === "/health") { + return new Response(JSON.stringify({ + transports: { + sse: "/sse", + http: "/mcp" + } + })); + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**Clients can choose:** +- SSE: `"url": "https://worker.dev/sse"` +- HTTP: `"url": "https://worker.dev/mcp"` + +**Why this works:** +- `/sse` and `/mcp` don't conflict +- Each transport has isolated namespace +- Health check available at root `/` + +--- + +## Best Practices + +### 1. Always Use `startsWith()` for Path Matching + +```typescript +// ✅ CORRECT +if (pathname.startsWith("/sse")) { ... } + +// ❌ WRONG +if (pathname === "/sse") { ... } +``` + +### 2. Add Health Check Endpoint + +```typescript +if (pathname === "/" || pathname === "/health") { + return new Response(JSON.stringify({ + name: "My MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + http: "/mcp" + }, + status: "ok" + })); +} +``` + +**Benefits:** +- Quickly verify Worker is running +- Discover available transports +- Debug connection issues + +### 3. Use Descriptive 404 Messages + +```typescript +return new Response(JSON.stringify({ + error: "Not Found", + requestedPath: pathname, + availablePaths: ["/sse", "/mcp", "/health"], + hint: "Client URL must include base path (e.g., /sse)" +}), { status: 404 }); +``` + +### 4. Test After Every Deployment + +```bash +# Deploy +npx wrangler deploy + +# Test immediately +curl https://YOUR-WORKER.workers.dev/sse + +# Update client config +# Restart Claude Desktop +``` + +### 5. Document Base Path in Comments + +```typescript +// SSE transport at /sse +// Client URL MUST be: https://worker.dev/sse +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +``` + +--- + +## Summary + +**Key Takeaways:** + +1. **Base path in `serveSSE()` determines client URL** + - `serveSSE("/sse")` → Client uses `https://worker.dev/sse` + - `serveSSE("/")` → Client uses `https://worker.dev` + +2. **Always use `pathname.startsWith()` for matching** + - Matches sub-paths like `/sse/tools/list` + +3. **Test with curl after deployment** + - `curl https://worker.dev/sse` should return server info + +4. **Update client config after every deployment** + - Development: `http://localhost:8788/sse` + - Production: `https://worker.workers.dev/sse` + +5. **Restart Claude Desktop after config changes** + - Config only loaded at startup + +**Remember:** The #1 MCP connection failure is URL path mismatch. Always verify your base paths match! diff --git a/references/oauth-providers.md b/references/oauth-providers.md new file mode 100644 index 0000000..0ae2a7f --- /dev/null +++ b/references/oauth-providers.md @@ -0,0 +1,190 @@ +# OAuth Provider Setup Guides + +Quick setup guides for common OAuth providers with Cloudflare MCP servers. + +--- + +## GitHub + +### 1. Create OAuth App +1. Go to https://github.com/settings/developers +2. Click "New OAuth App" +3. Fill in: + - **Application name**: My MCP Server + - **Homepage URL**: https://my-mcp.workers.dev + - **Authorization callback URL**: https://my-mcp.workers.dev/oauth/callback +4. Click "Register application" +5. Copy Client ID and Client Secret + +### 2. Configure Worker +```typescript +import { GitHubHandler } from "@cloudflare/workers-oauth-provider"; + +defaultHandler: new GitHubHandler({ + clientId: (env) => env.GITHUB_CLIENT_ID, + clientSecret: (env) => env.GITHUB_CLIENT_SECRET, + scopes: ["repo", "user:email"], +}) +``` + +### 3. Add Secrets +```bash +npx wrangler secret put GITHUB_CLIENT_ID +npx wrangler secret put GITHUB_CLIENT_SECRET +``` + +### Common Scopes +- `repo` - Full repo access +- `user:email` - Read user email +- `read:org` - Read org membership +- `write:org` - Manage org +- `admin:repo_hook` - Manage webhooks + +--- + +## Google + +### 1. Create OAuth Client +1. Go to https://console.cloud.google.com/apis/credentials +2. Click "Create Credentials" → "OAuth client ID" +3. Application type: "Web application" +4. Authorized redirect URIs: https://my-mcp.workers.dev/oauth/callback +5. Click "Create" +6. Copy Client ID and Client Secret + +### 2. Configure Worker +```typescript +import { GoogleHandler } from "@cloudflare/workers-oauth-provider"; + +defaultHandler: new GoogleHandler({ + clientId: (env) => env.GOOGLE_CLIENT_ID, + clientSecret: (env) => env.GOOGLE_CLIENT_SECRET, + scopes: ["openid", "email", "profile"], +}) +``` + +### Common Scopes +- `openid` - Required for OpenID Connect +- `email` - User email +- `profile` - Basic profile +- `https://www.googleapis.com/auth/drive.readonly` - Read Drive files +- `https://www.googleapis.com/auth/gmail.readonly` - Read Gmail + +--- + +## Azure AD + +### 1. Register Application +1. Go to https://portal.azure.com → Azure Active Directory +2. App registrations → New registration +3. Name: My MCP Server +4. Redirect URI: https://my-mcp.workers.dev/oauth/callback +5. Click "Register" +6. Copy Application (client) ID +7. Certificates & secrets → New client secret +8. Copy secret value + +### 2. Configure Worker +```typescript +import { AzureADHandler } from "@cloudflare/workers-oauth-provider"; + +defaultHandler: new AzureADHandler({ + clientId: (env) => env.AZURE_CLIENT_ID, + clientSecret: (env) => env.AZURE_CLIENT_SECRET, + tenant: "common", // or specific tenant ID + scopes: ["openid", "email", "User.Read"], +}) +``` + +### Common Scopes +- `openid` - Required +- `email` - User email +- `User.Read` - Read user profile +- `Files.Read` - Read OneDrive files +- `Mail.Read` - Read email + +--- + +## Generic OAuth Provider + +### For any OAuth 2.1 provider +```typescript +import { GenericOAuthHandler } from "@cloudflare/workers-oauth-provider"; + +defaultHandler: new GenericOAuthHandler({ + authorizeUrl: "https://provider.com/oauth/authorize", + tokenUrl: "https://provider.com/oauth/token", + userInfoUrl: "https://provider.com/oauth/userinfo", + + clientId: (env) => env.OAUTH_CLIENT_ID, + clientSecret: (env) => env.OAUTH_CLIENT_SECRET, + scopes: ["openid", "email"], + + context: async (accessToken) => { + const response = await fetch("https://provider.com/oauth/userinfo", { + headers: { Authorization: `Bearer ${accessToken}` } + }); + const user = await response.json(); + + return { + userId: user.id, + email: user.email, + accessToken + }; + } +}) +``` + +--- + +## Dynamic Client Registration + +Skip manual OAuth app creation - let clients register automatically: + +```typescript +export default new OAuthProvider({ + allowDynamicClientRegistration: true, + // No clientId or clientSecret needed! +}) +``` + +**How it works**: +1. Client sends registration request +2. Server generates client credentials +3. Stored in KV namespace +4. Client uses credentials for OAuth flow + +**Pros**: +✅ No manual setup +✅ Works immediately +✅ No provider configuration + +**Cons**: +❌ Less control +❌ Can't track clients externally + +--- + +## Security Best Practices + +### Scopes +✅ Request minimal scopes needed +❌ Don't request `admin` or `delete` unless necessary + +### Secrets +✅ Use `npx wrangler secret put` +❌ Never commit secrets to git +❌ Never put secrets in wrangler.jsonc + +### Redirect URIs +✅ Use HTTPS in production +✅ Specify exact URI (not wildcard) +❌ Don't use localhost in production + +### Consent Screen +✅ Always enable in production: `allowConsentScreen: true` +❌ Never disable consent screen for public apps + +--- + +**Need help?** See `authentication.md` for full OAuth patterns. diff --git a/references/official-examples.md b/references/official-examples.md new file mode 100644 index 0000000..a55cd95 --- /dev/null +++ b/references/official-examples.md @@ -0,0 +1,170 @@ +# Official Cloudflare MCP Server Examples + +Curated list of official Cloudflare MCP servers and templates. + +--- + +## Cloudflare AI Demos Repository + +**Main Repository**: https://github.com/cloudflare/ai/tree/main/demos + +### Basic Templates + +**remote-mcp-authless** +- No authentication +- Basic tools example +- SSE + HTTP transports +- **Use for**: Quick start, internal tools +- **Template**: `npm create cloudflare@latest -- my-mcp --template=cloudflare/ai/demos/remote-mcp-authless` + +**remote-mcp-server** +- Base remote MCP server +- Minimal setup +- **Use for**: Foundation for custom servers + +--- + +### OAuth Integration Examples + +**remote-mcp-github-oauth** +- GitHub OAuth integration +- workers-oauth-provider +- User-scoped GitHub API tools +- **Use for**: GitHub integrations +- **Template**: `npm create cloudflare@latest -- my-mcp --template=cloudflare/ai/demos/remote-mcp-github-oauth` + +**remote-mcp-google-oauth** +- Google OAuth integration +- Google APIs access +- **Use for**: Google Workspace integrations + +**remote-mcp-auth0** +- Auth0 integration +- Enterprise auth +- **Use for**: Enterprise applications + +**remote-mcp-authkit** +- WorkOS AuthKit +- Full OAuth provider example +- **Use for**: Custom OAuth implementation + +**remote-mcp-logto** +- Logto identity platform +- **Use for**: Open-source identity provider + +**remote-mcp-descope-auth** +- Descope authentication +- **Use for**: No-code auth platform + +--- + +### Specialized MCP Servers + +**remote-mcp-server-autorag** +- AutoRAG integration +- AI-powered search +- **Use for**: RAG applications + +**remote-mcp-cf-access** +- Cloudflare Access protection +- Zero Trust security +- **Use for**: Corporate networks + +**mcp-slack-oauth** +- Slack integration +- Slack OAuth flow +- **Use for**: Slack bots and apps + +**mcp-stytch-b2b-okr-manager** +- Stytch B2B auth +- OKR management example +- **Use for**: B2B SaaS applications + +**mcp-stytch-consumer-todo-list** +- Stytch consumer auth +- Todo list example +- **Use for**: Consumer applications + +--- + +## Production MCP Servers + +### Cloudflare's Official MCP Servers + +**mcp-server-cloudflare** +- Repository: https://github.com/cloudflare/mcp-server-cloudflare +- **13 MCP servers** for Cloudflare services +- Blog: https://blog.cloudflare.com/thirteen-new-mcp-servers-from-cloudflare/ + +**Servers included**: +1. **Workers Bindings** - Manage D1, KV, R2, etc. +2. **Documentation Access** - Search Cloudflare docs +3. **Logpush Analytics** - Query logs +4. **AI Gateway Logs** - AI request logs +5. **Audit Logs** - Account activity +6. **DNS Analytics** - DNS query stats +7. **Browser Rendering** - Puppeteer automation +8. And 6 more... + +**workers-mcp** +- Repository: https://github.com/cloudflare/workers-mcp +- CLI tool to connect Workers to Claude Desktop +- **Use for**: Existing Workers → MCP integration + +--- + +## How to Use Templates + +### Deploy via CLI +```bash +npm create cloudflare@latest -- my-mcp-server \ + --template=cloudflare/ai/demos/TEMPLATE_NAME +``` + +### Deploy via Button +Visit demo URL and click "Deploy to Cloudflare" button. + +### Clone and Modify +```bash +git clone https://github.com/cloudflare/ai +cd ai/demos/TEMPLATE_NAME +npm install +npm run dev +``` + +--- + +## Documentation Links + +**Cloudflare Agents** +- Main docs: https://developers.cloudflare.com/agents/ +- MCP Guide: https://developers.cloudflare.com/agents/model-context-protocol/ +- Build Remote MCP: https://developers.cloudflare.com/agents/guides/remote-mcp-server/ +- Test Remote MCP: https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/ + +**Blog Posts** +- Remote MCP Launch: https://blog.cloudflare.com/remote-model-context-protocol-servers-mcp/ +- 13 MCP Servers: https://blog.cloudflare.com/thirteen-new-mcp-servers-from-cloudflare/ +- Building Agents: https://blog.cloudflare.com/building-ai-agents-with-mcp-authn-authz-and-durable-objects/ + +**Third-Party Examples** +- Stytch MCP Guide: https://stytch.com/blog/building-an-mcp-server-oauth-cloudflare-workers/ +- Auth0 MCP Guide: https://auth0.com/blog/secure-and-deploy-remote-mcp-servers-with-auth0-and-cloudflare/ + +--- + +## Community Examples + +Search GitHub for more: +- https://github.com/topics/cloudflare-mcp +- https://github.com/topics/mcp-server + +**Notable community servers**: +- Tennis court booking +- Google Calendar integration +- ChatGPT apps +- Strava integration + +--- + +**Last Updated**: 2025-11-04 diff --git a/references/transport-comparison.md b/references/transport-comparison.md new file mode 100644 index 0000000..4a9f6c3 --- /dev/null +++ b/references/transport-comparison.md @@ -0,0 +1,439 @@ +# MCP Transport Comparison: SSE vs Streamable HTTP + +**Detailed comparison of MCP transport methods for Cloudflare Workers** + +--- + +## Overview + +MCP supports two transport methods: +1. **SSE (Server-Sent Events)** - Legacy standard (2024) +2. **Streamable HTTP** - New standard (2025+) + +Both work on Cloudflare Workers. You can (and should) support both for maximum compatibility. + +--- + +## SSE (Server-Sent Events) + +### What It Is + +SSE is a W3C standard for server-to-client streaming over HTTP. The server holds the connection open and pushes events as they occur. + +**Technical Details:** +- Protocol: HTTP/1.1 or HTTP/2 +- Content-Type: `text/event-stream` +- Connection: Long-lived, unidirectional (server → client) +- Format: Plain text with `data:`, `event:`, `id:` fields + +### Code Example + +**Server:** +```typescript +MyMCP.serveSSE("/sse").fetch(request, env, ctx) +``` + +**Client config:** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/sse" + } + } +} +``` + +### Pros + +✅ **Wide compatibility** - Supported by all MCP clients (2024+) +✅ **Well-documented** - Lots of examples and tooling +✅ **Easy debugging** - Plain text format, human-readable +✅ **Works with proxies** - Most HTTP proxies support SSE +✅ **Battle-tested** - Used in production for years + +### Cons + +❌ **Less efficient** - Overhead from text encoding +❌ **Being deprecated** - MCP is moving to Streamable HTTP +❌ **Unidirectional** - Server can push, but client uses separate requests +❌ **Text-only** - Binary data must be base64-encoded +❌ **Connection limits** - Browsers limit SSE connections per domain + +### When to Use + +- **2024-2025 transition period** - Maximum compatibility +- **Debugging** - Easier to inspect traffic +- **Legacy clients** - Older MCP implementations +- **Development** - Simpler to test with curl/MCP Inspector + +--- + +## Streamable HTTP + +### What It Is + +Streamable HTTP is a modern standard for bidirectional streaming using HTTP/2+ streams. More efficient than SSE. + +**Technical Details:** +- Protocol: HTTP/2 or HTTP/3 +- Content-Type: `application/json` (streaming) +- Connection: Bidirectional (client ↔ server) +- Format: NDJSON (newline-delimited JSON) + +### Code Example + +**Server:** +```typescript +MyMCP.serve("/mcp").fetch(request, env, ctx) +``` + +**Client config:** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/mcp" + } + } +} +``` + +### Pros + +✅ **More efficient** - Binary-safe, less overhead +✅ **2025 standard** - MCP's future default +✅ **Bidirectional** - Full-duplex communication +✅ **Better streaming** - Natively supports streaming responses +✅ **HTTP/2 multiplexing** - Multiple streams over one connection + +### Cons + +❌ **Newer clients only** - Not all 2024 clients support it +❌ **Less tooling** - Fewer debugging tools than SSE +❌ **HTTP/2 required** - Cloudflare Workers support this automatically +❌ **More complex** - Harder to debug than plain text SSE + +### When to Use + +- **2025+** - Future-proof your implementation +- **Performance-critical** - High-throughput or low-latency needs +- **Modern clients** - Latest Claude Desktop, MCP Inspector +- **Production** - When paired with SSE fallback + +--- + +## Side-by-Side Comparison + +| Feature | SSE | Streamable HTTP | +|---------|-----|-----------------| +| **Protocol** | HTTP/1.1+ | HTTP/2+ | +| **Direction** | Unidirectional | Bidirectional | +| **Format** | Text (`text/event-stream`) | NDJSON | +| **Efficiency** | Lower | Higher | +| **Compatibility** | All MCP clients | 2025+ clients | +| **Debugging** | Easy (plain text) | Moderate (JSON) | +| **Binary data** | base64-encoded | Native support | +| **Cloudflare cost** | Standard | Standard (no difference) | +| **MCP Standard** | Legacy (2024) | Current (2025+) | + +--- + +## Supporting Both Transports + +**Best practice:** Serve both for maximum compatibility during 2024-2025 transition. + +### Implementation + +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + // SSE transport (legacy) + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + // HTTP transport (2025 standard) + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + // Health check showing available transports + if (pathname === "/" || pathname === "/health") { + return new Response(JSON.stringify({ + name: "My MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", // For legacy clients + http: "/mcp" // For modern clients + } + })); + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +### Client Configuration + +Clients can choose which transport to use: + +**Legacy client (SSE):** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/sse" + } + } +} +``` + +**Modern client (HTTP):** +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://worker.dev/mcp" + } + } +} +``` + +--- + +## Performance Comparison + +### Latency + +**SSE:** +- Initial connection: ~100-200ms +- Tool call: ~50-100ms +- Streaming response: Good (text-based) + +**Streamable HTTP:** +- Initial connection: ~100-200ms (similar) +- Tool call: ~40-80ms (slightly faster) +- Streaming response: Excellent (binary-safe) + +**Verdict:** Streamable HTTP is marginally faster, but difference is negligible for most use cases. + +--- + +### Bandwidth + +**Example: 1KB text response** + +**SSE:** +``` +data: {"content":[{"type":"text","text":"Hello"}]}\n\n +``` +- Overhead: `data: ` prefix, double newlines +- Total: ~1.05KB + +**Streamable HTTP:** +```json +{"content":[{"type":"text","text":"Hello"}]} +``` +- Overhead: None (pure JSON) +- Total: ~1.00KB + +**Verdict:** Streamable HTTP is 5-10% more efficient (text) and much better for binary data. + +--- + +### Connection Limits + +**SSE:** +- Browsers: 6 connections per domain (HTTP/1.1) +- Not an issue for CLI/Desktop clients + +**Streamable HTTP:** +- HTTP/2 multiplexing: Effectively unlimited +- Multiple streams over single connection + +**Verdict:** Streamable HTTP scales better for multi-connection scenarios. + +--- + +## Migration Path + +### 2024 (Now) + +**Recommendation:** Support both transports +- SSE for wide compatibility +- HTTP for future-proofing + +**Code:** +```typescript +// Support both +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(...); +} +if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(...); +} +``` + +--- + +### 2025 (Future) + +**Recommendation:** Deprecate SSE, keep as fallback +- HTTP as primary +- SSE for legacy clients only + +**Code:** +```typescript +// Prefer HTTP +if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(...); +} +// Legacy SSE fallback +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(...); +} +``` + +--- + +### 2026+ (Long-term) + +**Recommendation:** HTTP only +- Remove SSE support +- All clients updated to HTTP + +**Code:** +```typescript +// HTTP only +if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(...); +} +``` + +--- + +## Cloudflare Workers Considerations + +### Cost + +**Both transports cost the same** on Cloudflare Workers: +- Charged per request +- Charged per CPU time +- No difference in pricing + +### Performance + +**Cloudflare Workers natively support both:** +- SSE: Works perfectly (HTTP/1.1 and HTTP/2) +- HTTP/2: Automatic (no configuration needed) +- Streaming: Both transports can stream responses + +### Limits + +**No transport-specific limits:** +- Request size: 100MB (both) +- CPU time: 50ms-30s depending on plan (both) +- Concurrent requests: Unlimited (both) + +--- + +## Debugging + +### SSE Debugging + +**curl:** +```bash +curl -N https://worker.dev/sse +``` + +**Output:** +``` +data: {"jsonrpc":"2.0","method":"initialize",...} + +data: {"jsonrpc":"2.0","method":"tools/list",...} +``` + +**Human-readable:** ✅ Easy to inspect + +--- + +### HTTP Debugging + +**curl:** +```bash +curl https://worker.dev/mcp -H "Content-Type: application/json" -d '{...}' +``` + +**Output:** +```json +{"jsonrpc":"2.0","method":"initialize",...} +{"jsonrpc":"2.0","method":"tools/list",...} +``` + +**Human-readable:** ✅ Still readable (NDJSON) + +--- + +## When to Choose One Over the Other + +### Choose SSE When: + +- Supporting 2024 MCP clients +- Debugging connection issues (easier to inspect) +- Working with legacy systems +- Need maximum compatibility +- Developing/testing with basic tools + +### Choose Streamable HTTP When: + +- Building for 2025+ +- Performance matters (high-throughput) +- Using modern clients (latest Claude Desktop) +- Want future-proof implementation +- Need bidirectional streaming + +### Support Both When: + +- In production (2024-2025 transition) +- Serving diverse clients +- Want maximum compatibility +- No cost difference (same Worker code) + +--- + +## Summary + +**Current Recommendation (2024-2025):** +```typescript +// Support BOTH transports +if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); +} +if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); +} +``` + +**Why:** +- SSE: Maximum compatibility +- HTTP: Future-proof +- No cost difference +- Clients choose what they support + +**Future (2026+):** +- HTTP will be the only standard +- SSE will be deprecated +- But for now, support both! + +--- + +## Additional Resources + +- **MCP Specification**: https://modelcontextprotocol.io/ +- **SSE Spec (W3C)**: https://html.spec.whatwg.org/multipage/server-sent-events.html +- **HTTP/2 Spec (RFC 7540)**: https://httpwg.org/specs/rfc7540.html +- **Cloudflare Workers**: https://developers.cloudflare.com/workers/ diff --git a/references/transport.md b/references/transport.md new file mode 100644 index 0000000..8d6ccea --- /dev/null +++ b/references/transport.md @@ -0,0 +1,150 @@ +# MCP Transport Methods - SSE vs Streamable HTTP + +Comparison of the two MCP transport protocols supported by Cloudflare. + +--- + +## Quick Comparison + +| Feature | SSE | Streamable HTTP | +|---------|-----|-----------------| +| **Status** | Legacy | Current (2025) | +| **Efficiency** | Lower | Higher | +| **Adoption** | High (all clients) | Low (new clients) | +| **Endpoint** | `/sse` | `/mcp` | +| **Method** | `serveSSE()` | `serve()` | +| **Recommendation** | Support both | Support both | + +--- + +## SSE (Server-Sent Events) + +### Overview +- Original MCP transport +- Uses HTTP + Server-Sent Events +- Widely supported by all MCP clients + +### Implementation +```typescript +MyMCP.serveSSE("/sse").fetch(request, env, ctx) +``` + +### Client Configuration +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/sse" + } + } +} +``` + +### Pros +✅ Supported by all MCP clients +✅ Established protocol +✅ Works everywhere + +### Cons +❌ Less efficient +❌ Higher latency +❌ More bandwidth + +--- + +## Streamable HTTP + +### Overview +- New MCP transport (2025) +- Uses HTTP with streaming +- More efficient, lower latency + +### Implementation +```typescript +MyMCP.serve("/mcp").fetch(request, env, ctx) +``` + +### Client Configuration +```json +{ + "mcpServers": { + "my-mcp": { + "url": "https://my-mcp.workers.dev/mcp" + } + } +} +``` + +### Pros +✅ More efficient +✅ Lower latency +✅ Less bandwidth +✅ Better error handling + +### Cons +❌ Not all clients support yet +❌ Newer standard + +--- + +## Supporting Both (Recommended) + +```typescript +export default { + fetch(request: Request, env: Env, ctx: ExecutionContext) { + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + return new Response("Not Found", { status: 404 }); + } +}; +``` + +**Why support both?** +- Maximum client compatibility +- Smooth transition as clients upgrade +- No breaking changes + +--- + +## With OAuth + +```typescript +import { OAuthProvider } from "@cloudflare/workers-oauth-provider"; + +export default new OAuthProvider({ + // ... OAuth config ... + + apiHandlers: { + "/sse": MyMCP.serveSSE("/sse"), + "/mcp": MyMCP.serve("/mcp") + } +}); +``` + +--- + +## Testing + +### Test SSE +```bash +npx @modelcontextprotocol/inspector@latest +# Enter: http://localhost:8788/sse +``` + +### Test Streamable HTTP +```bash +# Use mcp-remote adapter +npx mcp-remote http://localhost:8788/mcp +``` + +--- + +**Recommendation**: **Support both transports** for maximum compatibility. diff --git a/templates/basic-mcp-server.ts b/templates/basic-mcp-server.ts new file mode 100644 index 0000000..52f855a --- /dev/null +++ b/templates/basic-mcp-server.ts @@ -0,0 +1,231 @@ +/** + * Basic MCP Server (No Authentication) + * + * A simple Model Context Protocol server with basic tools. + * Demonstrates the core McpAgent pattern without authentication. + * + * Perfect for: Internal tools, development, public APIs + * + * Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless + * + * ⚠️ CRITICAL URL CONFIGURATION: + * + * This template serves MCP at TWO base paths: + * - SSE transport: /sse + * - HTTP transport: /mcp + * + * Your client configuration MUST match: + * { + * "mcpServers": { + * "my-mcp": { + * "url": "https://YOUR-WORKER.workers.dev/sse" // ← Include /sse! + * } + * } + * } + * + * Common mistakes: + * ❌ "url": "https://YOUR-WORKER.workers.dev" // Missing /sse → 404 + * ❌ "url": "http://localhost:8788" // Wrong after deploy + * ✅ "url": "https://YOUR-WORKER.workers.dev/sse" // Correct! + * + * After deploying: + * 1. Test with: curl https://YOUR-WORKER.workers.dev/sse + * 2. Update client config with exact URL from step 1 + * 3. Restart Claude Desktop + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = { + // Add your environment bindings here + // Example: MY_KV: KVNamespace; +}; + +/** + * MyMCP extends McpAgent to create a stateless MCP server + */ +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "My MCP Server", + version: "1.0.0", + }); + + /** + * Initialize tools, resources, and prompts + * Called automatically by McpAgent base class + */ + async init() { + // Simple calculation tool + this.server.tool( + "add", + "Add two numbers together", + { + a: z.number().describe("First number"), + b: z.number().describe("Second number"), + }, + async ({ a, b }) => ({ + content: [ + { + type: "text", + text: `The sum of ${a} + ${b} = ${a + b}`, + }, + ], + }) + ); + + // Calculator tool with operations + this.server.tool( + "calculate", + "Perform basic arithmetic operations", + { + operation: z + .enum(["add", "subtract", "multiply", "divide"]) + .describe("The arithmetic operation to perform"), + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }, + async ({ operation, a, b }) => { + let result: number; + + switch (operation) { + case "add": + result = a + b; + break; + case "subtract": + result = a - b; + break; + case "multiply": + result = a * b; + break; + case "divide": + if (b === 0) { + return { + content: [ + { + type: "text", + text: "Error: Division by zero is not allowed", + }, + ], + isError: true, + }; + } + result = a / b; + break; + } + + return { + content: [ + { + type: "text", + text: `Result: ${a} ${operation} ${b} = ${result}`, + }, + ], + }; + } + ); + + // Example resource (optional) + this.server.resource({ + uri: "about://server", + name: "About this server", + description: "Information about this MCP server", + mimeType: "text/plain", + }, async () => ({ + contents: [{ + uri: "about://server", + mimeType: "text/plain", + text: "This is a basic MCP server running on Cloudflare Workers" + }] + })); + } +} + +/** + * Worker fetch handler + * Supports both SSE and Streamable HTTP transports + * + * ⚠️ URL CONFIGURATION GUIDE: + * + * Option 1: Serve at /sse (current setup) + * ----------------------------------------- + * Server code (below): MyMCP.serveSSE("/sse").fetch(...) + * Client config: "url": "https://worker.dev/sse" + * Tools available at: https://worker.dev/sse/tools/list + * + * Option 2: Serve at root / (alternative) + * ----------------------------------------- + * Server code: MyMCP.serveSSE("/").fetch(...) + * Client config: "url": "https://worker.dev" + * Tools available at: https://worker.dev/tools/list + * + * The base path argument MUST match what the client expects! + * + * TESTING YOUR CONFIGURATION: + * 1. Deploy: npx wrangler deploy + * 2. Test: curl https://YOUR-WORKER.workers.dev/sse + * 3. Configure: Use exact URL from step 2 in client config + * 4. Restart: Restart Claude Desktop to load new config + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // Handle CORS preflight (for browser-based clients like MCP Inspector) + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", + }, + }); + } + + // SSE transport (legacy, but widely supported) + // ⚠️ IMPORTANT: Use pathname.startsWith() to match sub-paths like /sse/tools/list + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + // ↑ Base path "/sse" means client URL must be: https://worker.dev/sse + } + + // Streamable HTTP transport (2025 standard) + // ⚠️ IMPORTANT: Use pathname.startsWith() to match sub-paths like /mcp/tools/list + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + // ↑ Base path "/mcp" means client URL must be: https://worker.dev/mcp + } + + // Health check endpoint (useful for debugging connection issues) + // Test with: curl https://YOUR-WORKER.workers.dev/ + if (pathname === "/" || pathname === "/health") { + return new Response( + JSON.stringify({ + name: "My MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + http: "/mcp", + }, + status: "ok", + timestamp: new Date().toISOString(), + }), + { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/templates/claude_desktop_config.json b/templates/claude_desktop_config.json new file mode 100644 index 0000000..7b1c273 --- /dev/null +++ b/templates/claude_desktop_config.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://modelcontextprotocol.io/schemas/client-config.json", + "mcpServers": { + "my-mcp-server-local": { + "comment": "Local MCP server (development)", + "url": "http://localhost:8788/sse" + }, + "my-mcp-server-remote": { + "comment": "Remote MCP server (production)", + "url": "https://my-mcp-server.your-account.workers.dev/sse" + }, + "my-mcp-oauth-server": { + "comment": "MCP server with OAuth authentication", + "url": "https://my-mcp-oauth.your-account.workers.dev/sse", + "auth": { + "type": "oauth", + "authorizationUrl": "https://my-mcp-oauth.your-account.workers.dev/authorize", + "tokenUrl": "https://my-mcp-oauth.your-account.workers.dev/token" + } + } + } +} diff --git a/templates/mcp-bearer-auth.ts b/templates/mcp-bearer-auth.ts new file mode 100644 index 0000000..19ef53b --- /dev/null +++ b/templates/mcp-bearer-auth.ts @@ -0,0 +1,384 @@ +/** + * MCP Server with Bearer Token Authentication + * + * Demonstrates Bearer token authentication pattern for custom auth systems. + * Shows middleware pattern for validating Authorization headers. + * + * Based on: https://github.com/cloudflare/ai/tree/main/demos/mcp-server-bearer-auth + * + * ═══════════════════════════════════════════════════════════════ + * 🔐 BEARER TOKEN AUTHENTICATION + * ═══════════════════════════════════════════════════════════════ + * + * This pattern is for: + * - Custom authentication systems + * - API key validation + * - Service-to-service communication + * - Integration with existing auth backends + * + * NOT for: + * - OAuth (use OAuth Proxy pattern instead) + * - Public APIs (use authless pattern) + * - Enterprise SSO (use Auth0/Okta integrations) + * + * ═══════════════════════════════════════════════════════════════ + * 📋 HOW IT WORKS + * ═══════════════════════════════════════════════════════════════ + * + * 1. Client sends request with Authorization header: + * Authorization: Bearer YOUR_TOKEN_HERE + * + * 2. Worker validates token (check against database, external API, etc.) + * + * 3. If valid: Pass token to MCP server via ctx.props + * + * 4. If invalid: Return 401 Unauthorized + * + * 5. MCP tools can access token via this.props.bearerToken + * + * ═══════════════════════════════════════════════════════════════ + * 🔧 CONFIGURATION + * ═══════════════════════════════════════════════════════════════ + * + * Option 1: Static token list (simple, for development) + * Option 2: Check against KV store (production) + * Option 3: Validate with external API (most flexible) + * + * This template shows all three approaches. + * + * ═══════════════════════════════════════════════════════════════ + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = { + // Optional: KV for token storage + AUTH_TOKENS?: KVNamespace; + + // Optional: API endpoint for token validation + AUTH_API_URL?: string; +}; + +/** + * Props passed to MCP server after authentication + */ +type Props = { + bearerToken: string; // The validated token + userId?: string; // Optional: User ID from token +}; + +/** + * MCP Server with bearer token authentication + */ +export class MyMCP extends McpAgent, Props> { + server = new McpServer({ + name: "Bearer Auth MCP Server", + version: "1.0.0", + }); + + async init() { + // ═══════════════════════════════════════════════════════════════ + // TOOL 1: Echo with Auth Info + // ═══════════════════════════════════════════════════════════════ + // Demonstrates accessing bearer token from props + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "echo_auth", + "Echo back your message along with auth info", + { + message: z.string().describe("Message to echo"), + }, + async ({ message }) => { + // Access authenticated user info + const token = this.props?.bearerToken || "none"; + const userId = this.props?.userId || "unknown"; + + return { + content: [ + { + type: "text", + text: `Message: ${message}\n\nAuth Info:\n- Token: ${token.substring(0, 10)}...\n- User ID: ${userId}`, + }, + ], + }; + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 2: Protected Tool Example + // ═══════════════════════════════════════════════════════════════ + // Only accessible to authenticated users + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "protected_action", + "Perform a protected action (requires auth)", + { + action: z.string().describe("Action to perform"), + }, + async ({ action }) => { + // Verify authentication (should always pass if middleware worked) + if (!this.props?.bearerToken) { + return { + content: [ + { + type: "text", + text: "Error: Unauthenticated. This should never happen if middleware is working.", + }, + ], + isError: true, + }; + } + + // Perform protected action + return { + content: [ + { + type: "text", + text: `Protected action "${action}" performed successfully by user ${this.props.userId}`, + }, + ], + }; + } + ); + } +} + +/** + * Validate bearer token + * + * Three validation strategies (choose one): + * 1. Static list (development) + * 2. KV store lookup (production) + * 3. External API validation (most flexible) + */ +async function validateToken( + token: string, + env: Env +): Promise<{ valid: boolean; userId?: string }> { + // ═══════════════════════════════════════════════════════════════ + // Strategy 1: Static Token List (Development Only!) + // ═══════════════════════════════════════════════════════════════ + // ⚠️ DON'T use in production! Tokens exposed in code. + // ═══════════════════════════════════════════════════════════════ + const VALID_TOKENS = { + "dev-token-123": "user-1", + "dev-token-456": "user-2", + }; + + if (VALID_TOKENS[token]) { + return { valid: true, userId: VALID_TOKENS[token] }; + } + + // ═══════════════════════════════════════════════════════════════ + // Strategy 2: KV Store Lookup (Production) + // ═══════════════════════════════════════════════════════════════ + // Store tokens in KV: { "token-abc123": "user-id" } + // ═══════════════════════════════════════════════════════════════ + if (env.AUTH_TOKENS) { + const userId = await env.AUTH_TOKENS.get(token); + if (userId) { + return { valid: true, userId }; + } + } + + // ═══════════════════════════════════════════════════════════════ + // Strategy 3: External API Validation (Most Flexible) + // ═══════════════════════════════════════════════════════════════ + // Validate token with external auth service + // ═══════════════════════════════════════════════════════════════ + if (env.AUTH_API_URL) { + try { + const response = await fetch(`${env.AUTH_API_URL}/validate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return { valid: true, userId: data.userId }; + } + } catch (error) { + console.error("Token validation error:", error); + } + } + + // ═══════════════════════════════════════════════════════════════ + // Token Invalid + // ═══════════════════════════════════════════════════════════════ + return { valid: false }; +} + +/** + * Worker fetch handler with bearer auth middleware + * + * ═══════════════════════════════════════════════════════════════ + * 🔐 AUTHENTICATION FLOW + * ═══════════════════════════════════════════════════════════════ + * + * 1. Extract Authorization header + * 2. Validate bearer token + * 3. If valid: Pass to MCP server with user context + * 4. If invalid: Return 401 Unauthorized + * + * Client configuration: + * { + * "mcpServers": { + * "my-mcp": { + * "url": "https://YOUR-WORKER.workers.dev/sse", + * "headers": { + * "Authorization": "Bearer YOUR_TOKEN_HERE" + * } + * } + * } + * } + * + * ═══════════════════════════════════════════════════════════════ + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // ═══════════════════════════════════════════════════════════════ + // Handle CORS Preflight (no auth required) + // ═══════════════════════════════════════════════════════════════ + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", + }, + }); + } + + // ═══════════════════════════════════════════════════════════════ + // Health Check (no auth required) + // ═══════════════════════════════════════════════════════════════ + if (pathname === "/" || pathname === "/health") { + return new Response( + JSON.stringify({ + name: "Bearer Auth MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + http: "/mcp", + }, + auth: "Bearer token required", + status: "ok", + timestamp: new Date().toISOString(), + }), + { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + + // ═══════════════════════════════════════════════════════════════ + // Authentication Middleware + // ═══════════════════════════════════════════════════════════════ + // Extract and validate bearer token before serving MCP + // ═══════════════════════════════════════════════════════════════ + + // 1. Extract Authorization header + const authHeader = request.headers.get("Authorization"); + + if (!authHeader) { + return new Response( + JSON.stringify({ + error: "Unauthorized", + message: "Missing Authorization header", + hint: 'Include header: Authorization: Bearer YOUR_TOKEN', + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": 'Bearer realm="MCP Server"', + }, + } + ); + } + + // 2. Check Bearer format + if (!authHeader.startsWith("Bearer ")) { + return new Response( + JSON.stringify({ + error: "Unauthorized", + message: "Invalid Authorization header format", + hint: 'Use format: Authorization: Bearer YOUR_TOKEN', + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": 'Bearer realm="MCP Server"', + }, + } + ); + } + + // 3. Extract token + const token = authHeader.substring(7); // Remove "Bearer " prefix + + // 4. Validate token + const { valid, userId } = await validateToken(token, env); + + if (!valid) { + return new Response( + JSON.stringify({ + error: "Unauthorized", + message: "Invalid bearer token", + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + "WWW-Authenticate": 'Bearer realm="MCP Server"', + }, + } + ); + } + + // 5. Authentication successful! Set props and pass to MCP server + const props: Props = { + bearerToken: token, + userId, + }; + + // ═══════════════════════════════════════════════════════════════ + // SSE Transport (with auth) + // ═══════════════════════════════════════════════════════════════ + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, { + ...ctx, + props, + }); + } + + // ═══════════════════════════════════════════════════════════════ + // HTTP Transport (with auth) + // ═══════════════════════════════════════════════════════════════ + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, { + ...ctx, + props, + }); + } + + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/templates/mcp-http-fundamentals.ts b/templates/mcp-http-fundamentals.ts new file mode 100644 index 0000000..b97cf41 --- /dev/null +++ b/templates/mcp-http-fundamentals.ts @@ -0,0 +1,210 @@ +/** + * MCP HTTP Fundamentals - Minimal Example + * + * The SIMPLEST working MCP server demonstrating ONLY URL configuration. + * Perfect for understanding how base paths work before adding features. + * + * This template focuses on THE #1 MISTAKE: URL path mismatches + * + * ═══════════════════════════════════════════════════════════════ + * ⚠️ CRITICAL: URL CONFIGURATION EXPLAINED + * ═══════════════════════════════════════════════════════════════ + * + * CONCEPT: The base path you use in serveSSE() determines the client URL + * + * Example A: Serving at /sse + * --------------------------- + * Code: MyMCP.serveSSE("/sse").fetch(...) + * Client URL: "https://worker.dev/sse" ✅ + * Wrong URL: "https://worker.dev" ❌ 404! + * + * Example B: Serving at root / + * ---------------------------- + * Code: MyMCP.serveSSE("/").fetch(...) + * Client URL: "https://worker.dev" ✅ + * Wrong URL: "https://worker.dev/sse" ❌ 404! + * + * Example C: Serving at /api/mcp + * ------------------------------- + * Code: MyMCP.serveSSE("/api/mcp").fetch(...) + * Client URL: "https://worker.dev/api/mcp" ✅ + * Wrong URL: "https://worker.dev/sse" ❌ 404! + * + * The pattern: pathname.startsWith("/sse") matches ALL paths like: + * - /sse + * - /sse/tools/list + * - /sse/tools/call + * - /sse/resources/list + * + * ═══════════════════════════════════════════════════════════════ + * 📋 POST-DEPLOYMENT CHECKLIST + * ═══════════════════════════════════════════════════════════════ + * + * After running `npx wrangler deploy`: + * + * 1. Note the deployed URL (e.g., https://my-mcp.my-account.workers.dev) + * + * 2. Test the endpoint: + * curl https://my-mcp.my-account.workers.dev/sse + * Should return: {"name":"My MCP Server", ...} (not 404!) + * + * 3. Update Claude Desktop config with EXACT URL from step 2: + * ~/.config/claude/claude_desktop_config.json: + * { + * "mcpServers": { + * "my-mcp": { + * "url": "https://my-mcp.my-account.workers.dev/sse" + * } + * } + * } + * + * 4. Restart Claude Desktop + * + * 5. Verify connection in Claude Desktop (check for tools) + * + * ═══════════════════════════════════════════════════════════════ + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = {}; + +/** + * Minimal MCP server with ONE simple tool + */ +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "My MCP Server", + version: "1.0.0", + }); + + async init() { + // One simple tool to verify connection + this.server.tool( + "echo", + "Echo back the provided message (useful for testing connection)", + { + message: z.string().describe("The message to echo back"), + }, + async ({ message }) => ({ + content: [ + { + type: "text", + text: `Echo: ${message}`, + }, + ], + }) + ); + } +} + +/** + * Worker fetch handler demonstrating URL configuration + * + * ═══════════════════════════════════════════════════════════════ + * 🔍 HOW THIS WORKS + * ═══════════════════════════════════════════════════════════════ + * + * Request flow: + * 1. Client sends: https://worker.dev/sse + * 2. Worker receives request + * 3. Extract pathname: new URL(request.url).pathname === "/sse" + * 4. Check: pathname.startsWith("/sse") → TRUE + * 5. Call: MyMCP.serveSSE("/sse").fetch(...) → Handle MCP request + * 6. MCP tools available at: + * - /sse/tools/list + * - /sse/tools/call + * - etc. + * + * If client sends: https://worker.dev (missing /sse): + * 1. pathname === "/" + * 2. Check: pathname.startsWith("/sse") → FALSE + * 3. Falls through to 404 + * + * ═══════════════════════════════════════════════════════════════ + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // ═══════════════════════════════════════════════════════════════ + // SSE Transport at /sse + // ═══════════════════════════════════════════════════════════════ + // This matches: + // - /sse (initial connection) + // - /sse/tools/list (list available tools) + // - /sse/tools/call (execute tool) + // - /sse/resources/list (list resources) + // - etc. + // + // Client URL MUST be: https://worker.dev/sse + // ═══════════════════════════════════════════════════════════════ + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + // ═══════════════════════════════════════════════════════════════ + // Health Check Endpoint + // ═══════════════════════════════════════════════════════════════ + // Test with: curl https://YOUR-WORKER.workers.dev/ + // Useful for: + // - Verifying Worker is deployed + // - Debugging connection issues + // - Discovering available transports + // ═══════════════════════════════════════════════════════════════ + if (pathname === "/" || pathname === "/health") { + return new Response( + JSON.stringify( + { + name: "My MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + }, + status: "ok", + timestamp: new Date().toISOString(), + help: { + clientConfig: { + url: `${new URL(request.url).origin}/sse`, + }, + testCommand: `curl ${new URL(request.url).origin}/sse`, + }, + }, + null, + 2 + ), + { + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + // ═══════════════════════════════════════════════════════════════ + // 404 Not Found + // ═══════════════════════════════════════════════════════════════ + // If you're seeing this: + // - Check client URL includes /sse + // - Try: curl https://YOUR-WORKER.workers.dev/ to see available paths + // ═══════════════════════════════════════════════════════════════ + return new Response( + JSON.stringify({ + error: "Not Found", + requestedPath: pathname, + availablePaths: ["/sse", "/", "/health"], + hint: "Client URL must be: https://YOUR-WORKER.workers.dev/sse", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + } + ); + }, +}; diff --git a/templates/mcp-oauth-proxy.ts b/templates/mcp-oauth-proxy.ts new file mode 100644 index 0000000..8a443b8 --- /dev/null +++ b/templates/mcp-oauth-proxy.ts @@ -0,0 +1,329 @@ +/** + * MCP Server with OAuth Proxy (GitHub Example) + * + * Uses Cloudflare's workers-oauth-provider to proxy OAuth to GitHub. + * This pattern lets you integrate with GitHub, Google, Azure, etc. + * + * Perfect for: Authenticated API access, user-scoped tools + * + * Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth + * + * ⚠️ CRITICAL OAuth URL CONFIGURATION: + * + * ALL OAuth URLs must use the SAME domain and protocol! + * + * Client configuration after deployment: + * { + * "mcpServers": { + * "github-mcp": { + * "url": "https://YOUR-WORKER.workers.dev/sse", + * "auth": { + * "type": "oauth", + * "authorizationUrl": "https://YOUR-WORKER.workers.dev/authorize", + * "tokenUrl": "https://YOUR-WORKER.workers.dev/token" + * } + * } + * } + * } + * + * Common mistakes: + * ❌ Mixed protocols: http://... and https://... + * ❌ Missing /sse in main URL + * ❌ Wrong domain after deployment (still using localhost) + * ❌ Typos in endpoint paths (/authorize vs /auth) + * + * Post-deployment checklist: + * 1. Deploy: npx wrangler deploy + * 2. Note deployed URL + * 3. Update ALL three URLs in client config + * 4. Test OAuth flow in Claude Desktop + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider"; +import { z } from "zod"; +import { Octokit } from "@octokit/rest"; + +type Env = { + OAUTH_KV: KVNamespace; // Required for OAuth token storage + GITHUB_CLIENT_ID?: string; // Optional: pre-register client + GITHUB_CLIENT_SECRET?: string; // Optional: pre-register client +}; + +/** + * Props passed to MCP server after OAuth authentication + * Contains user info and access token + */ +type Props = { + login: string; // GitHub username + name: string; // User's display name + email: string; // User's email + accessToken: string; // GitHub API access token +}; + +/** + * Allowlist for sensitive operations (optional) + * Replace with your GitHub usernames + */ +const ALLOWED_USERNAMES = new Set(["your-github-username"]); + +/** + * MyMCP extends McpAgent with OAuth-authenticated context + */ +export class MyMCP extends McpAgent, Props> { + server = new McpServer({ + name: "GitHub MCP Server", + version: "1.0.0", + }); + + /** + * Initialize tools with authenticated context + * this.props contains user info and accessToken + */ + async init() { + // Create Octokit client with user's access token + const octokit = new Octokit({ + auth: this.props!.accessToken, + }); + + // Tool: List user's repositories + this.server.tool( + "list_repos", + "List GitHub repositories for the authenticated user", + { + visibility: z + .enum(["all", "public", "private"]) + .default("all") + .describe("Filter by repository visibility"), + sort: z + .enum(["created", "updated", "pushed", "full_name"]) + .default("updated") + .describe("Sort order"), + per_page: z.number().min(1).max(100).default(30).describe("Results per page"), + }, + async ({ visibility, sort, per_page }) => { + try { + const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser({ + visibility, + sort, + per_page, + }); + + const repoList = repos + .map( + (repo) => + `- ${repo.full_name} (${repo.visibility}) - ${repo.description || "No description"}` + ) + .join("\n"); + + return { + content: [ + { + type: "text", + text: `Found ${repos.length} repositories:\n\n${repoList}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error fetching repositories: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: Get repository details + this.server.tool( + "get_repo", + "Get detailed information about a GitHub repository", + { + owner: z.string().describe("Repository owner (username or org)"), + repo: z.string().describe("Repository name"), + }, + async ({ owner, repo }) => { + try { + const { data: repository } = await octokit.rest.repos.get({ + owner, + repo, + }); + + return { + content: [ + { + type: "text", + text: `# ${repository.full_name} + +${repository.description || "No description"} + +**Stars**: ${repository.stargazers_count} +**Forks**: ${repository.forks_count} +**Open Issues**: ${repository.open_issues_count} +**Language**: ${repository.language || "Not specified"} +**License**: ${repository.license?.name || "None"} +**Homepage**: ${repository.homepage || "None"} +**Created**: ${repository.created_at} +**Updated**: ${repository.updated_at}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error fetching repository: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: Create GitHub issue (requires write permissions) + this.server.tool( + "create_issue", + "Create a new issue in a GitHub repository", + { + owner: z.string().describe("Repository owner"), + repo: z.string().describe("Repository name"), + title: z.string().describe("Issue title"), + body: z.string().optional().describe("Issue description"), + labels: z.array(z.string()).optional().describe("Issue labels"), + }, + async ({ owner, repo, title, body, labels }) => { + try { + const { data: issue } = await octokit.rest.issues.create({ + owner, + repo, + title, + body, + labels, + }); + + return { + content: [ + { + type: "text", + text: `Created issue #${issue.number}: ${issue.title}\n\nURL: ${issue.html_url}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error creating issue: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Optional: Allowlist-protected tool + // Only registers if user is in ALLOWED_USERNAMES + if (ALLOWED_USERNAMES.has(this.props!.login)) { + this.server.tool( + "delete_repo", + "Delete a repository (DANGEROUS - restricted to allowlisted users)", + { + owner: z.string().describe("Repository owner"), + repo: z.string().describe("Repository name"), + }, + async ({ owner, repo }) => { + try { + await octokit.rest.repos.delete({ owner, repo }); + return { + content: [ + { + type: "text", + text: `Successfully deleted repository ${owner}/${repo}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error deleting repository: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + } + } +} + +/** + * OAuth Provider configuration + * Handles GitHub OAuth flow and token management + */ +export default new OAuthProvider({ + /** + * OAuth endpoints + */ + authorizeEndpoint: "/authorize", + tokenEndpoint: "/token", + clientRegistrationEndpoint: "/register", + + /** + * GitHub OAuth handler + * Automatically handles OAuth flow with GitHub + */ + defaultHandler: new GitHubHandler({ + // Optional: Pre-configure client credentials + // If not set, uses Dynamic Client Registration + clientId: (env: Env) => env.GITHUB_CLIENT_ID, + clientSecret: (env: Env) => env.GITHUB_CLIENT_SECRET, + + // Scopes: What permissions to request + scopes: ["repo", "user:email", "read:org"], + + // Context: Extract user info to pass to MCP server + context: async (accessToken: string) => { + const octokit = new Octokit({ auth: accessToken }); + const { data: user } = await octokit.rest.users.getAuthenticated(); + + return { + login: user.login, + name: user.name || user.login, + email: user.email || `${user.login}@github.com`, + accessToken, + }; + }, + }), + + /** + * KV namespace for token storage + * Must be bound in wrangler.jsonc + */ + kv: (env: Env) => env.OAUTH_KV, + + /** + * API handlers: MCP transport endpoints + */ + apiHandlers: { + "/sse": MyMCP.serveSSE("/sse"), + "/mcp": MyMCP.serve("/mcp"), + }, + + /** + * Security settings + */ + allowConsentScreen: true, // Show consent screen (required for production) + allowDynamicClientRegistration: true, // Allow clients to register on-the-fly +}); diff --git a/templates/mcp-stateful-do.ts b/templates/mcp-stateful-do.ts new file mode 100644 index 0000000..86e3e54 --- /dev/null +++ b/templates/mcp-stateful-do.ts @@ -0,0 +1,351 @@ +/** + * Stateful MCP Server with Durable Objects + * + * Uses Durable Objects to maintain per-session state. + * Each MCP client gets its own DO instance with persistent storage. + * + * Perfect for: Stateful applications, games, conversation history + * + * Based on: https://developers.cloudflare.com/agents/model-context-protocol/mcp-agent-api + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = { + MY_MCP: DurableObjectNamespace; // Binding to this DO class +}; + +/** + * Stateful MCP Server using Durable Objects + * Each instance has its own SQL database and persistent storage + */ +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "Stateful MCP Server", + version: "1.0.0", + }); + + /** + * Initialize tools that use persistent state + */ + async init() { + // Tool: Store a value + this.server.tool( + "store_value", + "Store a key-value pair in persistent storage", + { + key: z.string().describe("Storage key"), + value: z.string().describe("Value to store"), + }, + async ({ key, value }) => { + try { + // Use Durable Objects storage API for persistence + await this.state.storage.put(key, value); + + return { + content: [ + { + type: "text", + text: `Stored "${key}" = "${value}"`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error storing value: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: Retrieve a value + this.server.tool( + "get_value", + "Retrieve a stored value by key", + { + key: z.string().describe("Storage key"), + }, + async ({ key }) => { + try { + const value = await this.state.storage.get(key); + + if (value === undefined) { + return { + content: [ + { + type: "text", + text: `No value found for key "${key}"`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: `"${key}" = "${value}"`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error retrieving value: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: List all stored keys + this.server.tool( + "list_keys", + "List all stored keys", + {}, + async () => { + try { + const keys = await this.state.storage.list(); + const keyList = Array.from(keys.keys()).join(", "); + + if (keys.size === 0) { + return { + content: [ + { + type: "text", + text: "No keys stored", + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: `Stored keys (${keys.size}): ${keyList}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error listing keys: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: Delete a key + this.server.tool( + "delete_key", + "Delete a stored key-value pair", + { + key: z.string().describe("Storage key to delete"), + }, + async ({ key }) => { + try { + const existed = await this.state.storage.delete(key); + + if (!existed) { + return { + content: [ + { + type: "text", + text: `Key "${key}" did not exist`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: `Deleted key "${key}"`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error deleting key: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Example: Counter with persistent state + this.server.tool( + "increment_counter", + "Increment a persistent counter", + { + counter_name: z.string().default("default").describe("Counter name"), + }, + async ({ counter_name }) => { + try { + const key = `counter:${counter_name}`; + const current = (await this.state.storage.get(key)) || 0; + const newValue = current + 1; + + await this.state.storage.put(key, newValue); + + return { + content: [ + { + type: "text", + text: `Counter "${counter_name}" incremented: ${current} → ${newValue}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error incrementing counter: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Example: Store structured data (JSON) + this.server.tool( + "store_json", + "Store structured JSON data", + { + key: z.string().describe("Storage key"), + data: z.record(z.any()).describe("JSON data to store"), + }, + async ({ key, data }) => { + try { + await this.state.storage.put(key, data); + + return { + content: [ + { + type: "text", + text: `Stored JSON data under key "${key}"`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error storing JSON: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // Tool: Get JSON data + this.server.tool( + "get_json", + "Retrieve stored JSON data", + { + key: z.string().describe("Storage key"), + }, + async ({ key }) => { + try { + const data = await this.state.storage.get>(key); + + if (data === undefined) { + return { + content: [ + { + type: "text", + text: `No data found for key "${key}"`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(data, null, 2), + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: "text", + text: `Error retrieving JSON: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + } +} + +/** + * Worker fetch handler + * Routes requests to Durable Objects + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // Health check + if (pathname === "/") { + return new Response( + JSON.stringify({ + name: "Stateful MCP Server", + version: "1.0.0", + transports: ["/sse", "/mcp"], + stateful: true, + }), + { + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Route MCP requests to Durable Objects + if (pathname.startsWith("/sse") || pathname.startsWith("/mcp")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/templates/mcp-with-d1.ts b/templates/mcp-with-d1.ts new file mode 100644 index 0000000..40a73bb --- /dev/null +++ b/templates/mcp-with-d1.ts @@ -0,0 +1,587 @@ +/** + * MCP Server with D1 Database Integration + * + * Demonstrates D1 (Cloudflare's SQL database) integration for persistent data storage. + * Shows CRUD operations, SQL queries, and error handling. + * + * ═══════════════════════════════════════════════════════════════ + * 💾 D1 DATABASE INTEGRATION + * ═══════════════════════════════════════════════════════════════ + * + * This template shows: + * 1. D1 binding configuration + * 2. Schema creation and migrations + * 3. CRUD operations (Create, Read, Update, Delete) + * 4. SQL query patterns + * 5. Error handling for database operations + * 6. Prepared statements (SQL injection prevention) + * + * ═══════════════════════════════════════════════════════════════ + * 📋 REQUIRED SETUP + * ═══════════════════════════════════════════════════════════════ + * + * 1. Create D1 database: + * npx wrangler d1 create my-database + * + * 2. Add binding to wrangler.jsonc: + * { + * "d1_databases": [ + * { + * "binding": "DB", + * "database_name": "my-database", + * "database_id": "YOUR_DATABASE_ID" + * } + * ] + * } + * + * 3. Create schema (run locally or in wrangler): + * npx wrangler d1 execute my-database --local --file=schema.sql + * + * schema.sql: + * ```sql + * CREATE TABLE IF NOT EXISTS users ( + * id INTEGER PRIMARY KEY AUTOINCREMENT, + * name TEXT NOT NULL, + * email TEXT UNIQUE NOT NULL, + * created_at DATETIME DEFAULT CURRENT_TIMESTAMP + * ); + * ``` + * + * 4. Deploy: + * npx wrangler deploy + * + * Pricing: https://developers.cloudflare.com/d1/platform/pricing/ + * - Free tier: 5 GB storage, 5 million reads/day + * - Pay-as-you-go after free tier + * + * ═══════════════════════════════════════════════════════════════ + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = { + DB: D1Database; // D1 binding (configured in wrangler.jsonc) +}; + +/** + * User type (matches database schema) + */ +type User = { + id: number; + name: string; + email: string; + created_at: string; +}; + +/** + * MCP Server with D1 database tools + */ +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "D1 Database MCP Server", + version: "1.0.0", + }); + + async init() { + // ═══════════════════════════════════════════════════════════════ + // TOOL 1: Create User + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: INSERT with prepared statements + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "create_user", + "Create a new user in the database", + { + name: z.string().describe("User's full name"), + email: z.string().email().describe("User's email address"), + }, + async ({ name, email }) => { + try { + // Use prepared statement to prevent SQL injection + const result = await this.env.DB.prepare( + "INSERT INTO users (name, email) VALUES (?, ?)" + ) + .bind(name, email) + .run(); + + // Check if insert was successful + if (!result.success) { + throw new Error("Failed to insert user"); + } + + return { + content: [ + { + type: "text", + text: `User created successfully!\nID: ${result.meta.last_row_id}\nName: ${name}\nEmail: ${email}`, + }, + ], + }; + } catch (error) { + // Handle duplicate email error (UNIQUE constraint) + if (error.message.includes("UNIQUE constraint failed")) { + return { + content: [ + { + type: "text", + text: `Error: Email "${email}" is already registered.`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: `Error creating user: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 2: Get User by ID + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: SELECT with WHERE clause + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "get_user", + "Get a user by their ID", + { + id: z.number().int().positive().describe("User ID"), + }, + async ({ id }) => { + try { + const user = await this.env.DB.prepare( + "SELECT * FROM users WHERE id = ?" + ) + .bind(id) + .first(); + + if (!user) { + return { + content: [ + { + type: "text", + text: `User with ID ${id} not found.`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(user, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error fetching user: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 3: List All Users + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: SELECT all rows with pagination + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "list_users", + "List all users (with optional pagination)", + { + limit: z + .number() + .int() + .positive() + .max(100) + .default(10) + .optional() + .describe("Maximum number of users to return (default 10, max 100)"), + offset: z + .number() + .int() + .min(0) + .default(0) + .optional() + .describe("Number of users to skip (for pagination, default 0)"), + }, + async ({ limit = 10, offset = 0 }) => { + try { + // Get users with pagination + const { results: users } = await this.env.DB.prepare( + "SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?" + ) + .bind(limit, offset) + .all(); + + // Get total count + const { count } = await this.env.DB.prepare( + "SELECT COUNT(*) as count FROM users" + ).first<{ count: number }>(); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + users, + pagination: { + total: count, + limit, + offset, + showing: users.length, + }, + }, + null, + 2 + ), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error listing users: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 4: Update User + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: UPDATE with prepared statements + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "update_user", + "Update a user's information", + { + id: z.number().int().positive().describe("User ID to update"), + name: z.string().optional().describe("New name (optional)"), + email: z.string().email().optional().describe("New email (optional)"), + }, + async ({ id, name, email }) => { + try { + // Build dynamic UPDATE query based on provided fields + const updates: string[] = []; + const values: (string | number)[] = []; + + if (name !== undefined) { + updates.push("name = ?"); + values.push(name); + } + if (email !== undefined) { + updates.push("email = ?"); + values.push(email); + } + + if (updates.length === 0) { + return { + content: [ + { + type: "text", + text: "No fields to update. Provide name or email.", + }, + ], + isError: true, + }; + } + + // Add ID to values array + values.push(id); + + // Execute UPDATE + const result = await this.env.DB.prepare( + `UPDATE users SET ${updates.join(", ")} WHERE id = ?` + ) + .bind(...values) + .run(); + + if (!result.success) { + throw new Error("Failed to update user"); + } + + // Fetch updated user + const updatedUser = await this.env.DB.prepare( + "SELECT * FROM users WHERE id = ?" + ) + .bind(id) + .first(); + + return { + content: [ + { + type: "text", + text: `User updated successfully!\n\n${JSON.stringify(updatedUser, null, 2)}`, + }, + ], + }; + } catch (error) { + if (error.message.includes("UNIQUE constraint failed")) { + return { + content: [ + { + type: "text", + text: `Error: Email "${email}" is already in use.`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: `Error updating user: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 5: Delete User + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: DELETE with confirmation + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "delete_user", + "Delete a user from the database (⚠️ permanent!)", + { + id: z.number().int().positive().describe("User ID to delete"), + }, + async ({ id }) => { + try { + // Get user before deleting (for confirmation message) + const user = await this.env.DB.prepare( + "SELECT * FROM users WHERE id = ?" + ) + .bind(id) + .first(); + + if (!user) { + return { + content: [ + { + type: "text", + text: `User with ID ${id} not found.`, + }, + ], + isError: true, + }; + } + + // Delete user + const result = await this.env.DB.prepare( + "DELETE FROM users WHERE id = ?" + ) + .bind(id) + .run(); + + if (!result.success) { + throw new Error("Failed to delete user"); + } + + return { + content: [ + { + type: "text", + text: `User deleted successfully!\n\nDeleted: ${user.name} (${user.email})`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error deleting user: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 6: Search Users + // ═══════════════════════════════════════════════════════════════ + // Demonstrates: LIKE queries for text search + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "search_users", + "Search users by name or email", + { + query: z.string().describe("Search term (name or email)"), + }, + async ({ query }) => { + try { + const searchPattern = `%${query}%`; + + const { results: users } = await this.env.DB.prepare( + "SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY created_at DESC" + ) + .bind(searchPattern, searchPattern) + .all(); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + query, + results: users.length, + users, + }, + null, + 2 + ), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error searching users: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + } +} + +/** + * Worker fetch handler + * + * ═══════════════════════════════════════════════════════════════ + * 🔧 SETUP CHECKLIST + * ═══════════════════════════════════════════════════════════════ + * + * 1. Create D1 database: + * npx wrangler d1 create my-database + * + * 2. Note the database_id from output + * + * 3. Add to wrangler.jsonc: + * { + * "d1_databases": [{ + * "binding": "DB", + * "database_name": "my-database", + * "database_id": "YOUR_ID_HERE" + * }] + * } + * + * 4. Create schema: + * npx wrangler d1 execute my-database --local --command \ + * "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)" + * + * 5. Deploy: + * npx wrangler deploy + * + * 6. Client URL: + * "url": "https://YOUR-WORKER.workers.dev/sse" + * + * ═══════════════════════════════════════════════════════════════ + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // Handle CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", + }, + }); + } + + // SSE transport + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + // HTTP transport + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + // Health check with DB binding info + if (pathname === "/" || pathname === "/health") { + return new Response( + JSON.stringify({ + name: "D1 Database MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + http: "/mcp", + }, + features: { + database: !!env.DB, + }, + tools: [ + "create_user", + "get_user", + "list_users", + "update_user", + "delete_user", + "search_users", + ], + status: "ok", + timestamp: new Date().toISOString(), + }), + { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/templates/mcp-with-workers-ai.ts b/templates/mcp-with-workers-ai.ts new file mode 100644 index 0000000..89f086c --- /dev/null +++ b/templates/mcp-with-workers-ai.ts @@ -0,0 +1,325 @@ +/** + * MCP Server with Workers AI Integration + * + * Demonstrates Workers AI integration for image and text generation. + * Shows how to use AI binding in MCP tools. + * + * Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth + * + * ═══════════════════════════════════════════════════════════════ + * 🤖 WORKERS AI INTEGRATION + * ═══════════════════════════════════════════════════════════════ + * + * This template shows: + * 1. AI binding configuration (wrangler.jsonc) + * 2. Image generation with Flux + * 3. Text generation with Llama + * 4. Error handling for AI requests + * 5. Streaming responses + * + * ═══════════════════════════════════════════════════════════════ + * 📋 REQUIRED CONFIGURATION + * ═══════════════════════════════════════════════════════════════ + * + * wrangler.jsonc: + * { + * "ai": { + * "binding": "AI" + * } + * } + * + * No API keys required! Workers AI is built into Cloudflare Workers. + * + * Pricing: https://developers.cloudflare.com/workers-ai/platform/pricing/ + * - Free tier: 10,000 Neurons per day + * - Image generation: ~500 Neurons per image + * - Text generation: Varies by token count + * + * ═══════════════════════════════════════════════════════════════ + */ + +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = { + AI: Ai; // Workers AI binding (configured in wrangler.jsonc) +}; + +/** + * MCP Server with Workers AI tools + */ +export class MyMCP extends McpAgent { + server = new McpServer({ + name: "Workers AI MCP Server", + version: "1.0.0", + }); + + async init() { + // ═══════════════════════════════════════════════════════════════ + // TOOL 1: Generate Image with Flux + // ═══════════════════════════════════════════════════════════════ + // Model: @cf/black-forest-labs/flux-1-schnell + // Fast image generation model (2-4 seconds) + // Input: Text prompt → Output: Base64-encoded PNG + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "generate_image", + "Generate an image from a text prompt using Flux AI model", + { + prompt: z + .string() + .describe("Detailed description of the image to generate"), + num_steps: z + .number() + .min(1) + .max(8) + .default(4) + .optional() + .describe("Number of inference steps (1-8, default 4). Higher = better quality but slower"), + }, + async ({ prompt, num_steps = 4 }) => { + try { + // Call Workers AI + const response = await this.env.AI.run( + "@cf/black-forest-labs/flux-1-schnell", + { + prompt, + num_steps, + } + ); + + // Response is a base64-encoded PNG image + const imageBase64 = (response as { image: string }).image; + + return { + content: [ + { + type: "text", + text: `Generated image from prompt: "${prompt}"`, + }, + { + type: "image", + data: imageBase64, + mimeType: "image/png", + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error generating image: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 2: Generate Text with Llama + // ═══════════════════════════════════════════════════════════════ + // Model: @cf/meta/llama-3.1-8b-instruct + // Fast text generation model + // Input: User message → Output: AI-generated text + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "generate_text", + "Generate text using Llama AI model", + { + prompt: z.string().describe("The prompt or question for the AI"), + max_tokens: z + .number() + .min(1) + .max(2048) + .default(512) + .optional() + .describe("Maximum number of tokens to generate (default 512)"), + }, + async ({ prompt, max_tokens = 512 }) => { + try { + // Call Workers AI + const response = await this.env.AI.run( + "@cf/meta/llama-3.1-8b-instruct", + { + messages: [ + { + role: "user", + content: prompt, + }, + ], + max_tokens, + } + ); + + // Extract generated text + const text = (response as { response: string }).response; + + return { + content: [ + { + type: "text", + text, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error generating text: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + // ═══════════════════════════════════════════════════════════════ + // TOOL 3: List Available AI Models + // ═══════════════════════════════════════════════════════════════ + // Shows all models available in Workers AI + // Useful for discovering what's available + // ═══════════════════════════════════════════════════════════════ + this.server.tool( + "list_ai_models", + "List all available Workers AI models", + {}, + async () => { + // This is a static list, but you could dynamically fetch from CF API + const models = [ + { + name: "@cf/black-forest-labs/flux-1-schnell", + type: "Image Generation", + description: "Fast image generation (2-4s)", + }, + { + name: "@cf/meta/llama-3.1-8b-instruct", + type: "Text Generation", + description: "Fast text generation", + }, + { + name: "@cf/meta/llama-3.1-70b-instruct", + type: "Text Generation", + description: "High-quality text generation (slower)", + }, + { + name: "@cf/openai/whisper", + type: "Speech Recognition", + description: "Audio transcription", + }, + { + name: "@cf/baai/bge-base-en-v1.5", + type: "Text Embeddings", + description: "Generate embeddings for semantic search", + }, + ]; + + return { + content: [ + { + type: "text", + text: JSON.stringify(models, null, 2), + }, + ], + }; + } + ); + } +} + +/** + * Worker fetch handler + * + * ═══════════════════════════════════════════════════════════════ + * 🔧 CONFIGURATION NOTES + * ═══════════════════════════════════════════════════════════════ + * + * 1. AI Binding Setup (wrangler.jsonc): + * { + * "ai": { + * "binding": "AI" + * } + * } + * + * 2. Deploy: + * npx wrangler deploy + * + * 3. Test Tools: + * - generate_image: Creates PNG images from prompts + * - generate_text: Generates text responses + * - list_ai_models: Shows available models + * + * 4. Client URL: + * "url": "https://YOUR-WORKER.workers.dev/sse" + * + * ═══════════════════════════════════════════════════════════════ + */ +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const { pathname } = new URL(request.url); + + // Handle CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", + }, + }); + } + + // SSE transport + if (pathname.startsWith("/sse")) { + return MyMCP.serveSSE("/sse").fetch(request, env, ctx); + } + + // HTTP transport + if (pathname.startsWith("/mcp")) { + return MyMCP.serve("/mcp").fetch(request, env, ctx); + } + + // Health check with AI binding info + if (pathname === "/" || pathname === "/health") { + return new Response( + JSON.stringify({ + name: "Workers AI MCP Server", + version: "1.0.0", + transports: { + sse: "/sse", + http: "/mcp", + }, + features: { + ai: !!env.AI, + }, + models: [ + "flux-1-schnell (image generation)", + "llama-3.1-8b-instruct (text generation)", + ], + status: "ok", + timestamp: new Date().toISOString(), + }), + { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 0000000..79609d0 --- /dev/null +++ b/templates/package.json @@ -0,0 +1,24 @@ +{ + "name": "my-mcp-server", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "npx @modelcontextprotocol/inspector" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250122.0", + "wrangler": "^3.103.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.21.0", + "agents": "^0.2.20", + "zod": "^3.24.1" + }, + "optionalDependencies": { + "@cloudflare/workers-oauth-provider": "^0.0.13", + "@octokit/rest": "^21.0.2" + } +} diff --git a/templates/wrangler-basic.jsonc b/templates/wrangler-basic.jsonc new file mode 100644 index 0000000..cce9352 --- /dev/null +++ b/templates/wrangler-basic.jsonc @@ -0,0 +1,44 @@ +/** + * Basic MCP Server Configuration (No Authentication) + * + * This configuration supports a simple MCP server without: + * - Authentication + * - Durable Objects + * - External bindings + * + * Perfect for: Internal tools, development, public APIs + */ +{ + "name": "my-mcp-server", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + "compatibility_flags": ["nodejs_compat"], + + /** + * Account ID (required for deployment) + * Get your account ID: npx wrangler whoami + */ + "account_id": "YOUR_ACCOUNT_ID_HERE", + + /** + * Environment variables + * Use .dev.vars for local secrets + */ + "vars": { + "ENVIRONMENT": "production" + }, + + /** + * Node.js compatibility + * Required for @modelcontextprotocol/sdk + */ + "node_compat": true, + + /** + * Custom domains (optional) + * Replace with your domain + */ + // "routes": [ + // { "pattern": "mcp.example.com", "custom_domain": true } + // ] +} diff --git a/templates/wrangler-oauth.jsonc b/templates/wrangler-oauth.jsonc new file mode 100644 index 0000000..0e371f5 --- /dev/null +++ b/templates/wrangler-oauth.jsonc @@ -0,0 +1,90 @@ +/** + * MCP Server with OAuth Configuration + * + * This configuration supports: + * - OAuth authentication via workers-oauth-provider + * - KV namespace for token storage + * - Durable Objects for stateful sessions (optional) + * - Environment variables for OAuth credentials + * + * Perfect for: GitHub, Google, Azure OAuth integrations + */ +{ + "name": "my-mcp-oauth-server", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + "compatibility_flags": ["nodejs_compat"], + + /** + * Account ID (required for deployment) + * Get your account ID: npx wrangler whoami + */ + "account_id": "YOUR_ACCOUNT_ID_HERE", + + /** + * Environment variables + * IMPORTANT: Never commit secrets to version control! + * Use .dev.vars for local development + */ + "vars": { + "ENVIRONMENT": "production", + + /** + * Optional: Pre-configured OAuth client credentials + * If not set, Dynamic Client Registration is used + */ + // "GITHUB_CLIENT_ID": "your-client-id", + // "GOOGLE_CLIENT_ID": "your-client-id", + }, + + /** + * KV namespace for OAuth token storage + * REQUIRED for workers-oauth-provider + * + * Create KV namespace: npx wrangler kv namespace create OAUTH_KV + */ + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "YOUR_KV_NAMESPACE_ID_HERE", + "preview_id": "YOUR_PREVIEW_KV_NAMESPACE_ID_HERE" + } + ], + + /** + * Durable Objects configuration (optional, for stateful servers) + * Uncomment if your MCP server needs persistent state + */ + // "durable_objects": { + // "bindings": [ + // { + // "name": "MY_MCP", + // "class_name": "MyMCP", + // "script_name": "my-mcp-oauth-server" + // } + // ] + // }, + + /** + * Durable Objects migrations (required on first deployment if using DOs) + */ + // "migrations": [ + // { + // "tag": "v1", + // "new_classes": ["MyMCP"] + // } + // ], + + /** + * Node.js compatibility + * Required for @modelcontextprotocol/sdk and OAuth libraries + */ + "node_compat": true, + + /** + * Custom domains (optional) + */ + // "routes": [ + // { "pattern": "mcp.example.com", "custom_domain": true } + // ] +}