@playwright/mcp 0.0.29 → 0.0.30

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
@@ -10,7 +10,7 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit
10
10
 
11
11
  ### Requirements
12
12
  - Node.js 18 or newer
13
- - VS Code, Cursor, Windsurf, Claude Desktop or any other MCP client
13
+ - VS Code, Cursor, Windsurf, Claude Desktop, Goose or any other MCP client
14
14
 
15
15
  <!--
16
16
  // Generate using:
@@ -77,7 +77,7 @@ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, u
77
77
  <details>
78
78
  <summary><b>Install in Windsurf</b></summary>
79
79
 
80
- Follow Windsuff MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use following configuration:
80
+ Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use following configuration:
81
81
 
82
82
  ```js
83
83
  {
@@ -112,6 +112,28 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
112
112
  ```
113
113
  </details>
114
114
 
115
+ <details>
116
+ <summary><b>Install in Claude Code</b></summary>
117
+
118
+ Use the Claude Code CLI to add the Playwright MCP server:
119
+
120
+ ```bash
121
+ claude mcp add playwright npx @playwright/mcp@latest
122
+ ```
123
+ </details>
124
+
125
+ <details>
126
+ <summary><b>Install in Goose</b></summary>
127
+
128
+ #### Click the button to install:
129
+
130
+ [![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=%40playwright%2Fmcp%40latest&id=playwright&name=Playwright&description=Interact%20with%20web%20pages%20through%20structured%20accessibility%20snapshots%20using%20Playwright)
131
+
132
+ #### Or install manually:
133
+
134
+ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension".
135
+ </details>
136
+
115
137
  <details>
116
138
  <summary><b>Install in Qodo Gen</b></summary>
117
139
 
@@ -133,6 +155,25 @@ Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in
133
155
  Click <code>Save</code>.
134
156
  </details>
135
157
 
158
+ <details>
159
+ <summary><b>Install in Gemini CLI</b></summary>
160
+
161
+ Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use following configuration:
162
+
163
+ ```js
164
+ {
165
+ "mcpServers": {
166
+ "playwright": {
167
+ "command": "npx",
168
+ "args": [
169
+ "@playwright/mcp@latest"
170
+ ]
171
+ }
172
+ }
173
+ }
174
+ ```
175
+ </details>
176
+
136
177
  ### Configuration
137
178
 
138
179
  Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list:
@@ -316,9 +357,10 @@ npx @playwright/mcp@latest --config path/to/config.json
316
357
  };
317
358
 
318
359
  /**
319
- * Do not send image responses to the client.
360
+ * Whether to send image responses to the client. Can be "allow", "omit", or "auto".
361
+ * Defaults to "auto", images are omitted for Cursor clients and sent for all other clients.
320
362
  */
321
- noImageResponses?: boolean;
363
+ imageResponses?: 'allow' | 'omit' | 'auto';
322
364
  }
323
365
  ```
324
366
  </details>
@@ -436,6 +478,7 @@ X Y coordinate space, based on the provided screenshot.
436
478
  - Parameters:
437
479
  - `element` (string): Human-readable element description used to obtain permission to interact with the element
438
480
  - `ref` (string): Exact target element reference from the page snapshot
481
+ - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
439
482
  - Read-only: **false**
440
483
 
441
484
  <!-- NOTE: This has been generated via update-readme.js -->
package/lib/config.js CHANGED
@@ -88,6 +88,8 @@ export async function configFromCLIOptions(cliOptions) {
88
88
  if (cliOptions.proxyBypass)
89
89
  launchOptions.proxy.bypass = cliOptions.proxyBypass;
90
90
  }
91
+ if (cliOptions.device && cliOptions.cdpEndpoint)
92
+ throw new Error('Device emulation is not supported with cdpEndpoint.');
91
93
  // Context options
92
94
  const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
93
95
  if (cliOptions.storageState)
package/lib/program.js CHANGED
@@ -16,7 +16,7 @@
16
16
  import { program } from 'commander';
17
17
  // @ts-ignore
18
18
  import { startTraceViewerServer } from 'playwright-core/lib/server';
19
- import { startHttpTransport, startStdioTransport } from './transport.js';
19
+ import { startHttpServer, startHttpTransport, startStdioTransport } from './transport.js';
20
20
  import { resolveCLIConfig } from './config.js';
21
21
  import { Server } from './server.js';
22
22
  import { packageJSON } from './package.js';
@@ -51,10 +51,11 @@ program
51
51
  .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
52
52
  .action(async (options) => {
53
53
  const config = await resolveCLIConfig(options);
54
+ const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined;
54
55
  const server = new Server(config);
55
56
  server.setupExitWatchdog();
56
- if (config.server.port !== undefined)
57
- startHttpTransport(server);
57
+ if (httpServer)
58
+ startHttpTransport(httpServer, server);
58
59
  else
59
60
  await startStdioTransport(server);
60
61
  if (config.saveTrace) {
@@ -39,25 +39,33 @@ const elementSchema = z.object({
39
39
  element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
40
40
  ref: z.string().describe('Exact target element reference from the page snapshot'),
41
41
  });
42
+ const clickSchema = elementSchema.extend({
43
+ doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
44
+ });
42
45
  const click = defineTool({
43
46
  capability: 'core',
44
47
  schema: {
45
48
  name: 'browser_click',
46
49
  title: 'Click',
47
50
  description: 'Perform click on a web page',
48
- inputSchema: elementSchema,
51
+ inputSchema: clickSchema,
49
52
  type: 'destructive',
50
53
  },
51
54
  handle: async (context, params) => {
52
55
  const tab = context.currentTabOrDie();
53
56
  const locator = tab.snapshotOrDie().refLocator(params);
54
- const code = [
55
- `// Click ${params.element}`,
56
- `await page.${await generateLocator(locator)}.click();`
57
- ];
57
+ const code = [];
58
+ if (params.doubleClick) {
59
+ code.push(`// Double click ${params.element}`);
60
+ code.push(`await page.${await generateLocator(locator)}.dblclick();`);
61
+ }
62
+ else {
63
+ code.push(`// Click ${params.element}`);
64
+ code.push(`await page.${await generateLocator(locator)}.click();`);
65
+ }
58
66
  return {
59
67
  code,
60
- action: () => locator.click(),
68
+ action: () => params.doubleClick ? locator.dblclick() : locator.click(),
61
69
  captureSnapshot: true,
62
70
  waitForNetwork: true,
63
71
  };
@@ -66,7 +66,14 @@ export function sanitizeForFilePath(s) {
66
66
  return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
67
67
  }
68
68
  export async function generateLocator(locator) {
69
- return locator._generateLocatorString();
69
+ try {
70
+ return await locator._generateLocatorString();
71
+ }
72
+ catch (e) {
73
+ if (e instanceof Error && /locator._generateLocatorString: No element matching locator/.test(e.message))
74
+ throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.');
75
+ throw e;
76
+ }
70
77
  }
71
78
  export async function callOnPageNoTrace(page, callback) {
72
79
  return await page._wrapApiCall(() => callback(page), { internal: true });
package/lib/transport.js CHANGED
@@ -83,44 +83,51 @@ async function handleStreamable(server, req, res, sessions) {
83
83
  res.statusCode = 400;
84
84
  res.end('Invalid request');
85
85
  }
86
- export function startHttpTransport(server) {
86
+ export async function startHttpServer(config) {
87
+ const { host, port } = config;
88
+ const httpServer = http.createServer();
89
+ await new Promise((resolve, reject) => {
90
+ httpServer.on('error', reject);
91
+ httpServer.listen(port, host, () => {
92
+ resolve();
93
+ httpServer.removeListener('error', reject);
94
+ });
95
+ });
96
+ return httpServer;
97
+ }
98
+ export function startHttpTransport(httpServer, mcpServer) {
87
99
  const sseSessions = new Map();
88
100
  const streamableSessions = new Map();
89
- const httpServer = http.createServer(async (req, res) => {
101
+ httpServer.on('request', async (req, res) => {
90
102
  const url = new URL(`http://localhost${req.url}`);
91
103
  if (url.pathname.startsWith('/mcp'))
92
- await handleStreamable(server, req, res, streamableSessions);
104
+ await handleStreamable(mcpServer, req, res, streamableSessions);
93
105
  else
94
- await handleSSE(server, req, res, url, sseSessions);
106
+ await handleSSE(mcpServer, req, res, url, sseSessions);
95
107
  });
96
- const { host, port } = server.config.server;
97
- httpServer.listen(port, host, () => {
98
- const address = httpServer.address();
99
- assert(address, 'Could not bind server socket');
100
- let url;
101
- if (typeof address === 'string') {
102
- url = address;
103
- }
104
- else {
105
- const resolvedPort = address.port;
106
- let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
107
- if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
108
- resolvedHost = 'localhost';
109
- url = `http://${resolvedHost}:${resolvedPort}`;
110
- }
111
- const message = [
112
- `Listening on ${url}`,
113
- 'Put this in your client config:',
114
- JSON.stringify({
115
- 'mcpServers': {
116
- 'playwright': {
117
- 'url': `${url}/sse`
118
- }
108
+ const url = httpAddressToString(httpServer.address());
109
+ const message = [
110
+ `Listening on ${url}`,
111
+ 'Put this in your client config:',
112
+ JSON.stringify({
113
+ 'mcpServers': {
114
+ 'playwright': {
115
+ 'url': `${url}/sse`
119
116
  }
120
- }, undefined, 2),
121
- 'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
122
- ].join('\n');
123
- // eslint-disable-next-line no-console
124
- console.error(message);
125
- });
117
+ }
118
+ }, undefined, 2),
119
+ 'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
120
+ ].join('\n');
121
+ // eslint-disable-next-line no-console
122
+ console.error(message);
123
+ }
124
+ export function httpAddressToString(address) {
125
+ assert(address, 'Could not bind server socket');
126
+ if (typeof address === 'string')
127
+ return address;
128
+ const resolvedPort = address.port;
129
+ let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
130
+ if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
131
+ resolvedHost = 'localhost';
132
+ return `http://${resolvedHost}:${resolvedPort}`;
126
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,16 +40,19 @@
40
40
  "commander": "^13.1.0",
41
41
  "debug": "^4.4.1",
42
42
  "mime": "^4.0.7",
43
- "playwright": "1.53.0",
43
+ "playwright": "1.54.1",
44
+ "ws": "^8.18.1",
44
45
  "zod-to-json-schema": "^3.24.4"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@eslint/eslintrc": "^3.2.0",
48
49
  "@eslint/js": "^9.19.0",
49
- "@playwright/test": "1.53.0",
50
+ "@playwright/test": "1.54.1",
50
51
  "@stylistic/eslint-plugin": "^3.0.1",
52
+ "@types/chrome": "^0.0.315",
51
53
  "@types/debug": "^4.1.12",
52
54
  "@types/node": "^22.13.10",
55
+ "@types/ws": "^8.18.1",
53
56
  "@typescript-eslint/eslint-plugin": "^8.26.1",
54
57
  "@typescript-eslint/parser": "^8.26.1",
55
58
  "@typescript-eslint/utils": "^8.26.1",
@@ -1,16 +0,0 @@
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
- export {};