Skip to content

Commit ef06ea2

Browse files
committed
feat: support initial viewport in the CLI
1 parent a9fc863 commit ef06ea2

File tree

7 files changed

+102
-29
lines changed

7 files changed

+102
-29
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ The Chrome DevTools MCP server supports the following configuration option:
272272
Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
273273
- **Type:** string
274274

275+
- **`--viewport`**
276+
Initial viewport size for the Chromee instances started by the server. For example, `1280x720`
277+
- **Type:** string
278+
275279
<!-- END AUTO GENERATED OPTIONS -->
276280

277281
Pass them via the `args` property in the JSON configuration. For example:

src/browser.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const connectOptions: ConnectOptions = {
4444
protocolTimeout: 10_000,
4545
};
4646

47-
async function ensureBrowserConnected(browserURL: string) {
47+
export async function ensureBrowserConnected(browserURL: string) {
4848
if (browser?.connected) {
4949
return browser;
5050
}
@@ -64,6 +64,10 @@ interface McpLaunchOptions {
6464
headless: boolean;
6565
isolated: boolean;
6666
logFile?: fs.WriteStream;
67+
viewport?: {
68+
width: number;
69+
height: number;
70+
};
6771
}
6872

6973
export async function launch(options: McpLaunchOptions): Promise<Browser> {
@@ -115,6 +119,14 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
115119
browser.process()?.stderr?.pipe(options.logFile);
116120
browser.process()?.stdout?.pipe(options.logFile);
117121
}
122+
if (options.viewport) {
123+
const [page] = await browser.pages();
124+
// @ts-expect-error internal API for now.
125+
await page?.resize({
126+
contentWidth: options.viewport.width,
127+
contentHeight: options.viewport.height,
128+
});
129+
}
118130
return browser;
119131
} catch (error) {
120132
if (
@@ -134,7 +146,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
134146
}
135147
}
136148

137-
async function ensureBrowserLaunched(
149+
export async function ensureBrowserLaunched(
138150
options: McpLaunchOptions,
139151
): Promise<Browser> {
140152
if (browser?.connected) {
@@ -144,20 +156,4 @@ async function ensureBrowserLaunched(
144156
return browser;
145157
}
146158

147-
export async function resolveBrowser(options: {
148-
browserUrl?: string;
149-
executablePath?: string;
150-
customDevTools?: string;
151-
channel?: Channel;
152-
headless: boolean;
153-
isolated: boolean;
154-
logFile?: fs.WriteStream;
155-
}) {
156-
const browser = options.browserUrl
157-
? await ensureBrowserConnected(options.browserUrl)
158-
: await ensureBrowserLaunched(options);
159-
160-
return browser;
161-
}
162-
163159
export type Channel = 'stable' | 'canary' | 'beta' | 'dev';

src/cli.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ export const cliOptions = {
5454
describe:
5555
'Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.',
5656
},
57+
viewport: {
58+
type: 'string' as const,
59+
describe:
60+
'Initial viewport size for the Chromee instances started by the server. For example, `1280x720`',
61+
coerce: (arg: string | undefined) => {
62+
if (arg === undefined) {
63+
return;
64+
}
65+
const [width, height] = arg.split('x').map(Number);
66+
console.log(arg);
67+
if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) {
68+
throw new Error('Invalid viewport. Expected format is `1280x720`.');
69+
}
70+
return {
71+
width,
72+
height,
73+
};
74+
},
75+
},
5776
};
5877

5978
export function parseArguments(version: string, argv = process.argv) {
@@ -79,6 +98,10 @@ export function parseArguments(version: string, argv = process.argv) {
7998
['$0 --channel stable', 'Use stable Chrome installed on this system'],
8099
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
81100
['$0 --help', 'Print CLI options'],
101+
[
102+
'$0 --viewport 1280x720',
103+
'Launch Chrome with the initial viewport size of 1280x720px',
104+
],
82105
]);
83106

84107
return yargsInstance

src/main.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js';
1616
import {SetLevelRequestSchema} from '@modelcontextprotocol/sdk/types.js';
1717

1818
import type {Channel} from './browser.js';
19-
import {resolveBrowser} from './browser.js';
19+
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
2020
import {parseArguments} from './cli.js';
2121
import {logger, saveLogsToFile} from './logger.js';
2222
import {McpContext} from './McpContext.js';
@@ -69,15 +69,18 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
6969

7070
let context: McpContext;
7171
async function getContext(): Promise<McpContext> {
72-
const browser = await resolveBrowser({
73-
browserUrl: args.browserUrl,
74-
headless: args.headless,
75-
executablePath: args.executablePath,
76-
customDevTools: args.customDevtools,
77-
channel: args.channel as Channel,
78-
isolated: args.isolated,
79-
logFile,
80-
});
72+
const browser = args.browserUrl
73+
? await ensureBrowserConnected(args.browserUrl)
74+
: await ensureBrowserLaunched({
75+
headless: args.headless,
76+
executablePath: args.executablePath,
77+
customDevTools: args.customDevtools,
78+
channel: args.channel as Channel,
79+
isolated: args.isolated,
80+
logFile,
81+
viewport: args.viewport,
82+
});
83+
8184
if (context?.browser !== browser) {
8285
context = await McpContext.from(browser, logger);
8386
}

src/tools/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const uploadFile = defineTool({
197197
// a type=file element. In this case, we want to default to
198198
// Page.waitForFileChooser() and upload the file this way.
199199
try {
200-
const page = await context.getSelectedPage();
200+
const page = context.getSelectedPage();
201201
const [fileChooser] = await Promise.all([
202202
page.waitForFileChooser({timeout: 3000}),
203203
handle.asLocator().click(),

tests/browser.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,31 @@ describe('browser', () => {
4242
await browser1.close();
4343
}
4444
});
45+
46+
it('launches with the initial viewport', async () => {
47+
const tmpDir = os.tmpdir();
48+
const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`);
49+
const browser = await launch({
50+
headless: true,
51+
isolated: false,
52+
userDataDir: folderPath,
53+
executablePath: executablePath(),
54+
viewport: {
55+
width: 700,
56+
height: 500,
57+
},
58+
});
59+
try {
60+
const [page] = await browser.pages();
61+
const result = await page.evaluate(() => {
62+
return {width: window.innerWidth, height: window.innerHeight};
63+
});
64+
assert.deepStrictEqual(result, {
65+
width: 700,
66+
height: 500,
67+
});
68+
} finally {
69+
await browser.close();
70+
}
71+
});
4572
});

tests/cli.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,24 @@ describe('cli args parsing', () => {
5555
executablePath: '/tmp/test 123/chrome',
5656
});
5757
});
58+
59+
it('parses viewport', async () => {
60+
const args = parseArguments('1.0.0', [
61+
'node',
62+
'main.js',
63+
'--viewport',
64+
'888x777',
65+
]);
66+
assert.deepStrictEqual(args, {
67+
_: [],
68+
headless: false,
69+
isolated: false,
70+
$0: 'npx chrome-devtools-mcp@latest',
71+
channel: 'stable',
72+
viewport: {
73+
width: 888,
74+
height: 777,
75+
},
76+
});
77+
});
5878
});

0 commit comments

Comments
 (0)