@playwright/mcp 0.0.7 → 0.0.9
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 +18 -2
- package/lib/context.js +98 -33
- package/lib/index.js +3 -0
- package/lib/program.js +38 -5
- package/lib/server.js +2 -2
- package/lib/tools/common.js +18 -2
- package/lib/tools/utils.js +4 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -59,9 +59,25 @@ code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@playwrig
|
|
|
59
59
|
|
|
60
60
|
After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
|
|
61
61
|
|
|
62
|
+
### CLI Options
|
|
63
|
+
|
|
64
|
+
The Playwright MCP server supports the following command-line options:
|
|
65
|
+
|
|
66
|
+
- `--browser <browser>`: Browser or chrome channel to use. Possible values:
|
|
67
|
+
- `chrome`, `firefox`, `webkit`, `msedge`
|
|
68
|
+
- Chrome channels: `chrome-beta`, `chrome-canary`, `chrome-dev`
|
|
69
|
+
- Edge channels: `msedge-beta`, `msedge-canary`, `msedge-dev`
|
|
70
|
+
- Default: `chrome`
|
|
71
|
+
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
|
|
72
|
+
- `--executable-path <path>`: Path to the browser executable
|
|
73
|
+
- `--headless`: Run browser in headless mode (headed by default)
|
|
74
|
+
- `--port <port>`: Port to listen on for SSE transport
|
|
75
|
+
- `--user-data-dir <path>`: Path to the user data directory
|
|
76
|
+
- `--vision`: Run server that uses screenshots (Aria snapshots are used by default)
|
|
77
|
+
|
|
62
78
|
### User data directory
|
|
63
79
|
|
|
64
|
-
Playwright MCP will launch
|
|
80
|
+
Playwright MCP will launch the browser with the new profile, located at
|
|
65
81
|
|
|
66
82
|
```
|
|
67
83
|
- `%USERPROFILE%\AppData\Local\ms-playwright\mcp-chrome-profile` on Windows
|
|
@@ -69,7 +85,7 @@ Playwright MCP will launch Chrome browser with the new profile, located at
|
|
|
69
85
|
- `~/.cache/ms-playwright/mcp-chrome-profile` on Linux
|
|
70
86
|
```
|
|
71
87
|
|
|
72
|
-
All the logged in information will be stored in that profile, you can delete it between sessions if you'
|
|
88
|
+
All the logged in information will be stored in that profile, you can delete it between sessions if you'd like to clear the offline state.
|
|
73
89
|
|
|
74
90
|
|
|
75
91
|
### Running headless browser (Browser without GUI).
|
package/lib/context.js
CHANGED
|
@@ -47,21 +47,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
return result;
|
|
48
48
|
};
|
|
49
49
|
})();
|
|
50
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
+
};
|
|
50
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
54
|
exports.Context = void 0;
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const path_1 = __importDefault(require("path"));
|
|
52
57
|
const playwright = __importStar(require("playwright"));
|
|
58
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
53
59
|
class Context {
|
|
54
|
-
|
|
55
|
-
_launchOptions;
|
|
60
|
+
_options;
|
|
56
61
|
_browser;
|
|
57
62
|
_page;
|
|
58
63
|
_console = [];
|
|
59
64
|
_createPagePromise;
|
|
60
65
|
_fileChooser;
|
|
61
66
|
_lastSnapshotFrames = [];
|
|
62
|
-
constructor(
|
|
63
|
-
this.
|
|
64
|
-
this._launchOptions = launchOptions;
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this._options = options;
|
|
65
69
|
}
|
|
66
70
|
async createPage() {
|
|
67
71
|
if (this._createPagePromise)
|
|
@@ -93,6 +97,24 @@ class Context {
|
|
|
93
97
|
this._fileChooser = undefined;
|
|
94
98
|
this._console.length = 0;
|
|
95
99
|
}
|
|
100
|
+
async install() {
|
|
101
|
+
const channel = this._options.launchOptions?.channel ?? this._options.browserName ?? 'chrome';
|
|
102
|
+
const cli = path_1.default.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
|
103
|
+
const child = (0, child_process_1.fork)(cli, ['install', channel], {
|
|
104
|
+
stdio: 'pipe',
|
|
105
|
+
});
|
|
106
|
+
const output = [];
|
|
107
|
+
child.stdout?.on('data', data => output.push(data.toString()));
|
|
108
|
+
child.stderr?.on('data', data => output.push(data.toString()));
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
child.on('close', code => {
|
|
111
|
+
if (code === 0)
|
|
112
|
+
resolve(channel);
|
|
113
|
+
else
|
|
114
|
+
reject(new Error(`Failed to install browser: ${output.join('')}`));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
96
118
|
existingPage() {
|
|
97
119
|
if (!this._page)
|
|
98
120
|
throw new Error('Navigate to a location to create a page');
|
|
@@ -119,49 +141,92 @@ class Context {
|
|
|
119
141
|
this._fileChooser = undefined;
|
|
120
142
|
}
|
|
121
143
|
async _createPage() {
|
|
122
|
-
if (
|
|
123
|
-
const url = new URL(
|
|
124
|
-
if (this.
|
|
125
|
-
url.searchParams.set('
|
|
126
|
-
|
|
144
|
+
if (this._options.remoteEndpoint) {
|
|
145
|
+
const url = new URL(this._options.remoteEndpoint);
|
|
146
|
+
if (this._options.browserName)
|
|
147
|
+
url.searchParams.set('browser', this._options.browserName);
|
|
148
|
+
if (this._options.launchOptions)
|
|
149
|
+
url.searchParams.set('launch-options', JSON.stringify(this._options.launchOptions));
|
|
150
|
+
const browser = await playwright[this._options.browserName ?? 'chromium'].connect(String(url));
|
|
127
151
|
const page = await browser.newPage();
|
|
128
152
|
return { browser, page };
|
|
129
153
|
}
|
|
130
|
-
|
|
154
|
+
if (this._options.cdpEndpoint) {
|
|
155
|
+
const browser = await playwright.chromium.connectOverCDP(this._options.cdpEndpoint);
|
|
156
|
+
const browserContext = browser.contexts()[0];
|
|
157
|
+
let [page] = browserContext.pages();
|
|
158
|
+
if (!page)
|
|
159
|
+
page = await browserContext.newPage();
|
|
160
|
+
return { browser, page };
|
|
161
|
+
}
|
|
162
|
+
const context = await this._launchPersistentContext();
|
|
131
163
|
const [page] = context.pages();
|
|
132
164
|
return { page };
|
|
133
165
|
}
|
|
166
|
+
async _launchPersistentContext() {
|
|
167
|
+
try {
|
|
168
|
+
const browserType = this._options.browserName ? playwright[this._options.browserName] : playwright.chromium;
|
|
169
|
+
return await browserType.launchPersistentContext(this._options.userDataDir, this._options.launchOptions);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.message.includes('Executable doesn\'t exist'))
|
|
173
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
134
177
|
async allFramesSnapshot() {
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
this._lastSnapshotFrames = [];
|
|
179
|
+
const yaml = await this._allFramesSnapshot(this.existingPage());
|
|
180
|
+
return yaml.toString().trim();
|
|
181
|
+
}
|
|
182
|
+
async _allFramesSnapshot(frame) {
|
|
183
|
+
const frameIndex = this._lastSnapshotFrames.push(frame) - 1;
|
|
184
|
+
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true });
|
|
185
|
+
const snapshot = yaml_1.default.parseDocument(snapshotString);
|
|
186
|
+
const visit = async (node) => {
|
|
187
|
+
if (yaml_1.default.isPair(node)) {
|
|
188
|
+
await Promise.all([
|
|
189
|
+
visit(node.key).then(k => node.key = k),
|
|
190
|
+
visit(node.value).then(v => node.value = v)
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
else if (yaml_1.default.isSeq(node) || yaml_1.default.isMap(node)) {
|
|
194
|
+
node.items = await Promise.all(node.items.map(visit));
|
|
195
|
+
}
|
|
196
|
+
else if (yaml_1.default.isScalar(node)) {
|
|
197
|
+
if (typeof node.value === 'string') {
|
|
198
|
+
const value = node.value;
|
|
199
|
+
if (frameIndex > 0)
|
|
200
|
+
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
|
|
201
|
+
if (value.startsWith('iframe ')) {
|
|
202
|
+
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
203
|
+
if (ref) {
|
|
204
|
+
try {
|
|
205
|
+
const childSnapshot = await this._allFramesSnapshot(frame.frameLocator(`aria-ref=${ref}`));
|
|
206
|
+
return snapshot.createPair(node.value, childSnapshot);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return node;
|
|
216
|
+
};
|
|
217
|
+
await visit(snapshot.contents);
|
|
218
|
+
return snapshot;
|
|
153
219
|
}
|
|
154
220
|
refLocator(ref) {
|
|
155
|
-
|
|
156
|
-
let frame = page.mainFrame();
|
|
221
|
+
let frame = this._lastSnapshotFrames[0];
|
|
157
222
|
const match = ref.match(/^f(\d+)(.*)/);
|
|
158
223
|
if (match) {
|
|
159
224
|
const frameIndex = parseInt(match[1], 10);
|
|
160
|
-
if (!this._lastSnapshotFrames[frameIndex])
|
|
161
|
-
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
162
225
|
frame = this._lastSnapshotFrames[frameIndex];
|
|
163
226
|
ref = match[2];
|
|
164
227
|
}
|
|
228
|
+
if (!frame)
|
|
229
|
+
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
165
230
|
return frame.locator(`aria-ref=${ref}`);
|
|
166
231
|
}
|
|
167
232
|
}
|
package/lib/index.js
CHANGED
|
@@ -59,6 +59,7 @@ const commonTools = [
|
|
|
59
59
|
common.wait,
|
|
60
60
|
common.pdf,
|
|
61
61
|
common.close,
|
|
62
|
+
common.install,
|
|
62
63
|
];
|
|
63
64
|
const snapshotTools = [
|
|
64
65
|
common.navigate(true),
|
|
@@ -96,7 +97,9 @@ function createServer(options) {
|
|
|
96
97
|
version: packageJSON.version,
|
|
97
98
|
tools,
|
|
98
99
|
resources,
|
|
100
|
+
browserName: options?.browserName,
|
|
99
101
|
userDataDir: options?.userDataDir ?? '',
|
|
100
102
|
launchOptions: options?.launchOptions,
|
|
103
|
+
cdpEndpoint: options?.cdpEndpoint,
|
|
101
104
|
});
|
|
102
105
|
}
|
package/lib/program.js
CHANGED
|
@@ -32,20 +32,53 @@ const packageJSON = require('../package.json');
|
|
|
32
32
|
commander_1.program
|
|
33
33
|
.version('Version ' + packageJSON.version)
|
|
34
34
|
.name(packageJSON.name)
|
|
35
|
+
.option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
36
|
+
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
37
|
+
.option('--executable-path <path>', 'Path to the browser executable.')
|
|
35
38
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
|
39
|
+
.option('--port <port>', 'Port to listen on for SSE transport.')
|
|
36
40
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
37
41
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
38
|
-
.option('--port <port>', 'Port to listen on for SSE transport.')
|
|
39
42
|
.action(async (options) => {
|
|
43
|
+
let browserName;
|
|
44
|
+
let channel;
|
|
45
|
+
switch (options.browser) {
|
|
46
|
+
case 'chrome':
|
|
47
|
+
case 'chrome-beta':
|
|
48
|
+
case 'chrome-canary':
|
|
49
|
+
case 'chrome-dev':
|
|
50
|
+
case 'msedge':
|
|
51
|
+
case 'msedge-beta':
|
|
52
|
+
case 'msedge-canary':
|
|
53
|
+
case 'msedge-dev':
|
|
54
|
+
browserName = 'chromium';
|
|
55
|
+
channel = options.browser;
|
|
56
|
+
break;
|
|
57
|
+
case 'chromium':
|
|
58
|
+
browserName = 'chromium';
|
|
59
|
+
break;
|
|
60
|
+
case 'firefox':
|
|
61
|
+
browserName = 'firefox';
|
|
62
|
+
break;
|
|
63
|
+
case 'webkit':
|
|
64
|
+
browserName = 'webkit';
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
browserName = 'chromium';
|
|
68
|
+
channel = 'chrome';
|
|
69
|
+
}
|
|
40
70
|
const launchOptions = {
|
|
41
71
|
headless: !!options.headless,
|
|
42
|
-
channel
|
|
72
|
+
channel,
|
|
73
|
+
executablePath: options.executablePath,
|
|
43
74
|
};
|
|
44
|
-
const userDataDir = options.userDataDir ?? await createUserDataDir();
|
|
75
|
+
const userDataDir = options.userDataDir ?? await createUserDataDir(browserName);
|
|
45
76
|
const serverList = new server_1.ServerList(() => (0, index_1.createServer)({
|
|
77
|
+
browserName,
|
|
46
78
|
userDataDir,
|
|
47
79
|
launchOptions,
|
|
48
80
|
vision: !!options.vision,
|
|
81
|
+
cdpEndpoint: options.cdpEndpoint,
|
|
49
82
|
}));
|
|
50
83
|
setupExitWatchdog(serverList);
|
|
51
84
|
if (options.port) {
|
|
@@ -64,7 +97,7 @@ function setupExitWatchdog(serverList) {
|
|
|
64
97
|
});
|
|
65
98
|
}
|
|
66
99
|
commander_1.program.parse(process.argv);
|
|
67
|
-
async function createUserDataDir() {
|
|
100
|
+
async function createUserDataDir(browserName) {
|
|
68
101
|
let cacheDirectory;
|
|
69
102
|
if (process.platform === 'linux')
|
|
70
103
|
cacheDirectory = process.env.XDG_CACHE_HOME || path_1.default.join(os_1.default.homedir(), '.cache');
|
|
@@ -74,7 +107,7 @@ async function createUserDataDir() {
|
|
|
74
107
|
cacheDirectory = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
75
108
|
else
|
|
76
109
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
77
|
-
const result = path_1.default.join(cacheDirectory, 'ms-playwright',
|
|
110
|
+
const result = path_1.default.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
|
|
78
111
|
await fs_1.default.promises.mkdir(result, { recursive: true });
|
|
79
112
|
return result;
|
|
80
113
|
}
|
package/lib/server.js
CHANGED
|
@@ -21,8 +21,8 @@ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
|
21
21
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
22
22
|
const context_1 = require("./context");
|
|
23
23
|
function createServerWithTools(options) {
|
|
24
|
-
const { name, version, tools, resources
|
|
25
|
-
const context = new context_1.Context(
|
|
24
|
+
const { name, version, tools, resources } = options;
|
|
25
|
+
const context = new context_1.Context(options);
|
|
26
26
|
const server = new index_js_1.Server({ name, version }, {
|
|
27
27
|
capabilities: {
|
|
28
28
|
tools: {},
|
package/lib/tools/common.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.chooseFile = exports.close = exports.pdf = exports.pressKey = exports.wait = exports.goForward = exports.goBack = exports.navigate = void 0;
|
|
21
|
+
exports.install = exports.chooseFile = exports.close = exports.pdf = exports.pressKey = exports.wait = exports.goForward = exports.goBack = exports.navigate = void 0;
|
|
22
22
|
const os_1 = __importDefault(require("os"));
|
|
23
23
|
const path_1 = __importDefault(require("path"));
|
|
24
24
|
const zod_1 = require("zod");
|
|
@@ -119,7 +119,7 @@ exports.pdf = {
|
|
|
119
119
|
},
|
|
120
120
|
handle: async (context) => {
|
|
121
121
|
const page = context.existingPage();
|
|
122
|
-
const fileName = path_1.default.join(os_1.default.tmpdir(),
|
|
122
|
+
const fileName = path_1.default.join(os_1.default.tmpdir(), (0, utils_1.sanitizeForFilePath)(`page-${new Date().toISOString()}`)) + '.pdf';
|
|
123
123
|
await page.pdf({ path: fileName });
|
|
124
124
|
return {
|
|
125
125
|
content: [{
|
|
@@ -163,3 +163,19 @@ const chooseFile = snapshot => ({
|
|
|
163
163
|
},
|
|
164
164
|
});
|
|
165
165
|
exports.chooseFile = chooseFile;
|
|
166
|
+
exports.install = {
|
|
167
|
+
schema: {
|
|
168
|
+
name: 'browser_install',
|
|
169
|
+
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
|
170
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(zod_1.z.object({})),
|
|
171
|
+
},
|
|
172
|
+
handle: async (context) => {
|
|
173
|
+
const channel = await context.install();
|
|
174
|
+
return {
|
|
175
|
+
content: [{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: `Browser ${channel} installed`,
|
|
178
|
+
}],
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
package/lib/tools/utils.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.runAndWait = runAndWait;
|
|
19
19
|
exports.captureAriaSnapshot = captureAriaSnapshot;
|
|
20
|
+
exports.sanitizeForFilePath = sanitizeForFilePath;
|
|
20
21
|
async function waitForCompletion(page, callback) {
|
|
21
22
|
const requests = new Set();
|
|
22
23
|
let frameNavigated = false;
|
|
@@ -88,3 +89,6 @@ async function captureAriaSnapshot(context, status = '') {
|
|
|
88
89
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
function sanitizeForFilePath(s) {
|
|
93
|
+
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,18 +32,19 @@
|
|
|
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-1743163434000",
|
|
36
|
+
"yaml": "^2.7.1",
|
|
36
37
|
"zod-to-json-schema": "^3.24.4"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@eslint/eslintrc": "^3.2.0",
|
|
40
41
|
"@eslint/js": "^9.19.0",
|
|
41
|
-
"@playwright/test": "1.52.0-alpha-
|
|
42
|
+
"@playwright/test": "^1.52.0-alpha-1743163434000",
|
|
42
43
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
44
|
+
"@types/node": "^22.13.10",
|
|
43
45
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
44
46
|
"@typescript-eslint/parser": "^8.26.1",
|
|
45
47
|
"@typescript-eslint/utils": "^8.26.1",
|
|
46
|
-
"@types/node": "^22.13.10",
|
|
47
48
|
"eslint": "^9.19.0",
|
|
48
49
|
"eslint-plugin-import": "^2.31.0",
|
|
49
50
|
"eslint-plugin-notice": "^1.0.0",
|