@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/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
- const contextOptions = cliOptions.device ? devices[cliOptions.device] : undefined;
77
- return {
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, screenshotTools } from './tools.js';
20
+ import { snapshotTools, visionTools } from './tools.js';
21
21
  export async function createConnection(config) {
22
- const allTools = config.vision ? screenshotTools : snapshotTools;
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
- _browser;
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 context = await this._ensureBrowserContext();
76
+ const { browserContext } = await this._ensureBrowserContext();
79
77
  if (!this._currentTab)
80
- await context.newPage();
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 (this._browserContext && !this._tabs.length)
227
+ if (!this._tabs.length)
230
228
  void this.close();
231
229
  }
232
230
  async close() {
233
- if (!this._browserContext)
231
+ if (!this._browserContextPromise)
234
232
  return;
235
- const browserContext = this._browserContext;
236
- const browser = this._browser;
237
- this._createBrowserContextPromise = undefined;
238
- this._browserContext = undefined;
239
- this._browser = undefined;
240
- await browserContext?.close().then(async () => {
241
- await browser?.close();
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
- async _ensureBrowserContext() {
256
- if (!this._browserContext) {
257
- const context = await this._createBrowserContext();
258
- this._browser = context.browser;
259
- this._browserContext = context.browserContext;
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._createBrowserContextPromise;
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 _innerCreateBrowserContext() {
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
- const browserContext = await launchPersistentContext(this.config.browser);
293
- return { browserContext };
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
- return await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
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('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
24
- .option('--caps <caps>', 'Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
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('--executable-path <path>', 'Path to the browser executable.')
27
- .option('--headless', 'Run browser in headless mode, headed by default')
28
- .option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
29
- .option('--user-data-dir <path>', 'Path to the user data directory')
30
- .option('--port <port>', 'Port to listen on for SSE transport.')
31
- .option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
32
- .option('--allowed-origins <origins>', 'Semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
33
- .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)
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 = [];
@@ -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: [`// Internal to close the page`],
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
+ ];
@@ -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 { outputFile } from '../config.js';
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
  ];
@@ -67,3 +67,6 @@ export function sanitizeForFilePath(s) {
67
67
  return sanitize(s);
68
68
  return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
69
69
  }
70
+ export async function generateLocator(locator) {
71
+ return locator._generateLocatorString();
72
+ }
@@ -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 screen from './tools/screen.js';
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 screenshotTools = [
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
File without changes