u-foo 2.4.4 → 2.4.5
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.
- package/README.md +17 -1
- package/README.zh-CN.md +16 -1
- package/bin/ufoo.js +7 -0
- package/package.json +1 -1
- package/src/app/cli/run.js +16 -0
- package/src/runtime/contracts/mcpContract.js +39 -0
- package/src/runtime/daemon/mcpServer.js +786 -0
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Package: [u-foo on npm](https://www.npmjs.com/package/u-foo)
|
|
|
27
27
|
- Launch modes for internal, tmux, host, Terminal.app, and iTerm2 workflows.
|
|
28
28
|
- Built-in group templates for launching and orchestrating multi-agent workflows.
|
|
29
29
|
- `ucode`, a native ufoo coding-agent runtime.
|
|
30
|
+
- `ufoo mcp`, a local global MCP bridge for external MCP-capable agents.
|
|
30
31
|
|
|
31
32
|
## Requirements
|
|
32
33
|
|
|
@@ -56,7 +57,7 @@ Installed binaries:
|
|
|
56
57
|
|
|
57
58
|
| Binary | Purpose |
|
|
58
59
|
|---|---|
|
|
59
|
-
| `ufoo` | Main CLI, chat dashboard, daemon, groups, bus, context, memory, reports, and online helpers. |
|
|
60
|
+
| `ufoo` | Main CLI, chat dashboard, daemon, local global MCP bridge, groups, bus, context, memory, reports, and online helpers. |
|
|
60
61
|
| `uclaude` | Claude Code wrapper with ufoo bootstrap and bus identity. |
|
|
61
62
|
| `ucodex` | Codex wrapper with ufoo bootstrap and bus identity. |
|
|
62
63
|
| `uagy` | Antigravity wrapper with ufoo bootstrap and bus identity. |
|
|
@@ -96,6 +97,16 @@ Use global chat mode to switch between registered projects:
|
|
|
96
97
|
ufoo -g
|
|
97
98
|
```
|
|
98
99
|
|
|
100
|
+
For MCP-capable clients, configure the global stdio bridge once:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ufoo mcp
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The MCP bridge connects to the home-scoped global controller daemon and routes
|
|
107
|
+
project-scoped tools through the global project registry. It is not a separate
|
|
108
|
+
per-project MCP server mode.
|
|
109
|
+
|
|
99
110
|
## Runtime Model
|
|
100
111
|
|
|
101
112
|
```text
|
|
@@ -107,6 +118,11 @@ ufoo / ufoo chat
|
|
|
107
118
|
-> agents launch/providers/internal/controller/activity
|
|
108
119
|
-> coordination bus/context/memory/history/report/state/status
|
|
109
120
|
-> shared controller/worker tools and native ucode tools
|
|
121
|
+
|
|
122
|
+
ufoo mcp
|
|
123
|
+
-> home-scoped global controller daemon
|
|
124
|
+
-> ~/.ufoo/projects/runtime
|
|
125
|
+
-> selected project daemon for bus/report/activity state
|
|
110
126
|
```
|
|
111
127
|
|
|
112
128
|
Chat is a UI client. The daemon owns project runtime state. Agents communicate
|
package/README.zh-CN.md
CHANGED
|
@@ -25,6 +25,7 @@ npm 包:[u-foo](https://www.npmjs.com/package/u-foo)
|
|
|
25
25
|
- 支持 internal、tmux、host、Terminal.app、iTerm2 等启动模式。
|
|
26
26
|
- 内置 group 模板,用于启动和编排多 Agent 工作流。
|
|
27
27
|
- 提供原生 ufoo coding-agent 运行时 `ucode`。
|
|
28
|
+
- 提供 `ufoo mcp` 本机 global MCP bridge,供支持 MCP 的外部 Agent 接入。
|
|
28
29
|
|
|
29
30
|
## 环境要求
|
|
30
31
|
|
|
@@ -54,7 +55,7 @@ npm link
|
|
|
54
55
|
|
|
55
56
|
| 命令 | 用途 |
|
|
56
57
|
|---|---|
|
|
57
|
-
| `ufoo` | 主 CLI、chat 仪表盘、daemon、group、bus、context、memory、report 和 online helper。 |
|
|
58
|
+
| `ufoo` | 主 CLI、chat 仪表盘、daemon、本机 global MCP bridge、group、bus、context、memory、report 和 online helper。 |
|
|
58
59
|
| `uclaude` | Claude Code 包装器,注入 ufoo bootstrap 和 bus 身份。 |
|
|
59
60
|
| `ucodex` | Codex 包装器,注入 ufoo bootstrap 和 bus 身份。 |
|
|
60
61
|
| `uagy` | Antigravity 包装器,注入 ufoo bootstrap 和 bus 身份。 |
|
|
@@ -94,6 +95,15 @@ ucode
|
|
|
94
95
|
ufoo -g
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
给支持 MCP 的客户端使用时,只需要配置一次全局 stdio bridge:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ufoo mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
MCP bridge 会连接 home 级 global controller daemon,并通过全局项目 registry
|
|
105
|
+
把项目级工具路由到对应项目 daemon。它不是每个项目单独部署一个 MCP server 的模式。
|
|
106
|
+
|
|
97
107
|
## 运行模型
|
|
98
108
|
|
|
99
109
|
```text
|
|
@@ -105,6 +115,11 @@ ufoo / ufoo chat
|
|
|
105
115
|
-> agents launch/providers/internal/controller/activity
|
|
106
116
|
-> coordination bus/context/memory/history/report/state/status
|
|
107
117
|
-> shared controller/worker tools and native ucode tools
|
|
118
|
+
|
|
119
|
+
ufoo mcp
|
|
120
|
+
-> home-scoped global controller daemon
|
|
121
|
+
-> ~/.ufoo/projects/runtime
|
|
122
|
+
-> selected project daemon for bus/report/activity state
|
|
108
123
|
```
|
|
109
124
|
|
|
110
125
|
Chat 是 UI client。daemon 拥有项目运行态。Agent 通过 bus queue、prompt
|
package/bin/ufoo.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
const { runCli } = require("../src/app/cli/run");
|
|
4
4
|
const { runDaemonCli } = require("../src/runtime/daemon/run");
|
|
5
|
+
const { runMcpServer } = require("../src/runtime/daemon/mcpServer");
|
|
5
6
|
const { runChat } = require("../src/app/chat");
|
|
6
7
|
const { runInternalRunner } = require("../src/agents/internal/internalRunner");
|
|
7
8
|
const { resolveGlobalControllerProjectRoot } = require("../src/runtime/projects");
|
|
@@ -26,6 +27,12 @@ async function main() {
|
|
|
26
27
|
runDaemonCli(["daemon", ...argv.slice(1)]);
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
30
|
+
if (cmd === "mcp") {
|
|
31
|
+
await runMcpServer({
|
|
32
|
+
autoStart: !argv.includes("--no-auto-start"),
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
29
36
|
if (cmd === "agent-runner") {
|
|
30
37
|
const agentType = argv[1] || "codex";
|
|
31
38
|
const extraArgs = argv.slice(2);
|
package/package.json
CHANGED
package/src/app/cli/run.js
CHANGED
|
@@ -585,6 +585,16 @@ async function runCli(argv) {
|
|
|
585
585
|
if (opts.global === true) args.push("-g");
|
|
586
586
|
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...args]);
|
|
587
587
|
});
|
|
588
|
+
program
|
|
589
|
+
.command("mcp")
|
|
590
|
+
.description("Run the local global ufoo MCP bridge over stdio")
|
|
591
|
+
.option("--no-auto-start", "Do not auto-start the home-scoped global controller daemon")
|
|
592
|
+
.action((opts) => {
|
|
593
|
+
const repoRoot = getPackageRoot();
|
|
594
|
+
const args = ["mcp"];
|
|
595
|
+
if (opts.autoStart === false) args.push("--no-auto-start");
|
|
596
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...args]);
|
|
597
|
+
});
|
|
588
598
|
const project = program.command("project").description("Project runtime commands");
|
|
589
599
|
project
|
|
590
600
|
.command("list")
|
|
@@ -1771,6 +1781,12 @@ async function runCli(argv) {
|
|
|
1771
1781
|
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
|
|
1772
1782
|
return;
|
|
1773
1783
|
}
|
|
1784
|
+
if (cmd === "mcp") {
|
|
1785
|
+
const mcpArgs = ["mcp"];
|
|
1786
|
+
if (rest.includes("--no-auto-start")) mcpArgs.push("--no-auto-start");
|
|
1787
|
+
run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...mcpArgs]);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1774
1790
|
if (cmd === "project") {
|
|
1775
1791
|
const sub = String(rest[0] || "list").trim().toLowerCase();
|
|
1776
1792
|
const outputJson = rest.includes("--json");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
4
|
+
|
|
5
|
+
const MCP_ERROR_CODES = Object.freeze({
|
|
6
|
+
PARSE_ERROR: -32700,
|
|
7
|
+
INVALID_REQUEST: -32600,
|
|
8
|
+
METHOD_NOT_FOUND: -32601,
|
|
9
|
+
INVALID_PARAMS: -32602,
|
|
10
|
+
INTERNAL_ERROR: -32603,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function createJsonRpcResult(id, result) {
|
|
14
|
+
return {
|
|
15
|
+
jsonrpc: "2.0",
|
|
16
|
+
id,
|
|
17
|
+
result,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createJsonRpcError(id, code, message, data = undefined) {
|
|
22
|
+
const error = {
|
|
23
|
+
code,
|
|
24
|
+
message: String(message || "MCP request failed"),
|
|
25
|
+
};
|
|
26
|
+
if (data !== undefined) error.data = data;
|
|
27
|
+
return {
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
id: id === undefined ? null : id,
|
|
30
|
+
error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
MCP_PROTOCOL_VERSION,
|
|
36
|
+
MCP_ERROR_CODES,
|
|
37
|
+
createJsonRpcResult,
|
|
38
|
+
createJsonRpcError,
|
|
39
|
+
};
|
|
@@ -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
|
+
};
|