From 26b5e37ed4af375e2e263eb07a2f104650f9a713 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 14 Apr 2026 10:58:13 +0200 Subject: [PATCH] feat: apply DevTools header redactions by default This PR adds a CLI flag to enable redacting network headers in the same way they are redacted in DevTools. Note that sometimes it might prevent the agent from properly analysing network issues. Pass `--redact-headers=false` to revert to the previous behavior. --- README.md | 5 +++ src/McpResponse.ts | 7 +++ src/bin/chrome-devtools-mcp-cli-options.ts | 6 +++ src/formatters/NetworkFormatter.ts | 33 ++++++++++++-- src/index.ts | 2 + tests/McpContext.test.js.snapshot | 2 +- tests/McpResponse.test.js.snapshot | 8 ++-- tests/cli.test.ts | 2 + tests/formatters/NetworkFormatter.test.ts | 51 ++++++++++++++++++++++ tests/tools/network.test.js.snapshot | 18 ++++---- tests/utils.ts | 14 +++--- 11 files changed, 126 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 41743ac90..913a58687 100644 --- a/README.md +++ b/README.md @@ -620,6 +620,11 @@ The Chrome DevTools MCP server supports the following configuration option: Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks. - **Type:** boolean +- **`--redactNetworkHeaders`/ `--redact-network-headers`** + If true, redacts some of the network headers considered senstive before returning to the client. + - **Type:** boolean + - **Default:** `false` + Pass them via the `args` property in the JSON configuration. For example: diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 77898b5df..719c04626 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -185,6 +185,7 @@ export class McpResponse implements Response { #tabId?: string; #args: ParsedArguments; #page?: McpPage; + #redactNetworkHeaders = true; constructor(args: ParsedArguments) { this.#args = args; @@ -194,6 +195,10 @@ export class McpResponse implements Response { this.#page = page; } + setRedactNetworkHeaders(value: boolean): void { + this.#redactNetworkHeaders = value; + } + attachDevToolsData(data: DevToolsData): void { this.#devToolsData = data; } @@ -425,6 +430,7 @@ export class McpResponse implements Response { requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath, responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath, saveFile: (data, filename) => context.saveFile(data, filename), + redactNetworkHeaders: this.#redactNetworkHeaders, }); detailedNetworkRequest = formatter; } @@ -568,6 +574,7 @@ export class McpResponse implements Response { this.#networkRequestsOptions?.networkRequestIdInDevToolsUI, fetchData: false, saveFile: (data, filename) => context.saveFile(data, filename), + redactNetworkHeaders: this.#redactNetworkHeaders, }), ), ); diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 3cfbacac0..3c8bcf07d 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -261,6 +261,12 @@ export const cliOptions = { 'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)', hidden: true, }, + redactNetworkHeaders: { + type: 'boolean', + describe: + 'If true, redacts some of the network headers considered senstive before returning to the client.', + default: false, + }, } satisfies Record; export type ParsedArguments = ReturnType; diff --git a/src/formatters/NetworkFormatter.ts b/src/formatters/NetworkFormatter.ts index abfb0693e..045d28855 100644 --- a/src/formatters/NetworkFormatter.ts +++ b/src/formatters/NetworkFormatter.ts @@ -6,7 +6,11 @@ import {isUtf8} from 'node:buffer'; -import type {HTTPRequest, HTTPResponse} from '../third_party/index.js'; +import { + DevTools, + type HTTPRequest, + type HTTPResponse, +} from '../third_party/index.js'; const BODY_CONTEXT_SIZE_LIMIT = 10000; @@ -21,6 +25,7 @@ export interface NetworkFormatterOptions { data: Uint8Array, filename: string, ) => Promise<{filename: string}>; + redactNetworkHeaders: boolean; } interface NetworkRequestConcise { @@ -150,6 +155,20 @@ export class NetworkFormatter { }; } + #redactNetworkHeaders( + headers: Record, + ): Record { + const headersList = Object.entries(headers).map(item => { + return {name: item[0], value: item[1]}; + }); + const redacted = + DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList); + return redacted.reduce>((acc, item) => { + acc[item.name] = item.value; + return acc; + }, {}); + } + toJSONDetailed(): NetworkRequestDetailed { const redirectChain = this.#request.redirectChain(); const formattedRedirectChain = redirectChain.reverse().map(request => { @@ -159,16 +178,24 @@ export class NetworkFormatter { const formatter = new NetworkFormatter(request, { requestId: id, saveFile: this.#options.saveFile, + redactNetworkHeaders: this.#options.redactNetworkHeaders, }); return formatter.toJSON(); }); + const responseHeaders = this.#request.response()?.headers(); + return { ...this.toJSON(), - requestHeaders: this.#request.headers(), + requestHeaders: this.#options.redactNetworkHeaders + ? this.#redactNetworkHeaders(this.#request.headers()) + : this.#request.headers(), requestBody: this.#requestBody, requestBodyFilePath: this.#requestBodyFilePath, - responseHeaders: this.#request.response()?.headers(), + responseHeaders: + this.#options.redactNetworkHeaders && responseHeaders + ? this.#redactNetworkHeaders(responseHeaders) + : this.#request.response()?.headers(), responseBody: this.#responseBody, responseBodyFilePath: this.#responseBodyFilePath, failure: this.#request.failure()?.errorText, diff --git a/src/index.ts b/src/index.ts index 362f2348a..8fed38b83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,6 +191,8 @@ export async function createMcpServer( const response = serverArgs.slim ? new SlimMcpResponse(serverArgs) : new McpResponse(serverArgs); + + response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders); if ('pageScoped' in tool && tool.pageScoped) { const page = serverArgs.experimentalPageIdRouting && diff --git a/tests/McpContext.test.js.snapshot b/tests/McpContext.test.js.snapshot index cfdd6f902..3f69a75c9 100644 --- a/tests/McpContext.test.js.snapshot +++ b/tests/McpContext.test.js.snapshot @@ -6,7 +6,7 @@ exports[`McpContext > should include detailed network request in structured cont "url": "http://example.com/detail", "status": "pending", "requestHeaders": { - "content-size": "10" + "content-size": "" } } } diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index 1fb633b76..a0eb9bd1f 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -2,7 +2,7 @@ exports[`McpResponse > add network request when attached 1`] = ` ## Request http://example.com Status: pending ### Request Headers -- content-size:10 +- content-size: ## Network requests Showing 1-1 of 1 (Page 1 of 1). reqid=1 GET http://example.com [pending] @@ -16,7 +16,7 @@ exports[`McpResponse > add network request when attached 2`] = ` "url": "http://example.com", "status": "pending", "requestHeaders": { - "content-size": "10" + "content-size": "" } }, "pagination": { @@ -44,7 +44,7 @@ exports[`McpResponse > add network request when attached with POST data 1`] = ` ## Request http://example.com Status: 200 ### Request Headers -- content-size:10 +- content-size: ### Request Body {"request":"body"} ### Response Headers @@ -64,7 +64,7 @@ exports[`McpResponse > add network request when attached with POST data 2`] = ` "url": "http://example.com", "status": "200", "requestHeaders": { - "content-size": "10" + "content-size": "" }, "requestBody": "{\\"request\\":\\"body\\"}", "responseHeaders": { diff --git a/tests/cli.test.ts b/tests/cli.test.ts index b18a4532f..44347af2e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -23,6 +23,8 @@ describe('cli args parsing', () => { performanceCrux: true, 'usage-statistics': true, usageStatistics: true, + 'redact-network-headers': false, + redactNetworkHeaders: false, }; it('parses with default args', async () => { diff --git a/tests/formatters/NetworkFormatter.test.ts b/tests/formatters/NetworkFormatter.test.ts index d09e18184..a8fad2c26 100644 --- a/tests/formatters/NetworkFormatter.test.ts +++ b/tests/formatters/NetworkFormatter.test.ts @@ -31,6 +31,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -43,6 +44,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -56,6 +58,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -71,6 +74,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -86,6 +90,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -104,6 +109,7 @@ describe('NetworkFormatter', () => { const formatter = await NetworkFormatter.from(request, { requestId: 1, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -118,6 +124,7 @@ describe('NetworkFormatter', () => { requestId: 1, selectedInDevToolsUI: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); assert.equal( @@ -138,6 +145,7 @@ describe('NetworkFormatter', () => { requestId: 200, fetchData: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); assert.match(result, /test/); @@ -154,6 +162,7 @@ describe('NetworkFormatter', () => { requestId: 200, fetchData: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); @@ -176,6 +185,7 @@ describe('NetworkFormatter', () => { requestId: 20, fetchData: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); assert.match(result, /some text/); @@ -209,6 +219,7 @@ describe('NetworkFormatter', () => { await writeFile(filename, data); return {filename}; }, + redactNetworkHeaders: false, }); const json = formatter.toJSONDetailed() as { @@ -252,6 +263,7 @@ describe('NetworkFormatter', () => { await writeFile(filename, data); return {filename}; }, + redactNetworkHeaders: false, }); const reqContent = await readFile(reqPath, 'utf8'); @@ -272,6 +284,7 @@ describe('NetworkFormatter', () => { requestId: 200, fetchData: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); @@ -289,6 +302,7 @@ describe('NetworkFormatter', () => { requestId: 1, requestIdResolver: () => 2, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); assert.match(result, /Redirect chain/); @@ -322,6 +336,7 @@ describe('NetworkFormatter', () => { await writeFile(filename, data); return {filename}; }, + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); @@ -361,6 +376,7 @@ describe('NetworkFormatter', () => { await writeFile(filename, data); return {filename}; }, + redactNetworkHeaders: false, }); const result = formatter.toStringDetailed(); @@ -379,6 +395,7 @@ describe('NetworkFormatter', () => { requestId: 1, selectedInDevToolsUI: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toJSON(); assert.deepEqual(result, { @@ -404,6 +421,7 @@ describe('NetworkFormatter', () => { requestId: 1, fetchData: true, saveFile: async () => ({filename: ''}), + redactNetworkHeaders: false, }); const result = formatter.toJSONDetailed(); assert.deepEqual(result, { @@ -425,6 +443,38 @@ describe('NetworkFormatter', () => { }); }); + it('redacts headers', async () => { + const response = getMockResponse({ + headers: { + 'set-cookie': 'secret=123', + 'content-type': 'text/plain', + }, + }); + response.buffer = () => Promise.resolve(Buffer.from('response')); + const request = getMockRequest({ + response, + headers: { + cookie: 'secret=123', + 'user-agent': 'test', + }, + }); + const formatter = await NetworkFormatter.from(request, { + requestId: 1, + fetchData: true, + saveFile: async () => ({filename: ''}), + redactNetworkHeaders: true, + }); + const result = formatter.toJSONDetailed(); + assert.deepEqual(result.requestHeaders, { + cookie: '', + 'user-agent': 'test', + }); + assert.deepEqual(result.responseHeaders, { + 'set-cookie': '', + 'content-type': 'text/plain', + }); + }); + it('returns file paths in structured detailed data', async () => { const request = { method: () => 'POST', @@ -453,6 +503,7 @@ describe('NetworkFormatter', () => { await writeFile(filename, data); return {filename}; }, + redactNetworkHeaders: false, }); const result = formatter.toJSONDetailed() as { diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index b77255483..4d920f18c 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -5,23 +5,23 @@ Status: 200 - accept-language: - upgrade-insecure-requests:1 - user-agent: -- sec-ch-ua:"Not-A.Brand";v="24", "Chromium";v="146" -- sec-ch-ua-mobile:?0 -- sec-ch-ua-platform:"" +- sec-ch-ua: +- sec-ch-ua-mobile: +- sec-ch-ua-platform: - accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 - accept-encoding:gzip, deflate, br, zstd - connection:keep-alive - host:localhost: -- sec-fetch-dest:document -- sec-fetch-mode:navigate -- sec-fetch-site:none -- sec-fetch-user:?1 +- sec-fetch-dest: +- sec-fetch-mode: +- sec-fetch-site: +- sec-fetch-user: ### Response Headers - connection:keep-alive -- content-length:239 +- content-length: - content-type:text/html; charset=utf-8 - date: -- keep-alive:timeout=5 +- keep-alive: ### Response Body `; diff --git a/tests/utils.ts b/tests/utils.ts index 23257616f..f764fa9ce 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -142,6 +142,7 @@ export function getMockRequest( navigationRequest?: boolean; frame?: Frame; redirectChain?: HTTPRequest[]; + headers?: Record; } = {}, ): HTTPRequest { return { @@ -170,9 +171,11 @@ export function getMockRequest( return options.resourceType ?? 'document'; }, headers(): Record { - return { - 'content-size': '10', - }; + return ( + options.headers ?? { + 'content-size': '10', + } + ); }, redirectChain(): HTTPRequest[] { return options.redirectChain ?? []; @@ -190,6 +193,7 @@ export function getMockRequest( export function getMockResponse( options: { status?: number; + headers?: Record; } = {}, ): HTTPResponse { return { @@ -197,9 +201,9 @@ export function getMockResponse( return options.status ?? 200; }, headers(): Record { - return {}; + return options.headers ?? {}; }, - } as HTTPResponse; + } as unknown as HTTPResponse; } export function html(