cross-agent-teams-mcp 0.2.1 → 0.2.3

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 CHANGED
@@ -11,6 +11,8 @@ The package ships two bins:
11
11
  - `cross-agent-teams-mcp` — the daemon (HTTP MCP server on `127.0.0.1:9100`).
12
12
  - `cross-agent-teams-channel` — a stdio MCP proxy that Claude Code talks to over stdio. On startup it auto-bootstraps the daemon if no healthy daemon is reachable on the configured loopback URL.
13
13
 
14
+ The channel is now a full stdio↔HTTP transparent proxy: clients see the daemon's complete tool surface (`register_claude_self`, `list_agents`, `register_agent`, `bind_channel`, `send_message`, `task_*`, etc.) plus the `notifications/claude/channel` wake notifications. The proxy-internal tool `subscribe_channel_wake` is filtered out from the host-visible list.
15
+
14
16
  The recommended Claude Code MCP config collapses to a single stdio entry — no manual daemon startup needed:
15
17
 
16
18
  ```jsonc
@@ -59,7 +61,7 @@ Developers who want explicit lifecycle control still have `start-server.sh` / `s
59
61
 
60
62
  The previous workspace plugin `cross-agent-teams-channel` (which exposed a bin named `cross-agent-teams-proxy`) is removed. Its code now lives inside `cross-agent-teams-mcp` as the `cross-agent-teams-channel` bin. Any external MCP config that references the legacy `cross-agent-teams-proxy` bin or a hard-coded path under `plugins/cross-agent-teams-channel/dist/` must update to the unified single-bin form above.
61
63
 
62
- The HTTP `cross-agent-teams-mcp` MCP entry continues to work for users who prefer the explicit two-entry form.
64
+ The HTTP `cross-agent-teams-mcp` MCP entry continues to work for users who prefer the explicit two-entry form. The previous dual-entry config (HTTP daemon entry + stdio channel entry) still works unchanged; the HTTP entry simply duplicates what is now also reachable through the channel proxy.
63
65
 
64
66
  To run directly from source:
65
67
 
package/README.zh-CN.md CHANGED
@@ -11,6 +11,8 @@
11
11
  - `cross-agent-teams-mcp` — daemon (HTTP MCP server, 监听 `127.0.0.1:9100`).
12
12
  - `cross-agent-teams-channel` — Claude Code 用的 stdio MCP proxy. 启动时会自动 bootstrap daemon: 如果配置的 loopback URL 上没有健康的 daemon, 它会 spawn 一个 detached daemon 子进程, 等到 `/health` 返回 200 后再打开 MCP client。
13
13
 
14
+ channel 现在是一个完整的 stdio↔HTTP 透明代理: 客户端能看到 daemon 的全部工具表面 (`register_claude_self`, `list_agents`, `register_agent`, `bind_channel`, `send_message`, `task_*` 等), 同时保留 `notifications/claude/channel` wake 通知. 代理内部工具 `subscribe_channel_wake` 会从 host 可见列表中过滤掉.
15
+
14
16
  推荐的 Claude Code MCP config 折叠成单一 stdio entry, 不再需要手动起 daemon:
15
17
 
16
18
  ```jsonc
@@ -59,7 +61,7 @@ node dist/cli.js daemon --port 9100
59
61
 
60
62
  之前的 workspace plugin `cross-agent-teams-channel` (它暴露的 bin 名为 `cross-agent-teams-proxy`) 已删除. 代码合并进了 `cross-agent-teams-mcp`, 作为新的 `cross-agent-teams-channel` bin. 所有外部 MCP config 中引用旧 bin 名 `cross-agent-teams-proxy`, 或硬编码 `plugins/cross-agent-teams-channel/dist/` 路径的位置, 都需要改成上面的统一单 bin 形式。
61
63
 
62
- 直接 HTTP 连接 `cross-agent-teams-mcp` 的 MCP entry 仍然可用, 适合喜欢显式两 entry 的用户。
64
+ 直接 HTTP 连接 `cross-agent-teams-mcp` 的 MCP entry 仍然可用, 适合喜欢显式两 entry 的用户. 之前的双 entry 配置 (HTTP daemon entry + stdio channel entry) 不变仍然可用; HTTP entry 只是重复了 channel 代理现在已经透传的工具表面。
63
65
 
64
66
  如果你想直接跑源码, 可以用:
65
67
 
@@ -8,10 +8,17 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
 
9
9
  // src/channel/proxy.ts
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import {
12
+ CallToolRequestSchema,
13
+ ErrorCode,
14
+ ListToolsRequestSchema,
15
+ McpError
16
+ } from "@modelcontextprotocol/sdk/types.js";
17
+ var PROXY_INTERNAL_TOOL = "subscribe_channel_wake";
11
18
  function createProxyServer() {
12
19
  return new McpServer(
13
20
  { name: "cross-agent-teams-channel", version: "0.1.0" },
14
- { capabilities: { experimental: { "claude/channel": {} } } }
21
+ { capabilities: { tools: {}, experimental: { "claude/channel": {} } } }
15
22
  );
16
23
  }
17
24
  function relayChannelWake(server, params) {
@@ -28,6 +35,31 @@ function relayChannelWake(server, params) {
28
35
  } catch {
29
36
  }
30
37
  }
38
+ function installForwardingHandlers(server, getDaemonClient) {
39
+ const inner = server.server;
40
+ inner.setRequestHandler(ListToolsRequestSchema, async (request) => {
41
+ const client = getDaemonClient();
42
+ if (!client) {
43
+ throw new McpError(ErrorCode.InternalError, "daemon not connected");
44
+ }
45
+ const result = await client.listTools(request.params);
46
+ return {
47
+ ...result,
48
+ tools: result.tools.filter((t) => t.name !== PROXY_INTERNAL_TOOL)
49
+ };
50
+ });
51
+ inner.setRequestHandler(CallToolRequestSchema, async (request) => {
52
+ const name = request.params?.name;
53
+ if (name === PROXY_INTERNAL_TOOL) {
54
+ throw new McpError(ErrorCode.MethodNotFound, `tool not available: ${PROXY_INTERNAL_TOOL}`);
55
+ }
56
+ const client = getDaemonClient();
57
+ if (!client) {
58
+ throw new McpError(ErrorCode.InternalError, "daemon not connected");
59
+ }
60
+ return await client.callTool(request.params);
61
+ });
62
+ }
31
63
 
32
64
  // src/channel/daemon-client.ts
33
65
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -156,7 +188,8 @@ function runReconnectingProxy(config) {
156
188
  } catch {
157
189
  }
158
190
  }
159
- }
191
+ },
192
+ getClient: () => currentSeq?.client ?? null
160
193
  };
161
194
  }
162
195
 
@@ -259,8 +292,11 @@ var CliArgError = class extends Error {
259
292
  function buildStartupHint(csid) {
260
293
  const content = [
261
294
  `cross-agent-teams-mcp: your channel_session_id is ${csid}.`,
262
- `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.`,
263
- `Unified equivalent: register_agent({client: "claude-code", name: "<agent-name>", model: "<model>", ui_pid: $PPID}) \u2014 also without channel_session_id.`,
295
+ `You are connected to the cross-agent-teams (xats) collaboration server but NOT yet registered as an agent. Before doing other work in this session, ask the user with this exact English prompt (verbatim):`,
296
+ `"To collaborate via xats, please tell me: register to xats, name: <preferred-name>, team: <preferred-team>"`,
297
+ `When the user replies, call list_agents first to verify the chosen name is not already in use within that team; if it is taken, ask the user for a different name.`,
298
+ `Once you have a unique name, call register_claude_self({name, team, ui_pid: $PPID}) to complete registration. ui_pid is the Claude Code CLI parent process pid (run \`echo $PPID\` from a Bash tool to obtain it). Do NOT pass channel_session_id; the daemon auto-binds via ui_pid.`,
299
+ `The unified equivalent register_agent({client: "claude-code", name, team, model, ui_pid: $PPID}) also works without channel_session_id.`,
264
300
  `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.`,
265
301
  `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.`
266
302
  ].join(" ");
