diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 366c21ea4..9b88c8f1b 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -1,6 +1,6 @@ -# Chrome DevTools MCP Tool Reference (~6962 cl100k_base tokens) +# Chrome DevTools MCP Tool Reference (~7005 cl100k_base tokens) - **[Input automation](#input-automation)** (9 tools) - [`click`](#click) @@ -333,6 +333,7 @@ so returned values have to be JSON-serializable. }` - **args** (array) _(optional)_: An optional list of arguments to pass to the function. +- **dialogAction** (string) _(optional)_: Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept. --- diff --git a/src/McpPage.ts b/src/McpPage.ts index 36925f2ea..01f8473c2 100644 --- a/src/McpPage.ts +++ b/src/McpPage.ts @@ -117,7 +117,7 @@ export class McpPage implements ContextPage { waitForEventsAfterAction( action: () => Promise, - options?: {timeout?: number}, + options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string}, ): Promise { const helper = this.createWaitForHelper( this.cpuThrottlingRate, diff --git a/src/WaitForHelper.ts b/src/WaitForHelper.ts index 2dd0f48c1..f41ca84cc 100644 --- a/src/WaitForHelper.ts +++ b/src/WaitForHelper.ts @@ -5,7 +5,7 @@ */ import {logger} from './logger.js'; -import type {Page, Protocol, CdpPage} from './third_party/index.js'; +import type {Page, Protocol, CdpPage, Dialog} from './third_party/index.js'; import type {PredefinedNetworkConditions} from './third_party/index.js'; export class WaitForHelper { @@ -126,8 +126,24 @@ export class WaitForHelper { async waitForEventsAfterAction( action: () => Promise, - options?: {timeout?: number}, + options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string}, ): Promise { + if (options?.handleDialog) { + const dialogHandler = (dialog: Pick) => { + if (options.handleDialog === 'dismiss') { + void dialog.dismiss(); + } else if (options.handleDialog === 'accept') { + void dialog.accept(); + } else { + void dialog.accept(options.handleDialog); + } + }; + this.#page.on('dialog', dialogHandler); + this.#abortController.signal.addEventListener('abort', () => { + this.#page.off('dialog', dialogHandler); + }); + } + const navigationFinished = this.waitForNavigationStarted() .then(navigationStated => { if (navigationStated) { diff --git a/src/bin/cliDefinitions.ts b/src/bin/cliDefinitions.ts index aef789af3..abf934034 100644 --- a/src/bin/cliDefinitions.ts +++ b/src/bin/cliDefinitions.ts @@ -155,6 +155,13 @@ export const commands: Commands = { description: 'An optional list of arguments to pass to the function.', required: false, }, + dialogAction: { + name: 'dialogAction', + type: 'string', + description: + 'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.', + required: false, + }, }, }, fill: { diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json index a09d781dd..c09d46990 100644 --- a/src/telemetry/tool_call_metrics.json +++ b/src/telemetry/tool_call_metrics.json @@ -107,6 +107,10 @@ { "name": "args_count", "argType": "number" + }, + { + "name": "dialog_action_length", + "argType": "number" } ] }, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 2aa640f7f..b9b4a6af0 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -247,7 +247,7 @@ export type ContextPage = Readonly<{ clearDialog(): void; waitForEventsAfterAction( action: () => Promise, - options?: {timeout?: number}, + options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string}, ): Promise; getInPageTools(): ToolGroup | undefined; }>; diff --git a/src/tools/script.ts b/src/tools/script.ts index 725628bcd..337ad2e86 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -46,6 +46,12 @@ Example with arguments: \`(el) => { ) .optional() .describe(`An optional list of arguments to pass to the function.`), + dialogAction: zod + .string() + .optional() + .describe( + 'Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.', + ), ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}), ...(cliArgs?.categoryExtensions ? { @@ -64,6 +70,7 @@ Example with arguments: \`(el) => { args: uidArgs, function: fnString, pageId, + dialogAction, } = request.params; if (cliArgs?.categoryExtensions && serviceWorkerId) { @@ -77,11 +84,12 @@ Example with arguments: \`(el) => { } const worker = await getWebWorker(context, serviceWorkerId); - await context - .getSelectedMcpPage() - .waitForEventsAfterAction(async () => { + await context.getSelectedMcpPage().waitForEventsAfterAction( + async () => { await performEvaluation(worker, fnString, [], response); - }); + }, + {handleDialog: dialogAction ?? 'accept'}, + ); return; } @@ -101,9 +109,12 @@ Example with arguments: \`(el) => { const evaluatable = await getPageOrFrame(page, frames); - await mcpPage.waitForEventsAfterAction(async () => { - await performEvaluation(evaluatable, fnString, args, response); - }); + await mcpPage.waitForEventsAfterAction( + async () => { + await performEvaluation(evaluatable, fnString, args, response); + }, + {handleDialog: dialogAction ?? 'accept'}, + ); } finally { void Promise.allSettled(args.map(arg => arg.dispose())); } diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index 772ebc076..5057f8a09 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -98,6 +98,75 @@ describe('script', () => { }); }); + it('work for scripts that trigger dialogs', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPptrPage(); + + await page.setContent(html``); + + await evaluateScript().handler( + { + params: { + function: String(() => { + alert('hello'); + return 'Works'; + }), + }, + }, + response, + context, + ); + const lineEvaluation = response.responseLines.at(2)!; + assert.strictEqual(JSON.parse(lineEvaluation), 'Works'); + }); + }); + + it('work for scripts that trigger dialogs and dismiss them', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPptrPage(); + + await page.setContent(html``); + + await evaluateScript().handler( + { + params: { + function: String(() => { + return confirm('hello'); + }), + dialogAction: 'dismiss', + }, + }, + response, + context, + ); + const lineEvaluation = response.responseLines.at(2)!; + assert.strictEqual(JSON.parse(lineEvaluation), false); + }); + }); + + it('work for scripts that trigger prompts and fill them', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPptrPage(); + + await page.setContent(html``); + + await evaluateScript().handler( + { + params: { + function: String(() => { + return prompt('Enter your name:'); + }), + dialogAction: 'John Doe', + }, + }, + response, + context, + ); + const lineEvaluation = response.responseLines.at(2)!; + assert.strictEqual(JSON.parse(lineEvaluation), 'John Doe'); + }); + }); + it('work for async functions', async () => { await withMcpContext(async (response, context) => { const page = context.getSelectedPptrPage();