Skip to content

Commit 9c3063c

Browse files
authored
Merge branch 'main' into feat/update-cli
2 parents 873ee08 + bf3cb58 commit 9c3063c

11 files changed

Lines changed: 246 additions & 132 deletions

src/McpResponse.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {WebMCPTool} from 'puppeteer-core';
99
import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js';
1010
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
1111
import {HeapSnapshotFormatter} from './formatters/HeapSnapshotFormatter.js';
12+
import {isNodeLike} from './formatters/HeapSnapshotFormatter.js';
1213
import {IssueFormatter} from './formatters/IssueFormatter.js';
1314
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
1415
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
@@ -202,6 +203,7 @@ export class McpResponse implements Response {
202203
#args: ParsedArguments;
203204
#page?: McpPage;
204205
#redactNetworkHeaders = true;
206+
#error?: Error;
205207

206208
constructor(args: ParsedArguments) {
207209
this.#args = args;
@@ -306,6 +308,10 @@ export class McpResponse implements Response {
306308
};
307309
}
308310

311+
setError(error: Error): void {
312+
this.#error = error;
313+
}
314+
309315
attachNetworkRequest(
310316
reqId: number,
311317
options?: {requestFilePath?: string; responseFilePath?: string},
@@ -374,6 +380,10 @@ export class McpResponse implements Response {
374380
return this.#consoleDataOptions?.types;
375381
}
376382

383+
get error(): Error | undefined {
384+
return this.#error;
385+
}
386+
377387
appendResponseLine(value: string): void {
378388
this.#textResponseLines.push(value);
379389
}
@@ -661,6 +671,7 @@ export class McpResponse implements Response {
661671
lighthouseResult: this.#attachedLighthouseResult,
662672
inPageTools,
663673
webmcpTools,
674+
errorMessage: this.#error?.message,
664675
});
665676
}
666677

