machine-bridge-mcp 0.5.0 → 0.6.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.
@@ -20,7 +20,7 @@ export function expandHome(input = "") {
20
20
 
21
21
  export function resolveWorkspace(input = process.cwd()) {
22
22
  const resolved = path.resolve(expandHome(input));
23
- return realpathSync(resolved);
23
+ return realpathSync.native ? realpathSync.native(resolved) : realpathSync(resolved);
24
24
  }
25
25
 
26
26
  export function defaultStateRoot() {
@@ -52,9 +52,10 @@ export function saveGlobalConfig(config, stateRoot = defaultStateRoot()) {
52
52
 
53
53
  export function setSelectedWorkspace(workspace, stateRoot = defaultStateRoot()) {
54
54
  const root = expandHome(stateRoot);
55
+ const canonicalWorkspace = resolveWorkspace(workspace);
55
56
  const config = loadGlobalConfig(root);
56
- config.selectedWorkspace = workspace;
57
- config.selectedWorkspaceHash = workspaceHash(workspace);
57
+ config.selectedWorkspace = canonicalWorkspace;
58
+ config.selectedWorkspaceHash = workspaceHash(canonicalWorkspace);
58
59
  saveGlobalConfig(config, root);
59
60
  return config;
60
61
  }
@@ -79,7 +80,9 @@ export function removeStateRoot(stateRoot = defaultStateRoot()) {
79
80
  }
80
81
 
81
82
  export function workspaceHash(workspace) {
82
- return createHash("sha256").update(String(workspace)).digest("hex").slice(0, 24);
83
+ const canonical = resolveWorkspace(workspace);
84
+ const identity = process.platform === "win32" ? canonical.toLowerCase() : canonical;
85
+ return createHash("sha256").update(identity).digest("hex").slice(0, 24);
83
86
  }
84
87
 
85
88
  function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
@@ -87,9 +90,43 @@ function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
87
90
  }
88
91
 
89
92
 
93
+ function matchingLegacyProfileDir(canonicalWorkspace, stateRoot) {
94
+ const profilesRoot = path.join(stateRoot, "profiles");
95
+ if (!existsSync(profilesRoot)) return "";
96
+ const matches = [];
97
+ for (const entry of readdirSync(profilesRoot, { withFileTypes: true })) {
98
+ if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
99
+ const profileDir = path.join(profilesRoot, entry.name);
100
+ const stateFile = path.join(profileDir, "state.json");
101
+ if (!existsSync(stateFile)) continue;
102
+ try {
103
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
104
+ const storedWorkspace = value?.workspace?.path;
105
+ if (typeof storedWorkspace !== "string") continue;
106
+ if (sameWorkspaceIdentity(storedWorkspace, canonicalWorkspace)) matches.push(profileDir);
107
+ } catch {}
108
+ }
109
+ if (matches.length > 1) throw new Error("multiple Machine Bridge profiles refer to the same canonical workspace; remove or merge the duplicate state profiles");
110
+ return matches[0] || "";
111
+ }
112
+
113
+ function sameWorkspaceIdentity(left, right) {
114
+ try {
115
+ const a = resolveWorkspace(left);
116
+ const b = resolveWorkspace(right);
117
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
90
123
  export function loadState(workspace, options = {}) {
124
+ const canonicalWorkspace = resolveWorkspace(workspace);
91
125
  const stateRoot = ensureStateRoot(options.stateDir ? expandHome(options.stateDir) : defaultStateRoot());
92
- const profileDir = profileDirForWorkspace(workspace, stateRoot);
126
+ const canonicalProfileDir = profileDirForWorkspace(canonicalWorkspace, stateRoot);
127
+ const profileDir = existsSync(canonicalProfileDir)
128
+ ? canonicalProfileDir
129
+ : matchingLegacyProfileDir(canonicalWorkspace, stateRoot) || canonicalProfileDir;
93
130
  const statePath = path.join(profileDir, "state.json");
94
131
  ensureOwnerOnlyDir(profileDir);
95
132
  let state = {};
@@ -97,15 +134,16 @@ export function loadState(workspace, options = {}) {
97
134
  ownerOnlyFile(statePath);
98
135
  state = readJsonObjectOrBackup(statePath);
99
136
  }
100
- state.schemaVersion = 4;
137
+ state.schemaVersion = 5;
101
138
  state.workspace = {
102
- path: workspace,
103
- hash: workspaceHash(workspace),
139
+ path: canonicalWorkspace,
140
+ hash: path.basename(profileDir),
104
141
  updatedAt: new Date().toISOString(),
105
142
  };
106
143
  state.paths = { stateRoot, profileDir, statePath };
107
144
  state.worker ||= {};
108
145
  state.policy ||= {};
146
+ state.resources ||= {};
109
147
  delete state.localApi;
110
148
  return state;
111
149
  }
@@ -467,6 +505,11 @@ export function redactState(state) {
467
505
  if (clone.worker?.oauthPassword) clone.worker.oauthPassword = "<redacted>";
468
506
  if (clone.worker?.daemonSecret) clone.worker.daemonSecret = "<redacted>";
469
507
  if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = "<redacted>";
508
+ if (clone.resources && typeof clone.resources === "object") {
509
+ for (const value of Object.values(clone.resources)) {
510
+ if (value && typeof value === "object" && value.path) value.path = "<local-resource-path>";
511
+ }
512
+ }
470
513
  return clone;
471
514
  }
472
515
 
@@ -18,9 +18,9 @@ const MAX_LINE_BYTES = 8 * 1024 * 1024;
18
18
  const SLOW_TOOL_CALL_MS = 30_000;
19
19
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
20
20
 
21
- export async function runStdioServer({ workspace, policy, logLevel = "info" }) {
21
+ export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
22
22
  const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
23
- const runtime = new LocalDaemon({ workspace, policy, logger });
23
+ const runtime = new LocalDaemon({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
24
24
  const pending = new Map();
25
25
  let negotiatedVersion = MCP_PROTOCOL_VERSION;
26
26
  let initialized = false;
@@ -10,7 +10,14 @@
10
10
  "You are connected to a local workspace through machine-bridge-mcp.",
11
11
  "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
12
  "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
13
- "Filename sensitivity is not classified by this server; the MCP host may enforce additional independent rules.",
13
+ "Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
14
+ "Use diagnose_runtime to distinguish a request that reached the daemon from local filesystem, process-spawn, shell, managed-job-storage, and resource failures. A tool call blocked before any response cannot be diagnosed by the server.",
15
+ "Never request or return secret-file contents when a local resource alias can be used. Resources are registered only through the local machine-mcp CLI and may be injected by path, stdin, or environment without entering MCP arguments.",
16
+ "If execution-class tools are unavailable but write access remains, use stage_job to persist a reviewed plan without running it; the operator can launch it locally with machine-mcp job approve JOB_ID.",
17
+ "For multi-step, remote, long-running, or cleanup-sensitive work, prefer one start_job call with argv steps, job-scoped temporary_files, and idempotent finally_steps. The detached runner continues after MCP disconnects; inspect it with read_job or the local machine-mcp job CLI.",
18
+ "Prefer sending remote shell programs through a process stdin rather than creating remote helper files. If temporary files are necessary, use {{temp:name}} or explicit finally_steps.",
19
+ "Managed-job resource redaction is defense in depth, not a guarantee against transformed or partial secret output. Use capture_output=discard for steps that may echo credentials.",
20
+ "Do not use shell encoding, renaming, or alternate tools to bypass host or platform safety policy.",
14
21
  "run_process avoids shell parsing but is not an OS sandbox. exec_command is exposed only in shell mode and has the local user's authority.",
15
22
  "Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
16
23
  ]
@@ -603,6 +603,504 @@
603
603
  "additionalProperties": false
604
604
  }
605
605
  },
606
+ {
607
+ "name": "diagnose_runtime",
608
+ "title": "Diagnose runtime layers",
609
+ "description": "Run fixed, non-user-controlled local probes to distinguish MCP policy, local filesystem, process-spawn, shell, managed-job storage, and registered-resource failures. A successful response proves the request reached the local daemon; it cannot diagnose a host refusal that blocks the tool call itself.",
610
+ "availability": "always",
611
+ "annotations": {
612
+ "readOnlyHint": true,
613
+ "destructiveHint": false,
614
+ "idempotentHint": true,
615
+ "openWorldHint": false
616
+ },
617
+ "inputSchema": {
618
+ "type": "object",
619
+ "additionalProperties": false
620
+ }
621
+ },
622
+ {
623
+ "name": "list_local_resources",
624
+ "title": "List local resource aliases",
625
+ "description": "List names and validation status for locally registered file resources without returning their paths or contents. Resources are registered only through the local machine-mcp CLI.",
626
+ "availability": "write",
627
+ "annotations": {
628
+ "readOnlyHint": true,
629
+ "destructiveHint": false,
630
+ "idempotentHint": true,
631
+ "openWorldHint": false
632
+ },
633
+ "inputSchema": {
634
+ "type": "object",
635
+ "additionalProperties": false
636
+ }
637
+ },
638
+ {
639
+ "name": "stage_job",
640
+ "title": "Stage managed job for local approval",
641
+ "description": "Validate and persist a managed-job plan without starting any process. Use this when execution tools are unavailable or local operator review is required; launch later with machine-mcp job approve JOB_ID.",
642
+ "availability": "write",
643
+ "annotations": {
644
+ "readOnlyHint": false,
645
+ "destructiveHint": false,
646
+ "idempotentHint": false,
647
+ "openWorldHint": false
648
+ },
649
+ "inputSchema": {
650
+ "type": "object",
651
+ "properties": {
652
+ "name": {
653
+ "type": "string",
654
+ "maxLength": 128
655
+ },
656
+ "steps": {
657
+ "type": "array",
658
+ "minItems": 1,
659
+ "maxItems": 16,
660
+ "items": {
661
+ "type": "object",
662
+ "properties": {
663
+ "name": {
664
+ "type": "string",
665
+ "maxLength": 128
666
+ },
667
+ "argv": {
668
+ "type": "array",
669
+ "minItems": 1,
670
+ "maxItems": 256,
671
+ "items": {
672
+ "type": "string",
673
+ "maxLength": 16384
674
+ }
675
+ },
676
+ "cwd": {
677
+ "type": "string",
678
+ "maxLength": 4096
679
+ },
680
+ "env": {
681
+ "type": "object",
682
+ "maxProperties": 64,
683
+ "additionalProperties": {
684
+ "type": "string",
685
+ "maxLength": 16384
686
+ }
687
+ },
688
+ "env_resources": {
689
+ "type": "object",
690
+ "maxProperties": 32,
691
+ "additionalProperties": {
692
+ "type": "string",
693
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
694
+ }
695
+ },
696
+ "stdin": {
697
+ "type": "string",
698
+ "maxLength": 262144
699
+ },
700
+ "stdin_resource": {
701
+ "type": "string",
702
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
703
+ },
704
+ "timeout_seconds": {
705
+ "type": "integer",
706
+ "minimum": 1,
707
+ "maximum": 3600,
708
+ "default": 600
709
+ },
710
+ "allow_failure": {
711
+ "type": "boolean",
712
+ "default": false
713
+ },
714
+ "capture_output": {
715
+ "type": "string",
716
+ "enum": [
717
+ "redacted",
718
+ "discard"
719
+ ],
720
+ "default": "redacted"
721
+ }
722
+ },
723
+ "required": [
724
+ "argv"
725
+ ],
726
+ "additionalProperties": false
727
+ }
728
+ },
729
+ "finally_steps": {
730
+ "type": "array",
731
+ "maxItems": 16,
732
+ "items": {
733
+ "type": "object",
734
+ "properties": {
735
+ "name": {
736
+ "type": "string",
737
+ "maxLength": 128
738
+ },
739
+ "argv": {
740
+ "type": "array",
741
+ "minItems": 1,
742
+ "maxItems": 256,
743
+ "items": {
744
+ "type": "string",
745
+ "maxLength": 16384
746
+ }
747
+ },
748
+ "cwd": {
749
+ "type": "string",
750
+ "maxLength": 4096
751
+ },
752
+ "env": {
753
+ "type": "object",
754
+ "maxProperties": 64,
755
+ "additionalProperties": {
756
+ "type": "string",
757
+ "maxLength": 16384
758
+ }
759
+ },
760
+ "env_resources": {
761
+ "type": "object",
762
+ "maxProperties": 32,
763
+ "additionalProperties": {
764
+ "type": "string",
765
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
766
+ }
767
+ },
768
+ "stdin": {
769
+ "type": "string",
770
+ "maxLength": 262144
771
+ },
772
+ "stdin_resource": {
773
+ "type": "string",
774
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
775
+ },
776
+ "timeout_seconds": {
777
+ "type": "integer",
778
+ "minimum": 1,
779
+ "maximum": 3600,
780
+ "default": 600
781
+ },
782
+ "allow_failure": {
783
+ "type": "boolean",
784
+ "default": false
785
+ },
786
+ "capture_output": {
787
+ "type": "string",
788
+ "enum": [
789
+ "redacted",
790
+ "discard"
791
+ ],
792
+ "default": "redacted"
793
+ }
794
+ },
795
+ "required": [
796
+ "argv"
797
+ ],
798
+ "additionalProperties": false
799
+ }
800
+ },
801
+ "temporary_files": {
802
+ "type": "array",
803
+ "maxItems": 16,
804
+ "items": {
805
+ "type": "object",
806
+ "properties": {
807
+ "name": {
808
+ "type": "string",
809
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
810
+ },
811
+ "content": {
812
+ "type": "string",
813
+ "maxLength": 262144
814
+ },
815
+ "executable": {
816
+ "type": "boolean",
817
+ "default": false
818
+ }
819
+ },
820
+ "required": [
821
+ "name",
822
+ "content"
823
+ ],
824
+ "additionalProperties": false
825
+ }
826
+ }
827
+ },
828
+ "required": [
829
+ "steps"
830
+ ],
831
+ "additionalProperties": false
832
+ }
833
+ },
834
+ {
835
+ "name": "start_job",
836
+ "title": "Start managed job",
837
+ "description": "Durably accept a detached argv-based job with ordered steps, job-scoped temporary files, and guaranteed-attempt finally steps. The independent local runner continues if the MCP connection disappears. Use {{temp:name}}, {{resource:name}}, env_resources, or stdin_resource; registered resource contents never enter MCP arguments.",
838
+ "availability": "direct-exec",
839
+ "annotations": {
840
+ "readOnlyHint": false,
841
+ "destructiveHint": true,
842
+ "idempotentHint": false,
843
+ "openWorldHint": true
844
+ },
845
+ "inputSchema": {
846
+ "type": "object",
847
+ "properties": {
848
+ "name": {
849
+ "type": "string",
850
+ "maxLength": 128
851
+ },
852
+ "steps": {
853
+ "type": "array",
854
+ "minItems": 1,
855
+ "maxItems": 16,
856
+ "items": {
857
+ "type": "object",
858
+ "properties": {
859
+ "name": {
860
+ "type": "string",
861
+ "maxLength": 128
862
+ },
863
+ "argv": {
864
+ "type": "array",
865
+ "minItems": 1,
866
+ "maxItems": 256,
867
+ "items": {
868
+ "type": "string",
869
+ "maxLength": 16384
870
+ }
871
+ },
872
+ "cwd": {
873
+ "type": "string",
874
+ "maxLength": 4096
875
+ },
876
+ "env": {
877
+ "type": "object",
878
+ "maxProperties": 64,
879
+ "additionalProperties": {
880
+ "type": "string",
881
+ "maxLength": 16384
882
+ }
883
+ },
884
+ "env_resources": {
885
+ "type": "object",
886
+ "maxProperties": 32,
887
+ "additionalProperties": {
888
+ "type": "string",
889
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
890
+ }
891
+ },
892
+ "stdin": {
893
+ "type": "string",
894
+ "maxLength": 262144
895
+ },
896
+ "stdin_resource": {
897
+ "type": "string",
898
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
899
+ },
900
+ "timeout_seconds": {
901
+ "type": "integer",
902
+ "minimum": 1,
903
+ "maximum": 3600,
904
+ "default": 600
905
+ },
906
+ "allow_failure": {
907
+ "type": "boolean",
908
+ "default": false
909
+ },
910
+ "capture_output": {
911
+ "type": "string",
912
+ "enum": [
913
+ "redacted",
914
+ "discard"
915
+ ],
916
+ "default": "redacted"
917
+ }
918
+ },
919
+ "required": [
920
+ "argv"
921
+ ],
922
+ "additionalProperties": false
923
+ }
924
+ },
925
+ "finally_steps": {
926
+ "type": "array",
927
+ "maxItems": 16,
928
+ "items": {
929
+ "type": "object",
930
+ "properties": {
931
+ "name": {
932
+ "type": "string",
933
+ "maxLength": 128
934
+ },
935
+ "argv": {
936
+ "type": "array",
937
+ "minItems": 1,
938
+ "maxItems": 256,
939
+ "items": {
940
+ "type": "string",
941
+ "maxLength": 16384
942
+ }
943
+ },
944
+ "cwd": {
945
+ "type": "string",
946
+ "maxLength": 4096
947
+ },
948
+ "env": {
949
+ "type": "object",
950
+ "maxProperties": 64,
951
+ "additionalProperties": {
952
+ "type": "string",
953
+ "maxLength": 16384
954
+ }
955
+ },
956
+ "env_resources": {
957
+ "type": "object",
958
+ "maxProperties": 32,
959
+ "additionalProperties": {
960
+ "type": "string",
961
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
962
+ }
963
+ },
964
+ "stdin": {
965
+ "type": "string",
966
+ "maxLength": 262144
967
+ },
968
+ "stdin_resource": {
969
+ "type": "string",
970
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
971
+ },
972
+ "timeout_seconds": {
973
+ "type": "integer",
974
+ "minimum": 1,
975
+ "maximum": 3600,
976
+ "default": 600
977
+ },
978
+ "allow_failure": {
979
+ "type": "boolean",
980
+ "default": false
981
+ },
982
+ "capture_output": {
983
+ "type": "string",
984
+ "enum": [
985
+ "redacted",
986
+ "discard"
987
+ ],
988
+ "default": "redacted"
989
+ }
990
+ },
991
+ "required": [
992
+ "argv"
993
+ ],
994
+ "additionalProperties": false
995
+ }
996
+ },
997
+ "temporary_files": {
998
+ "type": "array",
999
+ "maxItems": 16,
1000
+ "items": {
1001
+ "type": "object",
1002
+ "properties": {
1003
+ "name": {
1004
+ "type": "string",
1005
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
1006
+ },
1007
+ "content": {
1008
+ "type": "string",
1009
+ "maxLength": 262144
1010
+ },
1011
+ "executable": {
1012
+ "type": "boolean",
1013
+ "default": false
1014
+ }
1015
+ },
1016
+ "required": [
1017
+ "name",
1018
+ "content"
1019
+ ],
1020
+ "additionalProperties": false
1021
+ }
1022
+ }
1023
+ },
1024
+ "required": [
1025
+ "steps"
1026
+ ],
1027
+ "additionalProperties": false
1028
+ }
1029
+ },
1030
+ {
1031
+ "name": "list_jobs",
1032
+ "title": "List managed jobs",
1033
+ "description": "List recent detached managed jobs and their lifecycle status without returning step output.",
1034
+ "availability": "write",
1035
+ "annotations": {
1036
+ "readOnlyHint": true,
1037
+ "destructiveHint": false,
1038
+ "idempotentHint": true,
1039
+ "openWorldHint": false
1040
+ },
1041
+ "inputSchema": {
1042
+ "type": "object",
1043
+ "properties": {
1044
+ "limit": {
1045
+ "type": "integer",
1046
+ "minimum": 1,
1047
+ "maximum": 50,
1048
+ "default": 20
1049
+ }
1050
+ },
1051
+ "additionalProperties": false
1052
+ }
1053
+ },
1054
+ {
1055
+ "name": "read_job",
1056
+ "title": "Read managed job",
1057
+ "description": "Return one managed job status plus bounded, resource-redacted step results. Registered resource paths and contents are never returned.",
1058
+ "availability": "direct-exec",
1059
+ "annotations": {
1060
+ "readOnlyHint": true,
1061
+ "destructiveHint": false,
1062
+ "idempotentHint": true,
1063
+ "openWorldHint": false
1064
+ },
1065
+ "inputSchema": {
1066
+ "type": "object",
1067
+ "properties": {
1068
+ "job_id": {
1069
+ "type": "string",
1070
+ "pattern": "^job_[A-Za-z0-9_-]{24,}$"
1071
+ }
1072
+ },
1073
+ "required": [
1074
+ "job_id"
1075
+ ],
1076
+ "additionalProperties": false
1077
+ }
1078
+ },
1079
+ {
1080
+ "name": "cancel_job",
1081
+ "title": "Cancel managed job",
1082
+ "description": "Request cancellation of a detached managed job. The runner terminates the active process tree and still attempts declared finally steps and local resource cleanup.",
1083
+ "availability": "write",
1084
+ "annotations": {
1085
+ "readOnlyHint": false,
1086
+ "destructiveHint": true,
1087
+ "idempotentHint": true,
1088
+ "openWorldHint": true
1089
+ },
1090
+ "inputSchema": {
1091
+ "type": "object",
1092
+ "properties": {
1093
+ "job_id": {
1094
+ "type": "string",
1095
+ "pattern": "^job_[A-Za-z0-9_-]{24,}$"
1096
+ }
1097
+ },
1098
+ "required": [
1099
+ "job_id"
1100
+ ],
1101
+ "additionalProperties": false
1102
+ }
1103
+ },
606
1104
  {
607
1105
  "name": "exec_command",
608
1106
  "title": "Execute shell command",
@@ -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.5.0";
6
+ const SERVER_VERSION = "0.6.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";