mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-10-12 00:25:14 +03:00
chore: allow to switch between browser connection methods (#815)
This commit is contained in:
@@ -40,17 +40,21 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {
|
||||
export type ClientInfo = { name?: string, version?: string, rootPath?: string };
|
||||
|
||||
export interface BrowserContextFactory {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>;
|
||||
}
|
||||
|
||||
class BaseContextFactory implements BrowserContextFactory {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly config: FullConfig;
|
||||
protected _browserPromise: Promise<playwright.Browser> | undefined;
|
||||
protected _tracesDir: string | undefined;
|
||||
readonly name: string;
|
||||
|
||||
constructor(name: string, config: FullConfig) {
|
||||
constructor(name: string, description: string, config: FullConfig) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@@ -101,7 +105,7 @@ class BaseContextFactory implements BrowserContextFactory {
|
||||
|
||||
class IsolatedContextFactory extends BaseContextFactory {
|
||||
constructor(config: FullConfig) {
|
||||
super('isolated', config);
|
||||
super('isolated', 'Create a new isolated browser context', config);
|
||||
}
|
||||
|
||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||
@@ -126,7 +130,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
||||
|
||||
class CdpContextFactory extends BaseContextFactory {
|
||||
constructor(config: FullConfig) {
|
||||
super('cdp', config);
|
||||
super('cdp', 'Connect to a browser over CDP', config);
|
||||
}
|
||||
|
||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||
@@ -140,7 +144,7 @@ class CdpContextFactory extends BaseContextFactory {
|
||||
|
||||
class RemoteContextFactory extends BaseContextFactory {
|
||||
constructor(config: FullConfig) {
|
||||
super('remote', config);
|
||||
super('remote', 'Connect to a browser using a remote endpoint', config);
|
||||
}
|
||||
|
||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||
@@ -158,6 +162,9 @@ class RemoteContextFactory extends BaseContextFactory {
|
||||
|
||||
class PersistentContextFactory implements BrowserContextFactory {
|
||||
readonly config: FullConfig;
|
||||
readonly name = 'persistent';
|
||||
readonly description = 'Create a new persistent browser context';
|
||||
|
||||
private _userDataDirs = new Set<string>();
|
||||
|
||||
constructor(config: FullConfig) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { z } from 'zod';
|
||||
import { FullConfig } from './config.js';
|
||||
import { Context } from './context.js';
|
||||
import { logUnhandledError } from './log.js';
|
||||
@@ -22,11 +23,16 @@ import { Response } from './response.js';
|
||||
import { SessionLog } from './sessionLog.js';
|
||||
import { filteredTools } from './tools.js';
|
||||
import { packageJSON } from './package.js';
|
||||
import { defineTool } from './tools/tool.js';
|
||||
|
||||
import type { Tool } from './tools/tool.js';
|
||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||
import type * as mcpServer from './mcp/server.js';
|
||||
import type { ServerBackend } from './mcp/server.js';
|
||||
import type { Tool } from './tools/tool.js';
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
export type FactoryList = NonEmptyArray<BrowserContextFactory>;
|
||||
|
||||
export class BrowserServerBackend implements ServerBackend {
|
||||
name = 'Playwright';
|
||||
@@ -38,10 +44,12 @@ export class BrowserServerBackend implements ServerBackend {
|
||||
private _config: FullConfig;
|
||||
private _browserContextFactory: BrowserContextFactory;
|
||||
|
||||
constructor(config: FullConfig, browserContextFactory: BrowserContextFactory) {
|
||||
constructor(config: FullConfig, factories: FactoryList) {
|
||||
this._config = config;
|
||||
this._browserContextFactory = browserContextFactory;
|
||||
this._browserContextFactory = factories[0];
|
||||
this._tools = filteredTools(config);
|
||||
if (factories.length > 1)
|
||||
this._tools.push(this._defineContextSwitchTool(factories));
|
||||
}
|
||||
|
||||
async initialize(server: mcpServer.Server): Promise<void> {
|
||||
@@ -87,4 +95,46 @@ export class BrowserServerBackend implements ServerBackend {
|
||||
serverClosed() {
|
||||
void this._context!.dispose().catch(logUnhandledError);
|
||||
}
|
||||
|
||||
private _defineContextSwitchTool(factories: FactoryList): Tool<any> {
|
||||
const self = this;
|
||||
return defineTool({
|
||||
capability: 'core',
|
||||
|
||||
schema: {
|
||||
name: 'browser_connect',
|
||||
title: 'Connect to a browser context',
|
||||
description: [
|
||||
'Connect to a browser using one of the available methods:',
|
||||
...factories.map(factory => `- "${factory.name}": ${factory.description}`),
|
||||
].join('\n'),
|
||||
inputSchema: z.object({
|
||||
method: z.enum(factories.map(factory => factory.name) as [string, ...string[]]).default(factories[0].name).describe('The method to use to connect to the browser'),
|
||||
}),
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
async handle(context, params, response) {
|
||||
const factory = factories.find(factory => factory.name === params.method);
|
||||
if (!factory) {
|
||||
response.addError('Unknown connection method: ' + params.method);
|
||||
return;
|
||||
}
|
||||
await self._setContextFactory(factory);
|
||||
response.addResult('Successfully changed connection method.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _setContextFactory(newFactory: BrowserContextFactory) {
|
||||
if (this._context) {
|
||||
const options = {
|
||||
...this._context.options,
|
||||
browserContextFactory: newFactory,
|
||||
};
|
||||
await this._context.dispose();
|
||||
this._context = new Context(options);
|
||||
}
|
||||
this._browserContextFactory = newFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class Context {
|
||||
readonly tools: Tool[];
|
||||
readonly config: FullConfig;
|
||||
readonly sessionLog: SessionLog | undefined;
|
||||
readonly options: ContextOptions;
|
||||
private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
|
||||
private _browserContextFactory: BrowserContextFactory;
|
||||
private _tabs: Tab[] = [];
|
||||
@@ -56,6 +57,7 @@ export class Context {
|
||||
this.tools = options.tools;
|
||||
this.config = options.config;
|
||||
this.sessionLog = options.sessionLog;
|
||||
this.options = options;
|
||||
this._browserContextFactory = options.browserContextFactory;
|
||||
this._clientInfo = options.clientInfo;
|
||||
testDebug('create context');
|
||||
|
||||
@@ -24,6 +24,9 @@ import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory
|
||||
const debugLogger = debug('pw:mcp:relay');
|
||||
|
||||
export class ExtensionContextFactory implements BrowserContextFactory {
|
||||
name = 'extension';
|
||||
description = 'Connect to a browser using the Playwright MCP extension';
|
||||
|
||||
private _browserChannel: string;
|
||||
private _relayPromise: Promise<CDPRelayServer> | undefined;
|
||||
private _browserPromise: Promise<playwright.Browser> | undefined;
|
||||
|
||||
@@ -22,6 +22,10 @@ import type { FullConfig } from '../config.js';
|
||||
|
||||
export async function runWithExtension(config: FullConfig) {
|
||||
const contextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome');
|
||||
const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
|
||||
const serverBackendFactory = () => new BrowserServerBackend(config, [contextFactory]);
|
||||
await mcpTransport.start(serverBackendFactory, config.server);
|
||||
}
|
||||
|
||||
export function createExtensionContextFactory(config: FullConfig) {
|
||||
return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome');
|
||||
}
|
||||
|
||||
@@ -27,10 +27,13 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
|
||||
const config = await resolveConfig(userConfig);
|
||||
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
||||
return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
|
||||
return mcpServer.createServer(new BrowserServerBackend(config, [factory]), false);
|
||||
}
|
||||
|
||||
class SimpleBrowserContextFactory implements BrowserContextFactory {
|
||||
name = 'custom';
|
||||
description = 'Connect to a browser using a custom context getter';
|
||||
|
||||
private readonly _contextGetter: () => Promise<BrowserContext>;
|
||||
|
||||
constructor(contextGetter: () => Promise<BrowserContext>) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export class Context {
|
||||
static async create(config: FullConfig) {
|
||||
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
|
||||
const browserContextFactory = contextFactory(config);
|
||||
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
|
||||
const server = mcpServer.createServer(new BrowserServerBackend(config, [browserContextFactory]), false);
|
||||
await client.connect(new InProcessTransport(server));
|
||||
await client.ping();
|
||||
return new Context(config, client);
|
||||
|
||||
@@ -21,8 +21,8 @@ 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 { runWithExtension } from './extension/main.js';
|
||||
import { BrowserServerBackend } from './browserServerBackend.js';
|
||||
import { createExtensionContextFactory, runWithExtension } from './extension/main.js';
|
||||
import { BrowserServerBackend, FactoryList } from './browserServerBackend.js';
|
||||
import { Context } from './context.js';
|
||||
import { contextFactory } from './browserContextFactory.js';
|
||||
import { runLoopTools } from './loopTools/main.js';
|
||||
@@ -56,6 +56,7 @@ program
|
||||
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
||||
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
|
||||
.addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
|
||||
.addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
|
||||
.addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
|
||||
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
||||
.action(async options => {
|
||||
@@ -78,7 +79,10 @@ program
|
||||
}
|
||||
|
||||
const browserContextFactory = contextFactory(config);
|
||||
const serverBackendFactory = () => new BrowserServerBackend(config, browserContextFactory);
|
||||
const factories: FactoryList = [browserContextFactory];
|
||||
if (options.connectTool)
|
||||
factories.push(createExtensionContextFactory(config));
|
||||
const serverBackendFactory = () => new BrowserServerBackend(config, factories);
|
||||
await mcpTransport.start(serverBackendFactory, config.server);
|
||||
|
||||
if (config.saveTrace) {
|
||||
|
||||
Reference in New Issue
Block a user