Skip to content
Open
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
9 changes: 7 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,14 @@ so returned values have to JSON-serializable.

### `list_console_messages`

**Description:** List all console messages for the currently selected page
**Description:** List console messages for the currently selected page with filtering options

**Parameters:** None
**Parameters:**

- **compact** (boolean) _(optional)_: Use compact format to reduce token usage (default: true)
- **includeTimestamp** (boolean) _(optional)_: Include timestamp information (default: false)
- **level** (enum: "log", "info", "warning", "error", "all") _(optional)_: Filter by log level (default: all)
- **limit** (number) _(optional)_: Maximum number of messages to return (default: 100)

---

Expand Down
60 changes: 58 additions & 2 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export class McpResponse implements Response {
#includeSnapshot = false;
#attachedNetworkRequestUrl?: string;
#includeConsoleData = false;
#consoleOptions?: {
level: string;
limit: number;
compact: boolean;
includeTimestamp: boolean;
};
#textResponseLines: string[] = [];
#formattedConsoleData?: string[];
#images: ImageContentData[] = [];
Expand Down Expand Up @@ -68,8 +74,24 @@ export class McpResponse implements Response {
};
}

setIncludeConsoleData(value: boolean): void {
setIncludeConsoleData(
value: boolean,
options?: {
level?: string;
limit?: number;
compact?: boolean;
includeTimestamp?: boolean;
},
): void {
this.#includeConsoleData = value;
if (value && options) {
this.#consoleOptions = {
level: options.level || 'all',
limit: options.limit || 100,
compact: options.compact !== false,
includeTimestamp: options.includeTimestamp || false,
};
}
}

