@playwright/mcp 0.0.22 → 0.0.25
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 +363 -186
- package/config.d.ts +6 -1
- package/index.js +1 -1
- package/lib/config.js +45 -5
- package/lib/connection.js +2 -2
- package/lib/context.js +50 -41
- package/lib/index.js +2 -1
- package/lib/pageSnapshot.js +8 -55
- package/lib/program.js +22 -13
- package/lib/tab.js +6 -9
- package/lib/tools/common.js +1 -22
- package/lib/tools/console.js +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/pdf.js +6 -3
- package/lib/tools/screenshot.js +77 -0
- package/lib/tools/snapshot.js +1 -58
- package/lib/tools/utils.js +3 -0
- package/lib/tools/wait.js +59 -0
- package/lib/tools.js +8 -3
- package/package.json +3 -4
- /package/lib/tools/{screen.js → vision.js} +0 -0
package/config.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type Config = {
|
|
|
28
28
|
*/
|
|
29
29
|
browserName?: 'chromium' | 'firefox' | 'webkit';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Keep the browser profile in memory, do not save it to disk.
|
|
33
|
+
*/
|
|
34
|
+
isolated?: boolean;
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Path to a user data directory for browser profile persistence.
|
|
33
38
|
* Temporary directory is created by default.
|
|
@@ -40,7 +45,7 @@ export type Config = {
|
|
|
40
45
|
*
|
|
41
46
|
* This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
|
|
42
47
|
*/
|
|
43
|
-
launchOptions?: playwright.
|
|
48
|
+
launchOptions?: playwright.LaunchOptions;
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
51
|
* Context options for the browser context.
|
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -25,6 +25,7 @@ const defaultConfig = {
|
|
|
25
25
|
launchOptions: {
|
|
26
26
|
channel: 'chrome',
|
|
27
27
|
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
|
28
|
+
chromiumSandbox: true,
|
|
28
29
|
},
|
|
29
30
|
contextOptions: {
|
|
30
31
|
viewport: null,
|
|
@@ -66,17 +67,51 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
66
67
|
browserName = 'chromium';
|
|
67
68
|
channel = 'chrome';
|
|
68
69
|
}
|
|
70
|
+
// Launch options
|
|
69
71
|
const launchOptions = {
|
|
70
72
|
channel,
|
|
71
73
|
executablePath: cliOptions.executablePath,
|
|
72
74
|
headless: cliOptions.headless,
|
|
73
75
|
};
|
|
74
|
-
if (browserName === 'chromium')
|
|
75
|
-
launchOptions.
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if (browserName === 'chromium') {
|
|
77
|
+
launchOptions.cdpPort = await findFreePort();
|
|
78
|
+
if (!cliOptions.sandbox) {
|
|
79
|
+
// --no-sandbox was passed, disable the sandbox
|
|
80
|
+
launchOptions.chromiumSandbox = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cliOptions.proxyServer) {
|
|
84
|
+
launchOptions.proxy = {
|
|
85
|
+
server: cliOptions.proxyServer
|
|
86
|
+
};
|
|
87
|
+
if (cliOptions.proxyBypass)
|
|
88
|
+
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
89
|
+
}
|
|
90
|
+
// Context options
|
|
91
|
+
const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
92
|
+
if (cliOptions.storageState)
|
|
93
|
+
contextOptions.storageState = cliOptions.storageState;
|
|
94
|
+
if (cliOptions.userAgent)
|
|
95
|
+
contextOptions.userAgent = cliOptions.userAgent;
|
|
96
|
+
if (cliOptions.viewportSize) {
|
|
97
|
+
try {
|
|
98
|
+
const [width, height] = cliOptions.viewportSize.split(',').map(n => +n);
|
|
99
|
+
if (isNaN(width) || isNaN(height))
|
|
100
|
+
throw new Error('bad values');
|
|
101
|
+
contextOptions.viewport = { width, height };
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (cliOptions.ignoreHttpsErrors)
|
|
108
|
+
contextOptions.ignoreHTTPSErrors = true;
|
|
109
|
+
if (cliOptions.blockServiceWorkers)
|
|
110
|
+
contextOptions.serviceWorkers = 'block';
|
|
111
|
+
const result = {
|
|
78
112
|
browser: {
|
|
79
113
|
browserName,
|
|
114
|
+
isolated: cliOptions.isolated,
|
|
80
115
|
userDataDir: cliOptions.userDataDir,
|
|
81
116
|
launchOptions,
|
|
82
117
|
contextOptions,
|
|
@@ -94,6 +129,11 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
94
129
|
},
|
|
95
130
|
outputDir: cliOptions.outputDir,
|
|
96
131
|
};
|
|
132
|
+
if (!cliOptions.imageResponses) {
|
|
133
|
+
// --no-image-responses was passed, disable image responses
|
|
134
|
+
result.noImageResponses = true;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
97
137
|
}
|
|
98
138
|
async function findFreePort() {
|
|
99
139
|
return new Promise((resolve, reject) => {
|
|
@@ -138,7 +178,7 @@ function mergeConfig(base, overrides) {
|
|
|
138
178
|
...pickDefined(overrides.browser?.contextOptions),
|
|
139
179
|
},
|
|
140
180
|
};
|
|
141
|
-
if (browser.browserName !== 'chromium')
|
|
181
|
+
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
|
142
182
|
delete browser.launchOptions.channel;
|
|
143
183
|
return {
|
|
144
184
|
...pickDefined(base),
|
package/lib/connection.js
CHANGED
|
@@ -17,9 +17,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
19
19
|
import { Context, packageJSON } from './context.js';
|
|
20
|
-
import { snapshotTools,
|
|
20
|
+
import { snapshotTools, visionTools } from './tools.js';
|
|
21
21
|
export async function createConnection(config) {
|
|
22
|
-
const allTools = config.vision ?
|
|
22
|
+
const allTools = config.vision ? visionTools : snapshotTools;
|
|
23
23
|
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
24
24
|
const context = new Context(tools, config);
|
|
25
25
|
const server = new Server({ name: 'Playwright', version: packageJSON.version }, {
|
package/lib/context.js
CHANGED
|
@@ -25,9 +25,7 @@ import { outputFile } from './config.js';
|
|
|
25
25
|
export class Context {
|
|
26
26
|
tools;
|
|
27
27
|
config;
|
|
28
|
-
|
|
29
|
-
_browserContext;
|
|
30
|
-
_createBrowserContextPromise;
|
|
28
|
+
_browserContextPromise;
|
|
31
29
|
_tabs = [];
|
|
32
30
|
_currentTab;
|
|
33
31
|
_modalStates = [];
|
|
@@ -65,7 +63,7 @@ export class Context {
|
|
|
65
63
|
return this._currentTab;
|
|
66
64
|
}
|
|
67
65
|
async newTab() {
|
|
68
|
-
const browserContext = await this._ensureBrowserContext();
|
|
66
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
69
67
|
const page = await browserContext.newPage();
|
|
70
68
|
this._currentTab = this._tabs.find(t => t.page === page);
|
|
71
69
|
return this._currentTab;
|
|
@@ -75,9 +73,9 @@ export class Context {
|
|
|
75
73
|
await this._currentTab.page.bringToFront();
|
|
76
74
|
}
|
|
77
75
|
async ensureTab() {
|
|
78
|
-
const
|
|
76
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
79
77
|
if (!this._currentTab)
|
|
80
|
-
await
|
|
78
|
+
await browserContext.newPage();
|
|
81
79
|
return this._currentTab;
|
|
82
80
|
}
|
|
83
81
|
async listTabsMarkdown() {
|
|
@@ -100,7 +98,7 @@ export class Context {
|
|
|
100
98
|
}
|
|
101
99
|
async run(tool, params) {
|
|
102
100
|
// Tab management is done outside of the action() call.
|
|
103
|
-
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
|
|
101
|
+
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
104
102
|
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
105
103
|
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
106
104
|
if (resultOverride)
|
|
@@ -226,20 +224,19 @@ ${code.join('\n')}
|
|
|
226
224
|
this._tabs.splice(index, 1);
|
|
227
225
|
if (this._currentTab === tab)
|
|
228
226
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
229
|
-
if (
|
|
227
|
+
if (!this._tabs.length)
|
|
230
228
|
void this.close();
|
|
231
229
|
}
|
|
232
230
|
async close() {
|
|
233
|
-
if (!this.
|
|
231
|
+
if (!this._browserContextPromise)
|
|
234
232
|
return;
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}).catch(() => { });
|
|
233
|
+
const promise = this._browserContextPromise;
|
|
234
|
+
this._browserContextPromise = undefined;
|
|
235
|
+
await promise.then(async ({ browserContext, browser }) => {
|
|
236
|
+
await browserContext.close().then(async () => {
|
|
237
|
+
await browser?.close();
|
|
238
|
+
}).catch(() => { });
|
|
239
|
+
});
|
|
243
240
|
}
|
|
244
241
|
async _setupRequestInterception(context) {
|
|
245
242
|
if (this.config.network?.allowedOrigins?.length) {
|
|
@@ -252,28 +249,24 @@ ${code.join('\n')}
|
|
|
252
249
|
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
|
253
250
|
}
|
|
254
251
|
}
|
|
255
|
-
|
|
256
|
-
if (!this.
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
await this._setupRequestInterception(this._browserContext);
|
|
261
|
-
for (const page of this._browserContext.pages())
|
|
262
|
-
this._onPageCreated(page);
|
|
263
|
-
this._browserContext.on('page', page => this._onPageCreated(page));
|
|
264
|
-
}
|
|
265
|
-
return this._browserContext;
|
|
266
|
-
}
|
|
267
|
-
async _createBrowserContext() {
|
|
268
|
-
if (!this._createBrowserContextPromise) {
|
|
269
|
-
this._createBrowserContextPromise = this._innerCreateBrowserContext();
|
|
270
|
-
void this._createBrowserContextPromise.catch(() => {
|
|
271
|
-
this._createBrowserContextPromise = undefined;
|
|
252
|
+
_ensureBrowserContext() {
|
|
253
|
+
if (!this._browserContextPromise) {
|
|
254
|
+
this._browserContextPromise = this._setupBrowserContext();
|
|
255
|
+
this._browserContextPromise.catch(() => {
|
|
256
|
+
this._browserContextPromise = undefined;
|
|
272
257
|
});
|
|
273
258
|
}
|
|
274
|
-
return this.
|
|
259
|
+
return this._browserContextPromise;
|
|
260
|
+
}
|
|
261
|
+
async _setupBrowserContext() {
|
|
262
|
+
const { browser, browserContext } = await this._createBrowserContext();
|
|
263
|
+
await this._setupRequestInterception(browserContext);
|
|
264
|
+
for (const page of browserContext.pages())
|
|
265
|
+
this._onPageCreated(page);
|
|
266
|
+
browserContext.on('page', page => this._onPageCreated(page));
|
|
267
|
+
return { browser, browserContext };
|
|
275
268
|
}
|
|
276
|
-
async
|
|
269
|
+
async _createBrowserContext() {
|
|
277
270
|
if (this.config.browser?.remoteEndpoint) {
|
|
278
271
|
const url = new URL(this.config.browser?.remoteEndpoint);
|
|
279
272
|
if (this.config.browser.browserName)
|
|
@@ -286,11 +279,26 @@ ${code.join('\n')}
|
|
|
286
279
|
}
|
|
287
280
|
if (this.config.browser?.cdpEndpoint) {
|
|
288
281
|
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
289
|
-
const browserContext = browser.contexts()[0];
|
|
282
|
+
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
290
283
|
return { browser, browserContext };
|
|
291
284
|
}
|
|
292
|
-
|
|
293
|
-
|
|
285
|
+
return this.config.browser?.isolated ?
|
|
286
|
+
await createIsolatedContext(this.config.browser) :
|
|
287
|
+
await launchPersistentContext(this.config.browser);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function createIsolatedContext(browserConfig) {
|
|
291
|
+
try {
|
|
292
|
+
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
293
|
+
const browserType = playwright[browserName];
|
|
294
|
+
const browser = await browserType.launch(browserConfig?.launchOptions);
|
|
295
|
+
const browserContext = await browser.newContext(browserConfig?.contextOptions);
|
|
296
|
+
return { browser, browserContext };
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error.message.includes('Executable doesn\'t exist'))
|
|
300
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
301
|
+
throw error;
|
|
294
302
|
}
|
|
295
303
|
}
|
|
296
304
|
async function launchPersistentContext(browserConfig) {
|
|
@@ -298,7 +306,8 @@ async function launchPersistentContext(browserConfig) {
|
|
|
298
306
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
299
307
|
const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
300
308
|
const browserType = playwright[browserName];
|
|
301
|
-
|
|
309
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
|
|
310
|
+
return { browserContext };
|
|
302
311
|
}
|
|
303
312
|
catch (error) {
|
|
304
313
|
if (error.message.includes('Executable doesn\'t exist'))
|
|
@@ -316,7 +325,7 @@ async function createUserDataDir(browserConfig) {
|
|
|
316
325
|
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
317
326
|
else
|
|
318
327
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
319
|
-
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions
|
|
328
|
+
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
320
329
|
await fs.promises.mkdir(result, { recursive: true });
|
|
321
330
|
return result;
|
|
322
331
|
}
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { createConnection as createConnectionImpl } from './connection.js';
|
|
16
17
|
export async function createConnection(config = {}) {
|
|
17
|
-
return
|
|
18
|
+
return createConnectionImpl(config);
|
|
18
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
|
@@ -20,21 +20,30 @@ import { packageJSON } from './context.js';
|
|
|
20
20
|
program
|
|
21
21
|
.version('Version ' + packageJSON.version)
|
|
22
22
|
.name(packageJSON.name)
|
|
23
|
-
.option('--
|
|
24
|
-
.option('--
|
|
23
|
+
.option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
|
|
24
|
+
.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)
|
|
25
|
+
.option('--block-service-workers', 'block service workers')
|
|
26
|
+
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
27
|
+
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
|
25
28
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
26
|
-
.option('--
|
|
27
|
-
.option('--
|
|
28
|
-
.option('--
|
|
29
|
-
.option('--
|
|
30
|
-
.option('--
|
|
31
|
-
.option('--
|
|
32
|
-
.option('--
|
|
33
|
-
.option('--
|
|
29
|
+
.option('--config <path>', 'path to the configuration file.')
|
|
30
|
+
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
|
|
31
|
+
.option('--executable-path <path>', 'path to the browser executable.')
|
|
32
|
+
.option('--headless', 'run browser in headless mode, headed by default')
|
|
33
|
+
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
34
|
+
.option('--ignore-https-errors', 'ignore https errors')
|
|
35
|
+
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
36
|
+
.option('--no-image-responses', 'do not send image responses to the client.')
|
|
37
|
+
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
38
|
+
.option('--output-dir <path>', 'path to the directory for output files.')
|
|
39
|
+
.option('--port <port>', 'port to listen on for SSE transport.')
|
|
40
|
+
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
|
41
|
+
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
|
42
|
+
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
|
43
|
+
.option('--user-agent <ua string>', 'specify user agent string')
|
|
44
|
+
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
45
|
+
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
34
46
|
.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.')
|
|
37
|
-
.option('--config <path>', 'Path to the configuration file.')
|
|
38
47
|
.action(async (options) => {
|
|
39
48
|
const config = await resolveConfig(options);
|
|
40
49
|
const connectionList = [];
|
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,6 +52,7 @@ export class Tab {
|
|
|
56
52
|
this._onPageClose(this);
|
|
57
53
|
}
|
|
58
54
|
async navigate(url) {
|
|
55
|
+
this._clearCollectedArtifacts();
|
|
59
56
|
const downloadEvent = this.page.waitForEvent('download').catch(() => { });
|
|
60
57
|
try {
|
|
61
58
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
@@ -85,8 +82,8 @@ export class Tab {
|
|
|
85
82
|
throw new Error('No snapshot available');
|
|
86
83
|
return this._snapshot;
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
return this.
|
|
85
|
+
consoleMessages() {
|
|
86
|
+
return this._consoleMessages;
|
|
90
87
|
}
|
|
91
88
|
requests() {
|
|
92
89
|
return this._requests;
|
package/lib/tools/common.js
CHANGED
|
@@ -15,26 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
|
-
const wait = captureSnapshot => defineTool({
|
|
19
|
-
capability: 'wait',
|
|
20
|
-
schema: {
|
|
21
|
-
name: 'browser_wait',
|
|
22
|
-
title: 'Wait',
|
|
23
|
-
description: 'Wait for a specified time in seconds',
|
|
24
|
-
inputSchema: z.object({
|
|
25
|
-
time: z.number().describe('The time to wait in seconds'),
|
|
26
|
-
}),
|
|
27
|
-
type: 'readOnly',
|
|
28
|
-
},
|
|
29
|
-
handle: async (context, params) => {
|
|
30
|
-
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
31
|
-
return {
|
|
32
|
-
code: [`// Waited for ${params.time} seconds`],
|
|
33
|
-
captureSnapshot,
|
|
34
|
-
waitForNetwork: false,
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
18
|
const close = defineTool({
|
|
39
19
|
capability: 'core',
|
|
40
20
|
schema: {
|
|
@@ -47,7 +27,7 @@ const close = defineTool({
|
|
|
47
27
|
handle: async (context) => {
|
|
48
28
|
await context.close();
|
|
49
29
|
return {
|
|
50
|
-
code: [
|
|
30
|
+
code: [`await page.close()`],
|
|
51
31
|
captureSnapshot: false,
|
|
52
32
|
waitForNetwork: false,
|
|
53
33
|
};
|
|
@@ -84,6 +64,5 @@ const resize = captureSnapshot => defineTool({
|
|
|
84
64
|
});
|
|
85
65
|
export default (captureSnapshot) => [
|
|
86
66
|
close,
|
|
87
|
-
wait(captureSnapshot),
|
|
88
67
|
resize(captureSnapshot)
|
|
89
68
|
];
|
package/lib/tools/console.js
CHANGED
|
@@ -25,7 +25,7 @@ const console = defineTool({
|
|
|
25
25
|
type: 'readOnly',
|
|
26
26
|
},
|
|
27
27
|
handle: async (context) => {
|
|
28
|
-
const messages = context.currentTabOrDie().
|
|
28
|
+
const messages = context.currentTabOrDie().consoleMessages();
|
|
29
29
|
const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
|
|
30
30
|
return {
|
|
31
31
|
code: [`// <internal code to get console messages>`],
|
package/lib/tools/install.js
CHANGED
|
@@ -28,7 +28,7 @@ const install = defineTool({
|
|
|
28
28
|
type: 'destructive',
|
|
29
29
|
},
|
|
30
30
|
handle: async (context) => {
|
|
31
|
-
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.
|
|
31
|
+
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
|
32
32
|
const cliUrl = import.meta.resolve('playwright/package.json');
|
|
33
33
|
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
|
34
34
|
const child = fork(cliPath, ['install', channel], {
|
package/lib/tools/pdf.js
CHANGED
|
@@ -17,18 +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',
|
|
24
27
|
title: 'Save as PDF',
|
|
25
28
|
description: 'Save page as PDF',
|
|
26
|
-
inputSchema:
|
|
29
|
+
inputSchema: pdfSchema,
|
|
27
30
|
type: 'readOnly',
|
|
28
31
|
},
|
|
29
|
-
handle: async (context) => {
|
|
32
|
+
handle: async (context, params) => {
|
|
30
33
|
const tab = context.currentTabOrDie();
|
|
31
|
-
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`);
|
|
32
35
|
const code = [
|
|
33
36
|
`// Save page as ${fileName}`,
|
|
34
37
|
`await page.pdf(${javascript.formatObject({ path: fileName })});`,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTool } from './tool.js';
|
|
18
|
+
import * as javascript from '../javascript.js';
|
|
19
|
+
import { outputFile } from '../config.js';
|
|
20
|
+
import { generateLocator } from './utils.js';
|
|
21
|
+
const screenshotSchema = z.object({
|
|
22
|
+
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
|
23
|
+
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
|
|
24
|
+
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
|
|
25
|
+
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
|
|
26
|
+
}).refine(data => {
|
|
27
|
+
return !!data.element === !!data.ref;
|
|
28
|
+
}, {
|
|
29
|
+
message: 'Both element and ref must be provided or neither.',
|
|
30
|
+
path: ['ref', 'element']
|
|
31
|
+
});
|
|
32
|
+
const screenshot = defineTool({
|
|
33
|
+
capability: 'core',
|
|
34
|
+
schema: {
|
|
35
|
+
name: 'browser_take_screenshot',
|
|
36
|
+
title: 'Take a screenshot',
|
|
37
|
+
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
|
38
|
+
inputSchema: screenshotSchema,
|
|
39
|
+
type: 'readOnly',
|
|
40
|
+
},
|
|
41
|
+
handle: async (context, params) => {
|
|
42
|
+
const tab = context.currentTabOrDie();
|
|
43
|
+
const snapshot = tab.snapshotOrDie();
|
|
44
|
+
const fileType = params.raw ? 'png' : 'jpeg';
|
|
45
|
+
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
46
|
+
const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
|
|
47
|
+
const isElementScreenshot = params.element && params.ref;
|
|
48
|
+
const code = [
|
|
49
|
+
`// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`,
|
|
50
|
+
];
|
|
51
|
+
const locator = params.ref ? snapshot.refLocator(params.ref) : null;
|
|
52
|
+
if (locator)
|
|
53
|
+
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
54
|
+
else
|
|
55
|
+
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
56
|
+
const includeBase64 = !context.config.noImageResponses;
|
|
57
|
+
const action = async () => {
|
|
58
|
+
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
|
59
|
+
return {
|
|
60
|
+
content: includeBase64 ? [{
|
|
61
|
+
type: 'image',
|
|
62
|
+
data: screenshot.toString('base64'),
|
|
63
|
+
mimeType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
|
64
|
+
}] : []
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
code,
|
|
69
|
+
action,
|
|
70
|
+
captureSnapshot: true,
|
|
71
|
+
waitForNetwork: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
export default [
|
|
76
|
+
screenshot,
|
|
77
|
+
];
|