@wordbricks/playwright-mcp 0.1.19 → 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.
Files changed (88) hide show
  1. package/README.md +54 -44
  2. package/cli-wrapper.js +15 -14
  3. package/cli.js +1 -1
  4. package/config.d.ts +11 -6
  5. package/index.d.ts +7 -5
  6. package/index.js +1 -1
  7. package/lib/browserContextFactory.js +131 -58
  8. package/lib/browserServerBackend.js +14 -12
  9. package/lib/config.js +60 -46
  10. package/lib/context.js +41 -39
  11. package/lib/extension/cdpRelay.js +67 -61
  12. package/lib/extension/extensionContextFactory.js +10 -10
  13. package/lib/frameworkPatterns.js +21 -21
  14. package/lib/hooks/antiBotDetectionHook.js +178 -0
  15. package/lib/hooks/core.js +11 -10
  16. package/lib/hooks/eventConsumer.js +29 -16
  17. package/lib/hooks/events.js +3 -3
  18. package/lib/hooks/formatToolCallEvent.js +3 -7
  19. package/lib/hooks/frameworkStateHook.js +40 -40
  20. package/lib/hooks/grouping.js +3 -3
  21. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  22. package/lib/hooks/networkFilters.js +24 -15
  23. package/lib/hooks/networkSetup.js +11 -6
  24. package/lib/hooks/networkTrackingHook.js +31 -19
  25. package/lib/hooks/pageHeightHook.js +9 -9
  26. package/lib/hooks/registry.js +18 -16
  27. package/lib/hooks/requireTabHook.js +3 -3
  28. package/lib/hooks/schema.js +44 -32
  29. package/lib/hooks/waitHook.js +7 -7
  30. package/lib/index.js +12 -10
  31. package/lib/mcp/inProcessTransport.js +3 -4
  32. package/lib/mcp/proxyBackend.js +43 -28
  33. package/lib/mcp/server.js +24 -19
  34. package/lib/mcp/tool.js +14 -8
  35. package/lib/mcp/transport.js +60 -53
  36. package/lib/playwrightTransformer.js +129 -106
  37. package/lib/program.js +54 -52
  38. package/lib/response.js +36 -30
  39. package/lib/sessionLog.js +19 -17
  40. package/lib/tab.js +41 -39
  41. package/lib/tools/common.js +19 -19
  42. package/lib/tools/console.js +11 -11
  43. package/lib/tools/dialogs.js +18 -15
  44. package/lib/tools/evaluate.js +26 -17
  45. package/lib/tools/extractFrameworkState.js +48 -37
  46. package/lib/tools/files.js +17 -14
  47. package/lib/tools/form.js +32 -23
  48. package/lib/tools/getSnapshot.js +14 -15
  49. package/lib/tools/getVisibleHtml.js +33 -17
  50. package/lib/tools/install.js +20 -20
  51. package/lib/tools/keyboard.js +29 -24
  52. package/lib/tools/mouse.js +29 -31
  53. package/lib/tools/navigate.js +19 -23
  54. package/lib/tools/network.js +12 -14
  55. package/lib/tools/networkDetail.js +68 -61
  56. package/lib/tools/networkSearch/bodySearch.js +46 -32
  57. package/lib/tools/networkSearch/grouping.js +15 -6
  58. package/lib/tools/networkSearch/helpers.js +4 -4
  59. package/lib/tools/networkSearch/searchHtml.js +25 -16
  60. package/lib/tools/networkSearch/urlSearch.js +56 -14
  61. package/lib/tools/networkSearch.js +65 -35
  62. package/lib/tools/pdf.js +13 -12
  63. package/lib/tools/repl.js +66 -54
  64. package/lib/tools/screenshot.js +57 -33
  65. package/lib/tools/scroll.js +29 -24
  66. package/lib/tools/snapshot.js +66 -49
  67. package/lib/tools/tabs.js +22 -19
  68. package/lib/tools/tool.js +5 -3
  69. package/lib/tools/utils.js +17 -13
  70. package/lib/tools/wait.js +24 -19
  71. package/lib/tools.js +21 -20
  72. package/lib/utils/adBlockFilter.js +29 -26
  73. package/lib/utils/codegen.js +20 -16
  74. package/lib/utils/extensionPath.js +4 -4
  75. package/lib/utils/fileUtils.js +17 -13
  76. package/lib/utils/graphql.js +69 -58
  77. package/lib/utils/guid.js +3 -3
  78. package/lib/utils/httpServer.js +9 -9
  79. package/lib/utils/log.js +3 -3
  80. package/lib/utils/manualPromise.js +7 -7
  81. package/lib/utils/networkFormat.js +7 -5
  82. package/lib/utils/package.js +4 -4
  83. package/lib/utils/sanitizeHtml.js +66 -34
  84. package/lib/utils/truncate.js +25 -25
  85. package/lib/utils/withTimeout.js +1 -1
  86. package/package.json +34 -57
  87. package/src/index.ts +27 -17
  88. package/LICENSE +0 -202
