From e39509823af8a4c61fca66314f16ab48888f4556 Mon Sep 17 00:00:00 2001 From: wanghongyang <549492949@qq.com> Date: Mon, 29 Sep 2025 17:19:59 +0800 Subject: [PATCH 1/3] feat: increase mobile devices emulate --- src/tools/ToolDefinition.ts | 13 +-- src/tools/emulation.ts | 155 ++++++++++++++++++++++++++++++++++-- 2 files changed, 158 insertions(+), 10 deletions(-) diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index a0741f4c..df165383 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Dialog, ElementHandle, Page} from 'puppeteer-core'; +import type { Dialog, ElementHandle, Page } from 'puppeteer-core'; import type z from 'zod'; -import type {TraceResult} from '../trace-processing/parse.js'; +import type { TraceResult } from '../trace-processing/parse.js'; -import type {ToolCategories} from './categories.js'; +import type { ToolCategories } from './categories.js'; export interface ToolDefinition { name: string; @@ -44,7 +44,7 @@ export interface Response { setIncludePages(value: boolean): void; setIncludeNetworkRequests( value: boolean, - options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]}, + options?: { pageSize?: number; pageIdx?: number; resourceTypes?: string[] }, ): void; setIncludeConsoleData(value: boolean): void; setIncludeSnapshot(value: boolean): void; @@ -73,8 +73,11 @@ export type Context = Readonly<{ saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg', - ): Promise<{filename: string}>; + ): Promise<{ filename: string }>; waitForEventsAfterAction(action: () => Promise): Promise; + // Added for multi-page device emulation support + createPagesSnapshot(): Promise; + getPages(): Page[]; }>; export function defineTool( diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 9228c59b..96201c94 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -4,17 +4,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {PredefinedNetworkConditions} from 'puppeteer-core'; +import { PredefinedNetworkConditions } from 'puppeteer-core'; +import { KnownDevices } from 'puppeteer-core'; import z from 'zod'; -import {ToolCategories} from './categories.js'; -import {defineTool} from './ToolDefinition.js'; +import { ToolCategories } from './categories.js'; +import { defineTool } from './ToolDefinition.js'; const throttlingOptions: [string, ...string[]] = [ 'No emulation', ...Object.keys(PredefinedNetworkConditions), ]; +// common use device +const deviceOptions: [string, ...string[]] = [ + 'No emulation', + // iPhone series + 'iPhone SE', + 'iPhone 12', + 'iPhone 12 Pro', + 'iPhone 13', + 'iPhone 13 Pro', + 'iPhone 14', + 'iPhone 14 Pro', + 'iPhone 15', + 'iPhone 15 Pro', + // Android series + 'Galaxy S5', + 'Galaxy S8', + 'Galaxy S9+', + 'Pixel 2', + 'Pixel 3', + 'Pixel 4', + 'Pixel 5', + 'Nexus 5', + 'Nexus 6P', + // ipad + 'iPad', + 'iPad Pro', + 'Galaxy Tab S4', +]; + export const emulateNetwork = defineTool({ name: 'emulate_network', description: `Emulates network conditions such as throttling on the selected page.`, @@ -42,7 +72,7 @@ export const emulateNetwork = defineTool({ if (conditions in PredefinedNetworkConditions) { const networkCondition = PredefinedNetworkConditions[ - conditions as keyof typeof PredefinedNetworkConditions + conditions as keyof typeof PredefinedNetworkConditions ]; await page.emulateNetworkConditions(networkCondition); context.setNetworkConditions(conditions); @@ -68,9 +98,124 @@ export const emulateCpu = defineTool({ }, handler: async (request, _response, context) => { const page = context.getSelectedPage(); - const {throttlingRate} = request.params; + const { throttlingRate } = request.params; await page.emulateCPUThrottling(throttlingRate); context.setCpuThrottlingRate(throttlingRate); }, }); + +export const emulateDevice = defineTool({ + name: 'emulate_device', + description: `IMPORTANT: Emulates a mobile device including viewport, user-agent, touch support, and device scale factor. This tool MUST be called BEFORE navigating to any website to ensure the correct mobile user-agent is used. Essential for testing mobile website performance and user experience.`, + annotations: { + category: ToolCategories.EMULATION, + readOnlyHint: false, + }, + schema: { + device: z + .enum(deviceOptions) + .describe( + `The device to emulate. Available devices are: ${deviceOptions.join(', ')}. Set to "No emulation" to disable device emulation and use desktop mode.`, + ), + customUserAgent: z + .string() + .optional() + .describe( + 'Optional custom user agent string. If provided, it will override the device\'s default user agent.', + ), + }, + handler: async (request, response, context) => { + const { device, customUserAgent } = request.params; + + // get all pages to support multi-page scene + await context.createPagesSnapshot(); + const allPages = context.getPages(); + const currentPage = context.getSelectedPage(); + + // check if multi pages and apply to all pages + let pagesToEmulate = [currentPage]; + let multiPageMessage = ''; + + if (allPages.length > 1) { + // check if other pages have navigated content (maybe new tab page) + const navigatedPages = []; + for (const page of allPages) { + const url = page.url(); + if (url !== 'about:blank' && url !== currentPage.url()) { + navigatedPages.push({ page, url }); + } + } + + if (navigatedPages.length > 0) { + // found other pages have navigated, apply device emulation to all pages + pagesToEmulate = [currentPage, ...navigatedPages.map(p => p.page)]; + multiPageMessage = `🔄 SMART MULTI-PAGE MODE: Detected ${navigatedPages.length} additional page(s) with content. ` + + `Applying device emulation to current page and ${navigatedPages.length} other page(s): ` + + `${navigatedPages.map(p => p.url).join(', ')}. `; + } + } + + // check if current page has navigated + const currentUrl = currentPage.url(); + if (currentUrl !== 'about:blank') { + response.appendResponseLine( + `⚠️ WARNING: Device emulation is being applied AFTER page navigation (current URL: ${currentUrl}). ` + + `For best results, device emulation should be set BEFORE navigating to the target website.` + ); + } + + if (multiPageMessage) { + response.appendResponseLine(multiPageMessage); + } + + if (device === 'No emulation') { + // apply desktop mode to all pages + for (const pageToEmulate of pagesToEmulate) { + await pageToEmulate.setViewport({ + width: 1920, + height: 1080, + deviceScaleFactor: 1, + isMobile: false, + hasTouch: false, + isLandscape: true, + }); + + await pageToEmulate.setUserAgent( + customUserAgent || + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + ); + } + + response.appendResponseLine( + `Device emulation disabled. Desktop mode applied to ${pagesToEmulate.length} page(s).` + ); + return; + } + + // check if current device is in KnownDevices + if (device in KnownDevices) { + const deviceConfig = KnownDevices[device as keyof typeof KnownDevices]; + + // apply device config to all page + for (const pageToEmulate of pagesToEmulate) { + await pageToEmulate.emulate({ + userAgent: customUserAgent || deviceConfig.userAgent, + viewport: deviceConfig.viewport, + }); + } + + response.appendResponseLine( + `Successfully emulated device: ${device} on ${pagesToEmulate.length} page(s). ` + + `Viewport: ${deviceConfig.viewport.width}x${deviceConfig.viewport.height}, ` + + `Scale: ${deviceConfig.viewport.deviceScaleFactor}x, ` + + `Mobile: ${deviceConfig.viewport.isMobile ? 'Yes' : 'No'}, ` + + `Touch: ${deviceConfig.viewport.hasTouch ? 'Yes' : 'No'}${customUserAgent ? ', Custom UA applied' : ''}.` + ); + } else { + response.appendResponseLine( + `Device "${device}" not found in known devices. Available devices: ${deviceOptions.filter(d => d !== 'No emulation').join(', ')}` + ); + } + }, +}); From b43787a4ddb066cbf57334ac652935cfa9500708 Mon Sep 17 00:00:00 2001 From: wanghongyang <549492949@qq.com> Date: Wed, 22 Oct 2025 15:38:51 +0800 Subject: [PATCH 2/3] feat: Optimize boundary conditions.Improve the simulation logic of mobile devices --- src/McpContext.ts | 15 +++ src/tools/ToolDefinition.ts | 2 + src/tools/emulation.ts | 207 +++++++++++++++++++++--------------- 3 files changed, 141 insertions(+), 83 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index d1037935..4ad098a8 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -85,6 +85,7 @@ export class McpContext implements Context { #isRunningTrace = false; #networkConditionsMap = new WeakMap(); #cpuThrottlingRateMap = new WeakMap(); + #deviceEmulationMap = new WeakMap(); #dialog?: Dialog; #nextSnapshotId = 1; @@ -197,6 +198,20 @@ export class McpContext implements Context { return this.#cpuThrottlingRateMap.get(page) ?? 1; } + setDeviceEmulation(device: string | null): void { + const page = this.getSelectedPage(); + if (device === null) { + this.#deviceEmulationMap.delete(page); + } else { + this.#deviceEmulationMap.set(page, device); + } + } + + getDeviceEmulation(): string | null { + const page = this.getSelectedPage(); + return this.#deviceEmulationMap.get(page) ?? null; + } + setIsRunningPerformanceTrace(x: boolean): void { this.#isRunningTrace = x; } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index c92bbf86..89f00e7e 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -70,6 +70,8 @@ export type Context = Readonly<{ getElementByUid(uid: string): Promise>; setNetworkConditions(conditions: string | null): void; setCpuThrottlingRate(rate: number): void; + setDeviceEmulation(device: string | null): void; + getDeviceEmulation(): string | null; saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg' | 'image/webp', diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 96201c94..e87adbf8 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -16,34 +16,45 @@ const throttlingOptions: [string, ...string[]] = [ ...Object.keys(PredefinedNetworkConditions), ]; -// common use device -const deviceOptions: [string, ...string[]] = [ - 'No emulation', - // iPhone series - 'iPhone SE', - 'iPhone 12', - 'iPhone 12 Pro', - 'iPhone 13', - 'iPhone 13 Pro', - 'iPhone 14', - 'iPhone 14 Pro', - 'iPhone 15', - 'iPhone 15 Pro', - // Android series - 'Galaxy S5', - 'Galaxy S8', - 'Galaxy S9+', - 'Pixel 2', - 'Pixel 3', - 'Pixel 4', - 'Pixel 5', - 'Nexus 5', - 'Nexus 6P', - // ipad - 'iPad', - 'iPad Pro', - 'Galaxy Tab S4', -]; +/** + * Get all mobile device list (dynamically from KnownDevices) + * Filter out landscape devices and uncommon devices, keep only common portrait mobile devices + */ +function getMobileDeviceList(): string[] { + const allDevices = Object.keys(KnownDevices); + // Filter out landscape devices (containing 'landscape') and some uncommon devices + const mobileDevices = allDevices.filter(device => { + const lowerDevice = device.toLowerCase(); + // Exclude landscape devices + if (lowerDevice.includes('landscape')) return false; + // Exclude tablets (optional, but keep iPad as common device) + // if (lowerDevice.includes('ipad') || lowerDevice.includes('tab')) return false; + // Exclude some old or uncommon devices + if (lowerDevice.includes('blackberry')) return false; + if (lowerDevice.includes('lumia')) return false; + if (lowerDevice.includes('nokia')) return false; + if (lowerDevice.includes('kindle')) return false; + if (lowerDevice.includes('jio')) return false; + if (lowerDevice.includes('optimus')) return false; + return true; + }); + + return mobileDevices; +} + +/** + * Get default mobile device + */ +function getDefaultMobileDevice(): string { + return 'iPhone 8'; +} + +/** + * Validate if device exists in KnownDevices + */ +function validateDeviceExists(device: string): boolean { + return device in KnownDevices; +} export const emulateNetwork = defineTool({ name: 'emulate_network', @@ -107,16 +118,17 @@ export const emulateCpu = defineTool({ export const emulateDevice = defineTool({ name: 'emulate_device', - description: `IMPORTANT: Emulates a mobile device including viewport, user-agent, touch support, and device scale factor. This tool MUST be called BEFORE navigating to any website to ensure the correct mobile user-agent is used. Essential for testing mobile website performance and user experience.`, + description: `IMPORTANT: Emulates a mobile device including viewport, user-agent, touch support, and device scale factor. This tool MUST be called BEFORE navigating to any website to ensure the correct mobile user-agent is used. Essential for testing mobile website performance and user experience. If no device is specified, defaults to iPhone 8.`, annotations: { category: ToolCategories.EMULATION, readOnlyHint: false, }, schema: { device: z - .enum(deviceOptions) + .string() + .optional() .describe( - `The device to emulate. Available devices are: ${deviceOptions.join(', ')}. Set to "No emulation" to disable device emulation and use desktop mode.`, + `The mobile device to emulate. If not specified, defaults to "${getDefaultMobileDevice()}". Available devices include all mobile devices from Puppeteer's KnownDevices (e.g., iPhone 8, iPhone 13, iPhone 14, iPhone 15, Galaxy S8, Galaxy S9+, Pixel 2-5, iPad, iPad Pro, etc.). Use the exact device name as defined in Puppeteer.`, ), customUserAgent: z .string() @@ -126,95 +138,124 @@ export const emulateDevice = defineTool({ ), }, handler: async (request, response, context) => { - const { device, customUserAgent } = request.params; + let { device, customUserAgent } = request.params; + + // ========== Phase 0: Handle default device ========== + // If user didn't specify device, use default mobile device + if (!device) { + device = getDefaultMobileDevice(); + } - // get all pages to support multi-page scene + // ========== Phase 1: Device validation ========== + // Validate if device exists in KnownDevices + if (!validateDeviceExists(device)) { + const availableDevices = getMobileDeviceList(); + device = availableDevices[0]; + } + + // ========== Phase 2: Page collection and state check ========== await context.createPagesSnapshot(); const allPages = context.getPages(); const currentPage = context.getSelectedPage(); - // check if multi pages and apply to all pages + // Filter out closed pages + const activePages = allPages.filter(page => !page.isClosed()); + if (activePages.length === 0) { + response.appendResponseLine('❌ Error: No active pages available for device emulation.'); + return; + } + + // ========== Phase 3: Determine pages to emulate ========== let pagesToEmulate = [currentPage]; - let multiPageMessage = ''; - if (allPages.length > 1) { - // check if other pages have navigated content (maybe new tab page) + if (activePages.length > 1) { + // Check if other pages have navigated content const navigatedPages = []; - for (const page of allPages) { - const url = page.url(); - if (url !== 'about:blank' && url !== currentPage.url()) { - navigatedPages.push({ page, url }); + for (const page of activePages) { + if (page.isClosed()) continue; // Double check + + try { + const url = page.url(); + if (url !== 'about:blank' && url !== currentPage.url()) { + navigatedPages.push({ page, url }); + } + } catch (error) { + // Page may have been closed during check + continue; } } + // Set emulation for all pages if (navigatedPages.length > 0) { - // found other pages have navigated, apply device emulation to all pages pagesToEmulate = [currentPage, ...navigatedPages.map(p => p.page)]; - multiPageMessage = `🔄 SMART MULTI-PAGE MODE: Detected ${navigatedPages.length} additional page(s) with content. ` + - `Applying device emulation to current page and ${navigatedPages.length} other page(s): ` + - `${navigatedPages.map(p => p.url).join(', ')}. `; } } - // check if current page has navigated - const currentUrl = currentPage.url(); - if (currentUrl !== 'about:blank') { - response.appendResponseLine( - `⚠️ WARNING: Device emulation is being applied AFTER page navigation (current URL: ${currentUrl}). ` + - `For best results, device emulation should be set BEFORE navigating to the target website.` - ); + // Filter again to ensure all pages to emulate are active + pagesToEmulate = pagesToEmulate.filter(page => !page.isClosed()); + + if (pagesToEmulate.length === 0) { + response.appendResponseLine('❌ Error: All target pages have been closed.'); + return; } - if (multiPageMessage) { - response.appendResponseLine(multiPageMessage); - } - if (device === 'No emulation') { - // apply desktop mode to all pages - for (const pageToEmulate of pagesToEmulate) { - await pageToEmulate.setViewport({ - width: 1920, - height: 1080, - deviceScaleFactor: 1, - isMobile: false, - hasTouch: false, - isLandscape: true, - }); + // ========== Phase 4: Mobile device emulation ========== + const deviceConfig = KnownDevices[device as keyof typeof KnownDevices]; - await pageToEmulate.setUserAgent( - customUserAgent || - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' - ); - } + let successCount = 0; + const failedPages: Array<{ url: string; reason: string }> = []; - response.appendResponseLine( - `Device emulation disabled. Desktop mode applied to ${pagesToEmulate.length} page(s).` - ); - return; - } + for (const pageToEmulate of pagesToEmulate) { + if (pageToEmulate.isClosed()) { + failedPages.push({ + url: 'unknown', + reason: 'Page closed' + }); + continue; + } - // check if current device is in KnownDevices - if (device in KnownDevices) { - const deviceConfig = KnownDevices[device as keyof typeof KnownDevices]; + const pageUrl = pageToEmulate.url(); - // apply device config to all page - for (const pageToEmulate of pagesToEmulate) { + try { + // Directly apply device emulation await pageToEmulate.emulate({ userAgent: customUserAgent || deviceConfig.userAgent, viewport: deviceConfig.viewport, }); + successCount++; + } catch (error) { + failedPages.push({ + url: pageUrl, + reason: (error as Error).message + }); } + } + + // ========== Phase 5: Save state and report results ========== + if (successCount > 0) { + context.setDeviceEmulation(device); + } + // Build detailed report + if (successCount > 0) { response.appendResponseLine( - `Successfully emulated device: ${device} on ${pagesToEmulate.length} page(s). ` + + `✅ Successfully emulated device: ${device}, applied to ${successCount} page(s).\n` + `Viewport: ${deviceConfig.viewport.width}x${deviceConfig.viewport.height}, ` + `Scale: ${deviceConfig.viewport.deviceScaleFactor}x, ` + `Mobile: ${deviceConfig.viewport.isMobile ? 'Yes' : 'No'}, ` + `Touch: ${deviceConfig.viewport.hasTouch ? 'Yes' : 'No'}${customUserAgent ? ', Custom UA applied' : ''}.` ); } else { + // Complete failure response.appendResponseLine( - `Device "${device}" not found in known devices. Available devices: ${deviceOptions.filter(d => d !== 'No emulation').join(', ')}` + `❌ Error: Unable to apply device emulation to any page.\n\n` + + `Failure details:\n${failedPages.map(p => ` - ${p.url}: ${p.reason}`).join('\n')}\n\n` + + `Diagnostic suggestions:\n` + + ` 1. Confirm all target pages are in active state\n` + + ` 2. Check if pages allow device emulation (some internal pages may restrict it)\n` + + ` 3. Try closing other pages and keep only one page\n` + + ` 4. Restart browser and retry` ); } }, From 59111390e24e1d03855b0ee82a5ec8654be28825 Mon Sep 17 00:00:00 2001 From: wanghongyang <549492949@qq.com> Date: Wed, 22 Oct 2025 16:26:32 +0800 Subject: [PATCH 3/3] feat: increase ts --- src/tools/ToolDefinition.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index c4322c34..14197716 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -87,6 +87,8 @@ export type Context = Readonly<{ setCpuThrottlingRate(rate: number): void; setDeviceEmulation(device: string | null): void; getDeviceEmulation(): string | null; + getPages:() => Page[]; + createPagesSnapshot(): Promise saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg' | 'image/webp',