mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-27 00:52:27 +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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type * as playwright from 'playwright';
|
||||||
|
|
||||||
export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
@ -24,12 +26,7 @@ export type Config = {
|
|||||||
/**
|
/**
|
||||||
* The type of browser to use.
|
* The type of browser to use.
|
||||||
*/
|
*/
|
||||||
type?: 'chrome' | 'chrome-beta' | 'chrome-canary' | 'chrome-dev' | 'chromium' | 'msedge' | 'msedge-beta' | 'msedge-canary' | 'msedge-dev' | 'firefox' | 'webkit';
|
browserName?: 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
||||||
/**
|
|
||||||
* Path to a custom browser executable.
|
|
||||||
*/
|
|
||||||
executablePath?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to a user data directory for browser profile persistence.
|
* Path to a user data directory for browser profile persistence.
|
||||||
@ -37,9 +34,19 @@ export type Config = {
|
|||||||
userDataDir?: string;
|
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.
|
* 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 { sanitizeForFilePath } from './tools/utils';
|
||||||
|
|
||||||
import type { Config } from '../config';
|
import type { Config, ToolCapability } from '../config';
|
||||||
import type { LaunchOptions, BrowserContextOptions } from 'playwright';
|
import type { LaunchOptions } from 'playwright';
|
||||||
|
|
||||||
export type BrowserOptions = {
|
export type CLIOptions = {
|
||||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
browser?: string;
|
||||||
launchOptions: LaunchOptions;
|
caps?: string;
|
||||||
contextOptions: BrowserContextOptions;
|
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 browserName: 'chromium' | 'firefox' | 'webkit';
|
||||||
let channel: string | undefined;
|
let channel: string | undefined;
|
||||||
switch (config.browser?.type) {
|
switch (cliOptions.browser) {
|
||||||
case 'chrome':
|
case 'chrome':
|
||||||
case 'chrome-beta':
|
case 'chrome-beta':
|
||||||
case 'chrome-canary':
|
case 'chrome-canary':
|
||||||
@ -44,7 +71,7 @@ export async function toBrowserOptions(config: Config): Promise<BrowserOptions>
|
|||||||
case 'msedge-canary':
|
case 'msedge-canary':
|
||||||
case 'msedge-dev':
|
case 'msedge-dev':
|
||||||
browserName = 'chromium';
|
browserName = 'chromium';
|
||||||
channel = config.browser.type;
|
channel = cliOptions.browser;
|
||||||
break;
|
break;
|
||||||
case 'firefox':
|
case 'firefox':
|
||||||
browserName = 'firefox';
|
browserName = 'firefox';
|
||||||
@ -58,23 +85,26 @@ export async function toBrowserOptions(config: Config): Promise<BrowserOptions>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const launchOptions: LaunchOptions = {
|
const launchOptions: LaunchOptions = {
|
||||||
headless: !!(config.browser?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)),
|
|
||||||
channel,
|
channel,
|
||||||
executablePath: config.browser?.executablePath,
|
executablePath: cliOptions.executablePath,
|
||||||
...{ assistantMode: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextOptions: BrowserContextOptions = {
|
|
||||||
viewport: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (browserName === 'chromium')
|
if (browserName === 'chromium')
|
||||||
(launchOptions as any).webSocketPort = await findFreePort();
|
(launchOptions as any).webSocketPort = await findFreePort();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
browserName,
|
browser: {
|
||||||
launchOptions,
|
browserName,
|
||||||
contextOptions,
|
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> {
|
export async function outputFile(config: Config, name: string): Promise<string> {
|
||||||
const result = config.outputDir ?? os.tmpdir();
|
const result = config.outputDir ?? os.tmpdir();
|
||||||
await fs.promises.mkdir(result, { recursive: true });
|
await fs.promises.mkdir(result, { recursive: true });
|
||||||
const fileName = sanitizeForFilePath(name);
|
const fileName = sanitizeForFilePath(name);
|
||||||
return path.join(result, fileName);
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import os from 'os';
|
|
||||||
|
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
|
||||||
import { waitForCompletion } from './tools/utils';
|
import { waitForCompletion } from './tools/utils';
|
||||||
import { ManualPromise } from './manualPromise';
|
import { ManualPromise } from './manualPromise';
|
||||||
import { toBrowserOptions } from './config';
|
|
||||||
import { Tab } from './tab';
|
import { Tab } from './tab';
|
||||||
|
|
||||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
|
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types';
|
||||||
import type { ModalState, Tool, ToolActionResult } from './tools/tool';
|
import type { ModalState, Tool, ToolActionResult } from './tools/tool';
|
||||||
import type { Config } from '../config';
|
import type { Config } from '../config';
|
||||||
import type { BrowserOptions } from './config';
|
|
||||||
|
|
||||||
type PendingAction = {
|
type PendingAction = {
|
||||||
dialogShown: ManualPromise<void>;
|
dialogShown: ManualPromise<void>;
|
||||||
@ -285,51 +279,32 @@ ${code.join('\n')}
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _innerCreateBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> {
|
private async _innerCreateBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> {
|
||||||
const browserOptions = await toBrowserOptions(this.config);
|
|
||||||
|
|
||||||
if (this.config.browser?.remoteEndpoint) {
|
if (this.config.browser?.remoteEndpoint) {
|
||||||
const url = new URL(this.config.browser?.remoteEndpoint);
|
const url = new URL(this.config.browser?.remoteEndpoint);
|
||||||
if (browserOptions.browserName)
|
if (this.config.browser.browserName)
|
||||||
url.searchParams.set('browser', browserOptions.browserName);
|
url.searchParams.set('browser', this.config.browser.browserName);
|
||||||
if (browserOptions.launchOptions)
|
if (this.config.browser.launchOptions)
|
||||||
url.searchParams.set('launch-options', JSON.stringify(browserOptions.launchOptions));
|
url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions));
|
||||||
const browser = await playwright[browserOptions.browserName ?? 'chromium'].connect(String(url));
|
const browser = await playwright[this.config.browser?.browserName ?? 'chromium'].connect(String(url));
|
||||||
const browserContext = await browser.newContext();
|
const browserContext = await browser.newContext();
|
||||||
return { browser, browserContext };
|
return { browser, browserContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.browser?.cdpEndpoint) {
|
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];
|
const browserContext = browser.contexts()[0];
|
||||||
return { browser, browserContext };
|
return { browser, browserContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
const browserContext = await launchPersistentContext(this.config.browser?.userDataDir, browserOptions);
|
const browserContext = await launchPersistentContext(this.config.browser);
|
||||||
return { browserContext };
|
return { browserContext };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUserDataDir(browserName: 'chromium' | 'firefox' | 'webkit') {
|
async function launchPersistentContext(browserConfig: Config['browser']): Promise<playwright.BrowserContext> {
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const browserType = browserOptions.browserName ? playwright[browserOptions.browserName] : playwright.chromium;
|
const browserType = browserConfig?.browserName ? playwright[browserConfig.browserName] : playwright.chromium;
|
||||||
return await browserType.launchPersistentContext(userDataDir, browserOptions.launchOptions);
|
return await browserType.launchPersistentContext(browserConfig?.userDataDir || '', browserConfig?.launchOptions);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.message.includes('Executable doesn\'t exist'))
|
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.`);
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
|
|
||||||
import { createServer } from './index';
|
import { createServer } from './index';
|
||||||
@ -23,7 +21,7 @@ import { ServerList } from './server';
|
|||||||
|
|
||||||
import { startHttpTransport, startStdioTransport } from './transport';
|
import { startHttpTransport, startStdioTransport } from './transport';
|
||||||
|
|
||||||
import type { Config, ToolCapability } from '../config';
|
import { resolveConfig } from './config';
|
||||||
|
|
||||||
const packageJSON = require('../package.json');
|
const packageJSON = require('../package.json');
|
||||||
|
|
||||||
@ -41,22 +39,7 @@ program
|
|||||||
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
.option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)')
|
||||||
.option('--config <path>', 'Path to the configuration file.')
|
.option('--config <path>', 'Path to the configuration file.')
|
||||||
.action(async options => {
|
.action(async options => {
|
||||||
const cliOverrides: Config = {
|
const config = await resolveConfig(options);
|
||||||
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 serverList = new ServerList(() => createServer(config));
|
const serverList = new ServerList(() => createServer(config));
|
||||||
setupExitWatchdog(serverList);
|
setupExitWatchdog(serverList);
|
||||||
|
|
||||||
@ -66,30 +49,6 @@ program
|
|||||||
await startStdioTransport(serverList);
|
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) {
|
function setupExitWatchdog(serverList: ServerList) {
|
||||||
const handleExit = async () => {
|
const handleExit = async () => {
|
||||||
setTimeout(() => process.exit(0), 15000);
|
setTimeout(() => process.exit(0), 15000);
|
||||||
|
@ -19,7 +19,6 @@ import path from 'path';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { defineTool } from './tool';
|
import { defineTool } from './tool';
|
||||||
import { toBrowserOptions } from '../config';
|
|
||||||
|
|
||||||
const install = defineTool({
|
const install = defineTool({
|
||||||
capability: 'install',
|
capability: 'install',
|
||||||
@ -30,8 +29,7 @@ const install = defineTool({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handle: async context => {
|
handle: async context => {
|
||||||
const browserOptions = await toBrowserOptions(context.config);
|
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.launchOptions.channel ?? context.config.browser?.launchOptions.browserName ?? 'chrome';
|
||||||
const channel = browserOptions.launchOptions?.channel ?? browserOptions.browserName ?? 'chrome';
|
|
||||||
const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js');
|
||||||
const child = fork(cli, ['install', channel], {
|
const child = fork(cli, ['install', channel], {
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user