@@ -323,6 +359,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
323
359
  relayChannelWake(hostServer, hint);
324
360
  }
325
361
  });
362
+ installForwardingHandlers(hostServer, () => controller.getClient());
326
363
  let stopped = false;
327
364
  const shutdown = async () => {
328
365
  if (stopped) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/channel-cli.ts","../src/channel/proxy.ts","../src/channel/daemon-client.ts","../src/channel/auto-daemon.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 './channel/proxy.js'\nimport { runReconnectingProxy } from './channel/daemon-client.js'\nimport { ensureDaemonHealthy } from './channel/auto-daemon.js'\n\nconst DEFAULT_DAEMON_URL = 'http://127.0.0.1:9100/mcp'\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 daemonUrl = DEFAULT_DAEMON_URL\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-channel: ${msg}\\n`)\n process.exit(2)\n }\n\n // Fresh csid per startup — no persistence. Multi-instance safe.\n const csid = randomUUID()\n\n try {\n await ensureDaemonHealthy({ daemonUrl: args.daemonUrl, log: process.stderr })\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-channel: ${msg}\\n`)\n process.exit(2)\n }\n\n const hostServer = createProxyServer()\n const stdioTransport = new StdioServerTransport()\n\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 // 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 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\nfunction isInvokedAsEntry(): boolean {\n const argv1 = process.argv[1]\n if (!argv1) return false\n try {\n return realpathSync(argv1) === fileURLToPath(import.meta.url)\n } catch {\n return false\n }\n}\n\nif (isInvokedAsEntry()) {\n main().catch((e) => {\n process.stderr.write(`cross-agent-teams-channel: ${e?.message ?? e}\\n`)\n process.exit(1)\n })\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-channel', 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","import { spawn, type SpawnOptions } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { mkdirSync, openSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport interface EnsureDaemonHealthyOptions {\n daemonUrl: string\n log?: NodeJS.WritableStream\n // Test-only injection points; production callers leave these undefined.\n fetchImpl?: typeof fetch\n spawnImpl?: typeof spawn\n daemonEntryOverride?: string\n probeTimeoutMs?: number\n pollAttempts?: number\n pollIntervalMs?: number\n logFilePath?: string\n}\n\nconst DEFAULT_LOG_PATH = join(homedir(), '.cross-agent-teams-mcp', 'daemon.log')\n\nfunction isLoopbackHost(host: string): boolean {\n return host === '127.0.0.1' || host === 'localhost'\n}\n\nfunction originOf(url: URL): string {\n return `${url.protocol}//${url.host}`\n}\n\nasync function probeHealth(\n origin: string,\n timeoutMs: number,\n fetchImpl: typeof fetch\n): Promise<boolean> {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), timeoutMs)\n try {\n const resp = await fetchImpl(`${origin}/health`, { signal: ac.signal })\n return resp.status >= 200 && resp.status < 300\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nfunction resolveDaemonEntry(): string {\n // Test escape hatch: when set, tests can inject a fake daemon entry without\n // having to mock child_process.spawn inside a subprocess.\n const override = process.env.CROSS_AGENT_TEAMS_CHANNEL_DAEMON_ENTRY\n if (override && override.length > 0) return override\n // After tsup bundles, this module is inlined into dist/channel-cli.js;\n // the daemon entry sits next to it as dist/cli.js. Resolving relative to\n // import.meta.url (which equals the channel-cli.js URL post-build) gives\n // the right neighbor file.\n return fileURLToPath(new URL('./cli.js', import.meta.url))\n}\n\nfunction ensureLogDir(logPath: string): void {\n try {\n mkdirSync(dirname(logPath), { recursive: true })\n } catch {\n // best-effort — spawn will fail clearly if path still unusable\n }\n}\n\nexport async function ensureDaemonHealthy(opts: EnsureDaemonHealthyOptions): Promise<void> {\n const fetchImpl = opts.fetchImpl ?? fetch\n const spawnImpl = opts.spawnImpl ?? spawn\n const probeTimeoutMs = opts.probeTimeoutMs ?? 2000\n const pollAttempts = opts.pollAttempts ?? 20\n const pollIntervalMs = opts.pollIntervalMs ?? 250\n const logFilePath = opts.logFilePath ?? DEFAULT_LOG_PATH\n\n let parsed: URL\n try {\n parsed = new URL(opts.daemonUrl)\n } catch {\n throw new Error(`invalid daemon URL: ${opts.daemonUrl}`)\n }\n\n const origin = originOf(parsed)\n const host = parsed.hostname\n const port = parsed.port ? Number(parsed.port) : null\n\n // Step 1: probe.\n if (await probeHealth(origin, probeTimeoutMs, fetchImpl)) return\n\n // Step 2: non-loopback → no spawn, error out.\n if (!isLoopbackHost(host)) {\n throw new Error(\n `daemon at ${opts.daemonUrl} not reachable; auto-spawn disabled for non-loopback URLs`\n )\n }\n\n // Step 3: spawn detached daemon.\n const entry = opts.daemonEntryOverride ?? resolveDaemonEntry()\n const args = ['daemon']\n if (port !== null && Number.isFinite(port)) {\n args.push('--port', String(port))\n }\n\n ensureLogDir(logFilePath)\n let logFd: number | undefined\n try {\n logFd = openSync(logFilePath, 'a')\n } catch {\n logFd = undefined\n }\n\n const spawnOptions: SpawnOptions = {\n detached: true,\n stdio: logFd !== undefined\n ? ['ignore', logFd, logFd]\n : 'ignore'\n }\n const child = spawnImpl(process.execPath, [entry, ...args], spawnOptions)\n try { child.unref() } catch { /* best-effort */ }\n\n // Step 4: poll /health.\n for (let i = 0; i < pollAttempts; i++) {\n await new Promise(r => setTimeout(r, pollIntervalMs))\n if (await probeHealth(origin, probeTimeoutMs, fetchImpl)) return\n }\n\n throw new Error(\n `daemon failed to become healthy at ${opts.daemonUrl} within bootstrap deadline; ` +\n `see log at ${logFilePath}`\n )\n}\n"],"mappings":";;;AACA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,iBAAAA,sBAAqB;AAC9B,SAAS,4BAA4B;;;ACJrC,SAAS,iBAAiB;AAEnB,SAAS,oBAA+B;AAC7C,SAAO,IAAI;AAAA,IACT,EAAE,MAAM,6BAA6B,SAAS,QAAQ;AAAA,IACtD,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,CAAC,EAAE,EAAE,EAAE;AAAA,EAC7D;AACF;AAOO,SAAS,iBAAiB,QAAmB,QAAiC;AACnF,MAAI;AACF,UAAM,QAAQ;AAAA,MACZ,QAAQ;AAAA,MACR;AAAA,IACF;AACA,UAAM,IAAK,OAAO,OAAO,aAAoD,KAAK;AAClF,QAAI,KAAK,OAAO,EAAE,UAAU,YAAY;AACtC,QAAE,MAAM,MAAM;AAAA,MAAoC,CAAC;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;;;AC3BA,SAAS,cAAc;AACvB,SAAS,qCAAqC;AA8B9C,eAAe,gBAAgB,MAAoC;AACjE,QAAM,IAAI;AACV,QAAM,OAAO,EAAE,UAAU,CAAC,GAAG;AAC7B,MAAI,OAAO,SAAS,SAAU,QAAO,CAAC;AACtC,MAAI;AAAE,WAAO,KAAK,MAAM,IAAI;AAAA,EAAgB,QAAQ;AAAE,WAAO,CAAC;AAAA,EAAE;AAClE;AAEA,eAAsB,wBACpB,QACqC;AACrC,QAAM,QAAkB,CAAC;AACzB,QAAM,YAAY,IAAI,8BAA8B,IAAI,IAAI,OAAO,SAAS,CAAC;AAC7E,QAAM,SAAS,IAAI,OAAO,EAAE,MAAM,6BAA6B,SAAS,QAAQ,CAAC;AAEjF,MAAI,OAAO,qBAAqB;AAC9B,WAAO,8BAA8B,OAAO,MAAM;AAChD,UAAI,EAAE,WAAW,8BAA8B;AAC7C,eAAO,oBAAqB,EAAE,MAAM;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,SAAS;AAI9B,QAAM,eAAe,MAAM,OAAO,SAAS;AAAA,IACzC,MAAM;AAAA,IACN,WAAW;AAAA,MACT,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM,iBAAiB,QAAQ,GAAG;AAAA,MAClC,MAAM;AAAA,MACN,eAAe,QAAQ;AAAA,MACvB,UAAU;AAAA,QACR,MAAM;AAAA,QACN,oBAAoB,OAAO;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,KAAK,gBAAgB;AAC3B,QAAM,YAAY,MAAM,gBAAgB,YAAY;AACpD,MAAI,EAAE,cAAc,YAAY;AAC9B,UAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EACvE;AAGA,QAAM,UAAU,MAAM,OAAO,SAAS;AAAA,IACpC,MAAM;AAAA,IACN,WAAW,EAAE,oBAAoB,OAAO,mBAAmB;AAAA,EAC7D,CAAC;AACD,QAAM,KAAK,wBAAwB;AACnC,QAAM,YAAY,MAAM,gBAAgB,OAAO;AAC/C,MAAI,EAAE,QAAQ,cAAc,UAAU,OAAO,MAAM;AACjD,UAAM,IAAI,MAAM,kCAAkC,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EAC/E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IACA,OAAO,YAAY;AACjB,UAAI;AAAE,cAAM,OAAO,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AACvD,UAAI;AAAE,cAAM,UAAU,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AAAA,IAC5D;AAAA,EACF;AACF;AAEO,SAAS,qBAAqB,QAA8D;AACjG,MAAI,UAAU;AACd,MAAI,aAAgD;AAEpD,iBAAe,kBAAkB,KAAgD;AAC/E,UAAM,WAAW,OAAO,yBAAyB;AACjD,QAAI,eAAe;AACnB,UAAM,eAAe,MAAM;AAAE,qBAAe;AAAA,IAAK;AACjD,UAAM,cAAc,IAAI,UAAU;AAClC,QAAI,UAAU,UAAU,MAAM;AAAE,oBAAc;AAAG,mBAAa;AAAA,IAAE;AAChE,WAAO,CAAC,gBAAgB,CAAC,SAAS;AAChC,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,QAAQ,CAAC;AAC9C,UAAI,gBAAgB,QAAS;AAC7B,UAAI;AACF,cAAM,IAAI,OAAO,SAAS,EAAE,MAAM,QAAQ,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;AAAA,MACtE,QAAQ;AACN,uBAAe;AACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,OAAsB;AACnC,WAAO,CAAC,SAAS;AACf,UAAI;AACF,cAAM,MAAM,MAAM,wBAAwB,MAAM;AAChD,qBAAa;AACb,YAAI,OAAO,mBAAoB,QAAO,mBAAmB,CAAC,GAAG,IAAI,KAAK,CAAC;AAEvE,cAAM,kBAAkB,GAAG;AAC3B,YAAI,OAAO,aAAc,QAAO,aAAa;AAC7C,YAAI;AAAE,gBAAM,IAAI,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AACpD,qBAAa;AAAA,MACf,QAAQ;AAAA,MAER;AACA,UAAI,QAAS;AACb,YAAM,OAAO,OAAO,oBAAoB;AACxC,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,IAAI,CAAC;AAAA,IAC5C;AAAA,EACF;AAEA,OAAK,KAAK;AAEV,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,gBAAU;AACV,UAAI,YAAY;AACd,YAAI;AAAE,gBAAM,WAAW,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AACF;;;AC1JA,SAAS,aAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAe9B,IAAM,mBAAmB,KAAK,QAAQ,GAAG,0BAA0B,YAAY;AAE/E,SAAS,eAAe,MAAuB;AAC7C,SAAO,SAAS,eAAe,SAAS;AAC1C;AAEA,SAAS,SAAS,KAAkB;AAClC,SAAO,GAAG,IAAI,QAAQ,KAAK,IAAI,IAAI;AACrC;AAEA,eAAe,YACb,QACA,WACA,WACkB;AAClB,QAAM,KAAK,IAAI,gBAAgB;AAC/B,QAAM,QAAQ,WAAW,MAAM,GAAG,MAAM,GAAG,SAAS;AACpD,MAAI;AACF,UAAM,OAAO,MAAM,UAAU,GAAG,MAAM,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC;AACtE,WAAO,KAAK,UAAU,OAAO,KAAK,SAAS;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,qBAA6B;AAGpC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;AAK5C,SAAO,cAAc,IAAI,IAAI,YAAY,YAAY,GAAG,CAAC;AAC3D;AAEA,SAAS,aAAa,SAAuB;AAC3C,MAAI;AACF,cAAU,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EACjD,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,oBAAoB,MAAiD;AACzF,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,cAAc,KAAK,eAAe;AAExC,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,KAAK,SAAS;AAAA,EACjC,QAAQ;AACN,UAAM,IAAI,MAAM,uBAAuB,KAAK,SAAS,EAAE;AAAA,EACzD;AAEA,QAAM,SAAS,SAAS,MAAM;AAC9B,QAAM,OAAO,OAAO;AACpB,QAAM,OAAO,OAAO,OAAO,OAAO,OAAO,IAAI,IAAI;AAGjD,MAAI,MAAM,YAAY,QAAQ,gBAAgB,SAAS,EAAG;AAG1D,MAAI,CAAC,eAAe,IAAI,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,aAAa,KAAK,SAAS;AAAA,IAC7B;AAAA,EACF;AAGA,QAAM,QAAQ,KAAK,uBAAuB,mBAAmB;AAC7D,QAAM,OAAO,CAAC,QAAQ;AACtB,MAAI,SAAS,QAAQ,OAAO,SAAS,IAAI,GAAG;AAC1C,SAAK,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,EAClC;AAEA,eAAa,WAAW;AACxB,MAAI;AACJ,MAAI;AACF,YAAQ,SAAS,aAAa,GAAG;AAAA,EACnC,QAAQ;AACN,YAAQ;AAAA,EACV;AAEA,QAAM,eAA6B;AAAA,IACjC,UAAU;AAAA,IACV,OAAO,UAAU,SACb,CAAC,UAAU,OAAO,KAAK,IACvB;AAAA,EACN;AACA,QAAM,QAAQ,UAAU,QAAQ,UAAU,CAAC,OAAO,GAAG,IAAI,GAAG,YAAY;AACxE,MAAI;AAAE,UAAM,MAAM;AAAA,EAAE,QAAQ;AAAA,EAAoB;AAGhD,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,cAAc,CAAC;AACpD,QAAI,MAAM,YAAY,QAAQ,gBAAgB,SAAS,EAAG;AAAA,EAC5D;AAEA,QAAM,IAAI;AAAA,IACR,sCAAsC,KAAK,SAAS,0CACtC,WAAW;AAAA,EAC3B;AACF;;;AHxHA,IAAM,qBAAqB;AAMpB,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,iBAAiB,MAA2E;AAC1G,QAAM,UAAU;AAAA,IACd,qDAAqD,IAAI;AAAA,IACzD;AAAA,IACA;AAAA,IACA,sCAAsC,IAAI;AAAA,IAC1C;AAAA,EACF,EAAE,KAAK,GAAG;AACV,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,QAAQ,yBAAyB,MAAM,oBAAoB;AAAA,EACrE;AACF;AAEO,SAAS,aAAa,MAAyB,MAAyB,QAAQ,KAAc;AACnG,MAAI;AAEJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,UAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,oBAAY;AAAM;AAAK;AAAA,MACzB;AAGE;AAAA,IACJ;AAAA,EACF;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,gBAAY,IAAI;AAAA,EAClB;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,gBAAY;AAAA,EACd;AACA,SAAO,EAAE,UAAU;AACrB;AAEA,eAAsB,KACpB,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAC9C,MAAyB,QAAQ,KAClB;AACf,MAAI;AACJ,MAAI;AACF,WAAO,aAAa,MAAM,GAAG;AAAA,EAC/B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,OAAO,MAAM,8BAA8B,GAAG;AAAA,CAAI;AAC1D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,OAAO,WAAW;AAExB,MAAI;AACF,UAAM,oBAAoB,EAAE,WAAW,KAAK,WAAW,KAAK,QAAQ,OAAO,CAAC;AAAA,EAC9E,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,OAAO,MAAM,8BAA8B,GAAG;AAAA,CAAI;AAC1D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAa,kBAAkB;AACrC,QAAM,iBAAiB,IAAI,qBAAqB;AAEhD,QAAM,aAAa,qBAAqB;AAAA,IACtC,WAAW,KAAK;AAAA,IAChB,oBAAoB;AAAA,IACpB,qBAAqB,CAAC,WAAW;AAC/B,uBAAiB,YAAY,MAA2D;AAAA,IAC1F;AAAA,IACA,oBAAoB,MAAM;AAGxB,YAAM,OAAO,iBAAiB,IAAI;AAClC,uBAAiB,YAAY,IAAI;AAAA,IACnC;AAAA,EACF,CAAC;AAED,MAAI,UAAU;AACd,QAAM,WAAW,YAA2B;AAC1C,QAAI,QAAS;AACb,cAAU;AACV,QAAI;AAAE,YAAM,WAAW,KAAK;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC1D,QAAI;AAAE,YAAM,WAAW,MAAM;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC3D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,iBAAe,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE;AAEjD,QAAM,WAAW,QAAQ,cAAc;AAEvC,UAAQ,GAAG,WAAW,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAC/C,UAAQ,GAAG,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAChD;AAEA,SAAS,mBAA4B;AACnC,QAAM,QAAQ,QAAQ,KAAK,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,aAAa,KAAK,MAAMC,eAAc,YAAY,GAAG;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAI,iBAAiB,GAAG;AACtB,OAAK,EAAE,MAAM,CAAC,MAAM;AAClB,YAAQ,OAAO,MAAM,8BAA8B,GAAG,WAAW,CAAC;AAAA,CAAI;AACtE,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":["fileURLToPath","fileURLToPath"]}
