u-foo 3.0.18 → 3.0.20

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.
@@ -31,9 +31,11 @@ the host's maximum pending-call window ends.
31
31
  ufoo wrapper already registered the Agent and direct injection is its receive
32
32
  path. Do not call MCP `register_agent` or arm a poll.
33
33
  2. When the variable is absent, reuse the exact subscriber already returned to
34
- this session by MCP `register_agent`, or call `register_agent` once. Do not
35
- call bare `ufoo bus join` or borrow another subscriber. Keep the returned
36
- value as the external Agent's identity.
34
+ this session by MCP `register_agent` together with its `agent_handle`, or
35
+ call `register_agent` once. Pass a stable `client_instance_id` when the host
36
+ exposes one. Do not call bare `ufoo bus join` or borrow another subscriber.
37
+ Keep the returned pair as the external Agent's identity. Never expose the
38
+ handle in messages, reports, logs, or the Cursor listener terminal.
37
39
  3. Select the receive mechanism from the host App's actual wake capability,
38
40
  never from Agent type:
39
41
 
@@ -59,6 +61,7 @@ prints stdout. Keep the wait inside one foreground MCP tool call instead:
59
61
 
60
62
  - `project_root`: the registered project root
61
63
  - `subscriber`: the caller-owned MCP subscriber
64
+ - `agent_handle`: the opaque handle returned with that registration
62
65
  - `after_seq`: `0` for the first wait, then the last returned `last_seq`
63
66
  - `timeout_seconds`: `600`
64
67
  2. Leave that tool call pending. Do not background it and do not start
@@ -66,7 +69,7 @@ prints stdout. Keep the wait inside one foreground MCP tool call instead:
66
69
  not invoke the model or consume model tokens.
67
70
  3. If it returns `status: "message"`, handle every returned message, then call
68
71
  MCP `ack_bus` with `through_seq: <last_seq>`. This preserves messages that
69
- arrived after the returned batch.
72
+ arrived after the returned batch. Include the same `agent_handle`.
70
73
  4. If it returns `status: "timeout"`, no message was received. If monitoring is
71
74
  still required, immediately call `wait_for_message` again with the same
72
75
  `after_seq`. The timeout return is the only periodic model wake.
@@ -127,6 +130,7 @@ For Cursor, handle every `[ufoo]<from:...>` event in the emitted batch.
127
130
  2. Acknowledge only after handling:
128
131
 
129
132
  - Codex App: MCP `ack_bus` with the returned `last_seq` as `through_seq`.
133
+ Include the caller-owned `agent_handle`.
130
134
  - Cursor: run the exact command printed by the stream:
131
135
 
132
136
  ```bash
@@ -136,8 +140,8 @@ For Cursor, handle every `[ufoo]<from:...>` event in the emitted batch.
136
140
  Preserve the sequence boundary so later messages stay pending.
137
141
  3. Reply only with a requested result, answer, blocker, or fact the sender
138
142
  needs. Use MCP `dispatch_message` with the same `project_root`,
139
- caller-owned subscriber, sender ID as `target`, and substantive result as
140
- `message`.
143
+ caller-owned subscriber and `agent_handle`, sender ID as `target`, and
144
+ substantive result as `message`.
141
145
 
142
146
  Do not reply to greetings, thanks, or acknowledgement-only messages. After
143
147
  sending, continue the current task. Keep the existing Cursor stream running, or
package/README.md CHANGED
@@ -27,8 +27,8 @@ 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 with pending-call bus wake for external
31
- MCP-capable agents.
30
+ - One loopback Streamable HTTP MCP server inside the home-scoped global
31
+ controller daemon, plus a disposable `ufoo mcp` stdio compatibility proxy.
32
32
 
33
33
  ## Requirements
34
34
 
@@ -58,7 +58,7 @@ Installed binaries:
58
58
 
59
59
  | Binary | Purpose |
60
60
  |---|---|
61
- | `ufoo` | Main CLI, chat dashboard, daemon, local global MCP bridge, groups, bus, context, memory, reports, and online helpers. |
61
+ | `ufoo` | Main CLI, chat dashboard, daemons, global MCP server/proxy, groups, bus, context, memory, reports, and online helpers. |
62
62
  | `uclaude` | Claude Code wrapper with ufoo bootstrap and bus identity. |
63
63
  | `ucodex` | Codex wrapper with ufoo bootstrap and bus identity. |
64
64
  | `uagy` | Antigravity wrapper with ufoo bootstrap and bus identity. |
@@ -100,15 +100,27 @@ Use global chat mode to switch between registered projects:
100
100
  ufoo -g
101
101
  ```
