@playwright/mcp 0.0.24 → 0.0.26

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/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.
@@ -89,6 +94,11 @@ export type Config = {
89
94
  */
90
95
  vision?: boolean;
91
96
 
97
+ /**
98
+ * Whether to save the Playwright trace of the session into the output directory.
99
+ */
100
+ saveTrace?: boolean;
101
+
92
102
  /**
93
103
  * The directory to save output files.
94
104
  */
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,
@@ -34,11 +35,19 @@ const defaultConfig = {
34
35
  allowedOrigins: undefined,
35
36
  blockedOrigins: undefined,
36
37
  },
38
+ outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
37
39
  };
38
- export async function resolveConfig(cliOptions) {
39
- const config = await loadConfig(cliOptions.config);
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);
40
45
  const cliOverrides = await configFromCLIOptions(cliOptions);
41
- return mergeConfig(defaultConfig, mergeConfig(config, cliOverrides));
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
+ return result;
42
51
  }
43
52
  export async function configFromCLIOptions(cliOptions) {
44
53
  let browserName;
@@ -66,17 +75,51 @@ export async function configFromCLIOptions(cliOptions) {
66
75
  browserName = 'chromium';
67
76
  channel = 'chrome';
68
77
  }
78
+ // Launch options
69
79
  const launchOptions = {
70
80
  channel,
71
81
  executablePath: cliOptions.executablePath,
72
82
  headless: cliOptions.headless,
73
83
  };
74
- if (browserName === 'chromium')
84
+ if (browserName === 'chromium') {
75
85
  launchOptions.cdpPort = await findFreePort();
76
- const contextOptions = cliOptions.device ? devices[cliOptions.device] : undefined;
77
- return {
86
+ if (!cliOptions.sandbox) {
87
+ // --no-sandbox was passed, disable the sandbox
88
+ launchOptions.chromiumSandbox = false;
89
+ }
90
+ }
91
+ if (cliOptions.proxyServer) {
92
+ launchOptions.proxy = {
93
+ server: cliOptions.proxyServer
94
+ };
95
+ if (cliOptions.proxyBypass)
96
+ launchOptions.proxy.bypass = cliOptions.proxyBypass;
97
+ }
98
+ // Context options
99
+ const contextOptions = cliOptions.device ? devices[cliOptions.device] : {};
100
+ if (cliOptions.storageState)
101
+ contextOptions.storageState = cliOptions.storageState;
102
+ if (cliOptions.userAgent)
103
+ contextOptions.userAgent = cliOptions.userAgent;
104
+ if (cliOptions.viewportSize) {
105
+ try {
106
+ const [width, height] = cliOptions.viewportSize.split(',').map(n => +n);
107
+ if (isNaN(width) || isNaN(height))
108
+ throw new Error('bad values');
109
+ contextOptions.viewport = { width, height };
110
+ }
111
+ catch (e) {
112
+ throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
113
+ }
114
+ }
115
+ if (cliOptions.ignoreHttpsErrors)
116
+ contextOptions.ignoreHTTPSErrors = true;
117
+ if (cliOptions.blockServiceWorkers)
118
+ contextOptions.serviceWorkers = 'block';
119
+ const result = {
78
120
  browser: {
79
121
  browserName,
122
+ isolated: cliOptions.isolated,
80
123
  userDataDir: cliOptions.userDataDir,
81
124
  launchOptions,
82
125
  contextOptions,
@@ -92,8 +135,14 @@ export async function configFromCLIOptions(cliOptions) {
92
135
  allowedOrigins: cliOptions.allowedOrigins,
93
136
  blockedOrigins: cliOptions.blockedOrigins,
94
137
  },
138
+ saveTrace: cliOptions.saveTrace,
95
139
  outputDir: cliOptions.outputDir,
96
140
  };
141
+ if (!cliOptions.imageResponses) {
142
+ // --no-image-responses was passed, disable image responses
143
+ result.noImageResponses = true;
144
+ }
145
+ return result;
97
146
  }
98
147
  async function findFreePort() {
99
148
  return new Promise((resolve, reject) => {
@@ -116,18 +165,17 @@ async function loadConfig(configFile) {
116
165
  }
117
166
  }
118
167
  export async function outputFile(config, name) {
119
- const result = config.outputDir ?? os.tmpdir();
120
- await fs.promises.mkdir(result, { recursive: true });
168
+ await fs.promises.mkdir(config.outputDir, { recursive: true });
121
169
  const fileName = sanitizeForFilePath(name);
122
- return path.join(result, fileName);
170
+ return path.join(config.outputDir, fileName);
123
171
  }
124
172
  function pickDefined(obj) {
125
173
  return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined));
126
174
  }
127
175
  function mergeConfig(base, overrides) {
128
176
  const browser = {
129
- ...pickDefined(base.browser),
130
- ...pickDefined(overrides.browser),
177
+ browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium',
178
+ isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
131
179
  launchOptions: {
132
180
  ...pickDefined(base.browser?.launchOptions),
133
181
  ...pickDefined(overrides.browser?.launchOptions),
@@ -137,6 +185,9 @@ function mergeConfig(base, overrides) {
137
185
  ...pickDefined(base.browser?.contextOptions),
138
186
  ...pickDefined(overrides.browser?.contextOptions),
139
187
  },
188
+ userDataDir: overrides.browser?.userDataDir ?? base.browser?.userDataDir,
189
+ cdpEndpoint: overrides.browser?.cdpEndpoint ?? base.browser?.cdpEndpoint,
190
+ remoteEndpoint: overrides.browser?.remoteEndpoint ?? base.browser?.remoteEndpoint,
140
191
  };
141
192
  if (browser.browserName !== 'chromium' && browser.launchOptions)
142
193
  delete browser.launchOptions.channel;
@@ -147,6 +198,6 @@ function mergeConfig(base, overrides) {
147
198
  network: {
148
199
  ...pickDefined(base.network),
149
200
  ...pickDefined(overrides.network),
150
- },
201
+ }
151
202
  };
152
203
  }
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, screenshotTools } from './tools.js';
20
+ import { snapshotTools, visionTools } from './tools.js';
21
21
  export async function createConnection(config) {
22
- const allTools = config.vision ? screenshotTools : snapshotTools;
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
@@ -18,16 +18,14 @@ 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';
25
25
  export class Context {
26
26
  tools;
27
27
  config;
28
- _browser;
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 context = await this._ensureBrowserContext();
76
+ const { browserContext } = await this._ensureBrowserContext();
79
77
  if (!this._currentTab)
80
- await context.newPage();
78
+ await browserContext.newPage();
81
79
  return this._currentTab;
82
80
  }
83
81
  async listTabsMarkdown() {
@@ -86,7 +84,7 @@ export class Context {
86
84
  const lines = ['### Open tabs'];
87
85
  for (let i = 0; i < this._tabs.length; i++) {
88
86
  const tab = this._tabs[i];
89
- const title = await tab.page.title();
87
+ const title = await tab.title();
90
88
  const url = tab.page.url();
91
89
  const current = tab === this._currentTab ? ' (current)' : '';
92
90
  lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
@@ -118,7 +116,7 @@ export class Context {
118
116
  let actionResult;
119
117
  try {
120
118
  if (waitForNetwork)
121
- actionResult = await waitForCompletion(this, tab.page, async () => racingAction?.()) ?? undefined;
119
+ actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
122
120
  else
123
121
  actionResult = await racingAction?.() ?? undefined;
124
122
  }
@@ -155,7 +153,7 @@ ${code.join('\n')}
155
153
  result.push(await this.listTabsMarkdown(), '');
156
154
  if (this.tabs().length > 1)
157
155
  result.push('### Current tab');
158
- result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.page.title()}`);
156
+ result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
159
157
  if (captureSnapshot && tab.hasSnapshot())
160
158
  result.push(tab.snapshotOrDie().text());
161
159
  const content = actionResult?.content ?? [];
@@ -170,10 +168,13 @@ ${code.join('\n')}
170
168
  };
171
169
  }
172
170
  async waitForTimeout(time) {
173
- if (this._currentTab && !this._javaScriptBlocked())
174
- await this._currentTab.page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
175
- else
171
+ if (!this._currentTab || this._javaScriptBlocked()) {
176
172
  await new Promise(f => setTimeout(f, time));
173
+ return;
174
+ }
175
+ await callOnPageNoTrace(this._currentTab.page, page => {
176
+ return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
177
+ });
177
178
  }
178
179
  async _raceAgainstModalDialogs(action) {
179
180
  this._pendingAction = {
@@ -226,20 +227,21 @@ ${code.join('\n')}
226
227
  this._tabs.splice(index, 1);
227
228
  if (this._currentTab === tab)
228
229
  this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
229
- if (this._browserContext && !this._tabs.length)
230
+ if (!this._tabs.length)
230
231
  void this.close();
231
232
  }
232
233
  async close() {
233
- if (!this._browserContext)
234
+ if (!this._browserContextPromise)
234
235
  return;
235
- const browserContext = this._browserContext;
236
- const browser = this._browser;
237
- this._createBrowserContextPromise = undefined;
238
- this._browserContext = undefined;
239
- this._browser = undefined;
240
- await browserContext?.close().then(async () => {
241
- await browser?.close();
242
- }).catch(() => { });
236
+ const promise = this._browserContextPromise;
237
+ this._browserContextPromise = undefined;
238
+ await promise.then(async ({ browserContext, browser }) => {
239
+ if (this.config.saveTrace)
240
+ await browserContext.tracing.stop();
241
+ await browserContext.close().then(async () => {
242
+ await browser?.close();
243
+ }).catch(() => { });
244
+ });
243
245
  }
244
246
  async _setupRequestInterception(context) {
245
247
  if (this.config.network?.allowedOrigins?.length) {
@@ -252,28 +254,32 @@ ${code.join('\n')}
252
254
  await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
253
255
  }
254
256
  }
255
- async _ensureBrowserContext() {
256
- if (!this._browserContext) {
257
- const context = await this._createBrowserContext();
258
- this._browser = context.browser;
259
- this._browserContext = context.browserContext;
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));
257
+ _ensureBrowserContext() {
258
+ if (!this._browserContextPromise) {
259
+ this._browserContextPromise = this._setupBrowserContext();
260
+ this._browserContextPromise.catch(() => {
261
+ this._browserContextPromise = undefined;
262
+ });
264
263
  }
265
- return this._browserContext;
264
+ return this._browserContextPromise;
266
265
  }
267
- async _createBrowserContext() {
268
- if (!this._createBrowserContextPromise) {
269
- this._createBrowserContextPromise = this._innerCreateBrowserContext();
270
- void this._createBrowserContextPromise.catch(() => {
271
- this._createBrowserContextPromise = undefined;
266
+ async _setupBrowserContext() {
267
+ const { browser, browserContext } = await this._createBrowserContext();
268
+ await this._setupRequestInterception(browserContext);
269
+ for (const page of browserContext.pages())
270
+ this._onPageCreated(page);
271
+ browserContext.on('page', page => this._onPageCreated(page));
272
+ if (this.config.saveTrace) {
273
+ await browserContext.tracing.start({
274
+ name: 'trace',
275
+ screenshots: false,
276
+ snapshots: true,
277
+ sources: false,
272
278
  });
273
279
  }
274
- return this._createBrowserContextPromise;
280
+ return { browser, browserContext };
275
281
  }
276
- async _innerCreateBrowserContext() {
282
+ async _createBrowserContext() {
277
283
  if (this.config.browser?.remoteEndpoint) {
278
284
  const url = new URL(this.config.browser?.remoteEndpoint);
279
285
  if (this.config.browser.browserName)
@@ -286,19 +292,35 @@ ${code.join('\n')}
286
292
  }
287
293
  if (this.config.browser?.cdpEndpoint) {
288
294
  const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
289
- const browserContext = browser.contexts()[0];
295
+ const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
290
296
  return { browser, browserContext };
291
297
  }
292
- const browserContext = await launchPersistentContext(this.config.browser);
293
- return { browserContext };
298
+ return this.config.browser?.isolated ?
299
+ await createIsolatedContext(this.config.browser) :
300
+ await launchPersistentContext(this.config.browser);
294
301
  }
295
302
  }
296
- async function launchPersistentContext(browserConfig) {
303
+ async function createIsolatedContext(browserConfig) {
297
304
  try {
298
305
  const browserName = browserConfig?.browserName ?? 'chromium';
299
- const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
300
306
  const browserType = playwright[browserName];
301
- return await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
307
+ const browser = await browserType.launch(browserConfig.launchOptions);
308
+ const browserContext = await browser.newContext(browserConfig.contextOptions);
309
+ return { browser, browserContext };
310
+ }
311
+ catch (error) {
312
+ if (error.message.includes('Executable doesn\'t exist'))
313
+ throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
314
+ throw error;
315
+ }
316
+ }
317
+ async function launchPersistentContext(browserConfig) {
318
+ try {
319
+ const browserName = browserConfig.browserName ?? 'chromium';
320
+ const userDataDir = browserConfig.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
321
+ const browserType = playwright[browserName];
322
+ const browserContext = await browserType.launchPersistentContext(userDataDir, { ...browserConfig.launchOptions, ...browserConfig.contextOptions });
323
+ return { browserContext };
302
324
  }
303
325
  catch (error) {
304
326
  if (error.message.includes('Executable doesn\'t exist'))
@@ -316,12 +338,9 @@ async function createUserDataDir(browserConfig) {
316
338
  cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
317
339
  else
318
340
  throw new Error('Unsupported platform: ' + process.platform);
319
- const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
341
+ const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
320
342
  await fs.promises.mkdir(result, { recursive: true });
321
343
  return result;
322
344
  }
323
- export async function generateLocator(locator) {
324
- return locator._generateLocatorString();
325
- }
326
345
  const __filename = url.fileURLToPath(import.meta.url);
327
346
  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
- export async function createConnection(config = {}) {
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
  }
@@ -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,11 +29,14 @@ export class PageSnapshot {
28
29
  return this._text;
29
30
  }
30
31
  async _build() {
31
- const yamlDocument = await this._page._snapshotForAI();
32
+ // FIXME: Rountrip evaluate to ensure _snapshotForAI works.
33
+ // This probably broke once we moved off locator snapshots
34
+ await this._page.evaluate(() => 1);
35
+ const snapshot = await callOnPageNoTrace(this._page, page => page._snapshotForAI());
32
36
  this._text = [
33
37
  `- Page Snapshot`,
34
38
  '```yaml',
35
- yamlDocument.toString({ indentSeq: false }).trim(),
39
+ snapshot,
36
40
  '```',
37
41
  ].join('\n');
38
42
  }
package/lib/program.js CHANGED
@@ -15,34 +15,53 @@
15
15
  */
16
16
  import { program } from 'commander';
17
17
  import { startHttpTransport, startStdioTransport } from './transport.js';
18
- import { resolveConfig } from './config.js';
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)
22
24
  .name(packageJSON.name)
23
- .option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
24
- .option('--caps <caps>', 'Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
25
+ .option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
26
+ .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)
27
+ .option('--block-service-workers', 'block service workers')
28
+ .option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
29
+ .option('--caps <caps>', 'comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
25
30
  .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
26
- .option('--executable-path <path>', 'Path to the browser executable.')
27
- .option('--headless', 'Run browser in headless mode, headed by default')
28
- .option('--device <device>', 'Device to emulate, for example: "iPhone 15"')
29
- .option('--user-data-dir <path>', 'Path to the user data directory')
30
- .option('--port <port>', 'Port to listen on for SSE transport.')
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)
31
+ .option('--config <path>', 'path to the configuration file.')
32
+ .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
33
+ .option('--executable-path <path>', 'path to the browser executable.')
34
+ .option('--headless', 'run browser in headless mode, headed by default')
35
+ .option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
36
+ .option('--ignore-https-errors', 'ignore https errors')
37
+ .option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
38
+ .option('--no-image-responses', 'do not send image responses to the client.')
39
+ .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
40
+ .option('--output-dir <path>', 'path to the directory for output files.')
41
+ .option('--port <port>', 'port to listen on for SSE transport.')
42
+ .option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
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.')
45
+ .option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
46
+ .option('--user-agent <ua string>', 'specify user agent string')
47
+ .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
48
+ .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
34
49
  .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
50
  .action(async (options) => {
39
- const config = await resolveConfig(options);
51
+ const config = await resolveCLIConfig(options);
40
52
  const connectionList = [];
41
53
  setupExitWatchdog(connectionList);
42
54
  if (options.port)
43
55
  startHttpTransport(config, +options.port, options.host, connectionList);
44
56
  else
45
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
+ }
46
65
  });
47
66
  function setupExitWatchdog(connectionList) {
48
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.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
82
+ await this.waitForLoadState('load', { timeout: 5000 });
76
83
  }
77
84
  hasSnapshot() {
78
85
  return !!this._snapshot;
@@ -15,45 +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_for',
22
- title: 'Wait for',
23
- description: 'Wait for text to appear or disappear or a specified time to pass',
24
- inputSchema: z.object({
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'),
28
- }),
29
- type: 'readOnly',
30
- },
31
- handle: async (context, params) => {
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
- }
50
- return {
51
- code,
52
- captureSnapshot,
53
- waitForNetwork: false,
54
- };
55
- },
56
- });
57
18
  const close = defineTool({
58
19
  capability: 'core',
59
20
  schema: {
@@ -66,7 +27,7 @@ const close = defineTool({
66
27
  handle: async (context) => {
67
28
  await context.close();
68
29
  return {
69
- code: [`// Internal to close the page`],
30
+ code: [`await page.close()`],
70
31
  captureSnapshot: false,
71
32
  waitForNetwork: false,
72
33
  };
@@ -103,6 +64,5 @@ const resize = captureSnapshot => defineTool({
103
64
  });
104
65
  export default (captureSnapshot) => [
105
66
  close,
106
- wait(captureSnapshot),
107
67
  resize(captureSnapshot)
108
68
  ];