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:
@@ -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:
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user