102
102
 
103
- For MCP-capable clients, configure the global stdio bridge once:
103
+ For Codex App, Codex CLI, and the Codex IDE extension, start global mode once
104
+ and install the shared direct HTTP configuration:
105
+
106
+ ```bash
107
+ ufoo -g
108
+ ufoo mcp configure codex
109
+ ```
110
+
111
+ The configuration points all three Codex surfaces at the same authenticated
112
+ loopback endpoint. Restart the Codex surface after configuring it.
113
+
114
+ For a host that has not been verified with direct HTTP, keep the compatible
115
+ stdio configuration:
104
116
 
105
117
  ```bash
106
118
  ufoo mcp
107
119
  ```
108
120
 
109
- The MCP bridge connects to the home-scoped global controller daemon and routes
110
- project-scoped tools through the global project registry. It is not a separate
111
- per-project MCP server mode.
121
+ This command is a stateless transport proxy into the same global server. It
122
+ does not own Agent registrations or project state. Inspect or restart the
123
+ singleton listener with `ufoo mcp status` and `ufoo mcp restart`.
112
124
 
113
125
  ## Runtime Model
114
126
 
@@ -122,10 +134,13 @@ ufoo / ufoo chat
122
134
  -> coordination bus/context/memory/history/report/state/status
123
135
  -> shared controller/worker tools and native ucode tools
124
136
 
125
- ufoo mcp
126
- -> home-scoped global controller daemon
137
+ Codex App / CLI / IDE -> Streamable HTTP --+
138
+ ufoo mcp stdio proxy ----------------------+
139
+ -> home-scoped global controller daemon
140
+ (one MCP listener and tool router)
127
141
  -> ~/.ufoo/projects/runtime
128
- -> selected project daemon for bus/report/activity state
142
+ -> ProjectRuntimeGateway
143
+ -> selected project daemon for bus/report/activity/wait state
129
144
  ```
130
145
 
131
146
  ### Agent Delivery Modes
@@ -140,7 +155,8 @@ inherited `UFOO_SUBSCRIBER_ID` before any helper terminal is started:
140
155
  or run a resident bus poll.
141
156
  - Externally hosted Agents have no wrapper-provided subscriber environment.
142
157
  They register themselves once through MCP `register_agent`, retain the
143
- returned subscriber, and select the host App's native no-token wait:
158
+ returned subscriber plus opaque `agent_handle`, and select the host App's
159
+ native no-token wait:
144
160
  Codex App keeps MCP `wait_for_message` pending, while Cursor monitors
145
161
  `ufoo bus poll --follow` background output with `notify_on_output`.
146
162
 
@@ -237,14 +253,18 @@ ufoo skills list --optional
237
253
  ufoo skills install ufoo-bus-poll --target /path/to/that/agent/skills
238
254
  ```
239
255
 
240
- Register once through MCP `register_agent` and retain its returned subscriber.
256
+ Register once through MCP `register_agent` and retain its returned subscriber
257
+ and `agent_handle`. Include the handle in heartbeat, activity, send, receive,
258
+ acknowledgement, report, and unregister calls. The handle is an ownership
259
+ capability: do not send it to peers or print it in reports.
241
260
 
242
261
  - **Codex App:** call MCP `wait_for_message` in the foreground with
243
- `after_seq: 0` and `timeout_seconds: 600`. The tool call stays pending inside
244
- ufoo; a message returns immediately and wakes the task without shell stdout.
245
- On timeout, re-arm with the same cursor. After handling a message response,
246
- call MCP `ack_bus` with its `last_seq` as `through_seq`, then re-arm with that
247
- `last_seq` when the Agent is idle again.
262
+ the registered subscriber and handle, `after_seq: 0`, and
263
+ `timeout_seconds: 600`. The tool call stays pending inside ufoo; a message
264
+ returns immediately and wakes the task without shell stdout. On timeout,
265
+ re-arm with the same cursor. After handling a message response, call MCP
266
+ `ack_bus` with the same handle and its `last_seq` as `through_seq`, then
267
+ re-arm with that `last_seq` when the Agent is idle again.
248
268
  - **Cursor:** bind the MCP subscriber and run
249
269
  `export UFOO_SUBSCRIBER_ID="<subscriber-id>"; exec ufoo bus poll
250
270
  "$UFOO_SUBSCRIBER_ID" --follow --interval 30` through the monitored
