brave-real-browser-mcp-server 2.36.0 → 2.37.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
@@ -14,7 +14,8 @@ A production-ready MCP (Model Context Protocol) server that combines Puppeteer w
14
14
 
15
15
  | Feature | Description |
16
16
  |---------|-------------|
17
- | MCP Server | Model Context Protocol compatible server |
17
+ | **MCP Server** | Model Context Protocol compatible server with 28 tools |
18
+ | **LSP Server** | Language Server Protocol for IDE code intelligence |
18
19
  | Brave Browser | Uses Brave instead of Chromium for better privacy |
19
20
  | 50+ Stealth Features | Passes all major bot detectors |
20
21
  | Built-in Ad Blocker | uBlock Origin filters with auto-update |
@@ -25,6 +26,7 @@ A production-ready MCP (Model Context Protocol) server that combines Puppeteer w
25
26
  | Auto-Install | Brave auto-installs if missing |
26
27
  | TypeScript Support | Full type definitions included |
27
28
  | ESM + CJS | Dual module support |
29
+ | **Multi-language** | English & Hindi language support |
28
30
  | **Auto-Update** | Daily automatic dependency updates |
29
31
  | **Auto-Publish** | Automatic NPM publishing on updates |
30
32
  | **Monorepo** | npm workspaces with linked packages |
@@ -127,6 +129,113 @@ AI: [Calls file_downloader with url="..."] -> Downloaded to ./downloads/image.pn
127
129
 
128
130
  ---
129
131
 
132
+ ## LSP Server (Language Server Protocol)
133
+
134
+ This package also includes a full-featured **LSP Server** for IDE code intelligence when writing browser automation scripts.
135
+
136
+ ### Quick Start LSP Server
137
+
138
+ ```bash
139
+ # Start the LSP server
140
+ npm run lsp
141
+ ```
142
+
143
+ ### LSP Capabilities
144
+
145
+ | Feature | Description |
146
+ |---------|-------------|
147
+ | **Autocomplete** | Tool names, parameters, and enum values |
148
+ | **Hover** | Full documentation on hover |
149
+ | **Diagnostics** | Error & warning detection (missing browser_init, etc.) |
150
+ | **Snippets** | Code templates for common workflows |
151
+ | **Refactoring** | Quick fixes (add browser_init, try-catch, etc.) |
152
+ | **Simulation** | Workflow simulation in IDE |
153
+ | **Multi-language** | English & Hindi support |
154
+
155
+ ### VS Code Setup
156
+
157
+ Create `.vscode/settings.json` in your project:
158
+
159
+ ```json
160
+ {
161
+ "braveRealBrowser.language": "en",
162
+ "braveRealBrowser.maxDiagnostics": 100,
163
+ "braveRealBrowser.enableSnippets": true,
164
+ "braveRealBrowser.enableSimulation": true,
165
+ "braveRealBrowser.enableRefactoring": true
166
+ }
167
+ ```
168
+
169
+ ### Diagnostic Features
170
+
171
+ The LSP detects common issues:
172
+
173
+ - Missing `browser_init()` before page operations
174
+ - Missing `navigate()` before content extraction
175
+ - Invalid selectors and URLs
176
+ - Missing required parameters
177
+ - Unclosed browser sessions
178
+ - Security issues (eval usage, etc.)
179
+
180
+ ### Quick Fixes
181
+
182
+ When diagnostics are detected, quick fixes are offered:
183
+
184
+ - Add `browser_init()` at start
185
+ - Add `navigate()` before page tools
186
+ - Add `browser_close()` at end
187
+ - Wrap in try-catch
188
+ - Extract to function
189
+
190
+ ---
191
+
192
+ ## Unified Architecture
193
+
194
+ Both MCP and LSP servers share the same tool definitions:
195
+
196
+ ```
197
+ src/
198
+ ├── shared/
199
+ │ └── tools.js # Single source of truth (28 tools)
200
+ ├── mcp/
201
+ │ ├── server.js # MCP server for AI agents
202
+ │ └── handlers.js # Tool implementations
203
+ ├── lsp/
204
+ │ ├── server.js # LSP server for IDEs
205
+ │ └── capabilities/ # Autocomplete, hover, diagnostics, etc.
206
+ └── index.js # Unified entry point
207
+ ```
208
+
209
+ ### Unified CLI
210
+
211
+ ```bash
212
+ # List all tools
213
+ node src/index.js --list
214
+
215
+ # Start MCP server (default)
216
+ node src/index.js mcp
217
+
218
+ # Start LSP server
219
+ node src/index.js lsp
220
+
221
+ # Show help
222
+ node src/index.js --help
223
+ ```
224
+
225
+ ### Tool Categories
226
+
227
+ | Category | Tools | Description |
228
+ |----------|-------|-------------|
229
+ | **Browser** | 3 | Browser lifecycle (init, close, cookies) |
230
+ | **Navigation** | 1 | Page navigation |
231
+ | **Interaction** | 8 | User actions (click, type, scroll, etc.) |
232
+ | **Extraction** | 10 | Content scraping (HTML, JSON, links, etc.) |
233
+ | **Network** | 3 | Network operations (recorder, download, trace) |
234
+ | **Analysis** | 1 | Page analysis (SEO, performance, etc.) |
235
+ | **Utility** | 2 | Helpers (wait, progress tracker) |
236
+
237
+ ---
238
+
130
239
  ## Monorepo Ecosystem
131
240
 
