diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index ba62cab..f14cd7d 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -28,10 +28,10 @@ import type { BrowserInfo, LaunchBrowserRequest } from './browserServer.js'; const testDebug = debug('pw:mcp:test'); -export function contextFactory(browserConfig: FullConfig['browser'], { forceCdp }: { forceCdp?: boolean } = {}): BrowserContextFactory { +export function contextFactory(browserConfig: FullConfig['browser']): BrowserContextFactory { if (browserConfig.remoteEndpoint) return new RemoteContextFactory(browserConfig); - if (browserConfig.cdpEndpoint || forceCdp) + if (browserConfig.cdpEndpoint) return new CdpContextFactory(browserConfig); if (browserConfig.isolated) return new IsolatedContextFactory(browserConfig); diff --git a/src/cdp-relay.ts b/src/cdpRelay.ts similarity index 89% rename from src/cdp-relay.ts rename to src/cdpRelay.ts index 13e8860..0c8c3cb 100644 --- a/src/cdp-relay.ts +++ b/src/cdpRelay.ts @@ -28,10 +28,14 @@ import { WebSocket, WebSocketServer } from 'ws'; import http from 'node:http'; import { EventEmitter } from 'node:events'; import debug from 'debug'; +import { httpAddressToString } from './transport.js'; -const debugLogger = debug('pw-mcp:cdp-relay'); +const debugLogger = debug('pw:mcp:relay'); -export class CDPBridgeServer extends EventEmitter { +const CDP_PATH = '/cdp'; +const EXTENSION_PATH = '/extension'; + +export class CDPRelayServer extends EventEmitter { private _wss: WebSocketServer; private _playwrightSocket: WebSocket | null = null; private _extensionSocket: WebSocket | null = null; @@ -40,9 +44,6 @@ export class CDPBridgeServer extends EventEmitter { sessionId: string; } | undefined; - public static readonly CDP_PATH = '/cdp'; - public static readonly EXTENSION_PATH = '/extension'; - constructor(server: http.Server) { super(); this._wss = new WebSocketServer({ server }); @@ -59,9 +60,9 @@ export class CDPBridgeServer extends EventEmitter { debugLogger(`New connection to ${url.pathname}`); - if (url.pathname === CDPBridgeServer.CDP_PATH) { + if (url.pathname === CDP_PATH) { this._handlePlaywrightConnection(ws); - } else if (url.pathname === CDPBridgeServer.EXTENSION_PATH) { + } else if (url.pathname === EXTENSION_PATH) { this._handleExtensionConnection(ws); } else { debugLogger(`Invalid path: ${url.pathname}`); @@ -287,16 +288,26 @@ export class CDPBridgeServer extends EventEmitter { } } +export async function startCDPRelayServer(httpServer: http.Server) { + const wsAddress = httpAddressToString(httpServer.address()).replace(/^http/, 'ws'); + const cdpRelayServer = new CDPRelayServer(httpServer); + process.on('exit', () => cdpRelayServer.stop()); + // eslint-disable-next-line no-console + console.error(`CDP relay server started on ${wsAddress}${EXTENSION_PATH} - Connect to it using the browser extension.`); + const cdpEndpoint = `${wsAddress}${CDP_PATH}`; + return cdpEndpoint; +} + // CLI usage if (import.meta.url === `file://${process.argv[1]}`) { const port = parseInt(process.argv[2], 10) || 9223; const httpServer = http.createServer(); await new Promise(resolve => httpServer.listen(port, resolve)); - const server = new CDPBridgeServer(httpServer); + const server = new CDPRelayServer(httpServer); console.error(`CDP Bridge Server listening on ws://localhost:${port}`); - console.error(`- Playwright MCP: ws://localhost:${port}${CDPBridgeServer.CDP_PATH}`); - console.error(`- Extension: ws://localhost:${port}${CDPBridgeServer.EXTENSION_PATH}`); + console.error(`- Playwright MCP: ws://localhost:${port}${CDP_PATH}`); + console.error(`- Extension: ws://localhost:${port}${EXTENSION_PATH}`); process.on('SIGINT', () => { debugLogger('\nShutting down bridge server...'); diff --git a/src/program.ts b/src/program.ts index 8bcc9b3..5a381d3 100644 --- a/src/program.ts +++ b/src/program.ts @@ -14,16 +14,15 @@ * limitations under the License. */ -import type http from 'http'; import { Option, program } from 'commander'; // @ts-ignore import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { httpAddressToString, startHttpTransport, startStdioTransport } from './transport.js'; +import { startHttpServer, startHttpTransport, startStdioTransport } from './transport.js'; import { resolveCLIConfig } from './config.js'; import { Server } from './server.js'; import { packageJSON } from './package.js'; -import { CDPBridgeServer } from './cdp-relay.js'; +import { startCDPRelayServer } from './cdpRelay.js'; program .version('Version ' + packageJSON.version) @@ -57,12 +56,19 @@ program .addOption(new Option('--extension', 'Allow connecting to a running browser instance (Edge/Chrome only). Requires the \'Playwright MCP\' browser extension to be installed.').hideHelp()) .action(async options => { const config = await resolveCLIConfig(options); - const server = new Server(config, { forceCdp: !!config.extension }); + const httpServer = config.server.port !== undefined ? await startHttpServer(config.server) : undefined; + if (config.extension) { + if (!httpServer) + throw new Error('--port parameter is required for extension mode'); + // Point CDP endpoint to the relay server. + config.browser.cdpEndpoint = await startCDPRelayServer(httpServer); + } + + const server = new Server(config); server.setupExitWatchdog(); - let httpServer: http.Server | undefined = undefined; - if (config.server.port !== undefined) - httpServer = await startHttpTransport(server); + if (httpServer) + await startHttpTransport(httpServer, server); else await startStdioTransport(server); @@ -73,14 +79,6 @@ program // eslint-disable-next-line no-console console.error('\nTrace viewer listening on ' + url); } - if (config.extension && httpServer) { - const wsAddress = httpAddressToString(httpServer.address()).replace(/^http/, 'ws'); - config.browser.cdpEndpoint = `${wsAddress}${CDPBridgeServer.CDP_PATH}`; - const cdpRelayServer = new CDPBridgeServer(httpServer); - process.on('exit', () => cdpRelayServer.stop()); - // eslint-disable-next-line no-console - console.error(`CDP relay server started on ${wsAddress}${CDPBridgeServer.EXTENSION_PATH} - Connect to it using the browser extension.`); - } }); function semicolonSeparatedList(value: string): string[] { diff --git a/src/server.ts b/src/server.ts index 14b33dd..8c143e1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -28,10 +28,10 @@ export class Server { private _browserConfig: FullConfig['browser']; private _contextFactory: BrowserContextFactory; - constructor(config: FullConfig, { forceCdp }: { forceCdp: boolean }) { + constructor(config: FullConfig) { this.config = config; this._browserConfig = config.browser; - this._contextFactory = contextFactory(this._browserConfig, { forceCdp }); + this._contextFactory = contextFactory(this._browserConfig); } async createConnection(transport: Transport): Promise { diff --git a/src/transport.ts b/src/transport.ts index ac9898c..2342fe9 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -97,18 +97,29 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res: res.end('Invalid request'); } -export async function startHttpTransport(server: Server): Promise { +export async function startHttpServer(config: { host?: string, port?: number }): Promise { + const { host, port } = config; + const httpServer = http.createServer(); + await new Promise((resolve, reject) => { + httpServer.on('error', reject); + httpServer.listen(port, host, () => { + resolve(); + httpServer.removeListener('error', reject); + }); + }); + return httpServer; +} + +export function startHttpTransport(httpServer: http.Server, mcpServer: Server) { const sseSessions = new Map(); const streamableSessions = new Map(); - const httpServer = http.createServer(async (req, res) => { + httpServer.on('request', async (req, res) => { const url = new URL(`http://localhost${req.url}`); if (url.pathname.startsWith('/mcp')) - await handleStreamable(server, req, res, streamableSessions); + await handleStreamable(mcpServer, req, res, streamableSessions); else - await handleSSE(server, req, res, url, sseSessions); + await handleSSE(mcpServer, req, res, url, sseSessions); }); - const { host, port } = server.config.server; - await new Promise(resolve => httpServer.listen(port, host, resolve)); const url = httpAddressToString(httpServer.address()); const message = [ `Listening on ${url}`, @@ -124,7 +135,6 @@ export async function startHttpTransport(server: Server): Promise { ].join('\n'); // eslint-disable-next-line no-console console.error(message); - return httpServer; } export function httpAddressToString(address: string | AddressInfo | null): string {