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 type ClientInfo = { name?: string, version?: string, rootPath?: string };
|
||||||
|
|
||||||
export interface BrowserContextFactory {
|
export interface BrowserContextFactory {
|
||||||
|
readonly name: string;
|
||||||
|
readonly description: string;
|
||||||
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>;
|
createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BaseContextFactory implements BrowserContextFactory {
|
class BaseContextFactory implements BrowserContextFactory {
|
||||||
|
readonly name: string;
|
||||||
|
readonly description: string;
|
||||||
readonly config: FullConfig;
|
readonly config: FullConfig;
|
||||||
protected _browserPromise: Promise<playwright.Browser> | undefined;
|
protected _browserPromise: Promise<playwright.Browser> | undefined;
|
||||||
protected _tracesDir: string | undefined;
|
protected _tracesDir: string | undefined;
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
constructor(name: string, config: FullConfig) {
|
constructor(name: string, description: string, config: FullConfig) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ class BaseContextFactory implements BrowserContextFactory {
|
|||||||
|
|
||||||
class IsolatedContextFactory extends BaseContextFactory {
|
class IsolatedContextFactory extends BaseContextFactory {
|
||||||
constructor(config: FullConfig) {
|
constructor(config: FullConfig) {
|
||||||
super('isolated', config);
|
super('isolated', 'Create a new isolated browser context', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||||
@@ -126,7 +130,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|||||||
|
|
||||||
class CdpContextFactory extends BaseContextFactory {
|
class CdpContextFactory extends BaseContextFactory {
|
||||||
constructor(config: FullConfig) {
|
constructor(config: FullConfig) {
|
||||||
super('cdp', config);
|
super('cdp', 'Connect to a browser over CDP', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||||
@@ -140,7 +144,7 @@ class CdpContextFactory extends BaseContextFactory {
|
|||||||
|
|
||||||
class RemoteContextFactory extends BaseContextFactory {
|
class RemoteContextFactory extends BaseContextFactory {
|
||||||
constructor(config: FullConfig) {
|
constructor(config: FullConfig) {
|
||||||
super('remote', config);
|
super('remote', 'Connect to a browser using a remote endpoint', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
|
||||||
@@ -158,6 +162,9 @@ class RemoteContextFactory extends BaseContextFactory {
|
|||||||
|
|
||||||
class PersistentContextFactory implements BrowserContextFactory {
|
class PersistentContextFactory implements BrowserContextFactory {
|
||||||
readonly config: FullConfig;
|
readonly config: FullConfig;
|
||||||
|
readonly name = 'persistent';
|
||||||
|
readonly description = 'Create a new persistent browser context';
|
||||||
|
|
||||||
private _userDataDirs = new Set<string>();
|
private _userDataDirs = new Set<string>();
|
||||||
|
|
||||||
constructor(config: FullConfig) {
|
constructor(config: FullConfig) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { z } from 'zod';
|
||||||
import { FullConfig } from './config.js';
|
import { FullConfig } from './config.js';
|
||||||
import { Context } from './context.js';
|
import { Context } from './context.js';
|
||||||
import { logUnhandledError } from './log.js';
|
import { logUnhandledError } from './log.js';
|
||||||
@@ -22,11 +23,16 @@ import { Response } from './response.js';
|
|||||||
import { SessionLog } from './sessionLog.js';
|
import { SessionLog } from './sessionLog.js';
|
||||||
import { filteredTools } from './tools.js';
|
import { filteredTools } from './tools.js';
|
||||||
import { packageJSON } from './package.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 { BrowserContextFactory } from './browserContextFactory.js';
|
||||||
import type * as mcpServer from './mcp/server.js';
|
import type * as mcpServer from './mcp/server.js';
|
||||||
import type { ServerBackend } 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 {
|
export class BrowserServerBackend implements ServerBackend {
|
||||||
name = 'Playwright';
|
name = 'Playwright';
|
||||||
@@ -38,10 +44,12 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
private _config: FullConfig;
|
private _config: FullConfig;
|
||||||
private _browserContextFactory: BrowserContextFactory;
|
private _browserContextFactory: BrowserContextFactory;
|
||||||
|
|
||||||
constructor(config: FullConfig, browserContextFactory: BrowserContextFactory) {
|
constructor(config: FullConfig, factories: FactoryList) {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._browserContextFactory = browserContextFactory;
|
this._browserContextFactory = factories[0];
|
||||||
this._tools = filteredTools(config);
|
this._tools = filteredTools(config);
|
||||||
|
if (factories.length > 1)
|
||||||
|
this._tools.push(this._defineContextSwitchTool(factories));
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(server: mcpServer.Server): Promise<void> {
|
async initialize(server: mcpServer.Server): Promise<void> {
|
||||||
@@ -87,4 +95,46 @@ export class BrowserServerBackend implements ServerBackend {
|
|||||||
serverClosed() {
|
serverClosed() {
|
||||||
void this._context!.dispose().catch(logUnhandledError);
|
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 tools: Tool[];
|
||||||
readonly config: FullConfig;
|
readonly config: FullConfig;
|
||||||
readonly sessionLog: SessionLog | undefined;
|
readonly sessionLog: SessionLog | undefined;
|
||||||
|
readonly options: ContextOptions;
|
||||||
private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
|
private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
|
||||||
private _browserContextFactory: BrowserContextFactory;
|
private _browserContextFactory: BrowserContextFactory;
|
||||||
private _tabs: Tab[] = [];
|
private _tabs: Tab[] = [];
|
||||||
@@ -56,6 +57,7 @@ export class Context {
|
|||||||
this.tools = options.tools;
|
this.tools = options.tools;
|
||||||
this.config = options.config;
|
this.config = options.config;
|
||||||
this.sessionLog = options.sessionLog;
|
this.sessionLog = options.sessionLog;
|
||||||
|
this.options = options;
|
||||||
this._browserContextFactory = options.browserContextFactory;
|
this._browserContextFactory = options.browserContextFactory;
|
||||||
this._clientInfo = options.clientInfo;
|
this._clientInfo = options.clientInfo;
|
||||||
testDebug('create context');
|
testDebug('create context');
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory
|
|||||||
const debugLogger = debug('pw:mcp:relay');
|
const debugLogger = debug('pw:mcp:relay');
|
||||||
|
|
||||||
export class ExtensionContextFactory implements BrowserContextFactory {
|
export class ExtensionContextFactory implements BrowserContextFactory {
|
||||||
|
name = 'extension';
|
||||||
|
description = 'Connect to a browser using the Playwright MCP extension';
|
||||||
|
|
||||||
private _browserChannel: string;
|
private _browserChannel: string;
|
||||||
private _relayPromise: Promise<CDPRelayServer> | undefined;
|
private _relayPromise: Promise<CDPRelayServer> | undefined;
|
||||||
private _browserPromise: Promise<playwright.Browser> | undefined;
|
private _browserPromise: Promise<playwright.Browser> | undefined;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import type { FullConfig } from '../config.js';
|
|||||||
|
|
||||||
export async function runWithExtension(config: FullConfig) {
|
export async function runWithExtension(config: FullConfig) {
|
||||||
const contextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome');
|
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);
|
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> {
|
export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
|
||||||
const config = await resolveConfig(userConfig);
|
const config = await resolveConfig(userConfig);
|
||||||
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
|
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 {
|
class SimpleBrowserContextFactory implements BrowserContextFactory {
|
||||||
|
name = 'custom';
|
||||||
|
description = 'Connect to a browser using a custom context getter';
|
||||||
|
|
||||||
private readonly _contextGetter: () => Promise<BrowserContext>;
|
private readonly _contextGetter: () => Promise<BrowserContext>;
|
||||||
|
|
||||||
constructor(contextGetter: () => Promise<BrowserContext>) {
|
constructor(contextGetter: () => Promise<BrowserContext>) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class Context {
|
|||||||
static async create(config: FullConfig) {
|
static async create(config: FullConfig) {
|
||||||
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
|
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
|
||||||
const browserContextFactory = contextFactory(config);
|
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.connect(new InProcessTransport(server));
|
||||||
await client.ping();
|
await client.ping();
|
||||||
return new Context(config, client);
|
return new Context(config, client);
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { startTraceViewerServer } from 'playwright-core/lib/server';
|
|||||||
import * as mcpTransport from './mcp/transport.js';
|
import * as mcpTransport from './mcp/transport.js';
|
||||||
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
|
||||||
import { packageJSON } from './package.js';
|
import { packageJSON } from './package.js';
|
||||||
import { runWithExtension } from './extension/main.js';
|
import { createExtensionContextFactory, runWithExtension } from './extension/main.js';
|
||||||
import { BrowserServerBackend } from './browserServerBackend.js';
|
import { BrowserServerBackend, FactoryList } from './browserServerBackend.js';
|
||||||
import { Context } from './context.js';
|
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';
|
||||||
@@ -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('--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"')
|
.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('--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('--loop-tools', 'Run loop tools').hideHelp())
|
||||||
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
|
||||||
.action(async options => {
|
.action(async options => {
|
||||||
@@ -78,7 +79,10 @@ program
|
|||||||
}
|
}
|
||||||
|
|
||||||
const browserContextFactory = contextFactory(config);
|
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);
|
await mcpTransport.start(serverBackendFactory, config.server);
|
||||||
|
|
||||||
if (config.saveTrace) {
|
if (config.saveTrace) {
|
||||||
|
|||||||
Reference in New Issue
Block a user