Middleware Utilities
MCPKit provides powerful utilities for composing, enhancing, and organizing middleware. This guide covers advanced patterns beyond basic middleware creation.
Middleware Chain Utilities
conditional() - Conditional Execution
Run middleware only when a condition is met:
import { conditional } from '@mcpkit-dev/core';
// Only run auth for non-public paths
const authMiddleware = conditional(
(ctx) => !ctx.path.startsWith('/public'),
jwtAuth({ secret: process.env.JWT_SECRET! })
);
// Multiple conditions
const adminOnly = conditional(
(ctx) => {
const auth = ctx.get('mcpkit:auth');
return auth?.roles?.includes('admin');
},
async (ctx, next) => {
// Admin-only logic
await next();
}
);
withTimeout() - Timeout Wrapper
Add timeout to any middleware:
import { withTimeout, TimeoutError } from '@mcpkit-dev/core';
// Timeout after 5 seconds
const timedMiddleware = withTimeout(
async (ctx, next) => {
await someSlowOperation();
await next();
},
5000 // 5 seconds
);
// Handle timeout errors
try {
await pipeline.execute(req, res);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Middleware timed out:', error.message);
}
}
withRetry() - Retry on Failure
Automatically retry failed middleware:
import { withRetry } from '@mcpkit-dev/core';
const resilientMiddleware = withRetry(
async (ctx, next) => {
await unreliableExternalCall();
await next();
},
{
maxRetries: 3,
delay: 1000, // 1 second between retries
backoff: 'exponential', // 1s, 2s, 4s
retryOn: (error) => error.code === 'ECONNRESET',
}
);
Options:
interface RetryOptions {
maxRetries: number;
delay?: number; // Base delay in ms
backoff?: 'none' | 'linear' | 'exponential';
retryOn?: (error: Error) => boolean;
onRetry?: (error: Error, attempt: number) => void;
}
withErrorHandler() - Error Handling
Wrap middleware with error handling:
import { withErrorHandler } from '@mcpkit-dev/core';
const safeMiddleware = withErrorHandler(
async (ctx, next) => {
await riskyOperation();
await next();
},
{
onError: (error, ctx) => {
console.error('Middleware error:', error);
// Optionally modify context or response
ctx.set('error', error);
},
rethrow: false, // Swallow error and continue
}
);
withCache() - Response Caching
Cache middleware results:
import { withCache } from '@mcpkit-dev/core';
const cachedMiddleware = withCache(
async (ctx, next) => {
// Expensive computation
const data = await fetchExpensiveData();
ctx.set('data', data);
await next();
},
{
key: (ctx) => `cache:${ctx.path}:${ctx.sessionId}`,
ttl: 60000, // 1 minute
store: new Map(), // Or use Redis, etc.
}
);
withHooks() - Add Lifecycle Hooks
Add before/after hooks to middleware:
import { withHooks } from '@mcpkit-dev/core';
const hookedMiddleware = withHooks(
async (ctx, next) => {
await mainLogic();
await next();
},
{
before: (ctx) => {
console.error('Before middleware');
ctx.set('startTime', Date.now());
},
after: (ctx) => {
const duration = Date.now() - ctx.get('startTime');
console.error(`After middleware: ${duration}ms`);
},
onError: (error, ctx) => {
console.error('Middleware failed:', error);
},
finally: (ctx) => {
console.error('Cleanup');
},
}
);
Middleware Composition
compose() - Combine Middleware
Combine multiple middleware into one:
import { compose } from '@mcpkit-dev/core';
const authAndLog = compose(
loggingMiddleware,
authMiddleware,
rateLimitMiddleware
);
// Use as single middleware
@MCPServer({
middleware: [authAndLog],
})
class MyServer {}
createMiddlewareGroup() - Named Groups
Create reusable middleware groups:
import { createMiddlewareGroup } from '@mcpkit-dev/core';
const securityGroup = createMiddlewareGroup('security', [
corsMiddleware,
helmetMiddleware,
rateLimitMiddleware,
authMiddleware,
]);
const observabilityGroup = createMiddlewareGroup('observability', [
tracingMiddleware,
metricsMiddleware,
loggingMiddleware,
]);
@MCPServer({
middleware: [
securityGroup,
observabilityGroup,
],
})
class MyServer {}
parallelMiddleware() - Run in Parallel
Execute independent middleware concurrently:
import { parallelMiddleware } from '@mcpkit-dev/core';
// These run in parallel, not sequentially
const parallel = parallelMiddleware([
async (ctx, next) => {
ctx.set('userPrefs', await fetchUserPrefs());
await next();
},
async (ctx, next) => {
ctx.set('featureFlags', await fetchFeatureFlags());
await next();
},
async (ctx, next) => {
ctx.set('config', await fetchConfig());
await next();
},
]);
// All three fetches happen concurrently
selectMiddleware() - Dynamic Selection
Choose middleware at runtime:
import { selectMiddleware } from '@mcpkit-dev/core';
const dynamicAuth = selectMiddleware((ctx) => {
const authHeader = ctx.request.headers['authorization'];
if (authHeader?.startsWith('Bearer ')) {
return jwtAuth({ secret: process.env.JWT_SECRET! });
} else if (authHeader?.startsWith('ApiKey ')) {
return apiKeyAuth({ keys: process.env.API_KEYS!.split(',') });
} else {
return async (ctx, next) => {
ctx.response.writeHead(401);
ctx.response.end('Unauthorized');
};
}
});
Middleware Pipeline
createPipeline() - Build Pipelines
Create a middleware pipeline with advanced features:
import { createPipeline, MiddlewarePipeline } from '@mcpkit-dev/core';
const pipeline = createPipeline();
// Add middleware with priority (lower = runs first)
pipeline.use(loggingMiddleware, { priority: 0 });
pipeline.use(authMiddleware, { priority: 10 });
pipeline.use(rateLimitMiddleware, { priority: 20 });
// Add named middleware for removal/replacement
pipeline.use(cacheMiddleware, { name: 'cache' });
// Remove middleware by name
pipeline.remove('cache');
// Replace middleware
pipeline.replace('auth', newAuthMiddleware);
// Execute pipeline
await pipeline.execute(request, response, url, sessionId, handler);
Pipeline Configuration
const pipeline = createPipeline({
// Error handler for all middleware
onError: (error, ctx) => {
console.error('Pipeline error:', error);
},
// Called before each middleware
onBefore: (middlewareName, ctx) => {
console.error(`Running: ${middlewareName}`);
},
// Called after each middleware
onAfter: (middlewareName, ctx, duration) => {
console.error(`${middlewareName} took ${duration}ms`);
},
});
Request Tracing Middleware
tracing() - Basic Correlation IDs
Add correlation IDs for request tracking:
import { tracing, getCorrelationId, CORRELATION_ID_KEY } from '@mcpkit-dev/core';
const tracingMiddleware = tracing({
headerName: 'x-correlation-id',
generateIfMissing: true,
includeInResponse: true,
log: true,
});
// In a tool handler
@Tool({ description: 'My tool' })
async myTool() {
const correlationId = getCorrelationId(this.context);
console.error(`[${correlationId}] Processing...`);
}
advancedTracing() - Full Span Support
Advanced tracing with nested spans:
import { advancedTracing, getTraceContext, TraceContext } from '@mcpkit-dev/core';
const tracingMiddleware = advancedTracing({
headerName: 'x-trace-id',
onSpanEnd: (span) => {
// Send to tracing backend
sendToJaeger(span);
},
});
// In a tool handler
@Tool({ description: 'Complex operation' })
async complexOperation() {
const traceCtx = getTraceContext(this.context);
// Create child spans
await traceCtx.trace('database.query', async () => {
return await db.query('SELECT * FROM users');
});
await traceCtx.trace('external.api', async () => {
return await fetch('https://api.example.com');
});
}
Authentication Middleware
apiKeyAuth() - API Key Authentication
import { apiKeyAuth } from '@mcpkit-dev/core';
const auth = apiKeyAuth({
keys: ['key1', 'key2', 'key3'],
header: 'x-api-key', // or 'authorization'
queryParam: 'api_key', // fallback
onUnauthorized: (ctx, reason) => {
ctx.response.writeHead(401, { 'Content-Type': 'application/json' });
ctx.response.end(JSON.stringify({ error: reason }));
},
});
jwtAuth() - JWT Authentication
import { jwtAuth } from '@mcpkit-dev/core';
const auth = jwtAuth({
secret: process.env.JWT_SECRET!,
algorithms: ['HS256'],
issuer: 'my-app',
audience: 'my-api',
clockTolerance: 60, // seconds
skipPaths: ['/health', '/public/*'],
getPrincipal: (payload, ctx) => ({
userId: payload.sub,
roles: payload.roles,
}),
});
bearerAuth() - Custom Token Validation
import { bearerAuth } from '@mcpkit-dev/core';
const auth = bearerAuth({
validate: async (token) => {
const session = await sessionStore.get(token);
if (!session) {
return { valid: false, error: 'Invalid token' };
}
return {
valid: true,
principal: session.user,
roles: session.roles,
};
},
});
Rate Limiting Middleware
import { rateLimit, MemoryRateLimitStore } from '@mcpkit-dev/core';
const limiter = rateLimit({
windowMs: 60000, // 1 minute
maxRequests: 100, // 100 requests per minute
keyGenerator: (ctx) => ctx.request.headers['x-api-key'] || ctx.sessionId,
store: new MemoryRateLimitStore(),
onRateLimited: (ctx, retryAfter) => {
ctx.response.writeHead(429, {
'Retry-After': String(retryAfter),
'Content-Type': 'application/json',
});
ctx.response.end(JSON.stringify({
error: 'Too many requests',
retryAfter,
}));
},
});
Custom Rate Limit Store (Redis)
import { RateLimitStore } from '@mcpkit-dev/core';
import Redis from 'ioredis';
class RedisRateLimitStore implements RateLimitStore {
private redis = new Redis();
async get(key: string): Promise<{ count: number; resetTime: number } | null> {
const data = await this.redis.get(`ratelimit:${key}`);
return data ? JSON.parse(data) : null;
}
async set(key: string, value: { count: number; resetTime: number }, ttl: number): Promise<void> {
await this.redis.setex(`ratelimit:${key}`, Math.ceil(ttl / 1000), JSON.stringify(value));
}
async increment(key: string): Promise<number> {
return await this.redis.incr(`ratelimit:${key}`);
}
}
const limiter = rateLimit({
store: new RedisRateLimitStore(),
// ...
});
Complete Example
Combining multiple utilities:
import {
MCPServer,
Tool,
compose,
conditional,
withTimeout,
withRetry,
withErrorHandler,
createMiddlewareGroup,
tracing,
jwtAuth,
rateLimit,
} from '@mcpkit-dev/core';
// Security middleware group
const security = createMiddlewareGroup('security', [
conditional(
(ctx) => !ctx.path.startsWith('/public'),
jwtAuth({ secret: process.env.JWT_SECRET! })
),
rateLimit({
windowMs: 60000,
maxRequests: 100,
}),
]);
// Observability middleware group
const observability = createMiddlewareGroup('observability', [
tracing({ log: true }),
withTimeout(
async (ctx, next) => {
const start = Date.now();
await next();
console.error(`Request took ${Date.now() - start}ms`);
},
30000
),
]);
// Resilience middleware
const resilience = withRetry(
withErrorHandler(
async (ctx, next) => {
await next();
},
{
onError: (error) => console.error('Request failed:', error),
rethrow: true,
}
),
{ maxRetries: 2, delay: 1000 }
);
@MCPServer({
name: 'production-server',
version: '1.0.0',
middleware: [
observability,
security,
resilience,
],
})
class ProductionServer {
@Tool({ description: 'Process data' })
async processData() {
return { success: true };
}
}
Best Practices
- Order matters: Put logging/tracing first, then auth, then rate limiting
- Use composition: Group related middleware with
createMiddlewareGroup() - Handle errors: Always use
withErrorHandler()for critical middleware - Set timeouts: Prevent hanging requests with
withTimeout() - Use conditional: Don't run auth on health check endpoints
- Name middleware: Use names for easier debugging and management
See Also
- Middleware Basics - Creating custom middleware
- Authentication - Auth middleware in depth
- Rate Limiting - Rate limiting patterns
- Plugins Guide - Packaging middleware as plugins