brave-mcp 1.2.0 → 1.5.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 (44) hide show
  1. package/README.md +62 -6
  2. package/build/src/HeapSnapshotManager.js +81 -23
  3. package/build/src/McpContext.js +77 -5
  4. package/build/src/McpResponse.js +117 -14
  5. package/build/src/bin/brave-devtools-cli-options.js +99 -3
  6. package/build/src/bin/brave-devtools-mcp-cli-options.js +48 -4
  7. package/build/src/bin/brave-devtools.js +25 -1
  8. package/build/src/bin/check-latest-version.js +1 -0
  9. package/build/src/daemon/daemon.js +1 -1
  10. package/build/src/devtools/DevtoolsUtils.js +3 -4
  11. package/build/src/devtools/McpHostBindingAdapter.js +165 -0
  12. package/build/src/formatters/HeapSnapshotFormatter.js +51 -0
  13. package/build/src/formatters/NetworkFormatter.js +3 -1
  14. package/build/src/index.js +2 -2
  15. package/build/src/issue-descriptions.js +6 -4
  16. package/build/src/third_party/THIRD_PARTY_NOTICES +3 -33
  17. package/build/src/third_party/bundled-packages.json +3 -4
  18. package/build/src/third_party/devtools-formatter-worker.js +1 -1
  19. package/build/src/third_party/devtools-heap-snapshot-worker.js +149 -3
  20. package/build/src/third_party/index.js +2839 -1085
  21. package/build/src/tools/memory.js +105 -4
  22. package/build/src/tools/screencast.js +31 -11
  23. package/build/src/tools/screenshot.js +158 -76
  24. package/build/src/tools/script.js +3 -10
  25. package/build/src/tools/thirdPartyDeveloper.js +6 -6
  26. package/build/src/utils/check-for-updates.js +1 -0
  27. package/build/src/utils/files.js +0 -4
  28. package/build/src/version.js +1 -1
  29. package/package.json +15 -6
  30. package/skills/a11y-debugging/SKILL.md +89 -0
  31. package/skills/a11y-debugging/references/a11y-snippets.md +92 -0
  32. package/skills/chrome-devtools/SKILL.md +72 -0
  33. package/skills/chrome-devtools-cli/SKILL.md +153 -0
  34. package/skills/chrome-devtools-cli/references/installation.md +14 -0
  35. package/skills/debug-optimize-lcp/SKILL.md +121 -0
  36. package/skills/debug-optimize-lcp/references/elements-and-size.md +27 -0
  37. package/skills/debug-optimize-lcp/references/lcp-breakdown.md +23 -0
  38. package/skills/debug-optimize-lcp/references/lcp-snippets.md +79 -0
  39. package/skills/debug-optimize-lcp/references/optimization-strategies.md +38 -0
  40. package/skills/memory-leak-debugging/SKILL.md +50 -0
  41. package/skills/memory-leak-debugging/references/common-leaks.md +33 -0
  42. package/skills/memory-leak-debugging/references/compare_snapshots.js +109 -0
  43. package/skills/memory-leak-debugging/references/memlab.md +29 -0
  44. package/skills/troubleshooting/SKILL.md +98 -0
package/README.md CHANGED
@@ -239,9 +239,9 @@ To use the Chrome DevTools MCP server follow the instructions from <a href="http
239
239
  "chrome-devtools": {
240
240
  "command": "npx",
241
241
  "args": [
242
+ "-y",
242
243
  "chrome-devtools-mcp@latest",
243
- "--browser-url=http://127.0.0.1:9222",
244
- "-y"
244
+ "--browser-url=http://127.0.0.1:9222"
245
245
  ]
246
246
  }
247
247
  }
@@ -632,11 +632,15 @@ 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** (7 tools)
635
+ - **Memory** (11 tools)
636
636
  - [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
637
637
  - [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
638
+ - [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
638
639
  - [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
639
640
  - [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
641
+ - [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
642
+ - [`get_heapsnapshot_duplicate_strings`](docs/tool-reference.md#get_heapsnapshot_duplicate_strings)
643
+ - [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
640
644
  - [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
641
645
  - [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
642
646
  - [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
@@ -669,14 +673,17 @@ The Brave DevTools MCP server supports the following configuration options:
669
673
  - **`--browserUrl`/ `--browser-url`, `-u`**
670
674
  Connect to a running, debuggable Brave instance (e.g. `http://127.0.0.1:9222`).
671
675
  - **Type:** string
676
+ - **Default:** `false`
672
677
 
673
678
  - **`--wsEndpoint`/ `--ws-endpoint`, `-w`**
674
679
  WebSocket endpoint to connect to a running Brave instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.
675
680
  - **Type:** string
681
+ - **Default:** `false`
676
682
 
677
683
  - **`--wsHeaders`/ `--ws-headers`**
678
684
  Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint.
679
685
  - **Type:** string
686
+ - **Default:** `false`
680
687
 
681
688
  - **`--headless`**
682
689
  Whether to run in headless (no UI) mode.
@@ -686,87 +693,108 @@ The Brave DevTools MCP server supports the following configuration options:
686
693
  - **`--executablePath`/ `--executable-path`, `-e`**
687
694
  Path to custom Brave executable. Can also be set via BRAVE_PATH environment variable.
688
695
  - **Type:** string
696
+ - **Default:** `false`
689
697
 
690
698
  - **`--isolated`**
691
699
  If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to false.
692
700
  - **Type:** boolean
701
+ - **Default:** `false`
693
702
 
694
703
  - **`--userDataDir`/ `--user-data-dir`**
695
704
  Path to the user data directory for Brave. Default is $HOME/.cache/brave-devtools-mcp/brave-profile$CHANNEL_SUFFIX_IF_NON_RELEASE
696
705
  - **Type:** string
706
+ - **Default:** `false`
697
707
 
698
708
  - **`--channel`**
699
709
  Specify a different Brave channel that should be used. The default is the release channel.
700
710
  - **Type:** string
701
711
  - **Choices:** `release`, `beta`, `nightly`, `dev`
712
+ - **Default:** `false`
702
713
 
703
714
  - **`--logFile`/ `--log-file`**
704
715
  Path to a file to write debug logs to. Set the env variable `DEBUG` to `*` to enable verbose logs. Useful for submitting bug reports.
705
716
  - **Type:** string
717
+ - **Default:** `false`
706
718
 
707
719
  - **`--viewport`**
708
720
  Initial viewport size for the Brave instances started by the server. For example, `1280x720`. In headless mode, max size is 3840x2160px.
709
721
  - **Type:** string
722
+ - **Default:** `false`
710
723
 
711
724
  - **`--proxyServer`/ `--proxy-server`**
712
725
  Proxy server configuration for Brave passed as --proxy-server when launching the browser. See https://www.chromium.org/developers/design-documents/network-settings/ for details.
713
726
  - **Type:** string
727
+ - **Default:** `false`
714
728
 
715
729
  - **`--acceptInsecureCerts`/ `--accept-insecure-certs`**
716
730
  If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.
717
731
  - **Type:** boolean
732
+ - **Default:** `false`
718
733
 
719
734
  - **`--experimentalPageIdRouting`/ `--experimental-page-id-routing`**
720
735
  Whether to expose pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions).
721
736
  - **Type:** boolean
737
+ - **Default:** `false`
722
738
 
723
739
  - **`--experimentalDevtools`/ `--experimental-devtools`**
724
740
  Whether to enable automation over DevTools targets
725
741
  - **Type:** boolean
742
+ - **Default:** `false`
726
743
 
727
744
  - **`--experimentalVision`/ `--experimental-vision`**
728
745
  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.
729
746
  - **Type:** boolean
747
+ - **Default:** `false`
730
748
 
731
749
  - **`--memoryDebugging`/ `--memory-debugging`, `-experimentalMemory`**
732
750
  Whether to enable memory debugging tools.
733
751
  - **Type:** boolean
752
+ - **Default:** `false`
734
753
 
735
754
  - **`--experimentalStructuredContent`/ `--experimental-structured-content`**
736
755
  Whether to output structured formatted content.
737
756
  - **Type:** boolean
757
+ - **Default:** `false`
738
758
 
739
759
  - **`--experimentalIncludeAllPages`/ `--experimental-include-all-pages`**
740
760
  Whether to include all kinds of pages such as webviews or background pages as pages.
741
761
  - **Type:** boolean
762
+ - **Default:** `false`
742
763
 
743
764
  - **`--experimentalScreencast`/ `--experimental-screencast`**
744
765
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
745
766
  - **Type:** boolean
767
+ - **Default:** `false`
746
768
 
747
769
  - **`--experimentalFfmpegPath`/ `--experimental-ffmpeg-path`**
748
770
  Path to ffmpeg executable for screencast recording.
749
771
  - **Type:** string
772
+ - **Default:** `false`
750
773
 
751
774
  - **`--categoryExperimentalWebmcp`/ `--category-experimental-webmcp`**
752
- Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
775
+ Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCP,DevToolsWebMCPSupport`
753
776
  - **Type:** boolean
777
+ - **Default:** `false`
754
778
 
755
779
  - **`--braveArg`/ `--brave-arg`**
756
780
  Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
757
781
  - **Type:** array
782
+ - **Default:** `false`
758
783
 
759
784
  - **`--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.
785
+ Restricts browser's 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
786
  - **Type:** array
787
+ - **Default:** `false`
762
788
 
763
789
  - **`--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.
790
+ Restricts browser's 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
791
  - **Type:** array
792
+ - **Default:** `false`
766
793
 
767
794
  - **`--ignoreDefaultBraveArg`/ `--ignore-default-brave-arg`**
768
795
  Explicitly disable default arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.
769
796
  - **Type:** array
797
+ - **Default:** `false`
770
798
 
771
799
  - **`--categoryEmulation`/ `--category-emulation`**
772
800
  Set to false to exclude tools related to emulation.
@@ -803,9 +831,31 @@ The Brave DevTools MCP server supports the following configuration options:
803
831
  - **Type:** boolean
804
832
  - **Default:** `false`
805
833
 
834
+ - **`--screenshotFormat`/ `--screenshot-format`**
835
+ 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").
836
+ - **Type:** string
837
+ - **Choices:** `jpeg`, `png`, `webp`
838
+ - **Default:** `false`
839
+
840
+ - **`--screenshotQuality`/ `--screenshot-quality`**
841
+ 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.
842
+ - **Type:** number
843
+ - **Default:** `false`
844
+
845
+ - **`--screenshotMaxWidth`/ `--screenshot-max-width`**
846
+ 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.
847
+ - **Type:** number
848
+ - **Default:** `false`
849
+
850
+ - **`--screenshotMaxHeight`/ `--screenshot-max-height`**
851
+ 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.
852
+ - **Type:** number
853
+ - **Default:** `false`
854
+
806
855
  - **`--slim`**
807
856
  Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
808
857
  - **Type:** boolean
858
+ - **Default:** `false`
809
859
 
810
860
  - **`--redactNetworkHeaders`/ `--redact-network-headers`**
811
861
  If true, redacts some of the network headers considered sensitive before returning to the client.
@@ -911,3 +961,9 @@ Please consult [these instructions](./docs/debugging-android.md) (originally wri
911
961
  ## Known limitations
912
962
 
913
963
  See [Troubleshooting](./docs/troubleshooting.md).
964
+
965
+ ## Integrating as a browser subagent
966
+
967
+ If you are developing agentic tooling and want to provide an integrated browser subagent as part of your product, we recommend building on top of Chrome DevTools for agents.
968
+
969
+ For a reference implementation, see the [Gemini CLI browser agent documentation](https://geminicli.com/docs/core/subagents/#browser-agent).
@@ -5,10 +5,10 @@
5
5
  */
6
6
  import fsSync from 'node:fs';
7
7
  import path from 'node:path';
8
- import { isNodeLike } from './formatters/HeapSnapshotFormatter.js';
9
8
  import { DevTools } from './third_party/index.js';
10
9
  import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
11
10
  export class HeapSnapshotManager {
11
+ #snapshotIdGenerator = createIdGenerator();
12
12
  #snapshots = new Map();
13
13
  async getSnapshot(filePath) {
14
14
  const absolutePath = path.resolve(filePath);
@@ -16,7 +16,8 @@ export class HeapSnapshotManager {
16
16
  if (cached) {
17
17
  return cached.snapshot;
18
18
  }
19
- const { snapshot, worker } = await this.#loadSnapshot(absolutePath);
19
+ const uid = this.#snapshotIdGenerator();
20
+ const { snapshot, worker } = await this.#loadSnapshot(absolutePath, uid);
20
21
  this.#snapshots.set(absolutePath, {
21
22
  snapshot,
22
23
  worker,
@@ -67,38 +68,72 @@ export class HeapSnapshotManager {
67
68
  const provider = snapshot.createNodesProviderForClass(className, filter);
68
69
  return await provider.serializeItemsRange(0, Infinity);
69
70
  }
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
71
  async getRetainers(filePath, nodeId) {
86
- const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
72
+ const snapshot = await this.getSnapshot(filePath);
73
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
87
74
  if (nodeIndex === undefined) {
88
75
  throw new Error(`Node with ID ${nodeId} not found`);
89
76
  }
90
- const snapshot = await this.getSnapshot(filePath);
91
77
  const provider = snapshot.createRetainingEdgesProvider(nodeIndex);
92
78
  return await provider.serializeItemsRange(0, Infinity);
93
79
  }
94
80
  async getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
95
- const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
81
+ const snapshot = await this.getSnapshot(filePath);
82
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
96
83
  if (nodeIndex === undefined) {
97
84
  throw new Error(`Node with ID ${nodeId} not found`);
98
85
  }
99
- const snapshot = await this.getSnapshot(filePath);
100
86
  return await snapshot.getRetainingPaths(nodeIndex, maxDepth, maxNodes, maxSiblings);
101
87
  }
88
+ async getDominatorsOf(filePath, nodeId) {
89
+ const snapshot = await this.getSnapshot(filePath);
90
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
91
+ if (nodeIndex === undefined) {
92
+ throw new Error(`Node with ID ${nodeId} not found`);
93
+ }
94
+ return await snapshot.getDominatorsOf(nodeIndex);
95
+ }
96
+ async getEdges(filePath, nodeId) {
97
+ const snapshot = await this.getSnapshot(filePath);
98
+ const nodeIndex = await snapshot.nodeIndexForId(nodeId);
99
+ if (nodeIndex === undefined) {
100
+ throw new Error(`Node with ID ${nodeId} not found`);
101
+ }
102
+ const provider = snapshot.createEdgesProvider(nodeIndex);
103
+ return await provider.serializeItemsRange(0, Infinity);
104
+ }
105
+ async getClassDiffs(baseFilePath, currentFilePath) {
106
+ const rawDiffs = await this.#getSortedRawClassDiffs(baseFilePath, currentFilePath);
107
+ return rawDiffs.map(rawDiff => ({
108
+ className: rawDiff.name,
109
+ addedCount: rawDiff.addedCount,
110
+ removedCount: rawDiff.removedCount,
111
+ countDelta: rawDiff.countDelta,
112
+ addedSize: rawDiff.addedSize,
113
+ removedSize: rawDiff.removedSize,
114
+ sizeDelta: rawDiff.sizeDelta,
115
+ }));
116
+ }
117
+ async getDetailedClassDiff(baseFilePath, currentFilePath, classIndex) {
118
+ const classDiffs = await this.#getSortedRawClassDiffs(baseFilePath, currentFilePath);
119
+ const rawDiff = classDiffs[classIndex];
120
+ if (!rawDiff) {
121
+ throw new Error(`Invalid classIndex: ${classIndex}. Total classes with changes: ${classDiffs.length}`);
122
+ }
123
+ return {
124
+ className: rawDiff.name,
125
+ addedCount: rawDiff.addedCount,
126
+ removedCount: rawDiff.removedCount,
127
+ countDelta: rawDiff.countDelta,
128
+ addedSize: rawDiff.addedSize,
129
+ removedSize: rawDiff.removedSize,
130
+ sizeDelta: rawDiff.sizeDelta,
131
+ addedIds: rawDiff.addedIds ?? [],
132
+ addedSelfSizes: rawDiff.addedSelfSizes ?? [],
133
+ deletedIds: rawDiff.deletedIds ?? [],
134
+ deletedSelfSizes: rawDiff.deletedSelfSizes ?? [],
135
+ };
136
+ }
102
137
  #getCachedSnapshot(filePath) {
103
138
  const absolutePath = path.resolve(filePath);
104
139
  const cached = this.#snapshots.get(absolutePath);
@@ -107,16 +142,35 @@ export class HeapSnapshotManager {
107
142
  }
108
143
  return cached;
109
144
  }
145
+ async #getSortedRawClassDiffs(baseFilePath, currentFilePath) {
146
+ const baseSnapshot = await this.getSnapshot(baseFilePath);
147
+ const currentSnapshot = await this.getSnapshot(currentFilePath);
148
+ const interfaceDefinitions = await currentSnapshot.interfaceDefinitions();
149
+ const aggregatesForDiff = await baseSnapshot.aggregatesForDiff(interfaceDefinitions);
150
+ const baseSnapshotId = baseSnapshot.uid;
151
+ if (baseSnapshotId === undefined) {
152
+ throw new Error('Base snapshot UID is undefined');
153
+ }
154
+ // DevTools calculateSnapshotDiff uses the first parameter (baseSnapshotId)
155
+ // as a cache key. We pass the unique UID of the base snapshot.
156
+ const rawDiffs = await currentSnapshot.calculateSnapshotDiff(baseSnapshotId, aggregatesForDiff);
157
+ // Return a filtered and sorted array here to ensure that
158
+ // compare_heapsnapshot_summary and compare_heapsnapshot_details agree
159
+ // on indices.
160
+ return Object.values(rawDiffs)
161
+ .filter(diff => diff.addedCount > 0 || diff.removedCount > 0)
162
+ .sort((a, b) => b.sizeDelta - a.sizeDelta);
163
+ }
110
164
  async resolveClassKeyFromId(filePath, id) {
111
165
  const cached = this.#getCachedSnapshot(filePath);
112
166
  return cached.idToClassKey.get(id);
113
167
  }
114
- async #loadSnapshot(absolutePath) {
168
+ async #loadSnapshot(absolutePath, uid) {
115
169
  const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
116
170
  /* noop */
117
171
  }, import.meta.resolve('./third_party/devtools-heap-snapshot-worker.js'));
118
172
  const { promise: snapshotPromise, resolve: resolveSnapshot } = Promise.withResolvers();
119
- const loaderProxy = workerProxy.createLoader(1, snapshotProxy => {
173
+ const loaderProxy = workerProxy.createLoader(uid, snapshotProxy => {
120
174
  resolveSnapshot(snapshotProxy);
121
175
  });
122
176
  const fileStream = fsSync.createReadStream(absolutePath, {
@@ -130,6 +184,10 @@ export class HeapSnapshotManager {
130
184
  const snapshot = await snapshotPromise;
131
185
  return { snapshot, worker: workerProxy };
132
186
  }
187
+ async getDuplicateStrings(filePath) {
188
+ const snapshot = await this.getSnapshot(filePath);
189
+ return await snapshot.getDuplicateStrings();
190
+ }
133
191
  hasSnapshots() {
134
192
  return this.#snapshots.size > 0;
135
193
  }
@@ -16,7 +16,7 @@ import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
16
16
  import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
17
17
  import { listPages } from './tools/pages.js';
18
18
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
19
- import { ensureExtension, getTempFilePath, resolveCanonicalPath, } from './utils/files.js';
19
+ import { getTempFilePath, resolveCanonicalPath } from './utils/files.js';
20
20
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
21
21
  const DEFAULT_TIMEOUT = 5_000;
22
22
  const NAVIGATION_TIMEOUT = 10_000;
@@ -47,7 +47,11 @@ export class McpContext {
47
47
  #heapSnapshotManager = new HeapSnapshotManager();
48
48
  #roots = undefined;
49
49
  constructor(browser, logger, options, locatorClass) {
50
- overrideDevToolsGlobals();
50
+ overrideDevToolsGlobals({
51
+ loadResource: (url) => {
52
+ return this.loadResource(url);
53
+ },
54
+ });
51
55
  this.browser = browser;
52
56
  this.logger = logger;
53
57
  this.#locatorClass = locatorClass;
@@ -152,6 +156,13 @@ export class McpContext {
152
156
  throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
153
157
  }
154
158
  }
159
+ async ensureExtension(filePath, extension) {
160
+ const resolvedPath = path.resolve(filePath);
161
+ const currentExtension = path.extname(resolvedPath);
162
+ const outputPath = `${resolvedPath.slice(0, resolvedPath.length - currentExtension.length)}${extension}`;
163
+ await this.validatePath(outputPath);
164
+ return outputPath;
165
+ }
155
166
  resolveCdpRequestId(page, cdpRequestId) {
156
167
  if (!cdpRequestId) {
157
168
  this.logger?.('no network request');
@@ -219,12 +230,15 @@ export class McpContext {
219
230
  const currentSetting = page.emulationSettings;
220
231
  await this.emulate(currentSetting, page.pptrPage);
221
232
  }
233
+ get #hasNetworkBlockOrAllowlist() {
234
+ return !!(this.#options.allowList || this.#options.blocklist);
235
+ }
222
236
  async emulate(options, targetPage) {
223
237
  const page = targetPage ?? this.getSelectedPptrPage();
224
238
  const mcpPage = this.#getMcpPage(page);
225
239
  const newSettings = { ...mcpPage.emulationSettings };
226
240
  // Skip network emulation if blocklist/allowlist is configured, as it conflicts with blocking rules in Puppeteer.
227
- if (this.#options.hasNetworkBlockOrAllowlist) {
241
+ if (this.#hasNetworkBlockOrAllowlist) {
228
242
  if (options.networkConditions !== undefined) {
229
243
  throw new Error('Network throttling is not supported when network blocking (allowlist/blocklist) is configured.');
230
244
  }
@@ -564,9 +578,8 @@ export class McpContext {
564
578
  return { filepath };
565
579
  }
566
580
  async saveFile(data, clientProvidedFilePath, extension) {
567
- await this.validatePath(clientProvidedFilePath);
581
+ const filePath = await this.ensureExtension(clientProvidedFilePath, extension);
568
582
  try {
569
- const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
570
583
  await fs.mkdir(path.dirname(filePath), { recursive: true });
571
584
  await fs.writeFile(filePath, data);
572
585
  return { filename: filePath };
@@ -642,6 +655,9 @@ export class McpContext {
642
655
  async getHeapSnapshotAggregates(filePath) {
643
656
  return await this.#heapSnapshotManager.getAggregates(filePath);
644
657
  }
658
+ async getHeapSnapshotDuplicateStrings(filePath) {
659
+ return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
660
+ }
645
661
  async getHeapSnapshotStats(filePath) {
646
662
  return await this.#heapSnapshotManager.getStats(filePath);
647
663
  }
@@ -663,5 +679,61 @@ export class McpContext {
663
679
  async getHeapSnapshotRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
664
680
  return await this.#heapSnapshotManager.getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings);
665
681
  }
682
+ async getHeapSnapshotDominators(filePath, nodeId) {
683
+ return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
684
+ }
685
+ #validateUrlNotBlocked(url) {
686
+ if (!this.#options.blocklist) {
687
+ return;
688
+ }
689
+ for (const block of this.#options.blocklist) {
690
+ const pattern = new URLPattern(block);
691
+ if (pattern.test(url)) {
692
+ throw new Error(`Blocked by blocklist: ${url}`);
693
+ }
694
+ }
695
+ }
696
+ #validateUrlAllowed(url) {
697
+ if (!this.#options.allowList) {
698
+ return;
699
+ }
700
+ for (const allow of this.#options.allowList) {
701
+ const pattern = new URLPattern(allow);
702
+ if (pattern.test(url)) {
703
+ return;
704
+ }
705
+ }
706
+ throw new Error(`Not allowed by allowlist: ${url}`);
707
+ }
708
+ async loadResource(path) {
709
+ const url = new URL(path);
710
+ this.#validateUrlNotBlocked(url);
711
+ switch (url.protocol) {
712
+ case 'https:':
713
+ case 'http:': {
714
+ this.#validateUrlAllowed(url);
715
+ const response = await fetch(url);
716
+ if (!response.ok) {
717
+ throw new Error(`Failed to load resource: ${url}`);
718
+ }
719
+ return response.text();
720
+ }
721
+ case 'file:': {
722
+ await this.validatePath(fileURLToPath(url));
723
+ return await fsPromises.readFile(url, 'utf-8');
724
+ }
725
+ default:
726
+ throw new Error(`Unsupported protocol for: ${url}`);
727
+ }
728
+ }
729
+ async getHeapSnapshotEdges(filePath, nodeId) {
730
+ return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
731
+ }
732
+ async getHeapSnapshotClassDiffs(baseFilePath, currentFilePath) {
733
+ return await this.#heapSnapshotManager.getClassDiffs(baseFilePath, currentFilePath);
734
+ }
735
+ async getHeapSnapshotDetailedClassDiff(baseFilePath, currentFilePath, classIndex) {
736
+ return await this.#heapSnapshotManager.getDetailedClassDiff(baseFilePath, currentFilePath, classIndex);
737
+ }
666
738
  }
667
739
  //# sourceMappingURL=McpContext.js.map