@wordbricks/playwright-mcp 0.1.19 → 0.1.22
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 +54 -44
- package/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +178 -0
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +29 -16
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +24 -15
- package/lib/hooks/networkSetup.js +11 -6
- package/lib/hooks/networkTrackingHook.js +31 -19
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +18 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +44 -32
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +68 -61
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +65 -35
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { fileURLToPath } from
|
|
17
|
-
import { Context } from
|
|
18
|
-
import {
|
|
19
|
-
import { Response } from
|
|
20
|
-
import { SessionLog } from
|
|
21
|
-
import { filteredTools } from
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { Context } from "./context.js";
|
|
18
|
+
import { toMcpTool } from "./mcp/tool.js";
|
|
19
|
+
import { Response } from "./response.js";
|
|
20
|
+
import { SessionLog } from "./sessionLog.js";
|
|
21
|
+
import { filteredTools } from "./tools.js";
|
|
22
|
+
import { logUnhandledError } from "./utils/log.js";
|
|
23
|
+
import { packageJSON } from "./utils/package.js";
|
|
24
24
|
export class BrowserServerBackend {
|
|
25
|
-
name =
|
|
25
|
+
name = "Playwright";
|
|
26
26
|
version = packageJSON.version;
|
|
27
27
|
_tools;
|
|
28
28
|
_context;
|
|
@@ -43,7 +43,9 @@ export class BrowserServerBackend {
|
|
|
43
43
|
const url = firstRootUri ? new URL(firstRootUri) : undefined;
|
|
44
44
|
rootPath = url ? fileURLToPath(url) : undefined;
|
|
45
45
|
}
|
|
46
|
-
this._sessionLog = this._config.saveSession
|
|
46
|
+
this._sessionLog = this._config.saveSession
|
|
47
|
+
? await SessionLog.create(this._config, rootPath)
|
|
48
|
+
: undefined;
|
|
47
49
|
this._context = new Context({
|
|
48
50
|
tools: this._tools,
|
|
49
51
|
config: this._config,
|
|
@@ -55,11 +57,11 @@ export class BrowserServerBackend {
|
|
|
55
57
|
await this._context.ensureTab();
|
|
56
58
|
}
|
|
57
59
|
async listTools() {
|
|
58
|
-
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
60
|
+
return this._tools.map((tool) => toMcpTool(tool.schema));
|
|
59
61
|
}
|
|
60
62
|
async callTool(name, rawArguments) {
|
|
61
63
|
const context = this._context;
|
|
62
|
-
const tool = context.tools.find(tool => tool.schema.name === name);
|
|
64
|
+
const tool = context.tools.find((tool) => tool.schema.name === name);
|
|
63
65
|
if (!tool)
|
|
64
66
|
throw new Error(`Tool "${name}" not found`);
|
|
65
67
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
|
package/lib/config.js
CHANGED
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import fs from
|
|
17
|
-
import os from
|
|
18
|
-
import path from
|
|
19
|
-
import { devices } from
|
|
20
|
-
import { sanitizeForFilePath } from
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import os from "os";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { devices } from "playwright-core";
|
|
20
|
+
import { sanitizeForFilePath } from "./utils/fileUtils.js";
|
|
21
21
|
const defaultConfig = {
|
|
22
22
|
browser: {
|
|
23
|
-
browserName:
|
|
23
|
+
browserName: "chromium",
|
|
24
24
|
launchOptions: {
|
|
25
|
-
channel:
|
|
26
|
-
headless: os.platform() ===
|
|
25
|
+
channel: "chrome",
|
|
26
|
+
headless: os.platform() === "linux" && !process.env.DISPLAY,
|
|
27
27
|
chromiumSandbox: true,
|
|
28
28
|
},
|
|
29
29
|
contextOptions: {
|
|
@@ -54,23 +54,23 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
54
54
|
let browserName;
|
|
55
55
|
let channel;
|
|
56
56
|
switch (cliOptions.browser) {
|
|
57
|
-
case
|
|
58
|
-
case
|
|
59
|
-
case
|
|
60
|
-
case
|
|
61
|
-
case
|
|
62
|
-
case
|
|
63
|
-
case
|
|
64
|
-
case
|
|
65
|
-
case
|
|
66
|
-
browserName =
|
|
57
|
+
case "chrome":
|
|
58
|
+
case "chrome-beta":
|
|
59
|
+
case "chrome-canary":
|
|
60
|
+
case "chrome-dev":
|
|
61
|
+
case "chromium":
|
|
62
|
+
case "msedge":
|
|
63
|
+
case "msedge-beta":
|
|
64
|
+
case "msedge-canary":
|
|
65
|
+
case "msedge-dev":
|
|
66
|
+
browserName = "chromium";
|
|
67
67
|
channel = cliOptions.browser;
|
|
68
68
|
break;
|
|
69
|
-
case
|
|
70
|
-
browserName =
|
|
69
|
+
case "firefox":
|
|
70
|
+
browserName = "firefox";
|
|
71
71
|
break;
|
|
72
|
-
case
|
|
73
|
-
browserName =
|
|
72
|
+
case "webkit":
|
|
73
|
+
browserName = "webkit";
|
|
74
74
|
break;
|
|
75
75
|
}
|
|
76
76
|
// Launch options
|
|
@@ -78,13 +78,25 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
78
78
|
channel,
|
|
79
79
|
executablePath: cliOptions.executablePath,
|
|
80
80
|
headless: cliOptions.headless,
|
|
81
|
+
// Ignore default automation flag to reduce bot detection
|
|
82
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
81
83
|
};
|
|
82
84
|
// --no-sandbox was passed, disable the sandbox
|
|
83
85
|
if (cliOptions.sandbox === false)
|
|
84
86
|
launchOptions.chromiumSandbox = false;
|
|
85
|
-
// Add default Chrome args to avoid automation detection
|
|
87
|
+
// Add default Chrome args to avoid automation detection and suppress popups
|
|
86
88
|
launchOptions.args = launchOptions.args || [];
|
|
87
|
-
launchOptions.args.push(
|
|
89
|
+
launchOptions.args.push("--disable-infobars",
|
|
90
|
+
// Password & credential popups
|
|
91
|
+
"--disable-save-password-bubble", "--password-store=basic", "--use-mock-keychain",
|
|
92
|
+
// Crash restore popup
|
|
93
|
+
"--hide-crash-restore-bubble",
|
|
94
|
+
// First run & default browser
|
|
95
|
+
"--no-first-run", "--no-default-browser-check",
|
|
96
|
+
// Disable various Chrome features (Translate, PasswordManager, Autofill, Sync)
|
|
97
|
+
"--disable-features=Translate,PasswordManager,PasswordManagerEnabled,PasswordManagerOnboarding,AutofillServerCommunication,CredentialManagerOnboarding", "--disable-sync",
|
|
98
|
+
// Disable password manager via experimental options
|
|
99
|
+
"--enable-features=DisablePasswordManager");
|
|
88
100
|
// --app was passed, add app mode argument
|
|
89
101
|
if (cliOptions.app) {
|
|
90
102
|
launchOptions.args = launchOptions.args || [];
|
|
@@ -93,9 +105,9 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
93
105
|
// --window-position was passed, add window position argument
|
|
94
106
|
if (cliOptions.windowPosition) {
|
|
95
107
|
try {
|
|
96
|
-
const [x, y] = cliOptions.windowPosition.split(
|
|
108
|
+
const [x, y] = cliOptions.windowPosition.split(",").map((n) => +n);
|
|
97
109
|
if (isNaN(x) || isNaN(y))
|
|
98
|
-
throw new Error(
|
|
110
|
+
throw new Error("bad values");
|
|
99
111
|
launchOptions.args = launchOptions.args || [];
|
|
100
112
|
launchOptions.args.push(`--window-position=${x},${y}`);
|
|
101
113
|
}
|
|
@@ -106,9 +118,9 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
106
118
|
// --window-size was passed, add window size argument
|
|
107
119
|
if (cliOptions.windowSize) {
|
|
108
120
|
try {
|
|
109
|
-
const [width, height] = cliOptions.windowSize.split(
|
|
121
|
+
const [width, height] = cliOptions.windowSize.split(",").map((n) => +n);
|
|
110
122
|
if (isNaN(width) || isNaN(height))
|
|
111
|
-
throw new Error(
|
|
123
|
+
throw new Error("bad values");
|
|
112
124
|
launchOptions.args = launchOptions.args || [];
|
|
113
125
|
launchOptions.args.push(`--window-size=${width},${height}`);
|
|
114
126
|
}
|
|
@@ -118,24 +130,26 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
118
130
|
}
|
|
119
131
|
if (cliOptions.proxyServer) {
|
|
120
132
|
launchOptions.proxy = {
|
|
121
|
-
server: cliOptions.proxyServer
|
|
133
|
+
server: cliOptions.proxyServer,
|
|
122
134
|
};
|
|
123
135
|
if (cliOptions.proxyBypass)
|
|
124
136
|
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
125
137
|
}
|
|
126
138
|
if (cliOptions.device && cliOptions.cdpEndpoint)
|
|
127
|
-
throw new Error(
|
|
139
|
+
throw new Error("Device emulation is not supported with cdpEndpoint.");
|
|
128
140
|
// Context options
|
|
129
|
-
const contextOptions = cliOptions.device
|
|
141
|
+
const contextOptions = cliOptions.device
|
|
142
|
+
? devices[cliOptions.device]
|
|
143
|
+
: {};
|
|
130
144
|
if (cliOptions.storageState)
|
|
131
145
|
contextOptions.storageState = cliOptions.storageState;
|
|
132
146
|
if (cliOptions.userAgent)
|
|
133
147
|
contextOptions.userAgent = cliOptions.userAgent;
|
|
134
148
|
if (cliOptions.viewportSize) {
|
|
135
149
|
try {
|
|
136
|
-
const [width, height] = cliOptions.viewportSize.split(
|
|
150
|
+
const [width, height] = cliOptions.viewportSize.split(",").map((n) => +n);
|
|
137
151
|
if (isNaN(width) || isNaN(height))
|
|
138
|
-
throw new Error(
|
|
152
|
+
throw new Error("bad values");
|
|
139
153
|
contextOptions.viewport = { width, height };
|
|
140
154
|
}
|
|
141
155
|
catch (e) {
|
|
@@ -145,7 +159,7 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
145
159
|
if (cliOptions.ignoreHttpsErrors)
|
|
146
160
|
contextOptions.ignoreHTTPSErrors = true;
|
|
147
161
|
if (cliOptions.blockServiceWorkers)
|
|
148
|
-
contextOptions.serviceWorkers =
|
|
162
|
+
contextOptions.serviceWorkers = "block";
|
|
149
163
|
const result = {
|
|
150
164
|
browser: {
|
|
151
165
|
browserName,
|
|
@@ -190,8 +204,8 @@ function configFromEnv() {
|
|
|
190
204
|
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
|
|
191
205
|
options.initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
|
|
192
206
|
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
|
|
193
|
-
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES ===
|
|
194
|
-
options.imageResponses =
|
|
207
|
+
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === "omit")
|
|
208
|
+
options.imageResponses = "omit";
|
|
195
209
|
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
|
|
196
210
|
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
|
197
211
|
options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);
|
|
@@ -210,16 +224,16 @@ async function loadConfig(configFile) {
|
|
|
210
224
|
if (!configFile)
|
|
211
225
|
return {};
|
|
212
226
|
try {
|
|
213
|
-
return JSON.parse(await fs.promises.readFile(configFile,
|
|
227
|
+
return JSON.parse(await fs.promises.readFile(configFile, "utf8"));
|
|
214
228
|
}
|
|
215
229
|
catch (error) {
|
|
216
230
|
throw new Error(`Failed to load config file: ${configFile}, ${error}`);
|
|
217
231
|
}
|
|
218
232
|
}
|
|
219
233
|
export async function outputFile(config, rootPath, name) {
|
|
220
|
-
const outputDir = config.outputDir
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
const outputDir = config.outputDir ??
|
|
235
|
+
(rootPath ? path.join(rootPath, ".playwright-mcp") : undefined) ??
|
|
236
|
+
path.join(os.tmpdir(), "playwright-mcp-output", sanitizeForFilePath(new Date().toISOString()));
|
|
223
237
|
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
224
238
|
const fileName = sanitizeForFilePath(name);
|
|
225
239
|
return path.join(outputDir, fileName);
|
|
@@ -231,7 +245,7 @@ function mergeConfig(base, overrides) {
|
|
|
231
245
|
const browser = {
|
|
232
246
|
...pickDefined(base.browser),
|
|
233
247
|
...pickDefined(overrides.browser),
|
|
234
|
-
browserName: overrides.browser?.browserName ?? base.browser?.browserName ??
|
|
248
|
+
browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? "chromium",
|
|
235
249
|
isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false,
|
|
236
250
|
launchOptions: {
|
|
237
251
|
...pickDefined(base.browser?.launchOptions),
|
|
@@ -243,7 +257,7 @@ function mergeConfig(base, overrides) {
|
|
|
243
257
|
...pickDefined(overrides.browser?.contextOptions),
|
|
244
258
|
},
|
|
245
259
|
};
|
|
246
|
-
if (browser.browserName !==
|
|
260
|
+
if (browser.browserName !== "chromium" && browser.launchOptions)
|
|
247
261
|
delete browser.launchOptions.channel;
|
|
248
262
|
return {
|
|
249
263
|
...pickDefined(base),
|
|
@@ -262,12 +276,12 @@ function mergeConfig(base, overrides) {
|
|
|
262
276
|
export function semicolonSeparatedList(value) {
|
|
263
277
|
if (!value)
|
|
264
278
|
return undefined;
|
|
265
|
-
return value.split(
|
|
279
|
+
return value.split(";").map((v) => v.trim());
|
|
266
280
|
}
|
|
267
281
|
export function commaSeparatedList(value) {
|
|
268
282
|
if (!value)
|
|
269
283
|
return undefined;
|
|
270
|
-
return value.split(
|
|
284
|
+
return value.split(",").map((v) => v.trim());
|
|
271
285
|
}
|
|
272
286
|
function envToNumber(value) {
|
|
273
287
|
if (!value)
|
|
@@ -275,9 +289,9 @@ function envToNumber(value) {
|
|
|
275
289
|
return +value;
|
|
276
290
|
}
|
|
277
291
|
function envToBoolean(value) {
|
|
278
|
-
if (value ===
|
|
292
|
+
if (value === "true" || value === "1")
|
|
279
293
|
return true;
|
|
280
|
-
if (value ===
|
|
294
|
+
if (value === "false" || value === "0")
|
|
281
295
|
return false;
|
|
282
296
|
return undefined;
|
|
283
297
|
}
|
package/lib/context.js
CHANGED
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import debug from
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import * as codegen from
|
|
25
|
-
import {
|
|
26
|
-
const testDebug = debug(
|
|
16
|
+
import debug from "debug";
|
|
17
|
+
import { outputFile } from "./config.js";
|
|
18
|
+
import { applyHooksToTools, hookRegistryMap } from "./hooks/core.js";
|
|
19
|
+
import { createEventStore, setEventStore } from "./hooks/events.js";
|
|
20
|
+
import { setupNetworkTracking } from "./hooks/networkSetup.js";
|
|
21
|
+
import { buildHookRegistry } from "./hooks/registry.js";
|
|
22
|
+
import { Tab } from "./tab.js";
|
|
23
|
+
import { shouldBlockRequest } from "./utils/adBlockFilter.js";
|
|
24
|
+
import * as codegen from "./utils/codegen.js";
|
|
25
|
+
import { logUnhandledError } from "./utils/log.js";
|
|
26
|
+
const testDebug = debug("pw:mcp:test");
|
|
27
27
|
const protocolPattern = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
28
28
|
const defaultPortForProtocol = (protocol) => {
|
|
29
|
-
if (protocol ===
|
|
30
|
-
return
|
|
31
|
-
if (protocol ===
|
|
32
|
-
return
|
|
33
|
-
return
|
|
29
|
+
if (protocol === "http:")
|
|
30
|
+
return "80";
|
|
31
|
+
if (protocol === "https:")
|
|
32
|
+
return "443";
|
|
33
|
+
return "";
|
|
34
34
|
};
|
|
35
35
|
const matchesOriginHost = (requestUrl, candidate) => {
|
|
36
36
|
const normalized = candidate.trim();
|
|
@@ -47,15 +47,16 @@ const matchesOriginHost = (requestUrl, candidate) => {
|
|
|
47
47
|
})();
|
|
48
48
|
if (!parsed)
|
|
49
49
|
return false;
|
|
50
|
-
const candidateHost = parsed.hostname.toLowerCase().replace(/\.+$/,
|
|
50
|
+
const candidateHost = parsed.hostname.toLowerCase().replace(/\.+$/, "");
|
|
51
51
|
if (!candidateHost)
|
|
52
52
|
return false;
|
|
53
53
|
const requestHost = requestUrl.hostname.toLowerCase();
|
|
54
|
-
if (requestHost !== candidateHost &&
|
|
54
|
+
if (requestHost !== candidateHost &&
|
|
55
|
+
!requestHost.endsWith(`.${candidateHost}`))
|
|
55
56
|
return false;
|
|
56
57
|
if (hasProtocol && parsed.protocol !== requestUrl.protocol)
|
|
57
58
|
return false;
|
|
58
|
-
if (parsed.port ===
|
|
59
|
+
if (parsed.port === "")
|
|
59
60
|
return true;
|
|
60
61
|
const candidatePort = parsed.port || defaultPortForProtocol(parsed.protocol);
|
|
61
62
|
if (!candidatePort)
|
|
@@ -88,11 +89,11 @@ export class Context {
|
|
|
88
89
|
this.options = options;
|
|
89
90
|
this._browserContextFactory = options.browserContextFactory;
|
|
90
91
|
this._clientInfo = options.clientInfo;
|
|
91
|
-
testDebug(
|
|
92
|
+
testDebug("create context");
|
|
92
93
|
Context._allContexts.add(this);
|
|
93
94
|
}
|
|
94
95
|
static async disposeAll() {
|
|
95
|
-
await Promise.all([...Context._allContexts].map(context => context.dispose()));
|
|
96
|
+
await Promise.all([...Context._allContexts].map((context) => context.dispose()));
|
|
96
97
|
}
|
|
97
98
|
tabs() {
|
|
98
99
|
return this._tabs;
|
|
@@ -108,7 +109,7 @@ export class Context {
|
|
|
108
109
|
async newTab() {
|
|
109
110
|
const { browserContext } = await this._ensureBrowserContext();
|
|
110
111
|
const page = await browserContext.newPage();
|
|
111
|
-
this._currentTab = this._tabs.find(t => t.page === page);
|
|
112
|
+
this._currentTab = this._tabs.find((t) => t.page === page);
|
|
112
113
|
return this._currentTab;
|
|
113
114
|
}
|
|
114
115
|
async selectTab(index) {
|
|
@@ -137,7 +138,7 @@ export class Context {
|
|
|
137
138
|
return outputFile(this.config, this._clientInfo.rootPath, name);
|
|
138
139
|
}
|
|
139
140
|
_onPageCreated(page) {
|
|
140
|
-
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
141
|
+
const tab = new Tab(this, page, (tab) => this._onPageClosed(tab));
|
|
141
142
|
this._tabs.push(tab);
|
|
142
143
|
if (!this._currentTab)
|
|
143
144
|
this._currentTab = tab;
|
|
@@ -156,7 +157,8 @@ export class Context {
|
|
|
156
157
|
}
|
|
157
158
|
async closeBrowserContext() {
|
|
158
159
|
if (!this._closeBrowserContextPromise)
|
|
159
|
-
this._closeBrowserContextPromise =
|
|
160
|
+
this._closeBrowserContextPromise =
|
|
161
|
+
this._closeBrowserContextImpl().catch(logUnhandledError);
|
|
160
162
|
await this._closeBrowserContextPromise;
|
|
161
163
|
this._closeBrowserContextPromise = undefined;
|
|
162
164
|
}
|
|
@@ -169,7 +171,7 @@ export class Context {
|
|
|
169
171
|
async _closeBrowserContextImpl() {
|
|
170
172
|
if (!this._browserContextPromise)
|
|
171
173
|
return;
|
|
172
|
-
testDebug(
|
|
174
|
+
testDebug("close context");
|
|
173
175
|
const promise = this._browserContextPromise;
|
|
174
176
|
this._browserContextPromise = undefined;
|
|
175
177
|
await promise.then(async ({ browserContext, close }) => {
|
|
@@ -179,12 +181,12 @@ export class Context {
|
|
|
179
181
|
});
|
|
180
182
|
}
|
|
181
183
|
async dispose() {
|
|
182
|
-
this._abortController.abort(
|
|
184
|
+
this._abortController.abort("MCP context disposed");
|
|
183
185
|
await this.closeBrowserContext();
|
|
184
186
|
Context._allContexts.delete(this);
|
|
185
187
|
}
|
|
186
188
|
async _setupRequestInterception(context) {
|
|
187
|
-
await context.route(
|
|
189
|
+
await context.route("**", (route) => {
|
|
188
190
|
const request = route.request();
|
|
189
191
|
const url = request.url();
|
|
190
192
|
const resourceType = request.resourceType();
|
|
@@ -198,20 +200,20 @@ export class Context {
|
|
|
198
200
|
}
|
|
199
201
|
const domain = urlObj.hostname;
|
|
200
202
|
if (shouldBlockRequest(url, domain, resourceType)) {
|
|
201
|
-
void route.abort(
|
|
203
|
+
void route.abort("blockedbyclient");
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
206
|
if (this.config.network?.allowedOrigins?.length) {
|
|
205
|
-
const isAllowed = this.config.network.allowedOrigins.some(allowed => matchesOriginHost(urlObj, allowed));
|
|
207
|
+
const isAllowed = this.config.network.allowedOrigins.some((allowed) => matchesOriginHost(urlObj, allowed));
|
|
206
208
|
if (!isAllowed) {
|
|
207
|
-
void route.abort(
|
|
209
|
+
void route.abort("blockedbyclient");
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
if (this.config.network?.blockedOrigins?.length) {
|
|
212
|
-
const isBlocked = this.config.network.blockedOrigins.some(blocked => matchesOriginHost(urlObj, blocked));
|
|
214
|
+
const isBlocked = this.config.network.blockedOrigins.some((blocked) => matchesOriginHost(urlObj, blocked));
|
|
213
215
|
if (isBlocked) {
|
|
214
|
-
void route.abort(
|
|
216
|
+
void route.abort("blockedbyclient");
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
217
219
|
}
|
|
@@ -229,7 +231,7 @@ export class Context {
|
|
|
229
231
|
}
|
|
230
232
|
async _setupBrowserContext() {
|
|
231
233
|
if (this._closeBrowserContextPromise)
|
|
232
|
-
throw new Error(
|
|
234
|
+
throw new Error("Another browser context is being closed.");
|
|
233
235
|
// TODO: move to the browser context factory to make it based on isolation mode.
|
|
234
236
|
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal);
|
|
235
237
|
const { browserContext } = result;
|
|
@@ -238,10 +240,10 @@ export class Context {
|
|
|
238
240
|
await InputRecorder.create(this, browserContext);
|
|
239
241
|
for (const page of browserContext.pages())
|
|
240
242
|
this._onPageCreated(page);
|
|
241
|
-
browserContext.on(
|
|
243
|
+
browserContext.on("page", (page) => this._onPageCreated(page));
|
|
242
244
|
if (this.config.saveTrace) {
|
|
243
245
|
await browserContext.tracing.start({
|
|
244
|
-
name:
|
|
246
|
+
name: "trace",
|
|
245
247
|
screenshots: false,
|
|
246
248
|
snapshots: true,
|
|
247
249
|
sources: false,
|
|
@@ -273,8 +275,8 @@ export class InputRecorder {
|
|
|
273
275
|
async _initialize() {
|
|
274
276
|
const sessionLog = this._context.sessionLog;
|
|
275
277
|
await this._browserContext._enableRecorder({
|
|
276
|
-
mode:
|
|
277
|
-
recorderMode:
|
|
278
|
+
mode: "recording",
|
|
279
|
+
recorderMode: "api",
|
|
278
280
|
}, {
|
|
279
281
|
actionAdded: (page, data, code) => {
|
|
280
282
|
if (this._context.isRunningTool())
|
|
@@ -293,11 +295,11 @@ export class InputRecorder {
|
|
|
293
295
|
signalAdded: (page, data) => {
|
|
294
296
|
if (this._context.isRunningTool())
|
|
295
297
|
return;
|
|
296
|
-
if (data.signal.name !==
|
|
298
|
+
if (data.signal.name !== "navigation")
|
|
297
299
|
return;
|
|
298
300
|
const tab = Tab.forPage(page);
|
|
299
301
|
const navigateAction = {
|
|
300
|
-
name:
|
|
302
|
+
name: "navigate",
|
|
301
303
|
url: data.signal.url,
|
|
302
304
|
signals: [],
|
|
303
305
|
};
|