@wordbricks/playwright-mcp 0.1.27 → 0.1.30

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.
@@ -109,11 +109,16 @@ async function hidePlaywrightMarkers(browserContext) {
109
109
  },
110
110
  };
111
111
  const proxiedWindow = new Proxy(window, windowHandler);
112
- Object.defineProperty(globalThis, "window", {
113
- value: proxiedWindow,
114
- configurable: false,
115
- writable: false,
116
- });
112
+ try {
113
+ Object.defineProperty(globalThis, "window", {
114
+ value: proxiedWindow,
115
+ configurable: true,
116
+ writable: false,
117
+ });
118
+ }
119
+ catch {
120
+ // Ignore if window property is already non-configurable
121
+ }
117
122
  });
118
123
  }
119
124
  export function contextFactory(config) {
@@ -232,6 +237,10 @@ class IsolatedContextFactory extends BaseContextFactory {
232
237
  ...this.config.browser.contextOptions,
233
238
  bypassCSP: true,
234
239
  });
240
+ // Grant local-network-access permission to suppress LNA prompt (Chrome 142+)
241
+ await browserContext
242
+ .grantPermissions(["local-network-access"])
243
+ .catch(() => { });
235
244
  await applyInitScript(browserContext, this.config);
236
245
  await hidePlaywrightMarkers(browserContext);
237
246
  return browserContext;
@@ -248,6 +257,10 @@ class CdpContextFactory extends BaseContextFactory {
248
257
  const browserContext = this.config.browser.isolated
249
258
  ? await browser.newContext({ bypassCSP: true })
250
259
  : browser.contexts()[0];
260
+ // Grant local-network-access permission to suppress LNA prompt (Chrome 142+)
261
+ await browserContext
262
+ .grantPermissions(["local-network-access"])
263
+ .catch(() => { });
251
264
  await applyInitScript(browserContext, this.config);
252
265
  await hidePlaywrightMarkers(browserContext);
253
266
  return browserContext;
@@ -323,6 +336,10 @@ class PersistentContextFactory {
323
336
  }
324
337
  // Get the initial page (persistent context creates one automatically)
325
338
  const initialPage = browserContext.pages()[0];
339
+ // Grant local-network-access permission to suppress LNA prompt (Chrome 142+)
340
+ await browserContext
341
+ .grantPermissions(["local-network-access"])
342
+ .catch(() => { });
326
343
  await applyInitScript(browserContext, this.config);
327
344
  await hidePlaywrightMarkers(browserContext);
328
345
  // Increment ref count for this persistent context
package/lib/config.js CHANGED
@@ -96,7 +96,7 @@ export function configFromCLIOptions(cliOptions) {
96
96
  // Disable Cast/Media Router and local network discovery
97
97
  "--media-router=0", "--disable-cast", "--disable-cast-streaming-hw-encoding",
98
98
  // Disable various Chrome features (Translate, PasswordManager, Autofill, Sync, MediaRouter, Cast)
99
- "--disable-features=Translate,PasswordManager,PasswordManagerEnabled,PasswordManagerOnboarding,AutofillServerCommunication,CredentialManagerOnboarding,MediaRouter,GlobalMediaControls,CastMediaRouteProvider,DialMediaRouteProvider,CastAllowAllIPs,EnableCastDiscovery,LocalNetworkAccessChecks", "--disable-sync",
99
+ "--disable-features=Translate,PasswordManager,PasswordManagerEnabled,PasswordManagerOnboarding,AutofillServerCommunication,CredentialManagerOnboarding,MediaRouter,GlobalMediaControls,CastMediaRouteProvider,DialMediaRouteProvider,CastAllowAllIPs,EnableCastDiscovery,LocalNetworkAccessCheck", "--disable-sync",
100
100
  // Disable password manager via experimental options
101
101
  "--enable-features=DisablePasswordManager");
102
102
  // --app was passed, add app mode argument
package/lib/program.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { Option, program } from "commander";
17
+ import dotenv from "dotenv";
18
+ import fs from "fs";
17
19
  import { contextFactory } from "./browserContextFactory.js";
18
20
  import { BrowserServerBackend } from "./browserServerBackend.js";
19
21
  import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList, } from "./config.js";
