/**
* MCP CLI End-to-End Tests
* Full workflow testing of the MCP server CLI interface
*/
import { spawn, ChildProcess } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
describe('MCP CLI E2E Tests', () => {
let serverProcess: ChildProcess;
const testTimeout = 30000;
beforeAll(async () => {
// Ensure the project is built
await runCommand('pnpm', ['build']);
});
afterEach(async () => {
if (serverProcess) {
serverProcess.kill('SIGTERM');
serverProcess = null as any;
}
});
describe('CLI Arguments', () => {
it('should show help when --help is provided', async () => {
const result = await runCommand('node', ['dist/index.js', '--help']);
expect(result.stdout).toContain('Claude Code AI Collaboration MCP Server');
expect(result.stdout).toContain('Usage:');
expect(result.stdout).toContain('Options:');
expect(result.stdout).toContain('--protocol');
expect(result.stdout).toContain('--providers');
expect(result.exitCode).toBe(0);
}, testTimeout);
it('should show version when --version is provided', async () => {
const result = await runCommand('node', ['dist/index.js', '--version']);
expect(result.stdout).toMatch(/^\d+\.\d+\.\d+/);
expect(result.exitCode).toBe(0);
}, testTimeout);
it('should handle unknown arguments gracefully', async () => {
const result = await runCommand('node', ['dist/index.js', '--unknown-option']);
expect(result.stderr).toContain('Unknown option: --unknown-option');
expect(result.exitCode).toBe(1);
}, testTimeout);
});
describe('Server Startup', () => {
it('should start with default configuration', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error' // Reduce log noise
}
});
let startupComplete = false;
let startupError = '';
// Monitor stderr for startup messages
process.stderr.on('data', (data) => {
const message = data.toString();
if (message.includes('MCP Server started successfully')) {
startupComplete = true;
}
if (message.includes('error') || message.includes('Error')) {
startupError += message;
}
});
// Wait for startup or timeout
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
process.kill('SIGTERM');
reject(new Error('Server startup timeout'));
}, 10000);
process.on('exit', (code) => {
clearTimeout(timeout);
if (code !== 0 && !startupComplete) {
reject(new Error(`Server exited with code ${code}: ${startupError}`));
} else {
resolve(code);
}
});
// Check if startup completed
const checkStartup = setInterval(() => {
if (startupComplete) {
clearInterval(checkStartup);
clearTimeout(timeout);
process.kill('SIGTERM');
resolve(0);
}
}, 100);
});
expect(startupComplete).toBe(true);
expect(startupError).toBe('');
}, testTimeout);
it('should start with custom protocol configuration', async () => {
const result = await runCommand('node', [
'dist/index.js',
'--protocol', 'stdio',
'--providers', 'deepseek',
'--no-cache'
], {
timeout: 5000,
killSignal: 'SIGTERM'
});
// The process should start and be killed by timeout, not error
expect(result.exitCode).not.toBe(1); // Not an error exit
}, testTimeout);
});
describe('Environment Variable Configuration', () => {
it('should respect environment variables', async () => {
const env = {
...process.env,
NODE_ENV: 'test',
MCP_PROTOCOL: 'stdio',
MCP_PROVIDERS: 'deepseek,openai',
MCP_DEFAULT_PROVIDER: 'deepseek',
MCP_DISABLE_CACHING: 'true',
LOG_LEVEL: 'error'
};
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env
});
let configDetected = false;
process.stderr.on('data', (data) => {
const message = data.toString();
if (message.includes('deepseek') || message.includes('stdio')) {
configDetected = true;
}
});
// Give it time to start and read config
await new Promise(resolve => setTimeout(resolve, 2000));
process.kill('SIGTERM');
// Wait for process to exit
await new Promise(resolve => process.on('exit', resolve));
// Config should have been read (though we can't easily verify exact values)
expect(process.killed || process.exitCode === 0).toBe(true);
}, testTimeout);
});
describe('Signal Handling', () => {
it('should handle SIGTERM gracefully', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
// Wait for startup
await new Promise(resolve => setTimeout(resolve, 2000));
// Send SIGTERM
process.kill('SIGTERM');
// Wait for graceful shutdown
const exitCode = await new Promise<number>((resolve) => {
process.on('exit', resolve);
});
expect(exitCode).toBe(0);
}, testTimeout);
it('should handle SIGINT gracefully', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
// Wait for startup
await new Promise(resolve => setTimeout(resolve, 2000));
// Send SIGINT
process.kill('SIGINT');
// Wait for graceful shutdown
const exitCode = await new Promise<number>((resolve) => {
process.on('exit', resolve);
});
expect(exitCode).toBe(0);
}, testTimeout);
});
describe('JSON-RPC Communication', () => {
it('should handle JSON-RPC requests via stdio', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
let responseReceived = false;
let responseData = '';
process.stdout.on('data', (data) => {
responseData += data.toString();
if (responseData.includes('"result"')) {
responseReceived = true;
}
});
// Wait for startup
await new Promise(resolve => setTimeout(resolve, 2000));
// Send initialize request
const initRequest = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'test-client',
version: '1.0.0'
}
}
};
process.stdin.write(JSON.stringify(initRequest) + '\n');
// Wait for response
await new Promise(resolve => setTimeout(resolve, 1000));
process.kill('SIGTERM');
await new Promise(resolve => process.on('exit', resolve));
expect(responseReceived).toBe(true);
// Parse the response
const lines = responseData.trim().split('\n');
const responseLine = lines.find(line => line.includes('"result"'));
if (responseLine) {
const response = JSON.parse(responseLine);
expect(response.jsonrpc).toBe('2.0');
expect(response.id).toBe(1);
expect(response.result).toBeDefined();
}
}, testTimeout);
it('should handle ping requests', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
let pingResponse = '';
process.stdout.on('data', (data) => {
pingResponse += data.toString();
});
// Wait for startup
await new Promise(resolve => setTimeout(resolve, 2000));
// Send ping request
const pingRequest = {
jsonrpc: '2.0',
id: 2,
method: 'ping'
};
process.stdin.write(JSON.stringify(pingRequest) + '\n');
// Wait for response
await new Promise(resolve => setTimeout(resolve, 1000));
process.kill('SIGTERM');
await new Promise(resolve => process.on('exit', resolve));
// Verify ping response
const lines = pingResponse.trim().split('\n');
const responseLine = lines.find(line => line.includes('"status":"ok"'));
expect(responseLine).toBeDefined();
if (responseLine) {
const response = JSON.parse(responseLine);
expect(response.result.status).toBe('ok');
expect(response.result.timestamp).toBeDefined();
}
}, testTimeout);
});
describe('Error Conditions', () => {
it('should handle malformed JSON input', async () => {
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
let errorResponse = '';
process.stdout.on('data', (data) => {
errorResponse += data.toString();
});
// Wait for startup
await new Promise(resolve => setTimeout(resolve, 2000));
// Send malformed JSON
process.stdin.write('{ invalid json }\n');
// Wait for error response
await new Promise(resolve => setTimeout(resolve, 1000));
process.kill('SIGTERM');
await new Promise(resolve => process.on('exit', resolve));
// Should receive parse error
expect(errorResponse).toContain('Parse error');
}, testTimeout);
it('should handle missing dependencies gracefully', async () => {
// This test would be more relevant in a real deployment scenario
// For now, just verify the server can start with minimal config
const result = await runCommand('node', [
'dist/index.js',
'--providers', 'deepseek', // Only one provider
'--no-cache',
'--no-metrics'
], {
timeout: 3000,
killSignal: 'SIGTERM'
});
// Should not exit with error
expect(result.exitCode).not.toBe(1);
}, testTimeout);
});
describe('Performance', () => {
it('should start within reasonable time', async () => {
const startTime = Date.now();
const process = spawn('node', ['dist/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'test',
LOG_LEVEL: 'error'
}
});
let startupComplete = false;
process.stderr.on('data', (data) => {
const message = data.toString();
if (message.includes('MCP Server started successfully')) {
startupComplete = true;
}
});
// Wait for startup
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
process.kill('SIGTERM');
reject(new Error('Startup timeout'));
}, 5000);
const checkStartup = setInterval(() => {
if (startupComplete) {
clearInterval(checkStartup);
clearTimeout(timeout);
resolve(true);
}
}, 100);
});
const startupTime = Date.now() - startTime;
process.kill('SIGTERM');
// Startup should be reasonably fast (less than 5 seconds)
expect(startupTime).toBeLessThan(5000);
expect(startupComplete).toBe(true);
}, testTimeout);
});
});
// Helper function to run commands with Promise interface
function runCommand(
command: string,
args: string[],
options: { timeout?: number; killSignal?: string } = {}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
return new Promise((resolve, reject) => {
const process = spawn(command, args, { stdio: 'pipe' });
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
process.on('exit', (code) => {
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: code || 0
});
});
process.on('error', (error) => {
reject(error);
});
// Handle timeout
if (options.timeout) {
setTimeout(() => {
process.kill(options.killSignal || 'SIGTERM');
}, options.timeout);
}
});
}