machine-bridge-mcp 0.8.2 → 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,9 +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
+ 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";
17
21
 
18
22
  export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
19
23
  const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
@@ -27,6 +31,26 @@ const SLOW_TOOL_CALL_MS = 30_000;
27
31
  const RUNTIME_TOOL_HANDLERS = Object.freeze({
28
32
  server_info: (runtime) => runtime.runtimeInfo(),
29
33
  project_overview: (runtime, _args, context) => runtime.projectOverview(context),
34
+ session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
35
+ agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
36
+ resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
37
+ list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
38
+ load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
39
+ list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
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),
30
54
  list_roots: (runtime) => runtime.listRoots(),
31
55
  list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
32
56
  list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
@@ -61,7 +85,7 @@ export function runtimeToolHandlerNames() {
61
85
  }
62
86
 
63
87
  export class LocalRuntime {
64
- 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 }) {
65
89
  const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
66
90
  const remoteSecret = secret || "";
67
91
  this.workspaceInput = resolve(workspace || process.cwd());
@@ -101,6 +125,33 @@ export class LocalRuntime {
101
125
  displayPath: (value) => this.displayPath(value),
102
126
  throwIfCancelled: (context) => this.throwIfCancelled(context),
103
127
  });
128
+ this.agentContextManager = new AgentContextManager({
129
+ workspace: this.workspace,
130
+ policy: this.policy,
131
+ displayPath: (value) => this.displayPath(value),
132
+ resolveExistingPath: (value) => this.resolveExistingPath(value),
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),
154
+ });
104
155
  this.relay = createRelayConnection(this, {
105
156
  workerUrl: remoteWorkerUrl,
106
157
  secret: remoteSecret,
@@ -157,8 +208,13 @@ export class LocalRuntime {
157
208
  };
158
209
  }
159
210
 
160
- start() {
211
+ async start() {
161
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
+ }
162
218
  return this.relay.start();
163
219
  }
164
220
 
@@ -166,6 +222,7 @@ export class LocalRuntime {
166
222
  this.relay?.stop();
167
223
  this.terminateActiveProcesses("SIGKILL");
168
224
  this.processSessionManager.clear();
225
+ this.browserBridgeManager?.stop();
169
226
  rmSync(this.runtimeDir, { recursive: true, force: true });
170
227
  }
171
228
 
@@ -264,6 +321,7 @@ export class LocalRuntime {
264
321
  cancelCall(callId, reason = "cancelled") {
265
322
  this.cancelledCalls.add(callId);
266
323
  this.processSessionManager.notifyCancellation();
324
+ this.browserBridgeManager?.cancelCall(callId);
267
325
  for (const child of this.callProcesses.get(callId) || []) terminateProcessTree(child, "SIGTERM");
268
326
  const children = [...(this.callProcesses.get(callId) || [])];
269
327
  if (children.length) {
@@ -725,6 +783,57 @@ export class LocalRuntime {
725
783
  };
726
784
  }
727
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
+
728
837
  async runDirectProcess(args, context = {}) {
729
838
  if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_process is disabled by daemon policy");
730
839
  const argv = validateArgv(args.argv);
@@ -733,6 +842,25 @@ export class LocalRuntime {
733
842
  return this.runProcess(argv[0], argv.slice(1), clampInt(args.timeout_seconds, 120, 1, 600) * 1000, false, 512 * 1024, context, cwd);
734
843
  }
735
844
 
845
+ async runLocalCommand(args, context = {}) {
846
+ if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_local_command is disabled by daemon policy");
847
+ const command = await this.agentContextManager.resolveLocalCommand(args, context);
848
+ const argv = validateArgv(command.argv);
849
+ const cwd = await this.resolveExistingPath(command.cwd);
850
+ if (!(await stat(cwd)).isDirectory()) throw new Error("registered command cwd is not a directory");
851
+ const requestedTimeout = args.timeout_seconds === undefined
852
+ ? command.timeoutSeconds
853
+ : clampInt(args.timeout_seconds, command.timeoutSeconds, 1, 600);
854
+ const timeoutSeconds = Math.min(requestedTimeout, command.timeoutSeconds);
855
+ const result = await this.runProcess(argv[0], argv.slice(1), timeoutSeconds * 1000, false, 512 * 1024, context, cwd);
856
+ return {
857
+ name: command.name,
858
+ cwd: this.displayPath(cwd),
859
+ timeout_seconds: timeoutSeconds,
860
+ ...result,
861
+ };
862
+ }
863
+
736
864
  async execCommand(command, timeoutSeconds, context = {}) {
737
865
  if (this.policy.execMode !== "shell") throw new Error("exec_command requires shell execution mode");
738
866
  if (!command || typeof command !== "string") throw new Error("command is required");
@@ -753,9 +881,10 @@ export class LocalRuntime {
753
881
  }
754
882
  }
755
883
 
756
- 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) {
757
885
  this.throwIfCancelled(context);
758
886
  return new Promise((resolvePromise, reject) => {
887
+ if (stdin !== null && Buffer.byteLength(String(stdin)) > 1024 * 1024) throw new Error("process stdin exceeds 1 MiB");
759
888
  const child = spawn(cmd, args, {
760
889
  cwd,
761
890
  env: executionEnv(this.workspace, { fullEnv: this.policy.minimalEnv === false, runtimeDir: this.runtimeDir }),
@@ -763,6 +892,10 @@ export class LocalRuntime {
763
892
  windowsHide: true,
764
893
  });
765
894
  this.activeProcesses.add(child);
895
+ if (stdin !== null) {
896
+ child.stdin.on("error", () => {});
897
+ child.stdin.end(String(stdin));
898
+ }
766
899
  if (context.callId) {
767
900
  const set = this.callProcesses.get(context.callId) || new Set();
768
901
  set.add(child);
@@ -933,6 +1066,15 @@ export class LocalRuntime {
933
1066
  }
934
1067
  }
935
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
+
936
1078
  function policyMatchesNamedProfile(policy) {
937
1079
  const named = POLICY_PROFILES[policy.profile];
938
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,6 +8,10 @@
8
8
  ],
9
9
  "instructions": [
10
10
  "You are connected to a local workspace through machine-bridge-mcp.",
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.",
11
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.",
12
16
  "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
13
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.",