brave-real-browser-mcp-server 2.27.0 → 2.27.2

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.
@@ -349,6 +349,15 @@ connection.onInitialize(() => {
349
349
  codeActionProvider: {
350
350
  codeActionKinds: [CodeActionKind.QuickFix],
351
351
  },
352
+ // Advanced LSP Features
353
+ definitionProvider: true,
354
+ referencesProvider: true,
355
+ workspaceSymbolProvider: true,
356
+ documentSymbolProvider: true,
357
+ renameProvider: {
358
+ prepareProvider: true,
359
+ },
360
+ documentFormattingProvider: true,
352
361
  },
353
362
  };
354
363
  });
@@ -572,6 +581,236 @@ connection.onCodeAction((params) => {
572
581
  // ============================================================
573
582
  // HELPERS
574
583
  // ============================================================
584
+ // Store document contents and tool locations
585
+ const documentToolLocations = new Map();
586
+ function findToolLocations(document) {
587
+ const text = document.getText();
588
+ const locations = [];
589
+ const toolCallPattern = /(\w+)\s*\(/g;
590
+ let match;
591
+ while ((match = toolCallPattern.exec(text)) !== null) {
592
+ const toolName = match[1];
593
+ if (TOOL_DEFINITIONS[toolName]) {
594
+ const startPos = document.positionAt(match.index);
595
+ const endPos = document.positionAt(match.index + toolName.length);
596
+ locations.push({
597
+ name: toolName,
598
+ range: { start: startPos, end: endPos },
599
+ uri: document.uri,
600
+ });
601
+ }
602
+ }
603
+ documentToolLocations.set(document.uri, locations);
604
+ return locations;
605
+ }
606
+ // ============================================================
607
+ // GO TO DEFINITION
608
+ // ============================================================
609
+ connection.onDefinition((params) => {
610
+ const document = documents.get(params.textDocument.uri);
611
+ if (!document)
612
+ return null;
613
+ const text = document.getText();
614
+ const offset = document.offsetAt(params.position);
615
+ // Find word at position
616
+ const wordStart = text.substring(0, offset).search(/\w+$/);
617
+ if (wordStart < 0)
618
+ return null;
619
+ const wordEnd = offset + text.substring(offset).search(/[^\w]|$/);
620
+ const word = text.substring(wordStart, wordEnd);
621
+ const def = TOOL_DEFINITIONS[word];
622
+ if (def) {
623
+ // Return the first occurrence of this tool in the document
624
+ const pattern = new RegExp(`\\b${word}\\s*\\(`);
625
+ const match = text.match(pattern);
626
+ if (match && match.index !== undefined) {
627
+ const startPos = document.positionAt(match.index);
628
+ const endPos = document.positionAt(match.index + word.length);
629
+ return {
630
+ uri: params.textDocument.uri,
631
+ range: { start: startPos, end: endPos },
632
+ };
633
+ }
634
+ }
635
+ return null;
636
+ });
637
+ // ============================================================
638
+ // FIND ALL REFERENCES
639
+ // ============================================================
640
+ connection.onReferences((params) => {
641
+ const document = documents.get(params.textDocument.uri);
642
+ if (!document)
643
+ return [];
644
+ const text = document.getText();
645
+ const offset = document.offsetAt(params.position);
646
+ // Find word at position
647
+ const wordStart = text.substring(0, offset).search(/\w+$/);
648
+ if (wordStart < 0)
649
+ return [];
650
+ const wordEnd = offset + text.substring(offset).search(/[^\w]|$/);
651
+ const word = text.substring(wordStart, wordEnd);
652
+ const references = [];
653
+ // Search in all open documents
654
+ documents.all().forEach((doc) => {
655
+ const docText = doc.getText();
656
+ const pattern = new RegExp(`\\b${word}\\b`, 'g');
657
+ let match;
658
+ while ((match = pattern.exec(docText)) !== null) {
659
+ const startPos = doc.positionAt(match.index);
660
+ const endPos = doc.positionAt(match.index + word.length);
661
+ references.push({
662
+ uri: doc.uri,
663
+ range: { start: startPos, end: endPos },
664
+ });
665
+ }
666
+ });
667
+ return references;
668
+ });
669
+ // ============================================================
670
+ // DOCUMENT SYMBOLS
671
+ // ============================================================
672
+ connection.onDocumentSymbol((params) => {
673
+ const document = documents.get(params.textDocument.uri);
674
+ if (!document)
675
+ return [];
676
+ const text = document.getText();
677
+ const symbols = [];
678
+ // Find all tool calls as symbols
679
+ const toolCallPattern = /(\w+)\s*\(\s*\{[^}]*\}\s*\)/g;
680
+ let match;
681
+ while ((match = toolCallPattern.exec(text)) !== null) {
682
+ const toolName = match[1];
683
+ if (TOOL_DEFINITIONS[toolName]) {
684
+ const startPos = document.positionAt(match.index);
685
+ const endPos = document.positionAt(match.index + match[0].length);
686
+ symbols.push({
687
+ name: toolName,
688
+ kind: 12, // Function
689
+ range: { start: startPos, end: endPos },
690
+ selectionRange: { start: startPos, end: document.positionAt(match.index + toolName.length) },
691
+ detail: TOOL_DEFINITIONS[toolName].category,
692
+ });
693
+ }
694
+ }
695
+ return symbols;
696
+ });
697
+ // ============================================================
698
+ // WORKSPACE SYMBOLS
699
+ // ============================================================
700
+ connection.onWorkspaceSymbol((params) => {
701
+ const query = params.query.toLowerCase();
702
+ const symbols = [];
703
+ // Return all matching tool definitions
704
+ for (const [name, def] of Object.entries(TOOL_DEFINITIONS)) {
705
+ if (name.toLowerCase().includes(query) || def.description.toLowerCase().includes(query)) {
706
+ symbols.push({
707
+ name: name,
708
+ kind: 12, // Function
709
+ location: {
710
+ uri: 'brave-mcp://tools/' + name,
711
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: name.length } },
712
+ },
713
+ containerName: def.category,
714
+ });
715
+ }
716
+ }
717
+ return symbols;
718
+ });
719
+ // ============================================================
720
+ // RENAME SYMBOL
721
+ // ============================================================
722
+ connection.onPrepareRename((params) => {
723
+ const document = documents.get(params.textDocument.uri);
724
+ if (!document)
725
+ return null;
726
+ const text = document.getText();
727
+ const offset = document.offsetAt(params.position);
728
+ // Find word at position
729
+ const wordStart = text.substring(0, offset).search(/\w+$/);
730
+ if (wordStart < 0)
731
+ return null;
732
+ const wordEnd = offset + text.substring(offset).search(/[^\w]|$/);
733
+ const word = text.substring(wordStart, wordEnd);
734
+ // Don't allow renaming built-in tools
735
+ if (TOOL_DEFINITIONS[word]) {
736
+ return { message: 'Cannot rename built-in MCP tool' };
737
+ }
738
+ const startPos = document.positionAt(wordStart);
739
+ const endPos = document.positionAt(wordEnd);
740
+ return { range: { start: startPos, end: endPos }, placeholder: word };
741
+ });
742
+ connection.onRenameRequest((params) => {
743
+ const document = documents.get(params.textDocument.uri);
744
+ if (!document)
745
+ return null;
746
+ const text = document.getText();
747
+ const offset = document.offsetAt(params.position);
748
+ // Find word at position
749
+ const wordStart = text.substring(0, offset).search(/\w+$/);
750
+ if (wordStart < 0)
751
+ return null;
752
+ const wordEnd = offset + text.substring(offset).search(/[^\w]|$/);
753
+ const oldName = text.substring(wordStart, wordEnd);
754
+ const newName = params.newName;
755
+ // Don't allow renaming built-in tools
756
+ if (TOOL_DEFINITIONS[oldName]) {
757
+ return null;
758
+ }
759
+ const changes = {};
760
+ // Replace all occurrences in all open documents
761
+ documents.all().forEach((doc) => {
762
+ const docText = doc.getText();
763
+ const pattern = new RegExp(`\\b${oldName}\\b`, 'g');
764
+ const edits = [];
765
+ let match;
766
+ while ((match = pattern.exec(docText)) !== null) {
767
+ const startPos = doc.positionAt(match.index);
768
+ const endPos = doc.positionAt(match.index + oldName.length);
769
+ edits.push({
770
+ range: { start: startPos, end: endPos },
771
+ newText: newName,
772
+ });
773
+ }
774
+ if (edits.length > 0) {
775
+ changes[doc.uri] = edits;
776
+ }
777
+ });
778
+ return { changes };
779
+ });
780
+ // ============================================================
781
+ // FORMAT DOCUMENT
782
+ // ============================================================
783
+ connection.onDocumentFormatting((params) => {
784
+ const document = documents.get(params.textDocument.uri);
785
+ if (!document)
786
+ return [];
787
+ const text = document.getText();
788
+ const edits = [];
789
+ // Format MCP tool calls consistently
790
+ let formatted = text
791
+ // Normalize tool call opening: tool({
792
+ .replace(/(\w+)\s*\(\s*\{\s*/g, '$1({ ')
793
+ // Normalize tool call closing: })
794
+ .replace(/\s*\}\s*\)/g, ' })')
795
+ // Fix parameter spacing: key: value
796
+ .replace(/(\w+)\s*:\s*/g, '$1: ')
797
+ // Remove trailing commas before }
798
+ .replace(/,\s*\}/g, ' }')
799
+ // Normalize await spacing
800
+ .replace(/await\s+/g, 'await ');
801
+ if (formatted !== text) {
802
+ edits.push({
803
+ range: {
804
+ start: { line: 0, character: 0 },
805
+ end: document.positionAt(text.length),
806
+ },
807
+ newText: formatted,
808
+ });
809
+ }
810
+ return edits;
811
+ });
812
+ // ============================================================
813
+ // HELPERS
575
814
  function formatToolDoc(def) {
576
815
  const parts = [];
577
816
  parts.push(`## ${def.name}`);
@@ -7,9 +7,103 @@ export const SERVER_INFO = {
7
7
  // MCP capabilities with LSP-compatible streaming support
8
8
  export const CAPABILITIES = {
9
9
  tools: {},
10
- resources: {},
11
- prompts: {},
10
+ resources: { subscribe: true, listChanged: true },
11
+ prompts: { listChanged: true },
12
12
  };
13
+ // MCP Resources - Dynamic browser state resources
14
+ export const RESOURCES = [
15
+ {
16
+ uri: 'browser://state',
17
+ name: 'Browser State',
18
+ description: 'Current browser instance state including page URL, title, and session info',
19
+ mimeType: 'application/json',
20
+ },
21
+ {
22
+ uri: 'browser://page/content',
23
+ name: 'Page Content',
24
+ description: 'Current page HTML content',
25
+ mimeType: 'text/html',
26
+ },
27
+ {
28
+ uri: 'browser://page/text',
29
+ name: 'Page Text',
30
+ description: 'Current page text content (no HTML)',
31
+ mimeType: 'text/plain',
32
+ },
33
+ {
34
+ uri: 'browser://cookies',
35
+ name: 'Browser Cookies',
36
+ description: 'All cookies in current browser session',
37
+ mimeType: 'application/json',
38
+ },
39
+ {
40
+ uri: 'browser://network/requests',
41
+ name: 'Network Requests',
42
+ description: 'Recent network requests captured during browsing',
43
+ mimeType: 'application/json',
44
+ },
45
+ {
46
+ uri: 'browser://console/logs',
47
+ name: 'Console Logs',
48
+ description: 'Browser console log messages',
49
+ mimeType: 'application/json',
50
+ },
51
+ ];
52
+ // MCP Prompts - Reusable automation workflows
53
+ export const PROMPTS = [
54
+ {
55
+ name: 'scrape_website',
56
+ description: 'Scrape content from a website with automatic navigation and extraction',
57
+ arguments: [
58
+ { name: 'url', description: 'URL to scrape', required: true },
59
+ { name: 'selector', description: 'CSS selector for target content', required: false },
60
+ { name: 'outputFormat', description: 'Output format (json, markdown, text)', required: false },
61
+ ],
62
+ },
63
+ {
64
+ name: 'extract_download_links',
65
+ description: 'Extract all download links from a page with quality and size info',
66
+ arguments: [
67
+ { name: 'url', description: 'Page URL to extract from', required: true },
68
+ { name: 'fileTypes', description: 'File types to extract (mp4, mkv, pdf, etc.)', required: false },
69
+ ],
70
+ },
71
+ {
72
+ name: 'monitor_page_changes',
73
+ description: 'Monitor a page for changes and notify when content updates',
74
+ arguments: [
75
+ { name: 'url', description: 'URL to monitor', required: true },
76
+ { name: 'selector', description: 'Element to watch for changes', required: true },
77
+ { name: 'interval', description: 'Check interval in seconds', required: false },
78
+ ],
79
+ },
80
+ {
81
+ name: 'automate_login',
82
+ description: 'Automate login to a website with credentials',
83
+ arguments: [
84
+ { name: 'url', description: 'Login page URL', required: true },
85
+ { name: 'usernameSelector', description: 'Username field selector', required: true },
86
+ { name: 'passwordSelector', description: 'Password field selector', required: true },
87
+ { name: 'submitSelector', description: 'Submit button selector', required: true },
88
+ ],
89
+ },
90
+ {
91
+ name: 'batch_screenshot',
92
+ description: 'Take screenshots of multiple URLs',
93
+ arguments: [
94
+ { name: 'urls', description: 'Comma-separated list of URLs', required: true },
95
+ { name: 'outputDir', description: 'Directory to save screenshots', required: true },
96
+ ],
97
+ },
98
+ {
99
+ name: 'extract_video_stream',
100
+ description: 'Extract video streaming URL from a page (m3u8, mp4)',
101
+ arguments: [
102
+ { name: 'url', description: 'Video page URL', required: true },
103
+ { name: 'quality', description: 'Preferred quality (highest, 1080p, 720p)', required: false },
104
+ ],
105
+ },
106
+ ];
13
107
  // Extended capabilities for streaming and auto-sync (for documentation/client info)
14
108
  export const EXTENDED_CAPABILITIES = {
15
109
  streaming: true,
@@ -17,10 +17,10 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
17
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
18
18
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
19
19
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
20
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, InitializeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
21
21
  import { randomUUID } from 'crypto';
22
22
  // Import core modules
23
- import { TOOLS, SERVER_INFO, CAPABILITIES, TOOL_NAMES } from './tool-definitions.js';
23
+ import { TOOLS, SERVER_INFO, CAPABILITIES, TOOL_NAMES, RESOURCES, PROMPTS } from './tool-definitions.js';
24
24
  import { getSharedEventBus } from './shared/event-bus.js';
25
25
  import { getProgressNotifier } from './transport/progress-notifier.js';
26
26
  import { getSessionManager } from './transport/session-manager.js';
@@ -69,8 +69,109 @@ mcpServer.setRequestHandler(InitializeRequestSchema, async (request) => {
69
69
  };
70
70
  });
71
71
  mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
72
- mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
73
- mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
72
+ mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: RESOURCES }));
73
+ mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
74
+ // Read Resource Handler
75
+ mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
76
+ const { uri } = request.params;
77
+ const page = getPageInstance();
78
+ debug(`ReadResource: ${uri}`);
79
+ try {
80
+ switch (uri) {
81
+ case 'browser://state': {
82
+ const state = page ? {
83
+ url: page.url(),
84
+ title: await page.title(),
85
+ isConnected: page.browser()?.connected ?? false,
86
+ viewport: page.viewport(),
87
+ } : { status: 'Browser not initialized' };
88
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(state, null, 2) }] };
89
+ }
90
+ case 'browser://page/content': {
91
+ if (!page)
92
+ throw new Error('Browser not initialized');
93
+ const html = await page.content();
94
+ return { contents: [{ uri, mimeType: 'text/html', text: html }] };
95
+ }
96
+ case 'browser://page/text': {
97
+ if (!page)
98
+ throw new Error('Browser not initialized');
99
+ const text = await page.evaluate(() => document.body.innerText);
100
+ return { contents: [{ uri, mimeType: 'text/plain', text }] };
101
+ }
102
+ case 'browser://cookies': {
103
+ if (!page)
104
+ throw new Error('Browser not initialized');
105
+ const cookies = await page.cookies();
106
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(cookies, null, 2) }] };
107
+ }
108
+ case 'browser://network/requests': {
109
+ // Return cached network requests from event bus
110
+ const history = eventBus.getHistory().filter(e => e.type.includes('network'));
111
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(history.slice(-50), null, 2) }] };
112
+ }
113
+ case 'browser://console/logs': {
114
+ const logs = eventBus.getHistory().filter(e => e.type.includes('console'));
115
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(logs.slice(-100), null, 2) }] };
116
+ }
117
+ default:
118
+ throw new Error(`Unknown resource: ${uri}`);
119
+ }
120
+ }
121
+ catch (error) {
122
+ const errorMessage = error instanceof Error ? error.message : String(error);
123
+ return { contents: [{ uri, mimeType: 'text/plain', text: `Error: ${errorMessage}` }] };
124
+ }
125
+ });
126
+ // Get Prompt Handler
127
+ mcpServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
128
+ const { name, arguments: args } = request.params;
129
+ debug(`GetPrompt: ${name}`);
130
+ const prompt = PROMPTS.find(p => p.name === name);
131
+ if (!prompt) {
132
+ throw new Error(`Unknown prompt: ${name}`);
133
+ }
134
+ // Generate prompt messages based on template
135
+ let messages = [];
136
+ switch (name) {
137
+ case 'scrape_website':
138
+ messages = [
139
+ { role: 'user', content: { type: 'text', text: `Scrape the website at ${args?.url || '[URL]'}. ${args?.selector ? `Focus on elements matching: ${args.selector}` : 'Extract main content.'}` } },
140
+ ];
141
+ break;
142
+ case 'extract_download_links':
143
+ messages = [
144
+ { role: 'user', content: { type: 'text', text: `Extract all download links from ${args?.url || '[URL]'}. ${args?.fileTypes ? `Filter for: ${args.fileTypes}` : 'Include all file types.'}` } },
145
+ ];
146
+ break;
147
+ case 'monitor_page_changes':
148
+ messages = [
149
+ { role: 'user', content: { type: 'text', text: `Monitor ${args?.url || '[URL]'} for changes in element: ${args?.selector || '[SELECTOR]'}. Check every ${args?.interval || 60} seconds.` } },
150
+ ];
151
+ break;
152
+ case 'automate_login':
153
+ messages = [
154
+ { role: 'user', content: { type: 'text', text: `Automate login to ${args?.url || '[URL]'}. Use ${args?.usernameSelector || '#username'} for username, ${args?.passwordSelector || '#password'} for password, and ${args?.submitSelector || 'button[type=submit]'} to submit.` } },
155
+ ];
156
+ break;
157
+ case 'batch_screenshot':
158
+ messages = [
159
+ { role: 'user', content: { type: 'text', text: `Take screenshots of: ${args?.urls || '[URLs]'}. Save to: ${args?.outputDir || './screenshots'}` } },
160
+ ];
161
+ break;
162
+ case 'extract_video_stream':
163
+ messages = [
164
+ { role: 'user', content: { type: 'text', text: `Extract video streaming URL from ${args?.url || '[URL]'}. Prefer quality: ${args?.quality || 'highest'}` } },
165
+ ];
166
+ break;
167
+ default:
168
+ messages = [{ role: 'user', content: { type: 'text', text: `Execute prompt: ${name}` } }];
169
+ }
170
+ return {
171
+ description: prompt.description,
172
+ messages,
173
+ };
174
+ });
74
175
  mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
75
176
  const { name, arguments: args } = request.params;
76
177
  debug(`Tool call: ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.27.0",
3
+ "version": "2.27.2",
4
4
  "description": "🦁 MCP server for Brave Real Browser - NPM Workspaces Monorepo with anti-detection features, SSE streaming, and LSP compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "latest",
52
52
  "@types/turndown": "latest",
53
- "brave-real-browser": "^2.8.0",
53
+ "brave-real-browser": "^2.8.2",
54
54
  "puppeteer-core": "^24.35.0",
55
55
  "turndown": "latest",
56
56
  "vscode-languageserver": "^9.0.1",