From 26779ceb20411541bd6587ab52d4afb87a5cb7bc Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 28 Apr 2025 15:04:59 -0700 Subject: [PATCH] chore: allow passing config file (#281) --- .npmignore | 1 + config.d.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++++ index.d.ts | 41 ++----------------------- src/index.ts | 31 +++++++------------ src/program.ts | 56 ++++++++++++++++++++++++++++------ src/tools/tool.ts | 2 +- 6 files changed, 139 insertions(+), 70 deletions(-) create mode 100644 config.d.ts diff --git a/.npmignore b/.npmignore index f29dce2..a2846ba 100644 --- a/.npmignore +++ b/.npmignore @@ -4,3 +4,4 @@ LICENSE !lib/**/*.js !cli.js !index.* +!config.d.ts diff --git a/config.d.ts b/config.d.ts new file mode 100644 index 0000000..637aff4 --- /dev/null +++ b/config.d.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install'; + +export type Config = { + /** + * The browser to use. + */ + browser?: { + /** + * 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; + + /** + * Path to a user data directory for browser profile persistence. + */ + userDataDir?: string; + + /** + * Whether to run the browser in headless mode (default: true). + */ + headless?: boolean; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + }, + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'tabs': Tab management features. + * - 'pdf': PDF generation and manipulation. + * - 'history': Browser history access. + * - 'wait': Wait and timing utilities. + * - 'files': File upload/download support. + * - 'install': Browser installation utilities. + */ + capabilities?: ToolCapability[]; + + /** + * Run server that uses screenshots (Aria snapshots are used by default). + */ + vision?: boolean; +}; diff --git a/index.d.ts b/index.d.ts index f1cb56d..900c478 100644 --- a/index.d.ts +++ b/index.d.ts @@ -17,44 +17,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install'; +import type { Config } from './config'; -type Options = { - /** - * The browser to use (e.g., 'chrome', 'chromium', 'firefox', 'webkit', 'msedge'). - */ - browser?: string; - /** - * Path to a user data directory for browser profile persistence. - */ - userDataDir?: string; - /** - * Whether to run the browser in headless mode (default: true). - */ - headless?: boolean; - /** - * Path to a custom browser executable. - */ - executablePath?: string; - /** - * Chrome DevTools Protocol endpoint to connect to an existing browser instance. - */ - cdpEndpoint?: string; - /** - * Enable vision capabilities (e.g., visual automation or OCR). - */ - vision?: boolean; - /** - * List of enabled tool capabilities. Possible values: - * - 'core': Core browser automation features. - * - 'tabs': Tab management features. - * - 'pdf': PDF generation and manipulation. - * - 'history': Browser history access. - * - 'wait': Wait and timing utilities. - * - 'files': File upload/download support. - * - 'install': Browser installation utilities. - */ - capabilities?: ToolCapability[]; -}; -export declare function createServer(options?: Options): Promise; +export declare function createServer(config?: Config): Promise; export {}; diff --git a/src/index.ts b/src/index.ts index 88c947e..c6559ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,8 @@ import snapshot from './tools/snapshot'; import tabs from './tools/tabs'; import screen from './tools/screen'; -import type { Tool, ToolCapability } from './tools/tool'; +import type { Tool } from './tools/tool'; +import type { Config } from '../config'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { LaunchOptions, BrowserContextOptions } from 'playwright'; @@ -64,22 +65,12 @@ const screenshotTools: Tool[] = [ ...tabs(false), ]; -type Options = { - browser?: string; - userDataDir?: string; - headless?: boolean; - executablePath?: string; - cdpEndpoint?: string; - vision?: boolean; - capabilities?: ToolCapability[]; -}; - const packageJSON = require('../package.json'); -export async function createServer(options?: Options): Promise { +export async function createServer(config?: Config): Promise { let browserName: 'chromium' | 'firefox' | 'webkit'; let channel: string | undefined; - switch (options?.browser) { + switch (config?.browser?.type) { case 'chrome': case 'chrome-beta': case 'chrome-canary': @@ -90,7 +81,7 @@ export async function createServer(options?: Options): Promise { case 'msedge-canary': case 'msedge-dev': browserName = 'chromium'; - channel = options.browser; + channel = config.browser.type; break; case 'firefox': browserName = 'firefox'; @@ -102,18 +93,18 @@ export async function createServer(options?: Options): Promise { browserName = 'chromium'; channel = 'chrome'; } - const userDataDir = options?.userDataDir ?? await createUserDataDir(browserName); + const userDataDir = config?.browser?.userDataDir ?? await createUserDataDir(browserName); const launchOptions: LaunchOptions & BrowserContextOptions = { - headless: !!(options?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)), + headless: !!(config?.browser?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)), channel, - executablePath: options?.executablePath, + executablePath: config?.browser?.executablePath, viewport: null, ...{ assistantMode: true }, }; - const allTools = options?.vision ? screenshotTools : snapshotTools; - const tools = allTools.filter(tool => !options?.capabilities || tool.capability === 'core' || options.capabilities.includes(tool.capability)); + const allTools = config?.vision ? screenshotTools : snapshotTools; + const tools = allTools.filter(tool => !config?.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability)); return createServerWithTools({ name: 'Playwright', version: packageJSON.version, @@ -122,7 +113,7 @@ export async function createServer(options?: Options): Promise { browserName, userDataDir, launchOptions, - cdpEndpoint: options?.cdpEndpoint, + cdpEndpoint: config?.browser?.cdpEndpoint, }); } diff --git a/src/program.ts b/src/program.ts index 4c5f563..c1c45bb 100644 --- a/src/program.ts +++ b/src/program.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import fs from 'fs'; + import { program } from 'commander'; import { createServer } from './index'; import { ServerList } from './server'; -import { ToolCapability } from './tools/tool'; import { startHttpTransport, startStdioTransport } from './transport'; +import type { Config, ToolCapability } from '../config'; + const packageJSON = require('../package.json'); program @@ -32,20 +35,29 @@ program .option('--cdp-endpoint ', 'CDP endpoint to connect to.') .option('--executable-path ', 'Path to the browser executable.') .option('--headless', 'Run browser in headless mode, headed by default') + .option('--user-data-dir ', 'Path to the user data directory') .option('--port ', 'Port to listen on for SSE transport.') .option('--host ', 'Host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') - .option('--user-data-dir ', 'Path to the user data directory') .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)') + .option('--config ', 'Path to the configuration file.') .action(async options => { - const serverList = new ServerList(() => createServer({ - browser: options.browser, - userDataDir: options.userDataDir, - headless: options.headless, - executablePath: options.executablePath, - vision: !!options.vision, - cdpEndpoint: options.cdpEndpoint, + 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 serverList = new ServerList(() => createServer(config)); setupExitWatchdog(serverList); if (options.port) @@ -54,6 +66,30 @@ program await startStdioTransport(serverList); }); +async function loadConfig(configFile: string | undefined, cliOverrides: Config): Promise { + 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); diff --git a/src/tools/tool.ts b/src/tools/tool.ts index ef9af39..7caebcb 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -18,7 +18,7 @@ import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types' import type { z } from 'zod'; import type { Context } from '../context'; import type * as playwright from 'playwright'; -export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install'; +import type { ToolCapability } from '../../config'; export type ToolSchema = { name: string;