UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

343 lines (297 loc) 14.4 kB
/** * @jest-environment node */ import { describe, it, expect, afterEach, beforeAll } from 'vitest'; import * as path from 'path'; import * as os from 'os'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { parseSdkToolResult, executeDebugSequence, isDockerAvailable, ensureDockerImage, getVolumeMount, generateContainerName, cleanupDocker, getContainerLogs } from './smoke-test-utils.js'; import { ensureDir, writeFile, remove } from 'fs-extra'; const TEST_TIMEOUT = 120000; // 120 seconds for container tests (increased to allow for Docker operations) const DOCKER_IMAGE = 'mcp-debugger:local'; let mcpSdkClient: Client | null = null; let activeContainerName: string | null = null; const projectRoot = process.cwd(); // Project testing philosophy: Tests should fail loudly when dependencies are unavailable // rather than being silently skipped. This ensures we're aware of missing requirements. describe('MCP Server E2E Container Smoke Test', () => { beforeAll(async () => { // Docker availability will be checked in each test // This allows us to provide specific error messages per test console.log(`[Container Smoke Test] Starting test suite at ${new Date().toISOString()}`); }); // Ensure cleanup even if test fails afterEach(async function() { console.log(`[Container Smoke Test] Cleaning up at ${new Date().toISOString()}...`); // Close MCP client if (mcpSdkClient) { try { await mcpSdkClient.close(); console.log('[Container Smoke Test] MCP client closed'); } catch (e) { console.error('[Container Smoke Test] Error closing MCP client:', e); } mcpSdkClient = null; } // Clean up any Docker containers if (activeContainerName) { try { await cleanupDocker(activeContainerName); console.log(`[Container Smoke Test] Cleaned up container: ${activeContainerName}`); } catch (e) { console.error(`[Container Smoke Test] Error cleaning up container ${activeContainerName}:`, e); } activeContainerName = null; } }, 30000); // 30 second timeout for cleanup it('should successfully debug fibonacci.py in containerized server', async function() { console.log(`\n[Container Smoke Test] TEST START: fibonacci.py test at ${new Date().toISOString()}`); // Check Docker availability first const dockerAvailable = await isDockerAvailable(); if (!dockerAvailable) { throw new Error('Docker is required for this test but is not available. Please install Docker and ensure it is running.'); } let debugSessionId: string | undefined; const startTime = Date.now(); try { // 1. Build Docker image if needed console.log(`[${Date.now() - startTime}ms] Building Docker image...`); await ensureDockerImage(DOCKER_IMAGE); console.log(`[${Date.now() - startTime}ms] Docker image ready`); // 2. Create MCP client and connect using stdio transport with docker run console.log(`[${Date.now() - startTime}ms] Creating MCP client with Docker transport...`); mcpSdkClient = new Client({ name: "e2e-container-smoke-test-client", version: "0.1.0" }); // Generate unique container name activeContainerName = generateContainerName('mcp-fibonacci-test'); console.log(`[${Date.now() - startTime}ms] Using container name: ${activeContainerName}`); // Mount examples directory at a safe location const examplesMount = getVolumeMount( path.join(projectRoot, 'examples'), '/workspace/host-examples' ); console.log(`[${Date.now() - startTime}ms] Volume mount: ${examplesMount}`); // Use docker run directly in StdioClientTransport const transport = new StdioClientTransport({ command: 'docker', args: [ 'run', '--rm', '-i', '--name', activeContainerName, '-v', examplesMount, '-e', 'MCP_CONTAINER=true', '-e', `MCP_HOST_WORKSPACE=${projectRoot}`, DOCKER_IMAGE, 'stdio' ] }); console.log(`[${Date.now() - startTime}ms] Connecting to containerized MCP server...`); await mcpSdkClient.connect(transport); console.log(`[${Date.now() - startTime}ms] MCP SDK Client connected via stdio to container.`); // 3. Execute debug sequence with relative path (container mode) console.log(`[${Date.now() - startTime}ms] Starting debug sequence...`); const relativeFibonacciPath = 'host-examples/python/fibonacci.py'; const result = await executeDebugSequence( mcpSdkClient, relativeFibonacciPath, 'E2E Container Smoke Test Session' ); expect(result.success).toBe(true); debugSessionId = result.sessionId; console.log(`[${Date.now() - startTime}ms] Debug sequence completed successfully.`); } catch (error) { console.error(`[${Date.now() - startTime}ms] Unexpected error during test execution:`, error); // Capture container logs for debugging if (activeContainerName) { try { const logs = await getContainerLogs(activeContainerName); console.error(`[Container Smoke Test] Container logs:\n${logs}`); } catch (logError) { console.error('[Container Smoke Test] Could not retrieve container logs:', logError); } } // Check if it's a docker issue if (error instanceof Error && error.message.includes('docker')) { console.error('[Container Smoke Test] Docker container failed to start:', error); } throw error; } finally { // 4. Cleanup if (debugSessionId && mcpSdkClient) { try { await mcpSdkClient.callTool({ name: 'close_debug_session', arguments: { sessionId: debugSessionId } }); console.log(`[Container Smoke Test] Debug session ${debugSessionId} closed.`); } catch (e) { console.error(`[Container Smoke Test] Error closing debug session ${debugSessionId}:`, e); } } } }, { timeout: TEST_TIMEOUT }); // Test path handling in container mode it('should handle paths naturally in container mode', async function() { console.log(`\n[Container Smoke Test] TEST START: path handling test at ${new Date().toISOString()}`); // Check Docker availability first const dockerAvailable = await isDockerAvailable(); if (!dockerAvailable) { throw new Error('Docker is required for this test but is not available. Please install Docker and ensure it is running.'); } const tempTestDir = path.join(os.tmpdir(), 'mcp-container-test-' + Date.now()); let debugSessionId: string | undefined; const startTime = Date.now(); try { // 1. Create a temporary test directory with a Python script console.log(`[${Date.now() - startTime}ms] Creating temp test directory: ${tempTestDir}`); await ensureDir(tempTestDir); const testScript = ` import time print("Container path test script") x = 42 # Line 3 - breakpoint here print(f"x = {x}") `; const testScriptPath = path.join(tempTestDir, 'test_container.py'); await writeFile(testScriptPath, testScript.trim()); // 2. Create MCP client with temp directory mounted console.log(`[${Date.now() - startTime}ms] Creating MCP client with temp directory mount...`); mcpSdkClient = new Client({ name: "e2e-container-path-test-client", version: "0.1.0" }); // Generate unique container name activeContainerName = generateContainerName('mcp-path-test'); console.log(`[${Date.now() - startTime}ms] Using container name: ${activeContainerName}`); // Mount temp directory at /workspace/test-mount to avoid overwriting the app const tempMount = getVolumeMount(tempTestDir, '/workspace/test-mount'); console.log(`[${Date.now() - startTime}ms] Volume mount: ${tempMount}`); // Use docker run directly in StdioClientTransport const transport = new StdioClientTransport({ command: 'docker', args: [ 'run', '--rm', '-i', '--name', activeContainerName, '-v', tempMount, '-e', 'MCP_CONTAINER=true', '-e', `MCP_HOST_WORKSPACE=${tempTestDir}`, DOCKER_IMAGE, 'stdio' ] }); console.log(`[${Date.now() - startTime}ms] Connecting to containerized MCP server...`); await mcpSdkClient.connect(transport); console.log(`[${Date.now() - startTime}ms] Connected to container.`); // 3. Create debug session console.log(`[${Date.now() - startTime}ms] Creating debug session...`); const createCall = await mcpSdkClient.callTool({ name: 'create_debug_session', arguments: { language: 'python', name: 'Container Path Test Session' } }); const createResponse = parseSdkToolResult(createCall); expect(createResponse.sessionId).toBeDefined(); debugSessionId = createResponse.sessionId; // 4. With "hands-off" approach, we pass paths through - container handles them console.log(`[${Date.now() - startTime}ms] Testing path handling with hands-off approach...`); console.log('[Container Smoke Test] Note: We no longer pre-validate paths'); console.log('[Container Smoke Test] Paths are passed through and handled by the container/debugpy'); // 5. Now test with a relative path (should work) console.log(`[${Date.now() - startTime}ms] Setting breakpoint with relative path...`); const relativeBreakpointCall = await mcpSdkClient.callTool({ name: 'set_breakpoint', arguments: { sessionId: debugSessionId, file: 'test-mount/test_container.py', // Path relative to /workspace line: 3 } }); const relativeBreakpointResponse = parseSdkToolResult(relativeBreakpointCall); expect(relativeBreakpointResponse.success).toBe(true); console.log('[Container Smoke Test] Relative path accepted successfully'); // 5.5. Log debug information about paths console.log('[Container Smoke Test] Container mount info:'); console.log(` - Host path: ${tempTestDir}`); console.log(` - Container path: /workspace/test-mount`); console.log(` - Script relative path: test-mount/test_container.py`); console.log(` - Expected container full path: /workspace/test-mount/test_container.py`); // 6. Start debugging with relative path console.log(`[${Date.now() - startTime}ms] Starting debugging with relative path...`); console.log('[Container Smoke Test] Test expectations:'); console.log(' - Container mode is enabled (MCP_CONTAINER=true)'); console.log(` - Host temp directory: ${tempTestDir}`); console.log(' - Container mount point: /workspace/test-mount'); console.log(' - File created: test_container.py'); console.log(' - Using relative path: test-mount/test_container.py'); console.log(' - Expected behavior: Server should resolve relative path from /workspace'); console.log(' - Expected full path in container: /workspace/test-mount/test_container.py'); const debugCall = await mcpSdkClient.callTool({ name: 'start_debugging', arguments: { sessionId: debugSessionId, scriptPath: 'test-mount/test_container.py', // Path relative to /workspace dapLaunchArgs: { stopOnEntry: false } } }); const debugResponse = parseSdkToolResult(debugCall); // Add detailed logging to understand the failure console.log('[Container Smoke Test] Debug response:', JSON.stringify(debugResponse, null, 2)); if (!debugResponse.success) { console.error('[Container Smoke Test] ❌ FAILURE: start_debugging failed with relative path'); console.error('[Container Smoke Test] Error message:', debugResponse.message || debugResponse.error); console.error('[Container Smoke Test] Full response:', debugResponse); console.error('[Container Smoke Test] This indicates a bug in the container path resolution logic.'); console.error('[Container Smoke Test] Possible root causes:'); console.error(' 1. Container working directory is not set to /workspace'); console.error(' 2. PathTranslator is not handling relative paths correctly in container mode'); console.error(' 3. The Python debugger launch is not using the translated path'); console.error(' 4. File permissions or visibility issue in the container'); } // Test should fail if relative path doesn't work expect(debugResponse.success).toBe(true); console.log('[Container Smoke Test] ✅ SUCCESS: Relative path handling works correctly in container mode'); } catch (error) { console.error(`[${Date.now() - startTime}ms] Error during path translation test:`, error); // Capture container logs for debugging if (activeContainerName) { try { const logs = await getContainerLogs(activeContainerName); console.error(`[Container Smoke Test] Container logs:\n${logs}`); } catch (logError) { console.error('[Container Smoke Test] Could not retrieve container logs:', logError); } } throw error; } finally { // Cleanup if (debugSessionId && mcpSdkClient) { try { await mcpSdkClient.callTool({ name: 'close_debug_session', arguments: { sessionId: debugSessionId } }); } catch (e) { console.error(`[Container Smoke Test] Error closing debug session:`, e); } } // Clean up temp directory if (tempTestDir) { try { await remove(tempTestDir); console.log('[Container Smoke Test] Temp directory cleaned up'); } catch (e) { console.error('[Container Smoke Test] Error cleaning up temp directory:', e); } } } }, { timeout: TEST_TIMEOUT }); });