From c99b275fed3cd8814ad720ea855d853fe731ea8f Mon Sep 17 00:00:00 2001 From: Samiya Caur Date: Fri, 24 Apr 2026 13:35:46 +0200 Subject: [PATCH] fix: Handle errors during tool calls and check if a dialog is open This change also adds a check for existence of open dialogs before trying to detect open devtools ran prettier add try catch block in index.ts to handle dialog add test add test ran prettier ran prettier update test to use snapshot assertion Update handling of errors from tool execution mark isError as true --- src/McpResponse.ts | 17 +++++++++++ src/WaitForHelper.ts | 6 ++++ src/index.ts | 57 ++++++++++++++++++++---------------- tests/index.test.js.snapshot | 4 +++ tests/index.test.ts | 31 ++++++++++++++++++++ 5 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index ae6a35721..246536b77 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -203,6 +203,7 @@ export class McpResponse implements Response { #args: ParsedArguments; #page?: McpPage; #redactNetworkHeaders = true; + #error?: Error; constructor(args: ParsedArguments) { this.#args = args; @@ -307,6 +308,10 @@ export class McpResponse implements Response { }; } + setError(error: Error): void { + this.#error = error; + } + attachNetworkRequest( reqId: number, options?: {requestFilePath?: string; responseFilePath?: string}, @@ -375,6 +380,10 @@ export class McpResponse implements Response { return this.#consoleDataOptions?.types; } + get error(): Error | undefined { + return this.#error; + } + appendResponseLine(value: string): void { this.#textResponseLines.push(value); } @@ -662,6 +671,7 @@ export class McpResponse implements Response { lighthouseResult: this.#attachedLighthouseResult, inPageTools, webmcpTools, + errorMessage: this.#error?.message, }); } @@ -680,6 +690,7 @@ export class McpResponse implements Response { lighthouseResult?: LighthouseData; inPageTools?: ToolGroup; webmcpTools?: WebMCPTool[]; + errorMessage?: string; }, ): {content: Array; structuredContent: object} { const structuredContent: { @@ -718,6 +729,7 @@ export class McpResponse implements Response { heapSnapshotNodes?: readonly object[]; extensionServiceWorkers?: object[]; extensionPages?: object[]; + errorMessage?: string; } = {}; const response = []; @@ -1080,6 +1092,11 @@ Call ${handleDialog.name} to handle it before continuing.`); } } + if (data.errorMessage) { + response.push(`Error: ${data.errorMessage}`); + structuredContent.errorMessage = data.errorMessage; + } + const text: TextContent = { type: 'text', text: response.join('\n'), diff --git a/src/WaitForHelper.ts b/src/WaitForHelper.ts index f41ca84cc..6a67224c3 100644 --- a/src/WaitForHelper.ts +++ b/src/WaitForHelper.ts @@ -128,8 +128,10 @@ export class WaitForHelper { action: () => Promise, options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string}, ): Promise { + let dialogOpened = false; if (options?.handleDialog) { const dialogHandler = (dialog: Pick) => { + dialogOpened = true; if (options.handleDialog === 'dismiss') { void dialog.dismiss(); } else if (options.handleDialog === 'accept') { @@ -167,6 +169,10 @@ export class WaitForHelper { try { await navigationFinished; + if (dialogOpened) { + return; + } + // Wait for stable dom after navigation so we execute in // the correct context await this.waitForStableDom(); diff --git a/src/index.ts b/src/index.ts index 9f522f63b..125d6ffde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,31 +232,35 @@ export async function createMcpServer( : new McpResponse(serverArgs); response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders); - if ('pageScoped' in tool && tool.pageScoped) { - const page = - serverArgs.experimentalPageIdRouting && - params.pageId && - !serverArgs.slim - ? context.getPageById(params.pageId) - : context.getSelectedMcpPage(); - response.setPage(page); - await tool.handler( - { - params, - page, - }, - response, - context, - ); - } else { - await tool.handler( - // @ts-expect-error types do not match. - { - params, - }, - response, - context, - ); + try { + if ('pageScoped' in tool && tool.pageScoped) { + const page = + serverArgs.experimentalPageIdRouting && + params.pageId && + !serverArgs.slim + ? context.getPageById(params.pageId) + : context.getSelectedMcpPage(); + response.setPage(page); + await tool.handler( + { + params, + page, + }, + response, + context, + ); + } else { + await tool.handler( + // @ts-expect-error types do not match. + { + params, + }, + response, + context, + ); + } + } catch (err) { + response.setError(err); } const {content, structuredContent} = await response.handle( tool.name, @@ -267,6 +271,9 @@ export async function createMcpServer( } = { content, }; + if (response.error) { + result.isError = true; + } success = true; if (serverArgs.experimentalStructuredContent) { result.structuredContent = structuredContent as Record< diff --git a/tests/index.test.js.snapshot b/tests/index.test.js.snapshot index 06a918df4..7d6410a02 100644 --- a/tests/index.test.js.snapshot +++ b/tests/index.test.js.snapshot @@ -5,3 +5,7 @@ exports[`e2e > calls a tool 1`] = ` exports[`e2e > calls a tool multiple times 1`] = ` [{"type":"text","text":"## Pages\\n1: about:blank [selected]"}] `; + +exports[`e2e > returns blocked message when dialog is opened during tool execution 1`] = ` +{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\nError: Failed to interact with the element with uid 1_1. The element did not become interactive within the configured timeout."}],"isError":true} +`; diff --git a/tests/index.test.ts b/tests/index.test.ts index fae2d2e85..0694b0aa7 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -245,6 +245,37 @@ describe('e2e', () => { }, ); }); + + it('returns blocked message when dialog is opened during tool execution', async t => { + await withClient(async client => { + // Navigate to a page with a button that triggers a dialog on click + await client.callTool({ + name: 'new_page', + arguments: { + url: `data:text/html,`, + }, + }); + + const snapshotResult = await client.callTool({ + name: 'take_snapshot', + arguments: {}, + }); + + const snapshotText = (snapshotResult.content as TextContent[])[0].text; + const match = snapshotText.match(/uid=(\d+_\d+)\s+button "Click me"/); + const uid = match ? match[1] : '1_1'; + + // Trigger the dialog + const result = await client.callTool({ + name: 'click', + arguments: { + uid, + }, + }); + + t.assert.snapshot?.(JSON.stringify(result)); + }); + }); }); async function getToolsWithFilteredCategories(