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.
- package/README.md +22 -2
- package/build/src/HeapSnapshotManager.js +25 -16
- package/build/src/McpContext.js +35 -1
- package/build/src/McpResponse.js +77 -7
- package/build/src/PageCollector.js +1 -1
- package/build/src/bin/chrome-devtools-cli-options.js +84 -0
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +45 -1
- package/build/src/{DevToolsConnectionAdapter.js → devtools/DevToolsConnectionAdapter.js} +1 -1
- package/build/src/{DevtoolsUtils.js → devtools/DevtoolsUtils.js} +75 -71
- package/build/src/devtools/McpHostBindingAdapter.js +165 -0
- package/build/src/formatters/ConsoleFormatter.js +1 -1
- package/build/src/formatters/HeapSnapshotFormatter.js +22 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +2 -2
- package/build/src/third_party/bundled-packages.json +1 -1
- package/build/src/third_party/devtools-formatter-worker.js +0 -1
- package/build/src/third_party/devtools-heap-snapshot-worker.js +54 -3
- package/build/src/third_party/index.js +449 -196
- package/build/src/tools/memory.js +76 -0
- package/build/src/tools/screencast.js +30 -9
- package/build/src/tools/screenshot.js +158 -76
- package/build/src/version.js +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
package/build/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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",
|