machine-bridge-mcp 0.9.0 → 0.11.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.
@@ -11,10 +11,13 @@ import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validat
11
11
  export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
12
12
  import { allToolNames, assertCanonicalFullPolicy, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, POLICY_PROFILES, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
13
13
  import { classifyOperationalError } from "./log.mjs";
14
- import { ManagedJobManager } from "./managed-jobs.mjs";
14
+ import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
15
15
  import { generateRegisteredSshKey } from "./resource-operations.mjs";
16
16
  import { expandHome } from "./state.mjs";
17
17
  import { AgentContextManager } from "./agent-context.mjs";
18
+ import { AppAutomationManager } from "./app-automation.mjs";
19
+ import { BrowserBridgeManager } from "./browser-bridge.mjs";
20
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
18
21
 
19
22
  export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
20
23
  const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
@@ -28,11 +31,26 @@ const SLOW_TOOL_CALL_MS = 30_000;
28
31
  const RUNTIME_TOOL_HANDLERS = Object.freeze({
29
32
  server_info: (runtime) => runtime.runtimeInfo(),
30
33
  project_overview: (runtime, _args, context) => runtime.projectOverview(context),
34
+ session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
31
35
  agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
36
+ resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
32
37
  list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
33
38
  load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
34
39
  list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
35
40
  run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
41
+ list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
42
+ open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
43
+ inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
44
+ operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
45
+ browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
46
+ pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
47
+ browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
48
+ browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
49
+ browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
50
+ browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
51
+ browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
52
+ browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
53
+ browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
36
54
  list_roots: (runtime) => runtime.listRoots(),
37
55
  list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
38
56
  list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
@@ -67,7 +85,7 @@ export function runtimeToolHandlerNames() {
67
85
  }
68
86
 
69
87
  export class LocalRuntime {
70
- constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
88
+ constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true }) {
71
89
  const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
72
90
  const remoteSecret = secret || "";
73
91
  this.workspaceInput = resolve(workspace || process.cwd());
@@ -113,6 +131,26 @@ export class LocalRuntime {
113
131
  displayPath: (value) => this.displayPath(value),
114
132
  resolveExistingPath: (value) => this.resolveExistingPath(value),
115
133
  throwIfCancelled: (context) => this.throwIfCancelled(context),
134
+ home: agentHome,
135
+ codexHome,
136
+ });
137
+ const runProcess = (cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin) => this.runProcess(cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
138
+ const readResourceText = (name) => this.readLocalResourceText(name);
139
+ const readResourceBinary = (name) => this.readLocalResourceBinary(name);
140
+ this.appAutomationManager = new AppAutomationManager({
141
+ policy: this.policy,
142
+ displayPath: (value) => this.displayPath(value),
143
+ runProcess,
144
+ readResourceText,
145
+ throwIfCancelled: (context) => this.throwIfCancelled(context),
146
+ });
147
+ this.browserBridgeManager = new BrowserBridgeManager({
148
+ policy: this.policy,
149
+ stateRoot: browserStateRoot,
150
+ runProcess,
151
+ readResourceText,
152
+ readResourceBinary,
153
+ throwIfCancelled: (context) => this.throwIfCancelled(context),
116
154
  });
117
155
  this.relay = createRelayConnection(this, {
118
156
  workerUrl: remoteWorkerUrl,
@@ -170,8 +208,13 @@ export class LocalRuntime {
170
208
  };
171
209
  }
172
210
 
173
- start() {
211
+ async start() {
174
212
  if (!this.relay) throw new Error("remote daemon start requires a Worker URL and daemon secret");
213
+ if (this.policy.profile === "full") {
214
+ void this.browserBridgeManager.ensureStarted().catch((error) => {
215
+ this.logger.warn?.("browser bridge did not start; browser tools remain unavailable", { error_class: classifyOperationalError(error) });
216
+ });
217
+ }
175
218
  return this.relay.start();
176
219
  }
177
220
 
@@ -179,6 +222,7 @@ export class LocalRuntime {
179
222
  this.relay?.stop();
180
223
  this.terminateActiveProcesses("SIGKILL");
181
224
  this.processSessionManager.clear();
225
+ this.browserBridgeManager?.stop();
182
226
  rmSync(this.runtimeDir, { recursive: true, force: true });
183
227
  }
184
228
 
@@ -277,6 +321,7 @@ export class LocalRuntime {
277
321
  cancelCall(callId, reason = "cancelled") {
278
322
  this.cancelledCalls.add(callId);
279
323
  this.processSessionManager.notifyCancellation();
324
+ this.browserBridgeManager?.cancelCall(callId);
280
325
  for (const child of this.callProcesses.get(callId) || []) terminateProcessTree(child, "SIGTERM");
281
326
  const children = [...(this.callProcesses.get(callId) || [])];
282
327
  if (children.length) {
@@ -738,6 +783,57 @@ export class LocalRuntime {
738
783
  };
739
784
  }
740
785
 
786
+
787
+ async sessionBootstrap(args = {}, context = {}) {
788
+ const bootstrap = await this.agentContextManager.sessionBootstrap(args, context);
789
+ bootstrap.local_automation = {
790
+ applications: this.appAutomationManager.capabilities(),
791
+ browser: this.policy.profile === "full" ? {
792
+ existing_profile: true,
793
+ extension_bridge: true,
794
+ status_tool: "browser_status",
795
+ } : null,
796
+ };
797
+ return bootstrap;
798
+ }
799
+
800
+ async resolveTaskCapabilities(args = {}, context = {}) {
801
+ const result = await this.agentContextManager.resolveTaskCapabilities(args, context);
802
+ const task = String(args.task || "");
803
+ if (/app|application|gui|window|应用|软件|窗口|界面/i.test(task) && this.policy.profile === "full") {
804
+ const applications = await this.appAutomationManager.listApplications({ query: "", max_results: 500 }, context).catch(() => ({ applications: [] }));
805
+ const lower = task.toLowerCase();
806
+ result.application_matches = applications.applications
807
+ .map((application) => ({ application, score: applicationMatchScore(lower, application) }))
808
+ .filter((item) => item.score > 0)
809
+ .sort((left, right) => right.score - left.score || left.application.name.localeCompare(right.application.name))
810
+ .slice(0, 20)
811
+ .map(({ application, score }) => ({ ...application, score }));
812
+ } else {
813
+ result.application_matches = [];
814
+ }
815
+ result.browser_backend = this.policy.profile === "full" ? { tool: "browser_status", existing_profile: true, extension_bridge: true } : null;
816
+ return result;
817
+ }
818
+
819
+ readLocalResourceBinary(name) {
820
+ const registry = this.managedJobManager.currentResources();
821
+ const resource = registry[name];
822
+ if (!resource) throw new Error(`unknown local resource: ${name}`);
823
+ const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
824
+ if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
825
+ return { buffer: readBoundedRegularFileSync(resource.path, 1024 * 1024), path: resource.path, size: inspected.size };
826
+ }
827
+
828
+ readLocalResourceText(name) {
829
+ const { buffer } = this.readLocalResourceBinary(name);
830
+ try {
831
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
832
+ } catch {
833
+ throw new Error(`local resource is not valid UTF-8 text: ${name}`);
834
+ }
835
+ }
836
+
741
837
  async runDirectProcess(args, context = {}) {
742
838
  if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_process is disabled by daemon policy");
743
839
  const argv = validateArgv(args.argv);
@@ -785,9 +881,10 @@ export class LocalRuntime {
785
881
  }
786
882
  }
787
883
 
788
- async runProcess(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = 512 * 1024, context = {}, cwd = this.workspace) {
884
+ async runProcess(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = 512 * 1024, context = {}, cwd = this.workspace, stdin = null) {
789
885
  this.throwIfCancelled(context);
790
886
  return new Promise((resolvePromise, reject) => {
887
+ if (stdin !== null && Buffer.byteLength(String(stdin)) > 1024 * 1024) throw new Error("process stdin exceeds 1 MiB");
791
888
  const child = spawn(cmd, args, {
792
889
  cwd,
793
890
  env: executionEnv(this.workspace, { fullEnv: this.policy.minimalEnv === false, runtimeDir: this.runtimeDir }),
@@ -795,6 +892,10 @@ export class LocalRuntime {
795
892
  windowsHide: true,
796
893
  });
797
894
  this.activeProcesses.add(child);
895
+ if (stdin !== null) {
896
+ child.stdin.on("error", () => {});
897
+ child.stdin.end(String(stdin));
898
+ }
798
899
  if (context.callId) {
799
900
  const set = this.callProcesses.get(context.callId) || new Set();
800
901
  set.add(child);
@@ -965,6 +1066,15 @@ export class LocalRuntime {
965
1066
  }
966
1067
  }
967
1068
 
1069
+ function applicationMatchScore(task, application) {
1070
+ const name = String(application.name || "").toLowerCase();
1071
+ const id = String(application.id || "").toLowerCase();
1072
+ if (!name) return 0;
1073
+ if (task.includes(name)) return 10 + Math.min(name.length, 20);
1074
+ const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
1075
+ return words.reduce((score, word) => score + (task.includes(word) ? 2 : 0), id && task.includes(id) ? 5 : 0);
1076
+ }
1077
+
968
1078
  function policyMatchesNamedProfile(policy) {
969
1079
  const named = POLICY_PROFILES[policy.profile];
970
1080
  if (!named) return false;
@@ -170,15 +170,18 @@ function lockPathForState(state, name) {
170
170
  return path.join(profileDir, name);
171
171
  }
172
172
 
173
- export function acquireDaemonLock(state) {
174
- return acquireProcessLock(daemonLockPathForState(state), state, "daemon");
173
+ export function acquireDaemonLock(state, metadata = {}) {
174
+ const details = {};
175
+ if (metadata?.mode === "foreground" || metadata?.mode === "service") details.mode = metadata.mode;
176
+ if (typeof metadata?.version === "string" && /^[0-9A-Za-z.+_-]{1,64}$/.test(metadata.version)) details.version = metadata.version;
177
+ return acquireProcessLock(daemonLockPathForState(state), state, "daemon", details);
175
178
  }
176
179
 
177
180
  export function acquireStartupLock(state) {
178
181
  return acquireProcessLock(startupLockPathForState(state), state, "startup");
179
182
  }
180
183
 
181
- function acquireProcessLock(lockPath, state, purpose) {
184
+ function acquireProcessLock(lockPath, state, purpose, details = {}) {
182
185
  ensureOwnerOnlyDir(path.dirname(lockPath));
183
186
  const token = randomBytes(16).toString("hex");
184
187
  const payload = {
@@ -188,6 +191,7 @@ function acquireProcessLock(lockPath, state, purpose) {
188
191
  workspace: state?.workspace?.path || "",
189
192
  startedAt: new Date().toISOString(),
190
193
  entryScript: process.argv[1] || "",
194
+ ...details,
191
195
  };
192
196
 
193
197
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -361,7 +365,7 @@ function assertValidStateMarker(marker, options = {}) {
361
365
  }
362
366
 
363
367
  function hasOnlyStateEntries(entries) {
364
- const allowed = new Set([STATE_MARKER, "config.json", "profiles", "logs"]);
368
+ const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "profiles", "logs"]);
365
369
  return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+(?:-[a-f0-9]{8})?$/.test(entry));
366
370
  }
367
371
 
@@ -17,9 +17,9 @@ const MAX_LINE_BYTES = 8 * 1024 * 1024;
17
17
  const SLOW_TOOL_CALL_MS = 30_000;
18
18
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
19
19
 
20
- export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
20
+ export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "" }) {
21
21
  const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
22
- const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
22
+ const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath, browserStateRoot });
23
23
  const pending = new Map();
24
24
  let negotiatedVersion = MCP_PROTOCOL_VERSION;
25
25
  let initialized = false;
@@ -52,7 +52,7 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
52
52
  async function handleLine(line) {
53
53
  const message = parseJsonRpcLine(line, send);
54
54
  if (!message || typeof message.method !== "string") return;
55
- if (handleControlMessage(message)) return;
55
+ if (await handleControlMessage(message)) return;
56
56
  if (!initialized) {
57
57
  send(rpcError(message.id, -32002, "Server is not initialized"));
58
58
  return;
@@ -60,14 +60,14 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
60
60
  await handleInitializedMessage(message);
61
61
  }
62
62
 
63
- function handleControlMessage(message) {
63
+ async function handleControlMessage(message) {
64
64
  if (message.method === "initialize") {
65
65
  const requested = asObject(message.params).protocolVersion;
66
66
  negotiatedVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
67
67
  ? requested
68
68
  : MCP_PROTOCOL_VERSION;
69
69
  initialized = true;
70
- send(rpcResult(message.id, initializationResult(negotiatedVersion)));
70
+ send(rpcResult(message.id, await initializationResult(negotiatedVersion, runtime)));
71
71
  return true;
72
72
  }
73
73
  if (message.method === "notifications/initialized") return true;
@@ -148,7 +148,12 @@ function parseJsonRpcLine(line, send) {
148
148
  return message;
149
149
  }
150
150
 
151
- function initializationResult(protocolVersion) {
151
+ async function initializationResult(protocolVersion, runtime) {
152
+ const bootstrap = await runtime.sessionBootstrap({ path: "." }).catch((error) => {
153
+ runtime.logger?.debug?.("session bootstrap unavailable during initialize", { error_class: classifyOperationalError(error) });
154
+ return null;
155
+ });
156
+ const localInstructions = bootstrap?.instructions ? `\n\n--- LOCAL SESSION INSTRUCTIONS ---\n${bootstrap.instructions}` : "";
152
157
  return {
153
158
  protocolVersion,
154
159
  capabilities: { tools: { listChanged: false }, logging: {} },
@@ -158,7 +163,7 @@ function initializationResult(protocolVersion) {
158
163
  version: PACKAGE_VERSION,
159
164
  description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
160
165
  },
161
- instructions: MCP_INSTRUCTIONS,
166
+ instructions: `${MCP_INSTRUCTIONS}${localInstructions}`,
162
167
  };
163
168
  }
164
169
 
@@ -8,7 +8,10 @@
8
8
  ],
9
9
  "instructions": [
10
10
  "You are connected to a local workspace through machine-bridge-mcp.",
11
- "For substantive workspace tasks, call agent_context for the relevant target path before editing or executing. Apply returned instruction files in precedence order, load only relevant local skills, and prefer registered local commands for repeatable workflows.",
11
+ "At the start of each substantive local task, call resolve_task_capabilities with the current user request and target path. Apply the returned session/project instructions, follow the automatically selected relevant skill when present, and prefer registered local commands for repeatable workflows.",
12
+ "The MCP initialize response automatically appends conservative built-in working agreements, bounded automatic project facts, the configured global model_instructions_file, and the current root instruction chain when the local daemon is reachable; session_bootstrap exposes the same material explicitly.",
13
+ "For existing-browser work, use the paired Machine Bridge browser extension. It operates the user current Chromium profile and tabs, can inspect DOM/source, fill complex forms, and inject sensitive text from registered local resources without returning those values.",
14
+ "For local application work, use structured application discovery and Accessibility actions. Arbitrary caller-supplied AppleScript or JavaScript is intentionally not accepted.",
12
15
  "Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
13
16
  "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
14
17
  "Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",