ScuttleBot

scuttlebot / tests / e2e / node_modules / playwright / lib / mcp / extension / cdpRelay.js
Source Blame History 351 lines
f7eb47b… lmata 1 "use strict";
f7eb47b… lmata 2 var __create = Object.create;
f7eb47b… lmata 3 var __defProp = Object.defineProperty;
f7eb47b… lmata 4 var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
f7eb47b… lmata 5 var __getOwnPropNames = Object.getOwnPropertyNames;
f7eb47b… lmata 6 var __getProtoOf = Object.getPrototypeOf;
f7eb47b… lmata 7 var __hasOwnProp = Object.prototype.hasOwnProperty;
f7eb47b… lmata 8 var __export = (target, all) => {
f7eb47b… lmata 9 for (var name in all)
f7eb47b… lmata 10 __defProp(target, name, { get: all[name], enumerable: true });
f7eb47b… lmata 11 };
f7eb47b… lmata 12 var __copyProps = (to, from, except, desc) => {
f7eb47b… lmata 13 if (from && typeof from === "object" || typeof from === "function") {
f7eb47b… lmata 14 for (let key of __getOwnPropNames(from))
f7eb47b… lmata 15 if (!__hasOwnProp.call(to, key) && key !== except)
f7eb47b… lmata 16 __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
f7eb47b… lmata 17 }
f7eb47b… lmata 18 return to;
f7eb47b… lmata 19 };
f7eb47b… lmata 20 var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
f7eb47b… lmata 21 // If the importer is in node compatibility mode or this is not an ESM
f7eb47b… lmata 22 // file that has been converted to a CommonJS file using a Babel-
f7eb47b… lmata 23 // compatible transform (i.e. "__esModule" has not been set), then set
f7eb47b… lmata 24 // "default" to the CommonJS "module.exports" for node compatibility.
f7eb47b… lmata 25 isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
f7eb47b… lmata 26 mod
f7eb47b… lmata 27 ));
f7eb47b… lmata 28 var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
f7eb47b… lmata 29 var cdpRelay_exports = {};
f7eb47b… lmata 30 __export(cdpRelay_exports, {
f7eb47b… lmata 31 CDPRelayServer: () => CDPRelayServer
f7eb47b… lmata 32 });
f7eb47b… lmata 33 module.exports = __toCommonJS(cdpRelay_exports);
f7eb47b… lmata 34 var import_child_process = require("child_process");
f7eb47b… lmata 35 var import_utilsBundle = require("playwright-core/lib/utilsBundle");
f7eb47b… lmata 36 var import_registry = require("playwright-core/lib/server/registry/index");
f7eb47b… lmata 37 var import_utils = require("playwright-core/lib/utils");
f7eb47b… lmata 38 var import_http2 = require("../sdk/http");
f7eb47b… lmata 39 var import_log = require("../log");
f7eb47b… lmata 40 var protocol = __toESM(require("./protocol"));
f7eb47b… lmata 41 const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
f7eb47b… lmata 42 class CDPRelayServer {
f7eb47b… lmata 43 constructor(server, browserChannel, userDataDir, executablePath) {
f7eb47b… lmata 44 this._playwrightConnection = null;
f7eb47b… lmata 45 this._extensionConnection = null;
f7eb47b… lmata 46 this._nextSessionId = 1;
f7eb47b… lmata 47 this._wsHost = (0, import_http2.addressToString)(server.address(), { protocol: "ws" });
f7eb47b… lmata 48 this._browserChannel = browserChannel;
f7eb47b… lmata 49 this._userDataDir = userDataDir;
f7eb47b… lmata 50 this._executablePath = executablePath;
f7eb47b… lmata 51 const uuid = crypto.randomUUID();
f7eb47b… lmata 52 this._cdpPath = `/cdp/${uuid}`;
f7eb47b… lmata 53 this._extensionPath = `/extension/${uuid}`;
f7eb47b… lmata 54 this._resetExtensionConnection();
f7eb47b… lmata 55 this._wss = new import_utilsBundle.wsServer({ server });
f7eb47b… lmata 56 this._wss.on("connection", this._onConnection.bind(this));
f7eb47b… lmata 57 }
f7eb47b… lmata 58 cdpEndpoint() {
f7eb47b… lmata 59 return `${this._wsHost}${this._cdpPath}`;
f7eb47b… lmata 60 }
f7eb47b… lmata 61 extensionEndpoint() {
f7eb47b… lmata 62 return `${this._wsHost}${this._extensionPath}`;
f7eb47b… lmata 63 }
f7eb47b… lmata 64 async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName) {
f7eb47b… lmata 65 debugLogger("Ensuring extension connection for MCP context");
f7eb47b… lmata 66 if (this._extensionConnection)
f7eb47b… lmata 67 return;
f7eb47b… lmata 68 this._connectBrowser(clientInfo, toolName);
f7eb47b… lmata 69 debugLogger("Waiting for incoming extension connection");
f7eb47b… lmata 70 await Promise.race([
f7eb47b… lmata 71 this._extensionConnectionPromise,
f7eb47b… lmata 72 new Promise((_, reject) => setTimeout(() => {
f7eb47b… lmata 73 reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`));
f7eb47b… lmata 74 }, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5e3)),
f7eb47b… lmata 75 new Promise((_, reject) => abortSignal.addEventListener("abort", reject))
f7eb47b… lmata 76 ]);
f7eb47b… lmata 77 debugLogger("Extension connection established");
f7eb47b… lmata 78 }
f7eb47b… lmata 79 _connectBrowser(clientInfo, toolName) {
f7eb47b… lmata 80 const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
f7eb47b… lmata 81 const url = new URL("chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html");
f7eb47b… lmata 82 url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
f7eb47b… lmata 83 const client = {
f7eb47b… lmata 84 name: clientInfo.name,
f7eb47b… lmata 85 version: clientInfo.version
f7eb47b… lmata 86 };
f7eb47b… lmata 87 url.searchParams.set("client", JSON.stringify(client));
f7eb47b… lmata 88 url.searchParams.set("protocolVersion", process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());
f7eb47b… lmata 89 if (toolName)
f7eb47b… lmata 90 url.searchParams.set("newTab", String(toolName === "browser_navigate"));
f7eb47b… lmata 91 const token = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
f7eb47b… lmata 92 if (token)
f7eb47b… lmata 93 url.searchParams.set("token", token);
f7eb47b… lmata 94 const href = url.toString();
f7eb47b… lmata 95 let executablePath = this._executablePath;
f7eb47b… lmata 96 if (!executablePath) {
f7eb47b… lmata 97 const executableInfo = import_registry.registry.findExecutable(this._browserChannel);
f7eb47b… lmata 98 if (!executableInfo)
f7eb47b… lmata 99 throw new Error(`Unsupported channel: "${this._browserChannel}"`);
f7eb47b… lmata 100 executablePath = executableInfo.executablePath("javascript");
f7eb47b… lmata 101 if (!executablePath)
f7eb47b… lmata 102 throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
f7eb47b… lmata 103 }
f7eb47b… lmata 104 const args = [];
f7eb47b… lmata 105 if (this._userDataDir)
f7eb47b… lmata 106 args.push(`--user-data-dir=${this._userDataDir}`);
f7eb47b… lmata 107 args.push(href);
f7eb47b… lmata 108 (0, import_child_process.spawn)(executablePath, args, {
f7eb47b… lmata 109 windowsHide: true,
f7eb47b… lmata 110 detached: true,
f7eb47b… lmata 111 shell: false,
f7eb47b… lmata 112 stdio: "ignore"
f7eb47b… lmata 113 });
f7eb47b… lmata 114 }
f7eb47b… lmata 115 stop() {
f7eb47b… lmata 116 this.closeConnections("Server stopped");
f7eb47b… lmata 117 this._wss.close();
f7eb47b… lmata 118 }
f7eb47b… lmata 119 closeConnections(reason) {
f7eb47b… lmata 120 this._closePlaywrightConnection(reason);
f7eb47b… lmata 121 this._closeExtensionConnection(reason);
f7eb47b… lmata 122 }
f7eb47b… lmata 123 _onConnection(ws2, request) {
f7eb47b… lmata 124 const url = new URL(`http://localhost${request.url}`);
f7eb47b… lmata 125 debugLogger(`New connection to ${url.pathname}`);
f7eb47b… lmata 126 if (url.pathname === this._cdpPath) {
f7eb47b… lmata 127 this._handlePlaywrightConnection(ws2);
f7eb47b… lmata 128 } else if (url.pathname === this._extensionPath) {
f7eb47b… lmata 129 this._handleExtensionConnection(ws2);
f7eb47b… lmata 130 } else {
f7eb47b… lmata 131 debugLogger(`Invalid path: ${url.pathname}`);
f7eb47b… lmata 132 ws2.close(4004, "Invalid path");
f7eb47b… lmata 133 }
f7eb47b… lmata 134 }
f7eb47b… lmata 135 _handlePlaywrightConnection(ws2) {
f7eb47b… lmata 136 if (this._playwrightConnection) {
f7eb47b… lmata 137 debugLogger("Rejecting second Playwright connection");
f7eb47b… lmata 138 ws2.close(1e3, "Another CDP client already connected");
f7eb47b… lmata 139 return;
f7eb47b… lmata 140 }
f7eb47b… lmata 141 this._playwrightConnection = ws2;
f7eb47b… lmata 142 ws2.on("message", async (data) => {
f7eb47b… lmata 143 try {
f7eb47b… lmata 144 const message = JSON.parse(data.toString());
f7eb47b… lmata 145 await this._handlePlaywrightMessage(message);
f7eb47b… lmata 146 } catch (error) {
f7eb47b… lmata 147 debugLogger(`Error while handling Playwright message
f7eb47b… lmata 148 ${data.toString()}
f7eb47b… lmata 149 `, error);
f7eb47b… lmata 150 }
f7eb47b… lmata 151 });
f7eb47b… lmata 152 ws2.on("close", () => {
f7eb47b… lmata 153 if (this._playwrightConnection !== ws2)
f7eb47b… lmata 154 return;
f7eb47b… lmata 155 this._playwrightConnection = null;
f7eb47b… lmata 156 this._closeExtensionConnection("Playwright client disconnected");
f7eb47b… lmata 157 debugLogger("Playwright WebSocket closed");
f7eb47b… lmata 158 });
f7eb47b… lmata 159 ws2.on("error", (error) => {
f7eb47b… lmata 160 debugLogger("Playwright WebSocket error:", error);
f7eb47b… lmata 161 });
f7eb47b… lmata 162 debugLogger("Playwright MCP connected");
f7eb47b… lmata 163 }
f7eb47b… lmata 164 _closeExtensionConnection(reason) {
f7eb47b… lmata 165 this._extensionConnection?.close(reason);
f7eb47b… lmata 166 this._extensionConnectionPromise.reject(new Error(reason));
f7eb47b… lmata 167 this._resetExtensionConnection();
f7eb47b… lmata 168 }
f7eb47b… lmata 169 _resetExtensionConnection() {
f7eb47b… lmata 170 this._connectedTabInfo = void 0;
f7eb47b… lmata 171 this._extensionConnection = null;
f7eb47b… lmata 172 this._extensionConnectionPromise = new import_utils.ManualPromise();
f7eb47b… lmata 173 void this._extensionConnectionPromise.catch(import_log.logUnhandledError);
f7eb47b… lmata 174 }
f7eb47b… lmata 175 _closePlaywrightConnection(reason) {
f7eb47b… lmata 176 if (this._playwrightConnection?.readyState === import_utilsBundle.ws.OPEN)
f7eb47b… lmata 177 this._playwrightConnection.close(1e3, reason);
f7eb47b… lmata 178 this._playwrightConnection = null;
f7eb47b… lmata 179 }
f7eb47b… lmata 180 _handleExtensionConnection(ws2) {
f7eb47b… lmata 181 if (this._extensionConnection) {
f7eb47b… lmata 182 ws2.close(1e3, "Another extension connection already established");
f7eb47b… lmata 183 return;
f7eb47b… lmata 184 }
f7eb47b… lmata 185 this._extensionConnection = new ExtensionConnection(ws2);
f7eb47b… lmata 186 this._extensionConnection.onclose = (c, reason) => {
f7eb47b… lmata 187 debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
f7eb47b… lmata 188 if (this._extensionConnection !== c)
f7eb47b… lmata 189 return;
f7eb47b… lmata 190 this._resetExtensionConnection();
f7eb47b… lmata 191 this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
f7eb47b… lmata 192 };
f7eb47b… lmata 193 this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
f7eb47b… lmata 194 this._extensionConnectionPromise.resolve();
f7eb47b… lmata 195 }
f7eb47b… lmata 196 _handleExtensionMessage(method, params) {
f7eb47b… lmata 197 switch (method) {
f7eb47b… lmata 198 case "forwardCDPEvent":
f7eb47b… lmata 199 const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
f7eb47b… lmata 200 this._sendToPlaywright({
f7eb47b… lmata 201 sessionId,
f7eb47b… lmata 202 method: params.method,
f7eb47b… lmata 203 params: params.params
f7eb47b… lmata 204 });
f7eb47b… lmata 205 break;
f7eb47b… lmata 206 }
f7eb47b… lmata 207 }
f7eb47b… lmata 208 async _handlePlaywrightMessage(message) {
f7eb47b… lmata 209 debugLogger("\u2190 Playwright:", `${message.method} (id=${message.id})`);
f7eb47b… lmata 210 const { id, sessionId, method, params } = message;
f7eb47b… lmata 211 try {
f7eb47b… lmata 212 const result = await this._handleCDPCommand(method, params, sessionId);
f7eb47b… lmata 213 this._sendToPlaywright({ id, sessionId, result });
f7eb47b… lmata 214 } catch (e) {
f7eb47b… lmata 215 debugLogger("Error in the extension:", e);
f7eb47b… lmata 216 this._sendToPlaywright({
f7eb47b… lmata 217 id,
f7eb47b… lmata 218 sessionId,
f7eb47b… lmata 219 error: { message: e.message }
f7eb47b… lmata 220 });
f7eb47b… lmata 221 }
f7eb47b… lmata 222 }
f7eb47b… lmata 223 async _handleCDPCommand(method, params, sessionId) {
f7eb47b… lmata 224 switch (method) {
f7eb47b… lmata 225 case "Browser.getVersion": {
f7eb47b… lmata 226 return {
f7eb47b… lmata 227 protocolVersion: "1.3",
f7eb47b… lmata 228 product: "Chrome/Extension-Bridge",
f7eb47b… lmata 229 userAgent: "CDP-Bridge-Server/1.0.0"
f7eb47b… lmata 230 };
f7eb47b… lmata 231 }
f7eb47b… lmata 232 case "Browser.setDownloadBehavior": {
f7eb47b… lmata 233 return {};
f7eb47b… lmata 234 }
f7eb47b… lmata 235 case "Target.setAutoAttach": {
f7eb47b… lmata 236 if (sessionId)
f7eb47b… lmata 237 break;
f7eb47b… lmata 238 const { targetInfo } = await this._extensionConnection.send("attachToTab", {});
f7eb47b… lmata 239 this._connectedTabInfo = {
f7eb47b… lmata 240 targetInfo,
f7eb47b… lmata 241 sessionId: `pw-tab-${this._nextSessionId++}`
f7eb47b… lmata 242 };
f7eb47b… lmata 243 debugLogger("Simulating auto-attach");
f7eb47b… lmata 244 this._sendToPlaywright({
f7eb47b… lmata 245 method: "Target.attachedToTarget",
f7eb47b… lmata 246 params: {
f7eb47b… lmata 247 sessionId: this._connectedTabInfo.sessionId,
f7eb47b… lmata 248 targetInfo: {
f7eb47b… lmata 249 ...this._connectedTabInfo.targetInfo,
f7eb47b… lmata 250 attached: true
f7eb47b… lmata 251 },
f7eb47b… lmata 252 waitingForDebugger: false
f7eb47b… lmata 253 }
f7eb47b… lmata 254 });
f7eb47b… lmata 255 return {};
f7eb47b… lmata 256 }
f7eb47b… lmata 257 case "Target.getTargetInfo": {
f7eb47b… lmata 258 return this._connectedTabInfo?.targetInfo;
f7eb47b… lmata 259 }
f7eb47b… lmata 260 }
f7eb47b… lmata 261 return await this._forwardToExtension(method, params, sessionId);
f7eb47b… lmata 262 }
f7eb47b… lmata 263 async _forwardToExtension(method, params, sessionId) {
f7eb47b… lmata 264 if (!this._extensionConnection)
f7eb47b… lmata 265 throw new Error("Extension not connected");
f7eb47b… lmata 266 if (this._connectedTabInfo?.sessionId === sessionId)
f7eb47b… lmata 267 sessionId = void 0;
f7eb47b… lmata 268 return await this._extensionConnection.send("forwardCDPCommand", { sessionId, method, params });
f7eb47b… lmata 269 }
f7eb47b… lmata 270 _sendToPlaywright(message) {
f7eb47b… lmata 271 debugLogger("\u2192 Playwright:", `${message.method ?? `response(id=${message.id})`}`);
f7eb47b… lmata 272 this._playwrightConnection?.send(JSON.stringify(message));
f7eb47b… lmata 273 }
f7eb47b… lmata 274 }
f7eb47b… lmata 275 class ExtensionConnection {
f7eb47b… lmata 276 constructor(ws2) {
f7eb47b… lmata 277 this._callbacks = /* @__PURE__ */ new Map();
f7eb47b… lmata 278 this._lastId = 0;
f7eb47b… lmata 279 this._ws = ws2;
f7eb47b… lmata 280 this._ws.on("message", this._onMessage.bind(this));
f7eb47b… lmata 281 this._ws.on("close", this._onClose.bind(this));
f7eb47b… lmata 282 this._ws.on("error", this._onError.bind(this));
f7eb47b… lmata 283 }
f7eb47b… lmata 284 async send(method, params) {
f7eb47b… lmata 285 if (this._ws.readyState !== import_utilsBundle.ws.OPEN)
f7eb47b… lmata 286 throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
f7eb47b… lmata 287 const id = ++this._lastId;
f7eb47b… lmata 288 this._ws.send(JSON.stringify({ id, method, params }));
f7eb47b… lmata 289 const error = new Error(`Protocol error: ${method}`);
f7eb47b… lmata 290 return new Promise((resolve, reject) => {
f7eb47b… lmata 291 this._callbacks.set(id, { resolve, reject, error });
f7eb47b… lmata 292 });
f7eb47b… lmata 293 }
f7eb47b… lmata 294 close(message) {
f7eb47b… lmata 295 debugLogger("closing extension connection:", message);
f7eb47b… lmata 296 if (this._ws.readyState === import_utilsBundle.ws.OPEN)
f7eb47b… lmata 297 this._ws.close(1e3, message);
f7eb47b… lmata 298 }
f7eb47b… lmata 299 _onMessage(event) {
f7eb47b… lmata 300 const eventData = event.toString();
f7eb47b… lmata 301 let parsedJson;
f7eb47b… lmata 302 try {
f7eb47b… lmata 303 parsedJson = JSON.parse(eventData);
f7eb47b… lmata 304 } catch (e) {
f7eb47b… lmata 305 debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
f7eb47b… lmata 306 this._ws.close();
f7eb47b… lmata 307 return;
f7eb47b… lmata 308 }
f7eb47b… lmata 309 try {
f7eb47b… lmata 310 this._handleParsedMessage(parsedJson);
f7eb47b… lmata 311 } catch (e) {
f7eb47b… lmata 312 debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
f7eb47b… lmata 313 this._ws.close();
f7eb47b… lmata 314 }
f7eb47b… lmata 315 }
f7eb47b… lmata 316 _handleParsedMessage(object) {
f7eb47b… lmata 317 if (object.id && this._callbacks.has(object.id)) {
f7eb47b… lmata 318 const callback = this._callbacks.get(object.id);
f7eb47b… lmata 319 this._callbacks.delete(object.id);
f7eb47b… lmata 320 if (object.error) {
f7eb47b… lmata 321 const error = callback.error;
f7eb47b… lmata 322 error.message = object.error;
f7eb47b… lmata 323 callback.reject(error);
f7eb47b… lmata 324 } else {
f7eb47b… lmata 325 callback.resolve(object.result);
f7eb47b… lmata 326 }
f7eb47b… lmata 327 } else if (object.id) {
f7eb47b… lmata 328 debugLogger("\u2190 Extension: unexpected response", object);
f7eb47b… lmata 329 } else {
f7eb47b… lmata 330 this.onmessage?.(object.method, object.params);
f7eb47b… lmata 331 }
f7eb47b… lmata 332 }
f7eb47b… lmata 333 _onClose(event) {
f7eb47b… lmata 334 debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
f7eb47b… lmata 335 this._dispose();
f7eb47b… lmata 336 this.onclose?.(this, event.reason);
f7eb47b… lmata 337 }
f7eb47b… lmata 338 _onError(event) {
f7eb47b… lmata 339 debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
f7eb47b… lmata 340 this._dispose();
f7eb47b… lmata 341 }
f7eb47b… lmata 342 _dispose() {
f7eb47b… lmata 343 for (const callback of this._callbacks.values())
f7eb47b… lmata 344 callback.reject(new Error("WebSocket closed"));
f7eb47b… lmata 345 this._callbacks.clear();
f7eb47b… lmata 346 }
f7eb47b… lmata 347 }
f7eb47b… lmata 348 // Annotate the CommonJS export names for ESM import in node:
f7eb47b… lmata 349 0 && (module.exports = {
f7eb47b… lmata 350 CDPRelayServer
f7eb47b… lmata 351 });

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button