Skip to content

Latest commit

 

History

History
248 lines (178 loc) · 7.83 KB

File metadata and controls

248 lines (178 loc) · 7.83 KB

mcp-debugger Architecture Overview

⚠️ DRAFT DOCUMENTATION
This documentation is based on mcp-debugger v0.10.0 architecture and will be refined based on real-world adapter development feedback.

From Python-Specific to Multi-Language Platform

The mcp-debugger has undergone a major architectural transformation, evolving from a Python-specific debugging tool into a multi-language debugging platform. This refactoring introduced a clean adapter pattern that separates language-agnostic core functionality from language-specific implementations.

Key Architectural Components

1. Language-Agnostic Core

The core components handle session management, process lifecycle, and DAP communication without any language-specific knowledge:

2. Debug Adapter Interface

The IDebugAdapter interface defines the contract that all language adapters must implement:

export interface IDebugAdapter extends EventEmitter {
  readonly language: DebugLanguage;
  readonly name: string;
  
  // Lifecycle
  initialize(): Promise<void>;
  dispose(): Promise<void>;
  
  // Environment validation
  validateEnvironment(): Promise<ValidationResult>;
  resolveExecutablePath(preferredPath?: string): Promise<string>;
  
  // DAP operations
  buildAdapterCommand(config: AdapterConfig): AdapterCommand;
  sendDapRequest<T>(command: string, args?: unknown): Promise<T>;
  handleDapEvent(event: DebugProtocol.Event): void;
  
  // ... 30+ methods total
}

3. Language Adapters

Each supported language implements the IDebugAdapter interface:

4. Adapter Registry

The AdapterRegistry manages available adapters:

registry.register('python', new PythonAdapterFactory());
registry.register('mock', new MockAdapterFactory());
// Future: registry.register('node', new NodeAdapterFactory());

Data Flow Architecture

sequenceDiagram
    participant Client as MCP Client
    participant Server as MCP Server
    participant SM as SessionManager
    participant AR as AdapterRegistry
    participant Adapter as Language Adapter
    participant PM as ProxyManager
    participant DAP as Debug Adapter Process
    
    Client->>Server: create_debug_session(language='python')
    Server->>SM: createSession(language)
    SM->>AR: create(language)
    AR->>Adapter: new PythonDebugAdapter()
    
    Client->>Server: start_debugging(sessionId)
    Server->>SM: startDebugging()
    SM->>Adapter: validateEnvironment()
    SM->>PM: new ProxyManager(adapter)
    PM->>Adapter: buildAdapterCommand()
    PM->>DAP: spawn debug adapter
    
    DAP-->>PM: DAP events
    PM-->>Adapter: handleDapEvent()
    Adapter-->>SM: emit events
    SM-->>Client: debugging updates
Loading

Key Design Decisions

1. Wrap, Don't Rewrite

The existing ProxyManager provides excellent process management. Rather than rewriting it, we inject adapters to handle language-specific concerns:

// Before: ProxyManager had Python-specific logic
class ProxyManager {
  private async spawnPythonDebugger() { /* ... */ }
}

// After: ProxyManager delegates to adapters
class ProxyManager {
  constructor(private adapter: IDebugAdapter) {}
  
  private async spawnDebugAdapter() {
    const command = this.adapter.buildAdapterCommand(this.config);
    // ... spawn using command
  }
}

2. Invariant Core, Variable Adapters

The core remains stable while adapters handle all language-specific variations:

  • Core handles: Process lifecycle, IPC, session state, DAP transport
  • Adapters handle: Executable discovery, command building, environment validation, language-specific quirks

3. Event-Driven Communication

Adapters extend EventEmitter for loose coupling:

adapter.on('stopped', (event) => {
  // Update UI, notify client
});

adapter.on('stateChanged', (oldState, newState) => {
  // Track adapter state transitions
});

Reality Check: Theory vs Practice

Path Handling Complexity

Theory: Adapters handle path translation cleanly
Reality: Path handling was abandoned entirely. TRUE HANDS-OFF approach - no path manipulation whatsoever. Debug adapters handle paths natively.

State Management

Theory: Clean state transitions following VALID_TRANSITIONS
Reality: Real adapters (like Python) don't enforce strict transitions. The mock adapter was made more permissive to match real-world behavior.

DAP Event Sequences

Theory: Simple event flow
Reality: Each debugger has quirks. See DAP Sequence Reference for real-world examples:

  • Node.js waits for debugger detach
  • Python handles sys.exit() specially
  • Event ordering matters: exited → terminated

Performance Characteristics

The adapter pattern adds minimal overhead:

Operation Before After Impact
Session Creation 100ms 105ms +5%
Breakpoint Setting <10ms <10ms None
Step Operations <50ms <50ms None
Memory per Session Baseline +~1MB Adapter instance

Testing Infrastructure

The refactoring improved testability:

  • 808 passing tests (100% success rate)
  • Mock adapter enables integration testing without external dependencies
  • Type safety throughout with TypeScript strict mode
  • Comprehensive test coverage for all components

Common Pitfalls

1. Forgetting Event Registration

Adapters must properly handle DAP events:

// ❌ Wrong: Forgetting to update state
handleDapEvent(event: DebugProtocol.Event): void {
  this.emit(event.event, event.body);
}

// ✅ Correct: Update internal state
handleDapEvent(event: DebugProtocol.Event): void {
  if (event.event === 'stopped') {
    this.currentThreadId = event.body?.threadId;
    this.transitionTo(AdapterState.DEBUGGING);
  }
  this.emit(event.event, event.body);
}

2. Incorrect State Transitions

The stopped event means PAUSED, not terminated:

// ❌ Wrong: Confusing stopped with terminated
if (event.event === 'stopped') {
  this.state = AdapterState.TERMINATED; // NO!
}

// ✅ Correct: Stopped = paused for debugging
if (event.event === 'stopped') {
  this.state = AdapterState.DEBUGGING; // Paused at breakpoint
}

3. Missing Terminated Event

Always send terminated when ending a session:

// ❌ Wrong: Only sending exited
async endSession() {
  this.emit('exited', { exitCode: 0 });
}

// ✅ Correct: Send both exited and terminated
async endSession(exitCode: number) {
  this.emit('exited', { exitCode });
  this.emit('terminated');
}

Next Steps

Version History

  • v0.10.0 - Current architecture with adapter pattern
  • v0.9.x - Python-specific implementation
  • v0.8.x - Initial MCP server implementation

This documentation reflects the state of mcp-debugger after the major refactoring completed in January 2025. For the refactoring history, see refactoring-summary.md.