From 3a66effd647074b172595005f945795fd4e9185a Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sat, 11 Jul 2026 00:00:21 -0600 Subject: [PATCH 1/2] fix(improvement): handle MCP verifier stdin pipe errors --- src/improvement/mcp-serve-verifier.ts | 4 ++++ tests/mcp-serve-verifier.test.ts | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/improvement/mcp-serve-verifier.ts b/src/improvement/mcp-serve-verifier.ts index d23a36a..cdab25c 100644 --- a/src/improvement/mcp-serve-verifier.ts +++ b/src/improvement/mcp-serve-verifier.ts @@ -100,6 +100,10 @@ export function mcpServeVerifier(spec: McpServeSpec): Verifier { // server crashed on boot); after we settle, our own SIGKILL fires here. failCandidate(`MCP server exited (code ${code}, signal ${signal}) before serving`) }) + child.stdin.on('error', (err) => { + // Pipe failures arrive asynchronously, outside send()'s try/catch. + failCandidate(`writing to MCP server stdin failed: ${err.message}`) + }) child.stderr.on('data', (d) => stderr.push(String(d))) const rl = createInterface({ input: child.stdout }) diff --git a/tests/mcp-serve-verifier.test.ts b/tests/mcp-serve-verifier.test.ts index 2b042ce..418c764 100644 --- a/tests/mcp-serve-verifier.test.ts +++ b/tests/mcp-serve-verifier.test.ts @@ -6,9 +6,17 @@ import { mcpServeVerifier } from '../src/improvement/mcp-serve-verifier' // A minimal stdio MCP server (newline-delimited JSON-RPC 2.0) used as the // "built server" the verifier boots. Variants exercise each outcome. -function serverScript(variant: 'ok' | 'no-tools' | 'crash' | 'hang'): string { +function serverScript(variant: 'ok' | 'no-tools' | 'crash' | 'hang' | 'closed-stdin'): string { if (variant === 'crash') return 'process.exit(1)\n' if (variant === 'hang') return 'setInterval(() => {}, 1000)\n' // never answers + if (variant === 'closed-stdin') { + return [ + 'import { closeSync } from "node:fs"', + 'closeSync(0)', + 'process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "fake", version: "0" } } }) + "\\n")', + 'setInterval(() => {}, 1000)', + ].join('\n') + } const tools = variant === 'no-tools' ? '[]' @@ -31,7 +39,7 @@ beforeEach(() => { }) afterEach(() => rmSync(dir, { recursive: true, force: true })) -function write(variant: 'ok' | 'no-tools' | 'crash' | 'hang'): string { +function write(variant: 'ok' | 'no-tools' | 'crash' | 'hang' | 'closed-stdin'): string { const path = join(dir, `server-${variant}.mjs`) writeFileSync(path, serverScript(variant)) return path @@ -57,6 +65,17 @@ describe('mcpServeVerifier — boot-and-probe', () => { expect(res.feedback).toMatch(/exited|serving/) }) + it('fails when the server closes stdin during the handshake', async () => { + const verify = mcpServeVerifier({ + command: 'node', + args: [write('closed-stdin')], + timeoutMs: 800, + }) + const res = await verify(dir) + expect(res.ok).toBe(false) + expect(res.feedback).toContain('writing to MCP server stdin failed') + }) + it('fails (does not hang) a server that never answers, via timeout', async () => { const verify = mcpServeVerifier({ command: 'node', args: [write('hang')], timeoutMs: 800 }) const res = await verify(dir) From 662e52c18d0839237f7c669d969b9e7a19c7bd69 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sat, 11 Jul 2026 00:17:02 -0600 Subject: [PATCH 2/2] test(improvement): make MCP pipe failure deterministic --- src/improvement/mcp-serve-verifier.ts | 10 +++++----- tests/mcp-serve-verifier.test.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/improvement/mcp-serve-verifier.ts b/src/improvement/mcp-serve-verifier.ts index cdab25c..0ca9e4e 100644 --- a/src/improvement/mcp-serve-verifier.ts +++ b/src/improvement/mcp-serve-verifier.ts @@ -73,6 +73,8 @@ export function mcpServeVerifier(spec: McpServeSpec): Verifier { const failCandidate = (msg: string) => settle(() => resolve({ ok: false, feedback: withStderr(msg) })) const setupFault = (err: Error) => settle(() => reject(err)) + const failStdin = (err: Error) => + failCandidate(`writing to MCP server stdin failed: ${err.message}`) const send = (msg: Record): boolean => { try { @@ -80,7 +82,7 @@ export function mcpServeVerifier(spec: McpServeSpec): Verifier { return true } catch (err) { // EPIPE: the server died mid-handshake — a failed candidate, not a fault. - failCandidate(`writing to MCP server stdin failed: ${(err as Error).message}`) + failStdin(err as Error) return false } } @@ -100,10 +102,8 @@ export function mcpServeVerifier(spec: McpServeSpec): Verifier { // server crashed on boot); after we settle, our own SIGKILL fires here. failCandidate(`MCP server exited (code ${code}, signal ${signal}) before serving`) }) - child.stdin.on('error', (err) => { - // Pipe failures arrive asynchronously, outside send()'s try/catch. - failCandidate(`writing to MCP server stdin failed: ${err.message}`) - }) + // Pipe failures normally arrive asynchronously, outside send()'s try/catch. + child.stdin.on('error', failStdin) child.stderr.on('data', (d) => stderr.push(String(d))) const rl = createInterface({ input: child.stdout }) diff --git a/tests/mcp-serve-verifier.test.ts b/tests/mcp-serve-verifier.test.ts index 418c764..ee9dbf5 100644 --- a/tests/mcp-serve-verifier.test.ts +++ b/tests/mcp-serve-verifier.test.ts @@ -12,8 +12,12 @@ function serverScript(variant: 'ok' | 'no-tools' | 'crash' | 'hang' | 'closed-st if (variant === 'closed-stdin') { return [ 'import { closeSync } from "node:fs"', - 'closeSync(0)', - 'process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: 1, result: { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "fake", version: "0" } } }) + "\\n")', + 'process.stdin.on("error", () => {})', + 'process.stdin.once("data", (chunk) => {', + ' const request = JSON.parse(String(chunk).trim())', + ' closeSync(0)', + ' process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "fake", version: "0" } } }) + "\\n")', + '})', 'setInterval(() => {}, 1000)', ].join('\n') }