brave-mcp 0.5.0 → 1.2.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 (120) hide show
  1. package/README.md +68 -9
  2. package/build/src/HeapSnapshotManager.js +55 -18
  3. package/build/src/McpContext.js +94 -32
  4. package/build/src/McpPage.js +9 -9
  5. package/build/src/McpResponse.js +167 -63
  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 +136 -40
  12. package/build/src/bin/brave-devtools-mcp-cli-options.js +22 -11
  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} +94 -19
  22. package/build/src/formatters/ConsoleFormatter.js +1 -1
  23. package/build/src/formatters/HeapSnapshotFormatter.js +44 -9
  24. package/build/src/formatters/IssueFormatter.js +4 -4
  25. package/build/src/index.js +24 -165
  26. package/build/src/telemetry/ClearcutLogger.js +35 -119
  27. package/build/src/telemetry/WatchdogClient.js +4 -4
  28. package/build/src/telemetry/errors.js +18 -0
  29. package/build/src/telemetry/flagUtils.js +4 -3
  30. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +3 -3
  31. package/build/src/telemetry/persistence.js +21 -3
  32. package/build/src/telemetry/transformation.js +134 -0
  33. package/build/src/telemetry/types.js +0 -8
  34. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  35. package/build/src/telemetry/watchdog/main.js +5 -5
  36. package/build/src/third_party/THIRD_PARTY_NOTICES +3637 -0
  37. package/build/src/third_party/bundled-packages.json +12 -0
  38. package/build/src/third_party/devtools-formatter-worker.js +15300 -7
  39. package/build/src/third_party/devtools-heap-snapshot-worker.js +9817 -7
  40. package/build/src/third_party/index.js +159331 -27
  41. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  87. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  88. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  89. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  90. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  91. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  92. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  93. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  94. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  95. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  96. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  97. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  98. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +4236 -4219
  99. package/build/src/tools/ToolDefinition.js +2 -2
  100. package/build/src/tools/console.js +7 -0
  101. package/build/src/tools/emulation.js +29 -2
  102. package/build/src/tools/extensions.js +5 -0
  103. package/build/src/tools/input.js +57 -19
  104. package/build/src/tools/lighthouse.js +18 -13
  105. package/build/src/tools/memory.js +97 -21
  106. package/build/src/tools/network.js +2 -2
  107. package/build/src/tools/pages.js +13 -5
  108. package/build/src/tools/performance.js +15 -11
  109. package/build/src/tools/screencast.js +2 -1
  110. package/build/src/tools/screenshot.js +1 -1
  111. package/build/src/tools/script.js +33 -11
  112. package/build/src/tools/slim/tools.js +3 -0
  113. package/build/src/tools/snapshot.js +3 -2
  114. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  115. package/build/src/tools/webmcp.js +2 -0
  116. package/build/src/trace-processing/parse.js +7 -7
  117. package/build/src/utils/files.js +43 -0
  118. package/build/src/version.js +4 -1
  119. package/package.json +21 -17
  120. 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,14 @@ 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** (7 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_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
641
+ - [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
642
+ - [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
640
643
  - **Extensions** (5 tools)
641
644
  - [`install_extension`](docs/tool-reference.md#install_extension)
642
645
  - [`list_extensions`](docs/tool-reference.md#list_extensions)
@@ -713,10 +716,30 @@ The Brave DevTools MCP server supports the following configuration options:
713
716
  If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
714
717
  - **Type:** boolean
715
718
 
719
+ - **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
720
+ Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
721
+ - **Type:** boolean
722
+
723
+ - **`--experimentalDevtools`/ `--experimental-devtools`**
724
+ Whether to enable automation over DevTools targets
725
+ - **Type:** boolean
726
+
716
727
  - **`--experimentalVision`/ `--experimental-vision`**
717
728
  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
729
  - **Type:** boolean
719
730
 
731
+ - **`--memoryDebugging`/ `--memory-debugging`, `-experimentalMemory`**
732
+ Whether to enable memory debugging tools.
733
+ - **Type:** boolean
734
+
735
+ - **`--experimentalStructuredContent`/ `--experimental-structured-content`**
736
+ Whether to output structured formatted content.
737
+ - **Type:** boolean
738
+
739
+ - **`--experimentalIncludeAllPages`/ `--experimental-include-all-pages`**
740
+ Whether to include all kinds of pages such as webviews or background pages as pages.
741
+ - **Type:** boolean
742
+
720
743
  - **`--experimentalScreencast`/ `--experimental-screencast`**
721
744
  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
745
  - **Type:** boolean
@@ -733,6 +756,14 @@ The Brave DevTools MCP server supports the following configuration options:
733
756
  Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
734
757
  - **Type:** array
735
758
 
759
+ - **`--blockedUrlPattern`/ `--blocked-url-pattern`**
760
+ 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.
761
+ - **Type:** array
762
+
763
+ - **`--allowedUrlPattern`/ `--allowed-url-pattern`**
764
+ 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.
765
+ - **Type:** array
766
+
736
767
  - **`--ignoreDefaultBraveArg`/ `--ignore-default-brave-arg`**
737
768
  Explicitly disable default arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
738
769
  - **Type:** array
@@ -777,7 +808,7 @@ The Brave DevTools MCP server supports the following configuration options:
777
808
  - **Type:** boolean
778
809
 
779
810
  - **`--redactNetworkHeaders`/ `--redact-network-headers`**
780
- If true, redacts some of the network headers considered senstive before returning to the client.
811
+ If true, redacts some of the network headers considered sensitive before returning to the client.
781
812
  - **Type:** boolean
782
813
  - **Default:** `false`
783
814
 
@@ -826,6 +857,34 @@ You can also run `npx chrome-devtools-mcp@latest --help` to see all available co
826
857
 
827
858
  ## Concepts
828
859
 
860
+ ### Concurrent sessions
861
+
862
+ Most MCP clients start one Chrome DevTools MCP server per conversation. If your
863
+ client shares a single server instance across concurrent agents or subagents,
864
+ start the server with `--experimentalPageIdRouting`. This exposes `pageId` on
865
+ page-scoped tools so each agent can route tool calls to the tab it is working
866
+ with.
867
+
868
+ ```json
869
+ {
870
+ "mcpServers": {
871
+ "chrome-devtools": {
872
+ "command": "npx",
873
+ "args": [
874
+ "-y",
875
+ "chrome-devtools-mcp@latest",
876
+ "--experimentalPageIdRouting"
877
+ ]
878
+ }
879
+ }
880
+ }
881
+ ```
882
+
883
+ If you run multiple independent MCP client sessions and want each session to
884
+ launch its own temporary Chrome profile, also pass `--isolated`. This avoids
885
+ sharing the default Chrome DevTools MCP user data directory between those
886
+ server instances.
887
+
829
888
  ### User data directory
830
889
 
831
890
  `brave-devtools-mcp` starts a Brave release channel instance using the following user
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import fsSync from 'node:fs';
7
7
  import path from 'node:path';
8
+ import { isNodeLike } from './formatters/HeapSnapshotFormatter.js';
8
9
  import { DevTools } from './third_party/index.js';
9
10
  import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
10
11
  export class HeapSnapshotManager {
@@ -19,8 +20,8 @@ export class HeapSnapshotManager {
19
20
  this.#snapshots.set(absolutePath, {
20
21
  snapshot,
21
22
  worker,
22
- uidToClassKey: new Map(),
23
- classKeyToUid: new Map(),
23
+ idToClassKey: new Map(),
24
+ classKeyToId: new Map(),
24
25
  idGenerator: createIdGenerator(),
25
26
  });
26
27
  return snapshot;
@@ -30,10 +31,10 @@ export class HeapSnapshotManager {
30
31
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
31
32
  const aggregates = await snapshot.aggregatesWithFilter(filter);
32
33
  for (const key of Object.keys(aggregates)) {
33
- const uid = await this.getOrCreateUidForClassKey(filePath, key);
34
+ const id = await this.getOrCreateIdForClassKey(filePath, key);
34
35
  const aggregate = aggregates[key];
35
36
  if (aggregate) {
36
- aggregate[stableIdSymbol] = uid;
37
+ aggregate[stableIdSymbol] = id;
37
38
  }
38
39
  }
39
40
  return aggregates;
@@ -46,26 +47,57 @@ export class HeapSnapshotManager {
46
47
  const snapshot = await this.getSnapshot(filePath);
47
48
  return snapshot.staticData;
48
49
  }
49
- async getOrCreateUidForClassKey(filePath, classKey) {
50
+ async getOrCreateIdForClassKey(filePath, classKey) {
50
51
  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);
52
+ let id = cached.classKeyToId.get(classKey);
53
+ if (!id) {
54
+ id = cached.idGenerator();
55
+ cached.classKeyToId.set(classKey, id);
56
+ cached.idToClassKey.set(id, classKey);
56
57
  }
57
- return uid;
58
+ return id;
58
59
  }
59
- async getNodesByUid(filePath, uid) {
60
+ async getNodesById(filePath, id) {
60
61
  const snapshot = await this.getSnapshot(filePath);
61
62
  const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
62
- const className = await this.resolveClassKeyFromUid(filePath, uid);
63
+ const className = await this.resolveClassKeyFromId(filePath, id);
63
64
  if (!className) {
64
- throw new Error(`Class with UID ${uid} not found in heap snapshot`);
65
+ throw new Error(`Class with ID ${id} not found in heap snapshot`);
65
66
  }
66
67
  const provider = snapshot.createNodesProviderForClass(className, filter);
67
- const range = await provider.serializeItemsRange(0, 1);
68
- return await provider.serializeItemsRange(0, range.totalLength);
68
+ return await provider.serializeItemsRange(0, Infinity);
69
+ }
70
+ async findNodeIndexById(filePath, nodeId) {
71
+ const snapshot = await this.getSnapshot(filePath);
72
+ const aggregates = await this.getAggregates(filePath);
73
+ const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
74
+ for (const classKey of Object.keys(aggregates)) {
75
+ const provider = snapshot.createNodesProviderForClass(classKey, filter);
76
+ const range = await provider.serializeItemsRange(0, Infinity);
77
+ for (const item of range.items) {
78
+ if (isNodeLike(item) && item.id === nodeId) {
79
+ return item.nodeIndex;
80
+ }
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ async getRetainers(filePath, nodeId) {
86
+ const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
87
+ if (nodeIndex === undefined) {
88
+ throw new Error(`Node with ID ${nodeId} not found`);
89
+ }
90
+ const snapshot = await this.getSnapshot(filePath);
91
+ const provider = snapshot.createRetainingEdgesProvider(nodeIndex);
92
+ return await provider.serializeItemsRange(0, Infinity);
93
+ }
94
+ async getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
95
+ const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
96
+ if (nodeIndex === undefined) {
97
+ throw new Error(`Node with ID ${nodeId} not found`);
98
+ }
99
+ const snapshot = await this.getSnapshot(filePath);
100
+ return await snapshot.getRetainingPaths(nodeIndex, maxDepth, maxNodes, maxSiblings);
69
101
  }
70
102
  #getCachedSnapshot(filePath) {
71
103
  const absolutePath = path.resolve(filePath);
@@ -75,9 +107,9 @@ export class HeapSnapshotManager {
75
107
  }
76
108
  return cached;
77
109
  }
78
- async resolveClassKeyFromUid(filePath, uid) {
110
+ async resolveClassKeyFromId(filePath, id) {
79
111
  const cached = this.#getCachedSnapshot(filePath);
80
- return cached.uidToClassKey.get(uid);
112
+ return cached.idToClassKey.get(id);
81
113
  }
82
114
  async #loadSnapshot(absolutePath) {
83
115
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
@@ -98,13 +130,18 @@ export class HeapSnapshotManager {
98
130
  const snapshot = await snapshotPromise;
99
131
  return { snapshot, worker: workerProxy };
100
132
  }
133
+ hasSnapshots() {
134
+ return this.#snapshots.size > 0;
135
+ }
101
136
  dispose(filePath) {
102
137
  const absolutePath = path.resolve(filePath);
103
138
  const cached = this.#snapshots.get(absolutePath);
104
139
  if (cached) {
105
140
  cached.worker.dispose();
106
141
  this.#snapshots.delete(absolutePath);
142
+ return true;
107
143
  }
144
+ return false;
108
145
  }
109
146
  }
110
147
  //# 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,7 @@ export class McpContext {
44
47
  #heapSnapshotManager = new HeapSnapshotManager();
45
48
  #roots = undefined;
46
49
  constructor(browser, logger, options, locatorClass) {
50
+ overrideDevToolsGlobals();
47
51
  this.browser = browser;
48
52
  this.logger = logger;
49
53
  this.#locatorClass = locatorClass;
@@ -62,19 +66,22 @@ export class McpContext {
62
66
  },
63
67
  };
64
68
  });
69
+ this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(this.browser);
65
70
  this.#devtoolsUniverseManager = new UniverseManager(this.browser);
66
71
  }
67
72
  async #init() {
68
73
  const pages = await this.createPagesSnapshot();
69
- await this.createExtensionServiceWorkersSnapshot();
74
+ const workers = await this.createExtensionServiceWorkersSnapshot();
70
75
  await this.#networkCollector.init(pages);
71
76
  await this.#consoleCollector.init(pages);
72
77
  await this.#devtoolsUniverseManager.init(pages);
78
+ await this.#serviceWorkerConsoleCollector.init(workers);
73
79
  }
74
80
  dispose() {
75
81
  this.#networkCollector.dispose();
76
82
  this.#consoleCollector.dispose();
77
83
  this.#devtoolsUniverseManager.dispose();
84
+ this.#serviceWorkerConsoleCollector.dispose();
78
85
  for (const mcpPage of this.#mcpPages.values()) {
79
86
  mcpPage.dispose();
80
87
  }
@@ -106,7 +113,7 @@ export class McpContext {
106
113
  setRoots(roots) {
107
114
  this.#roots = roots;
108
115
  }
109
- validatePath(filePath) {
116
+ async validatePath(filePath) {
110
117
  if (filePath === undefined) {
111
118
  return;
112
119
  }
@@ -114,19 +121,40 @@ export class McpContext {
114
121
  if (roots === undefined) {
115
122
  return;
116
123
  }
117
- const absolutePath = path.resolve(filePath);
124
+ let canonicalPath;
125
+ try {
126
+ canonicalPath = await resolveCanonicalPath(filePath);
127
+ }
128
+ catch (err) {
129
+ const errMsg = err instanceof Error ? err.message : String(err);
130
+ console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
131
+ throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
132
+ }
133
+ let allowed = false;
118
134
  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;
135
+ try {
136
+ const rootPathUri = root.uri;
137
+ const rootPath = path.resolve(fileURLToPath(rootPathUri));
138
+ const canonicalRoot = await fsPromises.realpath(rootPath);
139
+ if (canonicalPath === canonicalRoot ||
140
+ canonicalPath.startsWith(canonicalRoot + path.sep)) {
141
+ allowed = true;
142
+ break;
143
+ }
144
+ }
145
+ catch (rootErr) {
146
+ const errMsg = rootErr instanceof Error ? rootErr.message : String(rootErr);
147
+ console.warn(`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`);
148
+ // Skip this root if it cannot be resolved.
123
149
  }
124
150
  }
125
- throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
151
+ if (!allowed) {
152
+ throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
153
+ }
126
154
  }
127
155
  resolveCdpRequestId(page, cdpRequestId) {
128
156
  if (!cdpRequestId) {
129
- this.logger('no network request');
157
+ this.logger?.('no network request');
130
158
  return;
131
159
  }
132
160
  const request = this.#networkCollector.find(page.pptrPage, request => {
@@ -134,7 +162,7 @@ export class McpContext {
134
162
  return request.id === cdpRequestId;
135
163
  });
136
164
  if (!request) {
137
- this.logger('no network request for ' + cdpRequestId);
165
+ this.logger?.('no network request for ' + cdpRequestId);
138
166
  return;
139
167
  }
140
168
  return this.#networkCollector.getIdForResource(request);
@@ -195,7 +223,13 @@ export class McpContext {
195
223
  const page = targetPage ?? this.getSelectedPptrPage();
196
224
  const mcpPage = this.#getMcpPage(page);
197
225
  const newSettings = { ...mcpPage.emulationSettings };
198
- if (!options.networkConditions) {
226
+ // Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
227
+ if (this.#options.hasNetworkBlockOrAllowlist) {
228
+ if (options.networkConditions !== undefined) {
229
+ throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
230
+ }
231
+ }
232
+ else if (!options.networkConditions) {
199
233
  await page.emulateNetworkConditions(null);
200
234
  delete newSettings.networkConditions;
201
235
  }
@@ -213,12 +247,23 @@ export class McpContext {
213
247
  await page.emulateNetworkConditions(networkCondition);
214
248
  newSettings.networkConditions = options.networkConditions;
215
249
  }
250
+ const secondarySession = this.getDevToolsUniverse(mcpPage)?.session;
216
251
  if (!options.cpuThrottlingRate) {
217
252
  await page.emulateCPUThrottling(1);
253
+ if (secondarySession) {
254
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
255
+ rate: 1,
256
+ });
257
+ }
218
258
  delete newSettings.cpuThrottlingRate;
219
259
  }
220
260
  else {
221
261
  await page.emulateCPUThrottling(options.cpuThrottlingRate);
262
+ if (secondarySession) {
263
+ await secondarySession.send('Emulation.setCPUThrottlingRate', {
264
+ rate: options.cpuThrottlingRate,
265
+ });
266
+ }
222
267
  newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
223
268
  }
224
269
  if (!options.geolocation) {
@@ -250,7 +295,6 @@ export class McpContext {
250
295
  newSettings.colorScheme = options.colorScheme;
251
296
  }
252
297
  if (!options.viewport) {
253
- await page.setViewport(null);
254
298
  delete newSettings.viewport;
255
299
  }
256
300
  else {
@@ -260,14 +304,22 @@ export class McpContext {
260
304
  hasTouch: false,
261
305
  isLandscape: false,
262
306
  };
263
- const viewport = { ...defaults, ...options.viewport };
264
- await page.setViewport(viewport);
265
- newSettings.viewport = viewport;
307
+ newSettings.viewport = { ...defaults, ...options.viewport };
308
+ }
309
+ if (options.extraHttpHeaders !== undefined) {
310
+ await page.setExtraHTTPHeaders(options.extraHttpHeaders);
311
+ newSettings.extraHttpHeaders = options.extraHttpHeaders;
312
+ if (Object.keys(options.extraHttpHeaders).length === 0) {
313
+ delete newSettings.extraHttpHeaders;
314
+ }
266
315
  }
267
316
  mcpPage.emulationSettings = Object.keys(newSettings).length
268
317
  ? newSettings
269
318
  : {};
270
319
  this.#updateSelectedPageTimeouts();
320
+ // This should happen after updating the page timeouts.
321
+ // Setting the viewport can trigger a reload which we don't want to timeout.
322
+ await page.setViewport(newSettings.viewport ?? null);
271
323
  }
272
324
  setIsRunningPerformanceTrace(x) {
273
325
  this.#isRunningTrace = x;
@@ -333,9 +385,9 @@ export class McpContext {
333
385
  page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
334
386
  // 10sec should be enough for the load event to be emitted during
335
387
  // navigations.
336
- // Increased in case we throttle the network requests
388
+ // Increased in case we throttle the network requests or the CPU
337
389
  const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
338
- page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
390
+ page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier * cpuMultiplier);
339
391
  }
340
392
  // Linear scan over per-page snapshots. The page count is small (typically
341
393
  // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
@@ -372,6 +424,9 @@ export class McpContext {
372
424
  });
373
425
  return this.#extensionServiceWorkers;
374
426
  }
427
+ getServiceWorkerConsoleData(extensionId) {
428
+ return this.#serviceWorkerConsoleCollector.getData(extensionId);
429
+ }
375
430
  async createPagesSnapshot() {
376
431
  const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
377
432
  for (const page of allPages) {
@@ -381,7 +436,7 @@ export class McpContext {
381
436
  this.#mcpPages.set(page, mcpPage);
382
437
  // We emulate a focused page for all pages to support multi-agent workflows.
383
438
  void page.emulateFocusedPage(true).catch(error => {
384
- this.logger('Error turning on focused page emulation', error);
439
+ this.logger?.('Error turning on focused page emulation', error);
385
440
  });
386
441
  }
387
442
  mcpPage.isolatedContextName = isolatedContextNames.get(page);
@@ -427,7 +482,7 @@ export class McpContext {
427
482
  this.#extensionPages.set(target, page);
428
483
  }
429
484
  catch (e) {
430
- this.logger('Failed to get page for extension target', e);
485
+ this.logger?.('Failed to get page for extension target', e);
431
486
  }
432
487
  }
433
488
  }
@@ -462,7 +517,7 @@ export class McpContext {
462
517
  return { pages: allPages, isolatedContextNames };
463
518
  }
464
519
  async detectOpenDevToolsWindows() {
465
- this.logger('Detecting open DevTools windows');
520
+ this.logger?.('Detecting open DevTools windows');
466
521
  const { pages } = await this.#getAllPages();
467
522
  await Promise.all(pages.map(async (page) => {
468
523
  const mcpPage = this.#mcpPages.get(page);
@@ -499,7 +554,7 @@ export class McpContext {
499
554
  }
500
555
  async saveTemporaryFile(data, filename) {
501
556
  const filepath = await getTempFilePath(filename);
502
- this.validatePath(filepath);
557
+ await this.validatePath(filepath);
503
558
  try {
504
559
  await fs.writeFile(filepath, data);
505
560
  }
@@ -509,7 +564,7 @@ export class McpContext {
509
564
  return { filepath };
510
565
  }
511
566
  async saveFile(data, clientProvidedFilePath, extension) {
512
- this.validatePath(clientProvidedFilePath);
567
+ await this.validatePath(clientProvidedFilePath);
513
568
  try {
514
569
  const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
515
570
  await fs.mkdir(path.dirname(filePath), { recursive: true });
@@ -517,7 +572,7 @@ export class McpContext {
517
572
  return { filename: filePath };
518
573
  }
519
574
  catch (err) {
520
- this.logger(err);
575
+ this.logger?.(err);
521
576
  throw new Error('Could not save a file', { cause: err });
522
577
  }
523
578
  }
@@ -562,7 +617,6 @@ export class McpContext {
562
617
  await this.#networkCollector.init(pages);
563
618
  }
564
619
  async installExtension(extensionPath) {
565
- this.validatePath(extensionPath);
566
620
  const id = await this.browser.installExtension(extensionPath);
567
621
  return id;
568
622
  }
@@ -586,20 +640,28 @@ export class McpContext {
586
640
  return pptrExtensions.get(id);
587
641
  }
588
642
  async getHeapSnapshotAggregates(filePath) {
589
- this.validatePath(filePath);
590
643
  return await this.#heapSnapshotManager.getAggregates(filePath);
591
644
  }
592
645
  async getHeapSnapshotStats(filePath) {
593
- this.validatePath(filePath);
594
646
  return await this.#heapSnapshotManager.getStats(filePath);
595
647
  }
596
648
  async getHeapSnapshotStaticData(filePath) {
597
- this.validatePath(filePath);
598
649
  return await this.#heapSnapshotManager.getStaticData(filePath);
599
650
  }
600
- async getHeapSnapshotNodesByUid(filePath, uid) {
601
- this.validatePath(filePath);
602
- return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
651
+ async getHeapSnapshotNodesById(filePath, id) {
652
+ return await this.#heapSnapshotManager.getNodesById(filePath, id);
653
+ }
654
+ async getHeapSnapshotRetainers(filePath, nodeId) {
655
+ return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
656
+ }
657
+ async closeHeapSnapshot(filePath) {
658
+ return this.#heapSnapshotManager.dispose(filePath);
659
+ }
660
+ hasHeapSnapshots() {
661
+ return this.#heapSnapshotManager.hasSnapshots();
662
+ }
663
+ async getHeapSnapshotRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
664
+ return await this.#heapSnapshotManager.getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings);
603
665
  }
604
666
  }
605
667
  //# sourceMappingURL=McpContext.js.map
@@ -30,7 +30,7 @@ export class McpPage {
30
30
  // Dialog
31
31
  #dialog;
32
32
  #dialogHandler;
33
- thirdPartyDeveloperTools;
33
+ thirdPartyDeveloperTools = [];
34
34
  constructor(page, id) {
35
35
  this.pptrPage = page;
36
36
  this.id = id;
@@ -194,18 +194,18 @@ export class McpPage {
194
194
  for (const handle of oldHandles) {
195
195
  await handle
196
196
  .dispose()
197
- .catch(e => logger('Failed to dispose old handle', e));
197
+ .catch(e => logger?.('Failed to dispose old handle', e));
198
198
  }
199
199
  }
200
200
  const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
201
201
  const backendNodeId = await elementHandle.backendNodeId();
202
202
  if (!backendNodeId) {
203
- logger(`No backendNodeId for stashed DOM element with index ${index}`);
203
+ logger?.(`No backendNodeId for stashed DOM element with index ${index}`);
204
204
  return `stashed-${index}`;
205
205
  }
206
206
  const cdpElementId = this.resolveCdpElementId(backendNodeId);
207
207
  if (!cdpElementId) {
208
- logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
208
+ logger?.(`Could not get cdpElementId for backend node ${backendNodeId}`);
209
209
  return `stashed-${index}`;
210
210
  }
211
211
  return cdpElementId;
@@ -263,12 +263,12 @@ export class McpPage {
263
263
  }
264
264
  resolveCdpElementId(cdpBackendNodeId) {
265
265
  if (!cdpBackendNodeId) {
266
- logger('no cdpBackendNodeId');
266
+ logger?.('no cdpBackendNodeId');
267
267
  return;
268
268
  }
269
269
  const snapshot = this.textSnapshot;
270
270
  if (!snapshot) {
271
- logger('no text snapshot');
271
+ logger?.('no text snapshot');
272
272
  return;
273
273
  }
274
274
  // TODO: index by backendNodeId instead.
@@ -286,10 +286,10 @@ export class McpPage {
286
286
  }
287
287
  async getDevToolsData() {
288
288
  try {
289
- logger('Getting DevTools UI data');
289
+ logger?.('Getting DevTools UI data');
290
290
  const devtoolsPage = this.devToolsPage;
291
291
  if (!devtoolsPage) {
292
- logger('No DevTools page detected');
292
+ logger?.('No DevTools page detected');
293
293
  return {};
294
294
  }
295
295
  const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
@@ -307,7 +307,7 @@ export class McpPage {
307
307
  return { cdpBackendNodeId, cdpRequestId };
308
308
  }
309
309
  catch (err) {
310
- logger('error getting devtools data', err);
310
+ logger?.('error getting devtools data', err);
311
311
  }
312
312
  return {};
313
313
  }