diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index ecc835d..4d39562 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -42,27 +42,23 @@ export function contextFactory(config: FullConfig): BrowserContextFactory { export type ClientInfo = { name?: string, version?: string, rootPath?: string }; export interface BrowserContextFactory { - readonly name: string; - readonly description: string; createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; } class BaseContextFactory implements BrowserContextFactory { - readonly name: string; - readonly description: string; readonly config: FullConfig; + private _logName: string; protected _browserPromise: Promise | undefined; - constructor(name: string, description: string, config: FullConfig) { - this.name = name; - this.description = description; + constructor(name: string, config: FullConfig) { + this._logName = name; this.config = config; } protected async _obtainBrowser(clientInfo: ClientInfo): Promise { if (this._browserPromise) return this._browserPromise; - testDebug(`obtain browser (${this.name})`); + testDebug(`obtain browser (${this._logName})`); this._browserPromise = this._doObtainBrowser(clientInfo); void this._browserPromise.then(browser => { browser.on('disconnected', () => { @@ -79,7 +75,7 @@ class BaseContextFactory implements BrowserContextFactory { } async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - testDebug(`create browser context (${this.name})`); + testDebug(`create browser context (${this._logName})`); const browser = await this._obtainBrowser(clientInfo); const browserContext = await this._doCreateContext(browser); return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; @@ -90,12 +86,12 @@ class BaseContextFactory implements BrowserContextFactory { } private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) { - testDebug(`close browser context (${this.name})`); + testDebug(`close browser context (${this._logName})`); if (browser.contexts().length === 1) this._browserPromise = undefined; await browserContext.close().catch(logUnhandledError); if (browser.contexts().length === 0) { - testDebug(`close browser (${this.name})`); + testDebug(`close browser (${this._logName})`); await browser.close().catch(logUnhandledError); } } @@ -103,7 +99,7 @@ class BaseContextFactory implements BrowserContextFactory { class IsolatedContextFactory extends BaseContextFactory { constructor(config: FullConfig) { - super('isolated', 'Create a new isolated browser context', config); + super('isolated', config); } protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { @@ -128,7 +124,7 @@ class IsolatedContextFactory extends BaseContextFactory { class CdpContextFactory extends BaseContextFactory { constructor(config: FullConfig) { - super('cdp', 'Connect to a browser over CDP', config); + super('cdp', config); } protected override async _doObtainBrowser(): Promise { @@ -142,7 +138,7 @@ class CdpContextFactory extends BaseContextFactory { class RemoteContextFactory extends BaseContextFactory { constructor(config: FullConfig) { - super('remote', 'Connect to a browser using a remote endpoint', config); + super('remote', config); } protected override async _doObtainBrowser(): Promise { diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 1170b31..5286e03 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -21,7 +21,6 @@ import { logUnhandledError } from './utils/log.js'; import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; -import { packageJSON } from './utils/package.js'; import { toMcpTool } from './mcp/tool.js'; import type { Tool } from './tools/tool.js'; @@ -30,9 +29,6 @@ import type * as mcpServer from './mcp/server.js'; import type { ServerBackend } from './mcp/server.js'; export class BrowserServerBackend implements ServerBackend { - name = 'Playwright'; - version = packageJSON.version; - private _tools: Tool[]; private _context: Context | undefined; private _sessionLog: SessionLog | undefined; diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts index c042b0f..2c4287b 100644 --- a/src/extension/cdpRelay.ts +++ b/src/extension/cdpRelay.ts @@ -26,7 +26,7 @@ import { spawn } from 'child_process'; import http from 'http'; import debug from 'debug'; import { WebSocket, WebSocketServer } from 'ws'; -import { httpAddressToString } from '../utils/httpServer.js'; +import { httpAddressToString } from '../mcp/http.js'; import { logUnhandledError } from '../utils/log.js'; import { ManualPromise } from '../utils/manualPromise.js'; import type websocket from 'ws'; diff --git a/src/extension/extensionContextFactory.ts b/src/extension/extensionContextFactory.ts index 7bdfaa0..0346419 100644 --- a/src/extension/extensionContextFactory.ts +++ b/src/extension/extensionContextFactory.ts @@ -16,7 +16,7 @@ import debug from 'debug'; import * as playwright from 'playwright'; -import { startHttpServer } from '../utils/httpServer.js'; +import { startHttpServer } from '../mcp/http.js'; import { CDPRelayServer } from './cdpRelay.js'; import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js'; @@ -24,9 +24,6 @@ import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory const debugLogger = debug('pw:mcp:relay'); export class ExtensionContextFactory implements BrowserContextFactory { - name = 'extension'; - description = 'Connect to a browser using the Playwright MCP extension'; - private _browserChannel: string; private _userDataDir?: string; diff --git a/src/index.ts b/src/index.ts index 86a9280..6809cd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { BrowserServerBackend } from './browserServerBackend.js'; import { resolveConfig } from './config.js'; import { contextFactory } from './browserContextFactory.js'; import * as mcpServer from './mcp/server.js'; +import { packageJSON } from './utils/package.js'; import type { Config } from '../config.js'; import type { BrowserContext } from 'playwright'; @@ -27,7 +28,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { const config = await resolveConfig(userConfig); const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config); - return mcpServer.createServer(new BrowserServerBackend(config, factory), false); + return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false); } class SimpleBrowserContextFactory implements BrowserContextFactory { diff --git a/src/loopTools/context.ts b/src/loopTools/context.ts index 777ba61..dadb6b9 100644 --- a/src/loopTools/context.ts +++ b/src/loopTools/context.ts @@ -23,6 +23,7 @@ import { OpenAIDelegate } from '../loop/loopOpenAI.js'; import { ClaudeDelegate } from '../loop/loopClaude.js'; import { InProcessTransport } from '../mcp/inProcessTransport.js'; import * as mcpServer from '../mcp/server.js'; +import { packageJSON } from '../utils/package.js'; import type { LLMDelegate } from '../loop/loop.js'; import type { FullConfig } from '../config.js'; @@ -44,9 +45,9 @@ export class Context { } static async create(config: FullConfig) { - const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' }); + const client = new Client({ name: 'Playwright Proxy', version: packageJSON.version }); const browserContextFactory = contextFactory(config); - const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false); + const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false); await client.connect(new InProcessTransport(server)); await client.ping(); return new Context(config, client); diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index a8ea803..2d017ba 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -17,7 +17,6 @@ import dotenv from 'dotenv'; import * as mcpServer from '../mcp/server.js'; -import * as mcpTransport from '../mcp/transport.js'; import { packageJSON } from '../utils/package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; @@ -30,13 +29,16 @@ import type { Tool } from './tool.js'; export async function runLoopTools(config: FullConfig) { dotenv.config(); - const serverBackendFactory = () => new LoopToolsServerBackend(config); - await mcpTransport.start(serverBackendFactory, config.server); + const serverBackendFactory = { + name: 'Playwright', + nameInConfig: 'playwright-loop', + version: packageJSON.version, + create: () => new LoopToolsServerBackend(config) + }; + await mcpServer.start(serverBackendFactory, config.server); } class LoopToolsServerBackend implements ServerBackend { - readonly name = 'Playwright'; - readonly version = packageJSON.version; private _config: FullConfig; private _context: Context | undefined; private _tools: Tool[] = [perform, snapshot]; diff --git a/src/mcp/DEPS.list b/src/mcp/DEPS.list index 5870e2d..e43dcb5 100644 --- a/src/mcp/DEPS.list +++ b/src/mcp/DEPS.list @@ -1,2 +1 @@ [*] -../utils/ diff --git a/src/mcp/README.md b/src/mcp/README.md index 64edb62..b8b280e 100644 --- a/src/mcp/README.md +++ b/src/mcp/README.md @@ -1 +1 @@ -- Generic MCP utils, no dependencies on Playwright here. +- Generic MCP utils, no dependencies on anything. diff --git a/src/mcp/transport.ts b/src/mcp/http.ts similarity index 75% rename from src/mcp/transport.ts rename to src/mcp/http.ts index 06965b7..6890ddc 100644 --- a/src/mcp/transport.ts +++ b/src/mcp/http.ts @@ -14,33 +14,61 @@ * limitations under the License. */ +import assert from 'assert'; +import net from 'net'; import http from 'http'; import crypto from 'crypto'; + import debug from 'debug'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { httpAddressToString, startHttpServer } from '../utils/httpServer.js'; import * as mcpServer from './server.js'; import type { ServerBackendFactory } from './server.js'; -export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { - if (options.port !== undefined) { - const httpServer = await startHttpServer(options); - startHttpTransport(httpServer, serverBackendFactory); - } else { - await startStdioTransport(serverBackendFactory); - } -} - -async function startStdioTransport(serverBackendFactory: ServerBackendFactory) { - await mcpServer.connect(serverBackendFactory, new StdioServerTransport(), false); -} - const testDebug = debug('pw:mcp:test'); +export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise { + const { host, port } = config; + const httpServer = http.createServer(); + await new Promise((resolve, reject) => { + httpServer.on('error', reject); + abortSignal?.addEventListener('abort', () => { + httpServer.close(); + reject(new Error('Aborted')); + }); + httpServer.listen(port, host, () => { + resolve(); + httpServer.removeListener('error', reject); + }); + }); + return httpServer; +} + +export function httpAddressToString(address: string | net.AddressInfo | null): string { + assert(address, 'Could not bind server socket'); + if (typeof address === 'string') + return address; + const resolvedPort = address.port; + let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; + if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') + resolvedHost = 'localhost'; + return `http://${resolvedHost}:${resolvedPort}`; +} + +export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { + const sseSessions = new Map(); + const streamableSessions = new Map(); + httpServer.on('request', async (req, res) => { + const url = new URL(`http://localhost${req.url}`); + if (url.pathname.startsWith('/sse')) + await handleSSE(serverBackendFactory, req, res, url, sseSessions); + else + await handleStreamable(serverBackendFactory, req, res, streamableSessions); + }); +} + async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map) { if (req.method === 'POST') { const sessionId = url.searchParams.get('sessionId'); @@ -108,30 +136,3 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: res.statusCode = 400; res.end('Invalid request'); } - -function startHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { - const sseSessions = new Map(); - const streamableSessions = new Map(); - httpServer.on('request', async (req, res) => { - const url = new URL(`http://localhost${req.url}`); - if (url.pathname.startsWith('/sse')) - await handleSSE(serverBackendFactory, req, res, url, sseSessions); - else - await handleStreamable(serverBackendFactory, req, res, streamableSessions); - }); - const url = httpAddressToString(httpServer.address()); - const message = [ - `Listening on ${url}`, - 'Put this in your client config:', - JSON.stringify({ - 'mcpServers': { - 'playwright': { - 'url': `${url}/mcp` - } - } - }, undefined, 2), - 'For legacy SSE transport support, you can use the /sse endpoint instead.', - ].join('\n'); - // eslint-disable-next-line no-console - console.error(message); -} diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index c639fd5..da186c4 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -14,14 +14,12 @@ * limitations under the License. */ +import debug from 'debug'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { logUnhandledError } from '../utils/log.js'; -import { packageJSON } from '../utils/package.js'; - import type { ServerBackend, ClientVersion, Root } from './server.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; @@ -33,10 +31,9 @@ export type MCPProvider = { connect(): Promise; }; -export class ProxyBackend implements ServerBackend { - name = 'Playwright MCP Client Switcher'; - version = packageJSON.version; +const errorsDebug = debug('pw:mcp:errors'); +export class ProxyBackend implements ServerBackend { private _mcpProviders: MCPProvider[]; private _currentClient: Client | undefined; private _contextSwitchTool: Tool; @@ -72,7 +69,7 @@ export class ProxyBackend implements ServerBackend { } serverClosed?(): void { - void this._currentClient?.close().catch(logUnhandledError); + void this._currentClient?.close().catch(errorsDebug); } private async _callContextSwitchTool(params: any): Promise { @@ -115,7 +112,7 @@ export class ProxyBackend implements ServerBackend { await this._currentClient?.close(); this._currentClient = undefined; - const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version }); + const client = new Client({ name: 'Playwright MCP Proxy', version: '0.0.0' }); client.registerCapabilities({ roots: { listRoots: true, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 80c1461..e9b4944 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -15,10 +15,12 @@ */ import debug from 'debug'; + import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { ManualPromise } from '../utils/manualPromise.js'; -import { logUnhandledError } from '../utils/log.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { httpAddressToString, installHttpTransport, startHttpServer } from './http.js'; +import { InProcessTransport } from './inProcessTransport.js'; import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; @@ -26,28 +28,37 @@ export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; const serverDebug = debug('pw:mcp:server'); +const errorsDebug = debug('pw:mcp:errors'); export type ClientVersion = { name: string, version: string }; export interface ServerBackend { - name: string; - version: string; initialize?(clientVersion: ClientVersion, roots: Root[]): Promise; listTools(): Promise; callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; serverClosed?(): void; } -export type ServerBackendFactory = () => ServerBackend; +export type ServerBackendFactory = { + name: string; + nameInConfig: string; + version: string; + create: () => ServerBackend; +}; -export async function connect(serverBackendFactory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) { - const backend = serverBackendFactory(); - const server = createServer(backend, runHeartbeat); +export async function connect(factory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) { + const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat); await server.connect(transport); } -export function createServer(backend: ServerBackend, runHeartbeat: boolean): Server { - const initializedPromise = new ManualPromise(); - const server = new Server({ name: backend.name, version: backend.version }, { +export async function wrapInProcess(backend: ServerBackend): Promise { + const server = createServer('Internal', '0.0.0', backend, false); + return new InProcessTransport(server); +} + +export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server { + let initializedPromiseResolve = () => {}; + const initializedPromise = new Promise(resolve => initializedPromiseResolve = resolve); + const server = new Server({ name, version }, { capabilities: { tools: {}, } @@ -89,9 +100,9 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser } const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }; await backend.initialize?.(clientVersion, clientRoots); - initializedPromise.resolve(); + initializedPromiseResolve(); } catch (e) { - logUnhandledError(e); + errorsDebug(e); } }); addServerListener(server, 'close', () => backend.serverClosed?.()); @@ -120,3 +131,27 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste listener(); }; } + +export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { + if (options.port === undefined) { + await connect(serverBackendFactory, new StdioServerTransport(), false); + return; + } + + const httpServer = await startHttpServer(options); + await installHttpTransport(httpServer, serverBackendFactory); + const url = httpAddressToString(httpServer.address()); + + const mcpConfig: any = { mcpServers: { } }; + mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = { + url: `${url}/mcp` + }; + const message = [ + `Listening on ${url}`, + 'Put this in your client config:', + JSON.stringify(mcpConfig, undefined, 2), + 'For legacy SSE transport support, you can use the /sse endpoint instead.', + ].join('\n'); + // eslint-disable-next-line no-console + console.error(message); +} diff --git a/src/program.ts b/src/program.ts index bc313c2..210ae10 100644 --- a/src/program.ts +++ b/src/program.ts @@ -16,7 +16,6 @@ import { program, Option } from 'commander'; import * as mcpServer from './mcp/server.js'; -import * as mcpTransport from './mcp/transport.js'; import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js'; import { packageJSON } from './utils/package.js'; import { Context } from './context.js'; @@ -25,11 +24,8 @@ import { runLoopTools } from './loopTools/main.js'; import { ProxyBackend } from './mcp/proxyBackend.js'; import { BrowserServerBackend } from './browserServerBackend.js'; import { ExtensionContextFactory } from './extension/extensionContextFactory.js'; -import { InProcessTransport } from './mcp/inProcessTransport.js'; import type { MCPProvider } from './mcp/proxyBackend.js'; -import type { FullConfig } from './config.js'; -import type { BrowserContextFactory } from './browserContextFactory.js'; program .version('Version ' + packageJSON.version) @@ -71,12 +67,19 @@ program console.error('The --vision option is deprecated, use --caps=vision instead'); options.caps = 'vision'; } + const config = await resolveCLIConfig(options); + const browserContextFactory = contextFactory(config); + const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir); if (options.extension) { - const contextFactory = createExtensionContextFactory(config); - const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory); - await mcpTransport.start(serverBackendFactory, config.server); + const serverBackendFactory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ extension', + nameInConfig: 'playwright-extension', + version: packageJSON.version, + create: () => new BrowserServerBackend(config, extensionContextFactory) + }; + await mcpServer.start(serverBackendFactory, config.server); return; } @@ -85,11 +88,36 @@ program return; } - const browserContextFactory = contextFactory(config); - const providers: MCPProvider[] = [mcpProviderForBrowserContextFactory(config, browserContextFactory)]; - if (options.connectTool) - providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config))); - await mcpTransport.start(() => new ProxyBackend(providers), config.server); + if (options.connectTool) { + const providers: MCPProvider[] = [ + { + name: 'default', + description: 'Starts standalone browser', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)), + }, + { + name: 'extension', + description: 'Connect to a browser using the Playwright MCP extension', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)), + }, + ]; + const factory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ switch', + nameInConfig: 'playwright-switch', + version: packageJSON.version, + create: () => new ProxyBackend(providers), + }; + await mcpServer.start(factory, config.server); + return; + } + + const factory: mcpServer.ServerBackendFactory = { + name: 'Playwright', + nameInConfig: 'playwright', + version: packageJSON.version, + create: () => new BrowserServerBackend(config, browserContextFactory) + }; + await mcpServer.start(factory, config.server); }); function setupExitWatchdog() { @@ -108,19 +136,4 @@ function setupExitWatchdog() { process.on('SIGTERM', handleExit); } -function createExtensionContextFactory(config: FullConfig) { - return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir); -} - -function mcpProviderForBrowserContextFactory(config: FullConfig, browserContextFactory: BrowserContextFactory) { - return { - name: browserContextFactory.name, - description: browserContextFactory.description, - connect: async () => { - const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false); - return new InProcessTransport(server); - }, - }; -} - void program.parseAsync(process.argv); diff --git a/src/utils/httpServer.ts b/src/utils/httpServer.ts deleted file mode 100644 index 3102bd5..0000000 --- a/src/utils/httpServer.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import assert from 'assert'; -import http from 'http'; - -import type * as net from 'net'; - -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 httpAddressToString(address: string | net.AddressInfo | null): string { - assert(address, 'Could not bind server socket'); - if (typeof address === 'string') - return address; - const resolvedPort = address.port; - let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; - if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') - resolvedHost = 'localhost'; - return `http://${resolvedHost}:${resolvedPort}`; -}