chrome-devtools-mcp 0.20.3 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -20
- package/build/src/HeapSnapshotManager.js +94 -0
- package/build/src/McpContext.js +26 -49
- package/build/src/McpPage.js +16 -0
- package/build/src/McpResponse.js +220 -12
- package/build/src/PageCollector.js +14 -28
- package/build/src/WaitForHelper.js +31 -0
- package/build/src/bin/check-latest-version.js +25 -0
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +28 -9
- package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
- package/build/src/bin/chrome-devtools-mcp.js +1 -0
- package/build/src/bin/chrome-devtools.js +9 -3
- package/build/src/bin/cliDefinitions.js +15 -9
- package/build/src/daemon/client.js +1 -1
- package/build/src/daemon/daemon.js +2 -6
- package/build/src/daemon/utils.js +1 -0
- package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
- package/build/src/formatters/NetworkFormatter.js +24 -7
- package/build/src/index.js +22 -1
- package/build/src/telemetry/ClearcutLogger.js +145 -6
- package/build/src/telemetry/flagUtils.js +46 -4
- package/build/src/telemetry/toolMetricsUtils.js +88 -0
- package/build/src/telemetry/types.js +5 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
- package/build/src/third_party/THIRD_PARTY_NOTICES +1400 -483
- package/build/src/third_party/bundled-packages.json +6 -5
- package/build/src/third_party/devtools-formatter-worker.js +61 -66
- package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
- package/build/src/third_party/index.js +61622 -52803
- package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md +1 -0
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +10589 -4647
- package/build/src/tools/categories.js +5 -0
- package/build/src/tools/console.js +42 -39
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +5 -11
- package/build/src/tools/inPage.js +105 -0
- package/build/src/tools/input.js +18 -16
- package/build/src/tools/lighthouse.js +3 -3
- package/build/src/tools/memory.js +50 -5
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/pages.js +14 -6
- package/build/src/tools/performance.js +1 -1
- package/build/src/tools/screencast.js +2 -1
- package/build/src/tools/screenshot.js +3 -3
- package/build/src/tools/script.js +22 -16
- package/build/src/tools/tools.js +4 -0
- package/build/src/tools/webmcp.js +63 -0
- package/build/src/utils/check-for-updates.js +73 -0
- package/build/src/utils/files.js +4 -0
- package/build/src/utils/id.js +15 -0
- package/build/src/version.js +1 -1
- package/package.json +13 -9
- package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorNoCorpCrossOriginNoCorsRequest.md +0 -3
- package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorNoCorpCossOriginNoCorsRequest.md +0 -3
- package/build/src/utils/ExtensionRegistry.js +0 -35
package/build/src/McpResponse.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
|
|
7
|
+
import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
|
|
7
8
|
import { IssueFormatter } from './formatters/IssueFormatter.js';
|
|
8
9
|
import { NetworkFormatter } from './formatters/NetworkFormatter.js';
|
|
9
10
|
import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
|
|
@@ -12,6 +13,99 @@ import { DevTools } from './third_party/index.js';
|
|
|
12
13
|
import { handleDialog } from './tools/pages.js';
|
|
13
14
|
import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
|
|
14
15
|
import { paginate } from './utils/pagination.js';
|
|
16
|
+
export function replaceHtmlElementsWithUids(schema) {
|
|
17
|
+
if (typeof schema === 'boolean') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let isHtmlElement = false;
|
|
21
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
22
|
+
if (key === 'x-mcp-type' && value === 'HTMLElement') {
|
|
23
|
+
isHtmlElement = true;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (isHtmlElement) {
|
|
28
|
+
schema.properties = { uid: { type: 'string' } };
|
|
29
|
+
schema.required = ['uid'];
|
|
30
|
+
}
|
|
31
|
+
if (schema.properties) {
|
|
32
|
+
for (const key of Object.keys(schema.properties)) {
|
|
33
|
+
replaceHtmlElementsWithUids(schema.properties[key]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (schema.items) {
|
|
37
|
+
if (Array.isArray(schema.items)) {
|
|
38
|
+
for (const item of schema.items) {
|
|
39
|
+
replaceHtmlElementsWithUids(item);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
replaceHtmlElementsWithUids(schema.items);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (schema.anyOf) {
|
|
47
|
+
for (const s of schema.anyOf) {
|
|
48
|
+
replaceHtmlElementsWithUids(s);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (schema.allOf) {
|
|
52
|
+
for (const s of schema.allOf) {
|
|
53
|
+
replaceHtmlElementsWithUids(s);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (schema.oneOf) {
|
|
57
|
+
for (const s of schema.oneOf) {
|
|
58
|
+
replaceHtmlElementsWithUids(s);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function getToolGroup(page) {
|
|
63
|
+
// Check if there is a `devtoolstooldiscovery` event listener
|
|
64
|
+
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
|
|
65
|
+
// @ts-expect-error internal API
|
|
66
|
+
const client = page.pptrPage._client();
|
|
67
|
+
const { listeners } = await client.send('DOMDebugger.getEventListeners', {
|
|
68
|
+
objectId: windowHandle.remoteObject().objectId,
|
|
69
|
+
});
|
|
70
|
+
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const toolGroup = await page.pptrPage.evaluate(() => {
|
|
74
|
+
return new Promise(resolve => {
|
|
75
|
+
const event = new CustomEvent('devtoolstooldiscovery');
|
|
76
|
+
// @ts-expect-error Adding custom property
|
|
77
|
+
event.respondWith = (toolGroup) => {
|
|
78
|
+
if (!window.__dtmcp) {
|
|
79
|
+
window.__dtmcp = {};
|
|
80
|
+
}
|
|
81
|
+
window.__dtmcp.toolGroup = toolGroup;
|
|
82
|
+
// When receiving a toolGroup for the first time, expose a simple execution helper
|
|
83
|
+
if (!window.__dtmcp.executeTool) {
|
|
84
|
+
window.__dtmcp.executeTool = async (toolName, args) => {
|
|
85
|
+
if (!window.__dtmcp?.toolGroup) {
|
|
86
|
+
throw new Error('No tools found on the page');
|
|
87
|
+
}
|
|
88
|
+
const tool = window.__dtmcp.toolGroup.tools.find(t => t.name === toolName);
|
|
89
|
+
if (!tool) {
|
|
90
|
+
throw new Error(`Tool ${toolName} not found`);
|
|
91
|
+
}
|
|
92
|
+
return await tool.execute(args);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
resolve(toolGroup);
|
|
96
|
+
};
|
|
97
|
+
window.dispatchEvent(event);
|
|
98
|
+
// If the page does not synchronously call `event.respondWith`, return instead of timing out
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
resolve(undefined);
|
|
101
|
+
}, 0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
for (const tool of toolGroup?.tools ?? []) {
|
|
105
|
+
replaceHtmlElementsWithUids(tool.inputSchema);
|
|
106
|
+
}
|
|
107
|
+
return toolGroup;
|
|
108
|
+
}
|
|
15
109
|
export class McpResponse {
|
|
16
110
|
#includePages = false;
|
|
17
111
|
#includeExtensionServiceWorkers = false;
|
|
@@ -25,19 +119,26 @@ export class McpResponse {
|
|
|
25
119
|
#attachedLighthouseResult;
|
|
26
120
|
#textResponseLines = [];
|
|
27
121
|
#images = [];
|
|
122
|
+
#heapSnapshotOptions;
|
|
28
123
|
#networkRequestsOptions;
|
|
29
124
|
#consoleDataOptions;
|
|
30
125
|
#listExtensions;
|
|
126
|
+
#listInPageTools;
|
|
127
|
+
#listWebMcpTools;
|
|
31
128
|
#devToolsData;
|
|
32
129
|
#tabId;
|
|
33
130
|
#args;
|
|
34
131
|
#page;
|
|
132
|
+
#redactNetworkHeaders = true;
|
|
35
133
|
constructor(args) {
|
|
36
134
|
this.#args = args;
|
|
37
135
|
}
|
|
38
136
|
setPage(page) {
|
|
39
137
|
this.#page = page;
|
|
40
138
|
}
|
|
139
|
+
setRedactNetworkHeaders(value) {
|
|
140
|
+
this.#redactNetworkHeaders = value;
|
|
141
|
+
}
|
|
41
142
|
attachDevToolsData(data) {
|
|
42
143
|
this.#devToolsData = data;
|
|
43
144
|
}
|
|
@@ -59,6 +160,14 @@ export class McpResponse {
|
|
|
59
160
|
setListExtensions() {
|
|
60
161
|
this.#listExtensions = true;
|
|
61
162
|
}
|
|
163
|
+
setListInPageTools() {
|
|
164
|
+
if (this.#args.categoryInPageTools) {
|
|
165
|
+
this.#listInPageTools = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
setListWebMcpTools() {
|
|
169
|
+
this.#listWebMcpTools = true;
|
|
170
|
+
}
|
|
62
171
|
setIncludeNetworkRequests(value, options) {
|
|
63
172
|
if (!value) {
|
|
64
173
|
this.#networkRequestsOptions = undefined;
|
|
@@ -94,8 +203,8 @@ export class McpResponse {
|
|
|
94
203
|
includePreservedMessages: options?.includePreservedMessages,
|
|
95
204
|
};
|
|
96
205
|
}
|
|
97
|
-
attachNetworkRequest(
|
|
98
|
-
this.#attachedNetworkRequestId =
|
|
206
|
+
attachNetworkRequest(reqId, options) {
|
|
207
|
+
this.#attachedNetworkRequestId = reqId;
|
|
99
208
|
this.#attachedNetworkRequestOptions = options;
|
|
100
209
|
}
|
|
101
210
|
attachConsoleMessage(msgid) {
|
|
@@ -147,6 +256,22 @@ export class McpResponse {
|
|
|
147
256
|
appendResponseLine(value) {
|
|
148
257
|
this.#textResponseLines.push(value);
|
|
149
258
|
}
|
|
259
|
+
setHeapSnapshotAggregates(aggregates, options) {
|
|
260
|
+
this.#heapSnapshotOptions = {
|
|
261
|
+
...this.#heapSnapshotOptions,
|
|
262
|
+
include: true,
|
|
263
|
+
aggregates,
|
|
264
|
+
pagination: options,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
setHeapSnapshotStats(stats, staticData) {
|
|
268
|
+
this.#heapSnapshotOptions = {
|
|
269
|
+
...this.#heapSnapshotOptions,
|
|
270
|
+
include: true,
|
|
271
|
+
stats,
|
|
272
|
+
staticData,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
150
275
|
attachImage(value) {
|
|
151
276
|
this.#images.push(value);
|
|
152
277
|
}
|
|
@@ -159,6 +284,9 @@ export class McpResponse {
|
|
|
159
284
|
get snapshotParams() {
|
|
160
285
|
return this.#snapshotParams;
|
|
161
286
|
}
|
|
287
|
+
get listWebMcpTools() {
|
|
288
|
+
return this.#listWebMcpTools;
|
|
289
|
+
}
|
|
162
290
|
async handle(toolName, context) {
|
|
163
291
|
if (this.#includePages) {
|
|
164
292
|
await context.createPagesSnapshot();
|
|
@@ -176,8 +304,8 @@ export class McpResponse {
|
|
|
176
304
|
if (textSnapshot) {
|
|
177
305
|
const formatter = new SnapshotFormatter(textSnapshot);
|
|
178
306
|
if (this.#snapshotParams.filePath) {
|
|
179
|
-
await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
|
|
180
|
-
snapshot =
|
|
307
|
+
const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
|
|
308
|
+
snapshot = result.filename;
|
|
181
309
|
}
|
|
182
310
|
else {
|
|
183
311
|
snapshot = formatter;
|
|
@@ -196,7 +324,8 @@ export class McpResponse {
|
|
|
196
324
|
fetchData: true,
|
|
197
325
|
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
|
|
198
326
|
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
|
|
199
|
-
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
327
|
+
saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
|
|
328
|
+
redactNetworkHeaders: this.#redactNetworkHeaders,
|
|
200
329
|
});
|
|
201
330
|
detailedNetworkRequest = formatter;
|
|
202
331
|
}
|
|
@@ -223,14 +352,25 @@ export class McpResponse {
|
|
|
223
352
|
elementIdResolver: context.resolveCdpElementId.bind(context, this.#page),
|
|
224
353
|
});
|
|
225
354
|
if (!formatter.isValid()) {
|
|
226
|
-
throw new Error("Can't provide
|
|
355
|
+
throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
|
|
227
356
|
}
|
|
228
357
|
detailedConsoleMessage = formatter;
|
|
229
358
|
}
|
|
230
359
|
}
|
|
231
360
|
let extensions;
|
|
232
361
|
if (this.#listExtensions) {
|
|
233
|
-
extensions = context.listExtensions();
|
|
362
|
+
extensions = await context.listExtensions();
|
|
363
|
+
}
|
|
364
|
+
let inPageTools;
|
|
365
|
+
if (this.#listInPageTools) {
|
|
366
|
+
const page = this.#page ?? context.getSelectedMcpPage();
|
|
367
|
+
inPageTools = await getToolGroup(page);
|
|
368
|
+
page.inPageTools = inPageTools;
|
|
369
|
+
}
|
|
370
|
+
let webmcpTools;
|
|
371
|
+
if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
|
|
372
|
+
const page = this.#page ?? context.getSelectedMcpPage();
|
|
373
|
+
webmcpTools = page.getWebMcpTools();
|
|
234
374
|
}
|
|
235
375
|
let consoleMessages;
|
|
236
376
|
if (this.#consoleDataOptions?.include) {
|
|
@@ -294,7 +434,8 @@ export class McpResponse {
|
|
|
294
434
|
selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
|
|
295
435
|
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
|
|
296
436
|
fetchData: false,
|
|
297
|
-
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
437
|
+
saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
|
|
438
|
+
redactNetworkHeaders: this.#redactNetworkHeaders,
|
|
298
439
|
})));
|
|
299
440
|
}
|
|
300
441
|
}
|
|
@@ -308,6 +449,8 @@ export class McpResponse {
|
|
|
308
449
|
traceSummary: this.#attachedTraceSummary,
|
|
309
450
|
extensions,
|
|
310
451
|
lighthouseResult: this.#attachedLighthouseResult,
|
|
452
|
+
inPageTools,
|
|
453
|
+
webmcpTools,
|
|
311
454
|
});
|
|
312
455
|
}
|
|
313
456
|
format(toolName, context, data) {
|
|
@@ -473,6 +616,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
473
616
|
structuredContent.snapshot = data.snapshot.toJSON();
|
|
474
617
|
}
|
|
475
618
|
}
|
|
619
|
+
if (this.#heapSnapshotOptions?.include) {
|
|
620
|
+
response.push('## Heap Snapshot Data');
|
|
621
|
+
const stats = this.#heapSnapshotOptions.stats;
|
|
622
|
+
const staticData = this.#heapSnapshotOptions.staticData;
|
|
623
|
+
if (stats) {
|
|
624
|
+
response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
|
|
625
|
+
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
626
|
+
structuredContent.heapSnapshot.stats = stats;
|
|
627
|
+
}
|
|
628
|
+
if (staticData) {
|
|
629
|
+
response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
|
|
630
|
+
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
631
|
+
structuredContent.heapSnapshot.staticData = staticData;
|
|
632
|
+
}
|
|
633
|
+
const aggregates = this.#heapSnapshotOptions.aggregates;
|
|
634
|
+
if (aggregates) {
|
|
635
|
+
const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
|
|
636
|
+
const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
|
|
637
|
+
structuredContent.pagination = paginationData.pagination;
|
|
638
|
+
response.push(...paginationData.info);
|
|
639
|
+
const paginatedRecord = Object.fromEntries(paginationData.items);
|
|
640
|
+
const formatter = new HeapSnapshotFormatter(paginatedRecord);
|
|
641
|
+
response.push(formatter.toString());
|
|
642
|
+
structuredContent.heapSnapshotData = formatter.toJSON();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
476
645
|
if (data.detailedNetworkRequest) {
|
|
477
646
|
response.push(data.detailedNetworkRequest.toStringDetailed());
|
|
478
647
|
structuredContent.networkRequest =
|
|
@@ -484,20 +653,59 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
484
653
|
data.detailedConsoleMessage.toJSONDetailed();
|
|
485
654
|
}
|
|
486
655
|
if (data.extensions) {
|
|
487
|
-
|
|
656
|
+
const extensionArray = Array.from(data.extensions.values());
|
|
657
|
+
structuredContent.extensions = extensionArray;
|
|
488
658
|
response.push('## Extensions');
|
|
489
|
-
if (
|
|
659
|
+
if (extensionArray.length === 0) {
|
|
490
660
|
response.push('No extensions installed.');
|
|
491
661
|
}
|
|
492
662
|
else {
|
|
493
|
-
const extensionsMessage =
|
|
663
|
+
const extensionsMessage = extensionArray
|
|
494
664
|
.map(extension => {
|
|
495
|
-
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.
|
|
665
|
+
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.enabled ? 'Enabled' : 'Disabled'}`;
|
|
496
666
|
})
|
|
497
667
|
.join('\n');
|
|
498
668
|
response.push(extensionsMessage);
|
|
499
669
|
}
|
|
500
670
|
}
|
|
671
|
+
if (this.#listInPageTools) {
|
|
672
|
+
structuredContent.inPageTools = data.inPageTools ?? undefined;
|
|
673
|
+
response.push('## In-page tools');
|
|
674
|
+
if (!data.inPageTools || !data.inPageTools.tools) {
|
|
675
|
+
response.push('No in-page tools available.');
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
const toolGroup = data.inPageTools;
|
|
679
|
+
response.push(`${toolGroup.name}: ${toolGroup.description}`);
|
|
680
|
+
response.push('Available tools:');
|
|
681
|
+
const toolDefinitionsMessage = toolGroup.tools
|
|
682
|
+
.map(tool => {
|
|
683
|
+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}`;
|
|
684
|
+
})
|
|
685
|
+
.join('\n');
|
|
686
|
+
response.push(toolDefinitionsMessage);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (this.#listWebMcpTools && data.webmcpTools) {
|
|
690
|
+
structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
|
|
691
|
+
name,
|
|
692
|
+
description,
|
|
693
|
+
inputSchema,
|
|
694
|
+
annotations,
|
|
695
|
+
}));
|
|
696
|
+
response.push('## WebMCP tools');
|
|
697
|
+
if (data.webmcpTools.length === 0) {
|
|
698
|
+
response.push('No WebMCP tools available.');
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const webmcpToolsMessage = data.webmcpTools
|
|
702
|
+
.map(tool => {
|
|
703
|
+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
|
|
704
|
+
})
|
|
705
|
+
.join('\n');
|
|
706
|
+
response.push(webmcpToolsMessage);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
501
709
|
if (this.#networkRequestsOptions?.include && data.networkRequests) {
|
|
502
710
|
const requests = data.networkRequests;
|
|
503
711
|
response.push('## Network requests');
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { FakeIssuesManager } from './DevtoolsUtils.js';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { DevTools } from './third_party/index.js';
|
|
9
|
+
import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
|
|
9
10
|
export class UncaughtError {
|
|
10
11
|
details;
|
|
11
12
|
targetId;
|
|
@@ -14,16 +15,6 @@ export class UncaughtError {
|
|
|
14
15
|
this.targetId = targetId;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
function createIdGenerator() {
|
|
18
|
-
let i = 1;
|
|
19
|
-
return () => {
|
|
20
|
-
if (i === Number.MAX_SAFE_INTEGER) {
|
|
21
|
-
i = 0;
|
|
22
|
-
}
|
|
23
|
-
return i++;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export const stableIdSymbol = Symbol('stableIdSymbol');
|
|
27
18
|
export class PageCollector {
|
|
28
19
|
#browser;
|
|
29
20
|
#listenersInitializer;
|
|
@@ -198,42 +189,33 @@ class PageEventSubscriber {
|
|
|
198
189
|
#resetIssueAggregator() {
|
|
199
190
|
this.#issueManager = new FakeIssuesManager();
|
|
200
191
|
if (this.#issueAggregator) {
|
|
201
|
-
this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#
|
|
192
|
+
this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
|
|
202
193
|
}
|
|
203
194
|
this.#issueAggregator = new DevTools.IssueAggregator(this.#issueManager);
|
|
204
|
-
this.#issueAggregator.addEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#
|
|
195
|
+
this.#issueAggregator.addEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
|
|
205
196
|
}
|
|
206
197
|
async subscribe() {
|
|
207
198
|
this.#resetIssueAggregator();
|
|
208
199
|
this.#page.on('framenavigated', this.#onFrameNavigated);
|
|
209
|
-
this.#
|
|
200
|
+
this.#page.on('issue', this.#onIssueAdded);
|
|
210
201
|
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
|
|
211
|
-
try {
|
|
212
|
-
await this.#session.send('Audits.enable');
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
logger('Error subscribing to issues', error);
|
|
216
|
-
}
|
|
217
202
|
}
|
|
218
203
|
unsubscribe() {
|
|
219
204
|
this.#seenKeys.clear();
|
|
220
205
|
this.#seenIssues.clear();
|
|
221
206
|
this.#page.off('framenavigated', this.#onFrameNavigated);
|
|
222
|
-
this.#
|
|
207
|
+
this.#page.off('issue', this.#onIssueAdded);
|
|
223
208
|
this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
|
|
224
209
|
if (this.#issueAggregator) {
|
|
225
|
-
this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#
|
|
210
|
+
this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
|
|
226
211
|
}
|
|
227
|
-
void this.#session.send('Audits.disable').catch(() => {
|
|
228
|
-
// might fail.
|
|
229
|
-
});
|
|
230
212
|
}
|
|
231
|
-
#
|
|
213
|
+
#onAggregatedIssue = (event) => {
|
|
232
214
|
if (this.#seenIssues.has(event.data)) {
|
|
233
215
|
return;
|
|
234
216
|
}
|
|
235
217
|
this.#seenIssues.add(event.data);
|
|
236
|
-
this.#page.emit('
|
|
218
|
+
this.#page.emit('devtoolsAggregatedIssue', event.data);
|
|
237
219
|
};
|
|
238
220
|
#onExceptionThrown = (event) => {
|
|
239
221
|
this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
|
|
@@ -248,9 +230,13 @@ class PageEventSubscriber {
|
|
|
248
230
|
this.#seenIssues.clear();
|
|
249
231
|
this.#resetIssueAggregator();
|
|
250
232
|
};
|
|
251
|
-
#onIssueAdded = (
|
|
233
|
+
#onIssueAdded = (inspectorIssue) => {
|
|
252
234
|
try {
|
|
253
|
-
|
|
235
|
+
// DevTools currently defines this protocol issue code but has no
|
|
236
|
+
// IssuesManager handler for it, so calling into the mapper only warns.
|
|
237
|
+
if (String(inspectorIssue.code) === 'PerformanceIssue') {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
254
240
|
const issue = DevTools.createIssuesFromProtocolIssue(null,
|
|
255
241
|
// @ts-expect-error Protocol types diverge.
|
|
256
242
|
inspectorIssue)[0];
|
|
@@ -104,6 +104,23 @@ export class WaitForHelper {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
async waitForEventsAfterAction(action, options) {
|
|
107
|
+
if (options?.handleDialog) {
|
|
108
|
+
const dialogHandler = (dialog) => {
|
|
109
|
+
if (options.handleDialog === 'dismiss') {
|
|
110
|
+
void dialog.dismiss();
|
|
111
|
+
}
|
|
112
|
+
else if (options.handleDialog === 'accept') {
|
|
113
|
+
void dialog.accept();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
void dialog.accept(options.handleDialog);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
this.#page.on('dialog', dialogHandler);
|
|
120
|
+
this.#abortController.signal.addEventListener('abort', () => {
|
|
121
|
+
this.#page.off('dialog', dialogHandler);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
107
124
|
const navigationFinished = this.waitForNavigationStarted()
|
|
108
125
|
.then(navigationStated => {
|
|
109
126
|
if (navigationStated) {
|
|
@@ -137,3 +154,17 @@ export class WaitForHelper {
|
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
}
|
|
157
|
+
export function getNetworkMultiplierFromString(condition) {
|
|
158
|
+
const puppeteerCondition = condition;
|
|
159
|
+
switch (puppeteerCondition) {
|
|
160
|
+
case 'Fast 4G':
|
|
161
|
+
return 1;
|
|
162
|
+
case 'Slow 4G':
|
|
163
|
+
return 2.5;
|
|
164
|
+
case 'Fast 3G':
|
|
165
|
+
return 5;
|
|
166
|
+
case 'Slow 3G':
|
|
167
|
+
return 10;
|
|
168
|
+
}
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
const cachePath = process.argv[2];
|
|
10
|
+
if (cachePath) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://registry.npmjs.org/chrome-devtools-mcp/latest');
|
|
13
|
+
const data = response.ok ? await response.json() : null;
|
|
14
|
+
if (data &&
|
|
15
|
+
typeof data === 'object' &&
|
|
16
|
+
'version' in data &&
|
|
17
|
+
typeof data.version === 'string') {
|
|
18
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
19
|
+
await fs.writeFile(cachePath, JSON.stringify({ version: data.version }));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore errors.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -7,8 +7,8 @@ import { yargs, hideBin } from '../third_party/index.js';
|
|
|
7
7
|
export const cliOptions = {
|
|
8
8
|
autoConnect: {
|
|
9
9
|
type: 'boolean',
|
|
10
|
-
description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the
|
|
11
|
-
conflicts: ['isolated', 'executablePath'
|
|
10
|
+
description: 'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
|
|
11
|
+
conflicts: ['isolated', 'executablePath'],
|
|
12
12
|
default: false,
|
|
13
13
|
coerce: (value) => {
|
|
14
14
|
if (!value) {
|
|
@@ -21,7 +21,7 @@ export const cliOptions = {
|
|
|
21
21
|
type: 'string',
|
|
22
22
|
description: 'Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.',
|
|
23
23
|
alias: 'u',
|
|
24
|
-
conflicts: ['wsEndpoint'
|
|
24
|
+
conflicts: ['wsEndpoint'],
|
|
25
25
|
coerce: (url) => {
|
|
26
26
|
if (!url) {
|
|
27
27
|
return;
|
|
@@ -39,7 +39,7 @@ export const cliOptions = {
|
|
|
39
39
|
type: 'string',
|
|
40
40
|
description: 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
|
|
41
41
|
alias: 'w',
|
|
42
|
-
conflicts: ['browserUrl'
|
|
42
|
+
conflicts: ['browserUrl'],
|
|
43
43
|
coerce: (url) => {
|
|
44
44
|
if (!url) {
|
|
45
45
|
return;
|
|
@@ -102,7 +102,7 @@ export const cliOptions = {
|
|
|
102
102
|
channel: {
|
|
103
103
|
type: 'string',
|
|
104
104
|
description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
|
|
105
|
-
choices: ['
|
|
105
|
+
choices: ['canary', 'dev', 'beta', 'stable'],
|
|
106
106
|
conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
|
|
107
107
|
},
|
|
108
108
|
logFile: {
|
|
@@ -146,7 +146,12 @@ export const cliOptions = {
|
|
|
146
146
|
},
|
|
147
147
|
experimentalVision: {
|
|
148
148
|
type: 'boolean',
|
|
149
|
-
describe: 'Whether to enable
|
|
149
|
+
describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
|
|
150
|
+
hidden: false,
|
|
151
|
+
},
|
|
152
|
+
experimentalMemory: {
|
|
153
|
+
type: 'boolean',
|
|
154
|
+
describe: 'Whether to enable experimental memory tools.',
|
|
150
155
|
hidden: true,
|
|
151
156
|
},
|
|
152
157
|
experimentalStructuredContent: {
|
|
@@ -168,6 +173,10 @@ export const cliOptions = {
|
|
|
168
173
|
type: 'boolean',
|
|
169
174
|
describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
|
|
170
175
|
},
|
|
176
|
+
experimentalWebmcp: {
|
|
177
|
+
type: 'boolean',
|
|
178
|
+
describe: 'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
|
|
179
|
+
},
|
|
171
180
|
chromeArg: {
|
|
172
181
|
type: 'array',
|
|
173
182
|
describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
|
|
@@ -192,10 +201,15 @@ export const cliOptions = {
|
|
|
192
201
|
describe: 'Set to false to exclude tools related to network.',
|
|
193
202
|
},
|
|
194
203
|
categoryExtensions: {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
hidden: false,
|
|
206
|
+
default: false,
|
|
207
|
+
describe: 'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
|
|
208
|
+
},
|
|
209
|
+
categoryInPageTools: {
|
|
195
210
|
type: 'boolean',
|
|
196
211
|
hidden: true,
|
|
197
|
-
|
|
198
|
-
describe: 'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
|
|
212
|
+
describe: 'Set to true to enable tools exposed by the inspected page itself',
|
|
199
213
|
},
|
|
200
214
|
performanceCrux: {
|
|
201
215
|
type: 'boolean',
|
|
@@ -205,7 +219,7 @@ export const cliOptions = {
|
|
|
205
219
|
usageStatistics: {
|
|
206
220
|
type: 'boolean',
|
|
207
221
|
default: true,
|
|
208
|
-
describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.',
|
|
222
|
+
describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if `CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS` or `CI` env variables are set.',
|
|
209
223
|
},
|
|
210
224
|
clearcutEndpoint: {
|
|
211
225
|
type: 'string',
|
|
@@ -231,6 +245,11 @@ export const cliOptions = {
|
|
|
231
245
|
describe: 'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
|
|
232
246
|
hidden: true,
|
|
233
247
|
},
|
|
248
|
+
redactNetworkHeaders: {
|
|
249
|
+
type: 'boolean',
|
|
250
|
+
describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
|
|
251
|
+
default: false,
|
|
252
|
+
},
|
|
234
253
|
};
|
|
235
254
|
export function parseArguments(version, argv = process.argv) {
|
|
236
255
|
const yargsInstance = yargs(hideBin(argv))
|
|
@@ -9,8 +9,10 @@ import { createMcpServer, logDisclaimers } from '../index.js';
|
|
|
9
9
|
import { logger, saveLogsToFile } from '../logger.js';
|
|
10
10
|
import { computeFlagUsage } from '../telemetry/flagUtils.js';
|
|
11
11
|
import { StdioServerTransport } from '../third_party/index.js';
|
|
12
|
+
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
12
13
|
import { VERSION } from '../version.js';
|
|
13
14
|
import { cliOptions, parseArguments } from './chrome-devtools-mcp-cli-options.js';
|
|
15
|
+
await checkForUpdates('Run `npm install chrome-devtools-mcp@latest` to update.');
|
|
14
16
|
export const args = parseArguments(VERSION);
|
|
15
17
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
16
18
|
if (process.env['CI'] ||
|