cross-agent-teams-mcp 0.3.0 → 0.3.2
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 +24 -9
- package/README.zh-CN.md +24 -9
- package/dist/channel-cli.d.ts +18 -0
- package/dist/channel-cli.js +281 -0
- package/dist/channel-cli.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ An MCP daemon for cross-agent collaboration, with local delivery transports for
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
This package ships
|
|
9
|
+
This package ships two bins on the same npm name: the long-running HTTP daemon (`cross-agent-teams-mcp`) and a stdio channel proxy (`cross-agent-teams-channel`) that lets Claude Code receive `notifications/channel_wake` from the daemon. You start the daemon once on your machine, then point Claude Code at the proxy as an MCP server, and finally launch Claude with the experimental channel loader so it subscribes to wake notifications. There is no auto-bootstrap — the proxy MUST NOT and WILL NOT start a daemon for you; if the daemon is unreachable the proxy fails fast.
|
|
10
10
|
|
|
11
11
|
### 1. Start the daemon
|
|
12
12
|
|
|
@@ -22,28 +22,43 @@ You can verify the service with:
|
|
|
22
22
|
curl http://127.0.0.1:9100/health
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
### 2. Configure
|
|
25
|
+
### 2. Configure Claude Code's MCP client to use the channel proxy
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Add the channel proxy to `.mcp.json` (or `~/.claude.json`). The MCP server name MUST match the `server:<name>` suffix passed to Claude in step 3:
|
|
28
28
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
31
|
"mcpServers": {
|
|
32
32
|
"cross-agent-teams": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": [
|
|
35
|
+
"-y",
|
|
36
|
+
"-p",
|
|
37
|
+
"cross-agent-teams-mcp@latest",
|
|
38
|
+
"cross-agent-teams-channel",
|
|
39
|
+
"--daemon-url",
|
|
40
|
+
"http://127.0.0.1:9100/mcp"
|
|
41
|
+
]
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
If you started the daemon with `--token <t>`, set `CROSS_AGENT_TEAMS_MCP_DAEMON_URL` along with the appropriate header forwarding in your daemon-side configuration; the proxy itself reads only the daemon URL.
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
For Codex CLI, see [docs/configs/codex-cli.md](docs/configs/codex-cli.md) — Codex talks to the daemon directly over Streamable HTTP and does not need the channel proxy. For opencode see the "Using opencode with xats (tmux)" section below.
|
|
43
50
|
|
|
44
|
-
### 3.
|
|
51
|
+
### 3. Start Claude Code with the channel loader
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
```bash
|
|
54
|
+
claude --dangerously-load-development-channels server:cross-agent-teams
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The `server:<name>` suffix MUST equal the MCP server name configured in `.mcp.json` (`cross-agent-teams` above). Note this is **the MCP server key in `.mcp.json`**, not the npm bin name — the bin name happens to be `cross-agent-teams-channel`, but the MCP server key is yours to choose. If the names disagree, Claude Code's experimental channel loader will not wire the proxy in and you will not see channel wake notifications.
|
|
58
|
+
|
|
59
|
+
### 4. Register from inside the agent
|
|
60
|
+
|
|
61
|
+
Once Claude Code is connected through the proxy, register from inside the agent session — see `register_claude_self`, `register_codex_self`, and `register_agent` below.
|
|
47
62
|
|
|
48
63
|
### Running from source
|
|
49
64
|
|
package/README.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 快速开始
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
这个包在同一个 npm 名字下发布两个 bin: 长驻的 HTTP daemon (`cross-agent-teams-mcp`) 以及一个 stdio channel proxy (`cross-agent-teams-channel`), 后者让 Claude Code 能从 daemon 收到 `notifications/channel_wake`. 你先在本机起一次 daemon, 然后让 Claude Code 把 proxy 当作 MCP server 接进来, 最后用 channel loader 启动 Claude 让它订阅 wake 通知. 没有"自动拉起 daemon" 这种行为 — proxy 不会, 也禁止替你启动 daemon; daemon 不可达时 proxy 直接 fail-fast 退出非零.
|
|
10
10
|
|
|
11
11
|
### 1. 启动 daemon
|
|
12
12
|
|
|
@@ -22,28 +22,43 @@ npx -y cross-agent-teams-mcp@latest daemon --port 9100
|
|
|
22
22
|
curl http://127.0.0.1:9100/health
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
### 2.
|
|
25
|
+
### 2. 让 Claude Code 通过 channel proxy 连接
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
在 `.mcp.json` (或 `~/.claude.json`) 里加上 channel proxy. MCP server 的 key **必须** 等于第 3 步给 Claude 的 `server:<name>` 后缀:
|
|
28
28
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
31
|
"mcpServers": {
|
|
32
32
|
"cross-agent-teams": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": [
|
|
35
|
+
"-y",
|
|
36
|
+
"-p",
|
|
37
|
+
"cross-agent-teams-mcp@latest",
|
|
38
|
+
"cross-agent-teams-channel",
|
|
39
|
+
"--daemon-url",
|
|
40
|
+
"http://127.0.0.1:9100/mcp"
|
|
41
|
+
]
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
如果你在启动 daemon 时带了 `--token <t>`, 走环境变量 `CROSS_AGENT_TEAMS_MCP_DAEMON_URL` 配合 daemon 侧的 header 配置; proxy 自身只读取 daemon URL.
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
Codex CLI 的配置见 [docs/configs/codex-cli.md](docs/configs/codex-cli.md) — Codex 直接用 Streamable HTTP 连 daemon, 不需要 channel proxy. opencode 见下面的 "在 tmux 里使用 opencode" 章节.
|
|
43
50
|
|
|
44
|
-
### 3.
|
|
51
|
+
### 3. 用 channel loader 启动 Claude Code
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
```bash
|
|
54
|
+
claude --dangerously-load-development-channels server:cross-agent-teams
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`server:<name>` 后缀 **必须** 与 `.mcp.json` 里 MCP server 的 key (上例中是 `cross-agent-teams`) 完全一致. 注意这里说的是 **`.mcp.json` 里的 MCP server key**, 不是 npm bin 名 — bin 名碰巧叫 `cross-agent-teams-channel`, 但 MCP server key 你想叫什么都行. 名字不一致, Claude Code 的 experimental channel loader 不会把 proxy 接进来, 你也就收不到 channel wake 通知.
|
|
58
|
+
|
|
59
|
+
### 4. 在 agent 里完成注册
|
|
60
|
+
|
|
61
|
+
Claude Code 经由 proxy 连上之后, 在 agent 会话里调用 `register_claude_self` / `register_codex_self` / `register_agent` 完成注册, 详见后续章节.
|
|
47
62
|
|
|
48
63
|
### 从源码运行
|
|
49
64
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface CliArgs {
|
|
3
|
+
daemonUrl: string;
|
|
4
|
+
}
|
|
5
|
+
declare class CliArgError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
declare function buildStartupHint(csid: string): {
|
|
9
|
+
content: string;
|
|
10
|
+
meta: {
|
|
11
|
+
source: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
declare function parseCliArgs(argv: readonly string[], env?: NodeJS.ProcessEnv): CliArgs;
|
|
16
|
+
declare function main(argv?: readonly string[], env?: NodeJS.ProcessEnv): Promise<void>;
|
|
17
|
+
|
|
18
|
+
export { CliArgError, buildStartupHint, main, parseCliArgs };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// plugins/cross-agent-teams-channel/src/cli.ts
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
|
|
9
|
+
// plugins/cross-agent-teams-channel/src/proxy.ts
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
function createProxyServer() {
|
|
12
|
+
return new McpServer(
|
|
13
|
+
{ name: "cross-agent-teams-channel", version: "0.1.0" },
|
|
14
|
+
{ capabilities: { experimental: { "claude/channel": {} } } }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function relayChannelWake(server, params) {
|
|
18
|
+
try {
|
|
19
|
+
const notif = {
|
|
20
|
+
method: "notifications/claude/channel",
|
|
21
|
+
params
|
|
22
|
+
};
|
|
23
|
+
const p = server.server.notification(notif);
|
|
24
|
+
if (p && typeof p.catch === "function") {
|
|
25
|
+
p.catch(() => {
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// plugins/cross-agent-teams-channel/src/daemon-client.ts
|
|
33
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
34
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
35
|
+
async function parseToolResult(resp) {
|
|
36
|
+
const r = resp;
|
|
37
|
+
const text = r.content?.[0]?.text;
|
|
38
|
+
if (typeof text !== "string") return {};
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function runRegistrationSequence(config) {
|
|
46
|
+
const order = [];
|
|
47
|
+
const transport = new StreamableHTTPClientTransport(new URL(config.daemonUrl));
|
|
48
|
+
const client = new Client({ name: "cross-agent-teams-proxy", version: "0.1.0" });
|
|
49
|
+
if (config.notificationHandler) {
|
|
50
|
+
client.fallbackNotificationHandler = async (n) => {
|
|
51
|
+
if (n.method === "notifications/channel_wake") {
|
|
52
|
+
config.notificationHandler(n.params);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
await client.connect(transport);
|
|
57
|
+
const registerResp = await client.callTool({
|
|
58
|
+
name: "register_agent",
|
|
59
|
+
arguments: {
|
|
60
|
+
client: "custom",
|
|
61
|
+
client_name: "cross-agent-teams-channel",
|
|
62
|
+
model: "proxy",
|
|
63
|
+
role: "__channel_proxy__",
|
|
64
|
+
name: `channel-proxy-${process.pid}`,
|
|
65
|
+
team: "default",
|
|
66
|
+
claude_ui_pid: process.ppid,
|
|
67
|
+
delivery: {
|
|
68
|
+
kind: "claude-channel",
|
|
69
|
+
channel_session_id: config.channel_session_id
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
order.push("register_agent");
|
|
74
|
+
const regResult = await parseToolResult(registerResp);
|
|
75
|
+
if (!("agent_id" in regResult)) {
|
|
76
|
+
throw new Error(`register_agent failed: ${JSON.stringify(regResult)}`);
|
|
77
|
+
}
|
|
78
|
+
const subResp = await client.callTool({
|
|
79
|
+
name: "subscribe_channel_wake",
|
|
80
|
+
arguments: { channel_session_id: config.channel_session_id }
|
|
81
|
+
});
|
|
82
|
+
order.push("subscribe_channel_wake");
|
|
83
|
+
const subResult = await parseToolResult(subResp);
|
|
84
|
+
if (!("ok" in subResult) || subResult.ok !== true) {
|
|
85
|
+
throw new Error(`subscribe_channel_wake failed: ${JSON.stringify(subResult)}`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
order,
|
|
89
|
+
lastSubscribeResult: subResult,
|
|
90
|
+
client,
|
|
91
|
+
transport,
|
|
92
|
+
close: async () => {
|
|
93
|
+
try {
|
|
94
|
+
await client.close();
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await transport.close();
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function runReconnectingProxy(config) {
|
|
105
|
+
let stopped = false;
|
|
106
|
+
let currentSeq = null;
|
|
107
|
+
async function waitForDisconnect(seq) {
|
|
108
|
+
const interval = config.healthCheckIntervalMs ?? 200;
|
|
109
|
+
let disconnected = false;
|
|
110
|
+
const closeHandler = () => {
|
|
111
|
+
disconnected = true;
|
|
112
|
+
};
|
|
113
|
+
const prevOnClose = seq.transport.onclose;
|
|
114
|
+
seq.transport.onclose = () => {
|
|
115
|
+
prevOnClose?.();
|
|
116
|
+
closeHandler();
|
|
117
|
+
};
|
|
118
|
+
while (!disconnected && !stopped) {
|
|
119
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
120
|
+
if (disconnected || stopped) break;
|
|
121
|
+
try {
|
|
122
|
+
await seq.client.callTool({ name: "echo", arguments: { msg: "hb" } });
|
|
123
|
+
} catch {
|
|
124
|
+
disconnected = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function loop() {
|
|
130
|
+
while (!stopped) {
|
|
131
|
+
try {
|
|
132
|
+
const seq = await runRegistrationSequence(config);
|
|
133
|
+
currentSeq = seq;
|
|
134
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...seq.order]);
|
|
135
|
+
await waitForDisconnect(seq);
|
|
136
|
+
if (config.onDisconnect) config.onDisconnect();
|
|
137
|
+
try {
|
|
138
|
+
await seq.close();
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
currentSeq = null;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
if (stopped) break;
|
|
145
|
+
const wait = config.backoffInitialMs ?? 500;
|
|
146
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
void loop();
|
|
150
|
+
return {
|
|
151
|
+
stop: async () => {
|
|
152
|
+
stopped = true;
|
|
153
|
+
if (currentSeq) {
|
|
154
|
+
try {
|
|
155
|
+
await currentSeq.close();
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// plugins/cross-agent-teams-channel/src/cli.ts
|
|
164
|
+
var CliArgError = class extends Error {
|
|
165
|
+
constructor(message) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "CliArgError";
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function buildStartupHint(csid) {
|
|
171
|
+
const content = [
|
|
172
|
+
`cross-agent-teams-mcp: your channel_session_id is ${csid}.`,
|
|
173
|
+
`Preferred in Claude Code: call register_claude_self({name: "<agent-name>", ui_pid: $PPID}) from this session \u2014 do NOT pass channel_session_id here; the daemon auto-binds via ui_pid.`,
|
|
174
|
+
`Unified equivalent: register_agent({client: "claude-code", name: "<agent-name>", model: "<model>", ui_pid: $PPID}) \u2014 also without channel_session_id.`,
|
|
175
|
+
`bind_channel({channel_session_id: "${csid}"}) is the low-level rebind tool for an already-registered Claude host that needs to switch to a fresh csid; it is NOT the primary registration path.`,
|
|
176
|
+
`Do not use curl or another external HTTP client for Claude registration here \u2014 that would create a different MCP session, and follow-up tools in Claude Code could still see unknown_agent.`
|
|
177
|
+
].join(" ");
|
|
178
|
+
return {
|
|
179
|
+
content,
|
|
180
|
+
meta: { source: "cross_agent_teams_mcp", kind: "startup_bind_hint" }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function parseCliArgs(argv, env = process.env) {
|
|
184
|
+
let daemonUrl;
|
|
185
|
+
for (let i = 0; i < argv.length; i++) {
|
|
186
|
+
const flag = argv[i];
|
|
187
|
+
const next = argv[i + 1];
|
|
188
|
+
switch (flag) {
|
|
189
|
+
case "--daemon-url":
|
|
190
|
+
daemonUrl = next;
|
|
191
|
+
i++;
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!daemonUrl || daemonUrl.length === 0) {
|
|
198
|
+
daemonUrl = env.CROSS_AGENT_TEAMS_MCP_DAEMON_URL;
|
|
199
|
+
}
|
|
200
|
+
if (!daemonUrl || daemonUrl.length === 0) {
|
|
201
|
+
throw new CliArgError(
|
|
202
|
+
"missing --daemon-url (or CROSS_AGENT_TEAMS_MCP_DAEMON_URL env var)"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return { daemonUrl };
|
|
206
|
+
}
|
|
207
|
+
async function main(argv = process.argv.slice(2), env = process.env) {
|
|
208
|
+
let args;
|
|
209
|
+
try {
|
|
210
|
+
args = parseCliArgs(argv, env);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
213
|
+
process.stderr.write(`cross-agent-teams-proxy: ${msg}
|
|
214
|
+
`);
|
|
215
|
+
process.exit(2);
|
|
216
|
+
}
|
|
217
|
+
const csid = randomUUID();
|
|
218
|
+
const hostServer = createProxyServer();
|
|
219
|
+
const stdioTransport = new StdioServerTransport();
|
|
220
|
+
let registrationEverSucceeded = false;
|
|
221
|
+
const controller = runReconnectingProxy({
|
|
222
|
+
daemonUrl: args.daemonUrl,
|
|
223
|
+
channel_session_id: csid,
|
|
224
|
+
notificationHandler: (params) => {
|
|
225
|
+
relayChannelWake(hostServer, params);
|
|
226
|
+
},
|
|
227
|
+
onSequenceComplete: () => {
|
|
228
|
+
registrationEverSucceeded = true;
|
|
229
|
+
const hint = buildStartupHint(csid);
|
|
230
|
+
relayChannelWake(hostServer, hint);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
let stopped = false;
|
|
234
|
+
const shutdown = async () => {
|
|
235
|
+
if (stopped) return;
|
|
236
|
+
stopped = true;
|
|
237
|
+
try {
|
|
238
|
+
await controller.stop();
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await hostServer.close();
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
if (!registrationEverSucceeded) {
|
|
246
|
+
process.stderr.write(`cross-agent-teams-proxy: daemon unreachable at ${args.daemonUrl}
|
|
247
|
+
`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
process.exit(0);
|
|
251
|
+
};
|
|
252
|
+
stdioTransport.onclose = () => {
|
|
253
|
+
void shutdown();
|
|
254
|
+
};
|
|
255
|
+
await hostServer.connect(stdioTransport);
|
|
256
|
+
process.on("SIGTERM", () => {
|
|
257
|
+
void shutdown();
|
|
258
|
+
});
|
|
259
|
+
process.on("SIGINT", () => {
|
|
260
|
+
void shutdown();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function isEntry() {
|
|
264
|
+
try {
|
|
265
|
+
const metaPath = fileURLToPath(import.meta.url);
|
|
266
|
+
const argvPath = realpathSync(process.argv[1]);
|
|
267
|
+
return metaPath === argvPath;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (isEntry()) {
|
|
273
|
+
void main();
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
CliArgError,
|
|
277
|
+
buildStartupHint,
|
|
278
|
+
main,
|
|
279
|
+
parseCliArgs
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=channel-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../plugins/cross-agent-teams-channel/src/cli.ts","../plugins/cross-agent-teams-channel/src/proxy.ts","../plugins/cross-agent-teams-channel/src/daemon-client.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { randomUUID } from 'node:crypto'\nimport { realpathSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createProxyServer, relayChannelWake } from './proxy.js'\nimport { runReconnectingProxy } from './daemon-client.js'\n\ninterface CliArgs {\n daemonUrl: string\n}\n\nexport class CliArgError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliArgError'\n }\n}\n\nexport function buildStartupHint(csid: string): { content: string; meta: { source: string; kind: string } } {\n const content = [\n `cross-agent-teams-mcp: your channel_session_id is ${csid}.`,\n `Preferred in Claude Code: call register_claude_self({name: \"<agent-name>\", ui_pid: $PPID}) from this session — do NOT pass channel_session_id here; the daemon auto-binds via ui_pid.`,\n `Unified equivalent: register_agent({client: \"claude-code\", name: \"<agent-name>\", model: \"<model>\", ui_pid: $PPID}) — also without channel_session_id.`,\n `bind_channel({channel_session_id: \"${csid}\"}) is the low-level rebind tool for an already-registered Claude host that needs to switch to a fresh csid; it is NOT the primary registration path.`,\n `Do not use curl or another external HTTP client for Claude registration here — that would create a different MCP session, and follow-up tools in Claude Code could still see unknown_agent.`\n ].join(' ')\n return {\n content,\n meta: { source: 'cross_agent_teams_mcp', kind: 'startup_bind_hint' }\n }\n}\n\nexport function parseCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): CliArgs {\n let daemonUrl: string | undefined\n\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const next = argv[i + 1]\n switch (flag) {\n case '--daemon-url':\n daemonUrl = next; i++; break\n default:\n // Ignore unknown flags for forward-compat (including legacy\n // --agent-team / --agent-name, which are no longer honored).\n break\n }\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n daemonUrl = env.CROSS_AGENT_TEAMS_MCP_DAEMON_URL\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n throw new CliArgError(\n 'missing --daemon-url (or CROSS_AGENT_TEAMS_MCP_DAEMON_URL env var)'\n )\n }\n return { daemonUrl }\n}\n\nexport async function main(\n argv: readonly string[] = process.argv.slice(2),\n env: NodeJS.ProcessEnv = process.env\n): Promise<void> {\n let args: CliArgs\n try {\n args = parseCliArgs(argv, env)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-proxy: ${msg}\\n`)\n process.exit(2)\n }\n\n // Fresh csid per startup — no persistence. Multi-instance safe.\n const csid = randomUUID()\n\n const hostServer = createProxyServer()\n const stdioTransport = new StdioServerTransport()\n\n let registrationEverSucceeded = false\n const controller = runReconnectingProxy({\n daemonUrl: args.daemonUrl,\n channel_session_id: csid,\n notificationHandler: (params) => {\n relayChannelWake(hostServer, params as { content: string; meta: Record<string, string> })\n },\n onSequenceComplete: () => {\n registrationEverSucceeded = true\n // Announce csid to Claude via host-facing channel notification so Claude\n // can call bind_channel({channel_session_id}) to bind its own agent row.\n const hint = buildStartupHint(csid)\n relayChannelWake(hostServer, hint)\n }\n })\n\n let stopped = false\n const shutdown = async (): Promise<void> => {\n if (stopped) return\n stopped = true\n try { await controller.stop() } catch { /* best-effort */ }\n try { await hostServer.close() } catch { /* best-effort */ }\n if (!registrationEverSucceeded) {\n process.stderr.write(`cross-agent-teams-proxy: daemon unreachable at ${args.daemonUrl}\\n`)\n process.exit(1)\n }\n process.exit(0)\n }\n\n stdioTransport.onclose = () => { void shutdown() }\n\n await hostServer.connect(stdioTransport)\n\n process.on('SIGTERM', () => { void shutdown() })\n process.on('SIGINT', () => { void shutdown() })\n}\n\n// Entry-point check. The naive `import.meta.url === \\`file://${process.argv[1]}\\``\n// breaks when launched via an npm `.bin` symlink (npx, `npm install -g`):\n// process.argv[1] is the symlink path, while import.meta.url is already\n// resolved. Compare realpath-resolved file paths instead.\nfunction isEntry(): boolean {\n try {\n const metaPath = fileURLToPath(import.meta.url)\n const argvPath = realpathSync(process.argv[1])\n return metaPath === argvPath\n } catch {\n return false\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\nif (isEntry()) {\n void main()\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\nexport function createProxyServer(): McpServer {\n return new McpServer(\n { name: 'cross-agent-teams-channel', version: '0.1.0' },\n { capabilities: { experimental: { 'claude/channel': {} } } }\n )\n}\n\nexport interface ChannelWakeParams {\n content: string\n meta: Record<string, string>\n}\n\nexport function relayChannelWake(server: McpServer, params: ChannelWakeParams): void {\n try {\n const notif = {\n method: 'notifications/claude/channel',\n params: params as unknown as Record<string, unknown>\n }\n const p = (server.server.notification as (n: typeof notif) => Promise<void>)(notif)\n if (p && typeof p.catch === 'function') {\n p.catch(() => { /* host closed — drop silently */ })\n }\n } catch {\n // host transport closed or not yet connected — drop silently\n }\n}\n","import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\n\nexport interface RegistrationConfig {\n daemonUrl: string\n channel_session_id: string\n backoffInitialMs?: number\n backoffMaxMs?: number\n notificationHandler?: (payload: unknown) => void\n}\n\nexport interface ReconnectingProxyConfig extends RegistrationConfig {\n onSequenceComplete?: (order: string[]) => void\n onDisconnect?: () => void\n healthCheckIntervalMs?: number\n}\n\nexport interface ReconnectingProxyController {\n stop(): Promise<void>\n}\n\nexport interface RegistrationSequenceResult {\n order: string[]\n lastSubscribeResult: unknown\n client: Client\n transport: StreamableHTTPClientTransport\n close: () => Promise<void>\n}\n\ntype ToolResult = Record<string, unknown>\n\nasync function parseToolResult(resp: unknown): Promise<ToolResult> {\n const r = resp as { content?: Array<{ text?: string }> }\n const text = r.content?.[0]?.text\n if (typeof text !== 'string') return {}\n try { return JSON.parse(text) as ToolResult } catch { return {} }\n}\n\nexport async function runRegistrationSequence(\n config: RegistrationConfig\n): Promise<RegistrationSequenceResult> {\n const order: string[] = []\n const transport = new StreamableHTTPClientTransport(new URL(config.daemonUrl))\n const client = new Client({ name: 'cross-agent-teams-proxy', version: '0.1.0' })\n\n if (config.notificationHandler) {\n client.fallbackNotificationHandler = async (n) => {\n if (n.method === 'notifications/channel_wake') {\n config.notificationHandler!(n.params)\n }\n }\n }\n\n await client.connect(transport)\n\n // 1. register_agent as proxy — identity keyed on pid, stable across reconnects\n // so the (team, name) ON CONFLICT upsert reuses the same row instead of spamming new rows\n const registerResp = await client.callTool({\n name: 'register_agent',\n arguments: {\n client: 'custom',\n client_name: 'cross-agent-teams-channel',\n model: 'proxy',\n role: '__channel_proxy__',\n name: `channel-proxy-${process.pid}`,\n team: 'default',\n claude_ui_pid: process.ppid,\n delivery: {\n kind: 'claude-channel',\n channel_session_id: config.channel_session_id,\n },\n }\n })\n order.push('register_agent')\n const regResult = await parseToolResult(registerResp)\n if (!('agent_id' in regResult)) {\n throw new Error(`register_agent failed: ${JSON.stringify(regResult)}`)\n }\n\n // 2. subscribe_channel_wake — proxy's csid is fresh per startup\n const subResp = await client.callTool({\n name: 'subscribe_channel_wake',\n arguments: { channel_session_id: config.channel_session_id }\n })\n order.push('subscribe_channel_wake')\n const subResult = await parseToolResult(subResp)\n if (!('ok' in subResult) || subResult.ok !== true) {\n throw new Error(`subscribe_channel_wake failed: ${JSON.stringify(subResult)}`)\n }\n\n return {\n order,\n lastSubscribeResult: subResult,\n client,\n transport,\n close: async () => {\n try { await client.close() } catch { /* best-effort */ }\n try { await transport.close() } catch { /* best-effort */ }\n }\n }\n}\n\nexport function runReconnectingProxy(config: ReconnectingProxyConfig): ReconnectingProxyController {\n let stopped = false\n let currentSeq: RegistrationSequenceResult | null = null\n\n async function waitForDisconnect(seq: RegistrationSequenceResult): Promise<void> {\n const interval = config.healthCheckIntervalMs ?? 200\n let disconnected = false\n const closeHandler = () => { disconnected = true }\n const prevOnClose = seq.transport.onclose\n seq.transport.onclose = () => { prevOnClose?.(); closeHandler() }\n while (!disconnected && !stopped) {\n await new Promise(r => setTimeout(r, interval))\n if (disconnected || stopped) break\n try {\n await seq.client.callTool({ name: 'echo', arguments: { msg: 'hb' } })\n } catch {\n disconnected = true\n break\n }\n }\n }\n\n async function loop(): Promise<void> {\n while (!stopped) {\n try {\n const seq = await runRegistrationSequence(config)\n currentSeq = seq\n if (config.onSequenceComplete) config.onSequenceComplete([...seq.order])\n\n await waitForDisconnect(seq)\n if (config.onDisconnect) config.onDisconnect()\n try { await seq.close() } catch { /* best-effort */ }\n currentSeq = null\n } catch {\n // register/subscribe failed — wait and retry.\n }\n if (stopped) break\n const wait = config.backoffInitialMs ?? 500\n await new Promise(r => setTimeout(r, wait))\n }\n }\n\n void loop()\n\n return {\n stop: async () => {\n stopped = true\n if (currentSeq) {\n try { await currentSeq.close() } catch { /* best-effort */ }\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cross-agent-teams-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "MCP daemon for cross-agent collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"cross-agent-teams-mcp": "./dist/cli.js"
|
|
7
|
+
"cross-agent-teams-mcp": "./dist/cli.js",
|
|
8
|
+
"cross-agent-teams-channel": "./dist/channel-cli.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|