Skip to content

Commit 1ca9f66

Browse files
authored
feat(mcp): add headers capability (#37583)
1 parent d8b75e5 commit 1ca9f66

File tree

6 files changed

+181
-3
lines changed

6 files changed

+181
-3
lines changed

packages/playwright/src/mcp/browser/context.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export class Context {
5252
private _tabs: Tab[] = [];
5353
private _currentTab: Tab | undefined;
5454
private _clientInfo: ClientInfo;
55+
private _extraHTTPHeaders: Record<string, string> | undefined;
5556

5657
private static _allContexts: Set<Context> = new Set();
5758
private _closeBrowserContextPromise: Promise<void> | undefined;
@@ -220,6 +221,20 @@ export class Context {
220221
return browserContext;
221222
}
222223

224+
async setExtraHTTPHeaders(headers: Record<string, string>) {
225+
if (!Object.keys(headers).length)
226+
throw new Error('Please provide at least one header to set.');
227+
228+
for (const name of Object.keys(headers)) {
229+
if (!name.trim())
230+
throw new Error('Header names must be non-empty strings.');
231+
}
232+
233+
this._extraHTTPHeaders = { ...headers };
234+
const { browserContext } = await this._ensureBrowserContext();
235+
await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders);
236+
}
237+
223238
private _ensureBrowserContext() {
224239
if (!this._browserContextPromise) {
225240
this._browserContextPromise = this._setupBrowserContext();
@@ -240,6 +255,8 @@ export class Context {
240255
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
241256
const { browserContext } = result;
242257
await this._setupRequestInterception(browserContext);
258+
if (this._extraHTTPHeaders)
259+
await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders);
243260
if (this.sessionLog)
244261
await InputRecorder.create(this, browserContext);
245262
for (const page of browserContext.pages())

packages/playwright/src/mcp/browser/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import mouse from './tools/mouse';
2626
import navigate from './tools/navigate';
2727
import network from './tools/network';
2828
import pdf from './tools/pdf';
29+
import headers from './tools/headers';
2930
import snapshot from './tools/snapshot';
3031
import screenshot from './tools/screenshot';
3132
import tabs from './tools/tabs';
@@ -47,6 +48,7 @@ export const browserTools: Tool<any>[] = [
4748
...keyboard,
4849
...navigate,
4950
...network,
51+
...headers,
5052
...mouse,
5153
...pdf,
5254
...screenshot,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { z } from '../../sdk/bundle';
18+
import { defineTool } from './tool';
19+
20+
const setHeaders = defineTool({
21+
capability: 'headers',
22+
23+
schema: {
24+
name: 'browser_set_headers',
25+
title: 'Set extra HTTP headers',
26+
description: 'Persistently set custom HTTP headers on the active browser context.',
27+
inputSchema: z.object({
28+
headers: z.record(z.string(), z.string()).describe('Header names mapped to the values that should be sent with every request.'),
29+
}),
30+
type: 'action',
31+
},
32+
33+
handle: async (context, params, response) => {
34+
try {
35+
await context.setExtraHTTPHeaders(params.headers);
36+
} catch (error) {
37+
response.addError((error as Error).message);
38+
return;
39+
}
40+
41+
const count = Object.keys(params.headers).length;
42+
response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`);
43+
response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(params.headers, null, 2)});`);
44+
},
45+
});
46+
47+
export default [
48+
setHeaders,
49+
];

packages/playwright/src/mcp/config.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import type * as playwright from 'playwright-core';
1818

19-
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing';
19+
export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing' | 'headers';
2020

2121
export type Config = {
2222
/**
@@ -99,6 +99,7 @@ export type Config = {
9999
* - 'core': Core browser automation features.
100100
* - 'pdf': PDF generation and manipulation.
101101
* - 'vision': Coordinate-based interactions.
102+
* - 'headers': Manage persistent custom HTTP headers.
102103
*/
103104
capabilities?: ToolCapability[];
104105

@@ -171,4 +172,3 @@ export type Config = {
171172
*/
172173
imageResponses?: 'allow' | 'omit';
173174
};
174-

packages/playwright/src/mcp/program.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function decorateCommand(command: Command, version: string) {
3333
.option('--blocked-origins <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)
3434
.option('--block-service-workers', 'block service workers')
3535
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
36-
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
36+
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf, headers.', commaSeparatedList)
3737
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
3838
.option('--cdp-header <headers...>', 'CDP headers to send with the connect request, multiple can be specified.', headerParser)
3939
.option('--config <path>', 'path to the configuration file.')

tests/mcp/headers.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './fixtures';
18+
19+
test('headers tool requires capability', async ({ client, startClient }) => {
20+
const { tools } = await client.listTools();
21+
expect(tools.map(tool => tool.name)).not.toContain('browser_set_headers');
22+
23+
const { client: headersClient } = await startClient({ args: ['--caps=headers'] });
24+
const headersToolList = await headersClient.listTools();
25+
expect(headersToolList.tools.map(tool => tool.name)).toContain('browser_set_headers');
26+
});
27+
28+
test('browser_set_headers rejects empty input', async ({ startClient }) => {
29+
const { client } = await startClient({ args: ['--caps=headers'] });
30+
31+
const response = await client.callTool({
32+
name: 'browser_set_headers',
33+
arguments: { headers: {} },
34+
});
35+
36+
expect(response).toHaveResponse({
37+
isError: true,
38+
result: 'Please provide at least one header to set.',
39+
});
40+
});
41+
42+
test('browser_set_headers rejects header names without characters', async ({ startClient }) => {
43+
const { client } = await startClient({ args: ['--caps=headers'] });
44+
45+
const response = await client.callTool({
46+
name: 'browser_set_headers',
47+
arguments: { headers: { ' ': 'value' } },
48+
});
49+
50+
expect(response).toHaveResponse({
51+
isError: true,
52+
result: 'Header names must be non-empty strings.',
53+
});
54+
});
55+
56+
test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => {
57+
server.setContent('/first', '<title>First</title>', 'text/html');
58+
server.setContent('/second', '<title>Second</title>', 'text/html');
59+
60+
const { client } = await startClient({ args: ['--caps=headers'] });
61+
62+
expect(await client.callTool({
63+
name: 'browser_set_headers',
64+
arguments: {
65+
headers: { 'X-Tenant-ID': 'tenant-123' },
66+
},
67+
})).toHaveResponse({
68+
result: 'Configured 1 header for this session.',
69+
});
70+
71+
const firstRequestPromise = server.waitForRequest('/first');
72+
await client.callTool({
73+
name: 'browser_navigate',
74+
arguments: { url: `${server.PREFIX}/first` },
75+
});
76+
const firstRequest = await firstRequestPromise;
77+
expect(firstRequest.headers['x-tenant-id']).toBe('tenant-123');
78+
79+
const secondRequestPromise = server.waitForRequest('/second');
80+
await client.callTool({
81+
name: 'browser_navigate',
82+
arguments: { url: `${server.PREFIX}/second` },
83+
});
84+
const secondRequest = await secondRequestPromise;
85+
expect(secondRequest.headers['x-tenant-id']).toBe('tenant-123');
86+
});
87+
88+
test('browser_set_headers sends headers with requests', async ({ startClient, server }) => {
89+
server.setContent('/page', '<title>Page</title>', 'text/html');
90+
91+
const { client } = await startClient({ args: ['--caps=headers'] });
92+
93+
expect(await client.callTool({
94+
name: 'browser_set_headers',
95+
arguments: {
96+
headers: { 'X-Custom-Header': 'custom-value' },
97+
},
98+
})).toHaveResponse({
99+
result: 'Configured 1 header for this session.',
100+
});
101+
102+
const requestPromise = server.waitForRequest('/page');
103+
await client.callTool({
104+
name: 'browser_navigate',
105+
arguments: { url: `${server.PREFIX}/page` },
106+
});
107+
108+
const request = await requestPromise;
109+
expect(request.headers['x-custom-header']).toBe('custom-value');
110+
});

0 commit comments

Comments
 (0)