From d470e5be804151cf4e83efbb1071f17d022c86a0 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 6 May 2026 21:43:03 +0800 Subject: [PATCH 1/2] 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) --- src/mcp.zig | 2 +- src/tests.zig | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/mcp.zig b/src/mcp.zig index 94a4168..0eda574 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -494,7 +494,7 @@ pub const Tool = enum { codedb_ls, }; -const tools_list = +pub const tools_list = \\{"tools":[ \\{"name":"codedb_tree","description":"Whole-repo file tree with per-file language, line counts, and symbol counts. Use to orient in an unfamiliar project.","inputSchema":{"type":"object","properties":{"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":[]}}, \\{"name":"codedb_outline","description":"Symbol outline of one file: functions, structs, enums, imports, consts with line numbers. 4-15x smaller than reading the raw file. Run before codedb_read to find the lines you actually need.","inputSchema":{"type":"object","properties":{"path":{"type":"string","description":"File path relative to project root"},"compact":{"type":"boolean","description":"Condensed format without detail comments (default: false)"},"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":["path"]}}, diff --git a/src/tests.zig b/src/tests.zig index 8cf63d1..2f141e5 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -10452,6 +10452,43 @@ test "issue-424-A: bundle envelope errors carry the 'error:' prefix consistently try testing.expect(std.mem.indexOf(u8, out2.items, "error: missing 'tool'") != null); } +test "issue-434: codedb_bundle ops items schema requires arguments field" { + // The codedb_bundle inputSchema in tools_list advertises ops items as + // {required: ["tool"]} with arguments as a bare {type: "object"} that + // permits {}. Function-calling LLMs read the schema as authoritative and + // emit the minimum-valid payload — {tool: "...", arguments: {}} — which + // misroutes through the inline-args fallback and surfaces as + // "received keys: [tool, arguments]" from each sub-tool. Stage 1 fix: + // add "arguments" to the items.required array so models are forced to + // populate it. (Stage 2 — discriminated oneOf over tool — is a follow-up.) + const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, mcp_mod.tools_list, .{}); + defer parsed.deinit(); + + const tools = parsed.value.object.get("tools").?.array; + var bundle_schema: ?std.json.Value = null; + for (tools.items) |t| { + const name = t.object.get("name").?.string; + if (std.mem.eql(u8, name, "codedb_bundle")) { + bundle_schema = t.object.get("inputSchema").?; + break; + } + } + try testing.expect(bundle_schema != null); + + const ops = bundle_schema.?.object.get("properties").?.object.get("ops").?; + const items = ops.object.get("items").?; + const required = items.object.get("required").?.array; + + var has_tool = false; + var has_arguments = false; + for (required.items) |r| { + if (std.mem.eql(u8, r.string, "tool")) has_tool = true; + if (std.mem.eql(u8, r.string, "arguments")) has_arguments = true; + } + try testing.expect(has_tool); + try testing.expect(has_arguments); +} + test "issue-425: codedb_callers excludes substring matches in unrelated identifiers" { // handleCallers (mcp.zig:1339) currently calls searchContentWithScope(name) // which is a *substring* full-text search. The only de-dup it performs is From 7fb1e87a2a5850abe34bbdb49a57aae4154cacdd Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 6 May 2026 21:56:54 +0800 Subject: [PATCH 2/2] fix(mcp): require arguments in codedb_bundle ops items schema (#434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/mcp.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp.zig b/src/mcp.zig index 0eda574..156292b 100644 --- a/src/mcp.zig +++ b/src/mcp.zig @@ -509,7 +509,7 @@ pub const tools_list = \\{"name":"codedb_changes","description":"Files changed since a given sequence number. Pair with codedb_status to poll for updates.","inputSchema":{"type":"object","properties":{"since":{"type":"integer","description":"Sequence number to get changes since (default: 0)"}},"required":[]}}, \\{"name":"codedb_status","description":"Current indexed-file count, sequence number, and scan phase.","inputSchema":{"type":"object","properties":{"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":[]}}, \\{"name":"codedb_snapshot","description":"Pre-rendered JSON snapshot of the entire index — tree, outlines, symbols, deps. For caching or shipping to edge workers.","inputSchema":{"type":"object","properties":{"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":[]}}, - \\{"name":"codedb_bundle","description":"Run up to 20 codedb_* calls in one round-trip. Each op is either MCP-style {\"tool\":\"codedb_search\",\"arguments\":{\"query\":\"Agent\"}} or inline {\"tool\":\"codedb_search\",\"query\":\"Agent\"} — both are accepted. Example: {\"ops\":[{\"tool\":\"codedb_search\",\"arguments\":{\"query\":\"Agent\"}},{\"tool\":\"codedb_outline\",\"arguments\":{\"path\":\"src/main.zig\"}}]}. Best for parallel outline/symbol/search; avoid bundling large codedb_read calls — responses are not size-capped. If a sub-op reports `received keys: []`, the wrapper field is misnamed: use `arguments` (MCP spec), not `args`.","inputSchema":{"type":"object","properties":{"ops":{"type":"array","description":"Sub-tool calls to dispatch (max 20). Each item must have `tool`; pass per-op args either nested under `arguments` (MCP shape) or inline alongside `tool`.","items":{"type":"object","properties":{"tool":{"type":"string","description":"codedb_* tool name to invoke (e.g. codedb_outline, codedb_symbol, codedb_search, codedb_word, codedb_callers, codedb_read, codedb_deps, codedb_tree, codedb_hot, codedb_status, codedb_changes). Required."},"arguments":{"type":"object","description":"Per-call args matching that tool's inputSchema. The field MUST be named `arguments` (MCP `tools/call` convention) — `args` is silently ignored. May be omitted if you supply args inline at the op level instead."}},"required":["tool"]}},"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":["ops"]}}, + \\{"name":"codedb_bundle","description":"Run up to 20 codedb_* calls in one round-trip. Each op is either MCP-style {\"tool\":\"codedb_search\",\"arguments\":{\"query\":\"Agent\"}} or inline {\"tool\":\"codedb_search\",\"query\":\"Agent\"} — both are accepted. Example: {\"ops\":[{\"tool\":\"codedb_search\",\"arguments\":{\"query\":\"Agent\"}},{\"tool\":\"codedb_outline\",\"arguments\":{\"path\":\"src/main.zig\"}}]}. Best for parallel outline/symbol/search; avoid bundling large codedb_read calls — responses are not size-capped. If a sub-op reports `received keys: []`, the wrapper field is misnamed: use `arguments` (MCP spec), not `args`.","inputSchema":{"type":"object","properties":{"ops":{"type":"array","description":"Sub-tool calls to dispatch (max 20). Each item must have `tool` AND `arguments` (pass `{}` if the sub-tool takes none). Inline args alongside `tool` are still accepted as a fallback.","items":{"type":"object","properties":{"tool":{"type":"string","description":"codedb_* tool name to invoke (e.g. codedb_outline, codedb_symbol, codedb_search, codedb_word, codedb_callers, codedb_read, codedb_deps, codedb_tree, codedb_hot, codedb_status, codedb_changes). Required."},"arguments":{"type":"object","description":"Per-call args matching that tool's inputSchema. Field MUST be named `arguments` (MCP `tools/call` convention) — `args` is silently ignored. Pass `{}` only if the sub-tool takes no arguments. Required."}},"required":["tool","arguments"]}},"project":{"type":"string","description":"Optional absolute path to a different project (must have codedb.snapshot)"}},"required":["ops"]}}, \\{"name":"codedb_remote","description":"Query indexed public repos via api.wiki.codes. Pass action= one of: tree, outline, search, read, symbol, deps, score, cves, commits, branches, dep-history, policy, actions. Use action=actions first if unsure what a repo supports.","inputSchema":{"type":"object","properties":{"repo":{"type":"string","description":"GitHub repo in owner/repo format (e.g. vercel/next.js) or a raw wiki slug such as chromium."},"action":{"type":"string","enum":["tree","outline","search","read","actions","symbol","policy","deps","score","cves","commits","branches","dep-history"],"description":"What to query from api.wiki.codes: actions, tree, search, outline, read, symbol, policy, deps, score, cves, commits, branches, dep-history."},"query":{"type":"string","description":"Action-specific argument. search: text query. symbol: identifier name. outline: file path."},"path":{"type":"string","description":"For action=read: the file path to fetch."},"lines":{"type":"string","description":"For action=read: line range like '10-60' (1-indexed, inclusive). Omit for full file."},"limit":{"type":"integer","description":"For search/tree/deps/commits/branches/dep-history: cap the number of items returned (server may enforce its own ceiling)."},"offset":{"type":"integer","description":"For tree/deps/commits/branches/dep-history: skip the first N items (pagination)."},"prefix":{"type":"string","description":"For tree: only return paths starting with this prefix (e.g. 'src/')."},"expand":{"type":"boolean","description":"For tree: when true, return the full file list. When false returns a compact directory summary when supported."},"since":{"type":"string","description":"For commits/dep-history: ISO timestamp or commit SHA to start from."},"scope":{"type":"string","enum":["runtime","all"],"description":"For score/cves only. Defaults to runtime; use all to include dev/tooling dependencies."},"backend":{"type":"string","enum":["wiki"],"description":"Deprecated compatibility field. Only 'wiki' is accepted; requests always use api.wiki.codes."}},"required":["repo","action"]}}, \\{"name":"codedb_projects","description":"List every locally indexed project on this machine: path, data-dir hash, snapshot presence.","inputSchema":{"type":"object","properties":{},"required":[]}}, \\{"name":"codedb_index","description":"Index a local FOLDER (not a file). Builds outlines, trigrams, word index, and writes codedb.snapshot. After indexing, query it via the project= param on any other tool.","inputSchema":{"type":"object","properties":{"path":{"type":"string","description":"Absolute path to the FOLDER (not a file) to index, e.g. /Users/you/myproject"}},"required":["path"]}},