Skip to content

fix(mcp): make every tool reply client-usable — structuredContent, pipelining, config get (#1522) - #1523

Merged
DeusData merged 1 commit into
mainfrom
fix/1522-silent-empty-results
Aug 11, 2026
Merged

fix(mcp): make every tool reply client-usable — structuredContent, pipelining, config get (#1522)#1523
DeusData merged 1 commit into
mainfrom
fix/1522-silent-empty-results

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Fixes #1522 — and a third, adjacent regression the verification sweep surfaced. All three shipped in 0.10.0 and share one failure shape: an empty result with a success status, which an LLM client cannot distinguish from "nothing found".

Bug 1 — the entire tree-format surface rendered as `{}` in MCP clients (default format, all platforms)

#1488 emptied `structuredContent` while every tool still declared a blanket permissive `outputSchema`. Spec-honoring clients (Claude Code among them) read `structuredContent` as the result when a schema is declared — so `search_graph`, `trace_path`, `query_graph`, `get_architecture`, `search_code`, and `detect_changes` all returned visibly-empty `{}` on their default format. CLI looked fine (it prints content text), which is why the reporter's Windows setup was the first to see it.

Corrected contract: no tool declares an `outputSchema` (output is format-parameter-polymorphic — no static schema is truthful); object payloads keep parsed `structuredContent`; errors keep `structuredContent.error`; text payloads carry no key at all — preserving #1375's no-duplication win.

Bug 2 — ≥7 pipelined requests killed the server with zero output

The 8-frame frontend queue failed the whole session on overflow: any 7+ tool calls written in one stdin burst (agents issuing parallel tool calls pipeline exactly like this) died rc=1 with every buffered response lost. A full queue is now backpressure — the stdin reader blocks until the worker drains, bounded by the same stop/fail flags every teardown path already sets. Only a single frame exceeding the entire 12 MiB budget remains a hard failure.

Bug 3 — `config get` printed empty + exit 0 for every unset and unknown key

`list` printed stored-or-DEFAULT, `get` printed stored-or-EMPTY, nothing validated key names. One config-key table now drives help/list/get/set/reset; `get` prints the stored value or the key's real runtime default; unknown keys exit 1 with the known-key list on stderr.

Tests (each RED pre-fix and RED on revert)

  • `test_mcp.c` — no structuredContent key on text results; no outputSchema in tools/list; the table-enumeration guard binds all three envelope branches for every registered tool
  • `test_daemon_frontend.c` — over-capacity contract flipped from "session fails" to "backpressure without loss" (held first request + 32 over-capacity frames all answered, clean close)
  • `test_cli.c` — config command contract incl. unknown-key rejection on get/set/reset
  • `smoke-test.sh` — Phase 3z rewritten (it previously pinned the empty-object behavior as correct on the shipped artifact); new phases: 3z1 default-format client-usability + no-schema assertion, 3z2 24-deep pipelining, 3z3 config contract

End-to-end verification (locally built production binary)

0/15 tools declare a schema; text tools ABSENT / object tools POPULATED / error envelopes intact; pipelined bursts of 7, 24, and 64 all answered with rc=0; config prints real defaults and exits 1 on typos. `hook_augment` (`structuredContent.projects`) and `index_resilience` (`structuredContent.status`) consume object payloads — unaffected.

Ships in v0.10.1 immediately after merge.

@DeusData
DeusData force-pushed the fix/1522-silent-empty-results branch from 47b778f to 2675ba5 Compare August 10, 2026 22:52
…pelining, config get (#1522)

Three regressions shipped in 0.10.0 share one failure shape: an empty result
with a success status, indistinguishable from "nothing found" for the LLM
clients that are cbm's primary consumers.

1) structuredContent {} on the whole tree-format surface (#1522 bug 1).
   #1488 replaced the duplicated payload with an EMPTY structuredContent
   object while every tool still declared a blanket permissive outputSchema.
   Spec-honoring clients (Claude Code among them) treat structuredContent as
   THE result when a schema is declared, so search_graph, trace_path,
   query_graph, get_architecture, search_code, and detect_changes all rendered
   as literally "{}" on their DEFAULT format, on every platform. The corrected
   contract: no tool declares an outputSchema (tool output is
   format-parameter-polymorphic — no static schema is truthful), JSON-object
   payloads keep their parsed structuredContent, errors keep
   structuredContent.error, and text-shaped payloads carry NO structuredContent
   key at all — which also preserves #1375's no-duplication win.

2) Frontend queue overflow killed the session (found by the #1522 sweep).
   Any 7+ requests pipelined in one stdin burst — an agent issuing parallel
   tool calls does exactly this — overflowed the 8-frame frontend queue, which
   failed the whole session: rc=1 with ZERO bytes of output, every buffered
   response lost. A full queue is now backpressure: the stdin reader blocks
   until the worker drains (bounded by the same stop/fail flags every teardown
   path already sets); only a single frame larger than the entire 12 MiB byte
   budget — which could never be admitted — remains a hard failure.

3) config get printed "" with exit 0 for every unset and every unknown key
   (#1522 bug 2). list printed stored-or-DEFAULT while get printed
   stored-or-EMPTY, and no subcommand validated key names, so a typo was
   indistinguishable from a correctly-read setting. One config-key table now
   drives help, list, get, set, and reset: get prints the stored value or the
   key's real default (the same fallback the runtime readers use), and unknown
   keys error with exit 1 on get, set, and reset alike.

Tests — each RED on the pre-fix tree and RED again on revert:
  * test_mcp.c: text results carry no structuredContent key; tools/list
    declares no outputSchema; the tool-table guard now binds all three
    branches (absent / parsed-object / error) for every registered tool.
  * test_daemon_frontend.c: the over-capacity contract flips from
    "session fails" to "backpressure without loss" — the held first request
    plus all 32 over-capacity frames are answered and the run closes cleanly.
  * test_cli.c: the config command contract — defaults, round-trip, reset,
    and unknown-key rejection on all three subcommands.
  * smoke-test.sh: Phase 3z rewritten to the corrected structuredContent
    contract, new Phase 3z1 (default-format replies usable in schema-honoring
    clients, no outputSchema advertised), 3z2 (24 pipelined calls all
    answered), 3z3 (config defaults + unknown-key rejection) — all asserted
    against the SHIPPED artifact, where #1488's smoke phase previously pinned
    the empty-object behavior as correct.

Verified end-to-end on the locally built production binary: all 15 tools
declare no schema; text tools return ABSENT structuredContent, object tools
populated, error envelopes intact; 7/24/64-deep pipelined bursts all answered
with rc=0; config get prints real defaults and exits 1 on unknown keys.
hook_augment (structuredContent.projects) and index_resilience
(structuredContent.status) consume object payloads and are unaffected.

Fixes #1522.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData force-pushed the fix/1522-silent-empty-results branch from 2675ba5 to f59d24f Compare August 10, 2026 23:13
@DeusData
DeusData merged commit 367ba25 into main Aug 11, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.10.0: two silent empty-result paths — tree format returns {} over MCP (default), and config get returns empty for all keys but auto_index

1 participant