Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:43 +08:00
commit bd0f3b67e7
19 changed files with 5648 additions and 0 deletions

View File

@@ -0,0 +1,497 @@
# Authentication Guide for MCP Servers
Complete guide to implementing authentication in TypeScript MCP servers on Cloudflare Workers.
---
## Why Authentication Matters
**Without authentication:**
- ❌ Anyone can access your tools
- ❌ API abuse and DDoS attacks
- ❌ Data leaks and security breaches
- ❌ Unexpected Cloudflare costs
**With authentication:**
- ✅ Controlled access
- ✅ Usage tracking
- ✅ Rate limiting per user
- ✅ Audit trails
---
## Method 1: API Key Authentication (Recommended)
Best for: Most use cases, simple setup, good security.
### Setup
**1. Create KV namespace:**
```bash
wrangler kv namespace create MCP_API_KEYS
```
**2. Add to wrangler.jsonc:**
```jsonc
{
"kv_namespaces": [
{
"binding": "MCP_API_KEYS",
"id": "YOUR_NAMESPACE_ID"
}
]
}
```
**3. Generate API keys:**
```bash
# Generate secure key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Add to KV
wrangler kv key put --binding=MCP_API_KEYS "key:abc123xyz..." "true"
```
### Implementation
```typescript
import { Hono } from 'hono';
type Env = {
MCP_API_KEYS: KVNamespace;
};
const app = new Hono<{ Bindings: Env }>();
// Authentication middleware
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const apiKey = authHeader.replace('Bearer ', '');
const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);
if (!isValid) {
return c.json({ error: 'Invalid API key' }, 403);
}
// Optional: Track usage
const usageKey = `usage:${apiKey}:${new Date().toISOString().split('T')[0]}`;
const count = await c.env.MCP_API_KEYS.get(usageKey);
await c.env.MCP_API_KEYS.put(
usageKey,
String(parseInt(count || '0') + 1),
{ expirationTtl: 86400 * 7 }
);
await next();
});
app.post('/mcp', async (c) => {
// MCP handler (user is authenticated)
});
export default app;
```
### Key Management
```bash
# List all keys
wrangler kv key list --binding=MCP_API_KEYS --prefix="key:"
# Add new key
wrangler kv key put --binding=MCP_API_KEYS "key:newkey123" "true"
# Revoke key
wrangler kv key delete --binding=MCP_API_KEYS "key:oldkey456"
# Check usage
wrangler kv key get --binding=MCP_API_KEYS "usage:abc123:2025-10-28"
```
### Client Usage
```bash
curl -X POST https://mcp.example.com/mcp \
-H "Authorization: Bearer abc123xyz..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```
---
## Method 2: Cloudflare Zero Trust Access
Best for: Enterprise deployments, SSO integration, team access.
### Setup
**1. Configure Cloudflare Access:**
- Go to Zero Trust dashboard
- Create Access Application
- Set application domain (e.g., `mcp.example.com`)
- Configure identity providers (Google, GitHub, SAML)
- Set access policies (email domains, groups)
**2. Install JWT verification:**
```bash
npm install @tsndr/cloudflare-worker-jwt
```
**3. Implementation:**
```typescript
import { verify } from '@tsndr/cloudflare-worker-jwt';
type Env = {
CF_ACCESS_TEAM_DOMAIN: string; // e.g., "yourteam.cloudflareaccess.com"
};
app.use('/mcp', async (c, next) => {
const jwt = c.req.header('Cf-Access-Jwt-Assertion');
if (!jwt) {
return c.json({ error: 'Access denied' }, 403);
}
try {
// Verify JWT
const isValid = await verify(
jwt,
`https://${c.env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`
);
if (!isValid) {
return c.json({ error: 'Invalid token' }, 403);
}
// Decode to get user info
const payload = JSON.parse(atob(jwt.split('.')[1]));
// Optional: Add user to request context
c.set('user', payload);
await next();
} catch (error) {
return c.json({ error: 'Authentication failed' }, 403);
}
});
```
### Benefits
- ✅ SSO with Google, GitHub, Okta, etc.
- ✅ Team-based access control
- ✅ Automatic user management
- ✅ Audit logs in Zero Trust dashboard
---
## Method 3: OAuth 2.0
Best for: Public APIs, third-party integrations, user consent flows.
### Setup
**1. Choose OAuth provider:**
- Auth0
- Clerk
- Supabase
- Custom OAuth server
**2. Install dependencies:**
```bash
npm install oauth4webapi
```
**3. Implementation:**
```typescript
import * as oauth from 'oauth4webapi';
type Env = {
OAUTH_CLIENT_ID: string;
OAUTH_CLIENT_SECRET: string;
OAUTH_ISSUER: string; // e.g., "https://yourdomain.auth0.com"
};
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const accessToken = authHeader.replace('Bearer ', '');
try {
// Validate token with OAuth provider
const issuer = new URL(c.env.OAUTH_ISSUER);
const client = {
client_id: c.env.OAUTH_CLIENT_ID,
client_secret: c.env.OAUTH_CLIENT_SECRET
};
const response = await oauth.introspectionRequest(
issuer,
client,
accessToken
);
const result = await oauth.processIntrospectionResponse(issuer, client, response);
if (!result.active) {
return c.json({ error: 'Invalid token' }, 403);
}
// Token is valid, user info in result
c.set('user', result);
await next();
} catch (error) {
return c.json({ error: 'Authentication failed' }, 403);
}
});
```
---
## Method 4: JWT (Custom)
Best for: Microservices, existing JWT infrastructure.
### Implementation
```typescript
import { verify, sign } from '@tsndr/cloudflare-worker-jwt';
type Env = {
JWT_SECRET: string;
};
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const token = authHeader.replace('Bearer ', '');
try {
const isValid = await verify(token, c.env.JWT_SECRET);
if (!isValid) {
return c.json({ error: 'Invalid token' }, 403);
}
// Decode payload
const payload = JSON.parse(atob(token.split('.')[1]));
// Check expiration
if (payload.exp && payload.exp < Date.now() / 1000) {
return c.json({ error: 'Token expired' }, 403);
}
c.set('user', payload);
await next();
} catch (error) {
return c.json({ error: 'Authentication failed' }, 403);
}
});
// Generate JWT endpoint (for testing)
app.post('/auth/token', async (c) => {
const { username, password } = await c.req.json();
// Validate credentials (implement your logic)
if (username === 'admin' && password === 'secret') {
const token = await sign({
sub: username,
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
}, c.env.JWT_SECRET);
return c.json({ token });
}
return c.json({ error: 'Invalid credentials' }, 401);
});
```
---
## Method 5: mTLS (Mutual TLS)
Best for: High-security environments, machine-to-machine communication.
**Note:** Cloudflare Workers support mTLS for enterprise customers.
### Setup
**1. Enable mTLS in Cloudflare dashboard:**
- Go to SSL/TLS → Client Certificates
- Create client certificate
- Download certificate and private key
- Configure API Shield mTLS
**2. Implementation:**
```typescript
app.use('/mcp', async (c, next) => {
const clientCert = c.req.header('Cf-Client-Cert-Der-Base64');
if (!clientCert) {
return c.json({ error: 'Client certificate required' }, 401);
}
// Cloudflare validates certificate automatically
// You can add additional validation here
await next();
});
```
---
## Combined Authentication
Use multiple methods for flexibility:
```typescript
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader) {
return c.json({ error: 'Unauthorized' }, 401);
}
// Try API Key
if (authHeader.startsWith('Bearer ')) {
const token = authHeader.replace('Bearer ', '');
// Check if it's an API key
const isApiKey = await c.env.MCP_API_KEYS.get(`key:${token}`);
if (isApiKey) {
return next();
}
// Check if it's a JWT
try {
const isValid = await verify(token, c.env.JWT_SECRET);
if (isValid) {
return next();
}
} catch {}
}
return c.json({ error: 'Invalid credentials' }, 403);
});
```
---
## Security Best Practices
### Do's ✅
- ✅ Use HTTPS only (enforced by Cloudflare)
- ✅ Generate strong API keys (32+ bytes)
- ✅ Implement rate limiting per key
- ✅ Track usage per key
- ✅ Rotate keys regularly
- ✅ Store secrets in Wrangler secrets
- ✅ Log authentication failures
- ✅ Set token expiration
- ✅ Validate all inputs
- ✅ Use environment-specific keys
### Don'ts ❌
- ❌ Never hardcode API keys
- ❌ Don't log auth tokens
- ❌ Avoid weak tokens (< 16 bytes)
- ❌ Don't expose auth endpoints publicly
- ❌ Never return detailed auth errors to clients
- ❌ Don't skip CORS validation
- ❌ Avoid storing tokens in localStorage (use httpOnly cookies)
---
## Testing Authentication
### Local Testing
```bash
# Start dev server
wrangler dev
# Test with curl
curl -X POST http://localhost:8787/mcp \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```
### Production Testing
```bash
# Test authentication failure
curl -X POST https://mcp.example.com/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Expected: 401 Unauthorized
# Test with valid key
curl -X POST https://mcp.example.com/mcp \
-H "Authorization: Bearer VALID_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Expected: 200 OK + tool list
```
---
## Migration Guide
### Moving from No Auth → API Key Auth
**1. Deploy with optional auth:**
```typescript
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (authHeader) {
// Validate if provided
const apiKey = authHeader.replace('Bearer ', '');
const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);
if (!isValid) {
return c.json({ error: 'Invalid API key' }, 403);
}
}
// Continue even without auth (for migration period)
await next();
});
```
**2. Notify clients to add auth**
**3. Make auth required after transition:**
```typescript
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader) {
return c.json({ error: 'Authentication required' }, 401);
}
// ... validate
});
```
---
**Last Updated:** 2025-10-28
**Verified With:** Cloudflare Workers, @modelcontextprotocol/sdk@1.20.2

