Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/mcp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,11 @@ fn handleBundle(
}
const op_obj = &op.object;
const tool_name = getStr(op_obj, "tool") orelse {
w.print("--- [{d}] error ---\nmissing 'tool' field\n", .{i}) catch {};
if (op_obj.get("tool")) |_| {
w.print("--- [{d}] error ---\nerror: 'tool' must be a string\n", .{i}) catch {};
} else {
w.print("--- [{d}] error ---\nmissing 'tool' field\n", .{i}) catch {};
}
fail_count += 1;
continue;
};
Expand Down Expand Up @@ -1862,6 +1866,14 @@ fn handleBundle(
if (out.items.len + sub_out.items.len > 200 * 1024) {
w.print("--- [{d}] {s} ---\nTRUNCATED: adding this result would exceed 200KB. Use codedb_outline + targeted reads instead of full file reads.\n", .{ i, tool_name }) catch {};
fail_count += 1;
// Issue #413: surface a per-index marker for every op the bundle
// dropped after truncation, so callers can correlate by index
// instead of silently losing ops > i.
var dropped_idx: usize = i + 1;
while (dropped_idx < ops.len) : (dropped_idx += 1) {
w.print("--- [{d}] dropped ---\nOPS_DROPPED: response cap reached; this op was not executed.\n", .{dropped_idx}) catch {};
fail_count += 1;
}
break;
}

Expand Down
67 changes: 67 additions & 0 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9631,3 +9631,70 @@ test "issue-409: replacing whole file with empty content leaves a stray newline"
try testing.expectEqual(@as(usize, 0), after.len);
try testing.expectEqual(@as(u64, 0), result.new_size);
}

test "issue-412: bundle reports 'missing tool' for tool field of wrong type" {
var explorer = Explorer.init(testing.allocator);
defer explorer.deinit();
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, ".");
defer bench_ctx.deinit();

const bundle_json =
\\{"ops":[{"tool":123,"arguments":{"path":"x.zig"}}]}
;
const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, bundle_json, .{});
defer parsed.deinit();
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);
bench_ctx.runDispatch(io, testing.allocator, .codedb_bundle, &parsed.value.object, &out, &store, &explorer, &agents);

try testing.expect(std.mem.indexOf(u8, out.items, "missing 'tool' field") == null);
}

test "issue-413: bundle truncation drops subsequent ops without telling the caller" {
var explorer = Explorer.init(testing.allocator);
defer explorer.deinit();
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, ".");
defer bench_ctx.deinit();

// Index a single large file (~120KB) so two reads exceed the 200KB
// bundle cap. Bundle truncates and breaks out of the loop after op[1],
// emitting a TRUNCATED note — but op[2] is silently dropped.
var big: std.ArrayList(u8) = .empty;
defer big.deinit(testing.allocator);
while (big.items.len < 120 * 1024) {
try big.appendSlice(testing.allocator, "pub fn placeholder() void { _ = 0; }\n");
}
try explorer.indexFile("big.zig", big.items);
try explorer.indexFile("small.zig", "pub fn small() void {}\n");

// Three reads: first two exceed 200KB → truncate. op[2] is small.zig
// and should still surface — at minimum, the bundle output must
// mention it (e.g. as another truncated entry) so the caller knows
// their request had three ops, not one.
const bundle_json =
\\{"ops":[
\\ {"tool":"codedb_read","arguments":{"path":"big.zig"}},
\\ {"tool":"codedb_read","arguments":{"path":"big.zig"}},
\\ {"tool":"codedb_outline","arguments":{"path":"small.zig"}}
\\]}
;
const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, bundle_json, .{});
defer parsed.deinit();
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);
bench_ctx.runDispatch(io, testing.allocator, .codedb_bundle, &parsed.value.object, &out, &store, &explorer, &agents);

// op[2] (index 2) was sent — caller deserves to see something for it.
// Either its result, or an explicit "[2]" entry noting it was dropped.
try testing.expect(std.mem.indexOf(u8, out.items, "[2]") != null);
}
Loading