machine-bridge-mcp 0.6.2 → 0.8.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.
@@ -1,7 +1,6 @@
1
- import readline from "node:readline";
2
1
  import { readFileSync } from "node:fs";
3
2
  import { randomBytes } from "node:crypto";
4
- import { LocalDaemon } from "./daemon.mjs";
3
+ import { LocalRuntime } from "./runtime.mjs";
5
4
  import { classifyOperationalError, createLogger } from "./log.mjs";
6
5
  import {
7
6
  MCP_INSTRUCTIONS,
@@ -20,131 +19,208 @@ const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.js
20
19
 
21
20
  export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
22
21
  const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
23
- const runtime = new LocalDaemon({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
22
+ const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
24
23
  const pending = new Map();
25
24
  let negotiatedVersion = MCP_PROTOCOL_VERSION;
26
25
  let initialized = false;
27
26
 
28
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
29
27
  const writes = new Set();
30
28
  const send = (message) => {
31
29
  if (message === null || message === undefined) return;
32
30
  const line = `${JSON.stringify(message)}\n`;
33
31
  const promise = new Promise((resolvePromise, rejectPromise) => {
34
32
  process.stdout.write(line, (error) => error ? rejectPromise(error) : resolvePromise());
33
+ }).catch((error) => {
34
+ logger.warn("stdio output write failed", { error_class: classifyOperationalError(error) });
35
35
  }).finally(() => writes.delete(promise));
36
36
  writes.add(promise);
37
37
  };
38
38
 
39
- rl.on("line", (line) => {
40
- if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
41
- send(rpcError(null, -32600, "JSON-RPC message exceeds maximum size"));
42
- return;
43
- }
44
- void handleLine(line).catch((error) => {
45
- logger.error("stdio request handler failed", { error_class: classifyOperationalError(error) });
46
- });
47
- });
48
-
49
- await new Promise((resolvePromise, rejectPromise) => {
50
- rl.once("close", resolvePromise);
51
- rl.once("error", rejectPromise);
39
+ await consumeBoundedJsonLines(process.stdin, {
40
+ maxLineBytes: MAX_LINE_BYTES,
41
+ onOversize() { send(rpcError(null, -32600, "JSON-RPC message exceeds maximum size")); },
42
+ onLine(line) {
43
+ void handleLine(line).catch((error) => {
44
+ logger.error("stdio request handler failed", { error_class: classifyOperationalError(error) });
45
+ });
46
+ },
52
47
  });
53
48
  for (const callId of pending.values()) runtime.cancelCall(callId, "stdio input closed");
54
49
  await Promise.allSettled([...writes]);
55
50
  runtime.stop();
56
51
 
57
52
  async function handleLine(line) {
58
- let message;
59
- try { message = JSON.parse(line); } catch {
60
- send(rpcError(null, -32700, "Parse error"));
61
- return;
62
- }
63
- if (!isJsonRpcMessage(message)) {
64
- send(rpcError(null, -32600, "Invalid JSON-RPC message"));
53
+ const message = parseJsonRpcLine(line, send);
54
+ if (!message || typeof message.method !== "string") return;
55
+ if (handleControlMessage(message)) return;
56
+ if (!initialized) {
57
+ send(rpcError(message.id, -32002, "Server is not initialized"));
65
58
  return;
66
59
  }
67
- if (typeof message.method !== "string") return;
60
+ await handleInitializedMessage(message);
61
+ }
68
62
 
63
+ function handleControlMessage(message) {
69
64
  if (message.method === "initialize") {
70
65
  const requested = asObject(message.params).protocolVersion;
71
66
  negotiatedVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
72
67
  ? requested
73
68
  : MCP_PROTOCOL_VERSION;
74
69
  initialized = true;
75
- send(rpcResult(message.id, {
76
- protocolVersion: negotiatedVersion,
77
- capabilities: { tools: { listChanged: false }, logging: {} },
78
- serverInfo: {
79
- name: SERVER_NAME,
80
- title: "Machine Bridge MCP",
81
- version: PACKAGE_VERSION,
82
- description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
83
- },
84
- instructions: MCP_INSTRUCTIONS,
85
- }));
86
- return;
70
+ send(rpcResult(message.id, initializationResult(negotiatedVersion)));
71
+ return true;
87
72
  }
88
-
89
- if (message.method === "notifications/initialized") return;
73
+ if (message.method === "notifications/initialized") return true;
90
74
  if (message.method === "notifications/cancelled") {
91
75
  const requestId = asObject(message.params).requestId;
92
- const key = jsonRpcIdKey(requestId);
93
- const callId = pending.get(key);
76
+ const callId = pending.get(jsonRpcIdKey(requestId));
94
77
  if (callId) runtime.cancelCall(callId, "MCP cancellation notification");
95
- return;
78
+ return true;
96
79
  }
97
- if (message.method === "logging/setLevel") {
80
+ if (message.method === "logging/setLevel" || message.method === "ping") {
98
81
  send(rpcResult(message.id, {}));
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ async function handleInitializedMessage(message) {
88
+ if (message.method === "tools/list") {
89
+ send(rpcResult(message.id, { tools: toolsForPolicy(policy) }));
99
90
  return;
100
91
  }
101
- if (message.method === "ping") {
102
- send(rpcResult(message.id, {}));
92
+ if (message.method === "tools/call") {
93
+ await handleToolCall(message);
103
94
  return;
104
95
  }
105
- if (!initialized) {
106
- send(rpcError(message.id, -32002, "Server is not initialized"));
96
+ send(rpcError(message.id, -32601, `Method not found: ${message.method}`));
97
+ }
98
+
99
+ async function handleToolCall(message) {
100
+ if (!("id" in message) || message.id === null) {
101
+ send(rpcError(null, -32600, "tools/call requires a non-null request id"));
107
102
  return;
108
103
  }
109
- if (message.method === "tools/list") {
110
- send(rpcResult(message.id, { tools: toolsForPolicy(policy) }));
104
+ const params = asObject(message.params);
105
+ const name = typeof params.name === "string" ? params.name : "";
106
+ if (!name) {
107
+ send(rpcError(message.id, -32602, "tools/call requires a tool name"));
111
108
  return;
112
109
  }
113
- if (message.method === "tools/call") {
114
- const params = asObject(message.params);
115
- const name = typeof params.name === "string" ? params.name : "";
116
- if (!name) {
117
- send(rpcError(message.id, -32602, "tools/call requires a tool name"));
118
- return;
119
- }
120
- const args = asObject(params.arguments);
121
- const callId = `stdio_${randomBytes(16).toString("hex")}`;
122
- const key = jsonRpcIdKey(message.id);
123
- if (key && pending.has(key)) {
124
- send(rpcError(message.id, -32600, "Duplicate in-flight JSON-RPC request id"));
125
- return;
126
- }
127
- if (key) pending.set(key, callId);
128
- const started = Date.now();
129
- logger.debug("tool call started", { call_id: callId.slice(0, 20), tool: name });
130
- try {
131
- const result = await runtime.executeTool(name, args, { callId });
132
- send(rpcResult(message.id, toolResult(result)));
133
- const durationMs = Date.now() - started;
134
- logger.debug(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
135
- } catch (error) {
136
- const safeError = runtime.safeErrorMessage(error);
137
- send(rpcResult(message.id, toolResult({ error: safeError }, true)));
138
- const durationMs = Date.now() - started;
139
- logger.debug("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
140
- } finally {
141
- if (key) pending.delete(key);
142
- runtime.finishCall(callId);
143
- }
110
+ const args = asObject(params.arguments);
111
+ const callId = `stdio_${randomBytes(16).toString("hex")}`;
112
+ const key = jsonRpcIdKey(message.id);
113
+ if (key && pending.has(key)) {
114
+ send(rpcError(message.id, -32600, "Duplicate in-flight JSON-RPC request id"));
144
115
  return;
145
116
  }
146
- send(rpcError(message.id, -32601, `Method not found: ${message.method}`));
117
+ if (key) pending.set(key, callId);
118
+ const started = Date.now();
119
+ logger.debug("tool call started", { call_id: callId.slice(0, 20), tool: name });
120
+ try {
121
+ const result = await runtime.executeTool(name, args, { callId });
122
+ send(rpcResult(message.id, toolResult(result)));
123
+ const durationMs = Date.now() - started;
124
+ logger.debug(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
125
+ } catch (error) {
126
+ const safeError = runtime.safeErrorMessage(error, args);
127
+ send(rpcResult(message.id, toolResult({ error: safeError }, true)));
128
+ const durationMs = Date.now() - started;
129
+ logger.debug("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
130
+ } finally {
131
+ if (key) pending.delete(key);
132
+ runtime.finishCall(callId);
133
+ }
134
+ }
135
+
136
+ }
137
+
138
+ function parseJsonRpcLine(line, send) {
139
+ let message;
140
+ try { message = JSON.parse(line); } catch {
141
+ send(rpcError(null, -32700, "Parse error"));
142
+ return null;
143
+ }
144
+ if (!isJsonRpcMessage(message)) {
145
+ send(rpcError(null, -32600, "Invalid JSON-RPC message"));
146
+ return null;
147
147
  }
148
+ return message;
149
+ }
150
+
151
+ function initializationResult(protocolVersion) {
152
+ return {
153
+ protocolVersion,
154
+ capabilities: { tools: { listChanged: false }, logging: {} },
155
+ serverInfo: {
156
+ name: SERVER_NAME,
157
+ title: "Machine Bridge MCP",
158
+ version: PACKAGE_VERSION,
159
+ description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
160
+ },
161
+ instructions: MCP_INSTRUCTIONS,
162
+ };
163
+ }
164
+
165
+ function consumeBoundedJsonLines(stream, { maxLineBytes, onLine, onOversize }) {
166
+ return new Promise((resolvePromise, rejectPromise) => {
167
+ let chunks = [];
168
+ let bytes = 0;
169
+ let discarding = false;
170
+ let settled = false;
171
+
172
+ const resetLine = () => { chunks = []; bytes = 0; };
173
+ const emitLine = () => {
174
+ let line = bytes ? Buffer.concat(chunks, bytes) : Buffer.alloc(0);
175
+ if (line.length && line[line.length - 1] === 13) line = line.subarray(0, line.length - 1);
176
+ resetLine();
177
+ onLine(line.toString("utf8"));
178
+ };
179
+ const onData = (input) => {
180
+ const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
181
+ let offset = 0;
182
+ while (offset < buffer.length) {
183
+ const newline = buffer.indexOf(10, offset);
184
+ const end = newline === -1 ? buffer.length : newline;
185
+ const segment = buffer.subarray(offset, end);
186
+ if (discarding) {
187
+ if (newline !== -1) discarding = false;
188
+ } else if (bytes + segment.length > maxLineBytes) {
189
+ resetLine();
190
+ onOversize();
191
+ if (newline === -1) discarding = true;
192
+ } else {
193
+ if (segment.length) chunks.push(segment);
194
+ bytes += segment.length;
195
+ if (newline !== -1) emitLine();
196
+ }
197
+ offset = newline === -1 ? buffer.length : newline + 1;
198
+ }
199
+ };
200
+ const cleanup = () => {
201
+ stream.off("data", onData);
202
+ stream.off("end", onEnd);
203
+ stream.off("close", onEnd);
204
+ stream.off("error", onError);
205
+ };
206
+ const onEnd = () => {
207
+ if (settled) return;
208
+ settled = true;
209
+ cleanup();
210
+ if (!discarding && bytes > 0) emitLine();
211
+ resolvePromise();
212
+ };
213
+ const onError = (error) => {
214
+ if (settled) return;
215
+ settled = true;
216
+ cleanup();
217
+ rejectPromise(error);
218
+ };
219
+ stream.on("data", onData);
220
+ stream.once("end", onEnd);
221
+ stream.once("close", onEnd);
222
+ stream.once("error", onError);
223
+ });
148
224
  }
149
225
 
150
226
  function isJsonRpcMessage(value) {
@@ -12,6 +12,7 @@
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
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.",
15
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.",
16
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.",
17
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.",
@@ -638,7 +638,7 @@
638
638
  {
639
639
  "name": "generate_ssh_key_resource",
640
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. The private key content is never returned; only local paths, public fingerprint, permissions, and registration status are reported. Available only under canonical full policy.",
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
642
  "availability": "full",
643
643
  "annotations": {
644
644
  "readOnlyHint": false,
@@ -660,6 +660,10 @@
660
660
  "comment": {
661
661
  "type": "string",
662
662
  "maxLength": 256
663
+ },
664
+ "expose_paths": {
665
+ "type": "boolean",
666
+ "description": "Return local private/public key paths. Defaults to false."
663
667
  }
664
668
  },
665
669
  "required": [
@@ -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.2";
6
+ const SERVER_VERSION = "0.8.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";
@@ -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>;
@@ -292,13 +293,20 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
292
293
  }
293
294
 
294
295
  async webSocketClose(ws: WebSocket): Promise<void> {
296
+ await this.cleanupDaemonSocket(ws, "daemon disconnected");
297
+ }
298
+
299
+ async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
300
+ console.error(JSON.stringify({ level: "warn", message: "daemon_websocket_error", error_class: workerErrorClass(error) }));
301
+ await this.cleanupDaemonSocket(ws, "daemon transport error");
302
+ }
303
+
304
+ private async cleanupDaemonSocket(ws: WebSocket, message: string): Promise<void> {
295
305
  await this.scheduleCandidateAlarm();
296
- const attachment = this.daemonAttachment(ws);
297
- if (!attachment) return;
298
306
  for (const [id, pending] of this.pending) {
299
307
  if (pending.socket !== ws) continue;
300
308
  clearTimeout(pending.timeout);
301
- pending.reject(new Error("daemon disconnected"));
309
+ pending.reject(new Error(message));
302
310
  this.pending.delete(id);
303
311
  }
304
312
  }
@@ -361,6 +369,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
361
369
  if (request.method === "ping") return rpcResult(request.id, {});
362
370
  if (request.method === "tools/list") return rpcResult(request.id, { tools: this.allTools() });
363
371
  if (request.method === "tools/call") {
372
+ if (request.id === undefined || request.id === null) return rpcError(null, -32600, "tools/call requires a non-null request id");
364
373
  const params = asObject(request.params);
365
374
  const name = requiredString(params, "name");
366
375
  const args = asObject(params.arguments);
@@ -376,13 +385,22 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
376
385
 
377
386
  private async callTool(name: string, args: Record<string, unknown>, base: string, requestKey?: string): Promise<unknown> {
378
387
  if (name === "server_info") {
388
+ const daemon = this.daemonStatus(true);
389
+ const tools = this.allTools().map((tool) => tool.name);
379
390
  return {
380
391
  name: SERVER_NAME,
381
392
  version: SERVER_VERSION,
382
393
  mcp_url: `${base}/mcp`,
383
394
  oauth: this.authorizationServerMetadata(base),
384
- daemon: this.daemonStatus(true),
385
- tools: this.allTools().map((tool) => tool.name),
395
+ daemon,
396
+ tools,
397
+ tool_delivery: {
398
+ full_profile_scope: "local-daemon-and-relay-advertisement",
399
+ daemon_advertised_tool_count: daemon.tool_count,
400
+ relay_advertised_tool_count: tools.length,
401
+ host_exposed_tools_known_to_server: false,
402
+ host_may_expose_subset: true,
403
+ },
386
404
  };
387
405
  }
388
406
  if (workspaceTools.some((tool) => tool.name === name)) {
@@ -657,13 +675,15 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
657
675
  if (redirectUris.length > 5) {
658
676
  return json({ error: "invalid_client_metadata", error_description: "redirect_uris must contain at most 5 entries" }, 400);
659
677
  }
660
- const normalized = [...new Set(redirectUris.map((item) => String(item)))];
661
- if (normalized.some((item) => item.length > 1024)) {
678
+ const suppliedRedirectUris = redirectUris.map((item) => String(item));
679
+ if (suppliedRedirectUris.some((item) => item.length > 1024)) {
662
680
  return json({ error: "invalid_client_metadata", error_description: "redirect_uri is too long" }, 400);
663
681
  }
664
- if (!normalized.every(isAllowedRedirectUri)) {
665
- return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be https or local http" }, 400);
682
+ const canonicalRedirectUris = suppliedRedirectUris.map(normalizeRedirectUri);
683
+ if (canonicalRedirectUris.some((item) => item === null)) {
684
+ return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be canonicalizable https or local http URLs without credentials or fragments" }, 400);
666
685
  }
686
+ const normalized = [...new Set(canonicalRedirectUris as string[])];
667
687
 
668
688
  return this.withOAuthLock(async () => {
669
689
  const store = await this.oauthStore();
@@ -725,7 +745,11 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
725
745
  .filter(([key]) => AUTHORIZATION_FIELDS.has(key))
726
746
  .map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(String(value))}">`)
727
747
  .join("\n");
728
- const resource = authorization?.requestedResource ?? String(submitted?.resource ?? url.searchParams.get("resource") ?? `${base}/mcp`);
748
+ const resource = normalizeDisplayText(
749
+ authorization?.requestedResource ?? String(submitted?.resource ?? url.searchParams.get("resource") ?? `${base}/mcp`),
750
+ 1024,
751
+ `${base}/mcp`,
752
+ );
729
753
  const clientBlock = authorization
730
754
  ? `<p><strong>Client:</strong> ${escapeHtml(authorization.client.client_name)}</p>
731
755
  <p><strong>Redirect URI:</strong> <code>${escapeHtml(authorization.redirectUri)}</code></p>`
@@ -971,7 +995,7 @@ function isFreshDaemonCandidate(connectedAt: string): boolean {
971
995
 
972
996
  function sanitizeMetadataText(value: unknown, maxLength: number): string | undefined {
973
997
  if (typeof value !== "string") return undefined;
974
- const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
998
+ const normalized = value.replace(UNSAFE_DISPLAY_CONTROLS, " ").replace(/\s+/g, " ").trim();
975
999
  return normalized ? normalized.slice(0, maxLength) : undefined;
976
1000
  }
977
1001
 
@@ -1272,15 +1296,15 @@ async function pkceS256(verifier: string): Promise<string> {
1272
1296
  return base64Url(new Uint8Array(digest));
1273
1297
  }
1274
1298
 
1275
- function isAllowedRedirectUri(value: string): boolean {
1299
+ function normalizeRedirectUri(value: string): string | null {
1276
1300
  try {
1277
1301
  const url = new URL(value);
1278
- if (url.username || url.password || url.hash) return false;
1279
- if (url.protocol === "https:" && url.hostname) return true;
1280
- if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return true;
1281
- return false;
1302
+ if (url.username || url.password || url.hash) return null;
1303
+ if (url.protocol === "https:" && url.hostname) return url.toString();
1304
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return url.toString();
1305
+ return null;
1282
1306
  } catch {
1283
- return false;
1307
+ return null;
1284
1308
  }
1285
1309
  }
1286
1310
 
@@ -1359,9 +1383,9 @@ function searchParamsObject(params: URLSearchParams): Record<string, unknown> {
1359
1383
  return out;
1360
1384
  }
1361
1385
 
1362
- function normalizeDisplayText(value: string, maxLength: number): string {
1363
- const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
1364
- return (normalized || "MCP Client").slice(0, maxLength);
1386
+ function normalizeDisplayText(value: string, maxLength: number, fallback = "MCP Client"): string {
1387
+ const normalized = value.replace(UNSAFE_DISPLAY_CONTROLS, " ").replace(/\s+/g, " ").trim();
1388
+ return (normalized || fallback).slice(0, maxLength);
1365
1389
  }
1366
1390
 
1367
1391
  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": {