package/README.md CHANGED
@@ -142,49 +142,58 @@ Playwright MCP server supports following arguments. They can be provided in the
142
142
 
143
143
  ```
144
144
  > npx @wordbricks/playwright-mcp@latest --help
145
- --allowed-origins <origins> semicolon-separated list of origins to allow the
146
- browser to request. Default is to allow all.
147
- --blocked-origins <origins> semicolon-separated list of origins to block the
148
- browser from requesting. Blocklist is evaluated
149
- before allowlist. If used without the allowlist,
150
- requests not matching the blocklist are still
151
- allowed.
152
- --block-service-workers block service workers
153
- --browser <browser> browser or chrome channel to use, possible
154
- values: chrome, firefox, webkit, msedge.
155
- --caps <caps> comma-separated list of additional capabilities
156
- to enable, possible values: vision, pdf.
157
- --cdp-endpoint <endpoint> CDP endpoint to connect to.
158
- --config <path> path to the configuration file.
159
- --device <device> device to emulate, for example: "iPhone 15"
160
- --executable-path <path> path to the browser executable.
161
- --headless run browser in headless mode, headed by default
162
- --host <host> host to bind server to. Default is localhost. Use
163
- 0.0.0.0 to bind to all interfaces.
164
- --ignore-https-errors ignore https errors
165
- --isolated keep the browser profile in memory, do not save
166
- it to disk.
167
- --image-responses <mode> whether to send image responses to the client.
168
- Can be "allow" or "omit", Defaults to "allow".
169
- --no-sandbox disable the sandbox for all process types that
170
- are normally sandboxed.
171
- --output-dir <path> path to the directory for output files.
172
- --port <port> port to listen on for SSE transport.
173
- --proxy-bypass <bypass> comma-separated domains to bypass proxy, for
174
- example ".com,chromium.org,.domain.com"
175
- --proxy-server <proxy> specify proxy server, for example
176
- "http://myproxy:3128" or "socks5://myproxy:8080"
177
- --save-session Whether to save the Playwright MCP session into
178
- the output directory.
179
- --save-trace Whether to save the Playwright Trace of the
180
- session into the output directory.
181
- --storage-state <path> path to the storage state file for isolated
182
- sessions.
183
- --user-agent <ua string> specify user agent string
184
- --user-data-dir <path> path to the user data directory. If not
185
- specified, a temporary directory will be created.
186
- --viewport-size <size> specify browser viewport size in pixels, for
187
- example "1280, 720"
145
+ --allowed-origins <origins> semicolon-separated list of origins to allow the
146
+ browser to request. Default is to allow all.
147
+ --blocked-origins <origins> semicolon-separated list of origins to block the
148
+ browser from requesting. Blocklist is evaluated
149
+ before allowlist. If used without the allowlist,
150
+ requests not matching the blocklist are still
151
+ allowed.
152
+ --block-service-workers block service workers
153
+ --browser <browser> browser or chrome channel to use, possible
154
+ values: chrome, firefox, webkit, msedge.
155
+ --caps <caps> comma-separated list of additional capabilities
156
+ to enable, possible values: vision, pdf.
157
+ --cdp-endpoint <endpoint> CDP endpoint to connect to.
158
+ --config <path> path to the configuration file.
159
+ --device <device> device to emulate, for example: "iPhone 15"
160
+ --executable-path <path> path to the browser executable.
161
+ --headless run browser in headless mode, headed by default
162
+ --host <host> host to bind server to. Default is localhost.
163
+ Use 0.0.0.0 to bind to all interfaces.
164
+ --ignore-https-errors ignore https errors
165
+ --init-script <path> path to a JavaScript file to inject into all
166
+ pages using addInitScript.
167
+ --isolated keep the browser profile in memory, do not save
168
+ it to disk.
169
+ --image-responses <mode> whether to send image responses to the client.
170
+ Can be "allow" or "omit", Defaults to "allow".
171
+ --no-sandbox disable the sandbox for all process types that
172
+ are normally sandboxed.
173
+ --output-dir <path> path to the directory for output files.
174
+ --port <port> port to listen on for SSE transport.
175
+ --proxy-bypass <bypass> comma-separated domains to bypass proxy, for
176
+ example ".com,chromium.org,.domain.com"
177
+ --proxy-server <proxy> specify proxy server, for example
178
+ "http://myproxy:3128" or "socks5://myproxy:8080"
179
+ --save-session Whether to save the Playwright MCP session into
180
+ the output directory.
181
+ --save-trace Whether to save the Playwright Trace of the
182
+ session into the output directory.
183
+ --storage-state <path> path to the storage state file for isolated
184
+ sessions.
185
+ --user-agent <ua string> specify user agent string
186
+ --user-data-dir <path> path to the user data directory. If not
187
+ specified, a temporary directory will be
188
+ created.
189
+ --viewport-size <size> specify browser viewport size in pixels, for
190
+ example "1280, 720"
191
+ --window-position <x,y> specify Chrome window position in pixels, for
192
+ example "100,200"
193
+ --window-size <width,height> specify Chrome window size in pixels, for
194
+ example "1280,720"
195
+ --app <url> launch browser in app mode with the specified
196
+ URL
188
197
  ```
