@playwright/mcp 0.0.28 → 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 +76 -5
- package/config.d.ts +5 -0
- package/index.d.ts +1 -3
- package/lib/browserContextFactory.js +49 -1
- package/lib/browserServer.js +151 -0
- package/lib/config.js +5 -16
- package/lib/context.js +1 -1
- package/lib/fileUtils.js +32 -0
- package/lib/httpServer.js +201 -0
- package/lib/program.js +5 -3
- package/lib/tab.js +1 -1
- package/lib/tools/snapshot.js +14 -6
- package/lib/tools/utils.js +8 -1
- package/lib/transport.js +40 -33
- package/package.json +8 -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:
|
|
@@ -52,6 +52,12 @@ After installation, the Playwright MCP server will be available for use with you
|
|
|
52
52
|
<details>
|
|
53
53
|
<summary><b>Install in Cursor</b></summary>
|
|
54
54
|
|
|
55
|
+
#### Click the button to install:
|
|
56
|
+
|
|
57
|
+
[](https://cursor.com/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
|
|
58
|
+
|
|
59
|
+
#### Or install manually:
|
|
60
|
+
|
|
55
61
|
Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @playwright/mcp`. You can also verify config or add command like arguments via clicking `Edit`.
|
|
56
62
|
|
|
57
63
|
```js
|
|
@@ -71,7 +77,7 @@ Go to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, u
|
|
|
71
77
|
<details>
|
|
72
78
|
<summary><b>Install in Windsurf</b></summary>
|
|
73
79
|
|
|
74
|
-
Follow
|
|
80
|
+
Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use following configuration:
|
|
75
81
|
|
|
76
82
|
```js
|
|
77
83
|
{
|
|
@@ -106,6 +112,68 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
|
|
|
106
112
|
```
|
|
107
113
|
</details>
|
|
108
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
|
+
|
|
137
|
+
<details>
|
|
138
|
+
<summary><b>Install in Qodo Gen</b></summary>
|
|
139
|
+
|
|
140
|
+
Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the following configuration:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"playwright": {
|
|
146
|
+
"command": "npx",
|
|
147
|
+
"args": [
|
|
148
|
+
"@playwright/mcp@latest"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Click <code>Save</code>.
|
|
156
|
+
</details>
|
|
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
|
+
|
|
109
177
|
### Configuration
|
|
110
178
|
|
|
111
179
|
Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list:
|
|
@@ -124,6 +192,7 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
124
192
|
--block-service-workers block service workers
|
|
125
193
|
--browser <browser> browser or chrome channel to use, possible
|
|
126
194
|
values: chrome, firefox, webkit, msedge.
|
|
195
|
+
--browser-agent <endpoint> Use browser agent (experimental).
|
|
127
196
|
--caps <caps> comma-separated list of capabilities to enable,
|
|
128
197
|
possible values: tabs, pdf, history, wait, files,
|
|
129
198
|
install. Default is all.
|
|
@@ -288,9 +357,10 @@ npx @playwright/mcp@latest --config path/to/config.json
|
|
|
288
357
|
};
|
|
289
358
|
|
|
290
359
|
/**
|
|
291
|
-
*
|
|
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.
|
|
292
362
|
*/
|
|
293
|
-
|
|
363
|
+
imageResponses?: 'allow' | 'omit' | 'auto';
|
|
294
364
|
}
|
|
295
365
|
```
|
|
296
366
|
</details>
|
|
@@ -354,7 +424,7 @@ http.createServer(async (req, res) => {
|
|
|
354
424
|
// Creates a headless Playwright MCP server with SSE transport
|
|
355
425
|
const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
|
|
356
426
|
const transport = new SSEServerTransport('/messages', res);
|
|
357
|
-
await connection.connect(transport);
|
|
427
|
+
await connection.sever.connect(transport);
|
|
358
428
|
|
|
359
429
|
// ...
|
|
360
430
|
});
|
|
@@ -408,6 +478,7 @@ X Y coordinate space, based on the provided screenshot.
|
|
|
408
478
|
- Parameters:
|
|
409
479
|
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
410
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
|
|
411
482
|
- Read-only: **false**
|
|
412
483
|
|
|
413
484
|
<!-- NOTE: This has been generated via update-readme.js -->
|
package/config.d.ts
CHANGED
package/index.d.ts
CHANGED
|
@@ -16,13 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
-
import type { Config } from './config';
|
|
20
|
-
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
19
|
+
import type { Config } from './config.js';
|
|
21
20
|
import type { BrowserContext } from 'playwright';
|
|
22
21
|
|
|
23
22
|
export type Connection = {
|
|
24
23
|
server: Server;
|
|
25
|
-
connect(transport: Transport): Promise<void>;
|
|
26
24
|
close(): Promise<void>;
|
|
27
25
|
};
|
|
28
26
|
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs';
|
|
17
|
-
import
|
|
17
|
+
import net from 'node:net';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
19
20
|
import debug from 'debug';
|
|
20
21
|
import * as playwright from 'playwright';
|
|
22
|
+
import { userDataDir } from './fileUtils.js';
|
|
21
23
|
const testDebug = debug('pw:mcp:test');
|
|
22
24
|
export function contextFactory(browserConfig) {
|
|
23
25
|
if (browserConfig.remoteEndpoint)
|
|
@@ -26,6 +28,8 @@ export function contextFactory(browserConfig) {
|
|
|
26
28
|
return new CdpContextFactory(browserConfig);
|
|
27
29
|
if (browserConfig.isolated)
|
|
28
30
|
return new IsolatedContextFactory(browserConfig);
|
|
31
|
+
if (browserConfig.browserAgent)
|
|
32
|
+
return new BrowserServerContextFactory(browserConfig);
|
|
29
33
|
return new PersistentContextFactory(browserConfig);
|
|
30
34
|
}
|
|
31
35
|
class BaseContextFactory {
|
|
@@ -78,6 +82,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
78
82
|
super('isolated', browserConfig);
|
|
79
83
|
}
|
|
80
84
|
async _doObtainBrowser() {
|
|
85
|
+
await injectCdpPort(this.browserConfig);
|
|
81
86
|
const browserType = playwright[this.browserConfig.browserName];
|
|
82
87
|
return browserType.launch({
|
|
83
88
|
...this.browserConfig.launchOptions,
|
|
@@ -126,6 +131,7 @@ class PersistentContextFactory {
|
|
|
126
131
|
this.browserConfig = browserConfig;
|
|
127
132
|
}
|
|
128
133
|
async createContext() {
|
|
134
|
+
await injectCdpPort(this.browserConfig);
|
|
129
135
|
testDebug('create browser context (persistent)');
|
|
130
136
|
const userDataDir = this.browserConfig.userDataDir ?? await this._createUserDataDir();
|
|
131
137
|
this._userDataDirs.add(userDataDir);
|
|
@@ -177,3 +183,45 @@ class PersistentContextFactory {
|
|
|
177
183
|
return result;
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
export class BrowserServerContextFactory extends BaseContextFactory {
|
|
187
|
+
constructor(browserConfig) {
|
|
188
|
+
super('persistent', browserConfig);
|
|
189
|
+
}
|
|
190
|
+
async _doObtainBrowser() {
|
|
191
|
+
const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
browserType: this.browserConfig.browserName,
|
|
195
|
+
userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(),
|
|
196
|
+
launchOptions: this.browserConfig.launchOptions,
|
|
197
|
+
contextOptions: this.browserConfig.contextOptions,
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
const info = await response.json();
|
|
201
|
+
if (info.error)
|
|
202
|
+
throw new Error(info.error);
|
|
203
|
+
return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`);
|
|
204
|
+
}
|
|
205
|
+
async _doCreateContext(browser) {
|
|
206
|
+
return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
207
|
+
}
|
|
208
|
+
async _createUserDataDir() {
|
|
209
|
+
const dir = await userDataDir(this.browserConfig);
|
|
210
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function injectCdpPort(browserConfig) {
|
|
215
|
+
if (browserConfig.browserName === 'chromium')
|
|
216
|
+
browserConfig.launchOptions.cdpPort = await findFreePort();
|
|
217
|
+
}
|
|
218
|
+
async function findFreePort() {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const server = net.createServer();
|
|
221
|
+
server.listen(0, () => {
|
|
222
|
+
const { port } = server.address();
|
|
223
|
+
server.close(() => resolve(port));
|
|
224
|
+
});
|
|
225
|
+
server.on('error', reject);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
/* eslint-disable no-console */
|
|
17
|
+
import net from 'net';
|
|
18
|
+
import { program } from 'commander';
|
|
19
|
+
import playwright from 'playwright';
|
|
20
|
+
import { HttpServer } from './httpServer.js';
|
|
21
|
+
import { packageJSON } from './package.js';
|
|
22
|
+
class BrowserServer {
|
|
23
|
+
_server = new HttpServer();
|
|
24
|
+
_entries = [];
|
|
25
|
+
constructor() {
|
|
26
|
+
this._setupExitHandler();
|
|
27
|
+
}
|
|
28
|
+
async start(port) {
|
|
29
|
+
await this._server.start({ port });
|
|
30
|
+
this._server.routePath('/json/list', (req, res) => {
|
|
31
|
+
this._handleJsonList(res);
|
|
32
|
+
});
|
|
33
|
+
this._server.routePath('/json/launch', async (req, res) => {
|
|
34
|
+
void this._handleLaunchBrowser(req, res).catch(e => console.error(e));
|
|
35
|
+
});
|
|
36
|
+
this._setEntries([]);
|
|
37
|
+
}
|
|
38
|
+
_handleJsonList(res) {
|
|
39
|
+
const list = this._entries.map(browser => browser.info);
|
|
40
|
+
res.end(JSON.stringify(list));
|
|
41
|
+
}
|
|
42
|
+
async _handleLaunchBrowser(req, res) {
|
|
43
|
+
const request = await readBody(req);
|
|
44
|
+
let info = this._entries.map(entry => entry.info).find(info => info.userDataDir === request.userDataDir);
|
|
45
|
+
if (!info || info.error)
|
|
46
|
+
info = await this._newBrowser(request);
|
|
47
|
+
res.end(JSON.stringify(info));
|
|
48
|
+
}
|
|
49
|
+
async _newBrowser(request) {
|
|
50
|
+
const cdpPort = await findFreePort();
|
|
51
|
+
request.launchOptions.cdpPort = cdpPort;
|
|
52
|
+
const info = {
|
|
53
|
+
browserType: request.browserType,
|
|
54
|
+
userDataDir: request.userDataDir,
|
|
55
|
+
cdpPort,
|
|
56
|
+
launchOptions: request.launchOptions,
|
|
57
|
+
contextOptions: request.contextOptions,
|
|
58
|
+
};
|
|
59
|
+
const browserType = playwright[request.browserType];
|
|
60
|
+
const { browser, error } = await browserType.launchPersistentContext(request.userDataDir, {
|
|
61
|
+
...request.launchOptions,
|
|
62
|
+
...request.contextOptions,
|
|
63
|
+
handleSIGINT: false,
|
|
64
|
+
handleSIGTERM: false,
|
|
65
|
+
}).then(context => {
|
|
66
|
+
return { browser: context.browser(), error: undefined };
|
|
67
|
+
}).catch(error => {
|
|
68
|
+
return { browser: undefined, error: error.message };
|
|
69
|
+
});
|
|
70
|
+
this._setEntries([...this._entries, {
|
|
71
|
+
browser,
|
|
72
|
+
info: {
|
|
73
|
+
browserType: request.browserType,
|
|
74
|
+
userDataDir: request.userDataDir,
|
|
75
|
+
cdpPort,
|
|
76
|
+
launchOptions: request.launchOptions,
|
|
77
|
+
contextOptions: request.contextOptions,
|
|
78
|
+
error,
|
|
79
|
+
},
|
|
80
|
+
}]);
|
|
81
|
+
browser?.on('disconnected', () => {
|
|
82
|
+
this._setEntries(this._entries.filter(entry => entry.browser !== browser));
|
|
83
|
+
});
|
|
84
|
+
return info;
|
|
85
|
+
}
|
|
86
|
+
_updateReport() {
|
|
87
|
+
// Clear the current line and move cursor to top of screen
|
|
88
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
89
|
+
process.stdout.write(`Playwright Browser Server v${packageJSON.version}\n`);
|
|
90
|
+
process.stdout.write(`Listening on ${this._server.urlPrefix('human-readable')}\n\n`);
|
|
91
|
+
if (this._entries.length === 0) {
|
|
92
|
+
process.stdout.write('No browsers currently running\n');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
process.stdout.write('Running browsers:\n');
|
|
96
|
+
for (const entry of this._entries) {
|
|
97
|
+
const status = entry.browser ? 'running' : 'error';
|
|
98
|
+
const statusColor = entry.browser ? '\x1b[32m' : '\x1b[31m'; // green for running, red for error
|
|
99
|
+
process.stdout.write(`${statusColor}${entry.info.browserType}\x1b[0m (${entry.info.userDataDir}) - ${statusColor}${status}\x1b[0m\n`);
|
|
100
|
+
if (entry.info.error)
|
|
101
|
+
process.stdout.write(` Error: ${entry.info.error}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
_setEntries(entries) {
|
|
105
|
+
this._entries = entries;
|
|
106
|
+
this._updateReport();
|
|
107
|
+
}
|
|
108
|
+
_setupExitHandler() {
|
|
109
|
+
let isExiting = false;
|
|
110
|
+
const handleExit = async () => {
|
|
111
|
+
if (isExiting)
|
|
112
|
+
return;
|
|
113
|
+
isExiting = true;
|
|
114
|
+
setTimeout(() => process.exit(0), 15000);
|
|
115
|
+
for (const entry of this._entries)
|
|
116
|
+
await entry.browser?.close().catch(() => { });
|
|
117
|
+
process.exit(0);
|
|
118
|
+
};
|
|
119
|
+
process.stdin.on('close', handleExit);
|
|
120
|
+
process.on('SIGINT', handleExit);
|
|
121
|
+
process.on('SIGTERM', handleExit);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
program
|
|
125
|
+
.name('browser-agent')
|
|
126
|
+
.option('-p, --port <port>', 'Port to listen on', '9224')
|
|
127
|
+
.action(async (options) => {
|
|
128
|
+
await main(options);
|
|
129
|
+
});
|
|
130
|
+
void program.parseAsync(process.argv);
|
|
131
|
+
async function main(options) {
|
|
132
|
+
const server = new BrowserServer();
|
|
133
|
+
await server.start(+options.port);
|
|
134
|
+
}
|
|
135
|
+
function readBody(req) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const chunks = [];
|
|
138
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
139
|
+
req.on('end', () => resolve(JSON.parse(Buffer.concat(chunks).toString())));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function findFreePort() {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const server = net.createServer();
|
|
145
|
+
server.listen(0, () => {
|
|
146
|
+
const { port } = server.address();
|
|
147
|
+
server.close(() => resolve(port));
|
|
148
|
+
});
|
|
149
|
+
server.on('error', reject);
|
|
150
|
+
});
|
|
151
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'fs';
|
|
17
|
-
import net from 'net';
|
|
18
17
|
import os from 'os';
|
|
19
18
|
import path from 'path';
|
|
20
19
|
import { devices } from 'playwright';
|
|
@@ -48,8 +47,6 @@ export async function resolveCLIConfig(cliOptions) {
|
|
|
48
47
|
// Derive artifact output directory from config.outputDir
|
|
49
48
|
if (result.saveTrace)
|
|
50
49
|
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
|
51
|
-
if (result.browser.browserName === 'chromium')
|
|
52
|
-
result.browser.launchOptions.cdpPort = await findFreePort();
|
|
53
50
|
return result;
|
|
54
51
|
}
|
|
55
52
|
export async function configFromCLIOptions(cliOptions) {
|
|
@@ -91,6 +88,8 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
91
88
|
if (cliOptions.proxyBypass)
|
|
92
89
|
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
93
90
|
}
|
|
91
|
+
if (cliOptions.device && cliOptions.cdpEndpoint)
|
|
92
|
+
throw new Error('Device emulation is not supported with cdpEndpoint.');
|
|
94
93
|
// Context options
|
|
95
94
|
const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
96
95
|
if (cliOptions.storageState)
|
|
@@ -114,6 +113,7 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
114
113
|
contextOptions.serviceWorkers = 'block';
|
|
115
114
|
const result = {
|
|
116
115
|
browser: {
|
|
116
|
+
browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT,
|
|
117
117
|
browserName,
|
|
118
118
|
isolated: cliOptions.isolated,
|
|
119
119
|
userDataDir: cliOptions.userDataDir,
|
|
@@ -137,16 +137,6 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
137
137
|
};
|
|
138
138
|
return result;
|
|
139
139
|
}
|
|
140
|
-
async function findFreePort() {
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
const server = net.createServer();
|
|
143
|
-
server.listen(0, () => {
|
|
144
|
-
const { port } = server.address();
|
|
145
|
-
server.close(() => resolve(port));
|
|
146
|
-
});
|
|
147
|
-
server.on('error', reject);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
140
|
async function loadConfig(configFile) {
|
|
151
141
|
if (!configFile)
|
|
152
142
|
return {};
|
|
@@ -167,6 +157,8 @@ function pickDefined(obj) {
|
|
|
167
157
|
}
|
|
168
158
|
function mergeConfig(base, overrides) {
|
|
169
159
|
const browser = {
|
|
160
|
+
...pickDefined(base.browser),
|
|
161
|
+
...pickDefined(overrides.browser),
|
|
170
162
|
browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium',
|
|
171
163
|
isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
|
|
172
164
|
launchOptions: {
|
|
@@ -178,9 +170,6 @@ function mergeConfig(base, overrides) {
|
|
|
178
170
|
...pickDefined(base.browser?.contextOptions),
|
|
179
171
|
...pickDefined(overrides.browser?.contextOptions),
|
|
180
172
|
},
|
|
181
|
-
userDataDir: overrides.browser?.userDataDir ?? base.browser?.userDataDir,
|
|
182
|
-
cdpEndpoint: overrides.browser?.cdpEndpoint ?? base.browser?.cdpEndpoint,
|
|
183
|
-
remoteEndpoint: overrides.browser?.remoteEndpoint ?? base.browser?.remoteEndpoint,
|
|
184
173
|
};
|
|
185
174
|
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
|
186
175
|
delete browser.launchOptions.channel;
|
package/lib/context.js
CHANGED
|
@@ -67,7 +67,7 @@ export class Context {
|
|
|
67
67
|
}
|
|
68
68
|
currentTabOrDie() {
|
|
69
69
|
if (!this._currentTab)
|
|
70
|
-
throw new Error('No current snapshot available. Capture a snapshot
|
|
70
|
+
throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location first.');
|
|
71
71
|
return this._currentTab;
|
|
72
72
|
}
|
|
73
73
|
async newTab() {
|
package/lib/fileUtils.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
export function cacheDir() {
|
|
19
|
+
let cacheDirectory;
|
|
20
|
+
if (process.platform === 'linux')
|
|
21
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
22
|
+
else if (process.platform === 'darwin')
|
|
23
|
+
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
|
24
|
+
else if (process.platform === 'win32')
|
|
25
|
+
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
26
|
+
else
|
|
27
|
+
throw new Error('Unsupported platform: ' + process.platform);
|
|
28
|
+
return path.join(cacheDirectory, 'ms-playwright');
|
|
29
|
+
}
|
|
30
|
+
export async function userDataDir(browserConfig) {
|
|
31
|
+
return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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 fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import http from 'http';
|
|
19
|
+
import mime from 'mime';
|
|
20
|
+
import { ManualPromise } from './manualPromise.js';
|
|
21
|
+
export class HttpServer {
|
|
22
|
+
_server;
|
|
23
|
+
_urlPrefixPrecise = '';
|
|
24
|
+
_urlPrefixHumanReadable = '';
|
|
25
|
+
_port = 0;
|
|
26
|
+
_routes = [];
|
|
27
|
+
constructor() {
|
|
28
|
+
this._server = http.createServer(this._onRequest.bind(this));
|
|
29
|
+
decorateServer(this._server);
|
|
30
|
+
}
|
|
31
|
+
server() {
|
|
32
|
+
return this._server;
|
|
33
|
+
}
|
|
34
|
+
routePrefix(prefix, handler) {
|
|
35
|
+
this._routes.push({ prefix, handler });
|
|
36
|
+
}
|
|
37
|
+
routePath(path, handler) {
|
|
38
|
+
this._routes.push({ exact: path, handler });
|
|
39
|
+
}
|
|
40
|
+
port() {
|
|
41
|
+
return this._port;
|
|
42
|
+
}
|
|
43
|
+
async _tryStart(port, host) {
|
|
44
|
+
const errorPromise = new ManualPromise();
|
|
45
|
+
const errorListener = (error) => errorPromise.reject(error);
|
|
46
|
+
this._server.on('error', errorListener);
|
|
47
|
+
try {
|
|
48
|
+
this._server.listen(port, host);
|
|
49
|
+
await Promise.race([
|
|
50
|
+
new Promise(cb => this._server.once('listening', cb)),
|
|
51
|
+
errorPromise,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this._server.removeListener('error', errorListener);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async start(options = {}) {
|
|
59
|
+
const host = options.host || 'localhost';
|
|
60
|
+
if (options.preferredPort) {
|
|
61
|
+
try {
|
|
62
|
+
await this._tryStart(options.preferredPort, host);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (!e || !e.message || !e.message.includes('EADDRINUSE'))
|
|
66
|
+
throw e;
|
|
67
|
+
await this._tryStart(undefined, host);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await this._tryStart(options.port, host);
|
|
72
|
+
}
|
|
73
|
+
const address = this._server.address();
|
|
74
|
+
if (typeof address === 'string') {
|
|
75
|
+
this._urlPrefixPrecise = address;
|
|
76
|
+
this._urlPrefixHumanReadable = address;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this._port = address.port;
|
|
80
|
+
const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
81
|
+
this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`;
|
|
82
|
+
this._urlPrefixHumanReadable = `http://${host}:${address.port}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async stop() {
|
|
86
|
+
await new Promise(cb => this._server.close(cb));
|
|
87
|
+
}
|
|
88
|
+
urlPrefix(purpose) {
|
|
89
|
+
return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise;
|
|
90
|
+
}
|
|
91
|
+
serveFile(request, response, absoluteFilePath, headers) {
|
|
92
|
+
try {
|
|
93
|
+
for (const [name, value] of Object.entries(headers || {}))
|
|
94
|
+
response.setHeader(name, value);
|
|
95
|
+
if (request.headers.range)
|
|
96
|
+
this._serveRangeFile(request, response, absoluteFilePath);
|
|
97
|
+
else
|
|
98
|
+
this._serveFile(response, absoluteFilePath);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
_serveFile(response, absoluteFilePath) {
|
|
106
|
+
const content = fs.readFileSync(absoluteFilePath);
|
|
107
|
+
response.statusCode = 200;
|
|
108
|
+
const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream';
|
|
109
|
+
response.setHeader('Content-Type', contentType);
|
|
110
|
+
response.setHeader('Content-Length', content.byteLength);
|
|
111
|
+
response.end(content);
|
|
112
|
+
}
|
|
113
|
+
_serveRangeFile(request, response, absoluteFilePath) {
|
|
114
|
+
const range = request.headers.range;
|
|
115
|
+
if (!range || !range.startsWith('bytes=') || range.includes(', ') || [...range].filter(char => char === '-').length !== 1) {
|
|
116
|
+
response.statusCode = 400;
|
|
117
|
+
return response.end('Bad request');
|
|
118
|
+
}
|
|
119
|
+
// Parse the range header: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1
|
|
120
|
+
const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
|
|
121
|
+
// Both start and end (when passing to fs.createReadStream) and the range header are inclusive and start counting at 0.
|
|
122
|
+
let start;
|
|
123
|
+
let end;
|
|
124
|
+
const size = fs.statSync(absoluteFilePath).size;
|
|
125
|
+
if (startStr !== '' && endStr === '') {
|
|
126
|
+
// No end specified: use the whole file
|
|
127
|
+
start = +startStr;
|
|
128
|
+
end = size - 1;
|
|
129
|
+
}
|
|
130
|
+
else if (startStr === '' && endStr !== '') {
|
|
131
|
+
// No start specified: calculate start manually
|
|
132
|
+
start = size - +endStr;
|
|
133
|
+
end = size - 1;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
start = +startStr;
|
|
137
|
+
end = +endStr;
|
|
138
|
+
}
|
|
139
|
+
// Handle unavailable range request
|
|
140
|
+
if (Number.isNaN(start) || Number.isNaN(end) || start >= size || end >= size || start > end) {
|
|
141
|
+
// Return the 416 Range Not Satisfiable: https://datatracker.ietf.org/doc/html/rfc7233#section-4.4
|
|
142
|
+
response.writeHead(416, {
|
|
143
|
+
'Content-Range': `bytes */${size}`
|
|
144
|
+
});
|
|
145
|
+
return response.end();
|
|
146
|
+
}
|
|
147
|
+
// Sending Partial Content: https://datatracker.ietf.org/doc/html/rfc7233#section-4.1
|
|
148
|
+
response.writeHead(206, {
|
|
149
|
+
'Content-Range': `bytes ${start}-${end}/${size}`,
|
|
150
|
+
'Accept-Ranges': 'bytes',
|
|
151
|
+
'Content-Length': end - start + 1,
|
|
152
|
+
'Content-Type': mime.getType(path.extname(absoluteFilePath)),
|
|
153
|
+
});
|
|
154
|
+
const readable = fs.createReadStream(absoluteFilePath, { start, end });
|
|
155
|
+
readable.pipe(response);
|
|
156
|
+
}
|
|
157
|
+
_onRequest(request, response) {
|
|
158
|
+
if (request.method === 'OPTIONS') {
|
|
159
|
+
response.writeHead(200);
|
|
160
|
+
response.end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
request.on('error', () => response.end());
|
|
164
|
+
try {
|
|
165
|
+
if (!request.url) {
|
|
166
|
+
response.end();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const url = new URL('http://localhost' + request.url);
|
|
170
|
+
for (const route of this._routes) {
|
|
171
|
+
if (route.exact && url.pathname === route.exact) {
|
|
172
|
+
route.handler(request, response);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (route.prefix && url.pathname.startsWith(route.prefix)) {
|
|
176
|
+
route.handler(request, response);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
response.statusCode = 404;
|
|
181
|
+
response.end();
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
response.end();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function decorateServer(server) {
|
|
189
|
+
const sockets = new Set();
|
|
190
|
+
server.on('connection', socket => {
|
|
191
|
+
sockets.add(socket);
|
|
192
|
+
socket.once('close', () => sockets.delete(socket));
|
|
193
|
+
});
|
|
194
|
+
const close = server.close;
|
|
195
|
+
server.close = (callback) => {
|
|
196
|
+
for (const socket of sockets)
|
|
197
|
+
socket.destroy();
|
|
198
|
+
sockets.clear();
|
|
199
|
+
return close.call(server, callback);
|
|
200
|
+
};
|
|
201
|
+
}
|
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';
|
|
@@ -27,6 +27,7 @@ program
|
|
|
27
27
|
.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)
|
|
28
28
|
.option('--block-service-workers', 'block service workers')
|
|
29
29
|
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
30
|
+
.option('--browser-agent <endpoint>', 'Use browser agent (experimental).')
|
|
30
31
|
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
|
31
32
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
32
33
|
.option('--config <path>', 'path to the configuration file.')
|
|
@@ -50,10 +51,11 @@ program
|
|
|
50
51
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
51
52
|
.action(async (options) => {
|
|
52
53
|
const config = await resolveCLIConfig(options);
|
|
54
|
+
const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined;
|
|
53
55
|
const server = new Server(config);
|
|
54
56
|
server.setupExitWatchdog();
|
|
55
|
-
if (
|
|
56
|
-
startHttpTransport(server);
|
|
57
|
+
if (httpServer)
|
|
58
|
+
startHttpTransport(httpServer, server);
|
|
57
59
|
else
|
|
58
60
|
await startStdioTransport(server);
|
|
59
61
|
if (config.saveTrace) {
|
package/lib/tab.js
CHANGED
|
@@ -73,7 +73,7 @@ export class Tab {
|
|
|
73
73
|
// on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
|
|
74
74
|
const download = await Promise.race([
|
|
75
75
|
downloadEvent,
|
|
76
|
-
new Promise(resolve => setTimeout(resolve,
|
|
76
|
+
new Promise(resolve => setTimeout(resolve, 1000)),
|
|
77
77
|
]);
|
|
78
78
|
if (!download)
|
|
79
79
|
throw e;
|
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": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"ctest": "playwright test --project=chrome",
|
|
25
25
|
"ftest": "playwright test --project=firefox",
|
|
26
26
|
"wtest": "playwright test --project=webkit",
|
|
27
|
+
"run-server": "node lib/browserServer.js",
|
|
27
28
|
"clean": "rm -rf lib",
|
|
28
29
|
"npm-publish": "npm run clean && npm run build && npm run test && npm publish"
|
|
29
30
|
},
|
|
@@ -38,16 +39,20 @@
|
|
|
38
39
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
39
40
|
"commander": "^13.1.0",
|
|
40
41
|
"debug": "^4.4.1",
|
|
41
|
-
"
|
|
42
|
+
"mime": "^4.0.7",
|
|
43
|
+
"playwright": "1.54.1",
|
|
44
|
+
"ws": "^8.18.1",
|
|
42
45
|
"zod-to-json-schema": "^3.24.4"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@eslint/eslintrc": "^3.2.0",
|
|
46
49
|
"@eslint/js": "^9.19.0",
|
|
47
|
-
"@playwright/test": "1.
|
|
50
|
+
"@playwright/test": "1.54.1",
|
|
48
51
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
52
|
+
"@types/chrome": "^0.0.315",
|
|
49
53
|
"@types/debug": "^4.1.12",
|
|
50
54
|
"@types/node": "^22.13.10",
|
|
55
|
+
"@types/ws": "^8.18.1",
|
|
51
56
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
52
57
|
"@typescript-eslint/parser": "^8.26.1",
|
|
53
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 {};
|