Initial commit
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user