# MCP Server Customization Guide
This guide covers how to customize the template for your specific MCP server.
## Quick Start
After cloning/forking the template:
```bash
./scripts/setup.sh my-server-name
```
This handles:
- Creating a new KV namespace
- Updating all file references
- Installing dependencies
## Manual Customization
### 1. Server Identity
Update these files with your server name:
| File | What to Update |
|------|----------------|
| `wrangler.jsonc` | `name`, `class_name`, `new_sqlite_classes` |
| `package.json` | `name` |
| `src/index.ts` | Class name, `McpServer.name` |
| `src/admin/routes.ts` | `SERVER_INFO` object |
### 2. Google OAuth Scopes
Edit `src/oauth/google-handler.ts`:
```typescript
// Basic user info only
const GOOGLE_SCOPES = 'openid email profile';
// Add specific API scopes
const GOOGLE_SCOPES = 'openid email profile https://www.googleapis.com/auth/tasks';
```
Common scopes:
- `https://www.googleapis.com/auth/tasks` - Google Tasks
- `https://www.googleapis.com/auth/calendar` - Google Calendar
- `https://www.googleapis.com/auth/gmail.readonly` - Gmail (read-only)
- `https://www.googleapis.com/auth/drive.readonly` - Google Drive (read-only)
- `https://www.googleapis.com/auth/spreadsheets` - Google Sheets
### 3. Homepage
Edit `src/pages/homepage.ts`:
- Update title, description, badges in the `HomepageConfig`
- Update features grid
- Update tools section
- Keep the Jezweb CTA and footer
The homepage is extracted to a separate module with XSS protection built-in.
Follow the style guide in `~/.claude/rules/mcp-server-homepage.md`.
### 4. Admin Dashboard Metadata
Edit `SERVER_INFO` in `src/admin/routes.ts`:
```typescript
const SERVER_INFO = {
name: 'your-server-name',
version: '1.0.0',
description: 'Your server description.',
};
```
Also update the tools/resources/prompts lists in the API endpoints.
---
## Adding Tools
Tools are actions that Claude can invoke. Add them in the `init()` method:
```typescript
this.server.tool(
'tool_name', // Unique identifier (snake_case)
'Tool description.', // Shown to Claude - be descriptive!
{
// Zod schema for parameters
query: z.string().describe('Search query'),
limit: z.number().optional().default(10).describe('Max results'),
},
async ({ query, limit }) => {
try {
// Your implementation here
const results = await this.doSomething(query, limit);
return {
content: [{
type: 'text',
text: JSON.stringify(results, null, 2),
}],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
};
}
}
);
```
### Making Authorized API Calls
For Google APIs, use `authorizedFetch()`:
```typescript
const response = await this.authorizedFetch(
'https://www.googleapis.com/tasks/v1/users/@me/lists'
);
if (!response.ok) {
throw new Error(`API error: ${await response.text()}`);
}
const data = await response.json();
```
This automatically:
- Adds the Authorization header
- Refreshes expired tokens
- Persists new tokens to storage
---
## Adding Resources
Resources expose read-only data. Claude.ai doesn't support these yet (as of Dec 2025), but the API does.
```typescript
this.server.resource(
'resource_name', // Unique identifier
'mcp://your-server/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
status: 'active',
items: [...],
}, null, 2),
}],
})
);
```
Use cases:
- Server configuration/status
- Static reference data
- User profile information
- Feature flags
---
## Adding Prompts
Prompts are templated conversation starters (like slash commands). Claude.ai doesn't support these yet.
```typescript
this.server.prompt(
'prompt_name', // Unique identifier
'Prompt description', // Shown in prompt list
{
// Zod schema for arguments
topic: z.string().describe('Topic to discuss'),
style: z.enum(['formal', 'casual']).optional(),
},
async ({ topic, style }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: style === 'formal'
? `Please provide a formal analysis of: ${topic}`
: `Let's chat about: ${topic}`,
},
}],
})
);
```
Use cases:
- Standardized analysis templates
- Common query patterns
- Role-play setups
- Structured data requests
---
## Authentication Modes
### OAuth (Web Clients)
For Claude.ai and web clients, users authenticate via Google OAuth:
1. User connects MCP server in Claude.ai
2. Redirected to `/authorize` → approval dialog
3. Redirected to Google OAuth
4. Returns via `/callback` with tokens
5. Session established
### Bearer Token (Programmatic)
For CLI tools, ElevenLabs, etc., use Bearer tokens:
```bash
curl -X POST "https://your-server.workers.dev/mcp" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
...
```
Token options:
1. **Legacy AUTH_TOKEN** - Single token set via `wrangler secret put AUTH_TOKEN`
2. **Admin-created tokens** - Create via admin dashboard at `/admin`
### Admin Dashboard
Set `ADMIN_EMAILS` secret (comma-separated) to enable admin access:
```bash
echo "admin@example.com,another@example.com" | npx wrangler secret put ADMIN_EMAILS
```
Admin features:
- View server info
- Create/delete auth tokens
- View tools, resources, prompts
---
## Testing
### Local Development
```bash
npm run dev
```
Note: OAuth won't work locally without tunneling. Use Bearer token auth for testing.
### Test with curl
```bash
# Initialize
curl -X POST "https://your-server.workers.dev/mcp" \
-H "Authorization: Bearer YOUR_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}'
```
### Test with Claude Code
```bash
claude mcp add --transport http your-server --scope user https://your-server.workers.dev/mcp
```
---
## Troubleshooting
### Error 1101: "Missing namespace or room headers"
You're calling `stub.fetch()` directly. Always use the handler pattern:
```typescript
// WRONG
const stub = env.MCP_OBJECT.get(id);
return stub.fetch(request);
// CORRECT
const mcpHandler = MyMCP.serve('/mcp');
return mcpHandler.fetch(request, env, ctx);
```
### OAuth: Session expires after 1 hour
Ensure you're storing refresh tokens. Check:
1. `access_type: 'offline'` in authorize URL
2. `prompt: 'consent'` to force refresh token
3. `refreshToken` is saved in props
### Secret not working after `wrangler secret put`
Secrets require redeployment:
```bash
npx wrangler secret put SECRET_NAME
npx wrangler deploy # Required!
```
### 500 on /authorize
Add try-catch around `parseAuthRequest`:
```typescript
try {
oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
} catch (error) {
return c.text('Invalid OAuth request', 400);
}
```
---
## Security Features
The template includes built-in security protections:
### XSS Protection
All dynamic content in the admin dashboard is HTML-escaped. If adding new UI elements:
```typescript
// Use the escapeHtml function
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Use data attributes instead of inline handlers
// WRONG: onclick="doThing('${userInput}')"
// CORRECT: data-value="${escapeHtml(userInput)}"
```
### Input Validation
All tool inputs have max length limits. When adding tools:
```typescript
this.server.tool(
'my_tool',
'Description',
{
// Add .max() to prevent DoS
text: z.string().max(1_000_000).describe('Text input (max 1MB)'),
name: z.string().max(100).describe('Name (max 100 chars)'),
},
async ({ text, name }) => { ... }
);
```
### Rate Limiting
The template includes KV-based rate limiting. To add rate limits to new endpoints:
```typescript
import { checkRateLimit, RATE_LIMITS } from '../lib/rate-limit';
// In your route handler:
const rateLimit = await checkRateLimit(
c.env.OAUTH_KV,
`myendpoint:${userEmail}`,
{ windowMs: 60_000, maxRequests: 30 }
);
if (!rateLimit.allowed) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
```
### Session Security
Admin sessions use:
- `SameSite=Strict` cookies (CSRF protection)
- `Secure` flag (HTTPS only)
- `HttpOnly` flag (no JavaScript access)
### Token Authentication
For Bearer token auth, always use timing-safe comparison:
```typescript
import { timingSafeEqual } from '../lib/crypto';
// WRONG: if (token === env.AUTH_TOKEN)
// CORRECT:
if (env.AUTH_TOKEN && timingSafeEqual(token, env.AUTH_TOKEN)) {
// Valid token
}
```