Skip to content

Commit 15d942c

Browse files
authored
feat: add filters and pagination to the console messages tool (#387)
In the next pr, we will add ids to messages and another tool to get more details by id
1 parent d47aaa9 commit 15d942c

File tree

8 files changed

+268
-271
lines changed

8 files changed

+268
-271
lines changed

docs/tool-reference.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,11 @@ so returned values have to JSON-serializable.
300300

301301
**Description:** List all console messages for the currently selected page since the last navigation.
302302

303-
**Parameters:** None
303+
**Parameters:**
304+
305+
- **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page.
306+
- **pageSize** (integer) _(optional)_: Maximum number of messages to return. When omitted, returns all requests.
307+
- **types** (array) _(optional)_: Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.
304308

305309
---
306310

src/McpResponse.ts

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
ImageContent,
88
TextContent,
99
} from '@modelcontextprotocol/sdk/types.js';
10-
import type {ResourceType} from 'puppeteer-core';
10+
import type {ConsoleMessage, ResourceType} from 'puppeteer-core';
1111

1212
import {formatConsoleEvent} from './formatters/consoleFormatter.js';
1313
import {
@@ -29,20 +29,30 @@ interface NetworkRequestData {
2929
responseBody?: string;
3030
}
3131

32+
export interface ConsoleMessageData {
33+
type: string;
34+
message: string;
35+
args: string[];
36+
}
37+
3238
export class McpResponse implements Response {
3339
#includePages = false;
3440
#includeSnapshot = false;
3541
#includeVerboseSnapshot = false;
3642
#attachedNetworkRequestData?: NetworkRequestData;
37-
#includeConsoleData = false;
43+
#consoleMessagesData?: ConsoleMessageData[];
3844
#textResponseLines: string[] = [];
39-
#formattedConsoleData?: string[];
4045
#images: ImageContentData[] = [];
4146
#networkRequestsOptions?: {
4247
include: boolean;
4348
pagination?: PaginationOptions;
4449
resourceTypes?: ResourceType[];
4550
};
51+
#consoleDataOptions?: {
52+
include: boolean;
53+
pagination?: PaginationOptions;
54+
types?: string[];
55+
};
4656

4757
setIncludePages(value: boolean): void {
4858
this.#includePages = value;
@@ -79,8 +89,30 @@ export class McpResponse implements Response {
7989
};
8090
}
8191

82-
setIncludeConsoleData(value: boolean): void {
83-
this.#includeConsoleData = value;
92+
setIncludeConsoleData(
93+
value: boolean,
94+
options?: {
95+
pageSize?: number;
96+
pageIdx?: number;
97+
types?: string[];
98+
},
99+
): void {
100+
if (!value) {
101+
this.#consoleDataOptions = undefined;
102+
return;
103+
}
104+
105+
this.#consoleDataOptions = {
106+
include: value,
107+
pagination:
108+
options?.pageSize || options?.pageIdx
109+
? {
110+
pageSize: options.pageSize,
111+
pageIdx: options.pageIdx,
112+
}
113+
: undefined,
114+
types: options?.types,
115+
};
84116
}
85117

