Skip to content

Commit 376527f

Browse files
committed
feat: support device scale factor in --viewport
1 parent 06c2952 commit 376527f

5 files changed

Lines changed: 105 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ The Chrome DevTools MCP server supports the following configuration option:
502502
- **Type:** string
503503

504504
- **`--viewport`**
505-
Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
505+
Initial viewport size for the Chrome instances started by the server. For example, `1280x720` or `1280x720x2` to set the device scale factor. In headless mode, max size is 3840x2160px.
506506
- **Type:** string
507507

508508
- **`--proxyServer`/ `--proxy-server`**

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,39 @@
77
import type {YargsOptions} from '../third_party/index.js';
88
import {yargs, hideBin} from '../third_party/index.js';
99

10+
function parseViewportOption(arg: string | undefined) {
11+
if (arg === undefined) {
12+
return;
13+
}
14+
15+
const dimensions = arg.split('x');
16+
if (dimensions.length < 2 || dimensions.length > 3) {
17+
throw new Error(
18+
'Invalid viewport. Expected format is `1280x720` or `1280x720x2`.',
19+
);
20+
}
21+
22+
const [width, height, deviceScaleFactor] = dimensions.map(Number);
23+
if (
24+
!width ||
25+
!height ||
26+
Number.isNaN(width) ||
27+
Number.isNaN(height) ||
28+
(deviceScaleFactor !== undefined &&
29+
(deviceScaleFactor <= 0 || Number.isNaN(deviceScaleFactor)))
30+
) {
31+
throw new Error(
32+
'Invalid viewport. Expected format is `1280x720` or `1280x720x2`.',
33+
);
34+
}
35+
36+
return {
37+
width,
38+
height,
39+
...(deviceScaleFactor === undefined ? {} : {deviceScaleFactor}),
40+
};
41+
}
42+
1043
export const cliOptions = {
1144
autoConnect: {
1245
type: 'boolean',
@@ -124,20 +157,8 @@ export const cliOptions = {
124157
viewport: {
125158
type: 'string',
126159
describe:
127-
'Initial viewport size for the Chrome instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.',
128-
coerce: (arg: string | undefined) => {
129-
if (arg === undefined) {
130-
return;
131-
}
132-
const [width, height] = arg.split('x').map(Number);
133-
if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) {
134-
throw new Error('Invalid viewport. Expected format is `1280x720`.');
135-
}
136-
return {
137-
width,
138-
height,
139-
};
140-
},
160+
'Initial viewport size for the Chrome instances started by the server. For example, `1280x720` or `1280x720x2` to set the device scale factor. In headless mode, max size is 3840x2160px.',
161+
coerce: parseViewportOption,
141162
},
142163
proxyServer: {
143164
type: 'string',
@@ -297,6 +318,10 @@ export function parseArguments(version: string, argv = process.argv) {
297318
'$0 --viewport 1280x720',
298319
'Launch Chrome with the initial viewport size of 1280x720px',
299320
],
321+
[
322+
'$0 --viewport 1280x720x2',
323+
'Launch Chrome with the initial viewport size of 1280x720px and device scale factor 2',
324+
],
300325
[
301326
`$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`,
302327
'Launch Chrome without sandboxes. Use with caution.',

src/browser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ interface McpLaunchOptions {
144144
viewport?: {
145145
width: number;
146146
height: number;
147+
deviceScaleFactor?: number;
147148
};
148149
chromeArgs?: string[];
149150
ignoreDefaultChromeArgs?: string[];
@@ -242,6 +243,14 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
242243
contentWidth: options.viewport.width,
243244
contentHeight: options.viewport.height,
244245
});
246+
if (options.viewport.deviceScaleFactor !== undefined) {
247+
// page.resize() only affects the content size. Apply DPR separately.
248+
await page?.setViewport({
249+
width: options.viewport.width,
250+
height: options.viewport.height,
251+
deviceScaleFactor: options.viewport.deviceScaleFactor,
252+
});
253+
}
245254
}
246255
return browser;
247256
} catch (error) {

tests/browser.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,41 @@ describe('browser', () => {
7777
await browser.close();
7878
}
7979
});
80+
81+
it('launches with the initial viewport device scale factor', async () => {
82+
const tmpDir = os.tmpdir();
83+
const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`);
84+
const browser = await launch({
85+
headless: true,
86+
isolated: false,
87+
userDataDir: folderPath,
88+
executablePath: executablePath(),
89+
viewport: {
90+
width: 1501,
91+
height: 801,
92+
deviceScaleFactor: 2,
93+
},
94+
devtools: false,
95+
});
96+
try {
97+
const [page] = await browser.pages();
98+
const result = await page.evaluate(() => {
99+
return {
100+
width: window.innerWidth,
101+
height: window.innerHeight,
102+
devicePixelRatio: window.devicePixelRatio,
103+
};
104+
});
105+
assert.deepStrictEqual(result, {
106+
width: 1501,
107+
height: 801,
108+
devicePixelRatio: 2,
109+
});
110+
} finally {
111+
await browser.close();
112+
}
113+
});
114+
80115
it('connects to an existing browser with userDataDir', async () => {
81116
const tmpDir = os.tmpdir();
82117
const folderPath = path.join(tmpDir, `temp-folder-${crypto.randomUUID()}`);

tests/cli.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,27 @@ describe('cli args parsing', () => {
131131
});
132132
});
133133

134+
it('parses viewport with device scale factor', async () => {
135+
const args = parseArguments('1.0.0', [
136+
'node',
137+
'main.js',
138+
'--viewport',
139+
'888x777x2',
140+
]);
141+
assert.deepStrictEqual(args, {
142+
...defaultArgs,
143+
_: [],
144+
headless: false,
145+
$0: 'npx chrome-devtools-mcp@latest',
146+
channel: 'stable',
147+
viewport: {
148+
width: 888,
149+
height: 777,
150+
deviceScaleFactor: 2,
151+
},
152+
});
153+
});
154+
134155
it('parses chrome args', async () => {
135156
const args = parseArguments('1.0.0', [
136157
'node',

0 commit comments

Comments
 (0)