u-foo 2.4.4 → 2.4.6

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.
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const net = require("net");
5
+ const path = require("path");
6
+ const { spawn } = require("child_process");
7
+
8
+ const EventBus = require("../../coordination/bus");
9
+ const { getUfooPaths } = require("../../coordination/state/paths");
10
+ const { normalizeReportInput } = require("../../coordination/report/store");
11
+ const { enqueueAgentReport } = require("./reportControlBus");
12
+ const { isRunning, socketPath } = require("./index");
13
+ const {
14
+ normalizeProjectRoot,
15
+ resolveGlobalControllerProjectRoot,
16
+ isGlobalControllerProjectRoot,
17
+ listProjectRuntimes,
18
+ } = require("../projects");
19
+ const { resolveNodeExecutable } = require("../process/nodeExecutable");
20
+ const {
21
+ getToolDefinition,
22
+ assertToolAllowedForCallerTier,
23
+ } = require("../../tools/registry");
24
+ const { CALLER_TIERS } = require("../../tools/types");
25
+ const {
26
+ MCP_PROTOCOL_VERSION,
27
+ MCP_ERROR_CODES,
28
+ createJsonRpcResult,
29
+ createJsonRpcError,
30
+ } = require("../contracts/mcpContract");
31
+
32
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
33
+ const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
34
+
35
+ const EXPOSED_SHARED_TOOLS = Object.freeze([
36
+ "read_project_registry",
37
+ "read_bus_summary",
38
+ "read_prompt_history",
39
+ "read_open_decisions",
40
+ "list_agents",
41
+ "dispatch_message",
42
+ "ack_bus",
43
+ ]);
44
+
45
+ const CUSTOM_TOOL_DEFINITIONS = Object.freeze([
46
+ {
47
+ name: "ufoo_mcp_status",
48
+ description: "Read local global ufoo MCP bridge status and registered project summary.",
49
+ input_schema: {
50
+ type: "object",
51
+ properties: {},
52
+ additionalProperties: false,
53
+ },
54
+ handler: handleMcpStatus,
55
+ },
56
+ {
57
+ name: "register_agent",
58
+ description: "Register an externally launched agent into a registered project bus.",
59
+ input_schema: {
60
+ type: "object",
61
+ required: ["project_root"],
62
+ properties: {
63
+ project_root: { type: "string" },
64
+ agent_type: { type: "string" },
65
+ session_id: { type: "string" },
66
+ nickname: { type: "string" },
67
+ scoped_nickname: { type: "string" },
68
+ launch_mode: { type: "string" },
69
+ capabilities: { type: "object", additionalProperties: true },
70
+ },
71
+ additionalProperties: false,
72
+ },
73
+ handler: handleRegisterAgent,
74
+ },
75
+ {
76
+ name: "heartbeat_agent",
77
+ description: "Refresh a registered agent heartbeat in its project bus.",
78
+ input_schema: {
79
+ type: "object",
80
+ required: ["project_root", "subscriber"],
81
+ properties: {
82
+ project_root: { type: "string" },
83
+ subscriber: { type: "string" },
84
+ },
85
+ additionalProperties: false,
86
+ },
87
+ handler: handleHeartbeatAgent,
88
+ },
89
+ {
90
+ name: "publish_activity_state",
91
+ description: "Publish the caller agent activity state in its project bus metadata.",
92
+ input_schema: {
93
+ type: "object",
94
+ required: ["project_root", "subscriber", "activity_state"],
95
+ properties: {
96
+ project_root: { type: "string" },
97
+ subscriber: { type: "string" },
98
+ activity_state: { type: "string" },
99
+ detail: { type: "string" },
100
+ since: { type: "string" },
101
+ },
102
+ additionalProperties: false,
103
+ },
104
+ handler: handlePublishActivityState,
105
+ },
106
+ {
107
+ name: "update_agent_metadata",
108
+ description: "Update the caller agent nickname or MCP metadata in its project bus.",
109
+ input_schema: {
110
+ type: "object",
111
+ required: ["project_root", "subscriber"],
112
+ properties: {
113
+ project_root: { type: "string" },
114
+ subscriber: { type: "string" },
115
+ nickname: { type: "string" },
116
+ metadata: { type: "object", additionalProperties: true },
117
+ },
118
+ additionalProperties: false,
119
+ },
120
+ handler: handleUpdateAgentMetadata,
121
+ },
122
+ {
123
+ name: "poll_inbox",
124
+ description: "Read pending bus messages for the caller-owned subscriber queue without acknowledging them.",
125
+ input_schema: {
126
+ type: "object",
127
+ required: ["project_root", "subscriber"],
128
+ properties: {
129
+ project_root: { type: "string" },
130
+ subscriber: { type: "string" },
131
+ limit: { type: "integer", minimum: 1 },
132
+ },
133
+ additionalProperties: false,
134
+ },
135
+ handler: handlePollInbox,
136
+ },
137
+ {
138
+ name: "report_agent_status",
139
+ description: "Queue an agent task status report through the project daemon report-control queue.",
140
+ input_schema: {
141
+ type: "object",
142
+ required: ["project_root", "subscriber", "task_id", "phase"],
143
+ properties: {
144
+ project_root: { type: "string" },
145
+ subscriber: { type: "string" },
146
+ task_id: { type: "string" },
147
+ phase: { type: "string", enum: ["start", "progress", "done", "error"] },
148
+ message: { type: "string" },
149
+ summary: { type: "string" },
150
+ error: { type: "string" },
151
+ scope: { type: "string", enum: ["public", "private"] },
152
+ meta: { type: "object", additionalProperties: true },
153
+ },
154
+ additionalProperties: false,
155
+ },
156
+ handler: handleReportAgentStatus,
157
+ },
158
+ {
159
+ name: "unregister_agent",
160
+ description: "Mark an MCP-registered agent inactive in its project bus.",
161
+ input_schema: {
162
+ type: "object",
163
+ required: ["project_root", "subscriber"],
164
+ properties: {
165
+ project_root: { type: "string" },
166
+ subscriber: { type: "string" },
167
+ },
168
+ additionalProperties: false,
169
+ },
170
+ handler: handleUnregisterAgent,
171
+ },
172
+ ]);
173
+
174
+ function normalizeBusAgentType(agentType = "") {
175
+ const value = String(agentType || "").trim().toLowerCase();
176
+ if (!value) return "mcp-agent";
177
+ if (value === "claude") return "claude-code";
178
+ if (value === "ucode" || value === "ufoo") return "ufoo-code";
179
+ return value;
180
+ }
181
+
182
+ function nowIso() {
183
+ return new Date().toISOString();
184
+ }
185
+
186
+ function cloneJson(value) {
187
+ return JSON.parse(JSON.stringify(value || {}));
188
+ }
189
+
190
+ function withProjectRootSchema(schema, options = {}) {
191
+ const cloned = cloneJson(schema);
192
+ const properties = {
193
+ project_root: {
194
+ type: "string",
195
+ description: "Absolute project root from read_project_registry.",
196
+ },
197
+ subscriber: {
198
+ type: "string",
199
+ description: "Caller-owned subscriber id returned by register_agent.",
200
+ },
201
+ ...(cloned.properties || {}),
202
+ };
203
+ const required = Array.isArray(cloned.required) ? cloned.required.slice() : [];
204
+ if (!required.includes("project_root")) required.unshift("project_root");
205
+ if (options.requireSubscriber && !required.includes("subscriber")) required.push("subscriber");
206
+ cloned.properties = properties;
207
+ cloned.required = required;
208
+ cloned.additionalProperties = false;
209
+ return cloned;
210
+ }
211
+
212
+ function toMcpTool(definition, options = {}) {
213
+ const inputSchema = options.projectScoped
214
+ ? withProjectRootSchema(definition.input_schema, {
215
+ requireSubscriber: options.requireSubscriber,
216
+ })
217
+ : cloneJson(definition.input_schema);
218
+ return {
219
+ name: definition.name,
220
+ description: definition.description,
221
+ inputSchema,
222
+ };
223
+ }
224
+
225
+ function buildToolList() {
226
+ const shared = EXPOSED_SHARED_TOOLS
227
+ .map((name) => getToolDefinition(name))
228
+ .filter(Boolean)
229
+ .map((tool) => toMcpTool(tool, {
230
+ projectScoped: tool.name !== "read_project_registry",
231
+ requireSubscriber: tool.name === "dispatch_message" || tool.name === "ack_bus",
232
+ }));
233
+ const custom = CUSTOM_TOOL_DEFINITIONS.map((tool) => toMcpTool(tool));
234
+ return [...custom, ...shared];
235
+ }
236
+
237
+ function createMcpContent(result) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: JSON.stringify(result, null, 2),
243
+ },
244
+ ],
245
+ structuredContent: result,
246
+ };
247
+ }
248
+
249
+ function stripMcpRoutingArgs(args = {}) {
250
+ const next = { ...(args || {}) };
251
+ delete next.project_root;
252
+ delete next.projectRoot;
253
+ delete next.subscriber;
254
+ return next;
255
+ }
256
+
257
+ async function suppressConsoleToStderr(fn) {
258
+ const original = {
259
+ log: console.log,
260
+ info: console.info,
261
+ warn: console.warn,
262
+ error: console.error,
263
+ };
264
+ const write = (...parts) => {
265
+ const line = parts.map((part) => {
266
+ if (typeof part === "string") return part;
267
+ try {
268
+ return JSON.stringify(part);
269
+ } catch {
270
+ return String(part);
271
+ }
272
+ }).join(" ");
273
+ process.stderr.write(`${line}\n`);
274
+ };
275
+ console.log = write;
276
+ console.info = write;
277
+ console.warn = write;
278
+ console.error = write;
279
+ try {
280
+ return await Promise.resolve(fn());
281
+ } finally {
282
+ console.log = original.log;
283
+ console.info = original.info;
284
+ console.warn = original.warn;
285
+ console.error = original.error;
286
+ }
287
+ }
288
+
289
+ function createSessionId() {
290
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
291
+ }
292
+
293
+ function listRegisteredProjectRows() {
294
+ return listProjectRuntimes({ validate: true, cleanupTmp: true })
295
+ .filter((row) => !isGlobalControllerProjectRoot(row && row.project_root));
296
+ }
297
+
298
+ function resolveRegisteredProjectRoot(args = {}, options = {}) {
299
+ const raw = String(args.project_root || args.projectRoot || "").trim();
300
+ if (!raw) {
301
+ const err = new Error("project_root is required for project-scoped MCP tools");
302
+ err.code = "invalid_project_root";
303
+ throw err;
304
+ }
305
+ const normalized = normalizeProjectRoot(raw);
306
+ if (options.validateProjectRoot === false) return normalized;
307
+
308
+ const rows = listRegisteredProjectRows();
309
+ const match = rows.find((row) => normalizeProjectRoot(row.project_root) === normalized);
310
+ if (!match) {
311
+ const err = new Error(`project_root is not registered in the global runtime registry: ${normalized}`);
312
+ err.code = "unregistered_project_root";
313
+ throw err;
314
+ }
315
+ return match.project_root || normalized;
316
+ }
317
+
318
+ function ensureBusLoaded(projectRoot) {
319
+ const bus = new EventBus(projectRoot);
320
+ bus.ensureBus();
321
+ bus.loadBusData();
322
+ return bus;
323
+ }
324
+
325
+ function assertSubscriberExists(bus, subscriber) {
326
+ const meta = bus.subscriberManager.getSubscriber(subscriber);
327
+ if (!meta) {
328
+ const err = new Error(`subscriber not found: ${subscriber}`);
329
+ err.code = "subscriber_not_found";
330
+ throw err;
331
+ }
332
+ return meta;
333
+ }
334
+
335
+ function resolveSubscriberArg(args = {}) {
336
+ const subscriber = String(args.subscriber || args.source || "").trim();
337
+ if (!subscriber) {
338
+ const err = new Error("subscriber is required");
339
+ err.code = "invalid_subscriber";
340
+ throw err;
341
+ }
342
+ return subscriber;
343
+ }
344
+
345
+ function connectSocket(sockPath, timeoutMs = 500) {
346
+ return new Promise((resolve, reject) => {
347
+ let timer = null;
348
+ const client = net.createConnection(sockPath, () => {
349
+ if (timer) clearTimeout(timer);
350
+ resolve(client);
351
+ });
352
+ client.on("error", (err) => {
353
+ if (timer) clearTimeout(timer);
354
+ reject(err);
355
+ });
356
+ timer = setTimeout(() => {
357
+ const err = new Error(`connect timeout: ${sockPath}`);
358
+ err.code = "ETIMEDOUT";
359
+ try {
360
+ client.destroy(err);
361
+ } catch {
362
+ // ignore
363
+ }
364
+ reject(err);
365
+ }, timeoutMs);
366
+ if (typeof timer.unref === "function") timer.unref();
367
+ });
368
+ }
369
+
370
+ async function waitForSocket(projectRoot, timeoutMs = 3000) {
371
+ const sock = socketPath(projectRoot);
372
+ const started = Date.now();
373
+ while (Date.now() - started < timeoutMs) {
374
+ if (fs.existsSync(sock)) {
375
+ try {
376
+ const client = await connectSocket(sock, 250);
377
+ client.end();
378
+ return true;
379
+ } catch {
380
+ // retry
381
+ }
382
+ }
383
+ await new Promise((resolve) => setTimeout(resolve, 100));
384
+ }
385
+ return false;
386
+ }
387
+
388
+ async function ensureGlobalControllerDaemon(options = {}) {
389
+ if (options.autoStart === false) {
390
+ return {
391
+ root: resolveGlobalControllerProjectRoot(),
392
+ running: isRunning(resolveGlobalControllerProjectRoot()),
393
+ auto_started: false,
394
+ };
395
+ }
396
+
397
+ const root = resolveGlobalControllerProjectRoot();
398
+ const paths = getUfooPaths(root);
399
+ if (!fs.existsSync(paths.ufooDir) || !fs.existsSync(paths.busDir) || !fs.existsSync(paths.agentDir)) {
400
+ const UfooInit = require("../../app/cli/features/init");
401
+ const init = new UfooInit(PACKAGE_ROOT);
402
+ await suppressConsoleToStderr(() => init.init({
403
+ modules: "context,bus",
404
+ project: root,
405
+ controllerMode: true,
406
+ }));
407
+ }
408
+
409
+ if (isRunning(root)) {
410
+ return { root, running: true, auto_started: false };
411
+ }
412
+
413
+ const child = spawn(resolveNodeExecutable(), [path.join(PACKAGE_ROOT, "bin", "ufoo.js"), "daemon", "start"], {
414
+ detached: true,
415
+ stdio: "ignore",
416
+ cwd: root,
417
+ env: process.env,
418
+ });
419
+ child.on("error", () => {});
420
+ child.unref();
421
+ const running = await waitForSocket(root, options.startTimeoutMs || 3000);
422
+ return { root, running, auto_started: true };
423
+ }
424
+
425
+ async function handleMcpStatus(ctx = {}) {
426
+ const root = resolveGlobalControllerProjectRoot();
427
+ const projects = listRegisteredProjectRows();
428
+ return {
429
+ ok: true,
430
+ global_controller_root: root,
431
+ global_controller_sock: socketPath(root),
432
+ global_controller_running: isRunning(root),
433
+ auto_start: ctx.autoStart !== false,
434
+ project_count: projects.length,
435
+ projects,
436
+ };
437
+ }
438
+
439
+ async function handleRegisterAgent(ctx = {}, args = {}) {
440
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
441
+ const agentType = normalizeBusAgentType(args.agent_type || args.agentType || "mcp-agent");
442
+ const sessionId = String(args.session_id || args.sessionId || createSessionId()).trim();
443
+ const nickname = String(args.nickname || "").trim();
444
+ const launchMode = String(args.launch_mode || args.launchMode || "mcp").trim();
445
+ const capabilities = args.capabilities && typeof args.capabilities === "object"
446
+ ? args.capabilities
447
+ : null;
448
+ const bus = ensureBusLoaded(projectRoot);
449
+ const result = await bus.subscriberManager.join(sessionId, agentType, nickname, {
450
+ parentPid: process.pid,
451
+ launchMode,
452
+ scopedNickname: String(args.scoped_nickname || args.scopedNickname || nickname || "").trim(),
453
+ hostName: "ufoo-mcp",
454
+ hostSessionId: `mcp-${process.pid}`,
455
+ hostCapabilities: capabilities,
456
+ });
457
+ const subscriber = result.subscriber;
458
+ const meta = bus.subscriberManager.getSubscriber(subscriber) || {};
459
+ meta.activity_state = String(args.activity_state || "ready");
460
+ meta.activity_since = nowIso();
461
+ meta.mcp_bridge = true;
462
+ if (capabilities) meta.mcp_capabilities = capabilities;
463
+ bus.saveBusData();
464
+ return {
465
+ ok: true,
466
+ project_root: projectRoot,
467
+ subscriber_id: subscriber,
468
+ subscriber,
469
+ session_id: sessionId,
470
+ agent_type: agentType,
471
+ nickname: meta.nickname || result.nickname || "",
472
+ scoped_nickname: meta.scoped_nickname || result.scopedNickname || "",
473
+ launch_mode: launchMode,
474
+ };
475
+ }
476
+
477
+ async function handleHeartbeatAgent(ctx = {}, args = {}) {
478
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
479
+ const subscriber = resolveSubscriberArg(args);
480
+ const bus = ensureBusLoaded(projectRoot);
481
+ const meta = assertSubscriberExists(bus, subscriber);
482
+ bus.subscriberManager.updateLastSeen(subscriber);
483
+ meta.status = "active";
484
+ bus.saveBusData();
485
+ return {
486
+ ok: true,
487
+ project_root: projectRoot,
488
+ subscriber,
489
+ last_seen: meta.last_seen,
490
+ };
491
+ }
492
+
493
+ async function handlePublishActivityState(ctx = {}, args = {}) {
494
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
495
+ const subscriber = resolveSubscriberArg(args);
496
+ const activityState = String(args.activity_state || args.activityState || "").trim();
497
+ if (!activityState) {
498
+ const err = new Error("activity_state is required");
499
+ err.code = "invalid_activity_state";
500
+ throw err;
501
+ }
502
+ const bus = ensureBusLoaded(projectRoot);
503
+ const meta = assertSubscriberExists(bus, subscriber);
504
+ bus.subscriberManager.updateLastSeen(subscriber);
505
+ meta.status = "active";
506
+ meta.activity_state = activityState;
507
+ meta.activity_detail = String(args.detail || "").trim();
508
+ meta.activity_since = String(args.since || "").trim() || nowIso();
509
+ bus.saveBusData();
510
+ return {
511
+ ok: true,
512
+ project_root: projectRoot,
513
+ subscriber,
514
+ activity_state: meta.activity_state,
515
+ activity_detail: meta.activity_detail,
516
+ activity_since: meta.activity_since,
517
+ };
518
+ }
519
+
520
+ async function handleUpdateAgentMetadata(ctx = {}, args = {}) {
521
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
522
+ const subscriber = resolveSubscriberArg(args);
523
+ const bus = ensureBusLoaded(projectRoot);
524
+ const meta = assertSubscriberExists(bus, subscriber);
525
+ const nickname = String(args.nickname || "").trim();
526
+ if (nickname) {
527
+ await bus.subscriberManager.rename(subscriber, nickname);
528
+ }
529
+ const metadata = args.metadata && typeof args.metadata === "object" ? args.metadata : {};
530
+ if (Object.keys(metadata).length > 0) {
531
+ meta.mcp_metadata = {
532
+ ...(meta.mcp_metadata && typeof meta.mcp_metadata === "object" ? meta.mcp_metadata : {}),
533
+ ...metadata,
534
+ };
535
+ }
536
+ bus.subscriberManager.updateLastSeen(subscriber);
537
+ bus.saveBusData();
538
+ const nextMeta = bus.subscriberManager.getSubscriber(subscriber) || meta;
539
+ return {
540
+ ok: true,
541
+ project_root: projectRoot,
542
+ subscriber,
543
+ nickname: nextMeta.nickname || "",
544
+ scoped_nickname: nextMeta.scoped_nickname || nextMeta.nickname || "",
545
+ metadata: nextMeta.mcp_metadata || {},
546
+ };
547
+ }
548
+
549
+ async function handlePollInbox(ctx = {}, args = {}) {
550
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
551
+ const subscriber = resolveSubscriberArg(args);
552
+ const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
553
+ ? Math.floor(Number(args.limit))
554
+ : 50;
555
+ const bus = ensureBusLoaded(projectRoot);
556
+ assertSubscriberExists(bus, subscriber);
557
+ bus.subscriberManager.updateLastSeen(subscriber);
558
+ bus.saveBusData();
559
+ const pending = await bus.messageManager.check(subscriber);
560
+ return {
561
+ ok: true,
562
+ project_root: projectRoot,
563
+ subscriber,
564
+ count: pending.length,
565
+ messages: pending.slice(0, limit),
566
+ truncated: pending.length > limit,
567
+ };
568
+ }
569
+
570
+ async function handleReportAgentStatus(ctx = {}, args = {}) {
571
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
572
+ const subscriber = resolveSubscriberArg(args);
573
+ const report = normalizeReportInput({
574
+ ...args,
575
+ agent_id: subscriber,
576
+ source: "mcp",
577
+ });
578
+ const queued = await enqueueAgentReport(projectRoot, report, { publisher: subscriber });
579
+ return {
580
+ ok: true,
581
+ project_root: projectRoot,
582
+ status: "queued",
583
+ request_id: queued.request_id,
584
+ report,
585
+ queued,
586
+ };
587
+ }
588
+
589
+ async function handleUnregisterAgent(ctx = {}, args = {}) {
590
+ const projectRoot = resolveRegisteredProjectRoot(args, ctx);
591
+ const subscriber = resolveSubscriberArg(args);
592
+ const bus = ensureBusLoaded(projectRoot);
593
+ const ok = await bus.subscriberManager.leave(subscriber);
594
+ bus.saveBusData();
595
+ return {
596
+ ok,
597
+ project_root: projectRoot,
598
+ subscriber,
599
+ };
600
+ }
601
+
602
+ function findCustomTool(name) {
603
+ return CUSTOM_TOOL_DEFINITIONS.find((tool) => tool.name === name) || null;
604
+ }
605
+
606
+ async function invokeTool(name, args = {}, ctx = {}) {
607
+ const custom = findCustomTool(name);
608
+ if (custom) {
609
+ return custom.handler(ctx, args);
610
+ }
611
+
612
+ if (!EXPOSED_SHARED_TOOLS.includes(name)) {
613
+ const err = new Error(`unknown MCP tool: ${name}`);
614
+ err.code = "unknown_tool";
615
+ throw err;
616
+ }
617
+
618
+ const tool = assertToolAllowedForCallerTier(name, CALLER_TIERS.WORKER, {
619
+ tool_call_id: ctx.toolCallId,
620
+ });
621
+ const projectRoot = name === "read_project_registry"
622
+ ? resolveGlobalControllerProjectRoot()
623
+ : resolveRegisteredProjectRoot(args, ctx);
624
+ const subscriber = String(args.subscriber || args.source || "").trim();
625
+ const toolArgs = stripMcpRoutingArgs(args);
626
+ if (name === "dispatch_message" && !toolArgs.source && subscriber) {
627
+ toolArgs.source = subscriber;
628
+ }
629
+ const toolCtx = {
630
+ projectRoot,
631
+ subscriber,
632
+ caller_tier: CALLER_TIERS.WORKER,
633
+ };
634
+ return tool.handler(toolCtx, toolArgs);
635
+ }
636
+
637
+ class UfooMcpServer {
638
+ constructor(options = {}) {
639
+ this.options = {
640
+ autoStart: options.autoStart !== false,
641
+ validateProjectRoot: options.validateProjectRoot !== false,
642
+ startTimeoutMs: options.startTimeoutMs,
643
+ };
644
+ this.initialized = false;
645
+ this.startup = null;
646
+ }
647
+
648
+ async ensureStarted() {
649
+ if (!this.startup) {
650
+ this.startup = ensureGlobalControllerDaemon(this.options).catch((err) => {
651
+ process.stderr.write(`[ufoo-mcp] global controller start failed: ${err.message || err}\n`);
652
+ return {
653
+ root: resolveGlobalControllerProjectRoot(),
654
+ running: false,
655
+ auto_started: false,
656
+ error: err.message || String(err),
657
+ };
658
+ });
659
+ }
660
+ return this.startup;
661
+ }
662
+
663
+ async handleRequest(request) {
664
+ if (!request || typeof request !== "object") {
665
+ return createJsonRpcError(null, MCP_ERROR_CODES.INVALID_REQUEST, "Invalid JSON-RPC request");
666
+ }
667
+
668
+ const hasId = Object.prototype.hasOwnProperty.call(request, "id");
669
+ const id = hasId ? request.id : undefined;
670
+ const isNotification = !hasId;
671
+ const method = String(request.method || "");
672
+ const params = request.params && typeof request.params === "object" ? request.params : {};
673
+
674
+ if (isNotification) {
675
+ if (method === "notifications/initialized") {
676
+ this.initialized = true;
677
+ }
678
+ return null;
679
+ }
680
+
681
+ try {
682
+ if (method === "initialize") {
683
+ await this.ensureStarted();
684
+ return createJsonRpcResult(id, {
685
+ protocolVersion: params.protocolVersion || MCP_PROTOCOL_VERSION,
686
+ capabilities: {
687
+ tools: {
688
+ listChanged: false,
689
+ },
690
+ },
691
+ serverInfo: {
692
+ name: "ufoo-global-mcp",
693
+ version: PACKAGE_JSON.version || "0.0.0",
694
+ },
695
+ });
696
+ }
697
+
698
+ if (method === "ping") {
699
+ return createJsonRpcResult(id, {});
700
+ }
701
+
702
+ if (method === "tools/list") {
703
+ await this.ensureStarted();
704
+ return createJsonRpcResult(id, {
705
+ tools: buildToolList(),
706
+ });
707
+ }
708
+
709
+ if (method === "tools/call") {
710
+ await this.ensureStarted();
711
+ const name = String(params.name || "").trim();
712
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
713
+ if (!name) {
714
+ return createJsonRpcError(id, MCP_ERROR_CODES.INVALID_PARAMS, "tools/call requires params.name");
715
+ }
716
+ const result = await suppressConsoleToStderr(() => invokeTool(name, args, {
717
+ ...this.options,
718
+ toolCallId: id,
719
+ }));
720
+ return createJsonRpcResult(id, createMcpContent(result));
721
+ }
722
+
723
+ return createJsonRpcError(id, MCP_ERROR_CODES.METHOD_NOT_FOUND, `Unknown MCP method: ${method}`);
724
+ } catch (err) {
725
+ const data = {
726
+ code: err && err.code ? String(err.code) : "tool_error",
727
+ };
728
+ if (err && err.stack && process.env.UFOO_MCP_DEBUG === "1") data.stack = err.stack;
729
+ return createJsonRpcError(id, MCP_ERROR_CODES.INTERNAL_ERROR, err.message || String(err), data);
730
+ }
731
+ }
732
+ }
733
+
734
+ function createUfooMcpServer(options = {}) {
735
+ return new UfooMcpServer(options);
736
+ }
737
+
738
+ async function runMcpServer(options = {}) {
739
+ const input = options.input || process.stdin;
740
+ const output = options.output || process.stdout;
741
+ const server = createUfooMcpServer(options);
742
+ let buffer = "";
743
+
744
+ const writeMessage = (message) => {
745
+ if (!message) return;
746
+ output.write(`${JSON.stringify(message)}\n`);
747
+ };
748
+
749
+ input.setEncoding("utf8");
750
+ input.on("data", (chunk) => {
751
+ buffer += chunk;
752
+ const lines = buffer.split(/\r?\n/);
753
+ buffer = lines.pop() || "";
754
+ for (const line of lines) {
755
+ if (!line.trim()) continue;
756
+ let request;
757
+ try {
758
+ request = JSON.parse(line);
759
+ } catch (err) {
760
+ writeMessage(createJsonRpcError(null, MCP_ERROR_CODES.PARSE_ERROR, err.message || "Parse error"));
761
+ continue;
762
+ }
763
+ server.handleRequest(request)
764
+ .then(writeMessage)
765
+ .catch((err) => {
766
+ writeMessage(createJsonRpcError(
767
+ Object.prototype.hasOwnProperty.call(request, "id") ? request.id : null,
768
+ MCP_ERROR_CODES.INTERNAL_ERROR,
769
+ err.message || String(err)
770
+ ));
771
+ });
772
+ }
773
+ });
774
+
775
+ return server;
776
+ }
777
+
778
+ module.exports = {
779
+ EXPOSED_SHARED_TOOLS,
780
+ CUSTOM_TOOL_DEFINITIONS,
781
+ buildToolList,
782
+ createUfooMcpServer,
783
+ ensureGlobalControllerDaemon,
784
+ invokeTool,
785
+ runMcpServer,
786
+ };