132
241
  ```
@@ -398,9 +507,12 @@ console.log(blocker === sameBlocker); // true
398
507
 
399
508
  | Command | Description |
400
509
  |---------|-------------|
510
+ | `npm start` | Start unified server (MCP by default) |
401
511
  | `npm run dev` | Start MCP server |
402
512
  | `npm run mcp` | Start MCP server (alias) |
403
513
  | `npm run mcp:verbose` | Start MCP server with tool details |
514
+ | `npm run lsp` | Start LSP server for IDE intelligence |
515
+ | `npm run list` | List all 28 tools with categories |
404
516
  | `npm install` | Install all dependencies with workspace linking |
405
517
  | `npm test` | Run all tests (CJS + ESM) |
406
518
  | `npm run cjs_test` | Run CommonJS tests only |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-browser-mcp-server",
3
- "version": "2.36.0",
3
+ "version": "2.37.0",
4
4
  "description": "MCP Server for Brave Real Browser - Puppeteer with Brave Browser, Stealth Mode, Ad Blocker, and Turnstile Auto-Solver for undetectable web automation.",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.mjs",
@@ -28,10 +28,11 @@
28
28
  },
29
29
  "scripts": {
30
30
  "start": "node src/index.js",
31
- "dev": "node src/index.js mcp",
31
+ "dev": "node src/index.js",
32
32
  "mcp": "node src/index.js mcp",
33
33
  "mcp:verbose": "node src/index.js mcp --verbose",
34
34
  "lsp": "node src/index.js lsp",
35
+ "lsp:tcp": "node src/lsp/server.js --tcp",
35
36
  "list": "node src/index.js --list",
36
37
  "build": "echo 'Root package uses pre-built lib/ - no build needed'",
37
38
  "build:self": "echo 'Root package uses pre-built lib/ - no build needed'",
@@ -70,7 +71,7 @@
70
71
  "license": "ISC",
71
72
  "dependencies": {
72
73
  "@modelcontextprotocol/sdk": "^1.25.3",
73
- "brave-real-puppeteer-core": "^24.51.0",
74
+ "brave-real-puppeteer-core": "^24.52.0",
74
75
  "ghost-cursor": "^1.4.2",
75
76
  "puppeteer-extra": "^3.3.6",
76
77
  "puppeteer-extra-plugin-stealth": "^2.11.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-blocker",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Advanced uBlock Origin management and stealth features for Brave Real Browser",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -63,7 +63,7 @@
63
63
  "@types/adm-zip": "^0.5.5",
64
64
  "@types/fs-extra": "^11.0.4",
65
65
  "@types/node": "^20.0.0",
66
- "brave-real-puppeteer-core": "^24.51.0",
66
+ "brave-real-puppeteer-core": "^24.52.0",
67
67
  "mocha": "^10.4.0",
68
68
  "puppeteer-core": "^24.35.0",
69
69
  "sinon": "^17.0.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-launcher",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Launch Brave Browser with ease from node. Based on chrome-launcher with Brave-specific support.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -54,7 +54,7 @@
54
54
  "typescript": "^5.0.0"
55
55
  },
56
56
  "dependencies": {
57
- "brave-real-blocker": "^1.12.0",
57
+ "brave-real-blocker": "^1.13.0",
58
58
  "escape-string-regexp": "^4.0.0",
59
59
  "is-wsl": "^2.2.0",
60
60
  "which": "^4.0.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brave-real-puppeteer-core",
3
- "version": "24.51.0",
3
+ "version": "24.52.0",
4
4
  "description": "🦁 Brave Real-World Optimized Puppeteer & Playwright Core with 1-5ms ultra-fast timing, 50+ professional stealth features, intelligent browser auto-detection, and 100% bot detection bypass. Features cross-platform Brave browser integration, comprehensive anti-detection, and breakthrough performance improvements.",
5
5
  "keywords": [
6
6
  "automation",
@@ -132,7 +132,7 @@
132
132
  "test-version": "node ./scripts/test-version-management.js"
133
133
  },
134
134
  "dependencies": {
135
- "brave-real-launcher": "^1.18.0",
135
+ "brave-real-launcher": "^1.19.0",
136
136
  "get-east-asian-width": "^1.4.0",
137
137
  "yargs": "^18.0.0"
138
138
  },
package/src/index.js CHANGED
@@ -3,20 +3,19 @@
3
3
  * Brave Real Browser - Unified Entry Point
4
4
  *
5
5
  * Usage:
6
- * node src/index.js mcp - Start MCP Server (default)
7
- * node src/index.js lsp - Start LSP Server
8
- * node src/index.js --help - Show help
6
+ * node src/index.js - Start both MCP + LSP together (default)
7
+ * node src/index.js mcp - Start MCP Server only
8
+ * node src/index.js lsp - Start LSP Server only (STDIO)
9
+ * node src/index.js --help - Show help
9
10
  *
10
11
  * npm scripts:
11
- * npm run dev - Start MCP server
12
- * npm run mcp - Start MCP server
13
- * npm run lsp - Start LSP server
12
+ * npm run dev - Start MCP + LSP together
13
+ * npm run mcp - Start MCP server only
14
+ * npm run lsp - Start LSP server only
14
15
  *
15
- * For AI Assistants (MCP):
16
- * Claude Desktop, Cursor, Copilot, etc.
17
- *
18
- * For IDEs (LSP):
19
- * VS Code, Neovim, Sublime Text, etc.
16
+ * Architecture:
17
+ * MCP Server → STDIO transport (for AI agents: Claude, Cursor, Copilot)
18
+ * LSP Server → TCP :7777 (for IDEs: VS Code, Neovim)
20
19
  */
21
20
 
22
21
  const { TOOLS, TOOL_DISPLAY, CATEGORIES } = require('./shared/tools.js');
@@ -45,8 +44,9 @@ ${colors.bright}USAGE:${colors.reset}
45
44
  node src/index.js [mode] [options]
46
45
 
47
46
  ${colors.bright}MODES:${colors.reset}
48
- ${colors.green}mcp${colors.reset} Start MCP Server for AI agents (default)
49
- ${colors.green}lsp${colors.reset} Start LSP Server for IDE intelligence
47
+ ${colors.green}(default)${colors.reset} Start both MCP + LSP servers together
48
+ ${colors.green}mcp${colors.reset} Start MCP Server only (for AI agents)
49
+ ${colors.green}lsp${colors.reset} Start LSP Server only (for IDEs)
50
50
 
51
51
  ${colors.bright}OPTIONS:${colors.reset}
52
52
  ${colors.yellow}--help, -h${colors.reset} Show this help message
@@ -54,23 +54,25 @@ ${colors.bright}OPTIONS:${colors.reset}
54
54
  ${colors.yellow}--list${colors.reset} List all available tools
55
55
 
56
56
  ${colors.bright}EXAMPLES:${colors.reset}
57
- node src/index.js mcp # Start MCP server
58
- node src/index.js lsp # Start LSP server
57
+ node src/index.js # Start MCP + LSP together
58
+ node src/index.js mcp # Start MCP server only
59
+ node src/index.js lsp # Start LSP server only
59
60
  node src/index.js --list # List all tools
60
61
 
61
62
  ${colors.bright}NPM SCRIPTS:${colors.reset}
62
- npm run dev # Start MCP server
63
- npm run mcp # Start MCP server
64
- npm run lsp # Start LSP server
63
+ npm run dev # Start MCP + LSP together
64
+ npm run mcp # Start MCP server only
65
+ npm run lsp # Start LSP server only (STDIO)
66
+
67
+ ${colors.bright}ARCHITECTURE:${colors.reset}
68
+ ${colors.cyan}MCP Server${colors.reset} → STDIO transport → AI Agents (Claude, Cursor, Copilot)
69
+ ${colors.cyan}LSP Server${colors.reset} → TCP :7777 → IDEs (VS Code, Neovim)
65
70
 
66
71
  ${colors.bright}TOOL CATEGORIES (${TOOLS.length} tools):${colors.reset}
67
72
  ${Object.entries(CATEGORIES).map(([key, cat]) => {
68
73
  const count = TOOLS.filter(t => t.category === key).length;
69
74
  return ` ${cat.emoji} ${colors.yellow}${cat.name.padEnd(15)}${colors.reset} ${colors.dim}(${count} tools)${colors.reset}`;
70
75
  }).join('\n')}
71
-
72
- ${colors.dim}MCP: For AI assistants (Claude, Cursor, Copilot)
73
- LSP: For IDE code intelligence (VS Code, Neovim)${colors.reset}
74
76
  `);
75
77
  }
76
78
 
@@ -88,13 +90,45 @@ function listTools() {
88
90
  console.log(`${colors.dim}${'─'.repeat(50)}${colors.reset}`);
89
91
 
90
92
  for (const tool of tools) {
91
- const required = tool.inputSchema?.required || [];
92
93
  console.log(` ${tool.emoji} ${colors.yellow}${tool.name.padEnd(25)}${colors.reset} ${colors.dim}${tool.description.substring(0, 40)}${colors.reset}`);
93
94
  }
94
95
  console.log('');
95
96
  }
96
97
  }
97
98
 
