@playwright/mcp 0.0.20 → 0.0.24

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/lib/context.js CHANGED
@@ -13,6 +13,10 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import fs from 'node:fs';
17
+ import url from 'node:url';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
16
20
  import * as playwright from 'playwright';
17
21
  import { waitForCompletion } from './tools/utils.js';
18
22
  import { ManualPromise } from './manualPromise.js';
@@ -96,7 +100,7 @@ export class Context {
96
100
  }
97
101
  async run(tool, params) {
98
102
  // Tab management is done outside of the action() call.
99
- const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
103
+ const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
100
104
  const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
101
105
  const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
102
106
  if (resultOverride)
@@ -237,11 +241,23 @@ ${code.join('\n')}
237
241
  await browser?.close();
238
242
  }).catch(() => { });
239
243
  }
244
+ async _setupRequestInterception(context) {
245
+ if (this.config.network?.allowedOrigins?.length) {
246
+ await context.route('**', route => route.abort('blockedbyclient'));
247
+ for (const origin of this.config.network.allowedOrigins)
248
+ await context.route(`*://${origin}/**`, route => route.continue());
249
+ }
250
+ if (this.config.network?.blockedOrigins?.length) {
251
+ for (const origin of this.config.network.blockedOrigins)
252
+ await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
253
+ }
254
+ }
240
255
  async _ensureBrowserContext() {
241
256
  if (!this._browserContext) {
242
257
  const context = await this._createBrowserContext();
243
258
  this._browser = context.browser;
244
259
  this._browserContext = context.browserContext;
260
+ await this._setupRequestInterception(this._browserContext);
245
261
  for (const page of this._browserContext.pages())
246
262
  this._onPageCreated(page);
247
263
  this._browserContext.on('page', page => this._onPageCreated(page));
@@ -279,8 +295,10 @@ ${code.join('\n')}
279
295
  }
280
296
  async function launchPersistentContext(browserConfig) {
281
297
  try {
282
- const browserType = browserConfig?.browserName ? playwright[browserConfig.browserName] : playwright.chromium;
283
- return await browserType.launchPersistentContext(browserConfig?.userDataDir || '', { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
298
+ const browserName = browserConfig?.browserName ?? 'chromium';
299
+ const userDataDir = browserConfig?.userDataDir ?? await createUserDataDir({ ...browserConfig, browserName });
300
+ const browserType = playwright[browserName];
301
+ return await browserType.launchPersistentContext(userDataDir, { ...browserConfig?.launchOptions, ...browserConfig?.contextOptions });
284
302
  }
285
303
  catch (error) {
286
304
  if (error.message.includes('Executable doesn\'t exist'))
@@ -288,6 +306,22 @@ async function launchPersistentContext(browserConfig) {
288
306
  throw error;
289
307
  }
290
308
  }
309
+ async function createUserDataDir(browserConfig) {
310
+ let cacheDirectory;
311
+ if (process.platform === 'linux')
312
+ cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
313
+ else if (process.platform === 'darwin')
314
+ cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
315
+ else if (process.platform === 'win32')
316
+ cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
317
+ else
318
+ throw new Error('Unsupported platform: ' + process.platform);
319
+ const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
320
+ await fs.promises.mkdir(result, { recursive: true });
321
+ return result;
322
+ }
291
323
  export async function generateLocator(locator) {
292
324
  return locator._generateLocatorString();
293
325
  }
326
+ const __filename = url.fileURLToPath(import.meta.url);
327
+ export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
package/lib/index.js CHANGED
@@ -13,55 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { createServerWithTools } from './server.js';
17
- import common from './tools/common.js';
18
- import console from './tools/console.js';
19
- import dialogs from './tools/dialogs.js';
20
- import files from './tools/files.js';
21
- import install from './tools/install.js';
22
- import keyboard from './tools/keyboard.js';
23
- import navigate from './tools/navigate.js';
24
- import network from './tools/network.js';
25
- import pdf from './tools/pdf.js';
26
- import snapshot from './tools/snapshot.js';
27
- import tabs from './tools/tabs.js';
28
- import screen from './tools/screen.js';
29
- import testing from './tools/testing.js';
30
- const snapshotTools = [
31
- ...common(true),
32
- ...console,
33
- ...dialogs(true),
34
- ...files(true),
35
- ...install,
36
- ...keyboard(true),
37
- ...navigate(true),
38
- ...network,
39
- ...pdf,
40
- ...snapshot,
41
- ...tabs(true),
42
- ...testing,
43
- ];
44
- const screenshotTools = [
45
- ...common(false),
46
- ...console,
47
- ...dialogs(false),
48
- ...files(false),
49
- ...install,
50
- ...keyboard(false),
51
- ...navigate(false),
52
- ...network,
53
- ...pdf,
54
- ...screen,
55
- ...tabs(false),
56
- ...testing,
57
- ];
58
- import packageJSON from '../package.json' with { type: 'json' };
59
- export async function createServer(config = {}) {
60
- const allTools = config.vision ? screenshotTools : snapshotTools;
61
- const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability));
62
- return createServerWithTools({
63
- name: 'Playwright',
64
- version: packageJSON.version,
65
- tools,
66
- }, config);
16
+ import { createConnection as createConnectionImpl } from './connection.js';
17
+ export async function createConnection(config = {}) {
18
+ return createConnectionImpl(config);
67
19
  }
@@ -13,22 +13,22 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import yaml from 'yaml';
17
16
  export class PageSnapshot {
18
- _frameLocators = [];
17
+ _page;
19
18
  _text;
20
- constructor() {
19
+ constructor(page) {
20
+ this._page = page;
21
21
  }
22
22
  static async create(page) {
23
- const snapshot = new PageSnapshot();
24
- await snapshot._build(page);
23
+ const snapshot = new PageSnapshot(page);
24
+ await snapshot._build();
25
25
  return snapshot;
26
26
  }
27
27
  text() {
28
28
  return this._text;
29
29
  }
30
- async _build(page) {
31
- const yamlDocument = await this._snapshotFrame(page);
30
+ async _build() {
31
+ const yamlDocument = await this._page._snapshotForAI();
32
32
  this._text = [
33
33
  `- Page Snapshot`,
34
34
  '```yaml',
@@ -36,54 +36,7 @@ export class PageSnapshot {
36
36
  '```',
37
37
  ].join('\n');
38
38
  }
39
- async _snapshotFrame(frame) {
40
- const frameIndex = this._frameLocators.push(frame) - 1;
41
- const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
42
- const snapshot = yaml.parseDocument(snapshotString);
43
- const visit = async (node) => {
44
- if (yaml.isPair(node)) {
45
- await Promise.all([
46
- visit(node.key).then(k => node.key = k),
47
- visit(node.value).then(v => node.value = v)
48
- ]);
49
- }
50
- else if (yaml.isSeq(node) || yaml.isMap(node)) {
51
- node.items = await Promise.all(node.items.map(visit));
52
- }
53
- else if (yaml.isScalar(node)) {
54
- if (typeof node.value === 'string') {
55
- const value = node.value;
56
- if (frameIndex > 0)
57
- node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
58
- if (value.startsWith('iframe ')) {
59
- const ref = value.match(/\[ref=(.*)\]/)?.[1];
60
- if (ref) {
61
- try {
62
- const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
63
- return snapshot.createPair(node.value, childSnapshot);
64
- }
65
- catch (error) {
66
- return snapshot.createPair(node.value, '<could not take iframe snapshot>');
67
- }
68
- }
69
- }
70
- }
71
- }
72
- return node;
73
- };
74
- await visit(snapshot.contents);
75
- return snapshot;
76
- }
77
39
  refLocator(ref) {
78
- let frame = this._frameLocators[0];
79
- const match = ref.match(/^f(\d+)(.*)/);
80
- if (match) {
81
- const frameIndex = parseInt(match[1], 10);
82
- frame = this._frameLocators[frameIndex];
83
- ref = match[2];
84
- }
85
- if (!frame)
86
- throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
87
- return frame.locator(`aria-ref=${ref}`);
40
+ return this._page.locator(`aria-ref=${ref}`);
88
41
  }
89
42
  }
package/lib/program.js CHANGED
@@ -14,11 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { program } from 'commander';
17
- import { createServer } from './index.js';
18
- import { ServerList } from './server.js';
19
17
  import { startHttpTransport, startStdioTransport } from './transport.js';
20
18
  import { resolveConfig } from './config.js';
21
- import packageJSON from '../package.json' with { type: 'json' };
19
+ import { packageJSON } from './context.js';
22
20
  program
23
21
  .version('Version ' + packageJSON.version)
24
22
  .name(packageJSON.name)
@@ -31,25 +29,33 @@ program
31
29
  .option('--user-data-dir <path>', 'Path to the user data directory')
32
30
  .option('--port <port>', 'Port to listen on for SSE transport.')
33
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)
34
34
  .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.')
35
37
  .option('--config <path>', 'Path to the configuration file.')
36
38
  .action(async (options) => {
37
39
  const config = await resolveConfig(options);
38
- const serverList = new ServerList(() => createServer(config));
39
- setupExitWatchdog(serverList);
40
+ const connectionList = [];
41
+ setupExitWatchdog(connectionList);
40
42
  if (options.port)
41
- startHttpTransport(+options.port, options.host, serverList);
43
+ startHttpTransport(config, +options.port, options.host, connectionList);
42
44
  else
43
- await startStdioTransport(serverList);
45
+ await startStdioTransport(config, connectionList);
44
46
  });
45
- function setupExitWatchdog(serverList) {
47
+ function setupExitWatchdog(connectionList) {
46
48
  const handleExit = async () => {
47
49
  setTimeout(() => process.exit(0), 15000);
48
- await serverList.closeAll();
50
+ for (const connection of connectionList)
51
+ await connection.close();
49
52
  process.exit(0);
50
53
  };
51
54
  process.stdin.on('close', handleExit);
52
55
  process.on('SIGINT', handleExit);
53
56
  process.on('SIGTERM', handleExit);
54
57
  }
58
+ function semicolonSeparatedList(value) {
59
+ return value.split(';').map(v => v.trim());
60
+ }
55
61
  program.parse(process.argv);
package/lib/tab.js CHANGED
@@ -17,7 +17,7 @@ import { PageSnapshot } from './pageSnapshot.js';
17
17
  export class Tab {
18
18
  context;
19
19
  page;
20
- _console = [];
20
+ _consoleMessages = [];
21
21
  _requests = new Map();
22
22
  _snapshot;
23
23
  _onPageClose;
@@ -25,13 +25,9 @@ export class Tab {
25
25
  this.context = context;
26
26
  this.page = page;
27
27
  this._onPageClose = onPageClose;
28
- page.on('console', event => this._console.push(event));
28
+ page.on('console', event => this._consoleMessages.push(event));
29
29
  page.on('request', request => this._requests.set(request, null));
30
30
  page.on('response', response => this._requests.set(response.request(), response));
31
- page.on('framenavigated', frame => {
32
- if (!frame.parentFrame())
33
- this._clearCollectedArtifacts();
34
- });
35
31
  page.on('close', () => this._onClose());
36
32
  page.on('filechooser', chooser => {
37
33
  this.context.setModalState({
@@ -48,7 +44,7 @@ export class Tab {
48
44
  page.setDefaultTimeout(5000);
49
45
  }
50
46
  _clearCollectedArtifacts() {
51
- this._console.length = 0;
47
+ this._consoleMessages.length = 0;
52
48
  this._requests.clear();
53
49
  }
54
50
  _onClose() {
@@ -56,7 +52,25 @@ export class Tab {
56
52
  this._onPageClose(this);
57
53
  }
58
54
  async navigate(url) {
59
- await this.page.goto(url, { waitUntil: 'domcontentloaded' });
55
+ this._clearCollectedArtifacts();
56
+ const downloadEvent = this.page.waitForEvent('download').catch(() => { });
57
+ try {
58
+ await this.page.goto(url, { waitUntil: 'domcontentloaded' });
59
+ }
60
+ catch (_e) {
61
+ const e = _e;
62
+ const mightBeDownload = e.message.includes('net::ERR_ABORTED') // chromium
63
+ || e.message.includes('Download is starting'); // firefox + webkit
64
+ if (!mightBeDownload)
65
+ throw e;
66
+ // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
67
+ const download = await Promise.race([
68
+ downloadEvent,
69
+ new Promise(resolve => setTimeout(resolve, 500)),
70
+ ]);
71
+ if (!download)
72
+ throw e;
73
+ }
60
74
  // Cap load event to 5 seconds, the page is operational at this point.
61
75
  await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => { });
62
76
  }
@@ -68,8 +82,8 @@ export class Tab {
68
82
  throw new Error('No snapshot available');
69
83
  return this._snapshot;
70
84
  }
71
- console() {
72
- return this._console;
85
+ consoleMessages() {
86
+ return this._consoleMessages;
73
87
  }
74
88
  requests() {
75
89
  return this._requests;
@@ -18,16 +18,37 @@ import { defineTool } from './tool.js';
18
18
  const wait = captureSnapshot => defineTool({
19
19
  capability: 'wait',
20
20
  schema: {
21
- name: 'browser_wait',
22
- description: 'Wait for a specified time in seconds',
21
+ name: 'browser_wait_for',
22
+ title: 'Wait for',
23
+ description: 'Wait for text to appear or disappear or a specified time to pass',
23
24
  inputSchema: z.object({
24
- time: z.number().describe('The time to wait in seconds'),
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'),
25
28
  }),
29
+ type: 'readOnly',
26
30
  },
27
31
  handle: async (context, params) => {
28
- await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
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
+ }
29
50
  return {
30
- code: [`// Waited for ${params.time} seconds`],
51
+ code,
31
52
  captureSnapshot,
32
53
  waitForNetwork: false,
33
54
  };
@@ -37,8 +58,10 @@ const close = defineTool({
37
58
  capability: 'core',
38
59
  schema: {
39
60
  name: 'browser_close',
61
+ title: 'Close browser',
40
62
  description: 'Close the page',
41
63
  inputSchema: z.object({}),
64
+ type: 'readOnly',
42
65
  },
43
66
  handle: async (context) => {
44
67
  await context.close();
@@ -53,11 +76,13 @@ const resize = captureSnapshot => defineTool({
53
76
  capability: 'core',
54
77
  schema: {
55
78
  name: 'browser_resize',
79
+ title: 'Resize browser window',
56
80
  description: 'Resize the browser window',
57
81
  inputSchema: z.object({
58
82
  width: z.number().describe('Width of the browser window'),
59
83
  height: z.number().describe('Height of the browser window'),
60
84
  }),
85
+ type: 'readOnly',
61
86
  },
62
87
  handle: async (context, params) => {
63
88
  const tab = context.currentTabOrDie();
@@ -19,11 +19,13 @@ const console = defineTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_console_messages',
22
+ title: 'Get console messages',
22
23
  description: 'Returns all console messages',
23
24
  inputSchema: z.object({}),
25
+ type: 'readOnly',
24
26
  },
25
27
  handle: async (context) => {
26
- const messages = context.currentTabOrDie().console();
28
+ const messages = context.currentTabOrDie().consoleMessages();
27
29
  const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
28
30
  return {
29
31
  code: [`// <internal code to get console messages>`],
@@ -19,11 +19,13 @@ const handleDialog = captureSnapshot => defineTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_handle_dialog',
22
+ title: 'Handle a dialog',
22
23
  description: 'Handle a dialog',
23
24
  inputSchema: z.object({
24
25
  accept: z.boolean().describe('Whether to accept the dialog.'),
25
26
  promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
26
27
  }),
28
+ type: 'destructive',
27
29
  },
28
30
  handle: async (context, params) => {
29
31
  const dialogState = context.modalStates().find(state => state.type === 'dialog');
@@ -19,10 +19,12 @@ const uploadFile = captureSnapshot => defineTool({
19
19
  capability: 'files',
20
20
  schema: {
21
21
  name: 'browser_file_upload',
22
+ title: 'Upload files',
22
23
  description: 'Upload one or multiple files',
23
24
  inputSchema: z.object({
24
25
  paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
25
26
  }),
27
+ type: 'destructive',
26
28
  },
27
29
  handle: async (context, params) => {
28
30
  const modalState = context.modalStates().find(state => state.type === 'fileChooser');
@@ -17,19 +17,21 @@ import { fork } from 'child_process';
17
17
  import path from 'path';
18
18
  import { z } from 'zod';
19
19
  import { defineTool } from './tool.js';
20
- import { createRequire } from 'node:module';
21
- const require = createRequire(import.meta.url);
20
+ import { fileURLToPath } from 'node:url';
22
21
  const install = defineTool({
23
22
  capability: 'install',
24
23
  schema: {
25
24
  name: 'browser_install',
25
+ title: 'Install the browser specified in the config',
26
26
  description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
27
27
  inputSchema: z.object({}),
28
+ type: 'destructive',
28
29
  },
29
30
  handle: async (context) => {
30
- const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
31
- const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
32
- const child = fork(cli, ['install', channel], {
31
+ const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
32
+ const cliUrl = import.meta.resolve('playwright/package.json');
33
+ const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
34
+ const child = fork(cliPath, ['install', channel], {
33
35
  stdio: 'pipe',
34
36
  });
35
37
  const output = [];
@@ -19,10 +19,12 @@ const pressKey = captureSnapshot => defineTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_press_key',
22
+ title: 'Press a key',
22
23
  description: 'Press a key on the keyboard',
23
24
  inputSchema: z.object({
24
25
  key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
25
26
  }),
27
+ type: 'destructive',
26
28
  },
27
29
  handle: async (context, params) => {
28
30
  const tab = context.currentTabOrDie();
@@ -19,10 +19,12 @@ const navigate = captureSnapshot => defineTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_navigate',
22
+ title: 'Navigate to a URL',
22
23
  description: 'Navigate to a URL',
23
24
  inputSchema: z.object({
24
25
  url: z.string().describe('The URL to navigate to'),
25
26
  }),
27
+ type: 'destructive',
26
28
  },
27
29
  handle: async (context, params) => {
28
30
  const tab = await context.ensureTab();
@@ -42,8 +44,10 @@ const goBack = captureSnapshot => defineTool({
42
44
  capability: 'history',
43
45
  schema: {
44
46
  name: 'browser_navigate_back',
47
+ title: 'Go back',
45
48
  description: 'Go back to the previous page',
46
49
  inputSchema: z.object({}),
50
+ type: 'readOnly',
47
51
  },
48
52
  handle: async (context) => {
49
53
  const tab = await context.ensureTab();
@@ -63,8 +67,10 @@ const goForward = captureSnapshot => defineTool({
63
67
  capability: 'history',
64
68
  schema: {
65
69
  name: 'browser_navigate_forward',
70
+ title: 'Go forward',
66
71
  description: 'Go forward to the next page',
67
72
  inputSchema: z.object({}),
73
+ type: 'readOnly',
68
74
  },
69
75
  handle: async (context) => {
70
76
  const tab = context.currentTabOrDie();
@@ -19,8 +19,10 @@ const requests = defineTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_network_requests',
22
+ title: 'List network requests',
22
23
  description: 'Returns all network requests since loading the page',
23
24
  inputSchema: z.object({}),
25
+ type: 'readOnly',
24
26
  },
25
27
  handle: async (context) => {
26
28
  const requests = context.currentTabOrDie().requests();
package/lib/tools/pdf.js CHANGED
@@ -17,16 +17,21 @@ import { z } from 'zod';
17
17
  import { defineTool } from './tool.js';
18
18
  import * as javascript from '../javascript.js';
19
19
  import { outputFile } from '../config.js';
20
+ const pdfSchema = z.object({
21
+ filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
22
+ });
20
23
  const pdf = defineTool({
21
24
  capability: 'pdf',
22
25
  schema: {
23
26
  name: 'browser_pdf_save',
27
+ title: 'Save as PDF',
24
28
  description: 'Save page as PDF',
25
- inputSchema: z.object({}),
29
+ inputSchema: pdfSchema,
30
+ type: 'readOnly',
26
31
  },
27
- handle: async (context) => {
32
+ handle: async (context, params) => {
28
33
  const tab = context.currentTabOrDie();
29
- const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
34
+ const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
30
35
  const code = [
31
36
  `// Save page as ${fileName}`,
32
37
  `await page.pdf(${javascript.formatObject({ path: fileName })});`,
@@ -23,8 +23,10 @@ const screenshot = defineTool({
23
23
  capability: 'core',
24
24
  schema: {
25
25
  name: 'browser_screen_capture',
26
+ title: 'Take a screenshot',
26
27
  description: 'Take a screenshot of the current page',
27
28
  inputSchema: z.object({}),
29
+ type: 'readOnly',
28
30
  },
29
31
  handle: async (context) => {
30
32
  const tab = await context.ensureTab();
@@ -50,11 +52,13 @@ const moveMouse = defineTool({
50
52
  capability: 'core',
51
53
  schema: {
52
54
  name: 'browser_screen_move_mouse',
55
+ title: 'Move mouse',
53
56
  description: 'Move mouse to a given position',
54
57
  inputSchema: elementSchema.extend({
55
58
  x: z.number().describe('X coordinate'),
56
59
  y: z.number().describe('Y coordinate'),
57
60
  }),
61
+ type: 'readOnly',
58
62
  },
59
63
  handle: async (context, params) => {
60
64
  const tab = context.currentTabOrDie();
@@ -75,11 +79,13 @@ const click = defineTool({
75
79
  capability: 'core',
76
80
  schema: {
77
81
  name: 'browser_screen_click',
82
+ title: 'Click',
78
83
  description: 'Click left mouse button',
79
84
  inputSchema: elementSchema.extend({
80
85
  x: z.number().describe('X coordinate'),
81
86
  y: z.number().describe('Y coordinate'),
82
87
  }),
88
+ type: 'destructive',
83
89
  },
84
90
  handle: async (context, params) => {
85
91
  const tab = context.currentTabOrDie();
@@ -106,6 +112,7 @@ const drag = defineTool({
106
112
  capability: 'core',
107
113
  schema: {
108
114
  name: 'browser_screen_drag',
115
+ title: 'Drag mouse',
109
116
  description: 'Drag left mouse button',
110
117
  inputSchema: elementSchema.extend({
111
118
  startX: z.number().describe('Start X coordinate'),
@@ -113,6 +120,7 @@ const drag = defineTool({
113
120
  endX: z.number().describe('End X coordinate'),
114
121
  endY: z.number().describe('End Y coordinate'),
115
122
  }),
123
+ type: 'destructive',
116
124
  },
117
125
  handle: async (context, params) => {
118
126
  const tab = context.currentTabOrDie();
@@ -141,11 +149,13 @@ const type = defineTool({
141
149
  capability: 'core',
142
150
  schema: {
143
151
  name: 'browser_screen_type',
152
+ title: 'Type text',
144
153
  description: 'Type text',
145
154
  inputSchema: z.object({
146
155
  text: z.string().describe('Text to type into the element'),
147
156
  submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
148
157
  }),
158
+ type: 'destructive',
149
159
  },
150
160
  handle: async (context, params) => {
151
161
  const tab = context.currentTabOrDie();