Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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.
|
||||
121
plugin.lock.json
Normal file
121
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
403
references/authentication.md
Normal file
403
references/authentication.md
Normal 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
393
references/common-issues.md
Normal 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/
|
||||
712
references/debugging-guide.md
Normal file
712
references/debugging-guide.md
Normal 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!
|
||||
506
references/http-transport-fundamentals.md
Normal file
506
references/http-transport-fundamentals.md
Normal 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!
|
||||
190
references/oauth-providers.md
Normal file
190
references/oauth-providers.md
Normal 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.
|
||||
170
references/official-examples.md
Normal file
170
references/official-examples.md
Normal 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
|
||||
439
references/transport-comparison.md
Normal file
439
references/transport-comparison.md
Normal 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
150
references/transport.md
Normal 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.
|
||||
231
templates/basic-mcp-server.ts
Normal file
231
templates/basic-mcp-server.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
22
templates/claude_desktop_config.json
Normal file
22
templates/claude_desktop_config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
384
templates/mcp-bearer-auth.ts
Normal file
384
templates/mcp-bearer-auth.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
210
templates/mcp-http-fundamentals.ts
Normal file
210
templates/mcp-http-fundamentals.ts
Normal 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" },
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
329
templates/mcp-oauth-proxy.ts
Normal file
329
templates/mcp-oauth-proxy.ts
Normal 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
|
||||
});
|
||||
351
templates/mcp-stateful-do.ts
Normal file
351
templates/mcp-stateful-do.ts
Normal 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
587
templates/mcp-with-d1.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
325
templates/mcp-with-workers-ai.ts
Normal file
325
templates/mcp-with-workers-ai.ts
Normal 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
24
templates/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
44
templates/wrangler-basic.jsonc
Normal file
44
templates/wrangler-basic.jsonc
Normal 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 }
|
||||
// ]
|
||||
}
|
||||
90
templates/wrangler-oauth.jsonc
Normal file
90
templates/wrangler-oauth.jsonc
Normal 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 }
|
||||
// ]
|
||||
}
|
||||
Reference in New Issue
Block a user