| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- "use strict";
- var __create = Object.create;
- var __defProp = Object.defineProperty;
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
- var __getOwnPropNames = Object.getOwnPropertyNames;
- var __getProtoOf = Object.getPrototypeOf;
- var __hasOwnProp = Object.prototype.hasOwnProperty;
- var __export = (target, all) => {
- for (var name in all)
- __defProp(target, name, { get: all[name], enumerable: true });
- };
- var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
- };
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
- // If the importer is in node compatibility mode or this is not an ESM
- // file that has been converted to a CommonJS file using a Babel-
- // compatible transform (i.e. "__esModule" has not been set), then set
- // "default" to the CommonJS "module.exports" for node compatibility.
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
- mod
- ));
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
- var cdpRelay_exports = {};
- __export(cdpRelay_exports, {
- CDPRelayServer: () => CDPRelayServer
- });
- module.exports = __toCommonJS(cdpRelay_exports);
- var import_child_process = require("child_process");
- var import_utilsBundle = require("playwright-core/lib/utilsBundle");
- var import_registry = require("playwright-core/lib/server/registry/index");
- var import_utils = require("playwright-core/lib/utils");
- var import_http2 = require("../sdk/http");
- var import_log = require("../log");
- var protocol = __toESM(require("./protocol"));
- const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
- class CDPRelayServer {
- constructor(server, browserChannel, userDataDir, executablePath) {
- this._playwrightConnection = null;
- this._extensionConnection = null;
- this._nextSessionId = 1;
- this._wsHost = (0, import_http2.httpAddressToString)(server.address()).replace(/^http/, "ws");
- this._browserChannel = browserChannel;
- this._userDataDir = userDataDir;
- this._executablePath = executablePath;
- const uuid = crypto.randomUUID();
- this._cdpPath = `/cdp/${uuid}`;
- this._extensionPath = `/extension/${uuid}`;
- this._resetExtensionConnection();
- this._wss = new import_utilsBundle.wsServer({ server });
- this._wss.on("connection", this._onConnection.bind(this));
- }
- cdpEndpoint() {
- return `${this._wsHost}${this._cdpPath}`;
- }
- extensionEndpoint() {
- return `${this._wsHost}${this._extensionPath}`;
- }
- async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName) {
- debugLogger("Ensuring extension connection for MCP context");
- if (this._extensionConnection)
- return;
- this._connectBrowser(clientInfo, toolName);
- debugLogger("Waiting for incoming extension connection");
- await Promise.race([
- this._extensionConnectionPromise,
- new Promise((_, reject) => setTimeout(() => {
- 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.`));
- }, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5e3)),
- new Promise((_, reject) => abortSignal.addEventListener("abort", reject))
- ]);
- debugLogger("Extension connection established");
- }
- _connectBrowser(clientInfo, toolName) {
- const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
- const url = new URL("chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html");
- url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
- const client = {
- name: clientInfo.name,
- version: clientInfo.version
- };
- url.searchParams.set("client", JSON.stringify(client));
- url.searchParams.set("protocolVersion", process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());
- if (toolName)
- url.searchParams.set("newTab", String(toolName === "browser_navigate"));
- const token = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
- if (token)
- url.searchParams.set("token", token);
- const href = url.toString();
- let executablePath = this._executablePath;
- if (!executablePath) {
- const executableInfo = import_registry.registry.findExecutable(this._browserChannel);
- if (!executableInfo)
- throw new Error(`Unsupported channel: "${this._browserChannel}"`);
- executablePath = executableInfo.executablePath("javascript");
- if (!executablePath)
- throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
- }
- const args = [];
- if (this._userDataDir)
- args.push(`--user-data-dir=${this._userDataDir}`);
- args.push(href);
- (0, import_child_process.spawn)(executablePath, args, {
- windowsHide: true,
- detached: true,
- shell: false,
- stdio: "ignore"
- });
- }
- stop() {
- this.closeConnections("Server stopped");
- this._wss.close();
- }
- closeConnections(reason) {
- this._closePlaywrightConnection(reason);
- this._closeExtensionConnection(reason);
- }
- _onConnection(ws2, request) {
- const url = new URL(`http://localhost${request.url}`);
- debugLogger(`New connection to ${url.pathname}`);
- if (url.pathname === this._cdpPath) {
- this._handlePlaywrightConnection(ws2);
- } else if (url.pathname === this._extensionPath) {
- this._handleExtensionConnection(ws2);
- } else {
- debugLogger(`Invalid path: ${url.pathname}`);
- ws2.close(4004, "Invalid path");
- }
- }
- _handlePlaywrightConnection(ws2) {
- if (this._playwrightConnection) {
- debugLogger("Rejecting second Playwright connection");
- ws2.close(1e3, "Another CDP client already connected");
- return;
- }
- this._playwrightConnection = ws2;
- ws2.on("message", async (data) => {
- try {
- const message = JSON.parse(data.toString());
- await this._handlePlaywrightMessage(message);
- } catch (error) {
- debugLogger(`Error while handling Playwright message
- ${data.toString()}
- `, error);
- }
- });
- ws2.on("close", () => {
- if (this._playwrightConnection !== ws2)
- return;
- this._playwrightConnection = null;
- this._closeExtensionConnection("Playwright client disconnected");
- debugLogger("Playwright WebSocket closed");
- });
- ws2.on("error", (error) => {
- debugLogger("Playwright WebSocket error:", error);
- });
- debugLogger("Playwright MCP connected");
- }
- _closeExtensionConnection(reason) {
- this._extensionConnection?.close(reason);
- this._extensionConnectionPromise.reject(new Error(reason));
- this._resetExtensionConnection();
- }
- _resetExtensionConnection() {
- this._connectedTabInfo = void 0;
- this._extensionConnection = null;
- this._extensionConnectionPromise = new import_utils.ManualPromise();
- void this._extensionConnectionPromise.catch(import_log.logUnhandledError);
- }
- _closePlaywrightConnection(reason) {
- if (this._playwrightConnection?.readyState === import_utilsBundle.ws.OPEN)
- this._playwrightConnection.close(1e3, reason);
- this._playwrightConnection = null;
- }
- _handleExtensionConnection(ws2) {
- if (this._extensionConnection) {
- ws2.close(1e3, "Another extension connection already established");
- return;
- }
- this._extensionConnection = new ExtensionConnection(ws2);
- this._extensionConnection.onclose = (c, reason) => {
- debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
- if (this._extensionConnection !== c)
- return;
- this._resetExtensionConnection();
- this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
- };
- this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
- this._extensionConnectionPromise.resolve();
- }
- _handleExtensionMessage(method, params) {
- switch (method) {
- case "forwardCDPEvent":
- const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
- this._sendToPlaywright({
- sessionId,
- method: params.method,
- params: params.params
- });
- break;
- }
- }
- async _handlePlaywrightMessage(message) {
- debugLogger("\u2190 Playwright:", `${message.method} (id=${message.id})`);
- const { id, sessionId, method, params } = message;
- try {
- const result = await this._handleCDPCommand(method, params, sessionId);
- this._sendToPlaywright({ id, sessionId, result });
- } catch (e) {
- debugLogger("Error in the extension:", e);
- this._sendToPlaywright({
- id,
- sessionId,
- error: { message: e.message }
- });
- }
- }
- async _handleCDPCommand(method, params, sessionId) {
- switch (method) {
- case "Browser.getVersion": {
- return {
- protocolVersion: "1.3",
- product: "Chrome/Extension-Bridge",
- userAgent: "CDP-Bridge-Server/1.0.0"
- };
- }
- case "Browser.setDownloadBehavior": {
- return {};
- }
- case "Target.setAutoAttach": {
- if (sessionId)
- break;
- const { targetInfo } = await this._extensionConnection.send("attachToTab", {});
- this._connectedTabInfo = {
- targetInfo,
- sessionId: `pw-tab-${this._nextSessionId++}`
- };
- debugLogger("Simulating auto-attach");
- this._sendToPlaywright({
- method: "Target.attachedToTarget",
- params: {
- sessionId: this._connectedTabInfo.sessionId,
- targetInfo: {
- ...this._connectedTabInfo.targetInfo,
- attached: true
- },
- waitingForDebugger: false
- }
- });
- return {};
- }
- case "Target.getTargetInfo": {
- return this._connectedTabInfo?.targetInfo;
- }
- }
- return await this._forwardToExtension(method, params, sessionId);
- }
- async _forwardToExtension(method, params, sessionId) {
- if (!this._extensionConnection)
- throw new Error("Extension not connected");
- if (this._connectedTabInfo?.sessionId === sessionId)
- sessionId = void 0;
- return await this._extensionConnection.send("forwardCDPCommand", { sessionId, method, params });
- }
- _sendToPlaywright(message) {
- debugLogger("\u2192 Playwright:", `${message.method ?? `response(id=${message.id})`}`);
- this._playwrightConnection?.send(JSON.stringify(message));
- }
- }
- class ExtensionConnection {
- constructor(ws2) {
- this._callbacks = /* @__PURE__ */ new Map();
- this._lastId = 0;
- this._ws = ws2;
- this._ws.on("message", this._onMessage.bind(this));
- this._ws.on("close", this._onClose.bind(this));
- this._ws.on("error", this._onError.bind(this));
- }
- async send(method, params) {
- if (this._ws.readyState !== import_utilsBundle.ws.OPEN)
- throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
- const id = ++this._lastId;
- this._ws.send(JSON.stringify({ id, method, params }));
- const error = new Error(`Protocol error: ${method}`);
- return new Promise((resolve, reject) => {
- this._callbacks.set(id, { resolve, reject, error });
- });
- }
- close(message) {
- debugLogger("closing extension connection:", message);
- if (this._ws.readyState === import_utilsBundle.ws.OPEN)
- this._ws.close(1e3, message);
- }
- _onMessage(event) {
- const eventData = event.toString();
- let parsedJson;
- try {
- parsedJson = JSON.parse(eventData);
- } catch (e) {
- debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
- this._ws.close();
- return;
- }
- try {
- this._handleParsedMessage(parsedJson);
- } catch (e) {
- debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
- this._ws.close();
- }
- }
- _handleParsedMessage(object) {
- if (object.id && this._callbacks.has(object.id)) {
- const callback = this._callbacks.get(object.id);
- this._callbacks.delete(object.id);
- if (object.error) {
- const error = callback.error;
- error.message = object.error;
- callback.reject(error);
- } else {
- callback.resolve(object.result);
- }
- } else if (object.id) {
- debugLogger("\u2190 Extension: unexpected response", object);
- } else {
- this.onmessage?.(object.method, object.params);
- }
- }
- _onClose(event) {
- debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
- this._dispose();
- this.onclose?.(this, event.reason);
- }
- _onError(event) {
- debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
- this._dispose();
- }
- _dispose() {
- for (const callback of this._callbacks.values())
- callback.reject(new Error("WebSocket closed"));
- this._callbacks.clear();
- }
- }
- // Annotate the CommonJS export names for ESM import in node:
- 0 && (module.exports = {
- CDPRelayServer
- });
|