Skip to content
Merged
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
95 changes: 25 additions & 70 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {createStackTraceForConsoleMessage} from './DevtoolsUtils.js';
import type {ConsoleMessageData} from './formatters/consoleFormatter.js';
import {
formatConsoleEventShort,
formatConsoleEventVerbose,
} from './formatters/consoleFormatter.js';
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
import {IssueFormatter} from './formatters/IssueFormatter.js';
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
Expand Down Expand Up @@ -234,7 +229,7 @@ export class McpResponse implements Response {
detailedNetworkRequest = formatter;
}

let consoleData: ConsoleMessageData | IssueFormatter | undefined;
let consoleData: ConsoleFormatter | IssueFormatter | undefined;

if (this.#attachedConsoleMessageId) {
const message = context.getConsoleMessageById(
Expand All @@ -244,26 +239,11 @@ export class McpResponse implements Response {
if ('args' in message) {
const consoleMessage = message as ConsoleMessage;
const devTools = context.getDevToolsUniverse();
const stackTrace = devTools
? await createStackTraceForConsoleMessage(devTools, consoleMessage)
: undefined;

consoleData = {
consoleMessageStableId,
type: consoleMessage.type(),
message: consoleMessage.text(),
args: await Promise.all(
consoleMessage.args().map(async arg => {
const stringArg = await arg.jsonValue().catch(() => {
// Ignore errors.
});
return typeof stringArg === 'object'
? JSON.stringify(stringArg)
: String(stringArg);
}),
),
stackTrace,
};
consoleData = await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
fetchDetailedData: true,
devTools: devTools ?? undefined,
});
} else if (message instanceof DevTools.AggregatedIssue) {
const formatter = new IssueFormatter(message, {
id: consoleMessageStableId,
Expand All @@ -277,16 +257,13 @@ export class McpResponse implements Response {
}
consoleData = formatter;
} else {
consoleData = {
consoleMessageStableId,
type: 'error',
message: (message as Error).message,
args: [],
};
consoleData = await ConsoleFormatter.from(message as Error, {
id: consoleMessageStableId,
});
}
}

let consoleListData: Array<ConsoleMessageData | IssueFormatter> | undefined;
let consoleListData: Array<ConsoleFormatter | IssueFormatter> | undefined;
if (this.#consoleDataOptions?.include) {
let messages = context.getConsoleData(
this.#consoleDataOptions.includePreservedMessages,
Expand All @@ -308,36 +285,17 @@ export class McpResponse implements Response {
consoleListData = (
await Promise.all(
messages.map(
async (
item,
): Promise<ConsoleMessageData | IssueFormatter | null> => {
async (item): Promise<ConsoleFormatter | IssueFormatter | null> => {
const consoleMessageStableId =
context.getConsoleMessageStableId(item);
if ('args' in item) {
const consoleMessage = item as ConsoleMessage;
const devTools = context.getDevToolsUniverse();
const stackTrace = devTools
? await createStackTraceForConsoleMessage(
devTools,
consoleMessage,
)
: undefined;
return {
consoleMessageStableId,
type: consoleMessage.type(),
message: consoleMessage.text(),
args: await Promise.all(
consoleMessage.args().map(async arg => {
const stringArg = await arg.jsonValue().catch(() => {
// Ignore errors.
});
return typeof stringArg === 'object'
? JSON.stringify(stringArg)
: String(stringArg);
}),
),
stackTrace,
};
return await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
fetchDetailedData: true,
devTools: devTools ?? undefined,
});
}
if (item instanceof DevTools.AggregatedIssue) {
const formatter = new IssueFormatter(item, {
Expand All @@ -348,12 +306,9 @@ export class McpResponse implements Response {
}
return formatter;
}
return {
consoleMessageStableId,
type: 'error',
message: (item as Error).message,
args: [],
};
return await ConsoleFormatter.from(item as Error, {
id: consoleMessageStableId,
});
},
),
)
Expand Down Expand Up @@ -411,8 +366,8 @@ export class McpResponse implements Response {
toolName: string,
context: McpContext,
data: {
consoleData: ConsoleMessageData | IssueFormatter | undefined;
consoleListData: Array<ConsoleMessageData | IssueFormatter> | undefined;
consoleData: ConsoleFormatter | IssueFormatter | undefined;
consoleListData: Array<ConsoleFormatter | IssueFormatter> | undefined;
snapshot: SnapshotFormatter | string | undefined;
detailedNetworkRequest?: NetworkFormatter;
networkRequests?: NetworkFormatter[];
Expand Down Expand Up @@ -551,7 +506,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
if (message instanceof IssueFormatter) {
return message.toString();
}
return formatConsoleEventShort(message);
return message.toString();
}),
);
} else {
Expand Down Expand Up @@ -604,7 +559,7 @@ Call ${handleDialog.name} to handle it before continuing.`);

#formatConsoleData(
context: McpContext,
data: ConsoleMessageData | IssueFormatter | undefined,
data: ConsoleFormatter | IssueFormatter | undefined,
): string[] {
const response: string[] = [];
if (!data) {
Expand All @@ -614,7 +569,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
if (data instanceof IssueFormatter) {
response.push(data.toStringDetailed());
} else {
response.push(formatConsoleEventVerbose(data, context));
response.push(data.toStringDetailed());
}
return response;
}
Expand Down
174 changes: 174 additions & 0 deletions src/formatters/ConsoleFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
createStackTraceForConsoleMessage,
type TargetUniverse,
} from '../DevtoolsUtils.js';
import type * as DevTools from '../third_party/index.js';
import type {ConsoleMessage} from '../third_party/index.js';

export interface ConsoleFormatterOptions {
fetchDetailedData?: boolean;
id?: number;
devTools?: TargetUniverse;
resolvedStackTraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
}

export class ConsoleFormatter {
#msg: ConsoleMessage | Error;
#resolvedArgs: unknown[] = [];
#resolvedStackTrace?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
#id?: number;

private constructor(
msg: ConsoleMessage | Error,
options?: ConsoleFormatterOptions,
) {
this.#msg = msg;
this.#id = options?.id;
this.#resolvedStackTrace = options?.resolvedStackTraceForTesting;
}

static async from(
msg: ConsoleMessage | Error,
options?: ConsoleFormatterOptions,
): Promise<ConsoleFormatter> {
const formatter = new ConsoleFormatter(msg, options);
if (options?.fetchDetailedData) {
await formatter.#loadDetailedData(options?.devTools);
}
return formatter;
}

async #loadDetailedData(devTools?: TargetUniverse): Promise<void> {
if (this.#msg instanceof Error) {
return;
}

this.#resolvedArgs = await Promise.all(
this.#msg.args().map(arg => arg.jsonValue()),
);

if (devTools) {
this.#resolvedStackTrace = await createStackTraceForConsoleMessage(
devTools,
this.#msg,
);
}
}

// The short format for a console message.
toString(): string {
const type = this.#getType();
const text = this.#getText();
const argsCount =
this.#msg instanceof Error
? 0
: this.#resolvedArgs.length || this.#msg.args().length;
const idPart = this.#id !== undefined ? `msgid=${this.#id} ` : '';
return `${idPart}[${type}] ${text} (${argsCount} args)`;
}

// The verbose format for a console message, including all details.
toStringDetailed(): string {
const result = [
this.#id !== undefined ? `ID: ${this.#id}` : '',
`Message: ${this.#getType()}> ${this.#getText()}`,
this.#formatArgs(),
this.#formatStackTrace(this.#resolvedStackTrace),
].filter(line => !!line);
return result.join('\n');
}

#getType(): string {
if (this.#msg instanceof Error) {
return 'error';
}
return this.#msg.type();
}

#getText(): string {
if (this.#msg instanceof Error) {
return this.#msg.message;
}
return this.#msg.text();
}

#getArgs(): unknown[] {
if (this.#msg instanceof Error) {
return [];
}
if (this.#resolvedArgs.length > 0) {
const args = [...this.#resolvedArgs];
// If there is no text, the first argument serves as text (see formatMessage).
if (!this.#msg.text()) {
args.shift();
}
return args;
}
return [];
}

#formatArg(arg: unknown) {
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
}

#formatArgs(): string {
const args = this.#getArgs();

if (!args.length) {
return '';
}

const result = ['### Arguments'];

for (const [key, arg] of args.entries()) {
result.push(`Arg #${key}: ${this.#formatArg(arg)}`);
}

return result.join('\n');
}

#formatStackTrace(
stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
): string {
if (!stackTrace) {
return '';
}

return [
'### Stack trace',
this.#formatFragment(stackTrace.syncFragment),
...stackTrace.asyncFragments.map(this.#formatAsyncFragment.bind(this)),
].join('\n');
}

#formatFragment(
fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment,
): string {
return fragment.frames.map(this.#formatFrame.bind(this)).join('\n');
}

#formatAsyncFragment(
fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment,
): string {
const separatorLineLength = 40;
const prefix = `--- ${fragment.description || 'async'} `;
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
return separator + '\n' + this.#formatFragment(fragment);
}

#formatFrame(frame: DevTools.DevTools.StackTrace.StackTrace.Frame): string {
let result = `at ${frame.name ?? '<anonymous>'}`;
if (frame.uiSourceCode) {
result += ` (${frame.uiSourceCode.displayName()}:${frame.line}:${frame.column})`;
} else if (frame.url) {
result += ` (${frame.url}:${frame.line}:${frame.column})`;
}
return result;
}
}
Loading