86118
attachNetworkRequest(reqid: number): void {
@@ -98,14 +130,20 @@ export class McpResponse implements Response {
98130
}
99131

100132
get includeConsoleData(): boolean {
101-
return this.#includeConsoleData;
133+
return this.#consoleDataOptions?.include ?? false;
102134
}
103135
get attachedNetworkRequestId(): number | undefined {
104136
return this.#attachedNetworkRequestData?.networkRequestStableId;
105137
}
106138
get networkRequestsPageIdx(): number | undefined {
107139
return this.#networkRequestsOptions?.pagination?.pageIdx;
108140
}
141+
get consoleMessagesPageIdx(): number | undefined {
142+
return this.#consoleDataOptions?.pagination?.pageIdx;
143+
}
144+
get consoleMessagesTypes(): string[] | undefined {
145+
return this.#consoleDataOptions?.types;
146+
}
109147

110148
appendResponseLine(value: string): void {
111149
this.#textResponseLines.push(value);
@@ -142,8 +180,6 @@ export class McpResponse implements Response {
142180
await context.createTextSnapshot(this.#includeVerboseSnapshot);
143181
}
144182

145-
let formattedConsoleMessages: string[];
146-
147183
if (this.#attachedNetworkRequestData?.networkRequestStableId) {
148184
const request = context.getNetworkRequestById(
149185
this.#attachedNetworkRequestData.networkRequestStableId,
@@ -159,14 +195,35 @@ export class McpResponse implements Response {
159195
}
160196
}
161197

162-
if (this.#includeConsoleData) {
163-
const consoleMessages = context.getConsoleData();
164-
if (consoleMessages) {
165-
formattedConsoleMessages = await Promise.all(
166-
consoleMessages.map(message => formatConsoleEvent(message)),
167-
);
168-
this.#formattedConsoleData = formattedConsoleMessages;
169-
}
198+
if (this.#consoleDataOptions?.include) {
199+
const messages = context.getConsoleData();
200+
201+
this.#consoleMessagesData = await Promise.all(
202+
messages.map(async (item): Promise<ConsoleMessageData> => {
203+
if ('args' in item) {
204+
const consoleMessage = item as ConsoleMessage;
205+
return {
206+
type: consoleMessage.type(),
207+
message: consoleMessage.text(),
208+
args: await Promise.all(
209+
consoleMessage.args().map(async arg => {
210+
const stringArg = await arg.jsonValue().catch(() => {
211+
// Ignore errors.
212+
});
213+
return typeof stringArg === 'object'
214+
? JSON.stringify(stringArg)
215+
: String(stringArg);
216+
}),
217+
),
218+
};
219+
}
220+
return {
221+
type: 'error',
222+
message: (item as Error).message,
223+
args: [],
224+
};
225+
}),
226+
);
170227
}
171228

172229
return this.format(toolName, context);
@@ -264,10 +321,19 @@ Call ${handleDialog.name} to handle it before continuing.`);
264321
}
265322
}
266323

267-
if (this.#includeConsoleData && this.#formattedConsoleData) {
324+
if (this.#consoleDataOptions?.include) {
325+
const messages = this.#consoleMessagesData ?? [];
326+
268327
response.push('## Console messages');
269-
if (this.#formattedConsoleData.length) {
270-
response.push(...this.#formattedConsoleData);
328+
if (messages.length) {
329+
const data = this.#dataWithPagination(
330+
messages,
331+
this.#consoleDataOptions.pagination,
332+
);
333+
response.push(...data.info);
334+
response.push(
335+
...data.items.map(message => formatConsoleEvent(message)),
336+
);
271337
} else {
272338
response.push('<no console messages found>');
273339
}

src/formatters/consoleFormatter.ts

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

7-
import type {
8-
ConsoleMessage,
9-
JSHandle,
10-
ConsoleMessageLocation,
11-
} from 'puppeteer-core';
7+
import type {ConsoleMessageData} from '../McpResponse.js';
128

139
const logLevels: Record<string, string> = {
1410
log: 'Log',
@@ -19,78 +15,33 @@ const logLevels: Record<string, string> = {
1915
assert: 'Assert',
2016
};
2117

22-
export async function formatConsoleEvent(
23-
event: ConsoleMessage | Error,
24-
): Promise<string> {
25-
// Check if the event object has the .type() method, which is unique to ConsoleMessage
26-
if ('type' in event) {
27-
return await formatConsoleMessage(event);
28-
}
29-
return `Error: ${event.message}`;
30-
}
31-
32-
async function formatConsoleMessage(msg: ConsoleMessage): Promise<string> {
33-
const logLevel = logLevels[msg.type()];
34-
const args = msg.args();
18+
export function formatConsoleEvent(msg: ConsoleMessageData): string {
19+
const logLevel = logLevels[msg.type] ?? 'Log';
20+
const text = msg.message;
3521

36-
if (logLevel === 'Error') {
37-
let message = `${logLevel}> `;
38-
if (msg.text() === 'JSHandle@error') {
39-
const errorHandle = args[0] as JSHandle<Error>;
40-
message += await errorHandle
41-
.evaluate(error => {
42-
return error.toString();
43-
})
44-
.catch(() => {
45-
return 'Error occurred';
46-
});
47-
void errorHandle.dispose().catch();
22+
const formattedArgs = formatArgs(msg.args, text);
23+
return `${logLevel}> ${text} ${formattedArgs}`.trim();
24+
}
4825

49-
const formattedArgs = await formatArgs(args.slice(1));
50-
if (formattedArgs) {
51-
message += ` ${formattedArgs}`;
52-
}
53-
} else {
54-
message += msg.text();
55-
const formattedArgs = await formatArgs(args);
56-
if (formattedArgs) {
57-
message += ` ${formattedArgs}`;
58-
}
59-
for (const frame of msg.stackTrace()) {
60-
message += '\n' + formatStackFrame(frame);
61-
}
62-
}
63-
return message;
26+
// Only includes the first arg and indicates that there are more args
27+
function formatArgs(args: string[], messageText: string): string {
28+
if (args.length === 0) {
29+
return '';
6430
}
6531

66-
const formattedArgs = await formatArgs(args);
67-
const text = msg.text();
68-
69-
return `${logLevel}> ${formatStackFrame(
70-
msg.location(),
71-
)}: ${text} ${formattedArgs}`.trim();
72-
}
73-
74-
async function formatArgs(args: readonly JSHandle[]): Promise<string> {
75-
const argValues = await Promise.all(
76-
args.map(arg =>
77-
arg.jsonValue().catch(() => {
78-
// Ignore errors
79-
}),
80-
),
81-
);
32+
let formattedArgs = '';
33+
const firstArg = args[0];
8234

83-
return argValues
84-
.map(value => {
85-
return typeof value === 'object' ? JSON.stringify(value) : String(value);
86-
})
87-
.join(' ');
88-
}
35+
if (firstArg !== messageText) {
36+
formattedArgs +=
37+
typeof firstArg === 'object'
38+
? JSON.stringify(firstArg)
39+
: String(firstArg);
40+
}
8941

90-
function formatStackFrame(stackFrame: ConsoleMessageLocation): string {
91-
if (!stackFrame?.url) {
92-
return '<unknown>';
42+
if (args.length > 1) {
43+
return `${formattedArgs} ...`;
9344
}
94-
const filename = stackFrame.url.replace(/^.*\//, '');
95-
return `${filename}:${stackFrame.lineNumber}:${stackFrame.columnNumber}`;
45+
46+
return formattedArgs;
9647
}

src/tools/ToolDefinition.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export interface Response {
4747
value: boolean,
4848
options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]},
4949
): void;
50-
setIncludeConsoleData(value: boolean): void;
50+
setIncludeConsoleData(
51+
value: boolean,
52+
options?: {pageSize?: number; pageIdx?: number; types?: string[]},
53+
): void;
54+
setIncludeSnapshot(value: boolean): void;
5155
setIncludeSnapshot(value: boolean, verbose?: boolean): void;
5256
attachImage(value: ImageContentData): void;
5357
attachNetworkRequest(reqid: number): void;

src/tools/console.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,37 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type {ConsoleMessageType} from 'puppeteer-core';
8+
import z from 'zod';
9+
710
import {ToolCategories} from './categories.js';
811
import {defineTool} from './ToolDefinition.js';
912

13+
const FILTERABLE_MESSAGE_TYPES: readonly [
14+
ConsoleMessageType,
15+
...ConsoleMessageType[],
16+
] = [
17+
'log',
18+
'debug',
19+
'info',
20+
'error',
21+
'warn',
22+
'dir',
23+
'dirxml',
24+
'table',
25+
'trace',
26+
'clear',
27+
'startGroup',
28+
'startGroupCollapsed',
29+
'endGroup',
30+
'assert',
31+
'profile',
32+
'profileEnd',
33+
'count',
34+
'timeEnd',
35+
'verbose',
36+
];
37+
1038
export const consoleTool = defineTool({
1139
name: 'list_console_messages',
1240
description:
@@ -15,8 +43,35 @@ export const consoleTool = defineTool({
1543
category: ToolCategories.DEBUGGING,
1644
readOnlyHint: true,
1745
},
18-
schema: {},
19-
handler: async (_request, response) => {
20-
response.setIncludeConsoleData(true);
46+
schema: {
47+
pageSize: z
48+
.number()
49+
.int()
50+
.positive()
51+
.optional()
52+
.describe(
53+
'Maximum number of messages to return. When omitted, returns all requests.',
54+
),
55+
pageIdx: z
56+
.number()
57+
.int()
58+
.min(0)
59+
.optional()
60+
.describe(
61+
'Page number to return (0-based). When omitted, returns the first page.',
62+
),
63+
types: z
64+
.array(z.enum(FILTERABLE_MESSAGE_TYPES))
65+
.optional()
66+
.describe(
67+
'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.',
68+
),
69+
},
70+
handler: async (request, response) => {
71+
response.setIncludeConsoleData(true, {
72+
pageSize: request.params.pageSize,
73+
pageIdx: request.params.pageIdx,
74+
types: request.params.types,
75+
});
2176
},
2277
});

tests/McpResponse.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,7 @@ reqid=1 GET http://example.com [pending]`,
341341
// Cannot check the full text because it contains local file path
342342
assert.ok(
343343
result[0].text.toString().startsWith(`# test response
344-
## Console messages
345-
Log>`),
344+
## Console messages`),
346345
);
347346
assert.ok(result[0].text.toString().includes('Hello from the test'));
348347
});

0 commit comments

Comments
 (0)