@playwright/mcp 0.0.31 → 0.0.33

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 (52) hide show
  1. package/README.md +27 -6
  2. package/config.d.ts +5 -0
  3. package/index.d.ts +1 -6
  4. package/lib/browserContextFactory.js +64 -54
  5. package/lib/browserServerBackend.js +121 -0
  6. package/lib/config.js +10 -9
  7. package/lib/context.js +107 -182
  8. package/lib/extension/cdpRelay.js +346 -0
  9. package/lib/extension/extensionContextFactory.js +56 -0
  10. package/lib/extension/main.js +26 -0
  11. package/lib/httpServer.js +20 -182
  12. package/lib/index.js +6 -3
  13. package/lib/loop/loop.js +69 -0
  14. package/lib/loop/loopClaude.js +152 -0
  15. package/lib/loop/loopOpenAI.js +141 -0
  16. package/lib/loop/main.js +60 -0
  17. package/lib/loopTools/context.js +66 -0
  18. package/lib/loopTools/main.js +49 -0
  19. package/lib/loopTools/perform.js +32 -0
  20. package/lib/loopTools/snapshot.js +29 -0
  21. package/lib/loopTools/tool.js +18 -0
  22. package/lib/mcp/inProcessTransport.js +72 -0
  23. package/lib/mcp/server.js +93 -0
  24. package/lib/{transport.js → mcp/transport.js} +30 -42
  25. package/lib/package.js +3 -3
  26. package/lib/program.js +39 -9
  27. package/lib/response.js +165 -0
  28. package/lib/sessionLog.js +121 -0
  29. package/lib/tab.js +138 -24
  30. package/lib/tools/common.js +10 -23
  31. package/lib/tools/console.js +4 -15
  32. package/lib/tools/dialogs.js +12 -17
  33. package/lib/tools/evaluate.js +12 -21
  34. package/lib/tools/files.js +9 -16
  35. package/lib/tools/install.js +3 -7
  36. package/lib/tools/keyboard.js +28 -42
  37. package/lib/tools/mouse.js +27 -50
  38. package/lib/tools/navigate.js +12 -35
  39. package/lib/tools/network.js +5 -15
  40. package/lib/tools/pdf.js +7 -16
  41. package/lib/tools/screenshot.js +35 -33
  42. package/lib/tools/snapshot.js +44 -69
  43. package/lib/tools/tabs.js +10 -41
  44. package/lib/tools/tool.js +15 -0
  45. package/lib/tools/utils.js +2 -9
  46. package/lib/tools/wait.js +3 -6
  47. package/lib/tools.js +3 -0
  48. package/lib/utils.js +26 -0
  49. package/package.json +11 -6
  50. package/lib/connection.js +0 -81
  51. package/lib/pageSnapshot.js +0 -43
  52. package/lib/server.js +0 -48
package/README.md CHANGED
@@ -61,7 +61,7 @@ Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user),
61
61
 
62
62
  #### Click the button to install:
63
63
 
64
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
64
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)
65
65
 
66
66
  #### Or install manually:
67
67
 
@@ -88,6 +88,18 @@ Follow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/
88
88
  Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click "Add Extension".
89
89
  </details>
90
90
 
