From fa5e5b61768233c9376f34aa2692f76efa8550c0 Mon Sep 17 00:00:00 2001 From: breken Date: Fri, 9 Oct 2026 10:39:40 -0700 Subject: [PATCH] :bug: Log errors readably in MCP execute_code console output (#11930) * :bug: Log errors readably in MCP execute_code console output The console captured during execute_code rendered every object argument with JSON.stringify. An Error has no enumerable fields, so logging a caught error printed "{}" and the agent lost the message. A circular object or a BigInt made JSON.stringify throw, so a console.log call aborted the executed code with an unrelated serialization error. Format console arguments in a small pure module: errors by their string form (name and message), other objects as indented JSON, falling back to the string form when JSON cannot serialize the value. AI-assisted-by: claude-opus-5.5 Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com> * :recycle: Keep console formatting within ExecuteCodeTaskConsole --------- Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com> Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com> --- .../plugin/src/ExecuteCodeTaskConsole.test.ts | 23 +++++++++++++++++ .../task-handlers/ExecuteCodeTaskHandler.ts | 25 ++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts diff --git a/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts b/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts new file mode 100644 index 0000000000..40295aee4a --- /dev/null +++ b/mcp/packages/plugin/src/ExecuteCodeTaskConsole.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { ExecuteCodeTaskConsole } from "./task-handlers/ExecuteCodeTaskHandler.ts"; + +test("formats plain objects and primitives", () => { + const console = new ExecuteCodeTaskConsole(); + console.log({ a: 1 }, null, "text", 42, undefined); + assert.equal(console.getLog(), '[LOG] {\n "a": 1\n} null text 42 undefined\n'); +}); + +test("keeps the name and message of a logged error", () => { + const console = new ExecuteCodeTaskConsole(); + console.error("failed", new TypeError("shape is undefined")); + assert.equal(console.getLog(), "[ERROR] failed TypeError: shape is undefined\n"); +}); + +test("does not throw for values JSON cannot serialize", () => { + const console = new ExecuteCodeTaskConsole(); + const circular: { self?: unknown } = {}; + circular.self = circular; + console.log(circular, { big: 1n }); + assert.equal(console.getLog(), "[LOG] [object Object] [object Object]\n"); +}); diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index 47004e5cfe..b2fff14f05 100644 --- a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -8,7 +8,7 @@ import { PenpotUtils } from "../PenpotUtils.ts"; * Provides the same interface as the native console object but appends * all output to an internal log string that can be retrieved. */ -class ExecuteCodeTaskConsole { +export class ExecuteCodeTaskConsole { /** * Accumulated log output from all console method calls. */ @@ -30,15 +30,32 @@ class ExecuteCodeTaskConsole { return this.logOutput; } + /** + * Formats one argument of a captured console call. + * Errors have no enumerable fields, and values such as circular objects + * cannot be serialized to JSON, so fall back to their string form. + */ + private formatLogArgument(arg: unknown): string { + if (arg instanceof Error) { + return String(arg); + } + if (typeof arg === "object") { + try { + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } + return String(arg); + } + /** * Appends a formatted message to the log output. * @param level - Log level prefix (e.g., "LOG", "WARN", "ERROR") * @param args - Arguments to log, will be stringified and joined */ private appendToLog(level: string, ...args: any[]): void { - const message = args - .map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg))) - .join(" "); + const message = args.map((arg) => this.formatLogArgument(arg)).join(" "); this.logOutput += `[${level}] ${message}\n`; }