@playwright/mcp 0.0.32 → 0.0.33

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/tab.js CHANGED
@@ -17,13 +17,13 @@ import { EventEmitter } from 'events';
17
17
  import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
18
  import { logUnhandledError } from './log.js';
19
19
  import { ManualPromise } from './manualPromise.js';
20
- import { outputFile } from './config.js';
21
20
  export const TabEvents = {
22
21
  modalState: 'modalState'
23
22
  };
24
23
  export class Tab extends EventEmitter {
25
24
  context;
26
25
  page;
26
+ _lastTitle = 'about:blank';
27
27
  _consoleMessages = [];
28
28
  _recentConsoleMessages = [];
29
29
  _requests = new Map();
@@ -53,6 +53,10 @@ export class Tab extends EventEmitter {
53
53
  });
54
54
  page.setDefaultNavigationTimeout(60000);
55
55
  page.setDefaultTimeout(5000);
56
+ page[tabSymbol] = this;
57
+ }
58
+ static forPage(page) {
59
+ return page[tabSymbol];
56
60
  }
57
61
  modalStates() {
58
62
  return this._modalStates;
@@ -65,14 +69,7 @@ export class Tab extends EventEmitter {
65
69
  this._modalStates = this._modalStates.filter(state => state !== modalState);
66
70
  }
67
71
  modalStatesMarkdown() {
68
- const result = ['### Modal state'];
69
- if (this._modalStates.length === 0)
70
- result.push('- There is no modal state present');
71
- for (const state of this._modalStates) {
72
- const tool = this.context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
73
- result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
74
- }
75
- return result;
72
+ return renderModalStates(this.context, this.modalStates());
76
73
  }
77
74
  _dialogShown(dialog) {
78
75
  this.setModalState({
@@ -85,7 +82,7 @@ export class Tab extends EventEmitter {
85
82
  const entry = {
86
83
  download,
87
84
  finished: false,
88
- outputFile: await outputFile(this.context.config, download.suggestedFilename())
85
+ outputFile: await this.context.outputFile(download.suggestedFilename())
89
86
  };
90
87
  this._downloads.push(entry);
91
88
  await download.saveAs(entry.outputFile);
@@ -104,8 +101,16 @@ export class Tab extends EventEmitter {
104
101
  this._clearCollectedArtifacts();
105
102
  this._onPageClose(this);
106
103
  }
107
- async title() {
108
- return await callOnPageNoTrace(this.page, page => page.title());
104
+ async updateTitle() {
105
+ await this._raceAgainstModalStates(async () => {
106
+ this._lastTitle = await callOnPageNoTrace(this.page, page => page.title());
107
+ });
108
+ }
109
+ lastTitle() {
110
+ return this._lastTitle;
111
+ }
112
+ isCurrentTab() {
113
+ return this === this.context.currentTab();
109
114
  }
110
115
  async waitForLoadState(state, options) {
111
116
  await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError));
@@ -142,54 +147,46 @@ export class Tab extends EventEmitter {
142
147
  requests() {
143
148
  return this._requests;
144
149
  }
145
- _takeRecentConsoleMarkdown() {
146
- if (!this._recentConsoleMessages.length)
147
- return [];
148
- const result = this._recentConsoleMessages.map(message => {
149
- return `- ${trim(message.toString(), 100)}`;
150
- });
151
- return [`### New console messages`, ...result, ''];
152
- }
153
- _listDownloadsMarkdown() {
154
- if (!this._downloads.length)
155
- return [];
156
- const result = ['### Downloads'];
157
- for (const entry of this._downloads) {
158
- if (entry.finished)
159
- result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
160
- else
161
- result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
162
- }
163
- result.push('');
164
- return result;
165
- }
166
150
  async captureSnapshot() {
167
- const result = [];
168
- if (this.modalStates().length) {
169
- result.push(...this.modalStatesMarkdown());
170
- return result.join('\n');
171
- }
172
- result.push(...this._takeRecentConsoleMarkdown());
173
- result.push(...this._listDownloadsMarkdown());
174
- await this._raceAgainstModalStates(async () => {
151
+ let tabSnapshot;
152
+ const modalStates = await this._raceAgainstModalStates(async () => {
175
153
  const snapshot = await this.page._snapshotForAI();
176
- result.push(`### Page state`, `- Page URL: ${this.page.url()}`, `- Page Title: ${await this.page.title()}`, `- Page Snapshot:`, '```yaml', snapshot, '```');
154
+ tabSnapshot = {
155
+ url: this.page.url(),
156
+ title: await this.page.title(),
157
+ ariaSnapshot: snapshot,
158
+ modalStates: [],
159
+ consoleMessages: [],
160
+ downloads: this._downloads,
161
+ };
177
162
  });
178
- return result.join('\n');
163
+ if (tabSnapshot) {
164
+ // Assign console message late so that we did not lose any to modal state.
165
+ tabSnapshot.consoleMessages = this._recentConsoleMessages;
166
+ this._recentConsoleMessages = [];
167
+ }
168
+ return tabSnapshot ?? {
169
+ url: this.page.url(),
170
+ title: '',
171
+ ariaSnapshot: '',
172
+ modalStates,
173
+ consoleMessages: [],
174
+ downloads: [],
175
+ };
179
176
  }
180
177
  _javaScriptBlocked() {
181
178
  return this._modalStates.some(state => state.type === 'dialog');
182
179
  }
183
180
  async _raceAgainstModalStates(action) {
184
181
  if (this.modalStates().length)
185
- return this.modalStates()[0];
182
+ return this.modalStates();
186
183
  const promise = new ManualPromise();
187
- const listener = (modalState) => promise.resolve(modalState);
184
+ const listener = (modalState) => promise.resolve([modalState]);
188
185
  this.once(TabEvents.modalState, listener);
189
186
  return await Promise.race([
190
187
  action().then(() => {
191
188
  this.off(TabEvents.modalState, listener);
192
- return undefined;
189
+ return [];
193
190
  }),
194
191
  promise,
195
192
  ]);
@@ -239,8 +236,14 @@ function pageErrorToConsoleMessage(errorOrValue) {
239
236
  toString: () => String(errorOrValue),
240
237
  };
241
238
  }
242
- function trim(text, maxLength) {
243
- if (text.length <= maxLength)
244
- return text;
245
- return text.slice(0, maxLength) + '...';
239
+ export function renderModalStates(context, modalStates) {
240
+ const result = ['### Modal state'];
241
+ if (modalStates.length === 0)
242
+ result.push('- There is no modal state present');
243
+ for (const state of modalStates) {
244
+ const tool = context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
245
+ result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
246
+ }
247
+ return result;
246
248
  }
249
+ const tabSymbol = Symbol('tabSymbol');
@@ -43,7 +43,6 @@ const resize = defineTabTool({
43
43
  type: 'readOnly',
44
44
  },
45
45
  handle: async (tab, params, response) => {
46
- response.addCode(`// Resize browser window to ${params.width}x${params.height}`);
47
46
  response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
48
47
  await tab.waitForCompletion(async () => {
49
48
  await tab.page.setViewportSize({ width: params.width, height: params.height });
@@ -31,7 +31,6 @@ const uploadFile = defineTabTool({
31
31
  const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
32
32
  if (!modalState)
33
33
  throw new Error('No file chooser visible');
34
- response.addCode(`// Select files for upload`);
35
34
  response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
36
35
  tab.clearModalState(modalState);
37
36
  await tab.waitForCompletion(async () => {
@@ -57,12 +57,10 @@ const type = defineTabTool({
57
57
  await tab.waitForCompletion(async () => {
58
58
  if (params.slowly) {
59
59
  response.setIncludeSnapshot();
60
- response.addCode(`// Press "${params.text}" sequentially into "${params.element}"`);
61
60
  response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
62
61
  await locator.pressSequentially(params.text);
63
62
  }
64
63
  else {
65
- response.addCode(`// Fill "${params.text}" into "${params.element}"`);
66
64
  response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
67
65
  await locator.fill(params.text);
68
66
  }
@@ -30,7 +30,6 @@ const navigate = defineTool({
30
30
  const tab = await context.ensureTab();
31
31
  await tab.navigate(params.url);
32
32
  response.setIncludeSnapshot();
33
- response.addCode(`// Navigate to ${params.url}`);
34
33
  response.addCode(`await page.goto('${params.url}');`);
35
34
  },
36
35
  });
@@ -46,7 +45,6 @@ const goBack = defineTabTool({
46
45
  handle: async (tab, params, response) => {
47
46
  await tab.page.goBack();
48
47
  response.setIncludeSnapshot();
49
- response.addCode(`// Navigate back`);
50
48
  response.addCode(`await page.goBack();`);
51
49
  },
52
50
  });
@@ -62,7 +60,6 @@ const goForward = defineTabTool({
62
60
  handle: async (tab, params, response) => {
63
61
  await tab.page.goForward();
64
62
  response.setIncludeSnapshot();
65
- response.addCode(`// Navigate forward`);
66
63
  response.addCode(`await page.goForward();`);
67
64
  },
68
65
  });
package/lib/tools/pdf.js CHANGED
@@ -16,7 +16,6 @@
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
18
  import * as javascript from '../javascript.js';
19
- import { outputFile } from '../config.js';
20
19
  const pdfSchema = z.object({
21
20
  filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
22
21
  });
@@ -30,8 +29,7 @@ const pdf = defineTabTool({
30
29
  type: 'readOnly',
31
30
  },
32
31
  handle: async (tab, params, response) => {
33
- const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
34
- response.addCode(`// Save page as ${fileName}`);
32
+ const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`);
35
33
  response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
36
34
  response.addResult(`Saved page as ${fileName}`);
37
35
  await tab.page.pdf({ path: fileName });
@@ -16,10 +16,9 @@
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
18
  import * as javascript from '../javascript.js';
19
- import { outputFile } from '../config.js';
20
19
  import { generateLocator } from './utils.js';
21
20
  const screenshotSchema = z.object({
22
- raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
21
+ type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
23
22
  filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
24
23
  element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
25
24
  ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
@@ -45,11 +44,11 @@ const screenshot = defineTabTool({
45
44
  type: 'readOnly',
46
45
  },
47
46
  handle: async (tab, params, response) => {
48
- const fileType = params.raw ? 'png' : 'jpeg';
49
- const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
47
+ const fileType = params.type || 'png';
48
+ const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
50
49
  const options = {
51
50
  type: fileType,
52
- quality: fileType === 'png' ? undefined : 50,
51
+ quality: fileType === 'png' ? undefined : 90,
53
52
  scale: 'css',
54
53
  path: fileName,
55
54
  ...(params.fullPage !== undefined && { fullPage: params.fullPage })
@@ -65,10 +64,14 @@ const screenshot = defineTabTool({
65
64
  response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
66
65
  const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
67
66
  response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
68
- response.addImage({
69
- contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
70
- data: buffer
71
- });
67
+ // https://github.com/microsoft/playwright-mcp/issues/817
68
+ // Never return large images to LLM, saving them to the file system is enough.
69
+ if (!params.fullPage) {
70
+ response.addImage({
71
+ contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
72
+ data: buffer
73
+ });
74
+ }
72
75
  }
73
76
  });
74
77
  export default [
@@ -53,14 +53,10 @@ const click = defineTabTool({
53
53
  const locator = await tab.refLocator(params);
54
54
  const button = params.button;
55
55
  const buttonAttr = button ? `{ button: '${button}' }` : '';
56
- if (params.doubleClick) {
57
- response.addCode(`// Double click ${params.element}`);
56
+ if (params.doubleClick)
58
57
  response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
59
- }
60
- else {
61
- response.addCode(`// Click ${params.element}`);
58
+ else
62
59
  response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
63
- }
64
60
  await tab.waitForCompletion(async () => {
65
61
  if (params.doubleClick)
66
62
  await locator.dblclick({ button });
@@ -128,7 +124,6 @@ const selectOption = defineTabTool({
128
124
  handle: async (tab, params, response) => {
129
125
  response.setIncludeSnapshot();
130
126
  const locator = await tab.refLocator(params);
131
- response.addCode(`// Select options [${params.values.join(', ')}] in ${params.element}`);
132
127
  response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);
133
128
  await tab.waitForCompletion(async () => {
134
129
  await locator.selectOption(params.values);
package/lib/tools/tool.js CHANGED
@@ -23,10 +23,11 @@ export function defineTabTool(tool) {
23
23
  const tab = context.currentTabOrDie();
24
24
  const modalStates = tab.modalStates().map(state => state.type);
25
25
  if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
26
- throw new Error(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
27
- if (!tool.clearsModalState && modalStates.length)
28
- throw new Error(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
29
- return tool.handle(tab, params, response);
26
+ response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
27
+ else if (!tool.clearsModalState && modalStates.length)
28
+ response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
29
+ else
30
+ return tool.handle(tab, params, response);
30
31
  },
31
32
  };
32
33
  }
@@ -60,13 +60,6 @@ export async function waitForCompletion(tab, callback) {
60
60
  dispose();
61
61
  }
62
62
  }
63
- export function sanitizeForFilePath(s) {
64
- const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
65
- const separator = s.lastIndexOf('.');
66
- if (separator === -1)
67
- return sanitize(s);
68
- return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
69
- }
70
63
  export async function generateLocator(locator) {
71
64
  try {
72
65
  const { resolvedSelector } = await locator._resolveSelector();
package/lib/utils.js ADDED
@@ -0,0 +1,26 @@
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 crypto from 'crypto';
17
+ export function createHash(data) {
18
+ return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
19
+ }
20
+ export function sanitizeForFilePath(s) {
21
+ const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
22
+ const separator = s.lastIndexOf('.');
23
+ if (separator === -1)
24
+ return sanitize(s);
25
+ return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwright/mcp",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,18 +17,16 @@
17
17
  "license": "Apache-2.0",
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "build:extension": "tsc --project extension",
21
20
  "lint": "npm run update-readme && eslint . && tsc --noEmit",
22
21
  "lint-fix": "eslint . --fix",
23
22
  "update-readme": "node utils/update-readme.js",
24
23
  "watch": "tsc --watch",
25
- "watch:extension": "tsc --watch --project extension",
26
24
  "test": "playwright test",
27
25
  "ctest": "playwright test --project=chrome",
28
26
  "ftest": "playwright test --project=firefox",
29
27
  "wtest": "playwright test --project=webkit",
30
28
  "run-server": "node lib/browserServer.js",
31
- "clean": "rm -rf lib extension/lib",
29
+ "clean": "rm -rf lib",
32
30
  "npm-publish": "npm run clean && npm run build && npm run test && npm publish"
33
31
  },
34
32
  "exports": {
@@ -44,24 +42,25 @@
44
42
  "debug": "^4.4.1",
45
43
  "dotenv": "^17.2.0",
46
44
  "mime": "^4.0.7",
47
- "playwright": "1.55.0-alpha-1752701791000",
48
- "playwright-core": "1.55.0-alpha-1752701791000",
45
+ "playwright": "1.55.0-alpha-2025-08-07",
46
+ "playwright-core": "1.55.0-alpha-2025-08-07",
49
47
  "ws": "^8.18.1",
48
+ "zod": "^3.24.1",
50
49
  "zod-to-json-schema": "^3.24.4"
51
50
  },
52
51
  "devDependencies": {
53
52
  "@anthropic-ai/sdk": "^0.57.0",
54
53
  "@eslint/eslintrc": "^3.2.0",
55
54
  "@eslint/js": "^9.19.0",
56
- "@playwright/test": "1.55.0-alpha-1752701791000",
55
+ "@playwright/test": "1.55.0-alpha-2025-08-07",
57
56
  "@stylistic/eslint-plugin": "^3.0.1",
58
- "@types/chrome": "^0.0.315",
59
57
  "@types/debug": "^4.1.12",
60
58
  "@types/node": "^22.13.10",
61
59
  "@types/ws": "^8.18.1",
62
60
  "@typescript-eslint/eslint-plugin": "^8.26.1",
63
61
  "@typescript-eslint/parser": "^8.26.1",
64
62
  "@typescript-eslint/utils": "^8.26.1",
63
+ "esbuild": "^0.20.1",
65
64
  "eslint": "^9.19.0",
66
65
  "eslint-plugin-import": "^2.31.0",
67
66
  "eslint-plugin-notice": "^1.0.0",