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 received
  • onResponse - Called when a response is sent
  • onToolCall - Called when a tool is invoked
  • onToolSuccess - Called when a tool completes successfully
  • onToolError - Called when a tool fails
  • onError - 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