chrome-devtools-mcp 1.2.0 → 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.
@@ -148,4 +148,80 @@ export const closeHeapSnapshot = defineTool({
148
148
  response.appendResponseLine(`Closed heap snapshot: ${request.params.filePath}`);
149
149
  },
150
150
  });
151
+ export const getHeapSnapshotRetainingPaths = defineTool({
152
+ name: 'get_heapsnapshot_retaining_paths',
153
+ description: 'Loads a memory heapsnapshot and returns retaining paths for a specific node ID. This helps to understand why a node is not being garbage collected.',
154
+ annotations: {
155
+ category: ToolCategory.MEMORY,
156
+ readOnlyHint: true,
157
+ conditions: ['memoryDebugging'],
158
+ },
159
+ verifyFilesSchema: ['filePath'],
160
+ blockedByDialog: false,
161
+ schema: {
162
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
163
+ nodeId: zod.number().describe('The node ID to get retaining paths for.'),
164
+ maxDepth: zod
165
+ .number()
166
+ .optional()
167
+ .describe('The maximum depth to search for retaining paths.'),
168
+ maxNodes: zod
169
+ .number()
170
+ .optional()
171
+ .describe('The maximum number of nodes to return.'),
172
+ maxSiblings: zod
173
+ .number()
174
+ .optional()
175
+ .describe('The maximum number of siblings to return.'),
176
+ },
177
+ handler: async (request, response, context) => {
178
+ const retainingPaths = await context.getHeapSnapshotRetainingPaths(request.params.filePath, request.params.nodeId, request.params.maxDepth, request.params.maxNodes, request.params.maxSiblings);
179
+ response.setHeapSnapshotRetainingPaths(retainingPaths);
180
+ },
181
+ });
182
+ export const getHeapSnapshotEdges = defineTool({
183
+ name: 'get_heapsnapshot_edges',
184
+ description: 'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID.',
185
+ annotations: {
186
+ category: ToolCategory.MEMORY,
187
+ readOnlyHint: true,
188
+ conditions: ['memoryDebugging'],
189
+ },
190
+ blockedByDialog: false,
191
+ verifyFilesSchema: ['filePath'],
192
+ schema: {
193
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
194
+ nodeId: zod.number().describe('The node ID to get outgoing edges for.'),
195
+ pageIdx: zod.number().optional().describe('The page index for pagination.'),
196
+ pageSize: zod.number().optional().describe('The page size for pagination.'),
197
+ },
198
+ handler: async (request, response, context) => {
199
+ const edges = await context.getHeapSnapshotEdges(request.params.filePath, request.params.nodeId);
200
+ response.setHeapSnapshotNodes(edges, {
201
+ pageIdx: request.params.pageIdx,
202
+ pageSize: request.params.pageSize,
203
+ });
204
+ },
205
+ });
206
+ export const getHeapSnapshotDominators = defineTool({
207
+ name: 'get_heapsnapshot_dominators',
208
+ description: 'Loads a memory heapsnapshot and returns the dominator chain for a specific node ID. This helps to identify which objects are keeping the target node alive.',
209
+ annotations: {
210
+ category: ToolCategory.MEMORY,
211
+ readOnlyHint: true,
212
+ conditions: ['memoryDebugging'],
213
+ },
214
+ blockedByDialog: false,
215
+ verifyFilesSchema: ['filePath'],
216
+ schema: {
217
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
218
+ nodeId: zod
219
+ .number()
220
+ .describe('The node ID to get the dominator chain for.'),
221
+ },
222
+ handler: async (request, response, context) => {
223
+ const dominators = await context.getHeapSnapshotDominators(request.params.filePath, request.params.nodeId);
224
+ response.setHeapSnapshotDominators(dominators);
225
+ },
226
+ });
151
227
  //# sourceMappingURL=memory.js.map
@@ -36,16 +36,23 @@ export const startScreencast = definePageTool(args => ({
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. ' +
@@ -82,6 +102,7 @@ export const stopScreencast = definePageTool({
82
102
  handler: async (_request, response, context) => {
83
103
  const data = context.getScreenRecorder();
84
104
  if (!data) {
105
+ response.appendResponseLine('Error: no active screencast recording to stop.');
85
106
  return;
86
107
  }
87
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
- verifyFilesSchema: ['filePath'],
43
- handler: async (request, response, context) => {
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
@@ -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.2.0';
8
+ export const VERSION = '1.3.0';
9
9
  // x-release-please-end
10
10
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,7 +63,7 @@
63
63
  "@types/yargs": "^17.0.33",
64
64
  "@typescript-eslint/eslint-plugin": "^8.43.0",
65
65
  "@typescript-eslint/parser": "^8.43.0",
66
- "chrome-devtools-frontend": "1.0.1641723",
66
+ "chrome-devtools-frontend": "1.0.1646286",
67
67
  "core-js": "3.49.0",
68
68
  "debug": "4.4.3",
69
69
  "eslint": "^9.35.0",
@@ -73,7 +73,7 @@
73
73
  "lighthouse": "13.3.0",
74
74
  "prettier": "^3.6.2",
75
75
  "puppeteer": "25.1.0",
76
- "rollup": "4.61.0",
76
+ "rollup": "4.61.1",
77
77
  "rollup-plugin-cleanup": "^3.2.1",
78
78
  "rollup-plugin-license": "^3.6.0",
79
79
  "semver": "^7.7.4",