feat: support DevTools header redactions as an option (#1848)

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.
This commit is contained in:
Alex Rudenko
2026-04-14 15:21:11 +02:00
committed by GitHub
parent 49eed510e0
commit 5c398c4e7c
11 changed files with 126 additions and 22 deletions
+5
View File
@@ -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`
<!-- END AUTO GENERATED OPTIONS -->
Pass them via the `args` property in the JSON configuration. For example:
+7
View File
@@ -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,
}),
),
);
@@ -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<string, YargsOptions>;
export type ParsedArguments = ReturnType<typeof parseArguments>;
+30 -3
View File
@@ -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<ArrayBufferLike>,
filename: string,
) => Promise<{filename: string}>;
redactNetworkHeaders: boolean;
}
interface NetworkRequestConcise {
@@ -150,6 +155,20 @@ export class NetworkFormatter {
};
}
#redactNetworkHeaders(
headers: Record<string, string>,
): Record<string, string> {
const headersList = Object.entries(headers).map(item => {
return {name: item[0], value: item[1]};
});
const redacted =
DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
return redacted.reduce<Record<string, string>>((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,
+2
View File
@@ -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 &&
+1 -1
View File
@@ -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": "<redacted>"
}
}
}
+4 -4
View File
@@ -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:<redacted>
## 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": "<redacted>"
}
},
"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:<redacted>
### 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": "<redacted>"
},
"requestBody": "{\\"request\\":\\"body\\"}",
"responseHeaders": {
+2
View File
@@ -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 () => {
+51
View File
@@ -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: '<redacted>',
'user-agent': 'test',
});
assert.deepEqual(result.responseHeaders, {
'set-cookie': '<redacted>',
'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 {
+9 -9
View File
@@ -5,23 +5,23 @@ Status: 200
- accept-language:<lang>
- upgrade-insecure-requests:1
- user-agent:<user-agent>
- sec-ch-ua:"Not-A.Brand";v="24", "Chromium";v="146"
- sec-ch-ua-mobile:?0
- sec-ch-ua-platform:"<os>"
- sec-ch-ua:<redacted>
- sec-ch-ua-mobile:<redacted>
- sec-ch-ua-platform:<redacted>
- 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:<port>
- sec-fetch-dest:document
- sec-fetch-mode:navigate
- sec-fetch-site:none
- sec-fetch-user:?1
- sec-fetch-dest:<redacted>
- sec-fetch-mode:<redacted>
- sec-fetch-site:<redacted>
- sec-fetch-user:<redacted>
### Response Headers
- connection:keep-alive
- content-length:239
- content-length:<redacted>
- content-type:text/html; charset=utf-8
- date:<long date>
- keep-alive:timeout=5
- keep-alive:<redacted>
### Response Body
<not available anymore>
`;
+9 -5
View File
@@ -142,6 +142,7 @@ export function getMockRequest(
navigationRequest?: boolean;
frame?: Frame;
redirectChain?: HTTPRequest[];
headers?: Record<string, string>;
} = {},
): HTTPRequest {
return {
@@ -170,9 +171,11 @@ export function getMockRequest(
return options.resourceType ?? 'document';
},
headers(): Record<string, string> {
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<string, string>;
} = {},
): HTTPResponse {
return {
@@ -197,9 +201,9 @@ export function getMockResponse(
return options.status ?? 200;
},
headers(): Record<string, string> {
return {};
return options.headers ?? {};
},
} as HTTPResponse;
} as unknown as HTTPResponse;
}
export function html(