real-browser-mcp-server 1.1.2 → 1.1.4

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.
@@ -82,11 +82,37 @@ jobs:
82
82
 
83
83
  echo "📈 Selected increment type: $INCREMENT_TYPE"
84
84
 
85
+ # Calculate the new version before bumping
86
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
87
+ echo "📦 Current version: $CURRENT_VERSION"
88
+
89
+ # Calculate expected new version
90
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
91
+ case "$INCREMENT_TYPE" in
92
+ major) NEW_VERSION="$((MAJOR + 1)).0.0" ;;
93
+ minor) NEW_VERSION="$MAJOR.$((MINOR + 1)).0" ;;
94
+ patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" ;;
95
+ esac
96
+ echo "🔮 Expected new version: $NEW_VERSION"
97
+
98
+ # Check if tag already exists locally and remove it
99
+ if git tag -l "v$NEW_VERSION" | grep -q "v$NEW_VERSION"; then
100
+ echo "⚠️ Tag v$NEW_VERSION exists locally, removing..."
101
+ git tag -d "v$NEW_VERSION"
102
+ fi
103
+
104
+ # Check if tag exists on remote and delete it
105
+ if git ls-remote --tags origin "v$NEW_VERSION" | grep -q "v$NEW_VERSION"; then
106
+ echo "⚠️ Tag v$NEW_VERSION exists on remote, deleting..."
107
+ git push origin --delete "v$NEW_VERSION" || true
108
+ fi
109
+
85
110
  # Bump version, commit changes and create tag
86
111
  npm version $INCREMENT_TYPE -m "🔖 Release: v%s [skip ci]"
87
112
 
88
- # Capture new version for subsequent steps
113
+ # Capture actual new version for subsequent steps
89
114
  NEW_VERSION=$(node -p "require('./package.json').version")
115
+ echo "✅ New version: $NEW_VERSION"
90
116
  echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
91
117
  echo "version=v$NEW_VERSION" >> $GITHUB_OUTPUT
92
118
 
