@wordbricks/playwright-mcp 0.1.20 → 0.1.22
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/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +59 -52
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +21 -21
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +17 -17
- package/lib/hooks/networkSetup.js +9 -7
- package/lib/hooks/networkTrackingHook.js +21 -21
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +15 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +38 -38
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +58 -49
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +46 -36
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
package/cli-wrapper.js
CHANGED
|
@@ -15,33 +15,34 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { spawn } from
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
import { dirname, join } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
21
|
|
|
22
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
23
|
const __dirname = dirname(__filename);
|
|
24
24
|
|
|
25
|
-
const isBunRuntime =
|
|
26
|
-
const isRunViaBundle =
|
|
25
|
+
const isBunRuntime = "bun" in process.versions;
|
|
26
|
+
const isRunViaBundle =
|
|
27
|
+
process.env.npm_execpath && process.env.npm_execpath.includes("bunx");
|
|
27
28
|
|
|
28
29
|
if (!isBunRuntime && isRunViaBundle) {
|
|
29
|
-
const cliPath = join(__dirname,
|
|
30
|
+
const cliPath = join(__dirname, "cli.js");
|
|
30
31
|
const args = process.argv.slice(2);
|
|
31
32
|
|
|
32
|
-
const bunProcess = spawn(
|
|
33
|
-
stdio:
|
|
34
|
-
env: { ...process.env, FORCE_BUN_RUNTIME:
|
|
33
|
+
const bunProcess = spawn("bun", [cliPath, ...args], {
|
|
34
|
+
stdio: "inherit",
|
|
35
|
+
env: { ...process.env, FORCE_BUN_RUNTIME: "1" },
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
bunProcess.on(
|
|
38
|
+
bunProcess.on("exit", (code) => {
|
|
38
39
|
process.exit(code || 0);
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
bunProcess.on(
|
|
42
|
-
console.error(
|
|
43
|
-
import(
|
|
42
|
+
bunProcess.on("error", (err) => {
|
|
43
|
+
console.error("Failed to run with bun:", err.message);
|
|
44
|
+
import("./lib/program.js");
|
|
44
45
|
});
|
|
45
46
|
} else {
|
|
46
|
-
import(
|
|
47
|
+
import("./lib/program.js");
|
|
47
48
|
}
|
package/cli.js
CHANGED
package/config.d.ts
CHANGED
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type * as playwright from
|
|
17
|
+
import type * as playwright from "playwright-core";
|
|
18
18
|
|
|
19
|
-
export type ToolCapability =
|
|
19
|
+
export type ToolCapability =
|
|
20
|
+
| "core"
|
|
21
|
+
| "core-tabs"
|
|
22
|
+
| "core-install"
|
|
23
|
+
| "vision"
|
|
24
|
+
| "pdf";
|
|
20
25
|
|
|
21
26
|
export type Config = {
|
|
22
27
|
/**
|
|
@@ -26,7 +31,7 @@ export type Config = {
|
|
|
26
31
|
/**
|
|
27
32
|
* The type of browser to use.
|
|
28
33
|
*/
|
|
29
|
-
browserName?:
|
|
34
|
+
browserName?: "chromium" | "firefox" | "webkit";
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* Keep the browser profile in memory, do not save it to disk.
|
|
@@ -73,7 +78,7 @@ export type Config = {
|
|
|
73
78
|
* URL to launch in Chrome app mode.
|
|
74
79
|
*/
|
|
75
80
|
app?: string;
|
|
76
|
-
}
|
|
81
|
+
};
|
|
77
82
|
|
|
78
83
|
server?: {
|
|
79
84
|
/**
|
|
@@ -85,7 +90,7 @@ export type Config = {
|
|
|
85
90
|
* The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
|
|
86
91
|
*/
|
|
87
92
|
host?: string;
|
|
88
|
-
}
|
|
93
|
+
};
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* List of enabled tool capabilities. Possible values:
|
|
@@ -125,5 +130,5 @@ export type Config = {
|
|
|
125
130
|
/**
|
|
126
131
|
* Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
|
|
127
132
|
*/
|
|
128
|
-
imageResponses?:
|
|
133
|
+
imageResponses?: "allow" | "omit";
|
|
129
134
|
};
|
package/index.d.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { Server } from
|
|
19
|
-
import type { Config } from
|
|
20
|
-
import type { BrowserContext } from
|
|
18
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
+
import type { Config } from "./config.js";
|
|
20
|
+
import type { BrowserContext } from "playwright-core";
|
|
21
21
|
|
|
22
|
-
export declare function createConnection(
|
|
23
|
-
|
|
22
|
+
export declare function createConnection(
|
|
23
|
+
config?: Config,
|
|
24
|
+
contextGetter?: () => Promise<BrowserContext>,
|
|
25
|
+
): Promise<Server>;
|
package/index.js
CHANGED
|
@@ -13,22 +13,75 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import fs from
|
|
17
|
-
import net from
|
|
18
|
-
import path from
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
// @ts-
|
|
22
|
-
import {
|
|
23
|
-
// @ts-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { createHash } from
|
|
27
|
-
import {
|
|
28
|
-
const TIMEOUT_STR =
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import net from "node:net";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import ms from "ms";
|
|
20
|
+
import * as playwright from "playwright-core";
|
|
21
|
+
// @ts-expect-error
|
|
22
|
+
import { startTraceViewerServer } from "playwright-core/lib/server";
|
|
23
|
+
// @ts-expect-error
|
|
24
|
+
import { registryDirectory } from "playwright-core/lib/server/registry/index";
|
|
25
|
+
import { outputFile } from "./config.js";
|
|
26
|
+
import { createHash } from "./utils/guid.js";
|
|
27
|
+
import { logUnhandledError, testDebug } from "./utils/log.js";
|
|
28
|
+
const TIMEOUT_STR = "30m";
|
|
29
|
+
// Disable password manager in Chrome Preferences file
|
|
30
|
+
async function disablePasswordManagerInPrefs(userDataDir) {
|
|
31
|
+
const prefsPath = path.join(userDataDir, "Default", "Preferences");
|
|
32
|
+
const defaultDir = path.join(userDataDir, "Default");
|
|
33
|
+
// Ensure Default directory exists
|
|
34
|
+
await fs.promises.mkdir(defaultDir, { recursive: true });
|
|
35
|
+
let prefs = {};
|
|
36
|
+
try {
|
|
37
|
+
const existing = await fs.promises.readFile(prefsPath, "utf8");
|
|
38
|
+
prefs = JSON.parse(existing);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// File doesn't exist or is invalid, start fresh
|
|
42
|
+
}
|
|
43
|
+
// Disable password manager settings
|
|
44
|
+
prefs.credentials_enable_service = false;
|
|
45
|
+
prefs.credentials_enable_autosignin = false;
|
|
46
|
+
prefs.password_manager_enabled = false;
|
|
47
|
+
// Nested profile preferences
|
|
48
|
+
prefs.profile = prefs.profile || {};
|
|
49
|
+
prefs.profile.password_manager_enabled = false;
|
|
50
|
+
// Password manager specific settings
|
|
51
|
+
prefs.password_manager =
|
|
52
|
+
prefs.password_manager || {};
|
|
53
|
+
prefs.password_manager.saving_enabled = false;
|
|
54
|
+
prefs.password_manager.autofilling_enabled =
|
|
55
|
+
false;
|
|
56
|
+
await fs.promises.writeFile(prefsPath, JSON.stringify(prefs, null, 2));
|
|
57
|
+
}
|
|
29
58
|
async function applyInitScript(browserContext, config) {
|
|
59
|
+
// Clear stale auth mode from previous sessions on first page load
|
|
60
|
+
// Uses a session marker to avoid clearing during active auth navigation
|
|
61
|
+
const sessionId = Date.now().toString();
|
|
62
|
+
await browserContext.addInitScript((sid) => {
|
|
63
|
+
try {
|
|
64
|
+
const currentSession = sessionStorage.getItem("__nr_session__");
|
|
65
|
+
const authMode = localStorage.getItem("__nr_auth_mode__");
|
|
66
|
+
console.log("[NextRows:Init] Session check", {
|
|
67
|
+
currentSession,
|
|
68
|
+
newSessionId: sid,
|
|
69
|
+
authModeBefore: authMode,
|
|
70
|
+
});
|
|
71
|
+
if (currentSession !== sid) {
|
|
72
|
+
// New session - clear any stale auth state
|
|
73
|
+
localStorage.removeItem("__nr_auth_mode__");
|
|
74
|
+
localStorage.removeItem("__nr_auth_continue__");
|
|
75
|
+
sessionStorage.setItem("__nr_session__", sid);
|
|
76
|
+
console.log("[NextRows:Init] Cleared stale auth state for new session");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.log("[NextRows:Init] Error in session check", e);
|
|
81
|
+
}
|
|
82
|
+
}, sessionId);
|
|
30
83
|
if (config.browser.initScript) {
|
|
31
|
-
const scriptContent = await fs.promises.readFile(config.browser.initScript,
|
|
84
|
+
const scriptContent = await fs.promises.readFile(config.browser.initScript, "utf8");
|
|
32
85
|
await browserContext.addInitScript(scriptContent);
|
|
33
86
|
}
|
|
34
87
|
}
|
|
@@ -40,23 +93,23 @@ async function hidePlaywrightMarkers(browserContext) {
|
|
|
40
93
|
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
|
|
41
94
|
Object.getOwnPropertyNames = (obj) => {
|
|
42
95
|
const props = originalGetOwnPropertyNames(obj);
|
|
43
|
-
return props.filter((prop) => prop !==
|
|
96
|
+
return props.filter((prop) => prop !== "__pwInitScripts");
|
|
44
97
|
};
|
|
45
98
|
// Use a Proxy to handle access to `window`
|
|
46
99
|
const windowHandler = {
|
|
47
100
|
get(target, prop) {
|
|
48
|
-
if (prop ===
|
|
101
|
+
if (prop === "__pwInitScripts")
|
|
49
102
|
return undefined; // Hide the property
|
|
50
103
|
return Reflect.get(target, prop);
|
|
51
104
|
},
|
|
52
105
|
has(target, prop) {
|
|
53
|
-
if (prop ===
|
|
106
|
+
if (prop === "__pwInitScripts")
|
|
54
107
|
return false; // Prevent detection via "in" operator
|
|
55
108
|
return Reflect.has(target, prop);
|
|
56
109
|
},
|
|
57
110
|
};
|
|
58
111
|
const proxiedWindow = new Proxy(window, windowHandler);
|
|
59
|
-
Object.defineProperty(globalThis,
|
|
112
|
+
Object.defineProperty(globalThis, "window", {
|
|
60
113
|
value: proxiedWindow,
|
|
61
114
|
configurable: false,
|
|
62
115
|
writable: false,
|
|
@@ -88,14 +141,16 @@ class BaseContextFactory {
|
|
|
88
141
|
return this._browserPromise;
|
|
89
142
|
testDebug(`obtain browser (${this.name})`);
|
|
90
143
|
this._browserPromise = this._doObtainBrowser(clientInfo);
|
|
91
|
-
void this._browserPromise
|
|
92
|
-
|
|
144
|
+
void this._browserPromise
|
|
145
|
+
.then((browser) => {
|
|
146
|
+
browser.on("disconnected", () => {
|
|
93
147
|
this._browserPromise = undefined;
|
|
94
148
|
this._clearAutoCloseTimer();
|
|
95
149
|
});
|
|
96
150
|
// Start auto-close timer
|
|
97
151
|
this._startAutoCloseTimer(browser);
|
|
98
|
-
})
|
|
152
|
+
})
|
|
153
|
+
.catch(() => {
|
|
99
154
|
this._browserPromise = undefined;
|
|
100
155
|
this._clearAutoCloseTimer();
|
|
101
156
|
});
|
|
@@ -124,16 +179,19 @@ class BaseContextFactory {
|
|
|
124
179
|
}
|
|
125
180
|
}
|
|
126
181
|
async _doObtainBrowser(clientInfo) {
|
|
127
|
-
throw new Error(
|
|
182
|
+
throw new Error("Not implemented");
|
|
128
183
|
}
|
|
129
184
|
async createContext(clientInfo) {
|
|
130
185
|
testDebug(`create browser context (${this.name})`);
|
|
131
186
|
const browser = await this._obtainBrowser(clientInfo);
|
|
132
187
|
const browserContext = await this._doCreateContext(browser);
|
|
133
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
browserContext,
|
|
190
|
+
close: () => this._closeBrowserContext(browserContext, browser),
|
|
191
|
+
};
|
|
134
192
|
}
|
|
135
193
|
async _doCreateContext(browser) {
|
|
136
|
-
throw new Error(
|
|
194
|
+
throw new Error("Not implemented");
|
|
137
195
|
}
|
|
138
196
|
async _closeBrowserContext(browserContext, browser) {
|
|
139
197
|
testDebug(`close browser context (${this.name})`);
|
|
@@ -151,25 +209,29 @@ class BaseContextFactory {
|
|
|
151
209
|
}
|
|
152
210
|
class IsolatedContextFactory extends BaseContextFactory {
|
|
153
211
|
constructor(config) {
|
|
154
|
-
super(
|
|
212
|
+
super("isolated", "Create a new isolated browser context", config);
|
|
155
213
|
}
|
|
156
214
|
async _doObtainBrowser(clientInfo) {
|
|
157
215
|
await injectCdpPort(this.config.browser);
|
|
158
216
|
const browserType = playwright[this.config.browser.browserName];
|
|
159
|
-
return browserType
|
|
217
|
+
return browserType
|
|
218
|
+
.launch({
|
|
160
219
|
tracesDir: await startTraceServer(this.config, clientInfo.rootPath),
|
|
161
220
|
...this.config.browser.launchOptions,
|
|
162
221
|
handleSIGINT: false,
|
|
163
222
|
handleSIGTERM: false,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (error.message.includes(
|
|
223
|
+
})
|
|
224
|
+
.catch((error) => {
|
|
225
|
+
if (error.message.includes("Executable doesn't exist"))
|
|
167
226
|
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
168
227
|
throw error;
|
|
169
228
|
});
|
|
170
229
|
}
|
|
171
230
|
async _doCreateContext(browser) {
|
|
172
|
-
const browserContext = await browser.newContext(
|
|
231
|
+
const browserContext = await browser.newContext({
|
|
232
|
+
...this.config.browser.contextOptions,
|
|
233
|
+
bypassCSP: true,
|
|
234
|
+
});
|
|
173
235
|
await applyInitScript(browserContext, this.config);
|
|
174
236
|
await hidePlaywrightMarkers(browserContext);
|
|
175
237
|
return browserContext;
|
|
@@ -177,13 +239,15 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
177
239
|
}
|
|
178
240
|
class CdpContextFactory extends BaseContextFactory {
|
|
179
241
|
constructor(config) {
|
|
180
|
-
super(
|
|
242
|
+
super("cdp", "Connect to a browser over CDP", config);
|
|
181
243
|
}
|
|
182
244
|
async _doObtainBrowser() {
|
|
183
245
|
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
184
246
|
}
|
|
185
247
|
async _doCreateContext(browser) {
|
|
186
|
-
const browserContext = this.config.browser.isolated
|
|
248
|
+
const browserContext = this.config.browser.isolated
|
|
249
|
+
? await browser.newContext({ bypassCSP: true })
|
|
250
|
+
: browser.contexts()[0];
|
|
187
251
|
await applyInitScript(browserContext, this.config);
|
|
188
252
|
await hidePlaywrightMarkers(browserContext);
|
|
189
253
|
return browserContext;
|
|
@@ -191,17 +255,17 @@ class CdpContextFactory extends BaseContextFactory {
|
|
|
191
255
|
}
|
|
192
256
|
class RemoteContextFactory extends BaseContextFactory {
|
|
193
257
|
constructor(config) {
|
|
194
|
-
super(
|
|
258
|
+
super("remote", "Connect to a browser using a remote endpoint", config);
|
|
195
259
|
}
|
|
196
260
|
async _doObtainBrowser() {
|
|
197
261
|
const url = new URL(this.config.browser.remoteEndpoint);
|
|
198
|
-
url.searchParams.set(
|
|
262
|
+
url.searchParams.set("browser", this.config.browser.browserName);
|
|
199
263
|
if (this.config.browser.launchOptions)
|
|
200
|
-
url.searchParams.set(
|
|
264
|
+
url.searchParams.set("launch-options", JSON.stringify(this.config.browser.launchOptions));
|
|
201
265
|
return playwright[this.config.browser.browserName].connect(String(url));
|
|
202
266
|
}
|
|
203
267
|
async _doCreateContext(browser) {
|
|
204
|
-
const browserContext = await browser.newContext();
|
|
268
|
+
const browserContext = await browser.newContext({ bypassCSP: true });
|
|
205
269
|
await applyInitScript(browserContext, this.config);
|
|
206
270
|
await hidePlaywrightMarkers(browserContext);
|
|
207
271
|
return browserContext;
|
|
@@ -209,8 +273,8 @@ class RemoteContextFactory extends BaseContextFactory {
|
|
|
209
273
|
}
|
|
210
274
|
class PersistentContextFactory {
|
|
211
275
|
config;
|
|
212
|
-
name =
|
|
213
|
-
description =
|
|
276
|
+
name = "persistent";
|
|
277
|
+
description = "Create a new persistent browser context";
|
|
214
278
|
_userDataDirs = new Set();
|
|
215
279
|
_autoCloseTimer;
|
|
216
280
|
constructor(config) {
|
|
@@ -218,11 +282,14 @@ class PersistentContextFactory {
|
|
|
218
282
|
}
|
|
219
283
|
async createContext(clientInfo) {
|
|
220
284
|
await injectCdpPort(this.config.browser);
|
|
221
|
-
testDebug(
|
|
222
|
-
const userDataDir = this.config.browser.userDataDir ??
|
|
285
|
+
testDebug("create browser context (persistent)");
|
|
286
|
+
const userDataDir = this.config.browser.userDataDir ??
|
|
287
|
+
(await this._createUserDataDir(clientInfo.rootPath));
|
|
223
288
|
const tracesDir = await startTraceServer(this.config, clientInfo.rootPath);
|
|
224
289
|
this._userDataDirs.add(userDataDir);
|
|
225
|
-
testDebug(
|
|
290
|
+
testDebug("lock user data dir", userDataDir);
|
|
291
|
+
// Disable password manager in Chrome preferences before launch
|
|
292
|
+
await disablePasswordManagerInPrefs(userDataDir);
|
|
226
293
|
const browserType = playwright[this.config.browser.browserName];
|
|
227
294
|
for (let i = 0; i < 5; i++) {
|
|
228
295
|
try {
|
|
@@ -230,9 +297,9 @@ class PersistentContextFactory {
|
|
|
230
297
|
tracesDir,
|
|
231
298
|
...this.config.browser.launchOptions,
|
|
232
299
|
...this.config.browser.contextOptions,
|
|
300
|
+
bypassCSP: true,
|
|
233
301
|
handleSIGINT: false,
|
|
234
302
|
handleSIGTERM: false,
|
|
235
|
-
args: this.config.browser.launchOptions?.args || [],
|
|
236
303
|
});
|
|
237
304
|
await applyInitScript(browserContext, this.config);
|
|
238
305
|
await hidePlaywrightMarkers(browserContext);
|
|
@@ -242,11 +309,12 @@ class PersistentContextFactory {
|
|
|
242
309
|
return { browserContext, close };
|
|
243
310
|
}
|
|
244
311
|
catch (error) {
|
|
245
|
-
if (error.message.includes(
|
|
312
|
+
if (error.message.includes("Executable doesn't exist"))
|
|
246
313
|
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
247
|
-
if (error.message.includes(
|
|
314
|
+
if (error.message.includes("ProcessSingleton") ||
|
|
315
|
+
error.message.includes("Invalid URL")) {
|
|
248
316
|
// User data directory is already in use, try again.
|
|
249
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
317
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
250
318
|
continue;
|
|
251
319
|
}
|
|
252
320
|
throw error;
|
|
@@ -276,31 +344,36 @@ class PersistentContextFactory {
|
|
|
276
344
|
}
|
|
277
345
|
}
|
|
278
346
|
async _closeBrowserContext(browserContext, userDataDir) {
|
|
279
|
-
testDebug(
|
|
280
|
-
testDebug(
|
|
347
|
+
testDebug("close browser context (persistent)");
|
|
348
|
+
testDebug("release user data dir", userDataDir);
|
|
281
349
|
this._clearAutoCloseTimer();
|
|
282
350
|
await browserContext.close().catch(() => { });
|
|
283
351
|
this._userDataDirs.delete(userDataDir);
|
|
284
|
-
testDebug(
|
|
352
|
+
testDebug("close browser context complete (persistent)");
|
|
285
353
|
}
|
|
286
354
|
async _createUserDataDir(rootPath) {
|
|
287
355
|
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
|
|
288
|
-
const browserToken = this.config.browser.launchOptions?.channel ??
|
|
356
|
+
const browserToken = this.config.browser.launchOptions?.channel ??
|
|
357
|
+
this.config.browser?.browserName;
|
|
289
358
|
// Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead.
|
|
290
|
-
const rootPathToken = rootPath ? `-${createHash(rootPath)}` :
|
|
359
|
+
const rootPathToken = rootPath ? `-${createHash(rootPath)}` : "";
|
|
291
360
|
const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`);
|
|
292
361
|
await fs.promises.mkdir(result, { recursive: true });
|
|
293
362
|
return result;
|
|
294
363
|
}
|
|
295
364
|
}
|
|
296
365
|
async function injectCdpPort(browserConfig) {
|
|
297
|
-
const isBunRuntime =
|
|
298
|
-
|
|
366
|
+
const isBunRuntime = "bun" in process.versions;
|
|
367
|
+
const isWindows = process.platform === "win32";
|
|
368
|
+
// On Windows, always use CDP port because Bun has issues with --remote-debugging-pipe
|
|
369
|
+
// On other platforms, Bun handles pipes correctly
|
|
370
|
+
if (!isWindows && isBunRuntime)
|
|
299
371
|
return;
|
|
300
|
-
if (browserConfig.browserName !==
|
|
372
|
+
if (browserConfig.browserName !== "chromium")
|
|
301
373
|
return;
|
|
302
374
|
const launchOptions = browserConfig.launchOptions || {};
|
|
303
|
-
|
|
375
|
+
const cdpPort = await findFreePort();
|
|
376
|
+
launchOptions.cdpPort = cdpPort;
|
|
304
377
|
browserConfig.launchOptions = launchOptions;
|
|
305
378
|
}
|
|
306
379
|
async function findFreePort() {
|
|
@@ -310,7 +383,7 @@ async function findFreePort() {
|
|
|
310
383
|
const { port } = server.address();
|
|
311
384
|
server.close(() => resolve(port));
|
|
312
385
|
});
|
|
313
|
-
server.on(
|
|
386
|
+
server.on("error", reject);
|
|
314
387
|
});
|
|
315
388
|
}
|
|
316
389
|
async function startTraceServer(config, rootPath) {
|
|
@@ -318,9 +391,9 @@ async function startTraceServer(config, rootPath) {
|
|
|
318
391
|
return undefined;
|
|
319
392
|
const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`);
|
|
320
393
|
const server = await startTraceViewerServer();
|
|
321
|
-
const urlPrefix = server.urlPrefix(
|
|
322
|
-
const url = urlPrefix +
|
|
394
|
+
const urlPrefix = server.urlPrefix("human-readable");
|
|
395
|
+
const url = urlPrefix + "/trace/index.html?trace=" + tracesDir + "/trace.json";
|
|
323
396
|
// eslint-disable-next-line no-console
|
|
324
|
-
console.error(
|
|
397
|
+
console.error("\nTrace viewer listening on " + url);
|
|
325
398
|
return tracesDir;
|
|
326
399
|
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { fileURLToPath } from
|
|
17
|
-
import { Context } from
|
|
18
|
-
import {
|
|
19
|
-
import { Response } from
|
|
20
|
-
import { SessionLog } from
|
|
21
|
-
import { filteredTools } from
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { Context } from "./context.js";
|
|
18
|
+
import { toMcpTool } from "./mcp/tool.js";
|
|
19
|
+
import { Response } from "./response.js";
|
|
20
|
+
import { SessionLog } from "./sessionLog.js";
|
|
21
|
+
import { filteredTools } from "./tools.js";
|
|
22
|
+
import { logUnhandledError } from "./utils/log.js";
|
|
23
|
+
import { packageJSON } from "./utils/package.js";
|
|
24
24
|
export class BrowserServerBackend {
|
|
25
|
-
name =
|
|
25
|
+
name = "Playwright";
|
|
26
26
|
version = packageJSON.version;
|
|
27
27
|
_tools;
|
|
28
28
|
_context;
|
|
@@ -43,7 +43,9 @@ export class BrowserServerBackend {
|
|
|
43
43
|
const url = firstRootUri ? new URL(firstRootUri) : undefined;
|
|
44
44
|
rootPath = url ? fileURLToPath(url) : undefined;
|
|
45
45
|
}
|
|
46
|
-
this._sessionLog = this._config.saveSession
|
|
46
|
+
this._sessionLog = this._config.saveSession
|
|
47
|
+
? await SessionLog.create(this._config, rootPath)
|
|
48
|
+
: undefined;
|
|
47
49
|
this._context = new Context({
|
|
48
50
|
tools: this._tools,
|
|
49
51
|
config: this._config,
|
|
@@ -55,11 +57,11 @@ export class BrowserServerBackend {
|
|
|
55
57
|
await this._context.ensureTab();
|
|
56
58
|
}
|
|
57
59
|
async listTools() {
|
|
58
|
-
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
60
|
+
return this._tools.map((tool) => toMcpTool(tool.schema));
|
|
59
61
|
}
|
|
60
62
|
async callTool(name, rawArguments) {
|
|
61
63
|
const context = this._context;
|
|
62
|
-
const tool = context.tools.find(tool => tool.schema.name === name);
|
|
64
|
+
const tool = context.tools.find((tool) => tool.schema.name === name);
|
|
63
65
|
if (!tool)
|
|
64
66
|
throw new Error(`Tool "${name}" not found`);
|
|
65
67
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|