@playwright/mcp 0.0.3 → 0.0.5
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 +51 -1
- package/index.d.ts +40 -0
- package/index.js +19 -0
- package/lib/context.js +46 -35
- package/lib/index.js +100 -0
- package/lib/program.js +32 -77
- package/lib/resources/console.js +5 -7
- package/lib/server.js +47 -58
- package/lib/tools/common.js +5 -12
- package/lib/tools/screenshot.js +2 -2
- package/lib/tools/snapshot.js +46 -3
- package/lib/tools/utils.js +12 -3
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -33,7 +33,19 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit
|
|
|
33
33
|
|
|
34
34
|
#### Installation in VS Code
|
|
35
35
|
|
|
36
|
-
Install the Playwright MCP server
|
|
36
|
+
Install the Playwright MCP server in VS Code using one of these buttons:
|
|
37
|
+
|
|
38
|
+
<!--
|
|
39
|
+
// Generate using?:
|
|
40
|
+
const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["-y", "@playwright/mcp@latest"] });
|
|
41
|
+
const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
|
|
42
|
+
// Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
|
|
43
|
+
const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
[<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
|
|
47
|
+
|
|
48
|
+
Alternatively, you can install the Playwright MCP server using the VS Code CLI:
|
|
37
49
|
|
|
38
50
|
```bash
|
|
39
51
|
# For VS Code
|
|
@@ -47,6 +59,18 @@ code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@playwrig
|
|
|
47
59
|
|
|
48
60
|
After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
|
|
49
61
|
|
|
62
|
+
### User data directory
|
|
63
|
+
|
|
64
|
+
Playwright MCP will launch Chrome browser with the new profile, located at
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
- `%USERPROFILE%\AppData\Local\ms-playwright\mcp-chrome-profile` on Windows
|
|
68
|
+
- `~/Library/Caches/ms-playwright/mcp-chrome-profile` on macOS
|
|
69
|
+
- `~/.cache/ms-playwright/mcp-chrome-profile` on Linux
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
All the logged in information will be stored in that profile, you can delete it between sessions if you'dlike to clear the offline state.
|
|
73
|
+
|
|
50
74
|
|
|
51
75
|
### Running headless browser (Browser without GUI).
|
|
52
76
|
|
|
@@ -121,6 +145,20 @@ To use Vision Mode, add the `--vision` flag when starting the server:
|
|
|
121
145
|
Vision Mode works best with the computer use models that are able to interact with elements using
|
|
122
146
|
X Y coordinate space, based on the provided screenshot.
|
|
123
147
|
|
|
148
|
+
### Programmatic usage with custom transports
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { createServer } from '@playwright/mcp';
|
|
152
|
+
|
|
153
|
+
// ...
|
|
154
|
+
|
|
155
|
+
const server = createServer({
|
|
156
|
+
launchOptions: { headless: true }
|
|
157
|
+
});
|
|
158
|
+
transport = new SSEServerTransport("/messages", res);
|
|
159
|
+
server.connect(transport);
|
|
160
|
+
```
|
|
161
|
+
|
|
124
162
|
### Snapshot Mode
|
|
125
163
|
|
|
126
164
|
The Playwright MCP provides a set of tools for browser automation. Here are all available tools:
|
|
@@ -166,6 +204,13 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
|
|
|
166
204
|
- `text` (string): Text to type into the element
|
|
167
205
|
- `submit` (boolean): Whether to submit entered text (press Enter after)
|
|
168
206
|
|
|
207
|
+
- **browser_select_option**
|
|
208
|
+
- Description: Select option in a dropdown
|
|
209
|
+
- Parameters:
|
|
210
|
+
- `element` (string): Human-readable element description used to obtain permission to interact with the element
|
|
211
|
+
- `ref` (string): Exact target element reference from the page snapshot
|
|
212
|
+
- `values` (array): Array of values to select in the dropdown.
|
|
213
|
+
|
|
169
214
|
- **browser_press_key**
|
|
170
215
|
- Description: Press a key on the keyboard
|
|
171
216
|
- Parameters:
|
|
@@ -179,6 +224,11 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
|
|
|
179
224
|
- Description: Save page as PDF
|
|
180
225
|
- Parameters: None
|
|
181
226
|
|
|
227
|
+
- **browser_take_screenshot**
|
|
228
|
+
- Description: Capture screenshot of the page
|
|
229
|
+
- Parameters:
|
|
230
|
+
- `raw` (string): Optionally returns lossless PNG screenshot. JPEG by default.
|
|
231
|
+
|
|
182
232
|
- **browser_wait**
|
|
183
233
|
- Description: Wait for a specified time in seconds
|
|
184
234
|
- Parameters:
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { LaunchOptions } from 'playwright';
|
|
19
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
|
|
21
|
+
type Options = {
|
|
22
|
+
/**
|
|
23
|
+
* Path to the user data directory.
|
|
24
|
+
*/
|
|
25
|
+
userDataDir?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Launch options for the browser.
|
|
29
|
+
*/
|
|
30
|
+
launchOptions?: LaunchOptions;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Use screenshots instead of snapshots. Less accurate, reliable and overall
|
|
34
|
+
* slower, but contains visual representation of the page.
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
vision?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function createServer(options?: Options): Server;
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { createServer } = require('./lib/index');
|
|
19
|
+
module.exports = { createServer };
|
package/lib/context.js
CHANGED
|
@@ -51,56 +51,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
51
51
|
exports.Context = void 0;
|
|
52
52
|
const playwright = __importStar(require("playwright"));
|
|
53
53
|
class Context {
|
|
54
|
+
_userDataDir;
|
|
54
55
|
_launchOptions;
|
|
55
56
|
_browser;
|
|
56
57
|
_page;
|
|
57
58
|
_console = [];
|
|
58
|
-
|
|
59
|
-
constructor(launchOptions) {
|
|
59
|
+
_createPagePromise;
|
|
60
|
+
constructor(userDataDir, launchOptions) {
|
|
61
|
+
this._userDataDir = userDataDir;
|
|
60
62
|
this._launchOptions = launchOptions;
|
|
61
63
|
}
|
|
62
|
-
async
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
async close() {
|
|
71
|
-
const page = await this.ensurePage();
|
|
72
|
-
await page.close();
|
|
73
|
-
}
|
|
74
|
-
async _initialize() {
|
|
75
|
-
if (this._initializePromise)
|
|
76
|
-
return this._initializePromise;
|
|
77
|
-
this._initializePromise = (async () => {
|
|
78
|
-
this._browser = await createBrowser(this._launchOptions);
|
|
79
|
-
this._page = await this._browser.newPage();
|
|
80
|
-
this._page.on('console', event => this._console.push(event));
|
|
81
|
-
this._page.on('framenavigated', frame => {
|
|
64
|
+
async createPage() {
|
|
65
|
+
if (this._createPagePromise)
|
|
66
|
+
return this._createPagePromise;
|
|
67
|
+
this._createPagePromise = (async () => {
|
|
68
|
+
const { browser, page } = await this._createPage();
|
|
69
|
+
page.on('console', event => this._console.push(event));
|
|
70
|
+
page.on('framenavigated', frame => {
|
|
82
71
|
if (!frame.parentFrame())
|
|
83
72
|
this._console.length = 0;
|
|
84
73
|
});
|
|
85
|
-
|
|
74
|
+
page.on('close', () => this._onPageClose());
|
|
75
|
+
this._page = page;
|
|
76
|
+
this._browser = browser;
|
|
77
|
+
return page;
|
|
86
78
|
})();
|
|
87
|
-
return this.
|
|
79
|
+
return this._createPagePromise;
|
|
88
80
|
}
|
|
89
|
-
|
|
81
|
+
_onPageClose() {
|
|
90
82
|
const browser = this._browser;
|
|
91
|
-
|
|
83
|
+
const page = this._page;
|
|
84
|
+
void page?.context()?.close().then(() => browser?.close()).catch(() => { });
|
|
85
|
+
this._createPagePromise = undefined;
|
|
92
86
|
this._browser = undefined;
|
|
93
87
|
this._page = undefined;
|
|
94
88
|
this._console.length = 0;
|
|
95
|
-
void browser?.close();
|
|
96
89
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
90
|
+
existingPage() {
|
|
91
|
+
if (!this._page)
|
|
92
|
+
throw new Error('Navigate to a location to create a page');
|
|
93
|
+
return this._page;
|
|
94
|
+
}
|
|
95
|
+
async console() {
|
|
96
|
+
return this._console;
|
|
97
|
+
}
|
|
98
|
+
async close() {
|
|
99
|
+
if (!this._page)
|
|
100
|
+
return;
|
|
101
|
+
await this._page.close();
|
|
102
|
+
}
|
|
103
|
+
async _createPage() {
|
|
104
|
+
if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
|
|
105
|
+
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
|
|
106
|
+
if (this._launchOptions)
|
|
107
|
+
url.searchParams.set('launch-options', JSON.stringify(this._launchOptions));
|
|
108
|
+
const browser = await playwright.chromium.connect(String(url));
|
|
109
|
+
const page = await browser.newPage();
|
|
110
|
+
return { browser, page };
|
|
111
|
+
}
|
|
112
|
+
const context = await playwright.chromium.launchPersistentContext(this._userDataDir, this._launchOptions);
|
|
113
|
+
const [page] = context.pages();
|
|
114
|
+
return { page };
|
|
104
115
|
}
|
|
105
|
-
return await playwright.chromium.launch({ channel: 'chrome', ...launchOptions });
|
|
106
116
|
}
|
|
117
|
+
exports.Context = Context;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.createServer = createServer;
|
|
52
|
+
const server_1 = require("./server");
|
|
53
|
+
const snapshot = __importStar(require("./tools/snapshot"));
|
|
54
|
+
const common = __importStar(require("./tools/common"));
|
|
55
|
+
const screenshot = __importStar(require("./tools/screenshot"));
|
|
56
|
+
const console_1 = require("./resources/console");
|
|
57
|
+
const commonTools = [
|
|
58
|
+
common.pressKey,
|
|
59
|
+
common.wait,
|
|
60
|
+
common.pdf,
|
|
61
|
+
common.close,
|
|
62
|
+
];
|
|
63
|
+
const snapshotTools = [
|
|
64
|
+
common.navigate(true),
|
|
65
|
+
common.goBack(true),
|
|
66
|
+
common.goForward(true),
|
|
67
|
+
snapshot.snapshot,
|
|
68
|
+
snapshot.click,
|
|
69
|
+
snapshot.hover,
|
|
70
|
+
snapshot.type,
|
|
71
|
+
snapshot.selectOption,
|
|
72
|
+
snapshot.screenshot,
|
|
73
|
+
...commonTools,
|
|
74
|
+
];
|
|
75
|
+
const screenshotTools = [
|
|
76
|
+
common.navigate(false),
|
|
77
|
+
common.goBack(false),
|
|
78
|
+
common.goForward(false),
|
|
79
|
+
screenshot.screenshot,
|
|
80
|
+
screenshot.moveMouse,
|
|
81
|
+
screenshot.click,
|
|
82
|
+
screenshot.drag,
|
|
83
|
+
screenshot.type,
|
|
84
|
+
...commonTools,
|
|
85
|
+
];
|
|
86
|
+
const resources = [
|
|
87
|
+
console_1.console,
|
|
88
|
+
];
|
|
89
|
+
const packageJSON = require('../package.json');
|
|
90
|
+
function createServer(options) {
|
|
91
|
+
const tools = options?.vision ? screenshotTools : snapshotTools;
|
|
92
|
+
return (0, server_1.createServerWithTools)({
|
|
93
|
+
name: 'Playwright',
|
|
94
|
+
version: packageJSON.version,
|
|
95
|
+
tools,
|
|
96
|
+
resources,
|
|
97
|
+
userDataDir: options?.userDataDir ?? '',
|
|
98
|
+
launchOptions: options?.launchOptions,
|
|
99
|
+
});
|
|
100
|
+
}
|
package/lib/program.js
CHANGED
|
@@ -14,101 +14,56 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
-
}
|
|
23
|
-
Object.defineProperty(o, k2, desc);
|
|
24
|
-
}) : (function(o, m, k, k2) {
|
|
25
|
-
if (k2 === undefined) k2 = k;
|
|
26
|
-
o[k2] = m[k];
|
|
27
|
-
}));
|
|
28
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
-
}) : function(o, v) {
|
|
31
|
-
o["default"] = v;
|
|
32
|
-
});
|
|
33
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
-
var ownKeys = function(o) {
|
|
35
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
-
var ar = [];
|
|
37
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
-
return ar;
|
|
39
|
-
};
|
|
40
|
-
return ownKeys(o);
|
|
41
|
-
};
|
|
42
|
-
return function (mod) {
|
|
43
|
-
if (mod && mod.__esModule) return mod;
|
|
44
|
-
var result = {};
|
|
45
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
-
__setModuleDefault(result, mod);
|
|
47
|
-
return result;
|
|
48
|
-
};
|
|
49
|
-
})();
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
50
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const os_1 = __importDefault(require("os"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
51
24
|
const commander_1 = require("commander");
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const common = __importStar(require("./tools/common"));
|
|
55
|
-
const screenshot = __importStar(require("./tools/screenshot"));
|
|
56
|
-
const console_1 = require("./resources/console");
|
|
25
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
26
|
+
const index_1 = require("./index");
|
|
57
27
|
const packageJSON = require('../package.json');
|
|
58
28
|
commander_1.program
|
|
59
29
|
.version('Version ' + packageJSON.version)
|
|
60
30
|
.name(packageJSON.name)
|
|
61
31
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
|
32
|
+
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
62
33
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
63
34
|
.action(async (options) => {
|
|
64
35
|
const launchOptions = {
|
|
65
36
|
headless: !!options.headless,
|
|
37
|
+
channel: 'chrome',
|
|
66
38
|
};
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
resources,
|
|
73
|
-
}, launchOptions);
|
|
39
|
+
const server = (0, index_1.createServer)({
|
|
40
|
+
userDataDir: options.userDataDir ?? await userDataDir(),
|
|
41
|
+
launchOptions,
|
|
42
|
+
vision: !!options.vision,
|
|
43
|
+
});
|
|
74
44
|
setupExitWatchdog(server);
|
|
75
|
-
|
|
45
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
46
|
+
await server.connect(transport);
|
|
76
47
|
});
|
|
77
48
|
function setupExitWatchdog(server) {
|
|
78
49
|
process.stdin.on('close', async () => {
|
|
79
50
|
setTimeout(() => process.exit(0), 15000);
|
|
80
|
-
await server
|
|
51
|
+
await server.close();
|
|
81
52
|
process.exit(0);
|
|
82
53
|
});
|
|
83
54
|
}
|
|
84
|
-
const commonTools = [
|
|
85
|
-
common.pressKey,
|
|
86
|
-
common.wait,
|
|
87
|
-
common.pdf,
|
|
88
|
-
common.close,
|
|
89
|
-
];
|
|
90
|
-
const snapshotTools = [
|
|
91
|
-
common.navigate(true),
|
|
92
|
-
common.goBack(true),
|
|
93
|
-
common.goForward(true),
|
|
94
|
-
snapshot.snapshot,
|
|
95
|
-
snapshot.click,
|
|
96
|
-
snapshot.hover,
|
|
97
|
-
snapshot.type,
|
|
98
|
-
...commonTools,
|
|
99
|
-
];
|
|
100
|
-
const screenshotTools = [
|
|
101
|
-
common.navigate(false),
|
|
102
|
-
common.goBack(false),
|
|
103
|
-
common.goForward(false),
|
|
104
|
-
screenshot.screenshot,
|
|
105
|
-
screenshot.moveMouse,
|
|
106
|
-
screenshot.click,
|
|
107
|
-
screenshot.drag,
|
|
108
|
-
screenshot.type,
|
|
109
|
-
...commonTools,
|
|
110
|
-
];
|
|
111
|
-
const resources = [
|
|
112
|
-
console_1.console,
|
|
113
|
-
];
|
|
114
55
|
commander_1.program.parse(process.argv);
|
|
56
|
+
async function userDataDir() {
|
|
57
|
+
let cacheDirectory;
|
|
58
|
+
if (process.platform === 'linux')
|
|
59
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
|
|
60
|
+
else if (process.platform === 'darwin')
|
|
61
|
+
cacheDirectory = path_1.default.join(os_1.default.homedir(), 'Library', 'Caches');
|
|
62
|
+
else if (process.platform === 'win32')
|
|
63
|
+
cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
64
|
+
else
|
|
65
|
+
throw new Error('Unsupported platform: ' + process.platform);
|
|
66
|
+
const result = path_1.default.join(cacheDirectory, 'ms-playwright', 'mcp-chrome-profile');
|
|
67
|
+
await fs_1.default.promises.mkdir(result, { recursive: true });
|
|
68
|
+
return result;
|
|
69
|
+
}
|
package/lib/resources/console.js
CHANGED
|
@@ -23,14 +23,12 @@ exports.console = {
|
|
|
23
23
|
mimeType: 'text/plain',
|
|
24
24
|
},
|
|
25
25
|
read: async (context, uri) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const messages = await context.console();
|
|
27
|
+
const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
|
|
28
|
+
return [{
|
|
29
29
|
uri,
|
|
30
30
|
mimeType: 'text/plain',
|
|
31
|
-
text:
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
31
|
+
text: log
|
|
32
|
+
}];
|
|
35
33
|
},
|
|
36
34
|
};
|
package/lib/server.js
CHANGED
|
@@ -15,65 +15,54 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.
|
|
18
|
+
exports.createServerWithTools = createServerWithTools;
|
|
19
19
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
20
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
21
20
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
22
21
|
const context_1 = require("./context");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
async start() {
|
|
71
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
|
72
|
-
await this._server.connect(transport);
|
|
73
|
-
}
|
|
74
|
-
async stop() {
|
|
75
|
-
await this._server.close();
|
|
76
|
-
await this._context.close();
|
|
77
|
-
}
|
|
22
|
+
function createServerWithTools(options) {
|
|
23
|
+
const { name, version, tools, resources, userDataDir, launchOptions } = options;
|
|
24
|
+
const context = new context_1.Context(userDataDir, launchOptions);
|
|
25
|
+
const server = new index_js_1.Server({ name, version }, {
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
resources: {},
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
32
|
+
return { tools: tools.map(tool => tool.schema) };
|
|
33
|
+
});
|
|
34
|
+
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
35
|
+
return { resources: resources.map(resource => resource.schema) };
|
|
36
|
+
});
|
|
37
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
38
|
+
const tool = tools.find(tool => tool.schema.name === request.params.name);
|
|
39
|
+
if (!tool) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await tool.handle(context, request.params.arguments);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: 'text', text: String(error) }],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
|
|
57
|
+
const resource = resources.find(resource => resource.schema.uri === request.params.uri);
|
|
58
|
+
if (!resource)
|
|
59
|
+
return { contents: [] };
|
|
60
|
+
const contents = await resource.read(context, request.params.uri);
|
|
61
|
+
return { contents };
|
|
62
|
+
});
|
|
63
|
+
server.close = async () => {
|
|
64
|
+
await server.close();
|
|
65
|
+
await context.close();
|
|
66
|
+
};
|
|
67
|
+
return server;
|
|
78
68
|
}
|
|
79
|
-
exports.Server = Server;
|
package/lib/tools/common.js
CHANGED
|
@@ -35,7 +35,7 @@ const navigate = snapshot => ({
|
|
|
35
35
|
},
|
|
36
36
|
handle: async (context, params) => {
|
|
37
37
|
const validatedParams = navigateSchema.parse(params);
|
|
38
|
-
const page = await context.
|
|
38
|
+
const page = await context.createPage();
|
|
39
39
|
await page.goto(validatedParams.url, { waitUntil: 'domcontentloaded' });
|
|
40
40
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
41
41
|
await page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
@@ -58,10 +58,7 @@ const goBack = snapshot => ({
|
|
|
58
58
|
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(goBackSchema),
|
|
59
59
|
},
|
|
60
60
|
handle: async (context) => {
|
|
61
|
-
return await (0, utils_1.runAndWait)(context, 'Navigated back', async () =>
|
|
62
|
-
const page = await context.ensurePage();
|
|
63
|
-
await page.goBack();
|
|
64
|
-
}, snapshot);
|
|
61
|
+
return await (0, utils_1.runAndWait)(context, 'Navigated back', async (page) => page.goBack(), snapshot);
|
|
65
62
|
},
|
|
66
63
|
});
|
|
67
64
|
exports.goBack = goBack;
|
|
@@ -73,10 +70,7 @@ const goForward = snapshot => ({
|
|
|
73
70
|
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(goForwardSchema),
|
|
74
71
|
},
|
|
75
72
|
handle: async (context) => {
|
|
76
|
-
return await (0, utils_1.runAndWait)(context, 'Navigated forward', async () =>
|
|
77
|
-
const page = await context.ensurePage();
|
|
78
|
-
await page.goForward();
|
|
79
|
-
}, snapshot);
|
|
73
|
+
return await (0, utils_1.runAndWait)(context, 'Navigated forward', async (page) => page.goForward(), snapshot);
|
|
80
74
|
},
|
|
81
75
|
});
|
|
82
76
|
exports.goForward = goForward;
|
|
@@ -91,8 +85,7 @@ exports.wait = {
|
|
|
91
85
|
},
|
|
92
86
|
handle: async (context, params) => {
|
|
93
87
|
const validatedParams = waitSchema.parse(params);
|
|
94
|
-
|
|
95
|
-
await page.waitForTimeout(Math.min(10000, validatedParams.time * 1000));
|
|
88
|
+
await new Promise(f => setTimeout(f, Math.min(10000, validatedParams.time * 1000)));
|
|
96
89
|
return {
|
|
97
90
|
content: [{
|
|
98
91
|
type: 'text',
|
|
@@ -125,7 +118,7 @@ exports.pdf = {
|
|
|
125
118
|
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(pdfSchema),
|
|
126
119
|
},
|
|
127
120
|
handle: async (context) => {
|
|
128
|
-
const page =
|
|
121
|
+
const page = context.existingPage();
|
|
129
122
|
const fileName = path_1.default.join(os_1.default.tmpdir(), `/page-${new Date().toISOString()}.pdf`);
|
|
130
123
|
await page.pdf({ path: fileName });
|
|
131
124
|
return {
|
package/lib/tools/screenshot.js
CHANGED
|
@@ -26,7 +26,7 @@ exports.screenshot = {
|
|
|
26
26
|
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({})),
|
|
27
27
|
},
|
|
28
28
|
handle: async (context) => {
|
|
29
|
-
const page =
|
|
29
|
+
const page = context.existingPage();
|
|
30
30
|
const screenshot = await page.screenshot({ type: 'jpeg', quality: 50, scale: 'css' });
|
|
31
31
|
return {
|
|
32
32
|
content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/jpeg' }],
|
|
@@ -48,7 +48,7 @@ exports.moveMouse = {
|
|
|
48
48
|
},
|
|
49
49
|
handle: async (context, params) => {
|
|
50
50
|
const validatedParams = moveMouseSchema.parse(params);
|
|
51
|
-
const page =
|
|
51
|
+
const page = context.existingPage();
|
|
52
52
|
await page.mouse.move(validatedParams.x, validatedParams.y);
|
|
53
53
|
return {
|
|
54
54
|
content: [{ type: 'text', text: `Moved mouse to (${validatedParams.x}, ${validatedParams.y})` }],
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.type = exports.hover = exports.drag = exports.click = exports.snapshot = void 0;
|
|
21
|
+
exports.screenshot = exports.selectOption = exports.type = exports.hover = exports.drag = exports.click = exports.snapshot = void 0;
|
|
22
22
|
const zod_1 = require("zod");
|
|
23
23
|
const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
|
|
24
24
|
const utils_1 = require("./utils");
|
|
@@ -29,7 +29,7 @@ exports.snapshot = {
|
|
|
29
29
|
inputSchema: (0, zod_to_json_schema_1.default)(zod_1.z.object({})),
|
|
30
30
|
},
|
|
31
31
|
handle: async (context) => {
|
|
32
|
-
return await (0, utils_1.captureAriaSnapshot)(
|
|
32
|
+
return await (0, utils_1.captureAriaSnapshot)(context.existingPage());
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
const elementSchema = zod_1.z.object({
|
|
@@ -99,6 +99,49 @@ exports.type = {
|
|
|
99
99
|
}, true);
|
|
100
100
|
},
|
|
101
101
|
};
|
|
102
|
+
const selectOptionSchema = elementSchema.extend({
|
|
103
|
+
values: zod_1.z.array(zod_1.z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
|
|
104
|
+
});
|
|
105
|
+
exports.selectOption = {
|
|
106
|
+
schema: {
|
|
107
|
+
name: 'browser_select_option',
|
|
108
|
+
description: 'Select an option in a dropdown',
|
|
109
|
+
inputSchema: (0, zod_to_json_schema_1.default)(selectOptionSchema),
|
|
110
|
+
},
|
|
111
|
+
handle: async (context, params) => {
|
|
112
|
+
const validatedParams = selectOptionSchema.parse(params);
|
|
113
|
+
return await (0, utils_1.runAndWait)(context, `Selected option in "${validatedParams.element}"`, async (page) => {
|
|
114
|
+
const locator = refLocator(page, validatedParams.ref);
|
|
115
|
+
await locator.selectOption(validatedParams.values);
|
|
116
|
+
}, true);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const screenshotSchema = zod_1.z.object({
|
|
120
|
+
raw: zod_1.z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
|
121
|
+
});
|
|
122
|
+
exports.screenshot = {
|
|
123
|
+
schema: {
|
|
124
|
+
name: 'browser_take_screenshot',
|
|
125
|
+
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
|
126
|
+
inputSchema: (0, zod_to_json_schema_1.default)(screenshotSchema),
|
|
127
|
+
},
|
|
128
|
+
handle: async (context, params) => {
|
|
129
|
+
const validatedParams = screenshotSchema.parse(params);
|
|
130
|
+
const page = context.existingPage();
|
|
131
|
+
const options = validatedParams.raw ? { type: 'png', scale: 'css' } : { type: 'jpeg', quality: 50, scale: 'css' };
|
|
132
|
+
const screenshot = await page.screenshot(options);
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: 'image', data: screenshot.toString('base64'), mimeType: validatedParams.raw ? 'image/png' : 'image/jpeg' }],
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
102
138
|
function refLocator(page, ref) {
|
|
103
|
-
|
|
139
|
+
let frame = page.frames()[0];
|
|
140
|
+
const match = ref.match(/^f(\d+)(.*)/);
|
|
141
|
+
if (match) {
|
|
142
|
+
const frameIndex = parseInt(match[1], 10);
|
|
143
|
+
frame = page.frames()[frameIndex];
|
|
144
|
+
ref = match[2];
|
|
145
|
+
}
|
|
146
|
+
return frame.locator(`aria-ref=${ref}`);
|
|
104
147
|
}
|
package/lib/tools/utils.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.runAndWait = runAndWait;
|
|
19
|
+
exports.captureAllFrameSnapshot = captureAllFrameSnapshot;
|
|
19
20
|
exports.captureAriaSnapshot = captureAriaSnapshot;
|
|
20
21
|
async function waitForCompletion(page, callback) {
|
|
21
22
|
const requests = new Set();
|
|
@@ -65,21 +66,29 @@ async function waitForCompletion(page, callback) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
async function runAndWait(context, status, callback, snapshot = false) {
|
|
68
|
-
const page =
|
|
69
|
+
const page = context.existingPage();
|
|
69
70
|
await waitForCompletion(page, () => callback(page));
|
|
70
71
|
return snapshot ? captureAriaSnapshot(page, status) : {
|
|
71
72
|
content: [{ type: 'text', text: status }],
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
async function captureAllFrameSnapshot(page) {
|
|
76
|
+
const snapshots = await Promise.all(page.frames().map(frame => frame.locator('html').ariaSnapshot({ ref: true })));
|
|
77
|
+
const scopedSnapshots = snapshots.map((snapshot, frameIndex) => {
|
|
78
|
+
if (frameIndex === 0)
|
|
79
|
+
return snapshot;
|
|
80
|
+
return snapshot.replaceAll('[ref=', `[ref=f${frameIndex}`);
|
|
81
|
+
});
|
|
82
|
+
return scopedSnapshots.join('\n');
|
|
83
|
+
}
|
|
74
84
|
async function captureAriaSnapshot(page, status = '') {
|
|
75
|
-
const snapshot = await page.locator('html').ariaSnapshot({ ref: true });
|
|
76
85
|
return {
|
|
77
86
|
content: [{ type: 'text', text: `${status ? `${status}\n` : ''}
|
|
78
87
|
- Page URL: ${page.url()}
|
|
79
88
|
- Page Title: ${await page.title()}
|
|
80
89
|
- Page Snapshot
|
|
81
90
|
\`\`\`yaml
|
|
82
|
-
${
|
|
91
|
+
${await captureAllFrameSnapshot(page)}
|
|
83
92
|
\`\`\`
|
|
84
93
|
`
|
|
85
94
|
}],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,27 +18,27 @@
|
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"lint": "eslint .",
|
|
20
20
|
"watch": "tsc --watch",
|
|
21
|
-
"test": "playwright test"
|
|
21
|
+
"test": "playwright test",
|
|
22
|
+
"clean": "rm -rf lib",
|
|
23
|
+
"publish": "npm run clean && npm run build && npm run test && npm publish"
|
|
22
24
|
},
|
|
23
25
|
"exports": {
|
|
24
|
-
"./
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"./tools/snapshot": "./lib/tools/snapshot.js",
|
|
30
|
-
"./package.json": "./package.json"
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"default": "./index.js"
|
|
30
|
+
}
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
34
34
|
"commander": "^13.1.0",
|
|
35
|
-
"playwright": "1.52.0-alpha-
|
|
35
|
+
"playwright": "1.52.0-alpha-1743011787000",
|
|
36
36
|
"zod-to-json-schema": "^3.24.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@eslint/eslintrc": "^3.2.0",
|
|
40
40
|
"@eslint/js": "^9.19.0",
|
|
41
|
-
"@playwright/test": "1.52.0-alpha-
|
|
41
|
+
"@playwright/test": "1.52.0-alpha-1743011787000",
|
|
42
42
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
43
43
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
44
44
|
"@typescript-eslint/parser": "^8.26.1",
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"typescript": "^5.8.2"
|
|
51
51
|
},
|
|
52
52
|
"bin": {
|
|
53
|
-
"mcp": "cli.js"
|
|
53
|
+
"mcp-server-playwright": "cli.js"
|
|
54
54
|
}
|
|
55
55
|
}
|