Event Handlers
Monitor and log MCP operations using comprehensive event hooks for observability and debugging.
Overview
The MCP Server adapter provides comprehensive event hooks that allow you to monitor, log, and respond to all MCP operations. This is essential for debugging, analytics, and maintaining observability of your MCP server.
Available Events
The adapter supports the following events:
onRequest- Called when any MCP request is receivedonResponse- Called when a response is sentonToolCall- Called when a tool is invokedonToolSuccess- Called when a tool completes successfullyonToolError- Called when a tool failsonError- Called on general adapter errors
Setting Up Event Handlers
Builder Pattern
import { IgniterMcpServer } from '@igniter-js/adapter-mcp-server';
const { handler } = IgniterMcpServer
.create()
.router(AppRouter)
.withEvents({
onRequest: async (request, context) => {
console.log('MCP request received:', request.url);
},
onResponse: async (response, context) => {
console.log('MCP response sent');
},
onToolCall: async (toolName, args, context) => {
console.log(`Tool called: ${toolName}`, args);
},
onToolSuccess: async (toolName, result, duration, context) => {
console.log(`Tool ${toolName} completed in ${duration}ms`);
},
onToolError: async (toolName, error, context) => {
console.error(`Tool ${toolName} failed:`, error);
},
onError: async (error, context) => {
console.error('MCP adapter error:', error);
}
})
.build();Function API
import { createMcpAdapter } from '@igniter-js/adapter-mcp-server';
const { server } = createMcpAdapter({
router: AppRouter,
events: {
onRequest: async (request, context) => {
console.log('MCP request received:', request.url);
},
onResponse: async (response, context) => {
console.log('MCP response sent');
},
onToolCall: async (toolName, args, context) => {
console.log(`Tool called: ${toolName}`, args);
},
onToolSuccess: async (toolName, result, duration, context) => {
console.log(`Tool ${toolName} completed in ${duration}ms`);
},
onToolError: async (toolName, error, context) => {
console.error(`Tool ${toolName} failed:`, error);
},
onError: async (error, context) => {
console.error('MCP adapter error:', error);
}
}
});Event Details
onRequest
Called when any MCP request is received:
onRequest: async (request, context) => {
// request: The incoming Request object
// context: MCP context with router context, tools, etc.
console.log('Request URL:', request.url);
console.log('Request method:', request.method);
console.log('Request headers:', Object.fromEntries(request.headers));
// Log to analytics
await analytics.track('mcp_request', {
url: request.url,
method: request.method,
timestamp: context.timestamp,
});
}onResponse
Called when a response is sent:
onResponse: async (response, context) => {
// response: The response that was sent
// context: MCP context
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers));
// Log response metrics
await metrics.record('mcp_response', {
status: response.status,
timestamp: context.timestamp,
});
}onToolCall
Called when a tool is invoked (before execution):
onToolCall: async (toolName, args, context) => {
// toolName: Name of the tool being called
// args: Arguments passed to the tool
// context: MCP context
console.log(`Tool called: ${toolName}`);
console.log('Arguments:', args);
// Log to audit trail
await audit.log({
type: 'tool_call',
tool: toolName,
args: sanitizeArgs(args), // Remove sensitive data
timestamp: context.timestamp,
client: context.client,
});
// Rate limiting check
const rateLimitKey = `mcp:tool:${toolName}:${context.client}`;
const isRateLimited = await checkRateLimit(rateLimitKey);
if (isRateLimited) {
throw new Error('Rate limit exceeded');
}
}onToolSuccess
Called when a tool completes successfully:
onToolSuccess: async (toolName, result, duration, context) => {
// toolName: Name of the tool
// result: Tool execution result
// duration: Execution time in milliseconds
// context: MCP context
console.log(`Tool ${toolName} completed in ${duration}ms`);
// Log performance metrics
await metrics.record('tool_execution', {
tool: toolName,
duration,
success: true,
timestamp: context.timestamp,
});
// Alert on slow executions
if (duration > 5000) {
await alerts.send({
type: 'slow_tool_execution',
tool: toolName,
duration,
});
}
}onToolError
Called when a tool fails:
onToolError: async (toolName, error, context) => {
// toolName: Name of the tool that failed
// error: The error that occurred
// context: MCP context
console.error(`Tool ${toolName} failed:`, error);
// Log error to monitoring service
await monitoring.logError({
tool: toolName,
error: error.message,
stack: error.stack,
timestamp: context.timestamp,
});
// Send alert for critical failures
if (isCriticalTool(toolName)) {
await alerts.send({
type: 'critical_tool_failure',
tool: toolName,
error: error.message,
});
}
// Update error tracking
await errorTracking.increment({
tool: toolName,
errorType: error.constructor.name,
});
}onError
Called on general adapter errors (not tool-specific):
onError: async (error, context) => {
// error: The error that occurred
// context: MCP context
console.error('MCP adapter error:', error);
// Log to error tracking service
await errorTracking.log({
error: error.message,
stack: error.stack,
context: {
url: context.request?.url,
method: context.request?.method,
timestamp: context.timestamp,
},
});
// Send critical alert
await alerts.send({
type: 'mcp_adapter_error',
error: error.message,
severity: 'critical',
});
}Practical Examples
Example 1: Request Logging
.withEvents({
onRequest: async (request, context) => {
const logEntry = {
timestamp: new Date().toISOString(),
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || 'unknown',
};
// Log to file or database
await logger.info('MCP Request', logEntry);
}
})Example 2: Performance Monitoring
.withEvents({
onToolSuccess: async (toolName, result, duration, context) => {
// Track execution times
await metrics.histogram('tool_execution_duration', duration, {
tool: toolName,
});
// Track success rate
await metrics.increment('tool_executions_total', {
tool: toolName,
status: 'success',
});
// Alert on slow tools
if (duration > 10000) {
await alerts.warn(`Tool ${toolName} took ${duration}ms to execute`);
}
},
onToolError: async (toolName, error, context) => {
// Track error rate
await metrics.increment('tool_executions_total', {
tool: toolName,
status: 'error',
});
}
})Example 3: Audit Trail
.withEvents({
onToolCall: async (toolName, args, context) => {
// Create audit log entry
await audit.create({
action: 'tool_call',
tool: toolName,
user: context.context.user?.id || 'anonymous',
args: sanitizeForAudit(args),
timestamp: new Date(),
ip: context.request.headers.get('x-forwarded-for'),
});
}
})Example 4: Rate Limiting
.withEvents({
onToolCall: async (toolName, args, context) => {
const clientId = context.client || 'unknown';
const rateLimitKey = `mcp:${clientId}:${toolName}`;
const remaining = await rateLimiter.check(rateLimitKey, {
limit: 100, // 100 calls per window
window: 60000, // 1 minute window
});
if (remaining < 0) {
throw new Error('Rate limit exceeded. Please try again later.');
}
// Set rate limit headers in response
context.request.headers.set('X-RateLimit-Remaining', remaining.toString());
}
})Example 5: Error Tracking and Alerting
.withEvents({
onToolError: async (toolName, error, context) => {
// Track error frequency
const errorKey = `error:${toolName}:${error.constructor.name}`;
const count = await errorCounter.increment(errorKey);
// Alert if error threshold exceeded
if (count > 10) {
await alerts.send({
type: 'high_error_rate',
tool: toolName,
error: error.message,
count,
});
}
// Log to error tracking service
await sentry.captureException(error, {
tags: {
tool: toolName,
component: 'mcp_adapter',
},
extra: {
args: context.request.body,
name: toolName,
},
});
}
})Context in Event Handlers
All event handlers receive a context object with:
interface McpContext {
context: TContext; // Your router's context
tools: McpToolInfo[]; // Available tools
request: Request; // Current request
timestamp: number; // Request timestamp
client?: string; // Client identifier
server?: McpServer; // MCP server instance
}Best Practices
1. Keep Handlers Async
All event handlers are async and should use await for any async operations:
// ✅ Good
onToolCall: async (toolName, args, context) => {
await logger.log(toolName, args);
}
// ❌ Bad - Synchronous operations can block
onToolCall: (toolName, args, context) => {
fs.writeFileSync('log.txt', toolName); // Blocks!
}2. Don't Throw in Handlers
Event handlers shouldn't throw errors. Handle errors gracefully:
// ✅ Good
onToolCall: async (toolName, args, context) => {
try {
await logger.log(toolName, args);
} catch (error) {
console.error('Failed to log:', error);
// Don't throw - just log the error
}
}
// ❌ Bad - Throwing can break the flow
onToolCall: async (toolName, args, context) => {
await logger.log(toolName, args); // If this throws, tool won't execute!
}3. Sanitize Sensitive Data
Remove sensitive information before logging:
onToolCall: async (toolName, args, context) => {
const sanitized = sanitizeArgs(args, {
removeFields: ['password', 'token', 'secret'],
maskFields: ['email', 'creditCard'],
});
await logger.log(toolName, sanitized);
}4. Use Structured Logging
Use structured logging for better queryability:
onToolCall: async (toolName, args, context) => {
await logger.info({
event: 'tool_call',
tool: toolName,
args: sanitizeArgs(args),
timestamp: context.timestamp,
client: context.client,
});
}Next Steps
- Explore Advanced Configurations
- Check out Best Practices
- Learn about OAuth for securing your server