brave-mcp 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  control and inspect a live Brave browser. It acts as a Model-Context-Protocol
9
9
  (MCP) server, giving your AI coding assistant access to the full power of
10
10
  DevTools for reliable automation, in-depth debugging, and performance analysis.
11
+ A [CLI](docs/cli.md) is also provided for use without MCP.
11
12
 
12
13
  ## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md)
13
14
 
@@ -692,6 +693,10 @@ The Brave DevTools MCP server supports the following configuration options:
692
693
  Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
693
694
  - **Type:** boolean
694
695
 
696
+ - **`--experimentalWebmcp`/ `--experimental-webmcp`**
697
+ Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
698
+ - **Type:** boolean
699
+
695
700
  - **`--chromeArg`/ `--chrome-arg`**
696
701
  Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
697
702
  - **Type:** array
@@ -729,6 +734,11 @@ The Brave DevTools MCP server supports the following configuration options:
729
734
  Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
730
735
  - **Type:** boolean
731
736
 
737
+ - **`--redactNetworkHeaders`/ `--redact-network-headers`**
738
+ If true, redacts some of the network headers considered senstive before returning to the client.
739
+ - **Type:** boolean
740
+ - **Default:** `false`
741
+
732
742
  <!-- END AUTO GENERATED OPTIONS -->
733
743
 
734
744
  Pass them via the `args` property in the JSON configuration. For example:
@@ -13,7 +13,7 @@ import { PredefinedNetworkConditions } from './third_party/index.js';
13
13
  import { listPages } from './tools/pages.js';
14
14
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
15
15
  import { ExtensionRegistry, } from './utils/ExtensionRegistry.js';
16
- import { saveTemporaryFile } from './utils/files.js';
16
+ import { ensureExtension, saveTemporaryFile } from './utils/files.js';
17
17
  import { getNetworkMultiplierFromString } from './WaitForHelper.js';
18
18
  const DEFAULT_TIMEOUT = 5_000;
19
19
  const NAVIGATION_TIMEOUT = 10_000;
