豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
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
2 changes: 2 additions & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@
- **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.

---

Expand Down
22 changes: 22 additions & 0 deletions scripts/eval_scenarios/emulation_userAgent_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'node:assert';

import type {TestScenario} from '../eval_gemini.ts';

export const scenario: TestScenario = {
prompt: 'Emulate iPhone 14 user agent',
maxTurns: 2,
expectations: calls => {
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].name, 'emulate');
assert.deepStrictEqual(
calls[0].args.userAgent,
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
);
},
};
31 changes: 31 additions & 0 deletions scripts/eval_scenarios/emulation_viewport_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'node:assert';

import {KnownDevices} from 'puppeteer';

import type {TestScenario} from '../eval_gemini.ts';

export const scenario: TestScenario = {
prompt: 'Emulate iPhone 14 viewport',
maxTurns: 2,
expectations: calls => {
assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].name, 'emulate');
assert.deepStrictEqual(
{
...(calls[0].args.viewport as object),
// models might not send defaults.
isLandscape: KnownDevices['iPhone 14'].viewport.isLandscape ?? false,
},
{
...KnownDevices['iPhone 14'].viewport,
height: 844, // Puppeteer is wrong about the expected height.
},
);
},
};
31 changes: 31 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
Page,
SerializedAXNode,
PredefinedNetworkConditions,
Viewport,
} from './third_party/index.js';
import {listPages} from './tools/pages.js';
import {takeSnapshot} from './tools/snapshot.js';
Expand Down Expand Up @@ -115,6 +116,8 @@ export class McpContext implements Context {
#networkConditionsMap = new WeakMap<Page, string>();
#cpuThrottlingRateMap = new WeakMap<Page, number>();
#geolocationMap = new WeakMap<Page, GeolocationOptions>();
#viewportMap = new WeakMap<Page, Viewport>();
#userAgentMap = new WeakMap<Page, string>();
#dialog?: Dialog;

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

setViewport(viewport: Viewport | null): void {
const page = this.getSelectedPage();
if (viewport === null) {
this.#viewportMap.delete(page);
} else {
this.#viewportMap.set(page, viewport);
}
}

getViewport(): Viewport | null {
const page = this.getSelectedPage();
return this.#viewportMap.get(page) ?? null;
}

setUserAgent(userAgent: string | null): void {
const page = this.getSelectedPage();
if (userAgent === null) {
this.#userAgentMap.delete(page);
} else {
this.#userAgentMap.set(page, userAgent);
}
}

getUserAgent(): string | null {
const page = this.getSelectedPage();
return this.#userAgentMap.get(page) ?? null;
}

setIsRunningPerformanceTrace(x: boolean): void {
this.#isRunningTrace = x;
}
Expand Down
12 changes: 12 additions & 0 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,18 @@ export class McpResponse implements Response {
);
}

const viewport = context.getViewport();
if (viewport) {
response.push(`## Viewport emulation`);
response.push(`Emulating viewport: ${JSON.stringify(viewport)}`);
}

const userAgent = context.getUserAgent();
if (userAgent) {
response.push(`## UserAgent emulation`);
response.push(`Emulating userAgent: ${userAgent}`);
}

const cpuThrottlingRate = context.getCpuThrottlingRate();
if (cpuThrottlingRate > 1) {
response.push(`## CPU emulation`);
Expand Down
1 change: 1 addition & 0 deletions src/third_party/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {z as zod} from 'zod';
export {
Locator,
PredefinedNetworkConditions,
KnownDevices,
CDPSessionEvent,
} from 'puppeteer-core';
export {default as puppeteer} from 'puppeteer-core';
Expand Down
11 changes: 10 additions & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

import type {TextSnapshotNode, GeolocationOptions} from '../McpContext.js';
import {zod} from '../third_party/index.js';
import type {Dialog, ElementHandle, Page} from '../third_party/index.js';
import type {
Dialog,
ElementHandle,
Page,
Viewport,
} from '../third_party/index.js';
import type {TraceResult} from '../trace-processing/parse.js';
import type {PaginationOptions} from '../utils/types.js';

Expand Down Expand Up @@ -105,6 +110,10 @@ export type Context = Readonly<{
setNetworkConditions(conditions: string | null): void;
setCpuThrottlingRate(rate: number): void;
setGeolocation(geolocation: GeolocationOptions | null): void;
setViewport(viewport: Viewport | null): void;
getViewport(): Viewport | null;
setUserAgent(userAgent: string | null): void;
getUserAgent(): string | null;
saveTemporaryFile(
data: Uint8Array<ArrayBufferLike>,
mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
Expand Down
79 changes: 78 additions & 1 deletion src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
*/

import {zod, PredefinedNetworkConditions} from '../third_party/index.js';
Expand Down Expand Up @@ -55,10 +56,56 @@ export const emulate = defineTool({
.describe(
'Geolocation to emulate. Set to null to clear the geolocation override.',
),
userAgent: zod
.string()
.nullable()
Comment thread
Lightning00Blade marked this conversation as resolved.
.optional()
.describe(
'User agent to emulate. Set to null to clear the user agent override.',
),
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()
Comment thread
Lightning00Blade marked this conversation as resolved.
.optional()
.describe(
'Viewport to emulate. Set to null to reset to the default viewport.',
),
},
handler: async (request, _response, context) => {
const page = context.getSelectedPage();
const {networkConditions, cpuThrottlingRate, geolocation} = request.params;
const {
networkConditions,
cpuThrottlingRate,
geolocation,
userAgent,
viewport,
} = request.params;

if (networkConditions) {
if (networkConditions === 'No emulation') {
Expand Down Expand Up @@ -96,5 +143,35 @@ export const emulate = defineTool({
context.setGeolocation(geolocation);
}
}

if (userAgent !== undefined) {
if (userAgent === null) {
await page.setUserAgent({
userAgent: undefined,
});
context.setUserAgent(null);
} else {
await page.setUserAgent({
userAgent,
});
context.setUserAgent(userAgent);
}
}

if (viewport !== undefined) {
if (viewport === null) {
await page.setViewport(null);
context.setViewport(null);
} else {
const defaults = {
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
};
await page.setViewport({...defaults, ...viewport});
context.setViewport({...defaults, ...viewport});
}
}
},
});
12 changes: 12 additions & 0 deletions tests/McpResponse.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ Emulating: Slow 3G
Default navigation timeout set to 100000 ms
`;

exports[`McpResponse > adds userAgent emulation setting when it is set 1`] = `
# test response
## UserAgent emulation
Emulating userAgent: MyUA
`;

exports[`McpResponse > adds viewport emulation setting when it is set 1`] = `
# test response
## Viewport emulation
Emulating viewport: {"width":400,"height":400,"deviceScaleFactor":1}
`;

exports[`McpResponse > allows response text lines to be added 1`] = `
# test response
Testing 1
Expand Down
16 changes: 16 additions & 0 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ describe('McpResponse', () => {
});
});

it('adds viewport emulation setting when it is set', async t => {
await withMcpContext(async (response, context) => {
context.setViewport({width: 400, height: 400, deviceScaleFactor: 1});
const {content} = await response.handle('test', context);
t.assert.snapshot?.(getTextContent(content[0]));
});
});

it('adds userAgent emulation setting when it is set', async t => {
await withMcpContext(async (response, context) => {
context.setUserAgent('MyUA');
const {content} = await response.handle('test', context);
t.assert.snapshot?.(getTextContent(content[0]));
});
});

it('adds a prompt dialog', async t => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
Expand Down
Loading