From 73adb0fdf029dbec723050a8417fe131679df772 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 13 Aug 2025 14:09:37 -0700 Subject: [PATCH 1/3] chore: steer towards mcp types a bit (#880) --- src/browserServerBackend.ts | 10 +++-- src/inProcessMcpFactrory.ts | 45 -------------------- src/loopTools/context.ts | 2 +- src/loopTools/main.ts | 10 ++--- src/loopTools/tool.ts | 2 +- src/mcp/proxyBackend.ts | 84 +++++++++++++++++-------------------- src/mcp/server.ts | 38 +++++------------ src/program.ts | 27 ++++++++---- src/tools/tool.ts | 6 +-- src/utils/guid.ts | 4 ++ 10 files changed, 87 insertions(+), 141 deletions(-) delete mode 100644 src/inProcessMcpFactrory.ts diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 5dcfd79..25f68ef 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -22,7 +22,7 @@ import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; import { packageJSON } from './utils/package.js'; -import { toToolDefinition } from './tools/tool.js'; +import { toMcpTool } from './tools/tool.js'; import type { Tool } from './tools/tool.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; @@ -64,12 +64,14 @@ export class BrowserServerBackend implements ServerBackend { }); } - tools(): mcpServer.ToolDefinition[] { - return this._tools.map(tool => toToolDefinition(tool.schema)); + async listTools(): Promise { + return this._tools.map(tool => toMcpTool(tool.schema)); } - async callTool(name: string, rawArguments: any) { + async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) { const tool = this._tools.find(tool => tool.schema.name === name)!; + if (!tool) + throw new Error(`Tool "${name}" not found`); const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); const context = this._context!; const response = new Response(context, name, parsedArguments); diff --git a/src/inProcessMcpFactrory.ts b/src/inProcessMcpFactrory.ts deleted file mode 100644 index c29ceaf..0000000 --- a/src/inProcessMcpFactrory.ts +++ /dev/null @@ -1,45 +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 { BrowserContextFactory } from './browserContextFactory.js'; -import { BrowserServerBackend } from './browserServerBackend.js'; -import { InProcessTransport } from './mcp/inProcessTransport.js'; -import * as mcpServer from './mcp/server.js'; - -import type { FullConfig } from './config.js'; -import type { MCPFactory } from './mcp/proxyBackend.js'; -import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; - -export class InProcessMCPFactory implements MCPFactory { - name: string; - description: string; - - private _contextFactory: BrowserContextFactory; - private _config: FullConfig; - - constructor(contextFactory: BrowserContextFactory, config: FullConfig) { - this.name = contextFactory.name; - this.description = contextFactory.description; - this._contextFactory = contextFactory; - this._config = config; - } - - async create(): Promise { - const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false); - return new InProcessTransport(delegate); - } -} diff --git a/src/loopTools/context.ts b/src/loopTools/context.ts index 732af07..777ba61 100644 --- a/src/loopTools/context.ts +++ b/src/loopTools/context.ts @@ -52,7 +52,7 @@ export class Context { return new Context(config, client); } - async runTask(task: string, oneShot: boolean = false): Promise { + async runTask(task: string, oneShot: boolean = false): Promise { const messages = await runTask(this._delegate, this._client!, task, oneShot); const lines: string[] = []; diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index 7943fe3..eb8b1c5 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -22,7 +22,7 @@ import { packageJSON } from '../utils/package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; import { snapshot } from './snapshot.js'; -import { toToolDefinition } from '../tools/tool.js'; +import { toMcpTool } from '../tools/tool.js'; import type { FullConfig } from '../config.js'; import type { ServerBackend } from '../mcp/server.js'; @@ -49,13 +49,13 @@ class LoopToolsServerBackend implements ServerBackend { this._context = await Context.create(this._config); } - tools(): mcpServer.ToolDefinition[] { - return this._tools.map(tool => toToolDefinition(tool.schema)); + async listTools(): Promise { + return this._tools.map(tool => toMcpTool(tool.schema)); } - async callTool(name: string, rawArguments: any): Promise { + async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { const tool = this._tools.find(tool => tool.schema.name === name)!; - const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); + const parsedArguments = tool.schema.inputSchema.parse(args || {}); return await tool.handle(this._context!, parsedArguments); } diff --git a/src/loopTools/tool.ts b/src/loopTools/tool.ts index 27f1862..000f1cf 100644 --- a/src/loopTools/tool.ts +++ b/src/loopTools/tool.ts @@ -22,7 +22,7 @@ import type { ToolSchema } from '../tools/tool.js'; export type Tool = { schema: ToolSchema; - handle: (context: Context, params: z.output) => Promise; + handle: (context: Context, params: z.output) => Promise; }; export function defineTool(tool: Tool): Tool { diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index 1372ca4..e4083b5 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -24,65 +24,67 @@ import { packageJSON } from '../utils/package.js'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import type { ToolDefinition, ServerBackend, ToolResponse } from './server.js'; +import type { ServerBackend } from './server.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; -type NonEmptyArray = [T, ...T[]]; - -export type MCPFactory = { +export type MCPProvider = { name: string; description: string; - create(): Promise; + connect(): Promise; }; -export type MCPFactoryList = NonEmptyArray; - export class ProxyBackend implements ServerBackend { name = 'Playwright MCP Client Switcher'; version = packageJSON.version; - private _mcpFactories: MCPFactoryList; + private _mcpProviders: MCPProvider[]; private _currentClient: Client | undefined; - private _contextSwitchTool: ToolDefinition; - private _tools: ToolDefinition[] = []; - private _server: Server | undefined; + private _contextSwitchTool: Tool; + private _roots: Root[] = []; - constructor(clientFactories: MCPFactoryList) { - this._mcpFactories = clientFactories; + constructor(mcpProviders: MCPProvider[]) { + this._mcpProviders = mcpProviders; this._contextSwitchTool = this._defineContextSwitchTool(); } async initialize(server: Server): Promise { - this._server = server; - await this._setCurrentClient(this._mcpFactories[0]); + const version = server.getClientVersion(); + const capabilities = server.getClientCapabilities(); + if (capabilities?.roots && version && clientsWithRoots.includes(version.name)) { + const { roots } = await server.listRoots(); + this._roots = roots; + } + + await this._setCurrentClient(this._mcpProviders[0]); } - tools(): ToolDefinition[] { - if (this._mcpFactories.length === 1) - return this._tools; + async listTools(): Promise { + const response = await this._currentClient!.listTools(); + if (this._mcpProviders.length === 1) + return response.tools; return [ - ...this._tools, + ...response.tools, this._contextSwitchTool, ]; } - async callTool(name: string, rawArguments: any): Promise { + async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise { if (name === this._contextSwitchTool.name) - return this._callContextSwitchTool(rawArguments); - const result = await this._currentClient!.callTool({ + return this._callContextSwitchTool(args); + return await this._currentClient!.callTool({ name, - arguments: rawArguments, - }); - return result as unknown as ToolResponse; + arguments: args, + }) as CallToolResult; } serverClosed?(): void { void this._currentClient?.close().catch(logUnhandledError); } - private async _callContextSwitchTool(params: any): Promise { + private async _callContextSwitchTool(params: any): Promise { try { - const factory = this._mcpFactories.find(factory => factory.name === params.name); + const factory = this._mcpProviders.find(factory => factory.name === params.name); if (!factory) throw new Error('Unknown connection method: ' + params.name); @@ -98,16 +100,16 @@ export class ProxyBackend implements ServerBackend { } } - private _defineContextSwitchTool(): ToolDefinition { + private _defineContextSwitchTool(): Tool { return { name: 'browser_connect', description: [ 'Connect to a browser using one of the available methods:', - ...this._mcpFactories.map(factory => `- "${factory.name}": ${factory.description}`), + ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`), ].join('\n'), inputSchema: zodToJsonSchema(z.object({ - 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'], + name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), + }), { strictUnions: true }) as Tool['inputSchema'], annotations: { title: 'Connect to a browser context', readOnlyHint: true, @@ -116,7 +118,7 @@ export class ProxyBackend implements ServerBackend { }; } - private async _setCurrentClient(factory: MCPFactory) { + private async _setCurrentClient(factory: MCPProvider) { await this._currentClient?.close(); this._currentClient = undefined; @@ -126,23 +128,13 @@ export class ProxyBackend implements ServerBackend { 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(ListRootsRequestSchema, () => ({ roots: this._roots })); client.setRequestHandler(PingRequestSchema, () => ({})); - const transport = await factory.create(); + const transport = await factory.connect(); await client.connect(transport); - this._currentClient = client; - const tools = await this._currentClient.listTools(); - this._tools = tools.tools; } } + +const clientsWithRoots = ['Visual Studio Code', 'Visual Studio Code - Insiders']; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 39ae9f9..b79ad17 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -20,31 +20,19 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot import { ManualPromise } from '../utils/manualPromise.js'; import { logUnhandledError } from '../utils/log.js'; -import type { ImageContent, TextContent, Tool } from '@modelcontextprotocol/sdk/types.js'; +import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +export type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; const serverDebug = debug('pw:mcp:server'); -export type ClientCapabilities = { - roots?: { - listRoots?: boolean - }; -}; - -export type ToolResponse = { - content: (TextContent | ImageContent)[]; - isError?: boolean; -}; - -export type ToolDefinition = Tool; - export interface ServerBackend { name: string; version: string; initialize?(server: Server): Promise; - tools(): ToolDefinition[]; - callTool(name: string, rawArguments: any): Promise; + listTools(): Promise; + callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; serverClosed?(): void; } @@ -66,7 +54,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser server.setRequestHandler(ListToolsRequestSchema, async () => { serverDebug('listTools'); - const tools = backend.tools(); + const tools = await backend.listTools(); return { tools }; }); @@ -80,19 +68,13 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser startHeartbeat(server); } - const errorResult = (...messages: string[]) => ({ - content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }], - isError: true, - }); - const tools = backend.tools(); - 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.name, request.params.arguments || {}); + return await backend.callTool(request.params.name, request.params.arguments || {}); } catch (error) { - return errorResult(String(error)); + return { + content: [{ type: 'text', text: '### Result\n' + String(error) }], + isError: true, + }; } }); addServerListener(server, 'initialized', () => { diff --git a/src/program.ts b/src/program.ts index ad8fa33..ee0e64b 100644 --- a/src/program.ts +++ b/src/program.ts @@ -15,7 +15,7 @@ */ 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'; @@ -23,12 +23,13 @@ import { Context } from './context.js'; import { contextFactory } from './browserContextFactory.js'; import { runLoopTools } from './loopTools/main.js'; import { ProxyBackend } from './mcp/proxyBackend.js'; -import { InProcessMCPFactory } from './inProcessMcpFactrory.js'; import { BrowserServerBackend } from './browserServerBackend.js'; import { ExtensionContextFactory } from './extension/extensionContextFactory.js'; +import { InProcessTransport } from './mcp/inProcessTransport.js'; -import type { MCPFactoryList } from './mcp/proxyBackend.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) @@ -78,18 +79,17 @@ program await mcpTransport.start(serverBackendFactory, config.server); return; } + if (options.loopTools) { await runLoopTools(config); return; } const browserContextFactory = contextFactory(config); - const factories: MCPFactoryList = [ - new InProcessMCPFactory(browserContextFactory, config), - ]; + const providers: MCPProvider[] = [mcpProviderForBrowserContextFactory(config, browserContextFactory)]; if (options.connectTool) - factories.push(new InProcessMCPFactory(createExtensionContextFactory(config), config)); - await mcpTransport.start(() => new ProxyBackend(factories), config.server); + providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config))); + await mcpTransport.start(() => new ProxyBackend(providers), config.server); }); function setupExitWatchdog() { @@ -112,4 +112,15 @@ 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/tools/tool.ts b/src/tools/tool.ts index 383aae6..68d727e 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -22,7 +22,7 @@ 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 { ToolDefinition } from '../mcp/server.js'; +import type * as mcpServer from '../mcp/server.js'; export type FileUploadModalState = { type: 'fileChooser'; @@ -46,11 +46,11 @@ export type ToolSchema = { type: 'readOnly' | 'destructive'; }; -export function toToolDefinition(tool: ToolSchema): ToolDefinition { +export function toMcpTool(tool: ToolSchema): mcpServer.Tool { return { name: tool.name, description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as ToolDefinition['inputSchema'], + inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], annotations: { title: tool.title, readOnlyHint: tool.type === 'readOnly', diff --git a/src/utils/guid.ts b/src/utils/guid.ts index 391d0b6..1cc64e7 100644 --- a/src/utils/guid.ts +++ b/src/utils/guid.ts @@ -16,6 +16,10 @@ import crypto from 'crypto'; +export function createGuid(): string { + return crypto.randomBytes(16).toString('hex'); +} + export function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } From 12942b81d6c03faf46adb04a725e1b7d9c4b2977 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 13 Aug 2025 17:29:10 -0700 Subject: [PATCH 2/3] fix: wait for initialization to complete before listing tools (#886) --- src/mcp/server.ts | 1 + tests/fixtures.ts | 3 +++ tests/roots.spec.ts | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index b79ad17..3ac389e 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -54,6 +54,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser server.setRequestHandler(ListToolsRequestSchema, async () => { serverDebug('listTools'); + await initializedPromise; const tools = await backend.listTools(); return { tools }; }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 3c91edd..c02d5c4 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -47,6 +47,7 @@ type TestFixtures = { args?: string[], config?: Config, roots?: { name: string, uri: string }[], + rootsResponseDelay?: number, }) => Promise<{ client: Client, stderr: () => string }>; wsEndpoint: string; cdpServer: CDPServer; @@ -89,6 +90,8 @@ export const test = baseTest.extend( client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); if (options?.roots) { client.setRequestHandler(ListRootsRequestSchema, async request => { + if (options.rootsResponseDelay) + await new Promise(resolve => setTimeout(resolve, options.rootsResponseDelay)); return { roots: options.roots, }; diff --git a/tests/roots.spec.ts b/tests/roots.spec.ts index 0032d8b..a94191e 100644 --- a/tests/roots.spec.ts +++ b/tests/roots.spec.ts @@ -50,7 +50,7 @@ for (const mode of ['default', 'proxy']) { }); - test('check that trace is saved in workspace', async ({ startClient, server, mcpMode }, testInfo) => { + test('check that trace is saved in workspace', async ({ startClient, server }, testInfo) => { const rootPath = testInfo.outputPath('workspace'); const { client } = await startClient({ args: ['--save-trace', ...extraArgs], @@ -73,5 +73,15 @@ for (const mode of ['default', 'proxy']) { const [file] = await fs.promises.readdir(path.join(rootPath, '.playwright-mcp')); expect(file).toContain('traces'); }); + + test('should list all tools when listRoots is slow', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + clientName: 'Visual Studio Code', // Simulate VS Code client, roots only work with it + roots: [], + rootsResponseDelay: 1000, + }); + const tools = await client.listTools(); + expect(tools.tools.length).toBeGreaterThan(20); + }); }); } From badfd82202b575f3984337b886b8e8d4ead557d3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 13 Aug 2025 18:23:25 -0700 Subject: [PATCH 3/3] chore: move tool schema to mcp as it is used by all servers (#887) --- src/browserServerBackend.ts | 2 +- src/loopTools/DEPS.list | 1 - src/loopTools/main.ts | 2 +- src/loopTools/tool.ts | 2 +- src/mcp/tool.ts | 42 +++++++++++++++++++++++++++++++++++++ src/tools/tool.ts | 26 +---------------------- src/utils/fileUtils.ts | 6 ------ 7 files changed, 46 insertions(+), 35 deletions(-) create mode 100644 src/mcp/tool.ts diff --git a/src/browserServerBackend.ts b/src/browserServerBackend.ts index 25f68ef..e71a15d 100644 --- a/src/browserServerBackend.ts +++ b/src/browserServerBackend.ts @@ -22,7 +22,7 @@ import { Response } from './response.js'; import { SessionLog } from './sessionLog.js'; import { filteredTools } from './tools.js'; import { packageJSON } from './utils/package.js'; -import { toMcpTool } from './tools/tool.js'; +import { toMcpTool } from './mcp/tool.js'; import type { Tool } from './tools/tool.js'; import type { BrowserContextFactory } from './browserContextFactory.js'; diff --git a/src/loopTools/DEPS.list b/src/loopTools/DEPS.list index 9fe8b14..5cf594d 100644 --- a/src/loopTools/DEPS.list +++ b/src/loopTools/DEPS.list @@ -2,5 +2,4 @@ ../ ../loop/ ../mcp/ -../tools/ ../utils/ diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts index eb8b1c5..a8ea803 100644 --- a/src/loopTools/main.ts +++ b/src/loopTools/main.ts @@ -22,7 +22,7 @@ import { packageJSON } from '../utils/package.js'; import { Context } from './context.js'; import { perform } from './perform.js'; import { snapshot } from './snapshot.js'; -import { toMcpTool } from '../tools/tool.js'; +import { toMcpTool } from '../mcp/tool.js'; import type { FullConfig } from '../config.js'; import type { ServerBackend } from '../mcp/server.js'; diff --git a/src/loopTools/tool.ts b/src/loopTools/tool.ts index 000f1cf..8ea56aa 100644 --- a/src/loopTools/tool.ts +++ b/src/loopTools/tool.ts @@ -17,7 +17,7 @@ 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'; +import type { ToolSchema } from '../mcp/tool.js'; export type Tool = { diff --git a/src/mcp/tool.ts b/src/mcp/tool.ts new file mode 100644 index 0000000..aff266e --- /dev/null +++ b/src/mcp/tool.ts @@ -0,0 +1,42 @@ +/** + * 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 { zodToJsonSchema } from 'zod-to-json-schema'; + +import type { z } from 'zod'; +import type * as mcpServer from './server.js'; + +export type ToolSchema = { + name: string; + title: string; + description: string; + inputSchema: Input; + type: 'readOnly' | 'destructive'; +}; + +export function toMcpTool(tool: ToolSchema): mcpServer.Tool { + return { + name: tool.name, + description: tool.description, + inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], + annotations: { + title: tool.title, + readOnlyHint: tool.type === 'readOnly', + destructiveHint: tool.type === 'destructive', + openWorldHint: true, + }, + }; +} diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 68d727e..41995ec 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -14,15 +14,13 @@ * 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 * as mcpServer from '../mcp/server.js'; +import type { ToolSchema } from '../mcp/tool.js'; export type FileUploadModalState = { type: 'fileChooser'; @@ -38,28 +36,6 @@ export type DialogModalState = { export type ModalState = FileUploadModalState | DialogModalState; -export type ToolSchema = { - name: string; - title: string; - description: string; - inputSchema: Input; - type: 'readOnly' | 'destructive'; -}; - -export function toMcpTool(tool: ToolSchema): mcpServer.Tool { - return { - name: tool.name, - description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], - annotations: { - title: tool.title, - readOnlyHint: tool.type === 'readOnly', - destructiveHint: tool.type === 'destructive', - openWorldHint: true, - }, - }; -} - export type Tool = { capability: ToolCapability; schema: ToolSchema; diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 4ebf7e5..587db96 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -17,8 +17,6 @@ import os from 'node:os'; import path from 'node:path'; -import type { FullConfig } from '../config.js'; - export function cacheDir() { let cacheDirectory: string; if (process.platform === 'linux') @@ -32,10 +30,6 @@ export function cacheDir() { return path.join(cacheDirectory, 'ms-playwright'); } -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('.');