UNPKG

embark-mcp

Version:

MCP server proxy for Embark code search

404 lines (339 loc) 10.5 kB
#!/usr/bin/env node /** * Test script for multiple roots functionality * * This script tests whether the MCP server properly handles multiple root directories * and allows searching across different repositories. */ import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Configuration const SERVER_PATH = join(__dirname, 'dist', 'index.js'); // Colors for terminal output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', gray: '\x1b[90m', }; function log(message, color = colors.reset) { console.log(`${color}${message}${colors.reset}`); } function logSuccess(message) { log(`✓ ${message}`, colors.green); } function logError(message) { log(`✗ ${message}`, colors.red); } function logInfo(message) { log(`ℹ ${message}`, colors.blue); } function logDebug(message) { log(` ${message}`, colors.gray); } /** * Send a JSON-RPC request to the server */ function sendRequest(server, request) { return new Promise((resolve, reject) => { const requestStr = JSON.stringify(request) + '\n'; logDebug(`→ ${requestStr.trim()}`); let responseBuffer = ''; let resolved = false; const onData = (data) => { responseBuffer += data.toString(); // Try to parse complete JSON objects const lines = responseBuffer.split('\n'); responseBuffer = lines.pop(); // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const response = JSON.parse(line); logDebug(`← ${line}`); // Check if this is the response to our request if (response.id === request.id) { resolved = true; server.stdout.off('data', onData); resolve(response); } } catch (e) { // Not valid JSON, continue accumulating } } } }; server.stdout.on('data', onData); // Timeout after 30 seconds (longer for search operations) setTimeout(() => { if (!resolved) { server.stdout.off('data', onData); reject(new Error('Request timeout')); } }, 30000); server.stdin.write(requestStr); }); } /** * Test initialization with multiple roots */ async function testInitializeWithRoots(server, roots) { log('\n--- Testing initialization with multiple roots ---', colors.yellow); const request = { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: { roots: { listChanged: true, }, }, clientInfo: { name: 'test-client', version: '1.0.0', }, }, }; try { const response = await sendRequest(server, request); if (response.error) { logError(`Initialization error: ${JSON.stringify(response.error)}`); return false; } if (!response.result) { logError('Initialization response missing result'); return false; } logSuccess('Server initialized successfully'); return true; } catch (error) { logError(`Failed to initialize: ${error.message}`); return false; } } /** * Send roots/list_changed notification */ async function sendRootsNotification(server, roots) { log('\n--- Sending roots/list_changed notification ---', colors.yellow); logInfo(`Sending ${roots.length} roots:`); roots.forEach((root, i) => { logInfo(` ${i + 1}. ${root.uri}`); }); const notification = { jsonrpc: '2.0', method: 'notifications/roots/list_changed', }; const notificationStr = JSON.stringify(notification) + '\n'; logDebug(`→ ${notificationStr.trim()}`); server.stdin.write(notificationStr); // Wait a bit for processing await new Promise(resolve => setTimeout(resolve, 500)); logSuccess('Notification sent'); return true; } /** * Test tools/list */ async function testListTools(server) { log('\n--- Testing tools/list ---', colors.yellow); const request = { jsonrpc: '2.0', id: 3, method: 'tools/list', }; try { const response = await sendRequest(server, request); if (response.error) { logError(`Server returned error: ${JSON.stringify(response.error)}`); return false; } if (!response.result || !response.result.tools) { logError('Response missing tools'); return false; } logSuccess(`Found ${response.result.tools.length} tool(s)`); response.result.tools.forEach(tool => { logInfo(` - ${tool.name}`); }); return true; } catch (error) { logError(`Failed to list tools: ${error.message}`); return false; } } /** * Test semantic_code_search tool call across multiple repositories */ async function testSemanticSearch(server) { log('\n--- Testing semantic_code_search with multiple repositories ---', colors.yellow); const request = { jsonrpc: '2.0', id: 4, method: 'tools/call', params: { name: 'semantic_code_search', arguments: { text: 'function that handles logging or configuration', }, }, }; try { logInfo('Sending search request (this may take a few seconds)...'); const response = await sendRequest(server, request); if (response.error) { logError(`Server returned error: ${JSON.stringify(response.error)}`); return false; } if (!response.result || !response.result.content) { logError('Response missing content'); return false; } const content = response.result.content[0]?.text || ''; // Check if the response mentions multiple repositories const hasMultipleRepos = content.includes('## Repository:'); const repoMatches = content.match(/## Repository:/g); const repoCount = repoMatches ? repoMatches.length : 0; if (hasMultipleRepos) { logSuccess(`Search executed across ${repoCount} repository/repositories`); // Extract repository names from the response const repoNames = content.match(/## Repository: ([^\n]+)/g); if (repoNames) { repoNames.forEach((name, i) => { logInfo(` ${i + 1}. ${name.replace('## Repository: ', '')}`); }); } if (repoCount > 1) { logSuccess('Multi-repository search confirmed!'); } else { logInfo('Only one repository was searched (this is expected if only one has a Git remote)'); } } else { // Single repository response format if (content.includes('Found') && content.includes('results')) { logSuccess('Search executed successfully (single repository)'); logInfo('Note: Only one repository was discovered/searched'); } else { logError('Unexpected response format'); logDebug(content.substring(0, 200)); return false; } } return true; } catch (error) { logError(`Failed to execute search: ${error.message}`); return false; } } /** * Main test runner */ async function runTests() { log('='.repeat(60), colors.blue); log('MCP Server Multiple Roots Testing', colors.blue); log('='.repeat(60), colors.blue); logInfo(`Server path: ${SERVER_PATH}`); // Define test roots (multiple Git repositories) const testRoots = [ { uri: 'file:///Users/potomushto/Projects/temp/embark-mcp', name: 'Embark MCP', }, { uri: 'file:///tmp/test-repo-1', name: 'Test Repo 1', }, { uri: 'file:///tmp/test-repo-2', name: 'Test Repo 2', }, ]; // Spawn the server log('\n--- Starting MCP server ---', colors.yellow); const server = spawn('node', [SERVER_PATH], { env: { ...process.env, // Don't set REPOSITORY_GIT_REMOTE_URL to test roots discovery }, stdio: ['pipe', 'pipe', 'pipe'], }); // Log stderr for debugging server.stderr.on('data', (data) => { logDebug(`[stderr] ${data.toString().trim()}`); }); server.on('error', (error) => { logError(`Failed to start server: ${error.message}`); process.exit(1); }); // Wait a bit for server to start await new Promise(resolve => setTimeout(resolve, 500)); let allTestsPassed = true; try { // Test 1: Initialize with roots capability const initSuccess = await testInitializeWithRoots(server, testRoots); if (!initSuccess) { allTestsPassed = false; log('\n❌ Initialization test failed', colors.red); } // Test 2: Send roots notification if (initSuccess) { const rootsSuccess = await sendRootsNotification(server, testRoots); if (!rootsSuccess) { allTestsPassed = false; log('\n❌ Roots notification test failed', colors.red); } } // Test 3: List tools if (initSuccess) { const toolsSuccess = await testListTools(server); if (!toolsSuccess) { allTestsPassed = false; log('\n❌ Tools list test failed', colors.red); } } // Test 4: Execute semantic search to verify multi-repo functionality if (initSuccess) { const searchSuccess = await testSemanticSearch(server); if (!searchSuccess) { allTestsPassed = false; log('\n❌ Semantic search test failed', colors.red); } } // Summary log('\n' + '='.repeat(60), colors.blue); if (allTestsPassed) { log('✓ All tests passed!', colors.green); log('\nTest results:', colors.yellow); log('✓ Server initialized with multiple roots capability', colors.green); log('✓ Roots notification sent successfully', colors.green); log('✓ Tools listed successfully', colors.green); log('✓ Semantic search executed and verified', colors.green); log('\nNote: Check logs for details on repository discovery', colors.yellow); log('='.repeat(60), colors.blue); server.kill(); process.exit(0); } else { log('✗ Some tests failed', colors.red); log('='.repeat(60), colors.blue); server.kill(); process.exit(1); } } catch (error) { logError(`Test execution failed: ${error.message}`); console.error(error); server.kill(); process.exit(1); } } // Run tests runTests().catch((error) => { logError(`Fatal error: ${error.message}`); console.error(error); process.exit(1); });