91
+ <details>
92
+ <summary>LM Studio</summary>
93
+
94
+ #### Click the button to install:
95
+
96
+ [![Add MCP Server playwright to LM Studio](https://files.lmstudio.ai/deeplink/mcp-install-light.svg)](https://lmstudio.ai/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAcGxheXdyaWdodC9tY3BAbGF0ZXN0Il19)
97
+
98
+ #### Or install manually:
99
+
100
+ Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
101
+ </details>
102
+
91
103
  <details>
92
104
  <summary>Qodo Gen</summary>
93
105
 
@@ -99,7 +111,13 @@ Click <code>Save</code>.
99
111
  <details>
100
112
  <summary>VS Code</summary>
101
113
 
102
- You can also install the Playwright MCP server using the VS Code CLI:
114
+ #### Click the button to install:
115
+
116
+ [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D)
117
+
118
+ #### Or install manually:
119
+
120
+ Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the Playwright MCP server using the VS Code CLI:
103
121
 
104
122
  ```bash
105
123
  # For VS Code
@@ -156,6 +174,8 @@ Playwright MCP server supports following arguments. They can be provided in the
156
174
  example ".com,chromium.org,.domain.com"
157
175
  --proxy-server <proxy> specify proxy server, for example
158
176
  "http://myproxy:3128" or "socks5://myproxy:8080"
177
+ --save-session Whether to save the Playwright MCP session into
178
+ the output directory.
159
179
  --save-trace Whether to save the Playwright Trace of the
160
180
  session into the output directory.
161
181
  --storage-state <path> path to the storage state file for isolated
@@ -297,19 +317,19 @@ npx @playwright/mcp@latest --config path/to/config.json
297
317
  ### Standalone MCP server
298
318
 
299
319
  When running headed browser on system w/o display or from worker processes of the IDEs,
300
- run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable SSE transport.
320
+ run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport.
301
321
 
302
322
  ```bash
303
323
  npx @playwright/mcp@latest --port 8931
304
324
  ```
305
325
 
306
- And then in MCP client config, set the `url` to the SSE endpoint:
326
+ And then in MCP client config, set the `url` to the HTTP endpoint:
307
327
 
308
328
  ```js
309
329
  {
310
330
  "mcpServers": {
311
331
  "playwright": {
312
- "url": "http://localhost:8931/sse"
332
+ "url": "http://localhost:8931/mcp"
313
333
  }
314
334
  }
315
335
  }
@@ -524,10 +544,11 @@ http.createServer(async (req, res) => {
524
544
  - Title: Take a screenshot
525
545
  - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
526
546
  - Parameters:
527
- - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
547
+ - `type` (string, optional): Image format for the screenshot. Default is png.
528
548
  - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
529
549
  - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
530
550
  - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
551
+ - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.
531
552
  - Read-only: **true**
532
553
 
533
554
  <!-- NOTE: This has been generated via update-readme.js -->
package/config.d.ts CHANGED
@@ -85,6 +85,11 @@ export type Config = {
85
85
  */
86
86
  capabilities?: ToolCapability[];
87
87
 
88
+ /**
89
+ * Whether to save the Playwright session into the output directory.
90
+ */
91
+ saveSession?: boolean;
92
+
88
93
  /**
89
94
  * Whether to save the Playwright trace of the session into the output directory.
90
95
  */
package/index.d.ts CHANGED
@@ -19,10 +19,5 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
19
  import type { Config } from './config.js';
20
20
  import type { BrowserContext } from 'playwright';
21
21
 
22
- export type Connection = {
23
- server: Server;
24
- close(): Promise<void>;
25
- };
26
-
27
- export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Connection>;
22
+ export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Server>;
28
23
  export {};
@@ -13,28 +13,34 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import fs from 'node:fs';
17
- import net from 'node:net';
18
- import path from 'node:path';
19
- import os from 'node:os';
16
+ import fs from 'fs';
17
+ import net from 'net';
18
+ import path from 'path';
20
19
  import * as playwright from 'playwright';
20
+ // @ts-ignore
21
+ import { registryDirectory } from 'playwright-core/lib/server/registry/index';
21
22
  import { logUnhandledError, testDebug } from './log.js';
22
- export function contextFactory(browserConfig) {
23
- if (browserConfig.remoteEndpoint)
24
- return new RemoteContextFactory(browserConfig);
25
- if (browserConfig.cdpEndpoint)
26
- return new CdpContextFactory(browserConfig);
27
- if (browserConfig.isolated)
28
- return new IsolatedContextFactory(browserConfig);
29
- return new PersistentContextFactory(browserConfig);
23
+ import { createHash } from './utils.js';
24
+ import { outputFile } from './config.js';
25
+ export function contextFactory(config) {
26
+ if (config.browser.remoteEndpoint)
27
+ return new RemoteContextFactory(config);
28
+ if (config.browser.cdpEndpoint)
29
+ return new CdpContextFactory(config);
30
+ if (config.browser.isolated)
31
+ return new IsolatedContextFactory(config);
32
+ return new PersistentContextFactory(config);
30
33
  }
31
34
  class BaseContextFactory {
32
- browserConfig;
33
- _browserPromise;
34
35
  name;
35
- constructor(name, browserConfig) {
36
+ description;
37
+ config;
38
+ _browserPromise;
39
+ _tracesDir;
40
+ constructor(name, description, config) {
36
41
  this.name = name;
37
- this.browserConfig = browserConfig;
42
+ this.description = description;
43
+ this.config = config;
38
44
  }
39
45
  async _obtainBrowser() {
40
46
  if (this._browserPromise)
@@ -53,7 +59,9 @@ class BaseContextFactory {
53
59
  async _doObtainBrowser() {
54
60
  throw new Error('Not implemented');
55
61
  }
56
- async createContext() {
62
+ async createContext(clientInfo) {
63
+ if (this.config.saveTrace)
64
+ this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
57
65
  testDebug(`create browser context (${this.name})`);
58
66
  const browser = await this._obtainBrowser();
59
67
  const browserContext = await this._doCreateContext(browser);
@@ -74,14 +82,15 @@ class BaseContextFactory {
74
82
  }
75
83
  }
76
84
  class IsolatedContextFactory extends BaseContextFactory {
77
- constructor(browserConfig) {
78
- super('isolated', browserConfig);
85
+ constructor(config) {
86
+ super('isolated', 'Create a new isolated browser context', config);
79
87
  }
80
88
  async _doObtainBrowser() {
81
- await injectCdpPort(this.browserConfig);
82
- const browserType = playwright[this.browserConfig.browserName];
89
+ await injectCdpPort(this.config.browser);
90
+ const browserType = playwright[this.config.browser.browserName];
83
91
  return browserType.launch({
84
- ...this.browserConfig.launchOptions,
92
+ tracesDir: this._tracesDir,
93
+ ...this.config.browser.launchOptions,
85
94
  handleSIGINT: false,
86
95
  handleSIGTERM: false,
87
96
  }).catch(error => {
@@ -91,53 +100,59 @@ class IsolatedContextFactory extends BaseContextFactory {
91
100
  });
92
101
  }
93
102
  async _doCreateContext(browser) {
94
- return browser.newContext(this.browserConfig.contextOptions);
103
+ return browser.newContext(this.config.browser.contextOptions);
95
104
  }
96
105
  }
97
106
  class CdpContextFactory extends BaseContextFactory {
98
- constructor(browserConfig) {
99
- super('cdp', browserConfig);
107
+ constructor(config) {
108
+ super('cdp', 'Connect to a browser over CDP', config);
100
109
  }
101
110
  async _doObtainBrowser() {
102
- return playwright.chromium.connectOverCDP(this.browserConfig.cdpEndpoint);
111
+ return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
103
112
  }
104
113
  async _doCreateContext(browser) {
105
- return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
114
+ return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
106
115
  }
107
116
  }
108
117
  class RemoteContextFactory extends BaseContextFactory {
109
- constructor(browserConfig) {
110
- super('remote', browserConfig);
118
+ constructor(config) {
119
+ super('remote', 'Connect to a browser using a remote endpoint', config);
111
120
  }
112
121
  async _doObtainBrowser() {
113
- const url = new URL(this.browserConfig.remoteEndpoint);
114
- url.searchParams.set('browser', this.browserConfig.browserName);
115
- if (this.browserConfig.launchOptions)
116
- url.searchParams.set('launch-options', JSON.stringify(this.browserConfig.launchOptions));
117
- return playwright[this.browserConfig.browserName].connect(String(url));
122
+ const url = new URL(this.config.browser.remoteEndpoint);
123
+ url.searchParams.set('browser', this.config.browser.browserName);
124
+ if (this.config.browser.launchOptions)
125
+ url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
126
+ return playwright[this.config.browser.browserName].connect(String(url));
118
127
  }
119
128
  async _doCreateContext(browser) {
120
129
  return browser.newContext();
121
130
  }
122
131
  }
123
132
  class PersistentContextFactory {
124
- browserConfig;
133
+ config;
134
+ name = 'persistent';
135
+ description = 'Create a new persistent browser context';
125
136
  _userDataDirs = new Set();
126
- constructor(browserConfig) {
127
- this.browserConfig = browserConfig;
137
+ constructor(config) {
138
+ this.config = config;
128
139
  }
129
- async createContext() {
130
- await injectCdpPort(this.browserConfig);
140
+ async createContext(clientInfo) {
141
+ await injectCdpPort(this.config.browser);
131
142
  testDebug('create browser context (persistent)');
132
- const userDataDir = this.browserConfig.userDataDir ?? await this._createUserDataDir();
143
+ const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
144
+ let tracesDir;
145
+ if (this.config.saveTrace)
146
+ tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
133
147
  this._userDataDirs.add(userDataDir);
134
148
  testDebug('lock user data dir', userDataDir);
135
- const browserType = playwright[this.browserConfig.browserName];
149
+ const browserType = playwright[this.config.browser.browserName];
136
150
  for (let i = 0; i < 5; i++) {
137
151
  try {
138
152
  const browserContext = await browserType.launchPersistentContext(userDataDir, {
139
- ...this.browserConfig.launchOptions,
140
- ...this.browserConfig.contextOptions,
153
+ tracesDir,
154
+ ...this.config.browser.launchOptions,
155
+ ...this.config.browser.contextOptions,
141
156
  handleSIGINT: false,
142
157
  handleSIGTERM: false,
143
158
  });
@@ -164,17 +179,12 @@ class PersistentContextFactory {
164
179
  this._userDataDirs.delete(userDataDir);
165
180
  testDebug('close browser context complete (persistent)');
166
181
  }
167
- async _createUserDataDir() {
168
- let cacheDirectory;
169
- if (process.platform === 'linux')
170
- cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
171
- else if (process.platform === 'darwin')
172
- cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
173
- else if (process.platform === 'win32')
174
- cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
175
- else
176
- throw new Error('Unsupported platform: ' + process.platform);
177
- const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${this.browserConfig.launchOptions?.channel ?? this.browserConfig?.browserName}-profile`);
182
+ async _createUserDataDir(rootPath) {
183
+ const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
184
+ const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
185
+ // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead.
186
+ const rootPathToken = rootPath ? `-${createHash(rootPath)}` : '';
187
+ const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`);
178
188
  await fs.promises.mkdir(result, { recursive: true });
179
189
  return result;
180
190
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { fileURLToPath } from 'url';
17
+ import { z } from 'zod';
18
+ import { Context } from './context.js';
19
+ import { logUnhandledError } from './log.js';
20
+ import { Response } from './response.js';
21
+ import { SessionLog } from './sessionLog.js';
22
+ import { filteredTools } from './tools.js';
23
+ import { packageJSON } from './package.js';
24
+ import { defineTool } from './tools/tool.js';
25
+ export class BrowserServerBackend {
26
+ name = 'Playwright';
27
+ version = packageJSON.version;
28
+ _tools;
29
+ _context;
30
+ _sessionLog;
31
+ _config;
32
+ _browserContextFactory;
33
+ constructor(config, factories) {
34
+ this._config = config;
35
+ this._browserContextFactory = factories[0];
36
+ this._tools = filteredTools(config);
37
+ if (factories.length > 1)
38
+ this._tools.push(this._defineContextSwitchTool(factories));
39
+ }
40
+ async initialize(server) {
41
+ const capabilities = server.getClientCapabilities();
42
+ let rootPath;
43
+ if (capabilities.roots && (server.getClientVersion()?.name === 'Visual Studio Code' ||
44
+ server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) {
45
+ const { roots } = await server.listRoots();
46
+ const firstRootUri = roots[0]?.uri;
47
+ const url = firstRootUri ? new URL(firstRootUri) : undefined;
48
+ rootPath = url ? fileURLToPath(url) : undefined;
49
+ }
50
+ this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined;
51
+ this._context = new Context({
52
+ tools: this._tools,
53
+ config: this._config,
54
+ browserContextFactory: this._browserContextFactory,
55
+ sessionLog: this._sessionLog,
56
+ clientInfo: { ...server.getClientVersion(), rootPath },
57
+ });
58
+ }
59
+ tools() {
60
+ return this._tools.map(tool => tool.schema);
61
+ }
62
+ async callTool(schema, parsedArguments) {
63
+ const context = this._context;
64
+ const response = new Response(context, schema.name, parsedArguments);
65
+ const tool = this._tools.find(tool => tool.schema.name === schema.name);
66
+ context.setRunningTool(true);
67
+ try {
68
+ await tool.handle(context, parsedArguments, response);
69
+ await response.finish();
70
+ this._sessionLog?.logResponse(response);
71
+ }
72
+ catch (error) {
73
+ response.addError(String(error));
74
+ }
75
+ finally {
76
+ context.setRunningTool(false);
77
+ }
78
+ return response.serialize();
79
+ }
80
+ serverClosed() {
81
+ void this._context.dispose().catch(logUnhandledError);
82
+ }
83
+ _defineContextSwitchTool(factories) {
84
+ const self = this;
85
+ return defineTool({
86
+ capability: 'core',
87
+ schema: {
88
+ name: 'browser_connect',
89
+ title: 'Connect to a browser context',
90
+ description: [
91
+ 'Connect to a browser using one of the available methods:',
92
+ ...factories.map(factory => `- "${factory.name}": ${factory.description}`),
93
+ ].join('\n'),
94
+ inputSchema: z.object({
95
+ method: z.enum(factories.map(factory => factory.name)).default(factories[0].name).describe('The method to use to connect to the browser'),
96
+ }),
97
+ type: 'readOnly',
98
+ },
99
+ async handle(context, params, response) {
100
+ const factory = factories.find(factory => factory.name === params.method);
101
+ if (!factory) {
102
+ response.addError('Unknown connection method: ' + params.method);
103
+ return;
104
+ }
105
+ await self._setContextFactory(factory);
106
+ response.addResult('Successfully changed connection method.');
107
+ }
108
+ });
109
+ }
110
+ async _setContextFactory(newFactory) {
111
+ if (this._context) {
112
+ const options = {
113
+ ...this._context.options,
114
+ browserContextFactory: newFactory,
115
+ };
116
+ await this._context.dispose();
117
+ this._context = new Context(options);
118
+ }
119
+ this._browserContextFactory = newFactory;
120
+ }
121
+ }
package/lib/config.js CHANGED
@@ -17,7 +17,7 @@ import fs from 'fs';
17
17
  import os from 'os';
18
18
  import path from 'path';
19
19
  import { devices } from 'playwright';
20
- import { sanitizeForFilePath } from './tools/utils.js';
20
+ import { sanitizeForFilePath } from './utils.js';
21
21
  const defaultConfig = {
22
22
  browser: {
23
23
  browserName: 'chromium',
@@ -35,7 +35,7 @@ const defaultConfig = {
35
35
  blockedOrigins: undefined,
36
36
  },
37
37
  server: {},
38
- outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
38
+ saveTrace: false,
39
39
  };
40
40
  export async function resolveConfig(config) {
41
41
  return mergeConfig(defaultConfig, config);
@@ -48,9 +48,6 @@ export async function resolveCLIConfig(cliOptions) {
48
48
  result = mergeConfig(result, configInFile);
49
49
  result = mergeConfig(result, envOverrides);
50
50
  result = mergeConfig(result, cliOverrides);
51
- // Derive artifact output directory from config.outputDir
52
- if (result.saveTrace)
53
- result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
54
51
  return result;
55
52
  }
56
53
  export function configFromCLIOptions(cliOptions) {
@@ -83,7 +80,7 @@ export function configFromCLIOptions(cliOptions) {
83
80
  headless: cliOptions.headless,
84
81
  };
85
82
  // --no-sandbox was passed, disable the sandbox
86
- if (!cliOptions.sandbox)
83
+ if (cliOptions.sandbox === false)
87
84
  launchOptions.chromiumSandbox = false;
88
85
  if (cliOptions.proxyServer) {
89
86
  launchOptions.proxy = {
@@ -133,6 +130,7 @@ export function configFromCLIOptions(cliOptions) {
133
130
  allowedOrigins: cliOptions.allowedOrigins,
134
131
  blockedOrigins: cliOptions.blockedOrigins,
135
132
  },
133
+ saveSession: cliOptions.saveSession,
136
134
  saveTrace: cliOptions.saveTrace,
137
135
  outputDir: cliOptions.outputDir,
138
136
  imageResponses: cliOptions.imageResponses,
@@ -178,10 +176,13 @@ async function loadConfig(configFile) {
178
176
  throw new Error(`Failed to load config file: ${configFile}, ${error}`);
179
177
  }
180
178
  }
181
- export async function outputFile(config, name) {
182
- await fs.promises.mkdir(config.outputDir, { recursive: true });
179
+ export async function outputFile(config, rootPath, name) {
180
+ const outputDir = config.outputDir
181
+ ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined)
182
+ ?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString()));
183
+ await fs.promises.mkdir(outputDir, { recursive: true });
183
184
  const fileName = sanitizeForFilePath(name);
184
- return path.join(config.outputDir, fileName);
185
+ return path.join(outputDir, fileName);
185
186
  }
186
187
  function pickDefined(obj) {
187
188
  return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined));