#!/usr/bin/env node /** * MCP Test Client * * Programmatically tests the diligence MCP server by: * 1. Spawning the server as a child process * 2. Sending JSON-RPC messages via stdio * 3. Receiving and parsing responses * * Usage: * const client = new McpClient(); * await client.connect(); * const result = await client.callTool('status', {}); * await client.disconnect(); */ import { spawn } from 'child_process'; import { createInterface } from 'readline'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); export class McpClient { constructor(serverPath = join(__dirname, '..', 'index.mjs')) { this.serverPath = serverPath; this.process = null; this.requestId = 0; this.pendingRequests = new Map(); this.readline = null; } async connect() { return new Promise((resolve, reject) => { this.process = spawn('node', [this.serverPath], { stdio: ['pipe', 'pipe', 'pipe'], cwd: join(__dirname, 'fixture'), // Run in fixture directory }); this.readline = createInterface({ input: this.process.stdout, crlfDelay: Infinity, }); this.readline.on('line', (line) => { try { const message = JSON.parse(line); if (message.id !== undefined && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); this.pendingRequests.delete(message.id); if (message.error) { reject(new Error(message.error.message || JSON.stringify(message.error))); } else { resolve(message.result); } } } catch (e) { // Ignore non-JSON lines } }); this.process.stderr.on('data', (data) => { // Server logs to stderr if (process.env.DEBUG) { console.error('[server]', data.toString()); } }); this.process.on('error', reject); this.process.on('exit', (code) => { if (code !== 0 && code !== null) { console.error(`Server exited with code ${code}`); } }); // Initialize the MCP connection this._send({ jsonrpc: '2.0', id: this.requestId++, method: 'initialize', params: { protocolVersion: '0.1.0', clientInfo: { name: 'test-client', version: '1.0.0' }, capabilities: {}, }, }).then(() => { // Send initialized notification this._sendNotification('notifications/initialized', {}); resolve(); }).catch(reject); }); } async disconnect() { if (this.process) { this.process.kill('SIGTERM'); this.process = null; } if (this.readline) { this.readline.close(); this.readline = null; } } _send(message) { return new Promise((resolve, reject) => { if (!this.process) { reject(new Error('Not connected')); return; } this.pendingRequests.set(message.id, { resolve, reject }); this.process.stdin.write(JSON.stringify(message) + '\n'); // Timeout after 10 seconds setTimeout(() => { if (this.pendingRequests.has(message.id)) { this.pendingRequests.delete(message.id); reject(new Error('Request timeout')); } }, 10000); }); } _sendNotification(method, params) { if (!this.process) return; this.process.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params, }) + '\n'); } async listTools() { const result = await this._send({ jsonrpc: '2.0', id: this.requestId++, method: 'tools/list', params: {}, }); return result.tools; } async callTool(name, args = {}) { const result = await this._send({ jsonrpc: '2.0', id: this.requestId++, method: 'tools/call', params: { name, arguments: args }, }); // Extract text from content array if (result.content && result.content[0] && result.content[0].text) { return { text: result.content[0].text, isError: result.isError || false, }; } return result; } // Convenience methods for common workflows async status() { return this.callTool('status'); } async start(task) { return this.callTool('start', { task }); } async propose(proposal) { return this.callTool('propose', { proposal }); } async review(decision, reasoning) { return this.callTool('review', { decision, reasoning }); } async getWorkerBrief() { return this.callTool('get_worker_brief'); } async getReviewerBrief() { return this.callTool('get_reviewer_brief'); } async implement() { return this.callTool('implement'); } async complete(summary) { return this.callTool('complete', { summary }); } async abort(reason) { return this.callTool('abort', { reason }); } async approve(reason) { return this.callTool('approve', { reason }); } } // CLI usage for quick testing if (process.argv[1] === fileURLToPath(import.meta.url)) { const client = new McpClient(); try { console.log('Connecting to MCP server...'); await client.connect(); console.log('Connected!\n'); // List tools const tools = await client.listTools(); console.log('Available tools:'); tools.forEach(t => console.log(` - ${t.name}: ${t.description.slice(0, 60)}...`)); console.log(); // Check status const status = await client.status(); console.log('Status:'); console.log(status.text); await client.disconnect(); console.log('\nDisconnected.'); } catch (err) { console.error('Error:', err.message); await client.disconnect(); process.exit(1); } }