diff --git a/README.md b/README.md index b753475..4916dbd 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,10 @@ The Chrome DevTools MCP server supports the following configuration option: Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports. - **Type:** string +- **`--viewport`** + Initial viewport size for the Chromee instances started by the server. For example, `1280x720` + - **Type:** string + Pass them via the `args` property in the JSON configuration. For example: diff --git a/src/browser.ts b/src/browser.ts index 2a17120..7f08e05 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -44,7 +44,7 @@ const connectOptions: ConnectOptions = { protocolTimeout: 10_000, }; -async function ensureBrowserConnected(browserURL: string) { +export async function ensureBrowserConnected(browserURL: string) { if (browser?.connected) { return browser; } @@ -64,6 +64,10 @@ interface McpLaunchOptions { headless: boolean; isolated: boolean; logFile?: fs.WriteStream; + viewport?: { + width: number; + height: number; + }; } export async function launch(options: McpLaunchOptions): Promise { @@ -115,6 +119,14 @@ export async function launch(options: McpLaunchOptions): Promise { browser.process()?.stderr?.pipe(options.logFile); browser.process()?.stdout?.pipe(options.logFile); } + if (options.viewport) { + const [page] = await browser.pages(); + // @ts-expect-error internal API for now. + await page?.resize({ + contentWidth: options.viewport.width, + contentHeight: options.viewport.height, + }); + } return browser; } catch (error) { if ( @@ -134,7 +146,7 @@ export async function launch(options: McpLaunchOptions): Promise { } } -async function ensureBrowserLaunched( +export async function ensureBrowserLaunched( options: McpLaunchOptions, ): Promise { if (browser?.connected) { @@ -144,20 +156,4 @@ async function ensureBrowserLaunched( return browser; } -export async function resolveBrowser(options: { - browserUrl?: string; - executablePath?: string; - customDevTools?: string; - channel?: Channel; - headless: boolean; - isolated: boolean; - logFile?: fs.WriteStream; -}) { - const browser = options.browserUrl - ? await ensureBrowserConnected(options.browserUrl) - : await ensureBrowserLaunched(options); - - return browser; -} - export type Channel = 'stable' | 'canary' | 'beta' | 'dev'; diff --git a/src/cli.ts b/src/cli.ts index df07bfa..4a0c157 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -54,6 +54,24 @@ export const cliOptions = { describe: 'Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.', }, + viewport: { + type: 'string' as const, + describe: + 'Initial viewport size for the Chromee instances started by the server. For example, `1280x720`', + coerce: (arg: string | undefined) => { + if (arg === undefined) { + return; + } + const [width, height] = arg.split('x').map(Number); + if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) { + throw new Error('Invalid viewport. Expected format is `1280x720`.'); + } + return { + width, + height, + }; + }, + }, }; export function parseArguments(version: string, argv = process.argv) { @@ -79,6 +97,10 @@ export function parseArguments(version: string, argv = process.argv) { ['$0 --channel stable', 'Use stable Chrome installed on this system'], ['$0 --logFile /tmp/log.txt', 'Save logs to a file'], ['$0 --help', 'Print CLI options'], + [ + '$0 --viewport 1280x720', + 'Launch Chrome with the initial viewport size of 1280x720px', + ], ]); return yargsInstance diff --git a/src/main.ts b/src/main.ts index 6add9a9..91bd8c7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'; import {SetLevelRequestSchema} from '@modelcontextprotocol/sdk/types.js'; import type {Channel} from './browser.js'; -import {resolveBrowser} from './browser.js'; +import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js'; import {parseArguments} from './cli.js'; import {logger, saveLogsToFile} from './logger.js'; import {McpContext} from './McpContext.js'; @@ -69,15 +69,18 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => { let context: McpContext; async function getContext(): Promise { - const browser = await resolveBrowser({ - browserUrl: args.browserUrl, - headless: args.headless, - executablePath: args.executablePath, - customDevTools: args.customDevtools, - channel: args.channel as Channel, - isolated: args.isolated, - logFile, - }); + const browser = args.browserUrl + ? await ensureBrowserConnected(args.browserUrl) + : await ensureBrowserLaunched({ + headless: args.headless, + executablePath: args.executablePath, + customDevTools: args.customDevtools, + channel: args.channel as Channel, + isolated: args.isolated, + logFile, + viewport: args.viewport, + }); + if (context?.browser !== browser) { context = await McpContext.from(browser, logger); } diff --git a/src/tools/input.ts b/src/tools/input.ts index 541e941..eda04e8 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -197,7 +197,7 @@ export const uploadFile = defineTool({ // a type=file element. In this case, we want to default to // Page.waitForFileChooser() and upload the file this way. try { - const page = await context.getSelectedPage(); + const page = context.getSelectedPage(); const [fileChooser] = await Promise.all([ page.waitForFileChooser({timeout: 3000}), handle.asLocator().click(), diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 139a3b5..3dc8eff 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -42,4 +42,31 @@ describe('browser', () => { await browser1.close(); } }); + + it('launches with the initial viewport', async () => { + const tmpDir = os.tmpdir(); + const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`); + const browser = await launch({ + headless: true, + isolated: false, + userDataDir: folderPath, + executablePath: executablePath(), + viewport: { + width: 700, + height: 500, + }, + }); + try { + const [page] = await browser.pages(); + const result = await page.evaluate(() => { + return {width: window.innerWidth, height: window.innerHeight}; + }); + assert.deepStrictEqual(result, { + width: 700, + height: 500, + }); + } finally { + await browser.close(); + } + }); }); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 3508862..014faf9 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -55,4 +55,24 @@ describe('cli args parsing', () => { executablePath: '/tmp/test 123/chrome', }); }); + + it('parses viewport', async () => { + const args = parseArguments('1.0.0', [ + 'node', + 'main.js', + '--viewport', + '888x777', + ]); + assert.deepStrictEqual(args, { + _: [], + headless: false, + isolated: false, + $0: 'npx chrome-devtools-mcp@latest', + channel: 'stable', + viewport: { + width: 888, + height: 777, + }, + }); + }); });