# MCP Server Template for Cloudflare Workers
A production-ready template for building MCP (Model Context Protocol) servers on Cloudflare Workers with better-auth social login.
## Features
- **better-auth Social Login** - Google, Microsoft, and GitHub OAuth with automatic session management
- **MCP OAuth Provider** - Dynamic client registration for Claude.ai and Claude Code
- **Cloudflare Workers** - Serverless deployment with global edge distribution
- **D1 Database** - SQLite-compatible database with Drizzle ORM
- **Durable Objects** - Persistent MCP session storage
- **MCP Tools** - Example tools with error handling patterns
- **MCP Resources** - Read-only data exposure (future-ready for Claude.ai)
- **MCP Prompts** - Templated prompt definitions (future-ready for Claude.ai)
- **Marketing homepage** - Professional landing page in Jezweb style
- **Admin Dashboard** - Manage tokens, view tools/resources/prompts
- **AI Chat Testing** - Built-in AI chat to test MCP tools (Workers AI + external providers)
- **Multi-Provider AI** - Workers AI (free), OpenAI, Anthropic, Google AI Studio
- **Conversation Memory** - D1-backed persistent chat history with configurable TTL
- **Internal Agent** - Optional `ask_agent` tool with Workers AI gatekeeper for voice agents
## Quick Start
### 1. Clone and Install
```bash
# Copy this template to your new project
cp -r mcp-server-template-cloudflare my-new-mcp
cd my-new-mcp
# Install dependencies
npm install
```
### 2. Configure Cloudflare
Update `wrangler.jsonc`:
```jsonc
{
"name": "my-new-mcp", // Your worker name
"kv_namespaces": [
{
"binding": "OAUTH_KV",
"id": "YOUR_KV_NAMESPACE_ID" // Create with: npx wrangler kv:namespace create OAUTH_KV
}
],
"d1_databases": [
{
"binding": "DB",
"database_name": "my-mcp-db",
"database_id": "YOUR_D1_DATABASE_ID" // Create with: npx wrangler d1 create my-mcp-db
}
],
"durable_objects": {
"bindings": [
{
"name": "MCP_OBJECT",
"class_name": "MyMCP" // Update if you rename the class
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyMCP"] // Must match class_name above
}
],
"vars": {
"ENABLE_CONVERSATION_MEMORY": "true", // D1-backed chat history
"ENABLE_INTERNAL_AGENT": "false" // ask_agent tool (for voice agents)
}
}
```
### 3. Set Up OAuth Provider (Google)
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing
3. Enable the APIs you need (e.g., Google Tasks API, Calendar API)
4. Go to "APIs & Services" → "Credentials"
5. Create OAuth 2.0 Client ID (Web application)
6. Add authorized redirect URI: `https://your-worker.workers.dev/api/auth/callback/google`
> **Note**: better-auth uses `/api/auth/callback/{provider}` pattern for OAuth callbacks.
**Alternative Providers** (Microsoft Entra, GitHub):
- See `docs/BETTER_AUTH_ARCHITECTURE.md` for setup instructions
- Each provider requires its own OAuth app and secrets
### 4. Set Secrets
```bash
# Required: Google OAuth
echo "YOUR_GOOGLE_CLIENT_ID" | npx wrangler secret put GOOGLE_CLIENT_ID
echo "YOUR_GOOGLE_CLIENT_SECRET" | npx wrangler secret put GOOGLE_CLIENT_SECRET
# Required: better-auth session encryption
python3 -c "import secrets; print(secrets.token_hex(32))" | npx wrangler secret put BETTER_AUTH_SECRET
# Required: Cookie encryption for approved clients
python3 -c "import secrets; print(secrets.token_hex(32))" | npx wrangler secret put COOKIE_ENCRYPTION_KEY
# Optional: For Bearer token authentication
python3 -c "import secrets; print(secrets.token_urlsafe(32))" | npx wrangler secret put AUTH_TOKEN
# Optional: Microsoft Entra
# echo "YOUR_MS_CLIENT_ID" | npx wrangler secret put MICROSOFT_CLIENT_ID
# echo "YOUR_MS_CLIENT_SECRET" | npx wrangler secret put MICROSOFT_CLIENT_SECRET
# Optional: GitHub
# echo "YOUR_GH_CLIENT_ID" | npx wrangler secret put GITHUB_CLIENT_ID
# echo "YOUR_GH_CLIENT_SECRET" | npx wrangler secret put GITHUB_CLIENT_SECRET
```
### 5. Deploy
```bash
npx wrangler deploy
```
## Customization
### Update Server Identity
1. **`src/index.ts`**: Update class name, server name, and tools
2. **`src/oauth/google-handler.ts`**: Update GOOGLE_SCOPES and homepage content
3. **`wrangler.jsonc`**: Update worker name and class references
### Google OAuth Scopes
Edit `GOOGLE_SCOPES` in `src/oauth/google-handler.ts`:
```typescript
// Basic user info
const GOOGLE_SCOPES = 'openid email profile';
// Google Tasks
const GOOGLE_SCOPES = 'openid email profile https://www.googleapis.com/auth/tasks';
// Google Calendar (read-only)
const GOOGLE_SCOPES = 'openid email profile https://www.googleapis.com/auth/calendar.readonly';
// Gmail (read-only)
const GOOGLE_SCOPES = 'openid email profile https://www.googleapis.com/auth/gmail.readonly';
```
### Adding Tools
Add tools in the `init()` method of your MCP class:
```typescript
this.server.tool(
'tool_name', // Unique identifier
'Tool description.', // Shown to Claude
{
param1: z.string().describe('Parameter description'),
param2: z.number().optional().describe('Optional parameter'),
},
async ({ param1, param2 }) => {
try {
// For Google API calls, use authorizedFetch():
const response = await this.authorizedFetch(
`https://api.example.com/endpoint?param=${param1}`
);
if (!response.ok) {
const error = await response.text();
throw new Error(`API error: ${error}`);
}
const data = await response.json();
return {
content: [{ type: 'text', text: `Result: ${JSON.stringify(data)}` }],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
};
}
}
);
```
### Adding Resources
Resources expose read-only data that LLMs can access. Add resources in the `init()` method:
```typescript
this.server.resource(
'resource_name', // Unique identifier
'mcp://my-mcp/resource-path', // URI for the resource
{
description: 'What this resource provides',
mimeType: 'application/json',
},
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({
// Your data here
}, null, 2),
}],
})
);
```
> **Note**: Claude.ai doesn't support resources yet (as of Dec 2025), but the API does. Adding resources now future-proofs your server.
### Adding Prompts
Prompts are templated prompt definitions (like slash commands). Add prompts in the `init()` method:
```typescript
this.server.prompt(
'prompt_name', // Unique identifier
'Description of what this prompt does',
{
content: z.string().describe('Required parameter'),
option: z.string().optional().describe('Optional parameter'),
},
async ({ content, option }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: option
? `Process this with ${option}: ${content}`
: `Process this: ${content}`,
},
}],
})
);
```
> **Note**: Claude.ai doesn't support prompts yet (as of Dec 2025), but the API does. Adding prompts now future-proofs your server.
## Admin Dashboard
Access the admin dashboard at `/admin` after logging in with Google OAuth.
**Features:**
- View server info, tools, resources, and prompts
- Create and manage Bearer auth tokens
- AI Chat for testing MCP tools
**Admin Setup:**
Set admin emails (comma-separated):
```bash
echo "admin@example.com,user@example.com" | npx wrangler secret put ADMIN_EMAILS
```
### AI Chat Testing
The admin dashboard includes an AI-powered tool tester:
1. Click the chat bubble icon in the bottom-right corner
2. Select an AI provider (Workers AI is free)
3. Ask the AI to test tools, e.g., "test the hello tool with name John"
**Supported Providers:**
- **Workers AI** (Free) - Llama 3.3 70B and other models, no API key needed
- **OpenAI** - GPT-4o, GPT-4o-mini, o1
- **Anthropic** - Claude 3.5 Sonnet, Claude 3.5 Haiku
- **Google AI Studio** - Gemini 2.5 Pro, Gemini 2.5 Flash
- **Groq** - Fast inference with Llama 3.3 70B
All external providers use AI Gateway's **Compat endpoint** - a single OpenAI-compatible API that works for all providers. The gateway handles format conversion automatically.
**Setting up External Providers (BYOK - Recommended):**
The easiest way is to configure API keys in AI Gateway (no code changes needed):
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → **AI** → **AI Gateway**
2. Create a gateway named `default` (or use existing)
3. **Enable Authenticated Gateway** (required for BYOK)
4. Go to **Provider Keys** → **Add API Key**
5. Select provider (OpenAI, Anthropic, etc.) and enter your API key
6. Create a Gateway Token: User API Tokens → Create → select AI Gateway permissions
7. Set the token as a Worker secret: `echo "token" | npx wrangler secret put CF_AIG_TOKEN`
8. Redeploy: `npx wrangler deploy`
Keys are securely stored and automatically injected into requests.
**Alternative: Environment Secrets**
You can also set API keys as Worker secrets (overrides BYOK):
```bash
# Anthropic
echo "sk-ant-..." | npx wrangler secret put ANTHROPIC_API_KEY
# OpenAI
echo "sk-..." | npx wrangler secret put OPENAI_API_KEY
# If using Authenticated Gateway, also set:
echo "your-gateway-token" | npx wrangler secret put CF_AIG_TOKEN
# Don't forget to redeploy after setting secrets!
npx wrangler deploy
```
> **Note**: Workers AI is free and works out of the box. External providers go through [Cloudflare AI Gateway](https://dash.cloudflare.com/ai/gateway) for logging, caching, and centralized key management.
### AI Gateway Features
The template uses AI Gateway's **Compat endpoint** - a single OpenAI-compatible API for all providers. Additional features available:
**Per-Request Headers** (add to your AI calls):
| Header | Purpose | Example |
|--------|---------|---------|
| `cf-aig-cache-ttl` | Cache responses (seconds) | `3600` = 1 hour |
| `cf-aig-skip-cache` | Bypass cache | `true` |
| `cf-aig-request-timeout` | Trigger fallback if slow (ms) | `10000` |
| `cf-aig-metadata` | Tag for analytics | `{"userId":"..."}` |
**Response Headers** (check after AI calls):
| Header | Meaning |
|--------|---------|
| `cf-aig-cache-status` | `HIT` or `MISS` |
| `cf-aig-step` | Which fallback was used (0 = primary) |
**Dashboard-Only Features** (configure in Cloudflare dashboard):
- **Guardrails** - Content filtering, prompt injection detection
- **DLP** - Detect PII, secrets, source code in prompts/responses
- **Rate Limiting** - Gateway-level request limits
- **Dynamic Routing** - A/B testing, geographic routing, user-based routing
- **Analytics** - Usage metrics, costs, error rates
See [AI Gateway docs](https://developers.cloudflare.com/ai-gateway/) for details.
## Architecture
```
src/
├── index.ts # Main MCP class with tools, resources, prompts + helper methods
├── types.ts # TypeScript interfaces (Env, Props, ToolMetadata, ChatMessage)
│
├── lib/
│ ├── auth.ts # better-auth configuration (social providers, OAuth Provider plugin)
│ ├── db/
│ │ ├── index.ts # Drizzle D1 database setup
│ │ └── schema.ts # better-auth tables + custom tables (Drizzle ORM)
│ ├── ai/
│ │ ├── index.ts # AI Gateway client
│ │ ├── providers.ts # Provider/model registry
│ │ └── openrouter.ts # Dynamic model fetching from OpenRouter
│ ├── memory/
│ │ └── index.ts # D1-backed conversation memory
│ ├── agent/
│ │ └── index.ts # Internal agent pattern (Workers AI gatekeeper)
│ ├── crypto.ts # Timing-safe token comparison
│ └── rate-limit.ts # KV-based rate limiting
│
├── admin/
│ ├── routes.ts # Admin API endpoints
│ ├── ui.ts # Admin dashboard HTML/CSS/JS
│ ├── chat.ts # AI chat handler with tool execution + D1 memory
│ ├── middleware.ts # Admin auth middleware
│ ├── session.ts # Admin session management
│ └── tokens.ts # Bearer token CRUD
│
├── oauth/ # MCP OAuth protocol handlers
│ ├── better-auth-handler.ts # OAuth routes via better-auth sessions
│ └── workers-oauth-utils.ts # CSRF, state, approval dialog utilities
│
├── pages/
│ ├── homepage.ts # Server homepage (marketing landing page)
│ └── login.ts # Social login page (Google, Microsoft, GitHub)
│
├── tools/
│ ├── index.ts # Tool registry (single source of truth)
│ ├── types.ts # Tool type definitions
│ ├── utility.ts # Utility tools (no auth)
│ ├── user.ts # User tools (require OAuth)
│ └── examples.ts # Example API call patterns
│
├── resources/
│ ├── index.ts # Resource registry
│ ├── types.ts # Resource type definitions
│ └── server.ts # Server info resources
│
├── prompts/
│ ├── index.ts # Prompt registry
│ ├── types.ts # Prompt type definitions
│ └── templates.ts # Prompt templates
│
└── migrations/
├── 0001_better_auth_tables.sql # better-auth core tables (user, session, account, etc.)
├── 0002_custom_tables.sql # Custom tables (conversation memory, tool execution)
└── 0003_add_jwks_table.sql # JWT key rotation table (better-auth v1.4.0)
```
### Key Components
- **`MyMCP` class**: Extends `McpAgent` with your tools, resources, and prompts
- **`ensureValidToken()`**: Automatically refreshes expired tokens
- **`authorizedFetch()`**: Wrapper for API calls with auth
- **`BetterAuthHandler`**: Hono app handling OAuth routes via better-auth
- **`createAuth()`**: better-auth instance factory with D1 + Drizzle
### Included Examples
**Utility Tools** (no auth required):
| Name | Description |
|------|-------------|
| `hello` | Simple greeting |
| `get_current_time` | Current date/time in various timezones |
| `generate_uuid` | Generate UUID v4 (1-10) |
| `base64` | Encode/decode Base64 strings |
| `text_stats` | Count words, characters, lines |
| `random_number` | Generate random numbers in range |
| `json_format` | Format/validate JSON |
| `hash_text` | Generate SHA-256 hash |
**User Tools** (require OAuth):
| Name | Description |
|------|-------------|
| `get_user_info` | Returns authenticated user's Google info |
| `list_my_conversations` | List your conversation history |
**Example Tools**:
| Name | Description |
|------|-------------|
| `example_api_call` | Demonstrates `authorizedFetch()` pattern |
**Resources** (read-only data):
| Name | Description |
|------|-------------|
| `server_info` | Server metadata and capabilities |
| `user_profile` | Authenticated user's profile |
**Prompts** (templates):
| Name | Description |
|------|-------------|
| `summarize` | Content summarization template |
| `analyze` | Content analysis template (sentiment/technical/business) |
## Conversation Memory
The template includes optional D1-backed conversation memory for persistent chat history.
**Setup:**
```bash
# 1. Create D1 database
npx wrangler d1 create my-mcp-db
# 2. Add database_id to wrangler.jsonc d1_databases section
# 3. Run migrations
npx wrangler d1 execute my-mcp-db --local --file=migrations/0001_conversations.sql
npx wrangler d1 execute my-mcp-db --remote --file=migrations/0001_conversations.sql
# 4. Enable in wrangler.jsonc vars
"ENABLE_CONVERSATION_MEMORY": "true"
```
**Configuration:**
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_CONVERSATION_MEMORY` | `false` | Enable D1 conversation storage |
| `CONVERSATION_TTL_HOURS` | `168` | Auto-delete conversations after 7 days |
| `MAX_CONTEXT_MESSAGES` | `50` | Max messages to load for context |
When disabled, falls back to KV storage (backwards compatible).
## Internal Agent
The template includes an optional internal agent pattern for voice agents (e.g., ElevenLabs) and prompt injection protection.
When enabled, the server exposes an `ask_agent` tool that wraps all other tools behind a Workers AI gatekeeper.
**Enable:**
```jsonc
// In wrangler.jsonc vars
"ENABLE_INTERNAL_AGENT": "true",
"INTERNAL_AGENT_MODEL": "@cf/qwen/qwen2.5-coder-32b-instruct"
```
**How it works:**
1. External caller uses only `ask_agent` tool with natural language query
2. Workers AI gatekeeper validates the request
3. Internal agent selects and calls appropriate tools
4. Clean response returned (no raw tool exposure)
**Benefits:**
- Security layer against prompt injection from audio
- Minimal context passed to inner agent (fast)
- All tools available through single interface
**Configuration:**
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_INTERNAL_AGENT` | `false` | Enable `ask_agent` tool |
| `INTERNAL_AGENT_MODEL` | `@cf/qwen/qwen2.5-coder-32b-instruct` | Workers AI gatekeeper model |
## Token Refresh
The template includes automatic token refresh:
1. `authorizedFetch()` calls `ensureValidToken()` before each request
2. If token expires within 5 minutes, it's refreshed proactively
3. New tokens are persisted to Durable Object storage
4. 401 responses invalidate the stored token
This prevents sessions from disconnecting after 1 hour (Google token expiry).
## Testing
### Local Development
```bash
npx wrangler dev
```
### Test with curl
```bash
# Test MCP endpoint (requires AUTH_TOKEN secret set)
curl -X POST "https://your-worker.workers.dev/mcp" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
```
## Dependencies
- `@cloudflare/workers-oauth-provider` - OAuth 2.0 provider for Workers
- `@modelcontextprotocol/sdk` - MCP protocol implementation
- `agents` - McpAgent base class with Durable Object integration
- `hono` - Lightweight web framework for OAuth routes
- `zod` - Schema validation for tool parameters
## Security
The template includes multiple security layers:
**XSS Protection:**
- HTML escaping for all dynamic content in admin dashboard
- Data attributes with event delegation (no inline onclick handlers)
- Content Security Policy headers on admin routes
**Session Security:**
- `SameSite=Strict` cookies prevent CSRF attacks
- Timing-safe token comparison prevents timing attacks
- Secure, HttpOnly cookies for admin sessions
**Input Validation:**
- All tool inputs have max length limits (1MB default)
- Chat message size limit (100KB)
- Safe JSON parsing with fallbacks
**Rate Limiting:**
- Admin chat: 30 requests/minute per user
- Token creation: 10/hour per user
**Access Control:**
- User-owned conversations (OAuth email verification)
- Admin-only dashboard routes
- Bearer token authentication for programmatic access
## Future MCP Features
See `docs/MCP_FEATURES_PLAN.md` for planning around:
- **Tool Search** - `defer_loading` for 85% token reduction (10+ tools)
- **Sampling** - Server requests LLM completion from client (agentic workflows)
- **Completions** - Autocomplete for prompt/resource arguments
## License
MIT License - See [LICENSE](LICENSE) for details.
---
Built by [Jezweb](https://jezweb.com.au) — AI agents, MCP servers, and business automation.