@playwright/mcp 0.0.22 → 0.0.24

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 CHANGED
@@ -207,7 +207,7 @@ And then in MCP client config, set the `url` to the SSE endpoint:
207
207
  "mcpServers": {
208
208
  "playwright": {
209
209
  "command": "docker",
210
- "args": ["run", "-i", "--rm", "--init", "mcp/playwright"]
210
+ "args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"]
211
211
  }
212
212
  }
213
213
  }
@@ -216,7 +216,7 @@ And then in MCP client config, set the `url` to the SSE endpoint:
216
216
  You can build the Docker image yourself.
217
217
 
218
218
  ```
219
- docker build -t mcp/playwright .
219
+ docker build -t mcr.microsoft.com/playwright/mcp .
220
220
  ```
221
221
 
222
222
  ### Programmatic usage
@@ -224,14 +224,14 @@ docker build -t mcp/playwright .
224
224
  ```js
225
225
  import http from 'http';
226
226
 
227
- import { createServer } from '@playwright/mcp';
227
+ import { createConnection } from '@playwright/mcp';
228
228
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
229
229
 
230
230
  http.createServer(async (req, res) => {
231
231
  // ...
232
232
 
233
233
  // Creates a headless Playwright MCP server with SSE transport
234
- const connection = await createConnection({ headless: true });
234
+ const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
235
235
  const transport = new SSEServerTransport('/messages', res);
236
236
  await connection.connect(transport);
237
237
 
@@ -341,6 +341,7 @@ X Y coordinate space, based on the provided screenshot.
341
341
  - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
342
342
  - Parameters:
343
343
  - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
344
+ - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
344
345
  - `element` (string, optional): 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.
345
346
  - `ref` (string, optional): 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.
346
347
  - Read-only: **true**
@@ -501,7 +502,8 @@ X Y coordinate space, based on the provided screenshot.
501
502
  - **browser_pdf_save**
502
503
  - Title: Save as PDF
503
504
  - Description: Save page as PDF
504
- - Parameters: None
505
+ - Parameters:
506
+ - `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.
505
507
  - Read-only: **true**
506
508
 
507
509
  ### Utilities
@@ -516,11 +518,13 @@ X Y coordinate space, based on the provided screenshot.
516
518
 
517
519
  <!-- NOTE: This has been generated via update-readme.js -->
518
520
 
519
- - **browser_wait**
520
- - Title: Wait
521
- - Description: Wait for a specified time in seconds
521
+ - **browser_wait_for**
522
+ - Title: Wait for
523
+ - Description: Wait for text to appear or disappear or a specified time to pass
522
524
  - Parameters:
523
- - `time` (number): The time to wait in seconds
525
+ - `time` (number, optional): The time to wait in seconds
526
+ - `text` (string, optional): The text to wait for
527
+ - `textGone` (string, optional): The text to wait for to disappear
524
528
  - Read-only: **true**
525
529
 
526
530
  <!-- NOTE: This has been generated via update-readme.js -->
