MCP Server Debugging & Troubleshooting for ChatGPT Apps

Debugging Model Context Protocol (MCP) servers for ChatGPT applications presents unique challenges that differ from traditional API debugging. MCP servers operate in a streaming, stateful environment where interactions span multiple tool calls, widgets must render correctly across diverse clients, and performance directly impacts conversational flow. Common issues include widget rendering failures, tool handler timeouts, state synchronization problems, and authentication edge cases that only surface in production.

Effective MCP server debugging requires a comprehensive methodology: structured logging to trace request flows, real-time error tracking to capture production failures, systematic performance profiling to identify bottlenecks, and robust testing strategies to catch issues before deployment. The asynchronous, multi-turn nature of ChatGPT conversations means that bugs often manifest across tool call sequences rather than in isolated requests, demanding debugging approaches that capture conversation context and maintain correlation across distributed traces.

This guide provides production-ready debugging techniques specifically designed for MCP servers powering ChatGPT applications. You'll learn how to implement structured logging that preserves conversation context, integrate error tracking systems that categorize MCP-specific failures, leverage debugging tools like MCP Inspector for real-time analysis, troubleshoot common production issues, and build testing strategies that validate MCP protocol compliance. Whether you're debugging widget rendering problems, investigating performance degradation, or tracking down authentication failures, this guide equips you with the tools and techniques to diagnose and resolve issues efficiently.

Logging Best Practices for MCP Servers

Structured logging is the foundation of effective MCP server debugging. Unlike simple console.log statements, structured logs preserve context, enable filtering, and integrate with log aggregation systems. For MCP servers, logging must capture conversation IDs, user identifiers, tool invocations, widget rendering events, and performance metrics while remaining performant enough to avoid impacting response times.

Here's a production-ready structured logger implementation for MCP servers:

// src/utils/logger.ts
import { createLogger, format, transports, Logger as WinstonLogger } from 'winston';
import { v4 as uuidv4 } from 'uuid';

interface LogContext {
  conversationId?: string;
  userId?: string;
  toolName?: string;
  requestId?: string;
  sessionId?: string;
  [key: string]: any;
}

class MCPLogger {
  private logger: WinstonLogger;
  private defaultContext: LogContext;

  constructor(serviceName: string = 'mcp-server') {
    this.defaultContext = {};

    this.logger = createLogger({
      level: process.env.LOG_LEVEL || 'info',
      format: format.combine(
        format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
        format.errors({ stack: true }),
        format.splat(),
        format.json()
      ),
      defaultMeta: {
        service: serviceName,
        environment: process.env.NODE_ENV || 'development',
        version: process.env.APP_VERSION || '1.0.0'
      },
      transports: [
        // Console output for development
        new transports.Console({
          format: format.combine(
            format.colorize(),
            format.printf(({ timestamp, level, message, ...meta }) => {
              const contextStr = Object.keys(meta).length > 0
                ? JSON.stringify(meta, null, 2)
                : '';
              return `${timestamp} [${level}]: ${message} ${contextStr}`;
            })
          )
        }),

        // File output for production
        new transports.File({
          filename: 'logs/error.log',
          level: 'error',
          maxsize: 10485760, // 10MB
          maxFiles: 5
        }),
        new transports.File({
          filename: 'logs/combined.log',
          maxsize: 10485760,
          maxFiles: 10
        })
      ]
    });
  }

  /**
   * Set default context that will be included in all logs
   */
  setDefaultContext(context: LogContext): void {
    this.defaultContext = { ...this.defaultContext, ...context };
  }

  /**
   * Clear default context
   */
  clearDefaultContext(): void {
    this.defaultContext = {};
  }

  /**
   * Generate correlation ID for request tracking
   */
  generateCorrelationId(): string {
    return uuidv4();
  }

  /**
   * Log tool invocation
   */
  logToolInvocation(toolName: string, params: any, context: LogContext = {}): void {
    this.logger.info('Tool invoked', {
      ...this.defaultContext,
      ...context,
      toolName,
      params: this.sanitizeParams(params),
      eventType: 'tool_invocation'
    });
  }

  /**
   * Log tool completion
   */
  logToolCompletion(toolName: string, duration: number, context: LogContext = {}): void {
    this.logger.info('Tool completed', {
      ...this.defaultContext,
      ...context,
      toolName,
      duration,
      eventType: 'tool_completion'
    });
  }

