Skip to main content
Glama
mcp-client.test.ts9.1 kB
/** * Story 2: Pre-WebSocket Command Execution - MCPClient Unit Tests * * Test the MCPClient class for communicating with MCP server via stdin/stdout * These tests will validate the core functionality needed for pre-WebSocket command execution */ import { MCPClient } from './mcp-client'; import { MCPServerManager } from './mcp-server-manager'; import { ChildProcess } from 'child_process'; describe('MCPClient Unit Tests', () => { let mcpClient: MCPClient; let serverManager: MCPServerManager; let serverProcess: ChildProcess; beforeEach(async () => { // Start fresh MCP server for each test serverManager = new MCPServerManager(); await serverManager.start(); const processInfo = serverManager.getProcess(); if (!processInfo || !processInfo.stdin || !processInfo.stdout) { throw new Error('Failed to start MCP server for testing'); } serverProcess = { stdin: processInfo.stdin, stdout: processInfo.stdout } as ChildProcess; mcpClient = new MCPClient(serverProcess); }); afterEach(async () => { if (mcpClient) { await mcpClient.disconnect(); } if (serverManager) { await serverManager.stop(); } }); describe('Basic MCP Protocol Communication', () => { test('should send MCP request and receive response for ssh_connect', async () => { // This test will fail initially as MCPClient doesn't exist yet const connectParams = { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' }; const response = await mcpClient.callTool('ssh_connect', connectParams); expect(response).toBeDefined(); expect(response).toHaveProperty('success'); expect(typeof response.success).toBe('boolean'); }); test('should handle MCP protocol errors gracefully', async () => { // Test invalid method call const response = await mcpClient.callTool('invalid_method', {}); expect(response).toBeDefined(); expect(response.success).toBe(false); expect(response.error).toMatch(/Unknown tool|Method not found/i); }); test('should correlate requests and responses using unique IDs', async () => { // Send multiple requests concurrently to test ID correlation const requests = [ mcpClient.callTool('ssh_connect', { name: 'session1', host: 'localhost', username: 'user1', password: 'pass1' }), mcpClient.callTool('ssh_connect', { name: 'session2', host: 'localhost', username: 'user2', password: 'pass2' }) ]; const responses = await Promise.all(requests); expect(responses).toHaveLength(2); expect(responses[0]).toBeDefined(); expect(responses[1]).toBeDefined(); }); }); describe('Sequential Command Execution', () => { test('should execute commands in sequence - ssh_connect then ssh_exec', async () => { // First connect const connectResponse = await mcpClient.callTool('ssh_connect', { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' }); expect(connectResponse).toHaveProperty('success', true); // Then execute command const execResponse = await mcpClient.callTool('ssh_exec', { sessionName: 'test-session', command: 'ls' }); expect(execResponse).toHaveProperty('success'); if (execResponse.success) { expect(execResponse).toHaveProperty('result'); expect(execResponse.result).toHaveProperty('stdout'); } }); test('should wait for response before sending next command', async () => { const startTime = Date.now(); // Send connect command (this should take some time) const connectResponse = await mcpClient.callTool('ssh_connect', { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' }); const connectTime = Date.now(); expect(connectTime - startTime).toBeGreaterThan(0); // Ensure some time passed // Send exec command (should only start after connect completes) const execResponse = await mcpClient.callTool('ssh_exec', { sessionName: 'test-session', command: 'echo "sequential test"' }); const execTime = Date.now(); expect(execTime - connectTime).toBeGreaterThan(0); // Ensure sequential execution expect(connectResponse).toHaveProperty('success'); expect(execResponse).toHaveProperty('success'); }); }); describe('MCP Protocol Message Format', () => { test('should format MCP requests as valid JSON-RPC 2.0', async () => { // Verify message format by monitoring stdin directly let capturedMessage: string = ''; const originalWrite = serverProcess.stdin!.write.bind(serverProcess.stdin!); // Intercept writes to capture the actual JSON-RPC message serverProcess.stdin!.write = ((data: any) => { if (typeof data === 'string') { capturedMessage = data; } return originalWrite(data); }) as any; await mcpClient.callTool('ssh_connect', { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' }); // Restore original write method serverProcess.stdin!.write = originalWrite; // Parse and verify the captured message const parsedMessage = JSON.parse(capturedMessage.trim()); expect(parsedMessage).toMatchObject({ jsonrpc: '2.0', method: 'tools/call', params: { name: 'ssh_connect', arguments: { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' } }, id: expect.any(String) }); }); test('should parse MCP responses correctly', async () => { const response = await mcpClient.callTool('ssh_connect', { name: 'test-session', host: 'localhost', username: 'test_user', password: 'password123' }); // Response should be parsed from JSON and have expected structure expect(response).toBeInstanceOf(Object); expect(response).toHaveProperty('success'); expect(typeof response.success).toBe('boolean'); }); }); describe('Error Handling', () => { test('should handle connection failures', async () => { // Create client with invalid process const invalidProcess = { stdin: null, stdout: null } as any as ChildProcess; expect(() => new MCPClient(invalidProcess)) .toThrow(/Process must have stdin and stdout streams/); }); test('should handle malformed responses from server', async () => { // Test client handling of non-JSON output by using a real server with debug output // Create a separate server manager that might produce debug output const testServerManager = new MCPServerManager({ timeout: 15000 // Longer timeout to allow for debug output }); await testServerManager.start(); const testProcessInfo = testServerManager.getProcess(); if (!testProcessInfo || !testProcessInfo.stdin || !testProcessInfo.stdout) { throw new Error('Failed to start test MCP server'); } const testServerProcess = { stdin: testProcessInfo.stdin, stdout: testProcessInfo.stdout } as ChildProcess; const testClient = new MCPClient(testServerProcess); try { // Make a real call - the MCPClient should filter out any debug output automatically const response = await testClient.callTool('ssh_connect', { name: 'test-malformed', host: 'localhost', username: 'test_user', password: 'password123' }); // Should succeed despite potential debug output from server expect(response).toBeDefined(); expect(typeof response.success).toBe('boolean'); } finally { await testClient.disconnect(); await testServerManager.stop(); } }); test('should handle command timeout scenarios', async () => { // Test timeout with a real server but very short timeout // Create client with extremely short timeout to force timeout condition const clientWithTimeout = new MCPClient(serverProcess, { timeout: 50 }); try { // This should timeout because 50ms is too short for any real server response await expect(clientWithTimeout.callTool('ssh_connect', { name: 'test-timeout', host: 'localhost', username: 'test', password: 'test' })).rejects.toThrow(/timeout/i); } finally { await clientWithTimeout.disconnect(); } }); }); });

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/LightspeedDMS/ssh-mcp'

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