@@ -679,6 +690,7 @@ export class McpResponse implements Response {
679690
lighthouseResult?: LighthouseData;
680691
inPageTools?: ToolGroup<ToolDefinition>;
681692
webmcpTools?: WebMCPTool[];
693+
errorMessage?: string;
682694
},
683695
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
684696
const structuredContent: {
@@ -717,6 +729,7 @@ export class McpResponse implements Response {
717729
heapSnapshotNodes?: readonly object[];
718730
extensionServiceWorkers?: object[];
719731
extensionPages?: object[];
732+
errorMessage?: string;
720733
} = {};
721734

722735
const response = [];
@@ -945,8 +958,12 @@ Call ${handleDialog.name} to handle it before continuing.`);
945958
}
946959
const nodes = this.#heapSnapshotOptions.nodes;
947960
if (nodes) {
961+
const sortedItems = nodes.items
962+
.filter(isNodeLike)
963+
.sort((a, b) => b.retainedSize - a.retainedSize);
964+
948965
const paginationData = this.#dataWithPagination(
949-
nodes.items,
966+
sortedItems,
950967
this.#heapSnapshotOptions.pagination,
951968
);
952969

@@ -1075,6 +1092,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
10751092
}
10761093
}
10771094

1095+
if (data.errorMessage) {
1096+
response.push(`Error: ${data.errorMessage}`);
1097+
structuredContent.errorMessage = data.errorMessage;
1098+
}
1099+
10781100
const text: TextContent = {
10791101
type: 'text',
10801102
text: response.join('\n'),

src/WaitForHelper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ export class WaitForHelper {
128128
action: () => Promise<unknown>,
129129
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
130130
): Promise<void> {
131+
let dialogOpened = false;
131132
if (options?.handleDialog) {
132133
const dialogHandler = (dialog: Pick<Dialog, 'accept' | 'dismiss'>) => {
134+
dialogOpened = true;
133135
if (options.handleDialog === 'dismiss') {
134136
void dialog.dismiss();
135137
} else if (options.handleDialog === 'accept') {
@@ -167,6 +169,10 @@ export class WaitForHelper {
167169
try {
168170
await navigationFinished;
169171

172+
if (dialogOpened) {
173+
return;
174+
}
175+
170176
// Wait for stable dom after navigation so we execute in
171177
// the correct context
172178
await this.waitForStableDom();

src/formatters/HeapSnapshotFormatter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface FormattedSnapshotEntry {
1616
retainedSize: number;
1717
}
1818

19-
function isNodeLike(
19+
export function isNodeLike(
2020
item: unknown,
2121
): item is DevTools.HeapSnapshotModel.HeapSnapshotModel.Node {
2222
return (
@@ -55,7 +55,7 @@ export class HeapSnapshotFormatter {
5555
}
5656

5757
#getSortedAggregates(): AggregatedInfoWithUid[] {
58-
return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
58+
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
5959
}
6060

6161
toString(): string {
@@ -92,6 +92,6 @@ export class HeapSnapshotFormatter {
9292
): Array<
9393
[string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo]
9494
> {
95-
return Object.entries(aggregates).sort((a, b) => b[1].self - a[1].self);
95+
return Object.entries(aggregates).sort((a, b) => b[1].maxRet - a[1].maxRet);
9696
}
9797
}

src/index.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -305,31 +305,35 @@ export async function createMcpServer(
305305
: new McpResponse(serverArgs);
306306

307307
response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
308-
if ('pageScoped' in tool && tool.pageScoped) {
309-
const page =
310-
serverArgs.experimentalPageIdRouting &&
311-
params.pageId &&
312-
!serverArgs.slim
313-
? context.getPageById(params.pageId)
314-
: context.getSelectedMcpPage();
315-
response.setPage(page);
316-
await tool.handler(
317-
{
318-
params,
319-
page,
320-
},
321-
response,
322-
context,
323-
);
324-
} else {
325-
await tool.handler(
326-
// @ts-expect-error types do not match.
327-
{
328-
params,
329-
},
330-
response,
331-
context,
332-
);
308+
try {
309+
if ('pageScoped' in tool && tool.pageScoped) {
310+
const page =
311+
serverArgs.experimentalPageIdRouting &&
312+
params.pageId &&
313+
!serverArgs.slim
314+
? context.getPageById(params.pageId)
315+
: context.getSelectedMcpPage();
316+
response.setPage(page);
317+
await tool.handler(
318+
{
319+
params,
320+
page,
321+
},
322+
response,
323+
context,
324+
);
325+
} else {
326+
await tool.handler(
327+
// @ts-expect-error types do not match.
328+
{
329+
params,
330+
},
331+
response,
332+
context,
333+
);
334+
}
335+
} catch (err) {
336+
response.setError(err);
333337
}
334338
const {content, structuredContent} = await response.handle(
335339
tool.name,
@@ -340,6 +344,9 @@ export async function createMcpServer(
340344
} = {
341345
content,
342346
};
347+
if (response.error) {
348+
result.isError = true;
349+
}
343350
success = true;
344351
if (serverArgs.experimentalStructuredContent) {
345352
result.structuredContent = structuredContent as Record<

src/telemetry/ClearcutLogger.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,25 @@ export function transformArgType(zodType: ZodType): string {
9090
}
9191
}
9292

93+
const BUCKETS = [
94+
0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000,
95+
];
96+
97+
function bucketize(value: number): number {
98+
for (const bucket of BUCKETS) {
99+
if (bucket >= value) {
100+
return bucket;
101+
}
102+
}
103+
return BUCKETS[BUCKETS.length - 1];
104+
}
105+
93106
function transformValue(
94107
zodType: ZodType,
95108
value: unknown,
96109
): LoggedToolCallArgValue {
97110
if (zodType === 'ZodString') {
98-
return (value as string).length;
111+
return bucketize((value as string).length);
99112
} else if (zodType === 'ZodArray') {
100113
return (value as unknown[]).length;
101114
} else {

tests/formatters/HeapSnapshotFormatter.test.js.snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
exports[`HeapSnapshotFormatter > toString > formats data as CSV and sorts by self size 1`] = `
1+
exports[`HeapSnapshotFormatter > toString > formats data as CSV and sorts by retained size 1`] = `
22
uid,className,count,selfSize,maxRetainedSize
33
1,"ObjectA",10,100,1000
44
2,"ObjectB",5,50,500

tests/formatters/HeapSnapshotFormatter.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ describe('HeapSnapshotFormatter', () => {
3737
};
3838

3939
describe('toString', () => {
40-
it('formats data as CSV and sorts by self size', t => {
40+
it('formats data as CSV and sorts by retained size', t => {
4141
const formatter = new HeapSnapshotFormatter(mockAggregates);
4242
const result = formatter.toString();
4343
t.assert.snapshot?.(result);
4444
});
4545
});
4646

4747
describe('toJSON', () => {
48-
it('returns structured data sorted by self size', () => {
48+
it('returns structured data sorted by retained size', () => {
4949
const formatter = new HeapSnapshotFormatter(mockAggregates);
5050
const result = formatter.toJSON();
5151
assert.deepStrictEqual(result, [
@@ -68,18 +68,20 @@ describe('HeapSnapshotFormatter', () => {
6868
});
6969

7070
describe('sort', () => {
71-
it('sorts aggregates by self size descending', () => {
71+
it('sorts aggregates by retained size descending', () => {
7272
const unsortedAggregates: Record<
7373
string,
7474
DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
7575
> = {
7676
ObjectB: {
7777
name: 'ObjectB',
7878
self: 50,
79+
maxRet: 500,
7980
},
8081
ObjectA: {
8182
name: 'ObjectA',
8283
self: 100,
84+
maxRet: 1000,
8385
},
8486
} as unknown as Record<
8587
string,

tests/index.test.js.snapshot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ exports[`e2e > calls a tool 1`] = `
55
exports[`e2e > calls a tool multiple times 1`] = `
66
[{"type":"text","text":"## Pages\\n1: about:blank [selected]"}]
77
`;
8+
9+
exports[`e2e > returns blocked message when dialog is opened during tool execution 1`] = `
10+
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\nError: Failed to interact with the element with uid 1_1. The element did not become interactive within the configured timeout."}],"isError":true}
11+
`;

tests/index.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,37 @@ describe('e2e', () => {
245245
},
246246
);
247247
});
248+
249+
it('returns blocked message when dialog is opened during tool execution', async t => {
250+
await withClient(async client => {
251+
// Navigate to a page with a button that triggers a dialog on click
252+
await client.callTool({
253+
name: 'new_page',
254+
arguments: {
255+
url: `data:text/html,<button id="test" onclick="alert('test dialog')">Click me</button>`,
256+
},
257+
});
258+
259+
const snapshotResult = await client.callTool({
260+
name: 'take_snapshot',
261+
arguments: {},
262+
});
263+
264+
const snapshotText = (snapshotResult.content as TextContent[])[0].text;
265+
const match = snapshotText.match(/uid=(\d+_\d+)\s+button "Click me"/);
266+
const uid = match ? match[1] : '1_1';
267+
268+
// Trigger the dialog
269+
const result = await client.callTool({
270+
name: 'click',
271+
arguments: {
272+
uid,
273+
},
274+
});
275+
276+
t.assert.snapshot?.(JSON.stringify(result));
277+
});
278+
});
248279
});
249280

