fix(smoke): make the /inspect auth probe independent of concurrent traffic
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m3s
CI/CD / lint (pull_request) Successful in 2m5s
CI/CD / test (pull_request) Successful in 1m17s
CI/CD / build (pull_request) Successful in 2m12s
CI/CD / smoke (pull_request) Failing after 2m42s
CI/CD / publish (pull_request) Has been skipped

The probe waited for a 3s socket-inactivity timeout and treated the rejection
as proof the endpoint was reachable. That only holds when nothing else is
talking to mcplocal — /inspect streams every project's MCP traffic, so any
other smoke file running concurrently keeps the socket busy and the timeout
never fires. Adding one more traffic-generating smoke file was enough to tip
it into a 10s test timeout; it passed in isolation the whole time.

Resolve on response headers instead. That removes the dependence on the rest
of the suite being quiet, and it is a stronger assertion than before: the test
now reads the real status code and content-type, where a timeout-means-success
probe would have passed just as happily against a slow 401.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5NAoE7VJA5TWvHsVfEmUr
This commit is contained in:
Michal
2026-08-05 01:27:30 +01:00
parent 2da627e77f
commit de906f9c75

View File

@@ -32,6 +32,16 @@ function httpRequest(opts: {
headers?: Record<string, string>; headers?: Record<string, string>;
body?: string; body?: string;
timeout?: number; 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 }> { }): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parsed = new URL(opts.url); const parsed = new URL(opts.url);
@@ -46,6 +56,12 @@ function httpRequest(opts: {
timeout: opts.timeout ?? 10_000, timeout: opts.timeout ?? 10_000,
}, },
(res) => { (res) => {
if (opts.resolveOnHeaders === true) {
resolve({ status: res.statusCode ?? 0, headers: res.headers, body: '' });
res.destroy();
req.destroy();
return;
}
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => { res.on('end', () => {
@@ -92,22 +108,19 @@ describe('Smoke: Security — mcplocal unauthenticated endpoints', () => {
if (!available) return; if (!available) return;
// /inspect streams ALL MCP traffic (tool calls, arguments, responses) // /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({ const res = await httpRequest({
url: `${MCPLOCAL_URL}/inspect`, url: `${MCPLOCAL_URL}/inspect`,
method: 'GET', method: 'GET',
headers: { 'Accept': 'text/event-stream' }, headers: { 'Accept': 'text/event-stream' },
timeout: 3_000, timeout: 5_000,
}).catch((err) => { resolveOnHeaders: true,
// 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;
}); });
// Should be accessible without auth (documenting the vulnerability) // Should be accessible without auth (documenting the vulnerability)
expect(res.status).toBeLessThan(400); expect(res.status).toBeLessThan(400);
expect(res.headers['content-type']).toContain('text/event-stream');
console.log(` ⚠ /inspect accessible without auth (status ${res.status})`); console.log(` ⚠ /inspect accessible without auth (status ${res.status})`);
}, 10_000); }, 10_000);