Skip to content

Commit 6fb2723

Browse files
committed
feat: add emulate_device_profile tool
1 parent 9b4cd8e commit 6fb2723

File tree

4 files changed

+266
-3
lines changed

4 files changed

+266
-3
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,23 @@ Your MCP client should open the browser and record a performance trace.
205205
> [!NOTE]
206206
> The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser.
207207
208+
### Mobile emulation with Copilot prompts
209+
210+
When you are working inside VS Code Copilot (or any MCP-aware client), you can chain multiple tool invocations in a single prompt and let the agent run them sequentially. The example below opens a local site, switches to the built-in iPhone 12 Pro profile, applies Slow 4G throttling, records a 10-second performance trace, and finally surfaces LCP/CLS insights:
211+
212+
```
213+
Please use mcp chrome-devtools:
214+
1. navigate_page http://localhost:5173
215+
2. emulate_device_profile profile=iPhone-12-Pro
216+
3. emulate_network throttlingOption="Slow 4G"
217+
4. performance_start_trace duration=10000
218+
5. performance_stop_trace
219+
6. performance_analyze_insight focus="lcp,cls"
220+
```
221+
222+
> [!TIP]
223+
> The `profile` parameter is case-sensitive. Use `iPhone-12-Pro` exactly as written (other presets are listed in the [tool reference](./docs/tool-reference.md)).
224+
208225
## Tools
209226