View File

@@ -0,0 +1,262 @@
# Cloudflare Agents SDK vs Standalone TypeScript MCP
Decision guide for choosing between Cloudflare Agents SDK and standalone TypeScript MCP servers.
---
## Quick Decision Matrix
| Need | Cloudflare Agents | Standalone MCP |
|------|-------------------|----------------|
| Stateless tools | ⚠️ Overkill | ✅ Perfect |
| Stateful agents | ✅ Perfect | ❌ Not ideal |
| WebSockets | ✅ Built-in | ❌ Not supported |
| Persistent storage | ✅ SQLite (1GB) | ⚠️ Use D1/KV |
| Cost (low traffic) | ⚠️ Higher | ✅ Lower |
| Setup complexity | ⚠️ Medium | ✅ Simple |
| Portability | ❌ Cloudflare only | ✅ Any platform |
---
## Cloudflare Agents SDK
**Best for:**
- Chatbots with conversation history
- Long-running agent sessions
- WebSocket-based applications
- Agents that need memory
- Scheduled agent tasks
**Architecture:**
```
Client → Workers → Durable Objects (with SQLite) → AI Models
MCP Tools
```
**Example use cases:**
- AI chatbot with memory
- Customer support agent
- Multi-turn conversations
- Scheduled agent workflows
**Pricing:**
- Durable Objects: $0.15/million requests
- Storage: $0.20/GB-month
- WebSocket connections: $0.01/million messages
---
## Standalone TypeScript MCP
**Best for:**
- Stateless tool exposure
- API integrations
- Database queries
- Edge-deployed functions
- Simple MCP servers
**Architecture:**
```
Client → Workers → MCP Server → External APIs/D1/KV/R2
```
**Example use cases:**
- Weather API tool
- Database query tool
- File storage tool
- Calculator/utility tools
**Pricing:**
- Workers: $0.50/million requests (10ms CPU)
- No Durable Objects overhead
---
## Detailed Comparison
### State Management
**Cloudflare Agents:**
```typescript
import { Agent } from '@cloudflare/agents';
export class ChatAgent extends Agent {
async handleMessage(message: string) {
// Access SQLite storage
const history = await this.state.sql.exec(
'SELECT * FROM messages ORDER BY timestamp DESC LIMIT 10'
);
// State persists across requests
return `Based on our history: ${history}...`;
}
}
```
**Standalone MCP:**
```typescript
// Stateless - no built-in state
server.registerTool('query', { ... }, async (args, env) => {
// Must use external storage (D1, KV, etc.)
const data = await env.DB.prepare('SELECT ...').all();
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
});
```
### WebSocket Support
**Cloudflare Agents:**
```typescript
// Built-in WebSocket support
export class RealtimeAgent extends Agent {
async webSocketMessage(ws: WebSocket, message: string) {
// Handle real-time messages
ws.send(`Received: ${message}`);
}
}
```
**Standalone MCP:**
```typescript
// No WebSocket support
// Use HTTP/SSE only
app.post('/mcp', async (c) => {
// Request-response only
});
```
### Deployment
**Cloudflare Agents:**
```bash
# Requires Durable Objects migration
wrangler deploy
# Need to configure DO bindings
```
**Standalone MCP:**
```bash
# Simple deployment
wrangler deploy
# Works immediately
```
---
## Cost Analysis
### Low Traffic (1,000 requests/day)
**Cloudflare Agents:**
- Durable Objects: ~$0.0045/day
- Storage: ~$0.007/day
- **Total: ~$4/month**
**Standalone MCP:**
- Workers: ~$0.0015/day
- **Total: ~$0.50/month**
### High Traffic (1,000,000 requests/day)
**Cloudflare Agents:**
- Durable Objects: ~$150/day
- Storage: ~$6/day
- **Total: ~$4,680/month**
**Standalone MCP:**
- Workers: ~$15/day
- **Total: ~$450/month**
**Winner for cost:** Standalone MCP (10x cheaper at scale)
---
## Feature Comparison
| Feature | Agents SDK | Standalone |
|---------|------------|------------|
| **Tools** | ✅ | ✅ |
| **Resources** | ✅ | ✅ |
| **Prompts** | ✅ | ✅ |
| **Persistent State** | ✅ SQLite | ⚠️ D1/KV |
| **WebSockets** | ✅ | ❌ |
| **Scheduled Tasks** | ✅ Alarms | ⚠️ Cron Triggers |
| **Global Replication** | ✅ Automatic | ⚠️ Manual |
| **Cold Start** | ⚠️ Slower | ✅ Faster |
| **Portability** | ❌ CF only | ✅ Any platform |
---
## Migration Path
### From Standalone → Agents SDK
**When to migrate:**
- Need conversation history
- Want WebSocket support
- Require stateful agents
**Migration steps:**
1. Create Durable Object class extending `Agent`
2. Move tools to agent methods
3. Add state management (SQLite)
4. Update wrangler.jsonc with DO bindings
5. Deploy with migration
### From Agents SDK → Standalone
**When to migrate:**
- Don't need state persistence
- Want lower costs
- Need platform portability
**Migration steps:**
1. Extract tools to standalone MCP server
2. Move state to D1/KV if needed
3. Remove DO bindings
4. Simplify deployment
---
## Hybrid Approach
Use both for different use cases:
```
Cloudflare Workers
├── /agents (Agents SDK)
│ └── Chatbot with memory
└── /mcp (Standalone MCP)
├── Weather tool (stateless)
├── Database tool (stateless)
└── Calculator tool (stateless)
```
**Benefits:**
- Optimize cost per use case
- Use best tool for each job
- Maintain flexibility
---
## Recommendations
### Use Cloudflare Agents SDK when:
- ✅ Building conversational AI with memory
- ✅ Need real-time WebSocket connections
- ✅ Agents require persistent state
- ✅ Multi-turn interactions
- ✅ Budget allows higher costs
### Use Standalone TypeScript MCP when:
- ✅ Exposing stateless tools/APIs
- ✅ Cost optimization is priority
- ✅ Need platform portability
- ✅ Simple request-response pattern
- ✅ Edge deployment for low latency
---
**Last Updated:** 2025-10-28
**Verified:** Cloudflare Agents SDK + @modelcontextprotocol/sdk@1.20.2

