Repository navigation
Description
The lie
#3719 made every caught exception on both SKUs answer with one wire shape — McpHelpers.FormatError returns {"status":"error","message":"Error during get_x: …","hints":{"operation":"get_x"}} — and the payload contract now says "errors one shape". But a refusal is not an error, and the refusals never got a shape at all. Roughly four hundred validation sites on both SKUs still return the validator's bare sentence as the whole tool result: the ServerResolver bails ("Could not resolve server …", ~190 sites) and the ValidateHoursBack / ValidateTop / ValidateLimit / "Missing required parameter" / "No plan XML provided." sentences (~210 sites), all of the if (error != null) return error; idiom. A client parsing the result gets a string where every other outcome is JSON, and nothing in it says whether the tool failed, refused, or found nothing.
Seven PostgreSQL refusals took a different path and landed worse. They hand-build the envelope with McpHelpers.Status("error", …), and since #3719 the web surface reads status:"error" as a server fault:
get_pg_deadlocks—limit(Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPgDeadlockTools.cs:38)get_pg_deadlock_detail—limit(DarlingMcpPgDeadlockTools.cs:132)get_pg_log_events—limit(DarlingMcpPgLogEventTools.cs:46),family(:53),min_severity(:65)get_pg_server_config—limit(DarlingMcpPgServerStateTools.cs:615)get_pg_server_config_changes—limit(DarlingMcpPgServerStateTools.cs:716)
Before #3719, DarlingWebEndpoints.ClassifyToolResponse passed anything starting with { through as HTTP 200, so a bad limit on /api/read/get_pg_deadlocks was a 200 carrying {status:"error"}. Now (Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs:2776-2795) McpHelpers.IsErrorEnvelope is tested first and maps to ToolResponseKind.ServerError → 500. A client-correctable refusal that answered 200 yesterday answers 500 today — a wire regression for the dashboard's consumers, and the wrong code twice over: neither 200 nor 500 is the 400 a refusal deserves. #3719 stated this consequence in its body and pinned the present behaviour (DarlingWebEndpointsTests.ClassifyToolResponse_TheErrorEnvelope_IsServerError) so a ruling changes it knowingly; this issue is that ruling's home.
The bare-sentence refusals, meanwhile, happen to map to 400 on the web (ClassifyToolResponse's final arm: not JSON, not Error during → ClientError), which is the right HTTP code — but only by the accident of being unshaped. Shaping them as status:"error" without a ruling would move all ~400 of them to 500 too.
What is verified at source (origin/dev 98d52fe)
PerformanceMonitor.Common/Mcp/McpHelpers.cs:507FormatError,:537IsErrorEnvelope,:598Status(status, message, hints).DarlingWebEndpoints.cs:2776-2795ClassifyToolResponse: envelope → ServerError; JSON → passthrough;Error during→ ServerError; else ClientError.- The seven
Status("error", …)refusal sites listed above (git grep 'Status("error"' -- 'Darling/**/Mcp/DarlingMcpPg*.cs'returns exactly these seven). - MCP errors take one wire shape: FormatError returns the {status:"error"} envelope on both SKUs (Partial #3653, Q11) #3719's body, "Validation refusals" paragraph: "the fix needs a ruling (which status word —
errorcollides with the web 400/500 split;invalidalready means a write body that would not parse)".
The fix shape (after the ruling)
- Ruling: one status word for a refusal.
erroris taken (fault → 500) andinvalidis taken (a write body that would not parse). The candidates are a new word (refused/rejected) or wideninginvalidto "the request as given cannot be served" with the write-body case as one instance of it. Whatever the word,ClassifyToolResponsegrows an arm that maps it to 400, ahead of the envelope arm. - The seven: change
Status("error", …)to the ruled word at the seven sites — that alone restores them from 500 to a correct 400. - The ~400:
ServerResolver's miss and theValidate*helpers build the sentence today; the shape change belongs in those few producers (aMcpHelpers.Refusal(operation, sentence)twin ofFormatError), not at four hundred call sites.McpPayloadContractCensusTestsgets the arm: a refusal returns through the shared shape, and the census that already classifies every catch site classifies everyreturn error;site too. wwwroot/js/util.jsclassifyResponseandDarlingTriageEndpointlearn the word (both learnedstatus:"error"in MCP errors take one wire shape: FormatError returns the {status:"error"} envelope on both SKUs (Partial #3653, Q11) #3719 — same two sites).- Tool descriptions: MCP errors take one wire shape: FormatError returns the {status:"error"} envelope on both SKUs (Partial #3653, Q11) #3719's Q11 ruling asked for the wire change to be noted in the descriptions; the refusal word gets the same sentence.
What this does NOT do
It does not touch the four miss words (empty / not_found / …, #3703's lane) — a refusal is a fourth kind of outcome beside success, miss and fault, not a miss.
Activity
MCP refusals get their status word: every validation bail on both SKU…
erikdarlingdata commentedon Sep 20, 2026
Closed by #3776 (merged to dev).
The get_pg_log_events live pin reads the family / min_severity refusa…
An argument the tool does not declare is refused instead of dropped (#…