diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 3356b06a..034e8687 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -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) --- diff --git a/src/McpResponse.ts b/src/McpResponse.ts index eff7e708..aa2ce96b 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -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[] = []; @@ -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 { @@ -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(); + 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; } diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index 06f5365e..beb71887 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -21,20 +21,33 @@ const logLevels: Record = { export async function formatConsoleEvent( event: ConsoleMessage | Error, + options?: { + compact?: boolean; + includeTimestamp?: boolean; + }, ): Promise { // 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 { +async function formatConsoleMessage( + msg: ConsoleMessage, + options?: { + compact?: boolean; + includeTimestamp?: boolean; + }, +): Promise { 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; message += await errorHandle @@ -42,36 +55,56 @@ async function formatConsoleMessage(msg: ConsoleMessage): Promise { 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 { +async function formatArgs( + args: readonly JSHandle[], + compact = true, +): Promise { + if (compact && args.length === 0) { + return ''; + } + const argValues = await Promise.all( args.map(arg => arg.jsonValue().catch(() => { @@ -82,8 +115,25 @@ async function formatArgs(args: readonly JSHandle[]): Promise { 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(' '); } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index a0741f4c..26a0bc27 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -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; diff --git a/src/tools/console.ts b/src/tools/console.ts index 9a3ff114..5eaa8d4b 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -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, + }); }, }); diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index 4fd6213c..7902bd15 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -195,7 +189,7 @@ describe('consoleFormatter', () => { location: {}, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); + assert.equal(result, 'Log> Hello from iframe'); }); it('formats a console.log message from a removed iframe with partial location', async () => { @@ -208,7 +202,7 @@ describe('consoleFormatter', () => { }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); + assert.equal(result, 'Log> Hello from iframe'); }); }); });