View File

@@ -0,0 +1,528 @@
# Cloudflare Services Integration
Guide to integrating Cloudflare services (D1, KV, R2, Vectorize, Workers AI) with MCP servers.
---
## D1 (SQL Database)
### Setup
```bash
# Create database
wrangler d1 create my-database
# Add to wrangler.jsonc
```
```jsonc
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "YOUR_DATABASE_ID"
}
]
}
```
### MCP Tool Example
```typescript
server.registerTool(
'query-users',
{
description: 'Queries users from D1 database',
inputSchema: z.object({
email: z.string().optional(),
limit: z.number().default(10)
})
},
async ({ email, limit }, env) => {
const query = email
? 'SELECT * FROM users WHERE email = ? LIMIT ?'
: 'SELECT * FROM users LIMIT ?';
const params = email ? [email, limit] : [limit];
const result = await env.DB.prepare(query).bind(...params).all();
return {
content: [{
type: 'text',
text: JSON.stringify(result.results, null, 2)
}]
};
}
);
```
### Best Practices
- Use parameterized queries (never string interpolation)
- Add indexes for common queries
- Limit result set size
- Use transactions for multi-step operations
---
## KV (Key-Value Store)
### Setup
```bash
# Create namespace
wrangler kv namespace create CACHE
# Add to wrangler.jsonc
```
```jsonc
{
"kv_namespaces": [
{
"binding": "CACHE",
"id": "YOUR_NAMESPACE_ID"
}
]
}
```
### MCP Tool Example
```typescript
server.registerTool(
'cache-get',
{
description: 'Gets value from KV cache',
inputSchema: z.object({ key: z.string() })
},
async ({ key }, env) => {
const value = await env.CACHE.get(key);
return {
content: [{
type: 'text',
text: value || `Key "${key}" not found`
}]
};
}
);
server.registerTool(
'cache-set',
{
description: 'Sets value in KV cache',
inputSchema: z.object({
key: z.string(),
value: z.string(),
ttl: z.number().optional()
})
},
async ({ key, value, ttl }, env) => {
await env.CACHE.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
return {
content: [{ type: 'text', text: `Cached "${key}"` }]
};
}
);
```
---
## R2 (Object Storage)
### Setup
```bash
# Create bucket
wrangler r2 bucket create my-bucket
# Add to wrangler.jsonc
```
```jsonc
{
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "my-bucket"
}
]
}
```
### MCP Tool Example
```typescript
server.registerTool(
'r2-upload',
{
description: 'Uploads file to R2',
inputSchema: z.object({
key: z.string(),
content: z.string(),
contentType: z.string().optional()
})
},
async ({ key, content, contentType }, env) => {
await env.BUCKET.put(key, content, {
httpMetadata: {
contentType: contentType || 'text/plain'
}
});
return {
content: [{ type: 'text', text: `Uploaded to ${key}` }]
};
}
);
server.registerTool(
'r2-download',
{
description: 'Downloads file from R2',
inputSchema: z.object({ key: z.string() })
},
async ({ key }, env) => {
const object = await env.BUCKET.get(key);
if (!object) {
return {
content: [{ type: 'text', text: `File "${key}" not found` }],
isError: true
};
}
const text = await object.text();
return {
content: [{
type: 'text',
text: `File: ${key}\nSize: ${object.size} bytes\n\n${text}`
}]
};
}
);
```
---
## Vectorize (Vector Database)
### Setup
```bash
# Create index
wrangler vectorize create my-index --dimensions=768 --metric=cosine
# Add to wrangler.jsonc
```
```jsonc
{
"vectorize": [
{
"binding": "VECTORIZE",
"index_name": "my-index"
}
]
}
```
### MCP Tool Example
```typescript
server.registerTool(
'semantic-search',
{
description: 'Searches documents semantically',
inputSchema: z.object({
query: z.string(),
topK: z.number().default(5)
})
},
async ({ query, topK }, env) => {
// Generate embedding (using Workers AI)
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: query
});
// Search vector index
const results = await env.VECTORIZE.query(embedding.data[0], {
topK,
returnMetadata: true
});
return {
content: [{
type: 'text',
text: JSON.stringify(results.matches, null, 2)
}]
};
}
);
server.registerTool(
'index-document',
{
description: 'Indexes document for semantic search',
inputSchema: z.object({
id: z.string(),
text: z.string(),
metadata: z.record(z.any()).optional()
})
},
async ({ id, text, metadata }, env) => {
// Generate embedding
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text
});
// Insert into index
await env.VECTORIZE.insert([{
id,
values: embedding.data[0],
metadata: metadata || {}
}]);
return {
content: [{ type: 'text', text: `Indexed document "${id}"` }]
};
}
);
```
---
## Workers AI
### Setup
```jsonc
{
"ai": {
"binding": "AI"
}
}
```
### MCP Tool Example
```typescript
server.registerTool(
'generate-text',
{
description: 'Generates text using LLM',
inputSchema: z.object({
prompt: z.string(),
model: z.string().default('@cf/meta/llama-3-8b-instruct')
})
},
async ({ prompt, model }, env) => {
const response = await env.AI.run(model, {
prompt
});
return {
content: [{ type: 'text', text: response.response }]
};
}
);
server.registerTool(
'generate-image',
{
description: 'Generates image from text',
inputSchema: z.object({
prompt: z.string()
})
},
async ({ prompt }, env) => {
const response = await env.AI.run(
'@cf/stabilityai/stable-diffusion-xl-base-1.0',
{ prompt }
);
// Save to R2
const imageKey = `generated/${Date.now()}.png`;
await env.BUCKET.put(imageKey, response);
return {
content: [{
type: 'text',
text: `Image generated: ${imageKey}`
}]
};
}
);
```
---
## Queues
### Setup
```bash
# Create queue
wrangler queues create my-queue
# Add to wrangler.jsonc
```
```jsonc
{
"queues": {
"producers": [
{
"binding": "MY_QUEUE",
"queue": "my-queue"
}
],
"consumers": [
{
"queue": "my-queue",
"max_batch_size": 10,
"max_batch_timeout": 30
}
]
}
}
```
### MCP Tool Example
```typescript
server.registerTool(
'enqueue-task',
{
description: 'Enqueues background task',
inputSchema: z.object({
task: z.string(),
data: z.record(z.any())
})
},
async ({ task, data }, env) => {
await env.MY_QUEUE.send({
task,
data,
timestamp: Date.now()
});
return {
content: [{ type: 'text', text: `Task "${task}" enqueued` }]
};
}
);
```
**Consumer handler:**
```typescript
export default {
async fetch(request, env) {
return app.fetch(request, env);
},
async queue(batch, env) {
for (const message of batch.messages) {
console.log('Processing:', message.body);
// Process task...
}
}
};
```
---
## Analytics Engine
### Setup
```jsonc
{
"analytics_engine_datasets": [
{
"binding": "ANALYTICS"
}
]
}
```
### MCP Tool Example
```typescript
server.registerTool(
'log-event',
{
description: 'Logs analytics event',
inputSchema: z.object({
event: z.string(),
metadata: z.record(z.any()).optional()
})
},
async ({ event, metadata }, env) => {
env.ANALYTICS.writeDataPoint({
blobs: [event],
doubles: [Date.now()],
indexes: [event]
});
return {
content: [{ type: 'text', text: `Event "${event}" logged` }]
};
}
);
```
---
## Combining Services
### Example: RAG System
```typescript
server.registerTool(
'ask-question',
{
description: 'Answers question using RAG',
inputSchema: z.object({ question: z.string() })
},
async ({ question }, env) => {
// 1. Generate embedding
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: question
});
// 2. Search vector index
const results = await env.VECTORIZE.query(embedding.data[0], {
topK: 3,
returnMetadata: true
});
// 3. Get context from D1
const context = await env.DB
.prepare('SELECT content FROM documents WHERE id IN (?, ?, ?)')
.bind(...results.matches.map(m => m.id))
.all();
// 4. Generate answer with LLM
const prompt = `Context: ${JSON.stringify(context.results)}\n\nQuestion: ${question}`;
const answer = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt
});
// 5. Cache result
await env.CACHE.put(`answer:${question}`, answer.response, {
expirationTtl: 3600
});
return {
content: [{ type: 'text', text: answer.response }]
};
}
);
```
---
**Last Updated:** 2025-10-28