package/README.zh-CN.md CHANGED
@@ -25,8 +25,8 @@ 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,并用 pending tool call 唤醒支持
29
- MCP 的外部 Agent。
28
+ - home global controller daemon 内提供唯一的本机 Streamable HTTP MCP
29
+ server;`ufoo mcp` 仅作为可随时销毁的 stdio 兼容代理。
30
30
 
31
31
  ## 环境要求
32
32
 
@@ -56,7 +56,7 @@ npm link
56
56
 
57
57
  | 命令 | 用途 |
58
58
  |---|---|
59
- | `ufoo` | 主 CLI、chat 仪表盘、daemon、本机 global MCP bridge、group、bus、context、memory、report 和 online helper。 |
59
+ | `ufoo` | 主 CLI、chat 仪表盘、daemonglobal MCP server/proxy、group、bus、context、memory、report 和 online helper。 |
60
60
  | `uclaude` | Claude Code 包装器,注入 ufoo bootstrap 和 bus 身份。 |
61
61
  | `ucodex` | Codex 包装器,注入 ufoo bootstrap 和 bus 身份。 |
62
62
  | `uagy` | Antigravity 包装器,注入 ufoo bootstrap 和 bus 身份。 |
@@ -98,14 +98,18 @@ ucode
98
98
  ufoo -g
99
99
  ```
100
100
 
101
- 给支持 MCP 的客户端使用时,只需要配置一次全局 stdio bridge:
101
+ Codex App、Codex CLI Codex IDE extension 共用同一份配置。先启动一次
102
+ global mode,再写入经过验证的直接 HTTP 配置:
102
103
 
103
104
  ```bash
104
- ufoo mcp
105
+ ufoo -g
106
+ ufoo mcp configure codex
105
107
  ```
106
108
 
107
- MCP bridge 会连接 home global controller daemon,并通过全局项目 registry
108
- 把项目级工具路由到对应项目 daemon。它不是每个项目单独部署一个 MCP server 的模式。
109
+ 重启 Codex surface 后即可使用。尚未验证直接 HTTP host 继续配置
110
+ `ufoo mcp`;该命令只是连接同一 global server 的无状态 stdio 代理,不拥有
111
+ Agent 注册或项目状态。用 `ufoo mcp status` 检查唯一 listener,用
112
+ `ufoo mcp restart` 只重启 listener。
109
113
 
110
114
  ## 运行模型
111
115
 
@@ -119,10 +123,13 @@ ufoo / ufoo chat
119
123
  -> coordination bus/context/memory/history/report/state/status
120
124
  -> shared controller/worker tools and native ucode tools
121
125
 
122
- ufoo mcp
123
- -> home-scoped global controller daemon
126
+ Codex App / CLI / IDE -> Streamable HTTP --+
127
+ ufoo mcp stdio proxy ----------------------+
128
+ -> home-scoped global controller daemon
129
+ -> one MCP listener and tool router
124
130
  -> ~/.ufoo/projects/runtime
125
- -> selected project daemon for bus/report/activity state
131
+ -> ProjectRuntimeGateway
132
+ -> selected project daemon for bus/report/activity/wait state
126
133
  ```
127
134
 
128
135
  ### Agent 消息投递模式
@@ -135,9 +142,10 @@ ufoo 支持两种 Agent 投递模式,只根据启动辅助 terminal 之前,
135
142
  注入端点,因此可以直接注入 bus 消息。这类 Agent 复用环境中的身份,
136
143
  不通过 MCP 重复注册,也不运行常驻 bus poll。
137
144
  - 外部 host 托管的 Agent 没有包装器提供的 subscriber 环境变量。它通过 MCP
138
- `register_agent` 注册一次,保留返回的 subscriber,再选择宿主 App 原生的
139
- token 等待方式:Codex App 挂起 MCP `wait_for_message`,Cursor 则用
140
- `notify_on_output` 监控 `ufoo bus poll --follow` 的后台输出。
145
+ `register_agent` 注册一次,保留返回的 subscriber 和不透明
146
+ `agent_handle`,再选择宿主 App 原生的无 token 等待方式:Codex App 挂起
147
+ MCP `wait_for_message`,Cursor 则用 `notify_on_output` 监控
148
+ `ufoo bus poll --follow` 的后台输出。
141
149
 
142
150
  Agent 类型名和 subscriber 前缀只是路由元数据,不是能力判断条件。外部
143
151
  Cursor Agent 通过 MCP 注册后,可以在专用监听 terminal 内把返回的
