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.
Files changed (37) hide show
  1. package/README.md +28 -2
  2. package/build/src/DevtoolsUtils.js +59 -42
  3. package/build/src/McpContext.js +106 -13
  4. package/build/src/McpResponse.js +213 -132
  5. package/build/src/browser.js +1 -0
  6. package/build/src/cli.js +29 -6
  7. package/build/src/formatters/ConsoleFormatter.js +153 -0
  8. package/build/src/formatters/IssueFormatter.js +190 -0
  9. package/build/src/formatters/NetworkFormatter.js +226 -0
  10. package/build/src/formatters/SnapshotFormatter.js +6 -0
  11. package/build/src/logger.js +9 -0
  12. package/build/src/main.js +16 -3
  13. package/build/src/telemetry/clearcut-logger.js +86 -12
  14. package/build/src/telemetry/flag-utils.js +1 -1
  15. package/build/src/telemetry/metric-utils.js +14 -0
  16. package/build/src/telemetry/persistence.js +53 -0
  17. package/build/src/telemetry/types.js +6 -0
  18. package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
  19. package/build/src/telemetry/watchdog/main.js +127 -0
  20. package/build/src/telemetry/watchdog-client.js +60 -0
  21. package/build/src/third_party/THIRD_PARTY_NOTICES +6 -5
  22. package/build/src/third_party/devtools-formatter-worker.js +15451 -0
  23. package/build/src/third_party/index.js +1356 -282
  24. package/build/src/tools/categories.js +2 -0
  25. package/build/src/tools/emulation.js +83 -1
  26. package/build/src/tools/extensions.js +79 -0
  27. package/build/src/tools/input.js +58 -9
  28. package/build/src/tools/network.js +17 -3
  29. package/build/src/tools/pages.js +91 -46
  30. package/build/src/tools/performance.js +6 -20
  31. package/build/src/tools/tools.js +2 -0
  32. package/build/src/utils/ExtensionRegistry.js +35 -0
  33. package/package.json +9 -8
  34. package/build/src/formatters/consoleFormatter.js +0 -156
  35. package/build/src/formatters/networkFormatter.js +0 -77
  36. package/build/src/telemetry/clearcut-sender.js +0 -11
  37. 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
+ });
@@ -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
- response.includeSnapshot();
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
- response.includeSnapshot();
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
- response.includeSnapshot();
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
- await handle.asLocator().fill(value);
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
- response.includeSnapshot();
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
- response.includeSnapshot();
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
- response.includeSnapshot();
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
- response.includeSnapshot();
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
- response.includeSnapshot();
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: true,
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.`);
@@ -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
- await context.waitForEventsAfterAction(async () => {
126
- switch (request.params.type) {
127
- case 'url':
128
- if (!request.params.url) {
129
- throw new Error('A URL is required for navigation of type=url.');
130
- }
131
- try {
132
- await page.goto(request.params.url, options);
133
- response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
134
- }
135
- catch (error) {
136
- response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
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 { getInsightOutput, getTraceSummary, parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
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: '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.',
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
- const insightOutput = getInsightOutput(lastRecording, request.params.insightSetId, request.params.insightName);
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
- const traceSummaryText = getTraceSummary(result);
156
- response.appendResponseLine(traceSummaryText);
149
+ response.attachTraceSummary(result);
157
150
  }
158
151
  else {
159
- response.appendResponseLine('There was an unexpected error parsing the trace:');
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
  }