brave-mcp 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  control and inspect a live Brave browser. It acts as a Model-Context-Protocol
9
9
  (MCP) server, giving your AI coding assistant access to the full power of
10
10
  DevTools for reliable automation, in-depth debugging, and performance analysis.
11
+ A [CLI](docs/cli.md) is also provided for use without MCP.
11
12
 
12
13
  ## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md)
13
14
 
@@ -692,6 +693,10 @@ The Brave DevTools MCP server supports the following configuration options:
692
693
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
693
694
  - **Type:** boolean
694
695
 
696
+ - **`--experimentalWebmcp`/ `--experimental-webmcp`**
697
+ Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
698
+ - **Type:** boolean
699
+
695
700
  - **`--chromeArg`/ `--chrome-arg`**
696
701
  Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
697
702
  - **Type:** array
@@ -729,6 +734,11 @@ The Brave DevTools MCP server supports the following configuration options:
729
734
  Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
730
735
  - **Type:** boolean
731
736
 
737
+ - **`--redactNetworkHeaders`/ `--redact-network-headers`**
738
+ If true, redacts some of the network headers considered senstive before returning to the client.
739
+ - **Type:** boolean
740
+ - **Default:** `false`
741
+
732
742
  <!-- END AUTO GENERATED OPTIONS -->
733
743
 
734
744
  Pass them via the `args` property in the JSON configuration. For example:
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fsSync from 'node:fs';
7
+ import path from 'node:path';
8
+ import { DevTools } from './third_party/index.js';
9
+ export class HeapSnapshotManager {
10
+ #snapshots = new Map();
11
+ async getSnapshot(filePath) {
12
+ const absolutePath = path.resolve(filePath);
13
+ const cached = this.#snapshots.get(absolutePath);
14
+ if (cached) {
15
+ return cached.snapshot;
16
+ }
17
+ const { snapshot, worker } = await this.#loadSnapshot(absolutePath);
18
+ this.#snapshots.set(absolutePath, { snapshot, worker });
19
+ return snapshot;
20
+ }
21
+ async getAggregates(filePath) {
22
+ const snapshot = await this.getSnapshot(filePath);
23
+ const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
24
+ return await snapshot.aggregatesWithFilter(filter);
25
+ }
26
+ async getStats(filePath) {
27
+ const snapshot = await this.getSnapshot(filePath);
28
+ return await snapshot.getStatistics();
29
+ }
30
+ async getStaticData(filePath) {
31
+ const snapshot = await this.getSnapshot(filePath);
32
+ return snapshot.staticData;
33
+ }
34
+ async #loadSnapshot(absolutePath) {
35
+ const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
36
+ /* noop */
37
+ }, import.meta.resolve('./third_party/devtools-heap-snapshot-worker.js'));
38
+ const { promise: snapshotPromise, resolve: resolveSnapshot } = Promise.withResolvers();
39
+ const loaderProxy = workerProxy.createLoader(1, snapshotProxy => {
40
+ resolveSnapshot(snapshotProxy);
41
+ });
42
+ const fileStream = fsSync.createReadStream(absolutePath, {
43
+ encoding: 'utf-8',
44
+ highWaterMark: 1024 * 1024,
45
+ });
46
+ for await (const chunk of fileStream) {
47
+ await loaderProxy.write(chunk);
48
+ }
49
+ await loaderProxy.close();
50
+ const snapshot = await snapshotPromise;
51
+ return { snapshot, worker: workerProxy };
52
+ }
53
+ dispose(filePath) {
54
+ const absolutePath = path.resolve(filePath);
55
+ const cached = this.#snapshots.get(absolutePath);
56
+ if (cached) {
57
+ cached.worker.dispose();
58
+ this.#snapshots.delete(absolutePath);
59
+ }
60
+ }
61
+ }
@@ -6,6 +6,7 @@
6
6
  import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { UniverseManager } from './DevtoolsUtils.js';
9
+ import { HeapSnapshotManager } from './HeapSnapshotManager.js';
9
10
  import { McpPage } from './McpPage.js';
10
11
  import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
11
12
  import { Locator } from './third_party/index.js';
@@ -13,7 +14,7 @@ import { PredefinedNetworkConditions } from './third_party/index.js';
13
14
  import { listPages } from './tools/pages.js';
14
15
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
15
16
  import { ExtensionRegistry, } from './utils/ExtensionRegistry.js';
