Skip to content
Open
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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,23 @@ Your MCP client should open the browser and record a performance trace.
> [!NOTE]
> 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.

### Mobile emulation with Copilot prompts

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:

```
Please use mcp chrome-devtools:
1. navigate_page http://localhost:5173
2. emulate_device_profile profile=iPhone-12-Pro
3. emulate_network throttlingOption="Slow 4G"
4. performance_start_trace duration=10000
5. performance_stop_trace
6. performance_analyze_insight focus="lcp,cls"
```

> [!TIP]
> 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)).

## Tools

If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md).
Expand All @@ -227,8 +244,9 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`new_page`](docs/tool-reference.md#new_page)
- [`select_page`](docs/tool-reference.md#select_page)
- [`wait_for`](docs/tool-reference.md#wait_for)
- **Emulation** (3 tools)
- **Emulation** (4 tools)
- [`emulate_cpu`](docs/tool-reference.md#emulate_cpu)
- [`emulate_device_profile`](docs/tool-reference.md#emulate_device_profile)
- [`emulate_network`](docs/tool-reference.md#emulate_network)
- [`resize_page`](docs/tool-reference.md#resize_page)
- **Performance** (3 tools)
Expand Down
13 changes: 12 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
- [`new_page`](#new_page)
- [`select_page`](#select_page)
- [`wait_for`](#wait_for)
- **[Emulation](#emulation)** (3 tools)
- **[Emulation](#emulation)** (4 tools)
- [`emulate_cpu`](#emulate_cpu)
- [`emulate_device_profile`](#emulate_device_profile)
- [`emulate_network`](#emulate_network)
- [`resize_page`](#resize_page)
- **[Performance](#performance)** (3 tools)
Expand Down Expand Up @@ -198,6 +199,16 @@

---

### `emulate_device_profile`

**Description:** Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings.

**Parameters:**

- **profile** (enum: "iPhone-12-Pro", "Pixel-7") **(required)**: The device profile preset to apply. Supported profiles: iPhone-12-Pro, Pixel-7.

---

### `emulate_network`

**Description:** Emulates network conditions such as throttling on the selected page.
Expand Down
146 changes: 146 additions & 0 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {PredefinedNetworkConditions} from 'puppeteer-core';
import type {CDPSession, Protocol, Viewport} from 'puppeteer-core';
import z from 'zod';

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

const deviceProfileOptions = ['iPhone-12-Pro', 'Pixel-7'] as const;

type DeviceProfileName = (typeof deviceProfileOptions)[number];

interface DeviceProfileDefinition {
metrics: Protocol.Emulation.SetDeviceMetricsOverrideRequest;
touch: Protocol.Emulation.SetTouchEmulationEnabledRequest;
userAgent: Protocol.Network.SetUserAgentOverrideRequest;
viewport: Viewport;
locale?: string;
timezoneId?: string;
}

const DEVICE_PROFILES: Record<DeviceProfileName, DeviceProfileDefinition> = {
'iPhone-12-Pro': {
metrics: {
width: 390,
height: 844,
deviceScaleFactor: 3,
mobile: true,
screenWidth: 390,
screenHeight: 844,
screenOrientation: {
type: 'portraitPrimary',
angle: 0,
},
positionX: 0,
positionY: 0,
scale: 1,
},
touch: {
enabled: true,
maxTouchPoints: 5,
},
viewport: {
width: 390,
height: 844,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
userAgent: {
userAgent:
'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',
platform: 'iPhone',
acceptLanguage: 'en-US,en',
},
locale: 'en-US',
timezoneId: 'America/Los_Angeles',
},
'Pixel-7': {
metrics: {
width: 412,
height: 915,
deviceScaleFactor: 2.625,
mobile: true,
screenWidth: 412,
screenHeight: 915,
screenOrientation: {
type: 'portraitPrimary',
angle: 0,
},
positionX: 0,
positionY: 0,
scale: 1,
},
touch: {
enabled: true,
maxTouchPoints: 5,
},
viewport: {
width: 412,
height: 915,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
isLandscape: false,
},
userAgent: {
userAgent:
'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',
platform: 'Android',
acceptLanguage: 'en-US,en',
},
locale: 'en-US',
timezoneId: 'America/Los_Angeles',
},
};

function getClient(page: unknown): CDPSession {
return (page as { _client(): CDPSession })._client();
}

export const emulateNetwork = defineTool({
name: 'emulate_network',
description: `Emulates network conditions such as throttling on the selected page.`,
Expand Down Expand Up @@ -74,3 +169,54 @@ export const emulateCpu = defineTool({
context.setCpuThrottlingRate(throttlingRate);
},
});

export const emulateDeviceProfile = defineTool({
name: 'emulate_device_profile',
description:
'Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings.',
annotations: {
category: ToolCategories.EMULATION,
readOnlyHint: false,
},
schema: {
profile: z
.enum(deviceProfileOptions)
.describe(
`The device profile preset to apply. Supported profiles: ${deviceProfileOptions.join(', ')}.`,
),
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
const profileName = request.params.profile;
const profile = DEVICE_PROFILES[profileName];

if (!profile) {
throw new Error(`Unknown device profile: ${profileName}`);
}

const client = getClient(page);

await client.send('Emulation.setDeviceMetricsOverride', profile.metrics);
await client.send('Network.setUserAgentOverride', profile.userAgent);

if (profile.locale) {
await client.send('Emulation.setLocaleOverride', {
locale: profile.locale,
});
}

if (profile.timezoneId) {
await client.send('Emulation.setTimezoneOverride', {
timezoneId: profile.timezoneId,
});
}

await page.setViewport(profile.viewport);

await client.send('Emulation.setTouchEmulationEnabled', profile.touch);

response.appendResponseLine(
`Applied device profile "${profileName}" to the selected page.`,
);
},
});
90 changes: 89 additions & 1 deletion tests/tools/emulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';

import {emulateCpu, emulateNetwork} from '../../src/tools/emulation.js';
import {
emulateCpu,
emulateDeviceProfile,
emulateNetwork,
} from '../../src/tools/emulation.js';
import {withBrowser} from '../utils.js';

describe('emulation', () => {
Expand Down Expand Up @@ -136,4 +140,88 @@ describe('emulation', () => {
});
});
});

describe('device profile', () => {
it('applies iPhone 12 Pro preset', async () => {
await withBrowser(async (response, context) => {
await emulateDeviceProfile.handler(
{
params: {
profile: 'iPhone-12-Pro',
},
},
response,
context,
);

const page = context.getSelectedPage();
const result = await page.evaluate(() => {
return {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
devicePixelRatio: window.devicePixelRatio,
userAgent: navigator.userAgent,
language: navigator.language,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
maxTouchPoints: navigator.maxTouchPoints,
};
});

const viewport = page.viewport();

assert.strictEqual(result.screenWidth, 390);
assert.strictEqual(result.screenHeight, 844);
assert.strictEqual(result.devicePixelRatio, 3);
assert.ok(result.userAgent.includes('iPhone'));
assert.strictEqual(result.language, 'en-US');
assert.strictEqual(result.timeZone, 'America/Los_Angeles');
assert.strictEqual(result.maxTouchPoints, 5);
assert.deepStrictEqual(viewport, {
width: 390,
height: 844,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
});
assert.deepStrictEqual(response.responseLines, [
'Applied device profile "iPhone-12-Pro" to the selected page.',
]);
});
});

it('applies Pixel 7 preset', async () => {
await withBrowser(async (response, context) => {
await emulateDeviceProfile.handler(
{
params: {
profile: 'Pixel-7',
},
},
response,
context,
);

const page = context.getSelectedPage();
const result = await page.evaluate(() => {
return {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
devicePixelRatio: window.devicePixelRatio,
userAgent: navigator.userAgent,
maxTouchPoints: navigator.maxTouchPoints,
};
});

assert.strictEqual(result.screenWidth, 412);
assert.strictEqual(result.screenHeight, 915);
assert.strictEqual(result.devicePixelRatio, 2.625);
assert.ok(result.userAgent.includes('Android'));
assert.strictEqual(result.maxTouchPoints, 5);
assert.deepStrictEqual(response.responseLines, [
'Applied device profile "Pixel-7" to the selected page.',
]);
});
});
});
});