mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat: add extraHttpHeaders emulation to emulate tool (#1176)
## Summary Extend the existing `emulate` tool with an `extraHTTPHeaders` parameter that calls Puppeteer's `page.setExtraHTTPHeaders()` (which uses CDP `Network.setExtraHTTPHeaders` under the hood). Closes #1175 ## Approach Per [feedback from @natorion](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1175#issuecomment-4097587153), this integrates into the existing `emulate` tool rather than adding a standalone tool. The `emulate` tool is already the central hub for page-level state modifications (userAgent, viewport, networkConditions, geolocation, colorScheme), and custom HTTP headers fit naturally alongside them. This also avoids increasing the MCP tool count and LLM token overhead. ## Changes - **`src/types.ts`** — Added `extraHTTPHeaders?: Record<string, string>` to `EmulationSettings` - **`src/tools/emulation.ts`** — Added `extraHTTPHeaders` as an optional zod parameter on the `emulate` tool - **`src/McpContext.ts`** — Added handler logic in the `emulate()` method: - Calls `page.setExtraHTTPHeaders()` when `extraHTTPHeaders` is provided - Clears from settings when an empty `{}` is passed - Preserves existing headers when the param is **omitted** (unlike other emulation settings that reset when omitted) — prevents `emulate({colorScheme: "dark"})` from accidentally clearing previously-set headers - **`tests/tools/emulation.test.ts`** — Added 5 test cases: 1. Sets extra headers on requests 2. Clears headers with `{}` 3. Headers persist across navigations 4. Does not affect other emulation settings 5. Reports correctly per-page (new page has no headers) ## Use Case This enables setting custom HTTP headers on **all** requests — including the initial document navigation and `<script>` tag loads — which `initScript` cannot do since it runs after the document is already fetched. ## Usage ```js // Set headers emulate({ extraHTTPHeaders: { "X-Custom": "value", "Authorization": "Bearer token" } }) // Clear headers emulate({ extraHTTPHeaders: {} }) // Combine with other emulation settings emulate({ extraHTTPHeaders: { "X-Branch": "feature-1" }, userAgent: "MyBot/1.0" }) ``` --------- Co-authored-by: Alex Rudenko <alexrudenko@chromium.org> Co-authored-by: Nicholas Roscino <nroscino@google.com>
This commit is contained in:
@@ -256,6 +256,7 @@
|
||||
|
||||
- **colorScheme** (enum: "dark", "light", "auto") _(optional)_: [`Emulate`](#emulate) the dark or the light mode. Set to "auto" to reset to the default.
|
||||
- **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling
|
||||
- **extraHttpHeaders** (string) _(optional)_: Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.
|
||||
- **geolocation** (string) _(optional)_: Geolocation (`<latitude>,<longitude>`) to [`emulate`](#emulate). Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.
|
||||
- **networkConditions** (enum: "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") _(optional)_: Throttle network. Omit to disable throttling.
|
||||
- **userAgent** (string) _(optional)_: User agent to [`emulate`](#emulate). Set to empty string to clear the user agent override.
|
||||
|
||||
@@ -301,6 +301,7 @@ export class McpContext implements Context {
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light' | 'auto';
|
||||
viewport?: Viewport;
|
||||
extraHttpHeaders?: Record<string, string> | undefined;
|
||||
},
|
||||
targetPage?: Page,
|
||||
): Promise<void> {
|
||||
@@ -379,6 +380,14 @@ export class McpContext implements Context {
|
||||
newSettings.viewport = viewport;
|
||||
}
|
||||
|
||||
if (options.extraHttpHeaders !== undefined) {
|
||||
await page.setExtraHTTPHeaders(options.extraHttpHeaders);
|
||||
newSettings.extraHttpHeaders = options.extraHttpHeaders;
|
||||
if (Object.keys(options.extraHttpHeaders).length === 0) {
|
||||
delete newSettings.extraHttpHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
mcpPage.emulationSettings = Object.keys(newSettings).length
|
||||
? newSettings
|
||||
: {};
|
||||
|
||||
@@ -167,6 +167,13 @@ export const commands: Commands = {
|
||||
"Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.",
|
||||
required: false,
|
||||
},
|
||||
extraHttpHeaders: {
|
||||
name: 'extraHttpHeaders',
|
||||
type: 'string',
|
||||
description:
|
||||
'Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
evaluate_script: {
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
{
|
||||
"name": "viewport_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "extra_http_headers_length",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -14,6 +14,32 @@ import {
|
||||
viewportTransform,
|
||||
} from './ToolDefinition.js';
|
||||
|
||||
function headerStringTransform(
|
||||
value: string | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === '') {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (
|
||||
typeof parsed !== 'object' ||
|
||||
parsed === null ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
throw new Error('Headers must be a JSON object');
|
||||
}
|
||||
return parsed as Record<string, string>;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Invalid JSON for headers: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const throttlingOptions: [string, ...string[]] = [
|
||||
'Offline',
|
||||
...Object.keys(PredefinedNetworkConditions),
|
||||
@@ -65,6 +91,13 @@ export const emulate = definePageTool({
|
||||
.describe(
|
||||
`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`,
|
||||
),
|
||||
extraHttpHeaders: zod
|
||||
.string()
|
||||
.optional()
|
||||
.transform(headerStringTransform)
|
||||
.describe(
|
||||
'Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.',
|
||||
),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
handler: async (request, response, context) => {
|
||||
|
||||
@@ -31,4 +31,5 @@ export interface EmulationSettings {
|
||||
userAgent?: string;
|
||||
colorScheme?: 'dark' | 'light';
|
||||
viewport?: Viewport;
|
||||
extraHttpHeaders?: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import type {IncomingHttpHeaders} from 'node:http';
|
||||
import {beforeEach, describe, it} from 'node:test';
|
||||
|
||||
import {emulate} from '../../src/tools/emulation.js';
|
||||
@@ -571,6 +572,172 @@ describe('emulation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extraHttpHeaders', () => {
|
||||
it('sets extra headers on requests', async () => {
|
||||
let receivedHeaders: IncomingHttpHeaders = {};
|
||||
server.addRoute('/headers-test', async (req, res) => {
|
||||
receivedHeaders = req.headers;
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('<main>Headers Test</main>');
|
||||
});
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
const page = context.getSelectedPptrPage();
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {'X-Custom-Header': 'test-value'},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
await page.goto(server.getRoute('/headers-test'));
|
||||
assert.strictEqual(receivedHeaders['x-custom-header'], 'test-value');
|
||||
});
|
||||
});
|
||||
|
||||
it('clears extra headers when null is passed', async () => {
|
||||
let receivedHeaders: IncomingHttpHeaders = {};
|
||||
server.addRoute('/headers-clear', async (req, res) => {
|
||||
receivedHeaders = req.headers;
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('<main>Headers Clear</main>');
|
||||
});
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
const page = context.getSelectedPptrPage();
|
||||
// Set headers first
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {'X-To-Clear': 'value'},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
// Clear headers
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
await page.goto(server.getRoute('/headers-clear'));
|
||||
assert.strictEqual(receivedHeaders['x-to-clear'], undefined);
|
||||
assert.strictEqual(
|
||||
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('headers persist across navigations', async () => {
|
||||
const receivedHeaders: IncomingHttpHeaders[] = [];
|
||||
server.addRoute('/persist-one', async (req, res) => {
|
||||
receivedHeaders.push({...req.headers});
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('<main>Page One</main>');
|
||||
});
|
||||
server.addRoute('/persist-two', async (req, res) => {
|
||||
receivedHeaders.push({...req.headers});
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('<main>Page Two</main>');
|
||||
});
|
||||
|
||||
await withMcpContext(async (response, context) => {
|
||||
const page = context.getSelectedPptrPage();
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {'X-Persist': 'yes'},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
await page.goto(server.getRoute('/persist-one'));
|
||||
await page.goto(server.getRoute('/persist-two'));
|
||||
|
||||
assert.strictEqual(receivedHeaders[0]?.['x-persist'], 'yes');
|
||||
assert.strictEqual(receivedHeaders[1]?.['x-persist'], 'yes');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect other emulation settings', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
// Set userAgent first
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
userAgent: 'MyUA',
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
// Set extraHTTPHeaders separately
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {'X-Test': 'value'},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const settings = context.getSelectedMcpPage().emulationSettings;
|
||||
assert.deepStrictEqual(settings.extraHttpHeaders, {
|
||||
'X-Test': 'value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reports correctly for the currently selected page', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
await emulate.handler(
|
||||
{
|
||||
params: {
|
||||
extraHttpHeaders: {'X-Page': 'one'},
|
||||
},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
|
||||
{'X-Page': 'one'},
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
context.selectPage(page);
|
||||
|
||||
assert.strictEqual(
|
||||
context.getSelectedMcpPage().emulationSettings.extraHttpHeaders,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('colorScheme', () => {
|
||||
it('emulates color scheme', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
|
||||
Reference in New Issue
Block a user