@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 +47 -4
- package/lib/config.js +2 -0
- package/lib/program.js +4 -3
- package/lib/tools/snapshot.js +14 -6
- package/lib/tools/utils.js +8 -1
- package/lib/transport.js +40 -33
- package/package.json +6 -3
- package/lib/resources/resource.js +0 -16
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
|
|
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
|
+
[](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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
57
|
-
startHttpTransport(server);
|
|
57
|
+
if (httpServer)
|
|
58
|
+
startHttpTransport(httpServer, server);
|
|
58
59
|
else
|
|
59
60
|
await startStdioTransport(server);
|
|
60
61
|
if (config.saveTrace) {
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
56
|
-
|
|
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
|
};
|
package/lib/tools/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
104
|
+
await handleStreamable(mcpServer, req, res, streamableSessions);
|
|
93
105
|
else
|
|
94
|
-
await handleSSE(
|
|
106
|
+
await handleSSE(mcpServer, req, res, url, sseSessions);
|
|
95
107
|
});
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {};
|