Skip to content

Six MCP reads answer 'nothing happened' and 'nothing collected' with identical bytes, and three of them disagree between the SKUs #2485

Closed

Description

@erikdarlingdata

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

Activity

  1. erikdarlingdata commented on Aug 22, 2026

    @erikdarlingdata
    OwnerAuthor

    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_rules was not a "bare list" — both SKUs already returned total_count. The real defect is that enabled_only=true cannot distinguish "no rule was ever written" from "rules exist and have all lapsed".
    • compare_analysis was not a "bare comparison" — it already carried fact_count for 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 a new_issue or resolved_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_log instead, which lets empty say "N collector runs did execute and saw nothing".

    I got this wrong in #2497 and shipped it: get_blocking_stats used 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

    McpMissMessageParityPinTests asserts 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions