@bsbofmusic/agent-browser-mcp-opencode 0.1.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.
@@ -0,0 +1,72 @@
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 ADDED
@@ -0,0 +1,121 @@
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/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@bsbofmusic/agent-browser-mcp-opencode",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "start": "node dist/bootstrap.js",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "type": "module",
15
+ "private": false,
16
+ "bin": {
17
+ "agent-browser-mcp-opencode": "dist/bootstrap.js"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.26.0",
21
+ "zod": "^4.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^25.3.1",
25
+ "typescript": "^5.5.0"
26
+ }
27
+ }
@@ -0,0 +1,75 @@
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();
package/src/index.ts ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { z } from "zod";
3
+ import { spawn } from "node:child_process";
4
+ import { resolve } from "node:path";
5
+
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+
9
+ type RunResult = {
10
+ ok: boolean;
11
+ exitCode: number | null;
12
+ stdout: string;
13
+ stderr: string;
14
+ parsedJson?: unknown;
15
+ };
16
+
17
+ const env = process.env;
18
+ const AGENT_BROWSER_BIN = env.AGENT_BROWSER_BIN || "agent-browser";
19
+ const DEFAULT_SESSION = env.AGENT_BROWSER_SESSION || "opencode";
20
+ const STATE_PATH = env.AGENT_BROWSER_STATE || "";
21
+ const ALLOW_DOMAINS = (env.AGENT_BROWSER_ALLOW_DOMAINS || "").split(",").map(s => s.trim()).filter(Boolean);
22
+
23
+ const DEFAULT_TIMEOUT_MS = Number(env.AGENT_BROWSER_TIMEOUT_MS || "60000");
24
+ const MAX_OUTPUT_CHARS = Number(env.AGENT_BROWSER_MAX_OUTPUT_CHARS || "200000");
25
+
26
+ // helper: ensure literal type "text"
27
+ const txt = (text: string) => ({ type: "text" as const, text });
28
+
29
+ let queue = Promise.resolve();
30
+ function enqueue<T>(fn: () => Promise<T>): Promise<T> {
31
+ const next = queue.then(fn, fn);
32
+ queue = next.then(() => undefined, () => undefined);
33
+ return next;
34
+ }
35
+
36
+ function limitOutput(s: string): string {
37
+ if (s.length <= MAX_OUTPUT_CHARS) return s;
38
+ return s.slice(0, MAX_OUTPUT_CHARS) + "\n...[truncated]";
39
+ }
40
+
41
+ function domainAllowed(urlStr: string): boolean {
42
+ if (!ALLOW_DOMAINS.length) return true;
43
+ try {
44
+ const u = new URL(urlStr);
45
+ const host = u.hostname.toLowerCase();
46
+ return ALLOW_DOMAINS.some(d => host === d.toLowerCase() || host.endsWith("." + d.toLowerCase()));
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ async function runAgentBrowser(args: string[], timeoutMs = DEFAULT_TIMEOUT_MS): Promise<RunResult> {
53
+ return await enqueue(() => new Promise<RunResult>((resolveRun) => {
54
+ const finalArgs: string[] = [];
55
+ finalArgs.push(...args, "--json");
56
+ finalArgs.push("--session-name", DEFAULT_SESSION);
57
+ if (STATE_PATH) finalArgs.push("--state", STATE_PATH);
58
+
59
+ const child = spawn(AGENT_BROWSER_BIN, finalArgs, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
60
+ let stdout = "";
61
+ let stderr = "";
62
+
63
+ const timer = setTimeout(() => child.kill(), timeoutMs);
64
+
65
+ child.stdout.on("data", (d) => { stdout += d.toString("utf8"); });
66
+ child.stderr.on("data", (d) => { stderr += d.toString("utf8"); });
67
+
68
+ child.on("close", (code) => {
69
+ clearTimeout(timer);
70
+ const out = limitOutput(stdout.trim());
71
+ const err = limitOutput(stderr.trim());
72
+
73
+ let parsed: unknown | undefined;
74
+ try { parsed = out ? JSON.parse(out) : undefined; } catch {
75
+ const lines = out.split("\n").map(l => l.trim()).filter(Boolean);
76
+ for (let i = lines.length - 1; i >= 0; i--) {
77
+ try { parsed = JSON.parse(lines[i]); break; } catch {}
78
+ }
79
+ }
80
+ resolveRun({ ok: code === 0, exitCode: code, stdout: out, stderr: err, parsedJson: parsed });
81
+ });
82
+ }));
83
+ }
84
+
85
+ const server = new McpServer({ name: "agent-browser-mcp", version: "0.1.0" });
86
+
87
+ server.tool(
88
+ "browser_open",
89
+ "Open a URL in agent-browser (reuses session).",
90
+ { url: z.string().url(), headed: z.boolean().optional().default(false), timeoutMs: z.number().int().positive().optional() },
91
+ async ({ url, headed, timeoutMs }) => {
92
+ if (!domainAllowed(url)) return { content: [txt(`Blocked by allowlist: ${url}`)] };
93
+ const args = ["open", url];
94
+ if (headed) args.push("--headed");
95
+ const r = await runAgentBrowser(args, timeoutMs);
96
+ const content = [txt(r.ok ? "OK" : "FAILED"), txt(r.stdout || "")];
97
+ if (r.stderr) content.push(txt(`stderr:\n${r.stderr}`));
98
+ return { content };
99
+ }
100
+ );
101
+
102
+ server.tool(
103
+ "browser_click",
104
+ "Click an element by selector.",
105
+ { selector: z.string().min(1), timeoutMs: z.number().int().positive().optional() },
106
+ async ({ selector, timeoutMs }) => {
107
+ const r = await runAgentBrowser(["click", selector], timeoutMs);
108
+ return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
109
+ }
110
+ );
111
+
112
+ server.tool(
113
+ "browser_type",
114
+ "Type into an input/textarea by selector.",
115
+ { selector: z.string().min(1), text: z.string(), submit: z.boolean().optional().default(false), timeoutMs: z.number().int().positive().optional() },
116
+ async ({ selector, text, submit, timeoutMs }) => {
117
+ const args = ["type", selector, text];
118
+ if (submit) args.push("--submit");
119
+ const r = await runAgentBrowser(args, timeoutMs);
120
+ return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
121
+ }
122
+ );
123
+
124
+ server.tool(
125
+ "browser_extract",
126
+ "Extract text/html from a selector.",
127
+ { selector: z.string().min(1), mode: z.enum(["text", "html"]).optional().default("text"), timeoutMs: z.number().int().positive().optional() },
128
+ async ({ selector, mode, timeoutMs }) => {
129
+ const r = await runAgentBrowser(["extract", mode, selector], timeoutMs);
130
+ const payload = r.parsedJson ?? r.stdout;
131
+ return { content: [txt(typeof payload === "string" ? payload : JSON.stringify(payload, null, 2))] };
132
+ }
133
+ );
134
+
135
+ server.tool(
136
+ "browser_screenshot",
137
+ "Take a screenshot and save to a file path.",
138
+ { path: z.string().min(1).optional().default("agent-browser.png"), fullPage: z.boolean().optional().default(true), timeoutMs: z.number().int().positive().optional() },
139
+ async ({ path, fullPage, timeoutMs }) => {
140
+ const outPath = resolve(process.cwd(), path);
141
+ const args = ["screenshot", outPath];
142
+ if (fullPage) args.push("--full-page");
143
+ const r = await runAgentBrowser(args, timeoutMs);
144
+ const content = [txt(r.ok ? `OK: ${outPath}` : "FAILED")];
145
+ if (r.stdout) content.push(txt(r.stdout));
146
+ if (r.stderr) content.push(txt(`stderr:\n${r.stderr}`));
147
+ return { content };
148
+ }
149
+ );
150
+
151
+ server.tool(
152
+ "browser_set_viewport",
153
+ "Set viewport size.",
154
+ { width: z.number().int().min(200).max(4000), height: z.number().int().min(200).max(4000), timeoutMs: z.number().int().positive().optional() },
155
+ async ({ width, height, timeoutMs }) => {
156
+ const r = await runAgentBrowser(["set", "viewport", String(width), String(height)], timeoutMs);
157
+ return { content: [txt(r.ok ? (r.stdout || "OK") : (r.stderr || r.stdout || "FAILED"))] };
158
+ }
159
+ );
160
+
161
+ const transport = new StdioServerTransport();
162
+ await server.connect(transport);
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ES2022",
6
+ "moduleResolution": "Bundler",
7
+ "types": ["node"],
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "strict": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }