chore(extension): start relay before creating MCP server (#548)

* HTTPS server launched and the relay server is created before MCP
server. This way we can pass CDP endpoint to its constructor.
* MCP HTTP transport is added to precreated HTTP server.
* A bunch of renames to fix style issues.
This commit is contained in:
Yury Semikhatsky 2025-06-13 16:13:40 -07:00 committed by GitHub
parent 6c3f3b6576
commit 96e234012d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 55 additions and 36 deletions

View File

@ -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);

View File

@ -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<void>(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...');

View File

@ -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[] {

View File

@ -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<Connection> {

View File

@ -97,18 +97,29 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res:
res.end('Invalid request');
}
export async function startHttpTransport(server: Server): Promise<http.Server> {
export async function startHttpServer(config: { host?: string, port?: number }): Promise<http.Server> {
const { host, port } = config;
const httpServer = http.createServer();
await new Promise<void>((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<string, SSEServerTransport>();
const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
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<void>(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<http.Server> {
].join('\n');
// eslint-disable-next-line no-console
console.error(message);
return httpServer;
}
export function httpAddressToString(address: string | AddressInfo | null): string {