  /**
   * Log widget rendering
   */
  logWidgetRender(widgetType: string, displayMode: string, context: LogContext = {}): void {
    this.logger.info('Widget rendered', {
      ...this.defaultContext,
      ...context,
      widgetType,
      displayMode,
      eventType: 'widget_render'
    });
  }

  /**
   * Log errors with full stack traces
   */
  logError(error: Error, context: LogContext = {}): void {
    this.logger.error('Error occurred', {
      ...this.defaultContext,
      ...context,
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name
      },
      eventType: 'error'
    });
  }

  /**
   * Log performance metrics
   */
  logPerformance(operation: string, metrics: Record<string, number>, context: LogContext = {}): void {
    this.logger.info('Performance metrics', {
      ...this.defaultContext,
      ...context,
      operation,
      metrics,
      eventType: 'performance'
    });
  }

  /**
   * Sanitize sensitive parameters
   */
  private sanitizeParams(params: any): any {
    const sensitiveKeys = ['password', 'token', 'apiKey', 'secret', 'credential'];
    const sanitized = { ...params };

    for (const key of Object.keys(sanitized)) {
      if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
        sanitized[key] = '[REDACTED]';
      }
    }

    return sanitized;
  }

  // Standard log methods
  info(message: string, meta: LogContext = {}): void {
    this.logger.info(message, { ...this.defaultContext, ...meta });
  }

  warn(message: string, meta: LogContext = {}): void {
    this.logger.warn(message, { ...this.defaultContext, ...meta });
  }

  error(message: string, meta: LogContext = {}): void {
    this.logger.error(message, { ...this.defaultContext, ...meta });
  }

  debug(message: string, meta: LogContext = {}): void {
    this.logger.debug(message, { ...this.defaultContext, ...meta });
  }
}

// Export singleton instance
export const logger = new MCPLogger(process.env.SERVICE_NAME || 'mcp-server');
export default logger;

Use correlation IDs to trace requests across distributed systems. Generate a unique ID at the start of each conversation and include it in all log entries, making it easy to filter logs for specific user sessions. This is critical for debugging issues that span multiple tool calls or involve widget state updates.

Implement log levels appropriately: DEBUG for development details, INFO for normal operations, WARN for recoverable issues, ERROR for failures requiring attention. Configure production environments to INFO level to balance visibility with performance, then temporarily increase to DEBUG when investigating specific issues.

For production deployments, integrate with log aggregation services like Datadog, Splunk, or ELK Stack. These platforms enable searching across distributed logs, creating alerts based on error patterns, and building dashboards that visualize MCP server health metrics. The structured JSON format ensures compatibility with modern log analysis tools.

Error Tracking & Monitoring Integration

While logging captures what happened, error tracking systems like Sentry provide context about why errors occurred and how frequently they impact users. For MCP servers, error tracking must capture conversation context, tool invocation chains, widget rendering failures, and user impact metrics.

Here's a production-ready Sentry integration for MCP servers:

// src/utils/errorTracking.ts
import * as Sentry from '@sentry/node';
import { ProfilingIntegration } from '@sentry/profiling-node';
import { logger } from './logger';

interface ErrorContext {
  conversationId?: string;
  userId?: string;
  toolName?: string;
  widgetType?: string;
  displayMode?: string;
  [key: string]: any;
}

class ErrorTracker {
  private initialized: boolean = false;

  /**
   * Initialize Sentry error tracking
   */
  initialize(): void {
    if (this.initialized) {
      logger.warn('Error tracker already initialized');
      return;
    }

    Sentry.init({
      dsn: process.env.SENTRY_DSN,
      environment: process.env.NODE_ENV || 'development',
      release: process.env.APP_VERSION || '1.0.0',

      // Performance monitoring
      tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.1'),
      profilesSampleRate: parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE || '0.1'),

      integrations: [
        new ProfilingIntegration(),
        new Sentry.Integrations.Http({ tracing: true }),
        new Sentry.Integrations.Express({ app: undefined })
      ],

      // Filter out specific errors
      beforeSend(event, hint) {
        const error = hint.originalException;

        // Don't report expected errors
        if (error instanceof Error) {
          if (error.message.includes('User cancelled') ||
              error.message.includes('Request timeout')) {
            return null;
          }
        }

        return event;
      }
    });

    this.initialized = true;
    logger.info('Error tracker initialized', { service: 'sentry' });
  }

  /**
   * Capture MCP tool error
   */
  captureToolError(error: Error, toolName: string, params: any, context: ErrorContext = {}): void {
    Sentry.withScope((scope) => {
      scope.setTag('error_type', 'tool_error');
      scope.setTag('tool_name', toolName);
      scope.setContext('tool', {
        name: toolName,
        params: this.sanitizeData(params)
      });

      this.setCommonContext(scope, context);
      Sentry.captureException(error);
    });

    logger.logError(error, { toolName, ...context });
  }

  /**
   * Capture widget rendering error
   */
  captureWidgetError(
    error: Error,
    widgetType: string,
    displayMode: string,
    context: ErrorContext = {}
  ): void {
    Sentry.withScope((scope) => {
      scope.setTag('error_type', 'widget_error');
      scope.setTag('widget_type', widgetType);
      scope.setTag('display_mode', displayMode);
      scope.setContext('widget', {
        type: widgetType,
        displayMode
      });

      this.setCommonContext(scope, context);
      Sentry.captureException(error);
    });

    logger.logError(error, { widgetType, displayMode, ...context });
  }

  /**
   * Capture authentication error
   */
  captureAuthError(error: Error, authType: string, context: ErrorContext = {}): void {
    Sentry.withScope((scope) => {
      scope.setTag('error_type', 'auth_error');
      scope.setTag('auth_type', authType);
      scope.setContext('authentication', { type: authType });

      this.setCommonContext(scope, context);
      Sentry.captureException(error);
    });

    logger.logError(error, { authType, ...context });
  }

  /**
   * Capture performance issue
   */
  capturePerformanceIssue(
    operation: string,
    duration: number,
    threshold: number,
    context: ErrorContext = {}
  ): void {
    if (duration > threshold) {
      Sentry.withScope((scope) => {
        scope.setTag('issue_type', 'performance');
        scope.setTag('operation', operation);
        scope.setContext('performance', {
          operation,
          duration,
          threshold,
          exceeded_by: duration - threshold
        });

        this.setCommonContext(scope, context);
        Sentry.captureMessage(
          `Performance threshold exceeded: ${operation} took ${duration}ms (threshold: ${threshold}ms)`,
          'warning'
        );
      });

      logger.warn('Performance threshold exceeded', {
        operation,
        duration,
        threshold,
        ...context
      });
    }
  }

  /**
   * Start performance transaction
   */
  startTransaction(name: string, op: string): Sentry.Transaction {
    return Sentry.startTransaction({ name, op });
  }

  /**
   * Set user context
   */
  setUser(userId: string, email?: string): void {
    Sentry.setUser({ id: userId, email });
  }

  /**
   * Clear user context
   */
  clearUser(): void {
    Sentry.setUser(null);
  }

  /**
   * Add breadcrumb for debugging
   */
  addBreadcrumb(message: string, category: string, data?: Record<string, any>): void {
    Sentry.addBreadcrumb({
      message,
      category,
      data: this.sanitizeData(data),
      level: 'info'
    });
  }

  /**
   * Set common context across all captures
   */
  private setCommonContext(scope: Sentry.Scope, context: ErrorContext): void {
    if (context.conversationId) {
      scope.setTag('conversation_id', context.conversationId);
      scope.setContext('conversation', { id: context.conversationId });
    }

    if (context.userId) {
      scope.setUser({ id: context.userId });
    }

    // Add any additional context
    const extraContext = { ...context };
    delete extraContext.conversationId;
    delete extraContext.userId;
    delete extraContext.toolName;
    delete extraContext.widgetType;
    delete extraContext.displayMode;

    if (Object.keys(extraContext).length > 0) {
      scope.setContext('additional', extraContext);
    }
  }

  /**
   * Sanitize sensitive data before sending to Sentry
   */
  private sanitizeData(data: any): any {
    if (!data) return data;

    const sensitiveKeys = ['password', 'token', 'apiKey', 'secret', 'credential', 'authorization'];
    const sanitized = JSON.parse(JSON.stringify(data));

    const sanitizeObject = (obj: any): void => {
      for (const key of Object.keys(obj)) {
        if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
          obj[key] = '[REDACTED]';
        } else if (typeof obj[key] === 'object' && obj[key] !== null) {
          sanitizeObject(obj[key]);
        }
      }
    };

    sanitizeObject(sanitized);
    return sanitized;
  }
}

// Export singleton instance
export const errorTracker = new ErrorTracker();
export default errorTracker;

Configure Sentry alerts to notify your team when error rates exceed thresholds. For example, trigger alerts when widget rendering failures spike above 5% of requests, when tool handler errors exceed 10 per minute, or when authentication failures indicate a potential security issue. This proactive monitoring catches production problems before they impact large numbers of users.

Use Sentry's release tracking to correlate errors with specific deployments. Tag each release with version numbers, then filter errors by release to identify regressions introduced in recent updates. This accelerates root cause analysis by narrowing the scope to code changes between working and broken versions.

Implement breadcrumbs to capture the sequence of events leading to errors. For MCP servers, breadcrumbs should record tool invocations, widget state changes, authentication events, and external API calls. When an error occurs, Sentry displays the full breadcrumb trail, providing context that helps reconstruct the failure scenario.

Debugging Tools & Techniques

The MCP Inspector is the primary debugging tool for MCP servers, providing real-time visualization of tool definitions, request/response flows, and protocol compliance. Use it to validate that your server correctly implements the MCP specification before integrating with ChatGPT.

Here's how to use MCP Inspector effectively:

#!/bin/bash
# mcp-inspector-debug.sh - MCP Inspector debugging workflow

# Start your MCP server (example with Node.js server on port 3000)
echo "Starting MCP server..."
node dist/index.js &
SERVER_PID=$!

# Wait for server to be ready
sleep 2

# Run MCP Inspector
echo "Launching MCP Inspector..."
npx @modelcontextprotocol/inspector@latest http://localhost:3000/mcp

# Inspector will open in your browser at http://localhost:5173
# Use the web UI to:
# 1. View all registered tools and their schemas
# 2. Invoke tools with test parameters
# 3. Inspect request/response payloads
# 4. Validate widget rendering
# 5. Check protocol compliance

# Example: Test a tool via command line
echo "Testing 'search' tool via curl..."
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "search",
      "arguments": {
        "query": "debugging techniques",
        "limit": 5
      }
    }
  }' | jq .

# Check tool list
echo "Fetching tool list..."
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
  }' | jq .

# Monitor server logs in real-time
echo "Server logs:"
tail -f logs/combined.log &
LOG_PID=$!

# Wait for user input to stop
read -p "Press Enter to stop debugging session..."

# Cleanup
kill $SERVER_PID
kill $LOG_PID
echo "Debugging session ended."

For Chrome DevTools debugging of widget rendering, use the remote debugging protocol:

// src/debug/chromeDevTools.ts
import { spawn } from 'child_process';
import { logger } from '../utils/logger';

/**
 * Launch Chrome with remote debugging enabled
 */
export function launchChromeDebug(url: string, port: number = 9222): void {
  const chromeArgs = [
    `--remote-debugging-port=${port}`,
    '--no-first-run',
    '--no-default-browser-check',
    '--disable-background-networking',
    url
  ];

  const chrome = spawn('google-chrome', chromeArgs, {
    detached: true,
    stdio: 'ignore'
  });

  chrome.unref();

  logger.info('Chrome launched with remote debugging', {
    url,
    port,
    devToolsUrl: `chrome://inspect/#devices`
  });

  console.log(`\nChrome DevTools Protocol enabled at: http://localhost:${port}`);
  console.log(`Open chrome://inspect in Chrome to debug widgets\n`);
}

/**
 * Network analysis middleware for debugging
 */
export function createNetworkDebugMiddleware() {
  return (req: any, res: any, next: any) => {
    const start = Date.now();

    // Capture request details
    const requestData = {
      method: req.method,
      url: req.url,
      headers: req.headers,
      body: req.body
    };

    // Intercept response
    const originalSend = res.send;
    res.send = function(data: any) {
      const duration = Date.now() - start;

      logger.logPerformance('http_request', {
        duration,
        status: res.statusCode,
        size: Buffer.byteLength(JSON.stringify(data))
      }, {
        method: req.method,
        url: req.url
      });

      return originalSend.call(this, data);
    };

    next();
  };
}

Learn more about MCP Inspector usage in our dedicated tutorial and explore performance optimization techniques.

Production Troubleshooting Strategies

Production issues often manifest differently than development bugs. Memory leaks that accumulate over hours, connection pool exhaustion under load, and race conditions in concurrent tool invocations require systematic troubleshooting approaches.

Here's a production-ready performance profiler for MCP servers:

