machine-bridge-mcp 0.9.0 → 0.10.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/src/local/cli.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { createHash, randomBytes } from "node:crypto";
2
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
3
4
  import path, { join, resolve } from "node:path";
@@ -11,6 +12,7 @@ import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobM
11
12
  import { runWrangler } from "./shell.mjs";
12
13
  import { generateRegisteredSshKey } from "./resource-operations.mjs";
13
14
  import { runFullAccessTest } from "./full-access-test.mjs";
15
+ import { readBoundedRegularFileSync } from "./secure-file.mjs";
14
16
  import {
15
17
  acquireDaemonLock,
16
18
  acquireStartupLock,
@@ -58,6 +60,7 @@ const COMMAND_HANDLERS = Object.freeze({
58
60
  autostart: serviceCommand,
59
61
  "rotate-secrets": rotateSecretsCommand,
60
62
  resource: resourceCommand,
63
+ browser: browserCommand,
61
64
  job: jobCommand,
62
65
  uninstall: uninstallCommand,
63
66
  });
@@ -94,6 +97,7 @@ const COMMAND_OPTIONS = {
94
97
  service: new Set(["workspace", "stateDir", "quiet"]),
95
98
  autostart: new Set(["workspace", "stateDir", "quiet"]),
96
99
  resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
100
+ browser: new Set(["workspace", "stateDir", "json"]),
97
101
  job: new Set(["workspace", "stateDir", "json", "yes"]),
98
102
  uninstall: new Set(["stateDir", "keepWorker", "yes"]),
99
103
  };
@@ -149,6 +153,10 @@ const ACTION_POSITIONAL_RULES = Object.freeze({
149
153
  const action = String(args._[0] || "list");
150
154
  return { max: RESOURCE_POSITIONAL_LIMITS[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
151
155
  },
156
+ browser(args) {
157
+ const action = String(args._[0] || "status");
158
+ return { max: 1, tooMany: `browser ${action} received too many positional arguments` };
159
+ },
152
160
  job(args) {
153
161
  const action = String(args._[0] || "list");
154
162
  return { max: JOB_POSITIONAL_LIMITS[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
@@ -426,6 +434,7 @@ function createRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
426
434
  jobRoot: join(state.paths.profileDir, "jobs"),
427
435
  resources: state.resources,
428
436
  resourceStatePath: state.paths.statePath,
437
+ browserStateRoot: state.paths.stateRoot,
429
438
  onSuperseded: () => {
430
439
  daemonLock.release();
431
440
  process.exit(0);
@@ -537,6 +546,7 @@ async function stdioCommand(args) {
537
546
  jobRoot: join(state.paths.profileDir, "jobs"),
538
547
  resources: state.resources,
539
548
  resourceStatePath: state.paths.statePath,
549
+ browserStateRoot: state.paths.stateRoot,
540
550
  });
541
551
  }
542
552
 
@@ -698,6 +708,78 @@ function publicResourceInspection(name, inspected, { includePath = false, ...ext
698
708
  };
699
709
  }
700
710
 
711
+ async function browserCommand(args) {
712
+ const action = String(args._[0] || "status").toLowerCase();
713
+ const extensionPath = resolve(packageRoot, "browser-extension");
714
+ if (action === "path") {
715
+ if (args.json) console.log(JSON.stringify({ extension_path: extensionPath }, null, 2));
716
+ else console.log(extensionPath);
717
+ return;
718
+ }
719
+ if (!["status", "setup", "pair"].includes(action)) throw new Error(`Unknown browser action: ${action}`);
720
+ const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
721
+ const state = loadState(workspace, { stateDir: args.stateDir });
722
+ const pairingFile = join(state.paths.stateRoot, "browser-bridge.json");
723
+ if (!existsSync(pairingFile)) {
724
+ throw new Error("browser bridge is not initialized; start machine-mcp once, then run this command again");
725
+ }
726
+ ownerOnlyFile(pairingFile);
727
+ let pairing;
728
+ try {
729
+ pairing = JSON.parse(readBoundedRegularFileSync(pairingFile, 64 * 1024).toString("utf8"));
730
+ } catch {
731
+ throw new Error("browser bridge state is invalid; restart machine-mcp to repair it");
732
+ }
733
+ const port = Number(pairing.port);
734
+ if (!/^[A-Za-z0-9_-]{32,100}$/.test(String(pairing.token || ""))) throw new Error("browser bridge state contains an invalid token");
735
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("browser bridge state contains an invalid port");
736
+ const pairingUrl = `http://127.0.0.1:${port}/pair`;
737
+ const healthUrl = `http://127.0.0.1:${port}/healthz`;
738
+ const health = await fetch(healthUrl, { signal: AbortSignal.timeout(2000), cache: "no-store" })
739
+ .then(async (response) => response.ok ? await response.json() : null)
740
+ .catch(() => null);
741
+ const result = {
742
+ running: health?.ok === true && health?.broker === "machine-bridge-browser",
743
+ connected: health?.broker === "machine-bridge-browser" && health?.connected === true,
744
+ extension_path: extensionPath,
745
+ pairing_url: pairingUrl,
746
+ token_exposed: false,
747
+ };
748
+ if (action === "status") {
749
+ if (args.json) console.log(JSON.stringify(result, null, 2));
750
+ else {
751
+ console.log(`Browser bridge: ${result.running ? "running" : "not reachable"}`);
752
+ console.log(`Extension: ${result.connected ? "connected" : "not connected"}`);
753
+ console.log(`Extension path: ${extensionPath}`);
754
+ }
755
+ return;
756
+ }
757
+ if (!result.running) throw new Error("browser bridge is not reachable; keep machine-mcp running and retry");
758
+ await openExternal(pairingUrl);
759
+ if (args.json) console.log(JSON.stringify({ ...result, pairing_page_opened: true }, null, 2));
760
+ else {
761
+ console.log(`Extension path: ${extensionPath}`);
762
+ console.log("Load this directory once from the Chromium extensions page with Developer mode enabled.");
763
+ console.log(`Pairing page opened: ${pairingUrl}`);
764
+ }
765
+ }
766
+
767
+ function openExternal(target) {
768
+ const command = process.platform === "darwin"
769
+ ? { file: "open", args: [target] }
770
+ : process.platform === "win32"
771
+ ? { file: "cmd.exe", args: ["/d", "/s", "/c", "start", "", target] }
772
+ : { file: "xdg-open", args: [target] };
773
+ return new Promise((resolvePromise, rejectPromise) => {
774
+ const child = spawn(command.file, command.args, { detached: true, stdio: "ignore", windowsHide: true });
775
+ child.once("spawn", () => {
776
+ child.unref();
777
+ resolvePromise();
778
+ });
779
+ child.once("error", rejectPromise);
780
+ });
781
+ }
782
+
701
783
  async function jobCommand(args) {
702
784
  const action = String(args._[0] || "list").toLowerCase();
703
785
  const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
@@ -1367,6 +1449,9 @@ Commands:
1367
1449
  rotate-secrets Rotate MCP password and daemon secret in local state
1368
1450
  resource generate-ssh-key NAME [PATH]
1369
1451
  Generate/reuse an Ed25519 key locally and register its private file by alias
1452
+ browser status Show browser-extension bridge and connection status
1453
+ browser setup Print the extension path and open the local pairing page
1454
+ browser path Print the packaged unpacked-extension directory
1370
1455
  uninstall Delete known Worker(s), remove autostart and local state
1371
1456
 
1372
1457
  Start options:
@@ -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;
@@ -361,7 +361,7 @@ function assertValidStateMarker(marker, options = {}) {
361
361
  }
362
362
 
363
363
  function hasOnlyStateEntries(entries) {
364
- const allowed = new Set([STATE_MARKER, "config.json", "profiles", "logs"]);
364
+ const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "profiles", "logs"]);
365
365
  return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+(?:-[a-f0-9]{8})?$/.test(entry));
366
366
  }
367
367
 
@@ -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 the configured global model_instructions_file and 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.",