From e884b3aacbd80c28821b1676480f8786e213c011 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 12 Aug 2025 10:00:58 +0200 Subject: [PATCH] get started --- src/mcp/proxyBackend.ts | 12 +++++---- src/mcp/server.ts | 4 ++- src/program.ts | 4 ++- src/vscode/host.ts | 55 ++++++++++++++++++++++++++++++++++++++ src/vscode/main.ts | 58 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 src/vscode/host.ts create mode 100644 src/vscode/main.ts diff --git a/src/mcp/proxyBackend.ts b/src/mcp/proxyBackend.ts index c2e6eef..2372627 100644 --- a/src/mcp/proxyBackend.ts +++ b/src/mcp/proxyBackend.ts @@ -29,7 +29,7 @@ type NonEmptyArray = [T, ...T[]]; export type ClientFactory = { name: string; description: string; - create(): Promise; + create(options: any): Promise; }; export type ClientFactoryList = NonEmptyArray; @@ -49,7 +49,7 @@ export class ProxyBackend implements ServerBackend { } async initialize(server: Server): Promise { - await this._setCurrentClient(this._clientFactories[0]); + await this._setCurrentClient(this._clientFactories[0], undefined); } tools(): ToolSchema[] { @@ -81,7 +81,7 @@ export class ProxyBackend implements ServerBackend { if (!factory) throw new Error('Unknown connection method: ' + params.name); - await this._setCurrentClient(factory); + await this._setCurrentClient(factory, params.options); return { content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }], }; @@ -103,9 +103,11 @@ export class ProxyBackend implements ServerBackend { description: [ 'Connect to a browser using one of the available methods:', ...this._clientFactories.map(factory => `- "${factory.name}": ${factory.description}`), + `By default, you're connected to the first method. Only call this tool to change it.`, ].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'), + options: z.any().optional().describe('Options to pass to the connection method.'), }), type: 'readOnly', }, @@ -116,9 +118,9 @@ export class ProxyBackend implements ServerBackend { }); } - private async _setCurrentClient(factory: ClientFactory) { + private async _setCurrentClient(factory: ClientFactory, options: any) { await this._currentClient?.close(); - this._currentClient = await factory.create(); + this._currentClient = await factory.create(options); const tools = await this._currentClient.listTools(); this._tools = tools.tools.map(tool => ({ name: tool.name, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 5da54be..023e7fd 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -76,7 +76,9 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser return { tools: tools.map(tool => ({ name: tool.name, description: tool.description, - inputSchema: zodToJsonSchema(tool.inputSchema), + // TODO: we expect inputSchema to be a zod schema, but in the out-of-process case it's already a json schema. + // we should probably move the "zodToJsonSchema" call into defineTool. + inputSchema: tool.inputSchema.$schema ? tool.inputSchema : zodToJsonSchema(tool.inputSchema), annotations: { title: tool.title, readOnlyHint: tool.type === 'readOnly', diff --git a/src/program.ts b/src/program.ts index 24ebbae..ce464fc 100644 --- a/src/program.ts +++ b/src/program.ts @@ -21,6 +21,7 @@ import { startTraceViewerServer } from 'playwright-core/lib/server'; import * as mcpTransport from './mcp/transport.js'; import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js'; import { packageJSON } from './package.js'; +import { createVSCodeClientFactory } from './vscode/host.js'; import { createExtensionClientFactory, runWithExtension } from './extension/main.js'; import { Context } from './context.js'; import { contextFactory } from './browserContextFactory.js'; @@ -88,7 +89,8 @@ program if (options.connectTool) { const factories: ClientFactoryList = [ new InProcessClientFactory(browserContextFactory, config), - createExtensionClientFactory(config) + createExtensionClientFactory(config), + createVSCodeClientFactory(config), ]; serverBackendFactory = () => new ProxyBackend(factories); } else { diff --git a/src/vscode/host.ts b/src/vscode/host.ts new file mode 100644 index 0000000..14b2b25 --- /dev/null +++ b/src/vscode/host.ts @@ -0,0 +1,55 @@ +/** + * 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 { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { FullConfig } from '../config.js'; +import { ClientFactory } from '../mcp/proxyBackend.js'; +import { packageJSON } from '../package.js'; + +class VSCodeClientFactory implements ClientFactory { + name = 'vscode'; + description = 'Connect to a browser running in the Playwright VS Code extension'; + + constructor(private readonly _config: FullConfig) {} + + async create(options: any): Promise { + if (typeof options.connectionString !== 'string') + throw new Error('Missing options.connectionString'); + if (typeof options.lib !== 'string') + throw new Error('Missing options.library'); + + const client = new Client({ + name: this.name, + version: packageJSON.version + }); + await client.connect(new StdioClientTransport({ + command: process.execPath, + cwd: process.cwd(), + args: [ + new URL('./main.js', import.meta.url).pathname, + JSON.stringify(this._config), + options.connectionString, + options.lib, + ], + })); + await client.ping(); + return client; + } +} + +export function createVSCodeClientFactory(config: FullConfig): ClientFactory { + return new VSCodeClientFactory(config); +} diff --git a/src/vscode/main.ts b/src/vscode/main.ts new file mode 100644 index 0000000..e0755fe --- /dev/null +++ b/src/vscode/main.ts @@ -0,0 +1,58 @@ +/** + * 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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { BrowserContext } from 'playwright-core'; +import { FullConfig } from '../config.js'; +import * as mcpServer from '../mcp/server.js'; +import { BrowserServerBackend } from '../browserServerBackend.js'; +import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js'; + +const config: FullConfig = JSON.parse(process.argv[2]); +const connectionString = new URL(process.argv[3]); +const lib = process.argv[4]; + +const playwright = await import(lib).then(mod => mod.default ?? mod) as typeof import('playwright'); + +class VSCodeBrowserContextFactory implements BrowserContextFactory { + name = 'unused'; + description = 'unused'; + + async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise; }> { + connectionString.searchParams.set('launch-options', JSON.stringify({ + ...config.browser.launchOptions, + ...config.browser.contextOptions, + userDataDir: config.browser.userDataDir, + })); + + const browser = await playwright.chromium.connect(connectionString.toString()); + + const context = browser.contexts()[0] ?? await browser.newContext(config.browser.contextOptions); + + return { + browserContext: context, + close: async () => { + await browser.close(); + } + }; + } +} + +await mcpServer.connect( + () => new BrowserServerBackend(config, new VSCodeBrowserContextFactory()), + new StdioServerTransport(), + false +);