@@ -225,13 +233,16 @@ ufoo skills list --optional
225
233
  ufoo skills install ufoo-bus-poll --target /path/to/that/agent/skills
226
234
  ```
227
235
 
228
- 先通过 MCP `register_agent` 注册一次并保留返回的 subscriber
236
+ 先通过 MCP `register_agent` 注册一次并保留返回的 subscriber
237
+ `agent_handle`。heartbeat、activity、send、receive、ack、report 和
238
+ unregister 都带上该 handle;它是所有权凭证,不应发送给其他 Agent,也不应
239
+ 写进 report。
229
240
 
230
- - **Codex App:**前台调用 MCP `wait_for_message`,首次使用
231
- `after_seq: 0`、`timeout_seconds: 600`。工具调用在 ufoo 内保持 pending;
232
- 有消息立即返回并唤醒当前任务,不依赖 shell stdout。超时后用相同游标续挂;
233
- 处理消息后,用返回的 `last_seq` 作为 MCP `ack_bus.through_seq`,Agent
234
- 再次空闲时以该 `last_seq` 续挂。
241
+ - **Codex App:**前台调用 MCP `wait_for_message`,传入注册所得 subscriber
242
+ 和 handle,首次使用 `after_seq: 0`、`timeout_seconds: 600`。工具调用在
243
+ ufoo 内保持 pending;有消息立即返回并唤醒当前任务,不依赖 shell stdout
244
+ 超时后用相同游标续挂;处理消息后,用同一 handle 和返回的 `last_seq`
245
+ 调用 MCP `ack_bus`,Agent 再次空闲时以该 `last_seq` 续挂。
235
246
  - **Cursor:**在 monitored background shell 中绑定 MCP subscriber,再运行
236
247
  `export UFOO_SUBSCRIBER_ID="<subscriber-id>"; exec ufoo bus poll
237
248
  "$UFOO_SUBSCRIBER_ID" --follow --interval 30`,设置
@@ -23,18 +23,21 @@ capabilities from the Agent type or subscriber prefix.
23
23
  `register_agent`, run `ufoo bus join`, or start `ufoo bus poll`; the wrapper
24
24
  and daemon deliver follow-ups by direct prompt injection.
25
25
  - If `UFOO_SUBSCRIBER_ID` is absent, the Agent is externally hosted. Reuse the
26
- subscriber already returned to this session by MCP `register_agent`, or call
27
- `register_agent` once if none exists. Keep that returned subscriber as the
28
- session identity and invoke `$ufoo-bus-poll` to establish the receive path
29
- using this host App's own no-token wait and self-wake primitive. A
26
+ subscriber and `agent_handle` already returned to this session, or call
27
+ MCP `register_agent` once if none exists. When the host
28
+ exposes a stable local session identifier, pass it as `client_instance_id`
29
+ so a transport restart can recover the subscriber and rotate the handle.
30
+ Keep the returned pair as session-local identity; never send or report the
31
+ handle to another Agent. Invoke `$ufoo-bus-poll` to establish the receive
32
+ path using this host App's own no-token wait and self-wake primitive. A
30
33
  Cursor-style dedicated listener terminal may export the returned value as
31
34
  `UFOO_SUBSCRIBER_ID` after registration so its CLI commands share the
32
35
  identity. That helper-local export does not change the already selected
33
36
  external delivery mode and must not be treated as wrapper evidence.
34
37
 
35
38
  In the sections below, `<subscriber-id>` means the wrapper-provided
36
- `UFOO_SUBSCRIBER_ID` or the subscriber returned by MCP, according to this
37
- branch.
39
+ `UFOO_SUBSCRIBER_ID` or the subscriber returned by MCP. `<agent-handle>` means
40
+ the opaque capability returned with an external MCP registration.
38
41
 
39
42
  ## Synchronize workspace state
40
43
 
@@ -60,20 +63,25 @@ Treat these prompt prefixes as work inputs:
60
63
  For each received bus task:
61
64
 
62
65
  1. Execute it immediately within the current authority.
63
- 2. Acknowledge it only after handling:
66
+ 2. Acknowledge it only after handling. A wrapper-managed Agent uses:
64
67
 
65
68
  ```bash
66
69
  ufoo bus ack "<subscriber-id>"
