mcpbrowser 0.3.36 → 0.3.38
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 +5 -0
- package/src/actions/execute-javascript.js +6 -1
- package/src/actions/fetch-page.js +5 -0
- package/src/actions/get-current-html.js +5 -0
- 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/responses.js +34 -0
- package/src/mcp-browser.js +22 -4
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.38",
|
|
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": [
|
|
@@ -139,6 +139,11 @@ export const CLICK_ELEMENT_TOOL = {
|
|
|
139
139
|
type: "array",
|
|
140
140
|
items: { type: "string" },
|
|
141
141
|
description: "Suggested next actions"
|
|
142
|
+
},
|
|
143
|
+
recommendedPlugins: {
|
|
144
|
+
type: "array",
|
|
145
|
+
items: { type: "object" },
|
|
146
|
+
description: "Detected site-specific plugins available for this domain"
|
|
142
147
|
}
|
|
143
148
|
},
|
|
144
149
|
required: ["status", "fallbackUsed", "nativeAttempt", "currentUrl", "message", "html", "nextSteps"],
|
|
@@ -81,7 +81,12 @@ export const EXECUTE_JAVASCRIPT_TOOL = {
|
|
|
81
81
|
urlChanged: { type: 'boolean', description: 'True if page URL changed during execution' },
|
|
82
82
|
currentUrl: { type: 'string', description: 'URL after execution' },
|
|
83
83
|
nextSteps: { type: 'array', items: { type: 'string' } },
|
|
84
|
-
error: { type: ['object', 'null'], description: 'Error object when script throws or times out' }
|
|
84
|
+
error: { type: ['object', 'null'], description: 'Error object when script throws or times out' },
|
|
85
|
+
recommendedPlugins: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: { type: 'object' },
|
|
88
|
+
description: 'Detected site-specific plugins available for this domain'
|
|
89
|
+
}
|
|
85
90
|
},
|
|
86
91
|
required: ['type', 'executionTimeMs', 'truncated', 'urlChanged', 'currentUrl', 'nextSteps'],
|
|
87
92
|
additionalProperties: false
|
|
@@ -91,6 +91,11 @@ export const FETCH_WEBPAGE_TOOL = {
|
|
|
91
91
|
type: "array",
|
|
92
92
|
items: { type: "string" },
|
|
93
93
|
description: "Suggested next actions"
|
|
94
|
+
},
|
|
95
|
+
recommendedPlugins: {
|
|
96
|
+
type: "array",
|
|
97
|
+
items: { type: "object" },
|
|
98
|
+
description: "Detected site-specific plugins available for this domain"
|
|
94
99
|
}
|
|
95
100
|
},
|
|
96
101
|
required: ["currentUrl", "html", "nextSteps"],
|
|
@@ -83,6 +83,11 @@ export const GET_CURRENT_HTML_TOOL = {
|
|
|
83
83
|
type: "array",
|
|
84
84
|
items: { type: "string" },
|
|
85
85
|
description: "Suggested next actions"
|
|
86
|
+
},
|
|
87
|
+
recommendedPlugins: {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: { type: "object" },
|
|
90
|
+
description: "Detected site-specific plugins available for this domain"
|
|
86
91
|
}
|
|
87
92
|
},
|
|
88
93
|
required: ["currentUrl", "html", "nextSteps"],
|
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
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/registry.js — Single source of truth for all CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* To add a new CLI command:
|
|
5
|
+
* 1. Import the MCP TOOL definition and action function
|
|
6
|
+
* 2. Add an entry to CLI_REGISTRY below
|
|
7
|
+
* 3. Done — help, routing, flag mapping, coercion all auto-update
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeFileSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
import { fetchPage, FETCH_WEBPAGE_TOOL } from '../actions/fetch-page.js';
|
|
13
|
+
import { clickElement, CLICK_ELEMENT_TOOL } from '../actions/click-element.js';
|
|
14
|
+
import { typeText, TYPE_TEXT_TOOL } from '../actions/type-text.js';
|
|
15
|
+
import { executeJavascript, EXECUTE_JAVASCRIPT_TOOL } from '../actions/execute-javascript.js';
|
|
16
|
+
import { getCurrentHtml, GET_CURRENT_HTML_TOOL } from '../actions/get-current-html.js';
|
|
17
|
+
import { takeScreenshot, TAKE_SCREENSHOT_TOOL } from '../actions/take-screenshot.js';
|
|
18
|
+
import { scrollPage, SCROLL_PAGE_TOOL } from '../actions/scroll-page.js';
|
|
19
|
+
import { navigateHistory, NAVIGATE_HISTORY_TOOL } from '../actions/navigate-history.js';
|
|
20
|
+
import { closeTab, CLOSE_TAB_TOOL } from '../actions/close-tab.js';
|
|
21
|
+
|
|
22
|
+
import { htmlToText, getStructured, getPrimaryText } from './utils.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Factory for back/forward (deduplicated)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function makeHistoryCommand(direction) {
|
|
29
|
+
return {
|
|
30
|
+
cmd: direction,
|
|
31
|
+
tool: NAVIGATE_HISTORY_TOOL,
|
|
32
|
+
action: navigateHistory,
|
|
33
|
+
requiresFetch: true,
|
|
34
|
+
flagMap: { raw: '_raw' },
|
|
35
|
+
flagDefaults: {},
|
|
36
|
+
buildParams: (url, flags) => ({
|
|
37
|
+
url,
|
|
38
|
+
direction,
|
|
39
|
+
returnHtml: true,
|
|
40
|
+
removeUnnecessaryHTML: !flags.raw,
|
|
41
|
+
}),
|
|
42
|
+
formatOutput: (mcp, flags) => {
|
|
43
|
+
const s = getStructured(mcp);
|
|
44
|
+
const out = {};
|
|
45
|
+
if (s.previousUrl || s.currentUrl) {
|
|
46
|
+
out.stderr = `${s.previousUrl || '?'} → ${s.currentUrl || '?'}`;
|
|
47
|
+
}
|
|
48
|
+
if (s.html) out.stdout = flags.raw ? s.html : htmlToText(s.html);
|
|
49
|
+
return out;
|
|
50
|
+
},
|
|
51
|
+
examples: [`mcpbrowser ${direction} https://example.com`],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// CLI_REGISTRY
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export const CLI_REGISTRY = [
|
|
60
|
+
{
|
|
61
|
+
cmd: 'fetch',
|
|
62
|
+
tool: FETCH_WEBPAGE_TOOL,
|
|
63
|
+
action: fetchPage,
|
|
64
|
+
requiresFetch: false,
|
|
65
|
+
flagMap: { wait: 'postLoadWait', raw: '_raw' },
|
|
66
|
+
flagDefaults: {},
|
|
67
|
+
buildParams: (url, flags) => ({
|
|
68
|
+
url,
|
|
69
|
+
browser: flags.browser || '',
|
|
70
|
+
removeUnnecessaryHTML: !flags.raw,
|
|
71
|
+
postLoadWait: flags.wait ? Number(flags.wait) : 0
|
|
72
|
+
}),
|
|
73
|
+
formatOutput: (mcp, flags) => {
|
|
74
|
+
const html = getStructured(mcp).html || getPrimaryText(mcp);
|
|
75
|
+
return { stdout: flags.raw ? html : htmlToText(html) };
|
|
76
|
+
},
|
|
77
|
+
examples: [
|
|
78
|
+
'mcpbrowser fetch https://eng.ms/docs/my-page',
|
|
79
|
+
'mcpbrowser fetch https://portal.azure.com --browser edge --wait 5000',
|
|
80
|
+
'mcpbrowser fetch https://github.com --raw',
|
|
81
|
+
],
|
|
82
|
+
cliNote: '--raw Output full HTML instead of extracted text',
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
cmd: 'screenshot',
|
|
87
|
+
tool: TAKE_SCREENSHOT_TOOL,
|
|
88
|
+
action: takeScreenshot,
|
|
89
|
+
requiresFetch: true,
|
|
90
|
+
autoFetch: true, // screenshot auto-fetches the page first
|
|
91
|
+
flagMap: { 'full-page': 'fullPage' },
|
|
92
|
+
buildParams: (url, flags) => ({
|
|
93
|
+
url,
|
|
94
|
+
fullPage: !!flags['full-page']
|
|
95
|
+
}),
|
|
96
|
+
formatOutput: (mcp, flags) => {
|
|
97
|
+
const base64 = getStructured(mcp).screenshotBase64;
|
|
98
|
+
if (!base64) return { error: 'No screenshot data returned' };
|
|
99
|
+
const outFile = flags.output || 'screenshot.png';
|
|
100
|
+
writeFileSync(outFile, Buffer.from(base64, 'base64'));
|
|
101
|
+
return { stderr: `Screenshot saved to ${outFile}` };
|
|
102
|
+
},
|
|
103
|
+
examples: [
|
|
104
|
+
'mcpbrowser screenshot https://example.com --output page.png',
|
|
105
|
+
'mcpbrowser screenshot https://dashboard.corp.com --full-page',
|
|
106
|
+
],
|
|
107
|
+
cliNote: '--output <path> File path to save (default: screenshot.png)',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
cmd: 'click',
|
|
112
|
+
tool: CLICK_ELEMENT_TOOL,
|
|
113
|
+
action: clickElement,
|
|
114
|
+
requiresFetch: true,
|
|
115
|
+
flagMap: {},
|
|
116
|
+
buildParams: (url, flags) => ({
|
|
117
|
+
url,
|
|
118
|
+
selector: flags.selector || undefined,
|
|
119
|
+
text: flags.text || undefined,
|
|
120
|
+
returnHtml: flags.returnHtml !== 'false',
|
|
121
|
+
removeUnnecessaryHTML: true,
|
|
122
|
+
postClickWait: flags.postClickWait ? Number(flags.postClickWait) : 1000,
|
|
123
|
+
}),
|
|
124
|
+
validate: (flags) => {
|
|
125
|
+
if (!flags.selector && !flags.text) return '--selector or --text is required for click';
|
|
126
|
+
},
|
|
127
|
+
formatOutput: (mcp) => {
|
|
128
|
+
const html = getStructured(mcp).html;
|
|
129
|
+
return { stdout: html ? htmlToText(html) : getPrimaryText(mcp) };
|
|
130
|
+
},
|
|
131
|
+
examples: [
|
|
132
|
+
'mcpbrowser click https://example.com --selector "#login-btn"',
|
|
133
|
+
'mcpbrowser click https://example.com --text "Sign In"',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
cmd: 'type',
|
|
139
|
+
tool: TYPE_TEXT_TOOL,
|
|
140
|
+
action: typeText,
|
|
141
|
+
requiresFetch: true,
|
|
142
|
+
flagMap: {},
|
|
143
|
+
buildParams: (url, flags) => ({
|
|
144
|
+
url,
|
|
145
|
+
fields: [{ selector: flags.selector, text: flags.text }],
|
|
146
|
+
returnHtml: false
|
|
147
|
+
}),
|
|
148
|
+
validate: (flags) => {
|
|
149
|
+
if (!flags.selector || !flags.text) return '--selector and --text are required for type';
|
|
150
|
+
},
|
|
151
|
+
formatOutput: (mcp) => ({ stdout: getPrimaryText(mcp) }),
|
|
152
|
+
examples: [
|
|
153
|
+
'mcpbrowser type https://example.com --selector "#search" --text "query"',
|
|
154
|
+
'mcpbrowser type https://login.com --selector "input[name=email]" --text "user@corp.com"',
|
|
155
|
+
],
|
|
156
|
+
cliNote: 'CLI shorthand: --selector + --text fills a single field',
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
cmd: 'exec',
|
|
161
|
+
tool: EXECUTE_JAVASCRIPT_TOOL,
|
|
162
|
+
action: executeJavascript,
|
|
163
|
+
requiresFetch: true,
|
|
164
|
+
flagMap: {},
|
|
165
|
+
buildParams: (url, flags) => ({
|
|
166
|
+
url,
|
|
167
|
+
script: flags.script,
|
|
168
|
+
timeoutMs: flags.timeoutMs ? Number(flags.timeoutMs) : 30000,
|
|
169
|
+
returnType: flags.returnType || 'json',
|
|
170
|
+
}),
|
|
171
|
+
validate: (flags) => {
|
|
172
|
+
if (!flags.script) return '--script is required for exec';
|
|
173
|
+
},
|
|
174
|
+
formatOutput: (mcp) => {
|
|
175
|
+
const r = getStructured(mcp).result;
|
|
176
|
+
if (r !== undefined && r !== null) {
|
|
177
|
+
return { stdout: typeof r === 'string' ? r : JSON.stringify(r, null, 2) };
|
|
178
|
+
}
|
|
179
|
+
return { stdout: getPrimaryText(mcp) };
|
|
180
|
+
},
|
|
181
|
+
examples: [
|
|
182
|
+
'mcpbrowser exec https://example.com --script "document.title"',
|
|
183
|
+
'mcpbrowser exec https://mail.google.com --script "[...document.querySelectorAll(\'.zA\')].map(r=>r.textContent)"',
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
cmd: 'html',
|
|
189
|
+
tool: GET_CURRENT_HTML_TOOL,
|
|
190
|
+
action: getCurrentHtml,
|
|
191
|
+
requiresFetch: true,
|
|
192
|
+
flagMap: { raw: '_raw' },
|
|
193
|
+
buildParams: (url, flags) => ({
|
|
194
|
+
url,
|
|
195
|
+
removeUnnecessaryHTML: !flags.raw,
|
|
196
|
+
}),
|
|
197
|
+
formatOutput: (mcp) => ({ stdout: getStructured(mcp).html || getPrimaryText(mcp) }),
|
|
198
|
+
examples: [
|
|
199
|
+
'mcpbrowser html https://example.com',
|
|
200
|
+
'mcpbrowser html https://example.com --raw',
|
|
201
|
+
],
|
|
202
|
+
cliNote: '--raw Output raw HTML without cleanup',
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
cmd: 'scroll',
|
|
207
|
+
tool: SCROLL_PAGE_TOOL,
|
|
208
|
+
action: scrollPage,
|
|
209
|
+
requiresFetch: true,
|
|
210
|
+
flagMap: {},
|
|
211
|
+
buildParams: (url, flags) => {
|
|
212
|
+
const params = { url };
|
|
213
|
+
if (flags.selector) { params.selector = flags.selector; }
|
|
214
|
+
else if (flags.x !== undefined || flags.y !== undefined) {
|
|
215
|
+
if (flags.x !== undefined) params.x = Number(flags.x);
|
|
216
|
+
if (flags.y !== undefined) params.y = Number(flags.y);
|
|
217
|
+
} else {
|
|
218
|
+
params.direction = flags.direction || 'down';
|
|
219
|
+
if (flags.amount) params.amount = Number(flags.amount);
|
|
220
|
+
}
|
|
221
|
+
return params;
|
|
222
|
+
},
|
|
223
|
+
formatOutput: (mcp) => {
|
|
224
|
+
const s = getStructured(mcp);
|
|
225
|
+
return {
|
|
226
|
+
stdout: JSON.stringify({
|
|
227
|
+
scrollX: s.scrollX, scrollY: s.scrollY,
|
|
228
|
+
pageWidth: s.pageWidth, pageHeight: s.pageHeight,
|
|
229
|
+
viewportWidth: s.viewportWidth, viewportHeight: s.viewportHeight
|
|
230
|
+
}, null, 2)
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
examples: [
|
|
234
|
+
'mcpbrowser scroll https://example.com --direction down --amount 1000',
|
|
235
|
+
'mcpbrowser scroll https://example.com --selector "#footer"',
|
|
236
|
+
'mcpbrowser scroll https://example.com --x 0 --y 0',
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
makeHistoryCommand('back'),
|
|
241
|
+
makeHistoryCommand('forward'),
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
cmd: 'close',
|
|
245
|
+
tool: CLOSE_TAB_TOOL,
|
|
246
|
+
action: closeTab,
|
|
247
|
+
requiresFetch: false,
|
|
248
|
+
flagMap: {},
|
|
249
|
+
buildParams: (url) => ({ url }),
|
|
250
|
+
formatOutput: (mcp) => ({ stdout: getPrimaryText(mcp) }),
|
|
251
|
+
examples: ['mcpbrowser close https://example.com'],
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// Lookup map for O(1) command resolution
|
|
256
|
+
export const CMD_MAP = new Map(CLI_REGISTRY.map(entry => [entry.cmd, entry]));
|
package/src/cli/utils.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/utils.js — Shared utilities for CLI output formatting and safe data access.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert HTML to readable terminal text (lossy preview).
|
|
7
|
+
*/
|
|
8
|
+
export function htmlToText(html) {
|
|
9
|
+
return html
|
|
10
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
11
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
12
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
13
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
14
|
+
.replace(/<\/div>/gi, '\n')
|
|
15
|
+
.replace(/<\/li>/gi, '\n')
|
|
16
|
+
.replace(/<\/h[1-6]>/gi, '\n\n')
|
|
17
|
+
.replace(/<[^>]+>/g, '')
|
|
18
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ')
|
|
20
|
+
.replace(/[ \t]+/g, ' ')
|
|
21
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Safely get the primary text content from an MCP result.
|
|
27
|
+
* Falls back to empty string if content is missing.
|
|
28
|
+
*/
|
|
29
|
+
export function getPrimaryText(mcp) {
|
|
30
|
+
return mcp?.content?.[0]?.text ?? '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Safely get structuredContent from an MCP result.
|
|
35
|
+
* Returns an empty object if missing, so callers can destructure safely.
|
|
36
|
+
*/
|
|
37
|
+
export function getStructured(mcp) {
|
|
38
|
+
return mcp?.structuredContent ?? {};
|
|
39
|
+
}
|
package/src/core/responses.js
CHANGED
|
@@ -128,6 +128,23 @@ export class InformationalResponse extends MCPResponse {
|
|
|
128
128
|
}
|
|
129
129
|
return summary;
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Informational responses omit structuredContent to avoid schema violations.
|
|
134
|
+
* Their fields (message, reason, status) don't match tool-specific outputSchemas.
|
|
135
|
+
* @returns {Object} MCP-compliant response with text content only
|
|
136
|
+
*/
|
|
137
|
+
toMcpFormat() {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: this.getTextSummary()
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
isError: false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
/**
|
|
@@ -272,6 +289,23 @@ export class HttpStatusResponse extends MCPResponse {
|
|
|
272
289
|
}
|
|
273
290
|
return summary;
|
|
274
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* HTTP status responses omit structuredContent to avoid schema violations.
|
|
295
|
+
* Their fields (url, statusCode, etc.) don't match tool-specific outputSchemas.
|
|
296
|
+
* @returns {Object} MCP-compliant response with text content only
|
|
297
|
+
*/
|
|
298
|
+
toMcpFormat() {
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: this.getTextSummary()
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
isError: false
|
|
307
|
+
};
|
|
308
|
+
}
|
|
275
309
|
}
|
|
276
310
|
|
|
277
311
|
/**
|
package/src/mcp-browser.js
CHANGED
|
@@ -12,6 +12,9 @@ import { fileURLToPath } from 'url';
|
|
|
12
12
|
import { readFileSync } from 'fs';
|
|
13
13
|
import { dirname, join } from 'path';
|
|
14
14
|
|
|
15
|
+
// Import CLI mode
|
|
16
|
+
import { isCliMode, runCli } from './cli/index.js';
|
|
17
|
+
|
|
15
18
|
// Import response classes
|
|
16
19
|
import { ErrorResponse } from './core/responses.js';
|
|
17
20
|
import logger, { attachServer as attachLoggerServer } from './core/logger.js';
|
|
@@ -203,6 +206,9 @@ export {
|
|
|
203
206
|
scrollPage,
|
|
204
207
|
navigateHistory,
|
|
205
208
|
handleAcceptEula,
|
|
209
|
+
// CLI exports
|
|
210
|
+
isCliMode,
|
|
211
|
+
runCli,
|
|
206
212
|
// Plugin system exports
|
|
207
213
|
loadPlugins,
|
|
208
214
|
getLoadedPlugins,
|
|
@@ -216,8 +222,20 @@ export {
|
|
|
216
222
|
// Run the MCP server only if this is the main module (not imported for testing)
|
|
217
223
|
if (import.meta.url === new URL(process.argv[1], 'file://').href ||
|
|
218
224
|
fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
const argv = process.argv.slice(2);
|
|
226
|
+
if (isCliMode(argv)) {
|
|
227
|
+
// CLI mode: run command and exit
|
|
228
|
+
runCli(argv).then((code) => {
|
|
229
|
+
process.exit(code);
|
|
230
|
+
}).catch((err) => {
|
|
231
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
// MCP server mode (default): stdin/stdout JSON-RPC
|
|
236
|
+
main().catch((err) => {
|
|
237
|
+
logger.error(`Server failed: ${err.message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
223
241
|
}
|