@hanzo/dev
Version:
Hanzo Dev - Meta AI development CLI that manages and runs all LLMs and CLI tools
238 lines (201 loc) • 6.7 kB
text/typescript
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { MCPClient, MCPSession, MCPServerConfig } from '../src/lib/mcp-client';
import { EventEmitter } from 'events';
import * as child_process from 'child_process';
// Mock child_process
vi.mock('child_process');
describe('MCPClient', () => {
let client: MCPClient;
let mockProcess: any;
beforeEach(() => {
client = new MCPClient();
// Mock spawn to return a fake process
mockProcess = new EventEmitter();
mockProcess.stdin = { write: vi.fn() };
mockProcess.stdout = new EventEmitter();
mockProcess.stderr = new EventEmitter();
mockProcess.kill = vi.fn();
vi.mocked(child_process.spawn).mockReturnValue(mockProcess as any);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('stdio transport', () => {
test('should connect to MCP server via stdio', async () => {
const config: MCPServerConfig = {
name: 'test-server',
transport: 'stdio',
command: 'test-mcp-server',
args: ['--test']
};
// Start connection in background
const connectPromise = client.connect(config);
// Simulate server sending initialization response
setTimeout(() => {
mockProcess.stdout.emit('data', JSON.stringify({
jsonrpc: '2.0',
id: 1,
result: {
protocolVersion: '1.0',
serverInfo: { name: 'test-server', version: '1.0.0' }
}
}) + '\n');
// Simulate tools list response
mockProcess.stdout.emit('data', JSON.stringify({
jsonrpc: '2.0',
id: 2,
result: {
tools: [
{
name: 'test_tool',
description: 'A test tool',
parameters: {
type: 'object',
properties: {
input: { type: 'string' }
}
}
}
]
}
}) + '\n');
}, 10);
const session = await connectPromise;
expect(session).toBeDefined();
expect(session.tools).toHaveLength(1);
expect(session.tools[0].name).toBe('test_tool');
});
test('should handle server errors', async () => {
const config: MCPServerConfig = {
name: 'error-server',
transport: 'stdio',
command: 'failing-server'
};
const connectPromise = client.connect(config);
// Simulate process error
setTimeout(() => {
mockProcess.emit('error', new Error('Failed to start'));
}, 10);
await expect(connectPromise).rejects.toThrow('Failed to start');
});
});
describe('tool calling', () => {
test('should call tool on MCP server', async () => {
const session: MCPSession = {
serverName: 'test-server',
tools: [{
name: 'echo',
description: 'Echo input',
parameters: {
type: 'object',
properties: {
message: { type: 'string' }
}
}
}],
prompts: [],
resources: []
};
// Mock session in client
(client as any).sessions.set('test-server', session);
(client as any).processes.set('test-server', mockProcess);
// Start tool call
const callPromise = client.callTool('test-server', 'echo', { message: 'Hello' });
// Simulate server response
setTimeout(() => {
// Find the request that was sent
const writeCall = mockProcess.stdin.write.mock.calls[0];
const request = JSON.parse(writeCall[0]);
// Send response with same ID
mockProcess.stdout.emit('data', JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: {
output: 'Echo: Hello'
}
}) + '\n');
}, 10);
const result = await callPromise;
expect(result.output).toBe('Echo: Hello');
});
});
describe('session management', () => {
test('should list connected sessions', async () => {
// Mock two sessions
(client as any).sessions.set('server1', {
serverName: 'server1',
tools: [],
prompts: [],
resources: []
});
(client as any).sessions.set('server2', {
serverName: 'server2',
tools: [],
prompts: [],
resources: []
});
const sessions = client.listSessions();
expect(sessions).toHaveLength(2);
expect(sessions.map(s => s.serverName)).toContain('server1');
expect(sessions.map(s => s.serverName)).toContain('server2');
});
test('should disconnect from server', async () => {
const serverName = 'test-server';
// Mock session and process
(client as any).sessions.set(serverName, {
serverName,
tools: [],
prompts: [],
resources: []
});
(client as any).processes.set(serverName, mockProcess);
await client.disconnect(serverName);
expect(mockProcess.kill).toHaveBeenCalled();
expect(client.listSessions()).toHaveLength(0);
});
});
describe('error handling', () => {
test('should handle JSON-RPC errors', async () => {
const session: MCPSession = {
serverName: 'test-server',
tools: [{
name: 'failing_tool',
description: 'A tool that fails',
parameters: { type: 'object' }
}],
prompts: [],
resources: []
};
(client as any).sessions.set('test-server', session);
(client as any).processes.set('test-server', mockProcess);
const callPromise = client.callTool('test-server', 'failing_tool', {});
setTimeout(() => {
const writeCall = mockProcess.stdin.write.mock.calls[0];
const request = JSON.parse(writeCall[0]);
// Send error response
mockProcess.stdout.emit('data', JSON.stringify({
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601,
message: 'Method not found'
}
}) + '\n');
}, 10);
await expect(callPromise).rejects.toThrow('Method not found');
});
test('should handle malformed responses', async () => {
const config: MCPServerConfig = {
name: 'malformed-server',
transport: 'stdio',
command: 'test-server'
};
const connectPromise = client.connect(config);
setTimeout(() => {
// Send malformed JSON
mockProcess.stdout.emit('data', 'not valid json\n');
}, 10);
await expect(connectPromise).rejects.toThrow();
});
});
});