99
+ /**
100
+ * Start both MCP and LSP servers together
101
+ */
102
+ async function startBothServers() {
103
+ console.error('');
104
+ console.error(`${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════╗${colors.reset}`);
105
+ console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.bright}${colors.magenta}🦁 Brave Real Browser - Unified Server${colors.reset} ${colors.cyan}║${colors.reset}`);
106
+ console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.dim}MCP (AI Agents) + LSP (IDE Intelligence)${colors.reset} ${colors.cyan}║${colors.reset}`);
107
+ console.error(`${colors.bright}${colors.cyan}╚════════════════════════════════════════════════════════════╝${colors.reset}`);
108
+ console.error('');
109
+
110
+ // Start LSP Server on TCP first (background)
111
+ console.error(`${colors.bright}${colors.blue}📡 Starting LSP Server...${colors.reset}`);
112
+
113
+ try {
114
+ const { startTcpServer, LSP_PORT } = require('./lsp/server.js');
115
+ const { port } = await startTcpServer();
116
+ console.error(`${colors.bright}${colors.green}✅ LSP Server running on TCP :${port}${colors.reset}`);
117
+ console.error(`${colors.dim} IDEs can connect to: 127.0.0.1:${port}${colors.reset}`);
118
+ } catch (err) {
119
+ console.error(`${colors.yellow}⚠️ LSP Server failed: ${err.message}${colors.reset}`);
120
+ console.error(`${colors.dim} Continuing with MCP only...${colors.reset}`);
121
+ }
122
+
123
+ console.error('');
124
+
125
+ // Start MCP Server on STDIO (foreground)
126
+ console.error(`${colors.bright}${colors.blue}🚀 Starting MCP Server...${colors.reset}`);
127
+
128
+ // Import and run MCP server
129
+ require('./mcp/index.js');
130
+ }
131
+
98
132
  /**
99
133
  * Main entry point
100
134
  */
@@ -104,7 +138,7 @@ async function main() {
104
138
  // Parse arguments
105
139
  const hasHelp = args.includes('--help') || args.includes('-h');
106
140
  const hasList = args.includes('--list');
107
- const mode = args.find(a => ['mcp', 'lsp'].includes(a.toLowerCase())) || 'mcp';
141
+ const mode = args.find(a => ['mcp', 'lsp'].includes(a.toLowerCase()));
108
142
 
109
143
  if (hasHelp) {
110
144
  showHelp();
@@ -116,13 +150,16 @@ async function main() {
116
150
  process.exit(0);
117
151
  }
118
152
 
119
- // Start appropriate server
120
- if (mode.toLowerCase() === 'lsp') {
121
- // Start LSP Server
153
+ // Start appropriate server(s)
154
+ if (mode?.toLowerCase() === 'lsp') {
155
+ // Start LSP Server only (STDIO mode)
122
156
  require('./lsp/index.js');
123
- } else {
124
- // Start MCP Server (default)
157
+ } else if (mode?.toLowerCase() === 'mcp') {
158
+ // Start MCP Server only
125
159
  require('./mcp/index.js');
160
+ } else {
161
+ // Default: Start both servers together
162
+ await startBothServers();
126
163
  }
127
164
  }
128
165
 
@@ -133,6 +170,7 @@ module.exports = {
133
170
  CATEGORIES,
134
171
  startMCP: () => require('./mcp/index.js'),
135
172
  startLSP: () => require('./lsp/index.js'),
173
+ startBoth: startBothServers,
136
174
  };
137
175
 
138
176
  // Run if called directly
package/src/lsp/server.js CHANGED
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Brave Real Browser LSP Server
3
+ *
4
+ * Supports both STDIO and TCP transports:
5
+ * - STDIO: For direct IDE connections (npm run lsp)
6
+ * - TCP: For running alongside MCP server (npm run dev)
3
7
  */
8
+ const net = require('net');
4
9
  const { createConnection, TextDocuments, ProposedFeatures, TextDocumentSyncKind, MarkupKind } = require('vscode-languageserver/node');
5
10
  const { TextDocument } = require('vscode-languageserver-textdocument');
6
11
  const { TOOLS } = require('../mcp/tools.js');
@@ -12,67 +17,168 @@ const { getRefactorings } = require('./capabilities/refactoring.js');
12
17
  const { simulateWorkflow } = require('./capabilities/simulation.js');
13
18
  const { getLanguagePack } = require('./utils/i18n.js');
14
19
 
15
- const connection = createConnection(ProposedFeatures.all, process.stdin, process.stdout);
16
- const documents = new TextDocuments(TextDocument);
17
- let settings = { language: 'en', maxDiagnostics: 100, enableSnippets: true, enableSimulation: true, enableRefactoring: true };
18
-
19
- connection.onInitialize(() => ({
20
- capabilities: {
21
- textDocumentSync: TextDocumentSyncKind.Incremental,
22
- completionProvider: { resolveProvider: true, triggerCharacters: ['.', "'", '"', '(', '{', ' '] },
23
- hoverProvider: true,
24
- codeActionProvider: true,
25
- codeLensProvider: { resolveProvider: true },
26
- signatureHelpProvider: { triggerCharacters: ['(', ','] },
27
- documentSymbolProvider: true,
28
- },
29
- }));
30
-
31
- connection.onDidChangeConfiguration((change) => {
32
- if (change.settings?.braveRealBrowser) settings = { ...settings, ...change.settings.braveRealBrowser };
33
- documents.all().forEach(validateDocument);
34
- });
35
-
36
- connection.onCompletion((params) => {
37
- const doc = documents.get(params.textDocument.uri);
38
- return doc ? getCompletions(doc, params.position, TOOLS, getLanguagePack(settings.language), settings) : [];
39
- });
40
-
41
- connection.onCompletionResolve((item) => {
42
- const lang = getLanguagePack(settings.language);
43
- const tool = TOOLS.find(t => t.name === item.data?.toolName);
44
- if (tool && lang.tools[tool.name]) {
45
- const tl = lang.tools[tool.name];
46
- item.documentation = { kind: MarkupKind.Markdown, value: `### ${tool.emoji} ${tl.label}\n\n${tl.documentation}\n\n**Parameters:**\n${Object.entries(tl.parameters || {}).map(([k, v]) => `- \`${k}\`: ${v}`).join('\n')}` };
20
+ // Default settings
21
+ let settings = {
22
+ language: 'en',
23
+ maxDiagnostics: 100,
24
+ enableSnippets: true,
25
+ enableSimulation: true,
26
+ enableRefactoring: true
27
+ };
28
+
29
+ // LSP port for TCP mode
30
+ const LSP_PORT = process.env.LSP_PORT || 7777;
31
+
32
+ /**
33
+ * Setup LSP connection handlers
34
+ */
35
+ function setupConnection(connection) {
36
+ const documents = new TextDocuments(TextDocument);
37
+
38
+ connection.onInitialize(() => ({
39
+ capabilities: {
40
+ textDocumentSync: TextDocumentSyncKind.Incremental,
41
+ completionProvider: { resolveProvider: true, triggerCharacters: ['.', "'", '"', '(', '{', ' '] },
42
+ hoverProvider: true,
43
+ codeActionProvider: true,
44
+ codeLensProvider: { resolveProvider: true },
45
+ signatureHelpProvider: { triggerCharacters: ['(', ','] },
46
+ documentSymbolProvider: true,
47
+ },
48
+ }));
49
+
50
+ connection.onDidChangeConfiguration((change) => {
51
+ if (change.settings?.braveRealBrowser) {
52
+ settings = { ...settings, ...change.settings.braveRealBrowser };
53
+ }
54
+ documents.all().forEach((doc) => validateDocument(connection, doc));
55
+ });
56
+
57
+ connection.onCompletion((params) => {
58
+ const doc = documents.get(params.textDocument.uri);
59
+ return doc ? getCompletions(doc, params.position, TOOLS, getLanguagePack(settings.language), settings) : [];
60
+ });
61
+
62
+ connection.onCompletionResolve((item) => {
63
+ const lang = getLanguagePack(settings.language);
64
+ const tool = TOOLS.find(t => t.name === item.data?.toolName);
65
+ if (tool && lang.tools[tool.name]) {
66
+ const tl = lang.tools[tool.name];
67
+ item.documentation = {
68
+ kind: MarkupKind.Markdown,
69
+ value: `### ${tool.emoji} ${tl.label}\n\n${tl.documentation}\n\n**Parameters:**\n${Object.entries(tl.parameters || {}).map(([k, v]) => `- \`${k}\`: ${v}`).join('\n')}`
70
+ };
71
+ }
72
+ return item;
73
+ });
74
+
75
+ connection.onHover((params) => {
76
+ const doc = documents.get(params.textDocument.uri);
77
+ return doc ? getHoverInfo(doc, params.position, TOOLS, getLanguagePack(settings.language)) : null;
78
+ });
79
+
80
+ function validateDocument(conn, doc) {
81
+ conn.sendDiagnostics({
82
+ uri: doc.uri,
83
+ diagnostics: getDiagnostics(doc, TOOLS, getLanguagePack(settings.language), settings.maxDiagnostics)
84
+ });
47
85
  }
