mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +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');
|
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)
|
if (browserConfig.remoteEndpoint)
|
||||||
return new RemoteContextFactory(browserConfig);
|
return new RemoteContextFactory(browserConfig);
|
||||||
if (browserConfig.cdpEndpoint || forceCdp)
|
if (browserConfig.cdpEndpoint)
|
||||||
return new CdpContextFactory(browserConfig);
|
return new CdpContextFactory(browserConfig);
|
||||||
if (browserConfig.isolated)
|
if (browserConfig.isolated)
|
||||||
return new IsolatedContextFactory(browserConfig);
|
return new IsolatedContextFactory(browserConfig);
|
||||||
|
@ -28,10 +28,14 @@ import { WebSocket, WebSocketServer } from 'ws';
|
|||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import debug from 'debug';
|
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 _wss: WebSocketServer;
|
||||||
private _playwrightSocket: WebSocket | null = null;
|
private _playwrightSocket: WebSocket | null = null;
|
||||||
private _extensionSocket: WebSocket | null = null;
|
private _extensionSocket: WebSocket | null = null;
|
||||||
@ -40,9 +44,6 @@ export class CDPBridgeServer extends EventEmitter {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
public static readonly CDP_PATH = '/cdp';
|
|
||||||
public static readonly EXTENSION_PATH = '/extension';
|
|
||||||
|
|
||||||
constructor(server: http.Server) {
|
constructor(server: http.Server) {
|
||||||
super();
|
super();
|
||||||
this._wss = new WebSocketServer({ server });
|
this._wss = new WebSocketServer({ server });
|
||||||
@ -59,9 +60,9 @@ export class CDPBridgeServer extends EventEmitter {
|
|||||||
|
|
||||||
debugLogger(`New connection to ${url.pathname}`);
|
debugLogger(`New connection to ${url.pathname}`);
|
||||||
|
|
||||||
if (url.pathname === CDPBridgeServer.CDP_PATH) {
|
if (url.pathname === CDP_PATH) {
|
||||||
this._handlePlaywrightConnection(ws);
|
this._handlePlaywrightConnection(ws);
|
||||||
} else if (url.pathname === CDPBridgeServer.EXTENSION_PATH) {
|
} else if (url.pathname === EXTENSION_PATH) {
|
||||||
this._handleExtensionConnection(ws);
|
this._handleExtensionConnection(ws);
|
||||||
} else {
|
} else {
|
||||||
debugLogger(`Invalid path: ${url.pathname}`);
|
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
|
// CLI usage
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
const port = parseInt(process.argv[2], 10) || 9223;
|
const port = parseInt(process.argv[2], 10) || 9223;
|
||||||
const httpServer = http.createServer();
|
const httpServer = http.createServer();
|
||||||
await new Promise<void>(resolve => httpServer.listen(port, resolve));
|
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(`CDP Bridge Server listening on ws://localhost:${port}`);
|
||||||
console.error(`- Playwright MCP: ws://localhost:${port}${CDPBridgeServer.CDP_PATH}`);
|
console.error(`- Playwright MCP: ws://localhost:${port}${CDP_PATH}`);
|
||||||
console.error(`- Extension: ws://localhost:${port}${CDPBridgeServer.EXTENSION_PATH}`);
|
console.error(`- Extension: ws://localhost:${port}${EXTENSION_PATH}`);
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
debugLogger('\nShutting down bridge server...');
|
debugLogger('\nShutting down bridge server...');
|
@ -14,16 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type http from 'http';
|
|
||||||
import { Option, program } from 'commander';
|
import { Option, program } from 'commander';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { startTraceViewerServer } from 'playwright-core/lib/server';
|
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 { resolveCLIConfig } from './config.js';
|
||||||
import { Server } from './server.js';
|
import { Server } from './server.js';
|
||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './package.js';
|
||||||
import { CDPBridgeServer } from './cdp-relay.js';
|
import { startCDPRelayServer } from './cdpRelay.js';
|
||||||
|
|
||||||
program
|
program
|
||||||
.version('Version ' + packageJSON.version)
|
.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())
|
.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 => {
|
.action(async options => {
|
||||||
const config = await resolveCLIConfig(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();
|
server.setupExitWatchdog();
|
||||||
|
|
||||||
let httpServer: http.Server | undefined = undefined;
|
if (httpServer)
|
||||||
if (config.server.port !== undefined)
|
await startHttpTransport(httpServer, server);
|
||||||
httpServer = await startHttpTransport(server);
|
|
||||||
else
|
else
|
||||||
await startStdioTransport(server);
|
await startStdioTransport(server);
|
||||||
|
|
||||||
@ -73,14 +79,6 @@ program
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('\nTrace viewer listening on ' + url);
|
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[] {
|
function semicolonSeparatedList(value: string): string[] {
|
||||||
|
@ -28,10 +28,10 @@ export class Server {
|
|||||||
private _browserConfig: FullConfig['browser'];
|
private _browserConfig: FullConfig['browser'];
|
||||||
private _contextFactory: BrowserContextFactory;
|
private _contextFactory: BrowserContextFactory;
|
||||||
|
|
||||||
constructor(config: FullConfig, { forceCdp }: { forceCdp: boolean }) {
|
constructor(config: FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this._browserConfig = config.browser;
|
this._browserConfig = config.browser;
|
||||||
this._contextFactory = contextFactory(this._browserConfig, { forceCdp });
|
this._contextFactory = contextFactory(this._browserConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createConnection(transport: Transport): Promise<Connection> {
|
async createConnection(transport: Transport): Promise<Connection> {
|
||||||
|
@ -97,18 +97,29 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res:
|
|||||||
res.end('Invalid request');
|
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 sseSessions = new Map<string, SSEServerTransport>();
|
||||||
const streamableSessions = new Map<string, StreamableHTTPServerTransport>();
|
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}`);
|
const url = new URL(`http://localhost${req.url}`);
|
||||||
if (url.pathname.startsWith('/mcp'))
|
if (url.pathname.startsWith('/mcp'))
|
||||||
await handleStreamable(server, req, res, streamableSessions);
|
await handleStreamable(mcpServer, req, res, streamableSessions);
|
||||||
else
|
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 url = httpAddressToString(httpServer.address());
|
||||||
const message = [
|
const message = [
|
||||||
`Listening on ${url}`,
|
`Listening on ${url}`,
|
||||||
@ -124,7 +135,6 @@ export async function startHttpTransport(server: Server): Promise<http.Server> {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message);
|
console.error(message);
|
||||||
return httpServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function httpAddressToString(address: string | AddressInfo | null): string {
|
export function httpAddressToString(address: string | AddressInfo | null): string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user