@cotestdev/mcp_playwright 0.0.14 → 0.0.15
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/lib/mcp/browser/browserContextFactory.js +49 -13
- package/lib/mcp/browser/browserServerBackend.js +5 -2
- package/lib/mcp/browser/config.js +95 -23
- package/lib/mcp/browser/context.js +28 -3
- package/lib/mcp/browser/response.js +240 -57
- package/lib/mcp/browser/sessionLog.js +1 -1
- package/lib/mcp/browser/tab.js +96 -69
- package/lib/mcp/browser/tools/common.js +8 -8
- package/lib/mcp/browser/tools/console.js +6 -3
- package/lib/mcp/browser/tools/dialogs.js +13 -13
- package/lib/mcp/browser/tools/evaluate.js +9 -20
- package/lib/mcp/browser/tools/files.js +10 -5
- package/lib/mcp/browser/tools/form.js +11 -22
- package/lib/mcp/browser/tools/install.js +3 -3
- package/lib/mcp/browser/tools/keyboard.js +12 -12
- package/lib/mcp/browser/tools/mouse.js +14 -14
- package/lib/mcp/browser/tools/navigate.js +5 -5
- package/lib/mcp/browser/tools/network.js +16 -5
- package/lib/mcp/browser/tools/pdf.js +7 -18
- package/lib/mcp/browser/tools/runCode.js +77 -0
- package/lib/mcp/browser/tools/screenshot.js +44 -33
- package/lib/mcp/browser/tools/snapshot.js +42 -33
- package/lib/mcp/browser/tools/tabs.js +7 -10
- package/lib/mcp/browser/tools/tool.js +8 -7
- package/lib/mcp/browser/tools/tracing.js +4 -4
- package/lib/mcp/browser/tools/utils.js +50 -52
- package/lib/mcp/browser/tools/verify.js +23 -34
- package/lib/mcp/browser/tools/wait.js +6 -6
- package/lib/mcp/browser/tools.js +4 -3
- package/lib/mcp/extension/cdpRelay.js +1 -1
- package/lib/mcp/extension/extensionContextFactory.js +4 -3
- package/lib/mcp/log.js +2 -2
- package/lib/mcp/program.js +21 -29
- package/lib/mcp/sdk/exports.js +1 -5
- package/lib/mcp/sdk/http.js +37 -50
- package/lib/mcp/sdk/server.js +61 -9
- package/lib/mcp/sdk/tool.js +5 -4
- package/lib/mcp/test/browserBackend.js +67 -61
- package/lib/mcp/test/generatorTools.js +122 -0
- package/lib/mcp/test/plannerTools.js +144 -0
- package/lib/mcp/test/seed.js +82 -0
- package/lib/mcp/test/streams.js +10 -7
- package/lib/mcp/test/testBackend.js +44 -24
- package/lib/mcp/test/testContext.js +243 -14
- package/lib/mcp/test/testTools.js +23 -109
- package/lib/util.js +12 -6
- package/package.json +1 -1
|
@@ -29,7 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var browserContextFactory_exports = {};
|
|
30
30
|
__export(browserContextFactory_exports, {
|
|
31
31
|
SharedContextFactory: () => SharedContextFactory,
|
|
32
|
-
contextFactory: () => contextFactory
|
|
32
|
+
contextFactory: () => contextFactory,
|
|
33
|
+
identityBrowserContextFactory: () => identityBrowserContextFactory
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(browserContextFactory_exports);
|
|
35
36
|
var import_crypto = __toESM(require("crypto"));
|
|
@@ -53,6 +54,17 @@ function contextFactory(config) {
|
|
|
53
54
|
return new IsolatedContextFactory(config);
|
|
54
55
|
return new PersistentContextFactory(config);
|
|
55
56
|
}
|
|
57
|
+
function identityBrowserContextFactory(browserContext) {
|
|
58
|
+
return {
|
|
59
|
+
createContext: async (clientInfo, abortSignal, toolName) => {
|
|
60
|
+
return {
|
|
61
|
+
browserContext,
|
|
62
|
+
close: async () => {
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
56
68
|
class BaseContextFactory {
|
|
57
69
|
constructor(name, config) {
|
|
58
70
|
this._logName = name;
|
|
@@ -80,16 +92,20 @@ class BaseContextFactory {
|
|
|
80
92
|
const browser = await this._obtainBrowser(clientInfo);
|
|
81
93
|
const browserContext = await this._doCreateContext(browser);
|
|
82
94
|
await addInitScript(browserContext, this.config.browser.initScript);
|
|
83
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
browserContext,
|
|
97
|
+
close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
|
|
98
|
+
};
|
|
84
99
|
}
|
|
85
100
|
async _doCreateContext(browser) {
|
|
86
101
|
throw new Error("Not implemented");
|
|
87
102
|
}
|
|
88
|
-
async _closeBrowserContext(browserContext, browser) {
|
|
103
|
+
async _closeBrowserContext(browserContext, browser, afterClose) {
|
|
89
104
|
(0, import_log.testDebug)(`close browser context (${this._logName})`);
|
|
90
105
|
if (browser.contexts().length === 1)
|
|
91
106
|
this._browserPromise = void 0;
|
|
92
107
|
await browserContext.close().catch(import_log.logUnhandledError);
|
|
108
|
+
await afterClose();
|
|
93
109
|
if (browser.contexts().length === 0) {
|
|
94
110
|
(0, import_log.testDebug)(`close browser (${this._logName})`);
|
|
95
111
|
await browser.close().catch(import_log.logUnhandledError);
|
|
@@ -103,8 +119,8 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
103
119
|
async _doObtainBrowser(clientInfo) {
|
|
104
120
|
await injectCdpPort(this.config.browser);
|
|
105
121
|
const browserType = playwright[this.config.browser.browserName];
|
|
106
|
-
const tracesDir = await (
|
|
107
|
-
if (this.config.saveTrace)
|
|
122
|
+
const tracesDir = await computeTracesDir(this.config, clientInfo);
|
|
123
|
+
if (tracesDir && this.config.saveTrace)
|
|
108
124
|
await startTraceServer(this.config, tracesDir);
|
|
109
125
|
return browserType.launch({
|
|
110
126
|
tracesDir,
|
|
@@ -118,7 +134,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
118
134
|
});
|
|
119
135
|
}
|
|
120
136
|
async _doCreateContext(browser) {
|
|
121
|
-
return browser.newContext(this.config
|
|
137
|
+
return browser.newContext(browserContextOptionsFromConfig(this.config));
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
class CdpContextFactory extends BaseContextFactory {
|
|
@@ -158,8 +174,8 @@ class PersistentContextFactory {
|
|
|
158
174
|
await injectCdpPort(this.config.browser);
|
|
159
175
|
(0, import_log.testDebug)("create browser context (persistent)");
|
|
160
176
|
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
|
|
161
|
-
const tracesDir = await (
|
|
162
|
-
if (this.config.saveTrace)
|
|
177
|
+
const tracesDir = await computeTracesDir(this.config, clientInfo);
|
|
178
|
+
if (tracesDir && this.config.saveTrace)
|
|
163
179
|
await startTraceServer(this.config, tracesDir);
|
|
164
180
|
this._userDataDirs.add(userDataDir);
|
|
165
181
|
(0, import_log.testDebug)("lock user data dir", userDataDir);
|
|
@@ -168,7 +184,7 @@ class PersistentContextFactory {
|
|
|
168
184
|
const launchOptions = {
|
|
169
185
|
tracesDir,
|
|
170
186
|
...this.config.browser.launchOptions,
|
|
171
|
-
...this.config
|
|
187
|
+
...browserContextOptionsFromConfig(this.config),
|
|
172
188
|
handleSIGINT: false,
|
|
173
189
|
handleSIGTERM: false,
|
|
174
190
|
ignoreDefaultArgs: [
|
|
@@ -179,7 +195,7 @@ class PersistentContextFactory {
|
|
|
179
195
|
try {
|
|
180
196
|
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
|
|
181
197
|
await addInitScript(browserContext, this.config.browser.initScript);
|
|
182
|
-
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
|
198
|
+
const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
|
|
183
199
|
return { browserContext, close };
|
|
184
200
|
} catch (error) {
|
|
185
201
|
if (error.message.includes("Executable doesn't exist"))
|
|
@@ -193,12 +209,15 @@ class PersistentContextFactory {
|
|
|
193
209
|
}
|
|
194
210
|
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
|
195
211
|
}
|
|
196
|
-
async _closeBrowserContext(browserContext, userDataDir) {
|
|
212
|
+
async _closeBrowserContext(browserContext, userDataDir, afterClose) {
|
|
197
213
|
(0, import_log.testDebug)("close browser context (persistent)");
|
|
198
214
|
(0, import_log.testDebug)("release user data dir", userDataDir);
|
|
199
215
|
await browserContext.close().catch(() => {
|
|
200
216
|
});
|
|
217
|
+
await afterClose();
|
|
201
218
|
this._userDataDirs.delete(userDataDir);
|
|
219
|
+
if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
|
|
220
|
+
await import_fs.default.promises.rm(userDataDir, { recursive: true }).catch(import_log.logUnhandledError);
|
|
202
221
|
(0, import_log.testDebug)("close browser context complete (persistent)");
|
|
203
222
|
}
|
|
204
223
|
async _createUserDataDir(clientInfo) {
|
|
@@ -275,11 +294,28 @@ class SharedContextFactory {
|
|
|
275
294
|
if (!contextPromise)
|
|
276
295
|
return;
|
|
277
296
|
const { close } = await contextPromise;
|
|
278
|
-
await close()
|
|
297
|
+
await close(async () => {
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function computeTracesDir(config, clientInfo) {
|
|
302
|
+
if (!config.saveTrace && !config.capabilities?.includes("tracing"))
|
|
303
|
+
return;
|
|
304
|
+
return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
|
|
305
|
+
}
|
|
306
|
+
function browserContextOptionsFromConfig(config) {
|
|
307
|
+
const result = { ...config.browser.contextOptions };
|
|
308
|
+
if (config.saveVideo) {
|
|
309
|
+
result.recordVideo = {
|
|
310
|
+
dir: (0, import_config.tmpDir)(),
|
|
311
|
+
size: config.saveVideo
|
|
312
|
+
};
|
|
279
313
|
}
|
|
314
|
+
return result;
|
|
280
315
|
}
|
|
281
316
|
// Annotate the CommonJS export names for ESM import in node:
|
|
282
317
|
0 && (module.exports = {
|
|
283
318
|
SharedContextFactory,
|
|
284
|
-
contextFactory
|
|
319
|
+
contextFactory,
|
|
320
|
+
identityBrowserContextFactory
|
|
285
321
|
});
|
|
@@ -33,7 +33,7 @@ class BrowserServerBackend {
|
|
|
33
33
|
this._browserContextFactory = factory;
|
|
34
34
|
this._tools = (0, import_tools.filteredTools)(config);
|
|
35
35
|
}
|
|
36
|
-
async initialize(
|
|
36
|
+
async initialize(clientInfo) {
|
|
37
37
|
this._sessionLog = this._config.saveSession ? await import_sessionLog.SessionLog.create(this._config, clientInfo) : void 0;
|
|
38
38
|
this._context = new import_context.Context({
|
|
39
39
|
config: this._config,
|
|
@@ -52,6 +52,7 @@ class BrowserServerBackend {
|
|
|
52
52
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
|
53
53
|
const context = this._context;
|
|
54
54
|
const response = new import_response.Response(context, name, parsedArguments);
|
|
55
|
+
response.logBegin();
|
|
55
56
|
context.setRunningTool(name);
|
|
56
57
|
try {
|
|
57
58
|
await tool.handle(context, parsedArguments, response);
|
|
@@ -62,7 +63,9 @@ class BrowserServerBackend {
|
|
|
62
63
|
} finally {
|
|
63
64
|
context.setRunningTool(void 0);
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
response.logEnd();
|
|
67
|
+
const _meta = rawArguments?._meta;
|
|
68
|
+
return response.serialize({ _meta });
|
|
66
69
|
}
|
|
67
70
|
serverClosed() {
|
|
68
71
|
void this._context?.dispose().catch(import_log.logUnhandledError);
|
|
@@ -32,12 +32,16 @@ __export(config_exports, {
|
|
|
32
32
|
configFromCLIOptions: () => configFromCLIOptions,
|
|
33
33
|
defaultConfig: () => defaultConfig,
|
|
34
34
|
dotenvFileLoader: () => dotenvFileLoader,
|
|
35
|
+
enumParser: () => enumParser,
|
|
35
36
|
headerParser: () => headerParser,
|
|
36
37
|
numberParser: () => numberParser,
|
|
38
|
+
outputDir: () => outputDir,
|
|
37
39
|
outputFile: () => outputFile,
|
|
40
|
+
resolutionParser: () => resolutionParser,
|
|
38
41
|
resolveCLIConfig: () => resolveCLIConfig,
|
|
39
42
|
resolveConfig: () => resolveConfig,
|
|
40
|
-
semicolonSeparatedList: () => semicolonSeparatedList
|
|
43
|
+
semicolonSeparatedList: () => semicolonSeparatedList,
|
|
44
|
+
tmpDir: () => tmpDir
|
|
41
45
|
});
|
|
42
46
|
module.exports = __toCommonJS(config_exports);
|
|
43
47
|
var import_fs = __toESM(require("fs"));
|
|
@@ -59,12 +63,18 @@ const defaultConfig = {
|
|
|
59
63
|
viewport: null
|
|
60
64
|
}
|
|
61
65
|
},
|
|
66
|
+
console: {
|
|
67
|
+
level: "info"
|
|
68
|
+
},
|
|
62
69
|
network: {
|
|
63
70
|
allowedOrigins: void 0,
|
|
64
71
|
blockedOrigins: void 0
|
|
65
72
|
},
|
|
66
73
|
server: {},
|
|
67
74
|
saveTrace: false,
|
|
75
|
+
snapshot: {
|
|
76
|
+
mode: "incremental"
|
|
77
|
+
},
|
|
68
78
|
timeouts: {
|
|
69
79
|
action: 5e3,
|
|
70
80
|
navigation: 6e4
|
|
@@ -91,6 +101,14 @@ async function validateConfig(config) {
|
|
|
91
101
|
throw new Error(`Init script file does not exist: ${script}`);
|
|
92
102
|
}
|
|
93
103
|
}
|
|
104
|
+
if (config.browser.initPage) {
|
|
105
|
+
for (const page of config.browser.initPage) {
|
|
106
|
+
if (!await (0, import_util.fileExistsAsync)(page))
|
|
107
|
+
throw new Error(`Init page file does not exist: ${page}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (config.sharedBrowserContext && config.saveVideo)
|
|
111
|
+
throw new Error("saveVideo is not supported when sharedBrowserContext is true");
|
|
94
112
|
}
|
|
95
113
|
function configFromCLIOptions(cliOptions) {
|
|
96
114
|
let browserName;
|
|
@@ -136,16 +154,8 @@ function configFromCLIOptions(cliOptions) {
|
|
|
136
154
|
contextOptions.storageState = cliOptions.storageState;
|
|
137
155
|
if (cliOptions.userAgent)
|
|
138
156
|
contextOptions.userAgent = cliOptions.userAgent;
|
|
139
|
-
if (cliOptions.viewportSize)
|
|
140
|
-
|
|
141
|
-
const [width, height] = cliOptions.viewportSize.split(",").map((n) => +n);
|
|
142
|
-
if (isNaN(width) || isNaN(height))
|
|
143
|
-
throw new Error("bad values");
|
|
144
|
-
contextOptions.viewport = { width, height };
|
|
145
|
-
} catch (e) {
|
|
146
|
-
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
157
|
+
if (cliOptions.viewportSize)
|
|
158
|
+
contextOptions.viewport = cliOptions.viewportSize;
|
|
149
159
|
if (cliOptions.ignoreHttpsErrors)
|
|
150
160
|
contextOptions.ignoreHTTPSErrors = true;
|
|
151
161
|
if (cliOptions.blockServiceWorkers)
|
|
@@ -161,23 +171,31 @@ function configFromCLIOptions(cliOptions) {
|
|
|
161
171
|
contextOptions,
|
|
162
172
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
|
163
173
|
cdpHeaders: cliOptions.cdpHeader,
|
|
174
|
+
initPage: cliOptions.initPage,
|
|
164
175
|
initScript: cliOptions.initScript
|
|
165
176
|
},
|
|
166
177
|
server: {
|
|
167
178
|
port: cliOptions.port,
|
|
168
|
-
host: cliOptions.host
|
|
179
|
+
host: cliOptions.host,
|
|
180
|
+
allowedHosts: cliOptions.allowedHosts
|
|
169
181
|
},
|
|
170
182
|
capabilities: cliOptions.caps,
|
|
183
|
+
console: {
|
|
184
|
+
level: cliOptions.consoleLevel
|
|
185
|
+
},
|
|
171
186
|
network: {
|
|
172
187
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
173
188
|
blockedOrigins: cliOptions.blockedOrigins
|
|
174
189
|
},
|
|
175
190
|
saveSession: cliOptions.saveSession,
|
|
176
191
|
saveTrace: cliOptions.saveTrace,
|
|
192
|
+
saveVideo: cliOptions.saveVideo,
|
|
177
193
|
secrets: cliOptions.secrets,
|
|
178
194
|
sharedBrowserContext: cliOptions.sharedBrowserContext,
|
|
195
|
+
snapshot: cliOptions.snapshotMode ? { mode: cliOptions.snapshotMode } : void 0,
|
|
179
196
|
outputDir: cliOptions.outputDir,
|
|
180
197
|
imageResponses: cliOptions.imageResponses,
|
|
198
|
+
testIdAttribute: cliOptions.testIdAttribute,
|
|
181
199
|
timeouts: {
|
|
182
200
|
action: cliOptions.timeoutAction,
|
|
183
201
|
navigation: cliOptions.timeoutNavigation
|
|
@@ -187,6 +205,7 @@ function configFromCLIOptions(cliOptions) {
|
|
|
187
205
|
}
|
|
188
206
|
function configFromEnv() {
|
|
189
207
|
const options = {};
|
|
208
|
+
options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
|
|
190
209
|
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
|
191
210
|
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
|
192
211
|
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
|
@@ -195,31 +214,38 @@ function configFromEnv() {
|
|
|
195
214
|
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
|
|
196
215
|
options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
|
|
197
216
|
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
|
|
217
|
+
if (process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
|
|
218
|
+
options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
|
|
198
219
|
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
|
|
199
220
|
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
|
|
200
221
|
options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
|
|
201
222
|
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
|
|
202
223
|
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
|
|
203
224
|
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
|
|
225
|
+
const initPage = envToString(process.env.PLAYWRIGHT_MCP_INIT_PAGE);
|
|
226
|
+
if (initPage)
|
|
227
|
+
options.initPage = [initPage];
|
|
204
228
|
const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
|
|
205
229
|
if (initScript)
|
|
206
230
|
options.initScript = [initScript];
|
|
207
231
|
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
|
|
208
|
-
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES
|
|
209
|
-
options.imageResponses = "omit";
|
|
232
|
+
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
|
|
233
|
+
options.imageResponses = enumParser("--image-responses", ["allow", "omit"], process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
|
|
210
234
|
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
|
211
235
|
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
|
212
236
|
options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
|
|
213
237
|
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
|
214
238
|
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
|
|
215
239
|
options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
|
|
240
|
+
options.saveVideo = resolutionParser("--save-video", process.env.PLAYWRIGHT_MCP_SAVE_VIDEO);
|
|
216
241
|
options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE);
|
|
217
242
|
options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
|
|
243
|
+
options.testIdAttribute = envToString(process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
|
|
218
244
|
options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
|
|
219
245
|
options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
|
|
220
246
|
options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
|
|
221
247
|
options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
|
|
222
|
-
options.viewportSize =
|
|
248
|
+
options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
|
|
223
249
|
return configFromCLIOptions(options);
|
|
224
250
|
}
|
|
225
251
|
async function loadConfig(configFile) {
|
|
@@ -231,19 +257,31 @@ async function loadConfig(configFile) {
|
|
|
231
257
|
throw new Error(`Failed to load config file: ${configFile}, ${error}`);
|
|
232
258
|
}
|
|
233
259
|
}
|
|
234
|
-
|
|
260
|
+
function tmpDir() {
|
|
261
|
+
return import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output");
|
|
262
|
+
}
|
|
263
|
+
function outputDir(config, clientInfo) {
|
|
235
264
|
const rootPath = (0, import_server.firstRootPath)(clientInfo);
|
|
236
|
-
|
|
265
|
+
return config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(tmpDir(), String(clientInfo.timestamp));
|
|
266
|
+
}
|
|
267
|
+
async function outputFile(config, clientInfo, fileName, options) {
|
|
268
|
+
const file = await resolveFile(config, clientInfo, fileName, options);
|
|
269
|
+
await import_fs.default.promises.mkdir(import_path.default.dirname(file), { recursive: true });
|
|
270
|
+
(0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
|
|
271
|
+
return file;
|
|
272
|
+
}
|
|
273
|
+
async function resolveFile(config, clientInfo, fileName, options) {
|
|
274
|
+
const dir = outputDir(config, clientInfo);
|
|
237
275
|
if (options.origin === "code")
|
|
238
|
-
return import_path.default.resolve(
|
|
276
|
+
return import_path.default.resolve(dir, fileName);
|
|
239
277
|
if (options.origin === "llm") {
|
|
240
278
|
fileName = fileName.split("\\").join("/");
|
|
241
|
-
const resolvedFile = import_path.default.resolve(
|
|
242
|
-
if (!resolvedFile.startsWith(import_path.default.resolve(
|
|
243
|
-
throw new Error(`Resolved file path
|
|
279
|
+
const resolvedFile = import_path.default.resolve(dir, fileName);
|
|
280
|
+
if (!resolvedFile.startsWith(import_path.default.resolve(dir) + import_path.default.sep))
|
|
281
|
+
throw new Error(`Resolved file path ${resolvedFile} is outside of the output directory ${dir}. Use relative file names to stay within the output directory.`);
|
|
244
282
|
return resolvedFile;
|
|
245
283
|
}
|
|
246
|
-
return import_path.default.join(
|
|
284
|
+
return import_path.default.join(dir, sanitizeForFilePath(fileName));
|
|
247
285
|
}
|
|
248
286
|
function pickDefined(obj) {
|
|
249
287
|
return Object.fromEntries(
|
|
@@ -272,6 +310,10 @@ function mergeConfig(base, overrides) {
|
|
|
272
310
|
...pickDefined(base),
|
|
273
311
|
...pickDefined(overrides),
|
|
274
312
|
browser,
|
|
313
|
+
console: {
|
|
314
|
+
...pickDefined(base.console),
|
|
315
|
+
...pickDefined(overrides.console)
|
|
316
|
+
},
|
|
275
317
|
network: {
|
|
276
318
|
...pickDefined(base.network),
|
|
277
319
|
...pickDefined(overrides.network)
|
|
@@ -280,6 +322,10 @@ function mergeConfig(base, overrides) {
|
|
|
280
322
|
...pickDefined(base.server),
|
|
281
323
|
...pickDefined(overrides.server)
|
|
282
324
|
},
|
|
325
|
+
snapshot: {
|
|
326
|
+
...pickDefined(base.snapshot),
|
|
327
|
+
...pickDefined(overrides.snapshot)
|
|
328
|
+
},
|
|
283
329
|
timeouts: {
|
|
284
330
|
...pickDefined(base.timeouts),
|
|
285
331
|
...pickDefined(overrides.timeouts)
|
|
@@ -306,6 +352,23 @@ function numberParser(value) {
|
|
|
306
352
|
return void 0;
|
|
307
353
|
return +value;
|
|
308
354
|
}
|
|
355
|
+
function resolutionParser(name, value) {
|
|
356
|
+
if (!value)
|
|
357
|
+
return void 0;
|
|
358
|
+
if (value.includes("x")) {
|
|
359
|
+
const [width, height] = value.split("x").map((v) => +v);
|
|
360
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
|
361
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
362
|
+
return { width, height };
|
|
363
|
+
}
|
|
364
|
+
if (value.includes(",")) {
|
|
365
|
+
const [width, height] = value.split(",").map((v) => +v);
|
|
366
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
|
367
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
368
|
+
return { width, height };
|
|
369
|
+
}
|
|
370
|
+
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
|
371
|
+
}
|
|
309
372
|
function headerParser(arg, previous) {
|
|
310
373
|
if (!arg)
|
|
311
374
|
return previous || {};
|
|
@@ -314,6 +377,11 @@ function headerParser(arg, previous) {
|
|
|
314
377
|
result[name] = value;
|
|
315
378
|
return result;
|
|
316
379
|
}
|
|
380
|
+
function enumParser(name, options, value) {
|
|
381
|
+
if (!options.includes(value))
|
|
382
|
+
throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
317
385
|
function envToBoolean(value) {
|
|
318
386
|
if (value === "true" || value === "1")
|
|
319
387
|
return true;
|
|
@@ -337,10 +405,14 @@ function sanitizeForFilePath(s) {
|
|
|
337
405
|
configFromCLIOptions,
|
|
338
406
|
defaultConfig,
|
|
339
407
|
dotenvFileLoader,
|
|
408
|
+
enumParser,
|
|
340
409
|
headerParser,
|
|
341
410
|
numberParser,
|
|
411
|
+
outputDir,
|
|
342
412
|
outputFile,
|
|
413
|
+
resolutionParser,
|
|
343
414
|
resolveCLIConfig,
|
|
344
415
|
resolveConfig,
|
|
345
|
-
semicolonSeparatedList
|
|
416
|
+
semicolonSeparatedList,
|
|
417
|
+
tmpDir
|
|
346
418
|
});
|
|
@@ -32,11 +32,14 @@ __export(context_exports, {
|
|
|
32
32
|
InputRecorder: () => InputRecorder
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(context_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
35
36
|
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
|
|
37
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
38
|
+
var import_playwright_core = require("playwright-core");
|
|
36
39
|
var import_log = require("../log");
|
|
37
40
|
var import_tab = require("./tab");
|
|
38
41
|
var import_config = require("./config");
|
|
39
|
-
var
|
|
42
|
+
var import_utils2 = require("./tools/utils");
|
|
40
43
|
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
|
|
41
44
|
class Context {
|
|
42
45
|
constructor(options) {
|
|
@@ -135,7 +138,27 @@ class Context {
|
|
|
135
138
|
await promise.then(async ({ browserContext, close }) => {
|
|
136
139
|
if (this.config.saveTrace)
|
|
137
140
|
await browserContext.tracing.stop();
|
|
138
|
-
|
|
141
|
+
const videos = this.config.saveVideo ? browserContext.pages().map((page) => page.video()).filter((video) => !!video) : [];
|
|
142
|
+
await close(async () => {
|
|
143
|
+
for (const video of videos) {
|
|
144
|
+
const name = await this.outputFile((0, import_utils2.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
|
|
145
|
+
const p = await video.path();
|
|
146
|
+
if (import_fs.default.existsSync(p)) {
|
|
147
|
+
try {
|
|
148
|
+
await import_fs.default.promises.rename(p, name);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e.code !== "EXDEV")
|
|
151
|
+
(0, import_log.logUnhandledError)(e);
|
|
152
|
+
try {
|
|
153
|
+
await import_fs.default.promises.copyFile(p, name);
|
|
154
|
+
await import_fs.default.promises.unlink(p);
|
|
155
|
+
} catch (e2) {
|
|
156
|
+
(0, import_log.logUnhandledError)(e2);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
139
162
|
});
|
|
140
163
|
}
|
|
141
164
|
async dispose() {
|
|
@@ -170,6 +193,8 @@ class Context {
|
|
|
170
193
|
async _setupBrowserContext() {
|
|
171
194
|
if (this._closeBrowserContextPromise)
|
|
172
195
|
throw new Error("Another browser context is being closed.");
|
|
196
|
+
if (this.config.testIdAttribute)
|
|
197
|
+
import_playwright_core.selectors.setTestIdAttribute(this.config.testIdAttribute);
|
|
173
198
|
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
|
|
174
199
|
const { browserContext } = result;
|
|
175
200
|
await this._setupRequestInterception(browserContext);
|
|
@@ -190,7 +215,7 @@ class Context {
|
|
|
190
215
|
}
|
|
191
216
|
lookupSecret(secretName) {
|
|
192
217
|
if (!this.config.secrets?.[secretName])
|
|
193
|
-
return { value: secretName, code:
|
|
218
|
+
return { value: secretName, code: (0, import_utils.escapeWithQuotes)(secretName, "'") };
|
|
194
219
|
return {
|
|
195
220
|
value: this.config.secrets[secretName],
|
|
196
221
|
code: `process.env['${secretName}']`
|