465
references/common-errors.md Normal file
View File

@@ -0,0 +1,465 @@
# Common Errors in TypeScript MCP Servers
This document details 10+ production issues that occur when building MCP servers with TypeScript on Cloudflare Workers, along with their solutions.
---
## Error 1: Export Syntax Issues (CRITICAL)
**Error Message:**
```
Cannot read properties of undefined (reading 'map')
```
**Source:** honojs/hono#3955, honojs/vite-plugins#237
**Why It Happens:**
Vite + Cloudflare Workers require direct export of the Hono app, not an object wrapper. The object wrapper `{ fetch: app.fetch }` causes cryptic build errors because Vite's module resolution fails to properly handle the wrapped export.
**Prevention:**
```typescript
// ❌ WRONG - Causes build errors
export default { fetch: app.fetch };
// ✅ CORRECT - Direct export
export default app;
// Also works for Module Worker format:
export default {
fetch: app.fetch,
scheduled: async (event, env, ctx) => { /* cron handler */ }
};
// But ONLY if you need scheduled/queue/DO handlers
```
**How to Fix:**
1. Search your codebase for `export default { fetch:`
2. Replace with direct export: `export default app;`
3. Rebuild: `npm run build`
---
## Error 2: Unclosed Transport Connections
**Error Symptoms:**
- Memory leaks in production
- Hanging connections
- Gradual performance degradation
**Source:** Best practice from MCP SDK maintainers
**Why It Happens:**
`StreamableHTTPServerTransport` maintains an open connection. If not explicitly closed when the HTTP response ends, the connection remains open, consuming memory.
**Prevention:**
```typescript
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
// CRITICAL: Always close on response end
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
```
**How to Fix:**
1. Add `c.res.raw.on('close', () => transport.close());` after creating transport
2. Redeploy
---
## Error 3: Tool Schema Validation Failure
**Error Message:**
```
ListTools request handler fails to generate inputSchema
```
**Source:** GitHub modelcontextprotocol/typescript-sdk#1028
**Why It Happens:**
Zod schemas need to be converted to JSON Schema for MCP protocol compliance. The SDK handles this automatically, but errors occur if schemas are malformed or if manual conversion is attempted incorrectly.
**Prevention:**
```typescript
// ✅ CORRECT - SDK handles Zod → JSON Schema conversion
server.registerTool(
'tool-name',
{
description: 'Tool description',
inputSchema: z.object({
a: z.number().describe('First number'),
b: z.number().describe('Second number')
})
},
async ({ a, b }) => {
return { content: [{ type: 'text', text: String(a + b) }] };
}
);
// ❌ WRONG - Manual conversion not needed
import { zodToJsonSchema } from 'zod-to-json-schema';
server.registerTool('tool-name', {
inputSchema: zodToJsonSchema(schema) // Don't do this
}, handler);
```
**How to Fix:**
1. Ensure using SDK v1.20.2+
2. Pass Zod schema directly to `inputSchema`
3. Do NOT manually convert with `zodToJsonSchema()` unless absolutely necessary
---
## Error 4: Tool Arguments Not Passed to Handler
**Error Symptoms:**
- Handler receives `undefined` for all arguments
- Tool execution fails with "cannot read property"
**Source:** GitHub modelcontextprotocol/typescript-sdk#1026
**Why It Happens:**
Type mismatch between schema definition and handler signature. The SDK expects exact type alignment.
**Prevention:**
```typescript
// Define schema
const schema = z.object({
a: z.number(),
b: z.number()
});
// Infer type from schema
type Input = z.infer<typeof schema>;
// Use typed handler
server.registerTool(
'add',
{ description: 'Adds numbers', inputSchema: schema },
async (args: Input) => { // Type must match schema
// args.a and args.b are properly typed and passed
return {
content: [{ type: 'text', text: String(args.a + args.b) }]
};
}
);
```
**How to Fix:**
1. Use `z.infer<typeof schema>` to get the type
2. Type the handler parameter explicitly
3. Ensure parameter names match schema keys exactly
---
## Error 5: CORS Misconfiguration
**Error Symptoms:**
- Browser clients can't connect
- "No 'Access-Control-Allow-Origin' header" error
- Preflight (OPTIONS) requests fail
**Source:** Common production issue
**Why It Happens:**
MCP servers accessed from browsers need CORS headers. Cloudflare Workers don't add these by default.
**Prevention:**
```typescript
import { cors } from 'hono/cors';
app.use('/mcp', cors({
origin: [
'http://localhost:3000', // Dev
'https://your-app.com' // Prod
],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true // If using cookies/auth
}));
app.post('/mcp', async (c) => {
// MCP handler
});
```
**How to Fix:**
1. Install: `npm install hono`
2. Add CORS middleware before MCP endpoint
3. Update `origin` array with your domains
4. Redeploy
---
## Error 6: Missing Rate Limiting
**Error Symptoms:**
- API abuse
- DDoS vulnerability
- Unexpected high costs
**Source:** Production security best practice
**Why It Happens:**
Public MCP endpoints without rate limiting are vulnerable to abuse.
**Prevention:**
```typescript
app.post('/mcp', async (c) => {
// Rate limit by IP
const ip = c.req.header('CF-Connecting-IP') || 'unknown';
const rateLimitKey = `ratelimit:${ip}:${Math.floor(Date.now() / 60000)}`;
const count = await c.env.CACHE.get(rateLimitKey);
if (count && parseInt(count) >= 100) { // 100 req/min
return c.json({ error: 'Rate limit exceeded' }, 429);
}
await c.env.CACHE.put(
rateLimitKey,
String((parseInt(count || '0') + 1)),
{ expirationTtl: 60 }
);
// Continue with MCP handler
});
```
**How to Fix:**
1. Add KV namespace for rate limiting
2. Implement IP-based rate limiting
3. Adjust limits based on your needs
4. Consider using Cloudflare Rate Limiting (paid feature)
---
## Error 7: TypeScript Compilation Memory Issues
**Error Message:**
```
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
```
**Source:** GitHub modelcontextprotocol/typescript-sdk#985
**Why It Happens:**
The MCP SDK has a large dependency tree. TypeScript compilation can exceed default Node.js memory limits (512MB).
**Prevention:**
```json
// package.json
{
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=4096' tsc && vite build",
"typecheck": "NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit"
}
}
```
**How to Fix:**
1. Update build scripts with NODE_OPTIONS
2. Increase to 4096MB (4GB)
3. If still failing, try 8192MB
4. Consider using `tsup` instead of raw `tsc` for faster builds
---
## Error 8: UriTemplate ReDoS Vulnerability
**Error Symptoms:**
- Server hangs on certain URI patterns
- CPU maxed out
- Timeouts
**Source:** GitHub modelcontextprotocol/typescript-sdk#965 (Security)
**Why It Happens:**
Regex denial-of-service (ReDoS) in URI template parsing. Malicious URIs with nested patterns cause exponential regex evaluation.
**Prevention:**
Update to SDK v1.20.2 or later (includes fix):
```bash
npm install @modelcontextprotocol/sdk@latest
```
**How to Fix:**
1. Check current version: `npm list @modelcontextprotocol/sdk`
2. If < 1.20.2, update: `npm install @modelcontextprotocol/sdk@latest`
3. Rebuild and redeploy
---
## Error 9: Authentication Bypass
**Error Symptoms:**
- Unauthorized access to tools
- API abuse
- Data leaks
**Source:** Production security best practice
**Why It Happens:**
MCP servers deployed without authentication are publicly accessible.
**Prevention:**
```typescript
// Use API key authentication
app.use('/mcp', async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const apiKey = authHeader.replace('Bearer ', '');
const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);
if (!isValid) {
return c.json({ error: 'Invalid API key' }, 403);
}
await next();
});
```
**How to Fix:**
1. Add authentication middleware (see `authenticated-server.ts` template)
2. Create KV namespace for API keys
3. Generate secure API keys
4. Distribute keys securely to authorized clients
---
## Error 10: Environment Variable Leakage
**Error Symptoms:**
- Secrets exposed in logs
- API keys visible in error messages
- Security breach
**Source:** Cloudflare Workers security best practice
**Why It Happens:**
Logging or returning `env` objects exposes all secrets.
**Prevention:**
```typescript
// ❌ WRONG - Exposes ALL secrets
console.log('Env:', JSON.stringify(env));
return c.json({ env }, 500);
// ✅ CORRECT - Never log env directly
try {
const apiKey = env.MY_API_KEY; // Use specific keys only
// ... use apiKey
} catch (error) {
console.error('Operation failed:', error.message); // Safe
return c.json({ error: 'Internal error' }, 500);
}
```
**How to Fix:**
1. Search codebase for `console.log(env` or `JSON.stringify(env)`
2. Remove all env logging
3. Use specific environment variables only
4. Never return env in responses
---
## Additional Common Issues
### Issue 11: Missing Zod Descriptions
**Why It Matters:**
LLMs use parameter descriptions to understand tools. Missing descriptions = poor tool usage.
**Fix:**
```typescript
// ❌ BAD - No descriptions
z.object({ name: z.string(), age: z.number() })
// ✅ GOOD - Clear descriptions
z.object({
name: z.string().describe('User full name'),
age: z.number().describe('User age in years')
})
```
### Issue 12: Large Response Payloads
**Problem:**
Returning huge JSON objects or binary data causes timeouts.
**Fix:**
```typescript
// Limit response size
const MAX_RESPONSE_SIZE = 100000; // 100KB
server.registerTool('fetch-data', { ... }, async ({ url }) => {
const response = await fetch(url);
const text = await response.text();
if (text.length > MAX_RESPONSE_SIZE) {
return {
content: [{
type: 'text',
text: `Response too large (${text.length} bytes). First 100KB:\n${text.slice(0, MAX_RESPONSE_SIZE)}`
}]
};
}
return { content: [{ type: 'text', text }] };
});
```
### Issue 13: Not Handling Async Errors
**Problem:**
Unhandled promise rejections crash Workers.
**Fix:**
```typescript
server.registerTool('risky-operation', { ... }, async (args) => {
try {
const result = await riskyAsyncOperation(args);
return { content: [{ type: 'text', text: result }] };
} catch (error) {
return {
content: [{
type: 'text',
text: `Operation failed: ${(error as Error).message}`
}],
isError: true
};
}
});
```
---
## Debugging Checklist
When encountering MCP server issues:
1. [ ] Check SDK version (`npm list @modelcontextprotocol/sdk`)
2. [ ] Verify export syntax (direct export, not object wrapper)
3. [ ] Confirm transport is closed on response end
4. [ ] Test with MCP Inspector (`npx @modelcontextprotocol/inspector`)
5. [ ] Check Cloudflare Workers logs (`wrangler tail`)
6. [ ] Verify authentication middleware (if production)
7. [ ] Test CORS headers (if browser clients)
8. [ ] Review Zod schemas for proper types
9. [ ] Check rate limiting implementation
10. [ ] Ensure no env variables are logged
---
**Last Updated:** 2025-10-28
**Verified Against:** @modelcontextprotocol/sdk@1.20.2

