chrome-devtools-mcp 1.1.1 → 1.3.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 (109) hide show
  1. package/README.md +33 -4
  2. package/build/src/HeapSnapshotManager.js +30 -16
  3. package/build/src/McpContext.js +63 -9
  4. package/build/src/McpPage.js +9 -9
  5. package/build/src/McpResponse.js +176 -58
  6. package/build/src/PageCollector.js +8 -8
  7. package/build/src/ServiceWorkerCollector.js +171 -0
  8. package/build/src/TextSnapshot.js +1 -1
  9. package/build/src/ToolHandler.js +10 -4
  10. package/build/src/WaitForHelper.js +2 -2
  11. package/build/src/bin/chrome-devtools-cli-options.js +106 -4
  12. package/build/src/bin/chrome-devtools-mcp-cli-options.js +64 -5
  13. package/build/src/bin/chrome-devtools-mcp-main.js +5 -5
  14. package/build/src/browser.js +8 -4
  15. package/build/src/daemon/client.js +7 -7
  16. package/build/src/daemon/daemon.js +12 -12
  17. package/build/src/daemon/utils.js +2 -2
  18. package/build/src/{DevToolsConnectionAdapter.js → devtools/DevToolsConnectionAdapter.js} +1 -1
  19. package/build/src/{DevtoolsUtils.js → devtools/DevtoolsUtils.js} +79 -18
  20. package/build/src/devtools/McpHostBindingAdapter.js +165 -0
  21. package/build/src/formatters/ConsoleFormatter.js +1 -1
  22. package/build/src/formatters/HeapSnapshotFormatter.js +22 -0
  23. package/build/src/formatters/IssueFormatter.js +4 -4
  24. package/build/src/index.js +13 -1
  25. package/build/src/telemetry/ClearcutLogger.js +1 -1
  26. package/build/src/telemetry/WatchdogClient.js +4 -4
  27. package/build/src/telemetry/persistence.js +2 -2
  28. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  29. package/build/src/telemetry/watchdog/main.js +5 -5
  30. package/build/src/third_party/THIRD_PARTY_NOTICES +31 -1
  31. package/build/src/third_party/bundled-packages.json +2 -1
  32. package/build/src/third_party/devtools-formatter-worker.js +1 -1
  33. package/build/src/third_party/devtools-heap-snapshot-worker.js +161 -3
  34. package/build/src/third_party/index.js +7020 -5774
  35. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  36. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  37. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  38. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  39. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  40. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  41. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  87. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  88. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  89. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  90. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  91. package/build/src/tools/console.js +7 -0
  92. package/build/src/tools/emulation.js +1 -0
  93. package/build/src/tools/extensions.js +5 -2
  94. package/build/src/tools/input.js +11 -3
  95. package/build/src/tools/lighthouse.js +11 -6
  96. package/build/src/tools/memory.js +109 -10
  97. package/build/src/tools/network.js +2 -2
  98. package/build/src/tools/pages.js +13 -5
  99. package/build/src/tools/performance.js +8 -7
  100. package/build/src/tools/screencast.js +32 -10
  101. package/build/src/tools/screenshot.js +158 -76
  102. package/build/src/tools/script.js +1 -1
  103. package/build/src/tools/slim/tools.js +3 -0
  104. package/build/src/tools/snapshot.js +3 -2
  105. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  106. package/build/src/tools/webmcp.js +2 -0
  107. package/build/src/trace-processing/parse.js +5 -5
  108. package/build/src/version.js +1 -1
  109. package/package.json +4 -3
@@ -22,7 +22,7 @@ async function navigateWithInterception(page, action, allowListString, timeout)
22
22
  void interceptedRequest.continue();
23
23
  }
24
24
  else {
25
- logger(`Blocking request to: ${requestUrl}`);
25
+ logger?.(`Blocking request to: ${requestUrl}`);
26
26
  void interceptedRequest.abort('blockedbyclient');
27
27
  }
28
28
  };
