chrome-devtools-mcp 0.13.0 → 0.15.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 +28 -2
- package/build/src/DevtoolsUtils.js +59 -42
- package/build/src/McpContext.js +106 -13
- package/build/src/McpResponse.js +213 -132
- package/build/src/browser.js +1 -0
- package/build/src/cli.js +29 -6
- package/build/src/formatters/ConsoleFormatter.js +153 -0
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/formatters/NetworkFormatter.js +226 -0
- package/build/src/formatters/SnapshotFormatter.js +6 -0
- package/build/src/logger.js +9 -0
- package/build/src/main.js +16 -3
- package/build/src/telemetry/clearcut-logger.js +86 -12
- package/build/src/telemetry/flag-utils.js +1 -1
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +6 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
- package/build/src/telemetry/watchdog/main.js +127 -0
- package/build/src/telemetry/watchdog-client.js +60 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +6 -5
- package/build/src/third_party/devtools-formatter-worker.js +15451 -0
- package/build/src/third_party/index.js +1356 -282
- package/build/src/tools/categories.js +2 -0
- package/build/src/tools/emulation.js +83 -1
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/input.js +58 -9
- package/build/src/tools/network.js +17 -3
- package/build/src/tools/pages.js +91 -46
- package/build/src/tools/performance.js +6 -20
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/package.json +9 -8
- package/build/src/formatters/consoleFormatter.js +0 -156
- package/build/src/formatters/networkFormatter.js +0 -77
- package/build/src/telemetry/clearcut-sender.js +0 -11
- package/build/src/third_party/devtools.js +0 -6
|
@@ -11,6 +11,7 @@ export var ToolCategory;
|
|
|
11
11
|
ToolCategory["PERFORMANCE"] = "performance";
|
|
12
12
|
ToolCategory["NETWORK"] = "network";
|
|
13
13
|
ToolCategory["DEBUGGING"] = "debugging";
|
|
14
|
+
ToolCategory["EXTENSIONS"] = "extensions";
|
|
14
15
|
})(ToolCategory || (ToolCategory = {}));
|
|
15
16
|
export const labels = {
|
|
16
17
|
[ToolCategory.INPUT]: 'Input automation',
|
|
@@ -19,4 +20,5 @@ export const labels = {
|
|
|
19
20
|
[ToolCategory.PERFORMANCE]: 'Performance',
|
|
20
21
|
[ToolCategory.NETWORK]: 'Network',
|
|
21
22
|
[ToolCategory.DEBUGGING]: 'Debugging',
|
|
23
|
+
[ToolCategory.EXTENSIONS]: 'Extensions',
|
|
22
24
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @license
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*
|
|
5
6
|
*/
|
|
6
7
|
import { zod, PredefinedNetworkConditions } from '../third_party/index.js';
|
|
7
8
|
import { ToolCategory } from './categories.js';
|
|
@@ -45,10 +46,44 @@ export const emulate = defineTool({
|
|
|
45
46
|
.nullable()
|
|
46
47
|
.optional()
|
|
47
48
|
.describe('Geolocation to emulate. Set to null to clear the geolocation override.'),
|
|
49
|
+
userAgent: zod
|
|
50
|
+
.string()
|
|
51
|
+
.nullable()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('User agent to emulate. Set to null to clear the user agent override.'),
|
|
54
|
+
colorScheme: zod
|
|
55
|
+
.enum(['dark', 'light', 'auto'])
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Emulate the dark or the light mode. Set to "auto" to reset to the default.'),
|
|
58
|
+
viewport: zod
|
|
59
|
+
.object({
|
|
60
|
+
width: zod.number().int().min(0).describe('Page width in pixels.'),
|
|
61
|
+
height: zod.number().int().min(0).describe('Page height in pixels.'),
|
|
62
|
+
deviceScaleFactor: zod
|
|
63
|
+
.number()
|
|
64
|
+
.min(0)
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Specify device scale factor (can be thought of as dpr).'),
|
|
67
|
+
isMobile: zod
|
|
68
|
+
.boolean()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('Whether the meta viewport tag is taken into account. Defaults to false.'),
|
|
71
|
+
hasTouch: zod
|
|
72
|
+
.boolean()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Specifies if viewport supports touch events. This should be set to true for mobile devices.'),
|
|
75
|
+
isLandscape: zod
|
|
76
|
+
.boolean()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Specifies if viewport is in landscape mode. Defaults to false.'),
|
|
79
|
+
})
|
|
80
|
+
.nullable()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Viewport to emulate. Set to null to reset to the default viewport.'),
|
|
48
83
|
},
|
|
49
84
|
handler: async (request, _response, context) => {
|
|
50
85
|
const page = context.getSelectedPage();
|
|
51
|
-
const { networkConditions, cpuThrottlingRate, geolocation } = request.params;
|
|
86
|
+
const { networkConditions, cpuThrottlingRate, geolocation, userAgent, viewport, } = request.params;
|
|
52
87
|
if (networkConditions) {
|
|
53
88
|
if (networkConditions === 'No emulation') {
|
|
54
89
|
await page.emulateNetworkConditions(null);
|
|
@@ -83,5 +118,52 @@ export const emulate = defineTool({
|
|
|
83
118
|
context.setGeolocation(geolocation);
|
|
84
119
|
}
|
|
85
120
|
}
|
|
121
|
+
if (userAgent !== undefined) {
|
|
122
|
+
if (userAgent === null) {
|
|
123
|
+
await page.setUserAgent({
|
|
124
|
+
userAgent: undefined,
|
|
125
|
+
});
|
|
126
|
+
context.setUserAgent(null);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await page.setUserAgent({
|
|
130
|
+
userAgent,
|
|
131
|
+
});
|
|
132
|
+
context.setUserAgent(userAgent);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (request.params.colorScheme) {
|
|
136
|
+
if (request.params.colorScheme === 'auto') {
|
|
137
|
+
await page.emulateMediaFeatures([
|
|
138
|
+
{ name: 'prefers-color-scheme', value: '' },
|
|
139
|
+
]);
|
|
140
|
+
context.setColorScheme(null);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await page.emulateMediaFeatures([
|
|
144
|
+
{
|
|
145
|
+
name: 'prefers-color-scheme',
|
|
146
|
+
value: request.params.colorScheme,
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
context.setColorScheme(request.params.colorScheme);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (viewport !== undefined) {
|
|
153
|
+
if (viewport === null) {
|
|
154
|
+
await page.setViewport(null);
|
|
155
|
+
context.setViewport(null);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const defaults = {
|
|
159
|
+
deviceScaleFactor: 1,
|
|
160
|
+
isMobile: false,
|
|
161
|
+
hasTouch: false,
|
|
162
|
+
isLandscape: false,
|
|
163
|
+
};
|
|
164
|
+
await page.setViewport({ ...defaults, ...viewport });
|
|
165
|
+
context.setViewport({ ...defaults, ...viewport });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
86
168
|
},
|
|
87
169
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { zod } from '../third_party/index.js';
|
|
7
|
+
import { ToolCategory } from './categories.js';
|
|
8
|
+
import { defineTool } from './ToolDefinition.js';
|
|
9
|
+
const EXTENSIONS_CONDITION = 'experimentalExtensionSupport';
|
|
10
|
+
export const installExtension = defineTool({
|
|
11
|
+
name: 'install_extension',
|
|
12
|
+
description: 'Installs a Chrome extension from the given path.',
|
|
13
|
+
annotations: {
|
|
14
|
+
category: ToolCategory.EXTENSIONS,
|
|
15
|
+
readOnlyHint: false,
|
|
16
|
+
conditions: [EXTENSIONS_CONDITION],
|
|
17
|
+
},
|
|
18
|
+
schema: {
|
|
19
|
+
path: zod
|
|
20
|
+
.string()
|
|
21
|
+
.describe('Absolute path to the unpacked extension folder.'),
|
|
22
|
+
},
|
|
23
|
+
handler: async (request, response, context) => {
|
|
24
|
+
const { path } = request.params;
|
|
25
|
+
const id = await context.installExtension(path);
|
|
26
|
+
response.appendResponseLine(`Extension installed. Id: ${id}`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
export const uninstallExtension = defineTool({
|
|
30
|
+
name: 'uninstall_extension',
|
|
31
|
+
description: 'Uninstalls a Chrome extension by its ID.',
|
|
32
|
+
annotations: {
|
|
33
|
+
category: ToolCategory.EXTENSIONS,
|
|
34
|
+
readOnlyHint: false,
|
|
35
|
+
conditions: [EXTENSIONS_CONDITION],
|
|
36
|
+
},
|
|
37
|
+
schema: {
|
|
38
|
+
id: zod.string().describe('ID of the extension to uninstall.'),
|
|
39
|
+
},
|
|
40
|
+
handler: async (request, response, context) => {
|
|
41
|
+
const { id } = request.params;
|
|
42
|
+
await context.uninstallExtension(id);
|
|
43
|
+
response.appendResponseLine(`Extension uninstalled. Id: ${id}`);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
export const listExtensions = defineTool({
|
|
47
|
+
name: 'list_extensions',
|
|
48
|
+
description: 'Lists all extensions via this server, including their name, ID, version, and enabled status.',
|
|
49
|
+
annotations: {
|
|
50
|
+
category: ToolCategory.EXTENSIONS,
|
|
51
|
+
readOnlyHint: true,
|
|
52
|
+
conditions: [EXTENSIONS_CONDITION],
|
|
53
|
+
},
|
|
54
|
+
schema: {},
|
|
55
|
+
handler: async (_request, response, _context) => {
|
|
56
|
+
response.setListExtensions();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
export const reloadExtension = defineTool({
|
|
60
|
+
name: 'reload_extension',
|
|
61
|
+
description: 'Reloads an unpacked Chrome extension by its ID.',
|
|
62
|
+
annotations: {
|
|
63
|
+
category: ToolCategory.EXTENSIONS,
|
|
64
|
+
readOnlyHint: false,
|
|
65
|
+
conditions: [EXTENSIONS_CONDITION],
|
|
66
|
+
},
|
|
67
|
+
schema: {
|
|
68
|
+
id: zod.string().describe('ID of the extension to reload.'),
|
|
69
|
+
},
|
|
70
|
+
handler: async (request, response, context) => {
|
|
71
|
+
const { id } = request.params;
|
|
72
|
+
const extension = context.getExtension(id);
|
|
73
|
+
if (!extension) {
|
|
74
|
+
throw new Error(`Extension with ID ${id} not found.`);
|
|
75
|
+
}
|
|
76
|
+
await context.installExtension(extension.path);
|
|
77
|
+
response.appendResponseLine('Extension reloaded.');
|
|
78
|
+
},
|
|
79
|
+
});
|
package/build/src/tools/input.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { logger } from '../logger.js';
|
|
6
7
|
import { zod } from '../third_party/index.js';
|
|
7
8
|
import { parseKey } from '../utils/keyboard.js';
|
|
8
9
|
import { ToolCategory } from './categories.js';
|
|
@@ -11,6 +12,16 @@ const dblClickSchema = zod
|
|
|
11
12
|
.boolean()
|
|
12
13
|
.optional()
|
|
13
14
|
.describe('Set to true for double clicks. Default is false.');
|
|
15
|
+
const includeSnapshotSchema = zod
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Whether to include a snapshot in the response. Default is false.');
|
|
19
|
+
function handleActionError(error, uid) {
|
|
20
|
+
logger('failed to act using a locator', error);
|
|
21
|
+
throw new Error(`Failed to interact with the element with uid ${uid}. The element did not become interactive within the configured timeout.`, {
|
|
22
|
+
cause: error,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
14
25
|
export const click = defineTool({
|
|
15
26
|
name: 'click',
|
|
16
27
|
description: `Clicks on the provided element`,
|
|
@@ -23,6 +34,7 @@ export const click = defineTool({
|
|
|
23
34
|
.string()
|
|
24
35
|
.describe('The uid of an element on the page from the page content snapshot'),
|
|
25
36
|
dblClick: dblClickSchema,
|
|
37
|
+
includeSnapshot: includeSnapshotSchema,
|
|
26
38
|
},
|
|
27
39
|
handler: async (request, response, context) => {
|
|
28
40
|
const uid = request.params.uid;
|
|
@@ -36,7 +48,12 @@ export const click = defineTool({
|
|
|
36
48
|
response.appendResponseLine(request.params.dblClick
|
|
37
49
|
? `Successfully double clicked on the element`
|
|
38
50
|
: `Successfully clicked on the element`);
|
|
39
|
-
|
|
51
|
+
if (request.params.includeSnapshot) {
|
|
52
|
+
response.includeSnapshot();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
handleActionError(error, uid);
|
|
40
57
|
}
|
|
41
58
|
finally {
|
|
42
59
|
void handle.dispose();
|
|
@@ -55,6 +72,7 @@ export const clickAt = defineTool({
|
|
|
55
72
|
x: zod.number().describe('The x coordinate'),
|
|
56
73
|
y: zod.number().describe('The y coordinate'),
|
|
57
74
|
dblClick: dblClickSchema,
|
|
75
|
+
includeSnapshot: includeSnapshotSchema,
|
|
58
76
|
},
|
|
59
77
|
handler: async (request, response, context) => {
|
|
60
78
|
const page = context.getSelectedPage();
|
|
@@ -66,7 +84,9 @@ export const clickAt = defineTool({
|
|
|
66
84
|
response.appendResponseLine(request.params.dblClick
|
|
67
85
|
? `Successfully double clicked at the coordinates`
|
|
68
86
|
: `Successfully clicked at the coordinates`);
|
|
69
|
-
|
|
87
|
+
if (request.params.includeSnapshot) {
|
|
88
|
+
response.includeSnapshot();
|
|
89
|
+
}
|
|
70
90
|
},
|
|
71
91
|
});
|
|
72
92
|
export const hover = defineTool({
|
|
@@ -80,6 +100,7 @@ export const hover = defineTool({
|
|
|
80
100
|
uid: zod
|
|
81
101
|
.string()
|
|
82
102
|
.describe('The uid of an element on the page from the page content snapshot'),
|
|
103
|
+
includeSnapshot: includeSnapshotSchema,
|
|
83
104
|
},
|
|
84
105
|
handler: async (request, response, context) => {
|
|
85
106
|
const uid = request.params.uid;
|
|
@@ -89,7 +110,12 @@ export const hover = defineTool({
|
|
|
89
110
|
await handle.asLocator().hover();
|
|
90
111
|
});
|
|
91
112
|
response.appendResponseLine(`Successfully hovered over the element`);
|
|
92
|
-
|
|
113
|
+
if (request.params.includeSnapshot) {
|
|
114
|
+
response.includeSnapshot();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
handleActionError(error, uid);
|
|
93
119
|
}
|
|
94
120
|
finally {
|
|
95
121
|
void handle.dispose();
|
|
@@ -138,9 +164,16 @@ async function fillFormElement(uid, value, context) {
|
|
|
138
164
|
await selectOption(handle, aXNode, value);
|
|
139
165
|
}
|
|
140
166
|
else {
|
|
141
|
-
|
|
167
|
+
// Increase timeout for longer input values.
|
|
168
|
+
const timeoutPerChar = 10; // ms
|
|
169
|
+
const fillTimeout = context.getSelectedPage().getDefaultTimeout() +
|
|
170
|
+
value.length * timeoutPerChar;
|
|
171
|
+
await handle.asLocator().setTimeout(fillTimeout).fill(value);
|
|
142
172
|
}
|
|
143
173
|
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
handleActionError(error, uid);
|
|
176
|
+
}
|
|
144
177
|
finally {
|
|
145
178
|
void handle.dispose();
|
|
146
179
|
}
|
|
@@ -157,13 +190,17 @@ export const fill = defineTool({
|
|
|
157
190
|
.string()
|
|
158
191
|
.describe('The uid of an element on the page from the page content snapshot'),
|
|
159
192
|
value: zod.string().describe('The value to fill in'),
|
|
193
|
+
includeSnapshot: includeSnapshotSchema,
|
|
160
194
|
},
|
|
161
195
|
handler: async (request, response, context) => {
|
|
162
196
|
await context.waitForEventsAfterAction(async () => {
|
|
197
|
+
await context.getSelectedPage().keyboard.type(request.params.value);
|
|
163
198
|
await fillFormElement(request.params.uid, request.params.value, context);
|
|
164
199
|
});
|
|
165
200
|
response.appendResponseLine(`Successfully filled out the element`);
|
|
166
|
-
|
|
201
|
+
if (request.params.includeSnapshot) {
|
|
202
|
+
response.includeSnapshot();
|
|
203
|
+
}
|
|
167
204
|
},
|
|
168
205
|
});
|
|
169
206
|
export const drag = defineTool({
|
|
@@ -176,6 +213,7 @@ export const drag = defineTool({
|
|
|
176
213
|
schema: {
|
|
177
214
|
from_uid: zod.string().describe('The uid of the element to drag'),
|
|
178
215
|
to_uid: zod.string().describe('The uid of the element to drop into'),
|
|
216
|
+
includeSnapshot: includeSnapshotSchema,
|
|
179
217
|
},
|
|
180
218
|
handler: async (request, response, context) => {
|
|
181
219
|
const fromHandle = await context.getElementByUid(request.params.from_uid);
|
|
@@ -187,7 +225,9 @@ export const drag = defineTool({
|
|
|
187
225
|
await toHandle.drop(fromHandle);
|
|
188
226
|
});
|
|
189
227
|
response.appendResponseLine(`Successfully dragged an element`);
|
|
190
|
-
|
|
228
|
+
if (request.params.includeSnapshot) {
|
|
229
|
+
response.includeSnapshot();
|
|
230
|
+
}
|
|
191
231
|
}
|
|
192
232
|
finally {
|
|
193
233
|
void fromHandle.dispose();
|
|
@@ -209,6 +249,7 @@ export const fillForm = defineTool({
|
|
|
209
249
|
value: zod.string().describe('Value for the element'),
|
|
210
250
|
}))
|
|
211
251
|
.describe('Elements from snapshot to fill out.'),
|
|
252
|
+
includeSnapshot: includeSnapshotSchema,
|
|
212
253
|
},
|
|
213
254
|
handler: async (request, response, context) => {
|
|
214
255
|
for (const element of request.params.elements) {
|
|
@@ -217,7 +258,9 @@ export const fillForm = defineTool({
|
|
|
217
258
|
});
|
|
218
259
|
}
|
|
219
260
|
response.appendResponseLine(`Successfully filled out the form`);
|
|
220
|
-
|
|
261
|
+
if (request.params.includeSnapshot) {
|
|
262
|
+
response.includeSnapshot();
|
|
263
|
+
}
|
|
221
264
|
},
|
|
222
265
|
});
|
|
223
266
|
export const uploadFile = defineTool({
|
|
@@ -232,6 +275,7 @@ export const uploadFile = defineTool({
|
|
|
232
275
|
.string()
|
|
233
276
|
.describe('The uid of the file input element or an element that will open file chooser on the page from the page content snapshot'),
|
|
234
277
|
filePath: zod.string().describe('The local path of the file to upload'),
|
|
278
|
+
includeSnapshot: includeSnapshotSchema,
|
|
235
279
|
},
|
|
236
280
|
handler: async (request, response, context) => {
|
|
237
281
|
const { uid, filePath } = request.params;
|
|
@@ -256,7 +300,9 @@ export const uploadFile = defineTool({
|
|
|
256
300
|
throw new Error(`Failed to upload file. The element could not accept the file directly, and clicking it did not trigger a file chooser.`);
|
|
257
301
|
}
|
|
258
302
|
}
|
|
259
|
-
|
|
303
|
+
if (request.params.includeSnapshot) {
|
|
304
|
+
response.includeSnapshot();
|
|
305
|
+
}
|
|
260
306
|
response.appendResponseLine(`File uploaded from ${filePath}.`);
|
|
261
307
|
}
|
|
262
308
|
finally {
|
|
@@ -275,6 +321,7 @@ export const pressKey = defineTool({
|
|
|
275
321
|
key: zod
|
|
276
322
|
.string()
|
|
277
323
|
.describe('A key or a combination (e.g., "Enter", "Control+A", "Control++", "Control+Shift+R"). Modifiers: Control, Shift, Alt, Meta'),
|
|
324
|
+
includeSnapshot: includeSnapshotSchema,
|
|
278
325
|
},
|
|
279
326
|
handler: async (request, response, context) => {
|
|
280
327
|
const page = context.getSelectedPage();
|
|
@@ -290,6 +337,8 @@ export const pressKey = defineTool({
|
|
|
290
337
|
}
|
|
291
338
|
});
|
|
292
339
|
response.appendResponseLine(`Successfully pressed key: ${request.params.key}`);
|
|
293
|
-
|
|
340
|
+
if (request.params.includeSnapshot) {
|
|
341
|
+
response.includeSnapshot();
|
|
342
|
+
}
|
|
294
343
|
},
|
|
295
344
|
});
|
|
@@ -77,17 +77,28 @@ export const getNetworkRequest = defineTool({
|
|
|
77
77
|
description: `Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.`,
|
|
78
78
|
annotations: {
|
|
79
79
|
category: ToolCategory.NETWORK,
|
|
80
|
-
readOnlyHint:
|
|
80
|
+
readOnlyHint: false,
|
|
81
81
|
},
|
|
82
82
|
schema: {
|
|
83
83
|
reqid: zod
|
|
84
84
|
.number()
|
|
85
85
|
.optional()
|
|
86
86
|
.describe('The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.'),
|
|
87
|
+
requestFilePath: zod
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('The absolute or relative path to save the request body to. If omitted, the body is returned inline.'),
|
|
91
|
+
responseFilePath: zod
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('The absolute or relative path to save the response body to. If omitted, the body is returned inline.'),
|
|
87
95
|
},
|
|
88
96
|
handler: async (request, response, context) => {
|
|
89
97
|
if (request.params.reqid) {
|
|
90
|
-
response.attachNetworkRequest(request.params.reqid
|
|
98
|
+
response.attachNetworkRequest(request.params.reqid, {
|
|
99
|
+
requestFilePath: request.params.requestFilePath,
|
|
100
|
+
responseFilePath: request.params.responseFilePath,
|
|
101
|
+
});
|
|
91
102
|
}
|
|
92
103
|
else {
|
|
93
104
|
const data = await context.getDevToolsData();
|
|
@@ -96,7 +107,10 @@ export const getNetworkRequest = defineTool({
|
|
|
96
107
|
? context.resolveCdpRequestId(data.cdpRequestId)
|
|
97
108
|
: undefined;
|
|
98
109
|
if (reqid) {
|
|
99
|
-
response.attachNetworkRequest(reqid
|
|
110
|
+
response.attachNetworkRequest(reqid, {
|
|
111
|
+
requestFilePath: request.params.requestFilePath,
|
|
112
|
+
responseFilePath: request.params.responseFilePath,
|
|
113
|
+
});
|
|
100
114
|
}
|
|
101
115
|
else {
|
|
102
116
|
response.appendResponseLine(`Nothing is currently selected in the DevTools Network panel.`);
|
package/build/src/tools/pages.js
CHANGED
|
@@ -80,10 +80,14 @@ export const newPage = defineTool({
|
|
|
80
80
|
},
|
|
81
81
|
schema: {
|
|
82
82
|
url: zod.string().describe('URL to load in a new page.'),
|
|
83
|
+
background: zod
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
|
|
83
87
|
...timeoutSchema,
|
|
84
88
|
},
|
|
85
89
|
handler: async (request, response, context) => {
|
|
86
|
-
const page = await context.newPage();
|
|
90
|
+
const page = await context.newPage(request.params.background);
|
|
87
91
|
await context.waitForEventsAfterAction(async () => {
|
|
88
92
|
await page.goto(request.params.url, {
|
|
89
93
|
timeout: request.params.timeout,
|
|
@@ -109,6 +113,14 @@ export const navigatePage = defineTool({
|
|
|
109
113
|
.boolean()
|
|
110
114
|
.optional()
|
|
111
115
|
.describe('Whether to ignore cache on reload.'),
|
|
116
|
+
handleBeforeUnload: zod
|
|
117
|
+
.enum(['accept', 'decline'])
|
|
118
|
+
.optional()
|
|
119
|
+
.describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
|
|
120
|
+
initScript: zod
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
|
|
112
124
|
...timeoutSchema,
|
|
113
125
|
},
|
|
114
126
|
handler: async (request, response, context) => {
|
|
@@ -122,52 +134,85 @@ export const navigatePage = defineTool({
|
|
|
122
134
|
if (!request.params.type) {
|
|
123
135
|
request.params.type = 'url';
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
break;
|
|
139
|
-
case 'back':
|
|
140
|
-
try {
|
|
141
|
-
await page.goBack(options);
|
|
142
|
-
response.appendResponseLine(`Successfully navigated back to ${page.url()}.`);
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
case 'forward':
|
|
149
|
-
try {
|
|
150
|
-
await page.goForward(options);
|
|
151
|
-
response.appendResponseLine(`Successfully navigated forward to ${page.url()}.`);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
case 'reload':
|
|
158
|
-
try {
|
|
159
|
-
await page.reload({
|
|
160
|
-
...options,
|
|
161
|
-
ignoreCache: request.params.ignoreCache,
|
|
162
|
-
});
|
|
163
|
-
response.appendResponseLine(`Successfully reloaded the page.`);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
137
|
+
const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
|
|
138
|
+
const dialogHandler = (dialog) => {
|
|
139
|
+
if (dialog.type() === 'beforeunload') {
|
|
140
|
+
if (handleBeforeUnload === 'accept') {
|
|
141
|
+
response.appendResponseLine(`Accepted a beforeunload dialog.`);
|
|
142
|
+
void dialog.accept();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
response.appendResponseLine(`Declined a beforeunload dialog.`);
|
|
146
|
+
void dialog.dismiss();
|
|
147
|
+
}
|
|
148
|
+
// We are not going to report the dialog like regular dialogs.
|
|
149
|
+
context.clearDialog();
|
|
169
150
|
}
|
|
170
|
-
}
|
|
151
|
+
};
|
|
152
|
+
let initScriptId;
|
|
153
|
+
if (request.params.initScript) {
|
|
154
|
+
const { identifier } = await page.evaluateOnNewDocument(request.params.initScript);
|
|
155
|
+
initScriptId = identifier;
|
|
156
|
+
}
|
|
157
|
+
page.on('dialog', dialogHandler);
|
|
158
|
+
try {
|
|
159
|
+
await context.waitForEventsAfterAction(async () => {
|
|
160
|
+
switch (request.params.type) {
|
|
161
|
+
case 'url':
|
|
162
|
+
if (!request.params.url) {
|
|
163
|
+
throw new Error('A URL is required for navigation of type=url.');
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await page.goto(request.params.url, options);
|
|
167
|
+
response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'back':
|
|
174
|
+
try {
|
|
175
|
+
await page.goBack(options);
|
|
176
|
+
response.appendResponseLine(`Successfully navigated back to ${page.url()}.`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case 'forward':
|
|
183
|
+
try {
|
|
184
|
+
await page.goForward(options);
|
|
185
|
+
response.appendResponseLine(`Successfully navigated forward to ${page.url()}.`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case 'reload':
|
|
192
|
+
try {
|
|
193
|
+
await page.reload({
|
|
194
|
+
...options,
|
|
195
|
+
ignoreCache: request.params.ignoreCache,
|
|
196
|
+
});
|
|
197
|
+
response.appendResponseLine(`Successfully reloaded the page.`);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
page.off('dialog', dialogHandler);
|
|
208
|
+
if (initScriptId) {
|
|
209
|
+
await page
|
|
210
|
+
.removeScriptToEvaluateOnNewDocument(initScriptId)
|
|
211
|
+
.catch(error => {
|
|
212
|
+
logger(`Failed to remove init script`, error);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
171
216
|
response.setIncludePages(true);
|
|
172
217
|
},
|
|
173
218
|
});
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import zlib from 'node:zlib';
|
|
7
|
-
import { logger } from '../logger.js';
|
|
8
7
|
import { zod } from '../third_party/index.js';
|
|
9
|
-
import {
|
|
8
|
+
import { parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
|
|
10
9
|
import { ToolCategory } from './categories.js';
|
|
11
10
|
import { defineTool } from './ToolDefinition.js';
|
|
12
11
|
const filePathSchema = zod
|
|
@@ -15,7 +14,7 @@ const filePathSchema = zod
|
|
|
15
14
|
.describe('The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).');
|
|
16
15
|
export const startTrace = defineTool({
|
|
17
16
|
name: 'performance_start_trace',
|
|
18
|
-
description:
|
|
17
|
+
description: `Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.`,
|
|
19
18
|
annotations: {
|
|
20
19
|
category: ToolCategory.PERFORMANCE,
|
|
21
20
|
readOnlyHint: false,
|
|
@@ -23,7 +22,7 @@ export const startTrace = defineTool({
|
|
|
23
22
|
schema: {
|
|
24
23
|
reload: zod
|
|
25
24
|
.boolean()
|
|
26
|
-
.describe('Determines if, once tracing has started, the page should be automatically reloaded.'),
|
|
25
|
+
.describe('Determines if, once tracing has started, the current selected page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.'),
|
|
27
26
|
autoStop: zod
|
|
28
27
|
.boolean()
|
|
29
28
|
.describe('Determines if the trace recording should be automatically stopped.'),
|
|
@@ -120,12 +119,7 @@ export const analyzeInsight = defineTool({
|
|
|
120
119
|
response.appendResponseLine('No recorded traces found. Record a performance trace so you have Insights to analyze.');
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
|
-
|
|
124
|
-
if ('error' in insightOutput) {
|
|
125
|
-
response.appendResponseLine(insightOutput.error);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
response.appendResponseLine(insightOutput.output);
|
|
122
|
+
response.attachTraceInsight(lastRecording, request.params.insightSetId, request.params.insightName);
|
|
129
123
|
},
|
|
130
124
|
});
|
|
131
125
|
async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
@@ -152,20 +146,12 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
|
152
146
|
response.appendResponseLine('The performance trace has been stopped.');
|
|
153
147
|
if (traceResultIsSuccess(result)) {
|
|
154
148
|
context.storeTraceRecording(result);
|
|
155
|
-
|
|
156
|
-
response.appendResponseLine(traceSummaryText);
|
|
149
|
+
response.attachTraceSummary(result);
|
|
157
150
|
}
|
|
158
151
|
else {
|
|
159
|
-
|
|
160
|
-
response.appendResponseLine(result.error);
|
|
152
|
+
throw new Error(`There was an unexpected error parsing the trace: ${result.error}`);
|
|
161
153
|
}
|
|
162
154
|
}
|
|
163
|
-
catch (e) {
|
|
164
|
-
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
|
|
165
|
-
logger(`Error stopping performance trace: ${errorText}`);
|
|
166
|
-
response.appendResponseLine('An error occurred generating the response for this trace:');
|
|
167
|
-
response.appendResponseLine(errorText);
|
|
168
|
-
}
|
|
169
155
|
finally {
|
|
170
156
|
context.setIsRunningPerformanceTrace(false);
|
|
171
157
|
}
|