View File

@@ -0,0 +1,390 @@
# Deployment Guide for TypeScript MCP Servers
Complete guide to deploying MCP servers on Cloudflare Workers.
---
## Quick Deployment
```bash
# Build
npm run build
# Deploy
wrangler deploy
```
---
## Environment Setup
### Development (.dev.vars)
Create `.dev.vars` for local secrets:
```bash
WEATHER_API_KEY=abc123
DATABASE_URL=http://localhost:3306
```
**Never commit `.dev.vars` to git!**
### Production Secrets
```bash
# Set secrets
wrangler secret put WEATHER_API_KEY
wrangler secret put DATABASE_URL
# List secrets
wrangler secret list
# Delete secret
wrangler secret delete OLD_KEY
```
---
## Multiple Environments
**wrangler.jsonc:**
```jsonc
{
"name": "mcp-server",
"main": "src/index.ts",
"env": {
"staging": {
"name": "mcp-server-staging",
"vars": {
"ENVIRONMENT": "staging"
},
"d1_databases": [
{ "binding": "DB", "database_id": "staging-db-id" }
]
},
"production": {
"name": "mcp-server-production",
"vars": {
"ENVIRONMENT": "production"
},
"d1_databases": [
{ "binding": "DB", "database_id": "prod-db-id" }
]
}
}
}
```
**Deploy to specific environment:**
```bash
wrangler deploy --env staging
wrangler deploy --env production
```
---
## Custom Domains
### Setup
1. **Add domain in Cloudflare dashboard:**
- Workers & Pages → your worker → Settings → Domains & Routes
- Add custom domain: `mcp.example.com`
2. **Or via wrangler.jsonc:**
```jsonc
{
"routes": [
{
"pattern": "mcp.example.com/*",
"custom_domain": true
}
]
}
```
### SSL/TLS
Cloudflare provides automatic SSL certificates for custom domains.
---
## CI/CD with GitHub Actions
**.github/workflows/deploy.yml:**
```yaml
name: Deploy MCP Server
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
```
### Setup Secrets
1. **Get Cloudflare API token:**
- Dashboard → My Profile → API Tokens
- Create token with "Edit Cloudflare Workers" permissions
2. **Add to GitHub:**
- Repository → Settings → Secrets → Actions
- Add `CLOUDFLARE_API_TOKEN`
- Add `CLOUDFLARE_ACCOUNT_ID`
---
## Database Migrations
### D1 Migrations
**Create migration:**
```bash
wrangler d1 migrations create my-db add-users-table
```
**migrations/0001_add_users_table.sql:**
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
**Apply migrations:**
```bash
# Local
wrangler d1 migrations apply my-db --local
# Production
wrangler d1 migrations apply my-db --remote
```
**In CI/CD:**
```yaml
- name: Run D1 migrations
run: wrangler d1 migrations apply my-db --remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
```
---
## Monitoring & Logs
### Real-time Logs
```bash
# Tail logs
wrangler tail
# Filter by status
wrangler tail --status error
# Filter by method
wrangler tail --method POST
```
### Workers Analytics
View in dashboard:
- Requests per second
- Error rate
- CPU time
- Bandwidth
### Custom Logging
```typescript
app.post('/mcp', async (c) => {
console.log('MCP request:', {
method: c.req.method,
path: c.req.path,
timestamp: new Date().toISOString()
});
// ... handle request
console.log('MCP response:', { status: 200, duration: '15ms' });
});
```
---
## Rollback Strategy
### Quick Rollback
```bash
# List deployments
wrangler deployments list
# Rollback to specific deployment
wrangler rollback --deployment-id abc123
```
### Git-based Rollback
```bash
# Revert to previous commit
git revert HEAD
git push
# CI/CD will auto-deploy reverted version
```
---
## Performance Optimization
### 1. Enable Compression
Cloudflare automatically compresses responses. No configuration needed.
### 2. Caching
```typescript
app.get('/mcp-schema', async (c) => {
const schema = { ... };
return c.json(schema, 200, {
'Cache-Control': 'public, max-age=3600',
'CDN-Cache-Control': 'max-age=86400'
});
});
```
### 3. Edge Caching with KV
```typescript
async function getCachedOrFetch(key: string, fetcher: () => Promise<string>, env: Env) {
const cached = await env.CACHE.get(key);
if (cached) return cached;
const fresh = await fetcher();
await env.CACHE.put(key, fresh, { expirationTtl: 3600 });
return fresh;
}
```
---
## Health Checks
```typescript
app.get('/health', (c) => {
return c.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: '1.0.0'
});
});
```
**Monitor with UptimeRobot, Pingdom, etc.**
---
## Cost Optimization
### Workers Pricing
- Free: 100,000 requests/day
- Paid: $5/month + $0.50/million requests
### Tips
1. **Use KV for caching** (reduces computation)
2. **Optimize D1 queries** (use indexes)
3. **Batch operations** where possible
4. **Set reasonable rate limits**
5. **Monitor usage** in dashboard
---
## Security Checklist
Before production:
- [ ] Authentication implemented
- [ ] Rate limiting enabled
- [ ] CORS configured correctly
- [ ] Secrets in Wrangler secrets (not code)
- [ ] Error messages don't leak data
- [ ] HTTPS only (enforced by CF)
- [ ] Input validation on all tools
- [ ] SQL injection protection
- [ ] API keys rotated regularly
---
## Troubleshooting Deployments
### Deployment Fails
```bash
# Check syntax
npm run build
# Validate wrangler.jsonc
wrangler deploy --dry-run
# View detailed logs
wrangler deploy --verbose
```
### Worker Not Responding
```bash
# Check logs
wrangler tail
# Test locally first
wrangler dev
# Verify bindings
wrangler d1 list
wrangler kv namespace list
```
### Performance Issues
```bash
# Check CPU time
wrangler tail --status ok | grep "CPU time"
# Profile with Analytics
# Dashboard → Workers → Analytics
```
---
**Last Updated:** 2025-10-28

