brave-mcp 0.5.0 → 1.3.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 (121) hide show
  1. package/README.md +88 -10
  2. package/build/src/HeapSnapshotManager.js +56 -18
  3. package/build/src/McpContext.js +124 -32
  4. package/build/src/McpPage.js +9 -9
  5. package/build/src/McpResponse.js +216 -68
  6. package/build/src/PageCollector.js +8 -8
  7. package/build/src/ServiceWorkerCollector.js +171 -0
  8. package/build/src/TextSnapshot.js +1 -1
  9. package/build/src/ToolHandler.js +223 -0
  10. package/build/src/WaitForHelper.js +20 -6
  11. package/build/src/bin/brave-devtools-cli-options.js +180 -36
  12. package/build/src/bin/brave-devtools-mcp-cli-options.js +67 -12
  13. package/build/src/bin/brave-devtools-mcp-main.js +45 -6
  14. package/build/src/bin/brave-devtools.js +0 -2
  15. package/build/src/bin/check-latest-version.js +25 -1
  16. package/build/src/browser.js +45 -4
  17. package/build/src/daemon/client.js +19 -13
  18. package/build/src/daemon/daemon.js +74 -17
  19. package/build/src/daemon/utils.js +2 -2
  20. package/build/src/{DevToolsConnectionAdapter.js → devtools/DevToolsConnectionAdapter.js} +1 -1
  21. package/build/src/{DevtoolsUtils.js → devtools/DevtoolsUtils.js} +93 -19
  22. package/build/src/devtools/McpHostBindingAdapter.js +165 -0
  23. package/build/src/formatters/ConsoleFormatter.js +1 -1
  24. package/build/src/formatters/HeapSnapshotFormatter.js +52 -9
  25. package/build/src/formatters/IssueFormatter.js +4 -4
  26. package/build/src/index.js +24 -165
  27. package/build/src/telemetry/ClearcutLogger.js +35 -119
  28. package/build/src/telemetry/WatchdogClient.js +4 -4
  29. package/build/src/telemetry/errors.js +18 -0
  30. package/build/src/telemetry/flagUtils.js +4 -3
  31. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  32. package/build/src/telemetry/persistence.js +21 -3
  33. package/build/src/telemetry/transformation.js +134 -0
  34. package/build/src/telemetry/types.js +0 -8
  35. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  36. package/build/src/telemetry/watchdog/main.js +5 -5
  37. package/build/src/third_party/THIRD_PARTY_NOTICES +3637 -0
  38. package/build/src/third_party/bundled-packages.json +12 -0
  39. package/build/src/third_party/devtools-formatter-worker.js +15299 -7
  40. package/build/src/third_party/devtools-heap-snapshot-worker.js +9868 -7
  41. package/build/src/third_party/index.js +159584 -27
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  87. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  88. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  89. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  90. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  91. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  92. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  93. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  94. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  95. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  96. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  97. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  98. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  99. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
  100. package/build/src/tools/ToolDefinition.js +2 -2
  101. package/build/src/tools/console.js +7 -0
  102. package/build/src/tools/emulation.js +29 -2
  103. package/build/src/tools/extensions.js +5 -0
  104. package/build/src/tools/input.js +57 -19
  105. package/build/src/tools/lighthouse.js +18 -13
  106. package/build/src/tools/memory.js +142 -21
  107. package/build/src/tools/network.js +2 -2
  108. package/build/src/tools/pages.js +13 -5
  109. package/build/src/tools/performance.js +15 -11
  110. package/build/src/tools/screencast.js +32 -10
  111. package/build/src/tools/screenshot.js +158 -76
  112. package/build/src/tools/script.js +33 -11
  113. package/build/src/tools/slim/tools.js +3 -0
  114. package/build/src/tools/snapshot.js +3 -2
  115. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  116. package/build/src/tools/webmcp.js +2 -0
  117. package/build/src/trace-processing/parse.js +7 -7
  118. package/build/src/utils/files.js +43 -0
  119. package/build/src/version.js +4 -1
  120. package/package.json +21 -17
  121. package/build/src/telemetry/metricUtils.js +0 -15