// src/debug/performanceProfiler.ts
import { performance, PerformanceObserver } from 'perf_hooks';
import { logger } from '../utils/logger';
import { errorTracker } from '../utils/errorTracking';

interface PerformanceMetrics {
  operation: string;
  duration: number;
  timestamp: number;
  memory?: NodeJS.MemoryUsage;
  tags?: Record<string, string>;
}

class PerformanceProfiler {
  private metrics: PerformanceMetrics[] = [];
  private observer: PerformanceObserver | null = null;
  private thresholds: Map<string, number> = new Map();

  constructor() {
    this.initializeObserver();
  }

  /**
   * Initialize performance observer
   */
  private initializeObserver(): void {
    this.observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach((entry) => {
        this.recordMetric({
          operation: entry.name,
          duration: entry.duration,
          timestamp: entry.startTime
        });
      });
    });

    this.observer.observe({ entryTypes: ['measure'], buffered: true });
  }

  /**
   * Set performance threshold for operation
   */
  setThreshold(operation: string, thresholdMs: number): void {
    this.thresholds.set(operation, thresholdMs);
    logger.info('Performance threshold set', { operation, thresholdMs });
  }

  /**
   * Start measuring an operation
   */
  startMeasure(operation: string): void {
    performance.mark(`${operation}-start`);
  }

  /**
   * End measuring an operation
   */
  endMeasure(operation: string, tags?: Record<string, string>): number {
    const endMark = `${operation}-end`;
    const startMark = `${operation}-start`;

    performance.mark(endMark);
    performance.measure(operation, startMark, endMark);

    const measure = performance.getEntriesByName(operation)[0];
    const duration = measure.duration;

    // Clean up marks
    performance.clearMarks(startMark);
    performance.clearMarks(endMark);
    performance.clearMeasures(operation);

    // Record with memory snapshot
    this.recordMetric({
      operation,
      duration,
      timestamp: Date.now(),
      memory: process.memoryUsage(),
      tags
    });

    // Check threshold
    const threshold = this.thresholds.get(operation);
    if (threshold && duration > threshold) {
      errorTracker.capturePerformanceIssue(operation, duration, threshold, tags);
    }

    return duration;
  }

  /**
   * Measure async operation
   */
  async measureAsync<T>(
    operation: string,
    fn: () => Promise<T>,
    tags?: Record<string, string>
  ): Promise<T> {
    this.startMeasure(operation);
    try {
      const result = await fn();
      this.endMeasure(operation, tags);
      return result;
    } catch (error) {
      this.endMeasure(operation, { ...tags, error: 'true' });
      throw error;
    }
  }

  /**
   * Record metric
   */
  private recordMetric(metric: PerformanceMetrics): void {
    this.metrics.push(metric);

    // Log slow operations
    if (metric.duration > 1000) {
      logger.warn('Slow operation detected', {
        operation: metric.operation,
        duration: metric.duration,
        ...metric.tags
      });
    }

    // Keep only recent metrics (last 1000)
    if (this.metrics.length > 1000) {
      this.metrics.shift();
    }
  }

  /**
   * Get performance summary
   */
  getSummary(operation?: string): any {
    const relevantMetrics = operation
      ? this.metrics.filter(m => m.operation === operation)
      : this.metrics;

    if (relevantMetrics.length === 0) {
      return null;
    }

    const durations = relevantMetrics.map(m => m.duration);
    const sum = durations.reduce((a, b) => a + b, 0);
    const avg = sum / durations.length;
    const sorted = [...durations].sort((a, b) => a - b);

    return {
      operation,
      count: durations.length,
      avg: Math.round(avg * 100) / 100,
      min: Math.round(sorted[0] * 100) / 100,
      max: Math.round(sorted[sorted.length - 1] * 100) / 100,
      p50: Math.round(sorted[Math.floor(sorted.length * 0.5)] * 100) / 100,
      p95: Math.round(sorted[Math.floor(sorted.length * 0.95)] * 100) / 100,
      p99: Math.round(sorted[Math.floor(sorted.length * 0.99)] * 100) / 100
    };
  }

  /**
   * Get all operation summaries
   */
  getAllSummaries(): Record<string, any> {
    const operations = [...new Set(this.metrics.map(m => m.operation))];
    const summaries: Record<string, any> = {};

    operations.forEach(op => {
      summaries[op] = this.getSummary(op);
    });

    return summaries;
  }

  /**
   * Clear all metrics
   */
  clearMetrics(): void {
    this.metrics = [];
    performance.clearMarks();
    performance.clearMeasures();
  }
}

