name: workers-mcp description: "Create production-ready MCP servers on Cloudflare Workers with:. Use when Use this skill when the task matches its description and triggers.."
Workers MCP
Compliance
- Check against GOLD Industry Standards guide in ~/.codex/AGENTS.override.md
Overview
Create production-ready MCP servers on Cloudflare Workers with:
- workers-mcp SDK for zero-boilerplate MCP protocol
- Auth0 for OAuth 2.1 authentication
- Stripe for subscription licensing
- D1 for SQLite database
- Durable Objects for stateful operations
- Vectorize for semantic vector search
Quick Start
Scaffold New Project
cd /path/to/skill/assets
# Replace PROJECT_NAME with actual name
sed 's/PROJECT_NAME/my-server/g' wrangler.template.toml > ../../wrangler.toml
Then create project structure:
mkdir -p src/{workers/mcp,durable-objects,lib/{db,kv,auth,license,embeddings,monitoring,types},middleware}
mkdir -p migrations tests/{unit,integration,contract}
Core Dependencies
{
"dependencies": {
"workers-mcp": "^1.0.0",
"@cloudflare/workers-types": "^4.20241218.0",
"stripe": "^17.0.0"
},
"devDependencies": {
"wrangler": "^3.0.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
}
}
Minimal Worker Entry Point
// src/workers/mcp/index.ts
import { ProxyToSelf } from 'workers-mcp';
export default class MyMCPServer extends WorkerEntrypoint<Env> {
// This becomes an MCP tool automatically!
async hello(input: { name: string }): Promise<{ message: string }> {
return { message: `Hello, ${input.name}!` };
}
async fetch(request: Request): Promise<Response> {
return new ProxyToSelf(this).fetch(request);
}
}
Architecture Pattern
┌─────────────────────────────────────────────────────────────┐
│ CLOUDFLARE EDGE (300+ PoPs) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ WORKERS MCP SERVER (workers-mcp SDK) │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ ProxyToSelf Pattern │ │ │
│ │ │ ├─ free_tool() [FREE] │ │ │
│ │ │ ├─ another_free() [FREE] │ │ │
│ │ │ └─ pro_feature() [PRO LICENSED] │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └───────────────────────────┬──────────────────────────┘ │
└──────────────────────────────┼──────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Workers KV │ │ Durable Objects│ │ D1 Database │
│ (Cache/Auth) │ │ (Stateful) │ │ (SQLite) │
└────────────────┘ │ • Embeddings │ │ • Memories │
│ • Sessions │ │ • Tenants │
└────────────────┘ │ • Users │
│ │ └────────────────┘
▼ ▼
┌────────────────┐ ┌────────────────┐
│ R2 Storage │ │ External APIs │
│ (Data export) │ │ • OpenAI │
│ │ │ • Auth0 │
│ │ │ • Stripe │
│ │ │ • Vectorize │
└────────────────┘ └────────────────┘
MCP Tool Pattern
Free Tool (No License Required)
async recall(
input: { query: string; limit?: number }
): Promise<{ memories: any[]; total: number }> {
const userId = await this.getUserId();
const tenantId = await this.getTenantId(userId);
const stmt = this.env.DB.prepare(`
SELECT * FROM memories
WHERE tenant_id = ? AND deleted_at IS NULL
AND content LIKE ?
LIMIT ?
`);
const result = await stmt.bind(
tenantId,
`%${input.query}%`,
input.limit || 10
).all();
return { memories: result.results, total: result.results.length };
}
Licensed Tool (Pro Feature)
async semanticSearch(
input: { query: string; limit?: number }
): Promise<{ memories: any[]; total: number }> {
const userId = await this.getUserId();
const license = await this.license.verify(this.env, userId);
// Check license
if (!this.license.hasFeature(license, 'semantic_search')) {
throw new Error(
'semantic_search requires Pro subscription. ' +
'Upgrade at https://app.yourdomain.com/upgrade'
);
}
// Execute feature...
return { memories: [], total: 0 };
}
Helper Methods
private async getUserId(): Promise<string> {
const authHeader = this.request?.headers?.get('Authorization');
const token = authHeader?.replace('Bearer ', '');
if (!token) throw new Error('Authentication required');
const payload = await this.verifyJWT(token);
return payload.userId;
}
private async getTenantId(userId: string): Promise<string> {
const stmt = this.env.DB.prepare(
'SELECT tenant_id FROM users WHERE id = ?'
);
const result = await stmt.bind(userId).first();
if (!result) throw new Error('User not found');
return result.tenant_id;
}
Resources (Load as Needed)
workers-mcp SDK
See references/workers-mcp-sdk.md for:
- ProxyToSelf pattern details
- Tool definition patterns
- Environment interface
- Helper methods
- Testing with MCP Inspector
Load when: Implementing MCP tools or configuring workers-mcp SDK
D1 Schema Patterns
See references/d1-schema-patterns.md for:
- Tenant isolation pattern
- Soft delete pattern
- Full-text search (FTS5)
- JSON storage pattern
- Audit log pattern
- Index strategy
Load when: Designing D1 database schema or writing migrations
Auth0 Integration
See references/auth0-integration.md for:
- OAuth 2.1 flow implementation
- JWT validation
- User provisioning
- Authentication middleware
- Claude Desktop configuration
Load when: Setting up authentication or handling OAuth callbacks
Stripe Licensing
See references/stripe-licensing.md for:
- License verifier implementation
- Stripe client for webhooks
- Subscription management
- Feature-based access control
- Upgrade flow
Load when: Implementing licensing or handling Stripe webhooks
Vectorize Integration
See references/vectorize-integration.md for:
- Vectorize client wrapper
- Embedding generation with Durable Objects
- Tenant-namespaced vector search
- Soft delete pattern for vectors
Load when: Adding semantic search or managing embeddings
Wrangler Configuration
Copy and customize assets/wrangler.template.toml:
- Replace
PROJECT_NAMEwith actual project name - Set
account_idfromwrangler whoami - Create D1 database:
wrangler d1 create PROJECT_NAME - Create KV namespaces:
wrangler kv:namespace create CACHE - Create Vectorize index:
wrangler vectorize create PROJECT_NAME-index --dimensions=1536 --metric=cosine - Set secrets:
wrangler secret put OPENAI_API_KEY
Deployment Checklist
Pre-Deployment
- Create D1 database:
wrangler d1 create <name> - Create KV namespaces:
wrangler kv:namespace create CACHE - Create R2 bucket:
wrangler r2 bucket create <name> - Create Vectorize index:
wrangler vectorize create <name> --dimensions=1536 - Set all secrets:
wrangler secret put <NAME> - Run migrations:
wrangler d1 execute DB --file=migrations/001_initial.sql
Deployment
# Deploy to production
wrangler deploy
# Deploy to staging
wrangler deploy --env staging
# Test locally
wrangler dev
Post-Deployment
- Test health endpoint:
curl https://your-worker.workers.dev/health - Test with MCP Inspector:
npx @modelcontextprotocol/inspector https://your-worker.workers.dev - Verify OAuth flow in Auth0 dashboard
- Test Stripe webhook delivery
Common Patterns
Rate Limiting
const rateLimit = await this.rateLimiter.check(
this.env.RATE_LIMITS,
`tool:${userId}`,
100, // 100 requests
60000 // per minute
);
if (!rateLimit.allowed) {
throw new Error(`Rate limit exceeded. Retry in ${rateLimit.retryAfter}s`);
}
Caching with KV
// Check cache
const cached = await this.env.CACHE.get(`key:${id}`, 'json');
if (cached) return cached;
// Generate data
const data = await expensiveOperation();
// Cache for 5 minutes
await this.env.CACHE.put(`key:${id}`, JSON.stringify(data), {
expirationTtl: 300,
});
Batch Operations (D1 Transactions)
await this.env.DB.batch([
this.env.DB.prepare('INSERT INTO memories ...'),
this.env.DB.prepare('UPDATE tenants SET memory_count = ...')
]);
Environment Variables
| Variable | Purpose |
|---|---|
ENVIRONMENT |
development | staging | production |
BASE_URL |
Base URL for OAuth callbacks |
AUTH0_DOMAIN |
Your Auth0 domain |
AUTH0_CLIENT_ID |
Auth0 client ID |
STRIPE_PRO_PRICE_ID |
Stripe Pro price ID |
OPENAI_EMBEDDING_MODEL |
Default: text-embedding-3-small |
Secrets
| Secret | Purpose |
|---|---|
OPENAI_API_KEY |
OpenAI embeddings |
STRIPE_SECRET_KEY |
Stripe API |
STRIPE_WEBHOOK_SECRET |
Stripe webhook verification |
AUTH0_CLIENT_SECRET |
Auth0 OAuth |
JWT_SECRET |
JWT signing |
Claude Desktop Configuration
After OAuth flow, users configure Claude Desktop:
{
"mcpServers": {
"your-server": {
"url": "https://your-worker.workers.dev",
"headers": {
"Authorization": "Bearer YOUR_JWT_TOKEN"
}
}
}
}
Testing
Local Development
# Start dev server
wrangler dev
# Test with MCP Inspector
npx @modelcontextprotocol/inspector http://localhost:8787
Remote Testing
# Test deployed worker
npx @modelcontextprotocol/inspector https://your-worker.workers.dev
Unit Tests
import { describe, it, expect } from 'vitest';
describe('MyMCPServer', () => {
it('should remember a memory', async () => {
const server = new MyMCPServer(mockState, mockEnv);
const result = await server.remember({ content: 'Test' });
expect(result.id).toBeDefined();
});
});
Troubleshooting
Common Errors
-
"Invalid JWT": Verify
JWT_SECRETmatches between signing and verification - "Tenant not found": Ensure user provisioning runs after OAuth callback
- "Rate limit exceeded": Check KV namespace is bound correctly
-
"D1 error": Run migrations with
wrangler d1 execute DB --file=migrations/...
Debug Logging
console.log(JSON.stringify({
type: 'mcp_tool_execution',
tool: 'remember',
userId,
tenantId,
timestamp: Date.now(),
}));
Philosophy
- Simplicity First: Use workers-mcp SDK to avoid boilerplate
- Security by Default: All tools require authentication by default
- Tenant Isolation: All data scoped to tenant with cascade deletes
- Graceful Degradation: Soft deletes preserve data, downgrade preserves read access
- Observability: Structured logging for all operations
File Structure
PROJECT_NAME/
├── src/
│ ├── workers/mcp/
│ │ └── index.ts # Main Worker with ProxyToSelf
│ ├── durable-objects/
│ │ └── embedding-do.ts # Embedding generation DO
│ ├── lib/
│ │ ├── db/d1.ts # D1 client wrapper
│ │ ├── kv/cache.ts # KV cache wrapper
│ │ ├── auth/auth0.ts # Auth0 integration
│ │ ├── license/verifier.ts # License verification
│ │ └── embeddings/openai.ts # OpenAI embeddings
│ └── middleware/auth.ts # Auth middleware
├── migrations/
│ └── 001_initial.sql # D1 schema
├── wrangler.toml # Workers config
├── package.json
└── tsconfig.json
When to use
- Use this skill when the task matches its description and triggers.
- If the request is outside scope, route to the referenced skill.
Inputs
- User request details and any relevant files/links.
Outputs
- A structured response or artifact appropriate to the skill.
- Include
schema_version: 1if outputs are contract-bound.
Constraints
- Redact secrets/PII by default.
- Avoid destructive operations without explicit user direction.
Validation
- Run any relevant checks or scripts when available.
- Fail fast and report errors before proceeding.
Anti-patterns
- Avoid vague guidance without concrete steps.
- Do not invent results or commands.
Procedure
- Clarify scope and inputs.
- Execute the core workflow.
- Summarize outputs and next steps.
Antipatterns
- Do not add features outside the agreed scope.
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!