From 53c6b6dcb1bcec1d070018fd79bd64f503f19f28 Mon Sep 17 00:00:00 2001 From: Vicente Filho Date: Tue, 12 Aug 2025 17:19:09 -0300 Subject: [PATCH 1/6] fix: backtick quote escaping (#871) --- src/javascript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/javascript.ts b/src/javascript.ts index a1fabbd..a3bc8d0 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -27,7 +27,7 @@ export function escapeWithQuotes(text: string, char: string = '\'') { if (char === '"') return char + escapedText.replace(/["]/g, '\\"') + char; if (char === '`') - return char + escapedText.replace(/[`]/g, '`') + char; + return char + escapedText.replace(/[`]/g, '\\`') + char; throw new Error('Invalid escape char'); } From 7c4d67b3ae77515434cf8f94bd1cec3d535bc026 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 12 Aug 2025 13:19:25 -0700 Subject: [PATCH 2/6] chore: tool definition without zod (#873) --- src/browserServerBackend.ts | 13 ++++---- src/loopTools/DEPS.list | 1 + src/loopTools/main.ts | 11 ++++--- src/loopTools/tool.ts | 3 +- src/mcp/DEPS.list | 3 ++ src/mcp/proxyBackend.ts | 63 +++++++++++++++---------------------- src/mcp/server.ts | 34 +++++--------------- src/tools/tool.ts | 26 ++++++++++++++- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 0c20a37..b840d84 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -22,6 +22,7 @@ import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; import { packageJSON } from './package.js'; +import { toToolDefinition } from './tools/tool.js'; import type { Tool } from './tools/tool.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; @@ -65,15 +66,15 @@ export class BrowserServerBackend implements ServerBackend { }); } - tools(): mcpServer.ToolSchema[] { - return this._tools.map(tool => tool.schema); + tools(): mcpServer.ToolDefinition[] { + return this._tools.map(tool => toToolDefinition(tool.schema)); } - async callTool(schema: mcpServer.ToolSchema, rawArguments: any) { + async callTool(name: string, rawArguments: any) { + const tool = this._tools.find(tool => tool.schema.name === name)!; + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); const context = this._context!; - const parsedArguments = schema.inputSchema.parse(rawArguments || {}); - const response = new Response(context, schema.name, parsedArguments); - const tool = this._tools.find(tool => tool.schema.name === schema.name)!; + const response = new Response(context, name, parsedArguments); context.setRunningTool(true); try { await tool.handle(context, parsedArguments, response); diff --git a/src/loopTools/DEPS.list b/src/loopTools/DEPS.list index 2da9aac..07786f2 100644 --- a/src/loopTools/DEPS.list +++ b/src/loopTools/DEPS.list @@ -2,3 +2,4 @@ ../ ../loop/ ../mcp/ +../tools/ diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index fc788aa..3d9f346 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -22,6 +22,7 @@ import { packageJSON } from '../package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; import { snapshot } from './snapshot.js'; +import { toToolDefinition } from '../tools/tool.js'; import type { FullConfig } from '../config.js'; import type { ServerBackend } from '../mcp/server.js'; @@ -48,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend { this._context = await Context.create(this._config); } - tools(): mcpServer.ToolSchema[] { - return this._tools.map(tool => tool.schema); + tools(): mcpServer.ToolDefinition[] { + return this._tools.map(tool => toToolDefinition(tool.schema)); } - async callTool(schema: mcpServer.ToolSchema, rawArguments: any): Promise { - const tool = this._tools.find(tool => tool.schema.name === schema.name)!; - const parsedArguments = schema.inputSchema.parse(rawArguments || {}); + async callTool(name: string, rawArguments: any): Promise { + const tool = this._tools.find(tool => tool.schema.name === name)!; + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); return await tool.handle(this._context!, parsedArguments); } diff --git a/src/loopTools/tool.ts b/src/loopTools/tool.ts index 5399b08..27f1862 100644 --- a/src/loopTools/tool.ts +++ b/src/loopTools/tool.ts @@ -17,10 +17,11 @@ import type { z } from 'zod'; import type * as mcpServer from '../mcp/server.js'; import type { Context } from './context.js'; +import type { ToolSchema } from '../tools/tool.js'; export type Tool = { - schema: mcpServer.ToolSchema; + schema: ToolSchema; handle: (context: Context, params: z.output) => Promise; }; diff --git a/src/mcp/DEPS.list b/src/mcp/DEPS.list index 0ba24e6..9f8ba35 100644 --- a/src/mcp/DEPS.list +++ b/src/mcp/DEPS.list @@ -2,3 +2,6 @@ ../log.js ../manualPromise.js ../httpServer.js + +[proxyBackend.ts] +../package.js \ No newline at end of file diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index d90d1e2..983c833 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -15,12 +15,12 @@ */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; - import { z } from 'zod'; -import { ServerBackend, ToolResponse, ToolSchema } from './server.js'; -import { defineTool, Tool } from '../tools/tool.js'; -import { packageJSON } from '../package.js'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + import { logUnhandledError } from '../log.js'; +import { packageJSON } from '../package.js'; +import { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -40,8 +40,8 @@ export class ProxyBackend implements ServerBackend { private _clientFactories: ClientFactoryList; private _currentClient: Client | undefined; - private _contextSwitchTool: Tool; - private _tools: ToolSchema[] = []; + private _contextSwitchTool: ToolDefinition; + private _tools: ToolDefinition[] = []; private _server: Server | undefined; constructor(clientFactories: ClientFactoryList) { @@ -54,20 +54,20 @@ export class ProxyBackend implements ServerBackend { await this._setCurrentClient(this._clientFactories[0]); } - tools(): ToolSchema[] { + tools(): ToolDefinition[] { if (this._clientFactories.length === 1) return this._tools; return [ ...this._tools, - this._contextSwitchTool.schema, + this._contextSwitchTool, ]; } - async callTool(schema: ToolSchema, rawArguments: any): Promise { - if (schema.name === this._contextSwitchTool.schema.name) + async callTool(name: string, rawArguments: any): Promise { + if (name === this._contextSwitchTool.name) return this._callContextSwitchTool(rawArguments); const result = await this._currentClient!.callTool({ - name: schema.name, + name, arguments: rawArguments, }); return result as unknown as ToolResponse; @@ -95,39 +95,28 @@ export class ProxyBackend implements ServerBackend { } } - private _defineContextSwitchTool(): Tool { - return defineTool({ - capability: 'core', - - schema: { - name: 'browser_connect', + private _defineContextSwitchTool(): ToolDefinition { + return { + name: 'browser_connect', + description: [ + 'Connect to a browser using one of the available methods:', + ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), + ].join('\n'), + inputSchema: zodToJsonSchema(z.object({ + name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'), + }), { strictUnions: true }) as ToolDefinition['inputSchema'], + annotations: { title: 'Connect to a browser context', - description: [ - 'Connect to a browser using one of the available methods:', - ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), - ].join('\n'), - inputSchema: z.object({ - name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'), - }), - type: 'readOnly', + readOnlyHint: true, + openWorldHint: false, }, - - async handle() { - throw new Error('Unreachable'); - } - }); + }; } private async _setCurrentClient(factory: ClientFactory) { await this._currentClient?.close(); this._currentClient = await factory.create(this._server!); const tools = await this._currentClient.listTools(); - this._tools = tools.tools.map(tool => ({ - name: tool.name, - title: tool.title ?? '', - description: tool.description ?? '', - inputSchema: tool.inputSchema ?? z.object({}), - type: tool.annotations?.readOnlyHint ? 'readOnly' as const : 'destructive' as const, - })); + this._tools = tools.tools; } } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index f726f58..065298e 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import { z } from 'zod'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { zodToJsonSchema } from 'zod-to-json-schema'; import { ManualPromise } from '../manualPromise.js'; import { logUnhandledError } from '../log.js'; -import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; @@ -36,22 +34,14 @@ export type ToolResponse = { isError?: boolean; }; -export type ToolSchema = { - name: string; - title: string; - description: string; - inputSchema: Input; - type: 'readOnly' | 'destructive'; -}; - -export type ToolHandler = (toolName: string, params: any) => Promise; +export type ToolDefinition = Tool; export interface ServerBackend { name: string; version: string; initialize?(server: Server): Promise; - tools(): ToolSchema[]; - callTool(schema: ToolSchema, rawArguments: any): Promise; + tools(): ToolDefinition[]; + callTool(name: string, rawArguments: any): Promise; serverClosed?(): void; } @@ -73,17 +63,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = backend.tools(); - return { tools: tools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema instanceof z.ZodType ? zodToJsonSchema(tool.inputSchema) : tool.inputSchema, - annotations: { - title: tool.title, - readOnlyHint: tool.type === 'readOnly', - destructiveHint: tool.type === 'destructive', - openWorldHint: true, - }, - })) }; + return { tools }; }); let heartbeatRunning = false; @@ -100,12 +80,12 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser isError: true, }); const tools = backend.tools(); - const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema; + const tool = tools.find(tool => tool.name === request.params.name); if (!tool) return errorResult(`Error: Tool "${request.params.name}" not found`); try { - return await backend.callTool(tool, request.params.arguments || {}); + return await backend.callTool(tool.name, request.params.arguments || {}); } catch (error) { return errorResult(String(error)); } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index f0b7795..383aae6 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -14,13 +14,15 @@ * limitations under the License. */ +import { zodToJsonSchema } from 'zod-to-json-schema'; + import type { z } from 'zod'; import type { Context } from '../context.js'; import type * as playwright from 'playwright'; import type { ToolCapability } from '../../config.js'; import type { Tab } from '../tab.js'; import type { Response } from '../response.js'; -import type { ToolSchema } from '../mcp/server.js'; +import type { ToolDefinition } from '../mcp/server.js'; export type FileUploadModalState = { type: 'fileChooser'; @@ -36,6 +38,28 @@ export type DialogModalState = { export type ModalState = FileUploadModalState | DialogModalState; +export type ToolSchema = { + name: string; + title: string; + description: string; + inputSchema: Input; + type: 'readOnly' | 'destructive'; +}; + +export function toToolDefinition(tool: ToolSchema): ToolDefinition { + return { + name: tool.name, + description: tool.description, + inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as ToolDefinition['inputSchema'], + annotations: { + title: tool.title, + readOnlyHint: tool.type === 'readOnly', + destructiveHint: tool.type === 'destructive', + openWorldHint: true, + }, + }; +} + export type Tool = { capability: ToolCapability; schema: ToolSchema; From 2f41a3f6b11e2017bb8ce851cda48b49572e74d6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 12 Aug 2025 13:30:32 -0700 Subject: [PATCH 3/6] chore: roll Playwright to latest (#875) --- package-lock.json | 28 ++++++++++++++-------------- package.json | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ea84d2..3f31246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "debug": "^4.4.1", "dotenv": "^17.2.0", "mime": "^4.0.7", - "playwright": "1.55.0-alpha-2025-08-07", - "playwright-core": "1.55.0-alpha-2025-08-07", + "playwright": "1.55.0-alpha-2025-08-12", + "playwright-core": "1.55.0-alpha-2025-08-12", "ws": "^8.18.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.4" @@ -27,7 +27,7 @@ "@anthropic-ai/sdk": "^0.57.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.55.0-alpha-2025-08-07", + "@playwright/test": "1.55.0-alpha-2025-08-12", "@stylistic/eslint-plugin": "^3.0.1", "@types/debug": "^4.1.12", "@types/node": "^22.13.10", @@ -703,13 +703,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.55.0-alpha-2025-08-07", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-07.tgz", - "integrity": "sha512-N83L8JSSJ+E690HCbgzmXIcbRfM/rlh0uWZhbHbMp9q4qDPABSgvhm0HGiG345PV1ozoqcCI/mXLZPircsmPIA==", + "version": "1.55.0-alpha-2025-08-12", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-12.tgz", + "integrity": "sha512-lyq9MDSd4UcOWx5292AYLBfbYYCstg8iLb+lk6LdM69ps6bwmPloZO3Ol3JO3FQQ63qAuW9VD0w+ZYKL0lRmQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0-alpha-2025-08-07" + "playwright": "1.55.0-alpha-2025-08-12" }, "bin": { "playwright": "cli.js" @@ -3745,12 +3745,12 @@ } }, "node_modules/playwright": { - "version": "1.55.0-alpha-2025-08-07", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-07.tgz", - "integrity": "sha512-rH8kdQOZzhjxC6FOL9zSEDwPl88ZqQq9QEvRDONWhzKwRQ/jOXlEZRxm8QRCBdrLqBMTGHx/YOaP7MIV//rtIA==", + "version": "1.55.0-alpha-2025-08-12", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-12.tgz", + "integrity": "sha512-daZPM5gX0VTG6ae3/qOpEKc9NxoavkM2lfL0UIzTG0k+yK8ZeSPYo63iewZhVANsWRm0BT+XQ1NniAUOwWQ+xA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-alpha-2025-08-07" + "playwright-core": "1.55.0-alpha-2025-08-12" }, "bin": { "playwright": "cli.js" @@ -3763,9 +3763,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.0-alpha-2025-08-07", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-07.tgz", - "integrity": "sha512-NUuC6R0/dLk1QKiYoJL8NUsQAC6Je0C2BpuIg5h4wcvBwJ5TFldslmik17Txg3TXBSqwgG76DAl4Q6UdHGn54Q==", + "version": "1.55.0-alpha-2025-08-12", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-12.tgz", + "integrity": "sha512-4uxOd9xmeF6gqdsORzzlXd7p795vcACOiAGVHHEiTuFXsD83LYH+0C/SYLWB0Z+fAq4LdKGsy0qEfTm0JkY8Ig==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 596c43d..0ed3bb1 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "debug": "^4.4.1", "dotenv": "^17.2.0", "mime": "^4.0.7", - "playwright": "1.55.0-alpha-2025-08-07", - "playwright-core": "1.55.0-alpha-2025-08-07", + "playwright": "1.55.0-alpha-2025-08-12", + "playwright-core": "1.55.0-alpha-2025-08-12", "ws": "^8.18.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.4" @@ -53,7 +53,7 @@ "@anthropic-ai/sdk": "^0.57.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.55.0-alpha-2025-08-07", + "@playwright/test": "1.55.0-alpha-2025-08-12", "@stylistic/eslint-plugin": "^3.0.1", "@types/debug": "^4.1.12", "@types/node": "^22.13.10", From dbd44110f18f566676634cc217fc438098aab687 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 12 Aug 2025 13:41:08 -0700 Subject: [PATCH 4/6] chore: run test server per context (#874) Fixes https://github.com/microsoft/playwright-mcp/issues/869 --- src/browserContextFactory.ts | 35 ++++++++++++++++++++++------------- src/mcp/server.ts | 5 +++++ src/program.ts | 10 ---------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index 2d3fcff..9646bc5 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -21,6 +21,8 @@ import path from 'path'; import * as playwright from 'playwright'; // @ts-ignore import { registryDirectory } from 'playwright-core/lib/server/registry/index'; +// @ts-ignore +import { startTraceViewerServer } from 'playwright-core/lib/server'; import { logUnhandledError, testDebug } from './log.js'; import { createHash } from './utils.js'; import { outputFile } from './config.js'; @@ -50,7 +52,6 @@ class BaseContextFactory implements BrowserContextFactory { readonly description: string; readonly config: FullConfig; protected _browserPromise: Promise | undefined; - protected _tracesDir: string | undefined; constructor(name: string, description: string, config: FullConfig) { this.name = name; @@ -58,11 +59,11 @@ class BaseContextFactory implements BrowserContextFactory { this.config = config; } - protected async _obtainBrowser(): Promise { + protected async _obtainBrowser(clientInfo: ClientInfo): Promise { if (this._browserPromise) return this._browserPromise; testDebug(`obtain browser (${this.name})`); - this._browserPromise = this._doObtainBrowser(); + this._browserPromise = this._doObtainBrowser(clientInfo); void this._browserPromise.then(browser => { browser.on('disconnected', () => { this._browserPromise = undefined; @@ -73,16 +74,13 @@ class BaseContextFactory implements BrowserContextFactory { return this._browserPromise; } - protected async _doObtainBrowser(): Promise { + protected async _doObtainBrowser(clientInfo: ClientInfo): Promise { throw new Error('Not implemented'); } async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { - if (this.config.saveTrace) - this._tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`); - testDebug(`create browser context (${this.name})`); - const browser = await this._obtainBrowser(); + const browser = await this._obtainBrowser(clientInfo); const browserContext = await this._doCreateContext(browser); return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; } @@ -108,11 +106,11 @@ class IsolatedContextFactory extends BaseContextFactory { super('isolated', 'Create a new isolated browser context', config); } - protected override async _doObtainBrowser(): Promise { + protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; return browserType.launch({ - tracesDir: this._tracesDir, + tracesDir: await startTraceServer(this.config, clientInfo.rootPath), ...this.config.browser.launchOptions, handleSIGINT: false, handleSIGTERM: false, @@ -175,9 +173,7 @@ class PersistentContextFactory implements BrowserContextFactory { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); - let tracesDir: string | undefined; - if (this.config.saveTrace) - tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces-${Date.now()}`); + const tracesDir = await startTraceServer(this.config, clientInfo.rootPath); this._userDataDirs.add(userDataDir); testDebug('lock user data dir', userDataDir); @@ -242,3 +238,16 @@ async function findFreePort(): Promise { server.on('error', reject); }); } + +async function startTraceServer(config: FullConfig, rootPath: string | undefined): Promise { + if (!config.saveTrace) + return undefined; + + const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`); + const server = await startTraceViewerServer(); + const urlPrefix = server.urlPrefix('human-readable'); + const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json'; + // eslint-disable-next-line no-console + console.error('\nTrace viewer listening on ' + url); + return tracesDir; +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 065298e..6f06d82 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import debug from 'debug'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { ManualPromise } from '../manualPromise.js'; @@ -23,6 +24,8 @@ import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +const serverDebug = debug('pw:mcp:server'); + export type ClientCapabilities = { roots?: { listRoots?: boolean @@ -62,12 +65,14 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser }); server.setRequestHandler(ListToolsRequestSchema, async () => { + serverDebug('listTools'); const tools = backend.tools(); return { tools }; }); let heartbeatRunning = false; server.setRequestHandler(CallToolRequestSchema, async request => { + serverDebug('callTool', request); await initializedPromise; if (runHeartbeat && !heartbeatRunning) { diff --git a/src/program.ts b/src/program.ts index 24ebbae..13d9e32 100644 --- a/src/program.ts +++ b/src/program.ts @@ -15,8 +15,6 @@ */ import { program, Option } from 'commander'; -// @ts-ignore -import { startTraceViewerServer } from 'playwright-core/lib/server'; import * as mcpTransport from './mcp/transport.js'; import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js'; @@ -95,14 +93,6 @@ program serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory); } await mcpTransport.start(serverBackendFactory, config.server); - - if (config.saveTrace) { - const server = await startTraceViewerServer(); - const urlPrefix = server.urlPrefix('human-readable'); - const url = urlPrefix + '/trace/index.html?trace=' + config.browser.launchOptions.tracesDir + '/trace.json'; - // eslint-disable-next-line no-console - console.error('\nTrace viewer listening on ' + url); - } }); function setupExitWatchdog() { From c091a11d766e6586819c7800bd8a70f6b867575f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 12 Aug 2025 14:33:00 -0700 Subject: [PATCH 5/6] chore: extract utils folder (#876) --- src/DEPS.list | 1 + src/browserContextFactory.ts | 4 +-- src/browserServerBackend.ts | 4 +-- src/config.ts | 2 +- src/context.ts | 2 +- src/extension/DEPS.list | 2 +- src/extension/cdpRelay.ts | 6 ++-- src/extension/extensionContextFactory.ts | 2 +- src/extension/main.ts | 38 ------------------------ src/loopTools/DEPS.list | 1 + src/loopTools/main.ts | 2 +- src/mcp/DEPS.list | 7 +---- src/mcp/proxyBackend.ts | 4 +-- src/mcp/server.ts | 4 +-- src/mcp/transport.ts | 2 +- src/program.ts | 15 +++++++--- src/sessionLog.ts | 2 +- src/tab.ts | 4 +-- src/tools/DEPS.list | 4 +-- src/tools/evaluate.ts | 2 +- src/tools/keyboard.ts | 2 +- src/tools/pdf.ts | 2 +- src/tools/screenshot.ts | 2 +- src/tools/snapshot.ts | 2 +- src/{javascript.ts => utils/codegen.ts} | 0 src/{ => utils}/fileUtils.ts | 10 ++++++- src/{utils.ts => utils/guid.ts} | 8 ----- src/{ => utils}/httpServer.ts | 0 src/{ => utils}/log.ts | 0 src/{ => utils}/manualPromise.ts | 0 src/{ => utils}/package.ts | 2 +- tests/roots.spec.ts | 2 +- 32 files changed, 51 insertions(+), 87 deletions(-) delete mode 100644 src/extension/main.ts rename src/{javascript.ts => utils/codegen.ts} (100%) rename src/{ => utils}/fileUtils.ts (78%) rename src/{utils.ts => utils/guid.ts} (69%) rename src/{ => utils}/httpServer.ts (100%) rename src/{ => utils}/log.ts (100%) rename src/{ => utils}/manualPromise.ts (100%) rename src/{ => utils}/package.ts (92%) diff --git a/src/DEPS.list b/src/DEPS.list index c6b6794..c1e18f2 100644 --- a/src/DEPS.list +++ b/src/DEPS.list @@ -1,6 +1,7 @@ [*] ./tools/ ./mcp/ +./utils/ [program.ts] *** diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index 9646bc5..ecc835d 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -23,8 +23,8 @@ import * as playwright from 'playwright'; import { registryDirectory } from 'playwright-core/lib/server/registry/index'; // @ts-ignore import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { logUnhandledError, testDebug } from './log.js'; -import { createHash } from './utils.js'; +import { logUnhandledError, testDebug } from './utils/log.js'; +import { createHash } from './utils/guid.js'; import { outputFile } from './config.js'; import type { FullConfig } from './config.js'; diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index b840d84..4a6b1f6 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -17,11 +17,11 @@ import { fileURLToPath } from 'url'; import { FullConfig } from './config.js'; import { Context } from './context.js'; -import { logUnhandledError } from './log.js'; +import { logUnhandledError } from './utils/log.js'; import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; -import { packageJSON } from './package.js'; +import { packageJSON } from './utils/package.js'; import { toToolDefinition } from './tools/tool.js'; import type { Tool } from './tools/tool.js'; diff --git a/src/config.ts b/src/config.ts index 2dddac5..e579002 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { devices } from 'playwright'; -import { sanitizeForFilePath } from './utils.js'; +import { sanitizeForFilePath } from './utils/fileUtils.js'; import type { Config, ToolCapability } from '../config.js'; import type { BrowserContextOptions, LaunchOptions } from 'playwright'; diff --git a/src/context.ts b/src/context.ts index e84356d..52ebb59 100644 --- a/src/context.ts +++ b/src/context.ts @@ -17,7 +17,7 @@ import debug from 'debug'; import * as playwright from 'playwright'; -import { logUnhandledError } from './log.js'; +import { logUnhandledError } from './utils/log.js'; import { Tab } from './tab.js'; import { outputFile } from './config.js'; diff --git a/src/extension/DEPS.list b/src/extension/DEPS.list index 69e525a..796b333 100644 --- a/src/extension/DEPS.list +++ b/src/extension/DEPS.list @@ -1,3 +1,3 @@ [*] -../ ../mcp/ +../utils/ diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts index dc99fc0..53e639e 100644 --- a/src/extension/cdpRelay.ts +++ b/src/extension/cdpRelay.ts @@ -26,9 +26,9 @@ import { spawn } from 'child_process'; import http from 'http'; import debug from 'debug'; import { WebSocket, WebSocketServer } from 'ws'; -import { httpAddressToString } from '../httpServer.js'; -import { logUnhandledError } from '../log.js'; -import { ManualPromise } from '../manualPromise.js'; +import { httpAddressToString } from '../utils/httpServer.js'; +import { logUnhandledError } from '../utils/log.js'; +import { ManualPromise } from '../utils/manualPromise.js'; import type websocket from 'ws'; import type { ClientInfo } from '../browserContextFactory.js'; diff --git a/src/extension/extensionContextFactory.ts b/src/extension/extensionContextFactory.ts index 95a4f72..7bdfaa0 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 '../httpServer.js'; +import { startHttpServer } from '../utils/httpServer.js'; import { CDPRelayServer } from './cdpRelay.js'; import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js'; diff --git a/src/extension/main.ts b/src/extension/main.ts deleted file mode 100644 index f9fa177..0000000 --- a/src/extension/main.ts +++ /dev/null @@ -1,38 +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 { ExtensionContextFactory } from './extensionContextFactory.js'; -import { BrowserServerBackend } from '../browserServerBackend.js'; -import { InProcessClientFactory } from '../inProcessClient.js'; -import * as mcpTransport from '../mcp/transport.js'; - -import type { FullConfig } from '../config.js'; -import type { ClientFactory } from '../mcp/proxyBackend.js'; - -export async function runWithExtension(config: FullConfig) { - const contextFactory = createExtensionContextFactory(config); - const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory); - await mcpTransport.start(serverBackendFactory, config.server); -} - -export function createExtensionClientFactory(config: FullConfig): ClientFactory { - return new InProcessClientFactory(createExtensionContextFactory(config), config); -} - - -function createExtensionContextFactory(config: FullConfig) { - return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir); -} diff --git a/src/loopTools/DEPS.list b/src/loopTools/DEPS.list index 07786f2..9fe8b14 100644 --- a/src/loopTools/DEPS.list +++ b/src/loopTools/DEPS.list @@ -3,3 +3,4 @@ ../loop/ ../mcp/ ../tools/ +../utils/ diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index 3d9f346..7943fe3 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -18,7 +18,7 @@ import dotenv from 'dotenv'; import * as mcpServer from '../mcp/server.js'; import * as mcpTransport from '../mcp/transport.js'; -import { packageJSON } from '../package.js'; +import { packageJSON } from '../utils/package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; import { snapshot } from './snapshot.js'; diff --git a/src/mcp/DEPS.list b/src/mcp/DEPS.list index 9f8ba35..5870e2d 100644 --- a/src/mcp/DEPS.list +++ b/src/mcp/DEPS.list @@ -1,7 +1,2 @@ [*] -../log.js -../manualPromise.js -../httpServer.js - -[proxyBackend.ts] -../package.js \ No newline at end of file +../utils/ diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index 983c833..1128818 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -18,8 +18,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { logUnhandledError } from '../log.js'; -import { packageJSON } from '../package.js'; +import { logUnhandledError } from '../utils/log.js'; +import { packageJSON } from '../utils/package.js'; import { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 6f06d82..39ae9f9 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -17,8 +17,8 @@ import debug from 'debug'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { ManualPromise } from '../manualPromise.js'; -import { logUnhandledError } from '../log.js'; +import { ManualPromise } from '../utils/manualPromise.js'; +import { logUnhandledError } from '../utils/log.js'; import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; diff --git a/src/mcp/transport.ts b/src/mcp/transport.ts index 9d82b40..06965b7 100644 --- a/src/mcp/transport.ts +++ b/src/mcp/transport.ts @@ -21,7 +21,7 @@ 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 '../httpServer.js'; +import { httpAddressToString, startHttpServer } from '../utils/httpServer.js'; import * as mcpServer from './server.js'; import type { ServerBackendFactory } from './server.js'; diff --git a/src/program.ts b/src/program.ts index 13d9e32..2214c47 100644 --- a/src/program.ts +++ b/src/program.ts @@ -18,17 +18,18 @@ import { program, Option } from 'commander'; import * as mcpTransport from './mcp/transport.js'; import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js'; -import { packageJSON } from './package.js'; -import { createExtensionClientFactory, runWithExtension } from './extension/main.js'; +import { packageJSON } from './utils/package.js'; import { Context } from './context.js'; import { contextFactory } from './browserContextFactory.js'; import { runLoopTools } from './loopTools/main.js'; import { ProxyBackend } from './mcp/proxyBackend.js'; import { InProcessClientFactory } from './inProcessClient.js'; import { BrowserServerBackend } from './browserServerBackend.js'; +import { ExtensionContextFactory } from './extension/extensionContextFactory.js'; import type { ClientFactoryList } from './mcp/proxyBackend.js'; import type { ServerBackendFactory } from './mcp/server.js'; +import type { FullConfig } from './config.js'; program .version('Version ' + packageJSON.version) @@ -73,7 +74,9 @@ program const config = await resolveCLIConfig(options); if (options.extension) { - await runWithExtension(config); + const contextFactory = createExtensionContextFactory(config); + const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory); + await mcpTransport.start(serverBackendFactory, config.server); return; } if (options.loopTools) { @@ -86,7 +89,7 @@ program if (options.connectTool) { const factories: ClientFactoryList = [ new InProcessClientFactory(browserContextFactory, config), - createExtensionClientFactory(config) + new InProcessClientFactory(createExtensionContextFactory(config), config), ]; serverBackendFactory = () => new ProxyBackend(factories); } else { @@ -111,4 +114,8 @@ function setupExitWatchdog() { process.on('SIGTERM', handleExit); } +function createExtensionContextFactory(config: FullConfig) { + return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir); +} + void program.parseAsync(process.argv); diff --git a/src/sessionLog.ts b/src/sessionLog.ts index ba841b4..dfda6a0 100644 --- a/src/sessionLog.ts +++ b/src/sessionLog.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { Response } from './response.js'; -import { logUnhandledError } from './log.js'; +import { logUnhandledError } from './utils/log.js'; import { outputFile } from './config.js'; import type { FullConfig } from './config.js'; diff --git a/src/tab.ts b/src/tab.ts index d7f44fa..1b3fff0 100644 --- a/src/tab.ts +++ b/src/tab.ts @@ -17,8 +17,8 @@ import { EventEmitter } from 'events'; import * as playwright from 'playwright'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js'; -import { logUnhandledError } from './log.js'; -import { ManualPromise } from './manualPromise.js'; +import { logUnhandledError } from './utils/log.js'; +import { ManualPromise } from './utils/manualPromise.js'; import { ModalState } from './tools/tool.js'; import type { Context } from './context.js'; diff --git a/src/tools/DEPS.list b/src/tools/DEPS.list index 2467fe7..5870e2d 100644 --- a/src/tools/DEPS.list +++ b/src/tools/DEPS.list @@ -1,4 +1,2 @@ [*] -../javascript.js -../log.js -../manualPromise.js +../utils/ diff --git a/src/tools/evaluate.ts b/src/tools/evaluate.ts index 3023f39..9e4adff 100644 --- a/src/tools/evaluate.ts +++ b/src/tools/evaluate.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { defineTabTool } from './tool.js'; -import * as javascript from '../javascript.js'; +import * as javascript from '../utils/codegen.js'; import { generateLocator } from './utils.js'; import type * as playwright from 'playwright'; diff --git a/src/tools/keyboard.ts b/src/tools/keyboard.ts index 12e6e45..35d0016 100644 --- a/src/tools/keyboard.ts +++ b/src/tools/keyboard.ts @@ -19,7 +19,7 @@ import { z } from 'zod'; import { defineTabTool } from './tool.js'; import { elementSchema } from './snapshot.js'; import { generateLocator } from './utils.js'; -import * as javascript from '../javascript.js'; +import * as javascript from '../utils/codegen.js'; const pressKey = defineTabTool({ capability: 'core', diff --git a/src/tools/pdf.ts b/src/tools/pdf.ts index 3de0092..d57aac6 100644 --- a/src/tools/pdf.ts +++ b/src/tools/pdf.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { defineTabTool } from './tool.js'; -import * as javascript from '../javascript.js'; +import * as javascript from '../utils/codegen.js'; const pdfSchema = z.object({ filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'), diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 0e4b4ce..e7297ae 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { defineTabTool } from './tool.js'; -import * as javascript from '../javascript.js'; +import * as javascript from '../utils/codegen.js'; import { generateLocator } from './utils.js'; import type * as playwright from 'playwright'; diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index e8694d9..0a8717d 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { defineTabTool, defineTool } from './tool.js'; -import * as javascript from '../javascript.js'; +import * as javascript from '../utils/codegen.js'; import { generateLocator } from './utils.js'; const snapshot = defineTool({ diff --git a/src/javascript.ts b/src/utils/codegen.ts similarity index 100% rename from src/javascript.ts rename to src/utils/codegen.ts diff --git a/src/fileUtils.ts b/src/utils/fileUtils.ts similarity index 78% rename from src/fileUtils.ts rename to src/utils/fileUtils.ts index 4155b74..4ebf7e5 100644 --- a/src/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -17,7 +17,7 @@ import os from 'node:os'; import path from 'node:path'; -import type { FullConfig } from './config.js'; +import type { FullConfig } from '../config.js'; export function cacheDir() { let cacheDirectory: string; @@ -35,3 +35,11 @@ export function cacheDir() { export async function userDataDir(browserConfig: FullConfig['browser']) { return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`); } + +export function sanitizeForFilePath(s: string) { + const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); + const separator = s.lastIndexOf('.'); + if (separator === -1) + return sanitize(s); + return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); +} diff --git a/src/utils.ts b/src/utils/guid.ts similarity index 69% rename from src/utils.ts rename to src/utils/guid.ts index 6d1feb0..391d0b6 100644 --- a/src/utils.ts +++ b/src/utils/guid.ts @@ -19,11 +19,3 @@ import crypto from 'crypto'; export function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } - -export function sanitizeForFilePath(s: string) { - const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); - const separator = s.lastIndexOf('.'); - if (separator === -1) - return sanitize(s); - return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); -} diff --git a/src/httpServer.ts b/src/utils/httpServer.ts similarity index 100% rename from src/httpServer.ts rename to src/utils/httpServer.ts diff --git a/src/log.ts b/src/utils/log.ts similarity index 100% rename from src/log.ts rename to src/utils/log.ts diff --git a/src/manualPromise.ts b/src/utils/manualPromise.ts similarity index 100% rename from src/manualPromise.ts rename to src/utils/manualPromise.ts diff --git a/src/package.ts b/src/utils/package.ts similarity index 92% rename from src/package.ts rename to src/utils/package.ts index e599f68..e3c4bba 100644 --- a/src/package.ts +++ b/src/utils/package.ts @@ -19,4 +19,4 @@ import path from 'path'; import url from 'url'; const __filename = url.fileURLToPath(import.meta.url); -export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8')); +export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8')); diff --git a/tests/roots.spec.ts b/tests/roots.spec.ts index 1529aff..0032d8b 100644 --- a/tests/roots.spec.ts +++ b/tests/roots.spec.ts @@ -19,7 +19,7 @@ import path from 'path'; import { pathToFileURL } from 'url'; import { test, expect } from './fixtures.js'; -import { createHash } from '../src/utils.js'; +import { createHash } from '../src/utils/guid.js'; const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder'; From 8572ab300c8a4afec7ac68c7cbc89f62cbceb109 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 12 Aug 2025 18:05:45 -0700 Subject: [PATCH 6/6] chore: separate proxy client from external (#877) --- src/browserServerBackend.ts | 8 +-- ...ocessClient.ts => inProcessMcpFactrory.ts} | 25 ++------ src/mcp/proxyBackend.ts | 58 ++++++++++++++----- src/program.ts | 22 +++---- 4 files changed, 58 insertions(+), 55 deletions(-) rename src/{inProcessClient.ts => inProcessMcpFactrory.ts} (60%) diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 4a6b1f6..5dcfd79 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -46,11 +46,9 @@ export class BrowserServerBackend implements ServerBackend { } async initialize(server: mcpServer.Server): Promise { - const capabilities = server.getClientCapabilities() as mcpServer.ClientCapabilities; + const capabilities = server.getClientCapabilities(); let rootPath: string | undefined; - if (capabilities.roots && ( - server.getClientVersion()?.name === 'Visual Studio Code' || - server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) { + if (capabilities?.roots) { const { roots } = await server.listRoots(); const firstRootUri = roots[0]?.uri; const url = firstRootUri ? new URL(firstRootUri) : undefined; @@ -89,6 +87,6 @@ export class BrowserServerBackend implements ServerBackend { } serverClosed() { - void this._context!.dispose().catch(logUnhandledError); + void this._context?.dispose().catch(logUnhandledError); } } diff --git a/src/inProcessClient.ts b/src/inProcessMcpFactrory.ts similarity index 60% rename from src/inProcessClient.ts rename to src/inProcessMcpFactrory.ts index 390954c..c29ceaf 100644 --- a/src/inProcessClient.ts +++ b/src/inProcessMcpFactrory.ts @@ -15,18 +15,16 @@ */ -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { BrowserContextFactory } from './browserContextFactory.js'; import { BrowserServerBackend } from './browserServerBackend.js'; import { InProcessTransport } from './mcp/inProcessTransport.js'; import * as mcpServer from './mcp/server.js'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { FullConfig } from './config.js'; -import type { ClientFactory } from './mcp/proxyBackend.js'; +import type { MCPFactory } from './mcp/proxyBackend.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -export class InProcessClientFactory implements ClientFactory { +export class InProcessMCPFactory implements MCPFactory { name: string; description: string; @@ -40,21 +38,8 @@ export class InProcessClientFactory implements ClientFactory { this._config = config; } - async create(server: Server): Promise { - const client = new Client(server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }); - const clientCapabilities = server.getClientCapabilities(); - if (clientCapabilities) - client.registerCapabilities(clientCapabilities); - - if (clientCapabilities?.roots) { - client.setRequestHandler(ListRootsRequestSchema, async () => { - return await server.listRoots(); - }); - } - + async create(): Promise { const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false); - await client.connect(new InProcessTransport(delegate)); - await client.ping(); - return client; + return new InProcessTransport(delegate); } } diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index 1128818..1372ca4 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -14,48 +14,51 @@ * limitations under the License. */ -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 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 { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; type NonEmptyArray = [T, ...T[]]; -export type ClientFactory = { +export type MCPFactory = { name: string; description: string; - create(server: Server): Promise; + create(): Promise; }; -export type ClientFactoryList = NonEmptyArray; +export type MCPFactoryList = NonEmptyArray; export class ProxyBackend implements ServerBackend { name = 'Playwright MCP Client Switcher'; version = packageJSON.version; - private _clientFactories: ClientFactoryList; + private _mcpFactories: MCPFactoryList; private _currentClient: Client | undefined; private _contextSwitchTool: ToolDefinition; private _tools: ToolDefinition[] = []; private _server: Server | undefined; - constructor(clientFactories: ClientFactoryList) { - this._clientFactories = clientFactories; + constructor(clientFactories: MCPFactoryList) { + this._mcpFactories = clientFactories; this._contextSwitchTool = this._defineContextSwitchTool(); } async initialize(server: Server): Promise { this._server = server; - await this._setCurrentClient(this._clientFactories[0]); + await this._setCurrentClient(this._mcpFactories[0]); } tools(): ToolDefinition[] { - if (this._clientFactories.length === 1) + if (this._mcpFactories.length === 1) return this._tools; return [ ...this._tools, @@ -79,7 +82,7 @@ export class ProxyBackend implements ServerBackend { private async _callContextSwitchTool(params: any): Promise { try { - const factory = this._clientFactories.find(factory => factory.name === params.name); + const factory = this._mcpFactories.find(factory => factory.name === params.name); if (!factory) throw new Error('Unknown connection method: ' + params.name); @@ -100,10 +103,10 @@ export class ProxyBackend implements ServerBackend { name: 'browser_connect', description: [ 'Connect to a browser using one of the available methods:', - ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), + ...this._mcpFactories.map(factory => `- "${factory.name}": ${factory.description}`), ].join('\n'), inputSchema: zodToJsonSchema(z.object({ - name: z.enum(this._clientFactories.map(factory => factory.name) as [string, ...string[]]).default(this._clientFactories[0].name).describe('The method to use to connect to the browser'), + name: z.enum(this._mcpFactories.map(factory => factory.name) as [string, ...string[]]).default(this._mcpFactories[0].name).describe('The method to use to connect to the browser'), }), { strictUnions: true }) as ToolDefinition['inputSchema'], annotations: { title: 'Connect to a browser context', @@ -113,9 +116,32 @@ export class ProxyBackend implements ServerBackend { }; } - private async _setCurrentClient(factory: ClientFactory) { + private async _setCurrentClient(factory: MCPFactory) { await this._currentClient?.close(); - this._currentClient = await factory.create(this._server!); + this._currentClient = undefined; + + const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version }); + client.registerCapabilities({ + roots: { + listRoots: true, + }, + }); + client.setRequestHandler(ListRootsRequestSchema, async () => { + const clientName = this._server!.getClientVersion()?.name; + if (this._server!.getClientCapabilities()?.roots && ( + clientName === 'Visual Studio Code' || + clientName === 'Visual Studio Code - Insiders')) { + const { roots } = await this._server!.listRoots(); + return { roots }; + } + return { roots: [] }; + }); + client.setRequestHandler(PingRequestSchema, () => ({})); + + const transport = await factory.create(); + await client.connect(transport); + + this._currentClient = client; const tools = await this._currentClient.listTools(); this._tools = tools.tools; } diff --git a/src/program.ts b/src/program.ts index 2214c47..ad8fa33 100644 --- a/src/program.ts +++ b/src/program.ts @@ -23,12 +23,11 @@ import { Context } from './context.js'; import { contextFactory } from './browserContextFactory.js'; import { runLoopTools } from './loopTools/main.js'; import { ProxyBackend } from './mcp/proxyBackend.js'; -import { InProcessClientFactory } from './inProcessClient.js'; +import { InProcessMCPFactory } from './inProcessMcpFactrory.js'; import { BrowserServerBackend } from './browserServerBackend.js'; import { ExtensionContextFactory } from './extension/extensionContextFactory.js'; -import type { ClientFactoryList } from './mcp/proxyBackend.js'; -import type { ServerBackendFactory } from './mcp/server.js'; +import type { MCPFactoryList } from './mcp/proxyBackend.js'; import type { FullConfig } from './config.js'; program @@ -84,18 +83,13 @@ program return; } - let serverBackendFactory: ServerBackendFactory; const browserContextFactory = contextFactory(config); - if (options.connectTool) { - const factories: ClientFactoryList = [ - new InProcessClientFactory(browserContextFactory, config), - new InProcessClientFactory(createExtensionContextFactory(config), config), - ]; - serverBackendFactory = () => new ProxyBackend(factories); - } else { - serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory); - } - await mcpTransport.start(serverBackendFactory, config.server); + const factories: MCPFactoryList = [ + new InProcessMCPFactory(browserContextFactory, config), + ]; + if (options.connectTool) + factories.push(new InProcessMCPFactory(createExtensionContextFactory(config), config)); + await mcpTransport.start(() => new ProxyBackend(factories), config.server); }); function setupExitWatchdog() {