343
references/testing-guide.md Normal file
View File

@@ -0,0 +1,343 @@
# Testing Guide for TypeScript MCP Servers
Comprehensive testing strategies for MCP servers.
---
## Testing Levels
1. **Unit Tests** - Test tool logic in isolation
2. **Integration Tests** - Test with MCP Inspector
3. **E2E Tests** - Test with real MCP clients
---
## 1. Unit Testing with Vitest
### Setup
```bash
npm install -D vitest @cloudflare/vitest-pool-workers
```
**vitest.config.ts:**
```typescript
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' }
}
}
}
});
```
### Test Example
**src/tools/calculator.test.ts:**
```typescript
import { describe, it, expect } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
describe('Calculator Tool', () => {
it('should add two numbers', async () => {
const server = new McpServer({ name: 'test', version: '1.0.0' });
server.registerTool(
'add',
{
description: 'Adds numbers',
inputSchema: z.object({
a: z.number(),
b: z.number()
})
},
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }]
})
);
const result = await server.callTool('add', { a: 5, b: 3 });
expect(result.content[0].text).toBe('8');
});
it('should handle errors', async () => {
const server = new McpServer({ name: 'test', version: '1.0.0' });
server.registerTool(
'divide',
{
description: 'Divides numbers',
inputSchema: z.object({ a: z.number(), b: z.number() })
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: 'text', text: 'Division by zero' }],
isError: true
};
}
return { content: [{ type: 'text', text: String(a / b) }] };
}
);
const result = await server.callTool('divide', { a: 10, b: 0 });
expect(result.isError).toBe(true);
});
});
```
**Run tests:**
```bash
npx vitest
```
---
## 2. Integration Testing with MCP Inspector
### Setup
```bash
# Terminal 1: Start dev server
wrangler dev
# Terminal 2: Launch inspector
npx @modelcontextprotocol/inspector
```
### Inspector UI
1. Connect to: `http://localhost:8787/mcp`
2. View available tools/resources
3. Test tool execution
4. Inspect request/response
### Automated Testing
```typescript
import { test, expect } from '@playwright/test';
test('MCP server lists tools', async ({ page }) => {
await page.goto('http://localhost:8787');
const response = await page.request.post('http://localhost:8787/mcp', {
data: {
jsonrpc: '2.0',
method: 'tools/list',
id: 1
}
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.result.tools).toBeDefined();
expect(data.result.tools.length).toBeGreaterThan(0);
});
```
---
## 3. E2E Testing
### With curl
```bash
# List tools
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}'
# Call tool
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 5, "b": 3 }
},
"id": 2
}'
```
### With TypeScript
```typescript
import { test, expect } from 'vitest';
test('Full MCP flow', async () => {
const baseUrl = 'http://localhost:8787/mcp';
// List tools
const listResponse = await fetch(baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1
})
});
const listData = await listResponse.json();
expect(listData.result.tools).toBeDefined();
// Call tool
const callResponse = await fetch(baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'add',
arguments: { a: 5, b: 3 }
},
id: 2
})
});
const callData = await callResponse.json();
expect(callData.result.content[0].text).toBe('8');
});
```
---
## 4. Testing Authentication
```typescript
test('rejects without auth', async () => {
const response = await fetch('http://localhost:8787/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1
})
});
expect(response.status).toBe(401);
});
test('accepts with valid API key', async () => {
const response = await fetch('http://localhost:8787/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-key-123'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1
})
});
expect(response.status).toBe(200);
});
```
---
## 5. Load Testing
### With Artillery
```bash
npm install -D artillery
```
**artillery.yml:**
```yaml
config:
target: 'http://localhost:8787'
phases:
- duration: 60
arrivalRate: 10
scenarios:
- name: 'List tools'
flow:
- post:
url: '/mcp'
json:
jsonrpc: '2.0'
method: 'tools/list'
id: 1
```
```bash
npx artillery run artillery.yml
```
---
## 6. Mocking External APIs
```typescript
import { vi } from 'vitest';
test('weather tool with mocked API', async () => {
// Mock fetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
main: { temp: 20 },
weather: [{ description: 'sunny' }]
})
});
const server = new McpServer({ name: 'test', version: '1.0.0' });
// Register weather tool...
const result = await server.callTool('get-weather', { city: 'London' });
expect(result.content[0].text).toContain('20');
});
```
---
## CI/CD Testing
**GitHub Actions:**
```yaml
name: Test MCP Server
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- run: npm run build
# Integration test
- run: |
npm run dev &
sleep 5
curl -f http://localhost:8787/mcp \
-X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
```
---
**Last Updated:** 2025-10-28

