@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.
- package/README.md +27 -6
- package/config.d.ts +5 -0
- package/index.d.ts +1 -6
- package/lib/browserContextFactory.js +64 -54
- package/lib/browserServerBackend.js +121 -0
- package/lib/config.js +10 -9
- package/lib/context.js +107 -182
- package/lib/extension/cdpRelay.js +346 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/extension/main.js +26 -0
- package/lib/httpServer.js +20 -182
- package/lib/index.js +6 -3
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +141 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +66 -0
- package/lib/loopTools/main.js +49 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/server.js +93 -0
- package/lib/{transport.js → mcp/transport.js} +30 -42
- package/lib/package.js +3 -3
- package/lib/program.js +39 -9
- package/lib/response.js +165 -0
- package/lib/sessionLog.js +121 -0
- package/lib/tab.js +138 -24
- package/lib/tools/common.js +10 -23
- package/lib/tools/console.js +4 -15
- package/lib/tools/dialogs.js +12 -17
- package/lib/tools/evaluate.js +12 -21
- package/lib/tools/files.js +9 -16
- package/lib/tools/install.js +3 -7
- package/lib/tools/keyboard.js +28 -42
- package/lib/tools/mouse.js +27 -50
- package/lib/tools/navigate.js +12 -35
- package/lib/tools/network.js +5 -15
- package/lib/tools/pdf.js +7 -16
- package/lib/tools/screenshot.js +35 -33
- package/lib/tools/snapshot.js +44 -69
- package/lib/tools/tabs.js +10 -41
- package/lib/tools/tool.js +15 -0
- package/lib/tools/utils.js +2 -9
- package/lib/tools/wait.js +3 -6
- package/lib/tools.js +3 -0
- package/lib/utils.js +26 -0
- package/package.json +11 -6
- package/lib/connection.js +0 -81
- package/lib/pageSnapshot.js +0 -43
- 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
|
-
[](
|
|
64
|
+
[](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
|
+
[](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
|
-
|
|
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
|
|
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
|
|
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/
|
|
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
|
-
- `
|
|
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
|
|
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 '
|
|
17
|
-
import net from '
|
|
18
|
-
import path from '
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
return new
|
|
27
|
-
if (
|
|
28
|
-
return new
|
|
29
|
-
|
|
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
|
-
|
|
36
|
+
description;
|
|
37
|
+
config;
|
|
38
|
+
_browserPromise;
|
|
39
|
+
_tracesDir;
|
|
40
|
+
constructor(name, description, config) {
|
|
36
41
|
this.name = name;
|
|
37
|
-
this.
|
|
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(
|
|
78
|
-
super('isolated',
|
|
85
|
+
constructor(config) {
|
|
86
|
+
super('isolated', 'Create a new isolated browser context', config);
|
|
79
87
|
}
|
|
80
88
|
async _doObtainBrowser() {
|
|
81
|
-
await injectCdpPort(this.
|
|
82
|
-
const browserType = playwright[this.
|
|
89
|
+
await injectCdpPort(this.config.browser);
|
|
90
|
+
const browserType = playwright[this.config.browser.browserName];
|
|
83
91
|
return browserType.launch({
|
|
84
|
-
|
|
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.
|
|
103
|
+
return browser.newContext(this.config.browser.contextOptions);
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
class CdpContextFactory extends BaseContextFactory {
|
|
98
|
-
constructor(
|
|
99
|
-
super('cdp',
|
|
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.
|
|
111
|
+
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
103
112
|
}
|
|
104
113
|
async _doCreateContext(browser) {
|
|
105
|
-
return this.
|
|
114
|
+
return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
class RemoteContextFactory extends BaseContextFactory {
|
|
109
|
-
constructor(
|
|
110
|
-
super('remote',
|
|
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.
|
|
114
|
-
url.searchParams.set('browser', this.
|
|
115
|
-
if (this.
|
|
116
|
-
url.searchParams.set('launch-options', JSON.stringify(this.
|
|
117
|
-
return playwright[this.
|
|
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
|
-
|
|
133
|
+
config;
|
|
134
|
+
name = 'persistent';
|
|
135
|
+
description = 'Create a new persistent browser context';
|
|
125
136
|
_userDataDirs = new Set();
|
|
126
|
-
constructor(
|
|
127
|
-
this.
|
|
137
|
+
constructor(config) {
|
|
138
|
+
this.config = config;
|
|
128
139
|
}
|
|
129
|
-
async createContext() {
|
|
130
|
-
await injectCdpPort(this.
|
|
140
|
+
async createContext(clientInfo) {
|
|
141
|
+
await injectCdpPort(this.config.browser);
|
|
131
142
|
testDebug('create browser context (persistent)');
|
|
132
|
-
const userDataDir = this.
|
|
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.
|
|
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
|
-
|
|
140
|
-
...this.
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 './
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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));
|