@playwright/mcp 0.0.33 → 0.0.34
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 +31 -1
- package/lib/browserContextFactory.js +22 -14
- package/lib/browserServerBackend.js +17 -58
- package/lib/config.js +1 -1
- package/lib/context.js +1 -1
- package/lib/extension/cdpRelay.js +6 -3
- package/lib/extension/extensionContextFactory.js +1 -1
- package/lib/index.js +1 -1
- package/lib/loopTools/context.js +1 -1
- package/lib/loopTools/main.js +7 -5
- package/lib/mcp/proxyBackend.js +106 -0
- package/lib/mcp/server.js +29 -26
- package/lib/mcp/tool.js +29 -0
- package/lib/mcp/transport.js +1 -1
- package/lib/program.js +26 -18
- package/lib/sessionLog.js +1 -1
- package/lib/tab.js +2 -2
- package/lib/tools/evaluate.js +1 -1
- package/lib/tools/keyboard.js +1 -1
- package/lib/tools/pdf.js +1 -1
- package/lib/tools/screenshot.js +1 -1
- package/lib/tools/snapshot.js +1 -1
- package/lib/tools/wait.js +3 -4
- package/lib/{javascript.js → utils/codegen.js} +1 -1
- package/lib/{fileUtils.js → utils/fileUtils.js} +6 -2
- package/lib/{utils.js → utils/guid.js} +3 -7
- package/lib/{package.js → utils/package.js} +1 -1
- package/package.json +6 -5
- package/lib/extension/main.js +0 -26
- /package/lib/{httpServer.js → utils/httpServer.js} +0 -0
- /package/lib/{log.js → utils/log.js} +0 -0
- /package/lib/{manualPromise.js → utils/manualPromise.js} +0 -0
package/README.md
CHANGED
|
@@ -100,6 +100,29 @@ Go to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to you
|
|
|
100
100
|
Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.
|
|
101
101
|
</details>
|
|
102
102
|
|
|
103
|
+
<details>
|
|
104
|
+
<summary>opencode</summary>
|
|
105
|
+
|
|
106
|
+
Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"$schema": "https://opencode.ai/config.json",
|
|
111
|
+
"mcp": {
|
|
112
|
+
"playwright": {
|
|
113
|
+
"type": "local",
|
|
114
|
+
"command": [
|
|
115
|
+
"npx",
|
|
116
|
+
"@playwright/mcp@latest"
|
|
117
|
+
],
|
|
118
|
+
"enabled": true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
</details>
|
|
125
|
+
|
|
103
126
|
<details>
|
|
104
127
|
<summary>Qodo Gen</summary>
|
|
105
128
|
|
|
@@ -158,6 +181,9 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
158
181
|
--config <path> path to the configuration file.
|
|
159
182
|
--device <device> device to emulate, for example: "iPhone 15"
|
|
160
183
|
--executable-path <path> path to the browser executable.
|
|
184
|
+
--extension Connect to a running browser instance
|
|
185
|
+
(Edge/Chrome only). Requires the "Playwright MCP
|
|
186
|
+
Bridge" browser extension to be installed.
|
|
161
187
|
--headless run browser in headless mode, headed by default
|
|
162
188
|
--host <host> host to bind server to. Default is localhost. Use
|
|
163
189
|
0.0.0.0 to bind to all interfaces.
|
|
@@ -191,7 +217,7 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|
|
191
217
|
|
|
192
218
|
### User profile
|
|
193
219
|
|
|
194
|
-
You can run Playwright MCP with persistent profile like a regular browser (default),
|
|
220
|
+
You can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension.
|
|
195
221
|
|
|
196
222
|
**Persistent profile**
|
|
197
223
|
|
|
@@ -231,6 +257,10 @@ state [here](https://playwright.dev/docs/auth).
|
|
|
231
257
|
}
|
|
232
258
|
```
|
|
233
259
|
|
|
260
|
+
**Browser Extension**
|
|
261
|
+
|
|
262
|
+
The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [extension/README.md](extension/README.md) for installation and setup instructions.
|
|
263
|
+
|
|
234
264
|
### Configuration file
|
|
235
265
|
|
|
236
266
|
The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file
|
|
@@ -19,8 +19,10 @@ import path from 'path';
|
|
|
19
19
|
import * as playwright from 'playwright';
|
|
20
20
|
// @ts-ignore
|
|
21
21
|
import { registryDirectory } from 'playwright-core/lib/server/registry/index';
|
|
22
|
-
|
|
23
|
-
import {
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|
24
|
+
import { logUnhandledError, testDebug } from './utils/log.js';
|
|
25
|
+
import { createHash } from './utils/guid.js';
|
|
24
26
|
import { outputFile } from './config.js';
|
|
25
27
|
export function contextFactory(config) {
|
|
26
28
|
if (config.browser.remoteEndpoint)
|
|
@@ -36,17 +38,16 @@ class BaseContextFactory {
|
|
|
36
38
|
description;
|
|
37
39
|
config;
|
|
38
40
|
_browserPromise;
|
|
39
|
-
_tracesDir;
|
|
40
41
|
constructor(name, description, config) {
|
|
41
42
|
this.name = name;
|
|
42
43
|
this.description = description;
|
|
43
44
|
this.config = config;
|
|
44
45
|
}
|
|
45
|
-
async _obtainBrowser() {
|
|
46
|
+
async _obtainBrowser(clientInfo) {
|
|
46
47
|
if (this._browserPromise)
|
|
47
48
|
return this._browserPromise;
|
|
48
49
|
testDebug(`obtain browser (${this.name})`);
|
|
49
|
-
this._browserPromise = this._doObtainBrowser();
|
|
50
|
+
this._browserPromise = this._doObtainBrowser(clientInfo);
|
|
50
51
|
void this._browserPromise.then(browser => {
|
|
51
52
|
browser.on('disconnected', () => {
|
|
52
53
|
this._browserPromise = undefined;
|
|
@@ -56,14 +57,12 @@ class BaseContextFactory {
|
|
|
56
57
|
});
|
|
57
58
|
return this._browserPromise;
|
|
58
59
|
}
|
|
59
|
-
async _doObtainBrowser() {
|
|
60
|
+
async _doObtainBrowser(clientInfo) {
|
|
60
61
|
throw new Error('Not implemented');
|
|
61
62
|
}
|
|
62
63
|
async createContext(clientInfo) {
|
|
63
|
-
if (this.config.saveTrace)
|
|
64
|
-
this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
65
64
|
testDebug(`create browser context (${this.name})`);
|
|
66
|
-
const browser = await this._obtainBrowser();
|
|
65
|
+
const browser = await this._obtainBrowser(clientInfo);
|
|
67
66
|
const browserContext = await this._doCreateContext(browser);
|
|
68
67
|
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
|
69
68
|
}
|
|
@@ -85,11 +84,11 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
85
84
|
constructor(config) {
|
|
86
85
|
super('isolated', 'Create a new isolated browser context', config);
|
|
87
86
|
}
|
|
88
|
-
async _doObtainBrowser() {
|
|
87
|
+
async _doObtainBrowser(clientInfo) {
|
|
89
88
|
await injectCdpPort(this.config.browser);
|
|
90
89
|
const browserType = playwright[this.config.browser.browserName];
|
|
91
90
|
return browserType.launch({
|
|
92
|
-
tracesDir: this.
|
|
91
|
+
tracesDir: await startTraceServer(this.config, clientInfo.rootPath),
|
|
93
92
|
...this.config.browser.launchOptions,
|
|
94
93
|
handleSIGINT: false,
|
|
95
94
|
handleSIGTERM: false,
|
|
@@ -141,9 +140,7 @@ class PersistentContextFactory {
|
|
|
141
140
|
await injectCdpPort(this.config.browser);
|
|
142
141
|
testDebug('create browser context (persistent)');
|
|
143
142
|
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
|
|
144
|
-
|
|
145
|
-
if (this.config.saveTrace)
|
|
146
|
-
tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
143
|
+
const tracesDir = await startTraceServer(this.config, clientInfo.rootPath);
|
|
147
144
|
this._userDataDirs.add(userDataDir);
|
|
148
145
|
testDebug('lock user data dir', userDataDir);
|
|
149
146
|
const browserType = playwright[this.config.browser.browserName];
|
|
@@ -203,3 +200,14 @@ async function findFreePort() {
|
|
|
203
200
|
server.on('error', reject);
|
|
204
201
|
});
|
|
205
202
|
}
|
|
203
|
+
async function startTraceServer(config, rootPath) {
|
|
204
|
+
if (!config.saveTrace)
|
|
205
|
+
return undefined;
|
|
206
|
+
const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`);
|
|
207
|
+
const server = await startTraceViewerServer();
|
|
208
|
+
const urlPrefix = server.urlPrefix('human-readable');
|
|
209
|
+
const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json';
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.error('\nTrace viewer listening on ' + url);
|
|
212
|
+
return tracesDir;
|
|
213
|
+
}
|
|
@@ -14,14 +14,13 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
-
import { z } from 'zod';
|
|
18
17
|
import { Context } from './context.js';
|
|
19
|
-
import { logUnhandledError } from './log.js';
|
|
18
|
+
import { logUnhandledError } from './utils/log.js';
|
|
20
19
|
import { Response } from './response.js';
|
|
21
20
|
import { SessionLog } from './sessionLog.js';
|
|
22
21
|
import { filteredTools } from './tools.js';
|
|
23
|
-
import { packageJSON } from './package.js';
|
|
24
|
-
import {
|
|
22
|
+
import { packageJSON } from './utils/package.js';
|
|
23
|
+
import { toMcpTool } from './mcp/tool.js';
|
|
25
24
|
export class BrowserServerBackend {
|
|
26
25
|
name = 'Playwright';
|
|
27
26
|
version = packageJSON.version;
|
|
@@ -30,19 +29,14 @@ export class BrowserServerBackend {
|
|
|
30
29
|
_sessionLog;
|
|
31
30
|
_config;
|
|
32
31
|
_browserContextFactory;
|
|
33
|
-
constructor(config,
|
|
32
|
+
constructor(config, factory) {
|
|
34
33
|
this._config = config;
|
|
35
|
-
this._browserContextFactory =
|
|
34
|
+
this._browserContextFactory = factory;
|
|
36
35
|
this._tools = filteredTools(config);
|
|
37
|
-
if (factories.length > 1)
|
|
38
|
-
this._tools.push(this._defineContextSwitchTool(factories));
|
|
39
36
|
}
|
|
40
|
-
async initialize(
|
|
41
|
-
const capabilities = server.getClientCapabilities();
|
|
37
|
+
async initialize(clientVersion, roots) {
|
|
42
38
|
let rootPath;
|
|
43
|
-
if (
|
|
44
|
-
server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) {
|
|
45
|
-
const { roots } = await server.listRoots();
|
|
39
|
+
if (roots.length > 0) {
|
|
46
40
|
const firstRootUri = roots[0]?.uri;
|
|
47
41
|
const url = firstRootUri ? new URL(firstRootUri) : undefined;
|
|
48
42
|
rootPath = url ? fileURLToPath(url) : undefined;
|
|
@@ -53,16 +47,19 @@ export class BrowserServerBackend {
|
|
|
53
47
|
config: this._config,
|
|
54
48
|
browserContextFactory: this._browserContextFactory,
|
|
55
49
|
sessionLog: this._sessionLog,
|
|
56
|
-
clientInfo: { ...
|
|
50
|
+
clientInfo: { ...clientVersion, rootPath },
|
|
57
51
|
});
|
|
58
52
|
}
|
|
59
|
-
|
|
60
|
-
return this._tools.map(tool => tool.schema);
|
|
53
|
+
async listTools() {
|
|
54
|
+
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
61
55
|
}
|
|
62
|
-
async callTool(
|
|
56
|
+
async callTool(name, rawArguments) {
|
|
57
|
+
const tool = this._tools.find(tool => tool.schema.name === name);
|
|
58
|
+
if (!tool)
|
|
59
|
+
throw new Error(`Tool "${name}" not found`);
|
|
60
|
+
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
|
63
61
|
const context = this._context;
|
|
64
|
-
const response = new Response(context,
|
|
65
|
-
const tool = this._tools.find(tool => tool.schema.name === schema.name);
|
|
62
|
+
const response = new Response(context, name, parsedArguments);
|
|
66
63
|
context.setRunningTool(true);
|
|
67
64
|
try {
|
|
68
65
|
await tool.handle(context, parsedArguments, response);
|
|
@@ -78,44 +75,6 @@ export class BrowserServerBackend {
|
|
|
78
75
|
return response.serialize();
|
|
79
76
|
}
|
|
80
77
|
serverClosed() {
|
|
81
|
-
void this._context
|
|
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;
|
|
78
|
+
void this._context?.dispose().catch(logUnhandledError);
|
|
120
79
|
}
|
|
121
80
|
}
|
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 './utils.js';
|
|
20
|
+
import { sanitizeForFilePath } from './utils/fileUtils.js';
|
|
21
21
|
const defaultConfig = {
|
|
22
22
|
browser: {
|
|
23
23
|
browserName: 'chromium',
|
package/lib/context.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import debug from 'debug';
|
|
17
|
-
import { logUnhandledError } from './log.js';
|
|
17
|
+
import { logUnhandledError } from './utils/log.js';
|
|
18
18
|
import { Tab } from './tab.js';
|
|
19
19
|
import { outputFile } from './config.js';
|
|
20
20
|
const testDebug = debug('pw:mcp:test');
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
import { spawn } from 'child_process';
|
|
24
24
|
import debug from 'debug';
|
|
25
25
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
26
|
-
import { httpAddressToString } from '../httpServer.js';
|
|
27
|
-
import { logUnhandledError } from '../log.js';
|
|
28
|
-
import { ManualPromise } from '../manualPromise.js';
|
|
26
|
+
import { httpAddressToString } from '../utils/httpServer.js';
|
|
27
|
+
import { logUnhandledError } from '../utils/log.js';
|
|
28
|
+
import { ManualPromise } from '../utils/manualPromise.js';
|
|
29
29
|
// @ts-ignore
|
|
30
30
|
const { registry } = await import('playwright-core/lib/server/registry/index');
|
|
31
31
|
const debugLogger = debug('pw:mcp:relay');
|
|
@@ -66,6 +66,9 @@ export class CDPRelayServer {
|
|
|
66
66
|
debugLogger('Waiting for incoming extension connection');
|
|
67
67
|
await Promise.race([
|
|
68
68
|
this._extensionConnectionPromise,
|
|
69
|
+
new Promise((_, reject) => setTimeout(() => {
|
|
70
|
+
reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`));
|
|
71
|
+
}, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)),
|
|
69
72
|
new Promise((_, reject) => abortSignal.addEventListener('abort', reject))
|
|
70
73
|
]);
|
|
71
74
|
debugLogger('Extension connection established');
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import debug from 'debug';
|
|
17
17
|
import * as playwright from 'playwright';
|
|
18
|
-
import { startHttpServer } from '../httpServer.js';
|
|
18
|
+
import { startHttpServer } from '../utils/httpServer.js';
|
|
19
19
|
import { CDPRelayServer } from './cdpRelay.js';
|
|
20
20
|
const debugLogger = debug('pw:mcp:relay');
|
|
21
21
|
export class ExtensionContextFactory {
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as mcpServer from './mcp/server.js';
|
|
|
20
20
|
export async function createConnection(userConfig = {}, contextGetter) {
|
|
21
21
|
const config = await resolveConfig(userConfig);
|
|
22
22
|
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
|
23
|
-
return mcpServer.createServer(new BrowserServerBackend(config,
|
|
23
|
+
return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
|
|
24
24
|
}
|
|
25
25
|
class SimpleBrowserContextFactory {
|
|
26
26
|
name = 'custom';
|
package/lib/loopTools/context.js
CHANGED
|
@@ -39,7 +39,7 @@ export class Context {
|
|
|
39
39
|
static async create(config) {
|
|
40
40
|
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
|
|
41
41
|
const browserContextFactory = contextFactory(config);
|
|
42
|
-
const server = mcpServer.createServer(new BrowserServerBackend(config,
|
|
42
|
+
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
|
|
43
43
|
await client.connect(new InProcessTransport(server));
|
|
44
44
|
await client.ping();
|
|
45
45
|
return new Context(config, client);
|
package/lib/loopTools/main.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import dotenv from 'dotenv';
|
|
17
17
|
import * as mcpTransport from '../mcp/transport.js';
|
|
18
|
-
import { packageJSON } from '../package.js';
|
|
18
|
+
import { packageJSON } from '../utils/package.js';
|
|
19
19
|
import { Context } from './context.js';
|
|
20
20
|
import { perform } from './perform.js';
|
|
21
21
|
import { snapshot } from './snapshot.js';
|
|
22
|
+
import { toMcpTool } from '../mcp/tool.js';
|
|
22
23
|
export async function runLoopTools(config) {
|
|
23
24
|
dotenv.config();
|
|
24
25
|
const serverBackendFactory = () => new LoopToolsServerBackend(config);
|
|
@@ -36,11 +37,12 @@ class LoopToolsServerBackend {
|
|
|
36
37
|
async initialize() {
|
|
37
38
|
this._context = await Context.create(this._config);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
return this._tools.map(tool => tool.schema);
|
|
40
|
+
async listTools() {
|
|
41
|
+
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
41
42
|
}
|
|
42
|
-
async callTool(
|
|
43
|
-
const tool = this._tools.find(tool => tool.schema.name ===
|
|
43
|
+
async callTool(name, args) {
|
|
44
|
+
const tool = this._tools.find(tool => tool.schema.name === name);
|
|
45
|
+
const parsedArguments = tool.schema.inputSchema.parse(args || {});
|
|
44
46
|
return await tool.handle(this._context, parsedArguments);
|
|
45
47
|
}
|
|
46
48
|
serverClosed() {
|
|
@@ -0,0 +1,106 @@
|
|
|
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 { z } from 'zod';
|
|
17
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
18
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
19
|
+
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
20
|
+
import { logUnhandledError } from '../utils/log.js';
|
|
21
|
+
import { packageJSON } from '../utils/package.js';
|
|
22
|
+
export class ProxyBackend {
|
|
23
|
+
name = 'Playwright MCP Client Switcher';
|
|
24
|
+
version = packageJSON.version;
|
|
25
|
+
_mcpProviders;
|
|
26
|
+
_currentClient;
|
|
27
|
+
_contextSwitchTool;
|
|
28
|
+
_roots = [];
|
|
29
|
+
constructor(mcpProviders) {
|
|
30
|
+
this._mcpProviders = mcpProviders;
|
|
31
|
+
this._contextSwitchTool = this._defineContextSwitchTool();
|
|
32
|
+
}
|
|
33
|
+
async initialize(clientVersion, roots) {
|
|
34
|
+
this._roots = roots;
|
|
35
|
+
await this._setCurrentClient(this._mcpProviders[0]);
|
|
36
|
+
}
|
|
37
|
+
async listTools() {
|
|
38
|
+
const response = await this._currentClient.listTools();
|
|
39
|
+
if (this._mcpProviders.length === 1)
|
|
40
|
+
return response.tools;
|
|
41
|
+
return [
|
|
42
|
+
...response.tools,
|
|
43
|
+
this._contextSwitchTool,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
async callTool(name, args) {
|
|
47
|
+
if (name === this._contextSwitchTool.name)
|
|
48
|
+
return this._callContextSwitchTool(args);
|
|
49
|
+
return await this._currentClient.callTool({
|
|
50
|
+
name,
|
|
51
|
+
arguments: args,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
serverClosed() {
|
|
55
|
+
void this._currentClient?.close().catch(logUnhandledError);
|
|
56
|
+
}
|
|
57
|
+
async _callContextSwitchTool(params) {
|
|
58
|
+
try {
|
|
59
|
+
const factory = this._mcpProviders.find(factory => factory.name === params.name);
|
|
60
|
+
if (!factory)
|
|
61
|
+
throw new Error('Unknown connection method: ' + params.name);
|
|
62
|
+
await this._setCurrentClient(factory);
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
_defineContextSwitchTool() {
|
|
75
|
+
return {
|
|
76
|
+
name: 'browser_connect',
|
|
77
|
+
description: [
|
|
78
|
+
'Connect to a browser using one of the available methods:',
|
|
79
|
+
...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
|
|
80
|
+
].join('\n'),
|
|
81
|
+
inputSchema: zodToJsonSchema(z.object({
|
|
82
|
+
name: z.enum(this._mcpProviders.map(factory => factory.name)).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
|
|
83
|
+
}), { strictUnions: true }),
|
|
84
|
+
annotations: {
|
|
85
|
+
title: 'Connect to a browser context',
|
|
86
|
+
readOnlyHint: true,
|
|
87
|
+
openWorldHint: false,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async _setCurrentClient(factory) {
|
|
92
|
+
await this._currentClient?.close();
|
|
93
|
+
this._currentClient = undefined;
|
|
94
|
+
const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version });
|
|
95
|
+
client.registerCapabilities({
|
|
96
|
+
roots: {
|
|
97
|
+
listRoots: true,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
|
|
101
|
+
client.setRequestHandler(PingRequestSchema, () => ({}));
|
|
102
|
+
const transport = await factory.connect();
|
|
103
|
+
await client.connect(transport);
|
|
104
|
+
this._currentClient = client;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/lib/mcp/server.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import debug from 'debug';
|
|
16
17
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
18
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import { ManualPromise } from '../utils/manualPromise.js';
|
|
20
|
+
import { logUnhandledError } from '../utils/log.js';
|
|
21
|
+
const serverDebug = debug('pw:mcp:server');
|
|
21
22
|
export async function connect(serverBackendFactory, transport, runHeartbeat) {
|
|
22
23
|
const backend = serverBackendFactory();
|
|
23
24
|
const server = createServer(backend, runHeartbeat);
|
|
@@ -30,43 +31,45 @@ export function createServer(backend, runHeartbeat) {
|
|
|
30
31
|
tools: {},
|
|
31
32
|
}
|
|
32
33
|
});
|
|
33
|
-
const tools = backend.tools();
|
|
34
34
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
annotations: {
|
|
40
|
-
title: tool.title,
|
|
41
|
-
readOnlyHint: tool.type === 'readOnly',
|
|
42
|
-
destructiveHint: tool.type === 'destructive',
|
|
43
|
-
openWorldHint: true,
|
|
44
|
-
},
|
|
45
|
-
})) };
|
|
35
|
+
serverDebug('listTools');
|
|
36
|
+
await initializedPromise;
|
|
37
|
+
const tools = await backend.listTools();
|
|
38
|
+
return { tools };
|
|
46
39
|
});
|
|
47
40
|
let heartbeatRunning = false;
|
|
48
41
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
+
serverDebug('callTool', request);
|
|
49
43
|
await initializedPromise;
|
|
50
44
|
if (runHeartbeat && !heartbeatRunning) {
|
|
51
45
|
heartbeatRunning = true;
|
|
52
46
|
startHeartbeat(server);
|
|
53
47
|
}
|
|
54
|
-
const errorResult = (...messages) => ({
|
|
55
|
-
content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }],
|
|
56
|
-
isError: true,
|
|
57
|
-
});
|
|
58
|
-
const tool = tools.find(tool => tool.name === request.params.name);
|
|
59
|
-
if (!tool)
|
|
60
|
-
return errorResult(`Error: Tool "${request.params.name}" not found`);
|
|
61
48
|
try {
|
|
62
|
-
return await backend.callTool(
|
|
49
|
+
return await backend.callTool(request.params.name, request.params.arguments || {});
|
|
63
50
|
}
|
|
64
51
|
catch (error) {
|
|
65
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: '### Result\n' + String(error) }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
66
56
|
}
|
|
67
57
|
});
|
|
68
|
-
addServerListener(server, 'initialized', () => {
|
|
69
|
-
|
|
58
|
+
addServerListener(server, 'initialized', async () => {
|
|
59
|
+
try {
|
|
60
|
+
const capabilities = server.getClientCapabilities();
|
|
61
|
+
let clientRoots = [];
|
|
62
|
+
if (capabilities?.roots) {
|
|
63
|
+
const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] }));
|
|
64
|
+
clientRoots = roots;
|
|
65
|
+
}
|
|
66
|
+
const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
|
|
67
|
+
await backend.initialize?.(clientVersion, clientRoots);
|
|
68
|
+
initializedPromise.resolve();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
logUnhandledError(e);
|
|
72
|
+
}
|
|
70
73
|
});
|
|
71
74
|
addServerListener(server, 'close', () => backend.serverClosed?.());
|
|
72
75
|
return server;
|
package/lib/mcp/tool.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 { zodToJsonSchema } from 'zod-to-json-schema';
|
|
17
|
+
export function toMcpTool(tool) {
|
|
18
|
+
return {
|
|
19
|
+
name: tool.name,
|
|
20
|
+
description: tool.description,
|
|
21
|
+
inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }),
|
|
22
|
+
annotations: {
|
|
23
|
+
title: tool.title,
|
|
24
|
+
readOnlyHint: tool.type === 'readOnly',
|
|
25
|
+
destructiveHint: tool.type === 'destructive',
|
|
26
|
+
openWorldHint: true,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/lib/mcp/transport.js
CHANGED
|
@@ -18,7 +18,7 @@ import debug from 'debug';
|
|
|
18
18
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
19
19
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
20
20
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
-
import { httpAddressToString, startHttpServer } from '../httpServer.js';
|
|
21
|
+
import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
|
|
22
22
|
import * as mcpServer from './server.js';
|
|
23
23
|
export async function start(serverBackendFactory, options) {
|
|
24
24
|
if (options.port !== undefined) {
|
package/lib/program.js
CHANGED
|
@@ -14,16 +14,17 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { program, Option } from 'commander';
|
|
17
|
-
|
|
18
|
-
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|
17
|
+
import * as mcpServer from './mcp/server.js';
|
|
19
18
|
import * as mcpTransport from './mcp/transport.js';
|
|
20
19
|
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
|
21
|
-
import { packageJSON } from './package.js';
|
|
22
|
-
import { createExtensionContextFactory, runWithExtension } from './extension/main.js';
|
|
23
|
-
import { BrowserServerBackend } from './browserServerBackend.js';
|
|
20
|
+
import { packageJSON } from './utils/package.js';
|
|
24
21
|
import { Context } from './context.js';
|
|
25
22
|
import { contextFactory } from './browserContextFactory.js';
|
|
26
23
|
import { runLoopTools } from './loopTools/main.js';
|
|
24
|
+
import { ProxyBackend } from './mcp/proxyBackend.js';
|
|
25
|
+
import { BrowserServerBackend } from './browserServerBackend.js';
|
|
26
|
+
import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
|
|
27
|
+
import { InProcessTransport } from './mcp/inProcessTransport.js';
|
|
27
28
|
program
|
|
28
29
|
.version('Version ' + packageJSON.version)
|
|
29
30
|
.name(packageJSON.name)
|
|
@@ -36,6 +37,7 @@ program
|
|
|
36
37
|
.option('--config <path>', 'path to the configuration file.')
|
|
37
38
|
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
|
|
38
39
|
.option('--executable-path <path>', 'path to the browser executable.')
|
|
40
|
+
.option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.')
|
|
39
41
|
.option('--headless', 'run browser in headless mode, headed by default')
|
|
40
42
|
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
41
43
|
.option('--ignore-https-errors', 'ignore https errors')
|
|
@@ -52,7 +54,6 @@ program
|
|
|
52
54
|
.option('--user-agent <ua string>', 'specify user agent string')
|
|
53
55
|
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
|
54
56
|
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
55
|
-
.addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
|
|
56
57
|
.addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
|
|
57
58
|
.addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
|
|
58
59
|
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
|
@@ -65,7 +66,9 @@ program
|
|
|
65
66
|
}
|
|
66
67
|
const config = await resolveCLIConfig(options);
|
|
67
68
|
if (options.extension) {
|
|
68
|
-
|
|
69
|
+
const contextFactory = createExtensionContextFactory(config);
|
|
70
|
+
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
|
71
|
+
await mcpTransport.start(serverBackendFactory, config.server);
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
74
|
if (options.loopTools) {
|
|
@@ -73,18 +76,10 @@ program
|
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
78
|
const browserContextFactory = contextFactory(config);
|
|
76
|
-
const
|
|
79
|
+
const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
|
|
77
80
|
if (options.connectTool)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await mcpTransport.start(serverBackendFactory, config.server);
|
|
81
|
-
if (config.saveTrace) {
|
|
82
|
-
const server = await startTraceViewerServer();
|
|
83
|
-
const urlPrefix = server.urlPrefix('human-readable');
|
|
84
|
-
const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json';
|
|
85
|
-
// eslint-disable-next-line no-console
|
|
86
|
-
console.error('\nTrace viewer listening on ' + url);
|
|
87
|
-
}
|
|
81
|
+
providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
|
|
82
|
+
await mcpTransport.start(() => new ProxyBackend(providers), config.server);
|
|
88
83
|
});
|
|
89
84
|
function setupExitWatchdog() {
|
|
90
85
|
let isExiting = false;
|
|
@@ -100,4 +95,17 @@ function setupExitWatchdog() {
|
|
|
100
95
|
process.on('SIGINT', handleExit);
|
|
101
96
|
process.on('SIGTERM', handleExit);
|
|
102
97
|
}
|
|
98
|
+
function createExtensionContextFactory(config) {
|
|
99
|
+
return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
|
100
|
+
}
|
|
101
|
+
function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
|
|
102
|
+
return {
|
|
103
|
+
name: browserContextFactory.name,
|
|
104
|
+
description: browserContextFactory.description,
|
|
105
|
+
connect: async () => {
|
|
106
|
+
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
|
|
107
|
+
return new InProcessTransport(server);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
103
111
|
void program.parseAsync(process.argv);
|
package/lib/sessionLog.js
CHANGED
package/lib/tab.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { EventEmitter } from 'events';
|
|
17
17
|
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
|
18
|
-
import { logUnhandledError } from './log.js';
|
|
19
|
-
import { ManualPromise } from './manualPromise.js';
|
|
18
|
+
import { logUnhandledError } from './utils/log.js';
|
|
19
|
+
import { ManualPromise } from './utils/manualPromise.js';
|
|
20
20
|
export const TabEvents = {
|
|
21
21
|
modalState: 'modalState'
|
|
22
22
|
};
|
package/lib/tools/evaluate.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
|
-
import * as javascript from '../
|
|
18
|
+
import * as javascript from '../utils/codegen.js';
|
|
19
19
|
import { generateLocator } from './utils.js';
|
|
20
20
|
const evaluateSchema = z.object({
|
|
21
21
|
function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
|
package/lib/tools/keyboard.js
CHANGED
|
@@ -17,7 +17,7 @@ import { z } from 'zod';
|
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
18
|
import { elementSchema } from './snapshot.js';
|
|
19
19
|
import { generateLocator } from './utils.js';
|
|
20
|
-
import * as javascript from '../
|
|
20
|
+
import * as javascript from '../utils/codegen.js';
|
|
21
21
|
const pressKey = defineTabTool({
|
|
22
22
|
capability: 'core',
|
|
23
23
|
schema: {
|
package/lib/tools/pdf.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
|
-
import * as javascript from '../
|
|
18
|
+
import * as javascript from '../utils/codegen.js';
|
|
19
19
|
const pdfSchema = z.object({
|
|
20
20
|
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
|
21
21
|
});
|
package/lib/tools/screenshot.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
|
-
import * as javascript from '../
|
|
18
|
+
import * as javascript from '../utils/codegen.js';
|
|
19
19
|
import { generateLocator } from './utils.js';
|
|
20
20
|
const screenshotSchema = z.object({
|
|
21
21
|
type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool, defineTool } from './tool.js';
|
|
18
|
-
import * as javascript from '../
|
|
18
|
+
import * as javascript from '../utils/codegen.js';
|
|
19
19
|
import { generateLocator } from './utils.js';
|
|
20
20
|
const snapshot = defineTool({
|
|
21
21
|
capability: 'core',
|
package/lib/tools/wait.js
CHANGED
|
@@ -31,20 +31,19 @@ const wait = defineTool({
|
|
|
31
31
|
handle: async (context, params, response) => {
|
|
32
32
|
if (!params.text && !params.textGone && !params.time)
|
|
33
33
|
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
-
const code = [];
|
|
35
34
|
if (params.time) {
|
|
36
|
-
|
|
35
|
+
response.addCode(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
36
|
await new Promise(f => setTimeout(f, Math.min(30000, params.time * 1000)));
|
|
38
37
|
}
|
|
39
38
|
const tab = context.currentTabOrDie();
|
|
40
39
|
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
40
|
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
41
|
if (goneLocator) {
|
|
43
|
-
|
|
42
|
+
response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
43
|
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
44
|
}
|
|
46
45
|
if (locator) {
|
|
47
|
-
|
|
46
|
+
response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
47
|
await locator.waitFor({ state: 'visible' });
|
|
49
48
|
}
|
|
50
49
|
response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
|
|
@@ -25,7 +25,7 @@ export function escapeWithQuotes(text, char = '\'') {
|
|
|
25
25
|
if (char === '"')
|
|
26
26
|
return char + escapedText.replace(/["]/g, '\\"') + char;
|
|
27
27
|
if (char === '`')
|
|
28
|
-
return char + escapedText.replace(/[`]/g, '
|
|
28
|
+
return char + escapedText.replace(/[`]/g, '\\`') + char;
|
|
29
29
|
throw new Error('Invalid escape char');
|
|
30
30
|
}
|
|
31
31
|
export function quote(text) {
|
|
@@ -27,6 +27,10 @@ export function cacheDir() {
|
|
|
27
27
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
28
28
|
return path.join(cacheDirectory, 'ms-playwright');
|
|
29
29
|
}
|
|
30
|
-
export
|
|
31
|
-
|
|
30
|
+
export function sanitizeForFilePath(s) {
|
|
31
|
+
const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
|
32
|
+
const separator = s.lastIndexOf('.');
|
|
33
|
+
if (separator === -1)
|
|
34
|
+
return sanitize(s);
|
|
35
|
+
return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
|
|
32
36
|
}
|
|
@@ -14,13 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import crypto from 'crypto';
|
|
17
|
+
export function createGuid() {
|
|
18
|
+
return crypto.randomBytes(16).toString('hex');
|
|
19
|
+
}
|
|
17
20
|
export function createHash(data) {
|
|
18
21
|
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
|
|
19
22
|
}
|
|
20
|
-
export function sanitizeForFilePath(s) {
|
|
21
|
-
const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
|
22
|
-
const separator = s.lastIndexOf('.');
|
|
23
|
-
if (separator === -1)
|
|
24
|
-
return sanitize(s);
|
|
25
|
-
return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
|
|
26
|
-
}
|
|
@@ -17,4 +17,4 @@ import fs from 'fs';
|
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import url from 'url';
|
|
19
19
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
20
|
-
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
|
20
|
+
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
|
-
"lint": "npm run update-readme && eslint . && tsc --noEmit",
|
|
20
|
+
"lint": "npm run update-readme && npm run check-deps && eslint . && tsc --noEmit",
|
|
21
21
|
"lint-fix": "eslint . --fix",
|
|
22
|
+
"check-deps": "node utils/check-deps.js",
|
|
22
23
|
"update-readme": "node utils/update-readme.js",
|
|
23
24
|
"watch": "tsc --watch",
|
|
24
25
|
"test": "playwright test",
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
"debug": "^4.4.1",
|
|
43
44
|
"dotenv": "^17.2.0",
|
|
44
45
|
"mime": "^4.0.7",
|
|
45
|
-
"playwright": "1.55.0-alpha-2025-08-
|
|
46
|
-
"playwright-core": "1.55.0-alpha-2025-08-
|
|
46
|
+
"playwright": "1.55.0-alpha-2025-08-12",
|
|
47
|
+
"playwright-core": "1.55.0-alpha-2025-08-12",
|
|
47
48
|
"ws": "^8.18.1",
|
|
48
49
|
"zod": "^3.24.1",
|
|
49
50
|
"zod-to-json-schema": "^3.24.4"
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"@anthropic-ai/sdk": "^0.57.0",
|
|
53
54
|
"@eslint/eslintrc": "^3.2.0",
|
|
54
55
|
"@eslint/js": "^9.19.0",
|
|
55
|
-
"@playwright/test": "1.55.0-alpha-2025-08-
|
|
56
|
+
"@playwright/test": "1.55.0-alpha-2025-08-12",
|
|
56
57
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
57
58
|
"@types/debug": "^4.1.12",
|
|
58
59
|
"@types/node": "^22.13.10",
|
package/lib/extension/main.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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 { ExtensionContextFactory } from './extensionContextFactory.js';
|
|
17
|
-
import { BrowserServerBackend } from '../browserServerBackend.js';
|
|
18
|
-
import * as mcpTransport from '../mcp/transport.js';
|
|
19
|
-
export async function runWithExtension(config) {
|
|
20
|
-
const contextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
|
21
|
-
const serverBackendFactory = () => new BrowserServerBackend(config, [contextFactory]);
|
|
22
|
-
await mcpTransport.start(serverBackendFactory, config.server);
|
|
23
|
-
}
|
|
24
|
-
export function createExtensionContextFactory(config) {
|
|
25
|
-
return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
|
|
26
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|