// Export singleton instance
export const profiler = new PerformanceProfiler();
export default profiler;

For memory leak detection in long-running MCP servers:

// src/debug/memoryLeakDetector.ts
import { logger } from '../utils/logger';
import { errorTracker } from '../utils/errorTracking';

interface MemorySnapshot {
  timestamp: number;
  heapUsed: number;
  heapTotal: number;
  external: number;
  arrayBuffers: number;
  rss: number;
}

class MemoryLeakDetector {
  private snapshots: MemorySnapshot[] = [];
  private intervalId: NodeJS.Timeout | null = null;
  private thresholds = {
    heapGrowthMB: 50, // Alert if heap grows by 50MB
    snapshotCount: 10  // Number of snapshots to analyze
  };

  /**
   * Start monitoring memory usage
   */
  startMonitoring(intervalMs: number = 60000): void {
    if (this.intervalId) {
      logger.warn('Memory monitoring already started');
      return;
    }

    this.intervalId = setInterval(() => {
      this.takeSnapshot();
      this.analyzeMemoryTrend();
    }, intervalMs);

    logger.info('Memory leak detector started', { intervalMs });
  }

  /**
   * Stop monitoring
   */
  stopMonitoring(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
      logger.info('Memory leak detector stopped');
    }
  }

  /**
   * Take memory snapshot
   */
  private takeSnapshot(): void {
    const memUsage = process.memoryUsage();

    const snapshot: MemorySnapshot = {
      timestamp: Date.now(),
      heapUsed: memUsage.heapUsed,
      heapTotal: memUsage.heapTotal,
      external: memUsage.external,
      arrayBuffers: memUsage.arrayBuffers,
      rss: memUsage.rss
    };

    this.snapshots.push(snapshot);

    // Keep only recent snapshots
    if (this.snapshots.length > this.thresholds.snapshotCount * 2) {
      this.snapshots = this.snapshots.slice(-this.thresholds.snapshotCount * 2);
    }

    logger.debug('Memory snapshot taken', {
      heapUsedMB: Math.round(snapshot.heapUsed / 1024 / 1024),
      heapTotalMB: Math.round(snapshot.heapTotal / 1024 / 1024),
      rssMB: Math.round(snapshot.rss / 1024 / 1024)
    });
  }

  /**
   * Analyze memory trend for potential leaks
   */
  private analyzeMemoryTrend(): void {
    if (this.snapshots.length < this.thresholds.snapshotCount) {
      return;
    }

    const recent = this.snapshots.slice(-this.thresholds.snapshotCount);
    const oldest = recent[0];
    const newest = recent[recent.length - 1];

    const heapGrowthBytes = newest.heapUsed - oldest.heapUsed;
    const heapGrowthMB = heapGrowthBytes / 1024 / 1024;
    const timeDiffMinutes = (newest.timestamp - oldest.timestamp) / 1000 / 60;

    if (heapGrowthMB > this.thresholds.heapGrowthMB) {
      const growthRate = heapGrowthMB / timeDiffMinutes;

      logger.warn('Potential memory leak detected', {
        heapGrowthMB: Math.round(heapGrowthMB),
        timeDiffMinutes: Math.round(timeDiffMinutes),
        growthRateMBPerMin: Math.round(growthRate * 100) / 100,
        currentHeapMB: Math.round(newest.heapUsed / 1024 / 1024)
      });

      errorTracker.addBreadcrumb('Potential memory leak', 'memory', {
        heapGrowthMB,
        growthRateMBPerMin: growthRate
      });

      // Force garbage collection if available
      if (global.gc) {
        logger.info('Triggering manual garbage collection');
        global.gc();
      }
    }
  }

  /**
   * Get memory statistics
   */
  getStatistics(): any {
    if (this.snapshots.length === 0) {
      return null;
    }

    const recent = this.snapshots.slice(-this.thresholds.snapshotCount);
    const heapValues = recent.map(s => s.heapUsed / 1024 / 1024);
    const rssValues = recent.map(s => s.rss / 1024 / 1024);

    return {
      snapshotCount: recent.length,
      heap: {
        currentMB: Math.round(heapValues[heapValues.length - 1]),
        avgMB: Math.round(heapValues.reduce((a, b) => a + b) / heapValues.length),
        minMB: Math.round(Math.min(...heapValues)),
        maxMB: Math.round(Math.max(...heapValues))
      },
      rss: {
        currentMB: Math.round(rssValues[rssValues.length - 1]),
        avgMB: Math.round(rssValues.reduce((a, b) => a + b) / rssValues.length),
        minMB: Math.round(Math.min(...rssValues)),
        maxMB: Math.round(Math.max(...rssValues))
      }
    };
  }

  /**
   * Force manual snapshot and analysis
   */
  forceCheck(): void {
    this.takeSnapshot();
    this.analyzeMemoryTrend();
  }
}

