Skip to content

mcp: direct tool calls drop inline args when arguments is empty #512

@justrach

Description

@justrach

Problem

Direct MCP tools/call currently only dispatches params.arguments. If a client sends arguments: {} but also puts the real tool fields inline in params, the direct handler dispatches an empty argument object and reports missing 'path' / received keys: [] even though path was present on the request.

This is inconsistent with codedb_bundle, which already accepts the inline fallback shape for sub-operations. The MCP spec and Rust SDK still advertise canonical params.arguments; the bug is that our direct-call fallback and diagnostic path are less tolerant than the bundled path, making client wrapper issues look like dropped fields.

Failing Test

test "issue-512: direct tools call accepts inline args when arguments is empty" {
    var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
    defer explorer.deinit();
    try explorer.indexFile("src/main.zig", "pub fn main() void {}\n");

    var store = Store.init(testing.allocator);
    defer store.deinit();
    var agents = AgentRegistry.init(testing.allocator);
    defer agents.deinit();
    _ = try agents.register("__filesystem__");

    var bench_ctx = mcp_mod.BenchContext.init(testing.allocator, ".", Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
    defer bench_ctx.deinit();
    var telem = telemetry_mod.Telemetry.init(io, ".", testing.allocator, true);
    defer telem.deinit();

    const call_json =
        \\{"params":{"name":"codedb_outline","arguments":{},"path":"src/main.zig"}}
    ;
    const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, call_json, .{});
    defer parsed.deinit();

    const pipe = try cio.makePipe();
    defer _ = std.c.close(pipe[0]);
    defer _ = std.c.close(pipe[1]);

    bench_ctx.runHandleCall(
        io,
        testing.allocator,
        &parsed.value.object,
        .{ .handle = pipe[1] },
        std.json.Value{ .integer = 1 },
        &store,
        &explorer,
        &agents,
        &telem,
    );

    var response_buf: [16 * 1024]u8 = undefined;
    const n = try std.posix.read(pipe[0], &response_buf);
    const response = response_buf[0..n];

    try testing.expect(std.mem.indexOf(u8, response, "src/main.zig") != null);
    try testing.expect(std.mem.indexOf(u8, response, "missing 'path'") == null);
}

Current failure:

zig build test -Dtest-filter=issue-512
error: 'test_mcp.test.issue-512: direct tools call accepts inline args when arguments is empty' failed
src/test_mcp.zig:1295:5: expected response to include src/main.zig

Expected

Direct tools/call should keep canonical MCP behavior (params.arguments) and also recover when arguments is empty and real fields are present inline on params, matching codedb_bundle's fallback behavior.

Fix

Normalize direct call arguments before dispatch:

  • Prefer non-empty params.arguments.
  • If params.arguments is empty or absent, accept inline non-administrative fields from params.
  • Optionally accept params.args as a compatibility alias only when canonical arguments is absent/empty.
  • Keep malformed canonical arguments as a protocol error.
  • Update diagnostics so direct-call wrapper problems do not say "sub-op".

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p2Medium priority

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions