From 937c31de179f098bdc851c9c7e6de2aeb82a7698 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 2 Jul 2026 11:25:49 +0200 Subject: [PATCH 1/2] feat(mcp): surface non-2xx navigation status Add a `Page status` line to the browser_navigate page header, shown only when the main document response is non-2xx. Mirrors the existing `Page status: crashed` line and clears once a 2xx navigation lands. --- .../src/tools/backend/response.ts | 3 +++ .../playwright-core/src/tools/backend/tab.ts | 13 +++++++++-- tests/mcp/core.spec.ts | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/tools/backend/response.ts b/packages/playwright-core/src/tools/backend/response.ts index 54eb8c0cf6548..8466cffc1ed22 100644 --- a/packages/playwright-core/src/tools/backend/response.ts +++ b/packages/playwright-core/src/tools/backend/response.ts @@ -302,6 +302,9 @@ export function renderTabMarkdown(tab: TabHeader): string[] { lines.push(`- Page Title: ${tab.title}`); if (tab.crashed) lines.push(`- Page status: crashed`); + const status = tab.mainDocumentStatus; + if (status && (status.status < 200 || status.status >= 300)) + lines.push(`- Page status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`); if (tab.console.errors || tab.console.warnings) lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`); return lines; diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 65e85c1ae1004..f99bcc4600951 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -77,6 +77,7 @@ export type TabHeader = { url: string; current: boolean; crashed: boolean; + mainDocumentStatus?: { status: number, statusText: string }; console: { total: number, warnings: number, errors: number }; }; @@ -93,6 +94,7 @@ export class Tab extends EventEmitter { private _lastHeader: TabHeader = { title: 'about:blank', url: 'about:blank', current: false, crashed: false, console: { total: 0, warnings: 0, errors: 0 } }; private _downloads: Download[] = []; private _requests: playwright.Request[] = []; + private _mainDocumentStatus: { status: number, statusText: string } | undefined; private _onPageClose: (tab: Tab) => void; crashed = false; private _modalStates: ModalState[] = []; @@ -219,6 +221,7 @@ export class Tab extends EventEmitter { private _clearCollectedArtifacts() { this._downloads.length = 0; this._requests.length = 0; + this._mainDocumentStatus = undefined; this._recentEventEntries.length = 0; this._resetLogs(); } @@ -238,9 +241,12 @@ export class Tab extends EventEmitter { } private _handleResponse(response: playwright.Response) { - const timing = response.request().timing(); + const request = response.request(); + if (request.isNavigationRequest() && response.frame() === this.page.mainFrame()) + this._mainDocumentStatus = { status: response.status(), statusText: response.statusText() }; + const timing = request.timing(); const wallTime = timing.responseStart + timing.startTime; - this._addLogEntry({ type: 'request', wallTime, request: response.request() }); + this._addLogEntry({ type: 'request', wallTime, request }); } private _handleRequestFailed(request: playwright.Request) { @@ -284,6 +290,7 @@ export class Tab extends EventEmitter { url: this.page.url(), current: this.isCurrentTab(), crashed: this.crashed, + mainDocumentStatus: this._mainDocumentStatus, console: consoleCounts, }; @@ -583,6 +590,8 @@ function tabHeaderEquals(a: TabHeader, b: TabHeader): boolean { a.url === b.url && a.current === b.current && a.crashed === b.crashed && + a.mainDocumentStatus?.status === b.mainDocumentStatus?.status && + a.mainDocumentStatus?.statusText === b.mainDocumentStatus?.statusText && a.console.errors === b.console.errors && a.console.warnings === b.console.warnings && a.console.total === b.console.total; diff --git a/tests/mcp/core.spec.ts b/tests/mcp/core.spec.ts index 06423357b550d..504b62e791139 100644 --- a/tests/mcp/core.spec.ts +++ b/tests/mcp/core.spec.ts @@ -30,6 +30,28 @@ test('browser_navigate', async ({ client, server }) => { }); }); +test('browser_navigate surfaces non-2xx HTTP status', async ({ client, server }) => { + server.setRoute('/locked', (req, res) => { + res.writeHead(402, { 'Content-Type': 'text/html' }); + res.end('Payment RequiredPay up'); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/locked' }, + })).toHaveResponse({ + page: expect.stringContaining(`- Page status: 402 Payment Required`), + }); + + // A subsequent 2xx navigation must not carry a status line. + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).not.toHaveResponse({ + page: expect.stringContaining('Page status'), + }); +}); + test('browser_navigate blocks file:// URLs by default', async ({ client }) => { expect(await client.callTool({ name: 'browser_navigate', From c7309fc17598a09d017c54e3078721efae33d8d7 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 2 Jul 2026 15:10:07 +0200 Subject: [PATCH 2/2] fix(mcp): rename status label and skip redirect hops Rename the navigation status line to `HTTP status:` and only record the final landing response, so an intermediate redirect (e.g. 302) never shows. --- .../playwright-core/src/tools/backend/response.ts | 2 +- packages/playwright-core/src/tools/backend/tab.ts | 2 +- tests/mcp/core.spec.ts | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/tools/backend/response.ts b/packages/playwright-core/src/tools/backend/response.ts index 8466cffc1ed22..fcb2021e94cda 100644 --- a/packages/playwright-core/src/tools/backend/response.ts +++ b/packages/playwright-core/src/tools/backend/response.ts @@ -304,7 +304,7 @@ export function renderTabMarkdown(tab: TabHeader): string[] { lines.push(`- Page status: crashed`); const status = tab.mainDocumentStatus; if (status && (status.status < 200 || status.status >= 300)) - lines.push(`- Page status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`); + lines.push(`- HTTP status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`); if (tab.console.errors || tab.console.warnings) lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`); return lines; diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index f99bcc4600951..a2ac803ca4862 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -242,7 +242,7 @@ export class Tab extends EventEmitter { private _handleResponse(response: playwright.Response) { const request = response.request(); - if (request.isNavigationRequest() && response.frame() === this.page.mainFrame()) + if (request.isNavigationRequest() && response.frame() === this.page.mainFrame() && !request.redirectedTo()) this._mainDocumentStatus = { status: response.status(), statusText: response.statusText() }; const timing = request.timing(); const wallTime = timing.responseStart + timing.startTime; diff --git a/tests/mcp/core.spec.ts b/tests/mcp/core.spec.ts index 504b62e791139..61fd4627c28b2 100644 --- a/tests/mcp/core.spec.ts +++ b/tests/mcp/core.spec.ts @@ -40,15 +40,20 @@ test('browser_navigate surfaces non-2xx HTTP status', async ({ client, server }) name: 'browser_navigate', arguments: { url: server.PREFIX + '/locked' }, })).toHaveResponse({ - page: expect.stringContaining(`- Page status: 402 Payment Required`), + page: expect.stringContaining(`- HTTP status: 402 Payment Required`), }); - // A subsequent 2xx navigation must not carry a status line. + // A redirect to a 2xx page must not carry a status line: the intermediate + // 302 hop must not leak, and the final 2xx landing renders nothing. + server.setRoute('/redirect', (req, res) => { + res.writeHead(302, { location: server.HELLO_WORLD }); + res.end(); + }); expect(await client.callTool({ name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, + arguments: { url: server.PREFIX + '/redirect' }, })).not.toHaveResponse({ - page: expect.stringContaining('Page status'), + page: expect.stringContaining('HTTP status'), }); });