67
70
  ```
68
71
 
72
+ An external Agent calls MCP `ack_bus` with `project_root`, `subscriber`,
73
+ `agent_handle: "<agent-handle>"`, and the delivered sequence boundary.
69
74
  3. Reply only with a requested answer, delegated result, or fact the sender
70
75
  needs to continue. Do not reply with greetings or bare acknowledgements.
71
- 4. Emit a concise runtime report for delegated work:
76
+ 4. Emit a concise runtime report for delegated work. A wrapper-managed Agent
77
+ uses:
72
78
 
73
79
  ```bash
74
80
  ufoo report done "<summary>" --agent "<subscriber-id>"
75
81
  ```
76
82
 
83
+ An external Agent calls MCP `report_agent_status` with the same subscriber
84
+ and handle.
77
85
  After sending or broadcasting, continue the current task. Do not start an
78
86
  ad-hoc check or poll, sleep, or wait for a reply. A wrapper-managed Agent
79
87
  receives follow-ups by direct injection; an external Agent leaves its existing
@@ -29,8 +29,11 @@ signal. Evaluate it before creating or mutating helper terminals:
29
29
  value. Do not call MCP `register_agent`, run `ufoo bus join`, or arm resident
30
30
  polling.
31
31
  - Absent: this is an externally hosted Agent. Reuse this session's
32
- MCP-registered subscriber or call MCP `register_agent` exactly once. Keep the
33
- returned value as `<subscriber-id>` for later MCP and CLI operations. Invoke
32
+ MCP-registered subscriber and `agent_handle`, or call MCP `register_agent`
33
+ exactly once. If the host exposes a stable local session id, pass it as
34
+ `client_instance_id`. Keep the returned pair as `<subscriber-id>` and
35
+ `<agent-handle>` for later MCP operations. Treat the handle as a private
36
+ capability: do not send, log, or report it. Invoke
34
37
  `$ufoo-bus-poll` once at session start to select the receive path supported
35
38
  by the host App. That skill may bind the subscriber as
36
39
  `UFOO_SUBSCRIBER_ID` inside a dedicated Cursor listener terminal after MCP
@@ -49,7 +52,8 @@ ufoo bus check "<subscriber-id>"
49
52
  ```
50
53
 
51
54
  For an external Agent, use MCP `poll_inbox` with its `project_root` and
52
- caller-owned `<subscriber-id>` only when an explicit manual check is required.
55
+ caller-owned `<subscriber-id>` and `<agent-handle>` only when an explicit
56
+ manual check is required.
53
57
  If `$ufoo-bus-poll` woke the session with an emitted pending batch, handle that
54
58
  batch directly. Do not run a second check for the same wake.
55
59
 
@@ -68,6 +72,7 @@ For every pending batch:
68
72
  pending. For a `$ufoo-bus-poll` wake, run the exact `ack --through <seq>`
69
73
  line printed by the Cursor stream. A Codex App wait returns `last_seq`; pass
70
74
  it to MCP `ack_bus` as `through_seq`.
75
+ Every external `ack_bus` call also includes the caller-owned `agent_handle`.
71
76
  4. Reply only when the sender requested an answer, delegated work whose result
72
77
  is needed, or needs a discovered blocker or fact.
73
78
 
@@ -92,9 +97,9 @@ ufoo bus broadcast "<substantive message>"
92
97
  ```
93
98
 
94
99
  For an external Agent, call MCP `dispatch_message` with its `project_root`,
95
- caller-owned `subscriber`, `target`, and `message`. Use target `*` for a
96
- broadcast. This preserves the registered sender identity instead of creating a
97
- CLI-side identity.
100
+ caller-owned `subscriber`, `agent_handle`, `target`, and `message`. Use target
101
+ `*` for a broadcast. This preserves the registered sender identity instead of
102
+ creating a CLI-side identity.
98
103
 
99
104
  Target resolution order is exact ID, nickname, agent type, then `*`.
100
105
 
@@ -120,7 +125,8 @@ using the rules above and leave re-arming or stream lifecycle to
120
125
 
121
126
  ## Report delegated work
122
127
 
123
- Use the shared report contract when bus work represents a task:
128
+ Use the shared report contract when bus work represents a task. A
129
+ wrapper-managed Agent uses:
124
130
 
125
131
  ```bash
126
132
  ufoo report start "<task>" --task <id> --agent "<subscriber-id>"
