@automatebrowser/mcp 0.3.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/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +10 -0
- package/CHANGELOG.md +167 -0
- package/LICENSE +202 -0
- package/README.md +2092 -0
- package/dist/chunk-R5JREBXQ.js +211 -0
- package/dist/chunk-WAXVZKT5.js +5363 -0
- package/dist/cli.js +135 -0
- package/dist/index.js +287 -0
- package/dist/relay.js +919 -0
- package/package.json +80 -0
- package/skills/automate-browser/SKILL.md +181 -0
- package/skills/automate-browser/references/capture-and-diagnostics.md +486 -0
- package/skills/automate-browser/references/page-interaction.md +237 -0
- package/skills/automate-browser/references/reading-and-extraction.md +113 -0
- package/skills/automate-browser/references/sessions-and-state.md +186 -0
- package/skills/automate-browser/references/tabs-and-multi-agent.md +156 -0
- package/skills/automate-browser/references/tool-reference.md +236 -0
- package/skills/automate-browser/references/troubleshooting.md +136 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Context,
|
|
4
|
+
asToolError,
|
|
5
|
+
callTool,
|
|
6
|
+
package_default,
|
|
7
|
+
renderError,
|
|
8
|
+
selectTools
|
|
9
|
+
} from "./chunk-WAXVZKT5.js";
|
|
10
|
+
import {
|
|
11
|
+
debugLog
|
|
12
|
+
} from "./chunk-R5JREBXQ.js";
|
|
13
|
+
|
|
14
|
+
// src/cli.ts
|
|
15
|
+
import { randomUUID } from "crypto";
|
|
16
|
+
import { writeFileSync } from "fs";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { program } from "commander";
|
|
20
|
+
process.env.AUTOMATE_BROWSER_CLIENT_NAME ||= "cli";
|
|
21
|
+
process.env.AUTOMATE_BROWSER_INSTANCE_ID ||= randomUUID();
|
|
22
|
+
var ALIASES = {
|
|
23
|
+
navigate: { tool: "browser_navigate", positional: "url" },
|
|
24
|
+
"read-page": { tool: "browser_read_page" },
|
|
25
|
+
snapshot: { tool: "browser_snapshot" },
|
|
26
|
+
screenshot: { tool: "browser_screenshot" },
|
|
27
|
+
status: { tool: "browser_status" },
|
|
28
|
+
eval: { tool: "browser_eval", positional: "expression" },
|
|
29
|
+
tabs: { tool: "browser_list_tabs" }
|
|
30
|
+
};
|
|
31
|
+
var IMAGE_EXT = {
|
|
32
|
+
"image/png": "png",
|
|
33
|
+
"image/jpeg": "jpg",
|
|
34
|
+
"image/webp": "webp"
|
|
35
|
+
};
|
|
36
|
+
var TOOLS = selectTools("full").tools;
|
|
37
|
+
var write = (stream, text) => new Promise((resolve) => stream.write(text, () => resolve()));
|
|
38
|
+
function resolveTool(command) {
|
|
39
|
+
const alias = ALIASES[command];
|
|
40
|
+
const name = alias?.tool ?? command;
|
|
41
|
+
const tool = TOOLS.find((t) => t.schema.name === name) ?? TOOLS.find((t) => t.schema.name === `browser_${name}`);
|
|
42
|
+
if (!tool) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unknown command "${command}".
|
|
45
|
+
|
|
46
|
+
Aliases: ${Object.keys(ALIASES).join(", ")}
|
|
47
|
+
|
|
48
|
+
Tools:
|
|
49
|
+
${TOOLS.map((t) => ` ${t.schema.name}`).join("\n")}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { tool, positional: alias?.positional };
|
|
53
|
+
}
|
|
54
|
+
function buildArgs(command, positional, value, json) {
|
|
55
|
+
let args = {};
|
|
56
|
+
if (json) {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(json);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new Error(`--args is not valid JSON: ${err.message}`, { cause: err });
|
|
62
|
+
}
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
64
|
+
throw new Error(`--args must be a JSON object, e.g. --args '{"url":"https://example.com"}'`);
|
|
65
|
+
}
|
|
66
|
+
args = parsed;
|
|
67
|
+
}
|
|
68
|
+
if (value !== void 0) {
|
|
69
|
+
if (!positional) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`"${command}" takes no positional argument \u2014 pass its options with --args '{\u2026}'.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
args[positional] = value;
|
|
75
|
+
}
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
async function printResult(result, asJson) {
|
|
79
|
+
if (asJson) {
|
|
80
|
+
await write(process.stdout, JSON.stringify(result, null, 2) + "\n");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const part of result.content) {
|
|
84
|
+
if (part.type === "text") {
|
|
85
|
+
await write(process.stdout, part.text + "\n");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const file = join(
|
|
89
|
+
tmpdir(),
|
|
90
|
+
`automate-browser-${Date.now()}.${IMAGE_EXT[part.mimeType] ?? "bin"}`
|
|
91
|
+
);
|
|
92
|
+
writeFileSync(file, Buffer.from(part.data, "base64"));
|
|
93
|
+
await write(process.stdout, `${part.mimeType} saved \u2192 ${file}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
program.name("automate-browser").description(
|
|
98
|
+
"Drive your logged-in browser from the terminal, alongside any connected IDE agents."
|
|
99
|
+
).version(package_default.version).argument("<command>", `alias (${Object.keys(ALIASES).join(", ")}) or a tool name`).argument("[value]", "the alias's argument, e.g. the URL for `navigate`").option("--args <json>", "tool arguments as a JSON object").option("--json", "print the whole tool result as JSON, for scripting").addHelpText(
|
|
100
|
+
"after",
|
|
101
|
+
`
|
|
102
|
+
Examples:
|
|
103
|
+
automate-browser status
|
|
104
|
+
automate-browser navigate https://example.com
|
|
105
|
+
automate-browser eval 'document.title'
|
|
106
|
+
automate-browser read-page --args '{"format":"markdown"}'
|
|
107
|
+
automate-browser tabs --json
|
|
108
|
+
`
|
|
109
|
+
).action(
|
|
110
|
+
async (command, value, opts) => {
|
|
111
|
+
let context;
|
|
112
|
+
try {
|
|
113
|
+
const { tool, positional } = resolveTool(command);
|
|
114
|
+
const args = buildArgs(command, positional, value, opts.args);
|
|
115
|
+
context = new Context({ version: package_default.version });
|
|
116
|
+
await context.start().catch((err) => {
|
|
117
|
+
debugLog("[cli] relay link not ready:", String(err));
|
|
118
|
+
});
|
|
119
|
+
const result = await callTool(context, tool, args);
|
|
120
|
+
await printResult(result, opts.json === true);
|
|
121
|
+
await context.close();
|
|
122
|
+
process.exit(result.isError ? 1 : 0);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const typed = asToolError(err);
|
|
125
|
+
await write(
|
|
126
|
+
process.stderr,
|
|
127
|
+
`${typed ? renderError(typed) : err instanceof Error ? err.message : String(err)}
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
await context?.close().catch(() => void 0);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
void program.parseAsync(process.argv);
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Context,
|
|
4
|
+
activeSelection,
|
|
5
|
+
callTool,
|
|
6
|
+
errorResult,
|
|
7
|
+
logRootSources,
|
|
8
|
+
package_default,
|
|
9
|
+
selectTools,
|
|
10
|
+
setRoots
|
|
11
|
+
} from "./chunk-WAXVZKT5.js";
|
|
12
|
+
import {
|
|
13
|
+
appConfig,
|
|
14
|
+
debugLog,
|
|
15
|
+
log,
|
|
16
|
+
logPath
|
|
17
|
+
} from "./chunk-R5JREBXQ.js";
|
|
18
|
+
|
|
19
|
+
// src/index.ts
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { Option, program } from "commander";
|
|
22
|
+
|
|
23
|
+
// src/server.ts
|
|
24
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
25
|
+
import {
|
|
26
|
+
CallToolRequestSchema,
|
|
27
|
+
ListResourcesRequestSchema,
|
|
28
|
+
ListToolsRequestSchema,
|
|
29
|
+
ReadResourceRequestSchema,
|
|
30
|
+
RootsListChangedNotificationSchema
|
|
31
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
32
|
+
|
|
33
|
+
// src/tools/tool.ts
|
|
34
|
+
function wireSchema(schema) {
|
|
35
|
+
const strip = (value) => {
|
|
36
|
+
if (value === null || typeof value !== "object") return value;
|
|
37
|
+
const { $schema: _dialect, ...rest } = value;
|
|
38
|
+
return rest;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
...schema,
|
|
42
|
+
inputSchema: strip(schema.inputSchema),
|
|
43
|
+
...schema.outputSchema === void 0 ? {} : { outputSchema: strip(schema.outputSchema) }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/server.ts
|
|
48
|
+
async function createServerWithTools(options) {
|
|
49
|
+
const { name, version, tools, resources: resources2 } = options;
|
|
50
|
+
const context = new Context({ version });
|
|
51
|
+
void context.start().catch((err) => {
|
|
52
|
+
debugLog("[server] relay link not ready yet (will retry):", String(err));
|
|
53
|
+
});
|
|
54
|
+
const server = new Server(
|
|
55
|
+
{ name, version },
|
|
56
|
+
{
|
|
57
|
+
capabilities: {
|
|
58
|
+
tools: {},
|
|
59
|
+
resources: {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
const refreshRoots = async () => {
|
|
64
|
+
try {
|
|
65
|
+
if (!server.getClientCapabilities()?.roots) {
|
|
66
|
+
debugLog(
|
|
67
|
+
"[roots] client offers no roots; path sandbox uses cwd + tmpdir + AUTOMATE_BROWSER_WORKSPACE"
|
|
68
|
+
);
|
|
69
|
+
logRootSources();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { roots } = await server.listRoots();
|
|
73
|
+
setRoots(roots);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
debugLog("[roots] listRoots failed; keeping the current allow-list:", String(err));
|
|
76
|
+
logRootSources();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
server.oninitialized = () => void refreshRoots();
|
|
81
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, () => void refreshRoots());
|
|
82
|
+
} catch (err) {
|
|
83
|
+
debugLog("[roots] could not subscribe to roots changes:", String(err));
|
|
84
|
+
}
|
|
85
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
86
|
+
return { tools: tools.map((tool) => wireSchema(tool.schema)) };
|
|
87
|
+
});
|
|
88
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
89
|
+
return { resources: resources2.map((resource) => resource.schema) };
|
|
90
|
+
});
|
|
91
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
92
|
+
const tool = tools.find((tool2) => tool2.schema.name === request.params.name);
|
|
93
|
+
if (!tool) {
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Tool "${request.params.name}" not found` }],
|
|
96
|
+
isError: true
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
try {
|
|
101
|
+
const result = await callTool(context, tool, request.params.arguments);
|
|
102
|
+
debugLog(`[tool] name=${request.params.name} ms=${Date.now() - startedAt}`);
|
|
103
|
+
return result;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
debugLog(
|
|
106
|
+
`[tool] name=${request.params.name} ms=${Date.now() - startedAt} error=${String(error)}`
|
|
107
|
+
);
|
|
108
|
+
return errorResult(tool, error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
112
|
+
const resource = resources2.find((resource2) => resource2.schema.uri === request.params.uri);
|
|
113
|
+
if (!resource) {
|
|
114
|
+
return { contents: [] };
|
|
115
|
+
}
|
|
116
|
+
const contents = await resource.read(context, request.params.uri);
|
|
117
|
+
return { contents };
|
|
118
|
+
});
|
|
119
|
+
const originalClose = server.close.bind(server);
|
|
120
|
+
server.close = async () => {
|
|
121
|
+
await Promise.allSettled([originalClose(), context.close()]);
|
|
122
|
+
};
|
|
123
|
+
return server;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/utils/update-check.ts
|
|
127
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
128
|
+
var PACKAGE = "@automatebrowser/mcp";
|
|
129
|
+
var REGISTRY = `https://registry.npmjs.org/${PACKAGE}/latest`;
|
|
130
|
+
var CACHE_FILE = "update-check.json";
|
|
131
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
132
|
+
var TIMEOUT_MS = 3e3;
|
|
133
|
+
function compareVersions(a, b) {
|
|
134
|
+
const split = (v) => {
|
|
135
|
+
const s = String(v ?? "").trim().replace(/^v/, "");
|
|
136
|
+
const plus = s.indexOf("+");
|
|
137
|
+
const core = plus === -1 ? s : s.slice(0, plus);
|
|
138
|
+
const dash = core.indexOf("-");
|
|
139
|
+
return {
|
|
140
|
+
nums: (dash === -1 ? core : core.slice(0, dash)).split(".").map((n) => Number.parseInt(n, 10) || 0),
|
|
141
|
+
pre: dash === -1 ? "" : core.slice(dash + 1)
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const x = split(a);
|
|
145
|
+
const y = split(b);
|
|
146
|
+
for (let i = 0; i < 3; i++) {
|
|
147
|
+
const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
148
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
149
|
+
}
|
|
150
|
+
if (x.pre === y.pre) return 0;
|
|
151
|
+
if (!x.pre) return 1;
|
|
152
|
+
if (!y.pre) return -1;
|
|
153
|
+
return x.pre < y.pre ? -1 : 1;
|
|
154
|
+
}
|
|
155
|
+
function readCache(file) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
158
|
+
return typeof parsed?.checkedAt === "number" ? parsed : void 0;
|
|
159
|
+
} catch {
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function writeCache(file, cache) {
|
|
164
|
+
try {
|
|
165
|
+
writeFileSync(file, JSON.stringify(cache), "utf8");
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function notify(current, latest) {
|
|
170
|
+
log.warn(
|
|
171
|
+
`A newer ${PACKAGE} is available: ${current} \u2192 ${latest}. Update with \`npm i -g ${PACKAGE}\`. Set AUTOMATE_BROWSER_NO_UPDATE_CHECK=1 to silence this.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
async function checkForUpdate(current, opts = {}) {
|
|
175
|
+
if (process.env.AUTOMATE_BROWSER_NO_UPDATE_CHECK) return;
|
|
176
|
+
const file = opts.cacheFile ?? logPath(CACHE_FILE);
|
|
177
|
+
const now = opts.now ?? Date.now();
|
|
178
|
+
const announce = opts.onNotice ?? notify;
|
|
179
|
+
const cache = readCache(file);
|
|
180
|
+
if (cache?.latest && compareVersions(current, cache.latest) < 0) {
|
|
181
|
+
announce(current, cache.latest);
|
|
182
|
+
}
|
|
183
|
+
if (cache && now - cache.checkedAt < DAY_MS) return;
|
|
184
|
+
let latest;
|
|
185
|
+
try {
|
|
186
|
+
const res = await (opts.fetchImpl ?? fetch)(REGISTRY, {
|
|
187
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
188
|
+
headers: { accept: "application/json" }
|
|
189
|
+
});
|
|
190
|
+
if (res.ok) {
|
|
191
|
+
const body = await res.json();
|
|
192
|
+
if (typeof body?.version === "string") latest = body.version;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
writeCache(file, { checkedAt: now, ...latest ? { latest } : {} });
|
|
197
|
+
if (latest && latest !== cache?.latest && compareVersions(current, latest) < 0) {
|
|
198
|
+
announce(current, latest);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/index.ts
|
|
203
|
+
var FORCE_EXIT_MS = 5e3;
|
|
204
|
+
var PARENT_WATCH_MS = 5e3;
|
|
205
|
+
function processAlive(pid) {
|
|
206
|
+
try {
|
|
207
|
+
process.kill(pid, 0);
|
|
208
|
+
return true;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return err.code === "EPERM";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function installShutdownHandlers(server) {
|
|
214
|
+
let closing = false;
|
|
215
|
+
const shutdown = async (reason) => {
|
|
216
|
+
if (closing) return;
|
|
217
|
+
closing = true;
|
|
218
|
+
debugLog(`[shutdown] ${reason}`);
|
|
219
|
+
const forceTimer = setTimeout(() => {
|
|
220
|
+
debugLog("[shutdown] force exit after timeout");
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}, FORCE_EXIT_MS);
|
|
223
|
+
forceTimer.unref?.();
|
|
224
|
+
try {
|
|
225
|
+
await server.close();
|
|
226
|
+
} catch (err) {
|
|
227
|
+
debugLog("[shutdown] error during close:", err);
|
|
228
|
+
} finally {
|
|
229
|
+
clearTimeout(forceTimer);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
process.stdin.on("close", () => void shutdown("stdin closed"));
|
|
234
|
+
process.stdin.on("end", () => void shutdown("stdin ended"));
|
|
235
|
+
process.stdin.on("error", () => void shutdown("stdin error"));
|
|
236
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
237
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
238
|
+
const parentPid = process.ppid;
|
|
239
|
+
if (parentPid > 1) {
|
|
240
|
+
const watch = setInterval(() => {
|
|
241
|
+
if (!processAlive(parentPid)) {
|
|
242
|
+
clearInterval(watch);
|
|
243
|
+
void shutdown(`parent process ${parentPid} gone (orphaned)`);
|
|
244
|
+
}
|
|
245
|
+
}, PARENT_WATCH_MS);
|
|
246
|
+
watch.unref?.();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function installCrashGuards() {
|
|
250
|
+
process.on("uncaughtException", (err) => {
|
|
251
|
+
debugLog("[uncaughtException]", String(err));
|
|
252
|
+
});
|
|
253
|
+
process.on("unhandledRejection", (reason) => {
|
|
254
|
+
debugLog("[unhandledRejection]", String(reason));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
var resources = [];
|
|
258
|
+
async function createServer() {
|
|
259
|
+
return createServerWithTools({
|
|
260
|
+
name: appConfig.name,
|
|
261
|
+
version: package_default.version,
|
|
262
|
+
tools: activeSelection().tools,
|
|
263
|
+
resources
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
program.version("Version " + package_default.version).name(package_default.name).addOption(
|
|
267
|
+
new Option("--print-tools [profile]", "print tool schemas as JSON and exit").hideHelp()
|
|
268
|
+
).action(async (options) => {
|
|
269
|
+
if (options.printTools) {
|
|
270
|
+
const selection = typeof options.printTools === "string" ? selectTools(options.printTools) : activeSelection();
|
|
271
|
+
process.stdout.write(
|
|
272
|
+
JSON.stringify(selection.tools.map((tool) => wireSchema(tool.schema))) + "\n"
|
|
273
|
+
);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
installCrashGuards();
|
|
277
|
+
void checkForUpdate(package_default.version);
|
|
278
|
+
try {
|
|
279
|
+
const server = await createServer();
|
|
280
|
+
installShutdownHandlers(server);
|
|
281
|
+
const transport = new StdioServerTransport();
|
|
282
|
+
await server.connect(transport);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
debugLog("[startup] failed to start server:", String(err));
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
program.parse(process.argv);
|