machine-bridge-mcp 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ const NAVIGATION_ACTIONS = new Set(["navigate", "reload", "back", "forward"]);
2
+ const PAGE_ACTIONS = new Set([
3
+ "click", "double_click", "hover", "fill", "type_text", "select", "check", "uncheck",
4
+ "focus", "press", "submit", "scroll_into_view",
5
+ ]);
6
+ const FORM_ACTIONS = new Set(["fill", "select", "check", "uncheck", "click"]);
7
+ const WAIT_STATES = new Set(["attached", "detached", "visible", "hidden", "enabled", "editable", "checked", "unchecked"]);
8
+ const LOAD_STATES = new Set(["domcontentloaded", "complete"]);
9
+ const INPUT_MODES = new Set(["auto", "trusted", "dom"]);
10
+
11
+ export function normalizeBrowserAction(value) {
12
+ const action = String(value || "").trim();
13
+ if (!NAVIGATION_ACTIONS.has(action) && !PAGE_ACTIONS.has(action)) throw new Error("unsupported browser action");
14
+ return action;
15
+ }
16
+
17
+ export function normalizeFormAction(value) {
18
+ const action = String(value || "").trim();
19
+ if (!FORM_ACTIONS.has(action)) throw new Error("unsupported form field action");
20
+ return action;
21
+ }
22
+
23
+ export function normalizeBrowserSelector(value, action = "") {
24
+ if (NAVIGATION_ACTIONS.has(action)) return null;
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("selector must be an object");
26
+ const allowed = new Set(["ref", "css", "id", "name", "label", "text", "role", "placeholder", "index"]);
27
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`unknown selector field: ${key}`);
28
+ const output = {};
29
+ if (value.ref !== undefined) {
30
+ const ref = optionalString(value.ref, "selector.ref", 100);
31
+ if (ref) output.ref = ref;
32
+ }
33
+ for (const key of ["css", "id", "name", "label", "text", "role", "placeholder"]) {
34
+ if (value[key] === undefined) continue;
35
+ const normalized = optionalString(value[key], `selector.${key}`, 2000);
36
+ if (normalized) output[key] = normalized;
37
+ }
38
+ if (value.index !== undefined) output.index = optionalInteger(value.index, "selector.index", 0, 10000);
39
+ if (!Object.keys(output).length) throw new Error("selector requires at least one field");
40
+ if (output.ref && Object.keys(output).length !== 1) throw new Error("selector.ref cannot be combined with other selector fields");
41
+ return output;
42
+ }
43
+
44
+ export function normalizeInputMode(value) {
45
+ if (value === undefined || value === null || value === "") return "auto";
46
+ const mode = String(value);
47
+ if (!INPUT_MODES.has(mode)) throw new Error("input_mode must be auto, trusted, or dom");
48
+ return mode;
49
+ }
50
+
51
+ export function normalizeNavigationWait(value) {
52
+ if (value === undefined || value === null || value === "") return "none";
53
+ const wait = String(value);
54
+ if (!["none", ...LOAD_STATES].includes(wait)) throw new Error("wait_for must be none, domcontentloaded, or complete");
55
+ return wait;
56
+ }
57
+
58
+ export function normalizeBrowserWait(args = {}) {
59
+ const selector = args.selector === undefined ? null : normalizeBrowserSelector(args.selector, "wait");
60
+ const state = args.state === undefined || args.state === null || args.state === ""
61
+ ? (selector ? "visible" : "")
62
+ : String(args.state);
63
+ if (state && !WAIT_STATES.has(state)) throw new Error("state is not a supported browser wait state");
64
+ if (state && !selector) throw new Error("state requires selector");
65
+ const text = optionalString(args.text, "text", 4000);
66
+ const urlContains = optionalString(args.url_contains, "url_contains", 32768);
67
+ const loadState = args.load_state === undefined || args.load_state === null || args.load_state === "" ? "" : String(args.load_state);
68
+ if (loadState && !LOAD_STATES.has(loadState)) throw new Error("load_state must be domcontentloaded or complete");
69
+ if (!selector && !text && !urlContains && !loadState) throw new Error("browser_wait requires selector, text, url_contains, or load_state");
70
+ const timeoutSeconds = clampInt(args.timeout_seconds, 30, 1, 120);
71
+ return {
72
+ tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
73
+ frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
74
+ selector,
75
+ state,
76
+ text,
77
+ urlContains,
78
+ loadState,
79
+ timeoutMs: timeoutSeconds * 1000,
80
+ };
81
+ }
82
+
83
+ export function normalizeTabCommand(args = {}) {
84
+ const action = String(args.action || "").trim();
85
+ if (!["new", "activate", "close"].includes(action)) throw new Error("browser tab action must be new, activate, or close");
86
+ const tabId = optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER);
87
+ const rawUrl = optionalString(args.url, "url", 32768);
88
+ if (["activate", "close"].includes(action) && !tabId) throw new Error(`${action} requires tab_id`);
89
+ if (action !== "new" && rawUrl) throw new Error("url is only valid for new tabs");
90
+ return {
91
+ action,
92
+ tabId,
93
+ url: rawUrl ? validateNavigationUrl(rawUrl) : "",
94
+ active: args.active !== false,
95
+ };
96
+ }
97
+
98
+ export function validateNavigationUrl(value) {
99
+ if (!value) throw new Error("navigate requires url");
100
+ let parsed;
101
+ try { parsed = new URL(value); } catch { throw new Error("url must be an absolute URL"); }
102
+ if (!["http:", "https:", "file:"].includes(parsed.protocol)) throw new Error("url protocol must be http, https, or file");
103
+ return parsed.href;
104
+ }
105
+
106
+ export function optionalString(value, label, maxLength) {
107
+ if (value === undefined || value === null || value === "") return "";
108
+ if (typeof value !== "string" || value.includes("\0") || value.length > maxLength) {
109
+ throw new Error(`${label} must be a string of at most ${maxLength} characters without NUL bytes`);
110
+ }
111
+ return value;
112
+ }
113
+
114
+ export function optionalInteger(value, label, min, max) {
115
+ if (value === undefined || value === null || value === "") return null;
116
+ const number = Number(value);
117
+ if (!Number.isInteger(number) || number < min || number > max) throw new Error(`${label} must be an integer from ${min} to ${max}`);
118
+ return number;
119
+ }
120
+
121
+ export function clampInt(value, fallback, min, max) {
122
+ if (value === undefined || value === null || value === "") return fallback;
123
+ const number = Number(value);
124
+ if (!Number.isInteger(number) || number < min || number > max) throw new Error(`expected an integer from ${min} to ${max}`);
125
+ return number;
126
+ }
package/src/local/cli.mjs CHANGED
@@ -10,7 +10,7 @@ import { runStdioServer } from "./stdio.mjs";
10
10
  import { assertCanonicalFullPolicy, DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile, toolsForPolicy } from "./tools.mjs";
