@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/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' | 'pdf' | 'history' | 'wait' | 'files' | 'install' | 'testing';
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
- * - 'history': Browser history access.
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' | 'auto';
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 { userDataDir } from './fileUtils.js';
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 cliOverrides = await configFromCLIOptions(cliOptions);
46
- const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
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 async function configFromCLIOptions(cliOptions) {
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?.split(',').map((c) => c.trim()),
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 { snapshotTools, visionTools } from './tools.js';
20
+ import { allTools } from './tools.js';
21
21
  import { packageJSON } from './package.js';
22
22
  export function createConnection(config, browserContextFactory) {
23
- const allTools = config.vision ? visionTools : snapshotTools;
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 !this.clientVersion?.name.includes('cursor');
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 - 1];
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 + 1}:${current} [${title}] (${url})`);
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 - 1];
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
- let actionResult;
125
- try {
126
- if (waitForNetwork)
127
- actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
128
- else
129
- actionResult = await racingAction?.() ?? undefined;
130
- }
131
- finally {
132
- if (captureSnapshot && !this._javaScriptBlocked())
133
- await tab.captureSnapshot();
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(`- Ran Playwright code:
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 (this.tabs().length > 1)
161
- result.push(await this.listTabsMarkdown(), '');
162
- if (this.tabs().length > 1)
163
- result.push('### Current tab');
164
- result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
165
- if (captureSnapshot && tab.hasSnapshot())
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
- export {};
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');
@@ -31,7 +31,7 @@ export class PageSnapshot {
31
31
  async _build() {
32
32
  const snapshot = await callOnPageNoTrace(this._page, page => page._snapshotForAI());
33
33
  this._text = [
34
- `- Page Snapshot`,
34
+ `- Page Snapshot:`,
35
35
  '```yaml',
36
36
  snapshot,
37
37
  '```',
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('--browser-agent <endpoint>', 'Use browser agent (experimental).')
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", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.')
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
- .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
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 (config.server.port !== undefined)
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._consoleMessages.push(event));
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
  }
@@ -33,7 +33,7 @@ const close = defineTool({
33
33
  };
34
34
  },
35
35
  });
36
- const resize = captureSnapshot => defineTool({
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 (captureSnapshot) => [
65
+ export default [
66
66
  close,
67
- resize(captureSnapshot)
67
+ resize
68
68
  ];
@@ -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 => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
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 () => {
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTool } from './tool.js';
18
- const handleDialog = captureSnapshot => defineTool({
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 (captureSnapshot) => [
51
- handleDialog(captureSnapshot),
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
+ ];