brave-mcp 1.7.0 → 1.8.0

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.
Files changed (60) hide show
  1. package/README.md +72 -18
  2. package/build/src/McpContext.js +47 -24
  3. package/build/src/McpPage.js +2 -2
  4. package/build/src/McpResponse.js +17 -4
  5. package/build/src/ToolHandler.js +66 -15
  6. package/build/src/bin/brave-devtools-mcp-main.js +2 -2
  7. package/build/src/bin/brave-devtools.js +20 -28
  8. package/build/src/browser.js +3 -1
  9. package/build/src/{PageCollector.js → collectors/PageCollector.js} +4 -4
  10. package/build/src/{ServiceWorkerCollector.js → collectors/ServiceWorkerCollector.js} +1 -1
  11. package/build/src/{bin/brave-devtools-cli-options.js → config/cli-options.js} +295 -20
  12. package/build/src/{bin/brave-devtools-mcp-cli-options.js → config/mcp-options.js} +74 -8
  13. package/build/src/daemon/client.js +11 -2
  14. package/build/src/daemon/daemon.js +2 -2
  15. package/build/src/devtools/DevtoolsUtils.js +23 -34
  16. package/build/src/formatters/ConsoleFormatter.js +1 -1
  17. package/build/src/formatters/HeapSnapshotFormatter.js +8 -0
  18. package/build/src/index.js +33 -5
  19. package/build/src/{HeapSnapshotManager.js → processors/HeapSnapshotManager.js} +21 -5
  20. package/build/src/{trace-processing/parse.js → processors/PerformanceTrace.js} +1 -1
  21. package/build/src/telemetry/ClearcutLogger.js +13 -1
  22. package/build/src/telemetry/flagUtils.js +4 -3
  23. package/build/src/telemetry/types.js +4 -0
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +4 -63
  25. package/build/src/third_party/bundled-packages.json +3 -4
  26. package/build/src/third_party/devtools-heap-snapshot-worker.js +78 -20
  27. package/build/src/third_party/index.js +35615 -36338
  28. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
  29. package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
  30. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
  31. package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
  32. package/build/src/tools/console.js +3 -3
  33. package/build/src/tools/emulation.js +2 -2
  34. package/build/src/tools/extensions.js +7 -5
  35. package/build/src/tools/input.js +17 -10
  36. package/build/src/tools/lighthouse.js +3 -1
  37. package/build/src/tools/memory.js +112 -16
  38. package/build/src/tools/network.js +6 -3
  39. package/build/src/tools/pages.js +12 -16
  40. package/build/src/tools/performance.js +16 -24
  41. package/build/src/tools/pwa.js +15 -9
  42. package/build/src/tools/screencast.js +6 -4
  43. package/build/src/tools/screenshot.js +42 -9
  44. package/build/src/tools/script.js +19 -5
  45. package/build/src/tools/slim/tools.js +3 -3
  46. package/build/src/tools/snapshot.js +5 -3
  47. package/build/src/tools/thirdPartyDeveloper.js +2 -2
  48. package/build/src/tools/webmcp.js +2 -2
  49. package/build/src/{WaitForHelper.js → utils/WaitForHelper.js} +2 -2
  50. package/build/src/utils/bytes.js +105 -0
  51. package/build/src/utils/logger.js +25 -11
  52. package/build/src/version.js +1 -1
  53. package/package.json +12 -7
  54. package/skills/a11y-debugging/SKILL.md +2 -0
  55. package/skills/brave-devtools/SKILL.md +5 -4
  56. package/skills/brave-devtools-cli/SKILL.md +103 -64
  57. package/skills/debug-optimize-lcp/SKILL.md +9 -9
  58. package/skills/memory-leak-debugging/SKILL.md +6 -2
  59. package/skills/troubleshooting/SKILL.md +1 -0
  60. package/build/src/utils/string.js +0 -37
package/README.md CHANGED
@@ -1,16 +1,62 @@
1
1
  # Brave DevTools for agents
2
2
 