11
11
  import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
12
12
  import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobManager, publicResourceRegistry, validateResourceName } from "./managed-jobs.mjs";
13
- import { runWrangler } from "./shell.mjs";
13
+ import { run, runWrangler } from "./shell.mjs";
14
14
  import { generateRegisteredSshKey } from "./resource-operations.mjs";
15
15
  import { runFullAccessTest } from "./full-access-test.mjs";
16
16
  import { readBoundedRegularFileSync } from "./secure-file.mjs";
@@ -753,13 +753,26 @@ async function browserCommand(args) {
753
753
  connected: health?.broker === "machine-bridge-browser" && health?.connected === true,
754
754
  extension_path: extensionPath,
755
755
  pairing_url: pairingUrl,
756
+ expected_extension_version: typeof health?.expected_extension_version === "string" ? health.expected_extension_version : "",
757
+ extension_protocol: Number.isInteger(health?.extension_protocol) ? health.extension_protocol : null,
758
+ extension_version: typeof health?.extension_version === "string" ? health.extension_version : "",
759
+ extension_capabilities: Array.isArray(health?.extension_capabilities) ? health.extension_capabilities : [],
760
+ extension_reload_required: health?.extension_reload_required === true,
761
+ controls_existing_profile: health?.controls_existing_profile === true,
762
+ controls_extension_profile: health?.controls_extension_profile === true,
763
+ machine_bridge_launches_browser: health?.machine_bridge_launches_browser === true,
764
+ profile_identity_verifiable: health?.profile_identity_verifiable === true,
756
765
  token_exposed: false,
757
766
  };