16
- import { saveTemporaryFile } from './utils/files.js';
17
+ import { ensureExtension, saveTemporaryFile } from './utils/files.js';
17
18
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
18
19
  const DEFAULT_TIMEOUT = 5_000;
19
20
  const NAVIGATION_TIMEOUT = 10_000;
@@ -42,6 +43,7 @@ export class McpContext {
42
43
  #traceResults = [];
43
44
  #locatorClass;
44
45
  #options;
46
+ #heapSnapshotManager = new HeapSnapshotManager();
45
47
  constructor(browser, logger, options, locatorClass) {
46
48
  this.browser = browser;
47
49
  this.logger = logger;
@@ -56,7 +58,7 @@ export class McpContext {
56
58
  uncaughtError: event => {
57
59
  collect(event);
58
60
  },
59
- issue: event => {
61
+ devtoolsAggregatedIssue: event => {
60
62
  collect(event);
61
63
  },
62
64
  };
@@ -590,9 +592,9 @@ export class McpContext {
590
592
  async saveTemporaryFile(data, filename) {
591
593
  return await saveTemporaryFile(data, filename);
592
594
  }
593
- async saveFile(data, filename) {
595
+ async saveFile(data, clientProvidedFilePath, extension) {
594
596
  try {
595
- const filePath = path.resolve(filename);
597
+ const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
596
598
  await fs.mkdir(path.dirname(filePath), { recursive: true });
597
599
  await fs.writeFile(filePath, data);
598
600
  return { filename: filePath };
@@ -673,4 +675,13 @@ export class McpContext {
673
675
  getExtension(id) {
674
676
  return this.#extensionRegistry.getById(id);
675
677
  }
678
+ async getHeapSnapshotAggregates(filePath) {
679
+ return await this.#heapSnapshotManager.getAggregates(filePath);
680
+ }
681
+ async getHeapSnapshotStats(filePath) {
682
+ return await this.#heapSnapshotManager.getStats(filePath);
683
+ }
684
+ async getHeapSnapshotStaticData(filePath) {
685
+ return await this.#heapSnapshotManager.getStaticData(filePath);
686
+ }
676
687
  }
@@ -48,6 +48,9 @@ export class McpPage {
48
48
  getInPageTools() {
49
49
  return this.inPageTools;
50
50
  }
51
+ getWebMcpTools() {
52
+ return this.pptrPage.webmcp.tools();
53
+ }
51
54
  get networkConditions() {
52
55
  return this.emulationSettings.networkConditions ?? null;
53
56
  }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
7
+ import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
7
8
  import { IssueFormatter } from './formatters/IssueFormatter.js';
8
9
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
9
10
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
@@ -118,20 +119,26 @@ export class McpResponse {
118
119
  #attachedLighthouseResult;
119
120
  #textResponseLines = [];
120
121
  #images = [];
122
+ #heapSnapshotOptions;
121
123
  #networkRequestsOptions;
122
124
  #consoleDataOptions;
123
125
  #listExtensions;
124
126
  #listInPageTools;
127
+ #listWebMcpTools;
125
128
  #devToolsData;
126
129
  #tabId;
127
130
  #args;
128
131
  #page;
132
+ #redactNetworkHeaders = true;
129
133
  constructor(args) {
130
134
  this.#args = args;
131
135
  }
132
136
  setPage(page) {
133
137
  this.#page = page;
134
138
  }
139
+ setRedactNetworkHeaders(value) {
140
+ this.#redactNetworkHeaders = value;
141
+ }
135
142
  attachDevToolsData(data) {
136
143
  this.#devToolsData = data;
137
144
  }
@@ -158,6 +165,9 @@ export class McpResponse {
158
165
  this.#listInPageTools = true;
159
166
  }
160
167
  }
168
+ setListWebMcpTools() {
169
+ this.#listWebMcpTools = true;
170
+ }
161
171
  setIncludeNetworkRequests(value, options) {
162
172
  if (!value) {
163
173
  this.#networkRequestsOptions = undefined;
@@ -246,6 +256,22 @@ export class McpResponse {
246
256
  appendResponseLine(value) {
247
257
  this.#textResponseLines.push(value);
248
258
  }
259
+ setHeapSnapshotAggregates(aggregates, options) {
260
+ this.#heapSnapshotOptions = {
261
+ ...this.#heapSnapshotOptions,
262
+ include: true,
263
+ aggregates,
264
+ pagination: options,
265
+ };
266
+ }
267
+ setHeapSnapshotStats(stats, staticData) {
268
+ this.#heapSnapshotOptions = {
269
+ ...this.#heapSnapshotOptions,
270
+ include: true,
271
+ stats,
272
+ staticData,
273
+ };
274
+ }
249
275
  attachImage(value) {
250
276
  this.#images.push(value);
251
277
  }
@@ -258,6 +284,9 @@ export class McpResponse {
258
284
  get snapshotParams() {
259
285
  return this.#snapshotParams;
260
286
  }
287
+ get listWebMcpTools() {
288
+ return this.#listWebMcpTools;
289
+ }
261
290
  async handle(toolName, context) {
262
291
  if (this.#includePages) {
263
292
  await context.createPagesSnapshot();
@@ -275,8 +304,8 @@ export class McpResponse {
275
304
  if (textSnapshot) {
276
305
  const formatter = new SnapshotFormatter(textSnapshot);
277
306
  if (this.#snapshotParams.filePath) {
278
- await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
279
- snapshot = this.#snapshotParams.filePath;
307
+ const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
308
+ snapshot = result.filename;
280
309
  }
281
310
  else {
282
311
  snapshot = formatter;
@@ -295,7 +324,8 @@ export class McpResponse {
295
324
  fetchData: true,
296
325
  requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
297
326
  responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
298
- saveFile: (data, filename) => context.saveFile(data, filename),
327
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
328
+ redactNetworkHeaders: this.#redactNetworkHeaders,
299
329
  });
300
330
  detailedNetworkRequest = formatter;
301
331
  }
@@ -337,6 +367,11 @@ export class McpResponse {
337
367
  inPageTools = await getToolGroup(page);
338
368
  page.inPageTools = inPageTools;
339
369
  }
370
+ let webmcpTools;
371
+ if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
372
+ const page = this.#page ?? context.getSelectedMcpPage();
373
+ webmcpTools = page.getWebMcpTools();
374
+ }
340
375
  let consoleMessages;
341
376
  if (this.#consoleDataOptions?.include) {
342
377
  if (!this.#page) {
@@ -399,7 +434,8 @@ export class McpResponse {
399
434
  selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
400
435
  this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
401
436
  fetchData: false,
402
- saveFile: (data, filename) => context.saveFile(data, filename),
437
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
438
+ redactNetworkHeaders: this.#redactNetworkHeaders,
403
439
  })));
404
440
  }
405
441
  }
@@ -414,6 +450,7 @@ export class McpResponse {
414
450
  extensions,
415
451
  lighthouseResult: this.#attachedLighthouseResult,
416
452
  inPageTools,
453
+ webmcpTools,
417
454
  });
418
455
  }
419
456
  format(toolName, context, data) {
@@ -579,6 +616,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
579
616
  structuredContent.snapshot = data.snapshot.toJSON();
580
617
  }
581
618
  }
619
+ if (this.#heapSnapshotOptions?.include) {
620
+ response.push('## Heap Snapshot Data');
621
+ const stats = this.#heapSnapshotOptions.stats;
622
+ const staticData = this.#heapSnapshotOptions.staticData;
623
+ if (stats) {
624
+ response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
625
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
626
+ structuredContent.heapSnapshot.stats = stats;
627
+ }
628
+ if (staticData) {
629
+ response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
630
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
631
+ structuredContent.heapSnapshot.staticData = staticData;
632
+ }
633
+ const aggregates = this.#heapSnapshotOptions.aggregates;
634
+ if (aggregates) {
635
+ const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
636
+ const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
637
+ structuredContent.pagination = paginationData.pagination;
638
+ response.push(...paginationData.info);
639
+ const paginatedRecord = Object.fromEntries(paginationData.items);
640
+ const formatter = new HeapSnapshotFormatter(paginatedRecord);
641
+ response.push(formatter.toString());
642
+ structuredContent.heapSnapshotData = formatter.toJSON();
643
+ }
644
+ }
582
645
  if (data.detailedNetworkRequest) {
583
646
  response.push(data.detailedNetworkRequest.toStringDetailed());
584
647
  structuredContent.networkRequest =
@@ -622,6 +685,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
622
685
  response.push(toolDefinitionsMessage);
623
686
  }
624
687
  }
688
+ if (this.#listWebMcpTools && data.webmcpTools) {
689
+ structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
690
+ name,
691
+ description,
692
+ inputSchema,
693
+ annotations,
694
+ }));
695
+ response.push('## WebMCP tools');
696
+ if (data.webmcpTools.length === 0) {
697
+ response.push('No WebMCP tools available.');
698
+ }
699
+ else {
700
+ const webmcpToolsMessage = data.webmcpTools
701
+ .map(tool => {
702
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
703
+ })
704
+ .join('\n');
705
+ response.push(webmcpToolsMessage);
706
+ }
707
+ }
625
708
  if (this.#networkRequestsOptions?.include && data.networkRequests) {
626
709
  const requests = data.networkRequests;
627
710
  response.push('## Network requests');
@@ -206,7 +206,7 @@ class PageEventSubscriber {
206
206
  async subscribe() {
207
207
  this.#resetIssueAggregator();
208
208
  this.#page.on('framenavigated', this.#onFrameNavigated);
209
- this.#session.on('Audits.issueAdded', this.#onIssueAdded);
209
+ this.#page.on('issue', this.#onIssueAdded);
210
210
  this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
211
211
  try {
212
212
  await this.#session.send('Audits.enable');
@@ -219,7 +219,7 @@ class PageEventSubscriber {
219
219
  this.#seenKeys.clear();
220
220
  this.#seenIssues.clear();
221
221
  this.#page.off('framenavigated', this.#onFrameNavigated);
222
- this.#session.off('Audits.issueAdded', this.#onIssueAdded);
222
+ this.#page.off('issue', this.#onIssueAdded);
223
223
  this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
224
224
  if (this.#issueAggregator) {
225
225
  this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
@@ -233,7 +233,7 @@ class PageEventSubscriber {
233
233
  return;
234
234
  }
235
235
  this.#seenIssues.add(event.data);
236
- this.#page.emit('issue', event.data);
236
+ this.#page.emit('devtoolsAggregatedIssue', event.data);
237
237
  };
238
238
  #onExceptionThrown = (event) => {
239
239
  this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
@@ -248,9 +248,13 @@ class PageEventSubscriber {
248
248
  this.#seenIssues.clear();
249
249
  this.#resetIssueAggregator();
250
250
  };
251
- #onIssueAdded = (data) => {
251
+ #onIssueAdded = (inspectorIssue) => {
252
252
  try {
253
- const inspectorIssue = data.issue;
253
+ // DevTools currently defines this protocol issue code but has no
254
+ // IssuesManager handler for it, so calling into the mapper only warns.
255
+ if (String(inspectorIssue.code) === 'PerformanceIssue') {
256
+ return;
257
+ }
254
258
  const issue = DevTools.createIssuesFromProtocolIssue(null,
255
259
  // @ts-expect-error Protocol types diverge.
256
260
  inspectorIssue)[0];
@@ -149,6 +149,11 @@ export const cliOptions = {
149
149
  describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
150
150
  hidden: false,
151
151
  },
152
+ experimentalMemory: {
153
+ type: 'boolean',
154
+ describe: 'Whether to enable experimental memory tools.',
155
+ hidden: true,
156
+ },
152
157
  experimentalStructuredContent: {
153
158
  type: 'boolean',
154
159
  describe: 'Whether to output structured formatted content.',
@@ -168,6 +173,10 @@ export const cliOptions = {
168
173
  type: 'boolean',
169
174
  describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
170
175
  },
176
+ experimentalWebmcp: {
177
+ type: 'boolean',
178
+ describe: 'Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
179
+ },
171
180
  braveArg: {
172
181
  type: 'array',
173
182
  describe: 'Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.',
@@ -236,6 +245,11 @@ export const cliOptions = {
236
245
  describe: 'Set by Brave DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
237
246
  hidden: true,
238
247
  },
248
+ redactNetworkHeaders: {
249
+ type: 'boolean',
250
+ describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
251
+ default: false,
252
+ },
239
253
  };
240
254
  export function parseArguments(version, argv = process.argv) {
241
255
  const yargsInstance = yargs(hideBin(argv))
@@ -32,6 +32,7 @@ delete startCliOptions.viewport;
32
32
  // tools, they need to be enabled during CLI generation.
33
33
  delete startCliOptions.experimentalPageIdRouting;
34
34
  delete startCliOptions.experimentalVision;
35
+ delete startCliOptions.experimentalWebmcp;
35
36
  delete startCliOptions.experimentalInteropTools;
36
37
  delete startCliOptions.experimentalScreencast;
37
38
  delete startCliOptions.categoryEmulation;
@@ -175,13 +175,13 @@ export const commands = {
175
175
  requestFilePath: {
176
176
  name: 'requestFilePath',
177
177
  type: 'string',
178
- description: 'The absolute or relative path to save the request body to. If omitted, the body is returned inline.',
178
+ description: 'The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.',
179
179
  required: false,
180
180
  },
181
181
  responseFilePath: {
182
182
  name: 'responseFilePath',
183
183
  type: 'string',
184
- description: 'The absolute or relative path to save the response body to. If omitted, the body is returned inline.',
184
+ description: 'The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.',
185
185
  required: false,
186
186
  },
187
187
  },
@@ -314,7 +314,7 @@ export const commands = {
314
314
  },
315
315
  },
316
316
  list_pages: {
317
- description: 'Get a list of pages open in the browser.',
317
+ description: 'Get a list of pages open in the browser.',
318
318
  category: 'Navigation automation',
319
319
  args: {},
320
320
  },
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export class HeapSnapshotFormatter {
7
+ #aggregates;
8
+ constructor(aggregates) {
9
+ this.#aggregates = aggregates;
10
+ }
11
+ #getSortedAggregates() {
12
+ return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
13
+ }
14
+ toString() {
15
+ const sorted = this.#getSortedAggregates();
16
+ const lines = [];
17
+ lines.push('className,count,selfSize,maxRetainedSize');
18
+ for (const info of sorted) {
19
+ lines.push(`"${info.name}",${info.count},${info.self},${info.maxRet}`);
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+ toJSON() {
24
+ const sorted = this.#getSortedAggregates();
25
+ return sorted.map(info => ({
26
+ className: info.name,
27
+ count: info.count,
28
+ selfSize: info.self,
29
+ retainedSize: info.maxRet,
30
+ }));
31
+ }
32
+ static sort(aggregates) {
33
+ return Object.entries(aggregates).sort((a, b) => b[1].self - a[1].self);
34
+ }
35
+ }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  * */
6
6
  import { isUtf8 } from 'node:buffer';
7
+ import { DevTools, } from '../third_party/index.js';
7
8
  const BODY_CONTEXT_SIZE_LIMIT = 10000;
8
9
  export class NetworkFormatter {
9
10
  #request;
@@ -40,8 +41,8 @@ export class NetworkFormatter {
40
41
  throw new Error('saveFile is not provided');
41
42
  }
42
43
  if (data) {
43
- await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath);
44
- this.#requestBodyFilePath = this.#options.requestFilePath;
44
+ const result = await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath, '.network-request');
45
+ this.#requestBodyFilePath = result.filename;
45
46
  }
46
47
  else {
47
48
  this.#requestBody = requestBodyNotAvailableMessage;
@@ -66,8 +67,8 @@ export class NetworkFormatter {
66
67
  if (!this.#options.saveFile) {
67
68
  throw new Error('saveFile is not provided');
68
69
  }
69
- await this.#options.saveFile(buffer, this.#options.responseFilePath);
70
- this.#responseBodyFilePath = this.#options.responseFilePath;
70
+ const result = await this.#options.saveFile(buffer, this.#options.responseFilePath, '.network-response');
71
+ this.#responseBodyFilePath = result.filename;
71
72
  }
72
73
  catch {
73
74
  // Flatten error handling for buffer() failure and save failure
@@ -96,6 +97,16 @@ export class NetworkFormatter {
96
97
  selectedInDevToolsUI: this.#options.selectedInDevToolsUI,
97
98
  };
98
99
  }
100
+ #redactNetworkHeaders(headers) {
101
+ const headersList = Object.entries(headers).map(item => {
102
+ return { name: item[0], value: item[1] };
103
+ });
104
+ const redacted = DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
105
+ return redacted.reduce((acc, item) => {
106
+ acc[item.name] = item.value;
107
+ return acc;
108
+ }, {});
109
+ }
99
110
  toJSONDetailed() {
100
111
  const redirectChain = this.#request.redirectChain();
101
112
  const formattedRedirectChain = redirectChain.reverse().map(request => {
@@ -105,15 +116,21 @@ export class NetworkFormatter {
105
116
  const formatter = new NetworkFormatter(request, {
106
117
  requestId: id,
107
118
  saveFile: this.#options.saveFile,
119
+ redactNetworkHeaders: this.#options.redactNetworkHeaders,
108
120
  });
109
121
  return formatter.toJSON();
110
122
  });
123
+ const responseHeaders = this.#request.response()?.headers();
111
124
  return {
112
125
  ...this.toJSON(),
113
- requestHeaders: this.#request.headers(),
126
+ requestHeaders: this.#options.redactNetworkHeaders
127
+ ? this.#redactNetworkHeaders(this.#request.headers())
128
+ : this.#request.headers(),
114
129
  requestBody: this.#requestBody,
115
130
  requestBodyFilePath: this.#requestBodyFilePath,
116
- responseHeaders: this.#request.response()?.headers(),
131
+ responseHeaders: this.#options.redactNetworkHeaders && responseHeaders
132
+ ? this.#redactNetworkHeaders(responseHeaders)
133
+ : this.#request.response()?.headers(),
117
134
  responseBody: this.#responseBody,
118
135
  responseBodyFilePath: this.#responseBodyFilePath,
119
136
  failure: this.#request.failure()?.errorText,
@@ -210,7 +227,7 @@ function converNetworkRequestDetailedToStringDetailed(data) {
210
227
  response.push(`### Redirect chain`);
211
228
  let indent = 0;
212
229
  for (const request of redirectChain.reverse()) {
213
- response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)})}`);
230
+ response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`);
214
231
  indent++;
215
232
  }
216
233
  }
@@ -111,6 +111,10 @@ export async function createMcpServer(serverArgs, options) {
111
111
  !serverArgs.experimentalVision) {
112
112
  return;
113
113
  }
114
+ if (tool.annotations.conditions?.includes('experimentalMemory') &&
115
+ !serverArgs.experimentalMemory) {
116
+ return;
117
+ }
114
118
  if (tool.annotations.conditions?.includes('experimentalInteropTools') &&
115
119
  !serverArgs.experimentalInteropTools) {
116
120
  return;
@@ -119,6 +123,10 @@ export async function createMcpServer(serverArgs, options) {
119
123
  !serverArgs.experimentalScreencast) {
120
124
  return;
121
125
  }
126
+ if (tool.annotations.conditions?.includes('experimentalWebmcp') &&
127
+ !serverArgs.experimentalWebmcp) {
128
+ return;
129
+ }
122
130
  const schema = 'pageScoped' in tool &&
123
131
  tool.pageScoped &&
124
132
  serverArgs.experimentalPageIdRouting &&
@@ -141,6 +149,7 @@ export async function createMcpServer(serverArgs, options) {
141
149
  const response = serverArgs.slim
142
150
  ? new SlimMcpResponse(serverArgs)
143
151
  : new McpResponse(serverArgs);
152
+ response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
144
153
  if ('pageScoped' in tool && tool.pageScoped) {
145
154
  const page = serverArgs.experimentalPageIdRouting &&
146
155
  params.pageId &&
@@ -189,6 +198,8 @@ export async function createMcpServer(serverArgs, options) {
189
198
  finally {
190
199
  void clearcutLogger?.logToolInvocation({
191
200
  toolName: tool.name,
201
+ params,
202
+ schema,
192
203
  success,
193
204
  latencyMs: bucketizeLatency(Date.now() - startTime),
194
205
  });
@@ -38,14 +38,15 @@ export function getZodType(zodType) {
38
38
  throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
39
39
  }
40
40
  export function transformArgName(zodType, name) {
41
+ const snakeCaseName = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
41
42
  if (zodType === 'ZodString') {
42
- return `${name}_length`;
43
+ return `${snakeCaseName}_length`;
43
44
  }
44
45
  else if (zodType === 'ZodArray') {
45
- return `${name}_count`;
46
+ return `${snakeCaseName}_count`;
46
47
  }
47
48
  else {
48
- return name;
49
+ return snakeCaseName;
49
50
  }
50
51
  }
51
52
  export function transformArgType(zodType) {
@@ -168,15 +169,21 @@ export class ClearcutLogger {
168
169
  }
169
170
  }
170
171
  async logToolInvocation(args) {
172
+ const tool_invocation = {
173
+ tool_name: args.toolName,
174
+ success: args.success,
175
+ latency_ms: args.latencyMs,
176
+ };
177
+ if (Object.keys(args.params).length > 0) {
178
+ tool_invocation.tool_params = {
179
+ [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema),
180
+ };
181
+ }
171
182
  this.#watchdog.send({
172
183
  type: WatchdogMessageType.LOG_EVENT,
173
184
  payload: {
174
185
  mcp_client: this.#mcpClient,
175
- tool_invocation: {
176
- tool_name: args.toolName,
177
- success: args.success,
178
- latency_ms: args.latencyMs,
179
- },
186
+ tool_invocation: tool_invocation,
180
187
  },
181
188
  });
182
189
  }