48
- return item;
49
- });
50
86
 
51
- connection.onHover((params) => {
52
- const doc = documents.get(params.textDocument.uri);
53
- return doc ? getHoverInfo(doc, params.position, TOOLS, getLanguagePack(settings.language)) : null;
54
- });
87
+ documents.onDidChangeContent((change) => validateDocument(connection, change.document));
88
+
89
+ connection.onCodeAction((params) => {
90
+ const doc = documents.get(params.textDocument.uri);
91
+ return doc ? getRefactorings(doc, params.range, params.context, TOOLS, getLanguagePack(settings.language)) : [];
92
+ });
93
+
94
+ connection.onCodeLens((params) => {
95
+ const doc = documents.get(params.textDocument.uri);
96
+ return doc ? simulateWorkflow(doc, TOOLS, getLanguagePack(settings.language)) : [];
97
+ });
98
+
99
+ connection.onCodeLensResolve((lens) => lens);
55
100
 
56
- function validateDocument(doc) {
57
- connection.sendDiagnostics({ uri: doc.uri, diagnostics: getDiagnostics(doc, TOOLS, getLanguagePack(settings.language), settings.maxDiagnostics) });
101
+ connection.onRequest('braveRealBrowser/snippets', (params) =>
102
+ getSnippets(params?.category, TOOLS, getLanguagePack(settings.language))
103
+ );
104
+
105
+ documents.listen(connection);
106
+ connection.listen();
107
+
108
+ return connection;
58
109
  }
59
110
 
60
- documents.onDidChangeContent((change) => validateDocument(change.document));
111
+ /**
112
+ * Start LSP server in STDIO mode
113
+ */
114
+ function startStdioServer() {
115
+ const connection = createConnection(ProposedFeatures.all, process.stdin, process.stdout);
116
+ setupConnection(connection);
117
+ console.error('🦁 Brave Real Browser LSP Server started (STDIO)');
118
+ return connection;
119
+ }
61
120
 
62
- connection.onCodeAction((params) => {
63
- const doc = documents.get(params.textDocument.uri);
64
- return doc ? getRefactorings(doc, params.range, params.context, TOOLS, getLanguagePack(settings.language)) : [];
65
- });
121
+ /**
122
+ * Start LSP server in TCP mode
123
+ */
124
+ function startTcpServer(port = LSP_PORT) {
125
+ return new Promise((resolve, reject) => {
126
+ const server = net.createServer((socket) => {
127
+ console.error(`🔌 LSP client connected from ${socket.remoteAddress}`);
128
+
129
+ const connection = createConnection(ProposedFeatures.all, socket, socket);
130
+ setupConnection(connection);
131
+
132
+ socket.on('close', () => {
133
+ console.error('🔌 LSP client disconnected');
134
+ });
135
+ });
66
136
 
67
- connection.onCodeLens((params) => {
68
- const doc = documents.get(params.textDocument.uri);
69
- return doc ? simulateWorkflow(doc, TOOLS, getLanguagePack(settings.language)) : [];
70
- });
137
+ server.on('error', (err) => {
138
+ if (err.code === 'EADDRINUSE') {
139
+ console.error(`⚠️ LSP port ${port} already in use, trying ${port + 1}...`);
140
+ server.close();
141
+ startTcpServer(port + 1).then(resolve).catch(reject);
142
+ } else {
143
+ reject(err);
144
+ }
145
+ });
71
146
 
72
- connection.onCodeLensResolve((lens) => lens);
147
+ server.listen(port, '127.0.0.1', () => {
148
+ console.error(`🦁 Brave Real Browser LSP Server started (TCP: 127.0.0.1:${port})`);
149
+ resolve({ server, port });
150
+ });
151
+ });
152
+ }
73
153
 
74
- connection.onRequest('braveRealBrowser/snippets', (params) => getSnippets(params?.category, TOOLS, getLanguagePack(settings.language)));
154
+ /**
155
+ * Get transport mode from args or environment
156
+ */
157
+ function getTransportMode() {
158
+ if (process.argv.includes('--tcp')) return 'tcp';
159
+ if (process.argv.includes('--stdio')) return 'stdio';
160
+ if (process.env.LSP_TRANSPORT === 'tcp') return 'tcp';
161
+ // Default: STDIO for direct invocation, TCP when embedded
162
+ return process.env.LSP_EMBEDDED === 'true' ? 'tcp' : 'stdio';
163
+ }
75
164
 
76
- documents.listen(connection);
77
- connection.listen();
78
- console.error('🦁 Brave Real Browser LSP Server started');
165
+ // Export for programmatic use
166
+ module.exports = {
167
+ startStdioServer,
168
+ startTcpServer,
169
+ setupConnection,
170
+ LSP_PORT
171
+ };
172
+
173
+ // Auto-start if run directly
174
+ if (require.main === module) {
175
+ const mode = getTransportMode();
176
+ if (mode === 'tcp') {
177
+ startTcpServer().catch(err => {
178
+ console.error('❌ Failed to start LSP TCP server:', err.message);
179
+ process.exit(1);
180
+ });
181
+ } else {
182
+ startStdioServer();
183
+ }
184
+ }
@@ -2,6 +2,10 @@
2
2
  * Brave Real Browser MCP Server - Tool Handlers
3
3
  *
4
4
  * Implementation of all 28 browser automation tools
5
+ *
6
+ * Environment Variables:
7
+ * HEADLESS=true - Run browser in headless mode
8
+ * HEADLESS=false - Run browser in GUI mode (visible)
5
9
  */
6
10
 
7
11
  const path = require('path');
@@ -15,6 +19,61 @@ let networkRecords = [];
15
19
  let isRecordingNetwork = false;
16
20
  let progressTasks = {};
17
21
 
22
+ // Progress notification callback (set by server)
23
+ let progressCallback = null;
24
+
25
+ /**
26
+ * Set progress callback for real-time notifications
27
+ */
28
+ function setProgressCallback(callback) {
29
+ progressCallback = callback;
30
+ }
31
+
32
+ /**
33
+ * Send real-time progress notification
34
+ */
35
+ function notifyProgress(toolName, status, message, data = {}) {
36
+ const notification = {
37
+ tool: toolName,
38
+ status, // 'started' | 'progress' | 'completed' | 'error'
39
+ message,
40
+ timestamp: new Date().toISOString(),
41
+ ...data
42
+ };
43
+
44
+ // Log to stderr for visibility
45
+ const emoji = {
46
+ started: '🚀',
47
+ progress: '⏳',
48
+ completed: '✅',
49
+ error: '❌'
50
+ }[status] || '📌';
51
+
52
+ console.error(`${emoji} [${toolName}] ${message}`);
53
+
54
+ // Call progress callback if set
55
+ if (progressCallback) {
56
+ progressCallback(notification);
57
+ }
58
+
59
+ return notification;
60
+ }
61
+
62
+ /**
63
+ * Get headless setting from environment
64
+ */
65
+ function getHeadlessFromEnv() {
66
+ const envHeadless = process.env.HEADLESS;
67
+
68
+ if (envHeadless === undefined || envHeadless === null || envHeadless === '') {
69
+ return false; // Default: GUI mode
70
+ }
71
+
72
+ // Parse string to boolean
73
+ const value = envHeadless.toLowerCase().trim();
74
+ return value === 'true' || value === '1' || value === 'yes';
75
+ }
76
+
18
77
  /**
19
78
  * Get browser and page instances
20
79
  */