@@ -24,6 +26,22 @@ import { ProxyBackend } from "./mcp/proxyBackend.js";
24
26
  import * as mcpServer from "./mcp/server.js";
25
27
  import * as mcpTransport from "./mcp/transport.js";
26
28
  import { packageJSON } from "./utils/package.js";
29
+ export function dotenvFileLoader(value) {
30
+ if (!value)
31
+ return undefined;
32
+ if (!fs.existsSync(value))
33
+ return undefined;
34
+ return dotenv.parse(fs.readFileSync(value, "utf8"));
35
+ }
36
+ function loadSecretsFromCli(options) {
37
+ const secrets = dotenvFileLoader(options.secret);
38
+ if (!secrets)
39
+ return;
40
+ for (const [key, value] of Object.entries(secrets)) {
41
+ if (process.env[key] === undefined)
42
+ process.env[key] = value;
43
+ }
44
+ }
27
45
  program
28
46
  .version("Version " + packageJSON.version)
29
47
  .name(packageJSON.name)
@@ -52,6 +70,7 @@ program
52
70
  .option("--storage-state <path>", "path to the storage state file for isolated sessions.")
53
71
  .option("--user-agent <ua string>", "specify user agent string")
54
72
  .option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.")
73
+ .option("--secret <path>", "path to a file containing secrets in the dotenv format")
55
74
  .option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"')
56
75
  .option("--window-position <x,y>", 'specify Chrome window position in pixels, for example "100,200"')
57
76
  .option("--window-size <width,height>", 'specify Chrome window size in pixels, for example "1280,720"')
@@ -62,6 +81,7 @@ program
62
81
  .addOption(new Option("--vision", "Legacy option, use --caps=vision instead").hideHelp())
