@playwright/mcp 0.0.32 → 0.0.34

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/response.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ import { renderModalStates } from './tab.js';
16
17
  export class Response {
17
18
  _result = [];
18
19
  _code = [];
@@ -20,9 +21,10 @@ export class Response {
20
21
  _context;
21
22
  _includeSnapshot = false;
22
23
  _includeTabs = false;
23
- _snapshot;
24
+ _tabSnapshot;
24
25
  toolName;
25
26
  toolArgs;
27
+ _isError;
26
28
  constructor(context, toolName, toolArgs) {
27
29
  this._context = context;
28
30
  this.toolName = toolName;
@@ -31,6 +33,13 @@ export class Response {
31
33
  addResult(result) {
32
34
  this._result.push(result);
33
35
  }
36
+ addError(error) {
37
+ this._result.push(error);
38
+ this._isError = true;
39
+ }
40
+ isError() {
41
+ return this._isError;
42
+ }
34
43
  result() {
35
44
  return this._result.join('\n');
36
45
  }
@@ -52,16 +61,18 @@ export class Response {
52
61
  setIncludeTabs() {
53
62
  this._includeTabs = true;
54
63
  }
55
- async snapshot() {
56
- if (this._snapshot !== undefined)
57
- return this._snapshot;
64
+ async finish() {
65
+ // All the async snapshotting post-action is happening here.
66
+ // Everything below should race against modal states.
58
67
  if (this._includeSnapshot && this._context.currentTab())
59
- this._snapshot = await this._context.currentTabOrDie().captureSnapshot();
60
- else
61
- this._snapshot = '';
62
- return this._snapshot;
68
+ this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
69
+ for (const tab of this._context.tabs())
70
+ await tab.updateTitle();
63
71
  }
64
- async serialize() {
72
+ tabSnapshot() {
73
+ return this._tabSnapshot;
74
+ }
75
+ serialize() {
65
76
  const response = [];
66
77
  // Start with command result.
67
78
  if (this._result.length) {
@@ -79,11 +90,16 @@ ${this._code.join('\n')}
79
90
  }
80
91
  // List browser tabs.
81
92
  if (this._includeSnapshot || this._includeTabs)
82
- response.push(...(await this._context.listTabsMarkdown(this._includeTabs)));
93
+ response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
83
94
  // Add snapshot if provided.
84
- const snapshot = await this.snapshot();
85
- if (snapshot)
86
- response.push(snapshot, '');
95
+ if (this._tabSnapshot?.modalStates.length) {
96
+ response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates));
97
+ response.push('');
98
+ }
99
+ else if (this._tabSnapshot) {
100
+ response.push(renderTabSnapshot(this._tabSnapshot));
101
+ response.push('');
102
+ }
87
103
  // Main response part
88
104
  const content = [
89
105
  { type: 'text', text: response.join('\n') },
@@ -93,6 +109,57 @@ ${this._code.join('\n')}
93
109
  for (const image of this._images)
94
110
  content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType });
95
111
  }
96
- return { content };
112
+ return { content, isError: this._isError };
113
+ }
114
+ }
115
+ function renderTabSnapshot(tabSnapshot) {
116
+ const lines = [];
117
+ if (tabSnapshot.consoleMessages.length) {
118
+ lines.push(`### New console messages`);
119
+ for (const message of tabSnapshot.consoleMessages)
120
+ lines.push(`- ${trim(message.toString(), 100)}`);
121
+ lines.push('');
122
+ }
123
+ if (tabSnapshot.downloads.length) {
124
+ lines.push(`### Downloads`);
125
+ for (const entry of tabSnapshot.downloads) {
126
+ if (entry.finished)
127
+ lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
128
+ else
129
+ lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
130
+ }
131
+ lines.push('');
132
+ }
133
+ lines.push(`### Page state`);
134
+ lines.push(`- Page URL: ${tabSnapshot.url}`);
135
+ lines.push(`- Page Title: ${tabSnapshot.title}`);
136
+ lines.push(`- Page Snapshot:`);
137
+ lines.push('```yaml');
138
+ lines.push(tabSnapshot.ariaSnapshot);
139
+ lines.push('```');
140
+ return lines.join('\n');
141
+ }
142
+ function renderTabsMarkdown(tabs, force = false) {
143
+ if (tabs.length === 1 && !force)
144
+ return [];
145
+ if (!tabs.length) {
146
+ return [
147
+ '### Open tabs',
148
+ 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
149
+ '',
150
+ ];
97
151
  }
152
+ const lines = ['### Open tabs'];
153
+ for (let i = 0; i < tabs.length; i++) {
154
+ const tab = tabs[i];
155
+ const current = tab.isCurrentTab() ? ' (current)' : '';
156
+ lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
157
+ }
158
+ lines.push('');
159
+ return lines;
160
+ }
161
+ function trim(text, maxLength) {
162
+ if (text.length <= maxLength)
163
+ return text;
164
+ return text.slice(0, maxLength) + '...';
98
165
  }
package/lib/sessionLog.js CHANGED
@@ -15,56 +15,107 @@
15
15
  */
16
16
  import fs from 'fs';
17
17
  import path from 'path';
18
+ import { logUnhandledError } from './utils/log.js';
18
19
  import { outputFile } from './config.js';
19
- let sessionOrdinal = 0;
20
20
  export class SessionLog {
21
21
  _folder;
22
22
  _file;
23
23
  _ordinal = 0;
24
+ _pendingEntries = [];
25
+ _sessionFileQueue = Promise.resolve();
26
+ _flushEntriesTimeout;
24
27
  constructor(sessionFolder) {
25
28
  this._folder = sessionFolder;
26
29
  this._file = path.join(this._folder, 'session.md');
27
30
  }
28
- static async create(config) {
29
- const sessionFolder = await outputFile(config, `session-${(++sessionOrdinal).toString().padStart(3, '0')}`);
31
+ static async create(config, rootPath) {
32
+ const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`);
30
33
  await fs.promises.mkdir(sessionFolder, { recursive: true });
31
34
  // eslint-disable-next-line no-console
32
35
  console.error(`Session: ${sessionFolder}`);
33
36
  return new SessionLog(sessionFolder);
34
37
  }
35
- async log(response) {
36
- const prefix = `${(++this._ordinal).toString().padStart(3, '0')}`;
37
- const lines = [
38
- `### Tool: ${response.toolName}`,
39
- ``,
40
- `- Args`,
41
- '```json',
42
- JSON.stringify(response.toolArgs, null, 2),
43
- '```',
44
- ];
45
- if (response.result()) {
46
- lines.push(`- Result`, '```', response.result(), '```');
47
- }
48
- if (response.code()) {
49
- lines.push(`- Code`, '```js', response.code(), '```');
38
+ logResponse(response) {
39
+ const entry = {
40
+ timestamp: performance.now(),
41
+ toolCall: {
42
+ toolName: response.toolName,
43
+ toolArgs: response.toolArgs,
44
+ result: response.result(),
45
+ isError: response.isError(),
46
+ },
47
+ code: response.code(),
48
+ tabSnapshot: response.tabSnapshot(),
49
+ };
50
+ this._appendEntry(entry);
51
+ }
52
+ logUserAction(action, tab, code, isUpdate) {
53
+ code = code.trim();
54
+ if (isUpdate) {
55
+ const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
56
+ if (lastEntry.userAction?.name === action.name) {
57
+ lastEntry.userAction = action;
58
+ lastEntry.code = code;
59
+ return;
60
+ }
50
61
  }
51
- const snapshot = await response.snapshot();
52
- if (snapshot) {
53
- const fileName = `${prefix}.snapshot.yml`;
54
- await fs.promises.writeFile(path.join(this._folder, fileName), snapshot);
55
- lines.push(`- Snapshot: ${fileName}`);
62
+ if (action.name === 'navigate') {
63
+ // Already logged at this location.
64
+ const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
65
+ if (lastEntry?.tabSnapshot?.url === action.url)
66
+ return;
56
67
  }
57
- for (const image of response.images()) {
58
- const fileName = `${prefix}.screenshot.${extension(image.contentType)}`;
59
- await fs.promises.writeFile(path.join(this._folder, fileName), image.data);
60
- lines.push(`- Screenshot: ${fileName}`);
68
+ const entry = {
69
+ timestamp: performance.now(),
70
+ userAction: action,
71
+ code,
72
+ tabSnapshot: {
73
+ url: tab.page.url(),
74
+ title: '',
75
+ ariaSnapshot: action.ariaSnapshot || '',
76
+ modalStates: [],
77
+ consoleMessages: [],
78
+ downloads: [],
79
+ },
80
+ };
81
+ this._appendEntry(entry);
82
+ }
83
+ _appendEntry(entry) {
84
+ this._pendingEntries.push(entry);
85
+ if (this._flushEntriesTimeout)
86
+ clearTimeout(this._flushEntriesTimeout);
87
+ this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000);
88
+ }
89
+ async _flushEntries() {
90
+ clearTimeout(this._flushEntriesTimeout);
91
+ const entries = this._pendingEntries;
92
+ this._pendingEntries = [];
93
+ const lines = [''];
94
+ for (const entry of entries) {
95
+ const ordinal = (++this._ordinal).toString().padStart(3, '0');
96
+ if (entry.toolCall) {
97
+ lines.push(`### Tool call: ${entry.toolCall.toolName}`, `- Args`, '```json', JSON.stringify(entry.toolCall.toolArgs, null, 2), '```');
98
+ if (entry.toolCall.result) {
99
+ lines.push(entry.toolCall.isError ? `- Error` : `- Result`, '```', entry.toolCall.result, '```');
100
+ }
101
+ }
102
+ if (entry.userAction) {
103
+ const actionData = { ...entry.userAction };
104
+ delete actionData.ariaSnapshot;
105
+ delete actionData.selector;
106
+ delete actionData.signals;
107
+ lines.push(`### User action: ${entry.userAction.name}`, `- Args`, '```json', JSON.stringify(actionData, null, 2), '```');
108
+ }
109
+ if (entry.code) {
110
+ lines.push(`- Code`, '```js', entry.code, '```');
111
+ }
112
+ if (entry.tabSnapshot) {
113
+ const fileName = `${ordinal}.snapshot.yml`;
114
+ fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError);
115
+ lines.push(`- Snapshot: ${fileName}`);
116
+ }
117
+ lines.push('', '');
61
118
  }
62
- lines.push('', '');
63
- await fs.promises.appendFile(this._file, lines.join('\n'));
119
+ this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n')));
64
120
  }
65
121
  }
66
- function extension(contentType) {
67
- if (contentType === 'image/jpeg')
68
- return 'jpg';
69
- return 'png';
70
- }
package/lib/tab.js CHANGED
@@ -15,15 +15,15 @@
15
15
  */
16
16
  import { EventEmitter } from 'events';
17
17
  import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
- import { logUnhandledError } from './log.js';
19
- import { ManualPromise } from './manualPromise.js';
20
- import { outputFile } from './config.js';
18
+ import { logUnhandledError } from './utils/log.js';
19
+ import { ManualPromise } from './utils/manualPromise.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 });
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const evaluateSchema = z.object({
21
21
  function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
@@ -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 () => {
@@ -17,7 +17,7 @@ import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
18
  import { elementSchema } from './snapshot.js';
19
19
  import { generateLocator } from './utils.js';
20
- import * as javascript from '../javascript.js';
20
+ import * as javascript from '../utils/codegen.js';
21
21
  const pressKey = defineTabTool({
22
22
  capability: 'core',
23
23
  schema: {
@@ -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
@@ -15,8 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
19
- import { outputFile } from '../config.js';
18
+ import * as javascript from '../utils/codegen.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 });
@@ -15,11 +15,10 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
19
- import { outputFile } from '../config.js';
18
+ import * as javascript from '../utils/codegen.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 [
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from 'zod';
17
17
  import { defineTabTool, defineTool } from './tool.js';
18
- import * as javascript from '../javascript.js';
18
+ import * as javascript from '../utils/codegen.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const snapshot = defineTool({
21
21
  capability: 'core',
@@ -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();