412
references/tool-patterns.md Normal file
View File

@@ -0,0 +1,412 @@
# Common Tool Implementation Patterns
Production-tested patterns for implementing MCP tools in TypeScript.
---
## Pattern 1: External API Wrapper
Wrap external REST APIs as MCP tools.
```typescript
server.registerTool(
'fetch-weather',
{
description: 'Fetches weather data from OpenWeatherMap API',
inputSchema: z.object({
city: z.string().describe('City name'),
units: z.enum(['metric', 'imperial']).default('metric')
})
},
async ({ city, units }, env) => {
try {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${env.WEATHER_API_KEY}`
);
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const data = await response.json();
return {
content: [{
type: 'text',
text: JSON.stringify(data, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
**Best Practices:**
- Always validate API keys exist before calling
- Use proper URL encoding for parameters
- Handle HTTP errors gracefully
- Return structured error messages
- Consider caching responses in KV
---
## Pattern 2: Database Query Tool
Execute SQL queries on D1 database.
```typescript
server.registerTool(
'search-users',
{
description: 'Searches users in database',
inputSchema: z.object({
query: z.string().describe('Search query'),
limit: z.number().default(10).max(100)
})
},
async ({ query, limit }, env) => {
if (!env.DB) {
return {
content: [{ type: 'text', text: 'Database not configured' }],
isError: true
};
}
try {
const result = await env.DB
.prepare('SELECT id, name, email FROM users WHERE name LIKE ? LIMIT ?')
.bind(`%${query}%`, limit)
.all();
return {
content: [{
type: 'text',
text: `Found ${result.results.length} users:\n${JSON.stringify(result.results, null, 2)}`
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `Database error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
**Security:**
- ⚠️ Never allow raw SQL injection
- Use parameterized queries only
- Limit result set size
- Don't expose sensitive fields (passwords, tokens)
---
## Pattern 3: File Operations (R2)
Read/write files from R2 object storage.
```typescript
server.registerTool(
'get-file',
{
description: 'Retrieves file from R2 storage',
inputSchema: z.object({
key: z.string().describe('File key/path')
})
},
async ({ key }, env) => {
if (!env.BUCKET) {
return {
content: [{ type: 'text', text: 'R2 not configured' }],
isError: true
};
}
try {
const object = await env.BUCKET.get(key);
if (!object) {
return {
content: [{ type: 'text', text: `File "${key}" not found` }],
isError: true
};
}
const text = await object.text();
return {
content: [{
type: 'text',
text: `File: ${key}\nSize: ${object.size} bytes\n\n${text}`
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `R2 error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
---
## Pattern 4: Validation & Transformation
Transform and validate data.
```typescript
server.registerTool(
'validate-email',
{
description: 'Validates and normalizes email addresses',
inputSchema: z.object({
email: z.string().describe('Email to validate')
})
},
async ({ email }) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);
const normalized = email.toLowerCase().trim();
return {
content: [{
type: 'text',
text: JSON.stringify({
valid: isValid,
original: email,
normalized,
domain: isValid ? normalized.split('@')[1] : null
}, null, 2)
}]
};
}
);
```
---
## Pattern 5: Multi-Step Operations
Chain multiple operations together.
```typescript
server.registerTool(
'analyze-and-store',
{
description: 'Analyzes text and stores result',
inputSchema: z.object({
text: z.string(),
key: z.string()
})
},
async ({ text, key }, env) => {
try {
// Step 1: Analyze
const wordCount = text.split(/\s+/).length;
const charCount = text.length;
const analysis = {
wordCount,
charCount,
avgWordLength: (charCount / wordCount).toFixed(2),
timestamp: new Date().toISOString()
};
// Step 2: Store in KV
await env.CACHE.put(key, JSON.stringify(analysis));
return {
content: [{
type: 'text',
text: `Analysis complete and stored at "${key}":\n${JSON.stringify(analysis, null, 2)}`
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
---
## Pattern 6: Streaming Responses
Handle large responses efficiently.
```typescript
server.registerTool(
'fetch-large-file',
{
description: 'Fetches and summarizes large file',
inputSchema: z.object({
url: z.string().url()
})
},
async ({ url }) => {
const MAX_SIZE = 100000; // 100KB
try {
const response = await fetch(url);
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
let text = '';
let totalSize = 0;
while (true) {
const { done, value } = await reader.read();
if (done || totalSize >= MAX_SIZE) break;
text += new TextDecoder().decode(value);
totalSize += value.length;
}
return {
content: [{
type: 'text',
text: totalSize >= MAX_SIZE
? `File too large. First 100KB:\n${text}`
: text
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
---
## Pattern 7: Caching Tool Responses
Cache expensive operations.
```typescript
server.registerTool(
'get-exchange-rate',
{
description: 'Gets currency exchange rate (cached)',
inputSchema: z.object({
from: z.string().length(3),
to: z.string().length(3)
})
},
async ({ from, to }, env) => {
const cacheKey = `exchange:${from}:${to}`;
// Check cache first
const cached = await env.CACHE.get(cacheKey);
if (cached) {
return {
content: [{
type: 'text',
text: `${from}${to}: ${cached} (cached)`
}]
};
}
// Fetch fresh data
try {
const response = await fetch(
`https://api.exchangerate-api.com/v4/latest/${from}`
);
const data = await response.json();
const rate = data.rates[to];
// Cache for 1 hour
await env.CACHE.put(cacheKey, String(rate), { expirationTtl: 3600 });
return {
content: [{
type: 'text',
text: `${from}${to}: ${rate} (fresh)`
}]
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
);
```
---
## Error Handling Best Practices
```typescript
server.registerTool('example', { ... }, async (args, env) => {
try {
// Operation
} catch (error) {
// Safe error handling
const message = error instanceof Error
? error.message
: 'Unknown error';
// Don't leak sensitive data
const safeMessage = message.replace(/api[_-]?key[s]?[:\s]+[^\s]+/gi, '[REDACTED]');
return {
content: [{ type: 'text', text: `Error: ${safeMessage}` }],
isError: true
};
}
});
```
---
## Tool Response Formats
### Text Response
```typescript
return {
content: [{ type: 'text', text: 'Result text' }]
};
```
### Multiple Content Blocks
```typescript
return {
content: [
{ type: 'text', text: 'Summary' },
{ type: 'text', text: 'Details: ...' }
]
};
```
### Error Response
```typescript
return {
content: [{ type: 'text', text: 'Error message' }],
isError: true
};
```
---
**Last Updated:** 2025-10-28