package/config.d.ts CHANGED
@@ -40,7 +40,7 @@ export type Config = {
40
40
  *
41
41
  * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
42
42
  */
43
- launchOptions?: playwright.BrowserLaunchOptions;
43
+ launchOptions?: playwright.LaunchOptions;
44
44
 
45
45
  /**
46
46
  * Context options for the browser context.
package/index.js CHANGED
@@ -16,4 +16,4 @@
16
16
  */
17
17
 
18
18
  import { createConnection } from './lib/index';
19
- export default { createConnection };
19
+ export { createConnection };
package/lib/config.js CHANGED
@@ -72,7 +72,7 @@ export async function configFromCLIOptions(cliOptions) {
72
72
  headless: cliOptions.headless,
73
73
  };
74
74
  if (browserName === 'chromium')
75
- launchOptions.webSocketPort = await findFreePort();
75
+ launchOptions.cdpPort = await findFreePort();
76
76
  const contextOptions = cliOptions.device ? devices[cliOptions.device] : undefined;
77
77
  return {
78
78
  browser: {
@@ -138,7 +138,7 @@ function mergeConfig(base, overrides) {
138
138
  ...pickDefined(overrides.browser?.contextOptions),
139
139
  },
140
140
  };
141
- if (browser.browserName !== 'chromium')
141
+ if (browser.browserName !== 'chromium' && browser.launchOptions)
142
142
  delete browser.launchOptions.channel;
143
143
  return {
144
144
  ...pickDefined(base),
package/lib/context.js CHANGED
@@ -100,7 +100,7 @@ export class Context {
100
100
  }
101
101
  async run(tool, params) {
102
102
  // Tab management is done outside of the action() call.
103
- const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
103
+ const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
104
104
  const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
105
105
  const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
106
106
  if (resultOverride)
@@ -316,7 +316,7 @@ async function createUserDataDir(browserConfig) {
316
316
  cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
317
317
  else
318
318
  throw new Error('Unsupported platform: ' + process.platform);
319
- const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions.channel ?? browserConfig?.browserName}-profile`);
319
+ const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
320
320
  await fs.promises.mkdir(result, { recursive: true });
321
321
  return result;
322
322
  }
package/lib/index.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import { createConnection as createConnectionImpl } from './connection.js';
16
17
  export async function createConnection(config = {}) {
17
- return createConnection(config);
18
+ return createConnectionImpl(config);
18
19
  }
@@ -13,22 +13,22 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import yaml from 'yaml';
17
16
  export class PageSnapshot {
18
- _frameLocators = [];
17
+ _page;
19
18
  _text;
20
- constructor() {
19
+ constructor(page) {
20
+ this._page = page;
21
21
  }
22
22
  static async create(page) {
23
- const snapshot = new PageSnapshot();
24
- await snapshot._build(page);
23
+ const snapshot = new PageSnapshot(page);
24
+ await snapshot._build();
25
25
  return snapshot;
26
26
  }
27
27
  text() {
28
28
  return this._text;
29
29
  }
30
- async _build(page) {
31
- const yamlDocument = await this._snapshotFrame(page);
30
+ async _build() {
31
+ const yamlDocument = await this._page._snapshotForAI();
32
32
  this._text = [
33
33
  `- Page Snapshot`,
34
34
  '```yaml',
@@ -36,54 +36,7 @@ export class PageSnapshot {
36
36
  '```',
37
37
  ].join('\n');
38
38
  }
39
- async _snapshotFrame(frame) {
40
- const frameIndex = this._frameLocators.push(frame) - 1;
41
- const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
42
- const snapshot = yaml.parseDocument(snapshotString);
43
- const visit = async (node) => {
44
- if (yaml.isPair(node)) {
45
- await Promise.all([
46
- visit(node.key).then(k => node.key = k),
47
- visit(node.value).then(v => node.value = v)
48
- ]);
49
- }
50
- else if (yaml.isSeq(node) || yaml.isMap(node)) {
51
- node.items = await Promise.all(node.items.map(visit));
52
- }
53
- else if (yaml.isScalar(node)) {
54
- if (typeof node.value === 'string') {
55
- const value = node.value;
56
- if (frameIndex > 0)
57
- node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
58
- if (value.startsWith('iframe ')) {
59
- const ref = value.match(/\[ref=(.*)\]/)?.[1];
60
- if (ref) {
61
- try {
62
- const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
63
- return snapshot.createPair(node.value, childSnapshot);
64
- }
65
- catch (error) {
66
- return snapshot.createPair(node.value, '<could not take iframe snapshot>');
67
- }
68
- }
69
- }
70
- }
71
- }
72
- return node;
73
- };
74
- await visit(snapshot.contents);
75
- return snapshot;
76
- }
77
39
  refLocator(ref) {
78
- let frame = this._frameLocators[0];
79
- const match = ref.match(/^f(\d+)(.*)/);
80
- if (match) {
81
- const frameIndex = parseInt(match[1], 10);
82
- frame = this._frameLocators[frameIndex];
83
- ref = match[2];
84
- }
85
- if (!frame)
86
- throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
87
- return frame.locator(`aria-ref=${ref}`);
40
+ return this._page.locator(`aria-ref=${ref}`);
88
41
  }
89
42
  }
package/lib/tab.js CHANGED
@@ -17,7 +17,7 @@ import { PageSnapshot } from './pageSnapshot.js';
17
17
  export class Tab {
18
18
  context;
19
19
  page;
20
- _console = [];
20
+ _consoleMessages = [];
21
21
  _requests = new Map();
22
22
  _snapshot;
23
23
  _onPageClose;
@@ -25,13 +25,9 @@ export class Tab {
25
25
  this.context = context;
26
26
  this.page = page;
27
27
  this._onPageClose = onPageClose;
28
- page.on('console', event => this._console.push(event));
28
+ page.on('console', event => this._consoleMessages.push(event));
29
29
  page.on('request', request => this._requests.set(request, null));
30
30
  page.on('response', response => this._requests.set(response.request(), response));
31
- page.on('framenavigated', frame => {
32
- if (!frame.parentFrame())
33
- this._clearCollectedArtifacts();
34
- });
35
31
  page.on('close', () => this._onClose());
36
32
  page.on('filechooser', chooser => {
37
33
  this.context.setModalState({
@@ -48,7 +44,7 @@ export class Tab {
48
44
  page.setDefaultTimeout(5000);
49
45
  }
50
46
  _clearCollectedArtifacts() {
51
- this._console.length = 0;
47
+ this._consoleMessages.length = 0;
52
48
  this._requests.clear();
53
49
  }
54
50
  _onClose() {
@@ -56,6 +52,7 @@ export class Tab {
56
52
  this._onPageClose(this);
57
53
  }
58
54
  async navigate(url) {
55
+ this._clearCollectedArtifacts();
59
56
  const downloadEvent = this.page.waitForEvent('download').catch(() => { });
60
57
  try {
61
58
  await this.page.goto(url, { waitUntil: 'domcontentloaded' });
@@ -85,8 +82,8 @@ export class Tab {
85
82
  throw new Error('No snapshot available');
86
83
  return this._snapshot;
87
84
  }
88
- console() {
89
- return this._console;
85
+ consoleMessages() {
86
+ return this._consoleMessages;
90
87
  }
91
88
  requests() {
92
89
  return this._requests;
@@ -18,18 +18,37 @@ import { defineTool } from './tool.js';
18
18
  const wait = captureSnapshot => defineTool({
19
19
  capability: 'wait',
20
20
  schema: {
21
- name: 'browser_wait',
22
- title: 'Wait',
23
- description: 'Wait for a specified time in seconds',
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
24
  inputSchema: z.object({
25
- time: z.number().describe('The time to wait in seconds'),
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'),
26
28
  }),
27
29
  type: 'readOnly',
28
30
  },
29
31
  handle: async (context, params) => {
30
- await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
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
+ }
31
50
  return {
32
- code: [`// Waited for ${params.time} seconds`],
51
+ code,
33
52
  captureSnapshot,
34
53
  waitForNetwork: false,
35
54
  };
@@ -25,7 +25,7 @@ const console = defineTool({
25
25
  type: 'readOnly',
26
26
  },
27
27
  handle: async (context) => {
28
- const messages = context.currentTabOrDie().console();
28
+ const messages = context.currentTabOrDie().consoleMessages();
29
29
  const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
30
30
  return {
31
31
  code: [`// <internal code to get console messages>`],
@@ -28,7 +28,7 @@ const install = defineTool({
28
28
  type: 'destructive',
29
29
  },
30
30
  handle: async (context) => {
31
- const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
31
+ const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
32
32
  const cliUrl = import.meta.resolve('playwright/package.json');
33
33
  const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
34
34
  const child = fork(cliPath, ['install', channel], {
package/lib/tools/pdf.js CHANGED
@@ -17,18 +17,21 @@ import { z } from 'zod';
17
17
  import { defineTool } from './tool.js';
18
18
  import * as javascript from '../javascript.js';
19
19
  import { outputFile } from '../config.js';
20
+ const pdfSchema = z.object({
21
+ filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
22
+ });
20
23
  const pdf = defineTool({
21
24
  capability: 'pdf',
22
25
  schema: {
23
26
  name: 'browser_pdf_save',
24
27
  title: 'Save as PDF',
25
28
  description: 'Save page as PDF',
26
- inputSchema: z.object({}),
29
+ inputSchema: pdfSchema,
27
30
  type: 'readOnly',
28
31
  },
29
- handle: async (context) => {
32
+ handle: async (context, params) => {
30
33
  const tab = context.currentTabOrDie();
31
- const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
34
+ const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
32
35
  const code = [
33
36
  `// Save page as ${fileName}`,
34
37
  `await page.pdf(${javascript.formatObject({ path: fileName })});`,
@@ -188,6 +188,7 @@ const selectOption = defineTool({
188
188
  });
189
189
  const screenshotSchema = z.object({
190
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.'),
191
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.'),
192
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.'),
193
194
  }).refine(data => {
@@ -209,7 +210,7 @@ const screenshot = defineTool({
209
210
  const tab = context.currentTabOrDie();
210
211
  const snapshot = tab.snapshotOrDie();
211
212
  const fileType = params.raw ? 'png' : 'jpeg';
212
- const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.${fileType}`);
213
+ const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
213
214
  const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
214
215
  const isElementScreenshot = params.element && params.ref;
215
216
  const code = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
@@ -37,14 +37,13 @@
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.11.0",
39
39
  "commander": "^13.1.0",
40
- "playwright": "1.53.0-alpha-1746218818000",
41
- "yaml": "^2.7.1",
40
+ "playwright": "1.53.0-alpha-1746832516000",
42
41
  "zod-to-json-schema": "^3.24.4"
43
42
  },
44
43
  "devDependencies": {
45
44
  "@eslint/eslintrc": "^3.2.0",
46
45
  "@eslint/js": "^9.19.0",
47
- "@playwright/test": "1.53.0-alpha-1746218818000",
46
+ "@playwright/test": "1.53.0-alpha-1746832516000",
48
47
  "@stylistic/eslint-plugin": "^3.0.1",
49
48
  "@types/node": "^22.13.10",
50
49
  "@typescript-eslint/eslint-plugin": "^8.26.1",