@@ -38,9 +97,17 @@ function requireBrowser() {
38
97
  const handlers = {
39
98
  // 1. Browser Init
40
99
  async browser_init(params = {}) {
100
+ notifyProgress('browser_init', 'started', 'Initializing browser...');
101
+
41
102
  const { connect } = require('../../lib/cjs/index.js');
42
103
 
43
- const { headless = false, proxy = {}, turnstile = false, enableBlocker = true } = params;
104
+ // Get headless from params OR environment variable
105
+ const envHeadless = getHeadlessFromEnv();
106
+ const headless = params.headless !== undefined ? params.headless : envHeadless;
107
+
108
+ const { proxy = {}, turnstile = false, enableBlocker = true } = params;
109
+
110
+ notifyProgress('browser_init', 'progress', `Mode: ${headless ? 'Headless' : 'GUI (Visible)'}`, { headless });
44
111
 
45
112
  const result = await connect({
46
113
  headless,
@@ -53,10 +120,18 @@ const handlers = {
53
120
  pageInstance = result.page;
54
121
  blockerInstance = result.blocker;
55
122
 
123
+ const pid = browserInstance.process()?.pid;
124
+
125
+ notifyProgress('browser_init', 'completed', `Browser started (PID: ${pid})`, {
126
+ headless,
127
+ pid,
128
+ blockerEnabled: enableBlocker
129
+ });
130
+
56
131
  return {
57
132
  success: true,
58
- message: 'Browser initialized successfully',
59
- pid: browserInstance.process()?.pid,
133
+ message: `Browser initialized in ${headless ? 'headless' : 'GUI'} mode`,
134
+ pid,
60
135
  headless,
61
136
  blockerEnabled: enableBlocker
62
137
  };
@@ -67,12 +142,18 @@ const handlers = {
67
142
  const { page } = requireBrowser();
68
143
  const { url, waitUntil = 'networkidle2', timeout = 30000 } = params;
69
144
 
145
+ notifyProgress('navigate', 'started', `Navigating to: ${url}`);
146
+
70
147
  await page.goto(url, { waitUntil, timeout });
71
148
 
149
+ const title = await page.title();
150
+
151
+ notifyProgress('navigate', 'completed', `Loaded: ${title}`, { url: page.url(), title });
152
+
72
153
  return {
73
154
  success: true,
74
155
  url: page.url(),
75
- title: await page.title()
156
+ title
76
157
  };
77
158
  },
78
159
 
@@ -81,11 +162,14 @@ const handlers = {
81
162
  const { page } = requireBrowser();
82
163
  const { format = 'text', selector } = params;
83
164
 
165
+ notifyProgress('get_content', 'started', `Extracting ${format} content${selector ? ` from ${selector}` : ''}`);
166
+
84
167
  let content;
85
168
 
86
169
  if (selector) {
87
170
  const element = await page.$(selector);
88
171
  if (!element) {
172
+ notifyProgress('get_content', 'error', `Element not found: ${selector}`);
89
173
  return { success: false, error: `Element not found: ${selector}` };
90
174
  }
91
175
 
@@ -99,7 +183,6 @@ const handlers = {
99
183
  content = await page.content();
100
184
  } else if (format === 'markdown') {
101
185
  content = await page.evaluate(() => {
102
- // Simple HTML to Markdown conversion
103
186
  const body = document.body.innerText;
104
187
  return body;
105
188
  });
@@ -108,6 +191,8 @@ const handlers = {
108
191
  }
109
192
  }
110
193
 
194
+ notifyProgress('get_content', 'completed', `Extracted ${content.length} characters`, { format, length: content.length });
195
+
111
196
  return {
112
197
  success: true,
113
198
  content,
@@ -121,6 +206,8 @@ const handlers = {
121
206
  const { page } = requireBrowser();
122
207
  const { type = 'timeout', value, timeout = 30000 } = params;
123
208
 
209
+ notifyProgress('wait', 'started', `Waiting for ${type}: ${value}`);
210
+
124
211
  switch (type) {
125
212
  case 'selector':
126
213
  await page.waitForSelector(value, { timeout });
@@ -136,6 +223,8 @@ const handlers = {
136
223
  await new Promise(r => setTimeout(r, parseInt(value) || 1000));
137
224
  }
138
225
 
226
+ notifyProgress('wait', 'completed', `Wait completed: ${type}`, { type, value });
227
+
139
228
  return { success: true, type, value };
140
229
  },
141
230
 
@@ -144,19 +233,23 @@ const handlers = {
144
233
  const { page } = requireBrowser();
145
234
  const { selector, humanLike = true, clickCount = 1, delay = 0 } = params;
146
235
 
236
+ notifyProgress('click', 'started', `Clicking: ${selector}`);
237
+
147
238
  if (humanLike) {
148
239
  try {
149
240
  const { createCursor } = require('ghost-cursor');
150
241
  const cursor = createCursor(page);
151
242
  await cursor.click(selector);
243
+ notifyProgress('click', 'progress', 'Used human-like cursor movement');
152
244
  } catch (e) {
153
- // Fallback to regular click
154
245
  await page.click(selector, { clickCount, delay });
155
246
  }
156
247
  } else {
157
248
  await page.click(selector, { clickCount, delay });
158
249
  }
159
250
 
251
+ notifyProgress('click', 'completed', `Clicked: ${selector}`, { selector, humanLike });
252
+
160
253
  return { success: true, selector, clicked: true };
161
254
  },
162
255
 
@@ -165,13 +258,18 @@ const handlers = {
165
258
  const { page } = requireBrowser();
166
259
  const { selector, text, delay = 50, clear = false } = params;
167
260
 
261
+ notifyProgress('type', 'started', `Typing ${text.length} characters into ${selector}`);
262
+
168
263
  if (clear) {
169
264
  await page.click(selector, { clickCount: 3 });
170
265
  await page.keyboard.press('Backspace');
266
+ notifyProgress('type', 'progress', 'Cleared existing text');
171
267
  }
172
268
 
173
269
  await page.type(selector, text, { delay });
174
270
 
271
+ notifyProgress('type', 'completed', `Typed ${text.length} characters`, { selector, textLength: text.length });
272
+
175
273
  return { success: true, selector, textLength: text.length };
176
274
  },
177
275
 
@@ -179,12 +277,16 @@ const handlers = {
179
277
  async browser_close(params = {}) {
180
278
  const { force = false } = params;
181
279
 
280
+ notifyProgress('browser_close', 'started', 'Closing browser...');
281
+
182
282
  if (browserInstance) {
183
283
  try {
184
284
  await browserInstance.close();
285
+ notifyProgress('browser_close', 'progress', 'Browser closed gracefully');
185
286
  } catch (e) {
186
287
  if (force) {
187
288
  browserInstance.process()?.kill('SIGKILL');
289
+ notifyProgress('browser_close', 'progress', 'Browser force killed');
188
290
  }
189
291
  }
190
292
  browserInstance = null;
@@ -192,6 +294,8 @@ const handlers = {
192
294
  blockerInstance = null;
193
295
  }
194
296
 
297
+ notifyProgress('browser_close', 'completed', 'Browser closed');
298
+
195
299
  return { success: true, message: 'Browser closed' };
196
300
  },
197
301
 
@@ -200,22 +304,32 @@ const handlers = {
200
304
  const { page } = requireBrowser();
201
305
  const { type = 'auto', timeout = 30000 } = params;
202
306
 
203
- // Wait for turnstile/captcha to be solved (handled by turnstile option in connect)
307
+ notifyProgress('solve_captcha', 'started', `Solving ${type} captcha...`);
308
+
204
309
  const start = Date.now();
310
+ let attempts = 0;
205
311
 
206
312
  while (Date.now() - start < timeout) {
313
+ attempts++;
314
+
207
315
  const turnstileToken = await page.evaluate(() => {
208
316
  const input = document.querySelector('input[name="cf-turnstile-response"]');
209
317
  return input ? input.value : null;
210
318
  });
211
319
 
212
320
  if (turnstileToken) {
321
+ notifyProgress('solve_captcha', 'completed', `Captcha solved after ${attempts} checks`, { type: 'turnstile', attempts });
213
322
  return { success: true, type: 'turnstile', solved: true };
214
323
  }
215
324
 
325
+ if (attempts % 10 === 0) {
326
+ notifyProgress('solve_captcha', 'progress', `Still solving... (${attempts} checks)`, { attempts });
327
+ }
328
+
216
329
  await new Promise(r => setTimeout(r, 500));
217
330
  }
218
331
 
332
+ notifyProgress('solve_captcha', 'error', 'Captcha solving timeout');
219
333
  return { success: false, error: 'Captcha solving timeout' };
220
334
  },
221
335
 
@@ -229,11 +343,15 @@ const handlers = {
229
343
  ? (Math.random() > 0.5 ? 'down' : 'up')
230
344
  : direction;
231
345
 
346
+ notifyProgress('random_scroll', 'started', `Scrolling ${scrollDirection} ${scrollAmount}px`);
347
+
232
348
  await page.evaluate(({ scrollAmount, scrollDirection, smooth }) => {
233
349
  const y = scrollDirection === 'down' ? scrollAmount : -scrollAmount;
234
350
  window.scrollBy({ top: y, behavior: smooth ? 'smooth' : 'auto' });
235
351
  }, { scrollAmount, scrollDirection, smooth });
236
352
 
353
+ notifyProgress('random_scroll', 'completed', `Scrolled ${scrollDirection} ${scrollAmount}px`, { direction: scrollDirection, amount: scrollAmount });
354
+
237
355
  return { success: true, direction: scrollDirection, amount: scrollAmount };
238
356
  },
239
357
 
@@ -242,6 +360,8 @@ const handlers = {
242
360
  const { page } = requireBrowser();
243
361
  const { selector, xpath, text, multiple = false } = params;
244
362
 
363
+ notifyProgress('find_element', 'started', `Finding element: ${selector || xpath || text}`);
364
+
245
365
  let elements = [];
246
366
 
247
367
  if (selector) {
@@ -278,6 +398,8 @@ const handlers = {
278
398
  );
279
399
  }
280
400
 
401
+ notifyProgress('find_element', 'completed', `Found ${elements.length} element(s)`, { found: elements.length });
402
+
281
403
  return { success: true, found: elements.length, elements };
282
404
  },
283
405
 
@@ -286,6 +408,8 @@ const handlers = {
286
408
  const { page } = requireBrowser();
287
409
  const { filename, selector, includeImages = true, includeMeta = true } = params;
288
410
 
411
+ notifyProgress('save_content_as_markdown', 'started', `Saving to: ${filename}`);
412
+
289
413
  let markdown = '';
290
414
 
291
415
  if (includeMeta) {
@@ -304,6 +428,8 @@ const handlers = {
304
428
  const outputPath = path.resolve(filename);
305
429
  fs.writeFileSync(outputPath, markdown);
306
430
 
431
+ notifyProgress('save_content_as_markdown', 'completed', `Saved ${markdown.length} bytes to ${filename}`, { filename: outputPath, size: markdown.length });
432
+
307
433
  return { success: true, filename: outputPath, size: markdown.length };
308
434
  },
309
435
 
@@ -312,6 +438,8 @@ const handlers = {
312
438
  const { page } = requireBrowser();
313
439
  const { url, maxRedirects = 10, includeHeaders = false } = params;
314
440
 
441
+ notifyProgress('redirect_tracer', 'started', `Tracing redirects for: ${url}`);
442
+
315
443
  const redirects = [];
316
444
 
317
445
  const responseHandler = response => {
@@ -321,6 +449,7 @@ const handlers = {
321
449
  status: response.status(),
322
450
  headers: includeHeaders ? response.headers() : undefined
323
451
  });
452
+ notifyProgress('redirect_tracer', 'progress', `Redirect ${redirects.length}: ${response.status()}`, { status: response.status() });
324
453
  }
325
454
  };
326
455
 
@@ -330,6 +459,8 @@ const handlers = {
330
459
 
331
460
  page.off('response', responseHandler);
332
461
 
462
+ notifyProgress('redirect_tracer', 'completed', `Found ${redirects.length} redirects`, { redirectCount: redirects.length, finalUrl: page.url() });
463
+
333
464
  return {
334
465
  success: true,
335
466
  originalUrl: url,
@@ -344,6 +475,8 @@ const handlers = {
344
475
  const { page } = requireBrowser();
345
476
  const { pattern, flags = 'gi', source = 'html' } = params;
346
477
 
478
+ notifyProgress('search_regex', 'started', `Searching pattern: ${pattern}`);
479
+
347
480
  let content;
348
481
  if (source === 'html') {
349
482
  content = await page.content();
@@ -356,6 +489,8 @@ const handlers = {
356
489
  const regex = new RegExp(pattern, flags);
357
490
  const matches = content.match(regex) || [];
358
491
 
492
+ notifyProgress('search_regex', 'completed', `Found ${matches.length} matches`, { matchCount: matches.length });
493
+
359
494
  return { success: true, pattern, matchCount: matches.length, matches: matches.slice(0, 100) };
360
495
  },
361
496
 
@@ -364,6 +499,8 @@ const handlers = {
364
499
  const { page } = requireBrowser();
365
500
  const { source = 'page', selector, jsonPath } = params;
366
501
 
502
+ notifyProgress('extract_json', 'started', `Extracting JSON from: ${source}`);
503
+
367
504
  let jsonData = [];
368
505
 
369
506
  if (source === 'ld+json') {
@@ -384,6 +521,8 @@ const handlers = {
384
521
  try { jsonData = [JSON.parse(text)]; } catch {}
385
522
  }
386
523
 
524
+ notifyProgress('extract_json', 'completed', `Extracted ${jsonData.length} JSON objects`, { count: jsonData.length });
525
+
387
526
  return { success: true, source, count: jsonData.length, data: jsonData };
388
527
  },
389
528
 
@@ -392,6 +531,8 @@ const handlers = {
392
531
  const { page } = requireBrowser();
393
532
  const { types = ['all'] } = params;
394
533
 
534
+ notifyProgress('scrape_meta_tags', 'started', 'Extracting meta tags...');
535
+
395
536
  const meta = await page.evaluate(() => {
396
537
  const result = { meta: {}, og: {}, twitter: {} };
397
538
 
@@ -415,6 +556,9 @@ const handlers = {
415
556
  return result;
416
557
  });
417
558
 
559
+ const tagCount = Object.keys(meta.meta).length + Object.keys(meta.og).length + Object.keys(meta.twitter).length;
560
+ notifyProgress('scrape_meta_tags', 'completed', `Extracted ${tagCount} meta tags`, { tagCount });
561
+
418
562
  return { success: true, ...meta };
419
563
  },
420
564
 
@@ -423,6 +567,8 @@ const handlers = {
423
567
  const { page } = requireBrowser();
424
568
  const { key, modifiers = [], count = 1 } = params;
425
569
 
570
+ notifyProgress('press_key', 'started', `Pressing: ${modifiers.length ? modifiers.join('+') + '+' : ''}${key} x${count}`);
571
+
426
572
  for (let i = 0; i < count; i++) {
427
573
  if (modifiers.length > 0) {
428
574
  const keyCombo = [...modifiers, key].join('+');
@@ -432,6 +578,8 @@ const handlers = {
432
578
  }
433
579
  }
434
580
 
581
+ notifyProgress('press_key', 'completed', `Pressed ${key} ${count} time(s)`, { key, modifiers, count });
582
+
435
583
  return { success: true, key, modifiers, count };
436
584
  },
437
585
 
@@ -442,16 +590,20 @@ const handlers = {
442
590
  switch (action) {
443
591
  case 'start':
444
592
  progressTasks[taskName] = { progress: 0, startTime: Date.now() };
593
+ notifyProgress('progress_tracker', 'started', `Task started: ${taskName}`);
445
594
  break;
446
595
  case 'update':
447
596
  if (progressTasks[taskName]) {
448
597
  progressTasks[taskName].progress = progress;
598
+ notifyProgress('progress_tracker', 'progress', `${taskName}: ${progress}%`, { taskName, progress });
449
599
  }
450
600
  break;
451
601
  case 'complete':
452
602
  if (progressTasks[taskName]) {
453
603
  progressTasks[taskName].progress = 100;
454
604
  progressTasks[taskName].endTime = Date.now();
605
+ const duration = progressTasks[taskName].endTime - progressTasks[taskName].startTime;
606
+ notifyProgress('progress_tracker', 'completed', `${taskName} completed in ${duration}ms`, { taskName, duration });
455
607
  }
456
608
  break;
457
609
  }
@@ -464,6 +616,8 @@ const handlers = {
464
616
  const { page } = requireBrowser();
465
617
  const { types = ['all'], detailed = true } = params;
466
618
 
619
+ notifyProgress('deep_analysis', 'started', 'Analyzing page...');
620
+
467
621
  const analysis = await page.evaluate(() => {
468
622
  const result = {
469
623
  seo: {
@@ -495,6 +649,8 @@ const handlers = {
495
649
  return result;
496
650
  });
497
651
 
652
+ notifyProgress('deep_analysis', 'completed', `Analysis complete: ${analysis.performance.domElements} DOM elements`, { domElements: analysis.performance.domElements });
653
+
498
654
  return { success: true, url: page.url(), analysis };
499
655
  },
500
656
 
@@ -517,12 +673,15 @@ const handlers = {
517
673
  });
518
674
  }
519
675
  });
676
+ notifyProgress('network_recorder', 'started', 'Network recording started');
520
677
  break;
521
678
  case 'stop':
522
679
  isRecordingNetwork = false;
680
+ notifyProgress('network_recorder', 'completed', `Recording stopped: ${networkRecords.length} requests captured`);
523
681
  break;
524
682
  case 'clear':
525
683
  networkRecords = [];
684
+ notifyProgress('network_recorder', 'completed', 'Network records cleared');
526
685
  break;
527
686
  }
528
687
 
@@ -543,6 +702,8 @@ const handlers = {
543
702
  const { page } = requireBrowser();
544
703
  const { types = ['all'], selector, includeText = true } = params;
545
704
 
705
+ notifyProgress('link_harvester', 'started', 'Harvesting links...');
706
+
546
707
  const currentHost = new URL(page.url()).hostname;
547
708
 
548
709
  let links = await page.$$eval(selector ? `${selector} a` : 'a', (anchors, includeText) =>
@@ -566,6 +727,8 @@ const handlers = {
566
727
  });
567
728
  }
568
729
 
730
+ notifyProgress('link_harvester', 'completed', `Found ${links.length} links`, { count: links.length });
731
+
569
732
  return { success: true, count: links.length, links };
570
733
  },
571
734
 
@@ -574,24 +737,30 @@ const handlers = {
574
737
  const { page } = requireBrowser();
575
738
  const { action = 'get', name, value, domain, expires } = params;
576
739
 
740
+ notifyProgress('cookie_manager', 'started', `Cookie action: ${action}`);
741
+
577
742
  switch (action) {
578
743
  case 'get':
579
744
  const cookies = await page.cookies();
745
+ notifyProgress('cookie_manager', 'completed', `Retrieved ${cookies.length} cookies`);
580
746
  return { success: true, cookies: name ? cookies.filter(c => c.name === name) : cookies };
581
747
 
582
748
  case 'set':
583
749
  await page.setCookie({ name, value, domain: domain || new URL(page.url()).hostname, expires });
750
+ notifyProgress('cookie_manager', 'completed', `Cookie set: ${name}`);
584
751
  return { success: true, message: `Cookie ${name} set` };
585
752
 
586
753
  case 'delete':
587
754
  const toDelete = await page.cookies();
588
755
  const filtered = name ? toDelete.filter(c => c.name === name) : toDelete;
589
756
  await page.deleteCookie(...filtered);
757
+ notifyProgress('cookie_manager', 'completed', `Deleted ${filtered.length} cookie(s)`);
590
758
  return { success: true, message: `Deleted ${filtered.length} cookie(s)` };
591
759
 
592
760
  case 'clear':
593
761
  const allCookies = await page.cookies();
594
762
  await page.deleteCookie(...allCookies);
763
+ notifyProgress('cookie_manager', 'completed', `Cleared ${allCookies.length} cookies`);
595
764
  return { success: true, message: `Cleared ${allCookies.length} cookies` };
596
765
  }
597
766
 
@@ -603,12 +772,12 @@ const handlers = {
603
772
  const { page } = requireBrowser();
604
773
  const { url, filename, directory = './downloads' } = params;
605
774
 
606
- // Ensure directory exists
775
+ notifyProgress('file_downloader', 'started', `Downloading: ${url}`);
776
+
607
777
  if (!fs.existsSync(directory)) {
608
778
  fs.mkdirSync(directory, { recursive: true });
609
779
  }
610
780
 
611
- // Use page to download
612
781
  const response = await page.goto(url, { waitUntil: 'networkidle2' });
613
782
  const buffer = await response.buffer();
614
783
 
@@ -617,6 +786,8 @@ const handlers = {
617
786
 
618
787
  fs.writeFileSync(outputPath, buffer);
619
788
 
789
+ notifyProgress('file_downloader', 'completed', `Downloaded: ${outputFilename} (${buffer.length} bytes)`, { filename: outputPath, size: buffer.length });
790
+
620
791
  return { success: true, filename: outputPath, size: buffer.length };
621
792
  },
622
793
 
@@ -625,10 +796,13 @@ const handlers = {
625
796
  const { page } = requireBrowser();
626
797
  const { action = 'list', selector, index } = params;
627
798
 
799
+ notifyProgress('iframe_handler', 'started', `iFrame action: ${action}`);
800
+
628
801
  const frames = page.frames();
629
802
 
630
803
  switch (action) {
631
804
  case 'list':
805
+ notifyProgress('iframe_handler', 'completed', `Found ${frames.length} frames`);
632
806
  return {
633
807
  success: true,
634
808
  count: frames.length,
@@ -641,8 +815,10 @@ const handlers = {
641
815
  : frames[index];
642
816
 
643
817
  if (targetFrame) {
818
+ notifyProgress('iframe_handler', 'completed', `Switched to frame: ${targetFrame.url()}`);
644
819
  return { success: true, switched: true, url: targetFrame.url() };
645
820
  }
821
+ notifyProgress('iframe_handler', 'error', 'Frame not found');
646
822
  return { success: false, error: 'Frame not found' };
647
823
 
648
824
  case 'content':
@@ -652,11 +828,13 @@ const handlers = {
652
828
 
653
829
  if (frame) {
654
830
  const content = await frame.content();
831
+ notifyProgress('iframe_handler', 'completed', `Got frame content: ${content.length} chars`);
655
832
  return { success: true, content };
656
833
  }
657
834
  return { success: false, error: 'Frame not found' };
658
835
 
659
836
  case 'exit':
837
+ notifyProgress('iframe_handler', 'completed', 'Returned to main frame');
660
838
  return { success: true, message: 'Returned to main frame' };
661
839
  }
662
840
 
@@ -668,22 +846,21 @@ const handlers = {
668
846
  const { page } = requireBrowser();
669
847
  const { types = ['all'], quality = 'best' } = params;
670
848
 
849
+ notifyProgress('stream_extractor', 'started', 'Extracting streams...');
850
+
671
851
  const streams = await page.evaluate(() => {
672
852
  const result = { video: [], audio: [], hls: [], dash: [] };
673
853
 
674
- // Video elements
675
854
  document.querySelectorAll('video source, video').forEach(el => {
676
855
  const src = el.src || el.getAttribute('src');
677
856
  if (src) result.video.push({ src, type: el.type || 'video' });
678
857
  });
679
858
 
680
- // Audio elements
681
859
  document.querySelectorAll('audio source, audio').forEach(el => {
682
860
  const src = el.src || el.getAttribute('src');
683
861
  if (src) result.audio.push({ src, type: el.type || 'audio' });
684
862
  });
685
863
 
686
- // Find HLS/DASH in scripts
687
864
  const scripts = [...document.querySelectorAll('script')].map(s => s.textContent).join('\n');
688
865
  const hlsMatches = scripts.match(/https?:\/\/[^\s"']+\.m3u8[^\s"']*/gi) || [];
689
866
  const dashMatches = scripts.match(/https?:\/\/[^\s"']+\.mpd[^\s"']*/gi) || [];
@@ -694,6 +871,9 @@ const handlers = {
694
871
  return result;
695
872
  });
696
873
 
874
+ const totalStreams = streams.video.length + streams.audio.length + streams.hls.length + streams.dash.length;
875
+ notifyProgress('stream_extractor', 'completed', `Found ${totalStreams} streams`, { totalStreams });
876
+
697
877
  return { success: true, streams };
698
878
  },
699
879
 
@@ -702,8 +882,11 @@ const handlers = {
702
882
  const { page } = requireBrowser();
703
883
  const { selector, waitForJS = true, timeout = 10000 } = params;
704
884
 
885
+ notifyProgress('js_scrape', 'started', `Scraping: ${selector}`);
886
+
705
887
  if (waitForJS) {
706
888
  await page.waitForSelector(selector, { timeout });
889
+ notifyProgress('js_scrape', 'progress', 'Element found, extracting content...');
707
890
  }
708
891
 
709
892
  const content = await page.$eval(selector, el => ({
@@ -712,6 +895,8 @@ const handlers = {
712
895
  attributes: Object.fromEntries([...el.attributes].map(a => [a.name, a.value]))
713
896
  }));
714
897
 
898
+ notifyProgress('js_scrape', 'completed', `Scraped ${content.text.length} characters`, { selector });
899
+
715
900
  return { success: true, selector, content };
716
901
  },
717
902
 
@@ -720,8 +905,12 @@ const handlers = {
720
905
  const { page } = requireBrowser();
721
906
  const { code, returnValue = true } = params;
722
907
 
908
+ notifyProgress('execute_js', 'started', 'Executing JavaScript...');
909
+
723
910
  const result = await page.evaluate(code);
724
911
 
912
+ notifyProgress('execute_js', 'completed', 'JavaScript executed', { hasResult: result !== undefined });
913
+
725
914
  return { success: true, result: returnValue ? result : undefined };
726
915
  },
727
916
 
@@ -730,8 +919,9 @@ const handlers = {
730
919
  const { page } = requireBrowser();
731
920
  const { playerType = 'auto', action = 'info' } = params;
732
921
 
922
+ notifyProgress('player_api_hook', 'started', `Player ${action}: ${playerType}`);
923
+
733
924
  const playerInfo = await page.evaluate(({ playerType, action }) => {
734
- // Try to detect player
735
925
  if (window.player || window.videoPlayer || window.jwplayer) {
736
926
  const player = window.player || window.videoPlayer || (window.jwplayer && window.jwplayer());
737
927
 
@@ -763,7 +953,6 @@ const handlers = {
763
953
  }
764
954
  }
765
955
 
766
- // Fallback to HTML5 video
767
956
  const video = document.querySelector('video');
768
957
  if (video) {
769
958
  if (action === 'play') video.play();
@@ -782,6 +971,8 @@ const handlers = {
782
971
  return { detected: false };
783
972
  }, { playerType, action });
784
973
 
974
+ notifyProgress('player_api_hook', 'completed', playerInfo.detected ? `Player detected: ${playerInfo.type}` : 'No player found', { detected: playerInfo.detected });
975
+
785
976
  return { success: true, ...playerInfo };
786
977
  },
787
978
 
@@ -791,6 +982,11 @@ const handlers = {
791
982
  const { selector, data, submit = false, humanLike = true } = params;
792
983
 
793
984
  const formSelector = selector || 'form';
985
+ const fields = Object.keys(data || {});
986
+
987
+ notifyProgress('form_automator', 'started', `Filling form with ${fields.length} fields`);
988
+
989
+ let filledCount = 0;
794
990
 
795
991
  for (const [field, value] of Object.entries(data || {})) {
796
992
  const inputSelector = `${formSelector} [name="${field}"], ${formSelector} #${field}, ${formSelector} [placeholder*="${field}" i]`;
@@ -813,6 +1009,8 @@ const handlers = {
813
1009
  await page.type(inputSelector, String(value));
814
1010
  }
815
1011
  }
1012
+ filledCount++;
1013
+ notifyProgress('form_automator', 'progress', `Filled: ${field}`, { field, filledCount });
816
1014
  }
817
1015
  } catch (e) {
818
1016
  // Field not found, continue
@@ -821,9 +1019,12 @@ const handlers = {
821
1019
 
822
1020
  if (submit) {
823
1021
  await page.click(`${formSelector} [type="submit"], ${formSelector} button`);
1022
+ notifyProgress('form_automator', 'progress', 'Form submitted');
824
1023
  }
825
1024
 
826
- return { success: true, formSelector, fieldsProcessed: Object.keys(data || {}).length, submitted: submit };
1025
+ notifyProgress('form_automator', 'completed', `Filled ${filledCount}/${fields.length} fields`, { filledCount, submitted: submit });
1026
+
1027
+ return { success: true, formSelector, fieldsProcessed: filledCount, submitted: submit };
827
1028
  }
828
1029
  };
829
1030
 
@@ -834,12 +1035,14 @@ async function executeTool(name, params = {}) {
834
1035
  const handler = handlers[name];
835
1036
 
836
1037
  if (!handler) {
1038
+ notifyProgress(name, 'error', `Unknown tool: ${name}`);
837
1039
  return { success: false, error: `Unknown tool: ${name}` };
838
1040
  }
839
1041
 
840
1042
  try {
841
1043
  return await handler(params);
842
1044
  } catch (error) {
1045
+ notifyProgress(name, 'error', error.message);
843
1046
  return { success: false, error: error.message };
844
1047
  }
845
1048
  }
@@ -865,5 +1068,8 @@ module.exports = {
865
1068
  executeTool,
866
1069
  cleanup,
867
1070
  getState,
868
- requireBrowser
1071
+ requireBrowser,
1072
+ setProgressCallback,
1073
+ notifyProgress,
1074
+ getHeadlessFromEnv
869
1075
  };