Skip to content

fix(mcp): discriminated oneOf on codedb_bundle ops items (#437)#438

Closed
justrach wants to merge 4 commits intomainfrom
fix/issue-437-bundle-oneof
Closed

fix(mcp): discriminated oneOf on codedb_bundle ops items (#437)#438
justrach wants to merge 4 commits intomainfrom
fix/issue-437-bundle-oneof

Conversation

@justrach
Copy link
Copy Markdown
Owner

@justrach justrach commented May 6, 2026

Summary

  • Adds buildAugmentedToolsList to src/mcp.zig: parses tools_list at server startup and mutates the codedb_bundle ops items schema to include a discriminated oneOf array with one branch per dispatchable codedb_* sub-tool.
  • Each branch pins tool to a const (e.g. "codedb_outline") and binds arguments to that sub-tool's actual inputSchema, so once a model picks tool: "codedb_outline" the only matching branch requires arguments.path: string — there is no schema-minimal arguments: {} escape.
  • Falls back to the raw tools_list if augmentation fails (parse error / OOM) so clients always get a valid schema.

Fixes #437. Builds on #435 (Stage 1).

Why Stage 2

Stage 1 made arguments a required field but left it as a bare {type: "object"}. A schema-greedy function-calling model can still satisfy required: ["tool", "arguments"] by emitting {tool: "...", arguments: {}} — the bug morphs from "no arguments key" into "empty arguments value." Stage 2 closes that loophole at the schema level.

What's excluded from the oneOf

  • codedb_bundle (recursive — rejected at mcp.zig:1922)
  • codedb_edit (write op — rejected at mcp.zig:1927)

Both are still present as standalone tools in tools/list; they're just not valid sub-ops of codedb_bundle.

Numbers

  • Augmented schema is ~24KB (19 oneOf branches), up from ~12KB (one bare items schema). Roughly doubled, as expected.
  • One-time cost at server startup: parse + mutate + stringify on the existing in-memory tools_list const.

Caveats

  • oneOf enforcement varies across providers. Anthropic accepts it but isn't strict the way OpenAI structured-output is, and some MCP clients pass schemas straight through to whichever LLM they target. The #424 runtime inline-args fallback at mcp.zig:1948 stays as a backstop for non-conformant clients.

Branch contents

When stacked on top of main, this branch contains Stage 1 (#434/#435) and Stage 2 (#437):

  1. d470e5b — test for mcp: codedb_bundle ops schema permits empty arguments, function-calling LLMs emit {} #434
  2. 7fb1e87 — fix for mcp: codedb_bundle ops schema permits empty arguments, function-calling LLMs emit {} #434 (Stage 1)
  3. 15907ae — test for mcp: codedb_bundle ops items schema needs discriminated oneOf to constrain arguments contents #437
  4. 8c85c24 — fix for mcp: codedb_bundle ops items schema needs discriminated oneOf to constrain arguments contents #437 (Stage 2)

Once PR #435 merges, this PR will rebase to show only commits 3 and 4.

Test plan

  • zig build test — 508/508 pass (was 507/508 with the Stage 2 test failing before the fix).
  • Direct stdio MCP probe against the freshly-installed ~/bin/codedb confirms the served bundle items schema includes oneOf with 19 branches.
  • Behavioral check with a real MCP client / model emitting bundle calls under the new schema.

🤖 Generated with Claude Code

justrach and others added 4 commits May 6, 2026 21:43
The bundle inputSchema advertises ops items with required: ["tool"]
and arguments as a bare {type: "object"}, so function-calling LLMs
emit {tool, arguments: {}} as the minimum-valid payload. This test
asserts "arguments" is in items.required so models are forced to
populate it.

Also exposes tools_list as pub for the test to introspect.

Fails on main; fix follows in next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 1 of the bundle-schema fix. Prior schema had ops items with
required: ["tool"] and arguments as a bare {type: "object"}, so
function-calling LLMs read {} as a valid arguments payload and
emitted {tool, arguments: {}} as the minimum-valid call. The empty
object then triggered the #424 inline-args fallback, which used the
op object itself as the args bag and surfaced as
"received keys: [tool, arguments]" from each sub-tool.

Adding "arguments" to items.required forces the model to populate
it. The runtime inline-args fallback at mcp.zig:1948 stays as a
backstop for non-conformant clients.

Stage 2 (discriminated oneOf over tool, binding arguments to each
sub-tool's inputSchema) is a follow-up — it requires turning the
hand-rolled tools_list literal into a builder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 2 of the bundle-schema fix. Stage 1 (#434) made `arguments`
required at the items level, but the field is still a bare
{type: "object"} so a schema-greedy model can satisfy `required` by
emitting `arguments: {}`. This test asserts the items schema
contains a discriminated `oneOf` with one branch per dispatchable
codedb_* sub-tool, each pinning `tool` to a const and `arguments`
to that sub-tool's actual inputSchema.

Adds a stub `buildAugmentedToolsList` that returns the unaugmented
schema so the test fails at runtime instead of as a compile error.
The real builder lands in the fix commit.

Fails on this branch; fix follows in next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 2 of the bundle-schema fix. Stage 1 (#434) made `arguments`
required at the items level but left it as a bare {type: "object"};
a schema-greedy model could still satisfy the required check by
emitting `arguments: {}`. Stage 2 binds the *contents* of arguments
to each sub-tool's actual inputSchema via a discriminated oneOf on
`tool` (const) → `arguments` (sub-tool inputSchema). Once a model
picks `tool: "codedb_outline"`, the only matching branch requires
arguments.path:string — there is no schema-minimal escape.

`buildAugmentedToolsList` parses the existing tools_list literal
once at server startup, mutates the bundle items to add the oneOf,
and serializes back. No hand-maintained duplication — branches are
generated from the per-sub-tool schemas already advertised. Falls
back to the raw tools_list if augmentation fails (parse error / OOM)
so clients still get a valid schema.

codedb_bundle (recursive) and codedb_edit (write op) are excluded
from the oneOf since handleBundle rejects them at runtime anyway.

Schema payload roughly doubles (~12KB → ~24KB after augmentation,
19 branches across the dispatchable codedb_* tools).

Test: tests.zig now asserts the augmented schema contains oneOf
with branches that pin tool to a const and preserve each sub-tool's
required args (codedb_outline branch must require `path`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justrach added a commit that referenced this pull request May 6, 2026
…434, #436, #437) (#439)

* feat(explore): opt-in rerank-trace logger for offline tuning experiments

Adds a v0 trace logger that appends one JSON line per searchContent
invocation when enabled via .codedbrc (rerank_trace = true). Captures
{ts, query, results:[{path,line,score}]} so we can inspect the data
and decide whether online learning-to-rank from agent traces is worth
building.

Pure observation — does not change ranking behavior. Disabled by
default. Caps query at 256 bytes, results at 50 entries, and rotates
the file by truncate-clobber once it crosses 10 MB. All I/O errors
are swallowed; logging never breaks a search.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(explore): always score in rerankAndFinalize, not just when len > 1

Pre-fix the multi-signal scoring loop only ran when result_list had
more than one item — a micro-optimization that skipped sorting a
single result. With the rerank-trace logger added in 54b6b72, this
made single-result entries log score=0.0, making them indistinguishable
from genuinely zero-confidence matches in offline analysis.

The fix runs scoring unconditionally and keeps the sort guarded
behind len > 1. Cost: a few µs per single-result search — negligible.

Caught by end-to-end binary verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mcp): issue-434 failing test for codedb_bundle ops schema

The bundle inputSchema advertises ops items with required: ["tool"]
and arguments as a bare {type: "object"}, so function-calling LLMs
emit {tool, arguments: {}} as the minimum-valid payload. This test
asserts "arguments" is in items.required so models are forced to
populate it.

Also exposes tools_list as pub for the test to introspect.

Fails on main; fix follows in next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): require arguments in codedb_bundle ops items schema (#434)

Stage 1 of the bundle-schema fix. Prior schema had ops items with
required: ["tool"] and arguments as a bare {type: "object"}, so
function-calling LLMs read {} as a valid arguments payload and
emitted {tool, arguments: {}} as the minimum-valid call. The empty
object then triggered the #424 inline-args fallback, which used the
op object itself as the args bag and surfaced as
"received keys: [tool, arguments]" from each sub-tool.

Adding "arguments" to items.required forces the model to populate
it. The runtime inline-args fallback at mcp.zig:1948 stays as a
backstop for non-conformant clients.

Stage 2 (discriminated oneOf over tool, binding arguments to each
sub-tool's inputSchema) is a follow-up — it requires turning the
hand-rolled tools_list literal into a builder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mcp): issue-437 failing test for bundle items oneOf

Stage 2 of the bundle-schema fix. Stage 1 (#434) made `arguments`
required at the items level, but the field is still a bare
{type: "object"} so a schema-greedy model can satisfy `required` by
emitting `arguments: {}`. This test asserts the items schema
contains a discriminated `oneOf` with one branch per dispatchable
codedb_* sub-tool, each pinning `tool` to a const and `arguments`
to that sub-tool's actual inputSchema.

Adds a stub `buildAugmentedToolsList` that returns the unaugmented
schema so the test fails at runtime instead of as a compile error.
The real builder lands in the fix commit.

Fails on this branch; fix follows in next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): discriminated oneOf on codedb_bundle ops items (#437)

Stage 2 of the bundle-schema fix. Stage 1 (#434) made `arguments`
required at the items level but left it as a bare {type: "object"};
a schema-greedy model could still satisfy the required check by
emitting `arguments: {}`. Stage 2 binds the *contents* of arguments
to each sub-tool's actual inputSchema via a discriminated oneOf on
`tool` (const) → `arguments` (sub-tool inputSchema). Once a model
picks `tool: "codedb_outline"`, the only matching branch requires
arguments.path:string — there is no schema-minimal escape.

`buildAugmentedToolsList` parses the existing tools_list literal
once at server startup, mutates the bundle items to add the oneOf,
and serializes back. No hand-maintained duplication — branches are
generated from the per-sub-tool schemas already advertised. Falls
back to the raw tools_list if augmentation fails (parse error / OOM)
so clients still get a valid schema.

codedb_bundle (recursive) and codedb_edit (write op) are excluded
from the oneOf since handleBundle rejects them at runtime anyway.

Schema payload roughly doubles (~12KB → ~24KB after augmentation,
19 branches across the dispatchable codedb_* tools).

Test: tests.zig now asserts the augmented schema contains oneOf
with branches that pin tool to a const and preserve each sub-tool's
required args (codedb_outline branch must require `path`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release: v0.2.5808 — codedb_bundle schema fix + rerank-trace logger (#434, #436, #437)

Bundles three PRs:
- #435 (Stage 1, #434): require `arguments` on bundle ops items
- #438 (Stage 2, #437): discriminated oneOf per sub-tool
- #436: opt-in rerank-trace logger for offline tuning + score-on-len-1 fix

End-to-end Sonnet 4.6 verifies the schema constraint flows through
to model output: bundle calls now arrive with populated, correctly-
named `arguments` for each sub-op.

513/513 tests pass on the merged branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justrach
Copy link
Copy Markdown
Owner Author

justrach commented May 6, 2026

Superseded by #439 (release v0.2.5808). All commits from this branch landed in 907ac96 on main.

@justrach justrach closed this May 6, 2026
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.

mcp: codedb_bundle ops items schema needs discriminated oneOf to constrain arguments contents

1 participant