@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
|
@@ -0,0 +1,486 @@
|
|
|
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/extension/cdp-relay.ts
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
import debug from "debug";
|
|
23
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
24
|
+
import { httpAddressToString } from "../http-server.js";
|
|
25
|
+
import { logUnhandledError } from "../log.js";
|
|
26
|
+
import { ManualPromise } from "../manual-promise.js";
|
|
27
|
+
var { registry } = await import("playwright-core/lib/server/registry/index");
|
|
28
|
+
var debugLogger = debug("pw:mcp:relay");
|
|
29
|
+
var HTTP_TO_WS_REGEX = /^http/;
|
|
30
|
+
var EXTENSION_ID_REGEX = /^[a-p]{32}$/;
|
|
31
|
+
var DANGEROUS_PATH_PATTERNS = [
|
|
32
|
+
/[;&|`$()]/,
|
|
33
|
+
/\.\./,
|
|
34
|
+
/^https?:/,
|
|
35
|
+
/^\w+:/
|
|
36
|
+
];
|
|
37
|
+
var DANGEROUS_PROPS = ["__proto__", "constructor", "prototype"];
|
|
38
|
+
|
|
39
|
+
class CDPRelayServer {
|
|
40
|
+
_wsHost;
|
|
41
|
+
_browserChannel;
|
|
42
|
+
_cdpPath;
|
|
43
|
+
_extensionPath;
|
|
44
|
+
_wss;
|
|
45
|
+
_playwrightConnection = null;
|
|
46
|
+
_extensionConnection = null;
|
|
47
|
+
_nextSessionId = 1;
|
|
48
|
+
_connectedTabInfo;
|
|
49
|
+
_extensionConnectionPromise;
|
|
50
|
+
constructor(server, browserChannel) {
|
|
51
|
+
this._wsHost = httpAddressToString(server.address()).replace(HTTP_TO_WS_REGEX, "ws");
|
|
52
|
+
this._browserChannel = browserChannel;
|
|
53
|
+
const uuid = crypto.randomUUID();
|
|
54
|
+
this._cdpPath = `/cdp/${uuid}`;
|
|
55
|
+
this._extensionPath = `/extension/${uuid}`;
|
|
56
|
+
this._resetExtensionConnection();
|
|
57
|
+
this._wss = new WebSocketServer({ server });
|
|
58
|
+
this._wss.on("connection", this._onConnection.bind(this));
|
|
59
|
+
}
|
|
60
|
+
cdpEndpoint() {
|
|
61
|
+
return `${this._wsHost}${this._cdpPath}`;
|
|
62
|
+
}
|
|
63
|
+
extensionEndpoint() {
|
|
64
|
+
return `${this._wsHost}${this._extensionPath}`;
|
|
65
|
+
}
|
|
66
|
+
async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
|
|
67
|
+
debugLogger("Ensuring extension connection for MCP context");
|
|
68
|
+
if (this._extensionConnection) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this._connectBrowser(clientInfo);
|
|
72
|
+
debugLogger("Waiting for incoming extension connection");
|
|
73
|
+
await Promise.race([
|
|
74
|
+
this._extensionConnectionPromise,
|
|
75
|
+
new Promise((_, reject) => abortSignal.addEventListener("abort", reject))
|
|
76
|
+
]);
|
|
77
|
+
debugLogger("Extension connection established");
|
|
78
|
+
}
|
|
79
|
+
_connectBrowser(clientInfo) {
|
|
80
|
+
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
|
81
|
+
const extensionId = process.env.PLAYWRIGHT_MCP_EXTENSION_ID ?? "jakfalbnbhgkpmoaakfflhflbfpkailf";
|
|
82
|
+
if (!EXTENSION_ID_REGEX.test(extensionId)) {
|
|
83
|
+
throw new Error("Invalid Chrome extension ID format");
|
|
84
|
+
}
|
|
85
|
+
const url = new URL(`chrome-extension://${extensionId}/lib/ui/connect.html`);
|
|
86
|
+
url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
|
|
87
|
+
const sanitizedClientInfo = this._sanitizeClientInfo(clientInfo);
|
|
88
|
+
url.searchParams.set("client", JSON.stringify(sanitizedClientInfo));
|
|
89
|
+
const href = url.toString();
|
|
90
|
+
const executableInfo = registry.findExecutable(this._browserChannel);
|
|
91
|
+
if (!executableInfo) {
|
|
92
|
+
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
|
93
|
+
}
|
|
94
|
+
const executablePath = executableInfo.executablePath();
|
|
95
|
+
if (!executablePath) {
|
|
96
|
+
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
|
97
|
+
}
|
|
98
|
+
if (!this._isValidExecutablePath(executablePath)) {
|
|
99
|
+
throw new Error("Invalid executable path detected");
|
|
100
|
+
}
|
|
101
|
+
spawn(executablePath, [href], {
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
detached: true,
|
|
104
|
+
shell: false,
|
|
105
|
+
stdio: "ignore"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
_sanitizeClientInfo(clientInfo) {
|
|
109
|
+
const sanitized = {
|
|
110
|
+
name: typeof clientInfo.name === "string" ? clientInfo.name.slice(0, 100) : "unknown",
|
|
111
|
+
version: typeof clientInfo.version === "string" ? clientInfo.version.slice(0, 20) : "1.0.0"
|
|
112
|
+
};
|
|
113
|
+
for (const key of Object.keys(sanitized)) {
|
|
114
|
+
const value = sanitized[key];
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
sanitized[key] = value.replace(/<script[^>]*>.*?<\/script>/gi, "");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return sanitized;
|
|
120
|
+
}
|
|
121
|
+
_isValidExecutablePath(path) {
|
|
122
|
+
if (!path || typeof path !== "string") {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return !DANGEROUS_PATH_PATTERNS.some((pattern) => pattern.test(path));
|
|
126
|
+
}
|
|
127
|
+
_safeJsonParse(jsonString) {
|
|
128
|
+
try {
|
|
129
|
+
if (jsonString.includes("__proto__") || jsonString.includes("constructor") || jsonString.includes("prototype")) {
|
|
130
|
+
debugLogger("Potential prototype pollution attempt detected");
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const result = JSON.parse(jsonString);
|
|
134
|
+
if (result === null || typeof result !== "object") {
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
this._sanitizeObject(result);
|
|
138
|
+
return result;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
debugLogger("JSON parsing failed:", error);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
_sanitizeObject(obj) {
|
|
145
|
+
if (!obj || typeof obj !== "object") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const prop of DANGEROUS_PROPS) {
|
|
149
|
+
if (prop in obj) {
|
|
150
|
+
delete obj[prop];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const value of Object.values(obj)) {
|
|
154
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
155
|
+
this._sanitizeObject(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
_isValidCDPCommand(message) {
|
|
160
|
+
if (!message || typeof message !== "object") {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const cmd = message;
|
|
164
|
+
return typeof cmd.id === "number" && typeof cmd.method === "string" && (cmd.sessionId === undefined || typeof cmd.sessionId === "string") && (cmd.params === undefined || typeof cmd.params === "object" && cmd.params !== null);
|
|
165
|
+
}
|
|
166
|
+
stop() {
|
|
167
|
+
this.closeConnections("Server stopped");
|
|
168
|
+
this._wss.close();
|
|
169
|
+
}
|
|
170
|
+
closeConnections(reason) {
|
|
171
|
+
this._closePlaywrightConnection(reason);
|
|
172
|
+
this._closeExtensionConnection(reason);
|
|
173
|
+
}
|
|
174
|
+
_onConnection(ws, request) {
|
|
175
|
+
const url = new URL(`http://localhost${request.url}`);
|
|
176
|
+
debugLogger(`New connection to ${url.pathname}`);
|
|
177
|
+
if (url.pathname === this._cdpPath) {
|
|
178
|
+
this._handlePlaywrightConnection(ws);
|
|
179
|
+
} else if (url.pathname === this._extensionPath) {
|
|
180
|
+
this._handleExtensionConnection(ws);
|
|
181
|
+
} else {
|
|
182
|
+
debugLogger(`Invalid path: ${url.pathname}`);
|
|
183
|
+
ws.close(4004, "Invalid path");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
_handlePlaywrightConnection(ws) {
|
|
187
|
+
if (this._playwrightConnection) {
|
|
188
|
+
debugLogger("Rejecting second Playwright connection");
|
|
189
|
+
ws.close(1000, "Another CDP client already connected");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._playwrightConnection = ws;
|
|
193
|
+
ws.on("message", async (data) => {
|
|
194
|
+
try {
|
|
195
|
+
const messageString = data.toString();
|
|
196
|
+
if (messageString.length > 1048576) {
|
|
197
|
+
debugLogger("Message too large, rejecting");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const message = this._safeJsonParse(messageString);
|
|
201
|
+
if (message === null) {
|
|
202
|
+
debugLogger("Invalid JSON message received from Playwright");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await this._handlePlaywrightMessage(message);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const truncatedData = String(data).slice(0, 500);
|
|
208
|
+
debugLogger(`Error while handling Playwright message
|
|
209
|
+
${truncatedData}...
|
|
210
|
+
`, error);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
ws.on("close", () => {
|
|
214
|
+
if (this._playwrightConnection !== ws) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this._playwrightConnection = null;
|
|
218
|
+
this._closeExtensionConnection("Playwright client disconnected");
|
|
219
|
+
debugLogger("Playwright WebSocket closed");
|
|
220
|
+
});
|
|
221
|
+
ws.on("error", (error) => {
|
|
222
|
+
debugLogger("Playwright WebSocket error:", error);
|
|
223
|
+
});
|
|
224
|
+
debugLogger("Playwright MCP connected");
|
|
225
|
+
}
|
|
226
|
+
_closeExtensionConnection(reason) {
|
|
227
|
+
this._extensionConnection?.close(reason);
|
|
228
|
+
this._extensionConnectionPromise.reject(new Error(reason));
|
|
229
|
+
this._resetExtensionConnection();
|
|
230
|
+
}
|
|
231
|
+
_resetExtensionConnection() {
|
|
232
|
+
this._connectedTabInfo = undefined;
|
|
233
|
+
this._extensionConnection = null;
|
|
234
|
+
this._extensionConnectionPromise = new ManualPromise;
|
|
235
|
+
this._extensionConnectionPromise.catch(logUnhandledError);
|
|
236
|
+
}
|
|
237
|
+
_closePlaywrightConnection(reason) {
|
|
238
|
+
if (this._playwrightConnection?.readyState === WebSocket.OPEN) {
|
|
239
|
+
this._playwrightConnection.close(1000, reason);
|
|
240
|
+
}
|
|
241
|
+
this._playwrightConnection = null;
|
|
242
|
+
}
|
|
243
|
+
_handleExtensionConnection(ws) {
|
|
244
|
+
if (this._extensionConnection) {
|
|
245
|
+
ws.close(1000, "Another extension connection already established");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this._extensionConnection = new ExtensionConnection(ws);
|
|
249
|
+
this._extensionConnection.onclose = (c, reason) => {
|
|
250
|
+
debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
|
|
251
|
+
if (this._extensionConnection !== c) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this._resetExtensionConnection();
|
|
255
|
+
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
|
256
|
+
};
|
|
257
|
+
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
|
|
258
|
+
this._extensionConnectionPromise.resolve();
|
|
259
|
+
}
|
|
260
|
+
_handleExtensionMessage(method, params) {
|
|
261
|
+
switch (method) {
|
|
262
|
+
case "forwardCDPEvent": {
|
|
263
|
+
const sessionId = params.sessionId ?? this._connectedTabInfo?.sessionId;
|
|
264
|
+
this._sendToPlaywright({
|
|
265
|
+
sessionId,
|
|
266
|
+
method: params.method,
|
|
267
|
+
params: params.params
|
|
268
|
+
});
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "detachedFromTab":
|
|
272
|
+
debugLogger("← Debugger detached from tab:", params);
|
|
273
|
+
this._connectedTabInfo = undefined;
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
debugLogger(`← Extension: unhandled method ${method}`, params);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async _handlePlaywrightMessage(message) {
|
|
281
|
+
if (!this._isValidCDPCommand(message)) {
|
|
282
|
+
debugLogger("Invalid CDP command received from Playwright");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
debugLogger("← Playwright:", `${message.method} (id=${message.id})`);
|
|
286
|
+
const { id, sessionId, method, params } = message;
|
|
287
|
+
try {
|
|
288
|
+
const result = await this._handleCDPCommand(method, params, sessionId);
|
|
289
|
+
this._sendToPlaywright({ id, sessionId, result });
|
|
290
|
+
} catch (e) {
|
|
291
|
+
debugLogger("Error in the extension:", e);
|
|
292
|
+
this._sendToPlaywright({
|
|
293
|
+
id,
|
|
294
|
+
sessionId,
|
|
295
|
+
error: { message: e.message }
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async _handleCDPCommand(method, params, sessionId) {
|
|
300
|
+
switch (method) {
|
|
301
|
+
case "Browser.getVersion": {
|
|
302
|
+
return {
|
|
303
|
+
protocolVersion: "1.3",
|
|
304
|
+
product: "Chrome/Extension-Bridge",
|
|
305
|
+
userAgent: "CDP-Bridge-Server/1.0.0"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "Browser.setDownloadBehavior": {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
case "Target.setAutoAttach": {
|
|
312
|
+
if (sessionId) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
{
|
|
316
|
+
const result = await this._extensionConnection?.send("attachToTab");
|
|
317
|
+
const targetInfo = result.targetInfo;
|
|
318
|
+
this._connectedTabInfo = {
|
|
319
|
+
targetInfo,
|
|
320
|
+
sessionId: `pw-tab-${this._nextSessionId++}`
|
|
321
|
+
};
|
|
322
|
+
debugLogger("Simulating auto-attach");
|
|
323
|
+
this._sendToPlaywright({
|
|
324
|
+
method: "Target.attachedToTarget",
|
|
325
|
+
params: {
|
|
326
|
+
sessionId: this._connectedTabInfo.sessionId,
|
|
327
|
+
targetInfo: {
|
|
328
|
+
...this._connectedTabInfo.targetInfo,
|
|
329
|
+
attached: true
|
|
330
|
+
},
|
|
331
|
+
waitingForDebugger: false
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
case "Target.getTargetInfo": {
|
|
338
|
+
return this._connectedTabInfo?.targetInfo;
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
return await this._forwardToExtension(method, params, sessionId);
|
|
344
|
+
}
|
|
345
|
+
async _forwardToExtension(method, params, sessionId) {
|
|
346
|
+
if (!this._extensionConnection) {
|
|
347
|
+
throw new Error("Extension not connected");
|
|
348
|
+
}
|
|
349
|
+
let effectiveSessionId = sessionId;
|
|
350
|
+
if (this._connectedTabInfo?.sessionId === sessionId) {
|
|
351
|
+
effectiveSessionId = undefined;
|
|
352
|
+
}
|
|
353
|
+
return await this._extensionConnection.send("forwardCDPCommand", {
|
|
354
|
+
sessionId: effectiveSessionId,
|
|
355
|
+
method,
|
|
356
|
+
params
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
_sendToPlaywright(message) {
|
|
360
|
+
const messageDesc = message.method ?? `response(id=${message.id})`;
|
|
361
|
+
debugLogger("→ Playwright:", messageDesc);
|
|
362
|
+
this._playwrightConnection?.send(JSON.stringify(message));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
class ExtensionConnection {
|
|
367
|
+
_ws;
|
|
368
|
+
_callbacks = new Map;
|
|
369
|
+
_lastId = 0;
|
|
370
|
+
onmessage;
|
|
371
|
+
onclose;
|
|
372
|
+
constructor(ws) {
|
|
373
|
+
this._ws = ws;
|
|
374
|
+
this._ws.on("message", this._onMessage.bind(this));
|
|
375
|
+
this._ws.on("close", this._onClose.bind(this));
|
|
376
|
+
this._ws.on("error", this._onError.bind(this));
|
|
377
|
+
}
|
|
378
|
+
send(method, params, sessionId) {
|
|
379
|
+
if (this._ws.readyState !== WebSocket.OPEN) {
|
|
380
|
+
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
|
|
381
|
+
}
|
|
382
|
+
const id = ++this._lastId;
|
|
383
|
+
this._ws.send(JSON.stringify({ id, method, params, sessionId }));
|
|
384
|
+
const error = new Error(`Protocol error: ${method}`);
|
|
385
|
+
return new Promise((resolve, reject) => {
|
|
386
|
+
this._callbacks.set(id, { resolve, reject, error });
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
close(message) {
|
|
390
|
+
debugLogger("closing extension connection:", message);
|
|
391
|
+
if (this._ws.readyState === WebSocket.OPEN) {
|
|
392
|
+
this._ws.close(1000, message);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
_parseJsonSafely(jsonString) {
|
|
396
|
+
try {
|
|
397
|
+
if (jsonString.includes("__proto__") || jsonString.includes("constructor") || jsonString.includes("prototype")) {
|
|
398
|
+
debugLogger("Potential prototype pollution attempt detected");
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const result = JSON.parse(jsonString);
|
|
402
|
+
if (result === null || typeof result !== "object") {
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
this._sanitizeJsonObject(result);
|
|
406
|
+
return result;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
debugLogger("JSON parsing failed:", error);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
_sanitizeJsonObject(obj) {
|
|
413
|
+
if (!obj || typeof obj !== "object") {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const prop of DANGEROUS_PROPS) {
|
|
417
|
+
if (prop in obj) {
|
|
418
|
+
delete obj[prop];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
for (const value of Object.values(obj)) {
|
|
422
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
423
|
+
this._sanitizeJsonObject(value);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
_onMessage(event) {
|
|
428
|
+
const eventData = event.toString();
|
|
429
|
+
if (eventData.length > 1048576) {
|
|
430
|
+
debugLogger("<closing ws> Message too large, closing websocket");
|
|
431
|
+
this._ws.close();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const parsedJson = this._parseJsonSafely(eventData);
|
|
435
|
+
if (parsedJson === null) {
|
|
436
|
+
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData.slice(0, 200)}...`);
|
|
437
|
+
this._ws.close();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
this._handleParsedMessage(parsedJson);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
const errorMessage = e?.message;
|
|
444
|
+
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${errorMessage}`);
|
|
445
|
+
this._ws.close();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
_handleParsedMessage(object) {
|
|
449
|
+
if (object.id && this._callbacks.has(object.id)) {
|
|
450
|
+
const callback = this._callbacks.get(object.id);
|
|
451
|
+
if (!callback) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this._callbacks.delete(object.id);
|
|
455
|
+
if (object.error) {
|
|
456
|
+
const error = callback.error;
|
|
457
|
+
error.message = object.error;
|
|
458
|
+
callback.reject(error);
|
|
459
|
+
} else {
|
|
460
|
+
callback.resolve(object.result);
|
|
461
|
+
}
|
|
462
|
+
} else if (object.id) {
|
|
463
|
+
debugLogger("← Extension: unexpected response", object);
|
|
464
|
+
} else if (object.method) {
|
|
465
|
+
this.onmessage?.(object.method, object.params ?? {});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
_onClose(event) {
|
|
469
|
+
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
|
|
470
|
+
this._dispose();
|
|
471
|
+
this.onclose?.(this, event.reason);
|
|
472
|
+
}
|
|
473
|
+
_onError(event) {
|
|
474
|
+
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${String(event.target)}`);
|
|
475
|
+
this._dispose();
|
|
476
|
+
}
|
|
477
|
+
_dispose() {
|
|
478
|
+
for (const callback of this._callbacks.values()) {
|
|
479
|
+
callback.reject(new Error("WebSocket closed"));
|
|
480
|
+
}
|
|
481
|
+
this._callbacks.clear();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
export {
|
|
485
|
+
CDPRelayServer
|
|
486
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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/extension/extension-context-factory.ts
|
|
21
|
+
import debug from "debug";
|
|
22
|
+
import { chromium } from "playwright";
|
|
23
|
+
import { startHttpServer } from "../http-server.js";
|
|
24
|
+
import { CDPRelayServer } from "./cdp-relay.js";
|
|
25
|
+
var debugLogger = debug("pw:mcp:relay");
|
|
26
|
+
|
|
27
|
+
class ExtensionContextFactory {
|
|
28
|
+
name = "extension";
|
|
29
|
+
description = "Connect to a browser using the Playwright MCP extension";
|
|
30
|
+
_browserChannel;
|
|
31
|
+
_relayPromise;
|
|
32
|
+
_browserPromise;
|
|
33
|
+
constructor(browserChannel, _userDataDir) {
|
|
34
|
+
this._browserChannel = browserChannel;
|
|
35
|
+
}
|
|
36
|
+
async createContext(clientInfo, abortSignal) {
|
|
37
|
+
this._browserPromise ??= this._obtainBrowser(clientInfo, abortSignal);
|
|
38
|
+
const browser = await this._browserPromise;
|
|
39
|
+
return {
|
|
40
|
+
browserContext: browser.contexts()[0],
|
|
41
|
+
close: async () => {
|
|
42
|
+
debugLogger("close() called for browser context");
|
|
43
|
+
await browser.close();
|
|
44
|
+
this._browserPromise = undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async _obtainBrowser(clientInfo, abortSignal) {
|
|
49
|
+
this._relayPromise ??= this._startRelay(abortSignal);
|
|
50
|
+
const relay = await this._relayPromise;
|
|
51
|
+
abortSignal.throwIfAborted();
|
|
52
|
+
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
|
|
53
|
+
const browser = await chromium.connectOverCDP(relay.cdpEndpoint());
|
|
54
|
+
browser.on("disconnected", () => {
|
|
55
|
+
this._browserPromise = undefined;
|
|
56
|
+
debugLogger("Browser disconnected");
|
|
57
|
+
});
|
|
58
|
+
return browser;
|
|
59
|
+
}
|
|
60
|
+
async _startRelay(abortSignal) {
|
|
61
|
+
const httpServer = await startHttpServer({});
|
|
62
|
+
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel);
|
|
63
|
+
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
|
64
|
+
if (abortSignal.aborted) {
|
|
65
|
+
cdpRelayServer.stop();
|
|
66
|
+
} else {
|
|
67
|
+
abortSignal.addEventListener("abort", () => cdpRelayServer.stop());
|
|
68
|
+
}
|
|
69
|
+
return cdpRelayServer;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
ExtensionContextFactory
|
|
74
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
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/extension/main.ts
|
|
21
|
+
import { BrowserServerBackend } from "../browser-server-backend.js";
|
|
22
|
+
import { InProcessClientFactory } from "../in-process-client.js";
|
|
23
|
+
import { start } from "../mcp/transport.js";
|
|
24
|
+
import { ExtensionContextFactory } from "./extension-context-factory.js";
|
|
25
|
+
async function runWithExtension(config) {
|
|
26
|
+
const contextFactory = createExtensionContextFactory(config);
|
|
27
|
+
const factories = [contextFactory];
|
|
28
|
+
const serverBackendFactory = () => new BrowserServerBackend(config, factories);
|
|
29
|
+
await start(serverBackendFactory, config.server);
|
|
30
|
+
}
|
|
31
|
+
function createExtensionClientFactory(config) {
|
|
32
|
+
return new InProcessClientFactory(createExtensionContextFactory(config), config);
|
|
33
|
+
}
|
|
34
|
+
function createExtensionContextFactory(config) {
|
|
35
|
+
return new ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
runWithExtension,
|
|
39
|
+
createExtensionContextFactory,
|
|
40
|
+
createExtensionClientFactory
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
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/file-utils.ts
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
function cacheDir() {
|
|
24
|
+
let cacheDirectory;
|
|
25
|
+
if (process.platform === "linux") {
|
|
26
|
+
cacheDirectory = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache");
|
|
27
|
+
} else if (process.platform === "darwin") {
|
|
28
|
+
cacheDirectory = path.join(os.homedir(), "Library", "Caches");
|
|
29
|
+
} else if (process.platform === "win32") {
|
|
30
|
+
cacheDirectory = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
33
|
+
}
|
|
34
|
+
return path.join(cacheDirectory, "ms-playwright");
|
|
35
|
+
}
|
|
36
|
+
function userDataDir(browserConfig) {
|
|
37
|
+
return path.join(cacheDir(), "ms-playwright", `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
userDataDir,
|
|
41
|
+
cacheDir
|
|
42
|
+
};
|