opera-devtools-mcp 0.6.0 → 0.7.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 (51) hide show
  1. package/build/src/McpContext.js +78 -41
  2. package/build/src/McpPage.js +5 -1
  3. package/build/src/ToolHandler.js +45 -74
  4. package/build/src/bin/chrome-devtools-mcp-main.js +4 -4
  5. package/build/src/bin/chrome-devtools.js +29 -30
  6. package/build/src/bin/opera-devtools-cli-options.js +1 -1
  7. package/build/src/bin/opera-devtools-mcp-cli-options.js +1 -1
  8. package/build/src/browser.js +5 -16
  9. package/build/src/config/browser-options.js +126 -0
  10. package/build/src/config/category-options.js +81 -0
  11. package/build/src/{bin/chrome-devtools-cli-options.js → config/cli-options.js} +392 -26
  12. package/build/src/{bin/chrome-devtools-mcp-cli-options.js → config/mcp-options.js} +143 -164
  13. package/build/src/daemon/client.js +9 -0
  14. package/build/src/daemon/utils.js +6 -0
  15. package/build/src/devtools/DevtoolsUtils.js +27 -21
  16. package/build/src/formatters/NetworkFormatter.js +5 -2
  17. package/build/src/index.js +166 -100
  18. package/build/src/processors/HeapSnapshotManager.js +12 -0
  19. package/build/src/telemetry/ClearcutLogger.js +19 -6
  20. package/build/src/telemetry/transformation.js +4 -0
  21. package/build/src/telemetry/types.js +4 -0
  22. package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
  23. package/build/src/third_party/bundled-packages.json +3 -3
  24. package/build/src/third_party/devtools-formatter-worker.js +22 -0
  25. package/build/src/third_party/devtools-heap-snapshot-worker.js +100 -20
  26. package/build/src/third_party/index.js +15459 -14256
  27. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
  28. package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
  29. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
  30. package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
  31. package/build/src/tools/ToolDefinition.js +15 -0
  32. package/build/src/tools/categories.js +0 -6
  33. package/build/src/tools/comments.js +90 -0
  34. package/build/src/tools/console.js +1 -1
  35. package/build/src/tools/emulation.js +1 -1
  36. package/build/src/tools/extensions.js +1 -1
  37. package/build/src/tools/memory.js +60 -6
  38. package/build/src/tools/network.js +2 -2
  39. package/build/src/tools/pages.js +26 -15
  40. package/build/src/tools/performance.js +4 -3
  41. package/build/src/tools/screencast.js +3 -2
  42. package/build/src/tools/screenshot.js +39 -8
  43. package/build/src/tools/script.js +17 -4
  44. package/build/src/tools/slim/tools.js +41 -33
  45. package/build/src/tools/snapshot.js +1 -1
  46. package/build/src/tools/tools.js +2 -0
  47. package/build/src/utils/WaitForHelper.js +12 -1
  48. package/build/src/utils/bytes.js +105 -0
  49. package/build/src/utils/url.js +73 -0
  50. package/build/src/version.js +1 -1
  51. package/package.json +23 -4