1
+ {"version":3,"sources":["../src/channel-cli.ts","../src/channel/proxy.ts","../src/channel/daemon-client.ts","../src/channel/auto-daemon.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, installForwardingHandlers, relayChannelWake } from './channel/proxy.js'\nimport { runReconnectingProxy } from './channel/daemon-client.js'\nimport { ensureDaemonHealthy } from './channel/auto-daemon.js'\n\nconst DEFAULT_DAEMON_URL = 'http://127.0.0.1:9100/mcp'\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 `You are connected to the cross-agent-teams (xats) collaboration server but NOT yet registered as an agent. Before doing other work in this session, ask the user with this exact English prompt (verbatim):`,\n `\"To collaborate via xats, please tell me: register to xats, name: <preferred-name>, team: <preferred-team>\"`,\n `When the user replies, call list_agents first to verify the chosen name is not already in use within that team; if it is taken, ask the user for a different name.`,\n `Once you have a unique name, call register_claude_self({name, team, ui_pid: $PPID}) to complete registration. ui_pid is the Claude Code CLI parent process pid (run \\`echo $PPID\\` from a Bash tool to obtain it). Do NOT pass channel_session_id; the daemon auto-binds via ui_pid.`,\n `The unified equivalent register_agent({client: \"claude-code\", name, team, model, ui_pid: $PPID}) also works 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 daemonUrl = DEFAULT_DAEMON_URL\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-channel: ${msg}\\n`)\n process.exit(2)\n }\n\n // Fresh csid per startup — no persistence. Multi-instance safe.\n const csid = randomUUID()\n\n try {\n await ensureDaemonHealthy({ daemonUrl: args.daemonUrl, log: process.stderr })\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-channel: ${msg}\\n`)\n process.exit(2)\n }\n\n const hostServer = createProxyServer()\n const stdioTransport = new StdioServerTransport()\n\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 // 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 installForwardingHandlers(hostServer, () => controller.getClient())\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 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\nfunction isInvokedAsEntry(): boolean {\n const argv1 = process.argv[1]\n if (!argv1) return false\n try {\n return realpathSync(argv1) === fileURLToPath(import.meta.url)\n } catch {\n return false\n }\n}\n\nif (isInvokedAsEntry()) {\n main().catch((e) => {\n process.stderr.write(`cross-agent-teams-channel: ${e?.message ?? e}\\n`)\n process.exit(1)\n })\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n CallToolRequestSchema,\n ErrorCode,\n ListToolsRequestSchema,\n McpError\n} from '@modelcontextprotocol/sdk/types.js'\n\nconst PROXY_INTERNAL_TOOL = 'subscribe_channel_wake'\n\nexport function createProxyServer(): McpServer {\n return new McpServer(\n { name: 'cross-agent-teams-channel', version: '0.1.0' },\n { capabilities: { tools: {}, 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\nexport type GetDaemonClient = () => Client | null\n\nexport function installForwardingHandlers(\n server: McpServer,\n getDaemonClient: GetDaemonClient\n): void {\n const inner = server.server\n\n inner.setRequestHandler(ListToolsRequestSchema, async (request) => {\n const client = getDaemonClient()\n if (!client) {\n throw new McpError(ErrorCode.InternalError, 'daemon not connected')\n }\n const result = await client.listTools(request.params)\n return {\n ...result,\n tools: result.tools.filter((t) => t.name !== PROXY_INTERNAL_TOOL)\n }\n })\n\n inner.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params?.name\n if (name === PROXY_INTERNAL_TOOL) {\n throw new McpError(ErrorCode.MethodNotFound, `tool not available: ${PROXY_INTERNAL_TOOL}`)\n }\n const client = getDaemonClient()\n if (!client) {\n throw new McpError(ErrorCode.InternalError, 'daemon not connected')\n }\n return await client.callTool(request.params)\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 getClient(): Client | null\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-channel', 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 getClient: () => currentSeq?.client ?? null\n }\n}\n","import { spawn, type SpawnOptions } from 'node:child_process'\nimport { fileURLToPath } from 'node:url'\nimport { mkdirSync, openSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport interface EnsureDaemonHealthyOptions {\n daemonUrl: string\n log?: NodeJS.WritableStream\n // Test-only injection points; production callers leave these undefined.\n fetchImpl?: typeof fetch\n spawnImpl?: typeof spawn\n daemonEntryOverride?: string\n probeTimeoutMs?: number\n pollAttempts?: number\n pollIntervalMs?: number\n logFilePath?: string\n}\n\nconst DEFAULT_LOG_PATH = join(homedir(), '.cross-agent-teams-mcp', 'daemon.log')\n\nfunction isLoopbackHost(host: string): boolean {\n return host === '127.0.0.1' || host === 'localhost'\n}\n\nfunction originOf(url: URL): string {\n return `${url.protocol}//${url.host}`\n}\n\nasync function probeHealth(\n origin: string,\n timeoutMs: number,\n fetchImpl: typeof fetch\n): Promise<boolean> {\n const ac = new AbortController()\n const timer = setTimeout(() => ac.abort(), timeoutMs)\n try {\n const resp = await fetchImpl(`${origin}/health`, { signal: ac.signal })\n return resp.status >= 200 && resp.status < 300\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nfunction resolveDaemonEntry(): string {\n // Test escape hatch: when set, tests can inject a fake daemon entry without\n // having to mock child_process.spawn inside a subprocess.\n const override = process.env.CROSS_AGENT_TEAMS_CHANNEL_DAEMON_ENTRY\n if (override && override.length > 0) return override\n // After tsup bundles, this module is inlined into dist/channel-cli.js;\n // the daemon entry sits next to it as dist/cli.js. Resolving relative to\n // import.meta.url (which equals the channel-cli.js URL post-build) gives\n // the right neighbor file.\n return fileURLToPath(new URL('./cli.js', import.meta.url))\n}\n\nfunction ensureLogDir(logPath: string): void {\n try {\n mkdirSync(dirname(logPath), { recursive: true })\n } catch {\n // best-effort — spawn will fail clearly if path still unusable\n }\n}\n\nexport async function ensureDaemonHealthy(opts: EnsureDaemonHealthyOptions): Promise<void> {\n const fetchImpl = opts.fetchImpl ?? fetch\n const spawnImpl = opts.spawnImpl ?? spawn\n const probeTimeoutMs = opts.probeTimeoutMs ?? 2000\n const pollAttempts = opts.pollAttempts ?? 20\n const pollIntervalMs = opts.pollIntervalMs ?? 250\n const logFilePath = opts.logFilePath ?? DEFAULT_LOG_PATH\n\n let parsed: URL\n try {\n parsed = new URL(opts.daemonUrl)\n } catch {\n throw new Error(`invalid daemon URL: ${opts.daemonUrl}`)\n }\n\n const origin = originOf(parsed)\n const host = parsed.hostname\n const port = parsed.port ? Number(parsed.port) : null\n\n // Step 1: probe.\n if (await probeHealth(origin, probeTimeoutMs, fetchImpl)) return\n\n // Step 2: non-loopback → no spawn, error out.\n if (!isLoopbackHost(host)) {\n throw new Error(\n `daemon at ${opts.daemonUrl} not reachable; auto-spawn disabled for non-loopback URLs`\n )\n }\n\n // Step 3: spawn detached daemon.\n const entry = opts.daemonEntryOverride ?? resolveDaemonEntry()\n const args = ['daemon']\n if (port !== null && Number.isFinite(port)) {\n args.push('--port', String(port))\n }\n\n ensureLogDir(logFilePath)\n let logFd: number | undefined\n try {\n logFd = openSync(logFilePath, 'a')\n } catch {\n logFd = undefined\n }\n\n const spawnOptions: SpawnOptions = {\n detached: true,\n stdio: logFd !== undefined\n ? ['ignore', logFd, logFd]\n : 'ignore'\n }\n const child = spawnImpl(process.execPath, [entry, ...args], spawnOptions)\n try { child.unref() } catch { /* best-effort */ }\n\n // Step 4: poll /health.\n for (let i = 0; i < pollAttempts; i++) {\n await new Promise(r => setTimeout(r, pollIntervalMs))\n if (await probeHealth(origin, probeTimeoutMs, fetchImpl)) return\n }\n\n throw new Error(\n `daemon failed to become healthy at ${opts.daemonUrl} within bootstrap deadline; ` +\n `see log at ${logFilePath}`\n )\n}\n"],"mappings":";;;AACA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,iBAAAA,sBAAqB;AAC9B,SAAS,4BAA4B;;;ACJrC,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,IAAM,sBAAsB;AAErB,SAAS,oBAA+B;AAC7C,SAAO,IAAI;AAAA,IACT,EAAE,MAAM,6BAA6B,SAAS,QAAQ;AAAA,IACtD,EAAE,cAAc,EAAE,OAAO,CAAC,GAAG,cAAc,EAAE,kBAAkB,CAAC,EAAE,EAAE,EAAE;AAAA,EACxE;AACF;AAOO,SAAS,iBAAiB,QAAmB,QAAiC;AACnF,MAAI;AACF,UAAM,QAAQ;AAAA,MACZ,QAAQ;AAAA,MACR;AAAA,IACF;AACA,UAAM,IAAK,OAAO,OAAO,aAAoD,KAAK;AAClF,QAAI,KAAK,OAAO,EAAE,UAAU,YAAY;AACtC,QAAE,MAAM,MAAM;AAAA,MAAoC,CAAC;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAIO,SAAS,0BACd,QACA,iBACM;AACN,QAAM,QAAQ,OAAO;AAErB,QAAM,kBAAkB,wBAAwB,OAAO,YAAY;AACjE,UAAM,SAAS,gBAAgB;AAC/B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,SAAS,UAAU,eAAe,sBAAsB;AAAA,IACpE;AACA,UAAM,SAAS,MAAM,OAAO,UAAU,QAAQ,MAAM;AACpD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,OAAO,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,mBAAmB;AAAA,IAClE;AAAA,EACF,CAAC;AAED,QAAM,kBAAkB,uBAAuB,OAAO,YAAY;AAChE,UAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAI,SAAS,qBAAqB;AAChC,YAAM,IAAI,SAAS,UAAU,gBAAgB,uBAAuB,mBAAmB,EAAE;AAAA,IAC3F;AACA,UAAM,SAAS,gBAAgB;AAC/B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,SAAS,UAAU,eAAe,sBAAsB;AAAA,IACpE;AACA,WAAO,MAAM,OAAO,SAAS,QAAQ,MAAM;AAAA,EAC7C,CAAC;AACH;;;ACrEA,SAAS,cAAc;AACvB,SAAS,qCAAqC;AA+B9C,eAAe,gBAAgB,MAAoC;AACjE,QAAM,IAAI;AACV,QAAM,OAAO,EAAE,UAAU,CAAC,GAAG;AAC7B,MAAI,OAAO,SAAS,SAAU,QAAO,CAAC;AACtC,MAAI;AAAE,WAAO,KAAK,MAAM,IAAI;AAAA,EAAgB,QAAQ;AAAE,WAAO,CAAC;AAAA,EAAE;AAClE;AAEA,eAAsB,wBACpB,QACqC;AACrC,QAAM,QAAkB,CAAC;AACzB,QAAM,YAAY,IAAI,8BAA8B,IAAI,IAAI,OAAO,SAAS,CAAC;AAC7E,QAAM,SAAS,IAAI,OAAO,EAAE,MAAM,6BAA6B,SAAS,QAAQ,CAAC;AAEjF,MAAI,OAAO,qBAAqB;AAC9B,WAAO,8BAA8B,OAAO,MAAM;AAChD,UAAI,EAAE,WAAW,8BAA8B;AAC7C,eAAO,oBAAqB,EAAE,MAAM;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,SAAS;AAI9B,QAAM,eAAe,MAAM,OAAO,SAAS;AAAA,IACzC,MAAM;AAAA,IACN,WAAW;AAAA,MACT,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM,iBAAiB,QAAQ,GAAG;AAAA,MAClC,MAAM;AAAA,MACN,eAAe,QAAQ;AAAA,MACvB,UAAU;AAAA,QACR,MAAM;AAAA,QACN,oBAAoB,OAAO;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,KAAK,gBAAgB;AAC3B,QAAM,YAAY,MAAM,gBAAgB,YAAY;AACpD,MAAI,EAAE,cAAc,YAAY;AAC9B,UAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EACvE;AAGA,QAAM,UAAU,MAAM,OAAO,SAAS;AAAA,IACpC,MAAM;AAAA,IACN,WAAW,EAAE,oBAAoB,OAAO,mBAAmB;AAAA,EAC7D,CAAC;AACD,QAAM,KAAK,wBAAwB;AACnC,QAAM,YAAY,MAAM,gBAAgB,OAAO;AAC/C,MAAI,EAAE,QAAQ,cAAc,UAAU,OAAO,MAAM;AACjD,UAAM,IAAI,MAAM,kCAAkC,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EAC/E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IACA,OAAO,YAAY;AACjB,UAAI;AAAE,cAAM,OAAO,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AACvD,UAAI;AAAE,cAAM,UAAU,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AAAA,IAC5D;AAAA,EACF;AACF;AAEO,SAAS,qBAAqB,QAA8D;AACjG,MAAI,UAAU;AACd,MAAI,aAAgD;AAEpD,iBAAe,kBAAkB,KAAgD;AAC/E,UAAM,WAAW,OAAO,yBAAyB;AACjD,QAAI,eAAe;AACnB,UAAM,eAAe,MAAM;AAAE,qBAAe;AAAA,IAAK;AACjD,UAAM,cAAc,IAAI,UAAU;AAClC,QAAI,UAAU,UAAU,MAAM;AAAE,oBAAc;AAAG,mBAAa;AAAA,IAAE;AAChE,WAAO,CAAC,gBAAgB,CAAC,SAAS;AAChC,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,QAAQ,CAAC;AAC9C,UAAI,gBAAgB,QAAS;AAC7B,UAAI;AACF,cAAM,IAAI,OAAO,SAAS,EAAE,MAAM,QAAQ,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;AAAA,MACtE,QAAQ;AACN,uBAAe;AACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,OAAsB;AACnC,WAAO,CAAC,SAAS;AACf,UAAI;AACF,cAAM,MAAM,MAAM,wBAAwB,MAAM;AAChD,qBAAa;AACb,YAAI,OAAO,mBAAoB,QAAO,mBAAmB,CAAC,GAAG,IAAI,KAAK,CAAC;AAEvE,cAAM,kBAAkB,GAAG;AAC3B,YAAI,OAAO,aAAc,QAAO,aAAa;AAC7C,YAAI;AAAE,gBAAM,IAAI,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AACpD,qBAAa;AAAA,MACf,QAAQ;AAAA,MAER;AACA,UAAI,QAAS;AACb,YAAM,OAAO,OAAO,oBAAoB;AACxC,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,IAAI,CAAC;AAAA,IAC5C;AAAA,EACF;AAEA,OAAK,KAAK;AAEV,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,gBAAU;AACV,UAAI,YAAY;AACd,YAAI;AAAE,gBAAM,WAAW,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AAAA,MAC7D;AAAA,IACF;AAAA,IACA,WAAW,MAAM,YAAY,UAAU;AAAA,EACzC;AACF;;;AC5JA,SAAS,aAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAe9B,IAAM,mBAAmB,KAAK,QAAQ,GAAG,0BAA0B,YAAY;AAE/E,SAAS,eAAe,MAAuB;AAC7C,SAAO,SAAS,eAAe,SAAS;AAC1C;AAEA,SAAS,SAAS,KAAkB;AAClC,SAAO,GAAG,IAAI,QAAQ,KAAK,IAAI,IAAI;AACrC;AAEA,eAAe,YACb,QACA,WACA,WACkB;AAClB,QAAM,KAAK,IAAI,gBAAgB;AAC/B,QAAM,QAAQ,WAAW,MAAM,GAAG,MAAM,GAAG,SAAS;AACpD,MAAI;AACF,UAAM,OAAO,MAAM,UAAU,GAAG,MAAM,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC;AACtE,WAAO,KAAK,UAAU,OAAO,KAAK,SAAS;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,SAAS,qBAA6B;AAGpC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;AAK5C,SAAO,cAAc,IAAI,IAAI,YAAY,YAAY,GAAG,CAAC;AAC3D;AAEA,SAAS,aAAa,SAAuB;AAC3C,MAAI;AACF,cAAU,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EACjD,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,oBAAoB,MAAiD;AACzF,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,cAAc,KAAK,eAAe;AAExC,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,KAAK,SAAS;AAAA,EACjC,QAAQ;AACN,UAAM,IAAI,MAAM,uBAAuB,KAAK,SAAS,EAAE;AAAA,EACzD;AAEA,QAAM,SAAS,SAAS,MAAM;AAC9B,QAAM,OAAO,OAAO;AACpB,QAAM,OAAO,OAAO,OAAO,OAAO,OAAO,IAAI,IAAI;AAGjD,MAAI,MAAM,YAAY,QAAQ,gBAAgB,SAAS,EAAG;AAG1D,MAAI,CAAC,eAAe,IAAI,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,aAAa,KAAK,SAAS;AAAA,IAC7B;AAAA,EACF;AAGA,QAAM,QAAQ,KAAK,uBAAuB,mBAAmB;AAC7D,QAAM,OAAO,CAAC,QAAQ;AACtB,MAAI,SAAS,QAAQ,OAAO,SAAS,IAAI,GAAG;AAC1C,SAAK,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,EAClC;AAEA,eAAa,WAAW;AACxB,MAAI;AACJ,MAAI;AACF,YAAQ,SAAS,aAAa,GAAG;AAAA,EACnC,QAAQ;AACN,YAAQ;AAAA,EACV;AAEA,QAAM,eAA6B;AAAA,IACjC,UAAU;AAAA,IACV,OAAO,UAAU,SACb,CAAC,UAAU,OAAO,KAAK,IACvB;AAAA,EACN;AACA,QAAM,QAAQ,UAAU,QAAQ,UAAU,CAAC,OAAO,GAAG,IAAI,GAAG,YAAY;AACxE,MAAI;AAAE,UAAM,MAAM;AAAA,EAAE,QAAQ;AAAA,EAAoB;AAGhD,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,cAAc,CAAC;AACpD,QAAI,MAAM,YAAY,QAAQ,gBAAgB,SAAS,EAAG;AAAA,EAC5D;AAEA,QAAM,IAAI;AAAA,IACR,sCAAsC,KAAK,SAAS,0CACtC,WAAW;AAAA,EAC3B;AACF;;;AHxHA,IAAM,qBAAqB;AAMpB,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,iBAAiB,MAA2E;AAC1G,QAAM,UAAU;AAAA,IACd,qDAAqD,IAAI;AAAA,IACzD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,sCAAsC,IAAI;AAAA,IAC1C;AAAA,EACF,EAAE,KAAK,GAAG;AACV,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,QAAQ,yBAAyB,MAAM,oBAAoB;AAAA,EACrE;AACF;AAEO,SAAS,aAAa,MAAyB,MAAyB,QAAQ,KAAc;AACnG,MAAI;AAEJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,UAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,oBAAY;AAAM;AAAK;AAAA,MACzB;AAGE;AAAA,IACJ;AAAA,EACF;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,gBAAY,IAAI;AAAA,EAClB;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,gBAAY;AAAA,EACd;AACA,SAAO,EAAE,UAAU;AACrB;AAEA,eAAsB,KACpB,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAC9C,MAAyB,QAAQ,KAClB;AACf,MAAI;AACJ,MAAI;AACF,WAAO,aAAa,MAAM,GAAG;AAAA,EAC/B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,OAAO,MAAM,8BAA8B,GAAG;AAAA,CAAI;AAC1D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,OAAO,WAAW;AAExB,MAAI;AACF,UAAM,oBAAoB,EAAE,WAAW,KAAK,WAAW,KAAK,QAAQ,OAAO,CAAC;AAAA,EAC9E,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,OAAO,MAAM,8BAA8B,GAAG;AAAA,CAAI;AAC1D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,aAAa,kBAAkB;AACrC,QAAM,iBAAiB,IAAI,qBAAqB;AAEhD,QAAM,aAAa,qBAAqB;AAAA,IACtC,WAAW,KAAK;AAAA,IAChB,oBAAoB;AAAA,IACpB,qBAAqB,CAAC,WAAW;AAC/B,uBAAiB,YAAY,MAA2D;AAAA,IAC1F;AAAA,IACA,oBAAoB,MAAM;AAGxB,YAAM,OAAO,iBAAiB,IAAI;AAClC,uBAAiB,YAAY,IAAI;AAAA,IACnC;AAAA,EACF,CAAC;AAED,4BAA0B,YAAY,MAAM,WAAW,UAAU,CAAC;AAElE,MAAI,UAAU;AACd,QAAM,WAAW,YAA2B;AAC1C,QAAI,QAAS;AACb,cAAU;AACV,QAAI;AAAE,YAAM,WAAW,KAAK;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC1D,QAAI;AAAE,YAAM,WAAW,MAAM;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC3D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,iBAAe,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE;AAEjD,QAAM,WAAW,QAAQ,cAAc;AAEvC,UAAQ,GAAG,WAAW,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAC/C,UAAQ,GAAG,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAChD;AAEA,SAAS,mBAA4B;AACnC,QAAM,QAAQ,QAAQ,KAAK,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,aAAa,KAAK,MAAMC,eAAc,YAAY,GAAG;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAI,iBAAiB,GAAG;AACtB,OAAK,EAAE,MAAM,CAAC,MAAM;AAClB,YAAQ,OAAO,MAAM,8BAA8B,GAAG,WAAW,CAAC;AAAA,CAAI;AACtE,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":["fileURLToPath","fileURLToPath"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-agent-teams-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "MCP daemon for cross-agent collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@ export interface ReconnectingProxyConfig extends RegistrationConfig {
17
17
 
18
18
  export interface ReconnectingProxyController {
19
19
  stop(): Promise<void>
20
+ getClient(): Client | null
20
21
  }
21
22
 
22
23
  export interface RegistrationSequenceResult {
@@ -150,6 +151,7 @@ export function runReconnectingProxy(config: ReconnectingProxyConfig): Reconnect
150
151
  if (currentSeq) {
151
152
  try { await currentSeq.close() } catch { /* best-effort */ }
152
153
  }
153
- }
154
+ },
155
+ getClient: () => currentSeq?.client ?? null
154
156
  }
155
157
  }
@@ -1,9 +1,18 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
+ import {
4
+ CallToolRequestSchema,
5
+ ErrorCode,
6
+ ListToolsRequestSchema,
7
+ McpError
8
+ } from '@modelcontextprotocol/sdk/types.js'
9
+
10
+ const PROXY_INTERNAL_TOOL = 'subscribe_channel_wake'
2
11
 
3
12
  export function createProxyServer(): McpServer {
4
13
  return new McpServer(
5
14
  { name: 'cross-agent-teams-channel', version: '0.1.0' },
6
- { capabilities: { experimental: { 'claude/channel': {} } } }
15
+ { capabilities: { tools: {}, experimental: { 'claude/channel': {} } } }
7
16
  )
8
17
  }
9
18
 
@@ -26,3 +35,36 @@ export function relayChannelWake(server: McpServer, params: ChannelWakeParams):
26
35
  // host transport closed or not yet connected — drop silently
27
36
  }
28
37
  }
38
+
39
+ export type GetDaemonClient = () => Client | null
40
+
41
+ export function installForwardingHandlers(
42
+ server: McpServer,
43
+ getDaemonClient: GetDaemonClient
44
+ ): void {
45
+ const inner = server.server
46
+
47
+ inner.setRequestHandler(ListToolsRequestSchema, async (request) => {
48
+ const client = getDaemonClient()
49
+ if (!client) {
50
+ throw new McpError(ErrorCode.InternalError, 'daemon not connected')
51
+ }
52
+ const result = await client.listTools(request.params)
53
+ return {
54
+ ...result,
55
+ tools: result.tools.filter((t) => t.name !== PROXY_INTERNAL_TOOL)
56
+ }
57
+ })
58
+
59
+ inner.setRequestHandler(CallToolRequestSchema, async (request) => {
60
+ const name = request.params?.name
61
+ if (name === PROXY_INTERNAL_TOOL) {
62
+ throw new McpError(ErrorCode.MethodNotFound, `tool not available: ${PROXY_INTERNAL_TOOL}`)
63
+ }
64
+ const client = getDaemonClient()
65
+ if (!client) {
66
+ throw new McpError(ErrorCode.InternalError, 'daemon not connected')
67
+ }
68
+ return await client.callTool(request.params)
69
+ })
70
+ }
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'
3
3
  import { realpathSync } from 'node:fs'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
6
- import { createProxyServer, relayChannelWake } from './channel/proxy.js'
6
+ import { createProxyServer, installForwardingHandlers, relayChannelWake } from './channel/proxy.js'
7
7
  import { runReconnectingProxy } from './channel/daemon-client.js'
8
8
  import { ensureDaemonHealthy } from './channel/auto-daemon.js'
9
9
 
@@ -23,8 +23,11 @@ export class CliArgError extends Error {
23
23
  export function buildStartupHint(csid: string): { content: string; meta: { source: string; kind: string } } {
24
24
  const content = [
25
25
  `cross-agent-teams-mcp: your channel_session_id is ${csid}.`,
26
- `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.`,
27
- `Unified equivalent: register_agent({client: "claude-code", name: "<agent-name>", model: "<model>", ui_pid: $PPID}) — also without channel_session_id.`,
26
+ `You are connected to the cross-agent-teams (xats) collaboration server but NOT yet registered as an agent. Before doing other work in this session, ask the user with this exact English prompt (verbatim):`,
27
+ `"To collaborate via xats, please tell me: register to xats, name: <preferred-name>, team: <preferred-team>"`,
28
+ `When the user replies, call list_agents first to verify the chosen name is not already in use within that team; if it is taken, ask the user for a different name.`,
29
+ `Once you have a unique name, call register_claude_self({name, team, ui_pid: $PPID}) to complete registration. ui_pid is the Claude Code CLI parent process pid (run \`echo $PPID\` from a Bash tool to obtain it). Do NOT pass channel_session_id; the daemon auto-binds via ui_pid.`,
30
+ `The unified equivalent register_agent({client: "claude-code", name, team, model, ui_pid: $PPID}) also works without channel_session_id.`,
28
31
  `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.`,
29
32
  `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.`
30
33
  ].join(' ')
@@ -101,6 +104,8 @@ export async function main(
101
104
  }
102
105
  })
103
106
 
107
+ installForwardingHandlers(hostServer, () => controller.getClient())
108
+
104
109
  let stopped = false
105
110
  const shutdown = async (): Promise<void> => {
106
111
  if (stopped) return