chrome-devtools-mcp 0.26.0 → 1.1.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 +59 -10
- package/build/src/DevtoolsUtils.js +14 -1
- package/build/src/HeapSnapshotManager.js +42 -18
- package/build/src/McpContext.js +61 -23
- package/build/src/McpResponse.js +51 -21
- package/build/src/ToolHandler.js +30 -1
- package/build/src/WaitForHelper.js +18 -4
- package/build/src/bin/check-latest-version.js +25 -1
- package/build/src/bin/chrome-devtools-cli-options.js +81 -39
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +2 -8
- package/build/src/bin/chrome-devtools-mcp-main.js +38 -0
- package/build/src/browser.js +36 -2
- package/build/src/daemon/client.js +12 -6
- package/build/src/daemon/daemon.js +62 -5
- package/build/src/formatters/HeapSnapshotFormatter.js +30 -9
- package/build/src/index.js +3 -1
- package/build/src/telemetry/ClearcutLogger.js +8 -119
- package/build/src/telemetry/errors.js +4 -0
- package/build/src/telemetry/flagUtils.js +4 -3
- package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
- package/build/src/telemetry/persistence.js +20 -2
- package/build/src/telemetry/transformation.js +134 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +4 -719
- package/build/src/third_party/bundled-packages.json +2 -2
- package/build/src/third_party/devtools-formatter-worker.js +447 -114
- package/build/src/third_party/devtools-heap-snapshot-worker.js +2 -3
- package/build/src/third_party/index.js +3443 -30153
- package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
- package/build/src/tools/ToolDefinition.js +2 -2
- package/build/src/tools/emulation.js +28 -2
- package/build/src/tools/extensions.js +2 -0
- package/build/src/tools/input.js +19 -10
- package/build/src/tools/lighthouse.js +1 -1
- package/build/src/tools/memory.js +39 -17
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/performance.js +9 -6
- package/build/src/tools/screencast.js +1 -1
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +32 -10
- package/build/src/tools/snapshot.js +1 -1
- package/build/src/trace-processing/parse.js +2 -2
- package/build/src/utils/files.js +43 -0
- package/build/src/version.js +1 -1
- package/package.json +7 -4
- package/build/src/telemetry/metricUtils.js +0 -15
|
@@ -30,7 +30,7 @@ export function definePageTool(definition) {
|
|
|
30
30
|
}
|
|
31
31
|
export const CLOSE_PAGE_ERROR = 'The last open page cannot be closed. It is fine to keep it open.';
|
|
32
32
|
export const pageIdSchema = {
|
|
33
|
-
pageId: zod.number().
|
|
33
|
+
pageId: zod.number().describe('Targets a specific page by ID.'),
|
|
34
34
|
};
|
|
35
35
|
export const timeoutSchema = {
|
|
36
36
|
timeout: zod
|
|
@@ -64,7 +64,7 @@ export function geolocationTransform(arg) {
|
|
|
64
64
|
if (!arg) {
|
|
65
65
|
return undefined;
|
|
66
66
|
}
|
|
67
|
-
const [latitude, longitude] = arg.split('
|
|
67
|
+
const [latitude, longitude] = arg.split(',').map(Number);
|
|
68
68
|
return {
|
|
69
69
|
latitude,
|
|
70
70
|
longitude,
|
|
@@ -7,6 +7,26 @@
|
|
|
7
7
|
import { zod, PredefinedNetworkConditions } from '../third_party/index.js';
|
|
8
8
|
import { ToolCategory } from './categories.js';
|
|
9
9
|
import { definePageTool, geolocationTransform, viewportTransform, } from './ToolDefinition.js';
|
|
10
|
+
function headerStringTransform(value) {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (value === '') {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(value);
|
|
19
|
+
if (typeof parsed !== 'object' ||
|
|
20
|
+
parsed === null ||
|
|
21
|
+
Array.isArray(parsed)) {
|
|
22
|
+
throw new Error('Headers must be a JSON object');
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`Invalid JSON for headers: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
const throttlingOptions = [
|
|
11
31
|
'Offline',
|
|
12
32
|
...Object.keys(PredefinedNetworkConditions),
|
|
@@ -33,7 +53,7 @@ export const emulate = definePageTool({
|
|
|
33
53
|
.string()
|
|
34
54
|
.optional()
|
|
35
55
|
.transform(geolocationTransform)
|
|
36
|
-
.describe('Geolocation (`<latitude
|
|
56
|
+
.describe('Geolocation (`<latitude>,<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.'),
|
|
37
57
|
userAgent: zod
|
|
38
58
|
.string()
|
|
39
59
|
.optional()
|
|
@@ -47,11 +67,17 @@ export const emulate = definePageTool({
|
|
|
47
67
|
.optional()
|
|
48
68
|
.transform(viewportTransform)
|
|
49
69
|
.describe(`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`),
|
|
70
|
+
extraHttpHeaders: zod
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.transform(headerStringTransform)
|
|
74
|
+
.describe('Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.'),
|
|
50
75
|
},
|
|
51
76
|
blockedByDialog: true,
|
|
52
|
-
handler: async (request,
|
|
77
|
+
handler: async (request, response, context) => {
|
|
53
78
|
const page = request.page;
|
|
54
79
|
await context.emulate(request.params, page.pptrPage);
|
|
80
|
+
response.appendResponseLine('Emulation configured successfully');
|
|
55
81
|
},
|
|
56
82
|
});
|
|
57
83
|
//# sourceMappingURL=emulation.js.map
|
|
@@ -21,6 +21,7 @@ export const installExtension = defineTool({
|
|
|
21
21
|
blockedByDialog: false,
|
|
22
22
|
handler: async (request, response, context) => {
|
|
23
23
|
const { path } = request.params;
|
|
24
|
+
await context.validatePath(path);
|
|
24
25
|
const id = await context.installExtension(path);
|
|
25
26
|
response.appendResponseLine(`Extension installed. Id: ${id}`);
|
|
26
27
|
},
|
|
@@ -72,6 +73,7 @@ export const reloadExtension = defineTool({
|
|
|
72
73
|
if (!extension) {
|
|
73
74
|
throw new Error(`Extension with ID ${id} not found.`);
|
|
74
75
|
}
|
|
76
|
+
await context.validatePath(extension.path);
|
|
75
77
|
await context.installExtension(extension.path);
|
|
76
78
|
response.appendResponseLine('Extension reloaded.');
|
|
77
79
|
},
|
package/build/src/tools/input.js
CHANGED
|
@@ -85,7 +85,7 @@ export const click = definePageTool({
|
|
|
85
85
|
const aXNode = request.page.getAXNodeByUid(uid);
|
|
86
86
|
const shouldSelectNativeOption = !request.params.dblClick && aXNode?.role === 'option';
|
|
87
87
|
try {
|
|
88
|
-
await request.page.waitForEventsAfterAction(async () => {
|
|
88
|
+
const result = await request.page.waitForEventsAfterAction(async () => {
|
|
89
89
|
if (shouldSelectNativeOption &&
|
|
90
90
|
(await selectNativeSelectOption(handle))) {
|
|
91
91
|
return;
|
|
@@ -97,6 +97,7 @@ export const click = definePageTool({
|
|
|
97
97
|
response.appendResponseLine(request.params.dblClick
|
|
98
98
|
? `Successfully double clicked on the element`
|
|
99
99
|
: `Successfully clicked on the element`);
|
|
100
|
+
response.attachWaitForResult(result);
|
|
100
101
|
if (request.params.includeSnapshot) {
|
|
101
102
|
response.includeSnapshot();
|
|
102
103
|
}
|
|
@@ -126,14 +127,15 @@ export const clickAt = definePageTool({
|
|
|
126
127
|
blockedByDialog: true,
|
|
127
128
|
handler: async (request, response) => {
|
|
128
129
|
const page = request.page;
|
|
129
|
-
await page.waitForEventsAfterAction(async () => {
|
|
130
|
+
const result = await page.waitForEventsAfterAction(async () => {
|
|
130
131
|
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
|
|
131
|
-
|
|
132
|
+
count: request.params.dblClick ? 2 : 1,
|
|
132
133
|
});
|
|
133
134
|
});
|
|
134
135
|
response.appendResponseLine(request.params.dblClick
|
|
135
136
|
? `Successfully double clicked at the coordinates`
|
|
136
137
|
: `Successfully clicked at the coordinates`);
|
|
138
|
+
response.attachWaitForResult(result);
|
|
137
139
|
if (request.params.includeSnapshot) {
|
|
138
140
|
response.includeSnapshot();
|
|
139
141
|
}
|
|
@@ -157,10 +159,11 @@ export const hover = definePageTool({
|
|
|
157
159
|
const uid = request.params.uid;
|
|
158
160
|
const handle = await request.page.getElementByUid(uid);
|
|
159
161
|
try {
|
|
160
|
-
await request.page.waitForEventsAfterAction(async () => {
|
|
162
|
+
const result = await request.page.waitForEventsAfterAction(async () => {
|
|
161
163
|
await handle.asLocator().hover();
|
|
162
164
|
});
|
|
163
165
|
response.appendResponseLine(`Successfully hovered over the element`);
|
|
166
|
+
response.attachWaitForResult(result);
|
|
164
167
|
if (request.params.includeSnapshot) {
|
|
165
168
|
response.includeSnapshot();
|
|
166
169
|
}
|
|
@@ -269,10 +272,11 @@ export const fill = definePageTool({
|
|
|
269
272
|
blockedByDialog: true,
|
|
270
273
|
handler: async (request, response, context) => {
|
|
271
274
|
const page = request.page;
|
|
272
|
-
await page.waitForEventsAfterAction(async () => {
|
|
275
|
+
const result = await page.waitForEventsAfterAction(async () => {
|
|
273
276
|
await fillFormElement(request.params.uid, request.params.value, context, page);
|
|
274
277
|
});
|
|
275
278
|
response.appendResponseLine(`Successfully filled out the element`);
|
|
279
|
+
response.attachWaitForResult(result);
|
|
276
280
|
if (request.params.includeSnapshot) {
|
|
277
281
|
response.includeSnapshot();
|
|
278
282
|
}
|
|
@@ -292,13 +296,14 @@ export const typeText = definePageTool({
|
|
|
292
296
|
blockedByDialog: true,
|
|
293
297
|
handler: async (request, response) => {
|
|
294
298
|
const page = request.page;
|
|
295
|
-
await page.waitForEventsAfterAction(async () => {
|
|
299
|
+
const result = await page.waitForEventsAfterAction(async () => {
|
|
296
300
|
await page.pptrPage.keyboard.type(request.params.text);
|
|
297
301
|
if (request.params.submitKey) {
|
|
298
302
|
await page.pptrPage.keyboard.press(request.params.submitKey);
|
|
299
303
|
}
|
|
300
304
|
});
|
|
301
305
|
response.appendResponseLine(`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`);
|
|
306
|
+
response.attachWaitForResult(result);
|
|
302
307
|
},
|
|
303
308
|
});
|
|
304
309
|
export const drag = definePageTool({
|
|
@@ -318,12 +323,13 @@ export const drag = definePageTool({
|
|
|
318
323
|
const fromHandle = await request.page.getElementByUid(request.params.from_uid);
|
|
319
324
|
const toHandle = await request.page.getElementByUid(request.params.to_uid);
|
|
320
325
|
try {
|
|
321
|
-
await request.page.waitForEventsAfterAction(async () => {
|
|
326
|
+
const result = await request.page.waitForEventsAfterAction(async () => {
|
|
322
327
|
await fromHandle.drag(toHandle);
|
|
323
328
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
324
329
|
await toHandle.drop(fromHandle);
|
|
325
330
|
});
|
|
326
331
|
response.appendResponseLine(`Successfully dragged an element`);
|
|
332
|
+
response.attachWaitForResult(result);
|
|
327
333
|
if (request.params.includeSnapshot) {
|
|
328
334
|
response.includeSnapshot();
|
|
329
335
|
}
|
|
@@ -357,12 +363,14 @@ export const fillForm = definePageTool({
|
|
|
357
363
|
blockedByDialog: true,
|
|
358
364
|
handler: async (request, response, context) => {
|
|
359
365
|
const page = request.page;
|
|
366
|
+
let lastResult = {};
|
|
360
367
|
for (const element of request.params.elements) {
|
|
361
|
-
await page.waitForEventsAfterAction(async () => {
|
|
368
|
+
lastResult = await page.waitForEventsAfterAction(async () => {
|
|
362
369
|
await fillFormElement(element.uid, element.value, context, page);
|
|
363
370
|
});
|
|
364
371
|
}
|
|
365
372
|
response.appendResponseLine(`Successfully filled out the form`);
|
|
373
|
+
response.attachWaitForResult(lastResult);
|
|
366
374
|
if (request.params.includeSnapshot) {
|
|
367
375
|
response.includeSnapshot();
|
|
368
376
|
}
|
|
@@ -385,7 +393,7 @@ export const uploadFile = definePageTool({
|
|
|
385
393
|
blockedByDialog: true,
|
|
386
394
|
handler: async (request, response, context) => {
|
|
387
395
|
const { uid, filePath } = request.params;
|
|
388
|
-
context.validatePath(filePath);
|
|
396
|
+
await context.validatePath(filePath);
|
|
389
397
|
const handle = (await request.page.getElementByUid(uid));
|
|
390
398
|
try {
|
|
391
399
|
try {
|
|
@@ -434,7 +442,7 @@ export const pressKey = definePageTool({
|
|
|
434
442
|
const page = request.page;
|
|
435
443
|
const tokens = parseKey(request.params.key);
|
|
436
444
|
const [key, ...modifiers] = tokens;
|
|
437
|
-
await page.waitForEventsAfterAction(async () => {
|
|
445
|
+
const result = await page.waitForEventsAfterAction(async () => {
|
|
438
446
|
for (const modifier of modifiers) {
|
|
439
447
|
await page.pptrPage.keyboard.down(modifier);
|
|
440
448
|
}
|
|
@@ -444,6 +452,7 @@ export const pressKey = definePageTool({
|
|
|
444
452
|
}
|
|
445
453
|
});
|
|
446
454
|
response.appendResponseLine(`Successfully pressed key: ${request.params.key}`);
|
|
455
|
+
response.attachWaitForResult(result);
|
|
447
456
|
if (request.params.includeSnapshot) {
|
|
448
457
|
response.includeSnapshot();
|
|
449
458
|
}
|
|
@@ -40,7 +40,7 @@ export const lighthouseAudit = definePageTool({
|
|
|
40
40
|
];
|
|
41
41
|
const formats = ['json', 'html'];
|
|
42
42
|
const { mode = 'navigation', device = 'desktop', outputDirPath, } = request.params;
|
|
43
|
-
context.validatePath(outputDirPath);
|
|
43
|
+
await context.validatePath(outputDirPath);
|
|
44
44
|
const flags = {
|
|
45
45
|
onlyCategories: categories,
|
|
46
46
|
output: formats,
|
|
@@ -7,8 +7,8 @@ import { zod } from '../third_party/index.js';
|
|
|
7
7
|
import { ensureExtension } from '../utils/files.js';
|
|
8
8
|
import { ToolCategory } from './categories.js';
|
|
9
9
|
import { definePageTool, defineTool } from './ToolDefinition.js';
|
|
10
|
-
export const
|
|
11
|
-
name: '
|
|
10
|
+
export const takeHeapSnapshot = definePageTool({
|
|
11
|
+
name: 'take_heapsnapshot',
|
|
12
12
|
description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
|
|
13
13
|
annotations: {
|
|
14
14
|
category: ToolCategory.MEMORY,
|
|
@@ -22,15 +22,15 @@ export const takeMemorySnapshot = definePageTool({
|
|
|
22
22
|
blockedByDialog: true,
|
|
23
23
|
handler: async (request, response, context) => {
|
|
24
24
|
const page = request.page;
|
|
25
|
-
context.validatePath(request.params.filePath);
|
|
25
|
+
await context.validatePath(request.params.filePath);
|
|
26
26
|
await page.pptrPage.captureHeapSnapshot({
|
|
27
27
|
path: ensureExtension(request.params.filePath, '.heapsnapshot'),
|
|
28
28
|
});
|
|
29
29
|
response.appendResponseLine(`Heap snapshot saved to ${request.params.filePath}`);
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
|
-
export const
|
|
33
|
-
name: '
|
|
32
|
+
export const getHeapSnapshotSummary = defineTool({
|
|
33
|
+
name: 'get_heapsnapshot_summary',
|
|
34
34
|
description: 'Loads a memory heapsnapshot and returns snapshot summary stats.',
|
|
35
35
|
annotations: {
|
|
36
36
|
category: ToolCategory.MEMORY,
|
|
@@ -42,14 +42,14 @@ export const exploreMemorySnapshot = defineTool({
|
|
|
42
42
|
},
|
|
43
43
|
blockedByDialog: false,
|
|
44
44
|
handler: async (request, response, context) => {
|
|
45
|
-
context.validatePath(request.params.filePath);
|
|
45
|
+
await context.validatePath(request.params.filePath);
|
|
46
46
|
const stats = await context.getHeapSnapshotStats(request.params.filePath);
|
|
47
47
|
const staticData = await context.getHeapSnapshotStaticData(request.params.filePath);
|
|
48
48
|
response.setHeapSnapshotStats(stats, staticData);
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
|
-
export const
|
|
52
|
-
name: '
|
|
51
|
+
export const getHeapSnapshotDetails = defineTool({
|
|
52
|
+
name: 'get_heapsnapshot_details',
|
|
53
53
|
description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates.',
|
|
54
54
|
annotations: {
|
|
55
55
|
category: ToolCategory.MEMORY,
|
|
@@ -69,7 +69,7 @@ export const getMemorySnapshotDetails = defineTool({
|
|
|
69
69
|
},
|
|
70
70
|
blockedByDialog: false,
|
|
71
71
|
handler: async (request, response, context) => {
|
|
72
|
-
context.validatePath(request.params.filePath);
|
|
72
|
+
await context.validatePath(request.params.filePath);
|
|
73
73
|
const aggregates = await context.getHeapSnapshotAggregates(request.params.filePath);
|
|
74
74
|
response.setHeapSnapshotAggregates(aggregates, {
|
|
75
75
|
pageIdx: request.params.pageIdx,
|
|
@@ -77,9 +77,9 @@ export const getMemorySnapshotDetails = defineTool({
|
|
|
77
77
|
});
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
-
export const
|
|
81
|
-
name: '
|
|
82
|
-
description: 'Loads a memory heapsnapshot and returns instances of a specific class with their
|
|
80
|
+
export const getHeapSnapshotClassNodes = defineTool({
|
|
81
|
+
name: 'get_heapsnapshot_class_nodes',
|
|
82
|
+
description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs.',
|
|
83
83
|
annotations: {
|
|
84
84
|
category: ToolCategory.MEMORY,
|
|
85
85
|
readOnlyHint: true,
|
|
@@ -87,20 +87,42 @@ export const getNodesByClass = defineTool({
|
|
|
87
87
|
},
|
|
88
88
|
schema: {
|
|
89
89
|
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
|
|
90
|
-
|
|
91
|
-
.number()
|
|
92
|
-
.describe('The unique UID for the class, obtained from aggregates listing.'),
|
|
90
|
+
id: zod.number().describe('The ID for the class, obtained from details.'),
|
|
93
91
|
pageIdx: zod.number().optional().describe('The page index for pagination.'),
|
|
94
92
|
pageSize: zod.number().optional().describe('The page size for pagination.'),
|
|
95
93
|
},
|
|
96
94
|
blockedByDialog: false,
|
|
97
95
|
handler: async (request, response, context) => {
|
|
98
|
-
context.validatePath(request.params.filePath);
|
|
99
|
-
const nodes = await context.
|
|
96
|
+
await context.validatePath(request.params.filePath);
|
|
97
|
+
const nodes = await context.getHeapSnapshotNodesById(request.params.filePath, request.params.id);
|
|
100
98
|
response.setHeapSnapshotNodes(nodes, {
|
|
101
99
|
pageIdx: request.params.pageIdx,
|
|
102
100
|
pageSize: request.params.pageSize,
|
|
103
101
|
});
|
|
104
102
|
},
|
|
105
103
|
});
|
|
104
|
+
export const getHeapSnapshotRetainers = defineTool({
|
|
105
|
+
name: 'get_heapsnapshot_retainers',
|
|
106
|
+
description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID.',
|
|
107
|
+
annotations: {
|
|
108
|
+
category: ToolCategory.MEMORY,
|
|
109
|
+
readOnlyHint: true,
|
|
110
|
+
conditions: ['experimentalMemory'],
|
|
111
|
+
},
|
|
112
|
+
blockedByDialog: false,
|
|
113
|
+
schema: {
|
|
114
|
+
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
|
|
115
|
+
nodeId: zod.number().describe('The node ID to get retainers for.'),
|
|
116
|
+
pageIdx: zod.number().optional().describe('The page index for pagination.'),
|
|
117
|
+
pageSize: zod.number().optional().describe('The page size for pagination.'),
|
|
118
|
+
},
|
|
119
|
+
handler: async (request, response, context) => {
|
|
120
|
+
await context.validatePath(request.params.filePath);
|
|
121
|
+
const retainers = await context.getHeapSnapshotRetainers(request.params.filePath, request.params.nodeId);
|
|
122
|
+
response.setHeapSnapshotNodes(retainers, {
|
|
123
|
+
pageIdx: request.params.pageIdx,
|
|
124
|
+
pageSize: request.params.pageSize,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
});
|
|
106
128
|
//# sourceMappingURL=memory.js.map
|
|
@@ -96,8 +96,8 @@ export const getNetworkRequest = definePageTool({
|
|
|
96
96
|
},
|
|
97
97
|
blockedByDialog: true,
|
|
98
98
|
handler: async (request, response, context) => {
|
|
99
|
-
context.validatePath(request.params.requestFilePath);
|
|
100
|
-
context.validatePath(request.params.responseFilePath);
|
|
99
|
+
await context.validatePath(request.params.requestFilePath);
|
|
100
|
+
await context.validatePath(request.params.responseFilePath);
|
|
101
101
|
if (request.params.reqid) {
|
|
102
102
|
response.attachNetworkRequest(request.params.reqid, {
|
|
103
103
|
requestFilePath: request.params.requestFilePath,
|
|
@@ -33,7 +33,7 @@ export const startTrace = definePageTool({
|
|
|
33
33
|
},
|
|
34
34
|
blockedByDialog: true,
|
|
35
35
|
handler: async (request, response, context) => {
|
|
36
|
-
context.validatePath(request.params.filePath);
|
|
36
|
+
await context.validatePath(request.params.filePath);
|
|
37
37
|
if (context.isRunningPerformanceTrace()) {
|
|
38
38
|
response.appendResponseLine('Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.');
|
|
39
39
|
return;
|
|
@@ -78,7 +78,7 @@ export const startTrace = definePageTool({
|
|
|
78
78
|
}
|
|
79
79
|
if (request.params.autoStop) {
|
|
80
80
|
await new Promise(resolve => setTimeout(resolve, 5_000));
|
|
81
|
-
await stopTracingAndAppendOutput(page
|
|
81
|
+
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
|
|
82
82
|
}
|
|
83
83
|
else {
|
|
84
84
|
response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
|
|
@@ -97,12 +97,12 @@ export const stopTrace = definePageTool({
|
|
|
97
97
|
},
|
|
98
98
|
blockedByDialog: true,
|
|
99
99
|
handler: async (request, response, context) => {
|
|
100
|
-
context.validatePath(request.params.filePath);
|
|
100
|
+
await context.validatePath(request.params.filePath);
|
|
101
101
|
if (!context.isRunningPerformanceTrace()) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
const page = request.page;
|
|
105
|
-
await stopTracingAndAppendOutput(page
|
|
105
|
+
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
|
|
106
106
|
},
|
|
107
107
|
});
|
|
108
108
|
export const analyzeInsight = definePageTool({
|
|
@@ -132,7 +132,7 @@ export const analyzeInsight = definePageTool({
|
|
|
132
132
|
});
|
|
133
133
|
async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
134
134
|
try {
|
|
135
|
-
const traceEventsBuffer = await page.tracing.stop();
|
|
135
|
+
const traceEventsBuffer = await page.pptrPage.tracing.stop();
|
|
136
136
|
if (filePath && traceEventsBuffer) {
|
|
137
137
|
let dataToWrite = traceEventsBuffer;
|
|
138
138
|
if (filePath.endsWith('.gz')) {
|
|
@@ -150,7 +150,10 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
|
150
150
|
const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.json.gz' : '.json');
|
|
151
151
|
response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
|
|
152
152
|
}
|
|
153
|
-
const result = await parseRawTraceBuffer(traceEventsBuffer
|
|
153
|
+
const result = await parseRawTraceBuffer(traceEventsBuffer, {
|
|
154
|
+
cpuThrottling: page.cpuThrottlingRate,
|
|
155
|
+
networkThrottling: page.networkConditions ?? undefined,
|
|
156
|
+
});
|
|
154
157
|
response.appendResponseLine('The performance trace has been stopped.');
|
|
155
158
|
if (traceResultIsSuccess(result)) {
|
|
156
159
|
if (context.isCruxEnabled()) {
|
|
@@ -31,7 +31,7 @@ export const startScreencast = definePageTool(args => ({
|
|
|
31
31
|
},
|
|
32
32
|
blockedByDialog: false,
|
|
33
33
|
handler: async (request, response, context) => {
|
|
34
|
-
context.validatePath(request.params.filePath);
|
|
34
|
+
await context.validatePath(request.params.filePath);
|
|
35
35
|
if (context.getScreenRecorder() !== null) {
|
|
36
36
|
response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
|
|
37
37
|
return;
|
|
@@ -40,7 +40,7 @@ export const screenshot = definePageTool({
|
|
|
40
40
|
},
|
|
41
41
|
blockedByDialog: true,
|
|
42
42
|
handler: async (request, response, context) => {
|
|
43
|
-
context.validatePath(request.params.filePath);
|
|
43
|
+
await context.validatePath(request.params.filePath);
|
|
44
44
|
if (request.params.uid && request.params.fullPage) {
|
|
45
45
|
throw new Error('Providing both "uid" and "fullPage" is not allowed.');
|
|
46
46
|
}
|
|
@@ -32,6 +32,10 @@ Example with arguments: \`(el) => {
|
|
|
32
32
|
.describe('The uid of an element on the page from the page content snapshot'))
|
|
33
33
|
.optional()
|
|
34
34
|
.describe(`An optional list of arguments to pass to the function.`),
|
|
35
|
+
filePath: zod
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.'),
|
|
35
39
|
dialogAction: zod
|
|
36
40
|
.string()
|
|
37
41
|
.optional()
|
|
@@ -48,7 +52,8 @@ Example with arguments: \`(el) => {
|
|
|
48
52
|
},
|
|
49
53
|
blockedByDialog: true,
|
|
50
54
|
handler: async (request, response, context) => {
|
|
51
|
-
const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, } = request.params;
|
|
55
|
+
const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, filePath, } = request.params;
|
|
56
|
+
await context.validatePath(filePath);
|
|
52
57
|
if (cliArgs?.categoryExtensions && serviceWorkerId) {
|
|
53
58
|
if (uidArgs && uidArgs.length > 0) {
|
|
54
59
|
throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
|
|
@@ -57,9 +62,15 @@ Example with arguments: \`(el) => {
|
|
|
57
62
|
throw new Error('specify either a pageId or a serviceWorkerId.');
|
|
58
63
|
}
|
|
59
64
|
const worker = await getWebWorker(context, serviceWorkerId);
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const result = await context
|
|
66
|
+
.getSelectedMcpPage()
|
|
67
|
+
.waitForEventsAfterAction(async () => {
|
|
68
|
+
await performEvaluation(worker, fnString, [], response, {
|
|
69
|
+
filePath,
|
|
70
|
+
context,
|
|
71
|
+
});
|
|
62
72
|
}, { handleDialog: dialogAction ?? 'accept' });
|
|
73
|
+
response.attachWaitForResult(result);
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
76
|
const mcpPage = cliArgs?.experimentalPageIdRouting
|
|
@@ -75,9 +86,13 @@ Example with arguments: \`(el) => {
|
|
|
75
86
|
args.push(handle);
|
|
76
87
|
}
|
|
77
88
|
const evaluatable = await getPageOrFrame(page, frames);
|
|
78
|
-
await mcpPage.waitForEventsAfterAction(async () => {
|
|
79
|
-
await performEvaluation(evaluatable, fnString, args, response
|
|
89
|
+
const result = await mcpPage.waitForEventsAfterAction(async () => {
|
|
90
|
+
await performEvaluation(evaluatable, fnString, args, response, {
|
|
91
|
+
filePath,
|
|
92
|
+
context,
|
|
93
|
+
});
|
|
80
94
|
}, { handleDialog: dialogAction ?? 'accept' });
|
|
95
|
+
response.attachWaitForResult(result);
|
|
81
96
|
}
|
|
82
97
|
finally {
|
|
83
98
|
void Promise.allSettled(args.map(arg => arg.dispose()));
|
|
@@ -85,17 +100,24 @@ Example with arguments: \`(el) => {
|
|
|
85
100
|
},
|
|
86
101
|
};
|
|
87
102
|
});
|
|
88
|
-
const performEvaluation = async (evaluatable, fnString, args, response) => {
|
|
103
|
+
const performEvaluation = async (evaluatable, fnString, args, response, options) => {
|
|
89
104
|
const fn = await evaluatable.evaluateHandle(`(${fnString})`);
|
|
90
105
|
try {
|
|
91
106
|
const result = await evaluatable.evaluate(async (fn, ...args) => {
|
|
92
107
|
// @ts-expect-error no types for function fn
|
|
93
108
|
return JSON.stringify(await fn(...args));
|
|
94
109
|
}, fn, ...args);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
if (options?.filePath) {
|
|
111
|
+
const data = new TextEncoder().encode(result ?? 'undefined');
|
|
112
|
+
const { filename } = await options.context.saveFile(data, options.filePath, '.json');
|
|
113
|
+
response.appendResponseLine(`Script ran on page. Output saved to ${filename}.`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
response.appendResponseLine('Script ran on page and returned:');
|
|
117
|
+
response.appendResponseLine('```json');
|
|
118
|
+
response.appendResponseLine(`${result}`);
|
|
119
|
+
response.appendResponseLine('```');
|
|
120
|
+
}
|
|
99
121
|
}
|
|
100
122
|
finally {
|
|
101
123
|
void fn.dispose();
|
|
@@ -28,7 +28,7 @@ in the DevTools Elements panel (if any).`,
|
|
|
28
28
|
},
|
|
29
29
|
blockedByDialog: true,
|
|
30
30
|
handler: async (request, response, context) => {
|
|
31
|
-
context.validatePath(request.params.filePath);
|
|
31
|
+
await context.validatePath(request.params.filePath);
|
|
32
32
|
response.includeSnapshot({
|
|
33
33
|
verbose: request.params.verbose ?? false,
|
|
34
34
|
filePath: request.params.filePath,
|
|
@@ -9,7 +9,7 @@ const engine = DevTools.TraceEngine.TraceModel.Model.createWithAllHandlers();
|
|
|
9
9
|
export function traceResultIsSuccess(x) {
|
|
10
10
|
return 'parsedTrace' in x;
|
|
11
11
|
}
|
|
12
|
-
export async function parseRawTraceBuffer(buffer) {
|
|
12
|
+
export async function parseRawTraceBuffer(buffer, metadata) {
|
|
13
13
|
engine.resetProcessor();
|
|
14
14
|
if (!buffer) {
|
|
15
15
|
return {
|
|
@@ -25,7 +25,7 @@ export async function parseRawTraceBuffer(buffer) {
|
|
|
25
25
|
try {
|
|
26
26
|
const data = JSON.parse(asString);
|
|
27
27
|
const events = Array.isArray(data) ? data : data.traceEvents;
|
|
28
|
-
await engine.parse(events);
|
|
28
|
+
await engine.parse(events, { metadata });
|
|
29
29
|
const parsedTrace = engine.parsedTrace();
|
|
30
30
|
if (!parsedTrace) {
|
|
31
31
|
return {
|
package/build/src/utils/files.js
CHANGED
|
@@ -15,4 +15,47 @@ export function ensureExtension(filepath, extension) {
|
|
|
15
15
|
const ext = path.extname(filepath);
|
|
16
16
|
return filepath.slice(0, filepath.length - ext.length) + extension;
|
|
17
17
|
}
|
|
18
|
+
export async function resolveCanonicalPath(filePath) {
|
|
19
|
+
const absolutePath = path.resolve(filePath);
|
|
20
|
+
try {
|
|
21
|
+
// Get the true canonical path, resolving all symlinks.
|
|
22
|
+
return await fs.realpath(absolutePath);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err &&
|
|
26
|
+
typeof err === 'object' &&
|
|
27
|
+
'code' in err &&
|
|
28
|
+
err.code === 'ENOENT') {
|
|
29
|
+
// Find the nearest existing ancestor directory on the filesystem.
|
|
30
|
+
let current = absolutePath;
|
|
31
|
+
const missingSegments = [];
|
|
32
|
+
while (true) {
|
|
33
|
+
const parent = path.dirname(current);
|
|
34
|
+
if (parent === current) {
|
|
35
|
+
// Reached root directory but still couldn't resolve anything.
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const canonicalParent = await fs.realpath(parent);
|
|
40
|
+
return path.join(canonicalParent, path.basename(current), ...missingSegments);
|
|
41
|
+
}
|
|
42
|
+
catch (parentErr) {
|
|
43
|
+
if (parentErr &&
|
|
44
|
+
typeof parentErr === 'object' &&
|
|
45
|
+
'code' in parentErr &&
|
|
46
|
+
parentErr.code === 'ENOENT') {
|
|
47
|
+
missingSegments.unshift(path.basename(current));
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
throw parentErr;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
18
61
|
//# sourceMappingURL=files.js.map
|
package/build/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for Chrome DevTools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@types/yargs": "^17.0.33",
|
|
63
63
|
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
|
64
64
|
"@typescript-eslint/parser": "^8.43.0",
|
|
65
|
-
"chrome-devtools-frontend": "1.0.
|
|
65
|
+
"chrome-devtools-frontend": "1.0.1632065",
|
|
66
66
|
"core-js": "3.49.0",
|
|
67
67
|
"debug": "4.4.3",
|
|
68
68
|
"eslint": "^9.35.0",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"globals": "^17.0.0",
|
|
72
72
|
"lighthouse": "13.3.0",
|
|
73
73
|
"prettier": "^3.6.2",
|
|
74
|
-
"puppeteer": "
|
|
75
|
-
"rollup": "4.60.
|
|
74
|
+
"puppeteer": "25.1.0",
|
|
75
|
+
"rollup": "4.60.4",
|
|
76
76
|
"rollup-plugin-cleanup": "^3.2.1",
|
|
77
77
|
"rollup-plugin-license": "^3.6.0",
|
|
78
78
|
"semver": "^7.7.4",
|
|
@@ -84,5 +84,8 @@
|
|
|
84
84
|
},
|
|
85
85
|
"engines": {
|
|
86
86
|
"node": "^20.19.0 || ^22.12.0 || >=23"
|
|
87
|
+
},
|
|
88
|
+
"overrides": {
|
|
89
|
+
"puppeteer-core": "$puppeteer"
|
|
87
90
|
}
|
|
88
91
|
}
|