“No failed payments exist” is a much stronger claim than “this query returned zero rows.”
The result may be empty because:
- tenant or environment scope was wrong
- a partition has not arrived
- one source timed out
- a join removed unmatched records
- authorization hid the matching rows
- pagination or a timeout ended the search
So an MCP database result should distinguish:
- none found in the complete authorized population
- no visible matches in the caller's scope
- source unavailable
- incomplete search
- not answerable
- unknown
The final prose must preserve that state. “No visible matches” must never become “none exist.”
A negative-answer receipt should carry normalized scope, effective identity and policy, expected and observed sources, watermarks, counts before and after material joins, NULL/unmatched counts, pagination and truncation state, metric version, and trace ID.
Test the empty path deliberately: delayed sources, known hidden rows, unmatched foreign keys, timezone boundaries, stale definitions, and forced timeouts.
An empty result is data. An absence claim is a conclusion that needs evidence.
Full guide: Prove negative database answers before saying none exist
Top comments (1)
Semantic retrieval adds a seventh cause to that list and it's the one that fooled me longest. A vector search can come back empty because the match scored below a threshold, which means the answer was sitting right there and the ranker declined to show it. SQL fails the same way twice. Embedding recall doesn't. It never tells me it declined.
I run a memory server, so this is my entire failure surface. Two things I added after getting burned.
Every recall result I return carries the true total, not the returned count. "Here are 5" and "here are 5 of a real 89" are different answers, and only one of them is safe to summarize from. My enumerations come back with an explicit truncated flag for the same reason. A truncated list read as a complete one is your "none exist" in another costume.
I also stopped letting an empty semantic search stand as evidence. My rule now is that a literal exact-match pass has to run before anything may report nothing found. No LLM in that path. Plain substring, real total. It catches the case where the words are sitting in a stored record and the embedding simply didn't rank it. That happened often enough that I made it a rule instead of a habit.
The receipt is the part I'd take from you. I have most of those fields already. They're scattered across my responses. I've never shipped them as one object.