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.
- package/CHANGELOG.md +38 -0
- package/README.md +41 -12
- package/SECURITY.md +5 -5
- package/browser-extension/browser-operations.js +515 -0
- package/browser-extension/devtools-input.js +151 -0
- package/browser-extension/manifest.json +3 -2
- package/browser-extension/page-automation.js +540 -75
- package/browser-extension/pairing.js +1 -1
- package/browser-extension/service-worker.js +158 -258
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT.md +13 -1
- package/docs/LOCAL_AUTOMATION.md +18 -14
- package/docs/OPERATIONS.md +17 -9
- package/docs/TESTING.md +4 -4
- package/package.json +10 -4
- package/src/local/agent-context.mjs +1 -1
- package/src/local/browser-bridge.mjs +245 -91
- package/src/local/browser-command.mjs +126 -0
- package/src/local/cli.mjs +37 -5
- package/src/local/runtime.mjs +2 -0
- package/src/local/service.mjs +7 -3
- package/src/shared/tool-catalog.json +214 -8
- package/src/worker/index.ts +14 -8
|
@@ -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
|
|
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
|
package/src/local/runtime.mjs
CHANGED
|
@@ -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),
|
package/src/local/service.mjs
CHANGED
|
@@ -465,11 +465,15 @@ async function statusWindowsTask() {
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
function windowsCommand(spec) {
|
|
468
|
-
return [spec.node, ...daemonArgs(spec)].map(
|
|
468
|
+
return [spec.node, ...daemonArgs(spec)].map(windowsCommandLineArgument).join(" ");
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
function
|
|
472
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": [
|
package/src/worker/index.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
998
|
+
function oauthRedirect(location: URL): Response {
|
|
1000
999
|
return new Response(null, {
|
|
1001
|
-
status:
|
|
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;
|