210227
If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md).
@@ -227,8 +244,9 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
227244
- [`new_page`](docs/tool-reference.md#new_page)
228245
- [`select_page`](docs/tool-reference.md#select_page)
229246
- [`wait_for`](docs/tool-reference.md#wait_for)
230-
- **Emulation** (3 tools)
247+
- **Emulation** (4 tools)
231248
- [`emulate_cpu`](docs/tool-reference.md#emulate_cpu)
249+
- [`emulate_device_profile`](docs/tool-reference.md#emulate_device_profile)
232250
- [`emulate_network`](docs/tool-reference.md#emulate_network)
233251
- [`resize_page`](docs/tool-reference.md#resize_page)
234252
- **Performance** (3 tools)

docs/tool-reference.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
- [`new_page`](#new_page)
1919
- [`select_page`](#select_page)
2020
- [`wait_for`](#wait_for)
21-
- **[Emulation](#emulation)** (3 tools)
21+
- **[Emulation](#emulation)** (4 tools)
2222
- [`emulate_cpu`](#emulate_cpu)
23+
- [`emulate_device_profile`](#emulate_device_profile)
2324
- [`emulate_network`](#emulate_network)
2425
- [`resize_page`](#resize_page)
2526
- **[Performance](#performance)** (3 tools)
@@ -198,6 +199,16 @@
198199

199200
---
200201

202+
### `emulate_device_profile`
203+
204+
**Description:** Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings.
205+
206+
**Parameters:**
207+
208+
- **profile** (enum: "iPhone-12-Pro", "Pixel-7") **(required)**: The device profile preset to apply. Supported profiles: iPhone-12-Pro, Pixel-7.
209+
210+
---
211+
201212
### `emulate_network`
202213

203214
**Description:** Emulates network conditions such as throttling on the selected page.

src/tools/emulation.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import {PredefinedNetworkConditions} from 'puppeteer-core';
8+
import type {CDPSession, Protocol, Viewport} from 'puppeteer-core';
89
import z from 'zod';
910

1011
import {ToolCategories} from './categories.js';
@@ -15,6 +16,100 @@ const throttlingOptions: [string, ...string[]] = [
1516
...Object.keys(PredefinedNetworkConditions),
1617
];
1718

19+
const deviceProfileOptions = ['iPhone-12-Pro', 'Pixel-7'] as const;
20+
21+
type DeviceProfileName = (typeof deviceProfileOptions)[number];
22+
23+
interface DeviceProfileDefinition {
24+
metrics: Protocol.Emulation.SetDeviceMetricsOverrideRequest;
25+
touch: Protocol.Emulation.SetTouchEmulationEnabledRequest;
26+
userAgent: Protocol.Network.SetUserAgentOverrideRequest;
27+
viewport: Viewport;
28+
locale?: string;
29+
timezoneId?: string;
30+
}
31+
32+
const DEVICE_PROFILES: Record<DeviceProfileName, DeviceProfileDefinition> = {
33+
'iPhone-12-Pro': {
34+
metrics: {
35+
width: 390,
36+
height: 844,
37+
deviceScaleFactor: 3,
38+
mobile: true,
39+
screenWidth: 390,
40+
screenHeight: 844,
41+
screenOrientation: {
42+
type: 'portraitPrimary',
43+
angle: 0,
44+
},
45+
positionX: 0,
46+
positionY: 0,
47+
scale: 1,
48+
},
49+
touch: {
50+
enabled: true,
51+
maxTouchPoints: 5,
52+
},
53+
viewport: {
54+
width: 390,
55+
height: 844,
56+
deviceScaleFactor: 3,
57+
isMobile: true,
58+
hasTouch: true,
59+
isLandscape: false,
60+
},
61+
userAgent: {
62+
userAgent:
63+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
64+
platform: 'iPhone',
65+
acceptLanguage: 'en-US,en',
66+
},
67+
locale: 'en-US',
68+
timezoneId: 'America/Los_Angeles',
69+
},
70+
'Pixel-7': {
71+
metrics: {
72+
width: 412,
73+
height: 915,
74+
deviceScaleFactor: 2.625,
75+
mobile: true,
76+
screenWidth: 412,
77+
screenHeight: 915,
78+
screenOrientation: {
79+
type: 'portraitPrimary',
80+
angle: 0,
81+
},
82+
positionX: 0,
83+
positionY: 0,
84+
scale: 1,
85+
},
86+
touch: {
87+
enabled: true,
88+
maxTouchPoints: 5,
89+
},
90+
viewport: {
91+
width: 412,
92+
height: 915,
93+
deviceScaleFactor: 2.625,
94+
isMobile: true,
95+
hasTouch: true,
96+
isLandscape: false,
97+
},
98+
userAgent: {
99+
userAgent:
100+
'Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TD1A.221105.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
101+
platform: 'Android',
102+
acceptLanguage: 'en-US,en',
103+
},
104+
locale: 'en-US',
105+
timezoneId: 'America/Los_Angeles',
106+
},
107+
};
108+
109+
function getClient(page: unknown): CDPSession {
110+
return (page as { _client(): CDPSession })._client();
111+
}
112+
18113
export const emulateNetwork = defineTool({
19114
name: 'emulate_network',
20115
description: `Emulates network conditions such as throttling on the selected page.`,
@@ -74,3 +169,54 @@ export const emulateCpu = defineTool({
74169
context.setCpuThrottlingRate(throttlingRate);
75170
},
76171
});
172+
173+
export const emulateDeviceProfile = defineTool({
174+
name: 'emulate_device_profile',
175+
description:
176+
'Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings.',
177+
annotations: {
178+
category: ToolCategories.EMULATION,
179+
readOnlyHint: false,
180+
},
181+
schema: {
182+
profile: z
183+
.enum(deviceProfileOptions)
184+
.describe(
185+
`The device profile preset to apply. Supported profiles: ${deviceProfileOptions.join(', ')}.`,
186+
),
187+
},
188+
handler: async (request, response, context) => {
189+
const page = context.getSelectedPage();
190+
const profileName = request.params.profile;
191+
const profile = DEVICE_PROFILES[profileName];
192+
193+
if (!profile) {
194+
throw new Error(`Unknown device profile: ${profileName}`);
195+
}
196+
197+
const client = getClient(page);
198+
199+
await client.send('Emulation.setDeviceMetricsOverride', profile.metrics);
200+
await client.send('Network.setUserAgentOverride', profile.userAgent);
201+
202+
if (profile.locale) {
203+
await client.send('Emulation.setLocaleOverride', {
204+
locale: profile.locale,
205+
});
206+
}
207+
208+
if (profile.timezoneId) {
209+
await client.send('Emulation.setTimezoneOverride', {
210+
timezoneId: profile.timezoneId,
211+
});
212+
}
213+
214+
await page.setViewport(profile.viewport);
215+
216+
await client.send('Emulation.setTouchEmulationEnabled', profile.touch);
217+
218+
response.appendResponseLine(
219+
`Applied device profile "${profileName}" to the selected page.`,
220+
);
221+
},
222+
});

tests/tools/emulation.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import assert from 'node:assert';
77
import {describe, it} from 'node:test';
88

9-
import {emulateCpu, emulateNetwork} from '../../src/tools/emulation.js';
9+
import {
10+
emulateCpu,
11+
emulateDeviceProfile,
12+
emulateNetwork,
13+
} from '../../src/tools/emulation.js';
1014
import {withBrowser} from '../utils.js';
1115

1216
describe('emulation', () => {
@@ -136,4 +140,88 @@ describe('emulation', () => {
136140
});
137141
});
138142
});
143+
144+
describe('device profile', () => {
145+
it('applies iPhone 12 Pro preset', async () => {
146+
await withBrowser(async (response, context) => {
147+
await emulateDeviceProfile.handler(
148+
{
149+
params: {
150+
profile: 'iPhone-12-Pro',
151+
},
152+
},
153+
response,
154+
context,
155+
);
156+
157+
const page = context.getSelectedPage();
158+
const result = await page.evaluate(() => {
159+
return {
160+
screenWidth: window.screen.width,
161+
screenHeight: window.screen.height,
162+
devicePixelRatio: window.devicePixelRatio,
163+
userAgent: navigator.userAgent,
164+
language: navigator.language,
165+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
166+
maxTouchPoints: navigator.maxTouchPoints,
167+
};
168+
});
169+
170+
const viewport = page.viewport();
171+
172+
assert.strictEqual(result.screenWidth, 390);
173+
assert.strictEqual(result.screenHeight, 844);
174+
assert.strictEqual(result.devicePixelRatio, 3);
175+
assert.ok(result.userAgent.includes('iPhone'));
176+
assert.strictEqual(result.language, 'en-US');
177+
assert.strictEqual(result.timeZone, 'America/Los_Angeles');
178+
assert.strictEqual(result.maxTouchPoints, 5);
179+
assert.deepStrictEqual(viewport, {
180+
width: 390,
181+
height: 844,
182+
deviceScaleFactor: 3,
183+
isMobile: true,
184+
hasTouch: true,
185+
isLandscape: false,
186+
});
187+
assert.deepStrictEqual(response.responseLines, [
188+
'Applied device profile "iPhone-12-Pro" to the selected page.',
189+
]);
190+
});
191+
});
192+
193+
it('applies Pixel 7 preset', async () => {
194+
await withBrowser(async (response, context) => {
195+
await emulateDeviceProfile.handler(
196+
{
197+
params: {
198+
profile: 'Pixel-7',
199+
},
200+
},
201+
response,
202+
context,
203+
);
204+
205+
const page = context.getSelectedPage();
206+
const result = await page.evaluate(() => {
207+
return {
208+
screenWidth: window.screen.width,
209+
screenHeight: window.screen.height,
210+
devicePixelRatio: window.devicePixelRatio,
211+
userAgent: navigator.userAgent,
212+
maxTouchPoints: navigator.maxTouchPoints,
213+
};
214+
});
215+
216+
assert.strictEqual(result.screenWidth, 412);
217+
assert.strictEqual(result.screenHeight, 915);
218+
assert.strictEqual(result.devicePixelRatio, 2.625);
219+
assert.ok(result.userAgent.includes('Android'));
220+
assert.strictEqual(result.maxTouchPoints, 5);
221+
assert.deepStrictEqual(response.responseLines, [
222+
'Applied device profile "Pixel-7" to the selected page.',
223+
]);
224+
});
225+
});
226+
});
139227
});

0 commit comments

Comments
 (0)