@playwright/mcp 0.0.20 → 0.0.24
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 +95 -16
- package/config.d.ts +15 -15
- package/index.d.ts +8 -2
- package/index.js +2 -2
- package/lib/config.js +16 -18
- package/lib/{server.js → connection.js} +31 -28
- package/lib/context.js +37 -3
- package/lib/index.js +3 -51
- package/lib/pageSnapshot.js +8 -55
- package/lib/program.js +15 -9
- package/lib/tab.js +24 -10
- package/lib/tools/common.js +30 -5
- package/lib/tools/console.js +3 -1
- package/lib/tools/dialogs.js +2 -0
- package/lib/tools/files.js +2 -0
- package/lib/tools/install.js +7 -5
- package/lib/tools/keyboard.js +2 -0
- package/lib/tools/navigate.js +6 -0
- package/lib/tools/network.js +2 -0
- package/lib/tools/pdf.js +8 -3
- package/lib/tools/screen.js +10 -0
- package/lib/tools/snapshot.js +17 -2
- package/lib/tools/tabs.js +8 -0
- package/lib/tools/testing.js +2 -0
- package/lib/tools.js +56 -0
- package/lib/transport.js +23 -15
- package/package.json +5 -6
package/lib/context.js
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import url from 'node:url';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
16
20
|
import * as playwright from 'playwright';
|
|
17
21
|
import { waitForCompletion } from './tools/utils.js';
|
|
18
22
|
import { ManualPromise } from './manualPromise.js';
|
|
@@ -96,7 +100,7 @@ export class Context {
|
|
|
96
100
|
}
|
|
97
101
|
async run(tool, params) {
|
|
98
102
|
// Tab management is done outside of the action() call.
|
|
99
|
-
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
|
|
103
|
+
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
100
104
|
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
101
105
|
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
102
106
|
if (resultOverride)
|
|
@@ -237,11 +241,23 @@ ${code.join('\n')}
|
|
|
237
241
|
await browser?.close();
|
|
238
242
|
}).catch(() => { });
|
|
239
243
|
}
|
|
244
|
+
async _setupRequestInterception(context) {
|
|
245
|
+
if (this.config.network?.allowedOrigins?.length) {
|
|
246
|
+
await context.route('**', route => route.abort('blockedbyclient'));
|
|
247
|
+
for (const origin of this.config.network.allowedOrigins)
|
|
248
|
+
await context.route(`*://${origin}/**`, route => route.continue());
|
|
249
|
+
}
|
|
250
|
+
if (this.config.network?.blockedOrigins?.length) {
|
|
251
|
+
for (const origin of this.config.network.blockedOrigins)
|
|
252
|
+
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
240
255
|
async _ensureBrowserContext() {
|
|
241
256
|
if (!this._browserContext) {
|
|
242
257
|
const context = await this._createBrowserContext();
|
|
243
258
|
this._browser = context.browser;
|
|
244
259
|
this._browserContext = context.browserContext;
|
|
260
|
+
await this._setupRequestInterception(this._browserContext);
|
|
245
261
|
for (const page of this._browserContext.pages())
|
|
246
262
|
this._onPageCreated(page);
|
|
247
263
|
this._browserContext.on('page', page => this._onPageCreated(page));
|
|
@@ -279,8 +295,10 @@ ${code.join('\n')}
|
|
|
279
295
|
}
|
|
280
296
|
async function launchPersistentContext(browserConfig) {
|
|
281
297
|
try {
|
|
282
|
-
const
|
|
283
|
-
|
|
298
|
+
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
299
|
+
const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
300
|
+
const browserType = playwright[browserName];
|
|
301
|
+
return await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
|
284
302
|
}
|
|
285
303
|
catch (error) {
|
|
286
304
|
if (error.message.includes('Executable doesn\'t exist'))
|
|
@@ -288,6 +306,22 @@ async function launchPersistentContext(browserConfig) {
|
|
|
288
306
|
throw error;
|
|
289
307
|
}
|
|
290
308
|
}
|
|
309
|
+
async function createUserDataDir(browserConfig) {
|
|
310
|
+
let cacheDirectory;
|
|
311
|
+
if (process.platform === 'linux')
|
|
312
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
313
|
+
else if (process.platform === 'darwin')
|
|
314
|
+
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
|
315
|
+
else if (process.platform === 'win32')
|
|
316
|
+
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
317
|
+
else
|
|
318
|
+
throw new Error('Unsupported platform: ' + process.platform);
|
|
319
|
+
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
320
|
+
await fs.promises.mkdir(result, { recursive: true });
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
291
323
|
export async function generateLocator(locator) {
|
|
292
324
|
return locator._generateLocatorString();
|
|
293
325
|
}
|
|
326
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
327
|
+
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
package/lib/index.js
CHANGED
|
@@ -13,55 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import dialogs from './tools/dialogs.js';
|
|
20
|
-
import files from './tools/files.js';
|
|
21
|
-
import install from './tools/install.js';
|
|
22
|
-
import keyboard from './tools/keyboard.js';
|
|
23
|
-
import navigate from './tools/navigate.js';
|
|
24
|
-
import network from './tools/network.js';
|
|
25
|
-
import pdf from './tools/pdf.js';
|
|
26
|
-
import snapshot from './tools/snapshot.js';
|
|
27
|
-
import tabs from './tools/tabs.js';
|
|
28
|
-
import screen from './tools/screen.js';
|
|
29
|
-
import testing from './tools/testing.js';
|
|
30
|
-
const snapshotTools = [
|
|
31
|
-
...common(true),
|
|
32
|
-
...console,
|
|
33
|
-
...dialogs(true),
|
|
34
|
-
...files(true),
|
|
35
|
-
...install,
|
|
36
|
-
...keyboard(true),
|
|
37
|
-
...navigate(true),
|
|
38
|
-
...network,
|
|
39
|
-
...pdf,
|
|
40
|
-
...snapshot,
|
|
41
|
-
...tabs(true),
|
|
42
|
-
...testing,
|
|
43
|
-
];
|
|
44
|
-
const screenshotTools = [
|
|
45
|
-
...common(false),
|
|
46
|
-
...console,
|
|
47
|
-
...dialogs(false),
|
|
48
|
-
...files(false),
|
|
49
|
-
...install,
|
|
50
|
-
...keyboard(false),
|
|
51
|
-
...navigate(false),
|
|
52
|
-
...network,
|
|
53
|
-
...pdf,
|
|
54
|
-
...screen,
|
|
55
|
-
...tabs(false),
|
|
56
|
-
...testing,
|
|
57
|
-
];
|
|
58
|
-
import packageJSON from '../package.json' with { type: 'json' };
|
|
59
|
-
export async function createServer(config = {}) {
|
|
60
|
-
const allTools = config.vision ? screenshotTools : snapshotTools;
|
|
61
|
-
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
62
|
-
return createServerWithTools({
|
|
63
|
-
name: 'Playwright',
|
|
64
|
-
version: packageJSON.version,
|
|
65
|
-
tools,
|
|
66
|
-
}, config);
|
|
16
|
+
import { createConnection as createConnectionImpl } from './connection.js';
|
|
17
|
+
export async function createConnection(config = {}) {
|
|
18
|
+
return createConnectionImpl(config);
|
|
67
19
|
}
|
package/lib/pageSnapshot.js
CHANGED
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import yaml from 'yaml';
|
|
17
16
|
export class PageSnapshot {
|
|
18
|
-
|
|
17
|
+
_page;
|
|
19
18
|
_text;
|
|
20
|
-
constructor() {
|
|
19
|
+
constructor(page) {
|
|
20
|
+
this._page = page;
|
|
21
21
|
}
|
|
22
22
|
static async create(page) {
|
|
23
|
-
const snapshot = new PageSnapshot();
|
|
24
|
-
await snapshot._build(
|
|
23
|
+
const snapshot = new PageSnapshot(page);
|
|
24
|
+
await snapshot._build();
|
|
25
25
|
return snapshot;
|
|
26
26
|
}
|
|
27
27
|
text() {
|
|
28
28
|
return this._text;
|
|
29
29
|
}
|
|
30
|
-
async _build(
|
|
31
|
-
const yamlDocument = await this.
|
|
30
|
+
async _build() {
|
|
31
|
+
const yamlDocument = await this._page._snapshotForAI();
|
|
32
32
|
this._text = [
|
|
33
33
|
`- Page Snapshot`,
|
|
34
34
|
'```yaml',
|
|
@@ -36,54 +36,7 @@ export class PageSnapshot {
|
|
|
36
36
|
'```',
|
|
37
37
|
].join('\n');
|
|
38
38
|
}
|
|
39
|
-
async _snapshotFrame(frame) {
|
|
40
|
-
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
41
|
-
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
|
42
|
-
const snapshot = yaml.parseDocument(snapshotString);
|
|
43
|
-
const visit = async (node) => {
|
|
44
|
-
if (yaml.isPair(node)) {
|
|
45
|
-
await Promise.all([
|
|
46
|
-
visit(node.key).then(k => node.key = k),
|
|
47
|
-
visit(node.value).then(v => node.value = v)
|
|
48
|
-
]);
|
|
49
|
-
}
|
|
50
|
-
else if (yaml.isSeq(node) || yaml.isMap(node)) {
|
|
51
|
-
node.items = await Promise.all(node.items.map(visit));
|
|
52
|
-
}
|
|
53
|
-
else if (yaml.isScalar(node)) {
|
|
54
|
-
if (typeof node.value === 'string') {
|
|
55
|
-
const value = node.value;
|
|
56
|
-
if (frameIndex > 0)
|
|
57
|
-
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
|
|
58
|
-
if (value.startsWith('iframe ')) {
|
|
59
|
-
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
60
|
-
if (ref) {
|
|
61
|
-
try {
|
|
62
|
-
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
|
|
63
|
-
return snapshot.createPair(node.value, childSnapshot);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return node;
|
|
73
|
-
};
|
|
74
|
-
await visit(snapshot.contents);
|
|
75
|
-
return snapshot;
|
|
76
|
-
}
|
|
77
39
|
refLocator(ref) {
|
|
78
|
-
|
|
79
|
-
const match = ref.match(/^f(\d+)(.*)/);
|
|
80
|
-
if (match) {
|
|
81
|
-
const frameIndex = parseInt(match[1], 10);
|
|
82
|
-
frame = this._frameLocators[frameIndex];
|
|
83
|
-
ref = match[2];
|
|
84
|
-
}
|
|
85
|
-
if (!frame)
|
|
86
|
-
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
87
|
-
return frame.locator(`aria-ref=${ref}`);
|
|
40
|
+
return this._page.locator(`aria-ref=${ref}`);
|
|
88
41
|
}
|
|
89
42
|
}
|
package/lib/program.js
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { program } from 'commander';
|
|
17
|
-
import { createServer } from './index.js';
|
|
18
|
-
import { ServerList } from './server.js';
|
|
19
17
|
import { startHttpTransport, startStdioTransport } from './transport.js';
|
|
20
18
|
import { resolveConfig } from './config.js';
|
|
21
|
-
import packageJSON from '
|
|
19
|
+
import { packageJSON } from './context.js';
|
|
22
20
|
program
|
|
23
21
|
.version('Version ' + packageJSON.version)
|
|
24
22
|
.name(packageJSON.name)
|
|
@@ -31,25 +29,33 @@ program
|
|
|
31
29
|
.option('--user-data-dir <path>', 'Path to the user data directory')
|
|
32
30
|
.option('--port <port>', 'Port to listen on for SSE transport.')
|
|
33
31
|
.option('--host <host>', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
32
|
+
.option('--allowed-origins <origins>', 'Semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
|
33
|
+
.option('--blocked-origins <origins>', 'Semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
|
|
34
34
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
35
|
+
.option('--no-image-responses', 'Do not send image responses to the client.')
|
|
36
|
+
.option('--output-dir <path>', 'Path to the directory for output files.')
|
|
35
37
|
.option('--config <path>', 'Path to the configuration file.')
|
|
36
38
|
.action(async (options) => {
|
|
37
39
|
const config = await resolveConfig(options);
|
|
38
|
-
const
|
|
39
|
-
setupExitWatchdog(
|
|
40
|
+
const connectionList = [];
|
|
41
|
+
setupExitWatchdog(connectionList);
|
|
40
42
|
if (options.port)
|
|
41
|
-
startHttpTransport(+options.port, options.host,
|
|
43
|
+
startHttpTransport(config, +options.port, options.host, connectionList);
|
|
42
44
|
else
|
|
43
|
-
await startStdioTransport(
|
|
45
|
+
await startStdioTransport(config, connectionList);
|
|
44
46
|
});
|
|
45
|
-
function setupExitWatchdog(
|
|
47
|
+
function setupExitWatchdog(connectionList) {
|
|
46
48
|
const handleExit = async () => {
|
|
47
49
|
setTimeout(() => process.exit(0), 15000);
|
|
48
|
-
|
|
50
|
+
for (const connection of connectionList)
|
|
51
|
+
await connection.close();
|
|
49
52
|
process.exit(0);
|
|
50
53
|
};
|
|
51
54
|
process.stdin.on('close', handleExit);
|
|
52
55
|
process.on('SIGINT', handleExit);
|
|
53
56
|
process.on('SIGTERM', handleExit);
|
|
54
57
|
}
|
|
58
|
+
function semicolonSeparatedList(value) {
|
|
59
|
+
return value.split(';').map(v => v.trim());
|
|
60
|
+
}
|
|
55
61
|
program.parse(process.argv);
|
package/lib/tab.js
CHANGED
|
@@ -17,7 +17,7 @@ import { PageSnapshot } from './pageSnapshot.js';
|
|
|
17
17
|
export class Tab {
|
|
18
18
|
context;
|
|
19
19
|
page;
|
|
20
|
-
|
|
20
|
+
_consoleMessages = [];
|
|
21
21
|
_requests = new Map();
|
|
22
22
|
_snapshot;
|
|
23
23
|
_onPageClose;
|
|
@@ -25,13 +25,9 @@ export class Tab {
|
|
|
25
25
|
this.context = context;
|
|
26
26
|
this.page = page;
|
|
27
27
|
this._onPageClose = onPageClose;
|
|
28
|
-
page.on('console', event => this.
|
|
28
|
+
page.on('console', event => this._consoleMessages.push(event));
|
|
29
29
|
page.on('request', request => this._requests.set(request, null));
|
|
30
30
|
page.on('response', response => this._requests.set(response.request(), response));
|
|
31
|
-
page.on('framenavigated', frame => {
|
|
32
|
-
if (!frame.parentFrame())
|
|
33
|
-
this._clearCollectedArtifacts();
|
|
34
|
-
});
|
|
35
31
|
page.on('close', () => this._onClose());
|
|
36
32
|
page.on('filechooser', chooser => {
|
|
37
33
|
this.context.setModalState({
|
|
@@ -48,7 +44,7 @@ export class Tab {
|
|
|
48
44
|
page.setDefaultTimeout(5000);
|
|
49
45
|
}
|
|
50
46
|
_clearCollectedArtifacts() {
|
|
51
|
-
this.
|
|
47
|
+
this._consoleMessages.length = 0;
|
|
52
48
|
this._requests.clear();
|
|
53
49
|
}
|
|
54
50
|
_onClose() {
|
|
@@ -56,7 +52,25 @@ export class Tab {
|
|
|
56
52
|
this._onPageClose(this);
|
|
57
53
|
}
|
|
58
54
|
async navigate(url) {
|
|
59
|
-
|
|
55
|
+
this._clearCollectedArtifacts();
|
|
56
|
+
const downloadEvent = this.page.waitForEvent('download').catch(() => { });
|
|
57
|
+
try {
|
|
58
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
|
+
}
|
|
60
|
+
catch (_e) {
|
|
61
|
+
const e = _e;
|
|
62
|
+
const mightBeDownload = e.message.includes('net::ERR_ABORTED') // chromium
|
|
63
|
+
|| e.message.includes('Download is starting'); // firefox + webkit
|
|
64
|
+
if (!mightBeDownload)
|
|
65
|
+
throw e;
|
|
66
|
+
// on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
|
|
67
|
+
const download = await Promise.race([
|
|
68
|
+
downloadEvent,
|
|
69
|
+
new Promise(resolve => setTimeout(resolve, 500)),
|
|
70
|
+
]);
|
|
71
|
+
if (!download)
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
60
74
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
61
75
|
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
|
|
62
76
|
}
|
|
@@ -68,8 +82,8 @@ export class Tab {
|
|
|
68
82
|
throw new Error('No snapshot available');
|
|
69
83
|
return this._snapshot;
|
|
70
84
|
}
|
|
71
|
-
|
|
72
|
-
return this.
|
|
85
|
+
consoleMessages() {
|
|
86
|
+
return this._consoleMessages;
|
|
73
87
|
}
|
|
74
88
|
requests() {
|
|
75
89
|
return this._requests;
|
package/lib/tools/common.js
CHANGED
|
@@ -18,16 +18,37 @@ import { defineTool } from './tool.js';
|
|
|
18
18
|
const wait = captureSnapshot => defineTool({
|
|
19
19
|
capability: 'wait',
|
|
20
20
|
schema: {
|
|
21
|
-
name: '
|
|
22
|
-
|
|
21
|
+
name: 'browser_wait_for',
|
|
22
|
+
title: 'Wait for',
|
|
23
|
+
description: 'Wait for text to appear or disappear or a specified time to pass',
|
|
23
24
|
inputSchema: z.object({
|
|
24
|
-
time: z.number().describe('The time to wait in seconds'),
|
|
25
|
+
time: z.number().optional().describe('The time to wait in seconds'),
|
|
26
|
+
text: z.string().optional().describe('The text to wait for'),
|
|
27
|
+
textGone: z.string().optional().describe('The text to wait for to disappear'),
|
|
25
28
|
}),
|
|
29
|
+
type: 'readOnly',
|
|
26
30
|
},
|
|
27
31
|
handle: async (context, params) => {
|
|
28
|
-
|
|
32
|
+
if (!params.text && !params.textGone && !params.time)
|
|
33
|
+
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
+
const code = [];
|
|
35
|
+
if (params.time) {
|
|
36
|
+
code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
|
+
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
38
|
+
}
|
|
39
|
+
const tab = context.currentTabOrDie();
|
|
40
|
+
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
|
+
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
|
+
if (goneLocator) {
|
|
43
|
+
code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
|
+
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
|
+
}
|
|
46
|
+
if (locator) {
|
|
47
|
+
code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
|
+
await locator.waitFor({ state: 'visible' });
|
|
49
|
+
}
|
|
29
50
|
return {
|
|
30
|
-
code
|
|
51
|
+
code,
|
|
31
52
|
captureSnapshot,
|
|
32
53
|
waitForNetwork: false,
|
|
33
54
|
};
|
|
@@ -37,8 +58,10 @@ const close = defineTool({
|
|
|
37
58
|
capability: 'core',
|
|
38
59
|
schema: {
|
|
39
60
|
name: 'browser_close',
|
|
61
|
+
title: 'Close browser',
|
|
40
62
|
description: 'Close the page',
|
|
41
63
|
inputSchema: z.object({}),
|
|
64
|
+
type: 'readOnly',
|
|
42
65
|
},
|
|
43
66
|
handle: async (context) => {
|
|
44
67
|
await context.close();
|
|
@@ -53,11 +76,13 @@ const resize = captureSnapshot => defineTool({
|
|
|
53
76
|
capability: 'core',
|
|
54
77
|
schema: {
|
|
55
78
|
name: 'browser_resize',
|
|
79
|
+
title: 'Resize browser window',
|
|
56
80
|
description: 'Resize the browser window',
|
|
57
81
|
inputSchema: z.object({
|
|
58
82
|
width: z.number().describe('Width of the browser window'),
|
|
59
83
|
height: z.number().describe('Height of the browser window'),
|
|
60
84
|
}),
|
|
85
|
+
type: 'readOnly',
|
|
61
86
|
},
|
|
62
87
|
handle: async (context, params) => {
|
|
63
88
|
const tab = context.currentTabOrDie();
|
package/lib/tools/console.js
CHANGED
|
@@ -19,11 +19,13 @@ const console = defineTool({
|
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_console_messages',
|
|
22
|
+
title: 'Get console messages',
|
|
22
23
|
description: 'Returns all console messages',
|
|
23
24
|
inputSchema: z.object({}),
|
|
25
|
+
type: 'readOnly',
|
|
24
26
|
},
|
|
25
27
|
handle: async (context) => {
|
|
26
|
-
const messages = context.currentTabOrDie().
|
|
28
|
+
const messages = context.currentTabOrDie().consoleMessages();
|
|
27
29
|
const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
|
|
28
30
|
return {
|
|
29
31
|
code: [`// <internal code to get console messages>`],
|
package/lib/tools/dialogs.js
CHANGED
|
@@ -19,11 +19,13 @@ const handleDialog = captureSnapshot => defineTool({
|
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_handle_dialog',
|
|
22
|
+
title: 'Handle a dialog',
|
|
22
23
|
description: 'Handle a dialog',
|
|
23
24
|
inputSchema: z.object({
|
|
24
25
|
accept: z.boolean().describe('Whether to accept the dialog.'),
|
|
25
26
|
promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
|
|
26
27
|
}),
|
|
28
|
+
type: 'destructive',
|
|
27
29
|
},
|
|
28
30
|
handle: async (context, params) => {
|
|
29
31
|
const dialogState = context.modalStates().find(state => state.type === 'dialog');
|
package/lib/tools/files.js
CHANGED
|
@@ -19,10 +19,12 @@ const uploadFile = captureSnapshot => defineTool({
|
|
|
19
19
|
capability: 'files',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_file_upload',
|
|
22
|
+
title: 'Upload files',
|
|
22
23
|
description: 'Upload one or multiple files',
|
|
23
24
|
inputSchema: z.object({
|
|
24
25
|
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
|
25
26
|
}),
|
|
27
|
+
type: 'destructive',
|
|
26
28
|
},
|
|
27
29
|
handle: async (context, params) => {
|
|
28
30
|
const modalState = context.modalStates().find(state => state.type === 'fileChooser');
|
package/lib/tools/install.js
CHANGED
|
@@ -17,19 +17,21 @@ import { fork } from 'child_process';
|
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import { z } from 'zod';
|
|
19
19
|
import { defineTool } from './tool.js';
|
|
20
|
-
import {
|
|
21
|
-
const require = createRequire(import.meta.url);
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
22
21
|
const install = defineTool({
|
|
23
22
|
capability: 'install',
|
|
24
23
|
schema: {
|
|
25
24
|
name: 'browser_install',
|
|
25
|
+
title: 'Install the browser specified in the config',
|
|
26
26
|
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
|
27
27
|
inputSchema: z.object({}),
|
|
28
|
+
type: 'destructive',
|
|
28
29
|
},
|
|
29
30
|
handle: async (context) => {
|
|
30
|
-
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.
|
|
31
|
-
const
|
|
32
|
-
const
|
|
31
|
+
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
|
32
|
+
const cliUrl = import.meta.resolve('playwright/package.json');
|
|
33
|
+
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
|
34
|
+
const child = fork(cliPath, ['install', channel], {
|
|
33
35
|
stdio: 'pipe',
|
|
34
36
|
});
|
|
35
37
|
const output = [];
|
package/lib/tools/keyboard.js
CHANGED
|
@@ -19,10 +19,12 @@ const pressKey = captureSnapshot => defineTool({
|
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_press_key',
|
|
22
|
+
title: 'Press a key',
|
|
22
23
|
description: 'Press a key on the keyboard',
|
|
23
24
|
inputSchema: z.object({
|
|
24
25
|
key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
|
|
25
26
|
}),
|
|
27
|
+
type: 'destructive',
|
|
26
28
|
},
|
|
27
29
|
handle: async (context, params) => {
|
|
28
30
|
const tab = context.currentTabOrDie();
|
package/lib/tools/navigate.js
CHANGED
|
@@ -19,10 +19,12 @@ const navigate = captureSnapshot => defineTool({
|
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_navigate',
|
|
22
|
+
title: 'Navigate to a URL',
|
|
22
23
|
description: 'Navigate to a URL',
|
|
23
24
|
inputSchema: z.object({
|
|
24
25
|
url: z.string().describe('The URL to navigate to'),
|
|
25
26
|
}),
|
|
27
|
+
type: 'destructive',
|
|
26
28
|
},
|
|
27
29
|
handle: async (context, params) => {
|
|
28
30
|
const tab = await context.ensureTab();
|
|
@@ -42,8 +44,10 @@ const goBack = captureSnapshot => defineTool({
|
|
|
42
44
|
capability: 'history',
|
|
43
45
|
schema: {
|
|
44
46
|
name: 'browser_navigate_back',
|
|
47
|
+
title: 'Go back',
|
|
45
48
|
description: 'Go back to the previous page',
|
|
46
49
|
inputSchema: z.object({}),
|
|
50
|
+
type: 'readOnly',
|
|
47
51
|
},
|
|
48
52
|
handle: async (context) => {
|
|
49
53
|
const tab = await context.ensureTab();
|
|
@@ -63,8 +67,10 @@ const goForward = captureSnapshot => defineTool({
|
|
|
63
67
|
capability: 'history',
|
|
64
68
|
schema: {
|
|
65
69
|
name: 'browser_navigate_forward',
|
|
70
|
+
title: 'Go forward',
|
|
66
71
|
description: 'Go forward to the next page',
|
|
67
72
|
inputSchema: z.object({}),
|
|
73
|
+
type: 'readOnly',
|
|
68
74
|
},
|
|
69
75
|
handle: async (context) => {
|
|
70
76
|
const tab = context.currentTabOrDie();
|
package/lib/tools/network.js
CHANGED
|
@@ -19,8 +19,10 @@ const requests = defineTool({
|
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_network_requests',
|
|
22
|
+
title: 'List network requests',
|
|
22
23
|
description: 'Returns all network requests since loading the page',
|
|
23
24
|
inputSchema: z.object({}),
|
|
25
|
+
type: 'readOnly',
|
|
24
26
|
},
|
|
25
27
|
handle: async (context) => {
|
|
26
28
|
const requests = context.currentTabOrDie().requests();
|
package/lib/tools/pdf.js
CHANGED
|
@@ -17,16 +17,21 @@ import { z } from 'zod';
|
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
18
|
import * as javascript from '../javascript.js';
|
|
19
19
|
import { outputFile } from '../config.js';
|
|
20
|
+
const pdfSchema = z.object({
|
|
21
|
+
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
|
22
|
+
});
|
|
20
23
|
const pdf = defineTool({
|
|
21
24
|
capability: 'pdf',
|
|
22
25
|
schema: {
|
|
23
26
|
name: 'browser_pdf_save',
|
|
27
|
+
title: 'Save as PDF',
|
|
24
28
|
description: 'Save page as PDF',
|
|
25
|
-
inputSchema:
|
|
29
|
+
inputSchema: pdfSchema,
|
|
30
|
+
type: 'readOnly',
|
|
26
31
|
},
|
|
27
|
-
handle: async (context) => {
|
|
32
|
+
handle: async (context, params) => {
|
|
28
33
|
const tab = context.currentTabOrDie();
|
|
29
|
-
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
|
|
34
|
+
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
|
30
35
|
const code = [
|
|
31
36
|
`// Save page as ${fileName}`,
|
|
32
37
|
`await page.pdf(${javascript.formatObject({ path: fileName })});`,
|
package/lib/tools/screen.js
CHANGED
|
@@ -23,8 +23,10 @@ const screenshot = defineTool({
|
|
|
23
23
|
capability: 'core',
|
|
24
24
|
schema: {
|
|
25
25
|
name: 'browser_screen_capture',
|
|
26
|
+
title: 'Take a screenshot',
|
|
26
27
|
description: 'Take a screenshot of the current page',
|
|
27
28
|
inputSchema: z.object({}),
|
|
29
|
+
type: 'readOnly',
|
|
28
30
|
},
|
|
29
31
|
handle: async (context) => {
|
|
30
32
|
const tab = await context.ensureTab();
|
|
@@ -50,11 +52,13 @@ const moveMouse = defineTool({
|
|
|
50
52
|
capability: 'core',
|
|
51
53
|
schema: {
|
|
52
54
|
name: 'browser_screen_move_mouse',
|
|
55
|
+
title: 'Move mouse',
|
|
53
56
|
description: 'Move mouse to a given position',
|
|
54
57
|
inputSchema: elementSchema.extend({
|
|
55
58
|
x: z.number().describe('X coordinate'),
|
|
56
59
|
y: z.number().describe('Y coordinate'),
|
|
57
60
|
}),
|
|
61
|
+
type: 'readOnly',
|
|
58
62
|
},
|
|
59
63
|
handle: async (context, params) => {
|
|
60
64
|
const tab = context.currentTabOrDie();
|
|
@@ -75,11 +79,13 @@ const click = defineTool({
|
|
|
75
79
|
capability: 'core',
|
|
76
80
|
schema: {
|
|
77
81
|
name: 'browser_screen_click',
|
|
82
|
+
title: 'Click',
|
|
78
83
|
description: 'Click left mouse button',
|
|
79
84
|
inputSchema: elementSchema.extend({
|
|
80
85
|
x: z.number().describe('X coordinate'),
|
|
81
86
|
y: z.number().describe('Y coordinate'),
|
|
82
87
|
}),
|
|
88
|
+
type: 'destructive',
|
|
83
89
|
},
|
|
84
90
|
handle: async (context, params) => {
|
|
85
91
|
const tab = context.currentTabOrDie();
|
|
@@ -106,6 +112,7 @@ const drag = defineTool({
|
|
|
106
112
|
capability: 'core',
|
|
107
113
|
schema: {
|
|
108
114
|
name: 'browser_screen_drag',
|
|
115
|
+
title: 'Drag mouse',
|
|
109
116
|
description: 'Drag left mouse button',
|
|
110
117
|
inputSchema: elementSchema.extend({
|
|
111
118
|
startX: z.number().describe('Start X coordinate'),
|
|
@@ -113,6 +120,7 @@ const drag = defineTool({
|
|
|
113
120
|
endX: z.number().describe('End X coordinate'),
|
|
114
121
|
endY: z.number().describe('End Y coordinate'),
|
|
115
122
|
}),
|
|
123
|
+
type: 'destructive',
|
|
116
124
|
},
|
|
117
125
|
handle: async (context, params) => {
|
|
118
126
|
const tab = context.currentTabOrDie();
|
|
@@ -141,11 +149,13 @@ const type = defineTool({
|
|
|
141
149
|
capability: 'core',
|
|
142
150
|
schema: {
|
|
143
151
|
name: 'browser_screen_type',
|
|
152
|
+
title: 'Type text',
|
|
144
153
|
description: 'Type text',
|
|
145
154
|
inputSchema: z.object({
|
|
146
155
|
text: z.string().describe('Text to type into the element'),
|
|
147
156
|
submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
|
|
148
157
|
}),
|
|
158
|
+
type: 'destructive',
|
|
149
159
|
},
|
|
150
160
|
handle: async (context, params) => {
|
|
151
161
|
const tab = context.currentTabOrDie();
|