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,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": [
"./"
]
}

3
README.md Normal file
View File

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

1001
SKILL.md Normal file

File diff suppressed because it is too large Load Diff

121
plugin.lock.json Normal file
View File

@@ -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": []
}
}

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.

View File

@@ -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<Env> {
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<Response> {
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 });
},
};

View File

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

View File

@@ -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<Env, Record<string, never>, 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<Response> {
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 });
},
};

View File

@@ -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<Env> {
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<Response> {
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" },
}
);
},
};

View File

@@ -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<Env, Record<string, never>, 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
});

View File

@@ -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<Env> {
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<string>(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<number>(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<Record<string, any>>(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<Response> {
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 });
},
};

587
templates/mcp-with-d1.ts Normal file
View File

@@ -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<Env> {
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<User>();
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<User>();
// 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<User>();
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<User>();
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<User>();
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<Response> {
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 });
},
};

View File

@@ -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<Env> {
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<Response> {
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 });
},
};

24
templates/package.json Normal file
View File

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

View File

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

View File

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