🐛 Log errors readably in MCP execute_code console output (#11930)

* 🐛 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>

* ♻️ 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>
This commit is contained in:
breken 2026-10-09 10:39:40 -07:00 committed by GitHub
parent 6f33d8d9d8
commit fa5e5b6176
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 4 deletions

View File

@ -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");
});

View File

@ -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`;
}