Skip to main content
Glama
mcpServerTestHarness.ts8.1 kB
/** * MCP Server Test Harness * * Utilities for testing the MCP server by starting it in a subprocess * and communicating with it via the MCP protocol. */ import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; import { join } from 'path'; import { setTimeout as sleep } from 'timers/promises'; import { v4 as uuidv4 } from 'uuid'; export class McpServerTestHarness { private serverProcess: ChildProcessWithoutNullStreams | null = null; private messageQueue: Array<{ id: string; resolve: (value: any) => void; reject: (reason: any) => void; }> = []; private isServerReady = false; /** * Start the MCP server subprocess */ async start(): Promise<void> { // Start the server process this.serverProcess = spawn('node', [join(process.cwd(), 'build', 'index.js')], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_ENV: 'test' } }); // Handle stderr for logging/debugging this.serverProcess.stderr.on('data', (data) => { const logMessage = data.toString(); // For debugging console.log(`[Server stderr]: ${logMessage}`); if (logMessage.includes('tmux server started') || logMessage.includes('Terminally-mcp server running on stdio')) { console.log('Server is ready! Detected startup message.'); this.isServerReady = true; } }); // Handle stdout for MCP protocol responses this.serverProcess.stdout.on('data', (data) => { const dataStr = data.toString(); // Log the raw data for debugging console.log(`[Server stdout]: ${dataStr}`); // Check for server ready messages in stdout as well if (dataStr.includes('tmux server started') || dataStr.includes('Terminally-mcp server running on stdio')) { console.log('Server is ready! Detected startup message in stdout.'); this.isServerReady = true; } try { // Try to parse line-by-line in case we get multiple responses or partial data const lines = dataStr.trim().split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const response = JSON.parse(line); if (!response.id) continue; // Skip non-jsonrpc responses const pendingRequest = this.messageQueue.find(req => req.id === response.id); if (pendingRequest) { // Remove from queue this.messageQueue = this.messageQueue.filter(req => req.id !== response.id); if (response.error) { pendingRequest.reject(response.error); } else { pendingRequest.resolve(response.result); } } } catch (lineError) { // Skip non-JSON lines, they might be debug output // console.error('Error parsing line:', lineError); } } } catch (error) { console.error('Error processing server response:', error); } }); // Handle process exit this.serverProcess.on('exit', (code) => { if (code !== 0 && code !== null) { console.error(`Server process exited with code ${code}`); } this.isServerReady = false; this.serverProcess = null; }); // Wait for server to be ready let retries = 0; // Increased retries for potentially slower startup in test env while (!this.isServerReady && retries < 20) { await sleep(500); retries++; } if (!this.isServerReady) { console.error('Server readiness flag never became true.'); throw new Error('Server failed to start or become ready in time'); } } /** * Stop the MCP server subprocess */ async stop(): Promise<void> { if (this.serverProcess) { this.serverProcess.kill(); this.serverProcess = null; this.isServerReady = false; this.messageQueue = []; } } /** * Send an MCP protocol message to the server */ async sendRequest<T = any>(method: string, params: any): Promise<T> { if (!this.serverProcess || !this.isServerReady) { throw new Error('Server is not running'); } const id = uuidv4(); const request = { jsonrpc: '2.0', id, method, params }; const requestJson = JSON.stringify(request); console.log(`[Harness sending]: ${requestJson}`); // Log the request return new Promise<T>((resolve, reject) => { this.messageQueue.push({ id, resolve, reject }); this.serverProcess?.stdin.write(requestJson + '\n'); }); } /** * Get all available tools */ async listTools(): Promise<any[]> { return this.sendRequest('tools/list', {}); } /** * Call the create_tab tool */ async createTab(name?: string): Promise<string> { try { console.log('Calling create_tab tool...'); const response = await this.sendRequest<any>('tools/call', { name: 'create_tab', arguments: name ? { name } : {} }); // Parse the MCP response format if (!response || !response.content || !response.content[0]) { throw new Error(`Invalid response from create_tab: ${JSON.stringify(response)}`); } const result = JSON.parse(response.content[0].text); if (!result || !result.window_id) { throw new Error(`Invalid result from create_tab: ${JSON.stringify(result)}`); } return result.window_id; } catch (error) { console.error('Error calling create_tab:', error); throw error; } } /** * Call the stop_process tool to close a tab (kills the main process) */ async closeTab(windowId: string): Promise<boolean> { const response = await this.sendRequest<any>('tools/call', { name: 'stop_process', arguments: { window_id: windowId } }); // Parse the MCP response format if (!response || !response.content || !response.content[0]) { throw new Error(`Invalid response from stop_process: ${JSON.stringify(response)}`); } const result = JSON.parse(response.content[0].text); return result.success; } /** * Call the list_tabs tool */ async listTabs(): Promise<Array<{ window_id: string, name: string, active: boolean }>> { const response = await this.sendRequest<any>('tools/call', { name: 'list_tabs', arguments: {} }); // Parse the MCP response format if (!response || !response.content || !response.content[0]) { throw new Error(`Invalid response from list_tabs: ${JSON.stringify(response)}`); } const result = JSON.parse(response.content[0].text); return result.tabs; } /** * Call the execute_command tool */ async executeCommand(windowId: string, command: string, timeout?: number): Promise<string> { const response = await this.sendRequest<any>('tools/call', { name: 'execute_command', arguments: { window_id: windowId, command, timeout_ms: timeout } }); // Parse the MCP response format if (!response || !response.content || !response.content[0]) { throw new Error(`Invalid response from execute_command: ${JSON.stringify(response)}`); } const result = JSON.parse(response.content[0].text); return result.output; } /** * Call the read_logs_from_tab tool */ async readOutput(windowId: string, historyLimit?: number): Promise<string> { const response = await this.sendRequest<any>('tools/call', { name: 'read_logs_from_tab', arguments: { window_id: windowId, lines: historyLimit } }); // Parse the MCP response format if (!response || !response.content || !response.content[0]) { throw new Error(`Invalid response from read_logs_from_tab: ${JSON.stringify(response)}`); } const result = JSON.parse(response.content[0].text); return result.content; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NightTrek/Terminally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server