@cotestdev/mcp_playwright 0.0.14 → 0.0.16

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.
Files changed (49) hide show
  1. package/lib/mcp/browser/browserContextFactory.js +49 -13
  2. package/lib/mcp/browser/browserServerBackend.js +5 -2
  3. package/lib/mcp/browser/config.js +95 -23
  4. package/lib/mcp/browser/context.js +28 -3
  5. package/lib/mcp/browser/response.js +240 -57
  6. package/lib/mcp/browser/sessionLog.js +1 -1
  7. package/lib/mcp/browser/tab.js +96 -69
  8. package/lib/mcp/browser/tools/common.js +8 -8
  9. package/lib/mcp/browser/tools/console.js +6 -3
  10. package/lib/mcp/browser/tools/dialogs.js +13 -13
  11. package/lib/mcp/browser/tools/evaluate.js +9 -20
  12. package/lib/mcp/browser/tools/files.js +10 -5
  13. package/lib/mcp/browser/tools/form.js +11 -22
  14. package/lib/mcp/browser/tools/install.js +3 -3
  15. package/lib/mcp/browser/tools/keyboard.js +12 -12
  16. package/lib/mcp/browser/tools/mouse.js +14 -14
  17. package/lib/mcp/browser/tools/navigate.js +5 -5
  18. package/lib/mcp/browser/tools/network.js +16 -5
  19. package/lib/mcp/browser/tools/pdf.js +7 -18
  20. package/lib/mcp/browser/tools/runCode.js +77 -0
  21. package/lib/mcp/browser/tools/screenshot.js +44 -33
  22. package/lib/mcp/browser/tools/snapshot.js +42 -33
  23. package/lib/mcp/browser/tools/tabs.js +7 -10
  24. package/lib/mcp/browser/tools/tool.js +8 -7
  25. package/lib/mcp/browser/tools/tracing.js +4 -4
  26. package/lib/mcp/browser/tools/utils.js +50 -52
  27. package/lib/mcp/browser/tools/verify.js +23 -34
  28. package/lib/mcp/browser/tools/wait.js +6 -6
  29. package/lib/mcp/browser/tools.js +4 -3
  30. package/lib/mcp/extension/cdpRelay.js +1 -1
  31. package/lib/mcp/extension/extensionContextFactory.js +4 -3
  32. package/lib/mcp/log.js +2 -2
  33. package/lib/mcp/program.js +21 -29
  34. package/lib/mcp/sdk/exports.js +1 -5
  35. package/lib/mcp/sdk/http.js +37 -50
  36. package/lib/mcp/sdk/server.js +61 -9
  37. package/lib/mcp/sdk/tool.js +5 -4
  38. package/lib/mcp/test/browserBackend.js +67 -61
  39. package/lib/mcp/test/generatorTools.js +122 -0
  40. package/lib/mcp/test/plannerTools.js +144 -0
  41. package/lib/mcp/test/seed.js +82 -0
  42. package/lib/mcp/test/streams.js +10 -7
  43. package/lib/mcp/test/testBackend.js +44 -24
  44. package/lib/mcp/test/testContext.js +243 -14
  45. package/lib/mcp/test/testTools.js +23 -109
  46. package/lib/mcpBundle.js +84 -0
  47. package/lib/mcpBundleImpl/index.js +130 -0
  48. package/lib/util.js +12 -6
  49. package/package.json +1 -1
@@ -18,17 +18,24 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var response_exports = {};
20
20
  __export(response_exports, {
21
- Response: () => Response
21
+ RenderedResponse: () => RenderedResponse,
22
+ Response: () => Response,
23
+ parseResponse: () => parseResponse,
24
+ requestDebug: () => requestDebug
22
25
  });
23
26
  module.exports = __toCommonJS(response_exports);
27
+ var import_utilsBundle = require("playwright-core/lib/utilsBundle");
24
28
  var import_tab = require("./tab");
