How this skill is triggered — by the user, by Claude, or both
Slash command
/cf-mcp:cf-mcpThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
---
Scaffold a complete MCP server on Cloudflare Workers with Durable Objects + Google OAuth. This generates the exact architecture used by kwatch-mcp, reddit-mcp, klaviyo-mcp, and dataforseo-mcp.
OAuthProvider (entry point)
├── /authorize, /callback → GoogleHandler (Hono) → Google OAuth
├── /register, /token → OAuthProvider built-in
└── /mcp → McpAgent (Durable Object) → API Client → External API
Ask the user for these values (provide defaults where noted):
| Placeholder | Example | Notes |
|---|---|---|
{{SERVICE_NAME}} | stripe | Lowercase, kebab-safe. Used in package name, wrangler name, URLs |
{{SERVICE_NAME_UPPER}} | STRIPE | Uppercase. Used for env var prefix (STRIPE_API_KEY) |
{{SERVICE_NAME_CLASS}} | Stripe | PascalCase. Used for class names (StripeMCP, StripeClient) |
{{DESCRIPTION}} | MCP server for Stripe payment API | One-line description |
{{DOMAIN}} | taboogrow.com | Domain for custom route. Default: taboogrow.com |
{{GOOGLE_WORKSPACE_DOMAIN}} | taboogrow.com | Google Workspace domain for hd param. Set to NONE if not using Workspace |
{{ACCOUNT_ID}} | 27e3ec1d452356993cc7acfc1c99bcd6 | Cloudflare account ID. Default: 27e3ec1d452356993cc7acfc1c99bcd6 |
mkdir -p ~/code/{{SERVICE_NAME}}-mcp/src
cd ~/code/{{SERVICE_NAME}}-mcp
Copy all files from the plugin's templates/ directory into the new project:
| Template Source | Destination | Processing |
|---|---|---|
templates/src/index.ts.tmpl | src/index.ts | Replace all {{PLACEHOLDERS}} |
templates/src/google-handler.ts | src/google-handler.ts | Replace {{SERVICE_NAME}}, {{SERVICE_NAME_CLASS}}, {{DESCRIPTION}} |
templates/src/workers-oauth-utils.ts | src/workers-oauth-utils.ts | Copy as-is (no placeholders) |
templates/src/utils.ts.tmpl | src/utils.ts | Replace {{GOOGLE_WORKSPACE_DOMAIN}}. If NONE, remove the entire hd line |
templates/wrangler.toml.tmpl | wrangler.toml | Replace all {{PLACEHOLDERS}} |
templates/package.json.tmpl | package.json | Replace {{SERVICE_NAME}}, {{DESCRIPTION}} |
templates/tsconfig.json | tsconfig.json | Copy as-is |
templates/CLAUDE.md.tmpl | CLAUDE.md | Replace all {{PLACEHOLDERS}}. If GOOGLE_WORKSPACE_DOMAIN is NONE, remove the Workspace line |
If {{GOOGLE_WORKSPACE_DOMAIN}} is NONE: In src/utils.ts, delete the line:
url.searchParams.set('hd', '{{GOOGLE_WORKSPACE_DOMAIN}}');
And in CLAUDE.md, change the auth description to say "Authenticated via Google SSO OAuth 2.1" without the Workspace restriction.
bun install
git init && git add -A && git commit -m "Initial scaffold from cf-mcp template"
Use WebSearch and WebFetch to study the target service's API documentation. Identify:
Add any service-specific secrets to the Env interface in src/index.ts:
interface Env {
{{SERVICE_NAME_UPPER}}_API_KEY: string;
// Add more service-specific secrets here
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
COOKIE_ENCRYPTION_KEY: string;
MCP_OBJECT: DurableObjectNamespace;
OAUTH_KV: KVNamespace;
}
Modify the {{SERVICE_NAME_CLASS}}Client class in src/index.ts:
API_BASE_URLconst API_BASE_URL = 'https://api.service.com/v1';
class {{SERVICE_NAME_CLASS}}Client {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const body = await response.text();
switch (response.status) {
case 401:
throw new Error('Error: Authentication failed. Check your API key.');
case 403:
throw new Error('Error: Permission denied.');
case 404:
throw new Error('Error: Resource not found.');
case 429:
throw new Error('Error: Rate limit exceeded.');
default:
throw new Error(`Error: API returned ${response.status} - ${body}`);
}
}
return response.json() as Promise<T>;
}
}
Replace the example tool in init() with real tools. Follow this pattern:
Tool naming: {service}_{action}_{resource} — e.g., stripe_list_charges, stripe_get_customer
Tool registration pattern:
this.server.registerTool('{{SERVICE_NAME}}_list_items', {
title: 'List Items',
description: 'List all items with optional filtering. Returns item name, status, and metadata.',
inputSchema: {
status: z.string().optional().describe('Filter by status: active, archived, or all (default: active)'),
limit: z.string().optional().describe('Max results to return, 1-100 (default: 20)'),
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
}, async ({ status, limit }) => {
try {
console.log(JSON.stringify({
tool: '{{SERVICE_NAME}}_list_items',
status,
limit,
timestamp: new Date().toISOString(),
}));
const data = await client.request<unknown>(`/items?status=${status || 'active'}&limit=${limit || '20'}`);
return {
content: [{ type: 'text' as const, text: truncateResponse(data) }],
};
} catch (error) {
return {
isError: true,
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
};
}
});
Write tools (mutations):
this.server.registerTool('{{SERVICE_NAME}}_create_item', {
title: 'Create Item',
description: 'Create a new item with the given name and optional configuration as JSON.',
inputSchema: {
name: z.string().min(1).describe('Item name'),
config: z.string().optional().describe('Optional configuration as JSON string (e.g. {"key": "value"})'),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
}, async ({ name, config }) => {
try {
console.log(JSON.stringify({
tool: '{{SERVICE_NAME}}_create_item',
name,
timestamp: new Date().toISOString(),
}));
const parsedConfig = config ? JSON.parse(config) : undefined;
const data = await client.request<unknown>('/items', {
method: 'POST',
body: JSON.stringify({ name, config: parsedConfig }),
});
return {
content: [{ type: 'text' as const, text: truncateResponse(data) }],
};
} catch (error) {
return {
isError: true,
content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }],
};
}
});
.describe() on every field. Put validation constraints and defaults in the description text, not in Zod methods.readOnlyHint, destructiveHint, idempotentHint, openWorldHint accurately.isError: true with the error message.{ tool, ...keyParams, timestamp } via console.log(JSON.stringify(...)).truncateResponse() — the 25k character limit prevents oversized responses.CRITICAL: With zod v4 (^4.3.5) and the agents/mcp McpAgent pattern, only these Zod types work reliably in inputSchema:
z.string() — worksz.string().optional() — worksz.string().min(1) — worksz.string().describe('...') — worksThese types silently break tool registration (tools won't appear in the connector):
z.number(), z.number().min().max() — use z.string() and parse in the handlerz.enum([...]) — use z.string() and document valid values in .describe()z.array(z.string()) — use z.string() with comma-separated valuesz.record(z.unknown()) — use z.string() and accept JSON stringsz.object({...}) — use z.string() and accept JSON stringsz.boolean() — use z.string() and parse 'true'/'false'The failure is silent — init() completes, but the MCP connector shows "This connector has no tools available." with no errors in logs.
cd ~/code/{{SERVICE_NAME}}-mcp
wrangler kv namespace create "OAUTH_KV"
Copy the output ID and update wrangler.toml:
[[kv_namespaces]]
binding = "OAUTH_KV"
id = "PASTE_THE_ID_HERE"
wrangler secret put {{SERVICE_NAME_UPPER}}_API_KEY
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put COOKIE_ENCRYPTION_KEY
# Generate cookie key: openssl rand -hex 32
console.cloud.google.comhttps://{{SERVICE_NAME}}-mcp.{{DOMAIN}}/callbackbun run deploy
bun run logs
Visit https://{{SERVICE_NAME}}-mcp.{{DOMAIN}}/ in a browser — should see the approval dialog page.
In Claude Desktop or Claude Code Settings > Connectors > Add custom connector:
https://{{SERVICE_NAME}}-mcp.{{DOMAIN}}/mcpTest each registered tool to verify:
Update the generated CLAUDE.md with:
gh repo create taboogrow/{{SERVICE_NAME}}-mcp --private --source=. --push
bun install completes without errorswrangler.tomlwrangler secret puthttps://{{SERVICE_NAME}}-mcp.{{DOMAIN}}/callbackbun run deploy succeedsCLAUDE.md updated with actual tools and configAll CF MCP servers use the same dependency set, pinned to compatible versions:
{
"dependencies": {
"@cloudflare/workers-oauth-provider": "^0.2.0",
"@modelcontextprotocol/sdk": "^1.27.0",
"agents": "^0.5.0",
"hono": "^4.11.10",
"zod": "^4.3.5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241230.0",
"typescript": "^5.7.2",
"wrangler": "^4.59.3"
}
}
npx claudepluginhub adamlevoy/claude-plugins --plugin cf-mcpBuilds production-ready TypeScript MCP servers on Cloudflare Workers using @modelcontextprotocol/sdk, Hono HTTP transport, authentication, Cloudflare services, and error prevention.
MCPize Builder — takes a brief/PRD (from /mcpize:idea or custom) and builds a production-ready MCP server, end to end. Part of the MCPize suite (mcpize.com). Parses brief to extract tools, data strategy, secrets, and technical stack. Scaffolds project via mcpize CLI with the right template, implements all MCP tools with proper input validation and error handling, configures mcpize.yaml with secrets/credentials and HTTP transport, writes tests that verify MCP protocol compliance, and generates README + CLAUDE.md. Use this skill whenever someone wants to build an MCP server from a brief or PRD, has a spec they want to turn into a working server, says 'build this MCP', wants to implement tools from a brief, or references mcpize-build. Also trigger on: build mcp, implement mcp server, code mcp from brief, scaffold mcp project, create mcp server from spec, mcpize build, turn brief into server, implement brief, build from prd, mcp server from scratch.
Guides MCP server integration into Claude Code plugins via .mcp.json or plugin.json, covering stdio, SSE, HTTP for external tools like filesystems and APIs.