browser-automation-skill 0.71.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/LICENSE +21 -0
- package/README.md +144 -0
- package/SECURITY.md +39 -0
- package/SKILL.md +206 -0
- package/bin/cli.mjs +55 -0
- package/install.sh +143 -0
- package/package.json +54 -0
- package/references/adapter-candidates.md +40 -0
- package/references/browser-mcp-cheatsheet.md +132 -0
- package/references/browser-stats-cheatsheet.md +155 -0
- package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
- package/references/midscene-integration.md +359 -0
- package/references/obscura-cheatsheet.md +103 -0
- package/references/playwright-cli-cheatsheet.md +64 -0
- package/references/playwright-lib-cheatsheet.md +90 -0
- package/references/recipes/add-a-tool-adapter.md +134 -0
- package/references/recipes/agent-workflows/README.md +37 -0
- package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
- package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
- package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
- package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
- package/references/recipes/anti-patterns-tool-extension.md +182 -0
- package/references/recipes/body-bytes-not-body.md +139 -0
- package/references/recipes/cache-write-security.md +210 -0
- package/references/recipes/fingerprint-rescue.md +154 -0
- package/references/recipes/model-routing.md +143 -0
- package/references/recipes/path-security.md +138 -0
- package/references/recipes/privacy-canary.md +96 -0
- package/references/recipes/visual-rescue-hook.md +182 -0
- package/references/stats-prices.json +42 -0
- package/references/stats-schema.json +77 -0
- package/references/tool-versions.md +8 -0
- package/scripts/browser-add-site.sh +113 -0
- package/scripts/browser-assert.sh +106 -0
- package/scripts/browser-audit.sh +68 -0
- package/scripts/browser-baseline.sh +135 -0
- package/scripts/browser-click.sh +100 -0
- package/scripts/browser-creds-add.sh +254 -0
- package/scripts/browser-creds-list.sh +67 -0
- package/scripts/browser-creds-migrate.sh +122 -0
- package/scripts/browser-creds-remove.sh +69 -0
- package/scripts/browser-creds-rotate-totp.sh +109 -0
- package/scripts/browser-creds-show.sh +82 -0
- package/scripts/browser-creds-totp.sh +94 -0
- package/scripts/browser-do.sh +630 -0
- package/scripts/browser-doctor.sh +365 -0
- package/scripts/browser-drag.sh +90 -0
- package/scripts/browser-extract.sh +192 -0
- package/scripts/browser-fill.sh +142 -0
- package/scripts/browser-flow.sh +316 -0
- package/scripts/browser-history.sh +187 -0
- package/scripts/browser-hover.sh +92 -0
- package/scripts/browser-inspect.sh +188 -0
- package/scripts/browser-list-sessions.sh +78 -0
- package/scripts/browser-list-sites.sh +42 -0
- package/scripts/browser-login.sh +279 -0
- package/scripts/browser-mcp.sh +65 -0
- package/scripts/browser-migrate.sh +195 -0
- package/scripts/browser-open.sh +134 -0
- package/scripts/browser-press.sh +80 -0
- package/scripts/browser-remove-session.sh +72 -0
- package/scripts/browser-remove-site.sh +68 -0
- package/scripts/browser-replay.sh +206 -0
- package/scripts/browser-route.sh +174 -0
- package/scripts/browser-select.sh +122 -0
- package/scripts/browser-show-session.sh +57 -0
- package/scripts/browser-show-site.sh +37 -0
- package/scripts/browser-snapshot.sh +176 -0
- package/scripts/browser-stats.sh +522 -0
- package/scripts/browser-tab-close.sh +112 -0
- package/scripts/browser-tab-list.sh +70 -0
- package/scripts/browser-tab-switch.sh +111 -0
- package/scripts/browser-upload.sh +132 -0
- package/scripts/browser-use.sh +60 -0
- package/scripts/browser-vlm.sh +707 -0
- package/scripts/browser-wait.sh +97 -0
- package/scripts/install-git-hooks.sh +16 -0
- package/scripts/lib/capture.sh +356 -0
- package/scripts/lib/common.sh +262 -0
- package/scripts/lib/credential.sh +237 -0
- package/scripts/lib/fingerprint-rescue.js +123 -0
- package/scripts/lib/flow.sh +448 -0
- package/scripts/lib/flow_record.sh +210 -0
- package/scripts/lib/mask.sh +49 -0
- package/scripts/lib/memory.sh +427 -0
- package/scripts/lib/migrate.sh +390 -0
- package/scripts/lib/migrators/README.md +23 -0
- package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
- package/scripts/lib/migrators/recent_urls/README.md +13 -0
- package/scripts/lib/migrators/stats/README.md +24 -0
- package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
- package/scripts/lib/node/mcp-server.mjs +531 -0
- package/scripts/lib/node/mcp-tools.json +68 -0
- package/scripts/lib/node/playwright-driver.mjs +1104 -0
- package/scripts/lib/node/totp-core.mjs +52 -0
- package/scripts/lib/node/totp.mjs +52 -0
- package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
- package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
- package/scripts/lib/output.sh +79 -0
- package/scripts/lib/router.sh +342 -0
- package/scripts/lib/sanitize.sh +107 -0
- package/scripts/lib/secret/keychain.sh +91 -0
- package/scripts/lib/secret/libsecret.sh +74 -0
- package/scripts/lib/secret/plaintext.sh +75 -0
- package/scripts/lib/secret_backend_select.sh +57 -0
- package/scripts/lib/session.sh +153 -0
- package/scripts/lib/site.sh +126 -0
- package/scripts/lib/stats.sh +419 -0
- package/scripts/lib/tool/.gitkeep +0 -0
- package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
- package/scripts/lib/tool/obscura.sh +249 -0
- package/scripts/lib/tool/playwright-cli.sh +155 -0
- package/scripts/lib/tool/playwright-lib.sh +106 -0
- package/scripts/lib/verb_helpers.sh +222 -0
- package/scripts/lib/visual-rescue-default.sh +145 -0
- package/scripts/regenerate-docs.sh +99 -0
- package/uninstall.sh +51 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/lib/node/mcp-server.mjs
|
|
3
|
+
//
|
|
4
|
+
// Phase 14 (Proposal 2): expose browser-skill verbs as MCP tools (JSON-RPC
|
|
5
|
+
// 2.0 NDJSON over stdio). Protocol version 2024-11-05 — matches the version
|
|
6
|
+
// our chrome-devtools-bridge CLIENT already speaks, so we converge on one
|
|
7
|
+
// wire format across the codebase.
|
|
8
|
+
//
|
|
9
|
+
// Why: lets agent-browser / midscene / Stagehand / browser-use / Claude Code
|
|
10
|
+
// reuse our cache + telemetry + secrets vault without re-implementing them.
|
|
11
|
+
// We become the SHARED MIDDLEWARE other browser agents delegate to.
|
|
12
|
+
//
|
|
13
|
+
// Stage 1 surface: browser_open + browser_snapshot.
|
|
14
|
+
// Stage 2 surface (Phase 14 bundle): browser_click, browser_fill, browser_extract.
|
|
15
|
+
// Each tool spawns the matching scripts/browser-<verb>.sh and returns the
|
|
16
|
+
// verb's single-line summary JSON as MCP `content[0].text`.
|
|
17
|
+
//
|
|
18
|
+
// Env-var passthrough is WHITELISTED (Path 2). The MCP client's full env is
|
|
19
|
+
// NEVER inherited blindly — only well-known skill / VLM / OS / test prefixes
|
|
20
|
+
// flow through. Two reasons:
|
|
21
|
+
// 1. AP-7 alignment — MCP clients may carry their OWN secrets (API keys for
|
|
22
|
+
// OpenAI, Anthropic, etc.); passing those into our bash verbs could land
|
|
23
|
+
// them in stats.jsonl's argv_bytes count or observed-snapshot capture.
|
|
24
|
+
// 2. Determinism — verb behaviour shouldn't depend on whatever stray env
|
|
25
|
+
// the client process happened to have set.
|
|
26
|
+
// See ENV_WHITELIST_PREFIXES + ENV_WHITELIST_EXACT below.
|
|
27
|
+
|
|
28
|
+
import readline from 'node:readline';
|
|
29
|
+
import { spawn, execSync } from 'node:child_process';
|
|
30
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const SCRIPTS_DIR = join(HERE, '..', '..');
|
|
36
|
+
const ADAPTERS_DIR = join(HERE, '..', 'tool');
|
|
37
|
+
// BROWSER_SKILL_MCP_TOOLS_JSON env override lets tests point at a tmp JSON
|
|
38
|
+
// to verify auto-discovery picks up additions/removals.
|
|
39
|
+
const MCP_TOOLS_JSON = process.env.BROWSER_SKILL_MCP_TOOLS_JSON
|
|
40
|
+
|| join(HERE, 'mcp-tools.json');
|
|
41
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
42
|
+
|
|
43
|
+
// --- Auto-discovery (Phase 14+ A1) ---
|
|
44
|
+
//
|
|
45
|
+
// Tool definitions are AUTO-DERIVED at server startup by combining:
|
|
46
|
+
// 1. mcp-tools.json — allowlist + per-verb metadata (description, required,
|
|
47
|
+
// oneOf, schemaExtras, excludeFlags). Adding a verb to MCP = 1 JSON entry.
|
|
48
|
+
// 2. scripts/lib/tool/<adapter>.sh::tool_capabilities() — flag discovery.
|
|
49
|
+
// Run once per adapter at startup; flags union → schema properties.
|
|
50
|
+
//
|
|
51
|
+
// Result: the TOOLS array below is the OLD hand-maintained version (kept as
|
|
52
|
+
// fallback if discovery fails for any reason). buildToolsAutoDiscovered()
|
|
53
|
+
// runs at module load and OVERWRITES TOOLS with the discovered set when it
|
|
54
|
+
// succeeds. Adding a verb to an adapter + an entry in mcp-tools.json now
|
|
55
|
+
// exposes it via MCP without editing this file.
|
|
56
|
+
|
|
57
|
+
function readAdapterCapabilities(adapterFile) {
|
|
58
|
+
// Source the adapter in a subshell + invoke tool_capabilities; parse JSON.
|
|
59
|
+
// The adapter ABI guarantees tool_capabilities() returns valid JSON; if it
|
|
60
|
+
// doesn't, skip the adapter (logged to stderr) so one bad adapter doesn't
|
|
61
|
+
// sink discovery.
|
|
62
|
+
try {
|
|
63
|
+
const out = execSync(
|
|
64
|
+
`bash -c 'source "${adapterFile}" >/dev/null 2>&1 && tool_capabilities'`,
|
|
65
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
|
66
|
+
).toString();
|
|
67
|
+
return JSON.parse(out);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
process.stderr.write(`mcp-server: capability read failed for ${adapterFile}: ${e.message}\n`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function discoverFlagsByVerb() {
|
|
75
|
+
// Returns: { <verb>: { flags: Set<string>, adapters: string[] } }
|
|
76
|
+
const byVerb = {};
|
|
77
|
+
let adapterFiles;
|
|
78
|
+
try {
|
|
79
|
+
adapterFiles = readdirSync(ADAPTERS_DIR).filter((f) => f.endsWith('.sh'));
|
|
80
|
+
} catch {
|
|
81
|
+
return byVerb;
|
|
82
|
+
}
|
|
83
|
+
for (const file of adapterFiles) {
|
|
84
|
+
const adapterName = file.replace(/\.sh$/, '');
|
|
85
|
+
const caps = readAdapterCapabilities(join(ADAPTERS_DIR, file));
|
|
86
|
+
if (!caps || !caps.verbs) continue;
|
|
87
|
+
for (const [verb, def] of Object.entries(caps.verbs)) {
|
|
88
|
+
if (!byVerb[verb]) byVerb[verb] = { flags: new Set(), adapters: [] };
|
|
89
|
+
for (const flag of def.flags || []) {
|
|
90
|
+
byVerb[verb].flags.add(flag.replace(/^--/, ''));
|
|
91
|
+
}
|
|
92
|
+
byVerb[verb].adapters.push(adapterName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return byVerb;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loadToolsJson() {
|
|
99
|
+
try {
|
|
100
|
+
const raw = readFileSync(MCP_TOOLS_JSON, 'utf8');
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
// Strip _doc and _schema sentinel keys; what remains is the verb allowlist.
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
105
|
+
if (k.startsWith('_')) continue;
|
|
106
|
+
out[k] = v;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
process.stderr.write(`mcp-server: mcp-tools.json read failed: ${e.message}\n`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildToolsAutoDiscovered() {
|
|
116
|
+
const verbMeta = loadToolsJson();
|
|
117
|
+
if (!verbMeta) return null;
|
|
118
|
+
const discovered = discoverFlagsByVerb();
|
|
119
|
+
const tools = [];
|
|
120
|
+
|
|
121
|
+
for (const [verb, meta] of Object.entries(verbMeta)) {
|
|
122
|
+
const verbScript = `browser-${verb}.sh`;
|
|
123
|
+
const scriptPath = join(SCRIPTS_DIR, verbScript);
|
|
124
|
+
if (!existsSync(scriptPath)) {
|
|
125
|
+
process.stderr.write(`mcp-server: skipping ${verb} (no ${verbScript})\n`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const discInfo = discovered[verb] || { flags: new Set(), adapters: [] };
|
|
129
|
+
const excludeFlags = new Set(meta.excludeFlags || []);
|
|
130
|
+
const schemaExtras = meta.schemaExtras || {};
|
|
131
|
+
|
|
132
|
+
// Build properties: union of (adapter flags - excluded) + globals (site, tool).
|
|
133
|
+
const properties = {};
|
|
134
|
+
for (const flag of discInfo.flags) {
|
|
135
|
+
if (excludeFlags.has(flag)) continue;
|
|
136
|
+
properties[flag] = schemaExtras[flag] || { type: 'string' };
|
|
137
|
+
}
|
|
138
|
+
// Globals: site (always), tool (always — enum derived from adapters that
|
|
139
|
+
// support this verb).
|
|
140
|
+
properties.site = schemaExtras.site || { type: 'string' };
|
|
141
|
+
if (!('tool' in properties)) {
|
|
142
|
+
properties.tool = {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Adapter override',
|
|
145
|
+
enum: discInfo.adapters.length
|
|
146
|
+
? discInfo.adapters
|
|
147
|
+
: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const inputSchema = {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties,
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
};
|
|
156
|
+
if (meta.required) inputSchema.required = meta.required;
|
|
157
|
+
if (meta.oneOf) inputSchema.oneOf = meta.oneOf;
|
|
158
|
+
|
|
159
|
+
tools.push({
|
|
160
|
+
name: `browser_${verb}`,
|
|
161
|
+
description: meta.description,
|
|
162
|
+
inputSchema,
|
|
163
|
+
verbScript,
|
|
164
|
+
argMap: makeArgMap(meta),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return tools.length > 0 ? tools : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function makeArgMap(meta) {
|
|
172
|
+
const required = meta.required || [];
|
|
173
|
+
// Generic argMap: emit required flags first (so `text` always lands as
|
|
174
|
+
// `--text VAL`, deterministic ordering), then optional flags, then site/tool
|
|
175
|
+
// last for human-readability of stub-log argv.
|
|
176
|
+
return (args) => {
|
|
177
|
+
const out = [];
|
|
178
|
+
// Required first.
|
|
179
|
+
for (const key of required) {
|
|
180
|
+
if (args[key] === undefined || args[key] === null) continue;
|
|
181
|
+
_emitArg(out, key, args[key]);
|
|
182
|
+
}
|
|
183
|
+
// Optional next (anything not in required + not site/tool).
|
|
184
|
+
const skip = new Set([...required, 'site', 'tool']);
|
|
185
|
+
for (const [k, v] of Object.entries(args)) {
|
|
186
|
+
if (skip.has(k)) continue;
|
|
187
|
+
if (v === undefined || v === null) continue;
|
|
188
|
+
_emitArg(out, k, v);
|
|
189
|
+
}
|
|
190
|
+
// Globals last.
|
|
191
|
+
if (args.site) out.push('--site', args.site);
|
|
192
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
193
|
+
return out;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _emitArg(out, key, value) {
|
|
198
|
+
if (typeof value === 'boolean') {
|
|
199
|
+
if (value) out.push(`--${key}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
out.push(`--${key}`, String(value));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Legacy static TOOLS — kept as fallback if auto-discovery fails (e.g.
|
|
206
|
+
// mcp-tools.json missing in a stripped-down install). On boot, we try
|
|
207
|
+
// auto-discovery first; on success, this constant is OVERWRITTEN below.
|
|
208
|
+
let TOOLS = [
|
|
209
|
+
{
|
|
210
|
+
name: 'browser_open',
|
|
211
|
+
description:
|
|
212
|
+
'Open a URL via the routed browser adapter. Returns a summary JSON line ' +
|
|
213
|
+
'with verb, tool, status, url, duration_ms. Auto-derives a post-condition ' +
|
|
214
|
+
'check (url-include) so adapter-lies/redirect-to-login surface as ' +
|
|
215
|
+
'oblivious_success in stats.jsonl.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
url: { type: 'string', description: 'URL to navigate to' },
|
|
220
|
+
site: { type: 'string', description: 'Registered site name (see browser-add-site)' },
|
|
221
|
+
tool: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: 'Adapter override',
|
|
224
|
+
enum: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ['url'],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
},
|
|
230
|
+
verbScript: 'browser-open.sh',
|
|
231
|
+
argMap: (args) => {
|
|
232
|
+
const out = ['--url', args.url];
|
|
233
|
+
if (args.site) out.push('--site', args.site);
|
|
234
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
235
|
+
return out;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'browser_snapshot',
|
|
240
|
+
description:
|
|
241
|
+
'Capture an eN-indexed accessibility snapshot. Heavy YAML (> 2 KB) is ' +
|
|
242
|
+
'persisted under captures/snapshots/ and returned via snapshot_path + ' +
|
|
243
|
+
'n_refs in the summary; small payloads stay inline. With capture=true ' +
|
|
244
|
+
'the full body is also archived under captures/NNN/.',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
site: { type: 'string' },
|
|
249
|
+
tool: { type: 'string', enum: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'] },
|
|
250
|
+
capture: { type: 'boolean', description: 'Persist under captures/NNN/ (Phase 7)' },
|
|
251
|
+
},
|
|
252
|
+
additionalProperties: false,
|
|
253
|
+
},
|
|
254
|
+
verbScript: 'browser-snapshot.sh',
|
|
255
|
+
argMap: (args) => {
|
|
256
|
+
const out = [];
|
|
257
|
+
if (args.site) out.push('--site', args.site);
|
|
258
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
259
|
+
if (args.capture) out.push('--capture');
|
|
260
|
+
return out;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
// Stage 2 — Phase 14 bundle.
|
|
264
|
+
{
|
|
265
|
+
name: 'browser_click',
|
|
266
|
+
description:
|
|
267
|
+
'Click an element by eN ref (preferred — stable across the session ' +
|
|
268
|
+
'until the page mutates) or by CSS selector. Provide exactly one of ' +
|
|
269
|
+
'ref or selector.',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
ref: { type: 'string', description: 'eN ref from a prior browser_snapshot call' },
|
|
274
|
+
selector: { type: 'string', description: 'CSS selector (fallback when ref unavailable)' },
|
|
275
|
+
site: { type: 'string' },
|
|
276
|
+
tool: { type: 'string', enum: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'] },
|
|
277
|
+
},
|
|
278
|
+
oneOf: [{ required: ['ref'] }, { required: ['selector'] }],
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
},
|
|
281
|
+
verbScript: 'browser-click.sh',
|
|
282
|
+
argMap: (args) => {
|
|
283
|
+
const out = [];
|
|
284
|
+
if (args.ref) out.push('--ref', args.ref);
|
|
285
|
+
if (args.selector) out.push('--selector', args.selector);
|
|
286
|
+
if (args.site) out.push('--site', args.site);
|
|
287
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
288
|
+
return out;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'browser_fill',
|
|
293
|
+
description:
|
|
294
|
+
'Fill an input by eN ref or CSS selector with the given text. NOTE: ' +
|
|
295
|
+
'this tool deliberately does NOT expose a "secret" field — MCP has no ' +
|
|
296
|
+
'stdin channel and putting secrets in tool arguments would land them ' +
|
|
297
|
+
'in the request transcript. For secret values use scripts/browser-fill.sh ' +
|
|
298
|
+
'directly with --secret-stdin (AP-7).',
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
ref: { type: 'string' },
|
|
303
|
+
selector: { type: 'string' },
|
|
304
|
+
text: { type: 'string', description: 'Plain-text value to type (NEVER pass secrets here)' },
|
|
305
|
+
site: { type: 'string' },
|
|
306
|
+
tool: { type: 'string', enum: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'] },
|
|
307
|
+
},
|
|
308
|
+
required: ['text'],
|
|
309
|
+
oneOf: [{ required: ['ref'] }, { required: ['selector'] }],
|
|
310
|
+
additionalProperties: false,
|
|
311
|
+
},
|
|
312
|
+
verbScript: 'browser-fill.sh',
|
|
313
|
+
argMap: (args) => {
|
|
314
|
+
const out = [];
|
|
315
|
+
if (args.ref) out.push('--ref', args.ref);
|
|
316
|
+
if (args.selector) out.push('--selector', args.selector);
|
|
317
|
+
out.push('--text', args.text);
|
|
318
|
+
if (args.site) out.push('--site', args.site);
|
|
319
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
320
|
+
return out;
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'browser_extract',
|
|
325
|
+
description:
|
|
326
|
+
'Extract data via CSS selector or evaluated JS. selector returns ' +
|
|
327
|
+
'concatenated text content of matched nodes; eval returns the JS ' +
|
|
328
|
+
'expression result. --scrape multi-URL mode is intentionally NOT ' +
|
|
329
|
+
'exposed via MCP (use scripts/browser-extract.sh --scrape directly).',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
selector: { type: 'string', description: 'CSS selector to extract text from' },
|
|
334
|
+
eval: { type: 'string', description: 'JS expression evaluated in page context' },
|
|
335
|
+
site: { type: 'string' },
|
|
336
|
+
tool: { type: 'string', enum: ['playwright-cli', 'playwright-lib', 'chrome-devtools-mcp', 'obscura'] },
|
|
337
|
+
},
|
|
338
|
+
oneOf: [{ required: ['selector'] }, { required: ['eval'] }],
|
|
339
|
+
additionalProperties: false,
|
|
340
|
+
},
|
|
341
|
+
verbScript: 'browser-extract.sh',
|
|
342
|
+
argMap: (args) => {
|
|
343
|
+
const out = [];
|
|
344
|
+
if (args.selector) out.push('--selector', args.selector);
|
|
345
|
+
if (args.eval) out.push('--eval', args.eval);
|
|
346
|
+
if (args.site) out.push('--site', args.site);
|
|
347
|
+
if (args.tool) out.push('--tool', args.tool);
|
|
348
|
+
return out;
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
// --- Env whitelist (Phase 14 Path 2) ---
|
|
354
|
+
// Only env vars with a whitelisted prefix OR an exact-match name are passed
|
|
355
|
+
// to verb children. Everything else is dropped. This protects skill verbs
|
|
356
|
+
// from being polluted (or having their stats.jsonl polluted) by whatever
|
|
357
|
+
// the MCP client happened to have in its process env.
|
|
358
|
+
const ENV_WHITELIST_PREFIXES = [
|
|
359
|
+
'BROWSER_SKILL_', // skill internals (HOME, TRACE_ID, etc.)
|
|
360
|
+
'BROWSER_STATS_', // stats post-condition / model injection
|
|
361
|
+
'CLAUDE_', // CLAUDE_MODEL, CLAUDE_USAGE_*, CLAUDE_SESSION_ID
|
|
362
|
+
'MIDSCENE_MODEL_', // local-VLM endpoint config (Path 2 motivation)
|
|
363
|
+
'PLAYWRIGHT_', // PLAYWRIGHT_CLI_BIN + test injection
|
|
364
|
+
'CHROME_DEVTOOLS_', // CHROME_DEVTOOLS_MCP_BIN
|
|
365
|
+
'CHROME_USER_DATA_DIR', // session loading for cdt-mcp (Phase 5 part 1f)
|
|
366
|
+
'OBSCURA_', // obscura adapter knobs
|
|
367
|
+
'STUB_', // STUB_LOG_FILE etc — test injection seam
|
|
368
|
+
'FIXTURES_', // CHROME_DEVTOOLS_MCP_FIXTURES_DIR etc — test seam
|
|
369
|
+
'MCP_', // future MCP-specific overrides
|
|
370
|
+
];
|
|
371
|
+
const ENV_WHITELIST_EXACT = new Set([
|
|
372
|
+
// POSIX / shell essentials. Verb scripts assume these exist.
|
|
373
|
+
'PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'TMP', 'TEMP',
|
|
374
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'SHELL', 'TZ', 'PWD',
|
|
375
|
+
// Node + npm essentials so spawned bash can still find tooling.
|
|
376
|
+
'NODE_PATH', 'NPM_CONFIG_PREFIX',
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
function filteredEnv() {
|
|
380
|
+
const out = {};
|
|
381
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
382
|
+
if (ENV_WHITELIST_EXACT.has(key)) {
|
|
383
|
+
out[key] = value;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
for (const prefix of ENV_WHITELIST_PREFIXES) {
|
|
387
|
+
if (key.startsWith(prefix)) {
|
|
388
|
+
out[key] = value;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Auto-discovery replaces the legacy hand-maintained TOOLS array when
|
|
397
|
+
// mcp-tools.json is present + at least one verb resolves. Falls back to
|
|
398
|
+
// the static array on any failure (defensive: a stripped-down install
|
|
399
|
+
// missing mcp-tools.json or with broken adapter capabilities still works).
|
|
400
|
+
const _discovered = buildToolsAutoDiscovered();
|
|
401
|
+
if (_discovered && _discovered.length > 0) {
|
|
402
|
+
TOOLS = _discovered;
|
|
403
|
+
}
|
|
404
|
+
const TOOLS_BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
405
|
+
|
|
406
|
+
// --- Protocol I/O ---
|
|
407
|
+
|
|
408
|
+
function send(msg) {
|
|
409
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function reply(id, result) {
|
|
413
|
+
send({ jsonrpc: '2.0', id, result });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function replyError(id, code, message) {
|
|
417
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// --- Method handlers ---
|
|
421
|
+
|
|
422
|
+
function handleInitialize(id /* , params */) {
|
|
423
|
+
reply(id, {
|
|
424
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
425
|
+
capabilities: { tools: {} },
|
|
426
|
+
serverInfo: {
|
|
427
|
+
name: 'browser-skill',
|
|
428
|
+
version: '0.1.0',
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function handleToolsList(id) {
|
|
434
|
+
reply(id, {
|
|
435
|
+
tools: TOOLS.map((t) => ({
|
|
436
|
+
name: t.name,
|
|
437
|
+
description: t.description,
|
|
438
|
+
inputSchema: t.inputSchema,
|
|
439
|
+
})),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function handleToolsCall(id, params) {
|
|
444
|
+
const name = params?.name;
|
|
445
|
+
const args = params?.arguments ?? {};
|
|
446
|
+
const tool = TOOLS_BY_NAME[name];
|
|
447
|
+
if (!tool) {
|
|
448
|
+
replyError(id, -32602, `Unknown tool: ${name}`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let scriptArgs;
|
|
453
|
+
try {
|
|
454
|
+
scriptArgs = tool.argMap(args);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
replyError(id, -32602, `Invalid arguments: ${e.message}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const scriptPath = join(SCRIPTS_DIR, tool.verbScript);
|
|
461
|
+
const child = spawn('bash', [scriptPath, ...scriptArgs], {
|
|
462
|
+
env: filteredEnv(),
|
|
463
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
let stdout = '';
|
|
467
|
+
let stderr = '';
|
|
468
|
+
child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
|
|
469
|
+
child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
|
|
470
|
+
|
|
471
|
+
child.on('close', (code) => {
|
|
472
|
+
const lines = stdout.replace(/\n+$/, '').split('\n');
|
|
473
|
+
const lastLine = lines[lines.length - 1] ?? '';
|
|
474
|
+
let summary;
|
|
475
|
+
try {
|
|
476
|
+
summary = JSON.parse(lastLine);
|
|
477
|
+
} catch {
|
|
478
|
+
summary = {
|
|
479
|
+
status: code === 0 ? 'ok' : 'error',
|
|
480
|
+
stdout: stdout.slice(0, 2048),
|
|
481
|
+
stderr: stderr.slice(0, 2048),
|
|
482
|
+
exitCode: code,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
reply(id, {
|
|
486
|
+
content: [{ type: 'text', text: JSON.stringify(summary) }],
|
|
487
|
+
isError: code !== 0 && summary.status !== 'ok',
|
|
488
|
+
_meta: {
|
|
489
|
+
exitCode: code,
|
|
490
|
+
stderr: stderr.length > 0 ? stderr.slice(0, 2048) : undefined,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
child.on('error', (e) => {
|
|
496
|
+
replyError(id, -32603, `Failed to spawn ${tool.verbScript}: ${e.message}`);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function handleMessage(msg) {
|
|
501
|
+
switch (msg.method) {
|
|
502
|
+
case 'initialize': return handleInitialize(msg.id, msg.params);
|
|
503
|
+
case 'notifications/initialized': return; // notification — no reply
|
|
504
|
+
case 'tools/list': return handleToolsList(msg.id);
|
|
505
|
+
case 'tools/call': return handleToolsCall(msg.id, msg.params);
|
|
506
|
+
case 'ping': return reply(msg.id, {});
|
|
507
|
+
default:
|
|
508
|
+
if (msg.id !== undefined) {
|
|
509
|
+
replyError(msg.id, -32601, `Method not found: ${msg.method}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
515
|
+
rl.on('line', (line) => {
|
|
516
|
+
if (!line.trim()) return;
|
|
517
|
+
let msg;
|
|
518
|
+
try {
|
|
519
|
+
msg = JSON.parse(line);
|
|
520
|
+
} catch (e) {
|
|
521
|
+
process.stderr.write(`mcp-server: bad JSON line: ${e.message}\n`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
handleMessage(msg);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
if (msg && msg.id !== undefined) {
|
|
528
|
+
replyError(msg.id, -32603, `Internal error: ${e.message}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "MCP tool allowlist + metadata. The mcp-server.mjs auto-discovers FLAGS from each adapter's tool_capabilities() at startup, but only exposes verbs explicitly declared here. Adding a new verb to MCP = 1 entry here + ensure adapter capabilities declare it; NO mcp-server.mjs edits.",
|
|
3
|
+
"_schema": {
|
|
4
|
+
"<verb>": {
|
|
5
|
+
"description": "1-3 sentence human-readable explanation (what MCP clients see in tools/list)",
|
|
6
|
+
"required": "optional array of required input property names (e.g. ['url'])",
|
|
7
|
+
"oneOf": "optional array of {required: [...]} objects for mutually-exclusive arg groups",
|
|
8
|
+
"schemaExtras": "optional per-property schema overrides (descriptions, enums, formats); merged onto auto-derived {type:'string'}",
|
|
9
|
+
"globalProps": "optional list of global props to add beyond auto-derived flags (defaults: ['site','tool']); set to ['site'] to skip the tool override prop",
|
|
10
|
+
"excludeFlags": "optional list of bash-verb flag NAMES (without --) to NOT expose via MCP (AP-7: secrets, --raw, --dry-run, --secret-stdin)",
|
|
11
|
+
"scriptArgFlags": "optional explicit ordering hint for argMap output"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"open": {
|
|
16
|
+
"description": "Open a URL via the routed browser adapter. Returns a summary JSON line with verb, tool, status, url, duration_ms. Auto-derives a post-condition check (url-include) so adapter-lies/redirect-to-login surface as oblivious_success in stats.jsonl.",
|
|
17
|
+
"required": ["url"],
|
|
18
|
+
"schemaExtras": {
|
|
19
|
+
"url": { "type": "string", "description": "URL to navigate to" },
|
|
20
|
+
"site": { "type": "string", "description": "Registered site name (see browser-add-site)" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"snapshot": {
|
|
25
|
+
"description": "Capture an eN-indexed accessibility snapshot. Heavy YAML (> 2 KB) is persisted under captures/snapshots/ and returned via snapshot_path + n_refs in the summary; small payloads stay inline. With capture=true the full body is also archived under captures/NNN/.",
|
|
26
|
+
"schemaExtras": {
|
|
27
|
+
"capture": { "type": "boolean", "description": "Persist under captures/NNN/ (Phase 7)" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"click": {
|
|
32
|
+
"description": "Click an element by eN ref (preferred — stable across the session until the page mutates) or by CSS selector. Provide exactly one of ref or selector.",
|
|
33
|
+
"oneOf": [
|
|
34
|
+
{ "required": ["ref"] },
|
|
35
|
+
{ "required": ["selector"] }
|
|
36
|
+
],
|
|
37
|
+
"schemaExtras": {
|
|
38
|
+
"ref": { "type": "string", "description": "eN ref from a prior browser_snapshot call" },
|
|
39
|
+
"selector": { "type": "string", "description": "CSS selector (fallback when ref unavailable)" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"fill": {
|
|
44
|
+
"description": "Fill an input by eN ref or CSS selector with the given text. NOTE: this tool deliberately does NOT expose a 'secret' field — MCP has no stdin channel and putting secrets in tool arguments would land them in the request transcript. For secret values use scripts/browser-fill.sh directly with --secret-stdin (AP-7).",
|
|
45
|
+
"required": ["text"],
|
|
46
|
+
"oneOf": [
|
|
47
|
+
{ "required": ["ref"] },
|
|
48
|
+
{ "required": ["selector"] }
|
|
49
|
+
],
|
|
50
|
+
"excludeFlags": ["secret-stdin"],
|
|
51
|
+
"schemaExtras": {
|
|
52
|
+
"text": { "type": "string", "description": "Plain-text value to type (NEVER pass secrets here)" }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"extract": {
|
|
57
|
+
"description": "Extract data via CSS selector or evaluated JS. selector returns concatenated text content of matched nodes; eval returns the JS expression result. --scrape multi-URL mode is intentionally NOT exposed via MCP (use scripts/browser-extract.sh --scrape directly).",
|
|
58
|
+
"oneOf": [
|
|
59
|
+
{ "required": ["selector"] },
|
|
60
|
+
{ "required": ["eval"] }
|
|
61
|
+
],
|
|
62
|
+
"excludeFlags": ["scrape", "stealth", "concurrency"],
|
|
63
|
+
"schemaExtras": {
|
|
64
|
+
"selector": { "type": "string", "description": "CSS selector to extract text from" },
|
|
65
|
+
"eval": { "type": "string", "description": "JS expression evaluated in page context" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|