189
198
 
190
199
  <!--- End of options generated section -->
@@ -397,6 +406,7 @@ http.createServer(async (req, res) => {
397
406
  - `ref` (string): Exact target element reference from the page snapshot
398
407
  - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click
399
408
  - `button` (string, optional): Button to click, defaults to left
409
+ - `modifiers` (array, optional): Modifier keys to press
400
410
  - Read-only: **false**
401
411
 
402
412
  <!-- NOTE: This has been generated via update-readme.js -->
@@ -564,7 +574,7 @@ http.createServer(async (req, res) => {
564
574
  - Description: Scroll the page using mouse wheel with human-like behavior
565
575
  - Parameters:
566
576
  - `amount` (number): Vertical scroll amount in pixels (positive scrolls down, negative up)
567
- - Read-only: **false**
577
+ - Read-only: **true**
568
578
 
569
579
  <!-- NOTE: This has been generated via update-readme.js -->
570
580
 
package/cli-wrapper.js CHANGED
@@ -15,33 +15,34 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import { spawn } from 'child_process';
19
- import { fileURLToPath } from 'url';
20
- import { dirname, join } from 'path';
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 = 'bun' in process.versions;
26
- const isRunViaBundle = process.env.npm_execpath && process.env.npm_execpath.includes('bunx');
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, 'cli.js');
30
+ const cliPath = join(__dirname, "cli.js");
30
31
  const args = process.argv.slice(2);
31
32
 
32
- const bunProcess = spawn('bun', [cliPath, ...args], {
33
- stdio: 'inherit',
34
- env: { ...process.env, FORCE_BUN_RUNTIME: '1' }
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('exit', (code) => {
38
+ bunProcess.on("exit", (code) => {
38
39
  process.exit(code || 0);
39
40
  });
40
41
 
41
- bunProcess.on('error', (err) => {
42
- console.error('Failed to run with bun:', err.message);
43
- import('./lib/program.js');
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('./lib/program.js');
47
+ import("./lib/program.js");
47
48
  }
package/cli.js CHANGED
@@ -15,4 +15,4 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import './lib/program.js';
18
+ import "./lib/program.js";
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 'playwright-core';
17
+ import type * as playwright from "playwright-core";
18
18
 
19
- export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf';
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?: 'chromium' | 'firefox' | 'webkit';
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?: 'allow' | 'omit';
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 '@modelcontextprotocol/sdk/server/index.js';
19
- import type { Config } from './config.js';
20
- import type { BrowserContext } from 'playwright-core';
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(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Server>;
23
- export {};
22
+ export declare function createConnection(
23
+ config?: Config,
24
+ contextGetter?: () => Promise<BrowserContext>,
25
+ ): Promise<Server>;
package/index.js CHANGED
@@ -15,5 +15,5 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import { createConnection } from './lib/index.js';
18
+ import { createConnection } from "./lib/index.js";
19
19
  export { createConnection };
@@ -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 'fs';
17
- import net from 'net';
18
- import path from 'path';
19
- import * as playwright from 'playwright-core';
20
- import ms from 'ms';
21
- // @ts-ignore
22
- import { registryDirectory } from 'playwright-core/lib/server/registry/index';
23
- // @ts-ignore
24
- import { startTraceViewerServer } from 'playwright-core/lib/server';
25
- import { logUnhandledError, testDebug } from './utils/log.js';
26
- import { createHash } from './utils/guid.js';
27
- import { outputFile } from './config.js';
28
- const TIMEOUT_STR = '30m';
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, 'utf8');
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 !== '__pwInitScripts');
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 === '__pwInitScripts')
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 === '__pwInitScripts')
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, 'window', {
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.then(browser => {
92
- browser.on('disconnected', () => {
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
- }).catch(() => {
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('Not implemented');
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 { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
188
+ return {
189
+ browserContext,
190
+ close: () => this._closeBrowserContext(browserContext, browser),
191
+ };
134
192
  }
135
193
  async _doCreateContext(browser) {
136
- throw new Error('Not implemented');
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('isolated', 'Create a new isolated browser context', config);
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.launch({
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
- args: this.config.browser.launchOptions?.args || [],
165
- }).catch(error => {
166
- if (error.message.includes('Executable doesn\'t exist'))
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(this.config.browser.contextOptions);
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('cdp', 'Connect to a browser over CDP', config);
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 ? await browser.newContext() : browser.contexts()[0];
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('remote', 'Connect to a browser using a remote endpoint', config);
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('browser', this.config.browser.browserName);
262
+ url.searchParams.set("browser", this.config.browser.browserName);
199
263
  if (this.config.browser.launchOptions)
200
- url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
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 = 'persistent';
213
- description = 'Create a new persistent browser context';
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('create browser context (persistent)');
222
- const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
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('lock user data dir', userDataDir);
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('Executable doesn\'t exist'))
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('ProcessSingleton') || error.message.includes('Invalid URL')) {
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('close browser context (persistent)');
280
- testDebug('release user data dir', userDataDir);
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('close browser context complete (persistent)');
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 ?? this.config.browser?.browserName;
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 = 'bun' in process.versions;
298
- if (isBunRuntime)
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 !== 'chromium')
372
+ if (browserConfig.browserName !== "chromium")
301
373
  return;
302
374
  const launchOptions = browserConfig.launchOptions || {};
303
- launchOptions.cdpPort = await findFreePort();
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('error', reject);
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('human-readable');
322
- const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json';
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('\nTrace viewer listening on ' + url);
397
+ console.error("\nTrace viewer listening on " + url);
325
398
  return tracesDir;
326
399
  }