Skip to main content
Glama
mcp-stdio-cleanliness-validation.test.ts13.4 kB
import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; /** * CRITICAL TEST: MCP Protocol Stdio Cleanliness Validation * * PURPOSE: Ensure MCP server NEVER outputs to stdio, which would corrupt JSON-RPC protocol * CONTEXT: Code-reviewer found 60+ console statements that can break MCP communication * REQUIREMENT: MCP server must maintain clean stdio for JSON-RPC protocol integrity */ describe('MCP Protocol Stdio Cleanliness Validation', () => { let mcpProcess: ChildProcess; let stdoutBuffer: string = ''; let stderrBuffer: string = ''; let allProcessOutput: string = ''; const MCP_SERVER_PATH = path.join(__dirname, '../src/mcp-server.ts'); beforeEach(() => { stdoutBuffer = ''; stderrBuffer = ''; allProcessOutput = ''; }); afterEach(async () => { if (mcpProcess && !mcpProcess.killed) { mcpProcess.kill('SIGTERM'); await new Promise(resolve => { mcpProcess.on('exit', resolve); setTimeout(resolve, 2000); // Force cleanup after 2s }); } }); /** * TEST 1: Stdio Cleanliness During MCP Server Startup * REQUIREMENT: No console.log/console.error during server initialization */ test('MCP server startup produces ZERO console output to stdio', async () => { const startupPromise = new Promise<void>((resolve, reject) => { // Start MCP server mcpProcess = spawn('npx', ['tsx', MCP_SERVER_PATH], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); mcpProcess.stdout!.on('data', (data: Buffer) => { const output = data.toString(); stdoutBuffer += output; allProcessOutput += `STDOUT: ${output}`; }); mcpProcess.stderr!.on('data', (data: Buffer) => { const output = data.toString(); stderrBuffer += output; allProcessOutput += `STDERR: ${output}`; }); mcpProcess.on('error', (error) => { reject(new Error(`MCP process error: ${error.message}`)); }); // Allow 3 seconds for startup setTimeout(() => { resolve(); }, 3000); }); await startupPromise; // CRITICAL ASSERTION: Zero console output during startup expect(stderrBuffer.trim()).toBe(''); expect(stdoutBuffer).not.toMatch(/console\./); expect(allProcessOutput).not.toMatch(/Error|Warning|Log|Debug/i); // Log any violations for debugging if (stderrBuffer.length > 0 || stdoutBuffer.includes('console.')) { console.error('STDIO CLEANLINESS VIOLATION DETECTED:'); console.error('STDERR:', stderrBuffer); console.error('STDOUT (non-JSON):', stdoutBuffer.replace(/\{.*\}/g, '[JSON_REMOVED]')); } }); /** * TEST 2: Stdio Cleanliness During MCP JSON-RPC Communication * REQUIREMENT: Only valid JSON-RPC messages on stdout, nothing on stderr */ test('MCP server produces only valid JSON-RPC on stdout during communication', async () => { const communicationPromise = new Promise<void>((resolve, reject) => { mcpProcess = spawn('npx', ['tsx', MCP_SERVER_PATH], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); mcpProcess.stdout!.on('data', (data: Buffer) => { const output = data.toString(); stdoutBuffer += output; }); mcpProcess.stderr!.on('data', (data: Buffer) => { const output = data.toString(); stderrBuffer += output; allProcessOutput += `STDERR_VIOLATION: ${output}`; }); // Send MCP initialization message setTimeout(() => { const initMessage = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test-client", version: "1.0.0" } } }) + '\n'; mcpProcess.stdin!.write(initMessage); // Allow time for processing setTimeout(resolve, 2000); }, 1000); mcpProcess.on('error', reject); }); await communicationPromise; // CRITICAL ASSERTION: No stderr output during JSON-RPC communication expect(stderrBuffer.trim()).toBe(''); // CRITICAL ASSERTION: Only valid JSON-RPC on stdout const stdoutLines = stdoutBuffer.split('\n').filter(line => line.trim()); for (const line of stdoutLines) { if (line.trim()) { expect(() => JSON.parse(line)).not.toThrow(); const parsed = JSON.parse(line); expect(parsed).toHaveProperty('jsonrpc', '2.0'); } } // Log any violations if (stderrBuffer.length > 0) { console.error('JSON-RPC STDIO VIOLATION:', stderrBuffer); } }); /** * TEST 3: Console Statement Detection in Source Code * REQUIREMENT: Zero console.log/console.error in MCP server production code */ test('MCP server source code contains ZERO console statements', async () => { const sourceFiles = [ 'src/mcp-server.ts', 'src/orchestrator.ts', 'src/ssh-connection-manager.ts', 'src/web-server-manager.ts', 'src/command-state-manager.ts', 'src/types.ts' ]; const violations: string[] = []; for (const filePath of sourceFiles) { const fullPath = path.join(__dirname, '..', filePath); if (fs.existsSync(fullPath)) { const content = fs.readFileSync(fullPath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { // Only detect active console statements, not comments or commented-out code const trimmedLine = line.trim(); if (line.includes('console.') && !trimmedLine.startsWith('//') && !trimmedLine.startsWith('*') && !trimmedLine.startsWith('/*')) { violations.push(`${filePath}:${index + 1}: ${line.trim()}`); } }); } } // CRITICAL ASSERTION: Zero console statements in production code expect(violations).toEqual([]); if (violations.length > 0) { console.error('CONSOLE STATEMENT VIOLATIONS FOUND:'); violations.forEach(violation => console.error(violation)); } }); /** * TEST 4: Debug Mode Stdio Cleanliness * REQUIREMENT: Even in debug mode, MCP server must not pollute stdio */ test('MCP server in debug mode maintains stdio cleanliness', async () => { const debugPromise = new Promise<void>((resolve, reject) => { mcpProcess = spawn('npx', ['tsx', MCP_SERVER_PATH], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, DEBUG: '1', NODE_ENV: 'development' } }); mcpProcess.stdout!.on('data', (data: Buffer) => { stdoutBuffer += data.toString(); }); mcpProcess.stderr!.on('data', (data: Buffer) => { stderrBuffer += data.toString(); }); setTimeout(resolve, 3000); mcpProcess.on('error', reject); }); await debugPromise; // CRITICAL ASSERTION: Debug mode must not pollute stdio expect(stderrBuffer.trim()).toBe(''); // Verify stdout contains only JSON-RPC messages if (stdoutBuffer.trim()) { const lines = stdoutBuffer.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.trim()) { expect(() => JSON.parse(line)).not.toThrow(); } } } }); /** * TEST 5: Logging Abstraction Layer Verification * REQUIREMENT: Logger must route output away from stdio for MCP context */ test('Logger abstraction routes output correctly by transport type', async () => { const { Logger } = await import('../src/logger'); const fs = await import('fs'); const path = await import('path'); // TEST: 'stdio' transport produces NO console output const stdioLogger = new Logger('stdio', 'TEST'); // Capture console output during stdio logger usage const originalConsoleLog = console.log; const originalConsoleError = console.error; const originalConsoleWarn = console.warn; let consoleOutputDetected = false; console.log = (...args) => { consoleOutputDetected = true; originalConsoleLog(...args); }; console.error = (...args) => { consoleOutputDetected = true; originalConsoleError(...args); }; console.warn = (...args) => { consoleOutputDetected = true; originalConsoleWarn(...args); }; // Use stdio logger - should produce NO console output stdioLogger.info('Test message'); stdioLogger.error('Test error'); stdioLogger.warn('Test warning'); stdioLogger.debug('Test debug'); // Restore console console.log = originalConsoleLog; console.error = originalConsoleError; console.warn = originalConsoleWarn; // CRITICAL ASSERTION: stdio transport must not produce console output expect(consoleOutputDetected).toBe(false); // TEST: 'file' transport writes to log file const testLogPath = path.join(__dirname, 'test-log.txt'); const fileLogger = new Logger('file', 'TEST', testLogPath); fileLogger.info('File logger test message'); // Verify log file was created and contains message expect(fs.existsSync(testLogPath)).toBe(true); const logContent = fs.readFileSync(testLogPath, 'utf-8'); expect(logContent).toContain('File logger test message'); // Cleanup if (fs.existsSync(testLogPath)) { fs.unlinkSync(testLogPath); } // TEST: 'null' transport discards all output const nullLogger = new Logger('null', 'TEST'); // This should not throw and should not produce any output expect(() => { nullLogger.info('Null logger test'); nullLogger.error('Null logger error'); }).not.toThrow(); }); /** * TEST 6: Entry Point Error Handling Cleanliness * REQUIREMENT: Entry point error handlers must not pollute stdio during MCP execution * CRITICAL: mcp-server.ts main() and catch handlers must use stderr, not console.error */ test('Entry point error handlers maintain stdio cleanliness', async () => { // Test mcp-server.ts entry point with simulated startup error const errorTestPromise = new Promise<void>((resolve, reject) => { // Create a temporary invalid config to trigger error path const invalidConfigEnv = { ...process.env, SSH_KEY_PATH: '/invalid/nonexistent/path/key.pem' // Force error }; mcpProcess = spawn('npx', ['tsx', MCP_SERVER_PATH], { stdio: ['pipe', 'pipe', 'pipe'], env: invalidConfigEnv }); mcpProcess.stdout!.on('data', (data: Buffer) => { stdoutBuffer += data.toString(); }); mcpProcess.stderr!.on('data', (data: Buffer) => { stderrBuffer += data.toString(); }); mcpProcess.on('exit', () => { resolve(); }); mcpProcess.on('error', reject); // Allow time for error to occur and process to exit setTimeout(resolve, 5000); }); await errorTestPromise; // CRITICAL ASSERTION: Error output should go to stderr, not pollute stdout // Entry points can use stderr for critical errors, but NEVER stdout expect(stdoutBuffer.trim()).toBe(''); // No JSON pollution on stdout // If stderr contains error output, it should be process.stderr.write format, not console.error if (stderrBuffer.length > 0) { // Should not contain console.* patterns in stderr output expect(stderrBuffer).not.toMatch(/console\.[a-zA-Z]+\(/); } }); /** * TEST 7: Orchestrator Entry Point Stdio Cleanliness * REQUIREMENT: orchestrator.ts main() must not pollute stdio */ test('Orchestrator entry point maintains stdio cleanliness', async () => { const ORCHESTRATOR_PATH = path.join(__dirname, '../src/orchestrator.ts'); let orchestratorProcess: ChildProcess; let orchStdoutBuffer = ''; let orchStderrBuffer = ''; const orchTestPromise = new Promise<void>((resolve, reject) => { orchestratorProcess = spawn('npx', ['tsx', ORCHESTRATOR_PATH], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); orchestratorProcess.stdout!.on('data', (data: Buffer) => { orchStdoutBuffer += data.toString(); }); orchestratorProcess.stderr!.on('data', (data: Buffer) => { orchStderrBuffer += data.toString(); }); orchestratorProcess.on('error', reject); // Allow startup time then kill gracefully setTimeout(() => { if (orchestratorProcess && !orchestratorProcess.killed) { orchestratorProcess.kill('SIGTERM'); setTimeout(resolve, 1000); } else { resolve(); } }, 3000); }); await orchTestPromise; // CRITICAL ASSERTION: Orchestrator should produce minimal stdio output expect(orchStderrBuffer.trim()).toBe(''); // Stdout should only contain structured output if any, no console pollution expect(orchStdoutBuffer).not.toMatch(/console\./); expect(orchStdoutBuffer).not.toMatch(/Warning:|Error:|Log:/i); if (orchStderrBuffer.length > 0 || orchStdoutBuffer.includes('console.')) { console.error('ORCHESTRATOR STDIO VIOLATION:'); console.error('STDERR:', orchStderrBuffer); console.error('STDOUT:', orchStdoutBuffer); } }); });

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