diff --git a/src/mcplocal/tests/smoke/security.test.ts b/src/mcplocal/tests/smoke/security.test.ts index 19a4848..e19f6cd 100644 --- a/src/mcplocal/tests/smoke/security.test.ts +++ b/src/mcplocal/tests/smoke/security.test.ts @@ -32,6 +32,16 @@ function httpRequest(opts: { headers?: Record; body?: string; timeout?: number; + /** + * Resolve as soon as response headers arrive, without waiting for the body + * to end. Required for endpoints that never end: an SSE stream's socket only + * goes idle when nothing else is talking to mcplocal, so waiting for the + * inactivity timeout made this depend on whether other smoke files happened + * to be generating traffic concurrently. Resolving on headers is also a + * STRONGER assertion — the caller sees the real status instead of inferring + * "reachable" from a timeout, which would pass just as happily on a slow 401. + */ + resolveOnHeaders?: boolean; }): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> { return new Promise((resolve, reject) => { const parsed = new URL(opts.url); @@ -46,6 +56,12 @@ function httpRequest(opts: { timeout: opts.timeout ?? 10_000, }, (res) => { + if (opts.resolveOnHeaders === true) { + resolve({ status: res.statusCode ?? 0, headers: res.headers, body: '' }); + res.destroy(); + req.destroy(); + return; + } const chunks: Buffer[] = []; res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('end', () => { @@ -92,22 +108,19 @@ describe('Smoke: Security — mcplocal unauthenticated endpoints', () => { if (!available) return; // /inspect streams ALL MCP traffic (tool calls, arguments, responses) - // for ALL projects to any unauthenticated local client + // for ALL projects to any unauthenticated local client. The stream never + // ends, so take the status off the response headers and hang up. const res = await httpRequest({ url: `${MCPLOCAL_URL}/inspect`, method: 'GET', headers: { 'Accept': 'text/event-stream' }, - timeout: 3_000, - }).catch((err) => { - // Timeout is expected (SSE keeps connection open) — still means endpoint is accessible - if ((err as Error).message.includes('timed out')) { - return { status: 200, headers: {} as http.IncomingHttpHeaders, body: '' }; - } - throw err; + timeout: 5_000, + resolveOnHeaders: true, }); // Should be accessible without auth (documenting the vulnerability) expect(res.status).toBeLessThan(400); + expect(res.headers['content-type']).toContain('text/event-stream'); console.log(` ⚠ /inspect accessible without auth (status ${res.status})`); }, 10_000);