embark-mcp
Version:
MCP server proxy for Embark code search
404 lines (339 loc) • 10.5 kB
JavaScript
/**
* 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);
});