Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:23 +08:00
commit 5f996ee003
23 changed files with 6697 additions and 0 deletions

View File

@@ -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<Env> {
// 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/

393
references/common-issues.md Normal file
View File

@@ -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/

View File

@@ -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<Env> {
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!

View File

@@ -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!

View File

@@ -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.

View File

@@ -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

View File

@@ -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/

150
references/transport.md Normal file
View File

@@ -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.