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
1 change: 1 addition & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@

**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.
Expand Down
15 changes: 15 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class McpContext implements Context {
#geolocationMap = new WeakMap<Page, GeolocationOptions>();
#viewportMap = new WeakMap<Page, Viewport>();
#userAgentMap = new WeakMap<Page, string>();
#colorSchemeMap = new WeakMap<Page, 'dark' | 'light'>();
#dialog?: Dialog;

#pageIdMap = new WeakMap<Page, number>();
Expand Down Expand Up @@ -353,6 +354,20 @@ export class McpContext implements Context {
return this.#userAgentMap.get(page) ?? null;
}

setColorScheme(scheme: 'dark' | 'light' | null): void {
const page = this.getSelectedPage();
if (scheme === null) {
this.#colorSchemeMap.delete(page);
} else {
this.#colorSchemeMap.set(page, scheme);
}
}

getColorScheme(): 'dark' | 'light' | null {
const page = this.getSelectedPage();
return this.#colorSchemeMap.get(page) ?? null;
}

setIsRunningPerformanceTrace(x: boolean): void {
this.#isRunningTrace = x;
}
Expand Down
8 changes: 8 additions & 0 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export class McpResponse implements Response {
viewport?: object;
userAgent?: string;
cpuThrottlingRate?: number;
colorScheme?: string;
dialog?: {
type: string;
message: string;
Expand Down Expand Up @@ -482,6 +483,13 @@ export class McpResponse implements Response {
structuredContent.cpuThrottlingRate = cpuThrottlingRate;
}

const colorScheme = context.getColorScheme();
if (colorScheme) {
response.push(`## Color Scheme emulation`);
response.push(`Emulating: ${colorScheme}`);
structuredContent.colorScheme = colorScheme;
}

const dialog = context.getDialog();
if (dialog) {
const defaultValueIfNeeded =
Expand Down
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type Context = Readonly<{
getViewport(): Viewport | null;
setUserAgent(userAgent: string | null): void;
getUserAgent(): string | null;
setColorScheme(scheme: 'dark' | 'light' | null): void;
saveTemporaryFile(
data: Uint8Array<ArrayBufferLike>,
mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
Expand Down
23 changes: 23 additions & 0 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export const emulate = defineTool({
.describe(
'User agent to emulate. Set to null to clear the user agent override.',
),
colorScheme: zod
.enum(['dark', 'light', 'auto'])
.optional()
.describe(
'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.'),
Expand Down Expand Up @@ -158,6 +164,23 @@ export const emulate = defineTool({
}
}

if (request.params.colorScheme) {
if (request.params.colorScheme === 'auto') {
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: ''},
]);
context.setColorScheme(null);
} else {
await page.emulateMediaFeatures([
{
name: 'prefers-color-scheme',
value: request.params.colorScheme,
},
]);
context.setColorScheme(request.params.colorScheme);
}
}

if (viewport !== undefined) {
if (viewport === null) {
await page.setViewport(null);
Expand Down
12 changes: 12 additions & 0 deletions tests/McpResponse.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ exports[`McpResponse > adds an alert dialog 2`] = `
}
`;

exports[`McpResponse > adds color scheme emulation setting when it is set 1`] = `
# test response
## Color Scheme emulation
Emulating: dark
`;

exports[`McpResponse > adds color scheme emulation setting when it is set 2`] = `
{
"colorScheme": "dark"
}
`;

exports[`McpResponse > adds console messages when the setting is true 1`] = `
# test response
## Console messages
Expand Down
14 changes: 14 additions & 0 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ describe('McpResponse', () => {
});
});

it('adds color scheme emulation setting when it is set', async t => {
await withMcpContext(async (response, context) => {
context.setColorScheme('dark');
const {content, structuredContent} = await response.handle(
'test',
context,
);
t.assert.snapshot?.(getTextContent(content[0]));
t.assert.snapshot?.(
JSON.stringify(stabilizeStructuredContent(structuredContent), null, 2),
);
});
});

it('adds a prompt dialog', async t => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
Expand Down
105 changes: 105 additions & 0 deletions tests/tools/emulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,109 @@ describe('emulation', () => {
});
});
});

describe('colorScheme', () => {
it('emulates color scheme', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
colorScheme: 'dark',
},
},
response,
context,
);

assert.strictEqual(context.getColorScheme(), 'dark');
const page = context.getSelectedPage();
const scheme = await page.evaluate(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
);
assert.strictEqual(scheme, 'dark');
});
});

it('updates color scheme', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
colorScheme: 'dark',
},
},
response,
context,
);
assert.strictEqual(context.getColorScheme(), 'dark');

await emulate.handler(
{
params: {
colorScheme: 'light',
},
},
response,
context,
);
assert.strictEqual(context.getColorScheme(), 'light');
const page = context.getSelectedPage();
const scheme = await page.evaluate(() =>
window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark',
);
assert.strictEqual(scheme, 'light');
});
});

it('resets color scheme when set to auto', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();

const initial = await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
);

await emulate.handler(
{
params: {
colorScheme: 'dark',
},
},
response,
context,
);
assert.strictEqual(context.getColorScheme(), 'dark');
// Check manually that it is dark

assert.strictEqual(
await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
),
true,
);

await emulate.handler(
{
params: {
colorScheme: 'auto',
},
},
response,
context,
);

assert.strictEqual(context.getColorScheme(), null);
assert.strictEqual(
await page.evaluate(
() => window.matchMedia('(prefers-color-scheme: dark)').matches,
),
initial,
);
});
});
});
});