@bloopai/debugger-mcp
Version:
An MCP server that provides interactive debugging capabilities to AI coding agents
235 lines (215 loc) • 8.5 kB
text/typescript
import { expect } from 'chai';
import * as path from 'path';
import type { DebugProtocol } from '@bloopai/ergonomic-debugger-client';
import {
StartDebugSessionArgs,
GetSessionDetailsArgs,
TerminateSessionArgs,
} from '../src/types/toolArgs';
import { HandleGetSessionDetailsResult } from '../src/types/toolResults';
import { SessionState } from '../src/types';
import { McpAsyncEvent } from '../src/types/mcp_protocol_extensions';
import {
parseStdioClientToolResponse,
setupMcpClient,
cleanupMcpClient,
TestClientSetup,
SdkToolResponse,
} from './test-utils';
const PYTHON_EXAMPLE_PATH = path.join(
path.resolve(__dirname, '../'),
'examples',
'python-example.py',
);
describe('MCP Get Session Details Integration Test (uv)', function () {
this.timeout(60000);
let testClientSetup: TestClientSetup | null = null;
before(async function () {
console.log(
'[INFO] Starting before hook for MCP Get Session Details tests (uv)...',
);
testClientSetup = await setupMcpClient(
'mcp-get-session-details-uv-test-client',
);
console.log('[INFO] Before hook completed using shared setupMcpClient.');
});
after(async function () {
console.log(
'[INFO] Starting after hook for MCP Get Session Details tests (uv)...',
);
await cleanupMcpClient(testClientSetup);
console.log('[INFO] After hook completed using shared cleanupMcpClient.');
});
it('should retrieve detailed information for an active (paused) debug session using uv adapter', async function () {
expect(testClientSetup).to.exist;
expect(testClientSetup?.client).to.exist;
const client = testClientSetup!.client!;
const startArgs: StartDebugSessionArgs = {
targetPath: PYTHON_EXAMPLE_PATH,
targetType: 'python',
stopOnEntry: true,
};
console.log(
`[Test] Calling start_debug_session with args: ${JSON.stringify(startArgs)}`,
);
const startResponseRaw: SdkToolResponse = await client.callTool({
serverName: 'interactive-debugger',
name: 'start_debug_session',
arguments: { ...startArgs },
});
const startResponse = parseStdioClientToolResponse<{
sessionId: string;
stoppedEvent?: DebugProtocol.StoppedEvent['body'];
}>(startResponseRaw, 'start_debug_session');
console.log(
`[Test] start_debug_session response: ${JSON.stringify(startResponse, null, 2)}`,
);
expect(
startResponse.success,
`start_debug_session failed: ${startResponse.error}`,
).to.be.true;
expect(startResponse.result?.sessionId).to.be.a('string');
const sessionId = startResponse.result!.sessionId;
const stoppedEventInResponse = startResponse.asyncEvents?.find(
(e: McpAsyncEvent) =>
e.eventType === 'dap_event_stopped' && e.sessionId === sessionId,
);
if (!startResponse.result?.stoppedEvent && !stoppedEventInResponse) {
console.warn(
'[Test] dap_event_stopped not found in initial response or asyncEvents. Proceeding, assuming session is paused due to stopOnEntry.',
);
} else {
console.log(
'[Test] Session stopped on entry (either via direct result or asyncEvents).',
);
}
const getDetailsArgs: GetSessionDetailsArgs = { sessionId };
console.log(
`[Test] Calling get_session_details for session ${sessionId}...`,
);
const getDetailsResponseRaw: SdkToolResponse = await client.callTool({
serverName: 'interactive-debugger',
name: 'get_session_details',
arguments: { ...getDetailsArgs },
});
const getDetailsResponse =
parseStdioClientToolResponse<HandleGetSessionDetailsResult>(
getDetailsResponseRaw,
'get_session_details',
);
console.log(
`[Test] get_session_details response: ${JSON.stringify(getDetailsResponse, null, 2)}`,
);
expect(
getDetailsResponse.success,
`get_session_details failed: ${getDetailsResponse.error}`,
).to.be.true;
let details = getDetailsResponse.result;
expect(details).to.exist;
// If stopOnEntry is true, the session should ideally be stopped.
// If it's still initializing, wait a bit and retry get_session_details.
if (details && startArgs.stopOnEntry) {
const MAX_RETRIES = 5;
const RETRY_DELAY_MS = 300;
let retries = MAX_RETRIES;
// Poll if state is INITIALIZING or even ACTIVE, waiting for it to become STOPPED
while (
details!.sessionInfo.state !== SessionState.STOPPED &&
retries > 0
) {
console.log(
`[Test] Session state is ${details!.sessionInfo.state}, expecting STOPPED. Retrying get_session_details (retries left: ${retries}, delay: ${RETRY_DELAY_MS}ms)...`,
);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
const retryGetDetailsResponseRaw: SdkToolResponse =
await client.callTool({
serverName: 'interactive-debugger',
name: 'get_session_details',
arguments: { sessionId },
});
const retryGetDetailsResponse =
parseStdioClientToolResponse<HandleGetSessionDetailsResult>(
retryGetDetailsResponseRaw,
'get_session_details',
);
details = retryGetDetailsResponse.result;
expect(
retryGetDetailsResponse.success,
`Retry get_session_details failed: ${retryGetDetailsResponse.error}`,
).to.be.true;
expect(details).to.exist;
retries--;
}
console.log(
`[Test] Final get_session_details response state after ${MAX_RETRIES - retries} retries: ${details?.sessionInfo.state}`,
);
}
if (details) {
expect(details.sessionInfo.id).to.equal(sessionId);
expect(details.sessionInfo.targetPath).to.equal(PYTHON_EXAMPLE_PATH);
expect(details.sessionInfo.targetType).to.equal('python');
// If stopOnEntry was true, we now expect it to be STOPPED after the potential retry.
// If stopOnEntry was false, it would be RUNNING.
if (startArgs.stopOnEntry) {
expect(
details.sessionInfo.state,
`Expected session to be STOPPED after polling (stopOnEntry:true). Final state: ${details.sessionInfo.state}`,
).to.equal(SessionState.STOPPED);
} else {
expect(
details.sessionInfo.state,
'Expected session to be RUNNING (stopOnEntry:false or not specified)',
).to.equal(SessionState.RUNNING);
}
expect(details.threads).to.be.an('array').with.length.greaterThan(0);
if (details.threads && details.threads.length > 0) {
expect(details.threads[0].id).to.be.a('number');
expect(details.threads[0].name).to.be.a('string');
}
const wasActuallyStopped =
details.sessionInfo.state === SessionState.STOPPED ||
startResponse.result?.stoppedEvent ||
stoppedEventInResponse;
if (wasActuallyStopped) {
expect(
details.callStack,
'Expected callStack for a paused session',
).to.be.an('array');
if (details.callStack && details.callStack.length > 0) {
const topFrame = details.callStack[0];
expect(topFrame.id).to.be.a('number');
expect(topFrame.name).to.be.a('string');
expect(topFrame.line).to.be.a('number');
expect(topFrame.source).to.exist;
if (topFrame.source) {
expect(topFrame.source.path).to.be.a('string');
}
} else if (details.sessionInfo.state === SessionState.STOPPED) {
console.warn(
'[Test] Session is STOPPED but no call stack was returned.',
);
}
} else {
console.warn(
'[Test] Session was not confirmed stopped, skipping call stack check details.',
);
}
}
const terminateArgs: TerminateSessionArgs = { sessionId };
console.log(`[Test] Terminating session: ${sessionId}`);
const terminateResponseRaw: SdkToolResponse = await client.callTool({
serverName: 'interactive-debugger',
name: 'terminate_session',
arguments: { ...terminateArgs },
});
const terminateResponse = parseStdioClientToolResponse<unknown>(
terminateResponseRaw,
'terminate_session',
);
expect(
terminateResponse.success,
`terminate_session failed: ${terminateResponse.error}`,
).to.be.true;
console.log('[Test] Session terminated.');
});
});