758
767
  if (action === "status") {
759
768
  if (args.json) console.log(JSON.stringify(result, null, 2));
760
769
  else {
761
770
  console.log(`Browser bridge: ${result.running ? "running" : "not reachable"}`);
762
- console.log(`Extension: ${result.connected ? "connected" : "not connected"}`);
771
+ console.log(`Extension: ${result.connected ? "connected" : result.extension_reload_required ? "reload required" : "not connected"}`);
772
+ if (result.expected_extension_version) console.log(`Expected extension build: ${result.expected_extension_version}`);
773
+ if (result.extension_version || result.extension_protocol) console.log(`Connected extension build: ${result.extension_version || "unknown"} (protocol ${result.extension_protocol ?? "unknown"})`);
774
+ console.log(`Browser profile: ${result.controls_extension_profile ? "the Chromium profile where this extension is installed" : "unknown"}`);
775
+ if (result.controls_extension_profile) console.log(`Profile provenance: Machine Bridge did not launch the browser; daily-vs-isolated profile identity is not machine-verifiable.`);
763
776
  console.log(`Extension path: ${extensionPath}`);
764
777
  }
765
778
  return;
@@ -769,7 +782,8 @@ async function browserCommand(args) {
769
782
  if (args.json) console.log(JSON.stringify({ ...result, pairing_page_opened: true }, null, 2));
770
783
  else {
771
784
  console.log(`Extension path: ${extensionPath}`);
772
- console.log("Load this directory once from the Chromium extensions page with Developer mode enabled.");
785
+ console.log("Load this directory in the Chromium profile you use every day; Machine Bridge does not install it into Playwright or a separate automation profile.");
786
+ console.log("Enable Developer mode, choose Load unpacked, and reload the extension after each Machine Bridge upgrade.");
773
787
  console.log(`Pairing page opened: ${pairingUrl}`);
774
788
  }
775
789
  }
@@ -1124,6 +1138,10 @@ async function doctorCommand(args) {
1124
1138
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
1125
1139
  const checks = [];
1126
1140
  checks.push({ name: "node", ok: isSupportedNodeVersion(), detail: process.version });
1141
+ const npmCommand = npmVersionCommand();
1142
+ const npm = await run(npmCommand.file, npmCommand.args, { capture: true, allowFailure: true, timeoutMs: 10_000 });
1143
+ const npmDetail = sanitizeLines(npm.stdout || npm.stderr);
1144
+ checks.push({ name: "npm", ok: npm.code === 0 && isSupportedNpmVersion(npmDetail), detail: npmDetail || "unavailable" });
1127
1145
  const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true });
1128
1146
  checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
1129
1147
  const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
@@ -1557,10 +1575,22 @@ function validateWorkerName(value) {
1557
1575
  }
1558
1576
 
1559
1577
  export function isSupportedNodeVersion(version = process.versions.node) {
1560
- const major = Number(String(version || "").split(".")[0]);
1578
+ const major = Number(String(version || "").replace(/^v/, "").split(".")[0]);
1561
1579
  return Number.isInteger(major) && major >= 26;
1562
1580
  }
1563
1581
 
1582
+ export function isSupportedNpmVersion(version) {
1583
+ const major = Number(String(version || "").trim().replace(/^v/, "").split(".")[0]);
1584
+ return Number.isInteger(major) && major >= 12;
1585
+ }
1586
+
1587
+ export function npmVersionCommand(platform = process.platform, comspec = process.env.ComSpec) {
1588
+ if (platform === "win32") {
1589
+ return { file: comspec || "cmd.exe", args: ["/d", "/s", "/c", "npm --version"] };
1590
+ }
1591
+ return { file: "npm", args: ["--version"] };
1592
+ }
1593
+
1564
1594
  function assertNodeVersion() {
1565
1595
  if (!isSupportedNodeVersion()) throw new Error(`Node.js >=26 is required; current ${process.version}`);
1566
1596
  }
@@ -1568,8 +1598,10 @@ function assertNodeVersion() {
1568
1598
  function usage() {
1569
1599
  console.log(`machine-bridge-mcp
1570
1600
 
1601
+ Installation (run from a package-free temporary directory; Node.js >=26):
1602
+ npx --yes npm@12.0.1 install --global --omit=optional --allow-scripts=esbuild,workerd,sharp,fsevents machine-bridge-mcp@latest && machine-mcp
1603
+
1571
1604
  Usage:
1572
- npm install -g --omit=optional --allow-scripts=esbuild,workerd,sharp,fsevents machine-bridge-mcp@latest && machine-mcp
1573
1605
  npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
1574
1606
  ./mbm # from source checkout
1575
1607
  .\\mbm.cmd # from source checkout on Windows cmd
@@ -46,6 +46,8 @@ const RUNTIME_TOOL_HANDLERS = Object.freeze({
46
46
  browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
47
47
  pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
48
48
  browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
49
+ browser_manage_tabs: (runtime, args, context) => runtime.browserBridgeManager.manageTabs(args, context),
50
+ browser_wait: (runtime, args, context) => runtime.browserBridgeManager.wait(args, context),
49
51
  browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
50
52
  browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
51
53
  browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
@@ -465,11 +465,15 @@ async function statusWindowsTask() {
465
465
  }
466
466
 
467
467
  function windowsCommand(spec) {
468
- return [spec.node, ...daemonArgs(spec)].map(winQuote).join(" ");
468
+ return [spec.node, ...daemonArgs(spec)].map(windowsCommandLineArgument).join(" ");
469
469
  }
470
470
 
471
- function winQuote(value) {
472
- return `"${String(value).replaceAll('"', '\\"')}"`;
471
+ export function windowsCommandLineArgument(value) {
472
+ const text = String(value);
473
+ if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
474
+ const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
475
+ const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, (slashes) => `${slashes}${slashes}`);
476
+ return `"${escapedTrailingSlashes}"`;
473
477
  }
474
478
 
475
479
  export function systemdQuote(value) {
@@ -318,7 +318,7 @@
318
318
  {
319
319
  "name": "browser_status",
320
320
  "title": "Browser bridge status",
321
- "description": "Start or inspect the loopback browser-extension bridge used to automate the user's existing Chromium profile, windows, tabs, and login state.",
321
+ "description": "Start or inspect the loopback bridge for the Chromium profile where the extension is installed. Machine Bridge does not launch Playwright or another browser process, but cannot infer whether that profile is the user's daily or isolated profile; status reports authenticated extension version, protocol, capabilities, and reload state.",
322
322
  "availability": "full",
323
323
  "annotations": {
324
324
  "readOnlyHint": false,
@@ -334,7 +334,7 @@
334
334
  {
335
335
  "name": "pair_browser_extension",
336
336
  "title": "Pair browser extension",
337
- "description": "Open the local pairing page and return the packaged unpacked-extension path for one-time browser installation and pairing.",
337
+ "description": "Open the local pairing page and return the packaged unpacked-extension path for installation in the user's intended Chromium profile. Pairing is persisted only after an acknowledged version/capability handshake.",
338
338
  "availability": "full",
339
339
  "annotations": {
340
340
  "readOnlyHint": false,
@@ -385,10 +385,59 @@
385
385
  }
386
386
  }
387
387
  },
388
+ {
389
+ "name": "browser_manage_tabs",
390
+ "title": "Manage browser tabs",
391
+ "description": "Create, activate, or close tabs in the paired existing browser profile.",
392
+ "availability": "full",
393
+ "annotations": {
394
+ "readOnlyHint": false,
395
+ "destructiveHint": true,
396
+ "idempotentHint": false,
397
+ "openWorldHint": true
398
+ },
399
+ "inputSchema": {
400
+ "type": "object",
401
+ "additionalProperties": false,
402
+ "properties": {
403
+ "action": {
404
+ "type": "string",
405
+ "enum": [
406
+ "new",
407
+ "activate",
408
+ "close"
409
+ ]
410
+ },
411
+ "tab_id": {
412
+ "type": "integer",
413
+ "minimum": 1,
414
+ "description": "Required for activate and close."
415
+ },
416
+ "url": {
417
+ "type": "string",
418
+ "maxLength": 32768,
419
+ "description": "Optional absolute http, https, or file URL for a new tab."
420
+ },
421
+ "active": {
422
+ "type": "boolean",
423
+ "default": true
424
+ },
425
+ "timeout_seconds": {
426
+ "type": "integer",
427
+ "minimum": 1,
428
+ "maximum": 120,
429
+ "default": 30
430
+ }
431
+ },
432
+ "required": [
433
+ "action"
434
+ ]
435
+ }
436
+ },
388
437
  {
389
438
  "name": "browser_get_source",
390
439
  "title": "Read browser page source",
391
- "description": "Read bounded serialized current DOM HTML from the active or selected browser tab and optionally from all accessible frames.",
440
+ "description": "Read bounded serialized current DOM HTML from the active or selected browser tab. max_bytes is one aggregate budget across at most 64 accessible frames, with explicit frame, node, and byte truncation metadata.",
392
441
  "availability": "full",
393
442
  "annotations": {
394
443
  "readOnlyHint": true,
@@ -430,7 +479,7 @@
430
479
  {
431
480
  "name": "browser_inspect_page",
432
481
  "title": "Inspect browser page",
433
- "description": "Inspect bounded interactive DOM metadata across accessible frames for links, controls, form fields, accessible names, labels, and roles.",
482
+ "description": "Inspect a bounded snapshot-version-2 semantic representation across at most 64 accessible frames, with one aggregate element budget, bounded reusable refs, actionability state, bounded page-controlled metadata, and explicit scan/frame truncation.",
434
483
  "availability": "full",
435
484
  "annotations": {
436
485
  "readOnlyHint": true,
@@ -473,10 +522,115 @@
473
522
  }
474
523
  }
475
524
  },
525
+ {
526
+ "name": "browser_wait",
527
+ "title": "Wait for browser state",
528
+ "description": "Wait until all supplied URL, load, text, and element-state conditions are satisfied in an existing browser tab.",
529
+ "availability": "full",
530
+ "annotations": {
531
+ "readOnlyHint": true,
532
+ "destructiveHint": false,
533
+ "idempotentHint": true,
534
+ "openWorldHint": true
535
+ },
536
+ "inputSchema": {
537
+ "type": "object",
538
+ "additionalProperties": false,
539
+ "properties": {
540
+ "tab_id": {
541
+ "type": "integer",
542
+ "minimum": 1
543
+ },
544
+ "frame_id": {
545
+ "type": "integer",
546
+ "minimum": 0
547
+ },
548
+ "selector": {
549
+ "type": "object",
550
+ "properties": {
551
+ "ref": {
552
+ "type": "string",
553
+ "maxLength": 100,
554
+ "description": "Stable element reference returned by browser_inspect_page for the same document and frame."
555
+ },
556
+ "css": {
557
+ "type": "string",
558
+ "maxLength": 2000
559
+ },
560
+ "id": {
561
+ "type": "string",
562
+ "maxLength": 2000
563
+ },
564
+ "name": {
565
+ "type": "string",
566
+ "maxLength": 2000
567
+ },
568
+ "label": {
569
+ "type": "string",
570
+ "maxLength": 2000
571
+ },
572
+ "text": {
573
+ "type": "string",
574
+ "maxLength": 2000
575
+ },
576
+ "role": {
577
+ "type": "string",
578
+ "maxLength": 2000
579
+ },
580
+ "placeholder": {
581
+ "type": "string",
582
+ "maxLength": 2000
583
+ },
584
+ "index": {
585
+ "type": "integer",
586
+ "minimum": 0,
587
+ "maximum": 10000
588
+ }
589
+ },
590
+ "additionalProperties": false
591
+ },
592
+ "state": {
593
+ "type": "string",
594
+ "enum": [
595
+ "attached",
596
+ "detached",
597
+ "visible",
598
+ "hidden",
599
+ "enabled",
600
+ "editable",
601
+ "checked",
602
+ "unchecked"
603
+ ],
604
+ "description": "Element state to wait for. Defaults to visible only when selector is supplied."
605
+ },
606
+ "text": {
607
+ "type": "string",
608
+ "maxLength": 4000
609
+ },
610
+ "url_contains": {
611
+ "type": "string",
612
+ "maxLength": 32768
613
+ },
614
+ "load_state": {
615
+ "type": "string",
616
+ "enum": [
617
+ "domcontentloaded",
618
+ "complete"
619
+ ]
620
+ },
621
+ "timeout_seconds": {
622
+ "type": "integer",
623
+ "minimum": 1,
624
+ "maximum": 120,
625
+ "default": 30
626
+ }
627
+ }
628
+ }
629
+ },
476
630
  {
477
631
  "name": "browser_action",
478
632
  "title": "Operate browser page",
479
- "description": "Perform one structured navigation or DOM action in the user's existing browser tab without accepting arbitrary JavaScript.",
633
+ "description": "Perform one structured navigation or page action in the user's existing browser tab without arbitrary JavaScript. Automatic trusted-input fallback occurs only before any DevTools Input command starts; ambiguous post-dispatch failures require inspection before retry.",
480
634
  "availability": "full",
481
635
  "annotations": {
482
636
  "readOnlyHint": false,
@@ -506,12 +660,21 @@
506
660
  "submit",
507
661
  "reload",
508
662
  "back",
509
- "forward"
663
+ "forward",
664
+ "double_click",
665
+ "hover",
666
+ "type_text",
667
+ "scroll_into_view"
510
668
  ]
511
669
  },
512
670
  "selector": {
513
671
  "type": "object",
514
672
  "properties": {
673
+ "ref": {
674
+ "type": "string",
675
+ "maxLength": 100,
676
+ "description": "Stable element reference returned by browser_inspect_page for the same document and frame."
677
+ },
515
678
  "css": {
516
679
  "type": "string",
517
680
  "maxLength": 2000
@@ -582,6 +745,22 @@
582
745
  "frame_id": {
583
746
  "type": "integer",
584
747
  "minimum": 0
748
+ },
749
+ "input_mode": {
750
+ "type": "string",
751
+ "enum": [
752
+ "auto",
753
+ "trusted",
754
+ "dom"
755
+ ],
756
+ "default": "auto",
757
+ "description": "Use fixed DevTools Input commands when possible, require them, or use DOM events only."
758
+ },
759
+ "element_timeout_seconds": {
760
+ "type": "integer",
761
+ "minimum": 1,
762
+ "maximum": 60,
763
+ "default": 10
585
764
  }
586
765
  },
587
766
  "required": [
@@ -592,7 +771,7 @@
592
771
  {
593
772
  "name": "browser_fill_form",
594
773
  "title": "Fill complex browser form",
595
- "description": "Fill multiple fields and optionally submit a complex form in one structured browser transaction. Sensitive values can come from registered local resources and are not returned.",
774
+ "description": "Fill multiple fields and optionally submit a complex form. Sensitive values can come from registered local resources; errors identify possible earlier mutations without returning field values.",
596
775
  "availability": "full",
597
776
  "annotations": {
598
777
  "readOnlyHint": false,
@@ -618,6 +797,11 @@
618
797
  "selector": {
619
798
  "type": "object",
620
799
  "properties": {
800
+ "ref": {
801
+ "type": "string",
802
+ "maxLength": 100,
803
+ "description": "Stable element reference returned by browser_inspect_page for the same document and frame."
804
+ },
621
805
  "css": {
622
806
  "type": "string",
623
807
  "maxLength": 2000
@@ -691,6 +875,11 @@
691
875
  "submit_selector": {
692
876
  "type": "object",
693
877
  "properties": {
878
+ "ref": {
879
+ "type": "string",
880
+ "maxLength": 100,
881
+ "description": "Stable element reference returned by browser_inspect_page for the same document and frame."
882
+ },
694
883
  "css": {
695
884
  "type": "string",
696
885
  "maxLength": 2000
@@ -745,6 +934,12 @@
745
934
  "frame_id": {
746
935
  "type": "integer",
747
936
  "minimum": 0
937
+ },
938
+ "element_timeout_seconds": {
939
+ "type": "integer",
940
+ "minimum": 1,
941
+ "maximum": 60,
942
+ "default": 10
748
943
  }
749
944
  },
750
945
  "required": [
@@ -755,7 +950,7 @@
755
950
  {
756
951
  "name": "browser_screenshot",
757
952
  "title": "Capture browser screenshot",
758
- "description": "Capture the visible area of the active or selected tab from the paired existing browser profile and return it as MCP image content.",
953
+ "description": "Capture the visible area of the active or selected tab from the paired existing browser profile, restore the previous active tab when safe, avoid focusing another window, and return native MCP image content.",
759
954
  "availability": "full",
760
955
  "annotations": {
761
956
  "readOnlyHint": false,
@@ -815,6 +1010,11 @@
815
1010
  "selector": {
816
1011
  "type": "object",
817
1012
  "properties": {
1013
+ "ref": {
1014
+ "type": "string",
1015
+ "maxLength": 100,
1016
+ "description": "Stable element reference returned by browser_inspect_page for the same document and frame."
1017
+ },
818
1018
  "css": {
819
1019
  "type": "string",
820
1020
  "maxLength": 2000
@@ -885,6 +1085,12 @@
885
1085
  "frame_id": {
886
1086
  "type": "integer",
887
1087
  "minimum": 0
1088
+ },
1089
+ "element_timeout_seconds": {
1090
+ "type": "integer",
1091
+ "minimum": 1,
1092
+ "maximum": 60,
1093
+ "default": 10
888
1094
  }
889
1095
  },
890
1096
  "required": [
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
3
3
  import serverMetadata from "../shared/server-metadata.json";
4
4
 
5
5
  const SERVER_NAME = String(serverMetadata.name);
6
- const SERVER_VERSION = "0.13.0";
6
+ const SERVER_VERSION = "0.15.0";
7
7
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
8
8
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
9
9
  const JSONRPC_VERSION = "2.0";
@@ -817,6 +817,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
817
817
  client.last_used_at = now;
818
818
 
819
819
  const code = randomToken("mcp_code");
820
+ const redirectLocation = authorizationRedirectLocation(redirectUri, code, state);
820
821
  store.codes[code] = {
821
822
  client_id: clientId,
822
823
  redirect_uri: redirectUri,
@@ -829,9 +830,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
829
830
  pruneRecordByExpiry(store.codes, MAX_OAUTH_CODES);
830
831
  await this.ctx.storage.put("oauth", store);
831
832
 
832
- const params = new URLSearchParams({ code });
833
- if (state) params.set("state", state);
834
- return oauthRedirect(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}${params.toString()}`);
833
+ return oauthRedirect(redirectLocation);
835
834
  });
836
835
  }
837
836
 
@@ -996,11 +995,11 @@ function methodNotAllowed(allow: string): Response {
996
995
  });
997
996
  }
998
997
 
999
- function oauthRedirect(location: string): Response {
998
+ function oauthRedirect(location: URL): Response {
1000
999
  return new Response(null, {
1001
- status: 302,
1000
+ status: 303,
1002
1001
  headers: {
1003
- location,
1002
+ location: location.href,
1004
1003
  "cache-control": "no-store",
1005
1004
  "referrer-policy": "no-referrer",
1006
1005
  "x-content-type-options": "nosniff",
@@ -1008,6 +1007,13 @@ function oauthRedirect(location: string): Response {
1008
1007
  });
1009
1008
  }
1010
1009
 
1010
+ function authorizationRedirectLocation(redirectUri: string, code: string, state: string): URL {
1011
+ const location = new URL(redirectUri);
1012
+ location.searchParams.append("code", code);
1013
+ if (state) location.searchParams.append("state", state);
1014
+ return location;
1015
+ }
1016
+
1011
1017
  function isFreshDaemonCandidate(connectedAt: string): boolean {
1012
1018
  const timestamp = Date.parse(connectedAt);
1013
1019
  if (!Number.isFinite(timestamp)) return false;
@@ -1187,7 +1193,7 @@ function daemonToolTimeoutMs(name: string, args: Record<string, unknown>): numbe
1187
1193
  const configurable = new Set([
1188
1194
  "exec_command", "run_process", "run_local_command", "open_local_application",
1189
1195
  "inspect_local_application", "operate_local_application", "browser_list_tabs",
1190
- "browser_get_source", "browser_inspect_page", "browser_action", "browser_fill_form",
1196
+ "browser_manage_tabs", "browser_wait", "browser_get_source", "browser_inspect_page", "browser_action", "browser_fill_form",
1191
1197
  "browser_screenshot", "browser_upload_files",
1192
1198
  ]);
1193
1199
  if (!configurable.has(name)) return 60_000;