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 +10 -0
- package/build/src/HeapSnapshotManager.js +61 -0
- package/build/src/McpContext.js +15 -4
- package/build/src/McpPage.js +3 -0
- package/build/src/McpResponse.js +87 -4
- package/build/src/PageCollector.js +9 -5
- package/build/src/bin/brave-devtools-mcp-cli-options.js +14 -0
- package/build/src/bin/brave-devtools.js +1 -0
- package/build/src/bin/cliDefinitions.js +3 -3
- package/build/src/formatters/HeapSnapshotFormatter.js +35 -0
- package/build/src/formatters/NetworkFormatter.js +24 -7
- package/build/src/index.js +11 -0
- package/build/src/telemetry/ClearcutLogger.js +15 -8
- package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
- package/build/src/third_party/devtools-formatter-worker.js +6 -15432
- package/build/src/third_party/devtools-heap-snapshot-worker.js +7 -0
- package/build/src/third_party/index.js +24 -182509
- package/build/src/tools/categories.js +2 -0
- package/build/src/tools/lighthouse.js +2 -2
- package/build/src/tools/memory.js +20 -2
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/pages.js +5 -2
- package/build/src/tools/performance.js +1 -1
- package/build/src/tools/screencast.js +2 -1
- package/build/src/tools/screenshot.js +2 -2
- package/build/src/tools/tools.js +2 -0
- package/build/src/tools/webmcp.js +63 -0
- package/build/src/utils/check-for-updates.js +2 -1
- package/build/src/utils/files.js +4 -0
- package/build/src/version.js +1 -1
- package/package.json +5 -3
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
|
+
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
595
|
+
async saveFile(data, clientProvidedFilePath, extension) {
|
|
594
596
|
try {
|
|
595
|
-
const filePath = path.resolve(
|
|
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
|
}
|
package/build/src/McpPage.js
CHANGED
package/build/src/McpResponse.js
CHANGED
|
@@ -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 =
|
|
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.#
|
|
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.#
|
|
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('
|
|
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 = (
|
|
251
|
+
#onIssueAdded = (inspectorIssue) => {
|
|
252
252
|
try {
|
|
253
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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.#
|
|
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.#
|
|
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
|
}
|
package/build/src/index.js
CHANGED
|
@@ -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 `${
|
|
43
|
+
return `${snakeCaseName}_length`;
|
|
43
44
|
}
|
|
44
45
|
else if (zodType === 'ZodArray') {
|
|
45
|
-
return `${
|
|
46
|
+
return `${snakeCaseName}_count`;
|
|
46
47
|
}
|
|
47
48
|
else {
|
|
48
|
-
return
|
|
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
|
}
|