@playwright/mcp 0.0.4 → 0.0.6
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 +20 -13
- package/lib/context.js +52 -1
- package/lib/index.js +3 -0
- package/lib/program.js +68 -2
- package/lib/server.js +2 -1
- package/lib/tools/common.js +20 -3
- package/lib/tools/screenshot.js +2 -2
- package/lib/tools/snapshot.js +30 -14
- package/lib/tools/utils.js +16 -13
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -93,27 +93,19 @@ This mode is useful for background or batch operations.
|
|
|
93
93
|
### Running headed browser on Linux w/o DISPLAY
|
|
94
94
|
|
|
95
95
|
When running headed browser on system w/o display or from worker processes of the IDEs,
|
|
96
|
-
|
|
97
|
-
from environment with the DISPLAY
|
|
96
|
+
run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable SSE transport.
|
|
98
97
|
|
|
99
|
-
```
|
|
100
|
-
npx playwright
|
|
98
|
+
```bash
|
|
99
|
+
npx @playwright/mcp@latest --port 8931
|
|
101
100
|
```
|
|
102
101
|
|
|
103
|
-
And then in MCP config,
|
|
102
|
+
And then in MCP client config, set the `url` to the SSE endpoint:
|
|
104
103
|
|
|
105
104
|
```js
|
|
106
105
|
{
|
|
107
106
|
"mcpServers": {
|
|
108
107
|
"playwright": {
|
|
109
|
-
"
|
|
110
|
-
"args": [
|
|
111
|
-
"@playwright/mcp@latest"
|
|
112
|
-
],
|
|
113
|
-
"env": {
|
|
114
|
-
// Use the endpoint from the output of the server above.
|
|
115
|
-
"PLAYWRIGHT_WS_ENDPOINT": "ws://localhost:<port>/"
|
|
116
|
-
}
|
|
108
|
+
"url": "http://localhost:8931/sse"
|
|
117
109
|
}
|
|
118
110
|
}
|
|
119
111
|
}
|
|
@@ -211,6 +203,11 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
|
|
|
211
203
|
- `ref` (string): Exact target element reference from the page snapshot
|
|
212
204
|
- `values` (array): Array of values to select in the dropdown.
|
|
213
205
|
|
|
206
|
+
- **browser_choose_file**
|
|
207
|
+
- Description: Choose one or multiple files to upload
|
|
208
|
+
- Parameters:
|
|
209
|
+
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
|
210
|
+
|
|
214
211
|
- **browser_press_key**
|
|
215
212
|
- Description: Press a key on the keyboard
|
|
216
213
|
- Parameters:
|
|
@@ -224,6 +221,11 @@ The Playwright MCP provides a set of tools for browser automation. Here are all
|
|
|
224
221
|
- Description: Save page as PDF
|
|
225
222
|
- Parameters: None
|
|
226
223
|
|
|
224
|
+
- **browser_take_screenshot**
|
|
225
|
+
- Description: Capture screenshot of the page
|
|
226
|
+
- Parameters:
|
|
227
|
+
- `raw` (string): Optionally returns lossless PNG screenshot. JPEG by default.
|
|
228
|
+
|
|
227
229
|
- **browser_wait**
|
|
228
230
|
- Description: Wait for a specified time in seconds
|
|
229
231
|
- Parameters:
|
|
@@ -286,6 +288,11 @@ Vision Mode provides tools for visual-based interactions using screenshots. Here
|
|
|
286
288
|
- Parameters:
|
|
287
289
|
- `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`
|
|
288
290
|
|
|
291
|
+
- **browser_choose_file**
|
|
292
|
+
- Description: Choose one or multiple files to upload
|
|
293
|
+
- Parameters:
|
|
294
|
+
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
|
295
|
+
|
|
289
296
|
- **browser_save_as_pdf**
|
|
290
297
|
- Description: Save page as PDF
|
|
291
298
|
- Parameters: None
|
package/lib/context.js
CHANGED
|
@@ -57,6 +57,8 @@ class Context {
|
|
|
57
57
|
_page;
|
|
58
58
|
_console = [];
|
|
59
59
|
_createPagePromise;
|
|
60
|
+
_fileChooser;
|
|
61
|
+
_lastSnapshotFrames = [];
|
|
60
62
|
constructor(userDataDir, launchOptions) {
|
|
61
63
|
this._userDataDir = userDataDir;
|
|
62
64
|
this._launchOptions = launchOptions;
|
|
@@ -72,6 +74,9 @@ class Context {
|
|
|
72
74
|
this._console.length = 0;
|
|
73
75
|
});
|
|
74
76
|
page.on('close', () => this._onPageClose());
|
|
77
|
+
page.on('filechooser', chooser => this._fileChooser = chooser);
|
|
78
|
+
page.setDefaultNavigationTimeout(60000);
|
|
79
|
+
page.setDefaultTimeout(5000);
|
|
75
80
|
this._page = page;
|
|
76
81
|
this._browser = browser;
|
|
77
82
|
return page;
|
|
@@ -85,9 +90,10 @@ class Context {
|
|
|
85
90
|
this._createPagePromise = undefined;
|
|
86
91
|
this._browser = undefined;
|
|
87
92
|
this._page = undefined;
|
|
93
|
+
this._fileChooser = undefined;
|
|
88
94
|
this._console.length = 0;
|
|
89
95
|
}
|
|
90
|
-
|
|
96
|
+
existingPage() {
|
|
91
97
|
if (!this._page)
|
|
92
98
|
throw new Error('Navigate to a location to create a page');
|
|
93
99
|
return this._page;
|
|
@@ -100,6 +106,18 @@ class Context {
|
|
|
100
106
|
return;
|
|
101
107
|
await this._page.close();
|
|
102
108
|
}
|
|
109
|
+
async submitFileChooser(paths) {
|
|
110
|
+
if (!this._fileChooser)
|
|
111
|
+
throw new Error('No file chooser visible');
|
|
112
|
+
await this._fileChooser.setFiles(paths);
|
|
113
|
+
this._fileChooser = undefined;
|
|
114
|
+
}
|
|
115
|
+
hasFileChooser() {
|
|
116
|
+
return !!this._fileChooser;
|
|
117
|
+
}
|
|
118
|
+
clearFileChooser() {
|
|
119
|
+
this._fileChooser = undefined;
|
|
120
|
+
}
|
|
103
121
|
async _createPage() {
|
|
104
122
|
if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
|
|
105
123
|
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
|
|
@@ -113,5 +131,38 @@ class Context {
|
|
|
113
131
|
const [page] = context.pages();
|
|
114
132
|
return { page };
|
|
115
133
|
}
|
|
134
|
+
async allFramesSnapshot() {
|
|
135
|
+
const page = this.existingPage();
|
|
136
|
+
const visibleFrames = await page.locator('iframe').filter({ visible: true }).all();
|
|
137
|
+
this._lastSnapshotFrames = visibleFrames.map(frame => frame.contentFrame());
|
|
138
|
+
const snapshots = await Promise.all([
|
|
139
|
+
page.locator('html').ariaSnapshot({ ref: true }),
|
|
140
|
+
...this._lastSnapshotFrames.map(async (frame, index) => {
|
|
141
|
+
const snapshot = await frame.locator('html').ariaSnapshot({ ref: true });
|
|
142
|
+
const args = [];
|
|
143
|
+
const src = await frame.owner().getAttribute('src');
|
|
144
|
+
if (src)
|
|
145
|
+
args.push(`src=${src}`);
|
|
146
|
+
const name = await frame.owner().getAttribute('name');
|
|
147
|
+
if (name)
|
|
148
|
+
args.push(`name=${name}`);
|
|
149
|
+
return `\n# iframe ${args.join(' ')}\n` + snapshot.replaceAll('[ref=', `[ref=f${index}`);
|
|
150
|
+
})
|
|
151
|
+
]);
|
|
152
|
+
return snapshots.join('\n');
|
|
153
|
+
}
|
|
154
|
+
refLocator(ref) {
|
|
155
|
+
const page = this.existingPage();
|
|
156
|
+
let frame = page.mainFrame();
|
|
157
|
+
const match = ref.match(/^f(\d+)(.*)/);
|
|
158
|
+
if (match) {
|
|
159
|
+
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
|
+
frame = this._lastSnapshotFrames[frameIndex];
|
|
163
|
+
ref = match[2];
|
|
164
|
+
}
|
|
165
|
+
return frame.locator(`aria-ref=${ref}`);
|
|
166
|
+
}
|
|
116
167
|
}
|
|
117
168
|
exports.Context = Context;
|
package/lib/index.js
CHANGED
|
@@ -64,17 +64,20 @@ const snapshotTools = [
|
|
|
64
64
|
common.navigate(true),
|
|
65
65
|
common.goBack(true),
|
|
66
66
|
common.goForward(true),
|
|
67
|
+
common.chooseFile(true),
|
|
67
68
|
snapshot.snapshot,
|
|
68
69
|
snapshot.click,
|
|
69
70
|
snapshot.hover,
|
|
70
71
|
snapshot.type,
|
|
71
72
|
snapshot.selectOption,
|
|
73
|
+
snapshot.screenshot,
|
|
72
74
|
...commonTools,
|
|
73
75
|
];
|
|
74
76
|
const screenshotTools = [
|
|
75
77
|
common.navigate(false),
|
|
76
78
|
common.goBack(false),
|
|
77
79
|
common.goForward(false),
|
|
80
|
+
common.chooseFile(false),
|
|
78
81
|
screenshot.screenshot,
|
|
79
82
|
screenshot.moveMouse,
|
|
80
83
|
screenshot.click,
|
package/lib/program.js
CHANGED
|
@@ -18,12 +18,15 @@ 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
|
+
const http_1 = __importDefault(require("http"));
|
|
21
22
|
const fs_1 = __importDefault(require("fs"));
|
|
22
23
|
const os_1 = __importDefault(require("os"));
|
|
23
24
|
const path_1 = __importDefault(require("path"));
|
|
24
25
|
const commander_1 = require("commander");
|
|
25
26
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
27
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
26
28
|
const index_1 = require("./index");
|
|
29
|
+
const assert_1 = __importDefault(require("assert"));
|
|
27
30
|
const packageJSON = require('../package.json');
|
|
28
31
|
commander_1.program
|
|
29
32
|
.version('Version ' + packageJSON.version)
|
|
@@ -31,6 +34,7 @@ commander_1.program
|
|
|
31
34
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
|
32
35
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
33
36
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
37
|
+
.option('--port <port>', 'Port to listen on for SSE transport.')
|
|
34
38
|
.action(async (options) => {
|
|
35
39
|
const launchOptions = {
|
|
36
40
|
headless: !!options.headless,
|
|
@@ -39,10 +43,72 @@ commander_1.program
|
|
|
39
43
|
const server = (0, index_1.createServer)({
|
|
40
44
|
userDataDir: options.userDataDir ?? await userDataDir(),
|
|
41
45
|
launchOptions,
|
|
46
|
+
vision: !!options.vision,
|
|
42
47
|
});
|
|
43
48
|
setupExitWatchdog(server);
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
if (options.port) {
|
|
50
|
+
const sessions = new Map();
|
|
51
|
+
const httpServer = http_1.default.createServer(async (req, res) => {
|
|
52
|
+
if (req.method === 'POST') {
|
|
53
|
+
const host = req.headers.host ?? 'http://unknown';
|
|
54
|
+
const sessionId = new URL(host + req.url).searchParams.get('sessionId');
|
|
55
|
+
if (!sessionId) {
|
|
56
|
+
res.statusCode = 400;
|
|
57
|
+
res.end('Missing sessionId');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const transport = sessions.get(sessionId);
|
|
61
|
+
if (!transport) {
|
|
62
|
+
res.statusCode = 404;
|
|
63
|
+
res.end('Session not found');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await transport.handlePostMessage(req, res);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
else if (req.method === 'GET') {
|
|
70
|
+
const transport = new sse_js_1.SSEServerTransport('/sse', res);
|
|
71
|
+
sessions.set(transport.sessionId, transport);
|
|
72
|
+
res.on('close', () => {
|
|
73
|
+
sessions.delete(transport.sessionId);
|
|
74
|
+
});
|
|
75
|
+
await server.connect(transport);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
res.statusCode = 405;
|
|
80
|
+
res.end('Method not allowed');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
httpServer.listen(+options.port, () => {
|
|
84
|
+
const address = httpServer.address();
|
|
85
|
+
(0, assert_1.default)(address, 'Could not bind server socket');
|
|
86
|
+
let urlPrefixHumanReadable;
|
|
87
|
+
if (typeof address === 'string') {
|
|
88
|
+
urlPrefixHumanReadable = address;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const port = address.port;
|
|
92
|
+
let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
93
|
+
if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
|
|
94
|
+
resolvedHost = 'localhost';
|
|
95
|
+
urlPrefixHumanReadable = `http://${resolvedHost}:${port}`;
|
|
96
|
+
}
|
|
97
|
+
console.log(`Listening on ${urlPrefixHumanReadable}`);
|
|
98
|
+
console.log('Put this in your client config:');
|
|
99
|
+
console.log(JSON.stringify({
|
|
100
|
+
'mcpServers': {
|
|
101
|
+
'playwright': {
|
|
102
|
+
'url': `${urlPrefixHumanReadable}/sse`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, undefined, 2));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
110
|
+
await server.connect(transport);
|
|
111
|
+
}
|
|
46
112
|
});
|
|
47
113
|
function setupExitWatchdog(server) {
|
|
48
114
|
process.stdin.on('close', async () => {
|
package/lib/server.js
CHANGED
|
@@ -60,8 +60,9 @@ function createServerWithTools(options) {
|
|
|
60
60
|
const contents = await resource.read(context, request.params.uri);
|
|
61
61
|
return { contents };
|
|
62
62
|
});
|
|
63
|
+
const oldClose = server.close.bind(server);
|
|
63
64
|
server.close = async () => {
|
|
64
|
-
await
|
|
65
|
+
await oldClose();
|
|
65
66
|
await context.close();
|
|
66
67
|
};
|
|
67
68
|
return server;
|
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.close = exports.pdf = exports.pressKey = exports.wait = exports.goForward = exports.goBack = exports.navigate = void 0;
|
|
21
|
+
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");
|
|
@@ -40,7 +40,7 @@ const navigate = snapshot => ({
|
|
|
40
40
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
41
41
|
await page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
42
42
|
if (snapshot)
|
|
43
|
-
return (0, utils_1.captureAriaSnapshot)(
|
|
43
|
+
return (0, utils_1.captureAriaSnapshot)(context);
|
|
44
44
|
return {
|
|
45
45
|
content: [{
|
|
46
46
|
type: 'text',
|
|
@@ -118,7 +118,7 @@ exports.pdf = {
|
|
|
118
118
|
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(pdfSchema),
|
|
119
119
|
},
|
|
120
120
|
handle: async (context) => {
|
|
121
|
-
const page =
|
|
121
|
+
const page = context.existingPage();
|
|
122
122
|
const fileName = path_1.default.join(os_1.default.tmpdir(), `/page-${new Date().toISOString()}.pdf`);
|
|
123
123
|
await page.pdf({ path: fileName });
|
|
124
124
|
return {
|
|
@@ -146,3 +146,20 @@ exports.close = {
|
|
|
146
146
|
};
|
|
147
147
|
},
|
|
148
148
|
};
|
|
149
|
+
const chooseFileSchema = zod_1.z.object({
|
|
150
|
+
paths: zod_1.z.array(zod_1.z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
|
151
|
+
});
|
|
152
|
+
const chooseFile = snapshot => ({
|
|
153
|
+
schema: {
|
|
154
|
+
name: 'browser_choose_file',
|
|
155
|
+
description: 'Choose one or multiple files to upload',
|
|
156
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(chooseFileSchema),
|
|
157
|
+
},
|
|
158
|
+
handle: async (context, params) => {
|
|
159
|
+
const validatedParams = chooseFileSchema.parse(params);
|
|
160
|
+
return await (0, utils_1.runAndWait)(context, `Chose files ${validatedParams.paths.join(', ')}`, async () => {
|
|
161
|
+
await context.submitFileChooser(validatedParams.paths);
|
|
162
|
+
}, snapshot);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
exports.chooseFile = chooseFile;
|
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.selectOption = 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);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
const elementSchema = zod_1.z.object({
|
|
@@ -44,7 +44,7 @@ exports.click = {
|
|
|
44
44
|
},
|
|
45
45
|
handle: async (context, params) => {
|
|
46
46
|
const validatedParams = elementSchema.parse(params);
|
|
47
|
-
return (0, utils_1.runAndWait)(context, `"${validatedParams.element}" clicked`,
|
|
47
|
+
return (0, utils_1.runAndWait)(context, `"${validatedParams.element}" clicked`, () => context.refLocator(validatedParams.ref).click(), true);
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
const dragSchema = zod_1.z.object({
|
|
@@ -61,9 +61,9 @@ exports.drag = {
|
|
|
61
61
|
},
|
|
62
62
|
handle: async (context, params) => {
|
|
63
63
|
const validatedParams = dragSchema.parse(params);
|
|
64
|
-
return (0, utils_1.runAndWait)(context, `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, async (
|
|
65
|
-
const startLocator = refLocator(
|
|
66
|
-
const endLocator = refLocator(
|
|
64
|
+
return (0, utils_1.runAndWait)(context, `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, async () => {
|
|
65
|
+
const startLocator = context.refLocator(validatedParams.startRef);
|
|
66
|
+
const endLocator = context.refLocator(validatedParams.endRef);
|
|
67
67
|
await startLocator.dragTo(endLocator);
|
|
68
68
|
}, true);
|
|
69
69
|
},
|
|
@@ -76,7 +76,7 @@ exports.hover = {
|
|
|
76
76
|
},
|
|
77
77
|
handle: async (context, params) => {
|
|
78
78
|
const validatedParams = elementSchema.parse(params);
|
|
79
|
-
return (0, utils_1.runAndWait)(context, `Hovered over "${validatedParams.element}"`,
|
|
79
|
+
return (0, utils_1.runAndWait)(context, `Hovered over "${validatedParams.element}"`, () => context.refLocator(validatedParams.ref).hover(), true);
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
const typeSchema = elementSchema.extend({
|
|
@@ -91,8 +91,8 @@ exports.type = {
|
|
|
91
91
|
},
|
|
92
92
|
handle: async (context, params) => {
|
|
93
93
|
const validatedParams = typeSchema.parse(params);
|
|
94
|
-
return await (0, utils_1.runAndWait)(context, `Typed "${validatedParams.text}" into "${validatedParams.element}"`, async (
|
|
95
|
-
const locator = refLocator(
|
|
94
|
+
return await (0, utils_1.runAndWait)(context, `Typed "${validatedParams.text}" into "${validatedParams.element}"`, async () => {
|
|
95
|
+
const locator = context.refLocator(validatedParams.ref);
|
|
96
96
|
await locator.fill(validatedParams.text);
|
|
97
97
|
if (validatedParams.submit)
|
|
98
98
|
await locator.press('Enter');
|
|
@@ -110,12 +110,28 @@ exports.selectOption = {
|
|
|
110
110
|
},
|
|
111
111
|
handle: async (context, params) => {
|
|
112
112
|
const validatedParams = selectOptionSchema.parse(params);
|
|
113
|
-
return await (0, utils_1.runAndWait)(context, `Selected option in "${validatedParams.element}"`, async (
|
|
114
|
-
const locator = refLocator(
|
|
113
|
+
return await (0, utils_1.runAndWait)(context, `Selected option in "${validatedParams.element}"`, async () => {
|
|
114
|
+
const locator = context.refLocator(validatedParams.ref);
|
|
115
115
|
await locator.selectOption(validatedParams.values);
|
|
116
116
|
}, true);
|
|
117
117
|
},
|
|
118
118
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
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
|
+
};
|
package/lib/tools/utils.js
CHANGED
|
@@ -65,23 +65,26 @@ async function waitForCompletion(page, callback) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
async function runAndWait(context, status, callback, snapshot = false) {
|
|
68
|
-
const page =
|
|
68
|
+
const page = context.existingPage();
|
|
69
|
+
const dismissFileChooser = context.hasFileChooser();
|
|
69
70
|
await waitForCompletion(page, () => callback(page));
|
|
70
|
-
|
|
71
|
+
if (dismissFileChooser)
|
|
72
|
+
context.clearFileChooser();
|
|
73
|
+
const result = snapshot ? await captureAriaSnapshot(context, status) : {
|
|
71
74
|
content: [{ type: 'text', text: status }],
|
|
72
75
|
};
|
|
76
|
+
return result;
|
|
73
77
|
}
|
|
74
|
-
async function captureAriaSnapshot(
|
|
75
|
-
const
|
|
78
|
+
async function captureAriaSnapshot(context, status = '') {
|
|
79
|
+
const page = context.existingPage();
|
|
80
|
+
const lines = [];
|
|
81
|
+
if (status)
|
|
82
|
+
lines.push(`${status}`);
|
|
83
|
+
lines.push('', `- Page URL: ${page.url()}`, `- Page Title: ${await page.title()}`);
|
|
84
|
+
if (context.hasFileChooser())
|
|
85
|
+
lines.push(`- There is a file chooser visible that requires browser_choose_file to be called`);
|
|
86
|
+
lines.push(`- Page Snapshot`, '```yaml', await context.allFramesSnapshot(), '```', '');
|
|
76
87
|
return {
|
|
77
|
-
content: [{ type: 'text', text:
|
|
78
|
-
- Page URL: ${page.url()}
|
|
79
|
-
- Page Title: ${await page.title()}
|
|
80
|
-
- Page Snapshot
|
|
81
|
-
\`\`\`yaml
|
|
82
|
-
${snapshot}
|
|
83
|
-
\`\`\`
|
|
84
|
-
`
|
|
85
|
-
}],
|
|
88
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
86
89
|
};
|
|
87
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,9 @@
|
|
|
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
26
|
"./package.json": "./package.json",
|