machine-bridge-mcp 0.6.0 → 0.7.1

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.
@@ -9,7 +9,7 @@ export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((v
9
9
 
10
10
 
11
11
  export const DEFAULT_POLICY_PROFILE = "full";
12
- export const DEFAULT_POLICY_REVISION = 2;
12
+ export const DEFAULT_POLICY_REVISION = 3;
13
13
 
14
14
  export const POLICY_PROFILES = Object.freeze({
15
15
  review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
@@ -34,8 +34,9 @@ export function normalizePolicy(policy = {}) {
34
34
  : "off";
35
35
  const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
36
36
  const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
37
- return {
38
- profile: typeof policy.profile === "string" && policy.profile ? policy.profile : "custom",
37
+ const requestedProfile = typeof policy.profile === "string" && policy.profile ? policy.profile : "custom";
38
+ const normalized = {
39
+ profile: requestedProfile,
39
40
  origin,
40
41
  revision,
41
42
  allowWrite: policy.allowWrite === true,
@@ -45,6 +46,38 @@ export function normalizePolicy(policy = {}) {
45
46
  minimalEnv: policy.minimalEnv !== false,
46
47
  exposeAbsolutePaths: policy.exposeAbsolutePaths === true,
47
48
  };
49
+ if (POLICY_PROFILES[requestedProfile]) {
50
+ const canonical = POLICY_PROFILES[requestedProfile];
51
+ normalized.allowWrite = canonical.allowWrite;
52
+ normalized.allowExec = canonical.execMode !== "off";
53
+ normalized.execMode = canonical.execMode;
54
+ normalized.unrestrictedPaths = canonical.unrestrictedPaths;
55
+ normalized.minimalEnv = canonical.minimalEnv;
56
+ normalized.exposeAbsolutePaths = canonical.exposeAbsolutePaths;
57
+ }
58
+ return normalized;
59
+ }
60
+
61
+ export function isCanonicalFullPolicy(policy = {}) {
62
+ return policyCapabilitiesEqual(normalizePolicy(policy), POLICY_PROFILES.full);
63
+ }
64
+
65
+ export function assertCanonicalFullPolicy(policy = {}) {
66
+ const normalized = normalizePolicy(policy);
67
+ if (!isCanonicalFullPolicy(normalized) || normalized.profile !== "full") {
68
+ throw new Error("full profile invariant failed: full must enable writes, shell execution, unrestricted paths, full parent environment, and absolute paths");
69
+ }
70
+ const exposed = toolsForPolicy(normalized);
71
+ if (exposed.length !== catalog.length) throw new Error("full profile invariant failed: complete tool catalog is not exposed");
72
+ return normalized;
73
+ }
74
+
75
+ function policyCapabilitiesEqual(left, right) {
76
+ return left.allowWrite === right.allowWrite
77
+ && left.execMode === right.execMode
78
+ && left.unrestrictedPaths === right.unrestrictedPaths
79
+ && left.minimalEnv === right.minimalEnv
80
+ && left.exposeAbsolutePaths === right.exposeAbsolutePaths;
48
81
  }
49
82
 
50
83
  export function toolsForPolicy(policy = {}) {
@@ -92,6 +125,7 @@ function isAvailable(availability, policy) {
92
125
  if (availability === "write") return policy.allowWrite;
93
126
  if (availability === "direct-exec") return policy.execMode === "direct" || policy.execMode === "shell";
94
127
  if (availability === "shell-exec") return policy.execMode === "shell";
128
+ if (availability === "full") return policy.profile === "full" && isCanonicalFullPolicy(policy);
95
129
  return false;
96
130
  }
97
131
 
@@ -11,14 +11,17 @@
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
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
+ "A named full profile is canonical: it always enables writes, shell execution, unrestricted paths, the full parent environment, absolute paths, and the complete tool catalog. Any explicit narrowing is stored as custom.",
15
+ "The full catalog is what the local daemon and relay advertise. A connector host or platform can expose only a subset to a particular session; Machine Bridge cannot observe, authorize, or override that host-side filtering.",
14
16
  "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.",
17
+ "Never request or return secret-file contents when a local resource alias can be used. Resources are registered through the local machine-mcp CLI; under canonical full policy, generate_ssh_key_resource can generate and register an Ed25519 key without returning private content and may be injected by path, stdin, or environment without entering MCP arguments.",
16
18
  "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
19
  "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
20
  "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
21
  "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
22
  "Do not use shell encoding, renaming, or alternate tools to bypass host or platform safety policy.",
21
23
  "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.",
24
+ "Per-tool success, failure, cancellation, and timing events are debug-only; default logs contain lifecycle and infrastructure events, not a tool-call transcript.",
22
25
  "Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
23
26
  ]
24
27
  }
@@ -635,6 +635,43 @@
635
635
  "additionalProperties": false
636
636
  }
637
637
  },
638
+ {
639
+ "name": "generate_ssh_key_resource",
640
+ "title": "Generate SSH key resource",
641
+ "description": "Generate or reuse an Ed25519 SSH key pair on the local machine and register the private key file as a local resource alias. Private key content is never returned. Local paths are omitted unless expose_paths=true; fingerprint, permissions, and registration status are returned. Available only under canonical full policy.",
642
+ "availability": "full",
643
+ "annotations": {
644
+ "readOnlyHint": false,
645
+ "destructiveHint": false,
646
+ "idempotentHint": true,
647
+ "openWorldHint": false
648
+ },
649
+ "inputSchema": {
650
+ "type": "object",
651
+ "properties": {
652
+ "name": {
653
+ "type": "string",
654
+ "pattern": "^[a-z][a-z0-9._-]{0,63}$"
655
+ },
656
+ "path": {
657
+ "type": "string",
658
+ "maxLength": 4096
659
+ },
660
+ "comment": {
661
+ "type": "string",
662
+ "maxLength": 256
663
+ },
664
+ "expose_paths": {
665
+ "type": "boolean",
666
+ "description": "Return local private/public key paths. Defaults to false."
667
+ }
668
+ },
669
+ "required": [
670
+ "name"
671
+ ],
672
+ "additionalProperties": false
673
+ }
674
+ },
638
675
  {
639
676
  "name": "stage_job",
640
677
  "title": "Stage managed job for local approval",
@@ -3,7 +3,7 @@ import toolCatalog from "../shared/tool-catalog.json";
3
3
  import serverMetadata from "../shared/server-metadata.json";
4
4
 
5
5
  const SERVER_NAME = String(serverMetadata.name);
6
- const SERVER_VERSION = "0.6.0";
6
+ const SERVER_VERSION = "0.7.1";
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";
@@ -27,6 +27,7 @@ const AUTH_FAILURE_WINDOW_SECONDS = 10 * 60;
27
27
  const AUTH_BLOCK_SECONDS = 15 * 60;
28
28
  const AUTH_FAILURE_LIMIT = 10;
29
29
  const AUTHORIZATION_FIELDS = new Set(["response_type", "client_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "resource", "state"]);
30
+ const UNSAFE_DISPLAY_CONTROLS = /[\u0000-\u001f\u007f\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g;
30
31
 
31
32
  interface BridgeEnv {
32
33
  BRIDGE: DurableObjectNamespace<BridgeRoom>;
@@ -361,6 +362,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
361
362
  if (request.method === "ping") return rpcResult(request.id, {});
362
363
  if (request.method === "tools/list") return rpcResult(request.id, { tools: this.allTools() });
363
364
  if (request.method === "tools/call") {
365
+ if (request.id === undefined || request.id === null) return rpcError(null, -32600, "tools/call requires a non-null request id");
364
366
  const params = asObject(request.params);
365
367
  const name = requiredString(params, "name");
366
368
  const args = asObject(params.arguments);
@@ -376,13 +378,22 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
376
378
 
377
379
  private async callTool(name: string, args: Record<string, unknown>, base: string, requestKey?: string): Promise<unknown> {
378
380
  if (name === "server_info") {
381
+ const daemon = this.daemonStatus(true);
382
+ const tools = this.allTools().map((tool) => tool.name);
379
383
  return {
380
384
  name: SERVER_NAME,
381
385
  version: SERVER_VERSION,
382
386
  mcp_url: `${base}/mcp`,
383
387
  oauth: this.authorizationServerMetadata(base),
384
- daemon: this.daemonStatus(true),
385
- tools: this.allTools().map((tool) => tool.name),
388
+ daemon,
389
+ tools,
390
+ tool_delivery: {
391
+ full_profile_scope: "local-daemon-and-relay-advertisement",
392
+ daemon_advertised_tool_count: daemon.tool_count,
393
+ relay_advertised_tool_count: tools.length,
394
+ host_exposed_tools_known_to_server: false,
395
+ host_may_expose_subset: true,
396
+ },
386
397
  };
387
398
  }
388
399
  if (workspaceTools.some((tool) => tool.name === name)) {
@@ -657,13 +668,15 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
657
668
  if (redirectUris.length > 5) {
658
669
  return json({ error: "invalid_client_metadata", error_description: "redirect_uris must contain at most 5 entries" }, 400);
659
670
  }
660
- const normalized = [...new Set(redirectUris.map((item) => String(item)))];
661
- if (normalized.some((item) => item.length > 1024)) {
671
+ const suppliedRedirectUris = redirectUris.map((item) => String(item));
672
+ if (suppliedRedirectUris.some((item) => item.length > 1024)) {
662
673
  return json({ error: "invalid_client_metadata", error_description: "redirect_uri is too long" }, 400);
663
674
  }
664
- if (!normalized.every(isAllowedRedirectUri)) {
665
- return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be https or local http" }, 400);
675
+ const canonicalRedirectUris = suppliedRedirectUris.map(normalizeRedirectUri);
676
+ if (canonicalRedirectUris.some((item) => item === null)) {
677
+ return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be canonicalizable https or local http URLs without credentials or fragments" }, 400);
666
678
  }
679
+ const normalized = [...new Set(canonicalRedirectUris as string[])];
667
680
 
668
681
  return this.withOAuthLock(async () => {
669
682
  const store = await this.oauthStore();
@@ -725,7 +738,11 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
725
738
  .filter(([key]) => AUTHORIZATION_FIELDS.has(key))
726
739
  .map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(String(value))}">`)
727
740
  .join("\n");
728
- const resource = authorization?.requestedResource ?? String(submitted?.resource ?? url.searchParams.get("resource") ?? `${base}/mcp`);
741
+ const resource = normalizeDisplayText(
742
+ authorization?.requestedResource ?? String(submitted?.resource ?? url.searchParams.get("resource") ?? `${base}/mcp`),
743
+ 1024,
744
+ `${base}/mcp`,
745
+ );
729
746
  const clientBlock = authorization
730
747
  ? `<p><strong>Client:</strong> ${escapeHtml(authorization.client.client_name)}</p>
731
748
  <p><strong>Redirect URI:</strong> <code>${escapeHtml(authorization.redirectUri)}</code></p>`
@@ -971,7 +988,7 @@ function isFreshDaemonCandidate(connectedAt: string): boolean {
971
988
 
972
989
  function sanitizeMetadataText(value: unknown, maxLength: number): string | undefined {
973
990
  if (typeof value !== "string") return undefined;
974
- const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
991
+ const normalized = value.replace(UNSAFE_DISPLAY_CONTROLS, " ").replace(/\s+/g, " ").trim();
975
992
  return normalized ? normalized.slice(0, maxLength) : undefined;
976
993
  }
977
994
 
@@ -991,6 +1008,12 @@ function daemonPolicyAllows(availability: unknown, policy: Record<string, unknow
991
1008
  if (availability === "write") return policy.allowWrite === true;
992
1009
  if (availability === "direct-exec") return policy.execMode === "direct" || policy.execMode === "shell";
993
1010
  if (availability === "shell-exec") return policy.execMode === "shell";
1011
+ if (availability === "full") return policy.profile === "full"
1012
+ && policy.allowWrite === true
1013
+ && policy.execMode === "shell"
1014
+ && policy.unrestrictedPaths === true
1015
+ && policy.minimalEnv === false
1016
+ && policy.exposeAbsolutePaths === true;
994
1017
  return false;
995
1018
  }
996
1019
 
@@ -1266,15 +1289,15 @@ async function pkceS256(verifier: string): Promise<string> {
1266
1289
  return base64Url(new Uint8Array(digest));
1267
1290
  }
1268
1291
 
1269
- function isAllowedRedirectUri(value: string): boolean {
1292
+ function normalizeRedirectUri(value: string): string | null {
1270
1293
  try {
1271
1294
  const url = new URL(value);
1272
- if (url.username || url.password || url.hash) return false;
1273
- if (url.protocol === "https:" && url.hostname) return true;
1274
- if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return true;
1275
- return false;
1295
+ if (url.username || url.password || url.hash) return null;
1296
+ if (url.protocol === "https:" && url.hostname) return url.toString();
1297
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return url.toString();
1298
+ return null;
1276
1299
  } catch {
1277
- return false;
1300
+ return null;
1278
1301
  }
1279
1302
  }
1280
1303
 
@@ -1353,9 +1376,9 @@ function searchParamsObject(params: URLSearchParams): Record<string, unknown> {
1353
1376
  return out;
1354
1377
  }
1355
1378
 
1356
- function normalizeDisplayText(value: string, maxLength: number): string {
1357
- const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
1358
- return (normalized || "MCP Client").slice(0, maxLength);
1379
+ function normalizeDisplayText(value: string, maxLength: number, fallback = "MCP Client"): string {
1380
+ const normalized = value.replace(UNSAFE_DISPLAY_CONTROLS, " ").replace(/\s+/g, " ").trim();
1381
+ return (normalized || fallback).slice(0, maxLength);
1359
1382
  }
1360
1383
 
1361
1384
  function escapeHtml(value: string): string {
package/wrangler.jsonc CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "./node_modules/wrangler/config-schema.json",
3
3
  "name": "machine-bridge-mcp",
4
4
  "main": "src/worker/index.ts",
5
- "compatibility_date": "2026-07-09",
5
+ "compatibility_date": "2026-07-11",
6
6
  "compatibility_flags": ["nodejs_compat"],
7
7
  "workers_dev": true,
8
8
  "durable_objects": {