cross-agent-teams-mcp 0.2.3 → 0.2.4
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/dist/channel-cli.js +47 -22
- package/dist/channel-cli.js.map +1 -1
- package/package.json +1 -1
- package/src/channel/daemon-client.ts +41 -14
- package/src/channel-cli.ts +25 -13
package/dist/channel-cli.js
CHANGED
|
@@ -133,7 +133,7 @@ async function runRegistrationSequence(config) {
|
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
|
-
function runReconnectingProxy(config) {
|
|
136
|
+
async function runReconnectingProxy(config) {
|
|
137
137
|
let stopped = false;
|
|
138
138
|
let currentSeq = null;
|
|
139
139
|
async function waitForDisconnect(seq) {
|
|
@@ -158,27 +158,44 @@ function runReconnectingProxy(config) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
const initialSeq = await runRegistrationSequence(config);
|
|
162
|
+
currentSeq = initialSeq;
|
|
163
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...initialSeq.order]);
|
|
164
|
+
async function backgroundLoop() {
|
|
162
165
|
while (!stopped) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (config.onDisconnect) config.onDisconnect();
|
|
166
|
+
const seq = currentSeq;
|
|
167
|
+
if (!seq) {
|
|
168
|
+
const wait2 = config.backoffInitialMs ?? 500;
|
|
169
|
+
await new Promise((r) => setTimeout(r, wait2));
|
|
170
|
+
if (stopped) break;
|
|
169
171
|
try {
|
|
170
|
-
await
|
|
172
|
+
const next = await runRegistrationSequence(config);
|
|
173
|
+
currentSeq = next;
|
|
174
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...next.order]);
|
|
171
175
|
} catch {
|
|
172
176
|
}
|
|
173
|
-
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
await waitForDisconnect(seq);
|
|
180
|
+
if (config.onDisconnect) config.onDisconnect();
|
|
181
|
+
try {
|
|
182
|
+
await seq.close();
|
|
174
183
|
} catch {
|
|
175
184
|
}
|
|
185
|
+
currentSeq = null;
|
|
176
186
|
if (stopped) break;
|
|
177
187
|
const wait = config.backoffInitialMs ?? 500;
|
|
178
188
|
await new Promise((r) => setTimeout(r, wait));
|
|
189
|
+
if (stopped) break;
|
|
190
|
+
try {
|
|
191
|
+
const next = await runRegistrationSequence(config);
|
|
192
|
+
currentSeq = next;
|
|
193
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...next.order]);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
|
-
void
|
|
198
|
+
void backgroundLoop();
|
|
182
199
|
return {
|
|
183
200
|
stop: async () => {
|
|
184
201
|
stopped = true;
|
|
@@ -348,17 +365,25 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
348
365
|
}
|
|
349
366
|
const hostServer = createProxyServer();
|
|
350
367
|
const stdioTransport = new StdioServerTransport();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
368
|
+
let controller;
|
|
369
|
+
try {
|
|
370
|
+
controller = await runReconnectingProxy({
|
|
371
|
+
daemonUrl: args.daemonUrl,
|
|
372
|
+
channel_session_id: csid,
|
|
373
|
+
notificationHandler: (params) => {
|
|
374
|
+
relayChannelWake(hostServer, params);
|
|
375
|
+
},
|
|
376
|
+
onSequenceComplete: () => {
|
|
377
|
+
const hint = buildStartupHint(csid);
|
|
378
|
+
relayChannelWake(hostServer, hint);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
process.stderr.write(`cross-agent-teams-channel: initial daemon registration failed: ${msg}
|
|
384
|
+
`);
|
|
385
|
+
process.exit(2);
|
|
386
|
+
}
|
|
362
387
|
installForwardingHandlers(hostServer, () => controller.getClient());
|
|
363
388
|
let stopped = false;
|
|
364
389
|
const shutdown = async () => {
|
package/dist/channel-cli.js.map
CHANGED
|
@@ -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, 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":"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","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 // Await the first registration before connecting stdio so the proxy's\n // forwarding handlers see a live daemon-facing client on the very first\n // host request. Without this, a host that issues tools/list immediately\n // after initialize can race the background register/subscribe and observe\n // an empty / errored tool list which Claude Code caches for the session.\n let controller: Awaited<ReturnType<typeof runReconnectingProxy>>\n try {\n controller = await 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 } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-channel: initial daemon registration failed: ${msg}\\n`)\n process.exit(2)\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 async function runReconnectingProxy(config: ReconnectingProxyConfig): Promise<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 // First registration is awaited synchronously so callers can rely on\n // getClient() returning a live Client immediately after this resolves.\n // This eliminates a cold-start race where forwarded tools/list / tools/call\n // requests from a host (e.g. Claude Code) could arrive before the\n // daemon-facing client finished registering.\n const initialSeq = await runRegistrationSequence(config)\n currentSeq = initialSeq\n if (config.onSequenceComplete) config.onSequenceComplete([...initialSeq.order])\n\n async function backgroundLoop(): Promise<void> {\n while (!stopped) {\n const seq = currentSeq\n if (!seq) {\n // currentSeq cleared by a prior failed reconnect; wait and try again.\n const wait = config.backoffInitialMs ?? 500\n await new Promise(r => setTimeout(r, wait))\n if (stopped) break\n try {\n const next = await runRegistrationSequence(config)\n currentSeq = next\n if (config.onSequenceComplete) config.onSequenceComplete([...next.order])\n } catch {\n // try again next iteration\n }\n continue\n }\n\n await waitForDisconnect(seq)\n if (config.onDisconnect) config.onDisconnect()\n try { await seq.close() } catch { /* best-effort */ }\n currentSeq = null\n if (stopped) break\n\n const wait = config.backoffInitialMs ?? 500\n await new Promise(r => setTimeout(r, wait))\n if (stopped) break\n\n try {\n const next = await runRegistrationSequence(config)\n currentSeq = next\n if (config.onSequenceComplete) config.onSequenceComplete([...next.order])\n } catch {\n // currentSeq stays null; loop will retry after backoff.\n }\n }\n }\n\n void backgroundLoop()\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":"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","names":["fileURLToPath","wait","fileURLToPath"]}
|
package/package.json
CHANGED
|
@@ -101,7 +101,7 @@ export async function runRegistrationSequence(
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function runReconnectingProxy(config: ReconnectingProxyConfig): ReconnectingProxyController {
|
|
104
|
+
export async function runReconnectingProxy(config: ReconnectingProxyConfig): Promise<ReconnectingProxyController> {
|
|
105
105
|
let stopped = false
|
|
106
106
|
let currentSeq: RegistrationSequenceResult | null = null
|
|
107
107
|
|
|
@@ -123,27 +123,54 @@ export function runReconnectingProxy(config: ReconnectingProxyConfig): Reconnect
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
// First registration is awaited synchronously so callers can rely on
|
|
127
|
+
// getClient() returning a live Client immediately after this resolves.
|
|
128
|
+
// This eliminates a cold-start race where forwarded tools/list / tools/call
|
|
129
|
+
// requests from a host (e.g. Claude Code) could arrive before the
|
|
130
|
+
// daemon-facing client finished registering.
|
|
131
|
+
const initialSeq = await runRegistrationSequence(config)
|
|
132
|
+
currentSeq = initialSeq
|
|
133
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...initialSeq.order])
|
|
134
|
+
|
|
135
|
+
async function backgroundLoop(): Promise<void> {
|
|
127
136
|
while (!stopped) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
currentSeq
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
const seq = currentSeq
|
|
138
|
+
if (!seq) {
|
|
139
|
+
// currentSeq cleared by a prior failed reconnect; wait and try again.
|
|
140
|
+
const wait = config.backoffInitialMs ?? 500
|
|
141
|
+
await new Promise(r => setTimeout(r, wait))
|
|
142
|
+
if (stopped) break
|
|
143
|
+
try {
|
|
144
|
+
const next = await runRegistrationSequence(config)
|
|
145
|
+
currentSeq = next
|
|
146
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...next.order])
|
|
147
|
+
} catch {
|
|
148
|
+
// try again next iteration
|
|
149
|
+
}
|
|
150
|
+
continue
|
|
139
151
|
}
|
|
152
|
+
|
|
153
|
+
await waitForDisconnect(seq)
|
|
154
|
+
if (config.onDisconnect) config.onDisconnect()
|
|
155
|
+
try { await seq.close() } catch { /* best-effort */ }
|
|
156
|
+
currentSeq = null
|
|
140
157
|
if (stopped) break
|
|
158
|
+
|
|
141
159
|
const wait = config.backoffInitialMs ?? 500
|
|
142
160
|
await new Promise(r => setTimeout(r, wait))
|
|
161
|
+
if (stopped) break
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const next = await runRegistrationSequence(config)
|
|
165
|
+
currentSeq = next
|
|
166
|
+
if (config.onSequenceComplete) config.onSequenceComplete([...next.order])
|
|
167
|
+
} catch {
|
|
168
|
+
// currentSeq stays null; loop will retry after backoff.
|
|
169
|
+
}
|
|
143
170
|
}
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
void
|
|
173
|
+
void backgroundLoop()
|
|
147
174
|
|
|
148
175
|
return {
|
|
149
176
|
stop: async () => {
|
package/src/channel-cli.ts
CHANGED
|
@@ -90,19 +90,31 @@ export async function main(
|
|
|
90
90
|
const hostServer = createProxyServer()
|
|
91
91
|
const stdioTransport = new StdioServerTransport()
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
// Await the first registration before connecting stdio so the proxy's
|
|
94
|
+
// forwarding handlers see a live daemon-facing client on the very first
|
|
95
|
+
// host request. Without this, a host that issues tools/list immediately
|
|
96
|
+
// after initialize can race the background register/subscribe and observe
|
|
97
|
+
// an empty / errored tool list which Claude Code caches for the session.
|
|
98
|
+
let controller: Awaited<ReturnType<typeof runReconnectingProxy>>
|
|
99
|
+
try {
|
|
100
|
+
controller = await runReconnectingProxy({
|
|
101
|
+
daemonUrl: args.daemonUrl,
|
|
102
|
+
channel_session_id: csid,
|
|
103
|
+
notificationHandler: (params) => {
|
|
104
|
+
relayChannelWake(hostServer, params as { content: string; meta: Record<string, string> })
|
|
105
|
+
},
|
|
106
|
+
onSequenceComplete: () => {
|
|
107
|
+
// Announce csid to Claude via host-facing channel notification so Claude
|
|
108
|
+
// can call bind_channel({channel_session_id}) to bind its own agent row.
|
|
109
|
+
const hint = buildStartupHint(csid)
|
|
110
|
+
relayChannelWake(hostServer, hint)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
115
|
+
process.stderr.write(`cross-agent-teams-channel: initial daemon registration failed: ${msg}\n`)
|
|
116
|
+
process.exit(2)
|
|
117
|
+
}
|
|
106
118
|
|
|
107
119
|
installForwardingHandlers(hostServer, () => controller.getClient())
|
|
108
120
|
|