@@ -130,3 +136,6 @@ ufoo report error "<reason>" --task <id> --agent "<subscriber-id>"
130
136
  ```
131
137
 
132
138
  Use `--scope private` only for helper-internal reports.
139
+
140
+ An external Agent calls MCP `report_agent_status` with `project_root`,
141
+ `subscriber`, `agent_handle`, the task id, phase, and summary.
package/bin/ufoo.js CHANGED
@@ -14,16 +14,21 @@ function hasGlobalModeFlag(args = []) {
14
14
  }
15
15
 
16
16
  function printMcpHelp() {
17
- console.log("Usage: ufoo mcp [options]");
17
+ console.log("Usage: ufoo mcp [status|restart|configure codex] [options]");
18
18
  console.log("");
19
- console.log("Run the local global ufoo MCP bridge over stdio.");
19
+ console.log("Run the stdio compatibility proxy or control the global MCP listener.");
20
20
  console.log("");
21
21
  console.log("Options:");
22
22
  console.log(" --no-auto-start Do not auto-start the home-scoped global controller daemon");
23
+ console.log(" --json Print status or restart results as JSON");
24
+ console.log(" --dry-run Print a sanitized Codex migration preview without writing it");
23
25
  console.log(" -h, --help Display help for the MCP bridge command");
24
26
  console.log("");
25
27
  console.log("Notes:");
26
28
  console.log(" Configure MCP-capable clients with command: ufoo mcp");
29
+ console.log(" Inspect the singleton listener: ufoo mcp status");
30
+ console.log(" Restart only the listener: ufoo mcp restart");
31
+ console.log(" Configure Codex App/CLI/IDE: ufoo mcp configure codex");
27
32
  console.log(" Example without daemon auto-start: ufoo mcp --no-auto-start");
28
33
  console.log(" Human diagnostics are available in chat: /mcp status, /mcp tools, /mcp help");
29
34
  }
@@ -47,9 +52,18 @@ async function main() {
47
52
  printMcpHelp();
48
53
  return;
49
54
  }
50
- await runMcpServer({
51
- autoStart: !argv.includes("--no-auto-start"),
52
- });
55
+ const operation = argv[1];
56
+ if (operation === "status" || operation === "restart") {
57
+ const { runMcpControlCli } = require("../src/runtime/daemon/mcpControl");
58
+ await runMcpControlCli(operation, { json: argv.includes("--json") });
59
+ } else if (operation === "configure") {
60
+ const { runMcpConfigureCli } = require("../src/runtime/daemon/mcpConfigure");
61
+ runMcpConfigureCli(argv[2], { dryRun: argv.includes("--dry-run") });
62
+ } else {
63
+ await runMcpServer({
64
+ autoStart: !argv.includes("--no-auto-start"),
65
+ });
66
+ }
53
67
  return;
54
68
  }
55
69
  if (cmd === "agent-runner") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.18",
3
+ "version": "3.0.20",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@anthropic-ai/claude-agent-sdk": "^0.2.138",
59
+ "@modelcontextprotocol/sdk": "^1.30.0",
59
60
  "@openai/codex-sdk": "^0.145.0",
60
61
  "@xterm/addon-serialize": "^0.14.0",
61
62
  "@xterm/headless": "^6.0.0",
@@ -357,10 +357,12 @@ function createCommandExecutor(options = {}) {
357
357
  }
358
358
 
359
359
  function logMcpHelp() {
360
- logMessage("system", "{cyan-fg}MCP bridge:{/cyan-fg} local stdio bridge for external MCP-capable agents");
361
- logMessage("system", " • Configure client command: ufoo mcp");
360
+ logMessage("system", "{cyan-fg}MCP server:{/cyan-fg} singleton listener in the global controller daemon");
361
+ logMessage("system", " • Configure Codex direct HTTP: ufoo mcp configure codex");
362
+ logMessage("system", " • Compatibility proxy command: ufoo mcp");
362
363
  logMessage("system", " • Disable daemon auto-start: ufoo mcp --no-auto-start");
363
- logMessage("system", " • Topology: one global bridge, project tools route through registered project daemons");
364
+ logMessage("system", " • Listener control: ufoo mcp status | ufoo mcp restart");
365
+ logMessage("system", " • Topology: one global tool router; project calls use registered project runtimes");
364
366
  logMessage("system", " • Chat diagnostics: /mcp status, /mcp tools, /mcp help");
365
367
  }
366
368
 
@@ -417,8 +419,12 @@ function createCommandExecutor(options = {}) {
417
419
  throw new Error("empty MCP status response");
418
420
  }
419
421
  const projects = Array.isArray(status.projects) ? status.projects : [];
420
- logMessage("system", "{cyan-fg}MCP bridge:{/cyan-fg} local stdio server");
421
- logMessage("system", "command: {cyan-fg}ufoo mcp{/cyan-fg}");
422
+ logMessage("system", "{cyan-fg}MCP server:{/cyan-fg} global Streamable HTTP listener");
423
+ logMessage("system", `listener: ${status.http?.running ? "{green-fg}running{/green-fg}" : "{yellow-fg}not running{/yellow-fg}"}`);
424
+ if (status.http?.endpoint) {
425
+ logMessage("system", ` • endpoint: ${escapeBlessed(status.http.endpoint)}`);
426
+ }
427
+ logMessage("system", " • stdio fallback: {cyan-fg}ufoo mcp{/cyan-fg}");
422
428
  logMessage("system", ` • global controller: ${escapeBlessed(status.global_controller_root || "")}`);
423
429
  logMessage("system", ` • daemon: ${status.global_controller_running ? "{green-fg}running{/green-fg}" : "{yellow-fg}not running{/yellow-fg}"}`);
424
430
  logMessage("system", " • client auto-start: enabled by default");
@@ -587,7 +587,7 @@ async function runCli(argv) {
587
587
  });
588
588
  program
589
589
  .command("mcp")
590
- .description("Run the local global ufoo MCP bridge over stdio")
590
+ .description("Run the global MCP stdio proxy; use bin entry for listener status/configuration")
591
591
  .option("--no-auto-start", "Do not auto-start the home-scoped global controller daemon")
592
592
  .action((opts) => {
593
593
  const repoRoot = getPackageRoot();
@@ -1809,8 +1809,7 @@ async function runCli(argv) {
1809
1809
  return;
1810
1810
  }
1811
1811
  if (cmd === "mcp") {
1812
- const mcpArgs = ["mcp"];
1813
- if (rest.includes("--no-auto-start")) mcpArgs.push("--no-auto-start");
1812
+ const mcpArgs = ["mcp", ...rest];
1814
1813
  run(resolveNodeExecutable(), [path.join(repoRoot, "bin", "ufoo.js"), ...mcpArgs]);
1815
1814
  return;
1816
1815
  }
package/src/config.js CHANGED
@@ -36,6 +36,7 @@ const DEFAULT_CONFIG = {
36
36
  launchMode: "auto",
37
37
  agentProvider: "codex-cli",
38
38
  controllerMode: "main",
39
+ mcpPort: 47631,
39
40
  codexInternalThreadMode: "api",
40
41
  codexAuthPath: "",
41
42
  codexOauthRefreshWindowSec: 300,
@@ -125,6 +126,12 @@ function normalizeCodexInternalThreadMode(value) {
125
126
  return "legacy";
126
127
  }
127
128
 
129
+ function normalizeMcpPort(value) {
130
+ const parsed = Number.parseInt(value, 10);
131
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return DEFAULT_CONFIG.mcpPort;
132
+ return parsed;
133
+ }
134
+
128
135
  function normalizeCodexAuthPath(value) {
129
136
  return typeof value === "string" ? value.trim() : "";
130
137
  }
@@ -184,6 +191,7 @@ function loadConfig(projectRoot) {
184
191
  controllerMode: Object.prototype.hasOwnProperty.call(raw, "controllerMode")
185
192
  ? normalizeControllerMode(raw.controllerMode)
186
193
  : DEFAULT_CONFIG.controllerMode,
194
+ mcpPort: normalizeMcpPort(raw.mcpPort),
187
195
  codexInternalThreadMode: Object.prototype.hasOwnProperty.call(raw, "codexInternalThreadMode")
188
196
  ? normalizeCodexInternalThreadMode(raw.codexInternalThreadMode)
189
197
  : DEFAULT_CONFIG.codexInternalThreadMode,
@@ -240,6 +248,7 @@ function saveConfig(projectRoot, config) {
240
248
  merged.routerProvider = typeof merged.routerProvider === "string" ? merged.routerProvider.trim() : "";
241
249
  merged.routerModel = typeof merged.routerModel === "string" ? merged.routerModel.trim() : "";
242
250
  merged.controllerMode = normalizeControllerMode(merged.controllerMode);
251
+ merged.mcpPort = normalizeMcpPort(merged.mcpPort);
243
252
  merged.codexInternalThreadMode = normalizeCodexInternalThreadMode(merged.codexInternalThreadMode);
244
253
  merged.codexAuthPath = normalizeCodexAuthPath(merged.codexAuthPath);
245
254
  merged.codexOauthRefreshWindowSec = normalizeCodexOauthRefreshWindowSec(merged.codexOauthRefreshWindowSec);
@@ -291,6 +300,7 @@ module.exports = {
291
300
  defaultRouterProviderForAgentProvider,
292
301
  defaultRouterModelForProvider,
293
302
  normalizeControllerMode,
303
+ normalizeMcpPort,
294
304
  normalizeCodexInternalThreadMode,
295
305
  normalizeCodexAuthPath,
296
306
  normalizeCodexOauthRefreshWindowSec,
@@ -100,6 +100,15 @@ function hasProviderSession(meta) {
100
100
  return typeof meta?.provider_session_id === "string" && meta.provider_session_id.trim() !== "";
101
101
  }
102
102
 
103
+ function isManagedMcpRegistration(meta) {
104
+ return Boolean(meta && meta.mcp_bridge === true && meta.mcp_agent_handle_hash);
105
+ }
106
+
107
+ function isRecentMcpHeartbeat(meta, nowMs = Date.now()) {
108
+ const lastSeenMs = Date.parse(String(meta?.last_seen || ""));
109
+ return Number.isFinite(lastSeenMs) && nowMs - lastSeenMs <= 30 * 1000;
110
+ }
111
+
103
112
  /**
104
113
  * 订阅者管理
105
114
  */
@@ -458,6 +467,29 @@ class SubscriberManager {
458
467
  if (!this.busData.agents) return;
459
468
 
460
469
  for (const [id, meta] of Object.entries(this.busData.agents)) {
470
+ if (isManagedMcpRegistration(meta)) {
471
+ const expiresAtMs = Date.parse(String(meta.mcp_lease_expires_at || ""));
472
+ const expired = !Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now();
473
+ if (!expired || isRecentMcpHeartbeat(meta)) {
474
+ continue;
475
+ }
476
+ if (meta.status === "active") {
477
+ this.logRegistry("cleanup_inactive_mark", {
478
+ source: "bus.subscriber.cleanupInactive",
479
+ subscriber: id,
480
+ reason: "mcp_agent_lease_expired",
481
+ status: meta.status || "",
482
+ launch_mode: meta.launch_mode || "",
483
+ last_seen: meta.last_seen || "",
484
+ lease_expires_at: meta.mcp_lease_expires_at || "",
485
+ });
486
+ meta.status = "inactive";
487
+ meta.activity_state = "";
488
+ meta.mcp_revoked_at = getTimestamp();
489
+ this.cleanupSubscriberArtifacts(id);
490
+ }
491
+ continue;
492
+ }
461
493
  if (isInternalLaunchMode(meta)) {
462
494
  const recoverable = hasProviderSession(meta);
463
495
  if (meta.status === "inactive") {
@@ -24,6 +24,8 @@ function getUfooPaths(projectRoot) {
24
24
  const ufooDaemonPid = path.join(runDir, "ufoo-daemon.pid");
25
25
  const ufooDaemonLog = path.join(runDir, "ufoo-daemon.log");
26
26
  const ufooSock = path.join(runDir, "ufoo.sock");
27
+ const mcpToken = path.join(runDir, "mcp-token");
28
+ const mcpEndpoint = path.join(runDir, "mcp-endpoint.json");
27
29
 
28
30
  return {
29
31
  ufooDir,
@@ -46,6 +48,8 @@ function getUfooPaths(projectRoot) {
46
48
  ufooDaemonPid,
47
49
  ufooDaemonLog,
48
50
  ufooSock,
51
+ mcpToken,
52
+ mcpEndpoint,
49
53
  };
50
54
  }
51
55
 
@@ -20,6 +20,10 @@ const IPC_REQUEST_TYPES = {
20
20
  AGENT_REPORT: "agent_report",
21
21
  ASSIGN_ROLE: "assign_role",
22
22
  REFRESH_STATUS: "refresh_status",
23
+ CONTROL_PLANE_CALL: "control_plane_call",
24
+ CONTROL_PLANE_CANCEL: "control_plane_cancel",
25
+ MCP_STATUS: "mcp_status",
26
+ MCP_RESTART: "mcp_restart",
23
27
  };
24
28
 
25
29
  const IPC_RESPONSE_TYPES = {
@@ -29,6 +33,7 @@ const IPC_RESPONSE_TYPES = {
29
33
  ERROR: "error",
30
34
  BUS_SEND_OK: "bus_send_ok",
31
35
  REGISTER_OK: "register_ok",
36
+ CONTROL_PLANE_RESULT: "control_plane_result",
32
37
  };
33
38
 
34
39
  const BUS_STATUS_PHASES = {