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.
- package/.github/workflows/publish.yml +27 -1
- package/README.md +20 -5
- package/package.json +3 -2
- package/src/ai/core.js +2 -1
- package/src/ai/page-analyzer.js +3 -12
- package/src/mcp/handlers.js +355 -45
- package/src/mcp/server.js +9 -12
- package/src/shared/tools.js +68 -19
- package/test/mcp/smoke-test.js +141 -0
|
@@ -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
|
-
"
|
|
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 (
|
|
142
|
+
## 🌐 Complete MCP Tool Reference (25 Tools)
|
|
140
143
|
|
|
141
|
-
The server exposes
|
|
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
|
|
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.
|
|
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
|
-
|
|
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':
|
package/src/ai/page-analyzer.js
CHANGED
|
@@ -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
|
-
|
|
278
|
-
|
|
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;
|
package/src/mcp/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 += ``; }
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
2954
|
-
const
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
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
|
|
2961
|
-
await
|
|
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: '
|
|
2981
|
-
const buffer = await response.
|
|
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.
|
|
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: '
|
|
3973
|
-
await
|
|
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
|
|
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
|
|
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:
|
|
33
|
+
version: PKG_VERSION,
|
|
37
34
|
},
|
|
38
35
|
{
|
|
39
36
|
capabilities: {
|
package/src/shared/tools.js
CHANGED
|
@@ -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', '
|
|
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
|
+
});
|