From c82befe8b69182f565daa09a95fa76eaec523e25 Mon Sep 17 00:00:00 2001 From: lupuletic Date: Thu, 25 Sep 2025 17:03:27 +0100 Subject: [PATCH 1/6] feat(mcp): add headers capability --- docs/src/test-agents-js.md | 32 +++++ .../playwright/src/mcp/browser/context.ts | 9 ++ packages/playwright/src/mcp/browser/tools.ts | 2 + .../src/mcp/browser/tools/headers.ts | 57 ++++++++ packages/playwright/src/mcp/config.d.ts | 4 +- packages/playwright/src/mcp/program.ts | 2 +- tests/mcp/headers.spec.ts | 126 ++++++++++++++++++ 7 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 packages/playwright/src/mcp/browser/tools/headers.ts create mode 100644 tests/mcp/headers.spec.ts diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 2130fa3ed6cee..f2f51bd419325 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -237,3 +237,35 @@ Generated Playwright tests, aligned one-to-one with specs wherever feasible. ### Seed tests `seed.spec.ts` Seed tests provide a ready-to-use `page` context to bootstrap execution. + +### Custom headers for multi-tenant testing + +Opt into the `headers` capability when you need to drive tenant-aware applications that are keyed off HTTP headers instead of host names or paths. + +```bash +npx @playwright/mcp@latest --caps=headers +``` + +You can also enable the capability in your MCP configuration: + +```json +{ + "capabilities": ["headers"] +} +``` + +After enabling it, use the `browser_set_headers` tool to persist headers for the active browser context: + +```json +{ + "name": "browser_set_headers", + "arguments": { + "headers": { + "X-Tenant-ID": "tenant-123", + "Authorization": "Bearer token123" + } + } +} +``` + +All subsequent navigations and network requests reuse these headers, which makes it straightforward to swap tenants during a single session. diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index 80eadfda1c280..e741522c6ec40 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -51,6 +51,7 @@ export class Context { private _tabs: Tab[] = []; private _currentTab: Tab | undefined; private _clientInfo: ClientInfo; + private _extraHTTPHeaders: Record | undefined; private static _allContexts: Set = new Set(); private _closeBrowserContextPromise: Promise | undefined; @@ -210,6 +211,12 @@ export class Context { return browserContext; } + async setExtraHTTPHeaders(headers: Record) { + this._extraHTTPHeaders = { ...headers }; + const { browserContext } = await this._ensureBrowserContext(); + await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders); + } + private _ensureBrowserContext() { if (!this._browserContextPromise) { this._browserContextPromise = this._setupBrowserContext(); @@ -227,6 +234,8 @@ export class Context { const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName); const { browserContext } = result; await this._setupRequestInterception(browserContext); + if (this._extraHTTPHeaders) + await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders); if (this.sessionLog) await InputRecorder.create(this, browserContext); for (const page of browserContext.pages()) diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index e5927c2888f81..80feed565acc3 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -26,6 +26,7 @@ import mouse from './tools/mouse'; import navigate from './tools/navigate'; import network from './tools/network'; import pdf from './tools/pdf'; +import headers from './tools/headers'; import snapshot from './tools/snapshot'; import screenshot from './tools/screenshot'; import tabs from './tools/tabs'; @@ -47,6 +48,7 @@ export const browserTools: Tool[] = [ ...keyboard, ...navigate, ...network, + ...headers, ...mouse, ...pdf, ...screenshot, diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts new file mode 100644 index 0000000000000..2de8bfabf763b --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/headers.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTool } from './tool'; + +const setHeaders = defineTool({ + capability: 'headers', + + schema: { + name: 'browser_set_headers', + title: 'Set extra HTTP headers', + description: 'Persistently set custom HTTP headers on the active browser context.', + inputSchema: z.object({ + headers: z.record(z.string(), z.string()).describe('Header names mapped to the values that should be sent with every request.'), + }), + type: 'destructive', + }, + + handle: async (context, params, response) => { + const entries = Object.entries(params.headers); + if (!entries.length) { + response.addError('Please provide at least one header to set.'); + return; + } + + const invalidHeader = entries.find(([name]) => !name.trim()); + if (invalidHeader) { + response.addError('Header names must be non-empty strings.'); + return; + } + + const normalizedHeaders = Object.fromEntries(entries.map(([name, value]) => [name.trim(), value])); + await context.setExtraHTTPHeaders(normalizedHeaders); + + const count = entries.length; + response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`); + response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(normalizedHeaders, null, 2)});`); + }, +}); + +export default [ + setHeaders, +]; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index dd373b9883f03..49409629823e0 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -16,7 +16,7 @@ import type * as playwright from 'playwright-core'; -export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing'; +export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing' | 'headers'; export type Config = { /** @@ -99,6 +99,7 @@ export type Config = { * - 'core': Core browser automation features. * - 'pdf': PDF generation and manipulation. * - 'vision': Coordinate-based interactions. + * - 'headers': Manage persistent custom HTTP headers. */ capabilities?: ToolCapability[]; @@ -166,4 +167,3 @@ export type Config = { */ imageResponses?: 'allow' | 'omit'; }; - diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index baad3ab5b4b6c..04d6715201a47 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -34,7 +34,7 @@ export function decorateCommand(command: Command, version: string) { .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) .option('--block-service-workers', 'block service workers') .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') - .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) + .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf, headers.', commaSeparatedList) .option('--cdp-endpoint ', 'CDP endpoint to connect to.') .option('--cdp-header ', 'CDP headers to send with the connect request, multiple can be specified.', headerParser) .option('--config ', 'path to the configuration file.') diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts new file mode 100644 index 0000000000000..5456b8499f0b5 --- /dev/null +++ b/tests/mcp/headers.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('headers tool requires capability', async ({ client, startClient }) => { + const { tools } = await client.listTools(); + expect(tools.map(tool => tool.name)).not.toContain('browser_set_headers'); + + const { client: headersClient } = await startClient({ args: ['--caps=headers'] }); + const headersToolList = await headersClient.listTools(); + expect(headersToolList.tools.map(tool => tool.name)).toContain('browser_set_headers'); +}); + +test('browser_set_headers rejects empty input', async ({ startClient }) => { + const { client } = await startClient({ args: ['--caps=headers'] }); + + const response = await client.callTool({ + name: 'browser_set_headers', + arguments: { headers: {} }, + }); + + expect(response).toHaveResponse({ + isError: true, + result: 'Please provide at least one header to set.', + }); +}); + +test('browser_set_headers validates header names', async ({ startClient }) => { + const { client } = await startClient({ args: ['--caps=headers'] }); + + const response = await client.callTool({ + name: 'browser_set_headers', + arguments: { headers: { ' ': 'value' } }, + }); + + expect(response).toHaveResponse({ + isError: true, + result: 'Header names must be non-empty strings.', + }); +}); + +test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => { + server.setContent('/first', 'First', 'text/html'); + server.setContent('/second', 'Second', 'text/html'); + + const { client } = await startClient({ args: ['--caps=headers'] }); + + expect(await client.callTool({ + name: 'browser_set_headers', + arguments: { + headers: { 'X-Tenant-ID': 'tenant-123' }, + }, + })).toHaveResponse({ + result: 'Configured 1 header for this session.', + }); + + const firstRequestPromise = server.waitForRequest('/first'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/first` }, + }); + const firstRequest = await firstRequestPromise; + expect(firstRequest.headers['x-tenant-id']).toBe('tenant-123'); + + const secondRequestPromise = server.waitForRequest('/second'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/second` }, + }); + const secondRequest = await secondRequestPromise; + expect(secondRequest.headers['x-tenant-id']).toBe('tenant-123'); +}); + +test('browser_set_headers applies to all requests from the context', async ({ startClient, server }) => { + server.setRoute('/page', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(``); + }); + server.setRoute('/api/data', (req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end('{}'); + }); + + const { client } = await startClient({ args: ['--caps=headers'] }); + + expect(await client.callTool({ + name: 'browser_set_headers', + arguments: { + headers: { + 'X-Tenant-ID': 'tenant-456', + Authorization: 'Bearer token456', + }, + }, + })).toHaveResponse({ + result: 'Configured 2 headers for this session.', + }); + + const pageRequestPromise = server.waitForRequest('/page'); + const apiRequestPromise = server.waitForRequest('/api/data'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/page` }, + }); + + const [pageRequest, apiRequest] = await Promise.all([pageRequestPromise, apiRequestPromise]); + + expect(pageRequest.headers['x-tenant-id']).toBe('tenant-456'); + expect(pageRequest.headers['authorization']).toBe('Bearer token456'); + expect(apiRequest.headers['x-tenant-id']).toBe('tenant-456'); + expect(apiRequest.headers['authorization']).toBe('Bearer token456'); +}); From f8c731715973cbc9890229622beddb3bbdd22bad Mon Sep 17 00:00:00 2001 From: lupuletic Date: Fri, 26 Sep 2025 09:29:20 +0100 Subject: [PATCH 2/6] chore: address review feedback on MCP headers --- docs/src/test-agents-js.md | 32 ------------------- .../src/mcp/browser/tools/headers.ts | 11 ++----- tests/mcp/headers.spec.ts | 5 ++- 3 files changed, 4 insertions(+), 44 deletions(-) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index f2f51bd419325..2130fa3ed6cee 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -237,35 +237,3 @@ Generated Playwright tests, aligned one-to-one with specs wherever feasible. ### Seed tests `seed.spec.ts` Seed tests provide a ready-to-use `page` context to bootstrap execution. - -### Custom headers for multi-tenant testing - -Opt into the `headers` capability when you need to drive tenant-aware applications that are keyed off HTTP headers instead of host names or paths. - -```bash -npx @playwright/mcp@latest --caps=headers -``` - -You can also enable the capability in your MCP configuration: - -```json -{ - "capabilities": ["headers"] -} -``` - -After enabling it, use the `browser_set_headers` tool to persist headers for the active browser context: - -```json -{ - "name": "browser_set_headers", - "arguments": { - "headers": { - "X-Tenant-ID": "tenant-123", - "Authorization": "Bearer token123" - } - } -} -``` - -All subsequent navigations and network requests reuse these headers, which makes it straightforward to swap tenants during a single session. diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts index 2de8bfabf763b..1ed5bf4681b4b 100644 --- a/packages/playwright/src/mcp/browser/tools/headers.ts +++ b/packages/playwright/src/mcp/browser/tools/headers.ts @@ -37,18 +37,11 @@ const setHeaders = defineTool({ return; } - const invalidHeader = entries.find(([name]) => !name.trim()); - if (invalidHeader) { - response.addError('Header names must be non-empty strings.'); - return; - } - - const normalizedHeaders = Object.fromEntries(entries.map(([name, value]) => [name.trim(), value])); - await context.setExtraHTTPHeaders(normalizedHeaders); + await context.setExtraHTTPHeaders(params.headers); const count = entries.length; response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`); - response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(normalizedHeaders, null, 2)});`); + response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(params.headers, null, 2)});`); }, }); diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts index 5456b8499f0b5..dbe9600638e26 100644 --- a/tests/mcp/headers.spec.ts +++ b/tests/mcp/headers.spec.ts @@ -39,7 +39,7 @@ test('browser_set_headers rejects empty input', async ({ startClient }) => { }); }); -test('browser_set_headers validates header names', async ({ startClient }) => { +test('browser_set_headers defers invalid headers to Playwright', async ({ startClient }) => { const { client } = await startClient({ args: ['--caps=headers'] }); const response = await client.callTool({ @@ -49,7 +49,6 @@ test('browser_set_headers validates header names', async ({ startClient }) => { expect(response).toHaveResponse({ isError: true, - result: 'Header names must be non-empty strings.', }); }); @@ -102,7 +101,7 @@ test('browser_set_headers applies to all requests from the context', async ({ st arguments: { headers: { 'X-Tenant-ID': 'tenant-456', - Authorization: 'Bearer token456', + 'Authorization': 'Bearer token456', }, }, })).toHaveResponse({ From 37ac01e24c6e01b98edfd825081dbe7d64c58d4b Mon Sep 17 00:00:00 2001 From: lupuletic Date: Fri, 26 Sep 2025 13:37:41 +0100 Subject: [PATCH 3/6] test: stabilize MCP headers suite across browsers --- tests/mcp/headers.spec.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts index dbe9600638e26..fa259a4fe0a18 100644 --- a/tests/mcp/headers.spec.ts +++ b/tests/mcp/headers.spec.ts @@ -39,7 +39,9 @@ test('browser_set_headers rejects empty input', async ({ startClient }) => { }); }); -test('browser_set_headers defers invalid headers to Playwright', async ({ startClient }) => { +test('browser_set_headers defers invalid headers to Playwright', async ({ startClient, server }) => { + server.setContent('/check-invalid-header', 'Check', 'text/html'); + const { client } = await startClient({ args: ['--caps=headers'] }); const response = await client.callTool({ @@ -47,9 +49,25 @@ test('browser_set_headers defers invalid headers to Playwright', async ({ startC arguments: { headers: { ' ': 'value' } }, }); - expect(response).toHaveResponse({ - isError: true, + if (response.isError) { + expect(response).toHaveResponse({ + isError: true, + }); + return; + } + + const text = response.content[0]?.text ?? ''; + expect(text).toContain('Configured 1 header for this session.'); + + const requestPromise = server.waitForRequest('/check-invalid-header'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/check-invalid-header` }, }); + const request = await requestPromise; + expect(request.headers['']).toBeUndefined(); + expect(request.headers[' ']).toBeUndefined(); + expect(request.headers[' ']).toBeUndefined(); }); test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => { From 6ac2f2ed6c86e043016a19655ea080ac81ac1003 Mon Sep 17 00:00:00 2001 From: lupuletic Date: Fri, 26 Sep 2025 14:26:22 +0100 Subject: [PATCH 4/6] fix: guard MCP headers against whitespace names --- .../src/mcp/browser/tools/headers.ts | 6 +++++ tests/mcp/headers.spec.ts | 25 +++---------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts index 1ed5bf4681b4b..a5464e07c16a5 100644 --- a/packages/playwright/src/mcp/browser/tools/headers.ts +++ b/packages/playwright/src/mcp/browser/tools/headers.ts @@ -37,6 +37,12 @@ const setHeaders = defineTool({ return; } + const invalidHeader = entries.find(([name]) => !name.trim()); + if (invalidHeader) { + response.addError('Header names must be non-empty strings.'); + return; + } + await context.setExtraHTTPHeaders(params.headers); const count = entries.length; diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts index fa259a4fe0a18..0c722eb1fdf97 100644 --- a/tests/mcp/headers.spec.ts +++ b/tests/mcp/headers.spec.ts @@ -39,9 +39,7 @@ test('browser_set_headers rejects empty input', async ({ startClient }) => { }); }); -test('browser_set_headers defers invalid headers to Playwright', async ({ startClient, server }) => { - server.setContent('/check-invalid-header', 'Check', 'text/html'); - +test('browser_set_headers rejects header names without characters', async ({ startClient }) => { const { client } = await startClient({ args: ['--caps=headers'] }); const response = await client.callTool({ @@ -49,25 +47,10 @@ test('browser_set_headers defers invalid headers to Playwright', async ({ startC arguments: { headers: { ' ': 'value' } }, }); - if (response.isError) { - expect(response).toHaveResponse({ - isError: true, - }); - return; - } - - const text = response.content[0]?.text ?? ''; - expect(text).toContain('Configured 1 header for this session.'); - - const requestPromise = server.waitForRequest('/check-invalid-header'); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: `${server.PREFIX}/check-invalid-header` }, + expect(response).toHaveResponse({ + isError: true, + result: 'Header names must be non-empty strings.', }); - const request = await requestPromise; - expect(request.headers['']).toBeUndefined(); - expect(request.headers[' ']).toBeUndefined(); - expect(request.headers[' ']).toBeUndefined(); }); test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => { From fca309412918f2c345c995d063202ad83d44b35d Mon Sep 17 00:00:00 2001 From: lupuletic Date: Wed, 8 Oct 2025 20:39:36 +0100 Subject: [PATCH 5/6] chore(mcp): refactor headers validation and optimize tests --- .../playwright/src/mcp/browser/context.ts | 8 +++++ .../src/mcp/browser/tools/headers.ts | 17 ++++------- tests/mcp/headers.spec.ts | 30 +++++-------------- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index e741522c6ec40..4d8cd75ea058f 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -212,6 +212,14 @@ export class Context { } async setExtraHTTPHeaders(headers: Record) { + if (!Object.keys(headers).length) + throw new Error('Please provide at least one header to set.'); + + for (const name of Object.keys(headers)) { + if (!name.trim()) + throw new Error('Header names must be non-empty strings.'); + } + this._extraHTTPHeaders = { ...headers }; const { browserContext } = await this._ensureBrowserContext(); await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders); diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts index a5464e07c16a5..51cee339b537f 100644 --- a/packages/playwright/src/mcp/browser/tools/headers.ts +++ b/packages/playwright/src/mcp/browser/tools/headers.ts @@ -31,21 +31,14 @@ const setHeaders = defineTool({ }, handle: async (context, params, response) => { - const entries = Object.entries(params.headers); - if (!entries.length) { - response.addError('Please provide at least one header to set.'); + try { + await context.setExtraHTTPHeaders(params.headers); + } catch (error) { + response.addError((error as Error).message); return; } - const invalidHeader = entries.find(([name]) => !name.trim()); - if (invalidHeader) { - response.addError('Header names must be non-empty strings.'); - return; - } - - await context.setExtraHTTPHeaders(params.headers); - - const count = entries.length; + const count = Object.keys(params.headers).length; response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`); response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(params.headers, null, 2)});`); }, diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts index 0c722eb1fdf97..839e8d2b5a01f 100644 --- a/tests/mcp/headers.spec.ts +++ b/tests/mcp/headers.spec.ts @@ -85,42 +85,26 @@ test('browser_set_headers persists headers across navigations', async ({ startCl expect(secondRequest.headers['x-tenant-id']).toBe('tenant-123'); }); -test('browser_set_headers applies to all requests from the context', async ({ startClient, server }) => { - server.setRoute('/page', (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(``); - }); - server.setRoute('/api/data', (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end('{}'); - }); +test('browser_set_headers sends headers with requests', async ({ startClient, server }) => { + server.setContent('/page', 'Page', 'text/html'); const { client } = await startClient({ args: ['--caps=headers'] }); expect(await client.callTool({ name: 'browser_set_headers', arguments: { - headers: { - 'X-Tenant-ID': 'tenant-456', - 'Authorization': 'Bearer token456', - }, + headers: { 'X-Custom-Header': 'custom-value' }, }, })).toHaveResponse({ - result: 'Configured 2 headers for this session.', + result: 'Configured 1 header for this session.', }); - const pageRequestPromise = server.waitForRequest('/page'); - const apiRequestPromise = server.waitForRequest('/api/data'); - + const requestPromise = server.waitForRequest('/page'); await client.callTool({ name: 'browser_navigate', arguments: { url: `${server.PREFIX}/page` }, }); - const [pageRequest, apiRequest] = await Promise.all([pageRequestPromise, apiRequestPromise]); - - expect(pageRequest.headers['x-tenant-id']).toBe('tenant-456'); - expect(pageRequest.headers['authorization']).toBe('Bearer token456'); - expect(apiRequest.headers['x-tenant-id']).toBe('tenant-456'); - expect(apiRequest.headers['authorization']).toBe('Bearer token456'); + const request = await requestPromise; + expect(request.headers['x-custom-header']).toBe('custom-value'); }); From 050e111b7eed9c25184ac8405325ea29585ffb73 Mon Sep 17 00:00:00 2001 From: lupuletic Date: Fri, 17 Oct 2025 13:46:39 +0100 Subject: [PATCH 6/6] chore: update with main and fix lint errors --- packages/playwright/src/mcp/browser/tools/headers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts index 51cee339b537f..578eab1f4035a 100644 --- a/packages/playwright/src/mcp/browser/tools/headers.ts +++ b/packages/playwright/src/mcp/browser/tools/headers.ts @@ -27,7 +27,7 @@ const setHeaders = defineTool({ inputSchema: z.object({ headers: z.record(z.string(), z.string()).describe('Header names mapped to the values that should be sent with every request.'), }), - type: 'destructive', + type: 'action', }, handle: async (context, params, response) => {