package/README.md CHANGED
@@ -38,7 +38,7 @@ You can explicitly enable them with `--usage-statistics` if desired.
38
38
 
39
39
  ## Requirements
40
40
 
41
- - [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
41
+ - [Node.js](https://nodejs.org/) [LTS](https://github.com/nodejs/Release#release-schedule) version.
42
42
  - [Brave Browser](https://brave.com/) current release version or newer.
43
43
  - [npm](https://www.npmjs.com/)
44
44
 
@@ -279,7 +279,7 @@ To install Chrome DevTools MCP with skills, add the marketplace registry in Clau
279
279
  Then, install the plugin:
280
280
 
281
281
  ```sh
282
- /plugin install chrome-devtools-mcp
282
+ /plugin install chrome-devtools-mcp@chrome-devtools-plugins
283
283
  ```
284
284
 
285
285
  Restart Claude Code to have the MCP server and skills load (check with `/skills`).
@@ -367,7 +367,7 @@ and the expert guidance it needs to use them effectively.
367
367
 
368
368
  1. Open the **Command Palette** (`Cmd+Shift+P` on macOS or `Ctrl+Shift+P` on Windows/Linux).
369
369
  2. Search for and run the **Chat: Install Plugin From Source** command.
370
- 3. Paste in our repository URL: `https://github.com/ChromeDevTools/chrome-devtools-mcp`
370
+ 3. Paste in our repository name: `ChromeDevTools/chrome-devtools-mcp`.
371
371
 
372
372
  That's it! Your agent is now supercharged with Chrome DevTools capabilities.
373
373
 
@@ -632,11 +632,16 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
632
632
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
633
633
  - [`screencast_start`](docs/tool-reference.md#screencast_start)
634
634
  - [`screencast_stop`](docs/tool-reference.md#screencast_stop)
635
- - **Memory** (4 tools)
636
- - [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
637
- - [`get_memory_snapshot_details`](docs/tool-reference.md#get_memory_snapshot_details)
638
- - [`get_nodes_by_class`](docs/tool-reference.md#get_nodes_by_class)
639
- - [`load_memory_snapshot`](docs/tool-reference.md#load_memory_snapshot)
635
+ - **Memory** (9 tools)
636
+ - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
637
+ - [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
638
+ - [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
639
+ - [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
640
+ - [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
641
+ - [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
642
+ - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
643
+ - [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
644
+ - [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
640
645
  - **Extensions** (5 tools)
641
646
  - [`install_extension`](docs/tool-reference.md#install_extension)
642
647
  - [`list_extensions`](docs/tool-reference.md#list_extensions)
@@ -713,10 +718,30 @@ The Brave DevTools MCP server supports the following configuration options:
713
718
  If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
714
719
  - **Type:** boolean
715
720
 
721
+ - **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
722
+ Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
723
+ - **Type:** boolean
724
+
725
+ - **`--experimentalDevtools`/ `--experimental-devtools`**
726
+ Whether to enable automation over DevTools targets
727
+ - **Type:** boolean
728
+
716
729
  - **`--experimentalVision`/ `--experimental-vision`**
717
730
  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.
718
731
  - **Type:** boolean
719
732
 
733
+ - **`--memoryDebugging`/ `--memory-debugging`, `-experimentalMemory`**
734
+ Whether to enable memory debugging tools.
735
+ - **Type:** boolean
736
+
737
+ - **`--experimentalStructuredContent`/ `--experimental-structured-content`**
738
+ Whether to output structured formatted content.
739
+ - **Type:** boolean
740
+
741
+ - **`--experimentalIncludeAllPages`/ `--experimental-include-all-pages`**
742
+ Whether to include all kinds of pages such as webviews or background pages as pages.
743
+ - **Type:** boolean
744
+
720
745
  - **`--experimentalScreencast`/ `--experimental-screencast`**
721
746
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
722
747
  - **Type:** boolean
@@ -726,13 +751,21 @@ The Brave DevTools MCP server supports the following configuration options:
726
751
  - **Type:** string
727
752
 
728
753
  - **`--categoryExperimentalWebmcp`/ `--category-experimental-webmcp`**
729
- Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
754
+ Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCP,DevToolsWebMCPSupport`
730
755
  - **Type:** boolean
731
756
 
732
757
  - **`--braveArg`/ `--brave-arg`**
733
758
  Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
734
759
  - **Type:** array
735
760
 
761
+ - **`--blockedUrlPattern`/ `--blocked-url-pattern`**
762
+ Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
763
+ - **Type:** array
764
+
765
+ - **`--allowedUrlPattern`/ `--allowed-url-pattern`**
766
+ Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.
767
+ - **Type:** array
768
+
736
769
  - **`--ignoreDefaultBraveArg`/ `--ignore-default-brave-arg`**
737
770
  Explicitly disable default arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
738
771
  - **Type:** array
@@ -772,12 +805,29 @@ The Brave DevTools MCP server supports the following configuration options:
772
805
  - **Type:** boolean
773
806
  - **Default:** `false`
774
807
 
808
+ - **`--screenshotFormat`/ `--screenshot-format`**
809
+ Override the default output format used by take_screenshot when the caller does not specify one. JPEG and WebP are ~3-5x smaller than PNG, which helps reduce context size in AI conversations. Unset preserves the existing default ("png").
810
+ - **Type:** string
811
+ - **Choices:** `jpeg`, `png`, `webp`
812
+
813
+ - **`--screenshotQuality`/ `--screenshot-quality`**
814
+ Override the default compression quality (0-100) used by take_screenshot for JPEG and WebP when the caller does not specify one. Lower values mean smaller files. Ignored for PNG. Unset preserves the Puppeteer default.
815
+ - **Type:** number
816
+
817
+ - **`--screenshotMaxWidth`/ `--screenshot-max-width`**
818
+ Maximum width in pixels for screenshots. If the captured image is wider, it is downscaled (preserving aspect ratio) before being returned. Reduces context size in AI conversations. Unset means no resize.
819
+ - **Type:** number
820
+
821
+ - **`--screenshotMaxHeight`/ `--screenshot-max-height`**
822
+ Maximum height in pixels for screenshots. If the captured image is taller, it is downscaled (preserving aspect ratio) before being returned. Can be combined with --screenshot-max-width; the smaller scale factor wins. Unset means no resize.
823
+ - **Type:** number
824
+
775
825
  - **`--slim`**
776
826
  Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
777
827
  - **Type:** boolean
778
828
 
779
829
  - **`--redactNetworkHeaders`/ `--redact-network-headers`**
780
- If true, redacts some of the network headers considered senstive before returning to the client.
830
+ If true, redacts some of the network headers considered sensitive before returning to the client.
781
831
  - **Type:** boolean
782
832
  - **Default:** `false`
783
833
 
@@ -826,6 +876,34 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co
826
876
 
827
877
  ## Concepts
828
878
 
879
+ ### Concurrent sessions
880
+
881
+ Most MCP clients start one Chrome DevTools MCP server per conversation. If your
882
+ client shares a single server instance across concurrent agents or subagents,
883
+ start the server with `--experimentalPageIdRouting`. This exposes `pageId` on
884
+ page-scoped tools so each agent can route tool calls to the tab it is working
885
+ with.
886
+
887
+ ```json
888
+ {
889
+ "mcpServers": {
890
+ "chrome-devtools": {
891
+ "command": "npx",
892
+ "args": [
893
+ "-y",
894
+ "chrome-devtools-mcp@latest",
895
+ "--experimentalPageIdRouting"
896
+ ]
897
+ }
898
+ }
899
+ }
900
+ ```
901
+
902
+ If you run multiple independent MCP client sessions and want each session to
903
+ launch its own temporary Chrome profile, also pass `--isolated`. This avoids
904
+ sharing the default Chrome DevTools MCP user data directory between those
905
+ server instances.
906
+
829
907
  ### User data directory
830
908
 
831
909
  `brave-devtools-mcp` starts a Brave release channel instance using the following user
@@ -19,8 +19,8 @@ export class HeapSnapshotManager {
19
19
  this.#snapshots.set(absolutePath, {
20
20
  snapshot,
21
21
  worker,
22
- uidToClassKey: new Map(),
23
- classKeyToUid: new Map(),
22
+ idToClassKey: new Map(),
23
+ classKeyToId: new Map(),
24
24
  idGenerator: createIdGenerator(),
25
25
  });
26
26
  return snapshot;
@@ -30,10 +30,10 @@ export class HeapSnapshotManager {
30
30
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
31
31
  const aggregates = await snapshot.aggregatesWithFilter(filter);
32
32
  for (const key of Object.keys(aggregates)) {
33
- const uid = await this.getOrCreateUidForClassKey(filePath, key);
33
+ const id = await this.getOrCreateIdForClassKey(filePath, key);
34
34
  const aggregate = aggregates[key];
35
35
  if (aggregate) {
36
- aggregate[stableIdSymbol] = uid;
36
+ aggregate[stableIdSymbol] = id;
37
37
  }
38
38
  }
39
39
  return aggregates;
@@ -46,26 +46,59 @@ export class HeapSnapshotManager {
46
46
  const snapshot = await this.getSnapshot(filePath);
47
47
  return snapshot.staticData;
48
48
  }
49
- async getOrCreateUidForClassKey(filePath, classKey) {
49
+ async getOrCreateIdForClassKey(filePath, classKey) {
50
50
  const cached = this.#getCachedSnapshot(filePath);
51
- let uid = cached.classKeyToUid.get(classKey);
52
- if (!uid) {
53
- uid = cached.idGenerator();
54
- cached.classKeyToUid.set(classKey, uid);
55
- cached.uidToClassKey.set(uid, classKey);
51
+ let id = cached.classKeyToId.get(classKey);
52
+ if (!id) {
53
+ id = cached.idGenerator();
54
+ cached.classKeyToId.set(classKey, id);
55
+ cached.idToClassKey.set(id, classKey);
56
56
  }
57
- return uid;
57
+ return id;
58
58
  }
59
- async getNodesByUid(filePath, uid) {
59
+ async getNodesById(filePath, id) {
60
60
  const snapshot = await this.getSnapshot(filePath);
61
61
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
62
- const className = await this.resolveClassKeyFromUid(filePath, uid);
62
+ const className = await this.resolveClassKeyFromId(filePath, id);
63
63
  if (!className) {
64
- throw new Error(`Class with UID ${uid} not found in heap snapshot`);
64
+ throw new Error(`Class with ID ${id} not found in heap snapshot`);
65
65
  }
66
66
  const provider = snapshot.createNodesProviderForClass(className, filter);
67
- const range = await provider.serializeItemsRange(0, 1);
68
- return await provider.serializeItemsRange(0, range.totalLength);
67
+ return await provider.serializeItemsRange(0, Infinity);
68
+ }
69
+ async getRetainers(filePath, nodeId) {
70
+ const snapshot = await this.getSnapshot(filePath);
71
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
72
+ if (nodeIndex === undefined) {
73
+ throw new Error(`Node with ID ${nodeId} not found`);
74
+ }
75
+ const provider = snapshot.createRetainingEdgesProvider(nodeIndex);
76
+ return await provider.serializeItemsRange(0, Infinity);
77
+ }
78
+ async getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
79
+ const snapshot = await this.getSnapshot(filePath);
80
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
81
+ if (nodeIndex === undefined) {
82
+ throw new Error(`Node with ID ${nodeId} not found`);
83
+ }
84
+ return await snapshot.getRetainingPaths(nodeIndex, maxDepth, maxNodes, maxSiblings);
85
+ }
86
+ async getDominatorsOf(filePath, nodeId) {
87
+ const snapshot = await this.getSnapshot(filePath);
88
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
89
+ if (nodeIndex === undefined) {
90
+ throw new Error(`Node with ID ${nodeId} not found`);
91
+ }
92
+ return await snapshot.getDominatorsOf(nodeIndex);
93
+ }
94
+ async getEdges(filePath, nodeId) {
95
+ const snapshot = await this.getSnapshot(filePath);
96
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
97
+ if (nodeIndex === undefined) {
98
+ throw new Error(`Node with ID ${nodeId} not found`);
99
+ }
100
+ const provider = snapshot.createEdgesProvider(nodeIndex);
101
+ return await provider.serializeItemsRange(0, Infinity);
69
102
  }
70
103
  #getCachedSnapshot(filePath) {
71
104
  const absolutePath = path.resolve(filePath);
@@ -75,9 +108,9 @@ export class HeapSnapshotManager {
75
108
  }
76
109
  return cached;
77
110
  }
78
- async resolveClassKeyFromUid(filePath, uid) {
111
+ async resolveClassKeyFromId(filePath, id) {
79
112
  const cached = this.#getCachedSnapshot(filePath);
80
- return cached.uidToClassKey.get(uid);
113
+ return cached.idToClassKey.get(id);
81
114
  }
82
115
  async #loadSnapshot(absolutePath) {
83
116
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
@@ -98,13 +131,18 @@ export class HeapSnapshotManager {
98
131
  const snapshot = await snapshotPromise;
99
132
  return { snapshot, worker: workerProxy };
100
133
  }
134
+ hasSnapshots() {
135
+ return this.#snapshots.size > 0;
136
+ }
101
137
  dispose(filePath) {
102
138
  const absolutePath = path.resolve(filePath);
103
139
  const cached = this.#snapshots.get(absolutePath);
104
140
  if (cached) {
105
141
  cached.worker.dispose();
106
142
  this.#snapshots.delete(absolutePath);
143
+ return true;
107
144
  }
145
+ return false;
108
146
  }
109
147
  }
110
148
  //# sourceMappingURL=HeapSnapshotManager.js.map
@@ -4,17 +4,19 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import fs from 'node:fs/promises';
7
+ import fsPromises from 'node:fs/promises';
7
8
  import os from 'node:os';
8
9
  import path from 'node:path';
9
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
- import { UniverseManager } from './DevtoolsUtils.js';
11
+ import { overrideDevToolsGlobals, UniverseManager, } from './devtools/DevtoolsUtils.js';
11
12
  import { HeapSnapshotManager } from './HeapSnapshotManager.js';
12
13
  import { McpPage } from './McpPage.js';
13
14
  import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
15
+ import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
14
16
  import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
15
17
  import { listPages } from './tools/pages.js';
16
18
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
17
- import { ensureExtension, getTempFilePath } from './utils/files.js';
19
+ import { ensureExtension, getTempFilePath, resolveCanonicalPath, } from './utils/files.js';
18
20
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
19
21
  const DEFAULT_TIMEOUT = 5_000;
20
22
  const NAVIGATION_TIMEOUT = 10_000;
@@ -32,6 +34,7 @@ export class McpContext {
32
34
  #networkCollector;
33
35
  #consoleCollector;
34
36
  #devtoolsUniverseManager;
37
+ #serviceWorkerConsoleCollector;
35
38
  #isRunningTrace = false;
36
39
  #screenRecorderData = null;
37
40
  #nextPageId = 1;
@@ -44,6 +47,11 @@ export class McpContext {
44
47
  #heapSnapshotManager = new HeapSnapshotManager();
45
48
  #roots = undefined;
46
49
  constructor(browser, logger, options, locatorClass) {
50
+ overrideDevToolsGlobals({
51
+ loadResource: (url) => {
52
+ return this.loadResource(url);
53
+ },
54
+ });
47
55
  this.browser = browser;
48
56
  this.logger = logger;
49
57
  this.#locatorClass = locatorClass;
@@ -62,19 +70,22 @@ export class McpContext {
62
70
  },
63
71
  };
64
72
  });
73
+ this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(this.browser);
65
74
  this.#devtoolsUniverseManager = new UniverseManager(this.browser);
66
75
  }
67
76
  async #init() {
68
77
  const pages = await this.createPagesSnapshot();
69
- await this.createExtensionServiceWorkersSnapshot();
78
+ const workers = await this.createExtensionServiceWorkersSnapshot();
70
79
  await this.#networkCollector.init(pages);
71
80
  await this.#consoleCollector.init(pages);
72
81
  await this.#devtoolsUniverseManager.init(pages);
82
+ await this.#serviceWorkerConsoleCollector.init(workers);
73
83
  }
74
84
  dispose() {
75
85
  this.#networkCollector.dispose();
76
86
  this.#consoleCollector.dispose();
77
87
  this.#devtoolsUniverseManager.dispose();
88
+ this.#serviceWorkerConsoleCollector.dispose();
78
89
  for (const mcpPage of this.#mcpPages.values()) {
79
90
  mcpPage.dispose();
80
91
  }
@@ -106,7 +117,7 @@ export class McpContext {
106
117
  setRoots(roots) {
107
118
  this.#roots = roots;
108
119
  }
109
- validatePath(filePath) {
120
+ async validatePath(filePath) {
110
121
  if (filePath === undefined) {
111
122
  return;
112
123
  }
@@ -114,19 +125,40 @@ export class McpContext {
114
125
  if (roots === undefined) {
115
126
  return;
116
127
  }
117
- const absolutePath = path.resolve(filePath);
128
+ let canonicalPath;
129
+ try {
130
+ canonicalPath = await resolveCanonicalPath(filePath);
131
+ }
132
+ catch (err) {
133
+ const errMsg = err instanceof Error ? err.message : String(err);
134
+ console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
135
+ throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
136
+ }
137
+ let allowed = false;
118
138
  for (const root of roots) {
119
- const rootPath = path.resolve(fileURLToPath(root.uri));
120
- if (absolutePath === rootPath ||
121
- absolutePath.startsWith(rootPath + path.sep)) {
122
- return;
139
+ try {
140
+ const rootPathUri = root.uri;
141
+ const rootPath = path.resolve(fileURLToPath(rootPathUri));
142
+ const canonicalRoot = await fsPromises.realpath(rootPath);
143
+ if (canonicalPath === canonicalRoot ||
144
+ canonicalPath.startsWith(canonicalRoot + path.sep)) {
145
+ allowed = true;
146
+ break;
147
+ }
148
+ }
149
+ catch (rootErr) {
150
+ const errMsg = rootErr instanceof Error ? rootErr.message : String(rootErr);
151
+ console.warn(`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`);
152
+ // Skip this root if it cannot be resolved.
123
153
  }
124
154
  }
125
- throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
155
+ if (!allowed) {
156
+ throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
157
+ }
126
158
  }
127
159
  resolveCdpRequestId(page, cdpRequestId) {
128
160
  if (!cdpRequestId) {
129
- this.logger('no network request');
161
+ this.logger?.('no network request');
130
162
  return;
131
163
  }
132
164
  const request = this.#networkCollector.find(page.pptrPage, request => {
@@ -134,7 +166,7 @@ export class McpContext {
134
166
  return request.id === cdpRequestId;
135
167
  });
136
168
  if (!request) {
137
- this.logger('no network request for ' + cdpRequestId);
169
+ this.logger?.('no network request for ' + cdpRequestId);
138
170
  return;
139
171
  }
140
172
  return this.#networkCollector.getIdForResource(request);
@@ -195,7 +227,13 @@ export class McpContext {
195
227
  const page = targetPage ?? this.getSelectedPptrPage();
196
228
  const mcpPage = this.#getMcpPage(page);
197
229
  const newSettings = { ...mcpPage.emulationSettings };
198
- if (!options.networkConditions) {
230
+ // Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
231
+ if (this.#options.hasNetworkBlockOrAllowlist) {
232
+ if (options.networkConditions !== undefined) {
233
+ throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
234
+ }
235
+ }
236
+ else if (!options.networkConditions) {
199
237
  await page.emulateNetworkConditions(null);
200
238
  delete newSettings.networkConditions;
201
239
  }
@@ -213,12 +251,23 @@ export class McpContext {
213
251
  await page.emulateNetworkConditions(networkCondition);
214
252
  newSettings.networkConditions = options.networkConditions;
215
253
  }
254
+ const secondarySession = this.getDevToolsUniverse(mcpPage)?.session;
216
255
  if (!options.cpuThrottlingRate) {
217
256
  await page.emulateCPUThrottling(1);
257
+ if (secondarySession) {
258
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
259
+ rate: 1,
260
+ });
261
+ }
218
262
  delete newSettings.cpuThrottlingRate;
219
263
  }
220
264
  else {
221
265
  await page.emulateCPUThrottling(options.cpuThrottlingRate);
266
+ if (secondarySession) {
267
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
268
+ rate: options.cpuThrottlingRate,
269
+ });
270
+ }
222
271
  newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
223
272
  }
224
273
  if (!options.geolocation) {
@@ -250,7 +299,6 @@ export class McpContext {
250
299
  newSettings.colorScheme = options.colorScheme;
251
300
  }
252
301
  if (!options.viewport) {
253
- await page.setViewport(null);
254
302
  delete newSettings.viewport;
255
303
  }
256
304
  else {
@@ -260,14 +308,22 @@ export class McpContext {
260
308
  hasTouch: false,
261
309
  isLandscape: false,
262
310
  };
263
- const viewport = { ...defaults, ...options.viewport };
264
- await page.setViewport(viewport);
265
- newSettings.viewport = viewport;
311
+ newSettings.viewport = { ...defaults, ...options.viewport };
312
+ }
313
+ if (options.extraHttpHeaders !== undefined) {
314
+ await page.setExtraHTTPHeaders(options.extraHttpHeaders);
315
+ newSettings.extraHttpHeaders = options.extraHttpHeaders;
316
+ if (Object.keys(options.extraHttpHeaders).length === 0) {
317
+ delete newSettings.extraHttpHeaders;
318
+ }
266
319
  }
267
320
  mcpPage.emulationSettings = Object.keys(newSettings).length
268
321
  ? newSettings
269
322
  : {};
270
323
  this.#updateSelectedPageTimeouts();
324
+ // This should happen after updating the page timeouts.
325
+ // Setting the viewport can trigger a reload which we don't want to timeout.
326
+ await page.setViewport(newSettings.viewport ?? null);
271
327
  }
272
328
  setIsRunningPerformanceTrace(x) {
273
329
  this.#isRunningTrace = x;
@@ -333,9 +389,9 @@ export class McpContext {
333
389
  page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
334
390
  // 10sec should be enough for the load event to be emitted during
335
391
  // navigations.
336
- // Increased in case we throttle the network requests
392
+ // Increased in case we throttle the network requests or the CPU
337
393
  const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
338
- page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
394
+ page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier);
339
395
  }
340
396
  // Linear scan over per-page snapshots. The page count is small (typically
341
397
  // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
@@ -372,6 +428,9 @@ export class McpContext {
372
428
  });
373
429
  return this.#extensionServiceWorkers;
374
430
  }
431
+ getServiceWorkerConsoleData(extensionId) {
432
+ return this.#serviceWorkerConsoleCollector.getData(extensionId);
433
+ }
375
434
  async createPagesSnapshot() {
376
435
  const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
377
436
  for (const page of allPages) {
@@ -381,7 +440,7 @@ export class McpContext {
381
440
  this.#mcpPages.set(page, mcpPage);
382
441
  // We emulate a focused page for all pages to support multi-agent workflows.
383
442
  void page.emulateFocusedPage(true).catch(error => {
384
- this.logger('Error turning on focused page emulation', error);
443
+ this.logger?.('Error turning on focused page emulation', error);
385
444
  });
386
445
  }
387
446
  mcpPage.isolatedContextName = isolatedContextNames.get(page);
@@ -427,7 +486,7 @@ export class McpContext {
427
486
  this.#extensionPages.set(target, page);
428
487
  }
429
488
  catch (e) {
430
- this.logger('Failed to get page for extension target', e);
489
+ this.logger?.('Failed to get page for extension target', e);
431
490
  }
432
491
  }
433
492
  }
@@ -462,7 +521,7 @@ export class McpContext {
462
521
  return { pages: allPages, isolatedContextNames };
463
522
  }
464
523
  async detectOpenDevToolsWindows() {
465
- this.logger('Detecting open DevTools windows');
524
+ this.logger?.('Detecting open DevTools windows');
466
525
  const { pages } = await this.#getAllPages();
467
526
  await Promise.all(pages.map(async (page) => {
468
527
  const mcpPage = this.#mcpPages.get(page);
@@ -499,7 +558,7 @@ export class McpContext {
499
558
  }
500
559
  async saveTemporaryFile(data, filename) {
501
560
  const filepath = await getTempFilePath(filename);
502
- this.validatePath(filepath);
561
+ await this.validatePath(filepath);
503
562
  try {
504
563
  await fs.writeFile(filepath, data);
505
564
  }
@@ -509,7 +568,7 @@ export class McpContext {
509
568
  return { filepath };
510
569
  }
511
570
  async saveFile(data, clientProvidedFilePath, extension) {
512
- this.validatePath(clientProvidedFilePath);
571
+ await this.validatePath(clientProvidedFilePath);
513
572
  try {
514
573
  const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
515
574
  await fs.mkdir(path.dirname(filePath), { recursive: true });
@@ -517,7 +576,7 @@ export class McpContext {
517
576
  return { filename: filePath };
518
577
  }
519
578
  catch (err) {
520
- this.logger(err);
579
+ this.logger?.(err);
521
580
  throw new Error('Could not save a file', { cause: err });
522
581
  }
523
582
  }
@@ -562,7 +621,6 @@ export class McpContext {
562
621
  await this.#networkCollector.init(pages);
563
622
  }
564
623
  async installExtension(extensionPath) {
565
- this.validatePath(extensionPath);
566
624
  const id = await this.browser.installExtension(extensionPath);
567
625
  return id;
568
626
  }
@@ -586,20 +644,54 @@ export class McpContext {
586
644
  return pptrExtensions.get(id);
587
645
  }
588
646
  async getHeapSnapshotAggregates(filePath) {
589
- this.validatePath(filePath);
590
647
  return await this.#heapSnapshotManager.getAggregates(filePath);
591
648
  }
592
649
  async getHeapSnapshotStats(filePath) {
593
- this.validatePath(filePath);
594
650
  return await this.#heapSnapshotManager.getStats(filePath);
595
651
  }
596
652
  async getHeapSnapshotStaticData(filePath) {
597
- this.validatePath(filePath);
598
653
  return await this.#heapSnapshotManager.getStaticData(filePath);
599
654
  }
600
- async getHeapSnapshotNodesByUid(filePath, uid) {
601
- this.validatePath(filePath);
602
- return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
655
+ async getHeapSnapshotNodesById(filePath, id) {
656
+ return await this.#heapSnapshotManager.getNodesById(filePath, id);
657
+ }
658
+ async getHeapSnapshotRetainers(filePath, nodeId) {
659
+ return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
660
+ }
661
+ async closeHeapSnapshot(filePath) {
662
+ return this.#heapSnapshotManager.dispose(filePath);
663
+ }
664
+ hasHeapSnapshots() {
665
+ return this.#heapSnapshotManager.hasSnapshots();
666
+ }
667
+ async getHeapSnapshotRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
668
+ return await this.#heapSnapshotManager.getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings);
669
+ }
670
+ async getHeapSnapshotDominators(filePath, nodeId) {
671
+ return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
672
+ }
673
+ async loadResource(path) {
674
+ const url = new URL(path);
675
+ switch (url.protocol) {
676
+ case 'https:':
677
+ case 'http:': {
678
+ // TODO: Verify allow/block list
679
+ const response = await fetch(url);
680
+ if (!response.ok) {
681
+ throw new Error(`Failed to load resource: ${url}`);
682
+ }
683
+ return response.text();
684
+ }
685
+ case 'file:': {
686
+ await this.validatePath(fileURLToPath(url));
687
+ return await fsPromises.readFile(url, 'utf-8');
688
+ }
689
+ default:
690
+ throw new Error(`Unsupported protocol for: ${url}`);
691
+ }
692
+ }
693
+ async getHeapSnapshotEdges(filePath, nodeId) {
694
+ return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
603
695
  }
604
696
  }
605
697
  //# sourceMappingURL=McpContext.js.map