@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
253 lines (224 loc) • 5.97 kB
text/typescript
/**
* Pure message handlers for DAP protocol
*/
import {
DAPSessionState,
DAPProcessingResult,
DAPCommand,
ProxyMessage,
ProxyStatusMessage,
ProxyErrorMessage,
ProxyDapEventMessage,
ProxyDapResponseMessage
} from './types.js';
import {
setInitialized,
setAdapterConfigured,
setCurrentThreadId,
getPendingRequest,
removePendingRequest
} from './state.js';
/**
* Main handler for proxy messages
*/
export function handleProxyMessage(
state: DAPSessionState,
message: ProxyMessage
): DAPProcessingResult {
// Validate session ID
if (message.sessionId !== state.sessionId) {
return {
commands: [{
type: 'log',
level: 'warn',
message: `Session ID mismatch. Expected ${state.sessionId}, got ${message.sessionId}`
}]
};
}
switch (message.type) {
case 'status':
return handleStatusMessage(state, message);
case 'error':
return handleErrorMessage(state, message);
case 'dapEvent':
return handleDapEvent(state, message);
case 'dapResponse':
return handleDapResponse(state, message);
default:
return {
commands: [{
type: 'log',
level: 'warn',
message: 'Unknown message type',
data: message
}]
};
}
}
/**
* Handle status messages (Phase 1)
*/
function handleStatusMessage(
state: DAPSessionState,
message: ProxyStatusMessage
): DAPProcessingResult {
const commands: DAPCommand[] = [];
switch (message.status) {
case 'proxy_minimal_ran_ipc_test':
commands.push(
{ type: 'log', level: 'info', message: '[ProxyManager] IPC test message received' },
{ type: 'killProcess' }
);
break;
case 'dry_run_complete':
commands.push(
{ type: 'log', level: 'info', message: '[ProxyManager] Dry run complete' },
{ type: 'emitEvent', event: 'dry-run-complete', args: [message.command, message.script] }
);
break;
case 'adapter_configured_and_launched':
commands.push(
{ type: 'log', level: 'info', message: '[ProxyManager] Adapter configured and launched' },
{ type: 'emitEvent', event: 'adapter-configured', args: [] }
);
// Update state
let newState = setAdapterConfigured(state, true);
// If not initialized, mark as initialized and emit event
if (!state.initialized) {
newState = setInitialized(newState, true);
commands.push({ type: 'emitEvent', event: 'initialized', args: [] });
}
return { commands, newState };
case 'adapter_exited':
case 'dap_connection_closed':
case 'terminated':
commands.push(
{ type: 'log', level: 'info', message: `[ProxyManager] Status: ${message.status}` },
{
type: 'emitEvent',
event: 'exit',
args: [message.code || 1, message.signal || undefined]
}
);
break;
}
return { commands };
}
/**
* Handle error messages (Phase 1)
*/
function handleErrorMessage(
state: DAPSessionState,
message: ProxyErrorMessage
): DAPProcessingResult {
return {
commands: [
{
type: 'log',
level: 'error',
message: `[ProxyManager] Proxy error: ${message.message}`
},
{
type: 'emitEvent',
event: 'error',
args: [new Error(message.message)]
}
]
};
}
/**
* Handle DAP events (Phase 2 placeholder)
*/
function handleDapEvent(
state: DAPSessionState,
message: ProxyDapEventMessage
): DAPProcessingResult {
const commands: DAPCommand[] = [
{
type: 'log',
level: 'info',
message: `[ProxyManager] DAP event: ${message.event}`,
data: message.body
}
];
let newState = state;
switch (message.event) {
case 'stopped':
// Type guard for stopped event body
const body = message.body as { threadId?: number; reason?: string } | undefined;
const threadId = body?.threadId;
const reason = body?.reason || 'unknown';
if (threadId) {
newState = setCurrentThreadId(state, threadId);
}
commands.push({
type: 'emitEvent',
event: 'stopped',
args: [threadId, reason, message.body]
});
break;
case 'continued':
commands.push({
type: 'emitEvent',
event: 'continued',
args: []
});
break;
case 'terminated':
commands.push({
type: 'emitEvent',
event: 'terminated',
args: []
});
break;
case 'exited':
commands.push({
type: 'emitEvent',
event: 'exited',
args: []
});
break;
default:
// Forward unknown events as generic DAP events
commands.push({
type: 'emitEvent',
event: 'dap-event',
args: [message.event, message.body]
});
}
return { commands, newState };
}
/**
* Handle DAP responses (Phase 3 placeholder)
*/
function handleDapResponse(
state: DAPSessionState,
message: ProxyDapResponseMessage
): DAPProcessingResult {
const pending = getPendingRequest(state, message.requestId);
if (!pending) {
return {
commands: [{
type: 'log',
level: 'warn',
message: `[ProxyManager] Received response for unknown request: ${message.requestId}`
}]
};
}
// This will be expanded in Phase 3
// For now, just log and emit basic events
return {
commands: [],
newState: removePendingRequest(state, message.requestId)
};
}
/**
* Validate a message has required fields
*/
export function isValidProxyMessage(message: unknown): message is ProxyMessage {
if (typeof message !== 'object' || message === null) {
return false;
}
const msg = message as { sessionId?: unknown; type?: unknown };
return typeof msg.sessionId === 'string' && typeof msg.type === 'string';
}