Skip to main content
Glama
mcp-stdio-pollution-reproduction.test.ts6.56 kB
/** * TDD Reproduction Test: MCP Server Stdio Pollution Issue * * CRITICAL BUG: SSH Connection Manager constructor outputs console.log to stdout, * corrupting MCP JSON-RPC communication over stdio transport. * * Expected Error: "Expected substring: \"\"jsonrpc\":\"2.0\"\" * Received string: "🏗️ SSH CONNECTION MANAGER CONSTRUCTED\n"" * * This test reproduces the exact failure scenario with strict TDD methodology. */ import { spawn, ChildProcess } from "child_process"; import * as path from "path"; import * as fs from "fs"; describe("MCP Server Stdio Pollution - TDD Reproduction", () => { let serverProcess: ChildProcess; afterEach(async () => { if (serverProcess?.pid) { serverProcess.kill('SIGTERM'); // Wait for cleanup await new Promise<void>((resolve) => { serverProcess.once('exit', () => resolve()); setTimeout(() => resolve(), 2000); }); } }); describe("FAILING TEST - Stdio Pollution Detection", () => { it("should detect stdout pollution in MCP server initialization", async () => { // FAILING TEST: This should fail because of stdout pollution const serverPath = path.join(process.cwd(), "dist/src/mcp-server.js"); // Ensure server file exists expect(fs.existsSync(serverPath)).toBe(true); // Start MCP server process serverProcess = spawn("node", [serverPath], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env } }); // Test that first stdout output is clean JSON-RPC, not construction message const firstOutputPromise = new Promise<string>((resolve) => { serverProcess.stdout!.once('data', (data: Buffer) => { resolve(data.toString()); }); }); // Send initialize message to trigger server response 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"; serverProcess.stdin!.write(initMessage); // Wait for first output with timeout const firstOutput = await Promise.race([ firstOutputPromise, new Promise<string>((_, reject) => setTimeout(() => reject(new Error("No server output within timeout")), 5000) ) ]); // ASSERTION THAT SHOULD FAIL: First output should be JSON, not construction message expect(firstOutput).toContain('"jsonrpc":"2.0"'); expect(firstOutput).not.toContain('🏗️ SSH CONNECTION MANAGER CONSTRUCTED'); // Additional validation: Ensure it's valid JSON-RPC try { const parsed = JSON.parse(firstOutput.trim()); expect(parsed).toHaveProperty('jsonrpc', '2.0'); expect(parsed).toHaveProperty('id', 1); } catch (error) { fail(`First output is not valid JSON: ${firstOutput}`); } }, 10000); it("should maintain clean stdio channel for all MCP communication", async () => { // FAILING TEST: Verify no pollution in subsequent communication const serverPath = path.join(process.cwd(), "dist/src/mcp-server.js"); serverProcess = spawn("node", [serverPath], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env } }); let outputCount = 0; const outputs: string[] = []; // Collect multiple outputs const outputCollector = (data: Buffer) => { const output = data.toString(); outputs.push(output); outputCount++; }; serverProcess.stdout!.on('data', outputCollector); // Send multiple MCP messages const messages = [ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0.0" } } }, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }, { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "ssh_list_sessions", arguments: {} } } ]; for (const message of messages) { serverProcess.stdin!.write(JSON.stringify(message) + "\n"); // Wait between messages await new Promise(resolve => setTimeout(resolve, 100)); } // Wait for responses await new Promise(resolve => setTimeout(resolve, 1000)); // ASSERTIONS THAT SHOULD FAIL: All outputs should be clean JSON-RPC expect(outputs.length).toBeGreaterThan(0); // Join all outputs and check for pollution const allOutput = outputs.join(''); expect(allOutput).not.toContain('🏗️'); expect(allOutput).not.toContain('SSH CONNECTION MANAGER CONSTRUCTED'); // Split concatenated JSON responses by newlines and validate each const jsonLines = allOutput.split('\n').filter(line => line.trim()); for (let i = 0; i < jsonLines.length; i++) { const line = jsonLines[i].trim(); if (line.startsWith('{')) { try { const parsed = JSON.parse(line); expect(parsed).toHaveProperty('jsonrpc', '2.0'); } catch (error) { throw new Error(`JSON line ${i} is not valid: "${line}"`); } } } }, 10000); }); describe("Port File Management - TDD Reproduction", () => { it("should create port file with proper format and location", async () => { // FAILING TEST: Verify port file creation during server startup const serverPath = path.join(process.cwd(), "dist/src/mcp-server.js"); const portFilePath = path.join(process.cwd(), ".ssh-mcp-server.port"); // Clean up any existing port file try { fs.unlinkSync(portFilePath); } catch { // Ignore if doesn't exist } serverProcess = spawn("node", [serverPath], { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env } }); // Wait for server initialization await new Promise(resolve => setTimeout(resolve, 2000)); // ASSERTIONS THAT MIGHT FAIL: Port file should exist with valid content expect(fs.existsSync(portFilePath)).toBe(true); const portContent = fs.readFileSync(portFilePath, "utf8").trim(); const portNumber = parseInt(portContent); expect(portNumber).toBeGreaterThan(1000); expect(portNumber).toBeLessThan(65536); expect(isNaN(portNumber)).toBe(false); }, 10000); }); });

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