// Export singleton instance
export const memoryDetector = new MemoryLeakDetector();
export default memoryDetector;

Implement health check endpoints that expose server status and diagnostics:

// src/routes/health.ts
import { Router, Request, Response } from 'express';
import { profiler } from '../debug/performanceProfiler';
import { memoryDetector } from '../debug/memoryLeakDetector';
import { logger } from '../utils/logger';

const router = Router();

/**
 * Basic health check
 */
router.get('/health', (req: Request, res: Response) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

/**
 * Detailed health check with diagnostics
 */
router.get('/health/detailed', (req: Request, res: Response) => {
  const memUsage = process.memoryUsage();
  const memStats = memoryDetector.getStatistics();
  const perfSummaries = profiler.getAllSummaries();

  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: {
      heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024),
      heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024),
      rssMB: Math.round(memUsage.rss / 1024 / 1024),
      externalMB: Math.round(memUsage.external / 1024 / 1024),
      statistics: memStats
    },
    performance: perfSummaries,
    environment: {
      nodeVersion: process.version,
      platform: process.platform,
      arch: process.arch
    }
  });
});

/**
 * Readiness check for load balancers
 */
router.get('/ready', (req: Request, res: Response) => {
  // Check if server is ready to accept requests
  // Add database connection checks, external service checks, etc.

  const memUsage = process.memoryUsage();
  const heapUsedPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;

  if (heapUsedPercent > 90) {
    logger.warn('High memory usage detected', { heapUsedPercent });
    return res.status(503).json({
      status: 'not_ready',
      reason: 'high_memory_usage',
      heapUsedPercent: Math.round(heapUsedPercent)
    });
  }

  res.json({
    status: 'ready',
    timestamp: new Date().toISOString()
  });
});

export default router;

For comprehensive troubleshooting of connection and timeout issues, review our guides on MCP tool handler best practices and OpenTelemetry integration for distributed tracing.

Testing Strategies for MCP Servers

Robust testing prevents production issues by catching bugs early in development. For MCP servers, testing must validate protocol compliance, tool handler correctness, widget rendering across display modes, authentication flows, and performance under load.

Here's a production-ready integration test suite:

// tests/integration/mcpServer.test.ts
import request from 'supertest';
import { expect } from 'chai';
import { app } from '../../src/app';
import { logger } from '../../src/utils/logger';

