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
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ export const scenario: TestScenario = {
},
};
```

## Restrictions on JSON schema

- no .nullable(), no .object() types.
Comment thread
OrKoN marked this conversation as resolved.
- represent complex object as a short formatted string.

TODO: implement eslint for schema https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1076
12 changes: 6 additions & 6 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->

# Chrome DevTools MCP Tool Reference (~7324 cl100k_base tokens)
# Chrome DevTools MCP Tool Reference (~6919 cl100k_base tokens)

- **[Input automation](#input-automation)** (9 tools)
- [`click`](#click)
Expand Down Expand Up @@ -222,11 +222,11 @@
**Parameters:**

- **colorScheme** (enum: "dark", "light", "auto") _(optional)_: [`Emulate`](#emulate) the dark or the light mode. Set to "auto" to reset to the default.
- **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.
- **geolocation** (unknown) _(optional)_: Geolocation to [`emulate`](#emulate). Set to null to clear the geolocation override.
- **networkConditions** (enum: "No emulation", "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") _(optional)_: Throttle network. Set to "No emulation" to disable. If omitted, conditions remain unchanged.
- **userAgent** (unknown) _(optional)_: User agent to [`emulate`](#emulate). Set to null to clear the user agent override.
- **viewport** (unknown) _(optional)_: Viewport to [`emulate`](#emulate). Set to null to reset to the default viewport.
- **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling
- **geolocation** (string) _(optional)_: Geolocation (`&lt;latitude&gt;x&lt;longitude&gt;`) to [`emulate`](#emulate). Latitude between -90 and 90. Longitude between -180 and 180. Omit clear the geolocation override.
- **networkConditions** (enum: "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") _(optional)_: Throttle network. Omit to disable throttling.
- **userAgent** (string) _(optional)_: User agent to [`emulate`](#emulate). Set to empty string to clear the user agent override.
- **viewport** (string) _(optional)_: [`Emulate`](#emulate) device viewports '&lt;width&gt;x&lt;height&gt;x&lt;devicePixelRatio&gt;[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to [`emulate`](#emulate) mobile devices. 'landscape' to [`emulate`](#emulate) landscape mode.

---

Expand Down
156 changes: 68 additions & 88 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,115 +306,95 @@ export class McpContext implements Context {

async emulate(
options: {
networkConditions?: string | null;
cpuThrottlingRate?: number | null;
geolocation?: GeolocationOptions | null;
userAgent?: string | null;
colorScheme?: 'dark' | 'light' | 'auto' | null;
viewport?: Viewport | null;
networkConditions?: string;
cpuThrottlingRate?: number;
geolocation?: GeolocationOptions;
userAgent?: string;
colorScheme?: 'dark' | 'light' | 'auto';
viewport?: Viewport;
},
targetPage?: Page,
): Promise<void> {
const page = targetPage ?? this.getSelectedPptrPage();
const mcpPage = this.#getMcpPage(page);
const newSettings: EmulationSettings = {...mcpPage.emulationSettings};
let timeoutsNeedUpdate = false;

if (options.networkConditions !== undefined) {
timeoutsNeedUpdate = true;
if (
options.networkConditions === null ||
options.networkConditions === 'No emulation'
) {
await page.emulateNetworkConditions(null);
delete newSettings.networkConditions;
} else if (options.networkConditions === 'Offline') {
await page.emulateNetworkConditions({
offline: true,
download: 0,
upload: 0,
latency: 0,
});
newSettings.networkConditions = 'Offline';
} else if (options.networkConditions in PredefinedNetworkConditions) {
const networkCondition =
PredefinedNetworkConditions[
options.networkConditions as keyof typeof PredefinedNetworkConditions
];
await page.emulateNetworkConditions(networkCondition);
newSettings.networkConditions = options.networkConditions;
}
}

if (options.cpuThrottlingRate !== undefined) {
timeoutsNeedUpdate = true;
if (options.cpuThrottlingRate === null) {
await page.emulateCPUThrottling(1);
delete newSettings.cpuThrottlingRate;
} else {
await page.emulateCPUThrottling(options.cpuThrottlingRate);
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
}
if (!options.networkConditions) {
await page.emulateNetworkConditions(null);
delete newSettings.networkConditions;
Comment thread
OrKoN marked this conversation as resolved.
} else if (options.networkConditions === 'Offline') {
await page.emulateNetworkConditions({
offline: true,
download: 0,
upload: 0,
latency: 0,
});
newSettings.networkConditions = 'Offline';
} else if (options.networkConditions in PredefinedNetworkConditions) {
const networkCondition =
PredefinedNetworkConditions[
options.networkConditions as keyof typeof PredefinedNetworkConditions
];
await page.emulateNetworkConditions(networkCondition);
newSettings.networkConditions = options.networkConditions;
}

if (!options.cpuThrottlingRate) {
await page.emulateCPUThrottling(1);
delete newSettings.cpuThrottlingRate;
} else {
await page.emulateCPUThrottling(options.cpuThrottlingRate);
newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
}

if (options.geolocation !== undefined) {
if (options.geolocation === null) {
await page.setGeolocation({latitude: 0, longitude: 0});
delete newSettings.geolocation;
} else {
await page.setGeolocation(options.geolocation);
newSettings.geolocation = options.geolocation;
}
if (!options.geolocation) {
await page.setGeolocation({latitude: 0, longitude: 0});
delete newSettings.geolocation;
} else {
await page.setGeolocation(options.geolocation);
newSettings.geolocation = options.geolocation;
}

if (options.userAgent !== undefined) {
if (options.userAgent === null) {
await page.setUserAgent({userAgent: undefined});
delete newSettings.userAgent;
} else {
await page.setUserAgent({userAgent: options.userAgent});
newSettings.userAgent = options.userAgent;
}
if (!options.userAgent) {
await page.setUserAgent({userAgent: undefined});
delete newSettings.userAgent;
} else {
await page.setUserAgent({userAgent: options.userAgent});
newSettings.userAgent = options.userAgent;
}

if (options.colorScheme !== undefined) {
if (options.colorScheme === null || options.colorScheme === 'auto') {
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: ''},
]);
delete newSettings.colorScheme;
} else {
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: options.colorScheme},
]);
newSettings.colorScheme = options.colorScheme;
}
if (!options.colorScheme || options.colorScheme === 'auto') {
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: ''},
]);
delete newSettings.colorScheme;
} else {
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: options.colorScheme},
]);
newSettings.colorScheme = options.colorScheme;
}

if (options.viewport !== undefined) {
if (options.viewport === null) {
await page.setViewport(null);
delete newSettings.viewport;
} else {
const defaults = {
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
};
const viewport = {...defaults, ...options.viewport};
await page.setViewport(viewport);
newSettings.viewport = viewport;
}
if (!options.viewport) {
await page.setViewport(null);
delete newSettings.viewport;
} else {
const defaults = {
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
};
const viewport = {...defaults, ...options.viewport};
await page.setViewport(viewport);
newSettings.viewport = viewport;
}

mcpPage.emulationSettings = Object.keys(newSettings).length
? newSettings
: {};

if (timeoutsNeedUpdate) {
this.#updateSelectedPageTimeouts();
}
this.#updateSelectedPageTimeouts();
}

setIsRunningPerformanceTrace(x: boolean): void {
Expand Down
55 changes: 49 additions & 6 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,12 @@ export type Context = Readonly<{
restoreEmulation(page: ContextPage): Promise<void>;
emulate(
options: {
networkConditions?: string | null;
cpuThrottlingRate?: number | null;
geolocation?: GeolocationOptions | null;
userAgent?: string | null;
colorScheme?: 'dark' | 'light' | 'auto' | null;
viewport?: Viewport | null;
networkConditions?: string;
cpuThrottlingRate?: number;
geolocation?: GeolocationOptions;
userAgent?: string;
colorScheme?: 'dark' | 'light' | 'auto';
viewport?: Viewport;
},
targetPage?: Page,
): Promise<void>;
Expand Down Expand Up @@ -310,3 +310,46 @@ export const timeoutSchema = {
return value && value <= 0 ? undefined : value;
}),
};

export function viewportTransform(arg: string | undefined):
| {
width: number;
height: number;
deviceScaleFactor?: number;
isMobile?: boolean;
isLandscape?: boolean;
hasTouch?: boolean;
}
| undefined {
if (!arg) {
return undefined;
}
const [dimensions, ...tags] = arg.split(',');
const isMobile = tags.includes('mobile');
const hasTouch = tags.includes('touch');
const isLandscape = tags.includes('landscape');
const [width, height, dpr] = dimensions.split('x').map(Number) as [
number,
number,
number | undefined,
];
return {
width,
height,
deviceScaleFactor: dpr,
isMobile: isMobile,
isLandscape: isLandscape,
hasTouch: hasTouch,
};
}

export function geolocationTransform(arg: string | undefined) {
if (!arg) {
return undefined;
}
const [latitude, longitude] = arg.split('x').map(Number) as [number, number];
return {
latitude,
longitude,
};
}
65 changes: 14 additions & 51 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import {zod, PredefinedNetworkConditions} from '../third_party/index.js';

import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
import {
definePageTool,
geolocationTransform,
viewportTransform,
} from './ToolDefinition.js';

const throttlingOptions: [string, ...string[]] = [
'No emulation',
'Offline',
...Object.keys(PredefinedNetworkConditions),
];
Expand All @@ -27,41 +30,27 @@ export const emulate = definePageTool({
networkConditions: zod
.enum(throttlingOptions)
.optional()
.describe(
`Throttle network. Set to "No emulation" to disable. If omitted, conditions remain unchanged.`,
),
.describe(`Throttle network. Omit to disable throttling.`),
cpuThrottlingRate: zod
.number()
.min(1)
.max(20)
.optional()
.describe(
'Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.',
'Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling',
),
geolocation: zod
.object({
latitude: zod
.number()
.min(-90)
.max(90)
.describe('Latitude between -90 and 90.'),
longitude: zod
.number()
.min(-180)
.max(180)
.describe('Longitude between -180 and 180.'),
})
.nullable()
.string()
.optional()
.transform(geolocationTransform)
.describe(
'Geolocation to emulate. Set to null to clear the geolocation override.',
'Geolocation (`<latitude>x<longitude>`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit clear the geolocation override.',
),
userAgent: zod
.string()
.nullable()
.optional()
.describe(
'User agent to emulate. Set to null to clear the user agent override.',
'User agent to emulate. Set to empty string to clear the user agent override.',
),
colorScheme: zod
.enum(['dark', 'light', 'auto'])
Expand All @@ -70,37 +59,11 @@ export const emulate = definePageTool({
'Emulate the dark or the light mode. Set to "auto" to reset to the default.',
),
viewport: zod
.object({
width: zod.number().int().min(0).describe('Page width in pixels.'),
height: zod.number().int().min(0).describe('Page height in pixels.'),
deviceScaleFactor: zod
.number()
.min(0)
.optional()
.describe('Specify device scale factor (can be thought of as dpr).'),
isMobile: zod
.boolean()
.optional()
.describe(
'Whether the meta viewport tag is taken into account. Defaults to false.',
),
hasTouch: zod
.boolean()
.optional()
.describe(
'Specifies if viewport supports touch events. This should be set to true for mobile devices.',
),
isLandscape: zod
.boolean()
.optional()
.describe(
'Specifies if viewport is in landscape mode. Defaults to false.',
),
})
.nullable()
.string()
.optional()
.transform(viewportTransform)
.describe(
'Viewport to emulate. Set to null to reset to the default viewport.',
`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`,
),
},
handler: async (request, _response, context) => {
Expand Down
Loading
Loading