Skip to content

Commit 5b23fd0

Browse files
Copilotpelikhan
andauthored
Fix MCP parameter rendering for arrays and objects in Claude logs (#13969)
* Initial plan * Fix JavaScript rendering of Claude logs - handle array/object parameters Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * Add test for ToolsSearch array rendering fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent ab9e530 commit 5b23fd0

3 files changed

Lines changed: 177 additions & 1 deletion

File tree

actions/setup/js/log_parser_shared.cjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,30 @@ function formatMcpParameters(input) {
485485
const paramStrs = [];
486486
for (const key of keys.slice(0, 4)) {
487487
// Show up to 4 parameters
488-
const value = String(input[key] || "");
488+
const rawValue = input[key];
489+
let value;
490+
491+
if (Array.isArray(rawValue)) {
492+
// Format arrays as [item1, item2, ...]
493+
if (rawValue.length === 0) {
494+
value = "[]";
495+
} else if (rawValue.length <= 3) {
496+
// Show all items for small arrays
497+
const items = rawValue.map(item => (typeof item === "object" && item !== null ? JSON.stringify(item) : String(item)));
498+
value = `[${items.join(", ")}]`;
499+
} else {
500+
// Show first 2 items and count for larger arrays
501+
const items = rawValue.slice(0, 2).map(item => (typeof item === "object" && item !== null ? JSON.stringify(item) : String(item)));
502+
value = `[${items.join(", ")}, ...${rawValue.length - 2} more]`;
503+
}
504+
} else if (typeof rawValue === "object" && rawValue !== null) {
505+
// Format objects as JSON
506+
value = JSON.stringify(rawValue);
507+
} else {
508+
// Primitive values (string, number, boolean, null, undefined)
509+
value = String(rawValue || "");
510+
}
511+
489512
paramStrs.push(`${key}: ${truncateString(value, 40)}`);
490513
}
491514

actions/setup/js/log_parser_shared.test.cjs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,113 @@ describe("log_parser_shared.cjs", () => {
539539

540540
expect(result).toBe("");
541541
});
542+
543+
it("should format array values correctly", async () => {
544+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
545+
546+
const input = {
547+
items: ["item1", "item2", "item3"],
548+
};
549+
550+
const result = formatMcpParameters(input);
551+
552+
expect(result).toContain("items: [item1, item2, item3]");
553+
expect(result).not.toContain("[object Object]");
554+
});
555+
556+
it("should format small arrays (3 or fewer items)", async () => {
557+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
558+
559+
const input = {
560+
tags: ["tag1", "tag2"],
561+
};
562+
563+
const result = formatMcpParameters(input);
564+
565+
expect(result).toBe("tags: [tag1, tag2]");
566+
});
567+
568+
it("should format large arrays with ellipsis", async () => {
569+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
570+
571+
const input = {
572+
items: ["item1", "item2", "item3", "item4", "item5"],
573+
};
574+
575+
const result = formatMcpParameters(input);
576+
577+
expect(result).toContain("items: [item1, item2, ...3 more]");
578+
});
579+
580+
it("should format empty arrays", async () => {
581+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
582+
583+
const input = {
584+
emptyList: [],
585+
};
586+
587+
const result = formatMcpParameters(input);
588+
589+
expect(result).toBe("emptyList: []");
590+
});
591+
592+
it("should format object values as JSON", async () => {
593+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
594+
595+
const input = {
596+
config: { enabled: true, timeout: 30 },
597+
};
598+
599+
const result = formatMcpParameters(input);
600+
601+
expect(result).toContain('config: {"enabled":true,"timeout":30}');
602+
expect(result).not.toContain("[object Object]");
603+
});
604+
605+
it("should format arrays containing objects", async () => {
606+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
607+
608+
const input = {
609+
tools: [{ name: "tool1" }, { name: "tool2" }],
610+
};
611+
612+
const result = formatMcpParameters(input);
613+
614+
expect(result).toContain('tools: [{"name":"tool1"}, {"name":"tool2"}]');
615+
expect(result).not.toContain("[object Object]");
616+
});
617+
618+
it("should handle mixed parameter types", async () => {
619+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
620+
621+
const input = {
622+
query: "search term",
623+
filters: ["filter1", "filter2"],
624+
options: { caseSensitive: false },
625+
limit: 10,
626+
};
627+
628+
const result = formatMcpParameters(input);
629+
630+
expect(result).toContain("query: search term");
631+
expect(result).toContain("filters: [filter1, filter2]");
632+
expect(result).toContain('options: {"caseSensitive":false}');
633+
expect(result).toContain("limit: 10");
634+
});
635+
636+
it("should truncate long array representations", async () => {
637+
const { formatMcpParameters } = await import("./log_parser_shared.cjs");
638+
639+
const input = {
640+
longArray: ["a".repeat(50), "b".repeat(50)],
641+
};
642+
643+
const result = formatMcpParameters(input);
644+
645+
// Should be truncated to 40 chars
646+
expect(result.length).toBeLessThan(60);
647+
expect(result).toContain("...");
648+
});
542649
});
543650

544651
describe("formatInitializationSummary", () => {

actions/setup/js/parse_claude_log.test.cjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,5 +565,51 @@ describe("parse_claude_log.cjs", () => {
565565
const toolsSection = result.markdown.split("## 🤖 Reasoning")[0];
566566
expect(toolsSection).not.toMatch(/and \d+ more/);
567567
});
568+
569+
it("should handle ToolsSearch with array of objects (issue fix)", () => {
570+
const logWithToolsSearch = JSON.stringify([
571+
{
572+
type: "system",
573+
subtype: "init",
574+
session_id: "test-tools-search",
575+
tools: ["Bash", "mcp__serena__ToolsSearch"],
576+
model: "claude-sonnet-4-20250514",
577+
},
578+
{
579+
type: "assistant",
580+
message: {
581+
content: [
582+
{
583+
type: "tool_use",
584+
id: "tool_1",
585+
name: "mcp__serena__ToolsSearch",
586+
input: {
587+
query: "search term",
588+
tools: [
589+
{ name: "tool1", type: "function" },
590+
{ name: "tool2", type: "class" },
591+
{ name: "tool3", type: "function" },
592+
{ name: "tool4", type: "function" },
593+
{ name: "tool5", type: "class" },
594+
],
595+
},
596+
},
597+
],
598+
},
599+
},
600+
{ type: "result", total_cost_usd: 0.01, usage: { input_tokens: 100, output_tokens: 50 }, num_turns: 1 },
601+
]);
602+
const result = parseClaudeLog(logWithToolsSearch);
603+
604+
// Should NOT contain [object Object] - the bug we're fixing
605+
expect(result.markdown).not.toContain("[object Object]");
606+
607+
// Should contain properly formatted JSON array representation
608+
expect(result.markdown).toContain("serena::ToolsSearch");
609+
expect(result.markdown).toMatch(/tools:.*\[.*"name":"tool1"/);
610+
611+
// Should properly render the tool in initialization section
612+
expect(result.markdown).toContain("serena::ToolsSearch");
613+
});
568614
});
569615
});

0 commit comments

Comments
 (0)