@tontoko/fast-playwright-mcp 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/LICENSE +202 -0
- package/README.md +1047 -0
- package/cli.js +18 -0
- package/config.d.ts +124 -0
- package/index.d.ts +25 -0
- package/index.js +18 -0
- package/lib/actions.d.js +0 -0
- package/lib/batch/batch-executor.js +137 -0
- package/lib/browser-context-factory.js +252 -0
- package/lib/browser-server-backend.js +139 -0
- package/lib/config/constants.js +80 -0
- package/lib/config.js +405 -0
- package/lib/context.js +274 -0
- package/lib/diagnostics/common/diagnostic-base.js +63 -0
- package/lib/diagnostics/common/error-enrichment-utils.js +212 -0
- package/lib/diagnostics/common/index.js +56 -0
- package/lib/diagnostics/common/initialization-manager.js +210 -0
- package/lib/diagnostics/common/performance-tracker.js +132 -0
- package/lib/diagnostics/diagnostic-error.js +140 -0
- package/lib/diagnostics/diagnostic-level.js +123 -0
- package/lib/diagnostics/diagnostic-thresholds.js +347 -0
- package/lib/diagnostics/element-discovery.js +441 -0
- package/lib/diagnostics/enhanced-error-handler.js +376 -0
- package/lib/diagnostics/error-enrichment.js +157 -0
- package/lib/diagnostics/frame-reference-manager.js +179 -0
- package/lib/diagnostics/page-analyzer.js +639 -0
- package/lib/diagnostics/parallel-page-analyzer.js +129 -0
- package/lib/diagnostics/resource-manager.js +134 -0
- package/lib/diagnostics/smart-config.js +482 -0
- package/lib/diagnostics/smart-handle.js +118 -0
- package/lib/diagnostics/unified-system.js +717 -0
- package/lib/extension/cdp-relay.js +486 -0
- package/lib/extension/extension-context-factory.js +74 -0
- package/lib/extension/main.js +41 -0
- package/lib/file-utils.js +42 -0
- package/lib/generate-keys.js +75 -0
- package/lib/http-server.js +50 -0
- package/lib/in-process-client.js +64 -0
- package/lib/index.js +48 -0
- package/lib/javascript.js +90 -0
- package/lib/log.js +33 -0
- package/lib/loop/loop-claude.js +247 -0
- package/lib/loop/loop-open-ai.js +222 -0
- package/lib/loop/loop.js +174 -0
- package/lib/loop/main.js +46 -0
- package/lib/loopTools/context.js +76 -0
- package/lib/loopTools/main.js +65 -0
- package/lib/loopTools/perform.js +40 -0
- package/lib/loopTools/snapshot.js +37 -0
- package/lib/loopTools/tool.js +26 -0
- package/lib/manual-promise.js +125 -0
- package/lib/mcp/in-process-transport.js +91 -0
- package/lib/mcp/proxy-backend.js +127 -0
- package/lib/mcp/server.js +123 -0
- package/lib/mcp/transport.js +159 -0
- package/lib/package.js +28 -0
- package/lib/program.js +82 -0
- package/lib/response.js +493 -0
- package/lib/schemas/expectation.js +152 -0
- package/lib/session-log.js +210 -0
- package/lib/tab.js +417 -0
- package/lib/tools/base-tool-handler.js +141 -0
- package/lib/tools/batch-execute.js +150 -0
- package/lib/tools/common.js +65 -0
- package/lib/tools/console.js +60 -0
- package/lib/tools/diagnose/diagnose-analysis-runner.js +101 -0
- package/lib/tools/diagnose/diagnose-config-handler.js +130 -0
- package/lib/tools/diagnose/diagnose-report-builder.js +394 -0
- package/lib/tools/diagnose.js +147 -0
- package/lib/tools/dialogs.js +57 -0
- package/lib/tools/evaluate.js +67 -0
- package/lib/tools/files.js +53 -0
- package/lib/tools/find-elements.js +307 -0
- package/lib/tools/install.js +60 -0
- package/lib/tools/keyboard.js +93 -0
- package/lib/tools/mouse.js +110 -0
- package/lib/tools/navigate.js +82 -0
- package/lib/tools/network.js +50 -0
- package/lib/tools/pdf.js +46 -0
- package/lib/tools/screenshot.js +113 -0
- package/lib/tools/snapshot.js +158 -0
- package/lib/tools/tabs.js +97 -0
- package/lib/tools/tool.js +47 -0
- package/lib/tools/utils.js +131 -0
- package/lib/tools/wait.js +64 -0
- package/lib/tools.js +65 -0
- package/lib/types/batch.js +47 -0
- package/lib/types/diff.js +0 -0
- package/lib/types/performance.js +0 -0
- package/lib/types/threshold-base.js +0 -0
- package/lib/utils/array-utils.js +44 -0
- package/lib/utils/code-deduplication-utils.js +141 -0
- package/lib/utils/common-formatters.js +252 -0
- package/lib/utils/console-filter.js +64 -0
- package/lib/utils/diagnostic-report-utils.js +178 -0
- package/lib/utils/diff-formatter.js +126 -0
- package/lib/utils/disposable-manager.js +135 -0
- package/lib/utils/error-handler-middleware.js +77 -0
- package/lib/utils/image-processor.js +137 -0
- package/lib/utils/index.js +92 -0
- package/lib/utils/report-builder.js +189 -0
- package/lib/utils/request-logger.js +82 -0
- package/lib/utils/response-diff-detector.js +150 -0
- package/lib/utils/section-builder.js +62 -0
- package/lib/utils/tool-patterns.js +153 -0
- package/lib/utils.js +46 -0
- package/package.json +77 -0
package/cli.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import './lib/program.js';
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
|
|
17
|
+
import type * as playwright from 'playwright';
|
|
18
|
+
|
|
19
|
+
export type ToolCapability =
|
|
20
|
+
| 'core'
|
|
21
|
+
| 'core-tabs'
|
|
22
|
+
| 'core-install'
|
|
23
|
+
| 'vision'
|
|
24
|
+
| 'pdf';
|
|
25
|
+
|
|
26
|
+
export type Config = {
|
|
27
|
+
/**
|
|
28
|
+
* The browser to use.
|
|
29
|
+
*/
|
|
30
|
+
browser?: {
|
|
31
|
+
/**
|
|
32
|
+
* The type of browser to use.
|
|
33
|
+
*/
|
|
34
|
+
browserName?: 'chromium' | 'firefox' | 'webkit';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Keep the browser profile in memory, do not save it to disk.
|
|
38
|
+
*/
|
|
39
|
+
isolated?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Path to a user data directory for browser profile persistence.
|
|
43
|
+
* Temporary directory is created by default.
|
|
44
|
+
*/
|
|
45
|
+
userDataDir?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Launch options passed to
|
|
49
|
+
* @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
|
|
50
|
+
*
|
|
51
|
+
* This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
|
|
52
|
+
*/
|
|
53
|
+
launchOptions?: playwright.LaunchOptions;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Context options for the browser context.
|
|
57
|
+
*
|
|
58
|
+
* This is useful for settings options like `viewport`.
|
|
59
|
+
*/
|
|
60
|
+
contextOptions?: playwright.BrowserContextOptions;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
|
|
64
|
+
*/
|
|
65
|
+
cdpEndpoint?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remote endpoint to connect to an existing Playwright server.
|
|
69
|
+
*/
|
|
70
|
+
remoteEndpoint?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
server?: {
|
|
74
|
+
/**
|
|
75
|
+
* The port to listen on for SSE or MCP transport.
|
|
76
|
+
*/
|
|
77
|
+
port?: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
|
|
81
|
+
*/
|
|
82
|
+
host?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List of enabled tool capabilities. Possible values:
|
|
87
|
+
* - 'core': Core browser automation features.
|
|
88
|
+
* - 'pdf': PDF generation and manipulation.
|
|
89
|
+
* - 'vision': Coordinate-based interactions.
|
|
90
|
+
*/
|
|
91
|
+
capabilities?: ToolCapability[];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether to save the Playwright session into the output directory.
|
|
95
|
+
*/
|
|
96
|
+
saveSession?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether to save the Playwright trace of the session into the output directory.
|
|
100
|
+
*/
|
|
101
|
+
saveTrace?: boolean;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The directory to save output files.
|
|
105
|
+
*/
|
|
106
|
+
outputDir?: string;
|
|
107
|
+
|
|
108
|
+
network?: {
|
|
109
|
+
/**
|
|
110
|
+
* List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
|
111
|
+
*/
|
|
112
|
+
allowedOrigins?: string[];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
|
|
116
|
+
*/
|
|
117
|
+
blockedOrigins?: string[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
|
|
122
|
+
*/
|
|
123
|
+
imageResponses?: 'allow' | 'omit';
|
|
124
|
+
};
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
+
import type { Config } from './config.js';
|
|
20
|
+
import type { BrowserContext } from 'playwright';
|
|
21
|
+
|
|
22
|
+
export declare function createConnection(
|
|
23
|
+
config?: Config,
|
|
24
|
+
contextGetter?: () => Promise<BrowserContext>
|
|
25
|
+
): Promise<Server>;
|
package/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { createConnection } from './lib/index.js';
|
package/lib/actions.d.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// src/batch/batch-executor.ts
|
|
21
|
+
import { Response } from "../response.js";
|
|
22
|
+
import { mergeExpectations } from "../schemas/expectation.js";
|
|
23
|
+
import { getErrorMessage } from "../utils/common-formatters.js";
|
|
24
|
+
|
|
25
|
+
class BatchExecutor {
|
|
26
|
+
toolRegistry;
|
|
27
|
+
context;
|
|
28
|
+
constructor(context, toolRegistry) {
|
|
29
|
+
this.context = context;
|
|
30
|
+
this.toolRegistry = toolRegistry;
|
|
31
|
+
}
|
|
32
|
+
validateAllSteps(steps) {
|
|
33
|
+
for (const [index, step] of steps.entries()) {
|
|
34
|
+
const tool = this.toolRegistry.get(step.tool);
|
|
35
|
+
if (!tool) {
|
|
36
|
+
throw new Error(`Unknown tool: ${step.tool}`);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parseResult = tool.schema.inputSchema.safeParse({
|
|
40
|
+
...step.arguments,
|
|
41
|
+
expectation: step.expectation
|
|
42
|
+
});
|
|
43
|
+
if (!parseResult.success) {
|
|
44
|
+
throw new Error(`Invalid arguments: ${parseResult.error.message}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Invalid arguments for ${step.tool} at step ${index}: ${getErrorMessage(error)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async execute(options) {
|
|
52
|
+
const results = [];
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
let stopReason = "completed";
|
|
55
|
+
this.validateAllSteps(options.steps);
|
|
56
|
+
const executeSequentially = async (index) => {
|
|
57
|
+
if (index >= options.steps.length) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const step = options.steps[index];
|
|
61
|
+
const stepStartTime = Date.now();
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.executeStep(step, options.globalExpectation);
|
|
64
|
+
const stepEndTime = Date.now();
|
|
65
|
+
results.push({
|
|
66
|
+
stepIndex: index,
|
|
67
|
+
toolName: step.tool,
|
|
68
|
+
success: true,
|
|
69
|
+
result,
|
|
70
|
+
executionTimeMs: stepEndTime - stepStartTime
|
|
71
|
+
});
|
|
72
|
+
await executeSequentially(index + 1);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const stepEndTime = Date.now();
|
|
75
|
+
const errorMessage = getErrorMessage(error);
|
|
76
|
+
results.push({
|
|
77
|
+
stepIndex: index,
|
|
78
|
+
toolName: step.tool,
|
|
79
|
+
success: false,
|
|
80
|
+
error: errorMessage,
|
|
81
|
+
executionTimeMs: stepEndTime - stepStartTime
|
|
82
|
+
});
|
|
83
|
+
if (!step.continueOnError) {
|
|
84
|
+
stopReason = "error";
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await executeSequentially(index + 1);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
await executeSequentially(0);
|
|
91
|
+
const totalExecutionTime = Date.now() - startTime;
|
|
92
|
+
const successfulSteps = results.filter((r) => r.success).length;
|
|
93
|
+
const failedSteps = results.filter((r) => !r.success).length;
|
|
94
|
+
return {
|
|
95
|
+
steps: results,
|
|
96
|
+
totalSteps: options.steps.length,
|
|
97
|
+
successfulSteps,
|
|
98
|
+
failedSteps,
|
|
99
|
+
totalExecutionTimeMs: totalExecutionTime,
|
|
100
|
+
stopReason
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async executeStep(step, globalExpectation) {
|
|
104
|
+
const tool = this.toolRegistry.get(step.tool);
|
|
105
|
+
if (!tool) {
|
|
106
|
+
throw new Error(`Unknown tool: ${step.tool}`);
|
|
107
|
+
}
|
|
108
|
+
const mergedExpectation = this.mergeStepExpectations(step.tool, globalExpectation, step.expectation);
|
|
109
|
+
const argsWithExpectation = {
|
|
110
|
+
...step.arguments,
|
|
111
|
+
expectation: mergedExpectation
|
|
112
|
+
};
|
|
113
|
+
const response = new Response(this.context, step.tool, argsWithExpectation, mergedExpectation);
|
|
114
|
+
await tool.handle(this.context, argsWithExpectation, response);
|
|
115
|
+
await response.finish();
|
|
116
|
+
return response.serialize();
|
|
117
|
+
}
|
|
118
|
+
mergeStepExpectations(toolName, globalExpectation, stepExpectation) {
|
|
119
|
+
let merged = mergeExpectations(toolName);
|
|
120
|
+
if (globalExpectation) {
|
|
121
|
+
merged = mergeExpectations(toolName, {
|
|
122
|
+
...merged,
|
|
123
|
+
...globalExpectation
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (stepExpectation) {
|
|
127
|
+
merged = mergeExpectations(toolName, {
|
|
128
|
+
...merged,
|
|
129
|
+
...stepExpectation
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
BatchExecutor
|
|
137
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// src/browser-context-factory.ts
|
|
21
|
+
import { promises as fsPromises } from "node:fs";
|
|
22
|
+
import { createServer } from "node:net";
|
|
23
|
+
import { join as pathJoin } from "node:path";
|
|
24
|
+
import debug from "debug";
|
|
25
|
+
import {
|
|
26
|
+
chromium,
|
|
27
|
+
firefox,
|
|
28
|
+
webkit
|
|
29
|
+
} from "playwright";
|
|
30
|
+
import { registryDirectory } from "playwright-core/lib/server/registry/index";
|
|
31
|
+
import { outputFile } from "./config.js";
|
|
32
|
+
import { logUnhandledError, testDebug } from "./log.js";
|
|
33
|
+
import { createHash } from "./utils.js";
|
|
34
|
+
var browserDebug = debug("pw:mcp:browser");
|
|
35
|
+
function getBrowserType(browserName) {
|
|
36
|
+
switch (browserName) {
|
|
37
|
+
case "chromium":
|
|
38
|
+
return chromium;
|
|
39
|
+
case "firefox":
|
|
40
|
+
return firefox;
|
|
41
|
+
case "webkit":
|
|
42
|
+
return webkit;
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported browser: ${browserName}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function contextFactory(config) {
|
|
48
|
+
if (config.browser.remoteEndpoint) {
|
|
49
|
+
return new RemoteContextFactory(config);
|
|
50
|
+
}
|
|
51
|
+
if (config.browser.cdpEndpoint) {
|
|
52
|
+
return new CdpContextFactory(config);
|
|
53
|
+
}
|
|
54
|
+
if (config.browser.isolated) {
|
|
55
|
+
return new IsolatedContextFactory(config);
|
|
56
|
+
}
|
|
57
|
+
return new PersistentContextFactory(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class BaseContextFactory {
|
|
61
|
+
name;
|
|
62
|
+
description;
|
|
63
|
+
config;
|
|
64
|
+
_browserPromise;
|
|
65
|
+
_tracesDir;
|
|
66
|
+
constructor(name, description, config) {
|
|
67
|
+
this.name = name;
|
|
68
|
+
this.description = description;
|
|
69
|
+
this.config = config;
|
|
70
|
+
}
|
|
71
|
+
_obtainBrowser() {
|
|
72
|
+
if (this._browserPromise) {
|
|
73
|
+
return this._browserPromise;
|
|
74
|
+
}
|
|
75
|
+
testDebug(`obtain browser (${this.name})`);
|
|
76
|
+
this._browserPromise = this._doObtainBrowser();
|
|
77
|
+
this._browserPromise.then((browser) => {
|
|
78
|
+
browser.on("disconnected", () => {
|
|
79
|
+
this._browserPromise = undefined;
|
|
80
|
+
});
|
|
81
|
+
}).catch((error) => {
|
|
82
|
+
browserDebug("Browser connection failed:", error);
|
|
83
|
+
this._browserPromise = undefined;
|
|
84
|
+
});
|
|
85
|
+
return this._browserPromise;
|
|
86
|
+
}
|
|
87
|
+
_doObtainBrowser() {
|
|
88
|
+
throw new Error("Not implemented");
|
|
89
|
+
}
|
|
90
|
+
async createContext(clientInfo) {
|
|
91
|
+
if (this.config.saveTrace) {
|
|
92
|
+
this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
93
|
+
}
|
|
94
|
+
testDebug(`create browser context (${this.name})`);
|
|
95
|
+
const browser = await this._obtainBrowser();
|
|
96
|
+
const browserContext = await this._doCreateContext(browser);
|
|
97
|
+
return {
|
|
98
|
+
browserContext,
|
|
99
|
+
close: () => this._closeBrowserContext(browserContext, browser)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
_doCreateContext(_browser) {
|
|
103
|
+
throw new Error("Not implemented");
|
|
104
|
+
}
|
|
105
|
+
async _closeBrowserContext(browserContext, browser) {
|
|
106
|
+
testDebug(`close browser context (${this.name})`);
|
|
107
|
+
if (browser.contexts().length === 1) {
|
|
108
|
+
this._browserPromise = undefined;
|
|
109
|
+
}
|
|
110
|
+
await browserContext.close().catch(logUnhandledError);
|
|
111
|
+
if (browser.contexts().length === 0) {
|
|
112
|
+
testDebug(`close browser (${this.name})`);
|
|
113
|
+
await browser.close().catch(logUnhandledError);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class IsolatedContextFactory extends BaseContextFactory {
|
|
119
|
+
constructor(config) {
|
|
120
|
+
super("isolated", "Create a new isolated browser context", config);
|
|
121
|
+
}
|
|
122
|
+
async _doObtainBrowser() {
|
|
123
|
+
await injectCdpPort(this.config.browser);
|
|
124
|
+
const browserType = getBrowserType(this.config.browser.browserName);
|
|
125
|
+
return browserType.launch({
|
|
126
|
+
tracesDir: this._tracesDir,
|
|
127
|
+
...this.config.browser.launchOptions,
|
|
128
|
+
handleSIGINT: false,
|
|
129
|
+
handleSIGTERM: false
|
|
130
|
+
}).catch((error) => {
|
|
131
|
+
if (error.message.includes("Executable doesn't exist")) {
|
|
132
|
+
throw new Error("Browser specified in your config is not installed. Either install it (likely) or change the config.");
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
_doCreateContext(browser) {
|
|
138
|
+
return browser.newContext(this.config.browser.contextOptions);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class CdpContextFactory extends BaseContextFactory {
|
|
143
|
+
constructor(config) {
|
|
144
|
+
super("cdp", "Connect to a browser over CDP", config);
|
|
145
|
+
}
|
|
146
|
+
_doObtainBrowser() {
|
|
147
|
+
return chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
148
|
+
}
|
|
149
|
+
async _doCreateContext(browser) {
|
|
150
|
+
return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class RemoteContextFactory extends BaseContextFactory {
|
|
155
|
+
constructor(config) {
|
|
156
|
+
super("remote", "Connect to a browser using a remote endpoint", config);
|
|
157
|
+
}
|
|
158
|
+
_doObtainBrowser() {
|
|
159
|
+
const url = new URL(this.config.browser.remoteEndpoint);
|
|
160
|
+
url.searchParams.set("browser", this.config.browser.browserName);
|
|
161
|
+
if (this.config.browser.launchOptions) {
|
|
162
|
+
url.searchParams.set("launch-options", JSON.stringify(this.config.browser.launchOptions));
|
|
163
|
+
}
|
|
164
|
+
return getBrowserType(this.config.browser.browserName).connect(String(url));
|
|
165
|
+
}
|
|
166
|
+
_doCreateContext(browser) {
|
|
167
|
+
return browser.newContext();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class PersistentContextFactory {
|
|
172
|
+
config;
|
|
173
|
+
name = "persistent";
|
|
174
|
+
description = "Create a new persistent browser context";
|
|
175
|
+
_userDataDirs = new Set;
|
|
176
|
+
constructor(config) {
|
|
177
|
+
this.config = config;
|
|
178
|
+
}
|
|
179
|
+
async createContext(clientInfo) {
|
|
180
|
+
await injectCdpPort(this.config.browser);
|
|
181
|
+
testDebug("create browser context (persistent)");
|
|
182
|
+
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath);
|
|
183
|
+
let tracesDir;
|
|
184
|
+
if (this.config.saveTrace) {
|
|
185
|
+
tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`);
|
|
186
|
+
}
|
|
187
|
+
this._userDataDirs.add(userDataDir);
|
|
188
|
+
testDebug("lock user data dir", userDataDir);
|
|
189
|
+
const browserType = getBrowserType(this.config.browser.browserName);
|
|
190
|
+
const launchWithRetry = async (attempt) => {
|
|
191
|
+
if (attempt >= 5) {
|
|
192
|
+
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, {
|
|
196
|
+
tracesDir,
|
|
197
|
+
...this.config.browser.launchOptions,
|
|
198
|
+
...this.config.browser.contextOptions,
|
|
199
|
+
handleSIGINT: false,
|
|
200
|
+
handleSIGTERM: false
|
|
201
|
+
});
|
|
202
|
+
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
|
203
|
+
return { browserContext, close };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof Error && error.message.includes("Executable doesn't exist")) {
|
|
206
|
+
throw new Error("Browser specified in your config is not installed. Either install it (likely) or change the config.");
|
|
207
|
+
}
|
|
208
|
+
if (error instanceof Error && (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL"))) {
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
210
|
+
return launchWithRetry(attempt + 1);
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return launchWithRetry(0);
|
|
216
|
+
}
|
|
217
|
+
async _closeBrowserContext(browserContext, userDataDir) {
|
|
218
|
+
testDebug("close browser context (persistent)");
|
|
219
|
+
testDebug("release user data dir", userDataDir);
|
|
220
|
+
await browserContext.close().catch((error) => {
|
|
221
|
+
browserDebug("Failed to close browser context:", error);
|
|
222
|
+
});
|
|
223
|
+
this._userDataDirs.delete(userDataDir);
|
|
224
|
+
testDebug("close browser context complete (persistent)");
|
|
225
|
+
}
|
|
226
|
+
async _createUserDataDir(rootPath) {
|
|
227
|
+
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory;
|
|
228
|
+
const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName;
|
|
229
|
+
const rootPathToken = rootPath ? `-${createHash(rootPath)}` : "";
|
|
230
|
+
const result = pathJoin(dir, `mcp-${browserToken}${rootPathToken}`);
|
|
231
|
+
await fsPromises.mkdir(result, { recursive: true });
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function injectCdpPort(browserConfig) {
|
|
236
|
+
if (browserConfig.browserName === "chromium") {
|
|
237
|
+
browserConfig.launchOptions.cdpPort = await findFreePort();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function findFreePort() {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const server = createServer();
|
|
243
|
+
server.listen(0, () => {
|
|
244
|
+
const { port } = server.address();
|
|
245
|
+
server.close(() => resolve(port));
|
|
246
|
+
});
|
|
247
|
+
server.on("error", reject);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
contextFactory
|
|
252
|
+
};
|