250281
async function getToolsWithFilteredCategories(

tests/telemetry/ClearcutLogger.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,35 @@ describe('ClearcutLogger', () => {
235235
});
236236
});
237237

238+
it('bucketizes string lengths correctly', () => {
239+
const schema = {
240+
str0: zod.string(),
241+
str1: zod.string(),
242+
str3: zod.string(),
243+
str5: zod.string(),
244+
str10000: zod.string(),
245+
str10001: zod.string(),
246+
};
247+
248+
const params = {
249+
str0: '',
250+
str1: 'a',
251+
str3: 'abc',
252+
str5: 'abcde',
253+
str10000: 'a'.repeat(10000),
254+
str10001: 'a'.repeat(10001),
255+
};
256+
257+
const sanitized = sanitizeParams(params, schema);
258+
259+
assert.strictEqual(sanitized.str0_length, 0);
260+
assert.strictEqual(sanitized.str1_length, 1);
261+
assert.strictEqual(sanitized.str3_length, 5); // snaps to 5
262+
assert.strictEqual(sanitized.str5_length, 5);
263+
assert.strictEqual(sanitized.str10000_length, 10000);
264+
assert.strictEqual(sanitized.str10001_length, 10000); // snaps to 10000
265+
});
266+
238267
it('throws error for unsupported types', () => {
239268
const schema = {
240269
myObj: zod.object({foo: zod.string()}),

0 commit comments

Comments
 (0)