29
+ const requestDebug = (0, import_utilsBundle.debug)("pw:mcp:request");
25
30
  class Response {
26
31
  constructor(context, toolName, toolArgs) {
27
32
  this._result = [];
28
33
  this._code = [];
29
34
  this._images = [];
35
+ this._files = [];
30
36
  this._includeSnapshot = "none";
31
37
  this._includeTabs = false;
38
+ this._includeMetaOnly = false;
32
39
  this._context = context;
33
40
  this.toolName = toolName;
34
41
  this.toolArgs = toolArgs;
@@ -58,13 +65,29 @@ class Response {
58
65
  images() {
59
66
  return this._images;
60
67
  }
61
- setIncludeSnapshot(full) {
62
- this._includeSnapshot = full ?? "partial";
68
+ async addFile(fileName, options) {
69
+ const resolvedFile = await this._context.outputFile(fileName, options);
70
+ this._files.push({ fileName: resolvedFile, title: options.reason });
71
+ return resolvedFile;
72
+ }
73
+ setIncludeSnapshot() {
74
+ this._includeSnapshot = this._context.config.snapshot.mode;
75
+ }
76
+ setIncludeFullSnapshot() {
77
+ this._includeSnapshot = "full";
63
78
  }
64
79
  setIncludeTabs() {
65
80
  this._includeTabs = true;
66
81
  }
82
+ setIncludeModalStates(modalStates) {
83
+ this._includeModalStates = modalStates;
84
+ }
85
+ setIncludeMetaOnly() {
86
+ this._includeMetaOnly = true;
87
+ }
67
88
  async finish() {
89
+ if (this._tabSnapshot)
90
+ return;
68
91
  if (this._includeSnapshot !== "none" && this._context.currentTab())
69
92
  this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
70
93
  for (const tab of this._context.tabs())
@@ -73,89 +96,110 @@ class Response {
73
96
  tabSnapshot() {
74
97
  return this._tabSnapshot;
75
98
  }
76
- serialize() {
77
- const response = [];
78
- if (this._result.length) {
79
- response.push("### Result");
80
- response.push(this._result.join("\n"));
81
- response.push("");
82
- }
83
- if (this._code.length) {
84
- response.push(`### Ran Playwright code
85
- \`\`\`js
86
- ${this._code.join("\n")}
87
- \`\`\``);
88
- response.push("");
99
+ logBegin() {
100
+ if (requestDebug.enabled)
101
+ requestDebug(this.toolName, this.toolArgs);
102
+ }
103
+ logEnd() {
104
+ if (requestDebug.enabled)
105
+ requestDebug(this.serialize());
106
+ }
107
+ render() {
108
+ const renderedResponse = new RenderedResponse();
109
+ if (this._result.length)
110
+ renderedResponse.results.push(...this._result);
111
+ if (this._code.length)
112
+ renderedResponse.code.push(...this._code);
113
+ if (this._includeSnapshot !== "none" || this._includeTabs) {
114
+ const tabsMarkdown = renderTabsMarkdown(this._context.tabs(), this._includeTabs);
115
+ if (tabsMarkdown.length)
116
+ renderedResponse.states.tabs = tabsMarkdown.join("\n");
89
117
  }
90
- if (this._includeSnapshot !== "none" || this._includeTabs)
91
- response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
92
118
  if (this._tabSnapshot?.modalStates.length) {
93
- response.push(...(0, import_tab.renderModalStates)(this._context, this._tabSnapshot.modalStates));
94
- response.push("");
119
+ const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._tabSnapshot.modalStates);
120
+ renderedResponse.states.modal = modalStatesMarkdown.join("\n");
95
121
  } else if (this._tabSnapshot) {
96
- response.push(renderTabSnapshot(this._tabSnapshot, this._includeSnapshot === "full"));
97
- response.push("");
122
+ renderTabSnapshot(this._tabSnapshot, this._includeSnapshot, renderedResponse);
123
+ } else if (this._includeModalStates) {
124
+ const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._includeModalStates);
125
+ renderedResponse.states.modal = modalStatesMarkdown.join("\n");
126
+ }
127
+ if (this._files.length) {
128
+ const lines = [];
129
+ for (const file of this._files)
130
+ lines.push(`- [${file.title}](${file.fileName})`);
131
+ renderedResponse.updates.push({ category: "files", content: lines.join("\n") });
98
132
  }
133
+ return this._context.config.secrets ? renderedResponse.redact(this._context.config.secrets) : renderedResponse;
134
+ }
135
+ serialize(options = {}) {
136
+ const renderedResponse = this.render();
137
+ const includeMeta = options._meta && "dev.lowire/history" in options._meta && "dev.lowire/state" in options._meta;
138
+ const _meta = includeMeta ? renderedResponse.asMeta() : void 0;
99
139
  const content = [
100
- { type: "text", text: response.join("\n") }
140
+ {
141
+ type: "text",
142
+ text: renderedResponse.asText(this._includeMetaOnly ? { categories: ["files"] } : void 0)
143
+ }
101
144
  ];
102
- this._redactSecrets(content);
103
- return { content, isError: this._isError };
104
- }
105
- _redactSecrets(content) {
106
- if (!this._context.config.secrets)
107
- return;
108
- for (const item of content) {
109
- if (item.type !== "text")
110
- continue;
111
- for (const [secretName, secretValue] of Object.entries(this._context.config.secrets))
112
- item.text = item.text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
145
+ if (this._includeMetaOnly)
146
+ return { _meta, content, isError: this._isError };
147
+ if (this._context.config.imageResponses !== "omit") {
148
+ for (const image of this._images)
149
+ content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
113
150
  }
151
+ return {
152
+ _meta,
153
+ content,
154
+ isError: this._isError
155
+ };
114
156
  }
115
157
  }
116
- function renderTabSnapshot(tabSnapshot, fullSnapshot) {
117
- const lines = [];
158
+ function renderTabSnapshot(tabSnapshot, includeSnapshot, response) {
159
+ if (tabSnapshot.consoleMessages.length) {
160
+ const lines2 = [];
161
+ for (const message of tabSnapshot.consoleMessages)
162
+ lines2.push(`- ${trim(message.toString(), 100)}`);
163
+ response.updates.push({ category: "console", content: lines2.join("\n") });
164
+ }
118
165
  if (tabSnapshot.downloads.length) {
119
- lines.push(`### Downloads`);
166
+ const lines2 = [];
120
167
  for (const entry of tabSnapshot.downloads) {
121
168
  if (entry.finished)
122
- lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
169
+ lines2.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
123
170
  else
124
- lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
171
+ lines2.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
125
172
  }
126
- lines.push("");
173
+ response.updates.push({ category: "downloads", content: lines2.join("\n") });
174
+ }
175
+ if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff === "") {
176
+ return;
127
177
  }
128
- lines.push(`### Page state`);
178
+ const lines = [];
129
179
  lines.push(`- Page URL: ${tabSnapshot.url}`);
130
180
  lines.push(`- Page Title: ${tabSnapshot.title}`);
131
- if (!fullSnapshot && tabSnapshot.formattedAriaSnapshotDiff) {
132
- lines.push(`- Page Snapshot Diff:`);
133
- lines.push(tabSnapshot.formattedAriaSnapshotDiff);
134
- } else {
181
+ if (includeSnapshot !== "none") {
135
182
  lines.push(`- Page Snapshot:`);
136
183
  lines.push("```yaml");
137
- lines.push(tabSnapshot.ariaSnapshot);
184
+ if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff !== void 0)
185
+ lines.push(tabSnapshot.ariaSnapshotDiff);
186
+ else
187
+ lines.push(tabSnapshot.ariaSnapshot);
138
188
  lines.push("```");
139
189
  }
140
- return lines.join("\n");
190
+ response.states.page = lines.join("\n");
141
191
  }
142
192
  function renderTabsMarkdown(tabs, force = false) {
143
193
  if (tabs.length === 1 && !force)
144
194
  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
- ];
151
- }
152
- const lines = ["### Open tabs"];
195
+ if (!tabs.length)
196
+ return ['No open tabs. Use the "browser_navigate" tool to navigate to a page first.'];
197
+ const lines = [];
153
198
  for (let i = 0; i < tabs.length; i++) {
154
199
  const tab = tabs[i];
155
200
  const current = tab.isCurrentTab() ? " (current)" : "";
156
201
  lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
157
202
  }
158
- lines.push("");
159
203
  return lines;
160
204
  }
161
205
  function trim(text, maxLength) {
@@ -163,7 +207,146 @@ function trim(text, maxLength) {
163
207
  return text;
164
208
  return text.slice(0, maxLength) + "...";
165
209
  }
210
+ class RenderedResponse {
211
+ constructor(copy) {
212
+ this.states = {};
213
+ this.updates = [];
214
+ this.results = [];
215
+ this.code = [];
216
+ if (copy) {
217
+ this.states = copy.states;
218
+ this.updates = copy.updates;
219
+ this.results = copy.results;
220
+ this.code = copy.code;
221
+ }
222
+ }
223
+ asText(filter) {
224
+ const text = [];
225
+ if (this.results.length)
226
+ text.push(`### Result
227
+ ${this.results.join("\n")}
228
+ `);
229
+ if (this.code.length)
230
+ text.push(`### Ran Playwright code
231
+ ${this.code.join("\n")}
232
+ `);
233
+ for (const { category, content } of this.updates) {
234
+ if (filter && !filter.categories.includes(category))
235
+ continue;
236
+ if (!content.trim())
237
+ continue;
238
+ switch (category) {
239
+ case "console":
240
+ text.push(`### New console messages
241
+ ${content}
242
+ `);
243
+ break;
244
+ case "downloads":
245
+ text.push(`### Downloads
246
+ ${content}
247
+ `);
248
+ break;
249
+ case "files":
250
+ text.push(`### Files
251
+ ${content}
252
+ `);
253
+ break;
254
+ }
255
+ }
256
+ for (const [category, value] of Object.entries(this.states)) {
257
+ if (filter && !filter.categories.includes(category))
258
+ continue;
259
+ if (!value.trim())
260
+ continue;
261
+ switch (category) {
262
+ case "page":
263
+ text.push(`### Page state
264
+ ${value}
265
+ `);
266
+ break;
267
+ case "tabs":
268
+ text.push(`### Open tabs
269
+ ${value}
270
+ `);
271
+ break;
272
+ case "modal":
273
+ text.push(`### Modal state
274
+ ${value}
275
+ `);
276
+ break;
277
+ }
278
+ }
279
+ return text.join("\n");
280
+ }
281
+ asMeta() {
282
+ const codeUpdate = this.code.length ? { category: "code", content: this.code.join("\n") } : void 0;
283
+ const resultUpdate = this.results.length ? { category: "result", content: this.results.join("\n") } : void 0;
284
+ const updates = [resultUpdate, codeUpdate, ...this.updates].filter(Boolean);
285
+ return {
286
+ "dev.lowire/history": updates,
287
+ "dev.lowire/state": { ...this.states }
288
+ };
289
+ }
290
+ redact(secrets) {
291
+ const redactText = (text) => {
292
+ for (const [secretName, secretValue] of Object.entries(secrets))
293
+ text = text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
294
+ return text;
295
+ };
296
+ const updates = this.updates.map((update) => ({ ...update, content: redactText(update.content) }));
297
+ const results = this.results.map((result) => redactText(result));
298
+ const code = this.code.map((code2) => redactText(code2));
299
+ const states = Object.fromEntries(Object.entries(this.states).map(([key, value]) => [key, redactText(value)]));
300
+ return new RenderedResponse({ states, updates, results, code });
301
+ }
302
+ }
303
+ function parseSections(text) {
304
+ const sections = /* @__PURE__ */ new Map();
305
+ const sectionHeaders = text.split(/^### /m).slice(1);
306
+ for (const section of sectionHeaders) {
307
+ const firstNewlineIndex = section.indexOf("\n");
308
+ if (firstNewlineIndex === -1)
309
+ continue;
310
+ const sectionName = section.substring(0, firstNewlineIndex);
311
+ const sectionContent = section.substring(firstNewlineIndex + 1).trim();
312
+ sections.set(sectionName, sectionContent);
313
+ }
314
+ return sections;
315
+ }
316
+ function parseResponse(response) {
317
+ if (response.content?.[0].type !== "text")
318
+ return void 0;
319
+ const text = response.content[0].text;
320
+ const sections = parseSections(text);
321
+ const result = sections.get("Result");
322
+ const code = sections.get("Ran Playwright code");
323
+ const tabs = sections.get("Open tabs");
324
+ const pageState = sections.get("Page state");
325
+ const consoleMessages = sections.get("New console messages");
326
+ const modalState = sections.get("Modal state");
327
+ const downloads = sections.get("Downloads");
328
+ const files = sections.get("Files");
329
+ const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
330
+ const isError = response.isError;
331
+ const attachments = response.content.slice(1);
332
+ return {
333
+ result,
334
+ code: codeNoFrame,
335
+ tabs,
336
+ pageState,
337
+ consoleMessages,
338
+ modalState,
339
+ downloads,
340
+ files,
341
+ isError,
342
+ attachments,
343
+ _meta: response._meta
344
+ };
345
+ }
166
346
  // Annotate the CommonJS export names for ESM import in node:
167
347
  0 && (module.exports = {
168
- Response
348
+ RenderedResponse,
349
+ Response,
350
+ parseResponse,
351
+ requestDebug
169
352
  });
@@ -44,7 +44,7 @@ class SessionLog {
44
44
  this._file = import_path.default.join(this._folder, "session.md");
45
45
  }
46
46
  static async create(config, clientInfo) {
47
- const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code" });
47
+ const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code", reason: "Saving session" });
48
48
  await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
49
49
  console.error(`Session: ${sessionFolder}`);
50
50
  return new SessionLog(sessionFolder);