Repository navigation
Six MCP reads answer 'nothing happened' and 'nothing collected' with identical bytes, and three of them disagree between the SKUs #2485
Description
The MCP half of a defect just fixed on the web half in #2477.
get_blocking_trend serializes { server, hours_back, trend } unconditionally. On a server that had no blocking, trend is [] — the same bytes a server with nothing collected yet produces. There is no {status, message} envelope, no count, nothing.
That was visible on the web dashboard because renderPanel had real data in hand and fell through to the chart's "Not enough data points to chart yet" — a warming-up message on a server that simply never blocked. #2477 fixes the browser at the renderer.
It is worse for an agent than it was for a person. A human sees an empty chart in a page whose other panels are populated and infers the server is fine. An MCP client has only the JSON. An LLM asked "is this server blocking?" can reasonably answer "there is no data" when the true answer is "no, and that is good news" — and on a monitoring tool that is close to the worst available failure, because it converts a clean bill of health into an apparent gap in coverage.
Scope of the pattern
Scanned every [McpServerTool] in Darling/PerformanceMonitor.Darling.Service/Mcp and Lite/Mcp for a top-level collection with no early-return on empty.
Darling — no distinction between "none happened" and "none collected":
| Tool | Empty shape | Web read? |
|---|---|---|
get_blocking_trend |
trend: [] |
yes |
get_deadlock_trend |
trend: [] |
yes |
get_memory_clerks |
clerks: [] |
yes |
get_wait_types |
wait_types: [] |
yes |
get_mute_rules |
bare list | yes |
compare_analysis |
bare comparison | yes |
Neighbours that already do this correctly, which is what makes the six read as an omission rather than a decision: get_wait_stats, get_memory_stats, get_file_io_trend, get_memory_trend, get_query_duration_trend and ~50 others all return McpHelpers.Status("unavailable", "…"); the three *_changes tools go further and return a purpose-built NoChanges(...) carrying hints: { server, snapshot_count } — which is precisely the "queried successfully, zero rows, here is the denominator" shape the trend tools want; and list_servers guards its empty registry with prose. So the vocabulary exists and these did not adopt it.
Lite — the same six, plus three where the two SKUs disagree:
get_file_io_trend, get_memory_trend and get_query_duration_trend return a bare empty array on Lite while their Darling twins return a status envelope. Same tool name, same client, two different answers depending on which SKU it is pointed at. That is a parity break independent of this issue's main point and is arguably the more urgent half.
What the fix probably looks like
Not simply "add an envelope" — for a periodic sample, an absence is only interpretable with its denominator, which is the reasoning behind the PostgreSQL pg_blocking read carrying its own capture counts from collection_log: the edge table cannot tell "no blocking" from "not collected", both being an absence of rows, but the capture count can. get_blocking_trend has exactly that property.
So the shape worth converging on is the one NoChanges(...) already uses — a status plus hints naming what was examined — rather than a bare "unavailable", which would replace one ambiguity with a friendlier-sounding one. An agent needs to be able to tell "42 collection cycles ran in this window and none of them saw blocking" from "no collection cycles ran".
Both SKUs, and the shape has to match or the parity gap above just moves.
Related
- Web server page: twelve sub-tabs over the existing renderPanel seam #2477 fixes the browser half at the renderer (
vizLinerenders a descriptor's own sentence at exactly zero rows). - Enumerated collectors that yield zero items log SUCCESS indistinguishable from healthy #1837 established the empty-enumeration vocabulary for collectors — a run that succeeded and found nothing is not a failure, and says so. These reads are the same distinction one layer up.
Activity
Port the collection log to Lite rather than widen the divergence ratchet
"No deadlocks was recorded" is not a sentence
Four more reads that could not say which nothing they had found
"Has it ever captured" and "has it ever run" are different questions
Merge pull request #2500 from erikdarlingdata/feat/2485-empty-vs-unco…
Merge pull request #2502 from erikdarlingdata/feat/2485-trend-parity
Merge pull request #2505 from erikdarlingdata/feat/2485-remaining-reads
erikdarlingdata commentedon Aug 22, 2026
Done — all nine reads, both SKUs, word-for-word identical messages. Landed across #2500, #2502, #2505.
Two corrections to this issue's premise
get_mute_ruleswas not a "bare list" — both SKUs already returnedtotal_count. The real defect is thatenabled_only=truecannot distinguish "no rule was ever written" from "rules exist and have all lapsed".compare_analysiswas not a "bare comparison" — it already carriedfact_countfor both periods. The real defects were that all-zero counters read as "nothing changed" rather than "nothing to compare", and — missed here entirely — that one-sided emptiness lies louder: every fact becomes anew_issueorresolved_issue. That case now carries a caveat.The design call worth recording
For an edge table, the denominator has to be whether we LOOKED, not whether we ever FOUND anything.
Blocking and deadlocks only ever write a row when something went wrong. An existence probe on the data answers "never captured" for a server that has been collected perfectly for months and simply never blocked — so the read tells a healthy server its collection is broken. That is this issue's failure mode inverted, and a false alarm that sends someone to fix what is working.
So those reads count SUCCESS runs in
collection_loginstead, which letsemptysay "N collector runs did execute and saw nothing".I got this wrong in #2497 and shipped it:
get_blocking_statsused an event-existence probe on the reasoning that its verdict is about severity. That does not survive contact with a healthy server, because zero severity is the normal state. Corrected in #2508, along with the note that had defended it.New mechanism
McpMissMessageParityPinTestsasserts each shared sentence appears in both source trees. Every one of these messages exists twice, and nothing previously stopped one copy drifting from the other.Closing.
Serve the raw collection log, which had no endpoint on either surface
An empty blocking trend is not the same nothing as an uncollected one
Three tools told two SKUs' users two different stories
Write the release notes for the three waves that never got one