@playwright/mcp 0.0.25 → 0.0.27
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 +6 -1
- package/config.d.ts +7 -2
- package/index.js +1 -1
- package/lib/config.js +26 -23
- package/lib/connection.js +1 -2
- package/lib/context.js +34 -16
- package/lib/index.js +3 -1
- package/lib/pageSnapshot.js +5 -4
- package/lib/program.js +13 -3
- package/lib/tab.js +9 -2
- package/lib/tools/screenshot.js +2 -2
- package/lib/tools/snapshot.js +6 -6
- package/lib/tools/utils.js +11 -10
- package/lib/transport.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -137,7 +137,10 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
137
137
|
--ignore-https-errors ignore https errors
|
|
138
138
|
--isolated keep the browser profile in memory, do not save
|
|
139
139
|
it to disk.
|
|
140
|
-
--
|
|
140
|
+
--image-responses <mode> whether to send image responses to the client.
|
|
141
|
+
Can be "allow", "omit", or "auto". Defaults to
|
|
142
|
+
"auto", which sends images if the client can
|
|
143
|
+
display them.
|
|
141
144
|
--no-sandbox disable the sandbox for all process types that
|
|
142
145
|
are normally sandboxed.
|
|
143
146
|
--output-dir <path> path to the directory for output files.
|
|
@@ -146,6 +149,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
146
149
|
example ".com,chromium.org,.domain.com"
|
|
147
150
|
--proxy-server <proxy> specify proxy server, for example
|
|
148
151
|
"http://myproxy:3128" or "socks5://myproxy:8080"
|
|
152
|
+
--save-trace Whether to save the Playwright Trace of the
|
|
153
|
+
session into the output directory.
|
|
149
154
|
--storage-state <path> path to the storage state file for isolated
|
|
150
155
|
sessions.
|
|
151
156
|
--user-agent <ua string> specify user agent string
|
package/config.d.ts
CHANGED
|
@@ -94,6 +94,11 @@ export type Config = {
|
|
|
94
94
|
*/
|
|
95
95
|
vision?: boolean;
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Whether to save the Playwright trace of the session into the output directory.
|
|
99
|
+
*/
|
|
100
|
+
saveTrace?: boolean;
|
|
101
|
+
|
|
97
102
|
/**
|
|
98
103
|
* The directory to save output files.
|
|
99
104
|
*/
|
|
@@ -112,7 +117,7 @@ export type Config = {
|
|
|
112
117
|
};
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
|
-
*
|
|
120
|
+
* 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.
|
|
116
121
|
*/
|
|
117
|
-
|
|
122
|
+
imageResponses?: 'allow' | 'omit' | 'auto';
|
|
118
123
|
};
|
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -35,11 +35,21 @@ const defaultConfig = {
|
|
|
35
35
|
allowedOrigins: undefined,
|
|
36
36
|
blockedOrigins: undefined,
|
|
37
37
|
},
|
|
38
|
+
outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
|
|
38
39
|
};
|
|
39
|
-
export async function resolveConfig(
|
|
40
|
-
|
|
40
|
+
export async function resolveConfig(config) {
|
|
41
|
+
return mergeConfig(defaultConfig, config);
|
|
42
|
+
}
|
|
43
|
+
export async function resolveCLIConfig(cliOptions) {
|
|
44
|
+
const configInFile = await loadConfig(cliOptions.config);
|
|
41
45
|
const cliOverrides = await configFromCLIOptions(cliOptions);
|
|
42
|
-
|
|
46
|
+
const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
|
|
47
|
+
// Derive artifact output directory from config.outputDir
|
|
48
|
+
if (result.saveTrace)
|
|
49
|
+
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
|
50
|
+
if (result.browser.browserName === 'chromium')
|
|
51
|
+
result.browser.launchOptions.cdpPort = await findFreePort();
|
|
52
|
+
return result;
|
|
43
53
|
}
|
|
44
54
|
export async function configFromCLIOptions(cliOptions) {
|
|
45
55
|
let browserName;
|
|
@@ -63,9 +73,6 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
63
73
|
case 'webkit':
|
|
64
74
|
browserName = 'webkit';
|
|
65
75
|
break;
|
|
66
|
-
default:
|
|
67
|
-
browserName = 'chromium';
|
|
68
|
-
channel = 'chrome';
|
|
69
76
|
}
|
|
70
77
|
// Launch options
|
|
71
78
|
const launchOptions = {
|
|
@@ -73,13 +80,9 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
73
80
|
executablePath: cliOptions.executablePath,
|
|
74
81
|
headless: cliOptions.headless,
|
|
75
82
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// --no-sandbox was passed, disable the sandbox
|
|
80
|
-
launchOptions.chromiumSandbox = false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
+
// --no-sandbox was passed, disable the sandbox
|
|
84
|
+
if (!cliOptions.sandbox)
|
|
85
|
+
launchOptions.chromiumSandbox = false;
|
|
83
86
|
if (cliOptions.proxyServer) {
|
|
84
87
|
launchOptions.proxy = {
|
|
85
88
|
server: cliOptions.proxyServer
|
|
@@ -127,12 +130,10 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
127
130
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
128
131
|
blockedOrigins: cliOptions.blockedOrigins,
|
|
129
132
|
},
|
|
133
|
+
saveTrace: cliOptions.saveTrace,
|
|
130
134
|
outputDir: cliOptions.outputDir,
|
|
135
|
+
imageResponses: cliOptions.imageResponses,
|
|
131
136
|
};
|
|
132
|
-
if (!cliOptions.imageResponses) {
|
|
133
|
-
// --no-image-responses was passed, disable image responses
|
|
134
|
-
result.noImageResponses = true;
|
|
135
|
-
}
|
|
136
137
|
return result;
|
|
137
138
|
}
|
|
138
139
|
async function findFreePort() {
|
|
@@ -156,18 +157,17 @@ async function loadConfig(configFile) {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
export async function outputFile(config, name) {
|
|
159
|
-
|
|
160
|
-
await fs.promises.mkdir(result, { recursive: true });
|
|
160
|
+
await fs.promises.mkdir(config.outputDir, { recursive: true });
|
|
161
161
|
const fileName = sanitizeForFilePath(name);
|
|
162
|
-
return path.join(
|
|
162
|
+
return path.join(config.outputDir, fileName);
|
|
163
163
|
}
|
|
164
164
|
function pickDefined(obj) {
|
|
165
165
|
return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined));
|
|
166
166
|
}
|
|
167
167
|
function mergeConfig(base, overrides) {
|
|
168
168
|
const browser = {
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium',
|
|
170
|
+
isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
|
|
171
171
|
launchOptions: {
|
|
172
172
|
...pickDefined(base.browser?.launchOptions),
|
|
173
173
|
...pickDefined(overrides.browser?.launchOptions),
|
|
@@ -177,6 +177,9 @@ function mergeConfig(base, overrides) {
|
|
|
177
177
|
...pickDefined(base.browser?.contextOptions),
|
|
178
178
|
...pickDefined(overrides.browser?.contextOptions),
|
|
179
179
|
},
|
|
180
|
+
userDataDir: overrides.browser?.userDataDir ?? base.browser?.userDataDir,
|
|
181
|
+
cdpEndpoint: overrides.browser?.cdpEndpoint ?? base.browser?.cdpEndpoint,
|
|
182
|
+
remoteEndpoint: overrides.browser?.remoteEndpoint ?? base.browser?.remoteEndpoint,
|
|
180
183
|
};
|
|
181
184
|
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
|
182
185
|
delete browser.launchOptions.channel;
|
|
@@ -187,6 +190,6 @@ function mergeConfig(base, overrides) {
|
|
|
187
190
|
network: {
|
|
188
191
|
...pickDefined(base.network),
|
|
189
192
|
...pickDefined(overrides.network),
|
|
190
|
-
}
|
|
193
|
+
}
|
|
191
194
|
};
|
|
192
195
|
}
|
package/lib/connection.js
CHANGED
|
@@ -77,8 +77,7 @@ export class Connection {
|
|
|
77
77
|
await new Promise(resolve => {
|
|
78
78
|
this.server.oninitialized = () => resolve();
|
|
79
79
|
});
|
|
80
|
-
|
|
81
|
-
this.context.config.noImageResponses = true;
|
|
80
|
+
this.context.clientVersion = this.server.getClientVersion();
|
|
82
81
|
}
|
|
83
82
|
async close() {
|
|
84
83
|
await this.server.close();
|
package/lib/context.js
CHANGED
|
@@ -18,7 +18,7 @@ import url from 'node:url';
|
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import * as playwright from 'playwright';
|
|
21
|
-
import { waitForCompletion } from './tools/utils.js';
|
|
21
|
+
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
|
22
22
|
import { ManualPromise } from './manualPromise.js';
|
|
23
23
|
import { Tab } from './tab.js';
|
|
24
24
|
import { outputFile } from './config.js';
|
|
@@ -31,10 +31,18 @@ export class Context {
|
|
|
31
31
|
_modalStates = [];
|
|
32
32
|
_pendingAction;
|
|
33
33
|
_downloads = [];
|
|
34
|
+
clientVersion;
|
|
34
35
|
constructor(tools, config) {
|
|
35
36
|
this.tools = tools;
|
|
36
37
|
this.config = config;
|
|
37
38
|
}
|
|
39
|
+
clientSupportsImages() {
|
|
40
|
+
if (this.config.imageResponses === 'allow')
|
|
41
|
+
return true;
|
|
42
|
+
if (this.config.imageResponses === 'omit')
|
|
43
|
+
return false;
|
|
44
|
+
return !this.clientVersion?.name.includes('cursor');
|
|
45
|
+
}
|
|
38
46
|
modalStates() {
|
|
39
47
|
return this._modalStates;
|
|
40
48
|
}
|
|
@@ -84,7 +92,7 @@ export class Context {
|
|
|
84
92
|
const lines = ['### Open tabs'];
|
|
85
93
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
86
94
|
const tab = this._tabs[i];
|
|
87
|
-
const title = await tab.
|
|
95
|
+
const title = await tab.title();
|
|
88
96
|
const url = tab.page.url();
|
|
89
97
|
const current = tab === this._currentTab ? ' (current)' : '';
|
|
90
98
|
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
|
@@ -116,7 +124,7 @@ export class Context {
|
|
|
116
124
|
let actionResult;
|
|
117
125
|
try {
|
|
118
126
|
if (waitForNetwork)
|
|
119
|
-
actionResult = await waitForCompletion(this, tab
|
|
127
|
+
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
|
|
120
128
|
else
|
|
121
129
|
actionResult = await racingAction?.() ?? undefined;
|
|
122
130
|
}
|
|
@@ -153,7 +161,7 @@ ${code.join('\n')}
|
|
|
153
161
|
result.push(await this.listTabsMarkdown(), '');
|
|
154
162
|
if (this.tabs().length > 1)
|
|
155
163
|
result.push('### Current tab');
|
|
156
|
-
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.
|
|
164
|
+
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
|
|
157
165
|
if (captureSnapshot && tab.hasSnapshot())
|
|
158
166
|
result.push(tab.snapshotOrDie().text());
|
|
159
167
|
const content = actionResult?.content ?? [];
|
|
@@ -168,10 +176,13 @@ ${code.join('\n')}
|
|
|
168
176
|
};
|
|
169
177
|
}
|
|
170
178
|
async waitForTimeout(time) {
|
|
171
|
-
if (this._currentTab
|
|
172
|
-
await this._currentTab.page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
173
|
-
else
|
|
179
|
+
if (!this._currentTab || this._javaScriptBlocked()) {
|
|
174
180
|
await new Promise(f => setTimeout(f, time));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await callOnPageNoTrace(this._currentTab.page, page => {
|
|
184
|
+
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
185
|
+
});
|
|
175
186
|
}
|
|
176
187
|
async _raceAgainstModalDialogs(action) {
|
|
177
188
|
this._pendingAction = {
|
|
@@ -233,6 +244,8 @@ ${code.join('\n')}
|
|
|
233
244
|
const promise = this._browserContextPromise;
|
|
234
245
|
this._browserContextPromise = undefined;
|
|
235
246
|
await promise.then(async ({ browserContext, browser }) => {
|
|
247
|
+
if (this.config.saveTrace)
|
|
248
|
+
await browserContext.tracing.stop();
|
|
236
249
|
await browserContext.close().then(async () => {
|
|
237
250
|
await browser?.close();
|
|
238
251
|
}).catch(() => { });
|
|
@@ -264,6 +277,14 @@ ${code.join('\n')}
|
|
|
264
277
|
for (const page of browserContext.pages())
|
|
265
278
|
this._onPageCreated(page);
|
|
266
279
|
browserContext.on('page', page => this._onPageCreated(page));
|
|
280
|
+
if (this.config.saveTrace) {
|
|
281
|
+
await browserContext.tracing.start({
|
|
282
|
+
name: 'trace',
|
|
283
|
+
screenshots: false,
|
|
284
|
+
snapshots: true,
|
|
285
|
+
sources: false,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
267
288
|
return { browser, browserContext };
|
|
268
289
|
}
|
|
269
290
|
async _createBrowserContext() {
|
|
@@ -291,8 +312,8 @@ async function createIsolatedContext(browserConfig) {
|
|
|
291
312
|
try {
|
|
292
313
|
const browserName = browserConfig?.browserName ?? 'chromium';
|
|
293
314
|
const browserType = playwright[browserName];
|
|
294
|
-
const browser = await browserType.launch(browserConfig
|
|
295
|
-
const browserContext = await browser.newContext(browserConfig
|
|
315
|
+
const browser = await browserType.launch(browserConfig.launchOptions);
|
|
316
|
+
const browserContext = await browser.newContext(browserConfig.contextOptions);
|
|
296
317
|
return { browser, browserContext };
|
|
297
318
|
}
|
|
298
319
|
catch (error) {
|
|
@@ -303,10 +324,10 @@ async function createIsolatedContext(browserConfig) {
|
|
|
303
324
|
}
|
|
304
325
|
async function launchPersistentContext(browserConfig) {
|
|
305
326
|
try {
|
|
306
|
-
const browserName = browserConfig
|
|
307
|
-
const userDataDir = browserConfig
|
|
327
|
+
const browserName = browserConfig.browserName ?? 'chromium';
|
|
328
|
+
const userDataDir = browserConfig.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
|
|
308
329
|
const browserType = playwright[browserName];
|
|
309
|
-
const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig
|
|
330
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig.launchOptions, ...browserConfig.contextOptions });
|
|
310
331
|
return { browserContext };
|
|
311
332
|
}
|
|
312
333
|
catch (error) {
|
|
@@ -325,12 +346,9 @@ async function createUserDataDir(browserConfig) {
|
|
|
325
346
|
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
326
347
|
else
|
|
327
348
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
328
|
-
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig
|
|
349
|
+
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
329
350
|
await fs.promises.mkdir(result, { recursive: true });
|
|
330
351
|
return result;
|
|
331
352
|
}
|
|
332
|
-
export async function generateLocator(locator) {
|
|
333
|
-
return locator._generateLocatorString();
|
|
334
|
-
}
|
|
335
353
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
336
354
|
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { createConnection as createConnectionImpl } from './connection.js';
|
|
17
|
-
|
|
17
|
+
import { resolveConfig } from './config.js';
|
|
18
|
+
export async function createConnection(userConfig = {}) {
|
|
19
|
+
const config = await resolveConfig(userConfig);
|
|
18
20
|
return createConnectionImpl(config);
|
|
19
21
|
}
|
package/lib/pageSnapshot.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 { callOnPageNoTrace } from './tools/utils.js';
|
|
16
17
|
export class PageSnapshot {
|
|
17
18
|
_page;
|
|
18
19
|
_text;
|
|
@@ -28,15 +29,15 @@ export class PageSnapshot {
|
|
|
28
29
|
return this._text;
|
|
29
30
|
}
|
|
30
31
|
async _build() {
|
|
31
|
-
const
|
|
32
|
+
const snapshot = await callOnPageNoTrace(this._page, page => page._snapshotForAI());
|
|
32
33
|
this._text = [
|
|
33
34
|
`- Page Snapshot`,
|
|
34
35
|
'```yaml',
|
|
35
|
-
|
|
36
|
+
snapshot,
|
|
36
37
|
'```',
|
|
37
38
|
].join('\n');
|
|
38
39
|
}
|
|
39
|
-
refLocator(
|
|
40
|
-
return this._page.locator(`aria-ref=${ref}`);
|
|
40
|
+
refLocator(params) {
|
|
41
|
+
return this._page.locator(`aria-ref=${params.ref}`).describe(params.element);
|
|
41
42
|
}
|
|
42
43
|
}
|
package/lib/program.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { program } from 'commander';
|
|
17
17
|
import { startHttpTransport, startStdioTransport } from './transport.js';
|
|
18
|
-
import {
|
|
18
|
+
import { resolveCLIConfig } from './config.js';
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|
19
21
|
import { packageJSON } from './context.js';
|
|
20
22
|
program
|
|
21
23
|
.version('Version ' + packageJSON.version)
|
|
@@ -33,25 +35,33 @@ program
|
|
|
33
35
|
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
34
36
|
.option('--ignore-https-errors', 'ignore https errors')
|
|
35
37
|
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
36
|
-
.option('--
|
|
38
|
+
.option('--image-responses <mode>', '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.')
|
|
37
39
|
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
38
40
|
.option('--output-dir <path>', 'path to the directory for output files.')
|
|
39
41
|
.option('--port <port>', 'port to listen on for SSE transport.')
|
|
40
42
|
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
|
41
43
|
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
|
44
|
+
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
|
|
42
45
|
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
|
43
46
|
.option('--user-agent <ua string>', 'specify user agent string')
|
|
44
47
|
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
45
48
|
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
46
49
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
|
47
50
|
.action(async (options) => {
|
|
48
|
-
const config = await
|
|
51
|
+
const config = await resolveCLIConfig(options);
|
|
49
52
|
const connectionList = [];
|
|
50
53
|
setupExitWatchdog(connectionList);
|
|
51
54
|
if (options.port)
|
|
52
55
|
startHttpTransport(config, +options.port, options.host, connectionList);
|
|
53
56
|
else
|
|
54
57
|
await startStdioTransport(config, connectionList);
|
|
58
|
+
if (config.saveTrace) {
|
|
59
|
+
const server = await startTraceViewerServer();
|
|
60
|
+
const urlPrefix = server.urlPrefix('human-readable');
|
|
61
|
+
const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.error('\nTrace viewer listening on ' + url);
|
|
64
|
+
}
|
|
55
65
|
});
|
|
56
66
|
function setupExitWatchdog(connectionList) {
|
|
57
67
|
const handleExit = async () => {
|
package/lib/tab.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { PageSnapshot } from './pageSnapshot.js';
|
|
17
|
+
import { callOnPageNoTrace } from './tools/utils.js';
|
|
17
18
|
export class Tab {
|
|
18
19
|
context;
|
|
19
20
|
page;
|
|
@@ -51,9 +52,15 @@ export class Tab {
|
|
|
51
52
|
this._clearCollectedArtifacts();
|
|
52
53
|
this._onPageClose(this);
|
|
53
54
|
}
|
|
55
|
+
async title() {
|
|
56
|
+
return await callOnPageNoTrace(this.page, page => page.title());
|
|
57
|
+
}
|
|
58
|
+
async waitForLoadState(state, options) {
|
|
59
|
+
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(() => { }));
|
|
60
|
+
}
|
|
54
61
|
async navigate(url) {
|
|
55
62
|
this._clearCollectedArtifacts();
|
|
56
|
-
const downloadEvent = this.page.waitForEvent('download').catch(() => { });
|
|
63
|
+
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(() => { }));
|
|
57
64
|
try {
|
|
58
65
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
66
|
}
|
|
@@ -72,7 +79,7 @@ export class Tab {
|
|
|
72
79
|
throw e;
|
|
73
80
|
}
|
|
74
81
|
// Cap load event to 5 seconds, the page is operational at this point.
|
|
75
|
-
await this.
|
|
82
|
+
await this.waitForLoadState('load', { timeout: 5000 });
|
|
76
83
|
}
|
|
77
84
|
hasSnapshot() {
|
|
78
85
|
return !!this._snapshot;
|
package/lib/tools/screenshot.js
CHANGED
|
@@ -48,12 +48,12 @@ const screenshot = defineTool({
|
|
|
48
48
|
const code = [
|
|
49
49
|
`// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`,
|
|
50
50
|
];
|
|
51
|
-
const locator = params.ref ? snapshot.refLocator(params.ref) : null;
|
|
51
|
+
const locator = params.ref ? snapshot.refLocator({ element: params.element || '', ref: params.ref }) : null;
|
|
52
52
|
if (locator)
|
|
53
53
|
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
|
54
54
|
else
|
|
55
55
|
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
|
56
|
-
const includeBase64 =
|
|
56
|
+
const includeBase64 = context.clientSupportsImages();
|
|
57
57
|
const action = async () => {
|
|
58
58
|
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
|
59
59
|
return {
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -50,7 +50,7 @@ const click = defineTool({
|
|
|
50
50
|
},
|
|
51
51
|
handle: async (context, params) => {
|
|
52
52
|
const tab = context.currentTabOrDie();
|
|
53
|
-
const locator = tab.snapshotOrDie().refLocator(params
|
|
53
|
+
const locator = tab.snapshotOrDie().refLocator(params);
|
|
54
54
|
const code = [
|
|
55
55
|
`// Click ${params.element}`,
|
|
56
56
|
`await page.${await generateLocator(locator)}.click();`
|
|
@@ -79,8 +79,8 @@ const drag = defineTool({
|
|
|
79
79
|
},
|
|
80
80
|
handle: async (context, params) => {
|
|
81
81
|
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
|
82
|
-
const startLocator = snapshot.refLocator(params.startRef);
|
|
83
|
-
const endLocator = snapshot.refLocator(params.endRef);
|
|
82
|
+
const startLocator = snapshot.refLocator({ ref: params.startRef, element: params.startElement });
|
|
83
|
+
const endLocator = snapshot.refLocator({ ref: params.endRef, element: params.endElement });
|
|
84
84
|
const code = [
|
|
85
85
|
`// Drag ${params.startElement} to ${params.endElement}`,
|
|
86
86
|
`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`
|
|
@@ -104,7 +104,7 @@ const hover = defineTool({
|
|
|
104
104
|
},
|
|
105
105
|
handle: async (context, params) => {
|
|
106
106
|
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
|
107
|
-
const locator = snapshot.refLocator(params
|
|
107
|
+
const locator = snapshot.refLocator(params);
|
|
108
108
|
const code = [
|
|
109
109
|
`// Hover over ${params.element}`,
|
|
110
110
|
`await page.${await generateLocator(locator)}.hover();`
|
|
@@ -133,7 +133,7 @@ const type = defineTool({
|
|
|
133
133
|
},
|
|
134
134
|
handle: async (context, params) => {
|
|
135
135
|
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
|
136
|
-
const locator = snapshot.refLocator(params
|
|
136
|
+
const locator = snapshot.refLocator(params);
|
|
137
137
|
const code = [];
|
|
138
138
|
const steps = [];
|
|
139
139
|
if (params.slowly) {
|
|
@@ -173,7 +173,7 @@ const selectOption = defineTool({
|
|
|
173
173
|
},
|
|
174
174
|
handle: async (context, params) => {
|
|
175
175
|
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
|
176
|
-
const locator = snapshot.refLocator(params
|
|
176
|
+
const locator = snapshot.refLocator(params);
|
|
177
177
|
const code = [
|
|
178
178
|
`// Select options [${params.values.join(', ')}] in ${params.element}`,
|
|
179
179
|
`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`
|
package/lib/tools/utils.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
export async function waitForCompletion(context,
|
|
16
|
+
export async function waitForCompletion(context, tab, callback) {
|
|
17
17
|
const requests = new Set();
|
|
18
18
|
let frameNavigated = false;
|
|
19
19
|
let waitCallback = () => { };
|
|
@@ -30,22 +30,20 @@ export async function waitForCompletion(context, page, callback) {
|
|
|
30
30
|
frameNavigated = true;
|
|
31
31
|
dispose();
|
|
32
32
|
clearTimeout(timeout);
|
|
33
|
-
void
|
|
34
|
-
waitCallback();
|
|
35
|
-
});
|
|
33
|
+
void tab.waitForLoadState('load').then(waitCallback);
|
|
36
34
|
};
|
|
37
35
|
const onTimeout = () => {
|
|
38
36
|
dispose();
|
|
39
37
|
waitCallback();
|
|
40
38
|
};
|
|
41
|
-
page.on('request', requestListener);
|
|
42
|
-
page.on('requestfinished', requestFinishedListener);
|
|
43
|
-
page.on('framenavigated', frameNavigateListener);
|
|
39
|
+
tab.page.on('request', requestListener);
|
|
40
|
+
tab.page.on('requestfinished', requestFinishedListener);
|
|
41
|
+
tab.page.on('framenavigated', frameNavigateListener);
|
|
44
42
|
const timeout = setTimeout(onTimeout, 10000);
|
|
45
43
|
const dispose = () => {
|
|
46
|
-
page.off('request', requestListener);
|
|
47
|
-
page.off('requestfinished', requestFinishedListener);
|
|
48
|
-
page.off('framenavigated', frameNavigateListener);
|
|
44
|
+
tab.page.off('request', requestListener);
|
|
45
|
+
tab.page.off('requestfinished', requestFinishedListener);
|
|
46
|
+
tab.page.off('framenavigated', frameNavigateListener);
|
|
49
47
|
clearTimeout(timeout);
|
|
50
48
|
};
|
|
51
49
|
try {
|
|
@@ -70,3 +68,6 @@ export function sanitizeForFilePath(s) {
|
|
|
70
68
|
export async function generateLocator(locator) {
|
|
71
69
|
return locator._generateLocatorString();
|
|
72
70
|
}
|
|
71
|
+
export async function callOnPageNoTrace(page, callback) {
|
|
72
|
+
return await page._wrapApiCall(() => callback(page), { internal: true });
|
|
73
|
+
}
|
package/lib/transport.js
CHANGED
|
@@ -127,6 +127,6 @@ export function startHttpTransport(config, port, hostname, connectionList) {
|
|
|
127
127
|
'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
|
|
128
128
|
].join('\n');
|
|
129
129
|
// eslint-disable-next-line no-console
|
|
130
|
-
console.
|
|
130
|
+
console.error(message);
|
|
131
131
|
});
|
|
132
132
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
39
39
|
"commander": "^13.1.0",
|
|
40
|
-
"playwright": "1.53.0-alpha-
|
|
40
|
+
"playwright": "1.53.0-alpha-2025-05-27",
|
|
41
41
|
"zod-to-json-schema": "^3.24.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@eslint/eslintrc": "^3.2.0",
|
|
45
45
|
"@eslint/js": "^9.19.0",
|
|
46
|
-
"@playwright/test": "1.53.0-alpha-
|
|
46
|
+
"@playwright/test": "1.53.0-alpha-2025-05-27",
|
|
47
47
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
48
48
|
"@types/node": "^22.13.10",
|
|
49
49
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|