mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
chore: allow configuring raw Playwright options (#287)
Fixes: https://github.com/microsoft/playwright-mcp/issues/272
This commit is contained in:
parent
12e72a96c4
commit
80c9b93b72
23
config.d.ts
vendored
23
config.d.ts
vendored
@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as playwright from 'playwright';
|
||||
|
||||
export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
||||
|
||||
export type Config = {
|
||||
@ -24,12 +26,7 @@ export type Config = {
|
||||
/**
|
||||
* The type of browser to use.
|
||||
*/
|
||||
type?: 'chrome' | 'chrome-beta' | 'chrome-canary' | 'chrome-dev' | 'chromium' | 'msedge' | 'msedge-beta' | 'msedge-canary' | 'msedge-dev' | 'firefox' | 'webkit';
|
||||
|
||||
/**
|
||||
* Path to a custom browser executable.
|
||||
*/
|
||||
executablePath?: string;
|
||||
browserName?: 'chromium' | 'firefox' | 'webkit';
|
||||
|
||||
/**
|
||||
* Path to a user data directory for browser profile persistence.
|
||||
@ -37,9 +34,19 @@ export type Config = {
|
||||
userDataDir?: string;
|
||||
|
||||
/**
|
||||
* Whether to run the browser in headless mode (default: true).
|
||||
* Launch options passed to
|
||||
* @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
|
||||
*
|
||||
* This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
|
||||
*/
|
||||
headless?: boolean;
|
||||
launchOptions?: playwright.BrowserLaunchOptions;
|
||||
|
||||
/**
|
||||
* Context options for the browser context.
|
||||
*
|
||||
* This is useful for settings options like `viewport`.
|
||||
*/
|
||||
contextOptions?: playwright.BrowserContextOptions;
|
||||
|
||||
/**
|
||||
* Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
|
||||
|
116
src/config.ts
116
src/config.ts
@ -21,19 +21,46 @@ import path from 'path';
|
||||
|
||||
import { sanitizeForFilePath } from './tools/utils';
|
||||
|
||||
import type { Config } from '../config';
|
||||
import type { LaunchOptions, BrowserContextOptions } from 'playwright';
|
||||
import type { Config, ToolCapability } from '../config';
|
||||
import type { LaunchOptions } from 'playwright';
|
||||
|
||||
export type BrowserOptions = {
|
||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
||||
launchOptions: LaunchOptions;
|
||||
contextOptions: BrowserContextOptions;
|
||||
export type CLIOptions = {
|
||||
browser?: string;
|
||||
caps?: string;
|
||||
cdpEndpoint?: string;
|
||||
executablePath?: string;
|
||||
headless?: boolean;
|
||||
userDataDir?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
vision?: boolean;
|
||||
config?: string;
|
||||
};
|
||||
|
||||
export async function toBrowserOptions(config: Config): Promise<BrowserOptions> {
|
||||
const defaultConfig: Config = {
|
||||
browser: {
|
||||
browserName: 'chromium',
|
||||
userDataDir: os.tmpdir(),
|
||||
launchOptions: {
|
||||
channel: 'chrome',
|
||||
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
||||
},
|
||||
contextOptions: {
|
||||
viewport: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
|
||||
const config = await loadConfig(cliOptions.config);
|
||||
const cliOverrides = await configFromCLIOptions(cliOptions);
|
||||
return mergeConfig(defaultConfig, mergeConfig(config, cliOverrides));
|
||||
}
|
||||
|
||||
export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Config> {
|
||||
let browserName: 'chromium' | 'firefox' | 'webkit';
|
||||
let channel: string | undefined;
|
||||
switch (config.browser?.type) {
|
||||
switch (cliOptions.browser) {
|
||||
case 'chrome':
|
||||
case 'chrome-beta':
|
||||
case 'chrome-canary':
|
||||
@ -44,7 +71,7 @@ export async function toBrowserOptions(config: Config): Promise<BrowserOptions>
|
||||
case 'msedge-canary':
|
||||
case 'msedge-dev':
|
||||
browserName = 'chromium';
|
||||
channel = config.browser.type;
|
||||
channel = cliOptions.browser;
|
||||
break;
|
||||
case 'firefox':
|
||||
browserName = 'firefox';
|
||||
@ -58,23 +85,26 @@ export async function toBrowserOptions(config: Config): Promise<BrowserOptions>
|
||||
}
|
||||
|
||||
const launchOptions: LaunchOptions = {
|
||||
headless: !!(config.browser?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)),
|
||||
channel,
|
||||
executablePath: config.browser?.executablePath,
|
||||
...{ assistantMode: true },
|
||||
};
|
||||
|
||||
const contextOptions: BrowserContextOptions = {
|
||||
viewport: null,
|
||||
executablePath: cliOptions.executablePath,
|
||||
};
|
||||
|
||||
if (browserName === 'chromium')
|
||||
(launchOptions as any).webSocketPort = await findFreePort();
|
||||
|
||||
return {
|
||||
browserName,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
browser: {
|
||||
browserName,
|
||||
userDataDir: cliOptions.userDataDir ?? await createUserDataDir(browserName),
|
||||
launchOptions,
|
||||
cdpEndpoint: cliOptions.cdpEndpoint,
|
||||
},
|
||||
server: {
|
||||
port: cliOptions.port,
|
||||
host: cliOptions.host,
|
||||
},
|
||||
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
||||
vision: !!cliOptions.vision,
|
||||
};
|
||||
}
|
||||
|
||||
@ -89,9 +119,57 @@ async function findFreePort() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadConfig(configFile: string | undefined): Promise<Config> {
|
||||
if (!configFile)
|
||||
return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(configFile, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config file: ${configFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createUserDataDir(browserName: 'chromium' | 'firefox' | 'webkit') {
|
||||
let cacheDirectory: string;
|
||||
if (process.platform === 'linux')
|
||||
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
||||
else if (process.platform === 'darwin')
|
||||
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
||||
else if (process.platform === 'win32')
|
||||
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + process.platform);
|
||||
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
|
||||
await fs.promises.mkdir(result, { recursive: true });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function outputFile(config: Config, name: string): Promise<string> {
|
||||
const result = config.outputDir ?? os.tmpdir();
|
||||
await fs.promises.mkdir(result, { recursive: true });
|
||||
const fileName = sanitizeForFilePath(name);
|
||||
return path.join(result, fileName);
|
||||
}
|
||||
|
||||
function mergeConfig(base: Config, overrides: Config): Config {
|
||||
const browser: Config['browser'] = {
|
||||
...base.browser,
|
||||
...overrides.browser,
|
||||
launchOptions: {
|
||||
...base.browser?.launchOptions,
|
||||
...overrides.browser?.launchOptions,
|
||||
...{ assistantMode: true },
|
||||
},
|
||||
contextOptions: {
|
||||
...base.browser?.contextOptions,
|
||||
...overrides.browser?.contextOptions,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
browser,
|
||||
};
|
||||
}
|
||||
|
@ -14,21 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import * as playwright from 'playwright';
|
||||
|
||||
import { waitForCompletion } from './tools/utils';
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import { toBrowserOptions } from './config';
|
||||
import { Tab } from './tab';
|
||||
|
||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
|
||||
import type { ModalState, Tool, ToolActionResult } from './tools/tool';
|
||||
import type { Config } from '../config';
|
||||
import type { BrowserOptions } from './config';
|
||||
|
||||
type PendingAction = {
|
||||
dialogShown: ManualPromise<void>;
|
||||
@ -285,51 +279,32 @@ ${code.join('\n')}
|
||||
}
|
||||
|
||||
private async _innerCreateBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> {
|
||||
const browserOptions = await toBrowserOptions(this.config);
|
||||
|
||||
if (this.config.browser?.remoteEndpoint) {
|
||||
const url = new URL(this.config.browser?.remoteEndpoint);
|
||||
if (browserOptions.browserName)
|
||||
url.searchParams.set('browser', browserOptions.browserName);
|
||||
if (browserOptions.launchOptions)
|
||||
url.searchParams.set('launch-options', JSON.stringify(browserOptions.launchOptions));
|
||||
const browser = await playwright[browserOptions.browserName ?? 'chromium'].connect(String(url));
|
||||
if (this.config.browser.browserName)
|
||||
url.searchParams.set('browser', this.config.browser.browserName);
|
||||
if (this.config.browser.launchOptions)
|
||||
url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
|
||||
const browser = await playwright[this.config.browser?.browserName ?? 'chromium'].connect(String(url));
|
||||
const browserContext = await browser.newContext();
|
||||
return { browser, browserContext };
|
||||
}
|
||||
|
||||
if (this.config.browser?.cdpEndpoint) {
|
||||
const browser = await playwright.chromium.connectOverCDP(this.config.browser?.cdpEndpoint);
|
||||
const browser = await playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
||||
const browserContext = browser.contexts()[0];
|
||||
return { browser, browserContext };
|
||||
}
|
||||
|
||||
const browserContext = await launchPersistentContext(this.config.browser?.userDataDir, browserOptions);
|
||||
const browserContext = await launchPersistentContext(this.config.browser);
|
||||
return { browserContext };
|
||||
}
|
||||
}
|
||||
|
||||
async function createUserDataDir(browserName: 'chromium' | 'firefox' | 'webkit') {
|
||||
let cacheDirectory: string;
|
||||
if (process.platform === 'linux')
|
||||
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
||||
else if (process.platform === 'darwin')
|
||||
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
||||
else if (process.platform === 'win32')
|
||||
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
||||
else
|
||||
throw new Error('Unsupported platform: ' + process.platform);
|
||||
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`);
|
||||
await fs.promises.mkdir(result, { recursive: true });
|
||||
return result;
|
||||
}
|
||||
|
||||
async function launchPersistentContext(userDataDir: string | undefined, browserOptions: BrowserOptions): Promise<playwright.BrowserContext> {
|
||||
userDataDir = userDataDir ?? await createUserDataDir(browserOptions.browserName);
|
||||
|
||||
async function launchPersistentContext(browserConfig: Config['browser']): Promise<playwright.BrowserContext> {
|
||||
try {
|
||||
const browserType = browserOptions.browserName ? playwright[browserOptions.browserName] : playwright.chromium;
|
||||
return await browserType.launchPersistentContext(userDataDir, browserOptions.launchOptions);
|
||||
const browserType = browserConfig?.browserName ? playwright[browserConfig.browserName] : playwright.chromium;
|
||||
return await browserType.launchPersistentContext(browserConfig?.userDataDir || '', browserConfig?.launchOptions);
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('Executable doesn\'t exist'))
|
||||
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
||||
|
@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { program } from 'commander';
|
||||
|
||||
import { createServer } from './index';
|
||||
@ -23,7 +21,7 @@ import { ServerList } from './server';
|
||||
|
||||
import { startHttpTransport, startStdioTransport } from './transport';
|
||||
|
||||
import type { Config, ToolCapability } from '../config';
|
||||
import { resolveConfig } from './config';
|
||||
|
||||
const packageJSON = require('../package.json');
|
||||
|
||||
@ -41,22 +39,7 @@ program
|
||||
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
||||
.option('--config <path>', 'Path to the configuration file.')
|
||||
.action(async options => {
|
||||
const cliOverrides: Config = {
|
||||
browser: {
|
||||
type: options.browser,
|
||||
userDataDir: options.userDataDir,
|
||||
headless: options.headless,
|
||||
executablePath: options.executablePath,
|
||||
cdpEndpoint: options.cdpEndpoint,
|
||||
},
|
||||
server: {
|
||||
port: options.port,
|
||||
host: options.host,
|
||||
},
|
||||
capabilities: options.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
||||
vision: !!options.vision,
|
||||
};
|
||||
const config = await loadConfig(options.config, cliOverrides);
|
||||
const config = await resolveConfig(options);
|
||||
const serverList = new ServerList(() => createServer(config));
|
||||
setupExitWatchdog(serverList);
|
||||
|
||||
@ -66,30 +49,6 @@ program
|
||||
await startStdioTransport(serverList);
|
||||
});
|
||||
|
||||
async function loadConfig(configFile: string | undefined, cliOverrides: Config): Promise<Config> {
|
||||
if (!configFile)
|
||||
return cliOverrides;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(await fs.promises.readFile(configFile, 'utf8'));
|
||||
return {
|
||||
...config,
|
||||
...cliOverrides,
|
||||
browser: {
|
||||
...config.browser,
|
||||
...cliOverrides.browser,
|
||||
},
|
||||
server: {
|
||||
...config.server,
|
||||
...cliOverrides.server,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Error loading config file ${configFile}: ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function setupExitWatchdog(serverList: ServerList) {
|
||||
const handleExit = async () => {
|
||||
setTimeout(() => process.exit(0), 15000);
|
||||
|
@ -19,7 +19,6 @@ import path from 'path';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool';
|
||||
import { toBrowserOptions } from '../config';
|
||||
|
||||
const install = defineTool({
|
||||
capability: 'install',
|
||||
@ -30,8 +29,7 @@ const install = defineTool({
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
const browserOptions = await toBrowserOptions(context.config);
|
||||
const channel = browserOptions.launchOptions?.channel ?? browserOptions.browserName ?? 'chrome';
|
||||
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
|
||||
const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
||||
const child = fork(cli, ['install', channel], {
|
||||
stdio: 'pipe',
|
||||
|
Loading…
x
Reference in New Issue
Block a user