Skip to content
Closed
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
6 changes: 4 additions & 2 deletions src/adapters/cursor/protobuf-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,15 @@ function normalizeJsonText(text: string, toolName: string | undefined, state: Cu
* streamed onward), and/or as a structured protobuf map on `toolCallCompleted`. We emit the args
* exactly once, at completion, so they can always be schema-normalized regardless of which form
* arrived. The completed map wins when present (canonical); otherwise the buffered streamed text is
* used. Returns an empty string when there are no args (the bridge serializes that as `{}`).
* used. Preserve malformed buffered text so the downstream client rejects the call instead of
* silently treating it as a no-arg call. Returns an empty string only when there were no args (the
* bridge serializes that as `{}`).
*/
function resolveCompletedArgs(buffered: string, args: McpArgs | undefined, state: CursorProtobufEventState): string {
if (hasMcpArgBytes(args)) return decodeMcpArgsNormalized(args, state);
const name = mcpWireNameFromArgs(args);
if (isCompleteJson(buffered)) return normalizeJsonText(buffered, name, state);
return "";
return buffered;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject whitespace-only buffered arguments

When argsTextDelta contains only whitespace and completion has no argument map, returning the truthy buffer does not fail closed: toolCallArgumentsUsable trims it and accepts it as an empty no-arg call, while closeCurrentToolCall preserves the original whitespace instead of substituting {}. The response is therefore marked completed with arguments: " ", which is not valid JSON and can make the echoed call fail on the next turn. Reject whitespace-only non-empty buffers as malformed rather than forwarding them.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

}

const PATCH_BEGIN = "*** Begin Patch";
Expand Down
24 changes: 24 additions & 0 deletions tests/cursor-protobuf-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,30 @@ describe("Cursor protobuf tool-call events", () => {
expect(delta && delta.type === "tool_call_delta" ? JSON.parse(delta.arguments) : null).toEqual({ path: "a.txt" });
});

test("preserves incomplete streamed args when completion has no argument map", () => {
const state = createCursorProtobufEventState();
const toolCall = mcpToolCall("mcp__fs__read_file", {});

expect(mapCursorProtobufServerMessage(interaction({
case: "partialToolCall",
value: create(PartialToolCallUpdateSchema, {
callId: "call_1",
modelCallId: "model_1",
toolCall,
argsTextDelta: "{\"path\":",
}),
}), state)).toEqual([]);

expect(mapCursorProtobufServerMessage(interaction({
case: "toolCallCompleted",
value: create(ToolCallCompletedUpdateSchema, { callId: "call_1", modelCallId: "model_1", toolCall }),
}), state)).toEqual([
{ type: "tool_call_start", id: "call_1", name: "mcp__fs__read_file" },
{ type: "tool_call_delta", arguments: "{\"path\":" },
{ type: "tool_call_end", id: "call_1" },
]);
});

test("commits an advertised no-arg tool call instead of dropping it", () => {
// A completed client tool call with no args and no streamed text must still reach Codex when the
// tool is advertised (e.g. a no-arg list/status tool). The bridge serializes empty args as "{}".
Expand Down
Loading