brave-mcp 0.2.0 → 0.2.1

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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fsSync from 'node:fs';
7
+ import path from 'node:path';
8
+ import { DevTools } from './third_party/index.js';
9
+ export class HeapSnapshotManager {
10
+ #snapshots = new Map();
11
+ async getSnapshot(filePath) {
12
+ const absolutePath = path.resolve(filePath);
13
+ const cached = this.#snapshots.get(absolutePath);
14
+ if (cached) {
15
+ return cached.snapshot;
16
+ }
17
+ const { snapshot, worker } = await this.#loadSnapshot(absolutePath);
18
+ this.#snapshots.set(absolutePath, { snapshot, worker });
19
+ return snapshot;
20
+ }
21
+ async getAggregates(filePath) {
22
+ const snapshot = await this.getSnapshot(filePath);
23
+ const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
24
+ return await snapshot.aggregatesWithFilter(filter);
25
+ }
26
+ async getStats(filePath) {
27
+ const snapshot = await this.getSnapshot(filePath);
28
+ return await snapshot.getStatistics();
29
+ }
30
+ async getStaticData(filePath) {
31
+ const snapshot = await this.getSnapshot(filePath);
32
+ return snapshot.staticData;
33
+ }
34
+ async #loadSnapshot(absolutePath) {
35
+ const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
36
+ /* noop */
37
+ }, import.meta.resolve('./third_party/devtools-heap-snapshot-worker.js'));
38
+ const { promise: snapshotPromise, resolve: resolveSnapshot } = Promise.withResolvers();
39
+ const loaderProxy = workerProxy.createLoader(1, snapshotProxy => {
40
+ resolveSnapshot(snapshotProxy);
41
+ });
42
+ const fileStream = fsSync.createReadStream(absolutePath, {
43
+ encoding: 'utf-8',
44
+ highWaterMark: 1024 * 1024,
45
+ });
46
+ for await (const chunk of fileStream) {
47
+ await loaderProxy.write(chunk);
48
+ }
49
+ await loaderProxy.close();
50
+ const snapshot = await snapshotPromise;
51
+ return { snapshot, worker: workerProxy };
52
+ }
53
+ dispose(filePath) {
54
+ const absolutePath = path.resolve(filePath);
55
+ const cached = this.#snapshots.get(absolutePath);
56
+ if (cached) {
57
+ cached.worker.dispose();
58
+ this.#snapshots.delete(absolutePath);
59
+ }
60
+ }
61
+ }
@@ -6,6 +6,7 @@
6
6
  import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { UniverseManager } from './DevtoolsUtils.js';
9
+ import { HeapSnapshotManager } from './HeapSnapshotManager.js';
9
10
  import { McpPage } from './McpPage.js';
10
11
  import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
11
12
  import { Locator } from './third_party/index.js';
@@ -42,6 +43,7 @@ export class McpContext {
42
43
  #traceResults = [];
43
44
  #locatorClass;
44
45
  #options;
46
+ #heapSnapshotManager = new HeapSnapshotManager();
45
47
  constructor(browser, logger, options, locatorClass) {
46
48
  this.browser = browser;
47
49
  this.logger = logger;
@@ -673,4 +675,13 @@ export class McpContext {
673
675
  getExtension(id) {
674
676
  return this.#extensionRegistry.getById(id);
675
677
  }
678
+ async getHeapSnapshotAggregates(filePath) {
679
+ return await this.#heapSnapshotManager.getAggregates(filePath);
680
+ }
681
+ async getHeapSnapshotStats(filePath) {
682
+ return await this.#heapSnapshotManager.getStats(filePath);
683
+ }
684
+ async getHeapSnapshotStaticData(filePath) {
685
+ return await this.#heapSnapshotManager.getStaticData(filePath);
686
+ }
676
687
  }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
7
+ import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
7
8
  import { IssueFormatter } from './formatters/IssueFormatter.js';
8
9
  import { NetworkFormatter } from './formatters/NetworkFormatter.js';
9
10
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
@@ -118,6 +119,7 @@ export class McpResponse {
118
119
  #attachedLighthouseResult;
119
120
  #textResponseLines = [];
120
121
  #images = [];
122
+ #heapSnapshotOptions;
121
123
  #networkRequestsOptions;
122
124
  #consoleDataOptions;
123
125
  #listExtensions;
@@ -254,6 +256,22 @@ export class McpResponse {
254
256
  appendResponseLine(value) {
255
257
  this.#textResponseLines.push(value);
256
258
  }
259
+ setHeapSnapshotAggregates(aggregates, options) {
260
+ this.#heapSnapshotOptions = {
261
+ ...this.#heapSnapshotOptions,
262
+ include: true,
263
+ aggregates,
264
+ pagination: options,
265
+ };
266
+ }
267
+ setHeapSnapshotStats(stats, staticData) {
268
+ this.#heapSnapshotOptions = {
269
+ ...this.#heapSnapshotOptions,
270
+ include: true,
271
+ stats,
272
+ staticData,
273
+ };
274
+ }
257
275
  attachImage(value) {
258
276
  this.#images.push(value);
259
277
  }
@@ -598,6 +616,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
598
616
  structuredContent.snapshot = data.snapshot.toJSON();
599
617
  }
600
618
  }
619
+ if (this.#heapSnapshotOptions?.include) {
620
+ response.push('## Heap Snapshot Data');
621
+ const stats = this.#heapSnapshotOptions.stats;
622
+ const staticData = this.#heapSnapshotOptions.staticData;
623
+ if (stats) {
624
+ response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
625
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
626
+ structuredContent.heapSnapshot.stats = stats;
627
+ }
628
+ if (staticData) {
629
+ response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
630
+ structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
631
+ structuredContent.heapSnapshot.staticData = staticData;
632
+ }
633
+ const aggregates = this.#heapSnapshotOptions.aggregates;
634
+ if (aggregates) {
635
+ const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
636
+ const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
637
+ structuredContent.pagination = paginationData.pagination;
638
+ response.push(...paginationData.info);
639
+ const paginatedRecord = Object.fromEntries(paginationData.items);
640
+ const formatter = new HeapSnapshotFormatter(paginatedRecord);
641
+ response.push(formatter.toString());
642
+ structuredContent.heapSnapshotData = formatter.toJSON();
643
+ }
644
+ }
601
645
  if (data.detailedNetworkRequest) {
602
646
  response.push(data.detailedNetworkRequest.toStringDetailed());
603
647
  structuredContent.networkRequest =
@@ -250,6 +250,11 @@ class PageEventSubscriber {
250
250
  };
251
251
  #onIssueAdded = (inspectorIssue) => {
252
252
  try {
253
+ // DevTools currently defines this protocol issue code but has no
254
+ // IssuesManager handler for it, so calling into the mapper only warns.
255
+ if (String(inspectorIssue.code) === 'PerformanceIssue') {
256
+ return;
257
+ }
253
258
  const issue = DevTools.createIssuesFromProtocolIssue(null,
254
259
  // @ts-expect-error Protocol types diverge.
255
260
  inspectorIssue)[0];
@@ -149,6 +149,11 @@ export const cliOptions = {
149
149
  describe: '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.',
150
150
  hidden: false,
151
151
  },
152
+ experimentalMemory: {
153
+ type: 'boolean',
154
+ describe: 'Whether to enable experimental memory tools.',
155
+ hidden: true,
156
+ },
152
157
  experimentalStructuredContent: {
153
158
  type: 'boolean',
154
159
  describe: 'Whether to output structured formatted content.',
@@ -175,13 +175,13 @@ export const commands = {
175
175
  requestFilePath: {
176
176
  name: 'requestFilePath',
177
177
  type: 'string',
178
- description: 'The absolute or relative path to save the request body to. If omitted, the body is returned inline.',
178
+ description: 'The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.',
179
179
  required: false,
180
180
  },
181
181
  responseFilePath: {
182
182
  name: 'responseFilePath',
183
183
  type: 'string',
184
- description: 'The absolute or relative path to save the response body to. If omitted, the body is returned inline.',
184
+ description: 'The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.',
185
185
  required: false,
186
186
  },
187
187
  },
@@ -314,7 +314,7 @@ export const commands = {
314
314
  },
315
315
  },
316
316
  list_pages: {
317
- description: 'Get a list of pages open in the browser.',
317
+ description: 'Get a list of pages open in the browser.',
318
318
  category: 'Navigation automation',
319
319
  args: {},
320
320
  },
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export class HeapSnapshotFormatter {
7
+ #aggregates;
8
+ constructor(aggregates) {
9
+ this.#aggregates = aggregates;
10
+ }
11
+ #getSortedAggregates() {
12
+ return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
13
+ }
14
+ toString() {
15
+ const sorted = this.#getSortedAggregates();
16
+ const lines = [];
17
+ lines.push('className,count,selfSize,maxRetainedSize');
18
+ for (const info of sorted) {
19
+ lines.push(`"${info.name}",${info.count},${info.self},${info.maxRet}`);
20
+ }
21
+ return lines.join('\n');
22
+ }
23
+ toJSON() {
24
+ const sorted = this.#getSortedAggregates();
25
+ return sorted.map(info => ({
26
+ className: info.name,
27
+ count: info.count,
28
+ selfSize: info.self,
29
+ retainedSize: info.maxRet,
30
+ }));
31
+ }
32
+ static sort(aggregates) {
33
+ return Object.entries(aggregates).sort((a, b) => b[1].self - a[1].self);
34
+ }
35
+ }
@@ -227,7 +227,7 @@ function converNetworkRequestDetailedToStringDetailed(data) {
227
227
  response.push(`### Redirect chain`);
228
228
  let indent = 0;
229
229
  for (const request of redirectChain.reverse()) {
230
- response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)})}`);
230
+ response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`);
231
231
  indent++;
232
232
  }
233
233
  }
@@ -111,6 +111,10 @@ export async function createMcpServer(serverArgs, options) {
111
111
  !serverArgs.experimentalVision) {
112
112
  return;
113
113
  }
114
+ if (tool.annotations.conditions?.includes('experimentalMemory') &&
115
+ !serverArgs.experimentalMemory) {
116
+ return;
117
+ }
114
118
  if (tool.annotations.conditions?.includes('experimentalInteropTools') &&
115
119
  !serverArgs.experimentalInteropTools) {
116
120
  return;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import '../../node_modules/chrome-devtools-frontend/front_end/entrypoints/heap_snapshot_worker/heap_snapshot_worker-entrypoint.js';
@@ -8,6 +8,7 @@ import 'core-js/modules/es.set.union.v2.js';
8
8
  import 'core-js/proposals/iterator-helpers.js';
9
9
  export { default as yargs } from 'yargs';
10
10
  export { hideBin } from 'yargs/helpers';
11
+ export { default as semver } from 'semver';
11
12
  export { default as debug } from 'debug';
12
13
  export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
14
  export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -13,6 +13,7 @@ export var ToolCategory;
13
13
  ToolCategory["DEBUGGING"] = "debugging";
14
14
  ToolCategory["EXTENSIONS"] = "extensions";
15
15
  ToolCategory["IN_PAGE"] = "in-page";
16
+ ToolCategory["MEMORY"] = "memory";
16
17
  })(ToolCategory || (ToolCategory = {}));
17
18
  export const labels = {
18
19
  [ToolCategory.INPUT]: 'Input automation',
@@ -23,4 +24,5 @@ export const labels = {
23
24
  [ToolCategory.DEBUGGING]: 'Debugging',
24
25
  [ToolCategory.EXTENSIONS]: 'Extensions',
25
26
  [ToolCategory.IN_PAGE]: 'In-page tools',
27
+ [ToolCategory.MEMORY]: 'Memory',
26
28
  };
@@ -6,7 +6,7 @@
6
6
  import { zod } from '../third_party/index.js';
7
7
  import { ensureExtension } from '../utils/files.js';
8
8
  import { ToolCategory } from './categories.js';
9
- import { definePageTool } from './ToolDefinition.js';
9
+ import { definePageTool, defineTool } from './ToolDefinition.js';
10
10
  export const takeMemorySnapshot = definePageTool({
11
11
  name: 'take_memory_snapshot',
12
12
  description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
@@ -27,3 +27,20 @@ export const takeMemorySnapshot = definePageTool({
27
27
  response.appendResponseLine(`Heap snapshot saved to ${request.params.filePath}`);
28
28
  },
29
29
  });
30
+ export const exploreMemorySnapshot = defineTool({
31
+ name: 'load_memory_snapshot',
32
+ description: 'Loads a memory heapsnapshot and returns snapshot summary stats. ',
33
+ annotations: {
34
+ category: ToolCategory.MEMORY,
35
+ readOnlyHint: true,
36
+ conditions: ['experimentalMemory'],
37
+ },
38
+ schema: {
39
+ filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
40
+ },
41
+ handler: async (request, response, context) => {
42
+ const stats = await context.getHeapSnapshotStats(request.params.filePath);
43
+ const staticData = await context.getHeapSnapshotStaticData(request.params.filePath);
44
+ response.setHeapSnapshotStats(stats, staticData);
45
+ },
46
+ });
@@ -87,11 +87,11 @@ export const getNetworkRequest = definePageTool({
87
87
  requestFilePath: zod
88
88
  .string()
89
89
  .optional()
90
- .describe('The absolute or relative path to save the request body to. If omitted, the body is returned inline.'),
90
+ .describe('The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.'),
91
91
  responseFilePath: zod
92
92
  .string()
93
93
  .optional()
94
- .describe('The absolute or relative path to save the response body to. If omitted, the body is returned inline.'),
94
+ .describe('The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.'),
95
95
  },
96
96
  handler: async (request, response, context) => {
97
97
  if (request.params.reqid) {
@@ -10,7 +10,7 @@ import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './
10
10
  export const listPages = defineTool(args => {
11
11
  return {
12
12
  name: 'list_pages',
13
- description: `Get a list of pages ${args?.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`,
13
+ description: `Get a list of pages${args?.categoryExtensions ? ' including extension service workers' : ''} open in the browser.`,
14
14
  annotations: {
15
15
  category: ToolCategory.NAVIGATION,
16
16
  readOnlyHint: true,
@@ -8,6 +8,7 @@ import fs from 'node:fs/promises';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import process from 'node:process';
11
+ import { semver } from '../third_party/index.js';
11
12
  import { VERSION } from '../version.js';
12
13
  /**
13
14
  * Notifies the user if an update is available.
@@ -34,7 +35,7 @@ export async function checkForUpdates(message) {
34
35
  catch {
35
36
  // Ignore errors reading cache.
36
37
  }
37
- if (cachedVersion && cachedVersion !== VERSION) {
38
+ if (cachedVersion && semver.lt(VERSION, cachedVersion)) {
38
39
  console.warn(`\nUpdate available: ${VERSION} -> ${cachedVersion}\n${message}\n`);
39
40
  }
40
41
  const now = Date.now();
@@ -3,4 +3,4 @@
3
3
  * Copyright 2026 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- export const VERSION = '0.1.0';
6
+ export const VERSION = '0.2.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for Brave DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,6 +58,7 @@
58
58
  "@types/debug": "^4.1.12",
59
59
  "@types/filesystem": "^0.0.36",
60
60
  "@types/node": "^25.0.0",
61
+ "@types/semver": "^7.7.1",
61
62
  "@types/sinon": "^21.0.0",
62
63
  "@types/yargs": "^17.0.33",
63
64
  "@typescript-eslint/eslint-plugin": "^8.43.0",
@@ -75,6 +76,7 @@
75
76
  "rollup": "4.60.1",
76
77
  "rollup-plugin-cleanup": "^3.2.1",
77
78
  "rollup-plugin-license": "^3.6.0",
79
+ "semver": "^7.7.4",
78
80
  "sinon": "^21.0.0",
79
81
  "tiktoken": "^1.0.22",
80
82
  "typescript": "^6.0.2",