u-foo 3.0.17 → 3.0.19

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.
@@ -7,7 +7,10 @@ const { spawn } = require("child_process");
7
7
 
8
8
  const { getUfooPaths } = require("../../coordination/state/paths");
9
9
  const { isRunning, socketPath } = require("./index");
10
- const controlPlane = require("./controlPlaneService");
10
+ const {
11
+ MCP_EXPOSED_SHARED_TOOLS,
12
+ createLocalProjectRuntimeGateway,
13
+ } = require("./projectRuntimeGateway");
11
14
  const {
12
15
  normalizeProjectRoot,
13
16
  resolveGlobalControllerProjectRoot,
@@ -30,15 +33,8 @@ const {
30
33
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
31
34
  const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
32
35
 
33
- const EXPOSED_SHARED_TOOLS = Object.freeze([
34
- "read_project_registry",
35
- "read_bus_summary",
36
- "read_prompt_history",
37
- "read_open_decisions",
38
- "list_agents",
39
- "dispatch_message",
40
- "ack_bus",
41
- ]);
36
+ const EXPOSED_SHARED_TOOLS = MCP_EXPOSED_SHARED_TOOLS;
37
+ const DEFAULT_PROJECT_RUNTIME_GATEWAY = createLocalProjectRuntimeGateway();
42
38
 
43
39
  const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
44
40
  {
@@ -65,6 +61,10 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
65
61
  scoped_nickname: { type: "string" },
66
62
  launch_mode: { type: "string" },
67
63
  capabilities: { type: "object", additionalProperties: true },
64
+ client_instance_id: {
65
+ type: "string",
66
+ description: "Stable host-local instance id used to recover the same registration after transport restarts.",
67
+ },
68
68
  },
69
69
  additionalProperties: false,
70
70
  },
@@ -75,10 +75,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
75
75
  description: "Refresh a registered agent heartbeat in its project bus.",
76
76
  input_schema: {
77
77
  type: "object",
78
- required: ["project_root", "subscriber"],
78
+ required: ["project_root", "subscriber", "agent_handle"],
79
79
  properties: {
80
80
  project_root: { type: "string" },
81
81
  subscriber: { type: "string" },
82
+ agent_handle: { type: "string" },
82
83
  },
83
84
  additionalProperties: false,
84
85
  },
@@ -89,10 +90,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
89
90
  description: "Publish the caller agent activity state in its project bus metadata.",
90
91
  input_schema: {
91
92
  type: "object",
92
- required: ["project_root", "subscriber", "activity_state"],
93
+ required: ["project_root", "subscriber", "agent_handle", "activity_state"],
93
94
  properties: {
94
95
  project_root: { type: "string" },
95
96
  subscriber: { type: "string" },
97
+ agent_handle: { type: "string" },
96
98
  activity_state: { type: "string" },
97
99
  detail: { type: "string" },
98
100
  since: { type: "string" },
@@ -106,10 +108,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
106
108
  description: "Update the caller agent nickname or MCP metadata in its project bus.",
107
109
  input_schema: {
108
110
  type: "object",
109
- required: ["project_root", "subscriber"],
111
+ required: ["project_root", "subscriber", "agent_handle"],
110
112
  properties: {
111
113
  project_root: { type: "string" },
112
114
  subscriber: { type: "string" },
115
+ agent_handle: { type: "string" },
113
116
  nickname: { type: "string" },
114
117
  metadata: { type: "object", additionalProperties: true },
115
118
  },
@@ -122,10 +125,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
122
125
  description: "Read pending bus messages for the caller-owned subscriber queue without acknowledging them.",
123
126
  input_schema: {
124
127
  type: "object",
125
- required: ["project_root", "subscriber"],
128
+ required: ["project_root", "subscriber", "agent_handle"],
126
129
  properties: {
127
130
  project_root: { type: "string" },
128
131
  subscriber: { type: "string" },
132
+ agent_handle: { type: "string" },
129
133
  limit: { type: "integer", minimum: 1 },
130
134
  },
131
135
  additionalProperties: false,
@@ -137,10 +141,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
137
141
  description: "Keep a Codex App-compatible MCP tool call pending until the caller-owned bus queue receives messages after after_seq or the wait reaches its timeout.",
138
142
  input_schema: {
139
143
  type: "object",
140
- required: ["project_root", "subscriber"],
144
+ required: ["project_root", "subscriber", "agent_handle"],
141
145
  properties: {
142
146
  project_root: { type: "string" },
143
147
  subscriber: { type: "string" },
148
+ agent_handle: { type: "string" },
144
149
  after_seq: {
145
150
  type: "integer",
146
151
  minimum: 0,
@@ -165,10 +170,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
165
170
  description: "Queue an agent task status report through the project daemon report-control queue.",
166
171
  input_schema: {
167
172
  type: "object",
168
- required: ["project_root", "subscriber", "task_id", "phase"],
173
+ required: ["project_root", "subscriber", "agent_handle", "task_id", "phase"],
169
174
  properties: {
170
175
  project_root: { type: "string" },
171
176
  subscriber: { type: "string" },
177
+ agent_handle: { type: "string" },
172
178
  task_id: { type: "string" },
173
179
  phase: { type: "string", enum: ["start", "progress", "done", "error"] },
174
180
  message: { type: "string" },
@@ -186,10 +192,11 @@ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
186
192
  description: "Mark an MCP-registered agent inactive in its project bus.",
187
193
  input_schema: {
188
194
  type: "object",
189
- required: ["project_root", "subscriber"],
195
+ required: ["project_root", "subscriber", "agent_handle"],
190
196
  properties: {
191
197
  project_root: { type: "string" },
192
198
  subscriber: { type: "string" },
199
+ agent_handle: { type: "string" },
193
200
  },
194
201
  additionalProperties: false,
195
202
  },
@@ -212,11 +219,16 @@ function withProjectRootSchema(schema, options = {}) {
212
219
  type: "string",
213
220
  description: "Caller-owned subscriber id returned by register_agent.",
214
221
  },
222
+ agent_handle: {
223
+ type: "string",
224
+ description: "Opaque ownership capability returned by register_agent.",
225
+ },
215
226
  ...(cloned.properties || {}),
216
227
  };
217
228
  const required = Array.isArray(cloned.required) ? cloned.required.slice() : [];
218
229
  if (!required.includes("project_root")) required.unshift("project_root");
219
230
  if (options.requireSubscriber && !required.includes("subscriber")) required.push("subscriber");
231
+ if (options.requireAgentHandle && !required.includes("agent_handle")) required.push("agent_handle");
220
232
  cloned.properties = properties;
221
233
  cloned.required = required;
222
234
  cloned.additionalProperties = false;
@@ -227,6 +239,7 @@ function toMcpTool(definition, options = {}) {
227
239
  const inputSchema = options.projectScoped
228
240
  ? withProjectRootSchema(definition.input_schema, {
229
241
  requireSubscriber: options.requireSubscriber,
242
+ requireAgentHandle: options.requireAgentHandle,
230
243
  })
231
244
  : cloneJson(definition.input_schema);
232
245
  return {
@@ -243,6 +256,7 @@ function buildToolList() {
243
256
  .map((tool) => toMcpTool(tool, {
244
257
  projectScoped: tool.name !== "read_project_registry",
245
258
  requireSubscriber: tool.name === "dispatch_message" || tool.name === "ack_bus",
259
+ requireAgentHandle: tool.name === "dispatch_message" || tool.name === "ack_bus",
246
260
  }));
247
261
  const custom = CUSTOM_TOOL_DEFINITIONS.map((tool) => toMcpTool(tool));
248
262
  return [...custom, ...shared];
@@ -265,6 +279,8 @@ function stripMcpRoutingArgs(args = {}) {
265
279
  delete next.project_root;
266
280
  delete next.projectRoot;
267
281
  delete next.subscriber;
282
+ delete next.agent_handle;
283
+ delete next.agentHandle;
268
284
  return next;
269
285
  }
270
286
 
@@ -408,12 +424,50 @@ async function ensureGlobalControllerDaemon(options = {}) {
408
424
  async function handleMcpStatus(ctx = {}) {
409
425
  const root = resolveGlobalControllerProjectRoot();
410
426
  const projects = listRegisteredProjectRows();
427
+ let http = null;
428
+ if (typeof ctx.getMcpHttpStatus === "function") {
429
+ http = ctx.getMcpHttpStatus();
430
+ } else {
431
+ try {
432
+ const endpointPath = getUfooPaths(root).mcpEndpoint;
433
+ http = JSON.parse(fs.readFileSync(endpointPath, "utf8"));
434
+ const pidRunning = Number(http.pid) > 0 && (() => {
435
+ try {
436
+ process.kill(Number(http.pid), 0);
437
+ return true;
438
+ } catch {
439
+ return false;
440
+ }
441
+ })();
442
+ let healthRunning = false;
443
+ if (pidRunning && http.endpoint && typeof fetch === "function") {
444
+ const controller = new AbortController();
445
+ const timer = setTimeout(() => controller.abort(), 500);
446
+ if (typeof timer.unref === "function") timer.unref();
447
+ try {
448
+ const healthUrl = new URL(http.endpoint);
449
+ healthUrl.pathname = "/health";
450
+ healthUrl.search = "";
451
+ const response = await fetch(healthUrl, { signal: controller.signal });
452
+ healthRunning = response.ok;
453
+ } catch {
454
+ healthRunning = false;
455
+ } finally {
456
+ clearTimeout(timer);
457
+ }
458
+ }
459
+ http.running = pidRunning && healthRunning;
460
+ } catch {
461
+ http = { running: false };
462
+ }
463
+ }
411
464
  return {
412
465
  ok: true,
413
466
  global_controller_root: root,
414
467
  global_controller_sock: socketPath(root),
415
468
  global_controller_running: isRunning(root),
416
469
  auto_start: ctx.autoStart !== false,
470
+ http,
417
471
  project_count: projects.length,
418
472
  projects,
419
473
  };
@@ -421,48 +475,42 @@ async function handleMcpStatus(ctx = {}) {
421
475
 
422
476
  async function handleRegisterAgent(ctx = {}, args = {}) {
423
477
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
424
- return controlPlane.registerAgent(projectRoot, args);
478
+ return ctx.projectRuntimeGateway.call(projectRoot, "register_agent", args, ctx);
425
479
  }
426
480
 
427
481
  async function handleHeartbeatAgent(ctx = {}, args = {}) {
428
482
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
429
- return controlPlane.heartbeatAgent(projectRoot, args);
483
+ return ctx.projectRuntimeGateway.call(projectRoot, "heartbeat_agent", args, ctx);
430
484
  }
431
485
 
432
486
  async function handlePublishActivityState(ctx = {}, args = {}) {
433
487
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
434
- return controlPlane.publishActivityState(projectRoot, args);
488
+ return ctx.projectRuntimeGateway.call(projectRoot, "publish_activity_state", args, ctx);
435
489
  }
436
490
 
437
491
  async function handleUpdateAgentMetadata(ctx = {}, args = {}) {
438
492
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
439
- return controlPlane.updateAgentMetadata(projectRoot, args);
493
+ return ctx.projectRuntimeGateway.call(projectRoot, "update_agent_metadata", args, ctx);
440
494
  }
441
495
 
442
496
  async function handlePollInbox(ctx = {}, args = {}) {
443
497
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
444
- return controlPlane.pollInbox(projectRoot, args);
498
+ return ctx.projectRuntimeGateway.call(projectRoot, "poll_inbox", args, ctx);
445
499
  }
446
500
 
447
501
  async function handleWaitForMessage(ctx = {}, args = {}) {
448
502
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
449
- return controlPlane.waitForMessage(projectRoot, args, {
450
- signal: ctx.signal,
451
- pollIntervalMs: ctx.waitPollIntervalMs,
452
- heartbeatIntervalMs: ctx.waitHeartbeatIntervalMs,
453
- now: ctx.waitNow,
454
- sleep: ctx.waitSleep,
455
- });
503
+ return ctx.projectRuntimeGateway.call(projectRoot, "wait_for_message", args, ctx);
456
504
  }
457
505
 
458
506
  async function handleReportAgentStatus(ctx = {}, args = {}) {
459
507
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
460
- return controlPlane.reportAgentStatus(projectRoot, args);
508
+ return ctx.projectRuntimeGateway.call(projectRoot, "report_agent_status", args, ctx);
461
509
  }
462
510
 
463
511
  async function handleUnregisterAgent(ctx = {}, args = {}) {
464
512
  const projectRoot = resolveRegisteredProjectRoot(args, ctx);
465
- return controlPlane.unregisterAgent(projectRoot, args);
513
+ return ctx.projectRuntimeGateway.call(projectRoot, "unregister_agent", args, ctx);
466
514
  }
467
515
 
468
516
  function findCustomTool(name) {
@@ -481,23 +529,19 @@ async function invokeTool(name, args = {}, ctx = {}) {
481
529
  throw err;
482
530
  }
483
531
 
532
+ if (name !== "read_project_registry") {
533
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
534
+ return ctx.projectRuntimeGateway.call(projectRoot, name, args, ctx);
535
+ }
536
+
484
537
  const tool = assertToolAllowedForCallerTier(name, CALLER_TIERS.WORKER, {
485
538
  tool_call_id: ctx.toolCallId,
486
539
  });
487
- const projectRoot = name === "read_project_registry"
488
- ? resolveGlobalControllerProjectRoot()
489
- : resolveRegisteredProjectRoot(args, ctx);
490
- const subscriber = String(args.subscriber || args.source || "").trim();
491
- const toolArgs = stripMcpRoutingArgs(args);
492
- if (name === "dispatch_message" && !toolArgs.source && subscriber) {
493
- toolArgs.source = subscriber;
494
- }
495
- const toolCtx = {
496
- projectRoot,
497
- subscriber,
540
+ return tool.handler({
541
+ projectRoot: resolveGlobalControllerProjectRoot(),
542
+ subscriber: "",
498
543
  caller_tier: CALLER_TIERS.WORKER,
499
- };
500
- return tool.handler(toolCtx, toolArgs);
544
+ }, stripMcpRoutingArgs(args));
501
545
  }
502
546
 
503
547
  class UfooMcpServer {
@@ -510,10 +554,10 @@ class UfooMcpServer {
510
554
  waitHeartbeatIntervalMs: options.waitHeartbeatIntervalMs,
511
555
  waitNow: options.waitNow,
512
556
  waitSleep: options.waitSleep,
557
+ projectRuntimeGateway: options.projectRuntimeGateway || DEFAULT_PROJECT_RUNTIME_GATEWAY,
513
558
  };
514
559
  this.initialized = false;
515
560
  this.startup = null;
516
- this.registeredSubscribers = [];
517
561
  this.activeToolCalls = new Map();
518
562
  }
519
563
 
@@ -607,17 +651,6 @@ class UfooMcpServer {
607
651
  } finally {
608
652
  this.activeToolCalls.delete(id);
609
653
  }
610
- if (name === "register_agent" && result && result.subscriber && result.project_root) {
611
- this.registeredSubscribers.push({
612
- subscriber: result.subscriber,
613
- projectRoot: result.project_root,
614
- });
615
- }
616
- if (name === "unregister_agent" && result && result.subscriber) {
617
- this.registeredSubscribers = this.registeredSubscribers.filter(
618
- (entry) => entry.subscriber !== result.subscriber
619
- );
620
- }
621
654
  return createJsonRpcResult(id, createMcpContent(result));
622
655
  }
623
656
 
@@ -636,14 +669,6 @@ class UfooMcpServer {
636
669
  active.abort();
637
670
  }
638
671
  this.activeToolCalls.clear();
639
- for (const { subscriber, projectRoot } of this.registeredSubscribers) {
640
- try {
641
- controlPlane.unregisterAgent(projectRoot, { subscriber });
642
- } catch {
643
- // best-effort cleanup
644
- }
645
- }
646
- this.registeredSubscribers = [];
647
672
  }
648
673
  }
649
674
 
@@ -652,52 +677,18 @@ function createUfooMcpServer(options = {}) {
652
677
  }
653
678
 
654
679
  async function runMcpServer(options = {}) {
655
- const input = options.input || process.stdin;
656
- const output = options.output || process.stdout;
657
- const server = createUfooMcpServer(options);
658
- let buffer = "";
659
-
660
- const writeMessage = (message) => {
661
- if (!message) return;
662
- output.write(`${JSON.stringify(message)}\n`);
663
- };
664
-
665
- input.setEncoding("utf8");
666
- input.on("data", (chunk) => {
667
- buffer += chunk;
668
- const lines = buffer.split(/\r?\n/);
669
- buffer = lines.pop() || "";
670
- for (const line of lines) {
671
- if (!line.trim()) continue;
672
- let request;
673
- try {
674
- request = JSON.parse(line);
675
- } catch (err) {
676
- writeMessage(createJsonRpcError(null, MCP_ERROR_CODES.PARSE_ERROR, err.message || "Parse error"));
677
- continue;
678
- }
679
- server.handleRequest(request)
680
- .then(writeMessage)
681
- .catch((err) => {
682
- writeMessage(createJsonRpcError(
683
- Object.prototype.hasOwnProperty.call(request, "id") ? request.id : null,
684
- MCP_ERROR_CODES.INTERNAL_ERROR,
685
- err.message || String(err)
686
- ));
687
- });
688
- }
680
+ const { runMcpStdioProxy } = require("./mcpStdioProxy");
681
+ return runMcpStdioProxy({
682
+ ...options,
683
+ ensureDaemon: () => ensureGlobalControllerDaemon(options),
689
684
  });
690
-
691
- input.on("end", () => server.cleanup());
692
- input.on("close", () => server.cleanup());
693
-
694
- return server;
695
685
  }
696
686
 
697
687
  module.exports = {
698
688
  EXPOSED_SHARED_TOOLS,
699
689
  CUSTOM_TOOL_DEFINITIONS,
700
690
  buildToolList,
691
+ createMcpContent,
701
692
  createUfooMcpServer,
702
693
  ensureGlobalControllerDaemon,
703
694
  invokeTool,
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+
5
+ const { getUfooPaths } = require("../../coordination/state/paths");
6
+ const {
7
+ resolveGlobalControllerProjectRoot,
8
+ } = require("../projects");
9
+ const {
10
+ MCP_ERROR_CODES,
11
+ createJsonRpcError,
12
+ } = require("../contracts/mcpContract");
13
+
14
+ const DEFAULT_ENDPOINT_WAIT_MS = 5000;
15
+ const DEFAULT_ENDPOINT_POLL_MS = 50;
16
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
17
+
18
+ function sleep(ms) {
19
+ return new Promise((resolve) => setTimeout(resolve, ms));
20
+ }
21
+
22
+ function validateEndpoint(raw = "") {
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(String(raw || ""));
26
+ } catch {
27
+ throw new Error("Invalid global MCP endpoint");
28
+ }
29
+ if (parsed.protocol !== "http:" || !LOOPBACK_HOSTS.has(parsed.hostname) || parsed.pathname !== "/mcp") {
30
+ const err = new Error(`Refusing non-loopback global MCP endpoint: ${parsed.toString()}`);
31
+ err.code = "UFOO_MCP_INVALID_ENDPOINT";
32
+ throw err;
33
+ }
34
+ return parsed.toString();
35
+ }
36
+
37
+ function readConnectionFiles(projectRoot) {
38
+ const paths = getUfooPaths(projectRoot);
39
+ const endpointRecord = JSON.parse(fs.readFileSync(paths.mcpEndpoint, "utf8"));
40
+ const endpoint = validateEndpoint(endpointRecord.endpoint);
41
+ const token = String(fs.readFileSync(paths.mcpToken, "utf8") || "").trim();
42
+ if (!token) {
43
+ const err = new Error("Global MCP bearer token is empty");
44
+ err.code = "UFOO_MCP_EMPTY_TOKEN";
45
+ throw err;
46
+ }
47
+ return { endpoint, token };
48
+ }
49
+
50
+ class McpStdioProxy {
51
+ constructor(options = {}) {
52
+ this.input = options.input || process.stdin;
53
+ this.output = options.output || process.stdout;
54
+ this.errorOutput = options.errorOutput || process.stderr;
55
+ this.projectRoot = options.projectRoot || resolveGlobalControllerProjectRoot();
56
+ this.autoStart = options.autoStart !== false;
57
+ this.ensureDaemon = options.ensureDaemon || (async () => {});
58
+ this.fetch = options.fetch || globalThis.fetch;
59
+ this.endpoint = options.endpoint ? validateEndpoint(options.endpoint) : "";
60
+ this.token = String(options.token || "");
61
+ this.endpointWaitMs = Number(options.endpointWaitMs) || DEFAULT_ENDPOINT_WAIT_MS;
62
+ this.endpointPollMs = Number(options.endpointPollMs) || DEFAULT_ENDPOINT_POLL_MS;
63
+ this.sessionId = "";
64
+ this.startup = null;
65
+ this.initializing = null;
66
+ this.activeRequests = new Map();
67
+ this.closed = false;
68
+ this.buffer = "";
69
+ }
70
+
71
+ writeMessage(message) {
72
+ if (!message || this.closed) return;
73
+ this.output.write(`${JSON.stringify(message)}\n`);
74
+ }
75
+
76
+ writeError(message) {
77
+ this.errorOutput.write(`[ufoo-mcp-proxy] ${String(message || "")}\n`);
78
+ }
79
+
80
+ async connect() {
81
+ if (this.endpoint && this.token) return { endpoint: this.endpoint };
82
+ if (this.startup) return this.startup;
83
+ this.startup = (async () => {
84
+ if (this.autoStart) await this.ensureDaemon();
85
+ const deadline = Date.now() + this.endpointWaitMs;
86
+ let lastError = null;
87
+ do {
88
+ try {
89
+ const connection = readConnectionFiles(this.projectRoot);
90
+ this.endpoint = connection.endpoint;
91
+ this.token = connection.token;
92
+ return { endpoint: this.endpoint };
93
+ } catch (err) {
94
+ lastError = err;
95
+ }
96
+ if (!this.autoStart) break;
97
+ await sleep(this.endpointPollMs);
98
+ } while (Date.now() < deadline);
99
+ const err = new Error(
100
+ `Global MCP endpoint is unavailable: ${lastError ? lastError.message : "not started"}`
101
+ );
102
+ err.code = "UFOO_MCP_UNAVAILABLE";
103
+ throw err;
104
+ })();
105
+ try {
106
+ return await this.startup;
107
+ } catch (err) {
108
+ this.startup = null;
109
+ throw err;
110
+ }
111
+ }
112
+
113
+ async post(request, controller) {
114
+ await this.connect();
115
+ const headers = {
116
+ accept: "application/json, text/event-stream",
117
+ authorization: `Bearer ${this.token}`,
118
+ "content-type": "application/json",
119
+ };
120
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
121
+ const response = await this.fetch(this.endpoint, {
122
+ method: "POST",
123
+ headers,
124
+ body: JSON.stringify(request),
125
+ signal: controller.signal,
126
+ });
127
+ const nextSessionId = response.headers.get("mcp-session-id");
128
+ if (nextSessionId) this.sessionId = nextSessionId;
129
+ if (response.status === 202 || response.status === 204) return null;
130
+ const text = await response.text();
131
+ if (!text) {
132
+ if (response.ok) return null;
133
+ throw new Error(`Global MCP returned HTTP ${response.status}`);
134
+ }
135
+ let payload;
136
+ try {
137
+ payload = JSON.parse(text);
138
+ } catch {
139
+ throw new Error(`Global MCP returned invalid JSON (HTTP ${response.status})`);
140
+ }
141
+ return payload;
142
+ }
143
+
144
+ async forward(request) {
145
+ const hasId = Object.prototype.hasOwnProperty.call(request || {}, "id");
146
+ const id = hasId ? request.id : undefined;
147
+ const method = String((request && request.method) || "");
148
+ if (method !== "initialize" && this.initializing) {
149
+ await this.initializing;
150
+ }
151
+
152
+ const controller = new AbortController();
153
+ if (hasId) this.activeRequests.set(id, controller);
154
+ const operation = this.post(request, controller);
155
+ if (method === "initialize") this.initializing = operation;
156
+ try {
157
+ const response = await operation;
158
+ if (response) this.writeMessage(response);
159
+ return response;
160
+ } catch (err) {
161
+ if (hasId) {
162
+ const response = createJsonRpcError(
163
+ id,
164
+ MCP_ERROR_CODES.INTERNAL_ERROR,
165
+ err && err.message ? err.message : String(err),
166
+ { code: err && err.code ? String(err.code) : "mcp_proxy_error" }
167
+ );
168
+ this.writeMessage(response);
169
+ return response;
170
+ }
171
+ this.writeError(err && err.message ? err.message : err);
172
+ return null;
173
+ } finally {
174
+ if (hasId) this.activeRequests.delete(id);
175
+ if (method === "initialize") this.initializing = null;
176
+ }
177
+ }
178
+
179
+ handleLine(line) {
180
+ if (!line.trim()) return;
181
+ let request;
182
+ try {
183
+ request = JSON.parse(line);
184
+ } catch (err) {
185
+ this.writeMessage(createJsonRpcError(
186
+ null,
187
+ MCP_ERROR_CODES.PARSE_ERROR,
188
+ err.message || "Parse error"
189
+ ));
190
+ return;
191
+ }
192
+ void this.forward(request);
193
+ }
194
+
195
+ start() {
196
+ this.input.setEncoding("utf8");
197
+ this.input.on("data", (chunk) => {
198
+ this.buffer += chunk;
199
+ const lines = this.buffer.split(/\r?\n/);
200
+ this.buffer = lines.pop() || "";
201
+ for (const line of lines) this.handleLine(line);
202
+ });
203
+ this.input.on("end", () => {
204
+ void this.close();
205
+ });
206
+ this.input.on("close", () => {
207
+ void this.close();
208
+ });
209
+ return this;
210
+ }
211
+
212
+ async close() {
213
+ if (this.closed) return;
214
+ this.closed = true;
215
+ for (const controller of this.activeRequests.values()) controller.abort();
216
+ this.activeRequests.clear();
217
+ if (!this.endpoint || !this.token || !this.sessionId) return;
218
+ try {
219
+ await this.fetch(this.endpoint, {
220
+ method: "DELETE",
221
+ headers: {
222
+ accept: "application/json, text/event-stream",
223
+ authorization: `Bearer ${this.token}`,
224
+ "mcp-session-id": this.sessionId,
225
+ },
226
+ });
227
+ } catch {
228
+ // The adapter is disposable; the server will also close abandoned sessions.
229
+ }
230
+ }
231
+ }
232
+
233
+ function createMcpStdioProxy(options = {}) {
234
+ return new McpStdioProxy(options);
235
+ }
236
+
237
+ async function runMcpStdioProxy(options = {}) {
238
+ return createMcpStdioProxy(options).start();
239
+ }
240
+
241
+ module.exports = {
242
+ McpStdioProxy,
243
+ createMcpStdioProxy,
244
+ readConnectionFiles,
245
+ runMcpStdioProxy,
246
+ validateEndpoint,
247
+ };