Problem
Nothing in the connect-and-probe path has a timeout, and the tool is pointed at servers it explicitly assumes are untrustworthy.
In src/transport/probe.ts:
await client.connect(transport);
...
const tools = await safeList(async () => { const res = await client.listTools(); ... });
No timeout on connect, none on listTools, listResources, or listPrompts. The MCP SDK's request methods accept a RequestOptions with timeout, and none is passed. connectHttp in src/transport/http.ts builds a RequestInit without a signal. connectStdio in src/transport/stdio.ts spawns a child process and sets stderr: "ignore".
So:
- A server that accepts the connection and never answers
listTools hangs the audit forever. In CI that means a job that burns its full timeout budget and reports nothing.
- For stdio that hung child process is still alive.
client.close() is in the finally, but a promise that never settles never reaches finally.
- A server that answers with a very large tool list has no bound on what gets read into memory.
- With
stderr: "ignore", a server that fails to start produces a connect failure with no diagnostic, and the user has no way to see why.
Why it matters
The threat model of this tool is "I do not trust this MCP server, that is why I am scanning it". Hanging forever on a hostile target is a denial of service against the scanner, and it is the easiest possible evasion: a malicious server that stalls listTools is never audited, and the finding count is zero rather than an error.
Suggested approach
- Add a
--timeout <ms> flag with a sensible default (30s overall, per-request shorter), threaded through connectStdio, connectHttp, and probe.
- Pass
{ timeout } in the SDK RequestOptions for each list call, and wrap client.connect in a Promise.race with a rejecting timer.
- On timeout, kill the stdio child explicitly rather than relying on
close(), and exit 2 with a message naming which phase timed out. A timed-out audit must never look like a clean audit.
- Bound the response: cap the number of tools, resources, and prompts processed, and emit an informational finding when the cap is hit.
- Change
stderr: "ignore" to capture stderr and include the last few lines in the connect error message.
- Add a fixture server that hangs on
listTools, alongside the existing fixtures/mock-server.mjs, and test that the audit fails cleanly and promptly.
Done when
- Every network and IPC wait has a bounded timeout.
- A hanging server produces a non-zero exit with a clear message, not a hang.
- No child process outlives a failed audit.
- A hanging-server fixture is covered by a test.
If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.
Problem
Nothing in the connect-and-probe path has a timeout, and the tool is pointed at servers it explicitly assumes are untrustworthy.
In
src/transport/probe.ts:No timeout on
connect, none onlistTools,listResources, orlistPrompts. The MCP SDK's request methods accept aRequestOptionswithtimeout, and none is passed.connectHttpinsrc/transport/http.tsbuilds aRequestInitwithout a signal.connectStdioinsrc/transport/stdio.tsspawns a child process and setsstderr: "ignore".So:
listToolshangs the audit forever. In CI that means a job that burns its full timeout budget and reports nothing.client.close()is in thefinally, but a promise that never settles never reachesfinally.stderr: "ignore", a server that fails to start produces a connect failure with no diagnostic, and the user has no way to see why.Why it matters
The threat model of this tool is "I do not trust this MCP server, that is why I am scanning it". Hanging forever on a hostile target is a denial of service against the scanner, and it is the easiest possible evasion: a malicious server that stalls
listToolsis never audited, and the finding count is zero rather than an error.Suggested approach
--timeout <ms>flag with a sensible default (30s overall, per-request shorter), threaded throughconnectStdio,connectHttp, andprobe.{ timeout }in the SDKRequestOptionsfor each list call, and wrapclient.connectin aPromise.racewith a rejecting timer.close(), and exit 2 with a message naming which phase timed out. A timed-out audit must never look like a clean audit.stderr: "ignore"to capture stderr and include the last few lines in the connect error message.listTools, alongside the existingfixtures/mock-server.mjs, and test that the audit fails cleanly and promptly.Done when
If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.