package/README.md CHANGED
@@ -80,14 +80,17 @@ Add the server entry to your global MCP settings file (typically found at `%APPD
80
80
  {
81
81
  "mcpServers": {
82
82
  "real-browser-mcp-server": {
83
- "command": "node",
83
+ "type": "stdio",
84
+ "command": "C:/Program Files/nodejs/node.exe",
84
85
  "args": [
85
86
  "c:/Users/Admin/Desktop/Software/Real-Browser-Mcp-Server/src/index.js"
86
87
  ],
87
88
  "env": {
88
89
  "HEADLESS": "false"
89
90
  },
90
- "disabled": false
91
+ "disabled": false,
92
+ "autoApprove": [],
93
+ "timeout": 120
91
94
  }
92
95
  }
93
96
  }
@@ -136,9 +139,9 @@ Configure the server in your `opencode.jsonc` or standard MCP settings configura
136
139
 
137
140
  ---
138
141
 
139
- ## 🌐 Complete MCP Tool Reference (22 Tools)
142
+ ## 🌐 Complete MCP Tool Reference (25 Tools)
140
143
 
141
- The server exposes 22 highly optimized tools categorized into functional units:
144
+ The server exposes 25 highly optimized tools categorized into functional units:
142
145
 
143
146
  ### 🌐 Browser & Session
144
147
  | Tool Name | Description | Parameters |
@@ -182,6 +185,17 @@ The server exposes 22 highly optimized tools categorized into functional units:
182
185
  | `wait` | Smart delay with AI prediction or static timeout. | `duration` (number) |
183
186
  | `progress_tracker` | Track running automation progress with AI-estimated remaining times. | `step` (string), `percentage` (number) |
184
187
 
188
+ ### 📸 Capture
189
+ | Tool Name | Description | Parameters |
190
+ |:---|:---|:---|
191
+ | `screenshot` | Capture viewport, full page, or a specific element. Returns the image to the AI agent (base64) and can save to a file. | `fullPage` (boolean), `selector` (string), `format` (png/jpeg), `path` (string) |
192
+ | `save_as_pdf` | Save the current page as a PDF via Chromium print-to-PDF (headless mode only). | `path` (string), `format` (string), `landscape` (boolean) |
193
+
194
+ ### 👁️ AI Vision (Eyes)
195
+ | Tool Name | Description | Parameters |
196
+ |:---|:---|:---|
197
+ | `see_page` | Lets the AI **visually SEE** the page like human eyes: returns the actual screenshot image to the agent **plus** a "visual map" of every visible interactive element (button/link/input) with its on-screen position, text label, and a click-ready selector. Use it to look before deciding where to click/type. | `fullPage` (boolean), `format` (png/jpeg), `includeElements` (boolean), `maxElements` (number) |
198
+
185
199
  ---
186
200
 
187
201
  ## 📈 Evasion Performance & Test Coverage
@@ -291,11 +305,12 @@ Run these scripts from the project root directory:
291
305
  | `npm run dev` | Alias to start the MCP server. |
292
306
  | `npm run mcp` | Start the MCP server. |
293
307
  | `npm run mcp:verbose` | Start the MCP server with verbose logging on `stderr`. |
294
- | `npm run list` | Clean list of all 22 tools with emojis and categories. |
308
+ | `npm run list` | Clean list of all 25 tools with emojis and categories. |
295
309
  | `npm run build` | Validate workspace structure and confirm library status. |
296
310
  | `npm test` | Execute the full test suite (CJS & ESM). |
297
311
  | `npm run cjs_test` | Run CommonJS test scripts. |
298
312
  | `npm run esm_test` | Run ECMAScript Module test scripts. |
313
+ | `npm run mcp_test` | Fast, network-independent MCP smoke test (handshake + tool registry validation). |
299
314
 
300
315
  ---
301
316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "real-browser-mcp-server",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "MCP Server for Real Browser - Patchright (undetected Playwright fork) with 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",
@@ -27,7 +27,8 @@
27
27
  "list": "node src/index.js --list",
28
28
  "test": "npm run cjs_test && npm run esm_test",
29
29
  "esm_test": "node ./test/esm/test.mjs",
30
- "cjs_test": "node ./test/cjs/test.js"
30
+ "cjs_test": "node ./test/cjs/test.js",
31
+ "mcp_test": "node ./test/mcp/smoke-test.js"
31
32
  },
32
33
  "keywords": [
33
34
  "mcp-server",
package/src/ai/core.js CHANGED
@@ -242,7 +242,8 @@ class AICore {
242
242
  return await this.smartType(page, action.target, action.text, { humanLike });
243
243
 
244
244
  case 'navigate':
245
- await page.goto(action.url, { waitUntil: 'networkidle2' });
245
+ // Playwright/Patchright waitUntil: load|domcontentloaded|networkidle|commit
246
+ await page.goto(action.url, { waitUntil: 'networkidle' });
246
247
  return { success: true, url: action.url };
247
248
 
248
249
  case 'scroll':
@@ -1,13 +1,3 @@
1
- /**
2
- * AI Page Analyzer - Understand page structure and content
3
- *
4
- * Analyzes page to identify:
5
- * - Interactive elements (buttons, links, inputs)
6
- * - Forms and form fields
7
- * - Navigation structure
8
- * - Main content areas
9
- * - Media elements
10
- */
11
1
 
12
2
  class PageAnalyzer {
13
3
  constructor() {
@@ -274,8 +264,9 @@ class PageAnalyzer {
274
264
 
275
265
  // Add screenshot if requested
276
266
  if (includeScreenshot) {
277
- const screenshot = await page.screenshot({ encoding: 'base64', type: 'jpeg', quality: 50 });
278
- analysis.screenshot = screenshot;
267
+ // Playwright/Patchright returns a Buffer; convert to base64 ourselves
268
+ const buf = await page.screenshot({ type: 'jpeg', quality: 50 });
269
+ analysis.screenshot = Buffer.from(buf).toString('base64');
279
270
  }
280
271
 
281
272
  return analysis;
@@ -1,13 +1,3 @@
1
- /**
2
- * Brave Real Browser MCP Server - Tool Handlers
3
- *
4
- * Implementation of all 23 browser automation tools (optimized from 28)
5
- *
6
- * Environment Variables:
7
- * HEADLESS=true - Run browser in headless mode
8
- * HEADLESS=false - Run browser in GUI mode (visible)
9
- */
10
-
11
1
  const path = require('path');
12
2
  const fs = require('fs');
13
3
  const crypto = require('crypto');
@@ -106,6 +96,16 @@ function requireBrowser() {
106
96
  return { browser: browserInstance, page: pageInstance };
107
97
  }
108
98
 
99
+ /**
100
+ * Validate the waitUntil value for Playwright/Patchright.
101
+ * Only these states are supported: load | domcontentloaded | networkidle | commit.
102
+ * Any unsupported value safely falls back to 'networkidle'.
103
+ */
104
+ function resolveWaitUntil(value) {
105
+ const allowed = ['load', 'domcontentloaded', 'networkidle', 'commit'];
106
+ return allowed.includes(value) ? value : 'networkidle';
107
+ }
108
+
109
109
  /**
110
110
  * DECODER UTILITIES - URL, Base64, AES Decryption
111
111
  */
@@ -636,7 +636,7 @@ const handlers = {
636
636
  };
637
637
  });
638
638
 
639
- const pid = browserInstance.process()?.pid;
639
+ const pid = (typeof browserInstance.process === 'function') ? browserInstance.process()?.pid : null;
640
640
 
641
641
  notifyProgress('browser_init', 'completed', `Browser started (PID: ${pid})`, {
642
642
  headless,
@@ -656,7 +656,9 @@ const handlers = {
656
656
  // 2. Navigate (ENHANCED - handles context destroyed errors, retries)
657
657
  async navigate(params) {
658
658
  const { page } = requireBrowser();
659
- const { url, waitUntil = 'networkidle2', timeout = 30000, retries = 2 } = params;
659
+ let { url, waitUntil = 'networkidle', timeout = 30000, retries = 2 } = params;
660
+ // Playwright/Patchright wait states: load | domcontentloaded | networkidle | commit
661
+ waitUntil = resolveWaitUntil(waitUntil);
660
662
 
661
663
  notifyProgress('navigate', 'started', `Navigating to: ${url}`);
662
664
 
@@ -766,7 +768,7 @@ const handlers = {
766
768
  const url = rawHttpUrl || page.url();
767
769
  notifyProgress('get_content', 'in_progress', `Fetching raw HTTP (no JS) from: ${url}`);
768
770
  try {
769
- const cookies = await page.cookies(url);
771
+ const cookies = await page.context().cookies(url);
770
772
  const cookieStr = cookies.map(c => `${c.name}=${c.value}`).join('; ');
771
773
  const response = await fetch(url, {
772
774
  headers: {
@@ -797,7 +799,59 @@ const handlers = {
797
799
 
798
800
  let content;
799
801
 
800
- if (selector) {
802
+ // === markdown: real HTML→Markdown conversion (no external deps) ===
803
+ if (format === 'markdown') {
804
+ if (selector) {
805
+ const exists = await page.$(selector);
806
+ if (!exists) {
807
+ notifyProgress('get_content', 'error', `Element not found: ${selector}`);
808
+ return { success: false, error: `Element not found: ${selector}` };
809
+ }
810
+ }
811
+ content = await page.evaluate((sel) => {
812
+ const root = sel ? document.querySelector(sel) : document.body;
813
+ if (!root) return '';
814
+ const skip = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'SVG', 'CANVAS']);
815
+ const inline = (node) => {
816
+ let out = '';
817
+ node.childNodes.forEach(c => {
818
+ if (c.nodeType === 3) { out += c.textContent.replace(/\s+/g, ' '); return; }
819
+ if (c.nodeType !== 1 || skip.has(c.tagName)) return;
820
+ const t = c.tagName;
821
+ if (t === 'A') { const h = c.getAttribute('href') || ''; const x = inline(c).trim(); out += h ? `[${x}](${h})` : x; }
822
+ else if (t === 'STRONG' || t === 'B') out += `**${inline(c).trim()}**`;
823
+ else if (t === 'EM' || t === 'I') out += `*${inline(c).trim()}*`;
824
+ else if (t === 'CODE') out += '`' + c.textContent.trim() + '`';
825
+ else if (t === 'IMG') { const a = c.getAttribute('alt') || ''; const s = c.getAttribute('src') || ''; if (s) out += `![${a}](${s})`; }
826
+ else if (t === 'BR') out += '\n';
827
+ else out += inline(c);
828
+ });
829
+ return out;
830
+ };
831
+ const lines = [];
832
+ const walk = (node) => {
833
+ node.childNodes.forEach(c => {
834
+ if (c.nodeType === 3) { const x = c.textContent.trim(); if (x) lines.push(x); return; }
835
+ if (c.nodeType !== 1 || skip.has(c.tagName)) return;
836
+ const t = c.tagName;
837
+ if (/^H[1-6]$/.test(t)) lines.push('\n' + '#'.repeat(+t[1]) + ' ' + inline(c).trim() + '\n');
838
+ else if (t === 'P') { const x = inline(c).trim(); if (x) lines.push(x + '\n'); }
839
+ else if (t === 'UL' || t === 'OL') {
840
+ let i = 1;
841
+ c.querySelectorAll(':scope > li').forEach(li => lines.push((t === 'OL' ? (i++) + '. ' : '- ') + inline(li).trim()));
842
+ lines.push('');
843
+ }
844
+ else if (t === 'BLOCKQUOTE') lines.push('> ' + inline(c).trim() + '\n');
845
+ else if (t === 'PRE') lines.push('```\n' + c.textContent.trim() + '\n```\n');
846
+ else if (t === 'HR') lines.push('\n---\n');
847
+ else if (['A', 'STRONG', 'B', 'EM', 'I', 'CODE', 'IMG', 'SPAN', 'LABEL'].includes(t)) { const x = inline(c).trim(); if (x) lines.push(x); }
848
+ else walk(c);
849
+ });
850
+ };
851
+ walk(root);
852
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
853
+ }, selector || null);
854
+ } else if (selector) {
801
855
  const element = await page.$(selector);
802
856
  if (!element) {
803
857
  notifyProgress('get_content', 'error', `Element not found: ${selector}`);
@@ -812,11 +866,6 @@ const handlers = {
812
866
  } else {
813
867
  if (format === 'html') {
814
868
  content = await page.content();
815
- } else if (format === 'markdown') {
816
- content = await page.evaluate(() => {
817
- const body = document.body.innerText;
818
- return body;
819
- });
820
869
  } else {
821
870
  content = await page.evaluate(() => document.body.innerText);
822
871
  }
@@ -847,7 +896,7 @@ const handlers = {
847
896
  await page.waitForNavigation({ timeout });
848
897
  break;
849
898
  case 'networkidle':
850
- await page.waitForNetworkIdle({ timeout });
899
+ await page.waitForLoadState('networkidle', { timeout });
851
900
  break;
852
901
  case 'timeout':
853
902
  default:
@@ -1404,7 +1453,9 @@ const handlers = {
1404
1453
  notifyProgress('browser_close', 'progress', 'Browser closed gracefully');
1405
1454
  } catch (e) {
1406
1455
  if (force) {
1407
- browserInstance.process()?.kill('SIGKILL');
1456
+ if (typeof browserInstance.process === 'function') {
1457
+ browserInstance.process()?.kill('SIGKILL');
1458
+ }
1408
1459
  notifyProgress('browser_close', 'progress', 'Browser force killed');
1409
1460
  }
1410
1461
  }
@@ -1640,12 +1691,12 @@ const handlers = {
1640
1691
  try {
1641
1692
  const captchaEl = await targetFrame.$(detectedCaptchaSelector);
1642
1693
  if (captchaEl) {
1643
- captchaImageBase64 = await captchaEl.screenshot({ encoding: 'base64' });
1694
+ captchaImageBase64 = (await captchaEl.screenshot()).toString('base64');
1644
1695
  }
1645
1696
  } catch(e) {}
1646
1697
 
1647
1698
  // Take full context screenshot for host LLM fallback
1648
- const screenshotBase64 = await targetHandle.screenshot({ encoding: 'base64' });
1699
+ const screenshotBase64 = (await targetHandle.screenshot()).toString('base64');
1649
1700
 
1650
1701
  // ═══════════════════════════════════════════════════════════
1651
1702
  // STEP A: Server-Side Vision API (if aiMode enabled)
@@ -2104,7 +2155,7 @@ const handlers = {
2104
2155
  }
2105
2156
  }
2106
2157
  } else if (xpath) {
2107
- const handles = await page.$x(xpath);
2158
+ const handles = await page.$$(`xpath=${xpath}`);
2108
2159
  elements = await Promise.all(handles.map(h => h.evaluate(el => ({
2109
2160
  tag: el.tagName,
2110
2161
  text: el.textContent?.substring(0, 100)
@@ -2198,7 +2249,7 @@ const handlers = {
2198
2249
  page.on('framenavigated', frameNavigatedHandler);
2199
2250
 
2200
2251
  try {
2201
- await page.goto(url, { waitUntil: 'networkidle2', timeout });
2252
+ await page.goto(url, { waitUntil: 'networkidle', timeout });
2202
2253
 
2203
2254
  // If followJS is enabled, wait a bit and check for meta refreshes and JS redirects
2204
2255
  if (followJS) {
@@ -2938,29 +2989,51 @@ const handlers = {
2938
2989
 
2939
2990
  notifyProgress('cookie_manager', 'started', `Cookie action: ${action}`);
2940
2991
 
2992
+ // Playwright/Patchright: cookies are managed via the BrowserContext, not the page
2993
+ const context = page.context();
2994
+
2941
2995
  switch (action) {
2942
- case 'get':
2943
- const cookies = await page.cookies();
2996
+ case 'get': {
2997
+ const cookies = await context.cookies();
2944
2998
  notifyProgress('cookie_manager', 'completed', `Retrieved ${cookies.length} cookies`);
2945
2999
  return { success: true, cookies: name ? cookies.filter(c => c.name === name) : cookies };
3000
+ }
2946
3001
 
2947
- case 'set':
2948
- await page.setCookie({ name, value, domain: domain || new URL(page.url()).hostname, expires });
3002
+ case 'set': {
3003
+ await context.addCookies([{
3004
+ name,
3005
+ value,
3006
+ domain: domain || new URL(page.url()).hostname,
3007
+ path: '/',
3008
+ ...(expires ? { expires } : {})
3009
+ }]);
2949
3010
  notifyProgress('cookie_manager', 'completed', `Cookie set: ${name}`);
2950
3011
  return { success: true, message: `Cookie ${name} set` };
3012
+ }
2951
3013
 
2952
- case 'delete':
2953
- const toDelete = await page.cookies();
2954
- const filtered = name ? toDelete.filter(c => c.name === name) : toDelete;
2955
- await page.deleteCookie(...filtered);
2956
- notifyProgress('cookie_manager', 'completed', `Deleted ${filtered.length} cookie(s)`);
2957
- return { success: true, message: `Deleted ${filtered.length} cookie(s)` };
3014
+ case 'delete': {
3015
+ // Playwright has no per-cookie delete: clear all, then re-add the ones we keep
3016
+ const toDelete = await context.cookies();
3017
+ const remaining = name ? toDelete.filter(c => c.name !== name) : [];
3018
+ const removedCount = toDelete.length - remaining.length;
3019
+ await context.clearCookies();
3020
+ if (remaining.length) {
3021
+ await context.addCookies(remaining.map(c => ({
3022
+ name: c.name, value: c.value, domain: c.domain, path: c.path,
3023
+ ...(c.expires && c.expires > 0 ? { expires: c.expires } : {}),
3024
+ httpOnly: c.httpOnly, secure: c.secure, sameSite: c.sameSite
3025
+ })));
3026
+ }
3027
+ notifyProgress('cookie_manager', 'completed', `Deleted ${removedCount} cookie(s)`);
3028
+ return { success: true, message: `Deleted ${removedCount} cookie(s)` };
3029
+ }
2958
3030
 
2959
- case 'clear':
2960
- const allCookies = await page.cookies();
2961
- await page.deleteCookie(...allCookies);
3031
+ case 'clear': {
3032
+ const allCookies = await context.cookies();
3033
+ await context.clearCookies();
2962
3034
  notifyProgress('cookie_manager', 'completed', `Cleared ${allCookies.length} cookies`);
2963
3035
  return { success: true, message: `Cleared ${allCookies.length} cookies` };
3036
+ }
2964
3037
  }
2965
3038
 
2966
3039
  return { success: false, error: 'Invalid action' };
@@ -2977,8 +3050,8 @@ const handlers = {
2977
3050
  fs.mkdirSync(directory, { recursive: true });
2978
3051
  }
2979
3052
 
2980
- const response = await page.goto(url, { waitUntil: 'networkidle2' });
2981
- const buffer = await response.buffer();
3053
+ const response = await page.goto(url, { waitUntil: 'networkidle' });
3054
+ const buffer = await response.body();
2982
3055
 
2983
3056
  const outputFilename = filename || path.basename(new URL(url).pathname) || 'download';
2984
3057
  const outputPath = path.join(directory, outputFilename);
@@ -3516,7 +3589,7 @@ const handlers = {
3516
3589
  const inputType = await input.evaluate(el => el.type);
3517
3590
 
3518
3591
  if (tagName === 'select') {
3519
- await page.select(inputSelector, value);
3592
+ await page.selectOption(inputSelector, value);
3520
3593
  } else if (inputType === 'checkbox' || inputType === 'radio') {
3521
3594
  if (value) await input.click();
3522
3595
  } else {
@@ -3969,8 +4042,8 @@ const handlers = {
3969
4042
  notifyProgress('media_extractor', 'progress', `Processing ${i + 1}/${urls.length}: ${url}`);
3970
4043
 
3971
4044
  // Navigate to URL
3972
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
3973
- await page.waitForTimeout(2000); // Wait for media to load
4045
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
4046
+ await new Promise(r => setTimeout(r, 2000)); // Wait for media to load
3974
4047
 
3975
4048
  // Extract streams
3976
4049
  const streams = await extractStreamsFromContext(page, 'main');
@@ -4871,6 +4944,241 @@ const handlers = {
4871
4944
  filledFields,
4872
4945
  message: `Form filled: ${filledFields.join(', ')}`
4873
4946
  };
4947
+ },
4948
+
4949
+ // 23. Screenshot - capture viewport / full page / element (returns image to AI)
4950
+ async screenshot(params = {}) {
4951
+ const { page } = requireBrowser();
4952
+ const {
4953
+ fullPage = false,
4954
+ selector,
4955
+ format = 'png',
4956
+ quality,
4957
+ path: savePath,
4958
+ returnBase64 = true,
4959
+ omitBackground = false
4960
+ } = params;
4961
+
4962
+ notifyProgress('screenshot', 'started',
4963
+ `Capturing ${selector ? 'element' : (fullPage ? 'full page' : 'viewport')} screenshot`);
4964
+
4965
+ const opts = { type: format, fullPage: selector ? false : fullPage };
4966
+ if (format === 'jpeg' && typeof quality === 'number') opts.quality = quality;
4967
+ if (format === 'png' && omitBackground) opts.omitBackground = true;
4968
+
4969
+ let buffer;
4970
+ if (selector) {
4971
+ const element = await page.$(selector);
4972
+ if (!element) {
4973
+ notifyProgress('screenshot', 'error', `Element not found: ${selector}`);
4974
+ return { success: false, error: `Element not found: ${selector}` };
4975
+ }
4976
+ buffer = await element.screenshot(opts);
4977
+ } else {
4978
+ buffer = await page.screenshot(opts);
4979
+ }
4980
+
4981
+ let savedTo = null;
4982
+ if (savePath) {
4983
+ const dir = path.dirname(savePath);
4984
+ if (dir && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4985
+ fs.writeFileSync(savePath, buffer);
4986
+ savedTo = savePath;
4987
+ }
4988
+
4989
+ notifyProgress('screenshot', 'completed',
4990
+ `Screenshot captured (${buffer.length} bytes)${savedTo ? ' → ' + savedTo : ''}`);
4991
+
4992
+ const meta = { success: true, format, bytes: buffer.length, savedTo, url: page.url() };
4993
+
4994
+ if (returnBase64) {
4995
+ const base64 = Buffer.from(buffer).toString('base64');
4996
+ // mcpContent is returned directly to the AI agent (image + text summary)
4997
+ meta.mcpContent = [
4998
+ { type: 'image', data: base64, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' },
4999
+ { type: 'text', text: JSON.stringify({ success: true, format, bytes: buffer.length, savedTo, url: page.url() }, null, 2) }
5000
+ ];
5001
+ }
5002
+
5003
+ return meta;
5004
+ },
5005
+
5006
+ // 24. Save as PDF - Chromium print-to-PDF (headless mode only)
5007
+ async save_as_pdf(params = {}) {
5008
+ const { page } = requireBrowser();
5009
+ const {
5010
+ path: savePath = './downloads/page.pdf',
5011
+ format = 'A4',
5012
+ landscape = false,
5013
+ printBackground = true
5014
+ } = params;
5015
+
5016
+ notifyProgress('save_as_pdf', 'started', `Saving page as PDF: ${savePath}`);
5017
+
5018
+ const dir = path.dirname(savePath);
5019
+ if (dir && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
5020
+
5021
+ try {
5022
+ await page.pdf({ path: savePath, format, landscape, printBackground });
5023
+ } catch (e) {
5024
+ notifyProgress('save_as_pdf', 'error', e.message);
5025
+ return {
5026
+ success: false,
5027
+ error: `PDF generation failed (page.pdf() works only in headless mode): ${e.message}`
5028
+ };
5029
+ }
5030
+
5031
+ const stats = fs.existsSync(savePath) ? fs.statSync(savePath) : null;
5032
+ notifyProgress('save_as_pdf', 'completed', `PDF saved: ${savePath}`);
5033
+
5034
+ return {
5035
+ success: true,
5036
+ savedTo: savePath,
5037
+ bytes: stats ? stats.size : null,
5038
+ url: page.url()
5039
+ };
5040
+ },
5041
+
5042
+ // 25. See Page (AI Vision — "eyes": screenshot + visual map of interactive elements)
5043
+ async see_page(params = {}) {
5044
+ const { page } = requireBrowser();
5045
+ const {
5046
+ fullPage = false,
5047
+ format = 'jpeg',
5048
+ quality = 70,
5049
+ includeElements = true,
5050
+ maxElements = 60,
5051
+ path: savePath
5052
+ } = params;
5053
+
5054
+ notifyProgress('see_page', 'started', `👁️ Looking at the page (${fullPage ? 'full page' : 'viewport'})...`);
5055
+
5056
+ // 1. Capture what the page looks like (the "eyes")
5057
+ const shotOpts = { type: format, fullPage };
5058
+ if (format === 'jpeg' && typeof quality === 'number') shotOpts.quality = quality;
5059
+
5060
+ let buffer;
5061
+ try {
5062
+ buffer = await page.screenshot(shotOpts);
5063
+ } catch (e) {
5064
+ return { success: false, error: `Vision capture failed: ${e.message}` };
5065
+ }
5066
+
5067
+ // 2. Build a "visual map" of visible interactive elements (what a human can act on)
5068
+ let elements = [];
5069
+ let pageInfo = {};
5070
+ if (includeElements) {
5071
+ const data = await page.evaluate((maxEls) => {
5072
+ const out = [];
5073
+ const seen = new Set();
5074
+ const sel = 'a[href], button, input, select, textarea, [role="button"], [role="link"], [onclick], [tabindex]';
5075
+ const nodes = document.querySelectorAll(sel);
5076
+
5077
+ const cssPath = (el) => {
5078
+ if (el.id) return `#${CSS.escape(el.id)}`;
5079
+ if (el.name) return `${el.tagName.toLowerCase()}[name="${el.name}"]`;
5080
+ const parts = [];
5081
+ let node = el;
5082
+ while (node && node.nodeType === 1 && parts.length < 4) {
5083
+ let part = node.tagName.toLowerCase();
5084
+ if (node.classList.length) {
5085
+ const cls = Array.from(node.classList).slice(0, 2).map(c => '.' + CSS.escape(c)).join('');
5086
+ part += cls;
5087
+ }
5088
+ const parent = node.parentElement;
5089
+ if (parent) {
5090
+ const sibs = Array.from(parent.children).filter(c => c.tagName === node.tagName);
5091
+ if (sibs.length > 1) part += `:nth-of-type(${sibs.indexOf(node) + 1})`;
5092
+ }
5093
+ parts.unshift(part);
5094
+ node = node.parentElement;
5095
+ }
5096
+ return parts.join(' > ');
5097
+ };
5098
+
5099
+ for (const el of nodes) {
5100
+ if (out.length >= maxEls) break;
5101
+ const rect = el.getBoundingClientRect();
5102
+ // Only elements actually visible on screen
5103
+ if (rect.width < 2 || rect.height < 2) continue;
5104
+ const style = window.getComputedStyle(el);
5105
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
5106
+ if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth) {
5107
+ // outside current viewport — skip (we report what is seen)
5108
+ continue;
5109
+ }
5110
+
5111
+ const tag = el.tagName.toLowerCase();
5112
+ let label = (el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('title') || el.getAttribute('alt') || '').trim().replace(/\s+/g, ' ').slice(0, 80);
5113
+ let kind = tag;
5114
+ if (tag === 'a') kind = 'link';
5115
+ else if (tag === 'button' || el.getAttribute('role') === 'button') kind = 'button';
5116
+ else if (tag === 'input') kind = `input:${el.type || 'text'}`;
5117
+ else if (tag === 'select') kind = 'select';
5118
+ else if (tag === 'textarea') kind = 'textarea';
5119
+
5120
+ const selector = cssPath(el);
5121
+ if (seen.has(selector + '|' + label)) continue;
5122
+ seen.add(selector + '|' + label);
5123
+
5124
+ out.push({
5125
+ kind,
5126
+ text: label,
5127
+ selector,
5128
+ href: tag === 'a' ? el.href : undefined,
5129
+ box: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
5130
+ });
5131
+ }
5132
+
5133
+ return {
5134
+ elements: out,
5135
+ info: {
5136
+ title: document.title,
5137
+ url: location.href,
5138
+ viewport: { width: window.innerWidth, height: window.innerHeight },
5139
+ scrollY: Math.round(window.scrollY),
5140
+ scrollHeight: document.body ? document.body.scrollHeight : 0
5141
+ }
5142
+ };
5143
+ }, maxElements).catch(() => ({ elements: [], info: {} }));
5144
+
5145
+ elements = data.elements || [];
5146
+ pageInfo = data.info || {};
5147
+ }
5148
+
5149
+ // 3. Optionally save the image too
5150
+ let savedTo = null;
5151
+ if (savePath) {
5152
+ const dir = path.dirname(savePath);
5153
+ if (dir && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
5154
+ fs.writeFileSync(savePath, buffer);
5155
+ savedTo = savePath;
5156
+ }
5157
+
5158
+ notifyProgress('see_page', 'completed',
5159
+ `👁️ Saw the page: ${elements.length} interactive elements visible${savedTo ? ' (saved ' + savedTo + ')' : ''}`);
5160
+
5161
+ const base64 = Buffer.from(buffer).toString('base64');
5162
+ const summary = {
5163
+ success: true,
5164
+ url: pageInfo.url || page.url(),
5165
+ title: pageInfo.title,
5166
+ viewport: pageInfo.viewport,
5167
+ scroll: { y: pageInfo.scrollY, pageHeight: pageInfo.scrollHeight },
5168
+ visibleInteractiveElements: elements.length,
5169
+ elements,
5170
+ savedTo
5171
+ };
5172
+
5173
+ // Return BOTH the actual image (so the AI literally "sees" it) and the visual map text
5174
+ return {
5175
+ success: true,
5176
+ mcpContent: [
5177
+ { type: 'image', data: base64, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' },
5178
+ { type: 'text', text: JSON.stringify(summary, null, 2) }
5179
+ ],
5180
+ ...summary
5181
+ };
4874
5182
  }
4875
5183
  };
4876
5184
 
@@ -4950,7 +5258,7 @@ async function aiEnhancedSelector(page, selector, operation, options = {}) {
4950
5258
  * AI Features automatically applied:
4951
5259
  * - Auto-healing: If selector fails, AI tries to find alternatives
4952
5260
  * - Smart retry: Failed operations are retried with AI assistance
4953
- * - All 28 tools benefit from AI without any changes
5261
+ * - All tools benefit from AI without any changes
4954
5262
  */
4955
5263
  async function executeTool(name, params = {}) {
4956
5264
  const handler = handlers[name];
@@ -5063,7 +5371,9 @@ async function cleanup() {
5063
5371
  try {
5064
5372
  await browserInstance.close();
5065
5373
  } catch (e) {
5066
- browserInstance.process()?.kill('SIGKILL');
5374
+ if (typeof browserInstance.process === 'function') {
5375
+ browserInstance.process()?.kill('SIGKILL');
5376
+ }
5067
5377
  }
5068
5378
  browserInstance = null;
5069
5379
  pageInstance = null;
package/src/mcp/server.js CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * Brave Real Browser MCP Server
3
- *
4
- * Model Context Protocol Server with STDIO Transport
5
- * Supports: Claude, Cursor, Copilot, and other MCP-compatible AI assistants
6
- */
7
-
8
- // CRITICAL: Redirect ALL console.log to STDERR before ANY imports
9
- // MCP uses STDIO transport — STDOUT must contain ONLY JSON-RPC messages.
10
- // Any console.log from this code or ANY dependency (playwright, blocker, etc.)
11
- // will corrupt the JSON-RPC stream and cause parsing errors.
12
1
  const _originalConsoleLog = console.log;
13
2
  console.log = function (...args) {
14
3
  console.error(...args);
@@ -26,6 +15,14 @@ const {
26
15
  const { TOOLS } = require('./tools.js');
27
16
  const { executeTool, cleanup } = require('./handlers.js');
28
17
 
18
+ // Single source of truth: read version from package.json (avoids version drift)
19
+ let PKG_VERSION = '0.0.0';
20
+ try {
21
+ PKG_VERSION = require('../../package.json').version || PKG_VERSION;
22
+ } catch (e) {
23
+ console.error('⚠️ Could not read version from package.json:', e.message);
24
+ }
25
+
29
26
  /**
30
27
  * Create and configure MCP Server
31
28
  */
@@ -33,7 +30,7 @@ function createServer() {
33
30
  const server = new Server(
34
31
  {
35
32
  name: 'real-browser-mcp-server',
36
- version: '2.48.36',
33
+ version: PKG_VERSION,
37
34
  },
38
35
  {
39
36
  capabilities: {
@@ -1,21 +1,3 @@
1
- /**
2
- * Brave Real Browser MCP Server - Shared Tool Definitions
3
- *
4
- * OPTIMIZED: 22 tools (merged from 28)
5
- *
6
- * Merges Applied:
7
- * - iframe_handler + stream_extractor + player_api_hook → media_extractor
8
- * - get_content + js_scrape → get_content (enhanced)
9
- * - search_regex + extract_json + scrape_meta_tags → extract_data
10
- * - solve_captcha + form_automator → solve_captcha (enhanced)
11
- *
12
- * New Features:
13
- * - URL/Base64/AES Decoders built into media_extractor
14
- * - AI Auto-Healing Selectors
15
- * - Smart Retry Mechanisms
16
- * - Batch Operations
17
- */
18
-
19
1
  const TOOLS = [
20
2
  // 1. Browser Init
21
3
  {
@@ -59,7 +41,7 @@ const TOOLS = [
59
41
  type: 'object',
60
42
  properties: {
61
43
  url: { type: 'string' },
62
- waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'], default: 'networkidle2' },
44
+ waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle', 'commit'], default: 'networkidle' },
63
45
  timeout: { type: 'number', default: 30000 },
64
46
  retries: { type: 'number', default: 3, description: 'Auto-retry on failures' },
65
47
  smartWait: { type: 'boolean', default: true, description: 'AI-powered smart waiting' }
@@ -575,6 +557,71 @@ const TOOLS = [
575
557
  },
576
558
  required: ['code']
577
559
  }
560
+ },
561
+
562
+ // 23. Screenshot
563
+ {
564
+ name: 'screenshot',
565
+ emoji: '📸',
566
+ description: 'Capture a screenshot of the viewport, the full scrollable page, or a specific element. Returns the image directly to the AI agent (base64) and can optionally save it to a file. EFFICIENCY RULE: PREFER a single FULL-PAGE screenshot (set fullPage: true) so the entire page is captured in one shot, then complete ALL related work for that page from this single capture (read, click, type, extract). Take a SECOND screenshot ONLY IF the work genuinely cannot be finished from the first one, OR after the page actually changes (navigation, modal/popup, or new dynamic content loads). Do NOT take repeated screenshots of the SAME unchanged page.',
567
+ descriptionHindi: 'स्क्रीनशॉट लेना — viewport, पूरा पेज, या किसी element का। Image सीधे AI को मिलती है + file में save हो सकती है। नियम: पहले पूरे पेज का full-page (लॉन्ग) स्क्रीनशॉट लें (fullPage: true) ताकि पूरा पेज एक ही बार में दिख जाए, फिर उसी एक image से उस पेज का सारा काम (पढ़ना, क्लिक, टाइप, data निकालना) एक साथ complete करें। दूसरा स्क्रीनशॉट सिर्फ़ तभी लें जब पहले वाले से काम पूरा न हो पाए, या पेज सच में बदल जाए (navigation, modal/popup, या नया dynamic content)। बिना बदलाव के उसी पेज का बार-बार स्क्रीनशॉट न लें।',
568
+ category: 'capture',
569
+ requiresBrowser: true,
570
+ requiresPage: true,
571
+ inputSchema: {
572
+ type: 'object',
573
+ properties: {
574
+ fullPage: { type: 'boolean', default: false, description: 'Capture the full scrollable page' },
575
+ selector: { type: 'string', description: 'CSS selector to screenshot a specific element only' },
576
+ format: { type: 'string', enum: ['png', 'jpeg'], default: 'png' },
577
+ quality: { type: 'number', description: 'JPEG quality 0-100 (jpeg only)' },
578
+ path: { type: 'string', description: 'Optional file path to save the screenshot' },
579
+ returnBase64: { type: 'boolean', default: true, description: 'Return image to AI as base64 (MCP image content)' },
580
+ omitBackground: { type: 'boolean', default: false, description: 'Transparent background (png only)' }
581
+ }
582
+ }
583
+ },
584
+
585
+ // 24. Save as PDF
586
+ {
587
+ name: 'save_as_pdf',
588
+ emoji: '📑',
589
+ description: 'Save the current page as a PDF file using Chromium print-to-PDF. Note: works only in headless mode.',
590
+ descriptionHindi: 'पेज को PDF में सेव करना (Chromium print-to-PDF)। ध्यान: सिर्फ़ headless mode में काम करता है।',
591
+ category: 'capture',
592
+ requiresBrowser: true,
593
+ requiresPage: true,
594
+ inputSchema: {
595
+ type: 'object',
596
+ properties: {
597
+ path: { type: 'string', default: './downloads/page.pdf', description: 'File path to save the PDF' },
598
+ format: { type: 'string', default: 'A4', description: 'Paper format: A4, Letter, Legal, etc.' },
599
+ landscape: { type: 'boolean', default: false },
600
+ printBackground: { type: 'boolean', default: true, description: 'Include background graphics' }
601
+ }
602
+ }
603
+ },
604
+
605
+ // 25. See Page (AI Vision — "eyes")
606
+ {
607
+ name: 'see_page',
608
+ emoji: '👁️',
609
+ description: 'AI VISION ("eyes"): Visually SEE the current page exactly like a human does. Captures a screenshot and returns the actual image to the AI agent so it can visually understand the layout, AND returns a "visual map" of all visible interactive elements (buttons, links, inputs) with their on-screen position (x/y/width/height), text label, and a click-ready selector. Use this to look at a page before deciding where to click/type. EFFICIENCY RULE: PREFER a single FULL-PAGE view (set fullPage: true) so the whole page and all its interactive elements are mapped in one shot, then plan and perform ALL needed actions for that page (read, click, type, extract) from this single view. Call see_page a SECOND time ONLY IF the task genuinely cannot be completed from the first view, OR after the page actually changes — navigation, a modal/popup opens, or new dynamic content loads. Do NOT re-capture the SAME unchanged page repeatedly.',
610
+ descriptionHindi: 'AI विज़न ("आँखें"): पेज को इंसान की तरह देखना। स्क्रीनशॉट image सीधे AI को भेजता है ताकि वह layout देख सके + सभी दिखने वाले clickable elements का visual map (position + text + selector) देता है। नियम: पहले पूरे पेज का full-page view लें (fullPage: true) ताकि पूरा पेज और उसके सारे elements एक ही बार में map हो जाएँ, फिर उसी एक view से उस पेज के सारे ज़रूरी काम (पढ़ना, क्लिक, टाइप, data निकालना) एक साथ पूरे करें। दूसरी बार see_page सिर्फ़ तभी लें जब पहले view से काम पूरा न हो पाए, या पेज सच में बदल जाए (navigation, modal/popup खुले, या नया dynamic content load हो)। बिना बदलाव के उसी पेज का दोबारा स्क्रीनशॉट न लें।',
611
+ category: 'vision',
612
+ requiresBrowser: true,
613
+ requiresPage: true,
614
+ inputSchema: {
615
+ type: 'object',
616
+ properties: {
617
+ fullPage: { type: 'boolean', default: false, description: 'See the entire scrollable page (true) or just the current viewport (false)' },
618
+ format: { type: 'string', enum: ['png', 'jpeg'], default: 'jpeg', description: 'Image format (jpeg = smaller, faster for vision)' },
619
+ quality: { type: 'number', default: 70, description: 'JPEG quality 0-100 (lower = smaller image to the AI)' },
620
+ includeElements: { type: 'boolean', default: true, description: 'Include the visual map of interactive elements' },
621
+ maxElements: { type: 'number', default: 60, description: 'Max number of interactive elements to map' },
622
+ path: { type: 'string', description: 'Optional file path to also save the captured image' }
623
+ }
624
+ }
578
625
  }
579
626
  ];
580
627
 
@@ -586,6 +633,8 @@ const CATEGORIES = {
586
633
  extraction: { name: 'Extraction', emoji: '📄', description: 'Content extraction and scraping' },
587
634
  network: { name: 'Network', emoji: '📡', description: 'Network operations' },
588
635
  analysis: { name: 'Analysis', emoji: '🧠', description: 'Page analysis' },
636
+ capture: { name: 'Capture', emoji: '📸', description: 'Screenshots and PDF capture' },
637
+ vision: { name: 'Vision', emoji: '👁️', description: 'AI visual perception (sees pages like human eyes)' },
589
638
  utility: { name: 'Utility', emoji: '🛠️', description: 'Utility tools' }
590
639
  };
591
640
 
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Smoke Test — network-independent, fast.
4
+ *
5
+ * Spawns the MCP server over STDIO exactly like a real client (Cline/Claude),
6
+ * performs the JSON-RPC handshake, and verifies:
7
+ * 1. STDOUT carries ONLY clean JSON-RPC (no banner/log contamination)
8
+ * 2. `initialize` returns valid serverInfo (name + version from package.json)
9
+ * 3. `tools/list` returns all expected tools with valid schemas
10
+ * 4. Tool definitions match src/shared/tools.js (count + names)
11
+ * 5. Every tool in the registry has a handler implementation
12
+ *
13
+ * This does NOT launch a browser, so it runs in milliseconds and in CI.
14
+ * Exit code 0 = all pass, 1 = failure.
15
+ */
16
+
17
+ const { spawn } = require('child_process');
18
+ const path = require('path');
19
+ const assert = require('assert');
20
+
21
+ const ROOT = path.join(__dirname, '..', '..');
22
+ const SERVER = path.join(ROOT, 'src', 'index.js');
23
+ const { TOOLS } = require(path.join(ROOT, 'src', 'shared', 'tools.js'));
24
+ const { handlers } = require(path.join(ROOT, 'src', 'mcp', 'handlers.js'));
25
+ const PKG = require(path.join(ROOT, 'package.json'));
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+ function check(name, fn) {
30
+ try {
31
+ fn();
32
+ console.log(` \u2705 ${name}`);
33
+ passed++;
34
+ } catch (e) {
35
+ console.log(` \u274c ${name}\n ${e.message}`);
36
+ failed++;
37
+ }
38
+ }
39
+
40
+ function rpc(child, obj) {
41
+ child.stdin.write(JSON.stringify(obj) + '\n');
42
+ }
43
+
44
+ async function main() {
45
+ console.log('\n\ud83e\uddea MCP Smoke Test (no browser)\n');
46
+
47
+ // --- Static checks (no spawn needed) ---
48
+ console.log('Static checks:');
49
+ check('every registered tool has a handler', () => {
50
+ const missing = TOOLS.filter(t => typeof handlers[t.name] !== 'function').map(t => t.name);
51
+ assert.strictEqual(missing.length, 0, `missing handlers: ${missing.join(', ')}`);
52
+ });
53
+ check('every tool has name + inputSchema', () => {
54
+ const bad = TOOLS.filter(t => !t.name || !t.inputSchema || t.inputSchema.type !== 'object');
55
+ assert.strictEqual(bad.length, 0, `invalid tool defs: ${bad.map(t => t.name).join(', ')}`);
56
+ });
57
+ check('no duplicate tool names', () => {
58
+ const names = TOOLS.map(t => t.name);
59
+ const dupes = names.filter((n, i) => names.indexOf(n) !== i);
60
+ assert.strictEqual(dupes.length, 0, `duplicates: ${dupes.join(', ')}`);
61
+ });
62
+
63
+ // --- Live STDIO handshake ---
64
+ console.log('\nLive STDIO handshake:');
65
+ const child = spawn('node', [SERVER], {
66
+ stdio: ['pipe', 'pipe', 'pipe'],
67
+ env: { ...process.env, HEADLESS: 'true' },
68
+ });
69
+
70
+ const responses = {};
71
+ let stdoutBuf = '';
72
+ let nonJsonLines = 0;
73
+
74
+ child.stdout.on('data', (d) => {
75
+ stdoutBuf += d.toString();
76
+ let idx;
77
+ while ((idx = stdoutBuf.indexOf('\n')) >= 0) {
78
+ const line = stdoutBuf.slice(0, idx).trim();
79
+ stdoutBuf = stdoutBuf.slice(idx + 1);
80
+ if (!line) continue;
81
+ try {
82
+ const msg = JSON.parse(line);
83
+ if (msg.id !== undefined) responses[msg.id] = msg;
84
+ } catch (e) {
85
+ nonJsonLines++;
86
+ }
87
+ }
88
+ });
89
+
90
+ const wait = (ms) => new Promise(r => setTimeout(r, ms));
91
+
92
+ await wait(1200);
93
+ rpc(child, {
94
+ jsonrpc: '2.0', id: 1, method: 'initialize',
95
+ params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'smoke-test', version: '1.0.0' } },
96
+ });
97
+ await wait(1200);
98
+ rpc(child, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
99
+ await wait(1500);
100
+
101
+ child.kill();
102
+
103
+ check('STDOUT had zero non-JSON lines (clean JSON-RPC stream)', () => {
104
+ assert.strictEqual(nonJsonLines, 0, `${nonJsonLines} non-JSON line(s) leaked to STDOUT`);
105
+ });
106
+ check('initialize responded', () => {
107
+ assert.ok(responses[1], 'no response for id=1');
108
+ assert.ok(responses[1].result, 'initialize has no result');
109
+ });
110
+ check('serverInfo.name is correct', () => {
111
+ assert.strictEqual(responses[1].result.serverInfo.name, 'real-browser-mcp-server');
112
+ });
113
+ check('serverInfo.version matches package.json', () => {
114
+ assert.strictEqual(responses[1].result.serverInfo.version, PKG.version,
115
+ `server reported ${responses[1].result.serverInfo.version}, package.json is ${PKG.version}`);
116
+ });
117
+ check('tools/list responded', () => {
118
+ assert.ok(responses[2], 'no response for id=2');
119
+ assert.ok(Array.isArray(responses[2].result.tools), 'tools is not an array');
120
+ });
121
+ check(`tools/list returns ${TOOLS.length} tools`, () => {
122
+ assert.strictEqual(responses[2].result.tools.length, TOOLS.length);
123
+ });
124
+ check('tools/list names match registry', () => {
125
+ const live = responses[2].result.tools.map(t => t.name).sort();
126
+ const reg = TOOLS.map(t => t.name).sort();
127
+ assert.deepStrictEqual(live, reg);
128
+ });
129
+ check('every live tool has an inputSchema', () => {
130
+ const bad = responses[2].result.tools.filter(t => !t.inputSchema);
131
+ assert.strictEqual(bad.length, 0, `missing inputSchema: ${bad.map(t => t.name).join(', ')}`);
132
+ });
133
+
134
+ console.log(`\n\u2139 passed ${passed}, failed ${failed}\n`);
135
+ process.exit(failed === 0 ? 0 : 1);
136
+ }
137
+
138
+ main().catch(e => {
139
+ console.error('Smoke test crashed:', e);
140
+ process.exit(1);
141
+ });