attachNetworkRequest(url: string): void {
Expand Down Expand Up @@ -129,8 +151,42 @@ export class McpResponse implements Response {
if (this.#includeConsoleData) {
const consoleMessages = context.getConsoleData();
if (consoleMessages) {
let filteredMessages = consoleMessages;

// Filter by level if specified
if (
this.#consoleOptions?.level &&
this.#consoleOptions.level !== 'all'
) {
filteredMessages = consoleMessages.filter(message => {
if ('type' in message) {
return message.type() === this.#consoleOptions!.level;
}
return this.#consoleOptions!.level === 'error';
});
}

// Apply limit
if (this.#consoleOptions?.limit) {
filteredMessages = filteredMessages.slice(
0,
this.#consoleOptions.limit,
);
}

// Remove duplicates
const uniqueMessages = new Map<string, (typeof filteredMessages)[0]>();
for (const message of filteredMessages) {
const key = 'type' in message ? message.text() : message.message;
if (!uniqueMessages.has(key)) {
uniqueMessages.set(key, message);
}
}

formattedConsoleMessages = await Promise.all(
consoleMessages.map(message => formatConsoleEvent(message)),
Array.from(uniqueMessages.values()).map(message =>
formatConsoleEvent(message, this.#consoleOptions),
),
);
this.#formattedConsoleData = formattedConsoleMessages;
}
Expand Down
88 changes: 69 additions & 19 deletions src/formatters/consoleFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,90 @@ const logLevels: Record<string, string> = {

export async function formatConsoleEvent(
event: ConsoleMessage | Error,
options?: {
compact?: boolean;
includeTimestamp?: boolean;
},
): Promise<string> {
// Check if the event object has the .type() method, which is unique to ConsoleMessage
if ('type' in event) {
return await formatConsoleMessage(event);
return await formatConsoleMessage(event, options);
}
return `Error: ${event.message}`;
}

async function formatConsoleMessage(msg: ConsoleMessage): Promise<string> {
async function formatConsoleMessage(
msg: ConsoleMessage,
options?: {
compact?: boolean;
includeTimestamp?: boolean;
},
): Promise<string> {
const logLevel = logLevels[msg.type()];
const args = msg.args();
const compact = options?.compact !== false; // Default to true
const includeTimestamp = options?.includeTimestamp || false;

if (logLevel === 'Error') {
let message = `${logLevel}> `;
let message = compact ? 'Error> ' : `${logLevel}> `;

if (msg.text() === 'JSHandle@error') {
const errorHandle = args[0] as JSHandle<Error>;
message += await errorHandle
.evaluate(error => {
return error.toString();
})
.catch(() => {
return 'Error occured';
return 'Error occurred';
});
void errorHandle.dispose().catch();

const formattedArgs = await formatArgs(args.slice(1));
if (formattedArgs) {
message += ` ${formattedArgs}`;
if (!compact) {
const formattedArgs = await formatArgs(args.slice(1), compact);
if (formattedArgs) {
message += ` ${formattedArgs}`;
}
}
} else {
message += msg.text();
const formattedArgs = await formatArgs(args);
if (formattedArgs) {
message += ` ${formattedArgs}`;
}
for (const frame of msg.stackTrace()) {
message += '\n' + formatStackFrame(frame);
if (!compact) {
const formattedArgs = await formatArgs(args, compact);
if (formattedArgs) {
message += ` ${formattedArgs}`;
}
// Only include stack trace in non-compact mode
for (const frame of msg.stackTrace()) {
message += '\n' + formatStackFrame(frame);
}
}
}
return message;
}

const formattedArgs = await formatArgs(args);
const text = msg.text();

return `${logLevel}> ${formatStackFrame(
msg.location(),
)}: ${text} ${formattedArgs}`.trim();
if (compact) {
// Compact format: just the log level and text, no location/args
return `${logLevel}> ${text}`;
} else {
// Verbose format: include location and formatted args
const formattedArgs = await formatArgs(args, compact);
const locationInfo = includeTimestamp
? formatStackFrame(msg.location())
: '';

return `${logLevel}> ${locationInfo}: ${text} ${formattedArgs}`.trim();
}
}

async function formatArgs(args: readonly JSHandle[]): Promise<string> {
async function formatArgs(
args: readonly JSHandle[],
compact = true,
): Promise<string> {
if (compact && args.length === 0) {
return '';
}

const argValues = await Promise.all(
args.map(arg =>
arg.jsonValue().catch(() => {
Expand All @@ -82,8 +115,25 @@ async function formatArgs(args: readonly JSHandle[]): Promise<string> {

return argValues
.map(value => {
return typeof value === 'object' ? JSON.stringify(value) : String(value);
if (typeof value === 'object') {
if (compact) {
// In compact mode, simplify objects
if (Array.isArray(value)) {
return `[Array(${value.length})]`;
}
if (value === null) {
return 'null';
}
return '[Object]';
} else {
// In verbose mode, stringify with truncation
const json = JSON.stringify(value);
return json.length > 200 ? json.slice(0, 200) + '...' : json;
}
}
return String(value);
})
.filter(Boolean)
.join(' ');
}

Expand Down
10 changes: 9 additions & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ export interface Response {
value: boolean,
options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]},
): void;
setIncludeConsoleData(value: boolean): void;
setIncludeConsoleData(
value: boolean,
options?: {
level?: string;
limit?: number;
compact?: boolean;
includeTimestamp?: boolean;
},
): void;
setIncludeSnapshot(value: boolean): void;
attachImage(value: ImageContentData): void;
attachNetworkRequest(url: string): void;
Expand Down
43 changes: 39 additions & 4 deletions src/tools/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,53 @@
* SPDX-License-Identifier: Apache-2.0
*/

import z from 'zod';

import {ToolCategories} from './categories.js';
import {defineTool} from './ToolDefinition.js';

export const consoleTool = defineTool({
name: 'list_console_messages',
description: 'List all console messages for the currently selected page',
description:
'List console messages for the currently selected page with filtering options',
annotations: {
category: ToolCategories.DEBUGGING,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
response.setIncludeConsoleData(true);
schema: {
level: z
.enum(['log', 'info', 'warning', 'error', 'all'])
.optional()
.describe('Filter by log level (default: all)'),
limit: z
.number()
.min(1)
.max(1000)
.optional()
.describe('Maximum number of messages to return (default: 100)'),
compact: z
.boolean()
.optional()
.describe('Use compact format to reduce token usage (default: true)'),
includeTimestamp: z
.boolean()
.optional()
.describe('Include timestamp information (default: false)'),
},
handler: async (request, response) => {
const params = request.params as {
level?: string;
limit?: number;
compact?: boolean;
includeTimestamp?: boolean;
};

// Always pass options to enable compact mode by default
response.setIncludeConsoleData(true, {
level: params.level || 'all',
limit: params.limit || 100,
compact: params.compact ?? true, // Default to true
includeTimestamp: params.includeTimestamp ?? false,
});
},
});
22 changes: 8 additions & 14 deletions tests/formatters/consoleFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('consoleFormatter', () => {
},
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Log> script.js:10:5: Hello, world!');
assert.equal(result, 'Log> Hello, world!');
});

it('formats a console.log message with arguments', async () => {
Expand All @@ -87,10 +87,7 @@ describe('consoleFormatter', () => {
},
});
const result = await formatConsoleEvent(message);
assert.equal(
result,
'Log> script.js:10:5: Processing file: file.txt {"id":1,"status":"done"}',
);
assert.equal(result, 'Log> Processing file:');
});

it('formats a console.error message', async () => {
Expand All @@ -109,7 +106,7 @@ describe('consoleFormatter', () => {
args: ['details', {code: 500}],
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Error> Something went wrong: details {"code":500}');
assert.equal(result, 'Error> Something went wrong:');
});

it('formats a console.error message with a stack trace', async () => {
Expand All @@ -130,10 +127,7 @@ describe('consoleFormatter', () => {
],
});
const result = await formatConsoleEvent(message);
assert.equal(
result,
'Error> Something went wrong\nscript.js:10:5\nscript2.js:20:10',
);
assert.equal(result, 'Error> Something went wrong');
});

it('formats a console.error message with a JSHandle@error', async () => {
Expand All @@ -157,7 +151,7 @@ describe('consoleFormatter', () => {
},
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Warning> script.js:10:5: This is a warning');
assert.equal(result, 'Warning> This is a warning');
});

it('formats a console.info message', async () => {
Expand All @@ -171,7 +165,7 @@ describe('consoleFormatter', () => {
},
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Info> script.js:10:5: This is an info message');
assert.equal(result, 'Info> This is an info message');
});

it('formats a page error', async () => {
Expand All @@ -195,7 +189,7 @@ describe('consoleFormatter', () => {
location: {},
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Log> <unknown>: Hello from iframe');
assert.equal(result, 'Log> Hello from iframe');
});

it('formats a console.log message from a removed iframe with partial location', async () => {
Expand All @@ -208,7 +202,7 @@ describe('consoleFormatter', () => {
},
});
const result = await formatConsoleEvent(message);
assert.equal(result, 'Log> <unknown>: Hello from iframe');
assert.equal(result, 'Log> Hello from iframe');
});
});
});