@@ -0,0 +1 @@
1
+ # Fetching the provider's accounts list was blocked by the request's initiator's connection allowlists.
@@ -0,0 +1 @@
1
+ # Fetching the provider's FedCM manifest configuration was blocked by the request's initiator's connection allowlists.
@@ -0,0 +1 @@
1
+ # Fetching the provider's token was blocked by the request's initiator's connection allowlists.
@@ -0,0 +1 @@
1
+ # Fetching the provider's well-known file was blocked by the request's initiator's connection allowlists.
@@ -53,6 +53,15 @@ export function viewportTransform(arg) {
53
53
  const hasTouch = tags.includes('touch');
54
54
  const isLandscape = tags.includes('landscape');
55
55
  const [width, height, dpr] = dimensions.split('x').map(Number);
56
+ if (!Number.isFinite(width) || width <= 0) {
57
+ throw new Error(`Invalid viewport width "${width}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]' with a positive width.`);
58
+ }
59
+ if (!Number.isFinite(height) || height <= 0) {
60
+ throw new Error(`Invalid viewport height "${height}". Expected format '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]' with a positive height.`);
61
+ }
62
+ if (dpr !== undefined && (!Number.isFinite(dpr) || dpr <= 0)) {
63
+ throw new Error(`Invalid devicePixelRatio "${dpr}". Expected a positive number.`);
64
+ }
56
65
  return {
57
66
  width,
58
67
  height,
@@ -67,6 +76,12 @@ export function geolocationTransform(arg) {
67
76
  return undefined;
68
77
  }
69
78
  const [latitude, longitude] = arg.split(',').map(Number);
79
+ if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
80
+ throw new Error(`Invalid latitude "${latitude}". Latitude must be a number between -90 and 90.`);
81
+ }
82
+ if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
83
+ throw new Error(`Invalid longitude "${longitude}". Longitude must be a number between -180 and 180.`);
84
+ }
70
85
  return {
71
86
  latitude,
72
87
  longitude,
@@ -32,10 +32,4 @@ export const labels = {
32
32
  [ToolCategory.OPERA]: 'Opera',
33
33
  [ToolCategory.PWA]: 'Progressive Web Apps',
34
34
  };
35
- export const OFF_BY_DEFAULT_CATEGORIES = [
36
- ToolCategory.EXTENSIONS,
37
- ToolCategory.THIRD_PARTY,
38
- ToolCategory.WEBMCP,
39
- ToolCategory.PWA,
40
- ];
41
35
  //# sourceMappingURL=categories.js.map
@@ -0,0 +1,90 @@
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 { definePageTool } from './ToolDefinition.js';
9
+ export const openDevtools = definePageTool({
10
+ name: 'open_devtools',
11
+ description: 'Open a DevTools window for the selected page.',
12
+ annotations: {
13
+ category: ToolCategory.DEBUGGING,
14
+ readOnlyHint: true,
15
+ conditions: ['devtoolsComments'],
16
+ },
17
+ schema: {},
18
+ blockedByDialog: false,
19
+ verifyFilesSchema: [],
20
+ handler: async () => {
21
+ throw new Error('Not implemented');
22
+ },
23
+ });
24
+ export const getDevtoolsComments = definePageTool({
25
+ name: 'get_devtools_comments',
26
+ description: 'Retrieve user comments from the DevTools window for the page.',
27
+ annotations: {
28
+ category: ToolCategory.DEBUGGING,
29
+ readOnlyHint: true,
30
+ conditions: ['devtoolsComments'],
31
+ },
32
+ schema: {},
33
+ blockedByDialog: false,
34
+ verifyFilesSchema: {},
35
+ handler: async () => {
36
+ throw new Error('Not implemented');
37
+ },
38
+ });
39
+ export const resolveDevtoolsComment = definePageTool({
40
+ name: 'resolve_devtools_comment',
41
+ description: 'Append an agent reply to a DevTools comment thread and mark it as resolved.',
42
+ annotations: {
43
+ category: ToolCategory.DEBUGGING,
44
+ readOnlyHint: false,
45
+ conditions: ['devtoolsComments'],
46
+ },
47
+ schema: {
48
+ threadId: zod
49
+ .string()
50
+ .describe('The unique identifier of the comment thread to resolve (e.g. "comment-1").'),
51
+ replyText: zod
52
+ .string()
53
+ .optional()
54
+ .describe('Optional reply explanation from the AI agent to append to the resolved comment thread.'),
55
+ },
56
+ blockedByDialog: false,
57
+ verifyFilesSchema: {},
58
+ handler: async () => {
59
+ throw new Error('Not implemented');
60
+ },
61
+ });
62
+ export const revealInDevtools = definePageTool({
63
+ name: 'reveal_in_devtools',
64
+ description: 'Navigate DevTools to a specified panel and highlight a target DOM node or network request. The parameters uid and reqid are mutually exclusive.',
65
+ annotations: {
66
+ category: ToolCategory.DEBUGGING,
67
+ readOnlyHint: true,
68
+ conditions: ['devtoolsComments'],
69
+ },
70
+ schema: {
71
+ panelName: zod
72
+ .string()
73
+ .optional()
74
+ .describe('The target DevTools panel (e.g. "elements", "network", "sources", "console").'),
75
+ uid: zod
76
+ .string()
77
+ .optional()
78
+ .describe('Optional snapshot element UID to reveal in the Elements panel.'),
79
+ reqid: zod
80
+ .number()
81
+ .optional()
82
+ .describe('Optional network request ID to reveal in the Network panel.'),
83
+ },
84
+ blockedByDialog: false,
85
+ verifyFilesSchema: {},
86
+ handler: async () => {
87
+ throw new Error('Not implemented');
88
+ },
89
+ });
90
+ //# sourceMappingURL=comments.js.map
@@ -32,7 +32,7 @@ const LIST_CONSOLE_MESSAGES_TOOL_NAME = 'list_console_messages';
32
32
  export const listConsoleMessages = definePageTool(cliArgs => {
33
33
  return {
34
34
  name: LIST_CONSOLE_MESSAGES_TOOL_NAME,
35
- description: `List all console messages for the currently selected page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
35
+ description: `List all console messages for the target page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
36
36
  annotations: {
37
37
  category: ToolCategory.DEBUGGING,
38
38
  readOnlyHint: true,
@@ -33,7 +33,7 @@ const throttlingOptions = [
33
33
  ];
34
34
  export const emulate = definePageTool({
35
35
  name: 'emulate',
36
- description: `Emulates various features on the selected page.`,
36
+ description: `Emulates various features on the target page.`,
37
37
  annotations: {
38
38
  category: ToolCategory.EMULATION,
39
39
  readOnlyHint: false,
@@ -20,7 +20,7 @@ export const installExtension = defineTool({
20
20
  },
21
21
  blockedByDialog: false,
22
22
  verifyFilesSchema: {
23
- path: true,
23
+ path: { local: true },
24
24
  },
25
25
  handler: async (request, response, context) => {
26
26
  const { path } = request.params;
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
+ import { byteSizeRangeSchema } from '../utils/bytes.js';
7
8
  import { ToolCategory } from './categories.js';
8
9
  import { definePageTool, defineTool } from './ToolDefinition.js';
9
10
  const HEAP_SNAPSHOT_FILTERS = [
@@ -17,7 +18,7 @@ const HEAP_SNAPSHOT_FILTERS = [
17
18
  ];
18
19
  export const takeHeapSnapshot = definePageTool({
19
20
  name: 'take_heapsnapshot',
20
- description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
21
+ description: `Capture a heap snapshot of the target page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
21
22
  annotations: {
22
23
  category: ToolCategory.MEMORY,
23
24
  readOnlyHint: false,
@@ -239,10 +240,7 @@ export const getHeapSnapshotEdges = defineTool({
239
240
  .enum(['retainedSize', 'selfSize', 'name'])
240
241
  .optional()
241
242
  .describe('Sort order for edges. Default is retainedSize.'),
242
- minRetainedSize: zod
243
- .number()
244
- .optional()
245
- .describe('Minimum retained size in bytes for target nodes.'),
243
+ retainedSize: byteSizeRangeSchema('Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-") for target nodes. A single value is treated as a minimum. Currently, only the lower bound is applied.').optional(),
246
244
  excludePrimitives: zod
247
245
  .boolean()
248
246
  .optional()
@@ -253,7 +251,8 @@ export const getHeapSnapshotEdges = defineTool({
253
251
  handler: async (request, response, context) => {
254
252
  const edges = await context.getHeapSnapshotEdges(request.params.filePath, request.params.nodeId, {
255
253
  sortBy: request.params.sortBy ?? 'retainedSize',
256
- minRetainedSize: request.params.minRetainedSize,
254
+ // DevTools currently only supports a lower retained-size bound here.
255
+ minRetainedSize: request.params.retainedSize?.min,
257
256
  excludePrimitives: request.params.excludePrimitives ?? true,
258
257
  });
259
258
  response.setHeapSnapshotNodes(edges, {
@@ -367,4 +366,59 @@ export const getHeapSnapshotObjectDetails = defineTool({
367
366
  response.setHeapSnapshotObjectDetails(objectInfo);
368
367
  },
369
368
  });
369
+ export const queryHeapSnapshotObjects = defineTool({
370
+ name: 'query_heapsnapshot_objects',
371
+ description: 'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, retainedSize, selfSize, isDetached, sortBy).',
372
+ annotations: {
373
+ category: ToolCategory.MEMORY,
374
+ readOnlyHint: true,
375
+ conditions: ['memoryDebugging'],
376
+ },
377
+ blockedByDialog: false,
378
+ verifyFilesSchema: { filePath: true },
379
+ schema: {
380
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
381
+ className: zod
382
+ .string()
383
+ .optional()
384
+ .describe('Optional regex or text matching object class name.'),
385
+ propertyName: zod
386
+ .string()
387
+ .optional()
388
+ .describe('Optional property name filter for outgoing reference edges.'),
389
+ nodeType: zod
390
+ .string()
391
+ .optional()
392
+ .describe('Optional V8 node type filter (e.g. object, closure, string, array, code).'),
393
+ retainedSize: byteSizeRangeSchema('Inclusive retained size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.').optional(),
394
+ selfSize: byteSizeRangeSchema('Inclusive self size range (e.g. "1MB-2MB", "-1MB", or "1MB-"). A single value is treated as a minimum.').optional(),
395
+ isDetached: zod
396
+ .boolean()
397
+ .optional()
398
+ .describe('Whether to filter for detached DOM nodes.'),
399
+ sortBy: zod
400
+ .enum(['retainedSize', 'selfSize', 'id'])
401
+ .optional()
402
+ .describe('Sort order for results. Default is retainedSize.'),
403
+ pageIdx: zod.number().optional().describe('The page index for pagination.'),
404
+ pageSize: zod.number().optional().describe('The page size for pagination.'),
405
+ },
406
+ handler: async (request, response, context) => {
407
+ const range = await context.queryHeapSnapshotObjects(request.params.filePath, {
408
+ className: request.params.className,
409
+ propertyName: request.params.propertyName,
410
+ nodeType: request.params.nodeType,
411
+ minRetainedSize: request.params.retainedSize?.min,
412
+ maxRetainedSize: request.params.retainedSize?.max,
413
+ minSelfSize: request.params.selfSize?.min,
414
+ maxSelfSize: request.params.selfSize?.max,
415
+ isDetached: request.params.isDetached,
416
+ sortBy: request.params.sortBy,
417
+ });
418
+ response.setHeapSnapshotNodes(range, {
419
+ pageIdx: request.params.pageIdx,
420
+ pageSize: request.params.pageSize,
421
+ });
422
+ },
423
+ });
370
424
  //# sourceMappingURL=memory.js.map
@@ -29,7 +29,7 @@ const FILTERABLE_RESOURCE_TYPES = [
29
29
  ];
30
30
  export const listNetworkRequests = definePageTool({
31
31
  name: 'list_network_requests',
32
- description: `Lists the most recent requests for the currently selected page since the last navigation.`,
32
+ description: `Lists the most recent requests for the target page since the last navigation.`,
33
33
  annotations: {
34
34
  category: ToolCategory.NETWORK,
35
35
  readOnlyHint: true,
@@ -76,7 +76,7 @@ export const listNetworkRequests = definePageTool({
76
76
  });
77
77
  export const getNetworkRequest = definePageTool({
78
78
  name: 'get_network_request',
79
- description: `Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.`,
79
+ description: `Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel. Useful for inspecting request headers (including 'Cookie') and response headers (including 'Set-Cookie' and directives).`,
80
80
  annotations: {
81
81
  category: ToolCategory.NETWORK,
82
82
  readOnlyHint: false,
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { logger } from '../utils/logger.js';
8
+ import { validateUrl } from '../utils/url.js';
8
9
  import { ToolCategory } from './categories.js';
9
10
  import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './ToolDefinition.js';
10
11
  export const listPages = defineTool(args => {
@@ -84,7 +85,7 @@ export const closePage = defineTool({
84
85
  response.setListThirdPartyDeveloperTools();
85
86
  },
86
87
  });
87
- export const newPage = defineTool(() => {
88
+ export const newPage = defineTool(args => {
88
89
  return {
89
90
  name: 'new_page',
90
91
  description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
@@ -103,12 +104,16 @@ export const newPage = defineTool(() => {
103
104
  .optional()
104
105
  .describe('If specified, the page is created in an isolated browser context with the given name. ' +
105
106
  'Pages in the same browser context share cookies and storage. ' +
106
- 'Pages in different browser contexts are fully isolated.'),
107
+ 'Pages in different browser contexts are fully isolated (useful for clean-slate testing of cookies and authentication).'),
107
108
  ...timeoutSchema,
108
109
  },
109
110
  blockedByDialog: false,
110
111
  verifyFilesSchema: {},
111
112
  handler: async (request, response, context) => {
113
+ validateUrl(request.params.url, {
114
+ javascriptEvaluation: args?.javascriptEvaluation,
115
+ categoryExtensions: args?.categoryExtensions,
116
+ });
112
117
  const page = await context.newPage(request.params.background, request.params.isolatedContext);
113
118
  await page.waitForEventsAfterAction(async () => {
114
119
  await page.pptrPage.goto(request.params.url, {
@@ -120,7 +125,7 @@ export const newPage = defineTool(() => {
120
125
  },
121
126
  };
122
127
  });
123
- export const navigatePage = definePageTool(() => {
128
+ export const navigatePage = definePageTool(args => {
124
129
  return {
125
130
  name: 'navigate_page',
126
131
  description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
@@ -142,10 +147,14 @@ export const navigatePage = definePageTool(() => {
142
147
  .enum(['accept', 'dismiss'])
143
148
  .optional()
144
149
  .describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
145
- initScript: zod
146
- .string()
147
- .optional()
148
- .describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
150
+ ...(args?.javascriptEvaluation !== false
151
+ ? {
152
+ initScript: zod
153
+ .string()
154
+ .optional()
155
+ .describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
156
+ }
157
+ : {}),
149
158
  ...timeoutSchema,
150
159
  },
151
160
  blockedByDialog: false,
@@ -161,6 +170,12 @@ export const navigatePage = definePageTool(() => {
161
170
  if (!request.params.type) {
162
171
  request.params.type = 'url';
163
172
  }
173
+ if (request.params.url) {
174
+ validateUrl(request.params.url, {
175
+ javascriptEvaluation: args?.javascriptEvaluation,
176
+ categoryExtensions: args?.categoryExtensions,
177
+ });
178
+ }
164
179
  let initScriptId;
165
180
  if (request.params.initScript) {
166
181
  const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
@@ -239,7 +254,7 @@ export const navigatePage = definePageTool(() => {
239
254
  });
240
255
  export const resizePage = definePageTool({
241
256
  name: 'resize_page',
242
- description: `Resizes the selected page's window so that the page has specified dimension`,
257
+ description: `Resizes the page's window so that the page has specified dimension`,
243
258
  annotations: {
244
259
  category: ToolCategory.EMULATION,
245
260
  readOnlyHint: false,
@@ -335,15 +350,11 @@ export const getTabId = definePageTool({
335
350
  readOnlyHint: true,
336
351
  conditions: ['experimentalInteropTools'],
337
352
  },
338
- schema: {
339
- pageId: zod
340
- .number()
341
- .describe(`The ID of the page to get the tab ID for. Call ${listPages().name} to get available pages.`),
342
- },
353
+ schema: {},
343
354
  blockedByDialog: false,
344
355
  verifyFilesSchema: {},
345
- handler: async (request, response, context) => {
346
- const page = context.getPageById(request.params.pageId);
356
+ handler: async (request, response) => {
357
+ const page = request.page;
347
358
  const tabId = page.pptrPage._tabId;
348
359
  response.setTabId(tabId);
349
360
  response.appendResponseLine(`Tab ID: ${tabId}`);
@@ -15,7 +15,7 @@ const filePathSchema = zod
15
15
  .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
16
  export const startTrace = definePageTool({
17
17
  name: 'performance_start_trace',
18
- description: `Start a performance trace on the selected webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.`,
18
+ description: `Start a performance trace on the target webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.`,
19
19
  annotations: {
20
20
  category: ToolCategory.PERFORMANCE,
21
21
  readOnlyHint: false,
@@ -24,7 +24,7 @@ export const startTrace = definePageTool({
24
24
  reload: zod
25
25
  .boolean()
26
26
  .default(true)
27
- .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
+ .describe('Determines if, once tracing has started, the target 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.'),
28
28
  autoStop: zod
29
29
  .boolean()
30
30
  .default(true)
@@ -60,6 +60,7 @@ export const startTrace = definePageTool({
60
60
  ];
61
61
  await page.pptrPage.tracing.start({
62
62
  categories,
63
+ bufferSize: 1200 * 1000,
63
64
  });
64
65
  if (request.params.reload) {
65
66
  await page.pptrPage.goto(pageUrlForTracing, {
@@ -96,7 +97,7 @@ export const startTrace = definePageTool({
96
97
  });
97
98
  export const stopTrace = definePageTool({
98
99
  name: 'performance_stop_trace',
99
- description: 'Stop the active performance trace recording on the selected webpage.',
100
+ description: 'Stop the active performance trace recording on the target webpage.',
100
101
  annotations: {
101
102
  category: ToolCategory.PERFORMANCE,
102
103
  readOnlyHint: false,
@@ -16,7 +16,7 @@ async function generateTempFilePath() {
16
16
  const supportedExtensions = ['.webm', '.mp4'];
17
17
  export const startScreencast = definePageTool(args => ({
18
18
  name: 'screencast_start',
19
- description: `Starts recording a screencast (video) of the selected page in specified format.`,
19
+ description: `Starts recording a screencast (video) of the target page in specified format.`,
20
20
  annotations: {
21
21
  category: ToolCategory.DEBUGGING,
22
22
  readOnlyHint: false,
@@ -62,6 +62,7 @@ export const startScreencast = definePageTool(args => ({
62
62
  path: resolvedPath,
63
63
  format: format,
64
64
  ffmpegPath: args?.experimentalFfmpegPath,
65
+ fps: args?.experimentalScreencastFps,
65
66
  });
66
67
  }
67
68
  catch (err) {
@@ -91,7 +92,7 @@ export const startScreencast = definePageTool(args => ({
91
92
  }));
92
93
  export const stopScreencast = definePageTool({
93
94
  name: 'screencast_stop',
94
- description: 'Stops the active screencast recording on the selected page.',
95
+ description: 'Stops the active screencast recording on the target page.',
95
96
  annotations: {
96
97
  category: ToolCategory.DEBUGGING,
97
98
  readOnlyHint: false,
@@ -60,22 +60,41 @@ import { ToolCategory } from './categories.js';
60
60
  import { definePageTool } from './ToolDefinition.js';
61
61
  async function getSourceBox(page, element, fullPage) {
62
62
  if (element) {
63
- const box = await element.boundingBox();
64
- return box ?? undefined;
63
+ const viewport = page.viewport();
64
+ const [box, devicePixelRatio] = await Promise.all([
65
+ element.boundingBox(),
66
+ viewport
67
+ ? (viewport.deviceScaleFactor ?? 1)
68
+ : page.evaluate(() => window.devicePixelRatio),
69
+ ]);
70
+ return box ? { ...box, devicePixelRatio } : undefined;
65
71
  }
66
72
  if (fullPage) {
67
73
  const dims = await page.evaluate(() => ({
68
74
  width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
69
75
  height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
76
+ devicePixelRatio: window.devicePixelRatio,
70
77
  }));
71
78
  if (dims.width <= 0 || dims.height <= 0) {
72
79
  return undefined;
73
80
  }
74
- return { x: 0, y: 0, width: dims.width, height: dims.height };
81
+ return {
82
+ x: 0,
83
+ y: 0,
84
+ width: dims.width,
85
+ height: dims.height,
86
+ devicePixelRatio: dims.devicePixelRatio,
87
+ };
75
88
  }
76
89
  const viewport = page.viewport();
77
90
  if (viewport) {
78
- return { x: 0, y: 0, width: viewport.width, height: viewport.height };
91
+ return {
92
+ x: 0,
93
+ y: 0,
94
+ width: viewport.width,
95
+ height: viewport.height,
96
+ devicePixelRatio: viewport.deviceScaleFactor ?? 1,
97
+ };
79
98
  }
80
99
  // The browser is launched and connected with `defaultViewport: null`, so
81
100
  // `page.viewport()` stays null until something emulates one. Fall back to the
@@ -83,21 +102,33 @@ async function getSourceBox(page, element, fullPage) {
83
102
  const dims = await page.evaluate(() => ({
84
103
  width: window.innerWidth,
85
104
  height: window.innerHeight,
105
+ devicePixelRatio: window.devicePixelRatio,
86
106
  }));
87
107
  if (dims.width <= 0 || dims.height <= 0) {
88
108
  return undefined;
89
109
  }
90
- return { x: 0, y: 0, width: dims.width, height: dims.height };
110
+ return {
111
+ x: 0,
112
+ y: 0,
113
+ width: dims.width,
114
+ height: dims.height,
115
+ devicePixelRatio: dims.devicePixelRatio,
116
+ };
91
117
  }
92
118
  function computeDownscaleClip(box, maxWidth, maxHeight) {
93
- const widthScale = maxWidth !== undefined ? Math.min(1, maxWidth / box.width) : 1;
94
- const heightScale = maxHeight !== undefined ? Math.min(1, maxHeight / box.height) : 1;
119
+ const widthScale = maxWidth !== undefined
120
+ ? Math.min(1, maxWidth / (box.width * box.devicePixelRatio))
121
+ : 1;
122
+ const heightScale = maxHeight !== undefined
123
+ ? Math.min(1, maxHeight / (box.height * box.devicePixelRatio))
124
+ : 1;
95
125
  const scale = Math.min(widthScale, heightScale);
96
126
  if (scale >= 1) {
97
127
  return undefined;
98
128
  }
99
129
  // Skip degenerate sub-pixel results.
100
- if (Math.round(box.width * scale) < 1 || Math.round(box.height * scale) < 1) {
130
+ if (Math.round(box.width * box.devicePixelRatio * scale) < 1 ||
131
+ Math.round(box.height * box.devicePixelRatio * scale) < 1) {
101
132
  return undefined;
102
133
  }
103
134
  return {
@@ -61,14 +61,24 @@ import { defineTool, pageIdSchema } from './ToolDefinition.js';
61
61
  export const evaluateScript = defineTool(cliArgs => {
62
62
  return {
63
63
  name: 'evaluate_script',
64
- description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
64
+ description: `Evaluate a JavaScript function inside the target page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
65
65
  annotations: {
66
66
  category: ToolCategory.DEBUGGING,
67
67
  readOnlyHint: false,
68
+ conditions: ['javascriptEvaluation'],
68
69
  },
69
70
  schema: {
70
- ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
71
- function: zod.string().describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
71
+ ...(cliArgs?.pageIdRouting
72
+ ? cliArgs.categoryExtensions
73
+ ? {
74
+ pageId: zod
75
+ .number()
76
+ .optional()
77
+ .describe('Targets a specific page by ID. Required when not evaluating in a service worker.'),
78
+ }
79
+ : pageIdSchema
80
+ : {}),
81
+ function: zod.string().describe(`A JavaScript function declaration to be executed by the tool in the target page.
72
82
  Example without arguments: \`() => document.title\` or \`async () => await fetch("example.com")\`.
73
83
  Example with arguments: \`(el) => el.innerText\`
74
84
  `),
@@ -131,7 +141,10 @@ Example with arguments: \`(el) => el.innerText\`
131
141
  response.attachWaitForResult(result);
132
142
  return;
133
143
  }
134
- const mcpPage = cliArgs?.experimentalPageIdRouting
144
+ if (cliArgs?.categoryExtensions && cliArgs?.pageIdRouting && !pageId) {
145
+ throw new Error('specify either a pageId or a serviceWorkerId.');
146
+ }
147
+ const mcpPage = cliArgs?.pageIdRouting && request.params.pageId
135
148
  ? context.getPageById(request.params.pageId)
136
149
  : context.getSelectedMcpPage();
137
150
  const page = mcpPage.pptrPage;