mcpbrowser 0.3.35 → 0.3.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/package.json +2 -1
- package/src/actions/click-element.js +9 -6
- package/src/actions/execute-javascript.js +9 -6
- package/src/actions/fetch-page.js +10 -6
- package/src/actions/get-current-html.js +10 -6
- package/src/cli/args.js +81 -0
- package/src/cli/help.js +169 -0
- package/src/cli/index.js +130 -0
- package/src/cli/registry.js +256 -0
- package/src/cli/utils.js +39 -0
- package/src/core/plugin-loader.js +26 -5
- package/src/mcp-browser.js +22 -4
- package/src/plugins/_example/index.js +1 -0
- package/src/plugins/gcal/actions/check-availability.js +185 -0
- package/src/plugins/gcal/actions/create-event.js +238 -0
- package/src/plugins/gcal/actions/delete-event.js +138 -0
- package/src/plugins/gcal/actions/edit-event.js +244 -0
- package/src/plugins/gcal/actions/list-events.js +96 -0
- package/src/plugins/gcal/actions/read-event.js +174 -0
- package/src/plugins/gcal/actions/rsvp-event.js +149 -0
- package/src/plugins/gcal/actions/search-events.js +121 -0
- package/src/plugins/gcal/helpers.js +415 -0
- package/src/plugins/gcal/index.js +148 -0
- package/src/plugins/gcal/selectors.js +54 -0
- package/src/plugins/gmail/index.js +1 -0
- package/src/plugins.json +2 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ Example workflow for AI assistant to use MCPBrowser
|
|
|
38
38
|
- [scroll_page](#scroll_page)
|
|
39
39
|
- [take_screenshot](#take_screenshot)
|
|
40
40
|
- [close_tab](#close_tab)
|
|
41
|
+
- [CLI Mode](#cli-mode)
|
|
41
42
|
- [Configuration](#configuration-optional)
|
|
42
43
|
- [Troubleshooting](#troubleshooting)
|
|
43
44
|
- [Links](#links)
|
|
@@ -397,6 +398,38 @@ Closes the browser tab for the given URL's hostname. Removes the page from the t
|
|
|
397
398
|
- Reset to fresh state before new login
|
|
398
399
|
|
|
399
400
|
|
|
401
|
+
## CLI Mode
|
|
402
|
+
|
|
403
|
+
MCPBrowser can also be used as a **standalone command-line tool**, making it easy to use from shell scripts, CI/CD pipelines, or AI agents that work through shell commands (like GitHub Copilot CLI).
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
# Show help
|
|
407
|
+
mcpbrowser --help
|
|
408
|
+
|
|
409
|
+
# Fetch a page (handles auth, SSO, SPAs automatically)
|
|
410
|
+
mcpbrowser fetch https://eng.ms/docs/my-page
|
|
411
|
+
|
|
412
|
+
# Fetch with raw HTML output
|
|
413
|
+
mcpbrowser fetch https://github.com --browser chrome --raw
|
|
414
|
+
|
|
415
|
+
# Take a screenshot
|
|
416
|
+
mcpbrowser screenshot https://example.com --output page.png --full-page
|
|
417
|
+
|
|
418
|
+
# Click an element on a loaded page
|
|
419
|
+
mcpbrowser click https://example.com --selector "#login-btn"
|
|
420
|
+
|
|
421
|
+
# Type into a form field
|
|
422
|
+
mcpbrowser type https://example.com --selector "input[name=q]" --text "search query"
|
|
423
|
+
|
|
424
|
+
# Execute JavaScript
|
|
425
|
+
mcpbrowser exec https://example.com --script "document.title"
|
|
426
|
+
|
|
427
|
+
# Get current HTML of a loaded page
|
|
428
|
+
mcpbrowser html https://example.com
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**CLI vs MCP mode:** When run without arguments, MCPBrowser starts as an MCP server (stdin/stdout JSON-RPC). When run with a subcommand, it executes the command and exits — no MCP protocol needed.
|
|
432
|
+
|
|
400
433
|
|
|
401
434
|
## Configuration (Optional)
|
|
402
435
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpbrowser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.37",
|
|
4
4
|
"mcpName": "io.github.cherchyk/mcpbrowser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP browser server - fetch web pages using real Chrome/Edge/Brave browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"mcp": "node src/mcp-browser.js",
|
|
13
13
|
"test": "node tests/run-all.js",
|
|
14
14
|
"test:unit": "node tests/run-unit.js",
|
|
15
|
+
"test:cli": "node tests/cli.test.js",
|
|
15
16
|
"test:descriptions": "node tests/tool-selection/run-tool-selection-tests.js"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
@@ -28,7 +28,7 @@ import { getBrowser, getValidatedPage } from '../core/browser.js';
|
|
|
28
28
|
import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
|
|
29
29
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
30
30
|
import logger from '../core/logger.js';
|
|
31
|
-
import { getPluginNextSteps } from '../core/plugin-loader.js';
|
|
31
|
+
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -38,7 +38,7 @@ import { getPluginNextSteps } from '../core/plugin-loader.js';
|
|
|
38
38
|
* Structured response for click_element with JS fallback metadata
|
|
39
39
|
*/
|
|
40
40
|
export class ClickWithFallbackResponse extends MCPResponse {
|
|
41
|
-
constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [] }) {
|
|
41
|
+
constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [], recommendedPlugins = [] }) {
|
|
42
42
|
super(nextSteps);
|
|
43
43
|
this.status = status;
|
|
44
44
|
this.fallbackUsed = fallbackUsed;
|
|
@@ -48,6 +48,7 @@ export class ClickWithFallbackResponse extends MCPResponse {
|
|
|
48
48
|
this.currentUrl = currentUrl;
|
|
49
49
|
this.html = html;
|
|
50
50
|
this.message = message;
|
|
51
|
+
this.recommendedPlugins = recommendedPlugins;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
_getAdditionalFields() {
|
|
@@ -59,7 +60,8 @@ export class ClickWithFallbackResponse extends MCPResponse {
|
|
|
59
60
|
postClickWait: this.postClickWait,
|
|
60
61
|
currentUrl: this.currentUrl,
|
|
61
62
|
html: this.html,
|
|
62
|
-
message: this.message
|
|
63
|
+
message: this.message,
|
|
64
|
+
recommendedPlugins: this.recommendedPlugins
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -329,12 +331,12 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
329
331
|
|
|
330
332
|
const nextSteps = returnHtml
|
|
331
333
|
? [
|
|
334
|
+
...(html ? getPluginNextSteps(currentUrl, html) : []),
|
|
332
335
|
"Use MCPBrowser's click_element again to navigate further",
|
|
333
336
|
"Use MCPBrowser's type_text to fill forms if needed",
|
|
334
337
|
"Use MCPBrowser's get_current_html to refresh page state",
|
|
335
338
|
"Use MCPBrowser's take_screenshot if page has popups or visual content that's hard to parse from HTML",
|
|
336
|
-
"Use MCPBrowser's close_tab when finished"
|
|
337
|
-
...(html ? getPluginNextSteps(currentUrl, html) : [])
|
|
339
|
+
"Use MCPBrowser's close_tab when finished"
|
|
338
340
|
]
|
|
339
341
|
: [
|
|
340
342
|
"Use MCPBrowser's get_current_html to see updated page state",
|
|
@@ -354,7 +356,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
354
356
|
currentUrl,
|
|
355
357
|
html,
|
|
356
358
|
message,
|
|
357
|
-
nextSteps
|
|
359
|
+
nextSteps,
|
|
360
|
+
recommendedPlugins: html ? getRecommendedPlugins(currentUrl, html) : []
|
|
358
361
|
});
|
|
359
362
|
} catch (err) {
|
|
360
363
|
logger.error(`click_element failed: ${err.message}`);
|
|
@@ -7,7 +7,7 @@ import { waitForPageReady } from '../core/page.js';
|
|
|
7
7
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
8
8
|
import logger from '../core/logger.js';
|
|
9
9
|
import { serializeExecutionResult } from '../utils.js';
|
|
10
|
-
import { getPluginNextSteps } from '../core/plugin-loader.js';
|
|
10
|
+
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
11
11
|
|
|
12
12
|
// Shared execution defaults for script actions
|
|
13
13
|
export const EXECUTION_TIMEOUT_DEFAULT_MS = 30_000;
|
|
@@ -22,7 +22,7 @@ export const EXECUTION_RESULT_MAX_BYTES = 100_000;
|
|
|
22
22
|
* Structured response for execute_javascript action
|
|
23
23
|
*/
|
|
24
24
|
export class ExecuteJavascriptResponse extends MCPResponse {
|
|
25
|
-
constructor({ result, type, executionTimeMs, truncated = false, urlChanged = false, currentUrl = '', error = null, nextSteps = [] }) {
|
|
25
|
+
constructor({ result, type, executionTimeMs, truncated = false, urlChanged = false, currentUrl = '', error = null, nextSteps = [], recommendedPlugins = [] }) {
|
|
26
26
|
super(nextSteps);
|
|
27
27
|
|
|
28
28
|
this.result = result;
|
|
@@ -32,6 +32,7 @@ export class ExecuteJavascriptResponse extends MCPResponse {
|
|
|
32
32
|
this.urlChanged = urlChanged;
|
|
33
33
|
this.currentUrl = currentUrl;
|
|
34
34
|
this.error = error;
|
|
35
|
+
this.recommendedPlugins = recommendedPlugins;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
_getAdditionalFields() {
|
|
@@ -42,7 +43,8 @@ export class ExecuteJavascriptResponse extends MCPResponse {
|
|
|
42
43
|
truncated: this.truncated,
|
|
43
44
|
urlChanged: this.urlChanged,
|
|
44
45
|
currentUrl: this.currentUrl,
|
|
45
|
-
error: this.error || undefined
|
|
46
|
+
error: this.error || undefined,
|
|
47
|
+
recommendedPlugins: this.recommendedPlugins
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -224,11 +226,12 @@ export async function executeJavascript({ url, script, timeoutMs = EXECUTION_TIM
|
|
|
224
226
|
urlChanged,
|
|
225
227
|
currentUrl,
|
|
226
228
|
nextSteps: [
|
|
229
|
+
...getPluginNextSteps(currentUrl, ''),
|
|
227
230
|
'Use click_element or type_text for follow-up actions',
|
|
228
231
|
'Inspect urlChanged to decide if navigation occurred',
|
|
229
|
-
serialization.truncated ? 'Narrow your selector or reduce returned fields to avoid truncation' : 'Proceed with the returned data'
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
serialization.truncated ? 'Narrow your selector or reduce returned fields to avoid truncation' : 'Proceed with the returned data'
|
|
233
|
+
],
|
|
234
|
+
recommendedPlugins: getRecommendedPlugins(currentUrl, '')
|
|
232
235
|
});
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -8,7 +8,7 @@ import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extract
|
|
|
8
8
|
import { isLikelyAuthUrl, waitForAuth } from '../core/auth.js';
|
|
9
9
|
import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
|
|
10
10
|
import logger from '../core/logger.js';
|
|
11
|
-
import { getPluginNextSteps } from '../core/plugin-loader.js';
|
|
11
|
+
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -26,8 +26,9 @@ export class FetchPageSuccessResponse extends MCPResponse {
|
|
|
26
26
|
* @param {string} currentUrl - Final URL after redirects
|
|
27
27
|
* @param {string} html - Page HTML content
|
|
28
28
|
* @param {string[]} nextSteps - Suggested next actions
|
|
29
|
+
* @param {Array} [recommendedPlugins] - Detected plugin metadata
|
|
29
30
|
*/
|
|
30
|
-
constructor(currentUrl, html, nextSteps) {
|
|
31
|
+
constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
|
|
31
32
|
super(nextSteps);
|
|
32
33
|
|
|
33
34
|
if (typeof currentUrl !== 'string') {
|
|
@@ -39,12 +40,14 @@ export class FetchPageSuccessResponse extends MCPResponse {
|
|
|
39
40
|
|
|
40
41
|
this.currentUrl = currentUrl;
|
|
41
42
|
this.html = html;
|
|
43
|
+
this.recommendedPlugins = recommendedPlugins;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
_getAdditionalFields() {
|
|
45
47
|
return {
|
|
46
48
|
currentUrl: this.currentUrl,
|
|
47
|
-
html: this.html
|
|
49
|
+
html: this.html,
|
|
50
|
+
recommendedPlugins: this.recommendedPlugins
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -221,13 +224,14 @@ async function doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait }
|
|
|
221
224
|
page.url(),
|
|
222
225
|
processedHtml,
|
|
223
226
|
[
|
|
227
|
+
...getPluginNextSteps(page.url(), processedHtml),
|
|
224
228
|
"Use MCPBrowser's click_element to interact with buttons/links on the page",
|
|
225
229
|
"Use MCPBrowser's type_text to fill in form fields",
|
|
226
230
|
"Use MCPBrowser's get_current_html to re-check page state after interactions",
|
|
227
231
|
"Use MCPBrowser's take_screenshot if page has charts, images, or complex visual layout that's hard to understand from HTML",
|
|
228
|
-
"Use MCPBrowser's close_tab when finished to free browser resources"
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
"Use MCPBrowser's close_tab when finished to free browser resources"
|
|
233
|
+
],
|
|
234
|
+
getRecommendedPlugins(page.url(), processedHtml)
|
|
231
235
|
);
|
|
232
236
|
} catch (err) {
|
|
233
237
|
logger.error(`fetch_webpage failed: ${err.message || String(err)}`);
|
|
@@ -6,7 +6,7 @@ import { getBrowser, getValidatedPage } from '../core/browser.js';
|
|
|
6
6
|
import { extractAndProcessHtml } from '../core/page.js';
|
|
7
7
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
8
8
|
import logger from '../core/logger.js';
|
|
9
|
-
import { getPluginNextSteps } from '../core/plugin-loader.js';
|
|
9
|
+
import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -24,8 +24,9 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
|
|
|
24
24
|
* @param {string} currentUrl - Current page URL
|
|
25
25
|
* @param {string} html - Page HTML content
|
|
26
26
|
* @param {string[]} nextSteps - Suggested next actions
|
|
27
|
+
* @param {Array} [recommendedPlugins] - Detected plugin metadata
|
|
27
28
|
*/
|
|
28
|
-
constructor(currentUrl, html, nextSteps) {
|
|
29
|
+
constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
|
|
29
30
|
super(nextSteps);
|
|
30
31
|
|
|
31
32
|
if (typeof currentUrl !== 'string') {
|
|
@@ -37,12 +38,14 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
|
|
|
37
38
|
|
|
38
39
|
this.currentUrl = currentUrl;
|
|
39
40
|
this.html = html;
|
|
41
|
+
this.recommendedPlugins = recommendedPlugins;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
_getAdditionalFields() {
|
|
43
45
|
return {
|
|
44
46
|
currentUrl: this.currentUrl,
|
|
45
|
-
html: this.html
|
|
47
|
+
html: this.html,
|
|
48
|
+
recommendedPlugins: this.recommendedPlugins
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -158,12 +161,13 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
|
158
161
|
currentUrl,
|
|
159
162
|
html,
|
|
160
163
|
[
|
|
164
|
+
...getPluginNextSteps(currentUrl, html),
|
|
161
165
|
"Use MCPBrowser's click_element to interact with elements",
|
|
162
166
|
"Use MCPBrowser's type_text to fill forms",
|
|
163
167
|
"Use MCPBrowser's take_screenshot if page layout or visual content is hard to understand from HTML",
|
|
164
|
-
"Use MCPBrowser's close_tab to free resources when done"
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
"Use MCPBrowser's close_tab to free resources when done"
|
|
169
|
+
],
|
|
170
|
+
getRecommendedPlugins(currentUrl, html)
|
|
167
171
|
);
|
|
168
172
|
} catch (err) {
|
|
169
173
|
logger.error(`get_current_html failed: ${err.message}`);
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/args.js — Argument parsing and schema-driven flag coercion.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse CLI argv into { command, positional, flags }.
|
|
7
|
+
* Supports --flag value, --flag=value, --flag (boolean), -h, -v.
|
|
8
|
+
*/
|
|
9
|
+
export function parseArgs(argv) {
|
|
10
|
+
const flags = {};
|
|
11
|
+
const positional = [];
|
|
12
|
+
let command = null;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg === '--help' || arg === '-h') {
|
|
17
|
+
flags.help = true;
|
|
18
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
19
|
+
flags.version = true;
|
|
20
|
+
} else if (arg.startsWith('--')) {
|
|
21
|
+
const eqIdx = arg.indexOf('=');
|
|
22
|
+
if (eqIdx !== -1) {
|
|
23
|
+
// --flag=value
|
|
24
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
25
|
+
} else {
|
|
26
|
+
const key = arg.slice(2);
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (next && !next.startsWith('--')) {
|
|
29
|
+
flags[key] = next;
|
|
30
|
+
i++;
|
|
31
|
+
} else {
|
|
32
|
+
flags[key] = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else if (!command) {
|
|
36
|
+
command = arg;
|
|
37
|
+
} else {
|
|
38
|
+
positional.push(arg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { command, positional, flags };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Coerce flag values to correct types using MCP tool inputSchema.
|
|
47
|
+
* Returns { coerced, errors } where errors is an array of validation messages.
|
|
48
|
+
*/
|
|
49
|
+
export function coerceFlags(flags, tool, flagMap = {}) {
|
|
50
|
+
const props = tool?.inputSchema?.properties || {};
|
|
51
|
+
const coerced = { ...flags };
|
|
52
|
+
const errors = [];
|
|
53
|
+
|
|
54
|
+
// Build reverse map: CLI flag → MCP param name
|
|
55
|
+
const reverseMap = {};
|
|
56
|
+
for (const [cliFlag, mcpParam] of Object.entries(flagMap)) {
|
|
57
|
+
if (!mcpParam.startsWith('_')) reverseMap[cliFlag] = mcpParam;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(coerced)) {
|
|
61
|
+
// Skip built-in flags
|
|
62
|
+
if (key === 'help' || key === 'version' || key === 'json' || key === 'output') continue;
|
|
63
|
+
|
|
64
|
+
const mcpParam = reverseMap[key] || key;
|
|
65
|
+
const schema = props[mcpParam];
|
|
66
|
+
if (!schema) continue; // unknown flag — leave as-is
|
|
67
|
+
|
|
68
|
+
if (schema.type === 'number' && typeof value === 'string') {
|
|
69
|
+
const n = Number(value);
|
|
70
|
+
if (Number.isNaN(n)) {
|
|
71
|
+
errors.push(`--${key} must be a number, got '${value}'`);
|
|
72
|
+
} else {
|
|
73
|
+
coerced[key] = n;
|
|
74
|
+
}
|
|
75
|
+
} else if (schema.type === 'boolean' && typeof value === 'string') {
|
|
76
|
+
coerced[key] = value !== 'false' && value !== '0';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { coerced, errors };
|
|
81
|
+
}
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/help.js — Auto-generated help from CLI_REGISTRY + MCP tool schemas.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
import { CLI_REGISTRY } from './registry.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
export function getVersion() {
|
|
15
|
+
return JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8')).version;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format inputSchema properties into CLI option lines.
|
|
20
|
+
* Skips 'url' (positional) and internal-only params.
|
|
21
|
+
*/
|
|
22
|
+
function formatSchemaOptions(tool, entry) {
|
|
23
|
+
const props = tool.inputSchema?.properties || {};
|
|
24
|
+
const required = new Set(tool.inputSchema?.required || []);
|
|
25
|
+
const reverseMap = {};
|
|
26
|
+
for (const [cliFlag, mcpParam] of Object.entries(entry.flagMap || {})) {
|
|
27
|
+
if (!mcpParam.startsWith('_')) reverseMap[mcpParam] = cliFlag;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const [name, schema] of Object.entries(props)) {
|
|
32
|
+
if (name === 'url') continue;
|
|
33
|
+
|
|
34
|
+
const cliName = reverseMap[name] || name;
|
|
35
|
+
let typeHint = '';
|
|
36
|
+
if (schema.enum) {
|
|
37
|
+
typeHint = `<${schema.enum.filter(Boolean).join('|')}>`;
|
|
38
|
+
} else if (schema.type === 'string') {
|
|
39
|
+
typeHint = `<${cliName}>`;
|
|
40
|
+
} else if (schema.type === 'number') {
|
|
41
|
+
typeHint = '<n>';
|
|
42
|
+
} else if (schema.type === 'boolean') {
|
|
43
|
+
typeHint = '';
|
|
44
|
+
} else if (schema.type === 'array' && schema.items?.properties) {
|
|
45
|
+
lines.push(` --${cliName} (structured — see MCP schema)`);
|
|
46
|
+
const itemReq = new Set(schema.items.required || []);
|
|
47
|
+
for (const [iName, iProp] of Object.entries(schema.items.properties)) {
|
|
48
|
+
const ir = itemReq.has(iName) ? ' (required)' : '';
|
|
49
|
+
const id = iProp.default !== undefined ? ` [default: ${iProp.default}]` : '';
|
|
50
|
+
lines.push(` .${iName} (${iProp.type})${ir}${id} — ${iProp.description || ''}`);
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defVal = schema.default !== undefined ? ` [default: ${schema.default}]` : '';
|
|
56
|
+
const req = required.has(name) ? ' (required)' : '';
|
|
57
|
+
const desc = schema.description || '';
|
|
58
|
+
const shortDesc = desc.length > 80 ? desc.slice(0, 77) + '...' : desc;
|
|
59
|
+
lines.push(` --${cliName} ${typeHint}${req}${defVal} ${shortDesc}`);
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format outputSchema into a compact output description.
|
|
66
|
+
*/
|
|
67
|
+
function formatSchemaOutput(tool) {
|
|
68
|
+
const props = tool.outputSchema?.properties || {};
|
|
69
|
+
const lines = [];
|
|
70
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
71
|
+
let t = prop.type || 'any';
|
|
72
|
+
if (Array.isArray(t)) t = t.filter(x => x !== 'null').join('|');
|
|
73
|
+
if (t === 'array') t = `array<${prop.items?.type || 'any'}>`;
|
|
74
|
+
if (t === 'object' && !prop.description) continue;
|
|
75
|
+
lines.push(` ${name} (${t})${prop.description ? ' — ' + prop.description : ''}`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function printHelp() {
|
|
81
|
+
const version = getVersion();
|
|
82
|
+
const o = (s) => process.stdout.write(s + '\n');
|
|
83
|
+
|
|
84
|
+
o(`MCPBrowser v${version} — Browser automation for AI agents and CLI`);
|
|
85
|
+
o('');
|
|
86
|
+
o('USAGE');
|
|
87
|
+
o(' mcpbrowser Start MCP server (stdin/stdout)');
|
|
88
|
+
o(' mcpbrowser <command> <url> [options] Run a CLI command and exit');
|
|
89
|
+
o(' mcpbrowser -h | --help Show this help');
|
|
90
|
+
o(' mcpbrowser -v | --version Show version');
|
|
91
|
+
o('');
|
|
92
|
+
o('GLOBAL FLAGS');
|
|
93
|
+
o(' --json Output raw MCP result as JSON (for agent/programmatic use)');
|
|
94
|
+
o('');
|
|
95
|
+
o('WORKFLOW');
|
|
96
|
+
o(' fetch ──▶ click / type / exec / scroll ──▶ html ──▶ close');
|
|
97
|
+
o(' (load) (interact) (read) (cleanup)');
|
|
98
|
+
o('');
|
|
99
|
+
o(' Start with "fetch" to load a page, then interact. The browser keeps tabs');
|
|
100
|
+
o(' open between commands. Auth, SSO, CAPTCHAs are handled automatically.');
|
|
101
|
+
o('');
|
|
102
|
+
o('BROWSERS');
|
|
103
|
+
o(' chrome, edge, brave — auto-detected, or set with --browser');
|
|
104
|
+
o(' Uses your real browser profile (existing logins/cookies available).');
|
|
105
|
+
o('');
|
|
106
|
+
o('━'.repeat(70));
|
|
107
|
+
o('COMMANDS');
|
|
108
|
+
o('━'.repeat(70));
|
|
109
|
+
|
|
110
|
+
for (const entry of CLI_REGISTRY) {
|
|
111
|
+
o('');
|
|
112
|
+
const depTag = entry.requiresFetch ? ' [requires: fetch]' : '';
|
|
113
|
+
const desc = (entry.tool.description || '')
|
|
114
|
+
.replace(/\*\*/g, '')
|
|
115
|
+
.replace(/\\n/g, ' ')
|
|
116
|
+
.split('\n')[0];
|
|
117
|
+
|
|
118
|
+
o(`${entry.cmd} <url> [options]${depTag}`);
|
|
119
|
+
o(` ${desc}`);
|
|
120
|
+
o('');
|
|
121
|
+
|
|
122
|
+
const schemaOpts = formatSchemaOptions(entry.tool, entry);
|
|
123
|
+
if (schemaOpts.length > 0 || entry.cliNote) {
|
|
124
|
+
o(' Options:');
|
|
125
|
+
for (const line of schemaOpts) o(line);
|
|
126
|
+
if (entry.cliNote) o(` ${entry.cliNote}`);
|
|
127
|
+
o('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const outFields = formatSchemaOutput(entry.tool);
|
|
131
|
+
if (outFields.length > 0) {
|
|
132
|
+
o(' Output fields:');
|
|
133
|
+
for (const line of outFields) o(line);
|
|
134
|
+
o('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (entry.examples?.length) {
|
|
138
|
+
o(' Examples:');
|
|
139
|
+
for (const ex of entry.examples) o(` ${ex}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
o('');
|
|
144
|
+
o('━'.repeat(70));
|
|
145
|
+
o('MULTI-STEP EXAMPLE');
|
|
146
|
+
o('━'.repeat(70));
|
|
147
|
+
o('');
|
|
148
|
+
o(' mcpbrowser fetch https://app.example.com/login');
|
|
149
|
+
o(' mcpbrowser type https://app.example.com/login --selector "#email" --text "me@corp.com"');
|
|
150
|
+
o(' mcpbrowser type https://app.example.com/login --selector "#password" --text "secret"');
|
|
151
|
+
o(' mcpbrowser click https://app.example.com/login --text "Sign In"');
|
|
152
|
+
o(' mcpbrowser html https://app.example.com/dashboard');
|
|
153
|
+
o(' mcpbrowser screenshot https://app.example.com/dashboard --output dash.png');
|
|
154
|
+
o(' mcpbrowser close https://app.example.com');
|
|
155
|
+
o('');
|
|
156
|
+
o(' Use --json with any command for structured output:');
|
|
157
|
+
o(' mcpbrowser fetch https://example.com --json');
|
|
158
|
+
o('');
|
|
159
|
+
|
|
160
|
+
o('━'.repeat(70));
|
|
161
|
+
o('MCP SERVER MODE');
|
|
162
|
+
o('━'.repeat(70));
|
|
163
|
+
o('');
|
|
164
|
+
o(' No arguments → starts MCP server (stdin/stdout JSON-RPC).');
|
|
165
|
+
o(' CLI commands map 1:1 to MCP tools (fetch→fetch_webpage, etc.).');
|
|
166
|
+
o('');
|
|
167
|
+
o(' { "mcpServers": { "mcpbrowser": { "command": "npx", "args": ["-y", "mcpbrowser@latest"] } } }');
|
|
168
|
+
o('');
|
|
169
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/index.js — CLI entrypoint and generic command executor.
|
|
3
|
+
*
|
|
4
|
+
* Thin orchestration layer: parses args, coerces types, validates,
|
|
5
|
+
* dispatches to registry actions, and formats output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseArgs, coerceFlags } from './args.js';
|
|
9
|
+
import { CLI_REGISTRY, CMD_MAP } from './registry.js';
|
|
10
|
+
import { getVersion, printHelp } from './help.js';
|
|
11
|
+
import { getPrimaryText } from './utils.js';
|
|
12
|
+
|
|
13
|
+
import { fetchPage } from '../actions/fetch-page.js';
|
|
14
|
+
import { closeBrowser } from '../core/browser.js';
|
|
15
|
+
import logger from '../core/logger.js';
|
|
16
|
+
|
|
17
|
+
logger.setConsoleOutput(false);
|
|
18
|
+
|
|
19
|
+
export function isCliMode(argv) {
|
|
20
|
+
return argv.length > 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generic command executor — driven by CLI_REGISTRY entry.
|
|
25
|
+
*/
|
|
26
|
+
async function executeCommand(entry, url, flags) {
|
|
27
|
+
// Custom validation
|
|
28
|
+
if (entry.validate) {
|
|
29
|
+
const err = entry.validate(flags);
|
|
30
|
+
if (err) {
|
|
31
|
+
process.stderr.write(`Error: ${err}\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Schema-driven type coercion
|
|
37
|
+
const { coerced, errors } = coerceFlags(flags, entry.tool, entry.flagMap);
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
for (const e of errors) process.stderr.write(`Error: ${e}\n`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build params
|
|
44
|
+
const params = entry.buildParams
|
|
45
|
+
? entry.buildParams(url, coerced)
|
|
46
|
+
: { url, ...coerced };
|
|
47
|
+
|
|
48
|
+
// Call the MCP action
|
|
49
|
+
const result = await entry.action(params);
|
|
50
|
+
const mcp = result.toMcpFormat();
|
|
51
|
+
|
|
52
|
+
if (mcp.isError) {
|
|
53
|
+
process.stderr.write(`Error: ${getPrimaryText(mcp)}\n`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --json: output raw MCP result
|
|
58
|
+
if (coerced.json) {
|
|
59
|
+
const jsonOut = {
|
|
60
|
+
content: mcp.content,
|
|
61
|
+
...(mcp.structuredContent ? { structuredContent: mcp.structuredContent } : {})
|
|
62
|
+
};
|
|
63
|
+
process.stdout.write(JSON.stringify(jsonOut, null, 2) + '\n');
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Formatted output
|
|
68
|
+
const output = entry.formatOutput
|
|
69
|
+
? entry.formatOutput(mcp, coerced)
|
|
70
|
+
: { stdout: getPrimaryText(mcp) };
|
|
71
|
+
|
|
72
|
+
if (output.error) {
|
|
73
|
+
process.stderr.write(`Error: ${output.error}\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (output.stderr) process.stderr.write(output.stderr + '\n');
|
|
77
|
+
if (output.stdout) process.stdout.write(output.stdout + '\n');
|
|
78
|
+
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Main CLI entry point.
|
|
84
|
+
*/
|
|
85
|
+
export async function runCli(argv) {
|
|
86
|
+
const { command, positional, flags } = parseArgs(argv);
|
|
87
|
+
|
|
88
|
+
if (flags.help) { printHelp(); return 0; }
|
|
89
|
+
if (flags.version) { process.stdout.write(getVersion() + '\n'); return 0; }
|
|
90
|
+
if (!command) { printHelp(); return 0; }
|
|
91
|
+
|
|
92
|
+
const entry = CMD_MAP.get(command);
|
|
93
|
+
if (!entry) {
|
|
94
|
+
process.stderr.write(`Unknown command: ${command}\n`);
|
|
95
|
+
process.stderr.write(`Available: ${[...CMD_MAP.keys()].join(', ')}\n`);
|
|
96
|
+
process.stderr.write('Run mcpbrowser --help for usage\n');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const url = positional[0];
|
|
101
|
+
if (!url) {
|
|
102
|
+
process.stderr.write(`Error: <url> is required for '${command}'\n`);
|
|
103
|
+
process.stderr.write(`Usage: mcpbrowser ${command} <url> [options]\n`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let exitCode = 1;
|
|
108
|
+
try {
|
|
109
|
+
// Auto-fetch for commands that declare it (e.g. screenshot)
|
|
110
|
+
if (entry.autoFetch) {
|
|
111
|
+
const fetchResult = await fetchPage({ url, browser: flags.browser || '', removeUnnecessaryHTML: true });
|
|
112
|
+
const fetchMcp = fetchResult.toMcpFormat();
|
|
113
|
+
if (fetchMcp.isError) {
|
|
114
|
+
process.stderr.write(`Error loading page: ${getPrimaryText(fetchMcp)}\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
const actualUrl = fetchMcp.structuredContent?.currentUrl || url;
|
|
118
|
+
exitCode = await executeCommand(entry, actualUrl, flags);
|
|
119
|
+
} else {
|
|
120
|
+
exitCode = await executeCommand(entry, url, flags);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
124
|
+
exitCode = 1;
|
|
125
|
+
} finally {
|
|
126
|
+
try { await closeBrowser(); } catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return exitCode;
|
|
130
|
+
}
|