@playwright/mcp 0.0.29 → 0.0.31
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 +174 -292
- package/config.d.ts +3 -17
- package/lib/browserContextFactory.js +3 -35
- package/lib/config.js +64 -6
- package/lib/connection.js +2 -3
- package/lib/context.js +39 -29
- package/lib/{resources/resource.js → log.js} +6 -1
- package/lib/pageSnapshot.js +1 -1
- package/lib/program.js +14 -12
- package/lib/tab.js +37 -3
- package/lib/tools/common.js +4 -4
- package/lib/tools/console.js +1 -1
- package/lib/tools/dialogs.js +4 -4
- package/lib/tools/evaluate.js +62 -0
- package/lib/tools/files.js +5 -5
- package/lib/tools/install.js +1 -1
- package/lib/tools/keyboard.js +50 -4
- package/lib/tools/{vision.js → mouse.js} +14 -81
- package/lib/tools/navigate.js +12 -12
- package/lib/tools/screenshot.js +1 -1
- package/lib/tools/snapshot.js +18 -50
- package/lib/tools/tabs.js +14 -14
- package/lib/tools/utils.js +9 -1
- package/lib/tools/wait.js +6 -6
- package/lib/tools.js +12 -26
- package/lib/transport.js +40 -33
- package/package.json +7 -3
- package/lib/browserServer.js +0 -151
- package/lib/tools/testing.js +0 -60
package/config.d.ts
CHANGED
|
@@ -16,18 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
import type * as playwright from 'playwright';
|
|
18
18
|
|
|
19
|
-
export type ToolCapability = 'core' | 'tabs' | '
|
|
19
|
+
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf';
|
|
20
20
|
|
|
21
21
|
export type Config = {
|
|
22
22
|
/**
|
|
23
23
|
* The browser to use.
|
|
24
24
|
*/
|
|
25
25
|
browser?: {
|
|
26
|
-
/**
|
|
27
|
-
* Use browser agent (experimental).
|
|
28
|
-
*/
|
|
29
|
-
browserAgent?: string;
|
|
30
|
-
|
|
31
26
|
/**
|
|
32
27
|
* The type of browser to use.
|
|
33
28
|
*/
|
|
@@ -85,20 +80,11 @@ export type Config = {
|
|
|
85
80
|
/**
|
|
86
81
|
* List of enabled tool capabilities. Possible values:
|
|
87
82
|
* - 'core': Core browser automation features.
|
|
88
|
-
* - 'tabs': Tab management features.
|
|
89
83
|
* - 'pdf': PDF generation and manipulation.
|
|
90
|
-
* - '
|
|
91
|
-
* - 'wait': Wait and timing utilities.
|
|
92
|
-
* - 'files': File upload/download support.
|
|
93
|
-
* - 'install': Browser installation utilities.
|
|
84
|
+
* - 'vision': Coordinate-based interactions.
|
|
94
85
|
*/
|
|
95
86
|
capabilities?: ToolCapability[];
|
|
96
87
|
|
|
97
|
-
/**
|
|
98
|
-
* Run server that uses screenshots (Aria snapshots are used by default).
|
|
99
|
-
*/
|
|
100
|
-
vision?: boolean;
|
|
101
|
-
|
|
102
88
|
/**
|
|
103
89
|
* Whether to save the Playwright trace of the session into the output directory.
|
|
104
90
|
*/
|
|
@@ -124,5 +110,5 @@ export type Config = {
|
|
|
124
110
|
/**
|
|
125
111
|
* Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
|
|
126
112
|
*/
|
|
127
|
-
imageResponses?: 'allow' | 'omit'
|
|
113
|
+
imageResponses?: 'allow' | 'omit';
|
|
128
114
|
};
|
|
@@ -17,10 +17,8 @@ import fs from 'node:fs';
|
|
|
17
17
|
import net from 'node:net';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import os from 'node:os';
|
|
20
|
-
import debug from 'debug';
|
|
21
20
|
import * as playwright from 'playwright';
|
|
22
|
-
import {
|
|
23
|
-
const testDebug = debug('pw:mcp:test');
|
|
21
|
+
import { logUnhandledError, testDebug } from './log.js';
|
|
24
22
|
export function contextFactory(browserConfig) {
|
|
25
23
|
if (browserConfig.remoteEndpoint)
|
|
26
24
|
return new RemoteContextFactory(browserConfig);
|
|
@@ -28,8 +26,6 @@ export function contextFactory(browserConfig) {
|
|
|
28
26
|
return new CdpContextFactory(browserConfig);
|
|
29
27
|
if (browserConfig.isolated)
|
|
30
28
|
return new IsolatedContextFactory(browserConfig);
|
|
31
|
-
if (browserConfig.browserAgent)
|
|
32
|
-
return new BrowserServerContextFactory(browserConfig);
|
|
33
29
|
return new PersistentContextFactory(browserConfig);
|
|
34
30
|
}
|
|
35
31
|
class BaseContextFactory {
|
|
@@ -70,10 +66,10 @@ class BaseContextFactory {
|
|
|
70
66
|
testDebug(`close browser context (${this.name})`);
|
|
71
67
|
if (browser.contexts().length === 1)
|
|
72
68
|
this._browserPromise = undefined;
|
|
73
|
-
await browserContext.close().catch(
|
|
69
|
+
await browserContext.close().catch(logUnhandledError);
|
|
74
70
|
if (browser.contexts().length === 0) {
|
|
75
71
|
testDebug(`close browser (${this.name})`);
|
|
76
|
-
await browser.close().catch(
|
|
72
|
+
await browser.close().catch(logUnhandledError);
|
|
77
73
|
}
|
|
78
74
|
}
|
|
79
75
|
}
|
|
@@ -183,34 +179,6 @@ class PersistentContextFactory {
|
|
|
183
179
|
return result;
|
|
184
180
|
}
|
|
185
181
|
}
|
|
186
|
-
export class BrowserServerContextFactory extends BaseContextFactory {
|
|
187
|
-
constructor(browserConfig) {
|
|
188
|
-
super('persistent', browserConfig);
|
|
189
|
-
}
|
|
190
|
-
async _doObtainBrowser() {
|
|
191
|
-
const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), {
|
|
192
|
-
method: 'POST',
|
|
193
|
-
body: JSON.stringify({
|
|
194
|
-
browserType: this.browserConfig.browserName,
|
|
195
|
-
userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(),
|
|
196
|
-
launchOptions: this.browserConfig.launchOptions,
|
|
197
|
-
contextOptions: this.browserConfig.contextOptions,
|
|
198
|
-
}),
|
|
199
|
-
});
|
|
200
|
-
const info = await response.json();
|
|
201
|
-
if (info.error)
|
|
202
|
-
throw new Error(info.error);
|
|
203
|
-
return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`);
|
|
204
|
-
}
|
|
205
|
-
async _doCreateContext(browser) {
|
|
206
|
-
return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
207
|
-
}
|
|
208
|
-
async _createUserDataDir() {
|
|
209
|
-
const dir = await userDataDir(this.browserConfig);
|
|
210
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
211
|
-
return dir;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
182
|
async function injectCdpPort(browserConfig) {
|
|
215
183
|
if (browserConfig.browserName === 'chromium')
|
|
216
184
|
browserConfig.launchOptions.cdpPort = await findFreePort();
|
package/lib/config.js
CHANGED
|
@@ -42,14 +42,18 @@ export async function resolveConfig(config) {
|
|
|
42
42
|
}
|
|
43
43
|
export async function resolveCLIConfig(cliOptions) {
|
|
44
44
|
const configInFile = await loadConfig(cliOptions.config);
|
|
45
|
-
const
|
|
46
|
-
const
|
|
45
|
+
const envOverrides = configFromEnv();
|
|
46
|
+
const cliOverrides = configFromCLIOptions(cliOptions);
|
|
47
|
+
let result = defaultConfig;
|
|
48
|
+
result = mergeConfig(result, configInFile);
|
|
49
|
+
result = mergeConfig(result, envOverrides);
|
|
50
|
+
result = mergeConfig(result, cliOverrides);
|
|
47
51
|
// Derive artifact output directory from config.outputDir
|
|
48
52
|
if (result.saveTrace)
|
|
49
53
|
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
|
50
54
|
return result;
|
|
51
55
|
}
|
|
52
|
-
export
|
|
56
|
+
export function configFromCLIOptions(cliOptions) {
|
|
53
57
|
let browserName;
|
|
54
58
|
let channel;
|
|
55
59
|
switch (cliOptions.browser) {
|
|
@@ -88,6 +92,8 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
88
92
|
if (cliOptions.proxyBypass)
|
|
89
93
|
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
90
94
|
}
|
|
95
|
+
if (cliOptions.device && cliOptions.cdpEndpoint)
|
|
96
|
+
throw new Error('Device emulation is not supported with cdpEndpoint.');
|
|
91
97
|
// Context options
|
|
92
98
|
const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
93
99
|
if (cliOptions.storageState)
|
|
@@ -111,7 +117,6 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
111
117
|
contextOptions.serviceWorkers = 'block';
|
|
112
118
|
const result = {
|
|
113
119
|
browser: {
|
|
114
|
-
browserAgent: cliOptions.browserAgent ?? process.env.PW_BROWSER_AGENT,
|
|
115
120
|
browserName,
|
|
116
121
|
isolated: cliOptions.isolated,
|
|
117
122
|
userDataDir: cliOptions.userDataDir,
|
|
@@ -123,8 +128,7 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
123
128
|
port: cliOptions.port,
|
|
124
129
|
host: cliOptions.host,
|
|
125
130
|
},
|
|
126
|
-
capabilities: cliOptions.caps
|
|
127
|
-
vision: !!cliOptions.vision,
|
|
131
|
+
capabilities: cliOptions.caps,
|
|
128
132
|
network: {
|
|
129
133
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
130
134
|
blockedOrigins: cliOptions.blockedOrigins,
|
|
@@ -135,6 +139,35 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
135
139
|
};
|
|
136
140
|
return result;
|
|
137
141
|
}
|
|
142
|
+
function configFromEnv() {
|
|
143
|
+
const options = {};
|
|
144
|
+
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
|
145
|
+
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
|
146
|
+
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
|
147
|
+
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
|
|
148
|
+
options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
|
|
149
|
+
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
|
|
150
|
+
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
|
|
151
|
+
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
|
|
152
|
+
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
|
|
153
|
+
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
|
|
154
|
+
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
|
|
155
|
+
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
|
|
156
|
+
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
|
|
157
|
+
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit')
|
|
158
|
+
options.imageResponses = 'omit';
|
|
159
|
+
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
|
160
|
+
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
|
161
|
+
options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);
|
|
162
|
+
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
|
163
|
+
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
|
|
164
|
+
options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
|
|
165
|
+
options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
|
|
166
|
+
options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
|
|
167
|
+
options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
|
|
168
|
+
options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
|
|
169
|
+
return configFromCLIOptions(options);
|
|
170
|
+
}
|
|
138
171
|
async function loadConfig(configFile) {
|
|
139
172
|
if (!configFile)
|
|
140
173
|
return {};
|
|
@@ -185,3 +218,28 @@ function mergeConfig(base, overrides) {
|
|
|
185
218
|
},
|
|
186
219
|
};
|
|
187
220
|
}
|
|
221
|
+
export function semicolonSeparatedList(value) {
|
|
222
|
+
if (!value)
|
|
223
|
+
return undefined;
|
|
224
|
+
return value.split(';').map(v => v.trim());
|
|
225
|
+
}
|
|
226
|
+
export function commaSeparatedList(value) {
|
|
227
|
+
if (!value)
|
|
228
|
+
return undefined;
|
|
229
|
+
return value.split(',').map(v => v.trim());
|
|
230
|
+
}
|
|
231
|
+
function envToNumber(value) {
|
|
232
|
+
if (!value)
|
|
233
|
+
return undefined;
|
|
234
|
+
return +value;
|
|
235
|
+
}
|
|
236
|
+
function envToBoolean(value) {
|
|
237
|
+
if (value === 'true' || value === '1')
|
|
238
|
+
return true;
|
|
239
|
+
if (value === 'false' || value === '0')
|
|
240
|
+
return false;
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function envToString(value) {
|
|
244
|
+
return value ? value.trim() : undefined;
|
|
245
|
+
}
|
package/lib/connection.js
CHANGED
|
@@ -17,11 +17,10 @@ import { Server as McpServer } 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 } from './context.js';
|
|
20
|
-
import {
|
|
20
|
+
import { allTools } from './tools.js';
|
|
21
21
|
import { packageJSON } from './package.js';
|
|
22
22
|
export function createConnection(config, browserContextFactory) {
|
|
23
|
-
const
|
|
24
|
-
const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
|
|
23
|
+
const tools = allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
|
|
25
24
|
const context = new Context(tools, config, browserContextFactory);
|
|
26
25
|
const server = new McpServer({ name: 'Playwright', version: packageJSON.version }, {
|
|
27
26
|
capabilities: {
|
package/lib/context.js
CHANGED
|
@@ -37,11 +37,9 @@ export class Context {
|
|
|
37
37
|
testDebug('create context');
|
|
38
38
|
}
|
|
39
39
|
clientSupportsImages() {
|
|
40
|
-
if (this.config.imageResponses === 'allow')
|
|
41
|
-
return true;
|
|
42
40
|
if (this.config.imageResponses === 'omit')
|
|
43
41
|
return false;
|
|
44
|
-
return
|
|
42
|
+
return true;
|
|
45
43
|
}
|
|
46
44
|
modalStates() {
|
|
47
45
|
return this._modalStates;
|
|
@@ -77,7 +75,7 @@ export class Context {
|
|
|
77
75
|
return this._currentTab;
|
|
78
76
|
}
|
|
79
77
|
async selectTab(index) {
|
|
80
|
-
this._currentTab = this._tabs[index
|
|
78
|
+
this._currentTab = this._tabs[index];
|
|
81
79
|
await this._currentTab.page.bringToFront();
|
|
82
80
|
}
|
|
83
81
|
async ensureTab() {
|
|
@@ -95,12 +93,12 @@ export class Context {
|
|
|
95
93
|
const title = await tab.title();
|
|
96
94
|
const url = tab.page.url();
|
|
97
95
|
const current = tab === this._currentTab ? ' (current)' : '';
|
|
98
|
-
lines.push(`- ${i
|
|
96
|
+
lines.push(`- ${i}:${current} [${title}] (${url})`);
|
|
99
97
|
}
|
|
100
98
|
return lines.join('\n');
|
|
101
99
|
}
|
|
102
100
|
async closeTab(index) {
|
|
103
|
-
const tab = index === undefined ? this._currentTab : this._tabs[index
|
|
101
|
+
const tab = index === undefined ? this._currentTab : this._tabs[index];
|
|
104
102
|
await tab?.page.close();
|
|
105
103
|
return await this.listTabsMarkdown();
|
|
106
104
|
}
|
|
@@ -108,7 +106,6 @@ export class Context {
|
|
|
108
106
|
// Tab management is done outside of the action() call.
|
|
109
107
|
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
110
108
|
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
111
|
-
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
112
109
|
if (resultOverride)
|
|
113
110
|
return resultOverride;
|
|
114
111
|
if (!this._currentTab) {
|
|
@@ -121,25 +118,25 @@ export class Context {
|
|
|
121
118
|
}
|
|
122
119
|
const tab = this.currentTabOrDie();
|
|
123
120
|
// TODO: race against modal dialogs to resolve clicks.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
121
|
+
const actionResult = await this._raceAgainstModalDialogs(async () => {
|
|
122
|
+
try {
|
|
123
|
+
if (waitForNetwork)
|
|
124
|
+
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
|
|
125
|
+
else
|
|
126
|
+
return await action?.() ?? undefined;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (captureSnapshot && !this._javaScriptBlocked())
|
|
130
|
+
await tab.captureSnapshot();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
135
133
|
const result = [];
|
|
136
|
-
result.push(
|
|
134
|
+
result.push(`### Ran Playwright code
|
|
137
135
|
\`\`\`js
|
|
138
136
|
${code.join('\n')}
|
|
139
|
-
|
|
140
|
-
`);
|
|
137
|
+
\`\`\``);
|
|
141
138
|
if (this.modalStates().length) {
|
|
142
|
-
result.push(...this.modalStatesMarkdown());
|
|
139
|
+
result.push('', ...this.modalStatesMarkdown());
|
|
143
140
|
return {
|
|
144
141
|
content: [{
|
|
145
142
|
type: 'text',
|
|
@@ -147,6 +144,12 @@ ${code.join('\n')}
|
|
|
147
144
|
}],
|
|
148
145
|
};
|
|
149
146
|
}
|
|
147
|
+
const messages = tab.takeRecentConsoleMessages();
|
|
148
|
+
if (messages.length) {
|
|
149
|
+
result.push('', `### New console messages`);
|
|
150
|
+
for (const message of messages)
|
|
151
|
+
result.push(`- ${trim(message.toString(), 100)}`);
|
|
152
|
+
}
|
|
150
153
|
if (this._downloads.length) {
|
|
151
154
|
result.push('', '### Downloads');
|
|
152
155
|
for (const entry of this._downloads) {
|
|
@@ -155,15 +158,17 @@ ${code.join('\n')}
|
|
|
155
158
|
else
|
|
156
159
|
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
157
160
|
}
|
|
158
|
-
result.push('');
|
|
159
161
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
if (captureSnapshot && tab.hasSnapshot()) {
|
|
163
|
+
if (this.tabs().length > 1)
|
|
164
|
+
result.push('', await this.listTabsMarkdown());
|
|
165
|
+
if (this.tabs().length > 1)
|
|
166
|
+
result.push('', '### Current tab');
|
|
167
|
+
else
|
|
168
|
+
result.push('', '### Page state');
|
|
169
|
+
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
|
|
166
170
|
result.push(tab.snapshotOrDie().text());
|
|
171
|
+
}
|
|
167
172
|
const content = actionResult?.content ?? [];
|
|
168
173
|
return {
|
|
169
174
|
content: [
|
|
@@ -289,3 +294,8 @@ ${code.join('\n')}
|
|
|
289
294
|
return result;
|
|
290
295
|
}
|
|
291
296
|
}
|
|
297
|
+
function trim(text, maxLength) {
|
|
298
|
+
if (text.length <= maxLength)
|
|
299
|
+
return text;
|
|
300
|
+
return text.slice(0, maxLength) + '...';
|
|
301
|
+
}
|
|
@@ -13,4 +13,9 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
import debug from 'debug';
|
|
17
|
+
const errorsDebug = debug('pw:mcp:errors');
|
|
18
|
+
export function logUnhandledError(error) {
|
|
19
|
+
errorsDebug(error);
|
|
20
|
+
}
|
|
21
|
+
export const testDebug = debug('pw:mcp:test');
|
package/lib/pageSnapshot.js
CHANGED
package/lib/program.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { program } from 'commander';
|
|
16
|
+
import { program, Option } from 'commander';
|
|
17
17
|
// @ts-ignore
|
|
18
18
|
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|
19
|
-
import { startHttpTransport, startStdioTransport } from './transport.js';
|
|
20
|
-
import { resolveCLIConfig } from './config.js';
|
|
19
|
+
import { startHttpServer, startHttpTransport, startStdioTransport } from './transport.js';
|
|
20
|
+
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
|
21
21
|
import { Server } from './server.js';
|
|
22
22
|
import { packageJSON } from './package.js';
|
|
23
23
|
program
|
|
@@ -27,8 +27,7 @@ program
|
|
|
27
27
|
.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)
|
|
28
28
|
.option('--block-service-workers', 'block service workers')
|
|
29
29
|
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
|
30
|
-
.option('--
|
|
31
|
-
.option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
|
30
|
+
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
|
|
32
31
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
|
33
32
|
.option('--config <path>', 'path to the configuration file.')
|
|
34
33
|
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
|
|
@@ -37,7 +36,7 @@ program
|
|
|
37
36
|
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
38
37
|
.option('--ignore-https-errors', 'ignore https errors')
|
|
39
38
|
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
40
|
-
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow"
|
|
39
|
+
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
|
|
41
40
|
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
42
41
|
.option('--output-dir <path>', 'path to the directory for output files.')
|
|
43
42
|
.option('--port <port>', 'port to listen on for SSE transport.')
|
|
@@ -48,13 +47,19 @@ program
|
|
|
48
47
|
.option('--user-agent <ua string>', 'specify user agent string')
|
|
49
48
|
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
50
49
|
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
51
|
-
.
|
|
50
|
+
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
|
52
51
|
.action(async (options) => {
|
|
52
|
+
if (options.vision) {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error('The --vision option is deprecated, use --caps=vision instead');
|
|
55
|
+
options.caps = 'vision';
|
|
56
|
+
}
|
|
53
57
|
const config = await resolveCLIConfig(options);
|
|
58
|
+
const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined;
|
|
54
59
|
const server = new Server(config);
|
|
55
60
|
server.setupExitWatchdog();
|
|
56
|
-
if (
|
|
57
|
-
startHttpTransport(server);
|
|
61
|
+
if (httpServer)
|
|
62
|
+
startHttpTransport(httpServer, server);
|
|
58
63
|
else
|
|
59
64
|
await startStdioTransport(server);
|
|
60
65
|
if (config.saveTrace) {
|
|
@@ -65,7 +70,4 @@ program
|
|
|
65
70
|
console.error('\nTrace viewer listening on ' + url);
|
|
66
71
|
}
|
|
67
72
|
});
|
|
68
|
-
function semicolonSeparatedList(value) {
|
|
69
|
-
return value.split(';').map(v => v.trim());
|
|
70
|
-
}
|
|
71
73
|
void program.parseAsync(process.argv);
|
package/lib/tab.js
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { PageSnapshot } from './pageSnapshot.js';
|
|
17
17
|
import { callOnPageNoTrace } from './tools/utils.js';
|
|
18
|
+
import { logUnhandledError } from './log.js';
|
|
18
19
|
export class Tab {
|
|
19
20
|
context;
|
|
20
21
|
page;
|
|
21
22
|
_consoleMessages = [];
|
|
23
|
+
_recentConsoleMessages = [];
|
|
22
24
|
_requests = new Map();
|
|
23
25
|
_snapshot;
|
|
24
26
|
_onPageClose;
|
|
@@ -26,7 +28,8 @@ export class Tab {
|
|
|
26
28
|
this.context = context;
|
|
27
29
|
this.page = page;
|
|
28
30
|
this._onPageClose = onPageClose;
|
|
29
|
-
page.on('console', event => this.
|
|
31
|
+
page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event)));
|
|
32
|
+
page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
|
|
30
33
|
page.on('request', request => this._requests.set(request, null));
|
|
31
34
|
page.on('response', response => this._requests.set(response.request(), response));
|
|
32
35
|
page.on('close', () => this._onClose());
|
|
@@ -46,8 +49,13 @@ export class Tab {
|
|
|
46
49
|
}
|
|
47
50
|
_clearCollectedArtifacts() {
|
|
48
51
|
this._consoleMessages.length = 0;
|
|
52
|
+
this._recentConsoleMessages.length = 0;
|
|
49
53
|
this._requests.clear();
|
|
50
54
|
}
|
|
55
|
+
_handleConsoleMessage(message) {
|
|
56
|
+
this._consoleMessages.push(message);
|
|
57
|
+
this._recentConsoleMessages.push(message);
|
|
58
|
+
}
|
|
51
59
|
_onClose() {
|
|
52
60
|
this._clearCollectedArtifacts();
|
|
53
61
|
this._onPageClose(this);
|
|
@@ -56,11 +64,11 @@ export class Tab {
|
|
|
56
64
|
return await callOnPageNoTrace(this.page, page => page.title());
|
|
57
65
|
}
|
|
58
66
|
async waitForLoadState(state, options) {
|
|
59
|
-
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(
|
|
67
|
+
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError));
|
|
60
68
|
}
|
|
61
69
|
async navigate(url) {
|
|
62
70
|
this._clearCollectedArtifacts();
|
|
63
|
-
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(
|
|
71
|
+
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError));
|
|
64
72
|
try {
|
|
65
73
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
66
74
|
}
|
|
@@ -98,4 +106,30 @@ export class Tab {
|
|
|
98
106
|
async captureSnapshot() {
|
|
99
107
|
this._snapshot = await PageSnapshot.create(this.page);
|
|
100
108
|
}
|
|
109
|
+
takeRecentConsoleMessages() {
|
|
110
|
+
const result = this._recentConsoleMessages.slice();
|
|
111
|
+
this._recentConsoleMessages.length = 0;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function messageToConsoleMessage(message) {
|
|
116
|
+
return {
|
|
117
|
+
type: message.type(),
|
|
118
|
+
text: message.text(),
|
|
119
|
+
toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function pageErrorToConsoleMessage(errorOrValue) {
|
|
123
|
+
if (errorOrValue instanceof Error) {
|
|
124
|
+
return {
|
|
125
|
+
type: undefined,
|
|
126
|
+
text: errorOrValue.message,
|
|
127
|
+
toString: () => errorOrValue.stack || errorOrValue.message,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
type: undefined,
|
|
132
|
+
text: String(errorOrValue),
|
|
133
|
+
toString: () => String(errorOrValue),
|
|
134
|
+
};
|
|
101
135
|
}
|
package/lib/tools/common.js
CHANGED
|
@@ -33,7 +33,7 @@ const close = defineTool({
|
|
|
33
33
|
};
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
|
-
const resize =
|
|
36
|
+
const resize = defineTool({
|
|
37
37
|
capability: 'core',
|
|
38
38
|
schema: {
|
|
39
39
|
name: 'browser_resize',
|
|
@@ -57,12 +57,12 @@ const resize = captureSnapshot => defineTool({
|
|
|
57
57
|
return {
|
|
58
58
|
code,
|
|
59
59
|
action,
|
|
60
|
-
captureSnapshot,
|
|
60
|
+
captureSnapshot: true,
|
|
61
61
|
waitForNetwork: true
|
|
62
62
|
};
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
|
-
export default
|
|
65
|
+
export default [
|
|
66
66
|
close,
|
|
67
|
-
resize
|
|
67
|
+
resize
|
|
68
68
|
];
|
package/lib/tools/console.js
CHANGED
|
@@ -26,7 +26,7 @@ const console = defineTool({
|
|
|
26
26
|
},
|
|
27
27
|
handle: async (context) => {
|
|
28
28
|
const messages = context.currentTabOrDie().consoleMessages();
|
|
29
|
-
const log = messages.map(message =>
|
|
29
|
+
const log = messages.map(message => message.toString()).join('\n');
|
|
30
30
|
return {
|
|
31
31
|
code: [`// <internal code to get console messages>`],
|
|
32
32
|
action: async () => {
|
package/lib/tools/dialogs.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
|
-
const handleDialog =
|
|
18
|
+
const handleDialog = defineTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_handle_dialog',
|
|
@@ -41,12 +41,12 @@ const handleDialog = captureSnapshot => defineTool({
|
|
|
41
41
|
];
|
|
42
42
|
return {
|
|
43
43
|
code,
|
|
44
|
-
captureSnapshot,
|
|
44
|
+
captureSnapshot: true,
|
|
45
45
|
waitForNetwork: false,
|
|
46
46
|
};
|
|
47
47
|
},
|
|
48
48
|
clearsModalState: 'dialog',
|
|
49
49
|
});
|
|
50
|
-
export default
|
|
51
|
-
handleDialog
|
|
50
|
+
export default [
|
|
51
|
+
handleDialog,
|
|
52
52
|
];
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { generateLocator } from './utils.js';
|
|
20
|
+
const evaluateSchema = z.object({
|
|
21
|
+
function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
|
|
22
|
+
element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
|
|
23
|
+
ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
|
|
24
|
+
});
|
|
25
|
+
const evaluate = defineTool({
|
|
26
|
+
capability: 'core',
|
|
27
|
+
schema: {
|
|
28
|
+
name: 'browser_evaluate',
|
|
29
|
+
title: 'Evaluate JavaScript',
|
|
30
|
+
description: 'Evaluate JavaScript expression on page or element',
|
|
31
|
+
inputSchema: evaluateSchema,
|
|
32
|
+
type: 'destructive',
|
|
33
|
+
},
|
|
34
|
+
handle: async (context, params) => {
|
|
35
|
+
const tab = context.currentTabOrDie();
|
|
36
|
+
const code = [];
|
|
37
|
+
let locator;
|
|
38
|
+
if (params.ref && params.element) {
|
|
39
|
+
const snapshot = tab.snapshotOrDie();
|
|
40
|
+
locator = snapshot.refLocator({ ref: params.ref, element: params.element });
|
|
41
|
+
code.push(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
code.push(`await page.evaluate(${javascript.quote(params.function)});`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
code,
|
|
48
|
+
action: async () => {
|
|
49
|
+
const receiver = locator ?? tab.page;
|
|
50
|
+
const result = await receiver._evaluateFunction(params.function);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: '- Result: ' + (JSON.stringify(result, null, 2) || 'undefined') }],
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
captureSnapshot: false,
|
|
56
|
+
waitForNetwork: false,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
export default [
|
|
61
|
+
evaluate,
|
|
62
|
+
];
|