mirror of
				https://github.com/microsoft/playwright-mcp.git
				synced 2025-10-12 00:25:14 +03:00 
			
		
		
		
	chore: do not wrap mcp in proxy by default, drive-by deps fix (#909)
This commit is contained in:
		| @@ -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<void> }>; | ||||
| } | ||||
|  | ||||
| class BaseContextFactory implements BrowserContextFactory { | ||||
|   readonly name: string; | ||||
|   readonly description: string; | ||||
|   readonly config: FullConfig; | ||||
|   private _logName: string; | ||||
|   protected _browserPromise: Promise<playwright.Browser> | 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<playwright.Browser> { | ||||
|     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<void> }> { | ||||
|     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<playwright.Browser> { | ||||
| @@ -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<playwright.Browser> { | ||||
| @@ -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<playwright.Browser> { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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<BrowserContext>): Promise<Server> { | ||||
|   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 { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<any>[] = [perform, snapshot]; | ||||
|   | ||||
| @@ -1,2 +1 @@ | ||||
| [*] | ||||
| ../utils/ | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| - Generic MCP utils, no dependencies on Playwright here. | ||||
| - Generic MCP utils, no dependencies on anything. | ||||
|   | ||||
| @@ -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<http.Server> { | ||||
|   const { host, port } = config; | ||||
|   const httpServer = http.createServer(); | ||||
|   await new Promise<void>((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<string, SSEServerTransport>) { | ||||
|   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); | ||||
| } | ||||
| @@ -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<Transport>; | ||||
| }; | ||||
|  | ||||
| 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<CallToolResult> { | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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<void>; | ||||
|   listTools(): Promise<Tool[]>; | ||||
|   callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult>; | ||||
|   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<void>(); | ||||
|   const server = new Server({ name: backend.name, version: backend.version }, { | ||||
| export async function wrapInProcess(backend: ServerBackend): Promise<Transport> { | ||||
|   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<void>(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); | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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<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 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}`; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Pavel Feldman
					Pavel Feldman