Skip to main content
Glama
jezweb

MCP Server Template for Cloudflare Workers

by jezweb
README.md
# 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.