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.
- package/README.md +62 -6
- package/build/src/HeapSnapshotManager.js +81 -23
- package/build/src/McpContext.js +77 -5
- package/build/src/McpResponse.js +117 -14
- package/build/src/bin/brave-devtools-cli-options.js +99 -3
- package/build/src/bin/brave-devtools-mcp-cli-options.js +48 -4
- package/build/src/bin/brave-devtools.js +25 -1
- package/build/src/bin/check-latest-version.js +1 -0
- package/build/src/daemon/daemon.js +1 -1
- package/build/src/devtools/DevtoolsUtils.js +3 -4
- package/build/src/devtools/McpHostBindingAdapter.js +165 -0
- package/build/src/formatters/HeapSnapshotFormatter.js +51 -0
- package/build/src/formatters/NetworkFormatter.js +3 -1
- package/build/src/index.js +2 -2
- package/build/src/issue-descriptions.js +6 -4
- package/build/src/third_party/THIRD_PARTY_NOTICES +3 -33
- package/build/src/third_party/bundled-packages.json +3 -4
- package/build/src/third_party/devtools-formatter-worker.js +1 -1
- package/build/src/third_party/devtools-heap-snapshot-worker.js +149 -3
- package/build/src/third_party/index.js +2839 -1085
- package/build/src/tools/memory.js +105 -4
- package/build/src/tools/screencast.js +31 -11
- package/build/src/tools/screenshot.js +158 -76
- package/build/src/tools/script.js +3 -10
- package/build/src/tools/thirdPartyDeveloper.js +6 -6
- package/build/src/utils/check-for-updates.js +1 -0
- package/build/src/utils/files.js +0 -4
- package/build/src/version.js +1 -1
- package/package.json +15 -6
- package/skills/a11y-debugging/SKILL.md +89 -0
- package/skills/a11y-debugging/references/a11y-snippets.md +92 -0
- package/skills/chrome-devtools/SKILL.md +72 -0
- package/skills/chrome-devtools-cli/SKILL.md +153 -0
- package/skills/chrome-devtools-cli/references/installation.md +14 -0
- package/skills/debug-optimize-lcp/SKILL.md +121 -0
- package/skills/debug-optimize-lcp/references/elements-and-size.md +27 -0
- package/skills/debug-optimize-lcp/references/lcp-breakdown.md +23 -0
- package/skills/debug-optimize-lcp/references/lcp-snippets.md +79 -0
- package/skills/debug-optimize-lcp/references/optimization-strategies.md +38 -0
- package/skills/memory-leak-debugging/SKILL.md +50 -0
- package/skills/memory-leak-debugging/references/common-leaks.md +33 -0
- package/skills/memory-leak-debugging/references/compare_snapshots.js +109 -0
- package/skills/memory-leak-debugging/references/memlab.md +29 -0
- 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** (
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -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 {
|
|
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.#
|
|
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.
|
|
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
|