mirror of
https://github.com/coleam00/context-engineering-intro.git
synced 2025-12-17 17:55:29 +00:00
11 KiB
11 KiB
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
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
// 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)
// 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
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
// 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
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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
# 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.