Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- END AUTO GENERATED OPTIONS -->

Pass them via the `args` property in the JSON configuration. For example:
Expand Down
32 changes: 14 additions & 18 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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<Browser> {
Expand Down Expand Up @@ -115,6 +119,14 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
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 (
Expand All @@ -134,7 +146,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
}
}

async function ensureBrowserLaunched(
export async function ensureBrowserLaunched(
options: McpLaunchOptions,
): Promise<Browser> {
if (browser?.connected) {
Expand All @@ -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';
22 changes: 22 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
23 changes: 13 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -69,15 +69,18 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {

let context: McpContext;
async function getContext(): Promise<McpContext> {
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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
27 changes: 27 additions & 0 deletions tests/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
20 changes: 20 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
});
});
Loading