@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.
- package/lib/browserContextFactory.js +22 -5
- package/lib/config.js +1 -1
- package/lib/program.js +20 -0
- package/lib/response.js +11 -1
- package/lib/tools/getSnapshot.js +1 -1
- package/lib/tools/navigate.js +5 -0
- package/lib/tools/scroll.js +5 -0
- package/lib/tools/snapshot.js +5 -0
- package/lib/utils/screenshot.js +43 -0
- package/package.json +16 -15
|
@@ -109,11 +109,16 @@ async function hidePlaywrightMarkers(browserContext) {
|
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
const proxiedWindow = new Proxy(window, windowHandler);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
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
|
-
{
|
|
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") {
|
package/lib/tools/getSnapshot.js
CHANGED
|
@@ -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("```");
|
package/lib/tools/navigate.js
CHANGED
|
@@ -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
|
},
|
package/lib/tools/scroll.js
CHANGED
|
@@ -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];
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -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.
|
|
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.
|
|
50
|
-
"@ghostery/adblocker": "2.13.
|
|
51
|
-
"@
|
|
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.
|
|
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.
|
|
79
|
-
"@types/node": "
|
|
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.
|
|
85
|
-
"esbuild": "0.27.
|
|
86
|
-
"openai": "6.
|
|
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": {
|