chromiumfish 0.2.1 → 0.2.3

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
@@ -84,11 +84,35 @@ want to manage the lifecycle (`const { agent, close } = await launchAgent()`).
84
84
  > Needs a WebSocket: the Node 22+ global `WebSocket` is used automatically; on
85
85
  > Node <22 install the optional `ws` package (`npm install ws`).
86
86
 
87
+ ## External agents
88
+
89
+ Prefer a third-party framework (Hermes, OpenClaw, browser-use, Playwright, …)? `chromiumfish
90
+ serve` exposes a plain CDP endpoint — with your persona/proxy/timezone active — for any of them
91
+ to attach to:
92
+
93
+ ```bash
94
+ npx chromiumfish serve --persona-seed alice # -> http://127.0.0.1:9222
95
+ # e.g. Hermes ~/.hermes/config.yaml: browser: { cdp_url: "http://127.0.0.1:9222" }
96
+ ```
97
+
98
+ Or run it as an **MCP server** for Claude Code/Desktop, Cursor, etc.:
99
+
100
+ ```bash
101
+ npx chromiumfish mcp --persona-seed alice # exposes browser tools over MCP (stdio)
102
+ ```
103
+
104
+ Full guide: [chromiumfish.com/agents](https://chromiumfish.com/agents).
105
+
87
106
  ## CLI
88
107
 
89
108
  ```bash
90
109
  npx chromiumfish fetch [--browser-version X] [--force] # download + cache
91
110
  npx chromiumfish path # print binary path
111
+ npx chromiumfish serve [--port 9222] [--persona-seed S] # CDP endpoint for external agents
112
+ [--proxy URL] [--window-size WxH] [--timezone Z] [--headless]
113
+ [--browser-version X] [--extra-args ARGS] [--timeout S]
114
+ npx chromiumfish mcp [--persona-seed S] [--headed] # MCP server (Claude, Cursor, ...)
115
+ [--proxy URL] [--window-size WxH] [--port N] [--typing T] [--llm-key K]
92
116
  npx chromiumfish clear # wipe the cache
93
117
  npx chromiumfish --version
94
118
  ```
package/dist/agent.d.ts CHANGED
@@ -24,6 +24,18 @@ export declare class AgentResult {
24
24
  get recorded(): number;
25
25
  summary(): string;
26
26
  }
27
+ /** Minimal CDP-over-WebSocket client. */
28
+ export declare class CDP {
29
+ private ws;
30
+ private id;
31
+ private pending;
32
+ private waiters;
33
+ private constructor();
34
+ static connect(url: string): Promise<CDP>;
35
+ send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<any>;
36
+ waitEvent(method: string, timeoutMs: number): Promise<void>;
37
+ close(): void;
38
+ }
27
39
  export interface RunTaskOptions {
28
40
  /** Navigate here before the agent loop (the agent can also navigate itself). */
29
41
  url?: string;
@@ -44,7 +56,10 @@ export declare class AgentClient {
44
56
  constructor(port?: number, host?: string, timeoutMs?: number);
45
57
  private httpGet;
46
58
  /** Return {targetId, wsUrl}, reusing a real page or opening one. */
47
- private pickPage;
59
+ pickPage(): Promise<{
60
+ targetId: string;
61
+ wsUrl: string;
62
+ }>;
48
63
  runTask(goal: string, opts?: RunTaskOptions): Promise<AgentResult>;
49
64
  }
50
65
  export interface LaunchAgentOptions {
package/dist/agent.js CHANGED
@@ -165,7 +165,7 @@ async function getWebSocketCtor() {
165
165
  }
166
166
  }
167
167
  /** Minimal CDP-over-WebSocket client. */
168
- class CDP {
168
+ export class CDP {
169
169
  ws;
170
170
  id = 0;
171
171
  pending = new Map();
package/dist/cli.js CHANGED
@@ -1,7 +1,185 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import * as fs from "node:fs";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import * as path from "node:path";
3
7
  import { binaryPath, cacheRoot, fetchBrowser } from "./fetch.js";
8
+ import { buildArgs } from "./launcher.js";
9
+ import { resolveTimezone } from "./ip2tz.js";
4
10
  import { SDK_VERSION, browserVersion } from "./version.js";
11
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12
+ /** Read `--flag value` from argv, or undefined. */
13
+ function flag(argv, name) {
14
+ const i = argv.indexOf(name);
15
+ return i >= 0 ? argv[i + 1] : undefined;
16
+ }
17
+ /**
18
+ * Launch ChromiumFish as a bare CDP endpoint for external agent frameworks
19
+ * (Hermes, OpenClaw, browser-use, ...) to attach to. Unlike `launchAgent`, this
20
+ * adds NO `--agent-*` switches — it just exposes Chrome DevTools Protocol with
21
+ * the persona/proxy/timezone you pick, then blocks until interrupted.
22
+ */
23
+ async function serve(argv) {
24
+ const port = Number(flag(argv, "--port") ?? 9222);
25
+ const personaSeed = flag(argv, "--persona-seed");
26
+ const proxy = flag(argv, "--proxy");
27
+ const windowSize = flag(argv, "--window-size") ?? "1920x1080";
28
+ const timezone = flag(argv, "--timezone");
29
+ const headless = argv.includes("--headless");
30
+ const version = flag(argv, "--browser-version");
31
+ const extraArgsRaw = flag(argv, "--extra-args");
32
+ const timeoutSecs = Number(flag(argv, "--timeout") ?? 30);
33
+ // Validate up front — Python's argparse does this for us; here it's manual.
34
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
35
+ console.error(`invalid --port ${flag(argv, "--port")}; expected an integer 1-65535`);
36
+ return 1;
37
+ }
38
+ if (!Number.isFinite(timeoutSecs) || timeoutSecs <= 0) {
39
+ console.error(`invalid --timeout ${flag(argv, "--timeout")}; expected a positive number of seconds`);
40
+ return 1;
41
+ }
42
+ const [w, h] = windowSize.toLowerCase().split("x").map(Number);
43
+ if (!w || !h) {
44
+ console.error(`invalid --window-size ${windowSize}; expected WIDTHxHEIGHT, e.g. 1920x1080`);
45
+ return 1;
46
+ }
47
+ const chrome = await binaryPath(version); // fetches if not cached
48
+ const profile = mkdtempSync(path.join(tmpdir(), "cf-serve-"));
49
+ const cleanup = () => rmSync(profile, { recursive: true, force: true });
50
+ let proc;
51
+ const killTree = (sig) => {
52
+ if (!proc?.pid)
53
+ return;
54
+ try {
55
+ process.kill(-proc.pid, sig); // negative pid = the whole process group
56
+ }
57
+ catch {
58
+ try {
59
+ proc.kill(sig);
60
+ }
61
+ catch {
62
+ /* already gone */
63
+ }
64
+ }
65
+ };
66
+ // try/finally guarantees the browser tree + temp profile are torn down even
67
+ // if resolveTimezone/spawn throws or an early return fires — matches the
68
+ // Python handler's finally -> _teardown().
69
+ try {
70
+ const extra = [];
71
+ if (headless)
72
+ extra.push("--headless=new");
73
+ if (proxy)
74
+ extra.push(`--proxy-server=${proxy}`);
75
+ if (extraArgsRaw)
76
+ extra.push(...extraArgsRaw.split(",").filter(Boolean));
77
+ const args = [
78
+ `--remote-debugging-port=${port}`,
79
+ // External clients send an Origin header DevTools rejects unless allowed.
80
+ "--remote-allow-origins=*",
81
+ `--user-data-dir=${profile}`,
82
+ "--no-first-run",
83
+ "--no-default-browser-check",
84
+ ...buildArgs({ personaSeed, windowSize: [w, h], args: extra }),
85
+ ];
86
+ const env = { ...process.env };
87
+ if (timezone) {
88
+ const tz = timezone === "auto" ? await resolveTimezone({ proxy }) : timezone;
89
+ if (tz)
90
+ env.TZ = tz;
91
+ }
92
+ const base = `http://127.0.0.1:${port}`;
93
+ // detached: own process group, so we can signal the WHOLE tree (browser +
94
+ // GPU/renderer/network helpers) on exit instead of leaking the helpers.
95
+ proc = spawn(chrome, args, { stdio: "ignore", env, detached: true });
96
+ const deadline = Date.now() + timeoutSecs * 1000;
97
+ let ver = null;
98
+ for (;;) {
99
+ try {
100
+ const r = await fetch(`${base}/json/version`);
101
+ if (r.ok) {
102
+ ver = (await r.json());
103
+ break;
104
+ }
105
+ }
106
+ catch {
107
+ /* not up yet */
108
+ }
109
+ if (proc.exitCode !== null) {
110
+ console.error("ChromiumFish exited before the CDP endpoint came up");
111
+ return 1;
112
+ }
113
+ if (Date.now() > deadline) {
114
+ console.error("ChromiumFish did not expose its CDP endpoint in time");
115
+ return 1;
116
+ }
117
+ await sleep(400);
118
+ }
119
+ console.log(`ChromiumFish ${ver?.Browser ?? ""} ready — CDP endpoint for external agents`);
120
+ console.log(` HTTP : ${base} (discovery: ${base}/json/version)`);
121
+ if (ver?.webSocketDebuggerUrl)
122
+ console.log(` WS : ${ver.webSocketDebuggerUrl}`);
123
+ console.log("");
124
+ console.log("Attach an agent, e.g.:");
125
+ console.log(` Hermes ~/.hermes/config.yaml -> browser: { cdp_url: "${base}" }`);
126
+ console.log(` browser-use BrowserSession(cdp_url="${base}")`);
127
+ console.log(` OpenClaw profile cdpUrl: "${base}"`);
128
+ console.log("Ctrl-C to stop.");
129
+ await new Promise((resolve) => {
130
+ const stop = () => {
131
+ console.log("\nstopping…");
132
+ killTree("SIGTERM");
133
+ resolve();
134
+ };
135
+ process.on("SIGINT", stop);
136
+ process.on("SIGTERM", stop);
137
+ proc.on("exit", () => resolve());
138
+ });
139
+ return 0;
140
+ }
141
+ finally {
142
+ await sleep(300);
143
+ killTree("SIGKILL");
144
+ cleanup();
145
+ }
146
+ }
147
+ /**
148
+ * Run the MCP server exposing the browser to MCP clients (Claude, Cursor, …).
149
+ * Stdio is the JSON-RPC channel, so this path must never write to stdout.
150
+ */
151
+ async function runMcp(argv) {
152
+ const ws = (flag(argv, "--window-size") ?? "1920x1080").toLowerCase().split("x").map(Number);
153
+ if (!ws[0] || !ws[1]) {
154
+ console.error(`invalid --window-size; expected WIDTHxHEIGHT, e.g. 1920x1080`);
155
+ return 1;
156
+ }
157
+ const port = Number(flag(argv, "--port") ?? 9222);
158
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
159
+ console.error(`invalid --port; expected an integer 1-65535`);
160
+ return 1;
161
+ }
162
+ let mod;
163
+ try {
164
+ mod = await import("./mcp.js");
165
+ }
166
+ catch {
167
+ console.error("the MCP server needs @modelcontextprotocol/sdk + zod; run `npm i @modelcontextprotocol/sdk zod`");
168
+ return 1;
169
+ }
170
+ await mod.runServer({
171
+ personaSeed: flag(argv, "--persona-seed"),
172
+ headless: !argv.includes("--headed"),
173
+ windowSize: [ws[0], ws[1]],
174
+ proxy: flag(argv, "--proxy"),
175
+ port,
176
+ typing: flag(argv, "--typing") ?? "human",
177
+ apiKey: flag(argv, "--llm-key") ?? "",
178
+ apiBase: flag(argv, "--llm-base") ?? "",
179
+ model: flag(argv, "--llm-model") ?? "",
180
+ });
181
+ return 0;
182
+ }
5
183
  async function main(argv) {
6
184
  const cmd = argv[2];
7
185
  switch (cmd) {
@@ -26,6 +204,10 @@ async function main(argv) {
26
204
  }
27
205
  return 0;
28
206
  }
207
+ case "serve":
208
+ return await serve(argv);
209
+ case "mcp":
210
+ return await runMcp(argv);
29
211
  case "--version":
30
212
  case "-V":
31
213
  console.log(`chromiumfish ${SDK_VERSION} (browser ${browserVersion()})`);
@@ -37,6 +219,11 @@ async function main(argv) {
37
219
  "Usage:",
38
220
  " chromiumfish fetch [--browser-version X] [--force] download + cache",
39
221
  " chromiumfish path print binary path",
222
+ " chromiumfish serve [--port 9222] [--persona-seed S] CDP endpoint for agents",
223
+ " [--proxy URL] [--window-size WxH] [--timezone Z] [--headless]",
224
+ " [--browser-version X] [--extra-args ARGS] [--timeout S]",
225
+ " chromiumfish mcp [--persona-seed S] [--headed] MCP server (Claude, Cursor, ...)",
226
+ " [--proxy URL] [--window-size WxH] [--port N] [--typing T] [--llm-key K]",
40
227
  " chromiumfish clear wipe the cache",
41
228
  " chromiumfish --version",
42
229
  ].join("\n"));
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ChromiumFish MCP server — drive the stealth browser from any MCP client.
3
+ *
4
+ * Exposes ChromiumFish as a Model Context Protocol (https://modelcontextprotocol.io)
5
+ * server so MCP-speaking agents (Claude Code/Desktop, Cursor, Windsurf, …) can
6
+ * perceive and operate the hardened browser directly. Tools are driven over the
7
+ * Chrome DevTools Protocol against a ChromiumFish instance the server launches and
8
+ * holds for its lifetime — the persona/proxy/timezone you start it with stay
9
+ * active, so the agent operates a browser that already looks like a real person.
10
+ *
11
+ * npx chromiumfish mcp --persona-seed alice
12
+ *
13
+ * The granular tools (navigate/snapshot/get_text/screenshot/click/type_text/eval_js)
14
+ * need no LLM on the server side — the MCP client is the brain. `run_task` delegates
15
+ * a whole plain-language goal to the fork's native in-browser agent, which needs an
16
+ * OpenAI-compatible LLM (OPENAI_API_* / --llm-*). TypeScript port of `chromiumfish.mcp`.
17
+ */
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { type TypingSpeed } from "./agent.js";
20
+ export interface McpConfig {
21
+ personaSeed?: string;
22
+ headless?: boolean;
23
+ windowSize?: [number, number];
24
+ proxy?: string;
25
+ port?: number;
26
+ typing?: TypingSpeed;
27
+ apiKey?: string;
28
+ apiBase?: string;
29
+ model?: string;
30
+ }
31
+ export declare function buildServer(): McpServer;
32
+ /** Configure and run the MCP server over stdio (blocks until the client disconnects). */
33
+ export declare function runServer(config?: McpConfig): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * ChromiumFish MCP server — drive the stealth browser from any MCP client.
3
+ *
4
+ * Exposes ChromiumFish as a Model Context Protocol (https://modelcontextprotocol.io)
5
+ * server so MCP-speaking agents (Claude Code/Desktop, Cursor, Windsurf, …) can
6
+ * perceive and operate the hardened browser directly. Tools are driven over the
7
+ * Chrome DevTools Protocol against a ChromiumFish instance the server launches and
8
+ * holds for its lifetime — the persona/proxy/timezone you start it with stay
9
+ * active, so the agent operates a browser that already looks like a real person.
10
+ *
11
+ * npx chromiumfish mcp --persona-seed alice
12
+ *
13
+ * The granular tools (navigate/snapshot/get_text/screenshot/click/type_text/eval_js)
14
+ * need no LLM on the server side — the MCP client is the brain. `run_task` delegates
15
+ * a whole plain-language goal to the fork's native in-browser agent, which needs an
16
+ * OpenAI-compatible LLM (OPENAI_API_* / --llm-*). TypeScript port of `chromiumfish.mcp`.
17
+ */
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+ import { launchAgent, CDP } from "./agent.js";
22
+ import { SDK_VERSION } from "./version.js";
23
+ // Builds a readable, selector-addressable list of interactive elements. The raw
24
+ // `Page.getAnnotatedPageContent` CDP command returns the index-keyed proto the
25
+ // native agent consumes — not usable directly and not selector-addressable — so
26
+ // we perceive via the DOM. The selectors it emits are what click/type_text take.
27
+ const SNAPSHOT_JS = String.raw `
28
+ (function(){
29
+ function sel(el){
30
+ if (el.id) return '#'+CSS.escape(el.id);
31
+ var nm = el.getAttribute('name');
32
+ if (nm) return el.tagName.toLowerCase()+'[name="'+nm+'"]';
33
+ var path=[], e=el;
34
+ while(e && e.nodeType===1 && path.length<4){
35
+ var part=e.tagName.toLowerCase();
36
+ if(e.parentElement){
37
+ var sib=Array.prototype.filter.call(e.parentElement.children,function(c){return c.tagName===e.tagName;});
38
+ if(sib.length>1) part+=':nth-of-type('+(sib.indexOf(e)+1)+')';
39
+ }
40
+ path.unshift(part); e=e.parentElement;
41
+ }
42
+ return path.join(' > ');
43
+ }
44
+ function label(el){
45
+ return (el.getAttribute('aria-label')||el.value||el.placeholder||el.innerText||el.getAttribute('title')||'')
46
+ .trim().replace(/\s+/g,' ').slice(0,80);
47
+ }
48
+ var els=document.querySelectorAll('a,button,input,textarea,select,[role=button],[role=link],[onclick],[contenteditable=true]');
49
+ var out=[], n=0;
50
+ for(var i=0;i<els.length && n<200;i++){
51
+ var el=els[i];
52
+ if(!el.getClientRects().length) continue;
53
+ var role=el.tagName.toLowerCase()+(el.type?'['+el.type+']':'');
54
+ var line='['+n+'] '+role+' "'+label(el)+'" '+sel(el);
55
+ if(el.href) line+=' -> '+el.href;
56
+ out.push(line); n++;
57
+ }
58
+ return out.length ? out.join('\n') : '(no visible interactive elements)';
59
+ })()
60
+ `;
61
+ let _config = {};
62
+ let _session = null;
63
+ async function client() {
64
+ if (!_session) {
65
+ const extra = [];
66
+ if (_config.personaSeed)
67
+ extra.push(`--persona-seed=${_config.personaSeed}`);
68
+ if (_config.headless ?? true)
69
+ extra.push("--headless=new");
70
+ const [w, h] = _config.windowSize ?? [1920, 1080];
71
+ extra.push(`--window-size=${w},${h}`);
72
+ if (_config.proxy)
73
+ extra.push(`--proxy-server=${_config.proxy}`);
74
+ _session = await launchAgent({
75
+ port: _config.port ?? 9222,
76
+ apiKey: _config.apiKey ?? "",
77
+ apiBase: _config.apiBase ?? "",
78
+ model: _config.model ?? "",
79
+ typing: _config.typing ?? "human",
80
+ extraArgs: extra,
81
+ });
82
+ }
83
+ return _session.agent;
84
+ }
85
+ async function shutdown() {
86
+ if (_session) {
87
+ try {
88
+ await _session.close();
89
+ }
90
+ catch {
91
+ /* ignore */
92
+ }
93
+ _session = null;
94
+ }
95
+ }
96
+ /** Run `fn` against a short-lived CDP connection to the active page target. */
97
+ async function withPage(fn) {
98
+ const agent = await client();
99
+ const { targetId, wsUrl } = await agent.pickPage();
100
+ const cdp = await CDP.connect(wsUrl);
101
+ try {
102
+ return await fn(cdp, targetId);
103
+ }
104
+ finally {
105
+ cdp.close();
106
+ }
107
+ }
108
+ async function evalJs(cdp, expression) {
109
+ const res = await cdp.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
110
+ if (res.exceptionDetails) {
111
+ throw new Error(res.exceptionDetails.exception?.description || res.exceptionDetails.text || "eval error");
112
+ }
113
+ return res.result?.value;
114
+ }
115
+ const textResult = (text) => ({ content: [{ type: "text", text }] });
116
+ export function buildServer() {
117
+ const mcp = new McpServer({ name: "chromiumfish", version: SDK_VERSION });
118
+ mcp.registerTool("navigate", {
119
+ description: "Open a URL in the ChromiumFish browser and wait for it to load. Returns the resolved URL and title; call `snapshot` next to see what's on the page.",
120
+ inputSchema: { url: z.string().describe("The URL to open") },
121
+ }, async ({ url }) => textResult(await withPage(async (cdp) => {
122
+ await cdp.send("Page.enable");
123
+ await cdp.send("Page.navigate", { url });
124
+ await cdp.waitEvent("Page.loadEventFired", 30_000).catch(() => { });
125
+ const title = await evalJs(cdp, "document.title");
126
+ const href = await evalJs(cdp, "document.location.href");
127
+ return `Loaded ${href}\nTitle: ${title}`;
128
+ })));
129
+ mcp.registerTool("snapshot", {
130
+ description: 'List the page\'s visible interactive elements, one per line, as `[i] <role> "label" <css-selector>` (links also show `-> url`). Use the CSS selector with click/type_text. For prose use get_text; for anything else, eval_js.',
131
+ inputSchema: {},
132
+ }, async () => textResult((await withPage((cdp) => evalJs(cdp, SNAPSHOT_JS))) || "(no visible interactive elements)"));
133
+ mcp.registerTool("get_text", {
134
+ description: "Return the visible text of the current page (`document.body.innerText`). Best for reading articles/long content.",
135
+ inputSchema: {},
136
+ }, async () => textResult((await withPage((cdp) => evalJs(cdp, "document.body && document.body.innerText || ''"))) || "(empty)"));
137
+ mcp.registerTool("screenshot", { description: "Capture a PNG screenshot of the current viewport.", inputSchema: {} }, async () => {
138
+ const data = (await withPage(async (cdp) => (await cdp.send("Page.captureScreenshot", { format: "png" })).data));
139
+ return { content: [{ type: "image", data, mimeType: "image/png" }] };
140
+ });
141
+ mcp.registerTool("click", {
142
+ description: "Humanized, trusted click on the first element matching a CSS selector (cursor moves along a bezier path; `navigator.webdriver` stays false). Fails if nothing matches.",
143
+ inputSchema: { selector: z.string().describe("CSS selector of the element to click") },
144
+ }, async ({ selector }) => textResult(await withPage(async (cdp, targetId) => {
145
+ const r = (await cdp.send("Browser.humanizedClickSelector", { targetId, selector })) ?? {};
146
+ return `Clicked ${JSON.stringify(selector)} at (${r.x}, ${r.y})`;
147
+ })));
148
+ mcp.registerTool("type_text", {
149
+ description: "Click the element matching `selector` to focus it, type `text`, and optionally press Enter (`submit=true`) to submit a search/form.",
150
+ inputSchema: {
151
+ selector: z.string().describe("CSS selector of the field"),
152
+ text: z.string().describe("Text to type"),
153
+ submit: z.boolean().optional().describe("Press Enter after typing"),
154
+ },
155
+ }, async ({ selector, text, submit }) => textResult(await withPage(async (cdp, targetId) => {
156
+ await cdp.send("Browser.humanizedClickSelector", { targetId, selector });
157
+ await cdp.send("Input.insertText", { text });
158
+ if (submit) {
159
+ for (const type of ["keyDown", "keyUp"]) {
160
+ await cdp.send("Input.dispatchKeyEvent", { type, key: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
161
+ }
162
+ }
163
+ return `Typed into ${JSON.stringify(selector)}${submit ? " and pressed Enter" : ""}`;
164
+ })));
165
+ mcp.registerTool("eval_js", {
166
+ description: "Evaluate a JavaScript expression in the page and return its (JSON) result. Powerful escape hatch — read anything or act on the page.",
167
+ inputSchema: { expression: z.string().describe("JavaScript expression to evaluate") },
168
+ }, async ({ expression }) => {
169
+ const value = await withPage((cdp) => evalJs(cdp, expression));
170
+ let text;
171
+ try {
172
+ text = JSON.stringify(value);
173
+ }
174
+ catch {
175
+ text = String(value);
176
+ }
177
+ return textResult(text ?? "undefined");
178
+ });
179
+ mcp.registerTool("run_task", {
180
+ description: "Delegate a whole plain-language goal to ChromiumFish's native in-browser agent (perceive → think → act loop, humanized input). Best for multi-step flows. Requires an OpenAI-compatible LLM on the server (OPENAI_API_* / --llm-*); otherwise use the granular tools.",
181
+ inputSchema: { task: z.string().describe("Plain-language goal"), url: z.string().optional().describe("Page to start on") },
182
+ }, async ({ task, url }) => {
183
+ const agent = await client();
184
+ const r = await agent.runTask(task, { url: url || undefined });
185
+ const text = r.finalText || (r.success ? "(done)" : "Task did not complete. (If this needs the native agent, ensure an LLM is configured on the MCP server.)");
186
+ return textResult(text);
187
+ });
188
+ return mcp;
189
+ }
190
+ /** Configure and run the MCP server over stdio (blocks until the client disconnects). */
191
+ export async function runServer(config = {}) {
192
+ _config = config;
193
+ const server = buildServer();
194
+ const transport = new StdioServerTransport();
195
+ const stop = () => {
196
+ shutdown().finally(() => process.exit(0));
197
+ };
198
+ process.on("SIGINT", stop);
199
+ process.on("SIGTERM", stop);
200
+ try {
201
+ await server.connect(transport);
202
+ await new Promise((resolve) => {
203
+ transport.onclose = () => resolve();
204
+ });
205
+ }
206
+ finally {
207
+ await shutdown();
208
+ }
209
+ }
package/dist/version.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
7
  */
8
8
  /** SDK package version (kept in sync with package.json). */
9
- export declare const SDK_VERSION = "0.2.1";
9
+ export declare const SDK_VERSION = "0.2.3";
10
10
  /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
11
  export declare const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
12
12
  /** Public repo hosting the release assets. */
package/dist/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
7
  */
8
8
  /** SDK package version (kept in sync with package.json). */
9
- export const SDK_VERSION = "0.2.1";
9
+ export const SDK_VERSION = "0.2.3";
10
10
  /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
11
  export const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
12
12
  /** Public repo hosting the release assets. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromiumfish",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,5 +56,9 @@
56
56
  "devDependencies": {
57
57
  "@types/node": "^20.0.0",
58
58
  "typescript": "^5.4.0"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.29.0",
62
+ "zod": "^4.4.3"
59
63
  }
60
64
  }