/**
* MCP Client Helper for Testing
* Provides a simplified interface to interact with the MCP server for testing
*/
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
export class McpClient extends EventEmitter {
constructor(serverProcess) {
super();
this.process = serverProcess;
this.requestId = 1;
this.pendingRequests = new Map();
this.isConnected = false;
this.setupProcessHandlers();
}
setupProcessHandlers() {
let buffer = '';
this.process.stdout.on('data', (data) => {
buffer += data.toString();
// Process complete JSON-RPC messages
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
try {
const message = JSON.parse(line.trim());
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse message:', line, error.message);
}
}
}
});
this.process.stderr.on('data', (data) => {
// Server logs go to stderr, we can ignore or log them
const logLine = data.toString();
if (process.env.DEBUG_MCP) {
console.error('[MCP-SERVER]', logLine);
}
});
this.process.on('exit', (code) => {
this.isConnected = false;
console.error(`MCP server exited with code ${code}`);
});
}
handleMessage(message) {
if (message.id && this.pendingRequests.has(message.id)) {
const { resolve, reject } = this.pendingRequests.get(message.id);
this.pendingRequests.delete(message.id);
if (message.error) {
reject(new Error(`MCP Error: ${message.error.message || JSON.stringify(message.error)}`));
} else {
resolve(message.result);
}
}
}
async sendRequest(method, params = {}) {
return new Promise((resolve, reject) => {
const id = this.requestId++;
const request = {
jsonrpc: '2.0',
id,
method,
params
};
this.pendingRequests.set(id, { resolve, reject });
// Send request
this.process.stdin.write(JSON.stringify(request) + '\n');
// Set timeout
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error(`Request timeout for ${method}`));
}
}, 30000); // 30 second timeout
});
}
async initialize() {
try {
await this.sendRequest('initialize', {
protocolVersion: '2024-11-05',
capabilities: {
roots: {
listChanged: true
},
sampling: {}
},
clientInfo: {
name: 'test-client',
version: '1.0.0'
}
});
await this.sendRequest('initialized');
this.isConnected = true;
return true;
} catch (error) {
console.error('Failed to initialize MCP client:', error.message);
return false;
}
}
async listTools() {
try {
const result = await this.sendRequest('tools/list');
return result.tools || [];
} catch (error) {
console.error('Failed to list tools:', error.message);
return [];
}
}
async callTool(name, arguments_ = {}) {
try {
return await this.sendRequest('tools/call', {
name,
arguments: arguments_
});
} catch (error) {
console.error(`Failed to call tool ${name}:`, error.message);
throw error;
}
}
async close() {
if (this.process && !this.process.killed) {
this.process.kill();
}
}
}
export async function createMcpClient() {
// Start the MCP server process
const serverProcess = spawn('node', ['dist/index.js'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test'
}
});
const client = new McpClient(serverProcess);
// Wait a bit for the server to start
await new Promise(resolve => setTimeout(resolve, 1000));
// Initialize the connection
const initialized = await client.initialize();
if (!initialized) {
throw new Error('Failed to initialize MCP client');
}
return client;
}
export default McpClient;