63
82
  .action(async (options) => {
64
83
  setupExitWatchdog();
84
+ loadSecretsFromCli(options);
65
85
  if (options.vision) {
66
86
  // eslint-disable-next-line no-console
67
87
  console.error("The --vision option is deprecated, use --caps=vision instead");
package/lib/response.js CHANGED
@@ -21,6 +21,7 @@ export class Response {
21
21
  _events = [];
22
22
  _code = [];
23
23
  _images = [];
24
+ _screenshotUrl = null;
24
25
  _context;
25
26
  _includeSnapshot = false;
26
27
  _includeTabs = false;
@@ -62,6 +63,9 @@ export class Response {
62
63
  images() {
63
64
  return this._images;
64
65
  }
66
+ addScreenshotUrl(url) {
67
+ this._screenshotUrl = url;
68
+ }
65
69
  // NOTE Wordbricks Disabled: Page state logging not needed
66
70
  setIncludeSnapshot(full) {
67
71
  // this._includeSnapshot = full ?? 'incremental';
@@ -125,7 +129,13 @@ ${this._code.join("\n")}
125
129
  }
126
130
  // Main response part
127
131
  const content = [
128
- { type: "text", text: response.join("\n") },
132
+ {
133
+ type: "text",
134
+ text: response.join("\n"),
135
+ ...(this._screenshotUrl && {
136
+ _meta: { screenShotUrl: this._screenshotUrl },
137
+ }),
138
+ },
129
139
  ];
130
140
  // Image attachments.
131
141
  if (this._context.config.imageResponses !== "omit") {
@@ -22,7 +22,7 @@ const getSnapshot = defineTool({
22
22
  lines.push(`- Page Title: ${snapshot.title}`);
23
23
  lines.push(`- Page Snapshot:`);
24
24
  lines.push("```yaml");
25
- let aria = snapshot.ariaSnapshot || "";
25
+ let aria = JSON.stringify(snapshot.ariaSnapshot) || ""; // HOTFIX: JSON.stringify is hotfix, Find root cause
26
26
  aria = String(truncate(aria, { maxStringLength: MAX_LENGTH }));
27
27
  lines.push(aria);
28
28
  lines.push("```");
@@ -14,6 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from "zod";
17
+ import { captureAndUploadScreenshot } from "../utils/screenshot.js";
17
18
  import { defineTabTool, defineTool } from "./tool.js";
18
19
  const navigate = defineTool({
19
20
  capability: "core",
@@ -29,6 +30,10 @@ const navigate = defineTool({
29
30
  handle: async (context, params, response) => {
30
31
  const tab = await context.ensureTab();
31
32
  await tab.navigate(params.url);
33
+ const screenshotUrl = await captureAndUploadScreenshot(tab.page);
34
+ if (screenshotUrl) {
35
+ response.addScreenshotUrl(screenshotUrl);
36
+ }
32
37
  response.setIncludeSnapshot();
33
38
  response.addCode(`await page.goto('${params.url}');`);
34
39
  },
@@ -1,5 +1,6 @@
1
1
  import ms from "ms";
2
2
  import { z } from "zod";
3
+ import { captureAndUploadScreenshot } from "../utils/screenshot.js";
3
4
  import { defineTabTool } from "./tool.js";
4
5
  /**
5
6
  * Generate random number between min and max
@@ -126,6 +127,10 @@ const scrollWheel = defineTabTool({
126
127
  }));
127
128
  // Always show scroll position for scroll tool
128
129
  response.addResult(`Scroll position:\nBefore: x=${initialScrollPosition.x}, y=${initialScrollPosition.y}\nAfter: x=${finalScrollPosition.x}, y=${finalScrollPosition.y}`);
130
+ const screenshotUrl = await captureAndUploadScreenshot(tab.page);
131
+ if (screenshotUrl) {
132
+ response.addScreenshotUrl(screenshotUrl);
133
+ }
129
134
  },
130
135
  });
131
136
  export default [scrollWheel];
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { z } from "zod";
17
17
  import * as javascript from "../utils/codegen.js";
18
+ import { captureAndUploadScreenshot } from "../utils/screenshot.js";
18
19
  import { defineTabTool, defineTool } from "./tool.js";
19
20
  import { generateLocator } from "./utils.js";
20
21
  const snapshot = defineTool({
@@ -81,6 +82,10 @@ const click = defineTabTool({
81
82
  else
82
83
  await locator.click(options);
83
84
  });
85
+ const screenshotUrl = await captureAndUploadScreenshot(tab.page);
86
+ if (screenshotUrl) {
87
+ response.addScreenshotUrl(screenshotUrl);
88
+ }
84
89
  },
85
90
  });
86
91
  const drag = defineTabTool({
@@ -0,0 +1,43 @@
1
+ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
+ import { createGuid } from "./guid.js";
3
+ const S3_REGION = "us-west-1";
4
+ const ENABLE_SCREENSHOT_VALUES = new Set(["1", "true", "yes", "on"]);
5
+ export const isScreenshotEnabled = () => ENABLE_SCREENSHOT_VALUES.has((process.env.ENABLE_SCREENSHOT_ON_NAVIGATE ?? "").toLowerCase());
6
+ /**
7
+ * Captures a screenshot, uploads it to S3, and returns the URL.
8
+ * Waits 1 second before taking the screenshot to let the page settle.
9
+ */
10
+ export async function captureAndUploadScreenshot(page) {
11
+ if (!isScreenshotEnabled())
12
+ return null;
13
+ const bucket = process.env.SCREENSHOT_S3_BUCKET;
14
+ const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
15
+ const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
16
+ const hasCredentials = Boolean(AWS_ACCESS_KEY_ID) && Boolean(AWS_SECRET_ACCESS_KEY);
17
+ if (!bucket || !hasCredentials)
18
+ return null;
19
+ await page.waitForLoadState("networkidle");
20
+ const s3Client = new S3Client({ region: S3_REGION });
21
+ const screenshotUrl = await page
22
+ .screenshot({
23
+ type: "jpeg",
24
+ quality: 80,
25
+ scale: "css",
26
+ timeout: 10000,
27
+ })
28
+ .then(async (screenshot) => {
29
+ const objectKey = `playwright-mcp/screenshot/${createGuid()}.jpg`;
30
+ await s3Client.send(new PutObjectCommand({
31
+ Bucket: bucket,
32
+ Key: objectKey,
33
+ Body: screenshot,
34
+ ContentType: "image/jpeg",
35
+ }));
36
+ return `https://${bucket}.s3.${S3_REGION}.amazonaws.com/${objectKey}`;
37
+ })
38
+ .catch((error) => {
39
+ console.error("Failed to capture and upload screenshot", error);
40
+ return null;
41
+ });
42
+ return screenshotUrl;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordbricks/playwright-mcp",
3
- "version": "0.1.27",
3
+ "version": "0.1.30",
4
4
  "description": "Playwright Tools for MCP",
5
5
  "type": "module",
6
6
  "files": [
@@ -18,11 +18,11 @@
18
18
  "build": "tsc && bun run copy-filters",
19
19
  "copy-filters": "mkdir -p lib/filters && cp -r src/filters/*.txt lib/filters/ 2>/dev/null || true",
20
20
  "build:extension": "tsc --project extension",
21
- "dev": "concurrently --raw prefix none \"tsc --watch\" \"DEBUG=pw:mcp:* node --watch=lib cli.js --port 9224 --isolated\"",
22
- "dev:headless": "concurrently --raw prefix none \"tsc --watch\" \"DEBUG=pw:mcp:* node --watch=lib cli.js --port 9224 --isolated --headless\"",
23
- "start": "node cli.js --port 9224",
24
- "start:isolated": "node cli.js --port 9224 --isolated",
25
- "start:vision": "node cli.js --port 9224 --caps=vision",
21
+ "dev": "concurrently --raw prefix none \"tsc --watch\" \"DEBUG=pw:mcp:* node --watch=lib cli.js --port 9224 --isolated --secret .env.development\"",
22
+ "dev:headless": "concurrently --raw prefix none \"tsc --watch\" \"DEBUG=pw:mcp:* node --watch=lib cli.js --port 9224 --isolated --headless --secret .env.development\"",
23
+ "start": "node cli.js --port 9224 --secret .env.production",
24
+ "start:isolated": "node cli.js --port 9224 --isolated --secret .env.production",
25
+ "start:vision": "node cli.js --port 9224 --caps=vision --secret .env.production",
26
26
  "lint": "biome check --diagnostic-level=error",
27
27
  "format": "biome check --write --diagnostic-level=error",
28
28
  "check-types": "tsc --noEmit",
@@ -46,9 +46,10 @@
46
46
  "./cli.js": "./cli.js"
47
47
  },
48
48
  "dependencies": {
49
- "@fxts/core": "1.21.1",
50
- "@ghostery/adblocker": "2.13.0",
51
- "@modelcontextprotocol/sdk": "1.24.3",
49
+ "@fxts/core": "1.23.0",
50
+ "@ghostery/adblocker": "2.13.2",
51
+ "@aws-sdk/client-s3": "3.958.0",
52
+ "@modelcontextprotocol/sdk": "1.25.1",
52
53
  "cheerio": "1.1.2",
53
54
  "commander": "14.0.2",
54
55
  "content-type": "1.0.5",
@@ -60,7 +61,7 @@
60
61
  "lodash": "4.17.21",
61
62
  "mime": "4.1.0",
62
63
  "ms": "2.1.3",
63
- "playwright-core": "1.56.1",
64
+ "playwright-core": "1.57.0",
64
65
  "raw-body": "3.0.2",
65
66
  "typescript-parsec": "0.3.4",
66
67
  "ws": "8.18.3",
@@ -75,15 +76,15 @@
75
76
  "@types/content-type": "1.1.9",
76
77
  "@types/debug": "4.1.12",
77
78
  "@types/ms": "2.1.0",
78
- "@types/bun": "1.3.4",
79
- "@types/node": "24.10.1",
79
+ "@types/bun": "1.3.5",
80
+ "@types/node": "25.0.3",
80
81
  "@types/ws": "8.18.1",
81
82
  "concurrently": "9.2.1",
82
83
  "domelementtype": "2.3.0",
83
84
  "domhandler": "5.0.3",
84
- "devtools-protocol": "0.0.1558402",
85
- "esbuild": "0.27.1",
86
- "openai": "6.10.0",
85
+ "devtools-protocol": "0.0.1561482",
86
+ "esbuild": "0.27.2",
87
+ "openai": "6.15.0",
87
88
  "typescript": "5.9.3"
88
89
  },
89
90
  "bin": {