chrome-devtools-mcp 0.21.0 → 0.23.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 +87 -21
- package/build/src/HeapSnapshotManager.js +94 -0
- package/build/src/McpContext.js +26 -181
- package/build/src/McpPage.js +214 -0
- package/build/src/McpResponse.js +151 -13
- package/build/src/PageCollector.js +10 -24
- package/build/src/TextSnapshot.js +230 -0
- package/build/src/WaitForHelper.js +31 -0
- package/build/src/bin/check-latest-version.js +25 -0
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
- package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
- package/build/src/bin/chrome-devtools.js +25 -14
- package/build/src/bin/cliDefinitions.js +14 -8
- package/build/src/daemon/client.js +11 -11
- package/build/src/daemon/daemon.js +6 -9
- package/build/src/daemon/utils.js +19 -14
- package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
- package/build/src/formatters/NetworkFormatter.js +24 -7
- package/build/src/index.js +12 -1
- package/build/src/telemetry/ClearcutLogger.js +34 -12
- package/build/src/telemetry/flagUtils.js +46 -4
- package/build/src/telemetry/toolMetricsUtils.js +88 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
- package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
- package/build/src/third_party/bundled-packages.json +6 -4
- package/build/src/third_party/devtools-formatter-worker.js +61 -64
- package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
- package/build/src/third_party/index.js +62661 -60590
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
- package/build/src/tools/categories.js +3 -0
- package/build/src/tools/console.js +42 -39
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +5 -11
- package/build/src/tools/inPage.js +3 -13
- package/build/src/tools/input.js +15 -16
- package/build/src/tools/lighthouse.js +2 -2
- package/build/src/tools/memory.js +48 -3
- package/build/src/tools/network.js +4 -4
- package/build/src/tools/pages.js +212 -146
- package/build/src/tools/performance.js +1 -1
- package/build/src/tools/screencast.js +20 -8
- package/build/src/tools/screenshot.js +3 -3
- package/build/src/tools/script.js +22 -16
- package/build/src/tools/tools.js +2 -0
- package/build/src/tools/webmcp.js +63 -0
- package/build/src/utils/check-for-updates.js +73 -0
- package/build/src/utils/files.js +4 -0
- package/build/src/utils/id.js +15 -0
- package/build/src/version.js +1 -1
- package/package.json +13 -8
- package/build/src/utils/ExtensionRegistry.js +0 -35
package/build/src/tools/pages.js
CHANGED
|
@@ -7,10 +7,55 @@ import { logger } from '../logger.js';
|
|
|
7
7
|
import { zod } from '../third_party/index.js';
|
|
8
8
|
import { ToolCategory } from './categories.js';
|
|
9
9
|
import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './ToolDefinition.js';
|
|
10
|
+
async function navigateWithInterception(page, action, allowListString, timeout) {
|
|
11
|
+
const allowList = allowListString
|
|
12
|
+
? allowListString.split(',').map((p) => new URLPattern(p.trim()))
|
|
13
|
+
: undefined;
|
|
14
|
+
const requestHandler = (interceptedRequest) => {
|
|
15
|
+
if (!interceptedRequest.isNavigationRequest()) {
|
|
16
|
+
void interceptedRequest.continue();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const requestUrl = interceptedRequest.url();
|
|
20
|
+
const isAllowed = allowList.some((pattern) => pattern.test(requestUrl));
|
|
21
|
+
if (isAllowed) {
|
|
22
|
+
void interceptedRequest.continue();
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger(`Blocking request to: ${requestUrl}`);
|
|
26
|
+
void interceptedRequest.abort('blockedbyclient');
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const cleanupInterception = async () => {
|
|
30
|
+
if (allowList) {
|
|
31
|
+
page.pptrPage.off('request', requestHandler);
|
|
32
|
+
await page.pptrPage.setRequestInterception(false).catch(error => {
|
|
33
|
+
logger(`Failed to disable request interception`, error);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if (allowList) {
|
|
38
|
+
await page.pptrPage.setRequestInterception(true);
|
|
39
|
+
page.pptrPage.on('request', requestHandler);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await page.waitForEventsAfterAction(async () => {
|
|
43
|
+
try {
|
|
44
|
+
await action();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await cleanupInterception();
|
|
48
|
+
}
|
|
49
|
+
}, { timeout });
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await cleanupInterception();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
10
55
|
export const listPages = defineTool(args => {
|
|
11
56
|
return {
|
|
12
57
|
name: 'list_pages',
|
|
13
|
-
description: `Get a list of pages
|
|
58
|
+
description: `Get a list of pages${args?.categoryExtensions ? ' including extension service workers' : ''} open in the browser.`,
|
|
14
59
|
annotations: {
|
|
15
60
|
category: ToolCategory.NAVIGATION,
|
|
16
61
|
readOnlyHint: true,
|
|
@@ -19,6 +64,7 @@ export const listPages = defineTool(args => {
|
|
|
19
64
|
handler: async (_request, response) => {
|
|
20
65
|
response.setIncludePages(true);
|
|
21
66
|
response.setListInPageTools();
|
|
67
|
+
response.setListWebMcpTools();
|
|
22
68
|
},
|
|
23
69
|
};
|
|
24
70
|
});
|
|
@@ -43,6 +89,7 @@ export const selectPage = defineTool({
|
|
|
43
89
|
context.selectPage(page);
|
|
44
90
|
response.setIncludePages(true);
|
|
45
91
|
response.setListInPageTools();
|
|
92
|
+
response.setListWebMcpTools();
|
|
46
93
|
if (request.params.bringToFront) {
|
|
47
94
|
await page.pptrPage.bringToFront();
|
|
48
95
|
}
|
|
@@ -76,158 +123,177 @@ export const closePage = defineTool({
|
|
|
76
123
|
response.setListInPageTools();
|
|
77
124
|
},
|
|
78
125
|
});
|
|
79
|
-
export const newPage = defineTool({
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
export const newPage = defineTool(args => {
|
|
127
|
+
return {
|
|
128
|
+
name: 'new_page',
|
|
129
|
+
description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
|
|
130
|
+
annotations: {
|
|
131
|
+
category: ToolCategory.NAVIGATION,
|
|
132
|
+
readOnlyHint: false,
|
|
133
|
+
},
|
|
134
|
+
schema: {
|
|
135
|
+
url: zod.string().describe('URL to load in a new page.'),
|
|
136
|
+
background: zod
|
|
137
|
+
.boolean()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
|
|
140
|
+
isolatedContext: zod
|
|
141
|
+
.string()
|
|
142
|
+
.optional()
|
|
143
|
+
.describe('If specified, the page is created in an isolated browser context with the given name. ' +
|
|
144
|
+
'Pages in the same browser context share cookies and storage. ' +
|
|
145
|
+
'Pages in different browser contexts are fully isolated.'),
|
|
146
|
+
...(args?.experimentalNavigationAllowlist
|
|
147
|
+
? {
|
|
148
|
+
allowList: zod
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.'),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
...timeoutSchema,
|
|
155
|
+
},
|
|
156
|
+
handler: async (request, response, context) => {
|
|
157
|
+
const page = await context.newPage(request.params.background, request.params.isolatedContext);
|
|
158
|
+
await navigateWithInterception(page, () => page.pptrPage.goto(request.params.url, {
|
|
104
159
|
timeout: request.params.timeout,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
160
|
+
}), request.params.allowList, request.params.timeout);
|
|
161
|
+
response.setIncludePages(true);
|
|
162
|
+
response.setListInPageTools();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
110
165
|
});
|
|
111
|
-
export const navigatePage = definePageTool({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!request.params.type && !request.params.url) {
|
|
144
|
-
throw new Error('Either URL or a type is required.');
|
|
145
|
-
}
|
|
146
|
-
if (!request.params.type) {
|
|
147
|
-
request.params.type = 'url';
|
|
148
|
-
}
|
|
149
|
-
const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
|
|
150
|
-
const dialogHandler = (dialog) => {
|
|
151
|
-
if (dialog.type() === 'beforeunload') {
|
|
152
|
-
if (handleBeforeUnload === 'accept') {
|
|
153
|
-
response.appendResponseLine(`Accepted a beforeunload dialog.`);
|
|
154
|
-
void dialog.accept();
|
|
166
|
+
export const navigatePage = definePageTool(args => {
|
|
167
|
+
return {
|
|
168
|
+
name: 'navigate_page',
|
|
169
|
+
description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
|
|
170
|
+
annotations: {
|
|
171
|
+
category: ToolCategory.NAVIGATION,
|
|
172
|
+
readOnlyHint: false,
|
|
173
|
+
},
|
|
174
|
+
schema: {
|
|
175
|
+
type: zod
|
|
176
|
+
.enum(['url', 'back', 'forward', 'reload'])
|
|
177
|
+
.optional()
|
|
178
|
+
.describe('Navigate the page by URL, back or forward in history, or reload.'),
|
|
179
|
+
url: zod.string().optional().describe('Target URL (only type=url)'),
|
|
180
|
+
ignoreCache: zod
|
|
181
|
+
.boolean()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe('Whether to ignore cache on reload.'),
|
|
184
|
+
handleBeforeUnload: zod
|
|
185
|
+
.enum(['accept', 'decline'])
|
|
186
|
+
.optional()
|
|
187
|
+
.describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
|
|
188
|
+
initScript: zod
|
|
189
|
+
.string()
|
|
190
|
+
.optional()
|
|
191
|
+
.describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
|
|
192
|
+
...(args?.experimentalNavigationAllowlist
|
|
193
|
+
? {
|
|
194
|
+
allowList: zod
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe('Optional comma-separated list of URL patterns to allow. If provided, all other navigations will be blocked.'),
|
|
155
198
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
199
|
+
: {}),
|
|
200
|
+
...timeoutSchema,
|
|
201
|
+
},
|
|
202
|
+
handler: async (request, response) => {
|
|
203
|
+
const page = request.page;
|
|
204
|
+
const options = {
|
|
205
|
+
timeout: request.params.timeout,
|
|
206
|
+
};
|
|
207
|
+
if (!request.params.type && !request.params.url) {
|
|
208
|
+
throw new Error('Either URL or a type is required.');
|
|
209
|
+
}
|
|
210
|
+
if (!request.params.type) {
|
|
211
|
+
request.params.type = 'url';
|
|
212
|
+
}
|
|
213
|
+
const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
|
|
214
|
+
const dialogHandler = (dialog) => {
|
|
215
|
+
if (dialog.type() === 'beforeunload') {
|
|
216
|
+
if (handleBeforeUnload === 'accept') {
|
|
217
|
+
response.appendResponseLine(`Accepted a beforeunload dialog.`);
|
|
218
|
+
void dialog.accept();
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
response.appendResponseLine(`Declined a beforeunload dialog.`);
|
|
222
|
+
void dialog.dismiss();
|
|
223
|
+
}
|
|
224
|
+
// We are not going to report the dialog like regular dialogs.
|
|
225
|
+
page.clearDialog();
|
|
159
226
|
}
|
|
160
|
-
|
|
161
|
-
|
|
227
|
+
};
|
|
228
|
+
let initScriptId;
|
|
229
|
+
if (request.params.initScript) {
|
|
230
|
+
const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
|
|
231
|
+
initScriptId = identifier;
|
|
162
232
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
page.pptrPage.on('dialog', dialogHandler);
|
|
234
|
+
try {
|
|
235
|
+
await navigateWithInterception(page, async () => {
|
|
236
|
+
switch (request.params.type) {
|
|
237
|
+
case 'url':
|
|
238
|
+
if (!request.params.url) {
|
|
239
|
+
throw new Error('A URL is required for navigation of type=url.');
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await page.pptrPage.goto(request.params.url, options);
|
|
243
|
+
response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case 'back':
|
|
250
|
+
try {
|
|
251
|
+
await page.pptrPage.goBack(options);
|
|
252
|
+
response.appendResponseLine(`Successfully navigated back to ${page.pptrPage.url()}.`);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case 'forward':
|
|
259
|
+
try {
|
|
260
|
+
await page.pptrPage.goForward(options);
|
|
261
|
+
response.appendResponseLine(`Successfully navigated forward to ${page.pptrPage.url()}.`);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
case 'reload':
|
|
268
|
+
try {
|
|
269
|
+
await page.pptrPage.reload({
|
|
270
|
+
...options,
|
|
271
|
+
ignoreCache: request.params.ignoreCache,
|
|
272
|
+
});
|
|
273
|
+
response.appendResponseLine(`Successfully reloaded the page.`);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}, request.params.allowList, request.params.timeout);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
page.pptrPage.off('dialog', dialogHandler);
|
|
284
|
+
if (initScriptId) {
|
|
285
|
+
await page.pptrPage
|
|
286
|
+
.removeScriptToEvaluateOnNewDocument(initScriptId)
|
|
287
|
+
.catch(error => {
|
|
288
|
+
logger(`Failed to remove init script`, error);
|
|
289
|
+
});
|
|
215
290
|
}
|
|
216
|
-
}, { timeout: request.params.timeout });
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
page.pptrPage.off('dialog', dialogHandler);
|
|
220
|
-
if (initScriptId) {
|
|
221
|
-
await page.pptrPage
|
|
222
|
-
.removeScriptToEvaluateOnNewDocument(initScriptId)
|
|
223
|
-
.catch(error => {
|
|
224
|
-
logger(`Failed to remove init script`, error);
|
|
225
|
-
});
|
|
226
291
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
292
|
+
response.setIncludePages(true);
|
|
293
|
+
response.setListInPageTools();
|
|
294
|
+
response.setListWebMcpTools();
|
|
295
|
+
},
|
|
296
|
+
};
|
|
231
297
|
});
|
|
232
298
|
export const resizePage = definePageTool({
|
|
233
299
|
name: 'resize_page',
|
|
@@ -142,7 +142,7 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
-
const file = await context.saveFile(dataToWrite, filePath);
|
|
145
|
+
const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.json.gz' : '.json');
|
|
146
146
|
response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
|
|
147
147
|
}
|
|
148
148
|
const result = await parseRawTraceBuffer(traceEventsBuffer);
|
|
@@ -7,39 +7,51 @@ import fs from 'node:fs/promises';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { zod } from '../third_party/index.js';
|
|
10
|
+
import { ensureExtension } from '../utils/files.js';
|
|
10
11
|
import { ToolCategory } from './categories.js';
|
|
11
12
|
import { definePageTool } from './ToolDefinition.js';
|
|
12
13
|
async function generateTempFilePath() {
|
|
13
14
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
|
|
14
15
|
return path.join(dir, `screencast.mp4`);
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
const supportedExtensions = ['.webm', '.mp4'];
|
|
18
|
+
export const startScreencast = definePageTool(args => ({
|
|
17
19
|
name: 'screencast_start',
|
|
18
|
-
description:
|
|
20
|
+
description: `Starts recording a screencast (video) of the selected page in specified format.`,
|
|
19
21
|
annotations: {
|
|
20
22
|
category: ToolCategory.DEBUGGING,
|
|
21
23
|
readOnlyHint: false,
|
|
22
24
|
conditions: ['screencast'],
|
|
23
25
|
},
|
|
24
26
|
schema: {
|
|
25
|
-
|
|
27
|
+
filePath: zod
|
|
26
28
|
.string()
|
|
27
29
|
.optional()
|
|
28
|
-
.describe(
|
|
30
|
+
.describe(`Output file path (${supportedExtensions.join(',')} are supported). Uses mkdtemp to generate a unique path if not provided.`),
|
|
29
31
|
},
|
|
30
32
|
handler: async (request, response, context) => {
|
|
31
33
|
if (context.getScreenRecorder() !== null) {
|
|
32
34
|
response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
|
-
const filePath = request.params.
|
|
36
|
-
|
|
37
|
+
const filePath = request.params.filePath ?? (await generateTempFilePath());
|
|
38
|
+
let enforcedExtension = '.mp4';
|
|
39
|
+
let format = 'mp4';
|
|
40
|
+
for (const supportedExtension of supportedExtensions) {
|
|
41
|
+
if (filePath.endsWith(supportedExtension)) {
|
|
42
|
+
enforcedExtension = supportedExtension;
|
|
43
|
+
format = supportedExtension.substring(1);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const resolvedPath = ensureExtension(path.resolve(filePath), enforcedExtension);
|
|
37
48
|
const page = request.page;
|
|
38
49
|
let recorder;
|
|
39
50
|
try {
|
|
40
51
|
recorder = await page.pptrPage.screencast({
|
|
41
52
|
path: resolvedPath,
|
|
42
|
-
format:
|
|
53
|
+
format: format,
|
|
54
|
+
ffmpegPath: args?.experimentalFfmpegPath,
|
|
43
55
|
});
|
|
44
56
|
}
|
|
45
57
|
catch (err) {
|
|
@@ -53,7 +65,7 @@ export const startScreencast = definePageTool({
|
|
|
53
65
|
context.setScreenRecorder({ recorder, filePath: resolvedPath });
|
|
54
66
|
response.appendResponseLine(`Screencast recording started. The recording will be saved to ${resolvedPath}. Use ${stopScreencast.name} to stop recording.`);
|
|
55
67
|
},
|
|
56
|
-
});
|
|
68
|
+
}));
|
|
57
69
|
export const stopScreencast = definePageTool({
|
|
58
70
|
name: 'screencast_stop',
|
|
59
71
|
description: 'Stops the active screencast recording on the selected page.',
|
|
@@ -28,7 +28,7 @@ export const screenshot = definePageTool({
|
|
|
28
28
|
uid: zod
|
|
29
29
|
.string()
|
|
30
30
|
.optional()
|
|
31
|
-
.describe('The uid of an element on the page from the page content snapshot. If omitted takes a
|
|
31
|
+
.describe('The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.'),
|
|
32
32
|
fullPage: zod
|
|
33
33
|
.boolean()
|
|
34
34
|
.optional()
|
|
@@ -67,8 +67,8 @@ export const screenshot = definePageTool({
|
|
|
67
67
|
response.appendResponseLine("Took a screenshot of the current page's viewport.");
|
|
68
68
|
}
|
|
69
69
|
if (request.params.filePath) {
|
|
70
|
-
const
|
|
71
|
-
response.appendResponseLine(`Saved screenshot to ${
|
|
70
|
+
const result = await context.saveFile(screenshot, request.params.filePath, `.${format}`);
|
|
71
|
+
response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
|
|
72
72
|
}
|
|
73
73
|
else if (screenshot.length >= 2_000_000) {
|
|
74
74
|
const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot.${request.params.format}`);
|
|
@@ -9,7 +9,7 @@ import { defineTool, pageIdSchema } from './ToolDefinition.js';
|
|
|
9
9
|
export const evaluateScript = defineTool(cliArgs => {
|
|
10
10
|
return {
|
|
11
11
|
name: 'evaluate_script',
|
|
12
|
-
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
|
|
12
|
+
description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON,
|
|
13
13
|
so returned values have to be JSON-serializable.`,
|
|
14
14
|
annotations: {
|
|
15
15
|
category: ToolCategory.DEBUGGING,
|
|
@@ -32,18 +32,22 @@ Example with arguments: \`(el) => {
|
|
|
32
32
|
.describe('The uid of an element on the page from the page content snapshot'))
|
|
33
33
|
.optional()
|
|
34
34
|
.describe(`An optional list of arguments to pass to the function.`),
|
|
35
|
+
dialogAction: zod
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.'),
|
|
35
39
|
...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
|
|
36
40
|
...(cliArgs?.categoryExtensions
|
|
37
41
|
? {
|
|
38
42
|
serviceWorkerId: zod
|
|
39
43
|
.string()
|
|
40
44
|
.optional()
|
|
41
|
-
.describe(`
|
|
45
|
+
.describe(`The optional service worker id to evaluate the script in. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a service worker.`),
|
|
42
46
|
}
|
|
43
47
|
: {}),
|
|
44
48
|
},
|
|
45
49
|
handler: async (request, response, context) => {
|
|
46
|
-
const { serviceWorkerId, args: uidArgs, function: fnString, pageId, } = request.params;
|
|
50
|
+
const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, } = request.params;
|
|
47
51
|
if (cliArgs?.categoryExtensions && serviceWorkerId) {
|
|
48
52
|
if (uidArgs && uidArgs.length > 0) {
|
|
49
53
|
throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
|
|
@@ -52,7 +56,9 @@ Example with arguments: \`(el) => {
|
|
|
52
56
|
throw new Error('specify either a pageId or a serviceWorkerId.');
|
|
53
57
|
}
|
|
54
58
|
const worker = await getWebWorker(context, serviceWorkerId);
|
|
55
|
-
await
|
|
59
|
+
await context.getSelectedMcpPage().waitForEventsAfterAction(async () => {
|
|
60
|
+
await performEvaluation(worker, fnString, [], response);
|
|
61
|
+
}, { handleDialog: dialogAction ?? 'accept' });
|
|
56
62
|
return;
|
|
57
63
|
}
|
|
58
64
|
const mcpPage = cliArgs?.experimentalPageIdRouting
|
|
@@ -68,7 +74,9 @@ Example with arguments: \`(el) => {
|
|
|
68
74
|
args.push(handle);
|
|
69
75
|
}
|
|
70
76
|
const evaluatable = await getPageOrFrame(page, frames);
|
|
71
|
-
await
|
|
77
|
+
await mcpPage.waitForEventsAfterAction(async () => {
|
|
78
|
+
await performEvaluation(evaluatable, fnString, args, response);
|
|
79
|
+
}, { handleDialog: dialogAction ?? 'accept' });
|
|
72
80
|
}
|
|
73
81
|
finally {
|
|
74
82
|
void Promise.allSettled(args.map(arg => arg.dispose()));
|
|
@@ -76,19 +84,17 @@ Example with arguments: \`(el) => {
|
|
|
76
84
|
},
|
|
77
85
|
};
|
|
78
86
|
});
|
|
79
|
-
const performEvaluation = async (evaluatable, fnString, args, response
|
|
87
|
+
const performEvaluation = async (evaluatable, fnString, args, response) => {
|
|
80
88
|
const fn = await evaluatable.evaluateHandle(`(${fnString})`);
|
|
81
89
|
try {
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
response.appendResponseLine('```');
|
|
91
|
-
});
|
|
90
|
+
const result = await evaluatable.evaluate(async (fn, ...args) => {
|
|
91
|
+
// @ts-expect-error no types for function fn
|
|
92
|
+
return JSON.stringify(await fn(...args));
|
|
93
|
+
}, fn, ...args);
|
|
94
|
+
response.appendResponseLine('Script ran on page and returned:');
|
|
95
|
+
response.appendResponseLine('```json');
|
|
96
|
+
response.appendResponseLine(`${result}`);
|
|
97
|
+
response.appendResponseLine('```');
|
|
92
98
|
}
|
|
93
99
|
finally {
|
|
94
100
|
void fn.dispose();
|
package/build/src/tools/tools.js
CHANGED
|
@@ -18,6 +18,7 @@ import * as screenshotTools from './screenshot.js';
|
|
|
18
18
|
import * as scriptTools from './script.js';
|
|
19
19
|
import * as slimTools from './slim/tools.js';
|
|
20
20
|
import * as snapshotTools from './snapshot.js';
|
|
21
|
+
import * as webmcpTools from './webmcp.js';
|
|
21
22
|
export const createTools = (args) => {
|
|
22
23
|
const rawTools = args.slim
|
|
23
24
|
? Object.values(slimTools)
|
|
@@ -36,6 +37,7 @@ export const createTools = (args) => {
|
|
|
36
37
|
...Object.values(screenshotTools),
|
|
37
38
|
...Object.values(scriptTools),
|
|
38
39
|
...Object.values(snapshotTools),
|
|
40
|
+
...Object.values(webmcpTools),
|
|
39
41
|
];
|
|
40
42
|
const tools = [];
|
|
41
43
|
for (const tool of rawTools) {
|