mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-25 07:52:27 +08:00
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:
parent
6c3f3b6576
commit
96e234012d
@ -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);
|
||||
|
@ -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...');
|
@ -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[] {
|
||||
|
@ -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> {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user