@@ -30,7 +30,7 @@ async function navigateWithInterception(page, action, allowListString, timeout)
30
30
  if (allowList) {
31
31
  page.pptrPage.off('request', requestHandler);
32
32
  await page.pptrPage.setRequestInterception(false).catch(error => {
33
- logger(`Failed to disable request interception`, error);
33
+ logger?.(`Failed to disable request interception`, error);
34
34
  });
35
35
  }
36
36
  };
@@ -62,6 +62,7 @@ export const listPages = defineTool(args => {
62
62
  },
63
63
  schema: {},
64
64
  blockedByDialog: false,
65
+ verifyFilesSchema: [],
65
66
  handler: async (_request, response) => {
66
67
  response.setIncludePages(true);
67
68
  response.setListThirdPartyDeveloperTools();
@@ -86,6 +87,7 @@ export const selectPage = defineTool({
86
87
  .describe('Whether to focus the page and bring it to the top.'),
87
88
  },
88
89
  blockedByDialog: false,
90
+ verifyFilesSchema: [],
89
91
  handler: async (request, response, context) => {
90
92
  const page = context.getPageById(request.params.pageId);
91
93
  context.selectPage(page);
@@ -110,6 +112,7 @@ export const closePage = defineTool({
110
112
  .describe('The ID of the page to close. Call list_pages to list pages.'),
111
113
  },
112
114
  blockedByDialog: false,
115
+ verifyFilesSchema: [],
113
116
  handler: async (request, response, context) => {
114
117
  try {
115
118
  await context.closePage(request.params.pageId);
@@ -157,6 +160,7 @@ export const newPage = defineTool(args => {
157
160
  ...timeoutSchema,
158
161
  },
159
162
  blockedByDialog: false,
163
+ verifyFilesSchema: [],
160
164
  handler: async (request, response, context) => {
161
165
  const page = await context.newPage(request.params.background, request.params.isolatedContext);
162
166
  await navigateWithInterception(page, () => page.pptrPage.goto(request.params.url, {
@@ -204,6 +208,7 @@ export const navigatePage = definePageTool(args => {
204
208
  ...timeoutSchema,
205
209
  },
206
210
  blockedByDialog: false,
211
+ verifyFilesSchema: [],
207
212
  handler: async (request, response) => {
208
213
  const page = request.page;
209
214
  const options = {
@@ -290,7 +295,7 @@ export const navigatePage = definePageTool(args => {
290
295
  await page.pptrPage
291
296
  .removeScriptToEvaluateOnNewDocument(initScriptId)
292
297
  .catch(error => {
293
- logger(`Failed to remove init script`, error);
298
+ logger?.(`Failed to remove init script`, error);
294
299
  });
295
300
  }
296
301
  }
@@ -312,6 +317,7 @@ export const resizePage = definePageTool({
312
317
  height: zod.number().describe('Page height'),
313
318
  },
314
319
  blockedByDialog: false,
320
+ verifyFilesSchema: [],
315
321
  handler: async (request, response, _context) => {
316
322
  const page = request.page;
317
323
  try {
@@ -354,6 +360,7 @@ export const handleDialog = definePageTool({
354
360
  .describe('Optional prompt text to enter into the dialog.'),
355
361
  },
356
362
  blockedByDialog: false,
363
+ verifyFilesSchema: [],
357
364
  handler: async (request, response, _context) => {
358
365
  const page = request.page;
359
366
  const dialog = page.getDialog();
@@ -367,7 +374,7 @@ export const handleDialog = definePageTool({
367
374
  }
368
375
  catch (err) {
369
376
  // Likely already handled by the user outside of MCP.
370
- logger(err);
377
+ logger?.(err);
371
378
  }
372
379
  response.appendResponseLine('Successfully accepted the dialog');
373
380
  break;
@@ -378,7 +385,7 @@ export const handleDialog = definePageTool({
378
385
  }
379
386
  catch (err) {
380
387
  // Likely already handled.
381
- logger(err);
388
+ logger?.(err);
382
389
  }
383
390
  response.appendResponseLine('Successfully dismissed the dialog');
384
391
  break;
@@ -402,6 +409,7 @@ export const getTabId = definePageTool({
402
409
  .describe(`The ID of the page to get the tab ID for. Call ${listPages().name} to get available pages.`),
403
410
  },
404
411
  blockedByDialog: false,
412
+ verifyFilesSchema: [],
405
413
  handler: async (request, response, context) => {
406
414
  const page = context.getPageById(request.params.pageId);
407
415
  const tabId = page.pptrPage._tabId;
@@ -32,8 +32,8 @@ export const startTrace = definePageTool({
32
32
  filePath: filePathSchema,
33
33
  },
34
34
  blockedByDialog: true,
35
+ verifyFilesSchema: ['filePath'],
35
36
  handler: async (request, response, context) => {
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;
@@ -96,8 +96,8 @@ export const stopTrace = definePageTool({
96
96
  filePath: filePathSchema,
97
97
  },
98
98
  blockedByDialog: true,
99
+ verifyFilesSchema: ['filePath'],
99
100
  handler: async (request, response, context) => {
100
- await context.validatePath(request.params.filePath);
101
101
  if (!context.isRunningPerformanceTrace()) {
102
102
  return;
103
103
  }
@@ -121,6 +121,7 @@ export const analyzeInsight = definePageTool({
121
121
  .describe('The name of the Insight you want more information on. For example: "DocumentLatency" or "LCPBreakdown"'),
122
122
  },
123
123
  blockedByDialog: false,
124
+ verifyFilesSchema: [],
124
125
  handler: async (request, response, context) => {
125
126
  const lastRecording = context.recordedTraces().at(-1);
126
127
  if (!lastRecording) {
@@ -172,8 +173,8 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
172
173
  }
173
174
  /** We tell CrUXManager to fetch data so it's available when DevTools.PerformanceTraceFormatter is invoked */
174
175
  async function populateCruxData(result) {
175
- logger('populateCruxData called');
176
- const cruxManager = DevTools.CrUXManager.instance();
176
+ logger?.('populateCruxData called');
177
+ const cruxManager = DevTools.CrUXManager.CrUXManager.instance();
177
178
  // go/jtfbx. Yes, we're aware this API key is public. ;)
178
179
  cruxManager.setEndpointForTesting('https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=AIzaSyBn5gimNjhiEyA_euicSKko6IlD3HdgUfk');
179
180
  const cruxSetting = DevTools.Common.Settings.Settings.instance().createSetting('field-data', {
@@ -185,13 +186,13 @@ async function populateCruxData(result) {
185
186
  urls.push(result.parsedTrace.data.Meta.mainFrameURL);
186
187
  const urlSet = new Set(urls);
187
188
  if (urlSet.size === 0) {
188
- logger('No URLs found for CrUX data');
189
+ logger?.('No URLs found for CrUX data');
189
190
  return;
190
191
  }
191
- logger(`Fetching CrUX data for ${urlSet.size} URLs: ${Array.from(urlSet).join(', ')}`);
192
+ logger?.(`Fetching CrUX data for ${urlSet.size} URLs: ${Array.from(urlSet).join(', ')}`);
192
193
  const cruxData = await Promise.all(Array.from(urlSet).map(async (url) => {
193
194
  const data = await cruxManager.getFieldDataForPage(url);
194
- logger(`CrUX data for ${url}: ${data ? 'found' : 'not found'}`);
195
+ logger?.(`CrUX data for ${url}: ${data ? 'found' : 'not found'}`);
195
196
  return data;
196
197
  }));
197
198
  result.parsedTrace.metadata.cruxFieldData = cruxData;
@@ -30,22 +30,29 @@ export const startScreencast = definePageTool(args => ({
30
30
  .describe(`Output file path (${supportedExtensions.join(',')} are supported). Uses mkdtemp to generate a unique path if not provided.`),
31
31
  },
32
32
  blockedByDialog: false,
33
+ verifyFilesSchema: ['filePath'],
33
34
  handler: async (request, response, context) => {
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;
38
38
  }
39
- const filePath = request.params.filePath ?? (await generateTempFilePath());
40
- let enforcedExtension = '.mp4';
41
- let format = 'mp4';
42
- for (const supportedExtension of supportedExtensions) {
43
- if (filePath.endsWith(supportedExtension)) {
44
- enforcedExtension = supportedExtension;
45
- format = supportedExtension.substring(1);
46
- break;
47
- }
39
+ const requestedFilePath = request.params.filePath;
40
+ const filePath = requestedFilePath ?? (await generateTempFilePath());
41
+ // Match the extension case-insensitively so e.g. `.WEBM` is recognized as
42
+ // WebM. An explicitly requested but unsupported extension is rejected
43
+ // rather than being silently rewritten to `.mp4` (which would change both
44
+ // the format and the output path from what was requested). A missing
45
+ // extension falls back to `.mp4`. The matched extension is normalized to
46
+ // lower case.
47
+ const requestedExtension = path.extname(filePath);
48
+ const matchedExtension = supportedExtensions.find(supportedExtension => supportedExtension === requestedExtension.toLowerCase());
49
+ if (!matchedExtension && requestedExtension !== '') {
50
+ throw new Error(`Unsupported screencast file extension "${requestedExtension}". ` +
51
+ `Supported formats: ${supportedExtensions.join(', ')} (case-insensitive).`);
48
52
  }
53
+ const enforcedExtension = matchedExtension ?? '.mp4';
54
+ const format = (matchedExtension?.substring(1) ??
55
+ 'mp4');
49
56
  const resolvedPath = ensureExtension(path.resolve(filePath), enforcedExtension);
50
57
  const page = request.page;
51
58
  let recorder;
@@ -57,6 +64,19 @@ export const startScreencast = definePageTool(args => ({
57
64
  });
58
65
  }
59
66
  catch (err) {
67
+ // If we generated a temporary directory for this recording, remove it so
68
+ // a failed start (e.g. ffmpeg missing) does not leak an empty directory.
69
+ if (requestedFilePath === undefined) {
70
+ try {
71
+ await fs.rm(path.dirname(resolvedPath), {
72
+ recursive: true,
73
+ force: true,
74
+ });
75
+ }
76
+ catch {
77
+ // no-op
78
+ }
79
+ }
60
80
  const message = err instanceof Error ? err.message : String(err);
61
81
  if (message.includes('ENOENT') && message.includes('ffmpeg')) {
62
82
  throw new Error('ffmpeg is required for screencast recording but was not found. ' +
@@ -78,9 +98,11 @@ export const stopScreencast = definePageTool({
78
98
  },
79
99
  schema: {},
80
100
  blockedByDialog: false,
101
+ verifyFilesSchema: [],
81
102
  handler: async (_request, response, context) => {
82
103
  const data = context.getScreenRecorder();
83
104
  if (!data) {
105
+ response.appendResponseLine('Error: no active screencast recording to stop.');
84
106
  return;
85
107
  }
86
108
  try {
@@ -6,82 +6,164 @@
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { ToolCategory } from './categories.js';
8
8
  import { definePageTool } from './ToolDefinition.js';
9
- export const screenshot = definePageTool({
10
- name: 'take_screenshot',
11
- description: `Take a screenshot of the page or element.`,
12
- annotations: {
13
- category: ToolCategory.DEBUGGING,
14
- // Not read-only due to filePath param.
15
- readOnlyHint: false,
16
- },
17
- schema: {
18
- format: zod
19
- .enum(['png', 'jpeg', 'webp'])
20
- .default('png')
21
- .describe('Type of format to save the screenshot as. Default is "png"'),
22
- quality: zod
23
- .number()
24
- .min(0)
25
- .max(100)
26
- .optional()
27
- .describe('Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
28
- uid: zod
29
- .string()
30
- .optional()
31
- .describe('The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.'),
32
- fullPage: zod
33
- .boolean()
34
- .optional()
35
- .describe('If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.'),
36
- filePath: zod
37
- .string()
38
- .optional()
39
- .describe('The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.'),
40
- },
41
- blockedByDialog: true,
42
- handler: async (request, response, context) => {
43
- await context.validatePath(request.params.filePath);
44
- if (request.params.uid && request.params.fullPage) {
45
- throw new Error('Providing both "uid" and "fullPage" is not allowed.');
9
+ async function getSourceBox(page, element, fullPage) {
10
+ if (element) {
11
+ const box = await element.boundingBox();
12
+ return box ?? undefined;
13
+ }
14
+ if (fullPage) {
15
+ const dims = await page.evaluate(() => ({
16
+ width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
17
+ height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
18
+ }));
19
+ if (dims.width <= 0 || dims.height <= 0) {
20
+ return undefined;
46
21
  }
47
- let pageOrHandle;
48
- if (request.params.uid) {
49
- pageOrHandle = await request.page.getElementByUid(request.params.uid);
50
- }
51
- else {
52
- pageOrHandle = request.page.pptrPage;
53
- }
54
- const format = request.params.format;
55
- const quality = format === 'png' ? undefined : request.params.quality;
56
- const screenshot = await pageOrHandle.screenshot({
57
- type: format,
58
- fullPage: request.params.fullPage,
59
- quality,
60
- optimizeForSpeed: true, // Bonus: optimize encoding for speed
61
- });
62
- if (request.params.uid) {
63
- response.appendResponseLine(`Took a screenshot of node with uid "${request.params.uid}".`);
64
- }
65
- else if (request.params.fullPage) {
66
- response.appendResponseLine('Took a screenshot of the full current page.');
67
- }
68
- else {
69
- response.appendResponseLine("Took a screenshot of the current page's viewport.");
70
- }
71
- if (request.params.filePath) {
72
- const result = await context.saveFile(screenshot, request.params.filePath, `.${format}`);
73
- response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
74
- }
75
- else if (screenshot.length >= 2_000_000) {
76
- const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot.${request.params.format}`);
77
- response.appendResponseLine(`Saved screenshot to ${filepath}.`);
78
- }
79
- else {
80
- response.attachImage({
81
- mimeType: `image/${request.params.format}`,
82
- data: Buffer.from(screenshot).toString('base64'),
83
- });
84
- }
85
- },
22
+ return { x: 0, y: 0, width: dims.width, height: dims.height };
23
+ }
24
+ const viewport = page.viewport();
25
+ if (!viewport) {
26
+ return undefined;
27
+ }
28
+ return { x: 0, y: 0, width: viewport.width, height: viewport.height };
29
+ }
30
+ function computeDownscaleClip(box, maxWidth, maxHeight) {
31
+ const widthScale = maxWidth !== undefined ? Math.min(1, maxWidth / box.width) : 1;
32
+ const heightScale = maxHeight !== undefined ? Math.min(1, maxHeight / box.height) : 1;
33
+ const scale = Math.min(widthScale, heightScale);
34
+ if (scale >= 1) {
35
+ return undefined;
36
+ }
37
+ // Skip degenerate sub-pixel results.
38
+ if (Math.round(box.width * scale) < 1 || Math.round(box.height * scale) < 1) {
39
+ return undefined;
40
+ }
41
+ return {
42
+ x: box.x,
43
+ y: box.y,
44
+ width: box.width,
45
+ height: box.height,
46
+ scale,
47
+ };
48
+ }
49
+ export const screenshot = definePageTool(args => {
50
+ const { screenshotFormat, screenshotQuality, screenshotMaxWidth, screenshotMaxHeight, } = args ?? {};
51
+ const defaultFormat = screenshotFormat ?? 'png';
52
+ return {
53
+ name: 'take_screenshot',
54
+ description: `Take a screenshot of the page or element.`,
55
+ annotations: {
56
+ category: ToolCategory.DEBUGGING,
57
+ // Not read-only due to filePath param.
58
+ readOnlyHint: false,
59
+ },
60
+ schema: {
61
+ format: zod
62
+ .enum(['png', 'jpeg', 'webp'])
63
+ .default(defaultFormat)
64
+ .describe(`Type of format to save the screenshot as. Default is "${defaultFormat}"`),
65
+ quality: zod
66
+ .number()
67
+ .min(0)
68
+ .max(100)
69
+ .optional()
70
+ .describe('Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
71
+ uid: zod
72
+ .string()
73
+ .optional()
74
+ .describe('The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.'),
75
+ fullPage: zod
76
+ .boolean()
77
+ .optional()
78
+ .describe('If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.'),
79
+ filePath: zod
80
+ .string()
81
+ .optional()
82
+ .describe('The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.'),
83
+ },
84
+ blockedByDialog: true,
85
+ verifyFilesSchema: ['filePath'],
86
+ handler: async (request, response, context) => {
87
+ if (request.params.uid && request.params.fullPage) {
88
+ throw new Error('Providing both "uid" and "fullPage" is not allowed.');
89
+ }
90
+ const page = request.page.pptrPage;
91
+ const element = request.params.uid
92
+ ? await request.page.getElementByUid(request.params.uid)
93
+ : undefined;
94
+ const format = request.params.format;
95
+ const quality = format === 'png'
96
+ ? undefined
97
+ : (request.params.quality ?? screenshotQuality);
98
+ const fullPage = request.params.fullPage ?? false;
99
+ // Compute a downscale clip when --screenshot-max-width or
100
+ // --screenshot-max-height is set and the source exceeds either bound.
101
+ // The smaller scale factor wins so both bounds are respected while
102
+ // preserving aspect ratio.
103
+ let clip;
104
+ if (screenshotMaxWidth !== undefined ||
105
+ screenshotMaxHeight !== undefined) {
106
+ const box = await getSourceBox(page, element, fullPage);
107
+ if (box) {
108
+ clip = computeDownscaleClip(box, screenshotMaxWidth, screenshotMaxHeight);
109
+ }
110
+ }
111
+ let screenshot;
112
+ if (clip) {
113
+ // page.screenshot with clip lets the CDP scale param downscale the
114
+ // capture for viewport, full-page and element shots alike. We rely on
115
+ // Puppeteer's default of captureBeyondViewport=true when a clip is
116
+ // present so element/full-page captures below the fold still work.
117
+ screenshot = await page.screenshot({
118
+ type: format,
119
+ quality,
120
+ optimizeForSpeed: true,
121
+ clip,
122
+ });
123
+ }
124
+ else if (element) {
125
+ screenshot = await element.screenshot({
126
+ type: format,
127
+ quality,
128
+ optimizeForSpeed: true,
129
+ });
130
+ }
131
+ else {
132
+ screenshot = await page.screenshot({
133
+ type: format,
134
+ fullPage,
135
+ quality,
136
+ optimizeForSpeed: true,
137
+ });
138
+ }
139
+ if (request.params.uid) {
140
+ response.appendResponseLine(`Took a screenshot of node with uid "${request.params.uid}".`);
141
+ }
142
+ else if (fullPage) {
143
+ response.appendResponseLine('Took a screenshot of the full current page.');
144
+ }
145
+ else {
146
+ response.appendResponseLine("Took a screenshot of the current page's viewport.");
147
+ }
148
+ // Narrow `format` at the point of use: in the factory form of
149
+ // definePageTool TS widens the Schema generic, which loses the literal
150
+ // union from zod.enum on request.params.format.
151
+ const extension = format === 'jpeg' ? '.jpeg' : format === 'webp' ? '.webp' : '.png';
152
+ if (request.params.filePath) {
153
+ const result = await context.saveFile(screenshot, request.params.filePath, extension);
154
+ response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
155
+ }
156
+ else if (screenshot.length >= 2_000_000) {
157
+ const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot${extension}`);
158
+ response.appendResponseLine(`Saved screenshot to ${filepath}.`);
159
+ }
160
+ else {
161
+ response.attachImage({
162
+ mimeType: `image/${format}`,
163
+ data: Buffer.from(screenshot).toString('base64'),
164
+ });
165
+ }
166
+ },
167
+ };
86
168
  });
87
169
  //# sourceMappingURL=screenshot.js.map
@@ -51,9 +51,9 @@ Example with arguments: \`(el) => {
51
51
  : {}),
52
52
  },
53
53
  blockedByDialog: true,
54
+ verifyFilesSchema: ['filePath'],
54
55
  handler: async (request, response, context) => {
55
56
  const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, filePath, } = request.params;
56
- await context.validatePath(filePath);
57
57
  if (cliArgs?.categoryExtensions && serviceWorkerId) {
58
58
  if (uidArgs && uidArgs.length > 0) {
59
59
  throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
@@ -16,6 +16,7 @@ export const screenshot = definePageTool({
16
16
  },
17
17
  schema: {},
18
18
  blockedByDialog: true,
19
+ verifyFilesSchema: [],
19
20
  handler: async (request, response, context) => {
20
21
  const page = request.page;
21
22
  const screenshot = await page.pptrPage.screenshot({
@@ -37,6 +38,7 @@ export const navigate = definePageTool({
37
38
  url: zod.string().describe('URL to navigate to'),
38
39
  },
39
40
  blockedByDialog: false,
41
+ verifyFilesSchema: [],
40
42
  handler: async (request, response) => {
41
43
  const page = request.page;
42
44
  const options = {
@@ -71,6 +73,7 @@ export const evaluate = definePageTool({
71
73
  script: zod.string().describe(`JS script to run on the page`),
72
74
  },
73
75
  blockedByDialog: true,
76
+ verifyFilesSchema: [],
74
77
  handler: async (request, response) => {
75
78
  const page = request.page;
76
79
  try {
@@ -27,8 +27,8 @@ in the DevTools Elements panel (if any).`,
27
27
  .describe('The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response.'),
28
28
  },
29
29
  blockedByDialog: true,
30
- handler: async (request, response, context) => {
31
- await context.validatePath(request.params.filePath);
30
+ verifyFilesSchema: ['filePath'],
31
+ handler: async (request, response) => {
32
32
  response.includeSnapshot({
33
33
  verbose: request.params.verbose ?? false,
34
34
  filePath: request.params.filePath,
@@ -50,6 +50,7 @@ export const waitFor = definePageTool({
50
50
  ...timeoutSchema,
51
51
  },
52
52
  blockedByDialog: true,
53
+ verifyFilesSchema: [],
53
54
  handler: async (request, response, context) => {
54
55
  const page = request.page;
55
56
  await context.waitForTextOnPage(request.params.text, request.params.timeout, page.pptrPage);
@@ -21,6 +21,7 @@ export const listThirdPartyDeveloperTools = definePageTool({
21
21
  },
22
22
  schema: {},
23
23
  blockedByDialog: false,
24
+ verifyFilesSchema: [],
24
25
  handler: async (_request, response, _context) => {
25
26
  response.setListThirdPartyDeveloperTools();
26
27
  },
@@ -40,6 +41,7 @@ export const executeThirdPartyDeveloperTool = definePageTool({
40
41
  .describe('The JSON-stringified parameters to pass to the tool'),
41
42
  },
42
43
  blockedByDialog: false,
44
+ verifyFilesSchema: [],
43
45
  handler: async (request, response) => {
44
46
  const toolName = request.params.toolName;
45
47
  let params = {};
@@ -58,8 +60,16 @@ export const executeThirdPartyDeveloperTool = definePageTool({
58
60
  throw new Error(`Failed to parse params as JSON: ${errorMessage}`);
59
61
  }
60
62
  }
61
- const toolGroup = request.page.getThirdPartyDeveloperTools();
62
- const tool = toolGroup?.tools.find(t => t.name === toolName);
63
+ const toolGroups = request.page.getThirdPartyDeveloperTools();
64
+ let tool;
65
+ if (toolGroups) {
66
+ for (const group of toolGroups) {
67
+ tool = group.tools?.find(t => t.name === toolName);
68
+ if (tool) {
69
+ break;
70
+ }
71
+ }
72
+ }
63
73
  if (!tool) {
64
74
  throw new Error(`Tool ${toolName} not found`);
65
75
  }
@@ -15,6 +15,7 @@ export const listWebMcpTools = definePageTool({
15
15
  },
16
16
  schema: {},
17
17
  blockedByDialog: false,
18
+ verifyFilesSchema: [],
18
19
  handler: async (_request, response, _context) => {
19
20
  response.setListWebMcpTools();
20
21
  },
@@ -34,6 +35,7 @@ export const executeWebMcpTool = definePageTool({
34
35
  .describe('The JSON-stringified parameters to pass to the WebMCP tool'),
35
36
  },
36
37
  blockedByDialog: false,
38
+ verifyFilesSchema: [],
37
39
  handler: async (request, response) => {
38
40
  const toolName = request.params.toolName;
39
41
  let input = {};
@@ -40,7 +40,7 @@ export async function parseRawTraceBuffer(buffer, metadata) {
40
40
  }
41
41
  catch (e) {
42
42
  const errorText = e instanceof Error ? e.message : JSON.stringify(e);
43
- logger(`Unexpected error parsing trace: ${errorText}`);
43
+ logger?.(`Unexpected error parsing trace: ${errorText}`);
44
44
  return {
45
45
  error: errorText,
46
46
  };
@@ -51,9 +51,9 @@ const extraFormatDescriptions = `Information on performance traces may contain m
51
51
  ${DevTools.PerformanceTraceFormatter.callFrameDataFormatDescription}
52
52
 
53
53
  ${DevTools.PerformanceTraceFormatter.networkDataFormatDescription}`;
54
- export function getTraceSummary(result) {
54
+ export function getTraceSummary(result, deviceScope) {
55
55
  const focus = DevTools.AgentFocus.fromParsedTrace(result.parsedTrace);
56
- const formatter = new DevTools.PerformanceTraceFormatter(focus);
56
+ const formatter = new DevTools.PerformanceTraceFormatter(focus, deviceScope);
57
57
  const summaryText = formatter.formatTraceSummary();
58
58
  return `## Summary of Performance trace findings:
59
59
  ${summaryText}
@@ -61,7 +61,7 @@ ${summaryText}
61
61
  ## Details on call tree & network request formats:
62
62
  ${extraFormatDescriptions}`;
63
63
  }
64
- export function getInsightOutput(result, insightSetId, insightName) {
64
+ export function getInsightOutput(result, insightSetId, insightName, deviceScope) {
65
65
  if (!result.insights) {
66
66
  return {
67
67
  error: 'No Performance insights are available for this trace.',
@@ -79,7 +79,7 @@ export function getInsightOutput(result, insightSetId, insightName) {
79
79
  error: `No Insight with the name ${insightName} found. Double check the name you provided is accurate and try again.`,
80
80
  };
81
81
  }
82
- const formatter = new DevTools.PerformanceInsightFormatter(DevTools.AgentFocus.fromParsedTrace(result.parsedTrace), matchingInsight);
82
+ const formatter = new DevTools.PerformanceInsightFormatter(DevTools.AgentFocus.fromParsedTrace(result.parsedTrace), matchingInsight, deviceScope);
83
83
  return { output: formatter.formatInsight() };
84
84
  }
85
85
  //# sourceMappingURL=parse.js.map
@@ -5,6 +5,6 @@
5
5
  */
6
6
  // If moved update release-please config
7
7
  // x-release-please-start-version
8
- export const VERSION = '1.1.1';
8
+ export const VERSION = '1.3.0';
9
9
  // x-release-please-end
10
10
  //# sourceMappingURL=version.js.map