mirror of
https://github.com/coleam00/context-engineering-intro.git
synced 2025-12-18 10:15:27 +00:00
491 lines
11 KiB
Markdown
491 lines
11 KiB
Markdown
# 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. |