@@ -56,7 +56,7 @@ export class McpContext {
56
56
  uncaughtError: event => {
57
57
  collect(event);
58
58
  },
59
- issue: event => {
59
+ devtoolsAggregatedIssue: event => {
60
60
  collect(event);
61
61
  },
62
62
  };
@@ -590,9 +590,9 @@ export class McpContext {
590
590
  async saveTemporaryFile(data, filename) {
591
591
  return await saveTemporaryFile(data, filename);
592
592
  }
593
- async saveFile(data, filename) {
593
+ async saveFile(data, clientProvidedFilePath, extension) {
594
594
  try {
595
- const filePath = path.resolve(filename);
595
+ const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
596
596
  await fs.mkdir(path.dirname(filePath), { recursive: true });
597
597
  await fs.writeFile(filePath, data);
598
598
  return { filename: filePath };
@@ -48,6 +48,9 @@ export class McpPage {
48
48
  getInPageTools() {
49
49
  return this.inPageTools;
50
50
  }
51
+ getWebMcpTools() {
52
+ return this.pptrPage.webmcp.tools();
53
+ }
51
54
  get networkConditions() {
52
55
  return this.emulationSettings.networkConditions ?? null;
53
56
  }
@@ -122,16 +122,21 @@ export class McpResponse {
122
122
  #consoleDataOptions;
123
123
  #listExtensions;
124
124
  #listInPageTools;
125
+ #listWebMcpTools;
125
126
  #devToolsData;
126
127
  #tabId;
127
128
  #args;
128
129
  #page;
130
+ #redactNetworkHeaders = true;
129
131
  constructor(args) {
130
132
  this.#args = args;
131
133
  }
132
134
  setPage(page) {
133
135
  this.#page = page;
134
136
  }
137
+ setRedactNetworkHeaders(value) {
138
+ this.#redactNetworkHeaders = value;
139
+ }
135
140
  attachDevToolsData(data) {
136
141
  this.#devToolsData = data;
137
142
  }
@@ -158,6 +163,9 @@ export class McpResponse {
158
163
  this.#listInPageTools = true;
159
164
  }
160
165
  }
166
+ setListWebMcpTools() {
167
+ this.#listWebMcpTools = true;
168
+ }
161
169
  setIncludeNetworkRequests(value, options) {
162
170
  if (!value) {
163
171
  this.#networkRequestsOptions = undefined;
@@ -258,6 +266,9 @@ export class McpResponse {
258
266
  get snapshotParams() {
259
267
  return this.#snapshotParams;
260
268
  }
269
+ get listWebMcpTools() {
270
+ return this.#listWebMcpTools;
271
+ }
261
272
  async handle(toolName, context) {
262
273
  if (this.#includePages) {
263
274
  await context.createPagesSnapshot();
@@ -275,8 +286,8 @@ export class McpResponse {
275
286
  if (textSnapshot) {
276
287
  const formatter = new SnapshotFormatter(textSnapshot);
277
288
  if (this.#snapshotParams.filePath) {
278
- await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
279
- snapshot = this.#snapshotParams.filePath;
289
+ const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
290
+ snapshot = result.filename;
280
291
  }
281
292
  else {
282
293
  snapshot = formatter;
@@ -295,7 +306,8 @@ export class McpResponse {
295
306
  fetchData: true,
296
307
  requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
297
308
  responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
298
- saveFile: (data, filename) => context.saveFile(data, filename),
309
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
310
+ redactNetworkHeaders: this.#redactNetworkHeaders,
299
311
  });
300
312
  detailedNetworkRequest = formatter;
301
313
  }
@@ -337,6 +349,11 @@ export class McpResponse {
337
349
  inPageTools = await getToolGroup(page);
338
350
  page.inPageTools = inPageTools;
339
351
  }
352
+ let webmcpTools;
353
+ if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
354
+ const page = this.#page ?? context.getSelectedMcpPage();
355
+ webmcpTools = page.getWebMcpTools();
356
+ }
340
357
  let consoleMessages;
341
358
  if (this.#consoleDataOptions?.include) {
342
359
  if (!this.#page) {
@@ -399,7 +416,8 @@ export class McpResponse {
399
416
  selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
400
417
  this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
401
418
  fetchData: false,
402
- saveFile: (data, filename) => context.saveFile(data, filename),
419
+ saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
420
+ redactNetworkHeaders: this.#redactNetworkHeaders,
403
421
  })));
404
422
  }
405
423
  }
@@ -414,6 +432,7 @@ export class McpResponse {
414
432
  extensions,
415
433
  lighthouseResult: this.#attachedLighthouseResult,
416
434
  inPageTools,
435
+ webmcpTools,
417
436
  });
418
437
  }
419
438
  format(toolName, context, data) {
@@ -622,6 +641,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
622
641
  response.push(toolDefinitionsMessage);
623
642
  }
624
643
  }
644
+ if (this.#listWebMcpTools && data.webmcpTools) {
645
+ structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
646
+ name,
647
+ description,
648
+ inputSchema,
649
+ annotations,
650
+ }));
651
+ response.push('## WebMCP tools');
652
+ if (data.webmcpTools.length === 0) {
653
+ response.push('No WebMCP tools available.');
654
+ }
655
+ else {
656
+ const webmcpToolsMessage = data.webmcpTools
657
+ .map(tool => {
658
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
659
+ })
660
+ .join('\n');
661
+ response.push(webmcpToolsMessage);
662
+ }
663
+ }
625
664
  if (this.#networkRequestsOptions?.include && data.networkRequests) {
626
665
  const requests = data.networkRequests;
627
666
  response.push('## Network requests');
@@ -206,7 +206,7 @@ class PageEventSubscriber {
206
206
  async subscribe() {
207
207
  this.#resetIssueAggregator();
208
208
  this.#page.on('framenavigated', this.#onFrameNavigated);
209
- this.#session.on('Audits.issueAdded', this.#onIssueAdded);
209
+ this.#page.on('issue', this.#onIssueAdded);
210
210
  this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
211
211
  try {
212
212
  await this.#session.send('Audits.enable');
@@ -219,7 +219,7 @@ class PageEventSubscriber {
219
219
  this.#seenKeys.clear();
220
220
  this.#seenIssues.clear();
221
221
  this.#page.off('framenavigated', this.#onFrameNavigated);
222
- this.#session.off('Audits.issueAdded', this.#onIssueAdded);
222
+ this.#page.off('issue', this.#onIssueAdded);
223
223
  this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
224
224
  if (this.#issueAggregator) {
225
225
  this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
@@ -233,7 +233,7 @@ class PageEventSubscriber {
233
233
  return;
234
234
  }
235
235
  this.#seenIssues.add(event.data);
236
- this.#page.emit('issue', event.data);
236
+ this.#page.emit('devtoolsAggregatedIssue', event.data);
237
237
  };
238
238
  #onExceptionThrown = (event) => {
239
239
  this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
@@ -248,9 +248,8 @@ class PageEventSubscriber {
248
248
  this.#seenIssues.clear();
249
249
  this.#resetIssueAggregator();
250
250
  };
251
- #onIssueAdded = (data) => {
251
+ #onIssueAdded = (inspectorIssue) => {
252
252
  try {
253
- const inspectorIssue = data.issue;
254
253
  const issue = DevTools.createIssuesFromProtocolIssue(null,
255
254
  // @ts-expect-error Protocol types diverge.
256
255
  inspectorIssue)[0];
@@ -168,6 +168,10 @@ export const cliOptions = {
168
168
  type: 'boolean',
169
169
  describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
170
170
  },
171
+ experimentalWebmcp: {
172
+ type: 'boolean',
173
+ describe: 'Set to true to enable debugging WebMCP tools. Requires Brave with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
174
+ },
171
175
  braveArg: {
172
176
  type: 'array',
173
177
  describe: 'Additional arguments for Brave. Only applies when Brave is launched by brave-devtools-mcp.',
@@ -236,6 +240,11 @@ export const cliOptions = {
236
240
  describe: 'Set by Brave DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
237
241
  hidden: true,
238
242
  },
243
+ redactNetworkHeaders: {
244
+ type: 'boolean',
245
+ describe: 'If true, redacts some of the network headers considered senstive before returning to the client.',
246
+ default: false,
247
+ },
239
248
  };
240
249
  export function parseArguments(version, argv = process.argv) {
241
250
  const yargsInstance = yargs(hideBin(argv))
@@ -32,6 +32,7 @@ delete startCliOptions.viewport;
32
32
  // tools, they need to be enabled during CLI generation.
33
33
  delete startCliOptions.experimentalPageIdRouting;
34
34
  delete startCliOptions.experimentalVision;
35
+ delete startCliOptions.experimentalWebmcp;
35
36
  delete startCliOptions.experimentalInteropTools;
36
37
  delete startCliOptions.experimentalScreencast;
37
38
  delete startCliOptions.categoryEmulation;
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  * */
6
6
  import { isUtf8 } from 'node:buffer';
7
+ import { DevTools, } from '../third_party/index.js';
7
8
  const BODY_CONTEXT_SIZE_LIMIT = 10000;
8
9
  export class NetworkFormatter {
9
10
  #request;
@@ -40,8 +41,8 @@ export class NetworkFormatter {
40
41
  throw new Error('saveFile is not provided');
41
42
  }
42
43
  if (data) {
43
- await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath);
44
- this.#requestBodyFilePath = this.#options.requestFilePath;
44
+ const result = await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath, '.network-request');
45
+ this.#requestBodyFilePath = result.filename;
45
46
  }
46
47
  else {
47
48
  this.#requestBody = requestBodyNotAvailableMessage;
@@ -66,8 +67,8 @@ export class NetworkFormatter {
66
67
  if (!this.#options.saveFile) {
67
68
  throw new Error('saveFile is not provided');
68
69
  }
69
- await this.#options.saveFile(buffer, this.#options.responseFilePath);
70
- this.#responseBodyFilePath = this.#options.responseFilePath;
70
+ const result = await this.#options.saveFile(buffer, this.#options.responseFilePath, '.network-response');
71
+ this.#responseBodyFilePath = result.filename;
71
72
  }
72
73
  catch {
73
74
  // Flatten error handling for buffer() failure and save failure
@@ -96,6 +97,16 @@ export class NetworkFormatter {
96
97
  selectedInDevToolsUI: this.#options.selectedInDevToolsUI,
97
98
  };
98
99
  }
100
+ #redactNetworkHeaders(headers) {
101
+ const headersList = Object.entries(headers).map(item => {
102
+ return { name: item[0], value: item[1] };
103
+ });
104
+ const redacted = DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
105
+ return redacted.reduce((acc, item) => {
106
+ acc[item.name] = item.value;
107
+ return acc;
108
+ }, {});
109
+ }
99
110
  toJSONDetailed() {
100
111
  const redirectChain = this.#request.redirectChain();
101
112
  const formattedRedirectChain = redirectChain.reverse().map(request => {
@@ -105,15 +116,21 @@ export class NetworkFormatter {
105
116
  const formatter = new NetworkFormatter(request, {
106
117
  requestId: id,
107
118
  saveFile: this.#options.saveFile,
119
+ redactNetworkHeaders: this.#options.redactNetworkHeaders,
108
120
  });
109
121
  return formatter.toJSON();
110
122
  });
123
+ const responseHeaders = this.#request.response()?.headers();
111
124
  return {
112
125
  ...this.toJSON(),
113
- requestHeaders: this.#request.headers(),
126
+ requestHeaders: this.#options.redactNetworkHeaders
127
+ ? this.#redactNetworkHeaders(this.#request.headers())
128
+ : this.#request.headers(),
114
129
  requestBody: this.#requestBody,
115
130
  requestBodyFilePath: this.#requestBodyFilePath,
116
- responseHeaders: this.#request.response()?.headers(),
131
+ responseHeaders: this.#options.redactNetworkHeaders && responseHeaders
132
+ ? this.#redactNetworkHeaders(responseHeaders)
133
+ : this.#request.response()?.headers(),
117
134
  responseBody: this.#responseBody,
118
135
  responseBodyFilePath: this.#responseBodyFilePath,
119
136
  failure: this.#request.failure()?.errorText,
@@ -119,6 +119,10 @@ export async function createMcpServer(serverArgs, options) {
119
119
  !serverArgs.experimentalScreencast) {
120
120
  return;
121
121
  }
122
+ if (tool.annotations.conditions?.includes('experimentalWebmcp') &&
123
+ !serverArgs.experimentalWebmcp) {
124
+ return;
125
+ }
122
126
  const schema = 'pageScoped' in tool &&
123
127
  tool.pageScoped &&
124
128
  serverArgs.experimentalPageIdRouting &&
@@ -141,6 +145,7 @@ export async function createMcpServer(serverArgs, options) {
141
145
  const response = serverArgs.slim
142
146
  ? new SlimMcpResponse(serverArgs)
143
147
  : new McpResponse(serverArgs);
148
+ response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
144
149
  if ('pageScoped' in tool && tool.pageScoped) {
145
150
  const page = serverArgs.experimentalPageIdRouting &&
146
151
  params.pageId &&
@@ -189,6 +194,8 @@ export async function createMcpServer(serverArgs, options) {
189
194
  finally {
190
195
  void clearcutLogger?.logToolInvocation({
191
196
  toolName: tool.name,
197
+ params,
198
+ schema,
192
199
  success,
193
200
  latencyMs: bucketizeLatency(Date.now() - startTime),
194
201
  });
@@ -38,14 +38,15 @@ export function getZodType(zodType) {
38
38
  throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
39
39
  }
40
40
  export function transformArgName(zodType, name) {
41
+ const snakeCaseName = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
41
42
  if (zodType === 'ZodString') {
42
- return `${name}_length`;
43
+ return `${snakeCaseName}_length`;
43
44
  }
44
45
  else if (zodType === 'ZodArray') {
45
- return `${name}_count`;
46
+ return `${snakeCaseName}_count`;
46
47
  }
47
48
  else {
48
- return name;
49
+ return snakeCaseName;
49
50
  }
50
51
  }
51
52
  export function transformArgType(zodType) {
@@ -168,15 +169,21 @@ export class ClearcutLogger {
168
169
  }
169
170
  }
170
171
  async logToolInvocation(args) {
172
+ const tool_invocation = {
173
+ tool_name: args.toolName,
174
+ success: args.success,
175
+ latency_ms: args.latencyMs,
176
+ };
177
+ if (Object.keys(args.params).length > 0) {
178
+ tool_invocation.tool_params = {
179
+ [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema),
180
+ };
181
+ }
171
182
  this.#watchdog.send({
172
183
  type: WatchdogMessageType.LOG_EVENT,
173
184
  payload: {
174
185
  mcp_client: this.#mcpClient,
175
- tool_invocation: {
176
- tool_name: args.toolName,
177
- success: args.success,
178
- latency_ms: args.latencyMs,
179
- },
186
+ tool_invocation: tool_invocation,
180
187
  },
181
188
  });
182
189
  }
@@ -42,13 +42,14 @@ export class ClearcutSender {
42
42
  this.#sessionId = crypto.randomUUID();
43
43
  this.#sessionCreated = Date.now();
44
44
  }
45
- logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
46
- this.#addToBuffer({
45
+ const eventToSend = {
47
46
  ...event,
48
47
  session_id: this.#sessionId,
49
48
  app_version: this.#appVersion,
50
49
  os_type: this.#osType,
51
- });
50
+ };
51
+ logger('Enqueing telemetry event', JSON.stringify(eventToSend, null, 2));
52
+ this.#addToBuffer(eventToSend);
52
53
  if (!this.#timerStarted) {
53
54
  this.#timerStarted = true;
54
55
  this.#scheduleFlush(this.#flushIntervalMs);