@szymonrybczak/playwright-mcp 0.0.2 → 0.0.4
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/index.d.ts +21 -8
- package/lib/cjs/_virtual/_commonjsHelpers.js +9 -0
- package/lib/cjs/_virtual/browser.js +11 -0
- package/lib/cjs/_virtual/browser2.js +7 -0
- package/lib/cjs/cloudflare/package.json.js +10 -0
- package/lib/cjs/index.js +28 -0
- package/lib/cjs/node_modules/debug/src/browser.js +290 -0
- package/lib/cjs/node_modules/debug/src/common.js +307 -0
- package/lib/cjs/node_modules/ms/index.js +176 -0
- package/lib/cjs/package.js +9 -0
- package/lib/cjs/src/browserContextFactory.js +240 -0
- package/lib/cjs/src/config.js +77 -0
- package/lib/cjs/src/connection.js +74 -0
- package/lib/cjs/src/context.js +281 -0
- package/lib/cjs/src/fileUtils.js +25 -0
- package/lib/cjs/src/index.js +15 -0
- package/lib/cjs/src/javascript.js +41 -0
- package/lib/cjs/src/manualPromise.js +39 -0
- package/lib/cjs/src/pageSnapshot.js +35 -0
- package/lib/cjs/src/tab.js +91 -0
- package/lib/cjs/src/tools/common.js +60 -0
- package/lib/cjs/src/tools/console.js +36 -0
- package/lib/cjs/src/tools/dialogs.js +44 -0
- package/lib/cjs/src/tools/files.js +43 -0
- package/lib/cjs/src/tools/install.js +49 -0
- package/lib/cjs/src/tools/keyboard.js +38 -0
- package/lib/cjs/src/tools/navigate.js +85 -0
- package/lib/cjs/src/tools/network.js +43 -0
- package/lib/cjs/src/tools/pdf.js +42 -0
- package/lib/cjs/src/tools/screenshot.js +69 -0
- package/lib/cjs/src/tools/snapshot.js +195 -0
- package/lib/cjs/src/tools/tabs.js +110 -0
- package/lib/cjs/src/tools/testing.js +52 -0
- package/lib/cjs/src/tools/tool.js +9 -0
- package/lib/cjs/src/tools/utils.js +75 -0
- package/lib/cjs/src/tools/vision.js +181 -0
- package/lib/cjs/src/tools/wait.js +51 -0
- package/lib/cjs/src/tools.js +54 -0
- package/lib/esm/_virtual/_commonjsHelpers.js +5 -0
- package/lib/esm/_virtual/browser.js +7 -0
- package/lib/esm/_virtual/browser2.js +3 -0
- package/lib/esm/cloudflare/package.json.js +5 -0
- package/lib/esm/index.js +24 -0
- package/lib/esm/node_modules/debug/src/browser.js +286 -0
- package/lib/esm/node_modules/debug/src/common.js +303 -0
- package/lib/esm/node_modules/ms/index.js +172 -0
- package/lib/esm/package.js +5 -0
- package/lib/esm/src/browserContextFactory.js +216 -0
- package/lib/esm/src/config.js +72 -0
- package/lib/esm/src/connection.js +69 -0
- package/lib/esm/src/context.js +277 -0
- package/lib/esm/src/fileUtils.js +20 -0
- package/lib/esm/src/index.js +11 -0
- package/lib/esm/src/javascript.js +35 -0
- package/lib/esm/src/manualPromise.js +35 -0
- package/lib/esm/src/pageSnapshot.js +31 -0
- package/lib/esm/src/tab.js +87 -0
- package/lib/esm/src/tools/common.js +56 -0
- package/lib/esm/src/tools/console.js +32 -0
- package/lib/esm/src/tools/dialogs.js +40 -0
- package/lib/esm/src/tools/files.js +39 -0
- package/lib/esm/src/tools/install.js +45 -0
- package/lib/esm/src/tools/keyboard.js +34 -0
- package/lib/esm/src/tools/navigate.js +81 -0
- package/lib/esm/src/tools/network.js +39 -0
- package/lib/esm/src/tools/pdf.js +38 -0
- package/lib/esm/src/tools/screenshot.js +65 -0
- package/lib/esm/src/tools/snapshot.js +191 -0
- package/lib/esm/src/tools/tabs.js +106 -0
- package/lib/esm/src/tools/testing.js +48 -0
- package/lib/esm/src/tools/tool.js +5 -0
- package/lib/esm/src/tools/utils.js +68 -0
- package/lib/esm/src/tools/vision.js +177 -0
- package/lib/esm/src/tools/wait.js +47 -0
- package/lib/esm/src/tools.js +49 -0
- package/package.json +15 -44
- package/LICENSE +0 -202
- package/README.md +0 -508
- package/cli.js +0 -18
- package/config.d.ts +0 -128
- package/index.js +0 -19
- package/lib/browserContextFactory.js +0 -227
- package/lib/browserServer.js +0 -151
- package/lib/config.js +0 -189
- package/lib/connection.js +0 -82
- package/lib/context.js +0 -291
- package/lib/fileUtils.js +0 -32
- package/lib/httpServer.js +0 -201
- package/lib/index.js +0 -36
- package/lib/javascript.js +0 -49
- package/lib/manualPromise.js +0 -111
- package/lib/package.js +0 -20
- package/lib/pageSnapshot.js +0 -43
- package/lib/program.js +0 -72
- package/lib/server.js +0 -48
- package/lib/tab.js +0 -101
- package/lib/tools/common.js +0 -68
- package/lib/tools/console.js +0 -44
- package/lib/tools/dialogs.js +0 -52
- package/lib/tools/files.js +0 -51
- package/lib/tools/install.js +0 -57
- package/lib/tools/keyboard.js +0 -46
- package/lib/tools/navigate.js +0 -93
- package/lib/tools/network.js +0 -51
- package/lib/tools/pdf.js +0 -49
- package/lib/tools/screenshot.js +0 -77
- package/lib/tools/snapshot.js +0 -204
- package/lib/tools/tabs.js +0 -118
- package/lib/tools/testing.js +0 -60
- package/lib/tools/tool.js +0 -18
- package/lib/tools/utils.js +0 -80
- package/lib/tools/vision.js +0 -189
- package/lib/tools/wait.js +0 -59
- package/lib/tools.js +0 -61
- package/lib/transport.js +0 -133
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
var ms;
|
|
6
|
+
var hasRequiredMs;
|
|
7
|
+
|
|
8
|
+
function requireMs () {
|
|
9
|
+
if (hasRequiredMs) return ms;
|
|
10
|
+
hasRequiredMs = 1;
|
|
11
|
+
var s = 1000;
|
|
12
|
+
var m = s * 60;
|
|
13
|
+
var h = m * 60;
|
|
14
|
+
var d = h * 24;
|
|
15
|
+
var w = d * 7;
|
|
16
|
+
var y = d * 365.25;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse or format the given `val`.
|
|
20
|
+
*
|
|
21
|
+
* Options:
|
|
22
|
+
*
|
|
23
|
+
* - `long` verbose formatting [false]
|
|
24
|
+
*
|
|
25
|
+
* @param {String|Number} val
|
|
26
|
+
* @param {Object} [options]
|
|
27
|
+
* @throws {Error} throw an error if val is not a non-empty string or a number
|
|
28
|
+
* @return {String|Number}
|
|
29
|
+
* @api public
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
ms = function (val, options) {
|
|
33
|
+
options = options || {};
|
|
34
|
+
var type = typeof val;
|
|
35
|
+
if (type === 'string' && val.length > 0) {
|
|
36
|
+
return parse(val);
|
|
37
|
+
} else if (type === 'number' && isFinite(val)) {
|
|
38
|
+
return options.long ? fmtLong(val) : fmtShort(val);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
'val is not a non-empty string or a valid number. val=' +
|
|
42
|
+
JSON.stringify(val)
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse the given `str` and return milliseconds.
|
|
48
|
+
*
|
|
49
|
+
* @param {String} str
|
|
50
|
+
* @return {Number}
|
|
51
|
+
* @api private
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
function parse(str) {
|
|
55
|
+
str = String(str);
|
|
56
|
+
if (str.length > 100) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
|
|
60
|
+
str
|
|
61
|
+
);
|
|
62
|
+
if (!match) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
var n = parseFloat(match[1]);
|
|
66
|
+
var type = (match[2] || 'ms').toLowerCase();
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'years':
|
|
69
|
+
case 'year':
|
|
70
|
+
case 'yrs':
|
|
71
|
+
case 'yr':
|
|
72
|
+
case 'y':
|
|
73
|
+
return n * y;
|
|
74
|
+
case 'weeks':
|
|
75
|
+
case 'week':
|
|
76
|
+
case 'w':
|
|
77
|
+
return n * w;
|
|
78
|
+
case 'days':
|
|
79
|
+
case 'day':
|
|
80
|
+
case 'd':
|
|
81
|
+
return n * d;
|
|
82
|
+
case 'hours':
|
|
83
|
+
case 'hour':
|
|
84
|
+
case 'hrs':
|
|
85
|
+
case 'hr':
|
|
86
|
+
case 'h':
|
|
87
|
+
return n * h;
|
|
88
|
+
case 'minutes':
|
|
89
|
+
case 'minute':
|
|
90
|
+
case 'mins':
|
|
91
|
+
case 'min':
|
|
92
|
+
case 'm':
|
|
93
|
+
return n * m;
|
|
94
|
+
case 'seconds':
|
|
95
|
+
case 'second':
|
|
96
|
+
case 'secs':
|
|
97
|
+
case 'sec':
|
|
98
|
+
case 's':
|
|
99
|
+
return n * s;
|
|
100
|
+
case 'milliseconds':
|
|
101
|
+
case 'millisecond':
|
|
102
|
+
case 'msecs':
|
|
103
|
+
case 'msec':
|
|
104
|
+
case 'ms':
|
|
105
|
+
return n;
|
|
106
|
+
default:
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Short format for `ms`.
|
|
113
|
+
*
|
|
114
|
+
* @param {Number} ms
|
|
115
|
+
* @return {String}
|
|
116
|
+
* @api private
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
function fmtShort(ms) {
|
|
120
|
+
var msAbs = Math.abs(ms);
|
|
121
|
+
if (msAbs >= d) {
|
|
122
|
+
return Math.round(ms / d) + 'd';
|
|
123
|
+
}
|
|
124
|
+
if (msAbs >= h) {
|
|
125
|
+
return Math.round(ms / h) + 'h';
|
|
126
|
+
}
|
|
127
|
+
if (msAbs >= m) {
|
|
128
|
+
return Math.round(ms / m) + 'm';
|
|
129
|
+
}
|
|
130
|
+
if (msAbs >= s) {
|
|
131
|
+
return Math.round(ms / s) + 's';
|
|
132
|
+
}
|
|
133
|
+
return ms + 'ms';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Long format for `ms`.
|
|
138
|
+
*
|
|
139
|
+
* @param {Number} ms
|
|
140
|
+
* @return {String}
|
|
141
|
+
* @api private
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
function fmtLong(ms) {
|
|
145
|
+
var msAbs = Math.abs(ms);
|
|
146
|
+
if (msAbs >= d) {
|
|
147
|
+
return plural(ms, msAbs, d, 'day');
|
|
148
|
+
}
|
|
149
|
+
if (msAbs >= h) {
|
|
150
|
+
return plural(ms, msAbs, h, 'hour');
|
|
151
|
+
}
|
|
152
|
+
if (msAbs >= m) {
|
|
153
|
+
return plural(ms, msAbs, m, 'minute');
|
|
154
|
+
}
|
|
155
|
+
if (msAbs >= s) {
|
|
156
|
+
return plural(ms, msAbs, s, 'second');
|
|
157
|
+
}
|
|
158
|
+
return ms + ' ms';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pluralization helper.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
function plural(ms, msAbs, n, name) {
|
|
166
|
+
var isPlural = msAbs >= n * 1.5;
|
|
167
|
+
return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : '');
|
|
168
|
+
}
|
|
169
|
+
return ms;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { requireMs as __require };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import debug from '../_virtual/browser.js';
|
|
6
|
+
import * as playwright from '@szymonrybczak/patchright';
|
|
7
|
+
import { userDataDir } from './fileUtils.js';
|
|
8
|
+
|
|
9
|
+
const testDebug = debug("pw:mcp:test");
|
|
10
|
+
function contextFactory(browserConfig) {
|
|
11
|
+
if (browserConfig.remoteEndpoint)
|
|
12
|
+
return new RemoteContextFactory(browserConfig);
|
|
13
|
+
if (browserConfig.cdpEndpoint)
|
|
14
|
+
return new CdpContextFactory(browserConfig);
|
|
15
|
+
if (browserConfig.isolated)
|
|
16
|
+
return new IsolatedContextFactory(browserConfig);
|
|
17
|
+
if (browserConfig.browserAgent)
|
|
18
|
+
return new BrowserServerContextFactory(browserConfig);
|
|
19
|
+
return new PersistentContextFactory(browserConfig);
|
|
20
|
+
}
|
|
21
|
+
class BaseContextFactory {
|
|
22
|
+
browserConfig;
|
|
23
|
+
_browserPromise;
|
|
24
|
+
name;
|
|
25
|
+
constructor(name, browserConfig) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
this.browserConfig = browserConfig;
|
|
28
|
+
}
|
|
29
|
+
async _obtainBrowser() {
|
|
30
|
+
if (this._browserPromise)
|
|
31
|
+
return this._browserPromise;
|
|
32
|
+
testDebug(`obtain browser (${this.name})`);
|
|
33
|
+
this._browserPromise = this._doObtainBrowser();
|
|
34
|
+
void this._browserPromise.then((browser) => {
|
|
35
|
+
browser.on("disconnected", () => {
|
|
36
|
+
this._browserPromise = void 0;
|
|
37
|
+
});
|
|
38
|
+
}).catch(() => {
|
|
39
|
+
this._browserPromise = void 0;
|
|
40
|
+
});
|
|
41
|
+
return this._browserPromise;
|
|
42
|
+
}
|
|
43
|
+
async _doObtainBrowser() {
|
|
44
|
+
throw new Error("Not implemented");
|
|
45
|
+
}
|
|
46
|
+
async createContext() {
|
|
47
|
+
testDebug(`create browser context (${this.name})`);
|
|
48
|
+
const browser = await this._obtainBrowser();
|
|
49
|
+
const browserContext = await this._doCreateContext(browser);
|
|
50
|
+
return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) };
|
|
51
|
+
}
|
|
52
|
+
async _doCreateContext(browser) {
|
|
53
|
+
throw new Error("Not implemented");
|
|
54
|
+
}
|
|
55
|
+
async _closeBrowserContext(browserContext, browser) {
|
|
56
|
+
testDebug(`close browser context (${this.name})`);
|
|
57
|
+
if (browser.contexts().length === 1)
|
|
58
|
+
this._browserPromise = void 0;
|
|
59
|
+
await browserContext.close().catch(() => {
|
|
60
|
+
});
|
|
61
|
+
if (browser.contexts().length === 0) {
|
|
62
|
+
testDebug(`close browser (${this.name})`);
|
|
63
|
+
await browser.close().catch(() => {
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
class IsolatedContextFactory extends BaseContextFactory {
|
|
69
|
+
constructor(browserConfig) {
|
|
70
|
+
super("isolated", browserConfig);
|
|
71
|
+
}
|
|
72
|
+
async _doObtainBrowser() {
|
|
73
|
+
await injectCdpPort(this.browserConfig);
|
|
74
|
+
const browserType = playwright[this.browserConfig.browserName];
|
|
75
|
+
return browserType.launch({
|
|
76
|
+
...this.browserConfig.launchOptions,
|
|
77
|
+
handleSIGINT: false,
|
|
78
|
+
handleSIGTERM: false
|
|
79
|
+
}).catch((error) => {
|
|
80
|
+
if (error.message.includes("Executable doesn't exist"))
|
|
81
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async _doCreateContext(browser) {
|
|
86
|
+
return browser.newContext(this.browserConfig.contextOptions);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
class CdpContextFactory extends BaseContextFactory {
|
|
90
|
+
constructor(browserConfig) {
|
|
91
|
+
super("cdp", browserConfig);
|
|
92
|
+
}
|
|
93
|
+
async _doObtainBrowser() {
|
|
94
|
+
return playwright.chromium.connectOverCDP(this.browserConfig.cdpEndpoint);
|
|
95
|
+
}
|
|
96
|
+
async _doCreateContext(browser) {
|
|
97
|
+
return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
class RemoteContextFactory extends BaseContextFactory {
|
|
101
|
+
constructor(browserConfig) {
|
|
102
|
+
super("remote", browserConfig);
|
|
103
|
+
}
|
|
104
|
+
async _doObtainBrowser() {
|
|
105
|
+
const url = new URL(this.browserConfig.remoteEndpoint);
|
|
106
|
+
url.searchParams.set("browser", this.browserConfig.browserName);
|
|
107
|
+
if (this.browserConfig.launchOptions)
|
|
108
|
+
url.searchParams.set("launch-options", JSON.stringify(this.browserConfig.launchOptions));
|
|
109
|
+
return playwright[this.browserConfig.browserName].connect(String(url));
|
|
110
|
+
}
|
|
111
|
+
async _doCreateContext(browser) {
|
|
112
|
+
return browser.newContext();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
class PersistentContextFactory {
|
|
116
|
+
browserConfig;
|
|
117
|
+
_userDataDirs = /* @__PURE__ */ new Set();
|
|
118
|
+
constructor(browserConfig) {
|
|
119
|
+
this.browserConfig = browserConfig;
|
|
120
|
+
}
|
|
121
|
+
async createContext() {
|
|
122
|
+
await injectCdpPort(this.browserConfig);
|
|
123
|
+
testDebug("create browser context (persistent)");
|
|
124
|
+
const userDataDir2 = this.browserConfig.userDataDir ?? await this._createUserDataDir();
|
|
125
|
+
this._userDataDirs.add(userDataDir2);
|
|
126
|
+
testDebug("lock user data dir", userDataDir2);
|
|
127
|
+
const browserType = playwright[this.browserConfig.browserName];
|
|
128
|
+
for (let i = 0; i < 5; i++) {
|
|
129
|
+
try {
|
|
130
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir2, {
|
|
131
|
+
...this.browserConfig.launchOptions,
|
|
132
|
+
...this.browserConfig.contextOptions,
|
|
133
|
+
handleSIGINT: false,
|
|
134
|
+
handleSIGTERM: false
|
|
135
|
+
});
|
|
136
|
+
const close = () => this._closeBrowserContext(browserContext, userDataDir2);
|
|
137
|
+
return { browserContext, close };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.message.includes("Executable doesn't exist"))
|
|
140
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
141
|
+
if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL")) {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Browser is already in use for ${userDataDir2}, use --isolated to run multiple instances of the same browser`);
|
|
149
|
+
}
|
|
150
|
+
async _closeBrowserContext(browserContext, userDataDir2) {
|
|
151
|
+
testDebug("close browser context (persistent)");
|
|
152
|
+
testDebug("release user data dir", userDataDir2);
|
|
153
|
+
await browserContext.close().catch(() => {
|
|
154
|
+
});
|
|
155
|
+
this._userDataDirs.delete(userDataDir2);
|
|
156
|
+
testDebug("close browser context complete (persistent)");
|
|
157
|
+
}
|
|
158
|
+
async _createUserDataDir() {
|
|
159
|
+
let cacheDirectory;
|
|
160
|
+
if (process.platform === "linux")
|
|
161
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
162
|
+
else if (process.platform === "darwin")
|
|
163
|
+
cacheDirectory = path.join(os.homedir(), "Library", "Caches");
|
|
164
|
+
else if (process.platform === "win32")
|
|
165
|
+
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
166
|
+
else
|
|
167
|
+
throw new Error("Unsupported platform: " + process.platform);
|
|
168
|
+
const result = path.join(cacheDirectory, "ms-playwright", `mcp-${this.browserConfig.launchOptions?.channel ?? this.browserConfig?.browserName}-profile`);
|
|
169
|
+
await fs.promises.mkdir(result, { recursive: true });
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class BrowserServerContextFactory extends BaseContextFactory {
|
|
174
|
+
constructor(browserConfig) {
|
|
175
|
+
super("persistent", browserConfig);
|
|
176
|
+
}
|
|
177
|
+
async _doObtainBrowser() {
|
|
178
|
+
const response = await fetch(new URL(`/json/launch`, this.browserConfig.browserAgent), {
|
|
179
|
+
method: "POST",
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
browserType: this.browserConfig.browserName,
|
|
182
|
+
userDataDir: this.browserConfig.userDataDir ?? await this._createUserDataDir(),
|
|
183
|
+
launchOptions: this.browserConfig.launchOptions,
|
|
184
|
+
contextOptions: this.browserConfig.contextOptions
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
const info = await response.json();
|
|
188
|
+
if (info.error)
|
|
189
|
+
throw new Error(info.error);
|
|
190
|
+
return await playwright.chromium.connectOverCDP(`http://localhost:${info.cdpPort}/`);
|
|
191
|
+
}
|
|
192
|
+
async _doCreateContext(browser) {
|
|
193
|
+
return this.browserConfig.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
194
|
+
}
|
|
195
|
+
async _createUserDataDir() {
|
|
196
|
+
const dir = await userDataDir(this.browserConfig);
|
|
197
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
198
|
+
return dir;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function injectCdpPort(browserConfig) {
|
|
202
|
+
if (browserConfig.browserName === "chromium")
|
|
203
|
+
browserConfig.launchOptions.cdpPort = await findFreePort();
|
|
204
|
+
}
|
|
205
|
+
async function findFreePort() {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const server = net.createServer();
|
|
208
|
+
server.listen(0, () => {
|
|
209
|
+
const { port } = server.address();
|
|
210
|
+
server.close(() => resolve(port));
|
|
211
|
+
});
|
|
212
|
+
server.on("error", reject);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { BrowserServerContextFactory, contextFactory };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import '@szymonrybczak/patchright';
|
|
5
|
+
import { sanitizeForFilePath } from './tools/utils.js';
|
|
6
|
+
|
|
7
|
+
const defaultConfig = {
|
|
8
|
+
browser: {
|
|
9
|
+
browserName: "chromium",
|
|
10
|
+
launchOptions: {
|
|
11
|
+
channel: "chrome",
|
|
12
|
+
headless: os.platform() === "linux" && !process.env.DISPLAY,
|
|
13
|
+
chromiumSandbox: true
|
|
14
|
+
},
|
|
15
|
+
contextOptions: {
|
|
16
|
+
viewport: null
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
network: {
|
|
20
|
+
allowedOrigins: void 0,
|
|
21
|
+
blockedOrigins: void 0
|
|
22
|
+
},
|
|
23
|
+
server: {},
|
|
24
|
+
outputDir: path.join(os.tmpdir(), "playwright-mcp-output", sanitizeForFilePath((/* @__PURE__ */ new Date()).toISOString()))
|
|
25
|
+
};
|
|
26
|
+
async function resolveConfig(config) {
|
|
27
|
+
return mergeConfig(defaultConfig, config);
|
|
28
|
+
}
|
|
29
|
+
async function outputFile(config, name) {
|
|
30
|
+
await fs.promises.mkdir(config.outputDir, { recursive: true });
|
|
31
|
+
const fileName = sanitizeForFilePath(name);
|
|
32
|
+
return path.join(config.outputDir, fileName);
|
|
33
|
+
}
|
|
34
|
+
function pickDefined(obj) {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(obj ?? {}).filter(([_, v]) => v !== void 0)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function mergeConfig(base, overrides) {
|
|
40
|
+
const browser = {
|
|
41
|
+
...pickDefined(base.browser),
|
|
42
|
+
...pickDefined(overrides.browser),
|
|
43
|
+
browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? "chromium",
|
|
44
|
+
isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
|
|
45
|
+
launchOptions: {
|
|
46
|
+
...pickDefined(base.browser?.launchOptions),
|
|
47
|
+
...pickDefined(overrides.browser?.launchOptions),
|
|
48
|
+
...{ assistantMode: true }
|
|
49
|
+
},
|
|
50
|
+
contextOptions: {
|
|
51
|
+
...pickDefined(base.browser?.contextOptions),
|
|
52
|
+
...pickDefined(overrides.browser?.contextOptions)
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
if (browser.browserName !== "chromium" && browser.launchOptions)
|
|
56
|
+
delete browser.launchOptions.channel;
|
|
57
|
+
return {
|
|
58
|
+
...pickDefined(base),
|
|
59
|
+
...pickDefined(overrides),
|
|
60
|
+
browser,
|
|
61
|
+
network: {
|
|
62
|
+
...pickDefined(base.network),
|
|
63
|
+
...pickDefined(overrides.network)
|
|
64
|
+
},
|
|
65
|
+
server: {
|
|
66
|
+
...pickDefined(base.server),
|
|
67
|
+
...pickDefined(overrides.server)
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { outputFile, resolveConfig };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
4
|
+
import { Context } from './context.js';
|
|
5
|
+
import { visionTools, snapshotTools } from './tools.js';
|
|
6
|
+
import { packageJSON } from '../package.js';
|
|
7
|
+
|
|
8
|
+
function createConnection(config, browserContextFactory) {
|
|
9
|
+
const allTools = config.vision ? visionTools : snapshotTools;
|
|
10
|
+
const tools = allTools.filter((tool) => !config.capabilities || tool.capability === "core" || config.capabilities.includes(tool.capability));
|
|
11
|
+
const context = new Context(tools, config, browserContextFactory);
|
|
12
|
+
const server = new Server({ name: "Playwright", version: packageJSON.version }, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
18
|
+
return {
|
|
19
|
+
tools: tools.map((tool) => ({
|
|
20
|
+
name: tool.schema.name,
|
|
21
|
+
description: tool.schema.description,
|
|
22
|
+
inputSchema: zodToJsonSchema(tool.schema.inputSchema),
|
|
23
|
+
annotations: {
|
|
24
|
+
title: tool.schema.title,
|
|
25
|
+
readOnlyHint: tool.schema.type === "readOnly",
|
|
26
|
+
destructiveHint: tool.schema.type === "destructive",
|
|
27
|
+
openWorldHint: true
|
|
28
|
+
}
|
|
29
|
+
}))
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
33
|
+
const errorResult = (...messages) => ({
|
|
34
|
+
content: [{ type: "text", text: messages.join("\n") }],
|
|
35
|
+
isError: true
|
|
36
|
+
});
|
|
37
|
+
const tool = tools.find((tool2) => tool2.schema.name === request.params.name);
|
|
38
|
+
if (!tool)
|
|
39
|
+
return errorResult(`Tool "${request.params.name}" not found`);
|
|
40
|
+
const modalStates = context.modalStates().map((state) => state.type);
|
|
41
|
+
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
|
42
|
+
return errorResult(`The tool "${request.params.name}" can only be used when there is related modal state present.`, ...context.modalStatesMarkdown());
|
|
43
|
+
if (!tool.clearsModalState && modalStates.length)
|
|
44
|
+
return errorResult(`Tool "${request.params.name}" does not handle the modal state.`, ...context.modalStatesMarkdown());
|
|
45
|
+
try {
|
|
46
|
+
return await context.run(tool, request.params.arguments);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return errorResult(String(error));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return new Connection(server, context);
|
|
52
|
+
}
|
|
53
|
+
class Connection {
|
|
54
|
+
server;
|
|
55
|
+
context;
|
|
56
|
+
constructor(server, context) {
|
|
57
|
+
this.server = server;
|
|
58
|
+
this.context = context;
|
|
59
|
+
this.server.oninitialized = () => {
|
|
60
|
+
this.context.clientVersion = this.server.getClientVersion();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async close() {
|
|
64
|
+
await this.server.close();
|
|
65
|
+
await this.context.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { Connection, createConnection };
|