From e3c7ad724fea049162681a89d18db686348ba949 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 26 Sep 2025 18:00:29 -0700 Subject: [PATCH] feat(mcp): console error and faster wait --- packages/playwright/src/mcp/browser/tab.ts | 8 ++-- .../src/mcp/browser/tools/console.ts | 6 ++- .../playwright/src/mcp/browser/tools/utils.ts | 12 ++++-- tests/mcp/console.spec.ts | 39 ++++++++++++++++++- tests/mcp/fixtures.ts | 2 +- 5 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index c855931bfb18e..e655dffc89ea1 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -209,9 +209,9 @@ export class Tab extends EventEmitter { await this.waitForLoadState('load', { timeout: 5000 }); } - async consoleMessages(): Promise { + async consoleMessages(type?: 'error'): Promise { await this._initializedPromise; - return this._consoleMessages; + return this._consoleMessages.filter(message => type ? message.type === type : true); } async requests(): Promise> { @@ -314,13 +314,13 @@ function messageToConsoleMessage(message: playwright.ConsoleMessage): ConsoleMes function pageErrorToConsoleMessage(errorOrValue: Error | any): ConsoleMessage { if (errorOrValue instanceof Error) { return { - type: undefined, + type: 'error', text: errorOrValue.message, toString: () => errorOrValue.stack || errorOrValue.message, }; } return { - type: undefined, + type: 'error', text: String(errorOrValue), toString: () => String(errorOrValue), }; diff --git a/packages/playwright/src/mcp/browser/tools/console.ts b/packages/playwright/src/mcp/browser/tools/console.ts index 381cd419272f3..f721f88c3eba4 100644 --- a/packages/playwright/src/mcp/browser/tools/console.ts +++ b/packages/playwright/src/mcp/browser/tools/console.ts @@ -23,11 +23,13 @@ const console = defineTabTool({ name: 'browser_console_messages', title: 'Get console messages', description: 'Returns all console messages', - inputSchema: z.object({}), + inputSchema: z.object({ + onlyErrors: z.boolean().optional().describe('Only return error messages'), + }), type: 'readOnly', }, handle: async (tab, params, response) => { - const messages = await tab.consoleMessages(); + const messages = await tab.consoleMessages(params.onlyErrors ? 'error' : undefined); messages.map(message => response.addResult(message.toString())); }, }); diff --git a/packages/playwright/src/mcp/browser/tools/utils.ts b/packages/playwright/src/mcp/browser/tools/utils.ts index 6adae9428e8e4..e68aa6990b67b 100644 --- a/packages/playwright/src/mcp/browser/tools/utils.ts +++ b/packages/playwright/src/mcp/browser/tools/utils.ts @@ -25,13 +25,17 @@ export async function waitForCompletion(tab: Tab, callback: () => Promise) let waitCallback: () => void = () => {}; const waitBarrier = new Promise(f => { waitCallback = f; }); - const requestListener = (request: playwright.Request) => requests.add(request); - const requestFinishedListener = (request: playwright.Request) => { + const responseListener = (request: playwright.Request) => { requests.delete(request); if (!requests.size) waitCallback(); }; + const requestListener = (request: playwright.Request) => { + requests.add(request); + void request.response().then(() => responseListener(request)).catch(() => {}); + }; + const frameNavigateListener = (frame: playwright.Frame) => { if (frame.parentFrame()) return; @@ -47,13 +51,13 @@ export async function waitForCompletion(tab: Tab, callback: () => Promise) }; tab.page.on('request', requestListener); - tab.page.on('requestfinished', requestFinishedListener); + tab.page.on('requestfailed', responseListener); tab.page.on('framenavigated', frameNavigateListener); const timeout = setTimeout(onTimeout, 10000); const dispose = () => { tab.page.off('request', requestListener); - tab.page.off('requestfinished', requestFinishedListener); + tab.page.off('requestfailed', responseListener); tab.page.off('framenavigated', frameNavigateListener); clearTimeout(timeout); }; diff --git a/tests/mcp/console.spec.ts b/tests/mcp/console.spec.ts index 872e2730382dd..30d9cd84ec399 100644 --- a/tests/mcp/console.spec.ts +++ b/tests/mcp/console.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect } from './fixtures'; +import { test, expect, parseResponse } from './fixtures'; test('browser_console_messages', async ({ client, server }) => { server.setContent('/', ` @@ -98,3 +98,40 @@ test('recent console messages', async ({ client, server }) => { consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`), }); }); + +test('browser_console_messages errors only', async ({ client, server }) => { + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.HELLO_WORLD, + }, + }); + + console.log(performance.now()); + await client.callTool({ + name: 'browser_evaluate', + arguments: { + function: `async () => { + console.log("console.log"); + console.warn("console.warn"); + console.error("console.error"); + setTimeout(() => { throw new Error("unhandled"); }, 0); + await fetch('/missing'); + }`, + }, + }); + console.log(performance.now()); + + const response = parseResponse(await client.callTool({ + name: 'browser_console_messages', + arguments: { + onlyErrors: true, + }, + })); + console.log(performance.now()); + expect.soft(response.result).toContain('console.error'); + expect.soft(response.result).toContain('Error: unhandled'); + expect.soft(response.result).toContain('404'); + expect.soft(response.result).not.toContain('console.log'); + expect.soft(response.result).not.toContain('console.warn'); +}); diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 99aeac6203541..aae9a3f588543 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -254,7 +254,7 @@ export function formatOutput(output: string): string[] { return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); } -function parseResponse(response: any) { +export function parseResponse(response: any) { const text = response.content[0].text; const sections = parseSections(text);