@playwright/mcp 0.0.15 → 0.0.17
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 +124 -65
- package/config.d.ts +113 -0
- package/index.d.ts +2 -39
- package/lib/config.js +157 -0
- package/lib/context.js +34 -163
- package/lib/index.js +4 -59
- package/lib/pageSnapshot.js +96 -0
- package/lib/program.js +12 -85
- package/lib/server.js +14 -35
- package/lib/tab.js +81 -0
- package/lib/tools/install.js +1 -1
- package/lib/tools/pdf.js +2 -7
- package/lib/tools/snapshot.js +18 -19
- package/lib/transport.js +124 -0
- package/package.json +5 -5
package/lib/tab.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.Tab = void 0;
|
|
19
|
+
const pageSnapshot_1 = require("./pageSnapshot");
|
|
20
|
+
class Tab {
|
|
21
|
+
context;
|
|
22
|
+
page;
|
|
23
|
+
_console = [];
|
|
24
|
+
_requests = new Map();
|
|
25
|
+
_snapshot;
|
|
26
|
+
_onPageClose;
|
|
27
|
+
constructor(context, page, onPageClose) {
|
|
28
|
+
this.context = context;
|
|
29
|
+
this.page = page;
|
|
30
|
+
this._onPageClose = onPageClose;
|
|
31
|
+
page.on('console', event => this._console.push(event));
|
|
32
|
+
page.on('request', request => this._requests.set(request, null));
|
|
33
|
+
page.on('response', response => this._requests.set(response.request(), response));
|
|
34
|
+
page.on('framenavigated', frame => {
|
|
35
|
+
if (!frame.parentFrame())
|
|
36
|
+
this._clearCollectedArtifacts();
|
|
37
|
+
});
|
|
38
|
+
page.on('close', () => this._onClose());
|
|
39
|
+
page.on('filechooser', chooser => {
|
|
40
|
+
this.context.setModalState({
|
|
41
|
+
type: 'fileChooser',
|
|
42
|
+
description: 'File chooser',
|
|
43
|
+
fileChooser: chooser,
|
|
44
|
+
}, this);
|
|
45
|
+
});
|
|
46
|
+
page.on('dialog', dialog => this.context.dialogShown(this, dialog));
|
|
47
|
+
page.setDefaultNavigationTimeout(60000);
|
|
48
|
+
page.setDefaultTimeout(5000);
|
|
49
|
+
}
|
|
50
|
+
_clearCollectedArtifacts() {
|
|
51
|
+
this._console.length = 0;
|
|
52
|
+
this._requests.clear();
|
|
53
|
+
}
|
|
54
|
+
_onClose() {
|
|
55
|
+
this._clearCollectedArtifacts();
|
|
56
|
+
this._onPageClose(this);
|
|
57
|
+
}
|
|
58
|
+
async navigate(url) {
|
|
59
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
60
|
+
// Cap load event to 5 seconds, the page is operational at this point.
|
|
61
|
+
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
hasSnapshot() {
|
|
64
|
+
return !!this._snapshot;
|
|
65
|
+
}
|
|
66
|
+
snapshotOrDie() {
|
|
67
|
+
if (!this._snapshot)
|
|
68
|
+
throw new Error('No snapshot available');
|
|
69
|
+
return this._snapshot;
|
|
70
|
+
}
|
|
71
|
+
console() {
|
|
72
|
+
return this._console;
|
|
73
|
+
}
|
|
74
|
+
requests() {
|
|
75
|
+
return this._requests;
|
|
76
|
+
}
|
|
77
|
+
async captureSnapshot() {
|
|
78
|
+
this._snapshot = await pageSnapshot_1.PageSnapshot.create(this.page);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.Tab = Tab;
|
package/lib/tools/install.js
CHANGED
|
@@ -30,7 +30,7 @@ const install = (0, tool_1.defineTool)({
|
|
|
30
30
|
inputSchema: zod_1.z.object({}),
|
|
31
31
|
},
|
|
32
32
|
handle: async (context) => {
|
|
33
|
-
const channel = context.
|
|
33
|
+
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
|
|
34
34
|
const cli = path_1.default.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
|
35
35
|
const child = (0, child_process_1.fork)(cli, ['install', channel], {
|
|
36
36
|
stdio: 'pipe',
|
package/lib/tools/pdf.js
CHANGED
|
@@ -47,16 +47,11 @@ 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
|
-
};
|
|
53
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
const os_1 = __importDefault(require("os"));
|
|
55
|
-
const path_1 = __importDefault(require("path"));
|
|
56
51
|
const zod_1 = require("zod");
|
|
57
52
|
const tool_1 = require("./tool");
|
|
58
|
-
const utils_1 = require("./utils");
|
|
59
53
|
const javascript = __importStar(require("../javascript"));
|
|
54
|
+
const config_1 = require("../config");
|
|
60
55
|
const pdf = (0, tool_1.defineTool)({
|
|
61
56
|
capability: 'pdf',
|
|
62
57
|
schema: {
|
|
@@ -66,7 +61,7 @@ const pdf = (0, tool_1.defineTool)({
|
|
|
66
61
|
},
|
|
67
62
|
handle: async (context) => {
|
|
68
63
|
const tab = context.currentTabOrDie();
|
|
69
|
-
const fileName =
|
|
64
|
+
const fileName = await (0, config_1.outputFile)(context.config, `page-${new Date().toISOString()}'.pdf'`);
|
|
70
65
|
const code = [
|
|
71
66
|
`// Save page as ${fileName}`,
|
|
72
67
|
`await page.pdf(${javascript.formatObject({ path: fileName })});`,
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -47,17 +47,12 @@ 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
|
-
};
|
|
53
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
|
|
55
|
-
const os_1 = __importDefault(require("os"));
|
|
51
|
+
exports.generateLocator = generateLocator;
|
|
56
52
|
const zod_1 = require("zod");
|
|
57
|
-
const utils_1 = require("./utils");
|
|
58
|
-
const context_1 = require("../context");
|
|
59
|
-
const javascript = __importStar(require("../javascript"));
|
|
60
53
|
const tool_1 = require("./tool");
|
|
54
|
+
const javascript = __importStar(require("../javascript"));
|
|
55
|
+
const config_1 = require("../config");
|
|
61
56
|
const snapshot = (0, tool_1.defineTool)({
|
|
62
57
|
capability: 'core',
|
|
63
58
|
schema: {
|
|
@@ -90,7 +85,7 @@ const click = (0, tool_1.defineTool)({
|
|
|
90
85
|
const locator = tab.snapshotOrDie().refLocator(params.ref);
|
|
91
86
|
const code = [
|
|
92
87
|
`// Click ${params.element}`,
|
|
93
|
-
`await page.${await
|
|
88
|
+
`await page.${await generateLocator(locator)}.click();`
|
|
94
89
|
];
|
|
95
90
|
return {
|
|
96
91
|
code,
|
|
@@ -118,7 +113,7 @@ const drag = (0, tool_1.defineTool)({
|
|
|
118
113
|
const endLocator = snapshot.refLocator(params.endRef);
|
|
119
114
|
const code = [
|
|
120
115
|
`// Drag ${params.startElement} to ${params.endElement}`,
|
|
121
|
-
`await page.${await
|
|
116
|
+
`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`
|
|
122
117
|
];
|
|
123
118
|
return {
|
|
124
119
|
code,
|
|
@@ -140,7 +135,7 @@ const hover = (0, tool_1.defineTool)({
|
|
|
140
135
|
const locator = snapshot.refLocator(params.ref);
|
|
141
136
|
const code = [
|
|
142
137
|
`// Hover over ${params.element}`,
|
|
143
|
-
`await page.${await
|
|
138
|
+
`await page.${await generateLocator(locator)}.hover();`
|
|
144
139
|
];
|
|
145
140
|
return {
|
|
146
141
|
code,
|
|
@@ -169,17 +164,17 @@ const type = (0, tool_1.defineTool)({
|
|
|
169
164
|
const steps = [];
|
|
170
165
|
if (params.slowly) {
|
|
171
166
|
code.push(`// Press "${params.text}" sequentially into "${params.element}"`);
|
|
172
|
-
code.push(`await page.${await
|
|
167
|
+
code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
|
|
173
168
|
steps.push(() => locator.pressSequentially(params.text));
|
|
174
169
|
}
|
|
175
170
|
else {
|
|
176
171
|
code.push(`// Fill "${params.text}" into "${params.element}"`);
|
|
177
|
-
code.push(`await page.${await
|
|
172
|
+
code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
|
|
178
173
|
steps.push(() => locator.fill(params.text));
|
|
179
174
|
}
|
|
180
175
|
if (params.submit) {
|
|
181
176
|
code.push(`// Submit text`);
|
|
182
|
-
code.push(`await page.${await
|
|
177
|
+
code.push(`await page.${await generateLocator(locator)}.press('Enter');`);
|
|
183
178
|
steps.push(() => locator.press('Enter'));
|
|
184
179
|
}
|
|
185
180
|
return {
|
|
@@ -205,7 +200,7 @@ const selectOption = (0, tool_1.defineTool)({
|
|
|
205
200
|
const locator = snapshot.refLocator(params.ref);
|
|
206
201
|
const code = [
|
|
207
202
|
`// Select options [${params.values.join(', ')}] in ${params.element}`,
|
|
208
|
-
`await page.${await
|
|
203
|
+
`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`
|
|
209
204
|
];
|
|
210
205
|
return {
|
|
211
206
|
code,
|
|
@@ -236,7 +231,7 @@ const screenshot = (0, tool_1.defineTool)({
|
|
|
236
231
|
const tab = context.currentTabOrDie();
|
|
237
232
|
const snapshot = tab.snapshotOrDie();
|
|
238
233
|
const fileType = params.raw ? 'png' : 'jpeg';
|
|
239
|
-
const fileName =
|
|
234
|
+
const fileName = await (0, config_1.outputFile)(context.config, `page-${new Date().toISOString()}.${fileType}`);
|
|
240
235
|
const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
|
|
241
236
|
const isElementScreenshot = params.element && params.ref;
|
|
242
237
|
const code = [
|
|
@@ -244,17 +239,18 @@ const screenshot = (0, tool_1.defineTool)({
|
|
|
244
239
|
];
|
|
245
240
|
const locator = params.ref ? snapshot.refLocator(params.ref) : null;
|
|
246
241
|
if (locator)
|
|
247
|
-
code.push(`await page.${await
|
|
242
|
+
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
248
243
|
else
|
|
249
244
|
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
245
|
+
const includeBase64 = !context.config.tools?.browser_take_screenshot?.omitBase64;
|
|
250
246
|
const action = async () => {
|
|
251
247
|
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
|
252
248
|
return {
|
|
253
|
-
content: [{
|
|
249
|
+
content: includeBase64 ? [{
|
|
254
250
|
type: 'image',
|
|
255
251
|
data: screenshot.toString('base64'),
|
|
256
252
|
mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
|
257
|
-
}]
|
|
253
|
+
}] : []
|
|
258
254
|
};
|
|
259
255
|
};
|
|
260
256
|
return {
|
|
@@ -265,6 +261,9 @@ const screenshot = (0, tool_1.defineTool)({
|
|
|
265
261
|
};
|
|
266
262
|
}
|
|
267
263
|
});
|
|
264
|
+
async function generateLocator(locator) {
|
|
265
|
+
return locator._generateLocatorString();
|
|
266
|
+
}
|
|
268
267
|
exports.default = [
|
|
269
268
|
snapshot,
|
|
270
269
|
click,
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.startStdioTransport = startStdioTransport;
|
|
22
|
+
exports.startHttpTransport = startHttpTransport;
|
|
23
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
24
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
25
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
26
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
27
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
28
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
29
|
+
async function startStdioTransport(serverList) {
|
|
30
|
+
const server = await serverList.create();
|
|
31
|
+
await server.connect(new stdio_js_1.StdioServerTransport());
|
|
32
|
+
}
|
|
33
|
+
async function handleSSE(req, res, url, serverList, sessions) {
|
|
34
|
+
if (req.method === 'POST') {
|
|
35
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
res.statusCode = 400;
|
|
38
|
+
return res.end('Missing sessionId');
|
|
39
|
+
}
|
|
40
|
+
const transport = sessions.get(sessionId);
|
|
41
|
+
if (!transport) {
|
|
42
|
+
res.statusCode = 404;
|
|
43
|
+
return res.end('Session not found');
|
|
44
|
+
}
|
|
45
|
+
return await transport.handlePostMessage(req, res);
|
|
46
|
+
}
|
|
47
|
+
else if (req.method === 'GET') {
|
|
48
|
+
const transport = new sse_js_1.SSEServerTransport('/sse', res);
|
|
49
|
+
sessions.set(transport.sessionId, transport);
|
|
50
|
+
const server = await serverList.create();
|
|
51
|
+
res.on('close', () => {
|
|
52
|
+
sessions.delete(transport.sessionId);
|
|
53
|
+
serverList.close(server).catch(e => console.error(e));
|
|
54
|
+
});
|
|
55
|
+
return await server.connect(transport);
|
|
56
|
+
}
|
|
57
|
+
res.statusCode = 405;
|
|
58
|
+
res.end('Method not allowed');
|
|
59
|
+
}
|
|
60
|
+
async function handleStreamable(req, res, serverList, sessions) {
|
|
61
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
62
|
+
if (sessionId) {
|
|
63
|
+
const transport = sessions.get(sessionId);
|
|
64
|
+
if (!transport) {
|
|
65
|
+
res.statusCode = 404;
|
|
66
|
+
res.end('Session not found');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
return await transport.handleRequest(req, res);
|
|
70
|
+
}
|
|
71
|
+
if (req.method === 'POST') {
|
|
72
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
73
|
+
sessionIdGenerator: () => node_crypto_1.default.randomUUID(),
|
|
74
|
+
onsessioninitialized: sessionId => {
|
|
75
|
+
sessions.set(sessionId, transport);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
transport.onclose = () => {
|
|
79
|
+
if (transport.sessionId)
|
|
80
|
+
sessions.delete(transport.sessionId);
|
|
81
|
+
};
|
|
82
|
+
const server = await serverList.create();
|
|
83
|
+
await server.connect(transport);
|
|
84
|
+
return await transport.handleRequest(req, res);
|
|
85
|
+
}
|
|
86
|
+
res.statusCode = 400;
|
|
87
|
+
res.end('Invalid request');
|
|
88
|
+
}
|
|
89
|
+
function startHttpTransport(port, hostname, serverList) {
|
|
90
|
+
const sseSessions = new Map();
|
|
91
|
+
const streamableSessions = new Map();
|
|
92
|
+
const httpServer = node_http_1.default.createServer(async (req, res) => {
|
|
93
|
+
const url = new URL(`http://localhost${req.url}`);
|
|
94
|
+
if (url.pathname.startsWith('/mcp'))
|
|
95
|
+
await handleStreamable(req, res, serverList, streamableSessions);
|
|
96
|
+
else
|
|
97
|
+
await handleSSE(req, res, url, serverList, sseSessions);
|
|
98
|
+
});
|
|
99
|
+
httpServer.listen(port, hostname, () => {
|
|
100
|
+
const address = httpServer.address();
|
|
101
|
+
(0, node_assert_1.default)(address, 'Could not bind server socket');
|
|
102
|
+
let url;
|
|
103
|
+
if (typeof address === 'string') {
|
|
104
|
+
url = address;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const resolvedPort = address.port;
|
|
108
|
+
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
109
|
+
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
|
|
110
|
+
resolvedHost = 'localhost';
|
|
111
|
+
url = `http://${resolvedHost}:${resolvedPort}`;
|
|
112
|
+
}
|
|
113
|
+
console.log(`Listening on ${url}`);
|
|
114
|
+
console.log('Put this in your client config:');
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
'mcpServers': {
|
|
117
|
+
'playwright': {
|
|
118
|
+
'url': `${url}/sse`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, undefined, 2));
|
|
122
|
+
console.log('If your client supports streamable HTTP, you can use the /mcp endpoint instead.');
|
|
123
|
+
});
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
|
-
"lint": "eslint .",
|
|
19
|
+
"lint": "npm run update-readme && eslint .",
|
|
20
20
|
"update-readme": "node utils/update-readme.js",
|
|
21
21
|
"watch": "tsc --watch",
|
|
22
22
|
"test": "playwright test",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
38
38
|
"commander": "^13.1.0",
|
|
39
|
-
"playwright": "1.53.0-alpha-
|
|
39
|
+
"playwright": "1.53.0-alpha-2025-04-25",
|
|
40
40
|
"yaml": "^2.7.1",
|
|
41
41
|
"zod-to-json-schema": "^3.24.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@eslint/eslintrc": "^3.2.0",
|
|
45
45
|
"@eslint/js": "^9.19.0",
|
|
46
|
-
"@playwright/test": "1.53.0-alpha-
|
|
46
|
+
"@playwright/test": "1.53.0-alpha-2025-04-25",
|
|
47
47
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
48
48
|
"@types/node": "^22.13.10",
|
|
49
49
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|