@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
436 lines (340 loc) • 13 kB
Markdown
# MCP Debug Server - Component Design
This document provides detailed design information for the major components of the MCP Debug Server.
## SessionManager
**Location**: `src/session/session-manager.ts`
### Overview
SessionManager is the central orchestrator for all debug sessions. It implements a facade pattern, providing a simplified interface for complex debugging operations while managing the lifecycle of ProxyManager instances.
### Key Design Decisions
1. **One ProxyManager per Session**
- Each debug session gets its own ProxyManager instance
- Enables concurrent debugging of multiple scripts
- Isolates failures to individual sessions
2. **Event Handler Management**
- Uses WeakMap to track event handlers per session
- Ensures proper cleanup to prevent memory leaks
- From lines 121-122:
```typescript
// WeakMap to store event handlers for cleanup
private sessionEventHandlers = new WeakMap<ManagedSession, Map<string, (...args: any[]) => void>>();
```
3. **State Management**
- Delegates state storage to SessionStore
- Synchronizes state between ProxyManager events and session state
- Handles state transitions with logging
### Public API
```typescript
class SessionManager {
// Session lifecycle
async createSession(params: { language: DebugLanguage; name?: string; executablePath?: string; }): Promise<DebugSessionInfo>
async startDebugging(sessionId: string, scriptPath: string, scriptArgs?: string[], dapLaunchArgs?: Partial<CustomLaunchRequestArguments>, dryRunSpawn?: boolean): Promise<DebugResult>
async closeSession(sessionId: string): Promise<boolean>
async closeAllSessions(): Promise<void>
// Debugging operations
async setBreakpoint(sessionId: string, file: string, line: number, condition?: string): Promise<Breakpoint>
async stepOver(sessionId: string): Promise<DebugResult>
async stepInto(sessionId: string): Promise<DebugResult>
async stepOut(sessionId: string): Promise<DebugResult>
async continue(sessionId: string): Promise<DebugResult>
// Inspection
async getVariables(sessionId: string, variablesReference: number): Promise<Variable[]>
async getStackTrace(sessionId: string, threadId?: number): Promise<StackFrame[]>
async getScopes(sessionId: string, frameId: number): Promise<DebugProtocol.Scope[]>
// Query
public getSession(sessionId: string): ManagedSession | undefined
public getAllSessions(): DebugSessionInfo[]
}
```
### Event Handler Pattern
The SessionManager sets up comprehensive event handlers for each ProxyManager (lines 184-310):
```typescript
private setupProxyEventHandlers(
session: ManagedSession,
proxyManager: IProxyManager,
effectiveLaunchArgs: Partial<CustomLaunchRequestArguments>
): void {
const handlers = new Map<string, (...args: any[]) => void>();
// Named functions for each event
const handleStopped = (threadId: number, reason: string) => {
if (!effectiveLaunchArgs.stopOnEntry && reason === 'entry') {
// Auto-continue if stopOnEntry=false
this.continue(sessionId).catch(err => {
this.logger.error(`Error auto-continuing:`, err);
});
} else {
this._updateSessionState(session, SessionState.PAUSED);
}
};
// Register and track all handlers
proxyManager.on('stopped', handleStopped);
handlers.set('stopped', handleStopped);
// Store for cleanup
this.sessionEventHandlers.set(session, handlers);
}
```
### Cleanup Strategy
The cleanup mechanism ensures no memory leaks (lines 313-344):
```typescript
private cleanupProxyEventHandlers(session: ManagedSession, proxyManager: IProxyManager): void {
const handlers = this.sessionEventHandlers.get(session);
if (!handlers) return;
handlers.forEach((handler, eventName) => {
try {
proxyManager.removeListener(eventName, handler);
} catch (error) {
// Continue cleanup despite errors
}
});
this.sessionEventHandlers.delete(session);
}
```
## ProxyManager
**Location**: `src/proxy/proxy-manager.ts`
### Overview
ProxyManager handles the spawning and communication with debug proxy processes. It implements a robust message passing system with timeout handling and state management.
### Key Design Decisions
1. **Process Isolation**
- Each ProxyManager spawns a separate Node.js process
- Communication via IPC (Inter-Process Communication)
- Graceful shutdown with force-kill fallback
2. **Message Type System**
- Strongly typed messages using TypeScript discriminated unions
- From lines 92-117:
```typescript
type ProxyMessage =
| ProxyStatusMessage
| ProxyDapEventMessage
| ProxyDapResponseMessage
| ProxyErrorMessage;
```
3. **Functional Core Integration**
- Uses pure functions from dap-core for state management
- Commands pattern for side effects
- From lines 426-456:
```typescript
const result = handleProxyMessage(this.dapState, message);
// Execute commands from functional core
for (const command of result.commands) {
switch (command.type) {
case 'log':
this.logger[command.level](command.message, command.data);
break;
case 'emitEvent':
this.emit(command.event as any, ...command.args);
break;
// ...
}
}
```
### Request Tracking
ProxyManager tracks pending DAP requests with timeout handling:
```typescript
private pendingDapRequests = new Map<string, {
resolve: (response: DebugProtocol.Response) => void;
reject: (error: Error) => void;
command: string;
}>();
// Timeout handler (line 290)
setTimeout(() => {
if (this.pendingDapRequests.has(requestId)) {
this.pendingDapRequests.delete(requestId);
reject(new Error(ErrorMessages.dapRequestTimeout(command, 35)));
}
}, 35000);
```
### Process Management
The proxy script discovery mechanism (lines 297-315):
```typescript
private async findProxyScript(): Promise<string> {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Try .js first (for src/dev)
let proxyWorkerPath = path.resolve(__dirname, '../proxy/proxy-bootstrap.js');
if (!await this.fileSystem.pathExists(proxyWorkerPath)) {
// Fallback to .cjs (for dist)
proxyWorkerPath = path.resolve(__dirname, '../proxy/proxy-bootstrap.cjs');
if (!await this.fileSystem.pathExists(proxyWorkerPath)) {
throw new Error(`Bootstrap worker script not found.`);
}
}
return proxyWorkerPath;
}
```
## DAP Proxy Worker
**Location**: `src/proxy/dap-proxy-worker.ts`
### Overview
The ProxyWorker is the core business logic component that runs in the proxy process. It manages the debugpy adapter lifecycle and DAP protocol communication.
### State Machine
The worker implements a strict state machine:
```typescript
enum ProxyState {
UNINITIALIZED, // Initial state
INITIALIZING, // Setting up adapter
CONNECTED, // Ready for debugging
SHUTTING_DOWN, // Cleanup in progress
TERMINATED // Final state
}
```
### Initialization Sequence
From the `handleInitCommand` method (lines 67-124):
1. **State Validation**
```typescript
if (this.state !== ProxyState.UNINITIALIZED) {
throw new Error(`Invalid state for init: ${this.state}`);
}
```
2. **Logger Creation**
```typescript
const logPath = path.join(payload.logDir, `proxy-${payload.sessionId}.log`);
this.logger = await this.dependencies.loggerFactory(payload.sessionId, payload.logDir);
```
3. **Path Validation**
```typescript
if (!path.isAbsolute(payload.scriptPath)) {
throw new Error(`Script path is not absolute: ${payload.scriptPath}`);
}
const scriptExists = await this.dependencies.fileSystem.pathExists(payload.scriptPath);
```
4. **Dry Run Handling**
```typescript
if (payload.dryRunSpawn) {
this.handleDryRun(payload);
return;
}
```
### DAP Event Handling
The worker sets up comprehensive DAP event handlers (lines 198-248):
```typescript
private setupDapEventHandlers(): void {
this.connectionManager.setupEventHandlers(this.dapClient, {
onInitialized: async () => {
await this.handleInitializedEvent();
},
onStopped: (body) => {
this.logger!.info('[Worker] DAP event: stopped', body);
this.sendDapEvent('stopped', body);
},
onTerminated: (body) => {
this.logger!.info('[Worker] DAP event: terminated', body);
this.sendDapEvent('terminated', body);
this.shutdown();
},
// ... other handlers
});
}
```
### Request Timeout Management
The worker uses `CallbackRequestTracker` for timeout handling:
```typescript
// Track request (line 301)
this.requestTracker.track(payload.requestId, payload.dapCommand);
try {
const response = await this.dapClient.sendRequest(payload.dapCommand, payload.dapArgs);
this.requestTracker.complete(payload.requestId);
this.sendDapResponse(payload.requestId, true, response);
} catch (error) {
this.requestTracker.complete(payload.requestId);
this.sendDapResponse(payload.requestId, false, undefined, message);
}
```
## SessionStore
**Location**: `src/session/session-store.ts`
### Overview
SessionStore provides centralized storage and management for debug sessions with thread-safe operations.
### Design Features
1. **Centralized State Management**
- Single source of truth for session data
- Encapsulates state mutations
- Provides query methods
2. **ID Generation**
- Uses UUID v4 for session IDs
- Ensures uniqueness across sessions
3. **Type Safety**
- Strict typing for session data
- Separate internal (ManagedSession) and external (DebugSessionInfo) representations
### Key Methods
```typescript
class SessionStore {
// Creation
createSession(params: SessionCreateParams): DebugSessionInfo
// Retrieval
get(sessionId: string): ManagedSession | undefined
getOrThrow(sessionId: string): ManagedSession
getAll(): DebugSessionInfo[]
getAllManaged(): ManagedSession[]
// Updates
updateState(sessionId: string, state: SessionState): void
// Metadata
size(): number
}
```
## Error Message System
**Location**: `src/utils/error-messages.ts`
### Overview
Centralized error messages ensure consistency and provide helpful troubleshooting information to users.
### Design Principles
1. **User-Friendly Messages**
- Clear description of what went wrong
- Actionable troubleshooting steps
- Context about typical causes
2. **Parameterized Messages**
- Functions that accept context (timeouts, commands)
- Consistent formatting across errors
3. **Categories**
- DAP request timeouts
- Proxy initialization failures
- Step operation timeouts
- Adapter readiness timeouts
### Example Implementation
```typescript
export const ErrorMessages = {
dapRequestTimeout: (command: string, timeout: number) =>
`Debug adapter did not respond to '${command}' request within ${timeout}s. ` +
`This typically means the debug adapter has crashed or lost connection. ` +
`Try restarting your debug session. If the problem persists, check the debug adapter logs.`,
proxyInitTimeout: (timeout: number) =>
`Debug proxy initialization did not complete within ${timeout}s. ` +
`This may indicate that debugpy failed to start or is not installed. ` +
`Check that Python and debugpy are properly installed and accessible.`
};
```
## Dependency Injection System
**Location**: `src/interfaces/external-dependencies.ts`
### Overview
The dependency injection system enables comprehensive testing by abstracting all external dependencies behind interfaces.
### Interface Hierarchy
1. **Core Interfaces**
```typescript
export interface IFileSystem { /* fs operations */ }
export interface IProcessManager { /* process spawning */ }
export interface INetworkManager { /* network operations */ }
export interface ILogger { /* logging */ }
```
2. **Factory Interfaces**
```typescript
export interface IProxyManagerFactory {
create(): IProxyManager;
}
export interface ISessionStoreFactory {
create(): SessionStore;
}
```
3. **Aggregate Dependencies**
```typescript
export interface SessionManagerDependencies {
fileSystem: IFileSystem;
networkManager: INetworkManager;
logger: ILogger;
proxyManagerFactory: IProxyManagerFactory;
sessionStoreFactory: ISessionStoreFactory;
debugTargetLauncher: IDebugTargetLauncher;
}
```
### Benefits
1. **Testability** - Easy to mock external systems
2. **Flexibility** - Can swap implementations
3. **Clarity** - Clear dependency requirements
4. **Type Safety** - Compile-time dependency checking
## Next Steps
- See [Testing Architecture](./testing-architecture.md) for how these components are tested
- See [Dependency Injection Pattern](../patterns/dependency-injection.md) for detailed DI examples
- See [Error Handling Pattern](../patterns/error-handling.md) for error management strategies