3
- [![npm brave-mcp package](https://img.shields.io/npm/v/brave-mcp.svg)](https://npmjs.org/package/brave-mcp)
3
+ **Full Chrome DevTools MCP parity, rebuilt for Brave.**
4
4
 
5
- Brave DevTools for agents (`brave-mcp`) lets your coding agent (such as Antigravity, Claude, Cursor or Copilot)
6
- control and inspect a live Brave browser. It acts as a Model-Context-Protocol
7
- (MCP) server, giving your AI coding assistant access to the full power of
8
- Brave DevTools for reliable automation, in-depth debugging, and performance analysis.
9
- A [CLI](docs/cli.md) is also provided for use without MCP.
5
+ [![CI](https://github.com/triuzzi/brave-devtools-mcp/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/triuzzi/brave-devtools-mcp/actions/workflows/run-tests.yml)
6
+ [![npm](https://img.shields.io/npm/v/brave-mcp.svg?logo=npm)](https://www.npmjs.com/package/brave-mcp)
7
+ [![npm downloads](https://img.shields.io/npm/dm/brave-mcp.svg?logo=npm)](https://www.npmjs.com/package/brave-mcp)
8
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-active-5B5BD6)](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.triuzzi%2Fbrave-mcp)
9
+ [![npm provenance](https://img.shields.io/badge/npm-provenance-2E7D32)](https://registry.npmjs.org/-/npm/v1/attestations/brave-mcp@1.7.1)
10
+ [![upstream](https://img.shields.io/badge/upstream-0%20commits%20behind-brightgreen)](https://github.com/triuzzi/brave-devtools-mcp/compare/ChromeDevTools:main...main)
11
+ [![license](https://img.shields.io/github/license/triuzzi/brave-devtools-mcp)](./LICENSE)
10
12
 
11
- This Brave port tracks [ChromeDevTools/chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp) and preserves its complete tool surface.
13
+ ![Brave DevTools MCP: full Chrome DevTools MCP parity, rebuilt for Brave](./docs/assets/social-preview.png)
12
14
 
13
- ## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md)
15
+ `brave-mcp` gives Claude Code, Codex, Cursor, OpenCode, and other MCP clients direct access to Brave for browser automation, network and console debugging, performance analysis, screenshots, accessibility inspection, and memory profiling. A standalone [`brave-devtools`](docs/cli.md) CLI is included too.
16
+
17
+ ## Install in one command
18
+
19
+ ### Claude Code
20
+
21
+ ```bash
22
+ claude mcp add brave-devtools --scope user -- npx -y brave-mcp@latest
23
+ ```
24
+
25
+ ### Cursor
26
+
27
+ ```bash
28
+ cursor --add-mcp '{"name":"brave-devtools","command":"npx","args":["-y","brave-mcp@latest"]}'
29
+ ```
30
+
31
+ ### Codex
32
+
33
+ ```bash
34
+ codex mcp add brave-devtools -- npx -y brave-mcp@latest
35
+ ```
36
+
37
+ ### OpenCode
38
+
39
+ ```bash
40
+ opencode mcp add brave-devtools -- npx -y brave-mcp@latest
41
+ ```
42
+
43
+ Restart your client, then try this prompt:
44
+
45
+ > Open my app in Brave. Find console errors and failed network requests, inspect the accessibility tree, run Lighthouse, and explain the highest-impact issue.
46
+
47
+ ## Why use this instead of Chrome DevTools MCP?
48
+
49
+ | Capability | `brave-mcp` | `chrome-devtools-mcp` |
50
+ | ---------------------------------------- | --------------------------------------- | ------------------------------------ |
51
+ | Browser launched and discovered natively | Brave Release, Beta, and Nightly | Chrome Stable, Beta, Dev, and Canary |
52
+ | Attach to an existing browser | Brave profiles, HTTP, or WebSocket | Chrome profiles, HTTP, or WebSocket |
53
+ | Upstream DevTools features | Full parity; currently 0 commits behind | Source implementation |
54
+ | Usage telemetry | Disabled by default | Enabled by default |
55
+ | Standalone CLI | `brave-devtools` | `chrome-devtools` |
56
+
57
+ Use [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) when your target browser is Chrome. Use `brave-mcp` when your target browser is Brave and you want Brave-native discovery, profiles, channels, naming, and privacy defaults without giving up upstream features.
58
+
59
+ ## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design principles](./docs/design-principles.md)
14
60
 
15
61
  ## Key features
16
62
 
@@ -551,7 +597,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
551
597
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
552
598
  - [`screencast_start`](docs/tool-reference.md#screencast_start)
553
599
  - [`screencast_stop`](docs/tool-reference.md#screencast_stop)
554
- - **Memory** (12 tools)
600
+ - **Memory** (13 tools)
555
601
  - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
556
602
  - [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
557
603
  - [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
@@ -564,6 +610,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
564
610
  - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
565
611
  - [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
566
612
  - [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
613
+ - [`query_heapsnapshot_objects`](docs/tool-reference.md#query_heapsnapshot_objects)
567
614
  - **Extensions** (5 tools)
568
615
  - [`install_extension`](docs/tool-reference.md#install_extension)
569
616
  - [`list_extensions`](docs/tool-reference.md#list_extensions)
@@ -656,10 +703,10 @@ The Brave DevTools MCP server supports the following configuration option:
656
703
  - **Type:** boolean
657
704
  - **Default:** `false`
658
705
 
659
- - **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
660
- Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
706
+ - **`--pageIdRouting`/ `--page-id-routing`**
707
+ Require pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions). Use --no-page-id-routing to disable.
661
708
  - **Type:** boolean
662
- - **Default:** `false`
709
+ - **Default:** `true`
663
710
 
664
711
  - **`--experimentalDevtools`/ `--experimental-devtools`**
665
712
  Whether to enable automation over DevTools targets
@@ -844,18 +891,25 @@ You can also run `npx brave-mcp@latest --help` to see all available configuratio
844
891
 
845
892
  ### Concurrent sessions
846
893
 
847
- Most MCP clients start one Brave DevTools MCP server per conversation. If your
848
- client shares a single server instance across concurrent agents or subagents,
849
- start the server with `--experimentalPageIdRouting`. This exposes `pageId` on
850
- page-scoped tools so each agent can route tool calls to the tab it is working
851
- with.
894
+ Most MCP clients start one Brave DevTools MCP server per conversation.
895
+ By default, the server runs with `--pageIdRouting` enabled, making `pageId` a
896
+ required parameter on page-scoped tools (such as `click`, `fill`, `navigate_page`,
897
+ `take_snapshot`, etc.) so multiple agents or subagents sharing a server instance can
898
+ route tool calls directly to the specific tab they are working with.
899
+
900
+ For `evaluate_script`, `pageId` is required by default for targeting pages, but
901
+ becomes optional when `--categoryExtensions` is enabled so that `serviceWorkerId`
902
+ can be specified instead to evaluate inside an extension background service worker.
903
+
904
+ To disable this behavior and default to the currently selected page, pass
905
+ `--no-page-id-routing`.
852
906
 
853
907
  ```json
854
908
  {
855
909
  "mcpServers": {
856
910
  "brave-devtools": {
857
911
  "command": "npx",
858
- "args": ["-y", "brave-mcp@latest", "--experimentalPageIdRouting"]
912
+ "args": ["-y", "brave-mcp@latest"]
859
913
  }
860
914
  }
861
915
  }
@@ -8,9 +8,9 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
10
  import { overrideDevToolsGlobals } from './devtools/DevtoolsUtils.js';
11
- import { HeapSnapshotManager } from './HeapSnapshotManager.js';
11
+ import { HeapSnapshotManager } from './processors/HeapSnapshotManager.js';
12
12
  import { McpPage } from './McpPage.js';
13
- import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
13
+ import { ServiceWorkerConsoleCollector } from './collectors/ServiceWorkerCollector.js';
14
14
  import { Locator, } from './third_party/index.js';
15
15
  import { listPages } from './tools/pages.js';
16
16
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
@@ -137,28 +137,30 @@ export class McpContext {
137
137
  }
138
138
  async validatePath(filePath) {
139
139
  if (filePath === undefined) {
140
- return;
140
+ return undefined;
141
+ }
142
+ let canonicalPath;
143
+ try {
144
+ canonicalPath = await resolveCanonicalPath(filePath);
145
+ }
146
+ catch (err) {
147
+ const errMsg = err instanceof Error ? err.message : String(err);
148
+ console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
149
+ throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
141
150
  }
142
151
  // If the client never negotiated roots and the operator has explicitly
143
152
  // opted into unrestricted access via --allow-unrestricted-paths, restore
144
153
  // the previous permissive behavior and skip validation.
145
154
  if (this.#roots === undefined && this.#allowUnrestrictedPaths) {
146
- return;
155
+ // Canonical path might not exist yet so we fallback to
156
+ // path.resolve(filePath). Consumers should not follow symlinks.
157
+ return canonicalPath || path.resolve(filePath);
147
158
  }
148
159
  // roots() always returns at least the temp directory, even if the
149
160
  // connecting client never negotiated the optional `roots` capability.
150
161
  // Path validation must not be skipped just because no workspace roots
151
162
  // were configured.
152
163
  const roots = this.roots();
153
- let canonicalPath;
154
- try {
155
- canonicalPath = await resolveCanonicalPath(filePath);
156
- }
157
- catch (err) {
158
- const errMsg = err instanceof Error ? err.message : String(err);
159
- console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
160
- throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
161
- }
162
164
  let allowed = false;
163
165
  const resolvedRoots = await Promise.allSettled(roots.map(async (root) => {
164
166
  const rootPathUri = root.uri;
@@ -186,12 +188,12 @@ export class McpContext {
186
188
  if (!allowed) {
187
189
  throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
188
190
  }
191
+ return canonicalPath || path.resolve(filePath);
189
192
  }
190
193
  async ensureExtension(filePath, extension) {
191
- const resolvedPath = path.resolve(filePath);
192
- const currentExtension = path.extname(resolvedPath);
193
- const outputPath = `${resolvedPath.slice(0, resolvedPath.length - currentExtension.length)}${extension}`;
194
- await this.validatePath(outputPath);
194
+ const resolved = await this.validatePath(filePath);
195
+ const currentExtension = path.extname(resolved);
196
+ const outputPath = `${resolved.slice(0, resolved.length - currentExtension.length)}${extension}`;
195
197
  return outputPath;
196
198
  }
197
199
  async newPage(background, isolatedContextName) {
@@ -266,6 +268,21 @@ export class McpContext {
266
268
  }
267
269
  return page;
268
270
  }
271
+ getSelectedMcpPageUrl(page) {
272
+ let targetPage = page;
273
+ if (!targetPage) {
274
+ try {
275
+ targetPage = this.getSelectedMcpPage();
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ }
281
+ if (targetPage?.pptrPage?.isClosed() === false) {
282
+ return targetPage.pptrPage.url();
283
+ }
284
+ return undefined;
285
+ }
269
286
  async getDevToolsData(page) {
270
287
  const targetPage = page ?? this.#selectedPage;
271
288
  if (!targetPage) {
@@ -441,16 +458,16 @@ export class McpContext {
441
458
  return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
442
459
  }
443
460
  async #writeFile(filepath, data) {
444
- await this.validatePath(filepath);
461
+ const resolved = await this.validatePath(filepath);
445
462
  try {
446
- await fs.mkdir(path.dirname(filepath), { recursive: true });
463
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
447
464
  // Open the file with flags to:
448
465
  // - O_WRONLY: Write-only
449
466
  // - O_CREAT: Create if it doesn't exist
450
467
  // - O_TRUNC: Truncate to zero length if it exists
451
468
  // - O_NOFOLLOW: DO NOT follow symlinks.
452
469
  // - 0o600: Permissions: read/write for owner, no permissions for others.
453
- await fs.writeFile(filepath, data, {
470
+ await fs.writeFile(resolved, data, {
454
471
  flag: fs.constants.O_WRONLY |
455
472
  fs.constants.O_CREAT |
456
473
  fs.constants.O_TRUNC |
@@ -515,6 +532,9 @@ export class McpContext {
515
532
  async getHeapSnapshotDuplicateStrings(filePath) {
516
533
  return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
517
534
  }
535
+ async queryHeapSnapshotObjects(filePath, options) {
536
+ return await this.#heapSnapshotManager.queryObjects(filePath, options);
537
+ }
518
538
  async getHeapSnapshotStats(filePath) {
519
539
  return await this.#heapSnapshotManager.getStats(filePath);
520
540
  }
@@ -524,6 +544,9 @@ export class McpContext {
524
544
  async getHeapSnapshotNativeContextSizes(filePath) {
525
545
  return await this.#heapSnapshotManager.getNativeContextSizes(filePath);
526
546
  }
547
+ async getHeapSnapshotRetainedByContextSummary(filePath) {
548
+ return await this.#heapSnapshotManager.getRetainedByContextSummary(filePath);
549
+ }
527
550
  async getHeapSnapshotNodesById(filePath, id, filterName, objectId) {
528
551
  return await this.#heapSnapshotManager.getNodesById(filePath, id, filterName, objectId);
529
552
  }
@@ -582,15 +605,15 @@ export class McpContext {
582
605
  return response.text();
583
606
  }
584
607
  case 'file:': {
585
- await this.validatePath(fileURLToPath(url));
586
- return await fs.readFile(url, 'utf-8');
608
+ const resolved = await this.validatePath(fileURLToPath(url));
609
+ return await fs.readFile(resolved, 'utf-8');
587
610
  }
588
611
  default:
589
612
  throw new Error(`Unsupported protocol for: ${url}`);
590
613
  }
591
614
  }
592
- async getHeapSnapshotEdges(filePath, nodeId) {
593
- return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
615
+ async getHeapSnapshotEdges(filePath, nodeId, options) {
616
+ return await this.#heapSnapshotManager.getEdges(filePath, nodeId, options);
594
617
  }
595
618
  async getHeapSnapshotClassDiffs(baseFilePath, currentFilePath) {
596
619
  return await this.#heapSnapshotManager.getClassDiffs(baseFilePath, currentFilePath);
@@ -102,14 +102,14 @@ export function replaceHtmlElementsWithUids(schema) {
102
102
  }
103
103
  }
104
104
  import { createTargetUniverse, } from './devtools/DevtoolsUtils.js';
105
- import { ConsoleCollector, NetworkCollector, } from './PageCollector.js';
105
+ import { ConsoleCollector, NetworkCollector, } from './collectors/PageCollector.js';
106
106
  import { TextSnapshot } from './TextSnapshot.js';
107
107
  import { PredefinedNetworkConditions, } from './third_party/index.js';
108
108
  import { takeSnapshot } from './tools/snapshot.js';
109
109
  const DEFAULT_TIMEOUT = 5_000;
110
110
  const NAVIGATION_TIMEOUT = 10_000;
111
111
  import { logger } from './utils/logger.js';
112
- import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
112
+ import { getNetworkMultiplierFromString, WaitForHelper, } from './utils/WaitForHelper.js';
113
113
  /**
114
114
  * Per-page state wrapper. Consolidates dialog, snapshot, emulation,
115
115
  * and metadata that were previously scattered across Maps in McpContext.
@@ -8,11 +8,11 @@ import { HeapSnapshotFormatter, isEdgeLike, isNodeLike, } from './formatters/Hea
8
8
  import { IssueFormatter } from './formatters/IssueFormatter.js';
9
9
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
10
10
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
11
- import { UncaughtError } from './PageCollector.js';
11
+ import { UncaughtError } from './collectors/PageCollector.js';
12
12
  import { TextSnapshot } from './TextSnapshot.js';
13
13
  import { DevTools, getToonEncode, getGcfEncode } from './third_party/index.js';
14
14
  import { handleDialog, listPages } from './tools/pages.js';
15
- import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
15
+ import { getInsightOutput, getTraceSummary, } from './processors/PerformanceTrace.js';
16
16
  import { stableIdSymbol } from './utils/id.js';
17
17
  import { paginate } from './utils/pagination.js';
18
18
  const { formatBytesToKb } = DevTools.I18n.ByteUtilities;
@@ -196,13 +196,14 @@ export class McpResponse {
196
196
  pagination: options,
197
197
  };
198
198
  }
199
- setHeapSnapshotStats(stats, staticData, nativeContextSizes) {
199
+ setHeapSnapshotStats(stats, staticData, nativeContextSizes, retainedByContextSummary) {
200
200
  this.#heapSnapshotOptions = {
201
201
  ...this.#heapSnapshotOptions,
202
202
  include: true,
203
203
  stats,
204
204
  staticData,
205
205
  nativeContextSizes,
206
+ retainedByContextSummary,
206
207
  };
207
208
  }
208
209
  setHeapSnapshotNodes(nodes, options) {
@@ -651,7 +652,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
651
652
  for (const [insightName, model] of Object.entries(insightSet.model)) {
652
653
  structuredContent.traceInsights.push({
653
654
  insightName,
654
- insightKey: model.insightKey,
655
+ insightKey: typeof model === 'object' &&
656
+ model !== null &&
657
+ 'insightKey' in model
658
+ ? model.insightKey
659
+ : undefined,
655
660
  });
656
661
  }
657
662
  }
@@ -719,6 +724,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
719
724
  structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
720
725
  structuredContent.heapSnapshot.nativeContextSizes = nativeContextSizes;
721
726
  }
727
+ const retainedByContextSummary = this.#heapSnapshotOptions.retainedByContextSummary;
728
+ if (retainedByContextSummary) {
729
+ response.push('### Retained by Context Summary');
730
+ response.push(HeapSnapshotFormatter.formatRetainedByContextSummary(retainedByContextSummary));
731
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
732
+ structuredContent.heapSnapshot.retainedByContextSummary =
733
+ retainedByContextSummary;
734
+ }
722
735
  const aggregateData = this.#heapSnapshotOptions.aggregateData;
723
736
  if (aggregateData) {
724
737
  const sortedEntries = HeapSnapshotFormatter.sort(aggregateData.aggregates);
@@ -11,6 +11,8 @@ import { zod } from './third_party/index.js';
11
11
  import { labels, OFF_BY_DEFAULT_CATEGORIES } from './tools/categories.js';
12
12
  import { pageIdSchema } from './tools/ToolDefinition.js';
13
13
  import { logger } from './utils/logger.js';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
+ import { isLocalhost } from './utils/url.js';
14
16
  export function buildFlag(category) {
15
17
  return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
16
18
  }
@@ -82,6 +84,66 @@ function buildUnknownArgumentsMessage(toolName, unknownArgumentNames, expectedAr
82
84
  const correction = unknownArgumentNames.length === 1 ? 'Remove it' : 'Remove them';
83
85
  return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`;
84
86
  }
87
+ async function validateAndResolvePathOrUrl(filePathOrUrl, context) {
88
+ try {
89
+ const url = new URL(filePathOrUrl);
90
+ if (url.protocol === 'file:') {
91
+ return pathToFileURL(await context.validatePath(fileURLToPath(url))).href;
92
+ }
93
+ else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) {
94
+ return filePathOrUrl;
95
+ }
96
+ }
97
+ catch {
98
+ // Suppress parsing errors for regular file paths.
99
+ }
100
+ return await context.validatePath(filePathOrUrl);
101
+ }
102
+ function isLocalBrowser(context) {
103
+ if (context.browser.process()) {
104
+ return true;
105
+ }
106
+ const wsEndpoint = context.browser.wsEndpoint();
107
+ if (wsEndpoint && isLocalhost(wsEndpoint)) {
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ function shouldValidateFile(option, isLocal) {
113
+ if (option === true) {
114
+ return true;
115
+ }
116
+ if (typeof option === 'object' && option !== null) {
117
+ if (isLocal) {
118
+ return Boolean(option.local);
119
+ }
120
+ return Boolean(option.remote);
121
+ }
122
+ return false;
123
+ }
124
+ async function validateToolFiles(tool, params, context) {
125
+ const isLocal = isLocalBrowser(context);
126
+ for (const [key, option] of Object.entries(tool.verifyFilesSchema)) {
127
+ if (shouldValidateFile(option, isLocal)) {
128
+ const val = params[key];
129
+ if (typeof val === 'string') {
130
+ params[key] = await validateAndResolvePathOrUrl(val, context);
131
+ }
132
+ else if (Array.isArray(val)) {
133
+ const updated = [];
134
+ for (const item of val) {
135
+ if (typeof item === 'string') {
136
+ updated.push(await validateAndResolvePathOrUrl(item, context));
137
+ }
138
+ else {
139
+ throw new Error('Unexpected non-string value as a file path or URL');
140
+ }
141
+ }
142
+ params[key] = updated;
143
+ }
144
+ }
145
+ }
146
+ }
85
147
  export class ToolHandler {
86
148
  tool;
87
149
  serverArgs;
@@ -102,7 +164,7 @@ export class ToolHandler {
102
164
  this.inputSchema =
103
165
  'pageScoped' in tool &&
104
166
  tool.pageScoped &&
105
- serverArgs.experimentalPageIdRouting &&
167
+ serverArgs.pageIdRouting &&
106
168
  !serverArgs.slim
107
169
  ? { ...pageIdSchema, ...tool.schema }
108
170
  : tool.schema;
@@ -153,19 +215,11 @@ export class ToolHandler {
153
215
  }
154
216
  let page;
155
217
  try {
156
- if (this.tool.verifyFilesSchema) {
157
- for (const key of this.tool.verifyFilesSchema) {
158
- const filePath = params[key];
159
- const paths = Array.isArray(filePath) ? filePath : [filePath];
160
- for (const path of paths) {
161
- await context.validatePath(path);
162
- }
163
- }
164
- }
218
+ await validateToolFiles(this.tool, params, context);
165
219
  if (isPageScopedTool(this.tool)) {
166
220
  const pageId = typeof params.pageId === 'number' ? params.pageId : undefined;
167
221
  page =
168
- this.serverArgs.experimentalPageIdRouting &&
222
+ this.serverArgs.pageIdRouting &&
169
223
  pageId !== undefined &&
170
224
  !this.serverArgs.slim
171
225
  ? context.getPageById(pageId)
@@ -189,10 +243,7 @@ export class ToolHandler {
189
243
  response.setError(err);
190
244
  }
191
245
  devToolsData = await context.getDevToolsData(page);
192
- const targetPage = page ?? context.getSelectedMcpPage();
193
- if (targetPage?.pptrPage?.isClosed() === false) {
194
- pageUrl = targetPage.pptrPage.url();
195
- }
246
+ pageUrl = context.getSelectedMcpPageUrl(page);
196
247
  // Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat
197
248
  let dataFormat = 'default';
198
249
  if (this.serverArgs.experimentalDataFormat) {
@@ -13,7 +13,7 @@ import { StdioServerTransport } from '../third_party/index.js';
13
13
  import { checkForUpdates } from '../utils/check-for-updates.js';
14
14
  import { logger, saveLogsToFile } from '../utils/logger.js';
15
15
  import { VERSION } from '../version.js';
16
- import { cliOptions, parseArguments } from './brave-devtools-mcp-cli-options.js';
16
+ import { mcpOptions, parseArguments } from '../config/mcp-options.js';
17
17
  await checkForUpdates('Run `npm install brave-mcp@latest` to update.');
18
18
  export const args = parseArguments(VERSION);
19
19
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
@@ -68,5 +68,5 @@ await server.connect(transport);
68
68
  logger?.('Brave DevTools MCP Server connected');
69
69
  logDisclaimers(args);
70
70
  void ClearcutLogger.get()?.logDailyActiveIfNeeded();
71
- void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, cliOptions));
71
+ void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, mcpOptions));
72
72
  //# sourceMappingURL=brave-devtools-mcp-main.js.map
@@ -12,34 +12,26 @@ import { logDisclaimers } from '../index.js';
12
12
  import { hideBin, yargs } from '../third_party/index.js';
13
13
  import { checkForUpdates } from '../utils/check-for-updates.js';
14
14
  import { VERSION } from '../version.js';
15
- import { commands } from './brave-devtools-cli-options.js';
16
- import { cliOptions, parseArguments } from './brave-devtools-mcp-cli-options.js';
15
+ import { commands } from '../config/cli-options.js';
16
+ import { mcpOptions, parseArguments, getMcpOptionsForViaCli, } from '../config/mcp-options.js';
17
17
  await checkForUpdates('Run `npm install -g brave-mcp@latest` and `brave-devtools start` to update and restart the daemon.');
18
+ const DEFAULT_CLI_ARGS = ['--viaCli'];
18
19
  async function start(args, sessionId) {
19
- const combinedArgs = [...args, ...defaultArgs];
20
+ const combinedArgs = [...DEFAULT_CLI_ARGS, ...args];
20
21
  await startDaemon(combinedArgs, sessionId);
21
22
  logDisclaimers(parseArguments(VERSION, combinedArgs));
22
23
  }
23
- const defaultArgs = ['--viaCli', '--experimentalStructuredContent'];
24
- const startCliOptions = {
25
- ...cliOptions,
26
- };
27
- // Missing CLI serialization.
28
- delete startCliOptions.viewport;
29
- // Change the defaults for the CLI.
30
- delete startCliOptions.experimentalStructuredContent;
31
- delete startCliOptions.experimentalInteropTools;
32
- delete startCliOptions.experimentalPageIdRouting;
33
- if (!('default' in cliOptions.headless)) {
34
- throw new Error('headless cli option unexpectedly does not have a default');
24
+ function getCliOptions() {
25
+ const options = {
26
+ ...getMcpOptionsForViaCli(),
27
+ };
28
+ // Missing CLI serialization.
29
+ delete options.viewport;
30
+ // Change the defaults for the CLI.
31
+ delete options.experimentalStructuredContent;
32
+ delete options.experimentalInteropTools;
33
+ return options;
35
34
  }
36
- if ('default' in cliOptions.isolated) {
37
- throw new Error('isolated cli option unexpectedly has a default');
38
- }
39
- startCliOptions.headless.default = true;
40
- startCliOptions.isolated.description =
41
- 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.';
42
- startCliOptions.categoryExtensions.default = true;
43
35
  const y = yargs(hideBin(process.argv))
44
36
  .locale('en') // Force English to ensure error string matching works in .fail, all custom messages we output are in English anyways
45
37
  .scriptName('brave-devtools')
@@ -71,9 +63,9 @@ const y = yargs(hideBin(process.argv))
71
63
  console.error('💡 TIP FOR AI AGENT / DEVELOPER:');
72
64
  console.error('In the `brave-devtools` CLI:');
73
65
  console.error('1. Required parameters MUST be passed as positional arguments (without flags).');
74
- console.error(' - INCORRECT: brave-devtools evaluate_script --expression "() => document.title"');
75
- console.error(' - CORRECT: brave-devtools evaluate_script "() => document.title"');
76
- console.error('2. Optional parameters are passed as double-dash options/flags (e.g. --pageId 1).');
66
+ console.error(' - INCORRECT: brave-devtools click --pageId 1 --uid "1_2"');
67
+ console.error(' - CORRECT: brave-devtools click 1 "1_2"');
68
+ console.error('2. Optional parameters are passed as double-dash options/flags (e.g. --dblClick true).');
77
69
  console.error('3. Make sure to escape quotes properly for your shell environment.');
78
70
  console.error('Run `brave-devtools <command> --help` to see exact positional and optional parameters.');
79
71
  console.error('=========================================');
@@ -85,7 +77,7 @@ const y = yargs(hideBin(process.argv))
85
77
  process.exit(1);
86
78
  });
87
79
  y.command('start', 'Start or restart brave-devtools-mcp', y => y
88
- .options(startCliOptions)
80
+ .options(getCliOptions())
89
81
  .example('$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser')
90
82
  .strict(), async (argv) => {
91
83
  if (isDaemonRunning(argv.sessionId)) {
@@ -106,7 +98,7 @@ y.command('start', 'Start or restart brave-devtools-mcp', y => y
106
98
  argv.headless = true;
107
99
  }
108
100
  }
109
- const args = serializeArgs(cliOptions, argv);
101
+ const args = serializeArgs(mcpOptions, argv);
110
102
  await start(args, argv.sessionId);
111
103
  process.exit(0);
112
104
  }).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself
@@ -200,7 +192,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
200
192
  ? verifyDaemonVersion(sessionId, VERSION)
201
193
  : Promise.resolve(undefined);
202
194
  if (!isDaemonRunning(sessionId)) {
203
- await start(serializeArgs(cliOptions, argv), sessionId);
195
+ await start(serializeArgs(mcpOptions, argv), sessionId);
204
196
  }
205
197
  const commandArgs = {};
206
198
  for (const argName of Object.keys(args)) {
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { puppeteer } from './third_party/index.js';
11
- import { logger } from './utils/logger.js';
11
+ import { logger, puppeteerLogger } from './utils/logger.js';
12
12
  let browser;
13
13
  let browserMode;
14
14
  // Heavy pages (e.g. Studio module dev bundles >100MB) cannot ack
@@ -161,6 +161,7 @@ export async function ensureBrowserConnected(options) {
161
161
  protocolTimeout: PROTOCOL_TIMEOUT_MILLISECONDS,
162
162
  blocklist: options.blocklist,
163
163
  allowlist: options.allowlist,
164
+ logger: puppeteerLogger,
164
165
  };
165
166
  let autoConnect = false;
166
167
  if (options.wsEndpoint) {
@@ -281,6 +282,7 @@ export async function launch(options) {
281
282
  protocolTimeout: PROTOCOL_TIMEOUT_MILLISECONDS,
282
283
  blocklist: options.blocklist,
283
284
  allowlist: options.allowlist,
285
+ logger: puppeteerLogger,
284
286
  });
285
287
  if (options.logFile) {
286
288
  // FIXME: we are probably subscribing too late to catch startup logs. We
@@ -3,10 +3,10 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { FakeIssuesManager } from './devtools/DevtoolsUtils.js';
7
- import { DevTools } from './third_party/index.js';
8
- import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
9
- import { logger } from './utils/logger.js';
6
+ import { FakeIssuesManager } from '../devtools/DevtoolsUtils.js';
7
+ import { DevTools } from '../third_party/index.js';
8
+ import { createIdGenerator, stableIdSymbol, } from '../utils/id.js';
9
+ import { logger } from '../utils/logger.js';
10
10
  export class UncaughtError {
11
11
  details;
12
12
  targetId;
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { UncaughtError } from './PageCollector.js';
7
- import { createIdGenerator, stableIdSymbol } from './utils/id.js';
7
+ import { createIdGenerator, stableIdSymbol } from '../utils/id.js';
8
8
  const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
9
9
  export class ServiceWorkerSubscriber {
10
10
  #target;