chore: separate proxy client from external (#877)

This commit is contained in:
Yury Semikhatsky
2025-08-12 18:05:45 -07:00
committed by GitHub
parent c091a11d76
commit 8572ab300c
4 changed files with 58 additions and 55 deletions

View File

@@ -46,11 +46,9 @@ export class BrowserServerBackend implements ServerBackend {
} }
async initialize(server: mcpServer.Server): Promise<void> { async initialize(server: mcpServer.Server): Promise<void> {
const capabilities = server.getClientCapabilities() as mcpServer.ClientCapabilities; const capabilities = server.getClientCapabilities();
let rootPath: string | undefined; let rootPath: string | undefined;
if (capabilities.roots && ( if (capabilities?.roots) {
server.getClientVersion()?.name === 'Visual Studio Code' ||
server.getClientVersion()?.name === 'Visual Studio Code - Insiders')) {
const { roots } = await server.listRoots(); const { roots } = await server.listRoots();
const firstRootUri = roots[0]?.uri; const firstRootUri = roots[0]?.uri;
const url = firstRootUri ? new URL(firstRootUri) : undefined; const url = firstRootUri ? new URL(firstRootUri) : undefined;
@@ -89,6 +87,6 @@ export class BrowserServerBackend implements ServerBackend {
} }
serverClosed() { serverClosed() {
void this._context!.dispose().catch(logUnhandledError); void this._context?.dispose().catch(logUnhandledError);
} }
} }

View File

@@ -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 { BrowserContextFactory } from './browserContextFactory.js';
import { BrowserServerBackend } from './browserServerBackend.js'; import { BrowserServerBackend } from './browserServerBackend.js';
import { InProcessTransport } from './mcp/inProcessTransport.js'; import { InProcessTransport } from './mcp/inProcessTransport.js';
import * as mcpServer from './mcp/server.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 { 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; name: string;
description: string; description: string;
@@ -40,21 +38,8 @@ export class InProcessClientFactory implements ClientFactory {
this._config = config; this._config = config;
} }
async create(server: Server): Promise<Client> { async create(): Promise<Transport> {
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();
});
}
const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false); const delegate = mcpServer.createServer(new BrowserServerBackend(this._config, this._contextFactory), false);
await client.connect(new InProcessTransport(delegate)); return new InProcessTransport(delegate);
await client.ping();
return client;
} }
} }

View File

@@ -14,48 +14,51 @@
* limitations under the License. * limitations under the License.
*/ */
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { z } from 'zod'; import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema'; 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 { logUnhandledError } from '../utils/log.js';
import { packageJSON } from '../utils/package.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, ...T[]]; type NonEmptyArray<T> = [T, ...T[]];
export type ClientFactory = { export type MCPFactory = {
name: string; name: string;
description: string; description: string;
create(server: Server): Promise<Client>; create(): Promise<Transport>;
}; };
export type ClientFactoryList = NonEmptyArray<ClientFactory>; export type MCPFactoryList = NonEmptyArray<MCPFactory>;
export class ProxyBackend implements ServerBackend { export class ProxyBackend implements ServerBackend {
name = 'Playwright MCP Client Switcher'; name = 'Playwright MCP Client Switcher';
version = packageJSON.version; version = packageJSON.version;
private _clientFactories: ClientFactoryList; private _mcpFactories: MCPFactoryList;
private _currentClient: Client | undefined; private _currentClient: Client | undefined;
private _contextSwitchTool: ToolDefinition; private _contextSwitchTool: ToolDefinition;
private _tools: ToolDefinition[] = []; private _tools: ToolDefinition[] = [];
private _server: Server | undefined; private _server: Server | undefined;
constructor(clientFactories: ClientFactoryList) { constructor(clientFactories: MCPFactoryList) {
this._clientFactories = clientFactories; this._mcpFactories = clientFactories;
this._contextSwitchTool = this._defineContextSwitchTool(); this._contextSwitchTool = this._defineContextSwitchTool();
} }
async initialize(server: Server): Promise<void> { async initialize(server: Server): Promise<void> {
this._server = server; this._server = server;
await this._setCurrentClient(this._clientFactories[0]); await this._setCurrentClient(this._mcpFactories[0]);
} }
tools(): ToolDefinition[] { tools(): ToolDefinition[] {
if (this._clientFactories.length === 1) if (this._mcpFactories.length === 1)
return this._tools; return this._tools;
return [ return [
...this._tools, ...this._tools,
@@ -79,7 +82,7 @@ export class ProxyBackend implements ServerBackend {
private async _callContextSwitchTool(params: any): Promise<ToolResponse> { private async _callContextSwitchTool(params: any): Promise<ToolResponse> {
try { try {
const factory = this._clientFactories.find(factory => factory.name === params.name); const factory = this._mcpFactories.find(factory => factory.name === params.name);
if (!factory) if (!factory)
throw new Error('Unknown connection method: ' + params.name); throw new Error('Unknown connection method: ' + params.name);
@@ -100,10 +103,10 @@ export class ProxyBackend implements ServerBackend {
name: 'browser_connect', name: 'browser_connect',
description: [ description: [
'Connect to a browser using one of the available methods:', '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'), ].join('\n'),
inputSchema: zodToJsonSchema(z.object({ 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'], }), { strictUnions: true }) as ToolDefinition['inputSchema'],
annotations: { annotations: {
title: 'Connect to a browser context', 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(); 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(); const tools = await this._currentClient.listTools();
this._tools = tools.tools; this._tools = tools.tools;
} }

View File

@@ -23,12 +23,11 @@ import { Context } from './context.js';
import { contextFactory } from './browserContextFactory.js'; import { contextFactory } from './browserContextFactory.js';
import { runLoopTools } from './loopTools/main.js'; import { runLoopTools } from './loopTools/main.js';
import { ProxyBackend } from './mcp/proxyBackend.js'; import { ProxyBackend } from './mcp/proxyBackend.js';
import { InProcessClientFactory } from './inProcessClient.js'; import { InProcessMCPFactory } from './inProcessMcpFactrory.js';
import { BrowserServerBackend } from './browserServerBackend.js'; import { BrowserServerBackend } from './browserServerBackend.js';
import { ExtensionContextFactory } from './extension/extensionContextFactory.js'; import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
import type { ClientFactoryList } from './mcp/proxyBackend.js'; import type { MCPFactoryList } from './mcp/proxyBackend.js';
import type { ServerBackendFactory } from './mcp/server.js';
import type { FullConfig } from './config.js'; import type { FullConfig } from './config.js';
program program
@@ -84,18 +83,13 @@ program
return; return;
} }
let serverBackendFactory: ServerBackendFactory;
const browserContextFactory = contextFactory(config); const browserContextFactory = contextFactory(config);
if (options.connectTool) { const factories: MCPFactoryList = [
const factories: ClientFactoryList = [ new InProcessMCPFactory(browserContextFactory, config),
new InProcessClientFactory(browserContextFactory, config), ];
new InProcessClientFactory(createExtensionContextFactory(config), config), if (options.connectTool)
]; factories.push(new InProcessMCPFactory(createExtensionContextFactory(config), config));
serverBackendFactory = () => new ProxyBackend(factories); await mcpTransport.start(() => new ProxyBackend(factories), config.server);
} else {
serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
}
await mcpTransport.start(serverBackendFactory, config.server);
}); });
function setupExitWatchdog() { function setupExitWatchdog() {