Skip to content

Commit 22040ce

Browse files
committed
chore: create a console formatter classs
1 parent 6854d47 commit 22040ce

7 files changed

Lines changed: 411 additions & 393 deletions

src/McpResponse.ts

Lines changed: 25 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {createStackTraceForConsoleMessage} from './DevtoolsUtils.js';
8-
import type {ConsoleMessageData} from './formatters/consoleFormatter.js';
9-
import {
10-
formatConsoleEventShort,
11-
formatConsoleEventVerbose,
12-
} from './formatters/consoleFormatter.js';
7+
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
138
import {IssueFormatter} from './formatters/IssueFormatter.js';
149
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
1510
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
@@ -234,7 +229,7 @@ export class McpResponse implements Response {
234229
detailedNetworkRequest = formatter;
235230
}
236231

237-
let consoleData: ConsoleMessageData | IssueFormatter | undefined;
232+
let consoleData: ConsoleFormatter | IssueFormatter | undefined;
238233

239234
if (this.#attachedConsoleMessageId) {
240235
const message = context.getConsoleMessageById(
@@ -244,26 +239,11 @@ export class McpResponse implements Response {
244239
if ('args' in message) {
245240
const consoleMessage = message as ConsoleMessage;
246241
const devTools = context.getDevToolsUniverse();
247-
const stackTrace = devTools
248-
? await createStackTraceForConsoleMessage(devTools, consoleMessage)
249-
: undefined;
250-
251-
consoleData = {
252-
consoleMessageStableId,
253-
type: consoleMessage.type(),
254-
message: consoleMessage.text(),
255-
args: await Promise.all(
256-
consoleMessage.args().map(async arg => {
257-
const stringArg = await arg.jsonValue().catch(() => {
258-
// Ignore errors.
259-
});
260-
return typeof stringArg === 'object'
261-
? JSON.stringify(stringArg)
262-
: String(stringArg);
263-
}),
264-
),
265-
stackTrace,
266-
};
242+
consoleData = await ConsoleFormatter.from(consoleMessage, {
243+
id: consoleMessageStableId,
244+
fetchDetailedData: true,
245+
devTools: devTools ?? undefined,
246+
});
267247
} else if (message instanceof DevTools.AggregatedIssue) {
268248
const formatter = new IssueFormatter(message, {
269249
id: consoleMessageStableId,
@@ -277,16 +257,13 @@ export class McpResponse implements Response {
277257
}
278258
consoleData = formatter;
279259
} else {
280-
consoleData = {
281-
consoleMessageStableId,
282-
type: 'error',
283-
message: (message as Error).message,
284-
args: [],
285-
};
260+
consoleData = await ConsoleFormatter.from(message as Error, {
261+
id: consoleMessageStableId,
262+
});
286263
}
287264
}
288265

289-
let consoleListData: Array<ConsoleMessageData | IssueFormatter> | undefined;
266+
let consoleListData: Array<ConsoleFormatter | IssueFormatter> | undefined;
290267
if (this.#consoleDataOptions?.include) {
291268
let messages = context.getConsoleData(
292269
this.#consoleDataOptions.includePreservedMessages,
@@ -308,36 +285,17 @@ export class McpResponse implements Response {
308285
consoleListData = (
309286
await Promise.all(
310287
messages.map(
311-
async (
312-
item,
313-
): Promise<ConsoleMessageData | IssueFormatter | null> => {
288+
async (item): Promise<ConsoleFormatter | IssueFormatter | null> => {
314289
const consoleMessageStableId =
315290
context.getConsoleMessageStableId(item);
316291
if ('args' in item) {
317292
const consoleMessage = item as ConsoleMessage;
318293
const devTools = context.getDevToolsUniverse();
319-
const stackTrace = devTools
320-
? await createStackTraceForConsoleMessage(
321-
devTools,
322-
consoleMessage,
323-
)
324-
: undefined;
325-
return {
326-
consoleMessageStableId,
327-
type: consoleMessage.type(),
328-
message: consoleMessage.text(),
329-
args: await Promise.all(
330-
consoleMessage.args().map(async arg => {
331-
const stringArg = await arg.jsonValue().catch(() => {
332-
// Ignore errors.
333-
});
334-
return typeof stringArg === 'object'
335-
? JSON.stringify(stringArg)
336-
: String(stringArg);
337-
}),
338-
),
339-
stackTrace,
340-
};
294+
return await ConsoleFormatter.from(consoleMessage, {
295+
id: consoleMessageStableId,
296+
fetchDetailedData: true,
297+
devTools: devTools ?? undefined,
298+
});
341299
}
342300
if (item instanceof DevTools.AggregatedIssue) {
343301
const formatter = new IssueFormatter(item, {
@@ -348,12 +306,9 @@ export class McpResponse implements Response {
348306
}
349307
return formatter;
350308
}
351-
return {
352-
consoleMessageStableId,
353-
type: 'error',
354-
message: (item as Error).message,
355-
args: [],
356-
};
309+
return await ConsoleFormatter.from(item as Error, {
310+
id: consoleMessageStableId,
311+
});
357312
},
358313
),
359314
)
@@ -411,8 +366,8 @@ export class McpResponse implements Response {
411366
toolName: string,
412367
context: McpContext,
413368
data: {
414-
consoleData: ConsoleMessageData | IssueFormatter | undefined;
415-
consoleListData: Array<ConsoleMessageData | IssueFormatter> | undefined;
369+
consoleData: ConsoleFormatter | IssueFormatter | undefined;
370+
consoleListData: Array<ConsoleFormatter | IssueFormatter> | undefined;
416371
snapshot: SnapshotFormatter | string | undefined;
417372
detailedNetworkRequest?: NetworkFormatter;
418373
networkRequests?: NetworkFormatter[];
@@ -551,7 +506,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
551506
if (message instanceof IssueFormatter) {
552507
return message.toString();
553508
}
554-
return formatConsoleEventShort(message);
509+
return message.toString();
555510
}),
556511
);
557512
} else {
@@ -604,7 +559,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
604559

605560
#formatConsoleData(
606561
context: McpContext,
607-
data: ConsoleMessageData | IssueFormatter | undefined,
562+
data: ConsoleFormatter | IssueFormatter | undefined,
608563
): string[] {
609564
const response: string[] = [];
610565
if (!data) {
@@ -614,7 +569,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
614569
if (data instanceof IssueFormatter) {
615570
response.push(data.toStringDetailed());
616571
} else {
617-
response.push(formatConsoleEventVerbose(data, context));
572+
response.push(data.toStringDetailed());
618573
}
619574
return response;
620575
}

src/formatters/ConsoleFormatter.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
createStackTraceForConsoleMessage,
9+
type TargetUniverse,
10+
} from '../DevtoolsUtils.js';
11+
import type * as DevTools from '../third_party/index.js';
12+
import type {ConsoleMessage} from '../third_party/index.js';
13+
14+
export interface ConsoleFormatterOptions {
15+
fetchDetailedData?: boolean;
16+
id?: number;
17+
devTools?: TargetUniverse;
18+
resolvedStacktraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
19+
}
20+
21+
export class ConsoleFormatter {
22+
#msg: ConsoleMessage | Error;
23+
#resolvedArgs: unknown[] = [];
24+
#resolvedStackTrace?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
25+
#id?: number;
26+
27+
private constructor(
28+
msg: ConsoleMessage | Error,
29+
options?: ConsoleFormatterOptions,
30+
) {
31+
this.#msg = msg;
32+
this.#id = options?.id;
33+
this.#resolvedStackTrace = options?.resolvedStacktraceForTesting;
34+
}
35+
36+
static async from(
37+
msg: ConsoleMessage | Error,
38+
options?: ConsoleFormatterOptions,
39+
): Promise<ConsoleFormatter> {
40+
const formatter = new ConsoleFormatter(msg, options);
41+
if (options?.fetchDetailedData) {
42+
await formatter.#loadDetailedData(options?.devTools);
43+
}
44+
return formatter;
45+
}
46+
47+
async #loadDetailedData(devTools?: TargetUniverse): Promise<void> {
48+
if (this.#msg instanceof Error) {
49+
return;
50+
}
51+
52+
this.#resolvedArgs = await Promise.all(
53+
this.#msg.args().map(arg => arg.jsonValue()),
54+
);
55+
56+
if (devTools) {
57+
this.#resolvedStackTrace = await createStackTraceForConsoleMessage(
58+
devTools,
59+
this.#msg,
60+
);
61+
}
62+
}
63+
64+
// The short format for a console message.
65+
toString(): string {
66+
const type = this.#getType();
67+
const text = this.#getText();
68+
const argsCount =
69+
this.#msg instanceof Error
70+
? 0
71+
: this.#resolvedArgs.length || this.#msg.args().length;
72+
const idPart = this.#id !== undefined ? `msgid=${this.#id} ` : '';
73+
return `${idPart}[${type}] ${text} (${argsCount} args)`;
74+
}
75+
76+
// The verbose format for a console message, including all details.
77+
toStringDetailed(): string {
78+
const result = [
79+
this.#id !== undefined ? `ID: ${this.#id}` : '',
80+
`Message: ${this.#getType()}> ${this.#getText()}`,
81+
this.#formatArgs(),
82+
this.#formatStackTrace(this.#resolvedStackTrace),
83+
].filter(line => !!line);
84+
return result.join('\n');
85+
}
86+
87+
#getType(): string {
88+
if (this.#msg instanceof Error) {
89+
return 'error';
90+
}
91+
return this.#msg.type();
92+
}
93+
94+
#getText(): string {
95+
if (this.#msg instanceof Error) {
96+
return this.#msg.message;
97+
}
98+
return this.#msg.text();
99+
}
100+
101+
#getArgs(): unknown[] {
102+
if (this.#msg instanceof Error) {
103+
return [];
104+
}
105+
if (this.#resolvedArgs.length > 0) {
106+
const args = [...this.#resolvedArgs];
107+
// If there is no text, the first argument serves as text (see formatMessage).
108+
if (!this.#msg.text()) {
109+
args.shift();
110+
}
111+
return args;
112+
}
113+
return [];
114+
}
115+
116+
#formatArg(arg: unknown) {
117+
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
118+
}
119+
120+
#formatArgs(): string {
121+
const args = this.#getArgs();
122+
123+
if (!args.length) {
124+
return '';
125+
}
126+
127+
const result = ['### Arguments'];
128+
129+
for (const [key, arg] of args.entries()) {
130+
result.push(`Arg #${key}: ${this.#formatArg(arg)}`);
131+
}
132+
133+
return result.join('\n');
134+
}
135+
136+
#formatStackTrace(
137+
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
138+
): string {
139+
if (!stackTrace) {
140+
return '';
141+
}
142+
143+
return [
144+
'### Stack trace',
145+
this.#formatFragment(stackTrace.syncFragment),
146+
...stackTrace.asyncFragments.map(this.#formatAsyncFragment.bind(this)),
147+
].join('\n');
148+
}
149+
150+
#formatFragment(
151+
fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment,
152+
): string {
153+
return fragment.frames.map(this.#formatFrame.bind(this)).join('\n');
154+
}
155+
156+
#formatAsyncFragment(
157+
fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment,
158+
): string {
159+
const separatorLineLength = 40;
160+
const prefix = `--- ${fragment.description || 'async'} `;
161+
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
162+
return separator + '\n' + this.#formatFragment(fragment);
163+
}
164+
165+
#formatFrame(frame: DevTools.DevTools.StackTrace.StackTrace.Frame): string {
166+
let result = `at ${frame.name ?? '<anonymous>'}`;
167+
if (frame.uiSourceCode) {
168+
result += ` (${frame.uiSourceCode.displayName()}:${frame.line}:${frame.column})`;
169+
} else if (frame.url) {
170+
result += ` (${frame.url}:${frame.line}:${frame.column})`;
171+
}
172+
return result;
173+
}
174+
}

0 commit comments

Comments
 (0)