@playwright/mcp 0.0.24 → 0.0.25
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 +352 -179
- package/config.d.ts +5 -0
- package/lib/config.js +43 -3
- package/lib/connection.js +2 -2
- package/lib/context.js +48 -39
- package/lib/program.js +22 -13
- package/lib/tools/common.js +1 -41
- package/lib/tools/screenshot.js +77 -0
- package/lib/tools/snapshot.js +1 -59
- package/lib/tools/utils.js +3 -0
- package/lib/tools/wait.js +59 -0
- package/lib/tools.js +8 -3
- package/package.json +1 -1
- /package/lib/tools/{screen.js → vision.js} +0 -0
package/config.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type Config = {
|
|
|
28
28
|
*/
|
|
29
29
|
browserName?: 'chromium' | 'firefox' | 'webkit';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Keep the browser profile in memory, do not save it to disk.
|
|
33
|
+
*/
|
|
34
|
+
isolated?: boolean;
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Path to a user data directory for browser profile persistence.
|
|
33
38
|
* Temporary directory is created by default.
|
package/lib/config.js
CHANGED
|
@@ -25,6 +25,7 @@ const defaultConfig = {
|
|
|
25
25
|
launchOptions: {
|
|
26
26
|
channel: 'chrome',
|
|
27
27
|
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
|
28
|
+
chromiumSandbox: true,
|
|
28
29
|
},
|
|
29
30
|
contextOptions: {
|
|
30
31
|
viewport: null,
|
|
@@ -66,17 +67,51 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
66
67
|
browserName = 'chromium';
|
|
67
68
|
channel = 'chrome';
|
|
68
69
|
}
|
|
70
|
+
// Launch options
|
|
69
71
|
const launchOptions = {
|
|
70
72
|
channel,
|
|
71
73
|
executablePath: cliOptions.executablePath,
|
|
72
74
|
headless: cliOptions.headless,
|
|
73
75
|
};
|
|
74
|
-
if (browserName === 'chromium')
|
|
76
|
+
if (browserName === 'chromium') {
|
|
75
77
|
launchOptions.cdpPort = await findFreePort();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if (!cliOptions.sandbox) {
|
|
79
|
+
// --no-sandbox was passed, disable the sandbox
|
|
80
|
+
launchOptions.chromiumSandbox = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cliOptions.proxyServer) {
|
|
84
|
+
launchOptions.proxy = {
|
|
85
|
+
server: cliOptions.proxyServer
|
|
86
|
+
};
|
|
87
|
+
if (cliOptions.proxyBypass)
|
|
88
|
+
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
89
|
+
}
|
|
90
|
+
// Context options
|
|
91
|
+
const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
92
|
+
if (cliOptions.storageState)
|
|
93
|
+
contextOptions.storageState = cliOptions.storageState;
|
|
94
|
+
if (cliOptions.userAgent)
|
|
95
|
+
contextOptions.userAgent = cliOptions.userAgent;
|
|
96
|
+
if (cliOptions.viewportSize) {
|
|
97
|
+
try {
|
|
98
|
+
const [width, height] = cliOptions.viewportSize.split(',').map(n => +n);
|
|
99
|
+
if (isNaN(width) || isNaN(height))
|
|
100
|
+
throw new Error('bad values');
|
|
101
|
+
contextOptions.viewport = { width, height };
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (cliOptions.ignoreHttpsErrors)
|
|
108
|
+
contextOptions.ignoreHTTPSErrors = true;
|
|
109
|
+
if (cliOptions.blockServiceWorkers)
|
|
110
|
+
contextOptions.serviceWorkers = 'block';
|
|
111
|
+
const result = {
|
|
78
112
|
browser: {
|
|
79
113
|
browserName,
|
|
114
|
+
isolated: cliOptions.isolated,
|
|
80
115
|
userDataDir: cliOptions.userDataDir,
|
|
81
116
|
launchOptions,
|
|
82
117
|
contextOptions,
|
|
@@ -94,6 +129,11 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
94
129
|
},
|
|
95
130
|
outputDir: cliOptions.outputDir,
|
|
96
131
|
};
|
|
132
|
+
if (!cliOptions.imageResponses) {
|
|
133
|
+
// --no-image-responses was passed, disable image responses
|
|
134
|
+
result.noImageResponses = true;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
97
137
|
}
|
|
98
138
|
async function findFreePort() {
|
|
99
139
|
return new Promise((resolve, reject) => {
|
package/lib/connection.js
CHANGED
|
@@ -17,9 +17,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
19
19
|
import { Context, packageJSON } from './context.js';
|
|
20
|
-
import { snapshotTools,
|
|
20
|
+
import { snapshotTools, visionTools } from './tools.js';
|
|
21
21
|
export async function createConnection(config) {
|
|
22
|
-
const allTools = config.vision ?
|
|
22
|
+
const allTools = config.vision ? visionTools : snapshotTools;
|
|
23
23
|
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
24
24
|
const context = new Context(tools, config);
|
|
25
25
|
const server = new Server({ name: 'Playwright', version: packageJSON.version }, {
|
package/lib/context.js
CHANGED
|
@@ -25,9 +25,7 @@ import { outputFile } from './config.js';
|
|
|
25
25
|
export class Context {
|
|
26
26
|
tools;
|
|
27
27
|
config;
|
|
28
|
-
|
|
29
|
-
_browserContext;
|
|
30
|
-
_createBrowserContextPromise;
|
|
28
|
+
_browserContextPromise;
|
|
31
29
|
_tabs = [];
|
|
32
30
|
_currentTab;
|
|
33
31
|
_modalStates = [];
|
|
@@ -65,7 +63,7 @@ export class Context {
|
|
|
65
63
|
return this._currentTab;
|
|
66
64
|
}
|
|
67
65
|
async newTab() {
|
|
68
|
-
const browserContext = await this._ensureBrowserContext();
|
|
66
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
69
67
|
const page = await browserContext.newPage();
|
|
70
68
|
this._currentTab = this._tabs.find(t => t.page === page);
|
|
71
69
|
return this._currentTab;
|
|
@@ -75,9 +73,9 @@ export class Context {
|
|
|
75
73
|
await this._currentTab.page.bringToFront();
|
|
76
74
|
}
|
|
77
75
|
async ensureTab() {
|
|
78
|
-
const
|
|
76
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
79
77
|
if (!this._currentTab)
|
|
80
|
-
await
|
|
78
|
+
await browserContext.newPage();
|
|
81
79
|
return this._currentTab;
|
|
82
80
|
}
|
|
83
81
|
async listTabsMarkdown() {
|
|
@@ -226,20 +224,19 @@ ${code.join('\n')}
|
|
|
226
224
|
this._tabs.splice(index, 1);
|
|
227
225
|
if (this._currentTab === tab)
|
|
228
226
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
229
|
-
if (
|
|
227
|
+
if (!this._tabs.length)
|
|
230
228
|
void this.close();
|
|
231
229
|
}
|
|
232
230
|
async close() {
|
|
233
|
-
if (!this.
|
|
231
|
+
if (!this._browserContextPromise)
|
|
234
232
|
return;
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}).catch(() => { });
|
|
233
|
+
const promise = this._browserContextPromise;
|
|
234
|
+
this._browserContextPromise = undefined;
|
|
235
|
+
await promise.then(async ({ browserContext, browser }) => {
|
|
236
|
+
await browserContext.close().then(async () => {
|
|
237
|
+
await browser?.close();
|
|
238
|
+
}).catch(() => { });
|
|
239
|
+
});
|
|
243
240
|
}
|
|
244
241
|
async _setupRequestInterception(context) {
|
|
245
242
|
if (this.config.network?.allowedOrigins?.length) {
|
|
@@ -252,28 +249,24 @@ ${code.join('\n')}
|
|
|
252
249
|
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
|
253
250
|
}
|
|
254
251
|
}
|
|
255
|
-
|
|
256
|
-
if (!this.
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
await this._setupRequestInterception(this._browserContext);
|
|
261
|
-
for (const page of this._browserContext.pages())
|
|
262
|
-
this._onPageCreated(page);
|
|
263
|
-
this._browserContext.on('page', page => this._onPageCreated(page));
|
|
264
|
-
}
|
|
265
|
-
return this._browserContext;
|
|
266
|
-
}
|
|
267
|
-
async _createBrowserContext() {
|
|
268
|
-
if (!this._createBrowserContextPromise) {
|
|
269
|
-
this._createBrowserContextPromise = this._innerCreateBrowserContext();
|
|
270
|
-
void this._createBrowserContextPromise.catch(() => {
|
|
271
|
-
this._createBrowserContextPromise = undefined;
|
|
252
|
+
_ensureBrowserContext() {
|
|
253
|
+
if (!this._browserContextPromise) {
|
|
254
|
+
this._browserContextPromise = this._setupBrowserContext();
|
|
255
|
+
this._browserContextPromise.catch(() => {
|
|
256
|
+
this._browserContextPromise = undefined;
|
|
272
257
|
});
|
|
273
258
|
}
|
|
274
|
-
return this.
|
|
259
|
+
return this._browserContextPromise;
|
|
260
|
+
}
|
|
261
|
+
async _setupBrowserContext() {
|
|
262
|
+
const { browser, browserContext } = await this._createBrowserContext();
|
|
263
|
+
await this._setupRequestInterception(browserContext);
|
|
264
|
+
for (const page of browserContext.pages())
|
|
265
|
+
this._onPageCreated(page);
|
|
266
|
+
browserContext.on('page', page => this._onPageCreated(page));
|
|
267
|
+
return { browser, browserContext };
|
|
275
268
|
}
|
|
276
|
-
async
|
|
269
|
+
async _createBrowserContext() {
|
|
277
270
|
if (this.config.browser?.remoteEndpoint) {
|
|
278
271
|
const url = new URL(this.config.browser?.remoteEndpoint);
|
|
279
272
|
if (this.config.browser.browserName)
|
|
@@ -286,11 +279,26 @@ ${code.join('\n')}
|
|
|
286
279
|
}
|
|
287
280
|
if (this.config.browser?.cdpEndpoint) {
|
|
288
281
|
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
289
|
-
const browserContext = browser.contexts()[0];
|
|
282
|
+
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
290
283
|
return { browser, browserContext };
|
|
291
284
|
}
|
|
292
|
-
|
|
293
|
-
|
|
285
|
+
return this.config.browser?.isolated ?
|
|
286
|
+
await createIsolatedContext(this.config.browser) :
|
|
287
|
+
await launchPersistentContext(this.config.browser);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function createIsolatedContext(browserConfig) {
|
|
291
|
+
try {
|
|
292
|
+
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
293
|
+
const browserType = playwright[browserName];
|
|
294
|
+
const browser = await browserType.launch(browserConfig?.launchOptions);
|
|
295
|
+
const browserContext = await browser.newContext(browserConfig?.contextOptions);
|
|
296
|
+
return { browser, browserContext };
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error.message.includes('Executable doesn\'t exist'))
|
|
300
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
301
|
+
throw error;
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
async function launchPersistentContext(browserConfig) {
|
|
@@ -298,7 +306,8 @@ async function launchPersistentContext(browserConfig) {
|
|
|
298
306
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
299
307
|
const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
300
308
|
const browserType = playwright[browserName];
|
|
301
|
-
|
|
309
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
|
310
|
+
return { browserContext };
|
|
302
311
|
}
|
|
303
312
|
catch (error) {
|
|
304
313
|
if (error.message.includes('Executable doesn\'t exist'))
|
package/lib/program.js
CHANGED
|
@@ -20,21 +20,30 @@ import { packageJSON } from './context.js';
|
|
|
20
20
|
program
|
|
21
21
|
.version('Version ' + packageJSON.version)
|
|
22
22
|
.name(packageJSON.name)
|
|
23
|
-
.option('--
|
|
24
|
-
.option('--
|
|
23
|
+
.option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
|
24
|
+
.option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
|
25
|
+
.option('--block-service-workers', 'block service workers')
|
|
26
|
+
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
27
|
+
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
|
25
28
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
26
|
-
.option('--
|
|
27
|
-
.option('--
|
|
28
|
-
.option('--
|
|
29
|
-
.option('--
|
|
30
|
-
.option('--
|
|
31
|
-
.option('--
|
|
32
|
-
.option('--
|
|
33
|
-
.option('--
|
|
29
|
+
.option('--config <path>', 'path to the configuration file.')
|
|
30
|
+
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
|
|
31
|
+
.option('--executable-path <path>', 'path to the browser executable.')
|
|
32
|
+
.option('--headless', 'run browser in headless mode, headed by default')
|
|
33
|
+
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
34
|
+
.option('--ignore-https-errors', 'ignore https errors')
|
|
35
|
+
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
36
|
+
.option('--no-image-responses', 'do not send image responses to the client.')
|
|
37
|
+
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
38
|
+
.option('--output-dir <path>', 'path to the directory for output files.')
|
|
39
|
+
.option('--port <port>', 'port to listen on for SSE transport.')
|
|
40
|
+
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
|
41
|
+
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
|
42
|
+
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
|
43
|
+
.option('--user-agent <ua string>', 'specify user agent string')
|
|
44
|
+
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
45
|
+
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
34
46
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
35
|
-
.option('--no-image-responses', 'Do not send image responses to the client.')
|
|
36
|
-
.option('--output-dir <path>', 'Path to the directory for output files.')
|
|
37
|
-
.option('--config <path>', 'Path to the configuration file.')
|
|
38
47
|
.action(async (options) => {
|
|
39
48
|
const config = await resolveConfig(options);
|
|
40
49
|
const connectionList = [];
|
package/lib/tools/common.js
CHANGED
|
@@ -15,45 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
|
-
const wait = captureSnapshot => defineTool({
|
|
19
|
-
capability: 'wait',
|
|
20
|
-
schema: {
|
|
21
|
-
name: 'browser_wait_for',
|
|
22
|
-
title: 'Wait for',
|
|
23
|
-
description: 'Wait for text to appear or disappear or a specified time to pass',
|
|
24
|
-
inputSchema: z.object({
|
|
25
|
-
time: z.number().optional().describe('The time to wait in seconds'),
|
|
26
|
-
text: z.string().optional().describe('The text to wait for'),
|
|
27
|
-
textGone: z.string().optional().describe('The text to wait for to disappear'),
|
|
28
|
-
}),
|
|
29
|
-
type: 'readOnly',
|
|
30
|
-
},
|
|
31
|
-
handle: async (context, params) => {
|
|
32
|
-
if (!params.text && !params.textGone && !params.time)
|
|
33
|
-
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
-
const code = [];
|
|
35
|
-
if (params.time) {
|
|
36
|
-
code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
|
-
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
38
|
-
}
|
|
39
|
-
const tab = context.currentTabOrDie();
|
|
40
|
-
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
|
-
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
|
-
if (goneLocator) {
|
|
43
|
-
code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
|
-
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
|
-
}
|
|
46
|
-
if (locator) {
|
|
47
|
-
code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
|
-
await locator.waitFor({ state: 'visible' });
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
code,
|
|
52
|
-
captureSnapshot,
|
|
53
|
-
waitForNetwork: false,
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
18
|
const close = defineTool({
|
|
58
19
|
capability: 'core',
|
|
59
20
|
schema: {
|
|
@@ -66,7 +27,7 @@ const close = defineTool({
|
|
|
66
27
|
handle: async (context) => {
|
|
67
28
|
await context.close();
|
|
68
29
|
return {
|
|
69
|
-
code: [
|
|
30
|
+
code: [`await page.close()`],
|
|
70
31
|
captureSnapshot: false,
|
|
71
32
|
waitForNetwork: false,
|
|
72
33
|
};
|
|
@@ -103,6 +64,5 @@ const resize = captureSnapshot => defineTool({
|
|
|
103
64
|
});
|
|
104
65
|
export default (captureSnapshot) => [
|
|
105
66
|
close,
|
|
106
|
-
wait(captureSnapshot),
|
|
107
67
|
resize(captureSnapshot)
|
|
108
68
|
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tool.js';
|
|
18
|
+
import * as javascript from '../javascript.js';
|
|
19
|
+
import { outputFile } from '../config.js';
|
|
20
|
+
import { generateLocator } from './utils.js';
|
|
21
|
+
const screenshotSchema = z.object({
|
|
22
|
+
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
|
23
|
+
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
|
|
24
|
+
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
|
|
25
|
+
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
|
|
26
|
+
}).refine(data => {
|
|
27
|
+
return !!data.element === !!data.ref;
|
|
28
|
+
}, {
|
|
29
|
+
message: 'Both element and ref must be provided or neither.',
|
|
30
|
+
path: ['ref', 'element']
|
|
31
|
+
});
|
|
32
|
+
const screenshot = defineTool({
|
|
33
|
+
capability: 'core',
|
|
34
|
+
schema: {
|
|
35
|
+
name: 'browser_take_screenshot',
|
|
36
|
+
title: 'Take a screenshot',
|
|
37
|
+
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
|
38
|
+
inputSchema: screenshotSchema,
|
|
39
|
+
type: 'readOnly',
|
|
40
|
+
},
|
|
41
|
+
handle: async (context, params) => {
|
|
42
|
+
const tab = context.currentTabOrDie();
|
|
43
|
+
const snapshot = tab.snapshotOrDie();
|
|
44
|
+
const fileType = params.raw ? 'png' : 'jpeg';
|
|
45
|
+
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
46
|
+
const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
|
|
47
|
+
const isElementScreenshot = params.element && params.ref;
|
|
48
|
+
const code = [
|
|
49
|
+
`// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`,
|
|
50
|
+
];
|
|
51
|
+
const locator = params.ref ? snapshot.refLocator(params.ref) : null;
|
|
52
|
+
if (locator)
|
|
53
|
+
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
54
|
+
else
|
|
55
|
+
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
56
|
+
const includeBase64 = !context.config.noImageResponses;
|
|
57
|
+
const action = async () => {
|
|
58
|
+
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
|
59
|
+
return {
|
|
60
|
+
content: includeBase64 ? [{
|
|
61
|
+
type: 'image',
|
|
62
|
+
data: screenshot.toString('base64'),
|
|
63
|
+
mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
|
64
|
+
}] : []
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
code,
|
|
69
|
+
action,
|
|
70
|
+
captureSnapshot: true,
|
|
71
|
+
waitForNetwork: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
export default [
|
|
76
|
+
screenshot,
|
|
77
|
+
];
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
18
|
import * as javascript from '../javascript.js';
|
|
19
|
-
import {
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
20
|
const snapshot = defineTool({
|
|
21
21
|
capability: 'core',
|
|
22
22
|
schema: {
|
|
@@ -186,63 +186,6 @@ const selectOption = defineTool({
|
|
|
186
186
|
};
|
|
187
187
|
},
|
|
188
188
|
});
|
|
189
|
-
const screenshotSchema = z.object({
|
|
190
|
-
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
|
191
|
-
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
|
|
192
|
-
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
|
|
193
|
-
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
|
|
194
|
-
}).refine(data => {
|
|
195
|
-
return !!data.element === !!data.ref;
|
|
196
|
-
}, {
|
|
197
|
-
message: 'Both element and ref must be provided or neither.',
|
|
198
|
-
path: ['ref', 'element']
|
|
199
|
-
});
|
|
200
|
-
const screenshot = defineTool({
|
|
201
|
-
capability: 'core',
|
|
202
|
-
schema: {
|
|
203
|
-
name: 'browser_take_screenshot',
|
|
204
|
-
title: 'Take a screenshot',
|
|
205
|
-
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
|
206
|
-
inputSchema: screenshotSchema,
|
|
207
|
-
type: 'readOnly',
|
|
208
|
-
},
|
|
209
|
-
handle: async (context, params) => {
|
|
210
|
-
const tab = context.currentTabOrDie();
|
|
211
|
-
const snapshot = tab.snapshotOrDie();
|
|
212
|
-
const fileType = params.raw ? 'png' : 'jpeg';
|
|
213
|
-
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
214
|
-
const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
|
|
215
|
-
const isElementScreenshot = params.element && params.ref;
|
|
216
|
-
const code = [
|
|
217
|
-
`// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`,
|
|
218
|
-
];
|
|
219
|
-
const locator = params.ref ? snapshot.refLocator(params.ref) : null;
|
|
220
|
-
if (locator)
|
|
221
|
-
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
222
|
-
else
|
|
223
|
-
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
224
|
-
const includeBase64 = !context.config.noImageResponses;
|
|
225
|
-
const action = async () => {
|
|
226
|
-
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
|
227
|
-
return {
|
|
228
|
-
content: includeBase64 ? [{
|
|
229
|
-
type: 'image',
|
|
230
|
-
data: screenshot.toString('base64'),
|
|
231
|
-
mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
|
232
|
-
}] : []
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
return {
|
|
236
|
-
code,
|
|
237
|
-
action,
|
|
238
|
-
captureSnapshot: true,
|
|
239
|
-
waitForNetwork: false,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
export async function generateLocator(locator) {
|
|
244
|
-
return locator._generateLocatorString();
|
|
245
|
-
}
|
|
246
189
|
export default [
|
|
247
190
|
snapshot,
|
|
248
191
|
click,
|
|
@@ -250,5 +193,4 @@ export default [
|
|
|
250
193
|
hover,
|
|
251
194
|
type,
|
|
252
195
|
selectOption,
|
|
253
|
-
screenshot,
|
|
254
196
|
];
|
package/lib/tools/utils.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tool.js';
|
|
18
|
+
const wait = captureSnapshot => defineTool({
|
|
19
|
+
capability: 'wait',
|
|
20
|
+
schema: {
|
|
21
|
+
name: 'browser_wait_for',
|
|
22
|
+
title: 'Wait for',
|
|
23
|
+
description: 'Wait for text to appear or disappear or a specified time to pass',
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
time: z.number().optional().describe('The time to wait in seconds'),
|
|
26
|
+
text: z.string().optional().describe('The text to wait for'),
|
|
27
|
+
textGone: z.string().optional().describe('The text to wait for to disappear'),
|
|
28
|
+
}),
|
|
29
|
+
type: 'readOnly',
|
|
30
|
+
},
|
|
31
|
+
handle: async (context, params) => {
|
|
32
|
+
if (!params.text && !params.textGone && !params.time)
|
|
33
|
+
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
+
const code = [];
|
|
35
|
+
if (params.time) {
|
|
36
|
+
code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
|
+
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
38
|
+
}
|
|
39
|
+
const tab = context.currentTabOrDie();
|
|
40
|
+
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
|
+
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
|
+
if (goneLocator) {
|
|
43
|
+
code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
|
+
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
|
+
}
|
|
46
|
+
if (locator) {
|
|
47
|
+
code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
|
+
await locator.waitFor({ state: 'visible' });
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
code,
|
|
52
|
+
captureSnapshot,
|
|
53
|
+
waitForNetwork: false,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
export default (captureSnapshot) => [
|
|
58
|
+
wait(captureSnapshot),
|
|
59
|
+
];
|
package/lib/tools.js
CHANGED
|
@@ -24,8 +24,10 @@ import network from './tools/network.js';
|
|
|
24
24
|
import pdf from './tools/pdf.js';
|
|
25
25
|
import snapshot from './tools/snapshot.js';
|
|
26
26
|
import tabs from './tools/tabs.js';
|
|
27
|
-
import
|
|
27
|
+
import screenshot from './tools/screenshot.js';
|
|
28
28
|
import testing from './tools/testing.js';
|
|
29
|
+
import vision from './tools/vision.js';
|
|
30
|
+
import wait from './tools/wait.js';
|
|
29
31
|
export const snapshotTools = [
|
|
30
32
|
...common(true),
|
|
31
33
|
...console,
|
|
@@ -36,11 +38,13 @@ export const snapshotTools = [
|
|
|
36
38
|
...navigate(true),
|
|
37
39
|
...network,
|
|
38
40
|
...pdf,
|
|
41
|
+
...screenshot,
|
|
39
42
|
...snapshot,
|
|
40
43
|
...tabs(true),
|
|
41
44
|
...testing,
|
|
45
|
+
...wait(true),
|
|
42
46
|
];
|
|
43
|
-
export const
|
|
47
|
+
export const visionTools = [
|
|
44
48
|
...common(false),
|
|
45
49
|
...console,
|
|
46
50
|
...dialogs(false),
|
|
@@ -50,7 +54,8 @@ export const screenshotTools = [
|
|
|
50
54
|
...navigate(false),
|
|
51
55
|
...network,
|
|
52
56
|
...pdf,
|
|
53
|
-
...screen,
|
|
54
57
|
...tabs(false),
|
|
55
58
|
...testing,
|
|
59
|
+
...vision,
|
|
60
|
+
...wait(false),
|
|
56
61
|
];
|
package/package.json
CHANGED
|
File without changes
|