@bsbofmusic/agent-browser-mcp-opencode 0.1.1 → 1.0.0
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/CHANGELOG.md +29 -0
- package/README.md +237 -13
- package/THIRD_PARTY_NOTICES.md +291 -0
- package/index.js +186 -0
- package/package.json +33 -18
- package/src/detect.js +286 -0
- package/src/doctor.js +387 -0
- package/src/ensure.js +425 -0
- package/src/tools/actions/click.js +77 -0
- package/src/tools/actions/close.js +45 -0
- package/src/tools/actions/fill.js +77 -0
- package/src/tools/actions/find.js +107 -0
- package/src/tools/actions/getText.js +70 -0
- package/src/tools/actions/open.js +96 -0
- package/src/tools/actions/screenshot.js +81 -0
- package/src/tools/actions/snapshot.js +94 -0
- package/src/tools/actions/tab.js +91 -0
- package/src/tools/actions/wait.js +85 -0
- package/src/tools/doctor.js +42 -0
- package/src/tools/ensure.js +46 -0
- package/src/tools/exec.js +120 -0
- package/src/tools/help.js +76 -0
- package/src/tools/index.js +50 -0
- package/src/tools/version.js +83 -0
- package/dist/bootstrap.js +0 -72
- package/dist/index.js +0 -121
- package/src/bootstrap.ts +0 -75
- package/src/index.ts +0 -162
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fullEnsure, bootstrapEnsure } from '../ensure.js';
|
|
2
|
+
|
|
3
|
+
export const ensureTool = {
|
|
4
|
+
name: 'browser_ensure',
|
|
5
|
+
description: 'Manually trigger full environment repair. Installs or upgrades agent-browser CLI and Chromium browser. This is idempotent - can be run multiple times safely.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
force: {
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
description: 'Force reinstall even if already installed (default: false)',
|
|
12
|
+
default: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function ensureToolHandler(args = {}) {
|
|
19
|
+
const { force = false } = args;
|
|
20
|
+
const logs = [];
|
|
21
|
+
|
|
22
|
+
logs.push(`[${new Date().toISOString()}] browser_ensure called (force=${force})`);
|
|
23
|
+
|
|
24
|
+
if (force) {
|
|
25
|
+
const result = await fullEnsure();
|
|
26
|
+
return {
|
|
27
|
+
ok: result.ok,
|
|
28
|
+
logs: result.logs,
|
|
29
|
+
stdout: result.logs.join('\n'),
|
|
30
|
+
version: result.version,
|
|
31
|
+
latestVersion: result.latestVersion,
|
|
32
|
+
duration: result.duration,
|
|
33
|
+
nextSteps: result.ok ? [] : ['Check logs for errors', 'Run: browser_doctor for diagnosis'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await bootstrapEnsure();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: result.ok,
|
|
41
|
+
logs: result.logs,
|
|
42
|
+
stdout: result.logs.join('\n'),
|
|
43
|
+
duration: result.duration,
|
|
44
|
+
nextSteps: result.ok ? [] : result.nextSteps || [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
|
4
|
+
|
|
5
|
+
function log(level, message, data = null) {
|
|
6
|
+
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
7
|
+
if (levels[level] <= levels[LOG_LEVEL]) {
|
|
8
|
+
const timestamp = new Date().toISOString();
|
|
9
|
+
console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}`, data ? JSON.stringify(data) : '');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const execTool = {
|
|
14
|
+
name: 'browser_exec',
|
|
15
|
+
description: 'Execute any agent-browser CLI command. Use this for commands not covered by structured tools. Examples: agent-browser open <url>, agent-browser click <selector>, agent-browser fill <selector> <text>, agent-browser find role button click --name "Submit", etc.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
command: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'The full agent-browser command to execute (without "agent-browser" prefix). Example: "open example.com" or "click #submit"',
|
|
22
|
+
},
|
|
23
|
+
timeoutMs: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'Timeout in milliseconds (default: 120000)',
|
|
26
|
+
default: 120000,
|
|
27
|
+
},
|
|
28
|
+
cwd: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Working directory for the command',
|
|
31
|
+
},
|
|
32
|
+
json: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
description: 'Request JSON output from agent-browser (default: true)',
|
|
35
|
+
default: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['command'],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function execToolHandler(args) {
|
|
43
|
+
const { command, timeoutMs = 120000, cwd, json = true } = args;
|
|
44
|
+
const logs = [];
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
|
|
47
|
+
logs.push(`[${new Date().toISOString()}] Executing: agent-browser ${command}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const fullCommand = `agent-browser ${command}${json ? ' --json' : ''}`;
|
|
51
|
+
log('info', 'Executing command', { command: fullCommand, timeoutMs, cwd });
|
|
52
|
+
|
|
53
|
+
const options = {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
timeout: timeoutMs,
|
|
56
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (cwd) {
|
|
60
|
+
options.cwd = cwd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stdout = execSync(fullCommand, options);
|
|
64
|
+
const elapsed = Date.now() - startTime;
|
|
65
|
+
|
|
66
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
67
|
+
|
|
68
|
+
let parsedOutput = stdout;
|
|
69
|
+
if (json) {
|
|
70
|
+
try {
|
|
71
|
+
parsedOutput = JSON.parse(stdout);
|
|
72
|
+
} catch {
|
|
73
|
+
parsedOutput = stdout;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
logs,
|
|
80
|
+
stdout,
|
|
81
|
+
output: parsedOutput,
|
|
82
|
+
duration: elapsed,
|
|
83
|
+
};
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const elapsed = Date.now() - startTime;
|
|
86
|
+
const errorMsg = error.message || String(error);
|
|
87
|
+
const stderr = error.stderr || '';
|
|
88
|
+
|
|
89
|
+
logs.push(`[${new Date().toISOString()}] Error after ${elapsed}ms: ${errorMsg}`);
|
|
90
|
+
|
|
91
|
+
let parsedError = errorMsg;
|
|
92
|
+
if (json && stderr) {
|
|
93
|
+
try {
|
|
94
|
+
parsedError = JSON.parse(stderr);
|
|
95
|
+
} catch {
|
|
96
|
+
parsedError = stderr;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nextSteps = [
|
|
101
|
+
'Check if the command syntax is correct',
|
|
102
|
+
'Run: agent-browser --help to see available commands',
|
|
103
|
+
'Check if browser is running: agent-browser tab',
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (error.message?.includes('ENOENT') || error.message?.includes('not found')) {
|
|
107
|
+
nextSteps.push('Run: browser_ensure to install agent-browser');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
logs,
|
|
113
|
+
stderr,
|
|
114
|
+
error: errorMsg,
|
|
115
|
+
output: parsedError,
|
|
116
|
+
duration: elapsed,
|
|
117
|
+
nextSteps,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const helpTool = {
|
|
4
|
+
name: 'browser_help',
|
|
5
|
+
description: 'List all available agent-browser commands and capabilities. Provides self-discovery of MCP tools.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
command: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Optional: specific command to get help for (e.g., "open", "click", "snapshot")',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function helpToolHandler(args = {}) {
|
|
18
|
+
const { command } = args;
|
|
19
|
+
const logs = [];
|
|
20
|
+
|
|
21
|
+
logs.push(`[${new Date().toISOString()}] browser_help called`);
|
|
22
|
+
|
|
23
|
+
const toolDescription = `
|
|
24
|
+
=== Browser Automation MCP Tools ===
|
|
25
|
+
|
|
26
|
+
Core Tools:
|
|
27
|
+
- browser_ensure: Install/repair agent-browser and Chromium
|
|
28
|
+
- browser_doctor: Diagnose issues and get fix recommendations
|
|
29
|
+
- browser_help: Show this help message
|
|
30
|
+
- browser_version: Show version information
|
|
31
|
+
- browser_exec: Execute any agent-browser CLI command
|
|
32
|
+
|
|
33
|
+
Structured Actions:
|
|
34
|
+
- browser_open: Navigate to URL
|
|
35
|
+
- browser_snapshot: Get accessibility tree with refs
|
|
36
|
+
- browser_click: Click an element
|
|
37
|
+
- browser_fill: Fill a form field
|
|
38
|
+
- browser_screenshot: Take a screenshot
|
|
39
|
+
- browser_close: Close the browser
|
|
40
|
+
- browser_get_text: Get text content from element
|
|
41
|
+
- browser_find: Find element by semantic locator
|
|
42
|
+
- browser_wait: Wait for element/text/URL
|
|
43
|
+
- browser_tab: Manage browser tabs
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
let helpText = toolDescription;
|
|
47
|
+
|
|
48
|
+
if (command) {
|
|
49
|
+
try {
|
|
50
|
+
const helpOutput = execSync(`agent-browser ${command} --help`, { encoding: 'utf8', timeout: 10000 });
|
|
51
|
+
helpText = `=== Help for: ${command} ===\n\n${helpOutput}`;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
helpText = `Could not get help for "${command}". The command may not exist or require installation.`;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
try {
|
|
57
|
+
const cliHelp = execSync('agent-browser --help', { encoding: 'utf8', timeout: 10000 });
|
|
58
|
+
helpText += `\n\n=== agent-browser CLI Help ===\n\n${cliHelp}`;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
helpText += '\n\nRun browser_ensure first to install agent-browser.';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
logs,
|
|
67
|
+
stdout: helpText,
|
|
68
|
+
output: helpText,
|
|
69
|
+
commands: [
|
|
70
|
+
'open', 'snapshot', 'click', 'fill', 'screenshot', 'close',
|
|
71
|
+
'get', 'find', 'wait', 'tab', 'type', 'press', 'hover',
|
|
72
|
+
'select', 'check', 'uncheck', 'scroll', 'drag', 'upload',
|
|
73
|
+
'cookies', 'storage', 'network', 'back', 'forward', 'reload',
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execTool } from './exec.js';
|
|
2
|
+
import { ensureTool } from './ensure.js';
|
|
3
|
+
import { doctorTool } from './doctor.js';
|
|
4
|
+
import { helpTool } from './help.js';
|
|
5
|
+
import { versionTool } from './version.js';
|
|
6
|
+
import { openTool } from './actions/open.js';
|
|
7
|
+
import { snapshotTool } from './actions/snapshot.js';
|
|
8
|
+
import { clickTool } from './actions/click.js';
|
|
9
|
+
import { fillTool } from './actions/fill.js';
|
|
10
|
+
import { screenshotTool } from './actions/screenshot.js';
|
|
11
|
+
import { closeTool } from './actions/close.js';
|
|
12
|
+
import { getTextTool } from './actions/getText.js';
|
|
13
|
+
import { findTool } from './actions/find.js';
|
|
14
|
+
import { waitTool } from './actions/wait.js';
|
|
15
|
+
import { tabTool } from './actions/tab.js';
|
|
16
|
+
|
|
17
|
+
export const tools = [
|
|
18
|
+
ensureTool,
|
|
19
|
+
doctorTool,
|
|
20
|
+
helpTool,
|
|
21
|
+
versionTool,
|
|
22
|
+
execTool,
|
|
23
|
+
openTool,
|
|
24
|
+
snapshotTool,
|
|
25
|
+
clickTool,
|
|
26
|
+
fillTool,
|
|
27
|
+
screenshotTool,
|
|
28
|
+
closeTool,
|
|
29
|
+
getTextTool,
|
|
30
|
+
findTool,
|
|
31
|
+
waitTool,
|
|
32
|
+
tabTool,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const toolMap = {};
|
|
36
|
+
for (const tool of tools) {
|
|
37
|
+
toolMap[tool.name] = tool;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getTool(name) {
|
|
41
|
+
return toolMap[name];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function listTools() {
|
|
45
|
+
return tools.map(t => ({
|
|
46
|
+
name: t.name,
|
|
47
|
+
description: t.description,
|
|
48
|
+
inputSchema: t.inputSchema,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import ensure from '../ensure.js';
|
|
3
|
+
const { getAgentBrowserVersion, checkForUpdates } = ensure;
|
|
4
|
+
|
|
5
|
+
const MCP_VERSION = '1.0.0';
|
|
6
|
+
|
|
7
|
+
export const versionTool = {
|
|
8
|
+
name: 'browser_version',
|
|
9
|
+
description: 'Show version information for this MCP and the wrapped agent-browser CLI. Includes current version, latest available version, and update strategy.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function versionToolHandler(args = {}) {
|
|
17
|
+
const logs = [];
|
|
18
|
+
|
|
19
|
+
logs.push(`[${new Date().toISOString()}] browser_version called`);
|
|
20
|
+
|
|
21
|
+
const mcpVersion = MCP_VERSION;
|
|
22
|
+
const alwaysLatest = process.env.ALWAYS_LATEST !== '0';
|
|
23
|
+
const updateStrategy = process.env.UPDATE_STRATEGY || 'simple';
|
|
24
|
+
|
|
25
|
+
let agentBrowserInfo = {
|
|
26
|
+
installed: false,
|
|
27
|
+
version: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
agentBrowserInfo = await getAgentBrowserVersion();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
agentBrowserInfo = { installed: false, version: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let updateInfo = {
|
|
37
|
+
hasUpdate: null,
|
|
38
|
+
currentVersion: agentBrowserInfo.version,
|
|
39
|
+
latestVersion: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
updateInfo = await checkForUpdates();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
updateInfo = {
|
|
46
|
+
hasUpdate: null,
|
|
47
|
+
currentVersion: agentBrowserInfo.version,
|
|
48
|
+
latestVersion: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const output = {
|
|
53
|
+
mcp: {
|
|
54
|
+
version: mcpVersion,
|
|
55
|
+
name: '@bsbofmusic/agent-browser-mcp-opencode',
|
|
56
|
+
},
|
|
57
|
+
agentBrowser: {
|
|
58
|
+
installed: agentBrowserInfo.installed,
|
|
59
|
+
currentVersion: agentBrowserInfo.version,
|
|
60
|
+
latestVersion: updateInfo.latestVersion,
|
|
61
|
+
hasUpdate: updateInfo.hasUpdate,
|
|
62
|
+
},
|
|
63
|
+
updateStrategy: {
|
|
64
|
+
alwaysLatest,
|
|
65
|
+
strategy: updateStrategy,
|
|
66
|
+
description: updateStrategy === 'atomic'
|
|
67
|
+
? 'Atomic updates with staging directory and rollback'
|
|
68
|
+
: 'Simple updates without rollback',
|
|
69
|
+
},
|
|
70
|
+
environment: {
|
|
71
|
+
nodeVersion: process.version,
|
|
72
|
+
platform: process.platform,
|
|
73
|
+
arch: process.arch,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
logs,
|
|
80
|
+
stdout: JSON.stringify(output, null, 2),
|
|
81
|
+
...output,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/bootstrap.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
function run(cmd, args, opts = {}) {
|
|
6
|
-
const r = spawnSync(cmd, args, {
|
|
7
|
-
stdio: opts.silent ? "ignore" : "inherit",
|
|
8
|
-
shell: false,
|
|
9
|
-
windowsHide: true
|
|
10
|
-
});
|
|
11
|
-
return r.status ?? 1;
|
|
12
|
-
}
|
|
13
|
-
function ensureAgentBrowser() {
|
|
14
|
-
// 1) Prefer local install (no admin required)
|
|
15
|
-
const localBinWin = join(process.cwd(), "node_modules", ".bin", "agent-browser.cmd");
|
|
16
|
-
const localBinNix = join(process.cwd(), "node_modules", ".bin", "agent-browser");
|
|
17
|
-
const hasLocal = existsSync(localBinWin) || existsSync(localBinNix);
|
|
18
|
-
if (!hasLocal) {
|
|
19
|
-
console.error("[agent-browser-mcp] Installing agent-browser locally...");
|
|
20
|
-
const code = run("npm", ["i", "agent-browser"], { silent: false });
|
|
21
|
-
if (code === 0) {
|
|
22
|
-
return { runner: "local" };
|
|
23
|
-
}
|
|
24
|
-
console.error("[agent-browser-mcp] Local install failed, will try npx/global fallback...");
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
return { runner: "local" };
|
|
28
|
-
}
|
|
29
|
-
// 2) Fallback to npx (still no admin, but depends on npx resolving)
|
|
30
|
-
// We won't pre-install here; we'll just use npx to run it.
|
|
31
|
-
// If npx fails, try global.
|
|
32
|
-
const npxOk = run("npx", ["-y", "agent-browser", "--help"], { silent: true }) === 0;
|
|
33
|
-
if (npxOk)
|
|
34
|
-
return { runner: "npx" };
|
|
35
|
-
// 3) Last resort: global install (may require admin depending on policy)
|
|
36
|
-
console.error("[agent-browser-mcp] Installing agent-browser globally (may require permissions)...");
|
|
37
|
-
const g = run("npm", ["i", "-g", "agent-browser"], { silent: false });
|
|
38
|
-
if (g !== 0)
|
|
39
|
-
throw new Error("Failed to install agent-browser (local/npx/global all failed).");
|
|
40
|
-
return { runner: "global" };
|
|
41
|
-
}
|
|
42
|
-
function ensureChromium(runner) {
|
|
43
|
-
console.error("[agent-browser-mcp] Ensuring Chromium is installed (agent-browser install)...");
|
|
44
|
-
if (runner === "local") {
|
|
45
|
-
// Use local binary via npm exec (cross-platform)
|
|
46
|
-
const code = run("npm", ["exec", "--", "agent-browser", "install"], { silent: false });
|
|
47
|
-
if (code !== 0)
|
|
48
|
-
throw new Error("agent-browser install failed (local).");
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (runner === "npx") {
|
|
52
|
-
const code = run("npx", ["-y", "agent-browser", "install"], { silent: false });
|
|
53
|
-
if (code !== 0)
|
|
54
|
-
throw new Error("agent-browser install failed (npx).");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
// global
|
|
58
|
-
const code = run("agent-browser", ["install"], { silent: false });
|
|
59
|
-
if (code !== 0)
|
|
60
|
-
throw new Error("agent-browser install failed (global).");
|
|
61
|
-
}
|
|
62
|
-
function startMcp() {
|
|
63
|
-
console.error("[agent-browser-mcp] Starting MCP server...");
|
|
64
|
-
const code = run("node", [new URL("./index.js", import.meta.url).pathname], { silent: false });
|
|
65
|
-
process.exit(code);
|
|
66
|
-
}
|
|
67
|
-
function main() {
|
|
68
|
-
const { runner } = ensureAgentBrowser();
|
|
69
|
-
ensureChromium(runner);
|
|
70
|
-
startMcp();
|
|
71
|
-
}
|
|
72
|
-
main();
|
package/dist/index.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
const env = process.env;
|
|
8
|
-
const AGENT_BROWSER_BIN = env.AGENT_BROWSER_BIN || "agent-browser";
|
|
9
|
-
const DEFAULT_SESSION = env.AGENT_BROWSER_SESSION || "opencode";
|
|
10
|
-
const STATE_PATH = env.AGENT_BROWSER_STATE || "";
|
|
11
|
-
const ALLOW_DOMAINS = (env.AGENT_BROWSER_ALLOW_DOMAINS || "").split(",").map(s => s.trim()).filter(Boolean);
|
|
12
|
-
const DEFAULT_TIMEOUT_MS = Number(env.AGENT_BROWSER_TIMEOUT_MS || "60000");
|
|
13
|
-
const MAX_OUTPUT_CHARS = Number(env.AGENT_BROWSER_MAX_OUTPUT_CHARS || "200000");
|
|
14
|
-
// helper: ensure literal type "text"
|
|
15
|
-
const txt = (text) => ({ type: "text", text });
|
|
16
|
-
let queue = Promise.resolve();
|
|
17
|
-
function enqueue(fn) {
|
|
18
|
-
const next = queue.then(fn, fn);
|
|
19
|
-
queue = next.then(() => undefined, () => undefined);
|
|
20
|
-
return next;
|
|
21
|
-
}
|
|
22
|
-
function limitOutput(s) {
|
|
23
|
-
if (s.length <= MAX_OUTPUT_CHARS)
|
|
24
|
-
return s;
|
|
25
|
-
return s.slice(0, MAX_OUTPUT_CHARS) + "\n...[truncated]";
|
|
26
|
-
}
|
|
27
|
-
function domainAllowed(urlStr) {
|
|
28
|
-
if (!ALLOW_DOMAINS.length)
|
|
29
|
-
return true;
|
|
30
|
-
try {
|
|
31
|
-
const u = new URL(urlStr);
|
|
32
|
-
const host = u.hostname.toLowerCase();
|
|
33
|
-
return ALLOW_DOMAINS.some(d => host === d.toLowerCase() || host.endsWith("." + d.toLowerCase()));
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function runAgentBrowser(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
40
|
-
return await enqueue(() => new Promise((resolveRun) => {
|
|
41
|
-
const finalArgs = [];
|
|
42
|
-
finalArgs.push(...args, "--json");
|
|
43
|
-
finalArgs.push("--session-name", DEFAULT_SESSION);
|
|
44
|
-
if (STATE_PATH)
|
|
45
|
-
finalArgs.push("--state", STATE_PATH);
|
|
46
|
-
const child = spawn(AGENT_BROWSER_BIN, finalArgs, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
47
|
-
let stdout = "";
|
|
48
|
-
let stderr = "";
|
|
49
|
-
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
50
|
-
child.stdout.on("data", (d) => { stdout += d.toString("utf8"); });
|
|
51
|
-
child.stderr.on("data", (d) => { stderr += d.toString("utf8"); });
|
|
52
|
-
child.on("close", (code) => {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
const out = limitOutput(stdout.trim());
|
|
55
|
-
const err = limitOutput(stderr.trim());
|
|
56
|
-
let parsed;
|
|
57
|
-
try {
|
|
58
|
-
parsed = out ? JSON.parse(out) : undefined;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
const lines = out.split("\n").map(l => l.trim()).filter(Boolean);
|
|
62
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
63
|
-
try {
|
|
64
|
-
parsed = JSON.parse(lines[i]);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
catch { }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
resolveRun({ ok: code === 0, exitCode: code, stdout: out, stderr: err, parsedJson: parsed });
|
|
71
|
-
});
|
|
72
|
-
}));
|
|
73
|
-
}
|
|
74
|
-
const server = new McpServer({ name: "agent-browser-mcp", version: "0.1.0" });
|
|
75
|
-
server.tool("browser_open", "Open a URL in agent-browser (reuses session).", { url: z.string().url(), headed: z.boolean().optional().default(false), timeoutMs: z.number().int().positive().optional() }, async ({ url, headed, timeoutMs }) => {
|
|
76
|
-
if (!domainAllowed(url))
|
|
77
|
-
return { content: [txt(`Blocked by allowlist: ${url}`)] };
|
|
78
|
-
const args = ["open", url];
|
|
79
|
-
if (headed)
|
|
80
|
-
args.push("--headed");
|
|
81
|
-
const r = await runAgentBrowser(args, timeoutMs);
|
|
82
|
-
const content = [txt(r.ok ? "OK" : "FAILED"), txt(r.stdout || "")];
|
|
83
|
-
if (r.stderr)
|
|
84
|
-
content.push(txt(`stderr:\n${r.stderr}`));
|
|
85
|
-
return { content };
|
|
86
|
-
});
|
|
87
|
-
server.tool("browser_click", "Click an element by selector.", { selector: z.string().min(1), timeoutMs: z.number().int().positive().optional() }, async ({ selector, timeoutMs }) => {
|
|
88
|
-
const r = await runAgentBrowser(["click", selector], timeoutMs);
|
|
89
|
-
return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
|
|
90
|
-
});
|
|
91
|
-
server.tool("browser_type", "Type into an input/textarea by selector.", { selector: z.string().min(1), text: z.string(), submit: z.boolean().optional().default(false), timeoutMs: z.number().int().positive().optional() }, async ({ selector, text, submit, timeoutMs }) => {
|
|
92
|
-
const args = ["type", selector, text];
|
|
93
|
-
if (submit)
|
|
94
|
-
args.push("--submit");
|
|
95
|
-
const r = await runAgentBrowser(args, timeoutMs);
|
|
96
|
-
return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
|
|
97
|
-
});
|
|
98
|
-
server.tool("browser_extract", "Extract text/html from a selector.", { selector: z.string().min(1), mode: z.enum(["text", "html"]).optional().default("text"), timeoutMs: z.number().int().positive().optional() }, async ({ selector, mode, timeoutMs }) => {
|
|
99
|
-
const r = await runAgentBrowser(["extract", mode, selector], timeoutMs);
|
|
100
|
-
const payload = r.parsedJson ?? r.stdout;
|
|
101
|
-
return { content: [txt(typeof payload === "string" ? payload : JSON.stringify(payload, null, 2))] };
|
|
102
|
-
});
|
|
103
|
-
server.tool("browser_screenshot", "Take a screenshot and save to a file path.", { path: z.string().min(1).optional().default("agent-browser.png"), fullPage: z.boolean().optional().default(true), timeoutMs: z.number().int().positive().optional() }, async ({ path, fullPage, timeoutMs }) => {
|
|
104
|
-
const outPath = resolve(process.cwd(), path);
|
|
105
|
-
const args = ["screenshot", outPath];
|
|
106
|
-
if (fullPage)
|
|
107
|
-
args.push("--full-page");
|
|
108
|
-
const r = await runAgentBrowser(args, timeoutMs);
|
|
109
|
-
const content = [txt(r.ok ? `OK: ${outPath}` : "FAILED")];
|
|
110
|
-
if (r.stdout)
|
|
111
|
-
content.push(txt(r.stdout));
|
|
112
|
-
if (r.stderr)
|
|
113
|
-
content.push(txt(`stderr:\n${r.stderr}`));
|
|
114
|
-
return { content };
|
|
115
|
-
});
|
|
116
|
-
server.tool("browser_set_viewport", "Set viewport size.", { width: z.number().int().min(200).max(4000), height: z.number().int().min(200).max(4000), timeoutMs: z.number().int().positive().optional() }, async ({ width, height, timeoutMs }) => {
|
|
117
|
-
const r = await runAgentBrowser(["set", "viewport", String(width), String(height)], timeoutMs);
|
|
118
|
-
return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
|
|
119
|
-
});
|
|
120
|
-
const transport = new StdioServerTransport();
|
|
121
|
-
await server.connect(transport);
|
package/src/bootstrap.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
function run(cmd: string, args: string[], opts: { silent?: boolean } = {}) {
|
|
7
|
-
const r = spawnSync(cmd, args, {
|
|
8
|
-
stdio: opts.silent ? "ignore" : "inherit",
|
|
9
|
-
shell: false,
|
|
10
|
-
windowsHide: true
|
|
11
|
-
});
|
|
12
|
-
return r.status ?? 1;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function ensureAgentBrowser(): { runner: "local" | "npx" | "global" } {
|
|
16
|
-
// 1) Prefer local install (no admin required)
|
|
17
|
-
const localBinWin = join(process.cwd(), "node_modules", ".bin", "agent-browser.cmd");
|
|
18
|
-
const localBinNix = join(process.cwd(), "node_modules", ".bin", "agent-browser");
|
|
19
|
-
const hasLocal = existsSync(localBinWin) || existsSync(localBinNix);
|
|
20
|
-
|
|
21
|
-
if (!hasLocal) {
|
|
22
|
-
console.error("[agent-browser-mcp] Installing agent-browser locally...");
|
|
23
|
-
const code = run("npm", ["i", "agent-browser"], { silent: false });
|
|
24
|
-
if (code === 0) {
|
|
25
|
-
return { runner: "local" };
|
|
26
|
-
}
|
|
27
|
-
console.error("[agent-browser-mcp] Local install failed, will try npx/global fallback...");
|
|
28
|
-
} else {
|
|
29
|
-
return { runner: "local" };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 2) Fallback to npx (still no admin, but depends on npx resolving)
|
|
33
|
-
// We won't pre-install here; we'll just use npx to run it.
|
|
34
|
-
// If npx fails, try global.
|
|
35
|
-
const npxOk = run("npx", ["-y", "agent-browser", "--help"], { silent: true }) === 0;
|
|
36
|
-
if (npxOk) return { runner: "npx" };
|
|
37
|
-
|
|
38
|
-
// 3) Last resort: global install (may require admin depending on policy)
|
|
39
|
-
console.error("[agent-browser-mcp] Installing agent-browser globally (may require permissions)...");
|
|
40
|
-
const g = run("npm", ["i", "-g", "agent-browser"], { silent: false });
|
|
41
|
-
if (g !== 0) throw new Error("Failed to install agent-browser (local/npx/global all failed).");
|
|
42
|
-
return { runner: "global" };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function ensureChromium(runner: "local" | "npx" | "global") {
|
|
46
|
-
console.error("[agent-browser-mcp] Ensuring Chromium is installed (agent-browser install)...");
|
|
47
|
-
if (runner === "local") {
|
|
48
|
-
// Use local binary via npm exec (cross-platform)
|
|
49
|
-
const code = run("npm", ["exec", "--", "agent-browser", "install"], { silent: false });
|
|
50
|
-
if (code !== 0) throw new Error("agent-browser install failed (local).");
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (runner === "npx") {
|
|
54
|
-
const code = run("npx", ["-y", "agent-browser", "install"], { silent: false });
|
|
55
|
-
if (code !== 0) throw new Error("agent-browser install failed (npx).");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// global
|
|
59
|
-
const code = run("agent-browser", ["install"], { silent: false });
|
|
60
|
-
if (code !== 0) throw new Error("agent-browser install failed (global).");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function startMcp() {
|
|
64
|
-
console.error("[agent-browser-mcp] Starting MCP server...");
|
|
65
|
-
const code = run("node", [new URL("./index.js", import.meta.url).pathname], { silent: false });
|
|
66
|
-
process.exit(code);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function main() {
|
|
70
|
-
const { runner } = ensureAgentBrowser();
|
|
71
|
-
ensureChromium(runner);
|
|
72
|
-
startMcp();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
main();
|