Fix/mcp baseline scan llm error crash#190
Conversation
…ilures Fixes KeyValueSoftwareSystems#189. chatCompletionJsonContent's 400-retry loop discarded response bodies without draining them, which could exhaust the fetch connection pool on rapid repeated requests (e.g. an invalid model name that 400s on every retry) and throw an opaque TypeError: fetch failed instead of the intended LLM HTTP 400 error. Separately, scanResources and scanToolDescriptions called judgeToolResponse with no error handling, so any judge/LLM failure during MCP pre-flight baseline scans crashed the entire run instead of just that scan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughBaseline scanner functions scanResources and scanToolDescriptions now catch judgeToolResponse exceptions and convert them into mcpErrorJudge results instead of crashing. Separately, the OpenAI-compatible client adds a drainBody helper that consumes discarded response bodies before retrying HTTP 400 requests. ChangesRobust error handling for baseline scans and LLM retries
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
core/src/execute/baselineScanner.ts (1)
131-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract a helper to eliminate duplicated catch logic.
Both
scanResourcesandscanToolDescriptionsuse an identical try/catch pattern aroundjudgeToolResponsewith the same catch-block logic. Extracting a wrapper would remove the duplication and make the error-handling intent explicit:♻️ Proposed helper extraction
+async function safeJudgeToolResponse( + args: Parameters<typeof judgeToolResponse>[0] +): Promise<JudgeResult> { + try { + return await judgeToolResponse(args); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return mcpErrorJudge(message); + } +}Then both call sites simplify to:
- let judgeResult; - try { - judgeResult = await judgeToolResponse({ - model: judgeModelConfig, - evaluator: { ... }, - ... - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - judgeResult = mcpErrorJudge(message); - } + const judgeResult = await safeJudgeToolResponse({ + model: judgeModelConfig, + evaluator: { ... }, + ... + });This also eliminates the untyped
let judgeResult;(evolvingany) in favor of a properly typedconst.Also applies to: 185-207
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/src/execute/baselineScanner.ts` around lines 131 - 152, The try/catch around judgeToolResponse in baselineScanner is duplicated across scanResources and scanToolDescriptions, and the untyped let judgeResult can be avoided. Extract the shared judge execution/error-handling into a small helper that accepts the evaluator/tool metadata and returns the judge result, using the same mcpErrorJudge fallback in one place. Then update both scanResources and scanToolDescriptions to call that helper and use a typed const result instead of evolving any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@core/src/execute/baselineScanner.ts`:
- Around line 131-152: The try/catch around judgeToolResponse in baselineScanner
is duplicated across scanResources and scanToolDescriptions, and the untyped let
judgeResult can be avoided. Extract the shared judge execution/error-handling
into a small helper that accepts the evaluator/tool metadata and returns the
judge result, using the same mcpErrorJudge fallback in one place. Then update
both scanResources and scanToolDescriptions to call that helper and use a typed
const result instead of evolving any.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1bc75804-b9e5-40f3-9131-d3e0936f955a
⛔ Files ignored due to path filters (1)
assets/opfor-high-level.svgis excluded by!**/*.svg
📒 Files selected for processing (2)
core/src/execute/baselineScanner.tscore/src/llm/openaiCompatible.ts
Problem
Running
opfor runagainst an MCP target with a brokenattackerLlm/judgeLlmmodel (e.g. an invalid model name behind an OpenAI-compatible gateway like LiteLLM) could intermittently crash the entire run with a raw, unhandledTypeError: fetch failedinstead of the clean, actionableError: LLM HTTP 400: ...message the gateway actually returned.Root cause was two compounding issues:
chatCompletionJsonContent(core/src/llm/openaiCompatible.ts) retries up to 3 times on HTTP 400 (strippingjson_objectmode, thentemperature) to work around providers that reject those params. It can't distinguish "this param isn't supported" from "this model doesn't exist" — an invalid model returns 400 on every attempt regardless of these params. Each retry fired immediately, back-to-back, without ever draining the previous (discarded) response body. An unconsumedfetchresponse body holds its connection open; issuing several rapid requests to the same host without draining prior bodies can exhaust the connection pool and throw an opaqueTypeError: fetch failedthat masks the real, informativeLLM HTTP 400: ...error the function is designed to produce.chatCompletionJsonContentand the CLI's top-level handler caught this failure.judgeToolResponsecalls it with no try/catch, and bothscanResourcesandscanToolDescriptionsincore/src/execute/baselineScanner.tscalledjudgeToolResponsewith no try/catch either — so any transient failure during the MCP pre-flight baseline scans (which run before the real evaluator suite even starts) took down the entire run/report instead of failing just that one scan.Solution
chatCompletionJsonContentnow drains each discarded response's body (via a smalldrainBodyhelper) before firing the next retry attempt, preventing connection-pool exhaustion across the up-to-3 rapid requests.scanResourcesandscanToolDescriptionsnow wrap theirjudgeToolResponsecalls in try/catch, converting a caught error into anERROR-verdict result via the existingmcpErrorJudgehelper — consistent with the error-handling patternscanResourcesalready used fortarget.readResource()failures. A judge/LLM failure on one resource or tool no longer aborts the whole run; it's now recorded as anERRORand the scan continues to the next item.Changes
core/src/llm/openaiCompatible.ts— adddrainBody(), call it before each 400-retry inchatCompletionJsonContent.core/src/execute/baselineScanner.ts— wrap thejudgeToolResponsecalls inscanResourcesandscanToolDescriptionsin try/catch, falling back tomcpErrorJudge(message).Issue
Closes #189
How to test
owasp-mcp-top10) withattackerLlmpointed at anopenai-compatiblegateway and an intentionally invalidmodelname.opfor run --config <path>.ERRORwith the underlyingLLM HTTP 400: ...message surfaced in the judge'serrorMessage/reasoning, and the run continues through the rest of the evaluator suite (or completes/reports normally) instead of crashing with an unhandled exception.npm run build && npm run typecheck && npm test --workspace=coreall pass (verified locally — 158/158 core tests green).Screenshots
N/A — CLI/log-output bug fix, no UI changes.
Summary by CodeRabbit