describe('MCP Server Integration Tests', () => {
  before(() => {
    logger.info('Starting integration test suite');
  });

  describe('Protocol Compliance', () => {
    it('should return tool list with valid schema', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 1,
          method: 'tools/list'
        })
        .expect(200);

      expect(response.body).to.have.property('jsonrpc', '2.0');
      expect(response.body).to.have.property('id', 1);
      expect(response.body).to.have.property('result');
      expect(response.body.result).to.have.property('tools');
      expect(response.body.result.tools).to.be.an('array');

      // Validate each tool has required fields
      response.body.result.tools.forEach((tool: any) => {
        expect(tool).to.have.property('name');
        expect(tool).to.have.property('description');
        expect(tool).to.have.property('inputSchema');
        expect(tool.inputSchema).to.have.property('type', 'object');
        expect(tool.inputSchema).to.have.property('properties');
      });
    });

    it('should handle invalid JSON-RPC requests', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          method: 'tools/list' // Missing jsonrpc and id
        })
        .expect(400);

      expect(response.body).to.have.property('error');
    });

    it('should handle unknown methods', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 1,
          method: 'unknown/method'
        })
        .expect(200);

      expect(response.body).to.have.property('error');
      expect(response.body.error).to.have.property('code', -32601);
    });
  });

  describe('Tool Invocation', () => {
    it('should execute search tool successfully', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 2,
          method: 'tools/call',
          params: {
            name: 'search',
            arguments: {
              query: 'test query',
              limit: 5
            }
          }
        })
        .expect(200);

      expect(response.body).to.have.property('result');
      expect(response.body.result).to.have.property('content');
      expect(response.body.result).to.have.property('structuredContent');
      expect(response.body.result).to.have.property('_meta');
    });

    it('should validate tool parameters', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 3,
          method: 'tools/call',
          params: {
            name: 'search',
            arguments: {
              // Missing required 'query' parameter
              limit: 5
            }
          }
        })
        .expect(200);

      expect(response.body).to.have.property('error');
      expect(response.body.error.message).to.include('required');
    });

    it('should handle tool execution timeout', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 4,
          method: 'tools/call',
          params: {
            name: 'slowOperation',
            arguments: {}
          }
        })
        .timeout(5000)
        .expect(200);

      expect(response.body).to.have.property('error');
      expect(response.body.error.message).to.include('timeout');
    }).timeout(6000);
  });

  describe('Widget Rendering', () => {
    it('should return widget with correct mimeType', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 5,
          method: 'tools/call',
          params: {
            name: 'showResults',
            arguments: {
              data: [{ id: 1, name: 'Test' }]
            }
          }
        })
        .expect(200);

      expect(response.body.result).to.have.property('structuredContent');
      expect(response.body.result.structuredContent).to.have.property('mimeType', 'text/html+skybridge');
      expect(response.body.result.structuredContent).to.have.property('data');
    });

    it('should set appropriate display mode in _meta', async () => {
      const response = await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 6,
          method: 'tools/call',
          params: {
            name: 'showResults',
            arguments: { data: [] }
          }
        })
        .expect(200);

      expect(response.body.result).to.have.property('_meta');
      expect(response.body.result._meta).to.have.property('displayMode');
      expect(['inline', 'fullscreen', 'pip']).to.include(response.body.result._meta.displayMode);
    });
  });

  describe('Performance', () => {
    it('should respond to tool list within 100ms', async () => {
      const start = Date.now();

      await request(app)
        .post('/mcp')
        .send({
          jsonrpc: '2.0',
          id: 7,
          method: 'tools/list'
        })
        .expect(200);

      const duration = Date.now() - start;
      expect(duration).to.be.lessThan(100);
    });

    it('should handle concurrent requests', async () => {
      const requests = Array(10).fill(null).map((_, i) =>
        request(app)
          .post('/mcp')
          .send({
            jsonrpc: '2.0',
            id: 100 + i,
            method: 'tools/list'
          })
      );

      const responses = await Promise.all(requests);
      responses.forEach(response => {
        expect(response.status).to.equal(200);
        expect(response.body).to.have.property('result');
      });
    });
  });

  after(() => {
    logger.info('Integration test suite completed');
  });
});

Implement chaos engineering to test resilience under failure conditions. Use tools like Chaos Monkey to randomly terminate processes, introduce network latency, or simulate database failures. This validates that your MCP server gracefully handles infrastructure issues and recovers automatically.

For more testing strategies, explore our complete guide to building ChatGPT applications and error tracking with Sentry.

Conclusion: Build Reliable MCP Servers with Confidence

Effective debugging and troubleshooting are essential for maintaining production MCP servers that power ChatGPT applications. By implementing structured logging with correlation IDs, integrating error tracking systems like Sentry, leveraging debugging tools like MCP Inspector, building performance profilers and memory leak detectors, and creating comprehensive test suites, you ensure that your MCP servers deliver reliable, high-performance experiences to users.

The debugging techniques and production-ready code examples in this guide provide a foundation for building observable, maintainable MCP servers. Structured logging captures conversation context across multi-turn interactions, error tracking categorizes failures for efficient resolution, performance profilers identify bottlenecks before they impact users, and automated testing validates protocol compliance and functional correctness.

Ready to build ChatGPT applications with production-grade debugging built-in? MakeAIHQ is the no-code platform that generates MCP servers with enterprise logging, error tracking, and monitoring pre-configured. Our AI-powered editor creates ChatGPT apps with structured logging, Sentry integration, health check endpoints, and comprehensive test suites—all following the best practices outlined in this guide. From zero to production-ready ChatGPT App Store deployment in 48 hours, with debugging infrastructure that scales to millions of users. Start your free trial today and build reliable ChatGPT applications with confidence.


Further Reading:

External Resources: