brave-mcp 1.7.1 → 1.8.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 +18 -10
- package/build/src/McpContext.js +47 -24
- package/build/src/McpPage.js +2 -2
- package/build/src/McpResponse.js +17 -4
- package/build/src/ToolHandler.js +66 -15
- package/build/src/bin/brave-devtools-mcp-main.js +2 -2
- package/build/src/bin/brave-devtools.js +20 -28
- package/build/src/browser.js +3 -1
- package/build/src/{PageCollector.js → collectors/PageCollector.js} +4 -4
- package/build/src/{ServiceWorkerCollector.js → collectors/ServiceWorkerCollector.js} +1 -1
- package/build/src/{bin/brave-devtools-cli-options.js → config/cli-options.js} +295 -20
- package/build/src/{bin/brave-devtools-mcp-cli-options.js → config/mcp-options.js} +74 -8
- package/build/src/daemon/client.js +11 -2
- package/build/src/daemon/daemon.js +2 -2
- package/build/src/devtools/DevtoolsUtils.js +23 -34
- package/build/src/formatters/ConsoleFormatter.js +1 -1
- package/build/src/formatters/HeapSnapshotFormatter.js +8 -0
- package/build/src/index.js +33 -5
- package/build/src/{HeapSnapshotManager.js → processors/HeapSnapshotManager.js} +21 -5
- package/build/src/{trace-processing/parse.js → processors/PerformanceTrace.js} +1 -1
- package/build/src/telemetry/ClearcutLogger.js +13 -1
- package/build/src/telemetry/types.js +4 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +4 -63
- package/build/src/third_party/bundled-packages.json +3 -4
- package/build/src/third_party/devtools-heap-snapshot-worker.js +78 -20
- package/build/src/third_party/index.js +35586 -36331
- package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
- package/build/src/tools/console.js +3 -3
- package/build/src/tools/emulation.js +2 -2
- package/build/src/tools/extensions.js +7 -5
- package/build/src/tools/input.js +17 -10
- package/build/src/tools/lighthouse.js +3 -1
- package/build/src/tools/memory.js +112 -16
- package/build/src/tools/network.js +6 -3
- package/build/src/tools/pages.js +12 -16
- package/build/src/tools/performance.js +16 -24
- package/build/src/tools/pwa.js +11 -9
- package/build/src/tools/screencast.js +6 -4
- package/build/src/tools/screenshot.js +42 -9
- package/build/src/tools/script.js +19 -5
- package/build/src/tools/slim/tools.js +3 -3
- package/build/src/tools/snapshot.js +5 -3
- package/build/src/tools/thirdPartyDeveloper.js +2 -2
- package/build/src/tools/webmcp.js +2 -2
- package/build/src/{WaitForHelper.js → utils/WaitForHelper.js} +1 -1
- package/build/src/utils/bytes.js +105 -0
- package/build/src/utils/logger.js +25 -11
- package/build/src/version.js +1 -1
- package/package.json +12 -7
- package/skills/a11y-debugging/SKILL.md +2 -0
- package/skills/brave-devtools/SKILL.md +5 -4
- package/skills/brave-devtools-cli/SKILL.md +103 -64
- package/skills/debug-optimize-lcp/SKILL.md +9 -9
- package/skills/memory-leak-debugging/SKILL.md +6 -2
- package/skills/troubleshooting/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -597,7 +597,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
597
597
|
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
|
|
598
598
|
- [`screencast_start`](docs/tool-reference.md#screencast_start)
|
|
599
599
|
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
|
|
600
|
-
- **Memory** (
|
|
600
|
+
- **Memory** (13 tools)
|
|
601
601
|
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
|
|
602
602
|
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
|
|
603
603
|
- [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
|
|
@@ -610,6 +610,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
|
|
610
610
|
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
|
|
611
611
|
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
|
|
612
612
|
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
|
|
613
|
+
- [`query_heapsnapshot_objects`](docs/tool-reference.md#query_heapsnapshot_objects)
|
|
613
614
|
- **Extensions** (5 tools)
|
|
614
615
|
- [`install_extension`](docs/tool-reference.md#install_extension)
|
|
615
616
|
- [`list_extensions`](docs/tool-reference.md#list_extensions)
|
|
@@ -702,10 +703,10 @@ The Brave DevTools MCP server supports the following configuration option:
|
|
|
702
703
|
- **Type:** boolean
|
|
703
704
|
- **Default:** `false`
|
|
704
705
|
|
|
705
|
-
- **`--
|
|
706
|
-
|
|
706
|
+
- **`--pageIdRouting`/ `--page-id-routing`**
|
|
707
|
+
Require pageId on page-scoped tools and route requests by page ID (useful for concurrent agent sessions). Use --no-page-id-routing to disable.
|
|
707
708
|
- **Type:** boolean
|
|
708
|
-
- **Default:** `
|
|
709
|
+
- **Default:** `true`
|
|
709
710
|
|
|
710
711
|
- **`--experimentalDevtools`/ `--experimental-devtools`**
|
|
711
712
|
Whether to enable automation over DevTools targets
|
|
@@ -890,18 +891,25 @@ You can also run `npx brave-mcp@latest --help` to see all available configuratio
|
|
|
890
891
|
|
|
891
892
|
### Concurrent sessions
|
|
892
893
|
|
|
893
|
-
Most MCP clients start one Brave DevTools MCP server per conversation.
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
with.
|
|
894
|
+
Most MCP clients start one Brave DevTools MCP server per conversation.
|
|
895
|
+
By default, the server runs with `--pageIdRouting` enabled, making `pageId` a
|
|
896
|
+
required parameter on page-scoped tools (such as `click`, `fill`, `navigate_page`,
|
|
897
|
+
`take_snapshot`, etc.) so multiple agents or subagents sharing a server instance can
|
|
898
|
+
route tool calls directly to the specific tab they are working with.
|
|
899
|
+
|
|
900
|
+
For `evaluate_script`, `pageId` is required by default for targeting pages, but
|
|
901
|
+
becomes optional when `--categoryExtensions` is enabled so that `serviceWorkerId`
|
|
902
|
+
can be specified instead to evaluate inside an extension background service worker.
|
|
903
|
+
|
|
904
|
+
To disable this behavior and default to the currently selected page, pass
|
|
905
|
+
`--no-page-id-routing`.
|
|
898
906
|
|
|
899
907
|
```json
|
|
900
908
|
{
|
|
901
909
|
"mcpServers": {
|
|
902
910
|
"brave-devtools": {
|
|
903
911
|
"command": "npx",
|
|
904
|
-
"args": ["-y", "brave-mcp@latest"
|
|
912
|
+
"args": ["-y", "brave-mcp@latest"]
|
|
905
913
|
}
|
|
906
914
|
}
|
|
907
915
|
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -8,9 +8,9 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
10
|
import { overrideDevToolsGlobals } from './devtools/DevtoolsUtils.js';
|
|
11
|
-
import { HeapSnapshotManager } from './HeapSnapshotManager.js';
|
|
11
|
+
import { HeapSnapshotManager } from './processors/HeapSnapshotManager.js';
|
|
12
12
|
import { McpPage } from './McpPage.js';
|
|
13
|
-
import { ServiceWorkerConsoleCollector } from './ServiceWorkerCollector.js';
|
|
13
|
+
import { ServiceWorkerConsoleCollector } from './collectors/ServiceWorkerCollector.js';
|
|
14
14
|
import { Locator, } from './third_party/index.js';
|
|
15
15
|
import { listPages } from './tools/pages.js';
|
|
16
16
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
@@ -137,28 +137,30 @@ export class McpContext {
|
|
|
137
137
|
}
|
|
138
138
|
async validatePath(filePath) {
|
|
139
139
|
if (filePath === undefined) {
|
|
140
|
-
return;
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
let canonicalPath;
|
|
143
|
+
try {
|
|
144
|
+
canonicalPath = await resolveCanonicalPath(filePath);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
|
|
149
|
+
throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
|
|
141
150
|
}
|
|
142
151
|
// If the client never negotiated roots and the operator has explicitly
|
|
143
152
|
// opted into unrestricted access via --allow-unrestricted-paths, restore
|
|
144
153
|
// the previous permissive behavior and skip validation.
|
|
145
154
|
if (this.#roots === undefined && this.#allowUnrestrictedPaths) {
|
|
146
|
-
|
|
155
|
+
// Canonical path might not exist yet so we fallback to
|
|
156
|
+
// path.resolve(filePath). Consumers should not follow symlinks.
|
|
157
|
+
return canonicalPath || path.resolve(filePath);
|
|
147
158
|
}
|
|
148
159
|
// roots() always returns at least the temp directory, even if the
|
|
149
160
|
// connecting client never negotiated the optional `roots` capability.
|
|
150
161
|
// Path validation must not be skipped just because no workspace roots
|
|
151
162
|
// were configured.
|
|
152
163
|
const roots = this.roots();
|
|
153
|
-
let canonicalPath;
|
|
154
|
-
try {
|
|
155
|
-
canonicalPath = await resolveCanonicalPath(filePath);
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
159
|
-
console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
|
|
160
|
-
throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
|
|
161
|
-
}
|
|
162
164
|
let allowed = false;
|
|
163
165
|
const resolvedRoots = await Promise.allSettled(roots.map(async (root) => {
|
|
164
166
|
const rootPathUri = root.uri;
|
|
@@ -186,12 +188,12 @@ export class McpContext {
|
|
|
186
188
|
if (!allowed) {
|
|
187
189
|
throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
|
|
188
190
|
}
|
|
191
|
+
return canonicalPath || path.resolve(filePath);
|
|
189
192
|
}
|
|
190
193
|
async ensureExtension(filePath, extension) {
|
|
191
|
-
const
|
|
192
|
-
const currentExtension = path.extname(
|
|
193
|
-
const outputPath = `${
|
|
194
|
-
await this.validatePath(outputPath);
|
|
194
|
+
const resolved = await this.validatePath(filePath);
|
|
195
|
+
const currentExtension = path.extname(resolved);
|
|
196
|
+
const outputPath = `${resolved.slice(0, resolved.length - currentExtension.length)}${extension}`;
|
|
195
197
|
return outputPath;
|
|
196
198
|
}
|
|
197
199
|
async newPage(background, isolatedContextName) {
|
|
@@ -266,6 +268,21 @@ export class McpContext {
|
|
|
266
268
|
}
|
|
267
269
|
return page;
|
|
268
270
|
}
|
|
271
|
+
getSelectedMcpPageUrl(page) {
|
|
272
|
+
let targetPage = page;
|
|
273
|
+
if (!targetPage) {
|
|
274
|
+
try {
|
|
275
|
+
targetPage = this.getSelectedMcpPage();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (targetPage?.pptrPage?.isClosed() === false) {
|
|
282
|
+
return targetPage.pptrPage.url();
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
269
286
|
async getDevToolsData(page) {
|
|
270
287
|
const targetPage = page ?? this.#selectedPage;
|
|
271
288
|
if (!targetPage) {
|
|
@@ -441,16 +458,16 @@ export class McpContext {
|
|
|
441
458
|
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
|
|
442
459
|
}
|
|
443
460
|
async #writeFile(filepath, data) {
|
|
444
|
-
await this.validatePath(filepath);
|
|
461
|
+
const resolved = await this.validatePath(filepath);
|
|
445
462
|
try {
|
|
446
|
-
await fs.mkdir(path.dirname(
|
|
463
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
447
464
|
// Open the file with flags to:
|
|
448
465
|
// - O_WRONLY: Write-only
|
|
449
466
|
// - O_CREAT: Create if it doesn't exist
|
|
450
467
|
// - O_TRUNC: Truncate to zero length if it exists
|
|
451
468
|
// - O_NOFOLLOW: DO NOT follow symlinks.
|
|
452
469
|
// - 0o600: Permissions: read/write for owner, no permissions for others.
|
|
453
|
-
await fs.writeFile(
|
|
470
|
+
await fs.writeFile(resolved, data, {
|
|
454
471
|
flag: fs.constants.O_WRONLY |
|
|
455
472
|
fs.constants.O_CREAT |
|
|
456
473
|
fs.constants.O_TRUNC |
|
|
@@ -515,6 +532,9 @@ export class McpContext {
|
|
|
515
532
|
async getHeapSnapshotDuplicateStrings(filePath) {
|
|
516
533
|
return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
|
|
517
534
|
}
|
|
535
|
+
async queryHeapSnapshotObjects(filePath, options) {
|
|
536
|
+
return await this.#heapSnapshotManager.queryObjects(filePath, options);
|
|
537
|
+
}
|
|
518
538
|
async getHeapSnapshotStats(filePath) {
|
|
519
539
|
return await this.#heapSnapshotManager.getStats(filePath);
|
|
520
540
|
}
|
|
@@ -524,6 +544,9 @@ export class McpContext {
|
|
|
524
544
|
async getHeapSnapshotNativeContextSizes(filePath) {
|
|
525
545
|
return await this.#heapSnapshotManager.getNativeContextSizes(filePath);
|
|
526
546
|
}
|
|
547
|
+
async getHeapSnapshotRetainedByContextSummary(filePath) {
|
|
548
|
+
return await this.#heapSnapshotManager.getRetainedByContextSummary(filePath);
|
|
549
|
+
}
|
|
527
550
|
async getHeapSnapshotNodesById(filePath, id, filterName, objectId) {
|
|
528
551
|
return await this.#heapSnapshotManager.getNodesById(filePath, id, filterName, objectId);
|
|
529
552
|
}
|
|
@@ -582,15 +605,15 @@ export class McpContext {
|
|
|
582
605
|
return response.text();
|
|
583
606
|
}
|
|
584
607
|
case 'file:': {
|
|
585
|
-
await this.validatePath(fileURLToPath(url));
|
|
586
|
-
return await fs.readFile(
|
|
608
|
+
const resolved = await this.validatePath(fileURLToPath(url));
|
|
609
|
+
return await fs.readFile(resolved, 'utf-8');
|
|
587
610
|
}
|
|
588
611
|
default:
|
|
589
612
|
throw new Error(`Unsupported protocol for: ${url}`);
|
|
590
613
|
}
|
|
591
614
|
}
|
|
592
|
-
async getHeapSnapshotEdges(filePath, nodeId) {
|
|
593
|
-
return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
|
|
615
|
+
async getHeapSnapshotEdges(filePath, nodeId, options) {
|
|
616
|
+
return await this.#heapSnapshotManager.getEdges(filePath, nodeId, options);
|
|
594
617
|
}
|
|
595
618
|
async getHeapSnapshotClassDiffs(baseFilePath, currentFilePath) {
|
|
596
619
|
return await this.#heapSnapshotManager.getClassDiffs(baseFilePath, currentFilePath);
|
package/build/src/McpPage.js
CHANGED
|
@@ -102,14 +102,14 @@ export function replaceHtmlElementsWithUids(schema) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
import { createTargetUniverse, } from './devtools/DevtoolsUtils.js';
|
|
105
|
-
import { ConsoleCollector, NetworkCollector, } from './PageCollector.js';
|
|
105
|
+
import { ConsoleCollector, NetworkCollector, } from './collectors/PageCollector.js';
|
|
106
106
|
import { TextSnapshot } from './TextSnapshot.js';
|
|
107
107
|
import { PredefinedNetworkConditions, } from './third_party/index.js';
|
|
108
108
|
import { takeSnapshot } from './tools/snapshot.js';
|
|
109
109
|
const DEFAULT_TIMEOUT = 5_000;
|
|
110
110
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
111
111
|
import { logger } from './utils/logger.js';
|
|
112
|
-
import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
|
|
112
|
+
import { getNetworkMultiplierFromString, WaitForHelper, } from './utils/WaitForHelper.js';
|
|
113
113
|
/**
|
|
114
114
|
* Per-page state wrapper. Consolidates dialog, snapshot, emulation,
|
|
115
115
|
* and metadata that were previously scattered across Maps in McpContext.
|
package/build/src/McpResponse.js
CHANGED
|
@@ -8,11 +8,11 @@ import { HeapSnapshotFormatter, isEdgeLike, isNodeLike, } from './formatters/Hea
|
|
|
8
8
|
import { IssueFormatter } from './formatters/IssueFormatter.js';
|
|
9
9
|
import { NetworkFormatter } from './formatters/NetworkFormatter.js';
|
|
10
10
|
import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
|
|
11
|
-
import { UncaughtError } from './PageCollector.js';
|
|
11
|
+
import { UncaughtError } from './collectors/PageCollector.js';
|
|
12
12
|
import { TextSnapshot } from './TextSnapshot.js';
|
|
13
13
|
import { DevTools, getToonEncode, getGcfEncode } from './third_party/index.js';
|
|
14
14
|
import { handleDialog, listPages } from './tools/pages.js';
|
|
15
|
-
import { getInsightOutput, getTraceSummary } from './
|
|
15
|
+
import { getInsightOutput, getTraceSummary, } from './processors/PerformanceTrace.js';
|
|
16
16
|
import { stableIdSymbol } from './utils/id.js';
|
|
17
17
|
import { paginate } from './utils/pagination.js';
|
|
18
18
|
const { formatBytesToKb } = DevTools.I18n.ByteUtilities;
|
|
@@ -196,13 +196,14 @@ export class McpResponse {
|
|
|
196
196
|
pagination: options,
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
-
setHeapSnapshotStats(stats, staticData, nativeContextSizes) {
|
|
199
|
+
setHeapSnapshotStats(stats, staticData, nativeContextSizes, retainedByContextSummary) {
|
|
200
200
|
this.#heapSnapshotOptions = {
|
|
201
201
|
...this.#heapSnapshotOptions,
|
|
202
202
|
include: true,
|
|
203
203
|
stats,
|
|
204
204
|
staticData,
|
|
205
205
|
nativeContextSizes,
|
|
206
|
+
retainedByContextSummary,
|
|
206
207
|
};
|
|
207
208
|
}
|
|
208
209
|
setHeapSnapshotNodes(nodes, options) {
|
|
@@ -651,7 +652,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
651
652
|
for (const [insightName, model] of Object.entries(insightSet.model)) {
|
|
652
653
|
structuredContent.traceInsights.push({
|
|
653
654
|
insightName,
|
|
654
|
-
insightKey: model
|
|
655
|
+
insightKey: typeof model === 'object' &&
|
|
656
|
+
model !== null &&
|
|
657
|
+
'insightKey' in model
|
|
658
|
+
? model.insightKey
|
|
659
|
+
: undefined,
|
|
655
660
|
});
|
|
656
661
|
}
|
|
657
662
|
}
|
|
@@ -719,6 +724,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
719
724
|
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
720
725
|
structuredContent.heapSnapshot.nativeContextSizes = nativeContextSizes;
|
|
721
726
|
}
|
|
727
|
+
const retainedByContextSummary = this.#heapSnapshotOptions.retainedByContextSummary;
|
|
728
|
+
if (retainedByContextSummary) {
|
|
729
|
+
response.push('### Retained by Context Summary');
|
|
730
|
+
response.push(HeapSnapshotFormatter.formatRetainedByContextSummary(retainedByContextSummary));
|
|
731
|
+
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
732
|
+
structuredContent.heapSnapshot.retainedByContextSummary =
|
|
733
|
+
retainedByContextSummary;
|
|
734
|
+
}
|
|
722
735
|
const aggregateData = this.#heapSnapshotOptions.aggregateData;
|
|
723
736
|
if (aggregateData) {
|
|
724
737
|
const sortedEntries = HeapSnapshotFormatter.sort(aggregateData.aggregates);
|
package/build/src/ToolHandler.js
CHANGED
|
@@ -11,6 +11,8 @@ import { zod } from './third_party/index.js';
|
|
|
11
11
|
import { labels, OFF_BY_DEFAULT_CATEGORIES } from './tools/categories.js';
|
|
12
12
|
import { pageIdSchema } from './tools/ToolDefinition.js';
|
|
13
13
|
import { logger } from './utils/logger.js';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
import { isLocalhost } from './utils/url.js';
|
|
14
16
|
export function buildFlag(category) {
|
|
15
17
|
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
|
16
18
|
}
|
|
@@ -82,6 +84,66 @@ function buildUnknownArgumentsMessage(toolName, unknownArgumentNames, expectedAr
|
|
|
82
84
|
const correction = unknownArgumentNames.length === 1 ? 'Remove it' : 'Remove them';
|
|
83
85
|
return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`;
|
|
84
86
|
}
|
|
87
|
+
async function validateAndResolvePathOrUrl(filePathOrUrl, context) {
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL(filePathOrUrl);
|
|
90
|
+
if (url.protocol === 'file:') {
|
|
91
|
+
return pathToFileURL(await context.validatePath(fileURLToPath(url))).href;
|
|
92
|
+
}
|
|
93
|
+
else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) {
|
|
94
|
+
return filePathOrUrl;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Suppress parsing errors for regular file paths.
|
|
99
|
+
}
|
|
100
|
+
return await context.validatePath(filePathOrUrl);
|
|
101
|
+
}
|
|
102
|
+
function isLocalBrowser(context) {
|
|
103
|
+
if (context.browser.process()) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
const wsEndpoint = context.browser.wsEndpoint();
|
|
107
|
+
if (wsEndpoint && isLocalhost(wsEndpoint)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function shouldValidateFile(option, isLocal) {
|
|
113
|
+
if (option === true) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (typeof option === 'object' && option !== null) {
|
|
117
|
+
if (isLocal) {
|
|
118
|
+
return Boolean(option.local);
|
|
119
|
+
}
|
|
120
|
+
return Boolean(option.remote);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
async function validateToolFiles(tool, params, context) {
|
|
125
|
+
const isLocal = isLocalBrowser(context);
|
|
126
|
+
for (const [key, option] of Object.entries(tool.verifyFilesSchema)) {
|
|
127
|
+
if (shouldValidateFile(option, isLocal)) {
|
|
128
|
+
const val = params[key];
|
|
129
|
+
if (typeof val === 'string') {
|
|
130
|
+
params[key] = await validateAndResolvePathOrUrl(val, context);
|
|
131
|
+
}
|
|
132
|
+
else if (Array.isArray(val)) {
|
|
133
|
+
const updated = [];
|
|
134
|
+
for (const item of val) {
|
|
135
|
+
if (typeof item === 'string') {
|
|
136
|
+
updated.push(await validateAndResolvePathOrUrl(item, context));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new Error('Unexpected non-string value as a file path or URL');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
params[key] = updated;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
85
147
|
export class ToolHandler {
|
|
86
148
|
tool;
|
|
87
149
|
serverArgs;
|
|
@@ -102,7 +164,7 @@ export class ToolHandler {
|
|
|
102
164
|
this.inputSchema =
|
|
103
165
|
'pageScoped' in tool &&
|
|
104
166
|
tool.pageScoped &&
|
|
105
|
-
serverArgs.
|
|
167
|
+
serverArgs.pageIdRouting &&
|
|
106
168
|
!serverArgs.slim
|
|
107
169
|
? { ...pageIdSchema, ...tool.schema }
|
|
108
170
|
: tool.schema;
|
|
@@ -153,19 +215,11 @@ export class ToolHandler {
|
|
|
153
215
|
}
|
|
154
216
|
let page;
|
|
155
217
|
try {
|
|
156
|
-
|
|
157
|
-
for (const key of this.tool.verifyFilesSchema) {
|
|
158
|
-
const filePath = params[key];
|
|
159
|
-
const paths = Array.isArray(filePath) ? filePath : [filePath];
|
|
160
|
-
for (const path of paths) {
|
|
161
|
-
await context.validatePath(path);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
218
|
+
await validateToolFiles(this.tool, params, context);
|
|
165
219
|
if (isPageScopedTool(this.tool)) {
|
|
166
220
|
const pageId = typeof params.pageId === 'number' ? params.pageId : undefined;
|
|
167
221
|
page =
|
|
168
|
-
this.serverArgs.
|
|
222
|
+
this.serverArgs.pageIdRouting &&
|
|
169
223
|
pageId !== undefined &&
|
|
170
224
|
!this.serverArgs.slim
|
|
171
225
|
? context.getPageById(pageId)
|
|
@@ -189,10 +243,7 @@ export class ToolHandler {
|
|
|
189
243
|
response.setError(err);
|
|
190
244
|
}
|
|
191
245
|
devToolsData = await context.getDevToolsData(page);
|
|
192
|
-
|
|
193
|
-
if (targetPage?.pptrPage?.isClosed() === false) {
|
|
194
|
-
pageUrl = targetPage.pptrPage.url();
|
|
195
|
-
}
|
|
246
|
+
pageUrl = context.getSelectedMcpPageUrl(page);
|
|
196
247
|
// Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat
|
|
197
248
|
let dataFormat = 'default';
|
|
198
249
|
if (this.serverArgs.experimentalDataFormat) {
|
|
@@ -13,7 +13,7 @@ import { StdioServerTransport } from '../third_party/index.js';
|
|
|
13
13
|
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
14
14
|
import { logger, saveLogsToFile } from '../utils/logger.js';
|
|
15
15
|
import { VERSION } from '../version.js';
|
|
16
|
-
import {
|
|
16
|
+
import { mcpOptions, parseArguments } from '../config/mcp-options.js';
|
|
17
17
|
await checkForUpdates('Run `npm install brave-mcp@latest` to update.');
|
|
18
18
|
export const args = parseArguments(VERSION);
|
|
19
19
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
@@ -68,5 +68,5 @@ await server.connect(transport);
|
|
|
68
68
|
logger?.('Brave DevTools MCP Server connected');
|
|
69
69
|
logDisclaimers(args);
|
|
70
70
|
void ClearcutLogger.get()?.logDailyActiveIfNeeded();
|
|
71
|
-
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args,
|
|
71
|
+
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, mcpOptions));
|
|
72
72
|
//# sourceMappingURL=brave-devtools-mcp-main.js.map
|
|
@@ -12,34 +12,26 @@ import { logDisclaimers } from '../index.js';
|
|
|
12
12
|
import { hideBin, yargs } from '../third_party/index.js';
|
|
13
13
|
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
14
14
|
import { VERSION } from '../version.js';
|
|
15
|
-
import { commands } from '
|
|
16
|
-
import {
|
|
15
|
+
import { commands } from '../config/cli-options.js';
|
|
16
|
+
import { mcpOptions, parseArguments, getMcpOptionsForViaCli, } from '../config/mcp-options.js';
|
|
17
17
|
await checkForUpdates('Run `npm install -g brave-mcp@latest` and `brave-devtools start` to update and restart the daemon.');
|
|
18
|
+
const DEFAULT_CLI_ARGS = ['--viaCli'];
|
|
18
19
|
async function start(args, sessionId) {
|
|
19
|
-
const combinedArgs = [...
|
|
20
|
+
const combinedArgs = [...DEFAULT_CLI_ARGS, ...args];
|
|
20
21
|
await startDaemon(combinedArgs, sessionId);
|
|
21
22
|
logDisclaimers(parseArguments(VERSION, combinedArgs));
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
// Missing CLI serialization.
|
|
28
|
-
delete
|
|
29
|
-
// Change the defaults for the CLI.
|
|
30
|
-
delete
|
|
31
|
-
delete
|
|
32
|
-
|
|
33
|
-
if (!('default' in cliOptions.headless)) {
|
|
34
|
-
throw new Error('headless cli option unexpectedly does not have a default');
|
|
24
|
+
function getCliOptions() {
|
|
25
|
+
const options = {
|
|
26
|
+
...getMcpOptionsForViaCli(),
|
|
27
|
+
};
|
|
28
|
+
// Missing CLI serialization.
|
|
29
|
+
delete options.viewport;
|
|
30
|
+
// Change the defaults for the CLI.
|
|
31
|
+
delete options.experimentalStructuredContent;
|
|
32
|
+
delete options.experimentalInteropTools;
|
|
33
|
+
return options;
|
|
35
34
|
}
|
|
36
|
-
if ('default' in cliOptions.isolated) {
|
|
37
|
-
throw new Error('isolated cli option unexpectedly has a default');
|
|
38
|
-
}
|
|
39
|
-
startCliOptions.headless.default = true;
|
|
40
|
-
startCliOptions.isolated.description =
|
|
41
|
-
'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.';
|
|
42
|
-
startCliOptions.categoryExtensions.default = true;
|
|
43
35
|
const y = yargs(hideBin(process.argv))
|
|
44
36
|
.locale('en') // Force English to ensure error string matching works in .fail, all custom messages we output are in English anyways
|
|
45
37
|
.scriptName('brave-devtools')
|
|
@@ -71,9 +63,9 @@ const y = yargs(hideBin(process.argv))
|
|
|
71
63
|
console.error('💡 TIP FOR AI AGENT / DEVELOPER:');
|
|
72
64
|
console.error('In the `brave-devtools` CLI:');
|
|
73
65
|
console.error('1. Required parameters MUST be passed as positional arguments (without flags).');
|
|
74
|
-
console.error(' - INCORRECT: brave-devtools
|
|
75
|
-
console.error(' - CORRECT: brave-devtools
|
|
76
|
-
console.error('2. Optional parameters are passed as double-dash options/flags (e.g. --
|
|
66
|
+
console.error(' - INCORRECT: brave-devtools click --pageId 1 --uid "1_2"');
|
|
67
|
+
console.error(' - CORRECT: brave-devtools click 1 "1_2"');
|
|
68
|
+
console.error('2. Optional parameters are passed as double-dash options/flags (e.g. --dblClick true).');
|
|
77
69
|
console.error('3. Make sure to escape quotes properly for your shell environment.');
|
|
78
70
|
console.error('Run `brave-devtools <command> --help` to see exact positional and optional parameters.');
|
|
79
71
|
console.error('=========================================');
|
|
@@ -85,7 +77,7 @@ const y = yargs(hideBin(process.argv))
|
|
|
85
77
|
process.exit(1);
|
|
86
78
|
});
|
|
87
79
|
y.command('start', 'Start or restart brave-devtools-mcp', y => y
|
|
88
|
-
.options(
|
|
80
|
+
.options(getCliOptions())
|
|
89
81
|
.example('$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser')
|
|
90
82
|
.strict(), async (argv) => {
|
|
91
83
|
if (isDaemonRunning(argv.sessionId)) {
|
|
@@ -106,7 +98,7 @@ y.command('start', 'Start or restart brave-devtools-mcp', y => y
|
|
|
106
98
|
argv.headless = true;
|
|
107
99
|
}
|
|
108
100
|
}
|
|
109
|
-
const args = serializeArgs(
|
|
101
|
+
const args = serializeArgs(mcpOptions, argv);
|
|
110
102
|
await start(args, argv.sessionId);
|
|
111
103
|
process.exit(0);
|
|
112
104
|
}).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself
|
|
@@ -200,7 +192,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
|
|
|
200
192
|
? verifyDaemonVersion(sessionId, VERSION)
|
|
201
193
|
: Promise.resolve(undefined);
|
|
202
194
|
if (!isDaemonRunning(sessionId)) {
|
|
203
|
-
await start(serializeArgs(
|
|
195
|
+
await start(serializeArgs(mcpOptions, argv), sessionId);
|
|
204
196
|
}
|
|
205
197
|
const commandArgs = {};
|
|
206
198
|
for (const argName of Object.keys(args)) {
|
package/build/src/browser.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { puppeteer } from './third_party/index.js';
|
|
11
|
-
import { logger } from './utils/logger.js';
|
|
11
|
+
import { logger, puppeteerLogger } from './utils/logger.js';
|
|
12
12
|
let browser;
|
|
13
13
|
let browserMode;
|
|
14
14
|
// Heavy pages (e.g. Studio module dev bundles >100MB) cannot ack
|
|
@@ -161,6 +161,7 @@ export async function ensureBrowserConnected(options) {
|
|
|
161
161
|
protocolTimeout: PROTOCOL_TIMEOUT_MILLISECONDS,
|
|
162
162
|
blocklist: options.blocklist,
|
|
163
163
|
allowlist: options.allowlist,
|
|
164
|
+
logger: puppeteerLogger,
|
|
164
165
|
};
|
|
165
166
|
let autoConnect = false;
|
|
166
167
|
if (options.wsEndpoint) {
|
|
@@ -281,6 +282,7 @@ export async function launch(options) {
|
|
|
281
282
|
protocolTimeout: PROTOCOL_TIMEOUT_MILLISECONDS,
|
|
282
283
|
blocklist: options.blocklist,
|
|
283
284
|
allowlist: options.allowlist,
|
|
285
|
+
logger: puppeteerLogger,
|
|
284
286
|
});
|
|
285
287
|
if (options.logFile) {
|
|
286
288
|
// FIXME: we are probably subscribing too late to catch startup logs. We
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { FakeIssuesManager } from '
|
|
7
|
-
import { DevTools } from '
|
|
8
|
-
import { createIdGenerator, stableIdSymbol, } from '
|
|
9
|
-
import { logger } from '
|
|
6
|
+
import { FakeIssuesManager } from '../devtools/DevtoolsUtils.js';
|
|
7
|
+
import { DevTools } from '../third_party/index.js';
|
|
8
|
+
import { createIdGenerator, stableIdSymbol, } from '../utils/id.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
10
|
export class UncaughtError {
|
|
11
11
|
details;
|
|
12
12
|
targetId;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { UncaughtError } from './PageCollector.js';
|
|
7
|
-
import { createIdGenerator, stableIdSymbol } from '
|
|
7
|
+
import { createIdGenerator, stableIdSymbol } from '../utils/id.js';
|
|
8
8
|
const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
|
|
9
9
|
export class ServiceWorkerSubscriber {
|
|
10
10
|
#target;
|