mcp_patterns.md•11.1 kB
# MCP Server Development Patterns
This document contains proven patterns for developing Model Context Protocol (MCP) servers using TypeScript and Cloudflare Workers, based on the implementation in this codebase.
## Core MCP Server Architecture
### Base Server Class Pattern
```typescript
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Authentication props from OAuth flow
type Props = {
login: string;
name: string;
email: string;
accessToken: string;
};
export class CustomMCP extends McpAgent<Env, Record<string, never>, Props> {
server = new McpServer({
name: "Your MCP Server Name",
version: "1.0.0",
});
// CRITICAL: Implement cleanup for Durable Objects
async cleanup(): Promise<void> {
try {
// Close database connections
await closeDb();
console.log('Database connections closed successfully');
} catch (error) {
console.error('Error during database cleanup:', error);
}
}
// CRITICAL: Durable Objects alarm handler
async alarm(): Promise<void> {
await this.cleanup();
}
// Initialize all tools and resources
async init() {
// Register tools here
this.registerTools();
// Register resources if needed
this.registerResources();
}
private registerTools() {
// Tool registration logic
}
private registerResources() {
// Resource registration logic
}
}
```
### Tool Registration Pattern
```typescript
// Basic tool registration
this.server.tool(
"toolName",
"Tool description for the LLM",
{
param1: z.string().describe("Parameter description"),
param2: z.number().optional().describe("Optional parameter"),
},
async ({ param1, param2 }) => {
try {
// Tool implementation
const result = await performOperation(param1, param2);
return {
content: [
{
type: "text",
text: `Success: ${JSON.stringify(result, null, 2)}`
}
]
};
} catch (error) {
console.error('Tool error:', error);
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
isError: true
}
]
};
}
}
);
```
### Conditional Tool Registration (Based on Permissions)
```typescript
// Permission-based tool availability
const ALLOWED_USERNAMES = new Set<string>([
'admin1',
'admin2'
]);
// Register privileged tools only for authorized users
if (ALLOWED_USERNAMES.has(this.props.login)) {
this.server.tool(
"privilegedTool",
"Tool only available to authorized users",
{ /* parameters */ },
async (params) => {
// Privileged operation
return {
content: [
{
type: "text",
text: `Privileged operation executed by: ${this.props.login}`
}
]
};
}
);
}
```
## Database Integration Patterns
### Database Connection Pattern
```typescript
import { withDatabase, validateSqlQuery, isWriteOperation, formatDatabaseError } from "./database";
// Database operation with connection management
async function performDatabaseOperation(sql: string) {
try {
// Validate SQL query
const validation = validateSqlQuery(sql);
if (!validation.isValid) {
return {
content: [
{
type: "text",
text: `Invalid SQL query: ${validation.error}`,
isError: true
}
]
};
}
// Execute with automatic connection management
return await withDatabase(this.env.DATABASE_URL, async (db) => {
const results = await db.unsafe(sql);
return {
content: [
{
type: "text",
text: `**Query Results**\n\`\`\`sql\n${sql}\n\`\`\`\n\n**Results:**\n\`\`\`json\n${JSON.stringify(results, null, 2)}\n\`\`\`\n\n**Rows returned:** ${Array.isArray(results) ? results.length : 1}`
}
]
};
});
} catch (error) {
console.error('Database operation error:', error);
return {
content: [
{
type: "text",
text: `Database error: ${formatDatabaseError(error)}`,
isError: true
}
]
};
}
}
```
### Read vs Write Operation Handling
```typescript
// Check if operation is read-only
if (isWriteOperation(sql)) {
return {
content: [
{
type: "text",
text: "Write operations are not allowed with this tool. Use the privileged tool if you have write permissions.",
isError: true
}
]
};
}
```
## Authentication & Authorization Patterns
### OAuth Integration Pattern
```typescript
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import { GitHubHandler } from "./github-handler";
// OAuth configuration
export default new OAuthProvider({
apiHandlers: {
'/sse': MyMCP.serveSSE('/sse') as any,
'/mcp': MyMCP.serve('/mcp') as any,
},
authorizeEndpoint: "/authorize",
clientRegistrationEndpoint: "/register",
defaultHandler: GitHubHandler as any,
tokenEndpoint: "/token",
});
```
### User Permission Checking
```typescript
// Permission validation pattern
function hasPermission(username: string, operation: string): boolean {
const WRITE_PERMISSIONS = new Set(['admin1', 'admin2']);
const READ_PERMISSIONS = new Set(['user1', 'user2', ...WRITE_PERMISSIONS]);
switch (operation) {
case 'read':
return READ_PERMISSIONS.has(username);
case 'write':
return WRITE_PERMISSIONS.has(username);
default:
return false;
}
}
```
## Error Handling Patterns
### Standardized Error Response
```typescript
// Error response pattern
function createErrorResponse(error: Error, operation: string) {
console.error(`${operation} error:`, error);
return {
content: [
{
type: "text",
text: `${operation} failed: ${error.message}`,
isError: true
}
]
};
}
```
### Database Error Formatting
```typescript
// Use the built-in database error formatter
import { formatDatabaseError } from "./database";
try {
// Database operation
} catch (error) {
return {
content: [
{
type: "text",
text: `Database error: ${formatDatabaseError(error)}`,
isError: true
}
]
};
}
```
## Resource Registration Patterns
### Basic Resource Pattern
```typescript
// Resource registration
this.server.resource(
"resource://example/{id}",
"Resource description",
async (uri) => {
const id = uri.path.split('/').pop();
try {
const data = await fetchResourceData(id);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(data, null, 2)
}
]
};
} catch (error) {
throw new Error(`Failed to fetch resource: ${error.message}`);
}
}
);
```
## Testing Patterns
### Tool Testing Pattern
```typescript
// Test tool functionality
async function testTool(toolName: string, params: any) {
try {
const result = await server.callTool(toolName, params);
console.log(`${toolName} test passed:`, result);
return true;
} catch (error) {
console.error(`${toolName} test failed:`, error);
return false;
}
}
```
### Database Connection Testing
```typescript
// Test database connectivity
async function testDatabaseConnection() {
try {
await withDatabase(process.env.DATABASE_URL, async (db) => {
const result = await db`SELECT 1 as test`;
console.log('Database connection test passed:', result);
});
return true;
} catch (error) {
console.error('Database connection test failed:', error);
return false;
}
}
```
## Security Best Practices
### Input Validation
```typescript
// Always validate inputs with Zod
const inputSchema = z.object({
query: z.string().min(1).max(1000),
parameters: z.array(z.string()).optional()
});
// In tool handler
try {
const validated = inputSchema.parse(params);
// Use validated data
} catch (error) {
return createErrorResponse(error, "Input validation");
}
```
### SQL Injection Prevention
```typescript
// Use the built-in SQL validation
import { validateSqlQuery } from "./database";
const validation = validateSqlQuery(sql);
if (!validation.isValid) {
return createErrorResponse(new Error(validation.error), "SQL validation");
}
```
### Access Control
```typescript
// Always check permissions before executing sensitive operations
if (!hasPermission(this.props.login, 'write')) {
return {
content: [
{
type: "text",
text: "Access denied: insufficient permissions",
isError: true
}
]
};
}
```
## Performance Patterns
### Connection Pooling
```typescript
// Use the built-in connection pooling
import { withDatabase } from "./database";
// The withDatabase function handles connection pooling automatically
await withDatabase(databaseUrl, async (db) => {
// Database operations
});
```
### Resource Cleanup
```typescript
// Implement proper cleanup in Durable Objects
async cleanup(): Promise<void> {
try {
// Close database connections
await closeDb();
// Clean up other resources
await cleanupResources();
console.log('Cleanup completed successfully');
} catch (error) {
console.error('Cleanup error:', error);
}
}
```
## Common Gotchas
### 1. Missing Cleanup Implementation
- Always implement `cleanup()` method in Durable Objects
- Handle database connection cleanup properly
- Set up alarm handler for automatic cleanup
### 2. SQL Injection Vulnerabilities
- Always use `validateSqlQuery()` before executing SQL
- Never concatenate user input directly into SQL strings
- Use parameterized queries when possible
### 3. Permission Bypasses
- Check permissions for every sensitive operation
- Don't rely on tool registration alone for security
- Always validate user identity from props
### 4. Error Information Leakage
- Use `formatDatabaseError()` to sanitize error messages
- Don't expose internal system details in error responses
- Log detailed errors server-side, return generic messages to client
### 5. Resource Leaks
- Always use `withDatabase()` for database operations
- Implement proper error handling in async operations
- Clean up resources in finally blocks
## Environment Configuration
### Required Environment Variables
```typescript
// Environment type definition
interface Env {
DATABASE_URL: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
OAUTH_KV: KVNamespace;
// Add other bindings as needed
}
```
### Wrangler Configuration Pattern
```toml
# wrangler.toml
name = "mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "OAUTH_KV"
id = "your-kv-namespace-id"
[env.production]
# Production-specific configuration
```
This document provides the core patterns for building secure, scalable MCP servers using the proven architecture in this codebase.