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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/SECURITY.md +39 -0
  4. package/SKILL.md +206 -0
  5. package/bin/cli.mjs +55 -0
  6. package/install.sh +143 -0
  7. package/package.json +54 -0
  8. package/references/adapter-candidates.md +40 -0
  9. package/references/browser-mcp-cheatsheet.md +132 -0
  10. package/references/browser-stats-cheatsheet.md +155 -0
  11. package/references/chrome-devtools-mcp-cheatsheet.md +232 -0
  12. package/references/midscene-integration.md +359 -0
  13. package/references/obscura-cheatsheet.md +103 -0
  14. package/references/playwright-cli-cheatsheet.md +64 -0
  15. package/references/playwright-lib-cheatsheet.md +90 -0
  16. package/references/recipes/add-a-tool-adapter.md +134 -0
  17. package/references/recipes/agent-workflows/README.md +37 -0
  18. package/references/recipes/agent-workflows/cache-driven-bulk-operation.md +110 -0
  19. package/references/recipes/agent-workflows/flow-record-and-replay.md +102 -0
  20. package/references/recipes/agent-workflows/incremental-pattern-discovery.md +125 -0
  21. package/references/recipes/agent-workflows/login-then-scrape.md +100 -0
  22. package/references/recipes/anti-patterns-tool-extension.md +182 -0
  23. package/references/recipes/body-bytes-not-body.md +139 -0
  24. package/references/recipes/cache-write-security.md +210 -0
  25. package/references/recipes/fingerprint-rescue.md +154 -0
  26. package/references/recipes/model-routing.md +143 -0
  27. package/references/recipes/path-security.md +138 -0
  28. package/references/recipes/privacy-canary.md +96 -0
  29. package/references/recipes/visual-rescue-hook.md +182 -0
  30. package/references/stats-prices.json +42 -0
  31. package/references/stats-schema.json +77 -0
  32. package/references/tool-versions.md +8 -0
  33. package/scripts/browser-add-site.sh +113 -0
  34. package/scripts/browser-assert.sh +106 -0
  35. package/scripts/browser-audit.sh +68 -0
  36. package/scripts/browser-baseline.sh +135 -0
  37. package/scripts/browser-click.sh +100 -0
  38. package/scripts/browser-creds-add.sh +254 -0
  39. package/scripts/browser-creds-list.sh +67 -0
  40. package/scripts/browser-creds-migrate.sh +122 -0
  41. package/scripts/browser-creds-remove.sh +69 -0
  42. package/scripts/browser-creds-rotate-totp.sh +109 -0
  43. package/scripts/browser-creds-show.sh +82 -0
  44. package/scripts/browser-creds-totp.sh +94 -0
  45. package/scripts/browser-do.sh +630 -0
  46. package/scripts/browser-doctor.sh +365 -0
  47. package/scripts/browser-drag.sh +90 -0
  48. package/scripts/browser-extract.sh +192 -0
  49. package/scripts/browser-fill.sh +142 -0
  50. package/scripts/browser-flow.sh +316 -0
  51. package/scripts/browser-history.sh +187 -0
  52. package/scripts/browser-hover.sh +92 -0
  53. package/scripts/browser-inspect.sh +188 -0
  54. package/scripts/browser-list-sessions.sh +78 -0
  55. package/scripts/browser-list-sites.sh +42 -0
  56. package/scripts/browser-login.sh +279 -0
  57. package/scripts/browser-mcp.sh +65 -0
  58. package/scripts/browser-migrate.sh +195 -0
  59. package/scripts/browser-open.sh +134 -0
  60. package/scripts/browser-press.sh +80 -0
  61. package/scripts/browser-remove-session.sh +72 -0
  62. package/scripts/browser-remove-site.sh +68 -0
  63. package/scripts/browser-replay.sh +206 -0
  64. package/scripts/browser-route.sh +174 -0
  65. package/scripts/browser-select.sh +122 -0
  66. package/scripts/browser-show-session.sh +57 -0
  67. package/scripts/browser-show-site.sh +37 -0
  68. package/scripts/browser-snapshot.sh +176 -0
  69. package/scripts/browser-stats.sh +522 -0
  70. package/scripts/browser-tab-close.sh +112 -0
  71. package/scripts/browser-tab-list.sh +70 -0
  72. package/scripts/browser-tab-switch.sh +111 -0
  73. package/scripts/browser-upload.sh +132 -0
  74. package/scripts/browser-use.sh +60 -0
  75. package/scripts/browser-vlm.sh +707 -0
  76. package/scripts/browser-wait.sh +97 -0
  77. package/scripts/install-git-hooks.sh +16 -0
  78. package/scripts/lib/capture.sh +356 -0
  79. package/scripts/lib/common.sh +262 -0
  80. package/scripts/lib/credential.sh +237 -0
  81. package/scripts/lib/fingerprint-rescue.js +123 -0
  82. package/scripts/lib/flow.sh +448 -0
  83. package/scripts/lib/flow_record.sh +210 -0
  84. package/scripts/lib/mask.sh +49 -0
  85. package/scripts/lib/memory.sh +427 -0
  86. package/scripts/lib/migrate.sh +390 -0
  87. package/scripts/lib/migrators/README.md +23 -0
  88. package/scripts/lib/migrators/memory/v1_to_v2.sh +15 -0
  89. package/scripts/lib/migrators/recent_urls/README.md +13 -0
  90. package/scripts/lib/migrators/stats/README.md +24 -0
  91. package/scripts/lib/node/chrome-devtools-bridge.mjs +1812 -0
  92. package/scripts/lib/node/mcp-server.mjs +531 -0
  93. package/scripts/lib/node/mcp-tools.json +68 -0
  94. package/scripts/lib/node/playwright-driver.mjs +1104 -0
  95. package/scripts/lib/node/totp-core.mjs +52 -0
  96. package/scripts/lib/node/totp.mjs +52 -0
  97. package/scripts/lib/node/url-pattern-cluster.mjs +102 -0
  98. package/scripts/lib/node/url-pattern-resolver.mjs +77 -0
  99. package/scripts/lib/output.sh +79 -0
  100. package/scripts/lib/router.sh +342 -0
  101. package/scripts/lib/sanitize.sh +107 -0
  102. package/scripts/lib/secret/keychain.sh +91 -0
  103. package/scripts/lib/secret/libsecret.sh +74 -0
  104. package/scripts/lib/secret/plaintext.sh +75 -0
  105. package/scripts/lib/secret_backend_select.sh +57 -0
  106. package/scripts/lib/session.sh +153 -0
  107. package/scripts/lib/site.sh +126 -0
  108. package/scripts/lib/stats.sh +419 -0
  109. package/scripts/lib/tool/.gitkeep +0 -0
  110. package/scripts/lib/tool/chrome-devtools-mcp.sh +349 -0
  111. package/scripts/lib/tool/obscura.sh +249 -0
  112. package/scripts/lib/tool/playwright-cli.sh +155 -0
  113. package/scripts/lib/tool/playwright-lib.sh +106 -0
  114. package/scripts/lib/verb_helpers.sh +222 -0
  115. package/scripts/lib/visual-rescue-default.sh +145 -0
  116. package/scripts/regenerate-docs.sh +99 -0
  117. 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
+ }