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 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.35",
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
- ...getPluginNextSteps(currentUrl, '')
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
- ...getPluginNextSteps(page.url(), processedHtml)
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
- ...getPluginNextSteps(currentUrl, html)
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}`);
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }