cross-agent-teams-mcp 0.2.0
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/LICENSE +21 -0
- package/README.md +296 -0
- package/README.zh-CN.md +306 -0
- package/dist/channel-cli.d.ts +18 -0
- package/dist/channel-cli.js +358 -0
- package/dist/channel-cli.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4585 -0
- package/dist/cli.js.map +1 -0
- package/package.json +62 -0
- package/src/channel/auto-daemon.ts +130 -0
- package/src/channel/daemon-client.ts +155 -0
- package/src/channel/proxy.ts +28 -0
- package/src/channel-cli.ts +122 -0
- package/src/cli.ts +136 -0
- package/src/daemon/auth.ts +17 -0
- package/src/daemon/channel-wake-fanout.ts +39 -0
- package/src/daemon/channel-wake-send.ts +38 -0
- package/src/daemon/cleanup.ts +38 -0
- package/src/daemon/errors.ts +18 -0
- package/src/daemon/pid.ts +33 -0
- package/src/daemon/port.ts +16 -0
- package/src/daemon/runtime-identity.ts +238 -0
- package/src/daemon/server.ts +64 -0
- package/src/daemon/shutdown.ts +12 -0
- package/src/daemon/sse-fanout.ts +96 -0
- package/src/daemon/tmux-cli.ts +61 -0
- package/src/daemon/tmux-pane-detect.ts +276 -0
- package/src/lib/client-kind.ts +1 -0
- package/src/lib/default-team.ts +18 -0
- package/src/lib/delivery-spec.ts +172 -0
- package/src/lib/schema-diff.ts +79 -0
- package/src/mcp/agent-public-row.ts +52 -0
- package/src/mcp/auto-bind-channel.ts +106 -0
- package/src/mcp/auto-bind-codex-pane.ts +170 -0
- package/src/mcp/auto-poke-fanout.ts +129 -0
- package/src/mcp/bind-channel.ts +39 -0
- package/src/mcp/bind-runtime-identity.ts +43 -0
- package/src/mcp/broadcast-to-role.ts +127 -0
- package/src/mcp/broadcast.ts +115 -0
- package/src/mcp/codex-appserver-dispatch.ts +169 -0
- package/src/mcp/codex-appserver-rpc.ts +227 -0
- package/src/mcp/codex-pane-pre-register-repo.ts +57 -0
- package/src/mcp/delivery-status.ts +114 -0
- package/src/mcp/diff-contracts.ts +25 -0
- package/src/mcp/echo.ts +8 -0
- package/src/mcp/fanout-with-retry.ts +56 -0
- package/src/mcp/get-contract.ts +24 -0
- package/src/mcp/get-inbox.ts +57 -0
- package/src/mcp/identity.ts +8 -0
- package/src/mcp/pending-contract-events.ts +36 -0
- package/src/mcp/poke-guard.ts +32 -0
- package/src/mcp/poke-retry.ts +159 -0
- package/src/mcp/poke.ts +190 -0
- package/src/mcp/pre-register-codex-pane.ts +65 -0
- package/src/mcp/register-agent.ts +84 -0
- package/src/mcp/register-codex-self.ts +276 -0
- package/src/mcp/register-contract.ts +60 -0
- package/src/mcp/send-message.ts +159 -0
- package/src/mcp/subscribe-channel-wake.ts +31 -0
- package/src/mcp/subscribe-contract.ts +24 -0
- package/src/mcp/task-add.ts +37 -0
- package/src/mcp/task-claim.ts +54 -0
- package/src/mcp/task-complete.ts +36 -0
- package/src/mcp/task-list.ts +33 -0
- package/src/mcp/tools.ts +1240 -0
- package/src/mcp/transport-dispatch.ts +171 -0
- package/src/mcp/transport.ts +204 -0
- package/src/mcp/unregister-self.ts +46 -0
- package/src/storage/agents-repo.ts +328 -0
- package/src/storage/db.ts +13 -0
- package/src/storage/events-outbox.ts +44 -0
- package/src/storage/schema.ts +180 -0
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { AgentsRepo } from '../storage/agents-repo.js'
|
|
5
|
+
import { EventsOutbox } from '../storage/events-outbox.js'
|
|
6
|
+
import { RegisterAgentService } from './register-agent.js'
|
|
7
|
+
import { SendMessageService } from './send-message.js'
|
|
8
|
+
import { BroadcastService } from './broadcast.js'
|
|
9
|
+
import { BroadcastToRoleService } from './broadcast-to-role.js'
|
|
10
|
+
import { GetInboxService } from './get-inbox.js'
|
|
11
|
+
import { TaskAddService } from './task-add.js'
|
|
12
|
+
import { TaskClaimService } from './task-claim.js'
|
|
13
|
+
import { TaskCompleteService } from './task-complete.js'
|
|
14
|
+
import { TaskListService } from './task-list.js'
|
|
15
|
+
import { RegisterContractService } from './register-contract.js'
|
|
16
|
+
import { SubscribeContractService } from './subscribe-contract.js'
|
|
17
|
+
import { GetContractService } from './get-contract.js'
|
|
18
|
+
import { DiffContractsService } from './diff-contracts.js'
|
|
19
|
+
import { PendingContractEventsService } from './pending-contract-events.js'
|
|
20
|
+
import { GetDeliveryStatusService } from './delivery-status.js'
|
|
21
|
+
import { poke } from './poke.js'
|
|
22
|
+
import { wrapStorage } from '../daemon/errors.js'
|
|
23
|
+
import type { SseFanout } from '../daemon/sse-fanout.js'
|
|
24
|
+
import type { ChannelWakeFanout } from '../daemon/channel-wake-fanout.js'
|
|
25
|
+
import { SubscribeChannelWakeService } from './subscribe-channel-wake.js'
|
|
26
|
+
import { BindChannelService } from './bind-channel.js'
|
|
27
|
+
import { AutoBindChannelService } from './auto-bind-channel.js'
|
|
28
|
+
import { BindRuntimeIdentityService } from './bind-runtime-identity.js'
|
|
29
|
+
import { RegisterCodexSelfService } from './register-codex-self.js'
|
|
30
|
+
import { UnregisterSelfService } from './unregister-self.js'
|
|
31
|
+
import { toPublicAgentRow } from './agent-public-row.js'
|
|
32
|
+
import { detectTmuxPane } from '../daemon/tmux-pane-detect.js'
|
|
33
|
+
import { bindRuntimeIdentity } from '../daemon/runtime-identity.js'
|
|
34
|
+
import type { DetectAgentKind } from '../daemon/tmux-pane-detect.js'
|
|
35
|
+
import type { ClientKind } from '../lib/client-kind.js'
|
|
36
|
+
import { CodexPanePreRegRepo } from './codex-pane-pre-register-repo.js'
|
|
37
|
+
import {
|
|
38
|
+
PreRegisterCodexPaneService,
|
|
39
|
+
preRegisterCodexPaneInputSchema,
|
|
40
|
+
} from './pre-register-codex-pane.js'
|
|
41
|
+
import { autoBindCodexPane } from './auto-bind-codex-pane.js'
|
|
42
|
+
|
|
43
|
+
export interface AgentIdHolder { current: string | undefined }
|
|
44
|
+
|
|
45
|
+
type TextContent = { content: Array<{ type: 'text'; text: string }> }
|
|
46
|
+
|
|
47
|
+
function toText(value: unknown): TextContent {
|
|
48
|
+
return { content: [{ type: 'text', text: JSON.stringify(value) }] }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const deliverySchema = z.object({
|
|
52
|
+
kind: z.string(),
|
|
53
|
+
}).passthrough()
|
|
54
|
+
|
|
55
|
+
const clientSchema = z.enum(['codex', 'claude-code', 'opencode', 'custom'])
|
|
56
|
+
|
|
57
|
+
const detectTmuxPaneSchema = z.object({
|
|
58
|
+
agent: z.enum(['codex', 'claude-code', 'opencode', 'custom']),
|
|
59
|
+
cwd: z.string().optional(),
|
|
60
|
+
tty: z.string().optional(),
|
|
61
|
+
title_contains: z.string().optional(),
|
|
62
|
+
process_pattern: z.string().optional(),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const detectTmuxPaneArgsSchema = detectTmuxPaneSchema.superRefine((value, ctx) => {
|
|
66
|
+
if (value.agent === 'custom' && (!value.process_pattern || value.process_pattern.trim().length === 0)) {
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: z.ZodIssueCode.custom,
|
|
69
|
+
path: ['process_pattern'],
|
|
70
|
+
message: 'process_pattern is required when agent=custom',
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const bindRuntimeIdentitySchema = z.object({
|
|
76
|
+
agent: z.enum(['codex', 'claude-code', 'opencode', 'custom']),
|
|
77
|
+
ui_pid: z.number().int().positive().optional(),
|
|
78
|
+
ui_tty: z.string().optional(),
|
|
79
|
+
tmux_pane_id: z.string().min(1).optional(),
|
|
80
|
+
process_pattern: z.string().optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const bindRuntimeIdentityArgsSchema = bindRuntimeIdentitySchema.superRefine((value, ctx) => {
|
|
84
|
+
if (value.agent === 'custom' && (!value.process_pattern || value.process_pattern.trim().length === 0)) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
path: ['process_pattern'],
|
|
88
|
+
message: 'process_pattern is required when agent=custom',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
const hasPid = value.ui_pid !== undefined
|
|
92
|
+
const hasTtyPair =
|
|
93
|
+
value.ui_tty !== undefined &&
|
|
94
|
+
value.ui_tty.trim().length > 0 &&
|
|
95
|
+
value.tmux_pane_id !== undefined &&
|
|
96
|
+
value.tmux_pane_id.trim().length > 0
|
|
97
|
+
if (!hasPid && !hasTtyPair) {
|
|
98
|
+
ctx.addIssue({
|
|
99
|
+
code: z.ZodIssueCode.custom,
|
|
100
|
+
message: 'provide ui_pid, or ui_tty together with tmux_pane_id',
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const SEND_MESSAGE_DESC = [
|
|
106
|
+
'Private 1→1 message to another agent by name. By default auto-poke=true with quiet-guard (auto_poke:false opts out), and need_reply=true.',
|
|
107
|
+
'Set need_reply:false for FYI/no-response-needed messages; recipients see need_reply in get_inbox.',
|
|
108
|
+
'to_agent_name is the target\'s `name` within its team; this is the preferred addressing form. For UUID-based sends use send_message_by_id.',
|
|
109
|
+
'For multi-recipient use broadcast (same-team) or broadcast_to_role (same-team, by role).',
|
|
110
|
+
'除非用户明确指定 to_team, 不要跨 team 沟通 (explicitly set to_team only when user asks).',
|
|
111
|
+
'Reports poked, poke_skip_reasons (no_pane, guard_failed, tmux_unavailable, self); on guard_failed daemon retries at 30s/180s/600s (retry_scheduled, retry_delays_s); stops early on poked.',
|
|
112
|
+
'Auto-poke injects only a SHORT wake-up hint (新邮件 from <sender>, 请调 get_inbox 查看), NOT the body — read bodies via get_inbox.',
|
|
113
|
+
'Delivery is NOT filtered by online/idle (unlike broadcast\'s 5 min idle skip) — offline targets still receive the mailbox row.'
|
|
114
|
+
].join(' ')
|
|
115
|
+
|
|
116
|
+
const SEND_MESSAGE_BY_ID_DESC = [
|
|
117
|
+
'Private 1→1 message to another agent by agent_id (UUID). Use this when you already hold the target\'s agent_id; prefer send_message (by name) otherwise.',
|
|
118
|
+
'Same-team only: the recipient must belong to the caller\'s team. For cross-team sends use send_message with to_team.',
|
|
119
|
+
'By default auto-poke=true with quiet-guard (auto_poke:false opts out), and need_reply=true. Set need_reply:false for FYI/no-response-needed messages.',
|
|
120
|
+
'Reports poked, poke_skip_reasons (no_pane, guard_failed, tmux_unavailable, self); on guard_failed daemon retries at 30s/180s/600s (retry_scheduled, retry_delays_s); stops early on poked.',
|
|
121
|
+
'Auto-poke injects only a SHORT wake-up hint (新邮件 from <sender>, 请调 get_inbox 查看), NOT the body — read bodies via get_inbox.',
|
|
122
|
+
'Delivery is NOT filtered by online/idle — offline targets still receive the mailbox row.'
|
|
123
|
+
].join(' ')
|
|
124
|
+
|
|
125
|
+
const BROADCAST_DESC = [
|
|
126
|
+
'Same-team broadcast to every other agent in the caller team.',
|
|
127
|
+
'Auto-poke default true (quiet-guard + 30s/180s/600s retry; reports poked, poke_skip_reasons, retry_scheduled, retry_delays_s). auto_poke:false opts out.',
|
|
128
|
+
'For role filter use broadcast_to_role. For cross-team 1→1 use send_message({to_team}).',
|
|
129
|
+
'Auto-poke injects only a SHORT wake-up hint (新邮件 from <sender>, 请调 get_inbox 查看) — never the body. Read via get_inbox.',
|
|
130
|
+
'Skips agents idle > 5 min (offline).'
|
|
131
|
+
].join(' ')
|
|
132
|
+
|
|
133
|
+
const BROADCAST_TO_ROLE_DESC = [
|
|
134
|
+
'Same-team broadcast filtered by role. Strictly same-team — no cross-team variant.',
|
|
135
|
+
'For cross-team private 1→1 use send_message({to_team}).',
|
|
136
|
+
'Auto-poke default true with quiet-guard + 30s/180s/600s retry (auto_poke:false opts out); injects only a SHORT wake-up hint, not the message body. Recipients read via get_inbox.',
|
|
137
|
+
'Returns unknown_recipient when no same-team agent matches to_role.'
|
|
138
|
+
].join(' ')
|
|
139
|
+
|
|
140
|
+
function suppressTmuxHint(
|
|
141
|
+
args: { delivery?: { kind?: string } }
|
|
142
|
+
): boolean {
|
|
143
|
+
return args.delivery?.kind !== undefined && args.delivery.kind !== 'none'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function defaultClaudeSelfModel(
|
|
147
|
+
clientInfo: SessionClientInfo | undefined
|
|
148
|
+
): string {
|
|
149
|
+
const raw = `${clientInfo?.name ?? ''} ${clientInfo?.version ?? ''}`.trim()
|
|
150
|
+
if (/claude/i.test(raw)) return raw
|
|
151
|
+
return 'claude-code'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildAutoPokeHint(
|
|
155
|
+
row: { name?: string | null } | undefined,
|
|
156
|
+
fromAgentId: string
|
|
157
|
+
): string {
|
|
158
|
+
const dn = row?.name
|
|
159
|
+
const sender = typeof dn === 'string' && dn.length > 0
|
|
160
|
+
? `${dn} (${fromAgentId})`
|
|
161
|
+
: fromAgentId.slice(0, 8)
|
|
162
|
+
return `新邮件 from ${sender}, 请调 get_inbox 查看`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createAutoPokeImpl(
|
|
166
|
+
db: Database.Database,
|
|
167
|
+
_agents: AgentsRepo,
|
|
168
|
+
channelWakeFanout?: ChannelWakeFanout
|
|
169
|
+
): import('./auto-poke-fanout.js').AutoPokeFn {
|
|
170
|
+
return async (args) => {
|
|
171
|
+
const row = db
|
|
172
|
+
.prepare('SELECT name FROM agents WHERE agent_id=?')
|
|
173
|
+
.get(args.fromAgentId) as { name: string | null } | undefined
|
|
174
|
+
const hint = buildAutoPokeHint(row, args.fromAgentId)
|
|
175
|
+
const res = await poke(
|
|
176
|
+
{ db, callerAgentId: args.fromAgentId, allowCrossTeam: true, channelWakeFanout },
|
|
177
|
+
{ target_agent_id: args.targetAgentId, prompt: hint }
|
|
178
|
+
)
|
|
179
|
+
if ('ok' in res && res.ok) return { ok: true }
|
|
180
|
+
const err = (res as { error?: string }).error
|
|
181
|
+
if (err === 'tmux_unavailable') return { ok: false, reason: 'tmux_unavailable' }
|
|
182
|
+
if (err === 'tmux_pane_not_set') return { ok: false, reason: 'no_pane' }
|
|
183
|
+
if (err === 'no_transport_available') return { ok: false, reason: 'no_pane' }
|
|
184
|
+
if (err === 'self_poke_denied') return { ok: false, reason: 'self' }
|
|
185
|
+
return { ok: false, reason: 'guard_failed' }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface RegisterSuccessHook {
|
|
190
|
+
(agent_id: string, team: string): void
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface UnregisterSuccessHook {
|
|
194
|
+
(agent_id: string): void
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface TransportLike {
|
|
198
|
+
send(msg: Record<string, unknown>): Promise<void> | void
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface SessionClientInfo {
|
|
202
|
+
name?: string
|
|
203
|
+
version?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function inferRuntimeAgentKind(
|
|
207
|
+
args: { client?: ClientKind; delivery?: { kind?: string }; model: string },
|
|
208
|
+
clientInfo: SessionClientInfo | undefined
|
|
209
|
+
): DetectAgentKind | undefined {
|
|
210
|
+
if (args.client === 'custom') return undefined
|
|
211
|
+
if (args.client) return args.client
|
|
212
|
+
if (args.delivery?.kind === 'codex-appserver') return 'codex'
|
|
213
|
+
|
|
214
|
+
const raw = `${clientInfo?.name ?? ''} ${clientInfo?.version ?? ''} ${args.model}`.toLowerCase()
|
|
215
|
+
if (raw.includes('codex')) return 'codex'
|
|
216
|
+
if (raw.includes('gpt-')) return 'codex'
|
|
217
|
+
if (raw.includes('claude')) return 'claude-code'
|
|
218
|
+
if (raw.includes('opus') || raw.includes('sonnet')) return 'claude-code'
|
|
219
|
+
if (raw.includes('opencode')) return 'opencode'
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function registerBusinessTools(
|
|
224
|
+
server: McpServer,
|
|
225
|
+
db: Database.Database,
|
|
226
|
+
getCallerAgentId: () => string | undefined,
|
|
227
|
+
fanout?: SseFanout,
|
|
228
|
+
onRegisterSuccess?: RegisterSuccessHook,
|
|
229
|
+
getSessionId?: () => string | undefined,
|
|
230
|
+
channelWakeFanout?: ChannelWakeFanout,
|
|
231
|
+
getTransport?: () => TransportLike,
|
|
232
|
+
getSessionClientInfo?: () => SessionClientInfo | undefined,
|
|
233
|
+
onUnregisterSuccess?: UnregisterSuccessHook
|
|
234
|
+
): void {
|
|
235
|
+
const agents = new AgentsRepo(db)
|
|
236
|
+
const events = new EventsOutbox(db)
|
|
237
|
+
const registerSvc = new RegisterAgentService(db)
|
|
238
|
+
const bindRuntimeIdentitySvc = new BindRuntimeIdentityService(db)
|
|
239
|
+
const registerCodexSelfSvc = new RegisterCodexSelfService(registerSvc)
|
|
240
|
+
const unregisterSelfSvc = new UnregisterSelfService(db, agents)
|
|
241
|
+
|
|
242
|
+
const autoPokeImpl = createAutoPokeImpl(db, agents, channelWakeFanout)
|
|
243
|
+
|
|
244
|
+
const sendSvc = new SendMessageService(db, agents, events, { poke: autoPokeImpl })
|
|
245
|
+
const broadcastSvc = new BroadcastService(db, agents, { poke: autoPokeImpl })
|
|
246
|
+
const broadcastToRoleSvc = new BroadcastToRoleService(db, agents, events, { poke: autoPokeImpl })
|
|
247
|
+
const inboxSvc = new GetInboxService(db, agents)
|
|
248
|
+
const deliveryStatusSvc = new GetDeliveryStatusService(db)
|
|
249
|
+
const taskAddSvc = new TaskAddService(db, agents, events)
|
|
250
|
+
const taskClaimSvc = new TaskClaimService(db, agents, events)
|
|
251
|
+
const taskCompleteSvc = new TaskCompleteService(db, agents, events)
|
|
252
|
+
const taskListSvc = new TaskListService(db, agents)
|
|
253
|
+
const regContractSvc = new RegisterContractService(db, agents, events)
|
|
254
|
+
const subContractSvc = new SubscribeContractService(db, agents)
|
|
255
|
+
const getContractSvc = new GetContractService(db, agents)
|
|
256
|
+
const diffContractsSvc = new DiffContractsService(db, agents)
|
|
257
|
+
const pendingEventsSvc = new PendingContractEventsService(db, agents)
|
|
258
|
+
const codexPanePreRegRepo = new CodexPanePreRegRepo(db)
|
|
259
|
+
const preRegisterCodexPaneSvc = new PreRegisterCodexPaneService(codexPanePreRegRepo)
|
|
260
|
+
|
|
261
|
+
function caller(): string | undefined { return getCallerAgentId() }
|
|
262
|
+
|
|
263
|
+
async function run(fn: () => unknown): Promise<TextContent> {
|
|
264
|
+
const out = await wrapStorage(() => fn())
|
|
265
|
+
touchIfRegistered()
|
|
266
|
+
return toText(out)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function touchIfRegistered(): void {
|
|
270
|
+
const c = caller()
|
|
271
|
+
if (!c) return
|
|
272
|
+
try {
|
|
273
|
+
if (agents.findById(c)) agents.touch(c)
|
|
274
|
+
} catch { /* best-effort */ }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireAgent(): string | { error: 'unknown_agent' } {
|
|
278
|
+
const c = caller()
|
|
279
|
+
if (!c) return { error: 'unknown_agent' }
|
|
280
|
+
const row = agents.findById(c)
|
|
281
|
+
if (!row) return { error: 'unknown_agent' }
|
|
282
|
+
return c
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function autoBindRuntimeIdentity(
|
|
286
|
+
args: {
|
|
287
|
+
client?: ClientKind
|
|
288
|
+
model: string
|
|
289
|
+
delivery?: { kind?: string }
|
|
290
|
+
ui_pid?: number
|
|
291
|
+
},
|
|
292
|
+
callerAgentId: string
|
|
293
|
+
): Promise<boolean> {
|
|
294
|
+
const inferredAgent = inferRuntimeAgentKind(args, getSessionClientInfo?.())
|
|
295
|
+
if (!inferredAgent) return false
|
|
296
|
+
|
|
297
|
+
if (args.ui_pid !== undefined) {
|
|
298
|
+
const boundByPid = await bindRuntimeIdentitySvc.bind({
|
|
299
|
+
callerAgentId,
|
|
300
|
+
agent: inferredAgent,
|
|
301
|
+
ui_pid: args.ui_pid,
|
|
302
|
+
})
|
|
303
|
+
return 'ok' in boundByPid && boundByPid.ok
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (inferredAgent === 'codex') {
|
|
307
|
+
const auto = await autoBindCodexPane({
|
|
308
|
+
callerAgentId,
|
|
309
|
+
repo: codexPanePreRegRepo,
|
|
310
|
+
bindRuntimeIdentitySvc,
|
|
311
|
+
})
|
|
312
|
+
if (auto) return true
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const detected = await detectTmuxPane({ agent: inferredAgent })
|
|
316
|
+
if (!('ok' in detected) || !detected.ok) return false
|
|
317
|
+
|
|
318
|
+
const bound = await bindRuntimeIdentitySvc.bind({
|
|
319
|
+
callerAgentId,
|
|
320
|
+
agent: inferredAgent,
|
|
321
|
+
ui_tty: detected.pane.tty,
|
|
322
|
+
tmux_pane_id: detected.pane.pane_id,
|
|
323
|
+
})
|
|
324
|
+
return 'ok' in bound && bound.ok
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function preflightUiPidClient(
|
|
328
|
+
args: {
|
|
329
|
+
client?: ClientKind
|
|
330
|
+
model: string
|
|
331
|
+
delivery?: { kind?: string }
|
|
332
|
+
ui_pid?: number
|
|
333
|
+
}
|
|
334
|
+
): Promise<
|
|
335
|
+
| undefined
|
|
336
|
+
| {
|
|
337
|
+
error: 'ui_pid_client_mismatch'
|
|
338
|
+
detail: string
|
|
339
|
+
}
|
|
340
|
+
> {
|
|
341
|
+
if (args.ui_pid === undefined) return undefined
|
|
342
|
+
const inferredAgent = inferRuntimeAgentKind(args, getSessionClientInfo?.())
|
|
343
|
+
if (!inferredAgent) return undefined
|
|
344
|
+
|
|
345
|
+
const validated = await bindRuntimeIdentity({
|
|
346
|
+
agent: inferredAgent,
|
|
347
|
+
ui_pid: args.ui_pid,
|
|
348
|
+
})
|
|
349
|
+
if (!('error' in validated) || validated.error !== 'agent_process_mismatch') {
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
error: 'ui_pid_client_mismatch',
|
|
355
|
+
detail:
|
|
356
|
+
`ui_pid ${args.ui_pid} does not belong to client=\"${inferredAgent}\". ` +
|
|
357
|
+
'Pass the runtime kind for the process behind ui_pid; for example, use client="opencode" when ui_pid points at an opencode process.',
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const registerAgentInputSchema = z.object({
|
|
362
|
+
model: z.string(),
|
|
363
|
+
name: z.string().min(1).refine(v => v.trim().length > 0, { message: 'name must not be empty' }),
|
|
364
|
+
role: z.string().optional(),
|
|
365
|
+
team: z.string().optional(),
|
|
366
|
+
project_dir: z.string().min(1).optional(),
|
|
367
|
+
client: clientSchema,
|
|
368
|
+
client_name: z.string().min(1).optional(),
|
|
369
|
+
ui_pid: z.number().int().positive().optional().describe(
|
|
370
|
+
'STRONGLY RECOMMENDED. Visible agent UI process pid (e.g. Claude Code CLI pid — `$PPID` from a Bash tool call inside Claude Code). Enables one-shot pid → tty → pane binding at registration; without it, tmux-based cross-agent poke delivery typically stays off.'
|
|
371
|
+
),
|
|
372
|
+
channel_session_id: z.string().min(1).optional(),
|
|
373
|
+
thread_id: z.string().min(1).refine(v => v.trim().length > 0, { message: 'thread_id must not be empty' }).optional(),
|
|
374
|
+
ws_url: z.string().optional(),
|
|
375
|
+
auth_token_ref: z.string().min(1).optional(),
|
|
376
|
+
claude_ui_pid: z.number().int().positive().optional().describe(
|
|
377
|
+
"Internal field for the cross-agent-teams-mcp channel proxy. Stores the proxy's parent Claude Code UI pid (`process.ppid`) so that Claude Code hosts registering in the same lineage can auto-bind their claude-channel delivery. Only valid when role='__channel_proxy__'; rejected otherwise."
|
|
378
|
+
),
|
|
379
|
+
delivery: deliverySchema.optional(),
|
|
380
|
+
}).strict()
|
|
381
|
+
|
|
382
|
+
const registerAgentArgsSchema = registerAgentInputSchema.superRefine((value, ctx) => {
|
|
383
|
+
const hasCodexFields =
|
|
384
|
+
value.thread_id !== undefined ||
|
|
385
|
+
value.ws_url !== undefined ||
|
|
386
|
+
value.auth_token_ref !== undefined
|
|
387
|
+
if (hasCodexFields && value.client !== 'codex') {
|
|
388
|
+
ctx.addIssue({
|
|
389
|
+
code: z.ZodIssueCode.custom,
|
|
390
|
+
path: ['client'],
|
|
391
|
+
message: 'client=codex is required when thread_id, ws_url, or auth_token_ref is provided',
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
if (value.channel_session_id !== undefined && value.client !== 'claude-code') {
|
|
395
|
+
ctx.addIssue({
|
|
396
|
+
code: z.ZodIssueCode.custom,
|
|
397
|
+
path: ['client'],
|
|
398
|
+
message: 'client=claude-code is required when channel_session_id is provided',
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
if (value.client_name !== undefined && value.client !== 'custom') {
|
|
402
|
+
ctx.addIssue({
|
|
403
|
+
code: z.ZodIssueCode.custom,
|
|
404
|
+
path: ['client_name'],
|
|
405
|
+
message: 'client_name is only allowed when client=custom',
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
if (value.claude_ui_pid !== undefined && value.role !== '__channel_proxy__') {
|
|
409
|
+
ctx.addIssue({
|
|
410
|
+
code: z.ZodIssueCode.custom,
|
|
411
|
+
path: ['claude_ui_pid'],
|
|
412
|
+
message: "claude_ui_pid is only allowed when role='__channel_proxy__'",
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const registerClaudeSelfInputSchema = z.object({
|
|
418
|
+
name: z.string().min(1).refine(v => v.trim().length > 0, { message: 'name must not be empty' }),
|
|
419
|
+
model: z.string().optional(),
|
|
420
|
+
role: z.string().optional(),
|
|
421
|
+
team: z.string().optional(),
|
|
422
|
+
project_dir: z.string().min(1).optional(),
|
|
423
|
+
ui_pid: z.number().int().positive().optional().describe(
|
|
424
|
+
'STRONGLY RECOMMENDED. The Claude Code CLI pid (obtainable as `$PPID` from a Bash tool call inside Claude Code). Enables one-shot pid → tty → pane binding at registration; without it, tmux-based cross-agent poke delivery typically stays off until a separate `bind_runtime_identity(...)` call.'
|
|
425
|
+
),
|
|
426
|
+
channel_session_id: z.string().min(1).optional(),
|
|
427
|
+
}).strict()
|
|
428
|
+
|
|
429
|
+
const registerCodexSelfInputSchema = z.object({
|
|
430
|
+
name: z.string().min(1).refine(v => v.trim().length > 0, { message: 'name must not be empty' }),
|
|
431
|
+
model: z.string().optional(),
|
|
432
|
+
role: z.string().optional(),
|
|
433
|
+
team: z.string().optional(),
|
|
434
|
+
project_dir: z.string().min(1).optional(),
|
|
435
|
+
thread_id: z.string().min(1).refine(v => v.trim().length > 0, { message: 'thread_id must not be empty' }).optional(),
|
|
436
|
+
ws_url: z.string().min(1).optional(),
|
|
437
|
+
auth_token_ref: z.string().min(1).optional(),
|
|
438
|
+
}).strict()
|
|
439
|
+
|
|
440
|
+
async function executeRegister(
|
|
441
|
+
args: {
|
|
442
|
+
client?: ClientKind
|
|
443
|
+
client_name?: string
|
|
444
|
+
model: string
|
|
445
|
+
name: string
|
|
446
|
+
role?: string
|
|
447
|
+
team?: string
|
|
448
|
+
project_dir?: string
|
|
449
|
+
ui_pid?: number
|
|
450
|
+
channel_session_id?: string
|
|
451
|
+
thread_id?: string
|
|
452
|
+
ws_url?: string
|
|
453
|
+
auth_token_ref?: string
|
|
454
|
+
claude_ui_pid?: number
|
|
455
|
+
delivery?: { kind: string; [key: string]: unknown }
|
|
456
|
+
}
|
|
457
|
+
): Promise<unknown> {
|
|
458
|
+
let nativeDeliveryBound = suppressTmuxHint(args)
|
|
459
|
+
let autoBoundChannelCsid: string | undefined
|
|
460
|
+
const bindChannelSvc = channelWakeFanout
|
|
461
|
+
? new BindChannelService(db, channelWakeFanout)
|
|
462
|
+
: undefined
|
|
463
|
+
const autoBindChannelSvc = channelWakeFanout
|
|
464
|
+
? new AutoBindChannelService(db, channelWakeFanout)
|
|
465
|
+
: undefined
|
|
466
|
+
const connectionId = getSessionId?.() ?? caller()
|
|
467
|
+
if (!connectionId) return { error: 'unknown_agent' }
|
|
468
|
+
const uiPidClientError = await preflightUiPidClient(args)
|
|
469
|
+
if (uiPidClientError) return uiPidClientError
|
|
470
|
+
if (
|
|
471
|
+
args.client === 'claude-code' &&
|
|
472
|
+
args.channel_session_id !== undefined &&
|
|
473
|
+
args.ui_pid !== undefined &&
|
|
474
|
+
autoBindChannelSvc
|
|
475
|
+
) {
|
|
476
|
+
const proxyLookup = autoBindChannelSvc.lookup({
|
|
477
|
+
ui_pid: args.ui_pid,
|
|
478
|
+
})
|
|
479
|
+
if (
|
|
480
|
+
proxyLookup.ok &&
|
|
481
|
+
proxyLookup.channel_session_id !== args.channel_session_id
|
|
482
|
+
) {
|
|
483
|
+
return {
|
|
484
|
+
error: 'channel_session_id_ui_pid_mismatch',
|
|
485
|
+
detail: {
|
|
486
|
+
ui_pid_matched_csid: proxyLookup.channel_session_id,
|
|
487
|
+
supplied_csid: args.channel_session_id,
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const hasCodexTransportFields =
|
|
493
|
+
args.thread_id !== undefined ||
|
|
494
|
+
args.ws_url !== undefined ||
|
|
495
|
+
args.auth_token_ref !== undefined
|
|
496
|
+
const res =
|
|
497
|
+
args.client === 'codex' &&
|
|
498
|
+
args.delivery === undefined &&
|
|
499
|
+
hasCodexTransportFields
|
|
500
|
+
? await registerCodexSelfSvc.register({
|
|
501
|
+
connection_id: connectionId,
|
|
502
|
+
name: args.name,
|
|
503
|
+
model: args.model,
|
|
504
|
+
role: args.role,
|
|
505
|
+
team: args.team,
|
|
506
|
+
project_dir: args.project_dir,
|
|
507
|
+
thread_id: args.thread_id,
|
|
508
|
+
ws_url: args.ws_url,
|
|
509
|
+
auth_token_ref: args.auth_token_ref,
|
|
510
|
+
})
|
|
511
|
+
: registerSvc.register({
|
|
512
|
+
connection_id: connectionId,
|
|
513
|
+
client: args.client,
|
|
514
|
+
client_name: args.client_name,
|
|
515
|
+
model: args.model,
|
|
516
|
+
name: args.name,
|
|
517
|
+
role: args.role,
|
|
518
|
+
team: args.team,
|
|
519
|
+
project_dir: args.project_dir,
|
|
520
|
+
delivery: args.delivery,
|
|
521
|
+
claude_ui_pid: args.claude_ui_pid,
|
|
522
|
+
runtime_ui_pid:
|
|
523
|
+
args.client === 'claude-code' ? args.ui_pid : undefined,
|
|
524
|
+
})
|
|
525
|
+
if ('thread_id' in res && 'agent_id' in res) {
|
|
526
|
+
nativeDeliveryBound = true
|
|
527
|
+
}
|
|
528
|
+
if ('agent_id' in res) {
|
|
529
|
+
if (onRegisterSuccess) {
|
|
530
|
+
try { onRegisterSuccess(res.agent_id, res.team) } catch { /* best-effort */ }
|
|
531
|
+
} else if (fanout) {
|
|
532
|
+
try { fanout.rebind(res.agent_id, res.team) } catch { /* best-effort */ }
|
|
533
|
+
}
|
|
534
|
+
if (args.client === 'claude-code' && args.channel_session_id !== undefined) {
|
|
535
|
+
const channelBind = bindChannelSvc
|
|
536
|
+
? bindChannelSvc.bind({
|
|
537
|
+
callerAgentId: res.agent_id,
|
|
538
|
+
channel_session_id: args.channel_session_id,
|
|
539
|
+
})
|
|
540
|
+
: { error: 'unknown_channel_session' as const }
|
|
541
|
+
if ('ok' in channelBind && channelBind.ok) {
|
|
542
|
+
nativeDeliveryBound = true
|
|
543
|
+
} else {
|
|
544
|
+
return channelBind
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
args.client === 'claude-code' &&
|
|
549
|
+
args.channel_session_id === undefined &&
|
|
550
|
+
args.ui_pid !== undefined &&
|
|
551
|
+
autoBindChannelSvc
|
|
552
|
+
) {
|
|
553
|
+
const autoBind = autoBindChannelSvc.run({
|
|
554
|
+
callerAgentId: res.agent_id,
|
|
555
|
+
ui_pid: args.ui_pid,
|
|
556
|
+
})
|
|
557
|
+
if (autoBind.ok) {
|
|
558
|
+
autoBoundChannelCsid = autoBind.channel_session_id
|
|
559
|
+
nativeDeliveryBound = true
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const autoBound = await autoBindRuntimeIdentity(args, res.agent_id)
|
|
563
|
+
const envelope = autoBoundChannelCsid !== undefined
|
|
564
|
+
? { ...res, channel_session_id: autoBoundChannelCsid }
|
|
565
|
+
: res
|
|
566
|
+
if (autoBound) return envelope
|
|
567
|
+
if (!nativeDeliveryBound) {
|
|
568
|
+
return {
|
|
569
|
+
...envelope,
|
|
570
|
+
hint: "No usable tmux_pane_id is bound yet — automatic runtime binding did not converge for this session, so cross-agent poke delivery via tmux is still off. Call `bind_runtime_identity(...)` to bind explicitly, or use `detect_tmux_pane(...)` for debugging. Claude Code users who loaded the cross-agent-teams-mcp channel plugin can also route pokes via channel_session_id — that path does not require tmux binding."
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return envelope
|
|
574
|
+
}
|
|
575
|
+
return res
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function releaseRegisteredState(agentId: string): void {
|
|
579
|
+
const connectionId = getSessionId?.()
|
|
580
|
+
if (connectionId) registerSvc.releaseConnection(agentId, connectionId)
|
|
581
|
+
if (onUnregisterSuccess) {
|
|
582
|
+
try { onUnregisterSuccess(agentId) } catch { /* best-effort */ }
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
if (fanout) {
|
|
586
|
+
try { fanout.detach(agentId) } catch { /* best-effort */ }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// pre_register_codex_pane — callable by launchers before any agent row exists
|
|
591
|
+
server.registerTool(
|
|
592
|
+
'pre_register_codex_pane',
|
|
593
|
+
{
|
|
594
|
+
title: 'Pre-register codex tmux pane',
|
|
595
|
+
description: [
|
|
596
|
+
'Pre-register a pending tmux-pane claim so the launcher can claim a tmux pane before starting codex.',
|
|
597
|
+
'The launcher should call this with `$TMUX_PANE` and a freshly generated UUID, then `exec codex --remote ... -c xats.agent_id="\\"<uuid>\\""`.',
|
|
598
|
+
'When the codex agent later calls `register_agent({client:"codex"})` without `ui_pid`, the daemon uses the pending row to resolve the correct UI pid and auto-bind the pane.',
|
|
599
|
+
'Callable without a prior `register_agent` — launchers have no agent identity yet.',
|
|
600
|
+
'TTL defaults to 120 seconds and is capped at 600; pending rows are garbage-collected opportunistically.',
|
|
601
|
+
].join(' '),
|
|
602
|
+
inputSchema: preRegisterCodexPaneInputSchema,
|
|
603
|
+
},
|
|
604
|
+
async (args: unknown) => run(async () => preRegisterCodexPaneSvc.register(args))
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
// register_agent — bootstrap: callable before an agents row exists for this session
|
|
608
|
+
server.registerTool(
|
|
609
|
+
'detect_tmux_pane',
|
|
610
|
+
{
|
|
611
|
+
title: 'Detect tmux pane',
|
|
612
|
+
description: [
|
|
613
|
+
'Detect the tmux pane that is actually hosting a coding agent UI, even when the shell calling tools lives in a different pane.',
|
|
614
|
+
'The detector scans tmux panes globally, maps each pane to its tty, then inspects real tty processes instead of trusting `$TMUX_PANE` or tmux focus state alone.',
|
|
615
|
+
'Use `agent` to pick a built-in matcher for Codex, Claude Code, or opencode.',
|
|
616
|
+
'Optional `cwd`, `tty`, and `title_contains` narrow the search and make cross-directory multi-agent sessions much more reliable.',
|
|
617
|
+
'Returns either a single best pane, or an ambiguity/not-found result with candidates for debugging.'
|
|
618
|
+
].join(' '),
|
|
619
|
+
inputSchema: detectTmuxPaneSchema,
|
|
620
|
+
},
|
|
621
|
+
async (args: {
|
|
622
|
+
agent: 'codex' | 'claude-code' | 'opencode' | 'custom'
|
|
623
|
+
cwd?: string
|
|
624
|
+
tty?: string
|
|
625
|
+
title_contains?: string
|
|
626
|
+
process_pattern?: string
|
|
627
|
+
}) => run(async () => {
|
|
628
|
+
const parsed = detectTmuxPaneArgsSchema.safeParse(args)
|
|
629
|
+
if (!parsed.success) {
|
|
630
|
+
return {
|
|
631
|
+
error: 'invalid_arguments' as const,
|
|
632
|
+
detail: parsed.error.issues.map(issue => issue.message).join('; '),
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return detectTmuxPane({
|
|
636
|
+
agent: parsed.data.agent,
|
|
637
|
+
cwd: parsed.data.cwd,
|
|
638
|
+
tty: parsed.data.tty,
|
|
639
|
+
title_contains: parsed.data.title_contains,
|
|
640
|
+
process_pattern: parsed.data.process_pattern,
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
server.registerTool(
|
|
646
|
+
'register_agent',
|
|
647
|
+
{
|
|
648
|
+
title: 'Register agent',
|
|
649
|
+
description: [
|
|
650
|
+
'Register this session as an agent in a team.',
|
|
651
|
+
'This is the unified registration entry point.',
|
|
652
|
+
'Calling this tool again with the same `(team, name, role)` identity reuses the existing',
|
|
653
|
+
'`agent_id` and refreshes `tmux_pane_id` and `model`; no duplicate row is created.',
|
|
654
|
+
'Callers MUST pass `client` explicitly.',
|
|
655
|
+
'Use `client="custom"` for unsupported agent harnesses; optionally provide `client_name` for observability.',
|
|
656
|
+
'Claude Code sessions can pass `client="claude-code"` together with `channel_session_id` to bind channel delivery through this same tool. PREFERRED: pass only `ui_pid` (from `$PPID`) and let the daemon auto-bind channel delivery — do not pass `channel_session_id` explicitly on register. When BOTH `ui_pid` AND `channel_session_id` are supplied, the daemon runs a consistency check against the caller `ui_pid`\'s live channel proxy; if the proxy\'s csid does not match the supplied `channel_session_id`, the call is rejected with `channel_session_id_ui_pid_mismatch` before any agent row is written. Use `bind_channel` for low-level rebind after registration instead of supplying csid here.',
|
|
657
|
+
'Codex sessions can pass `client="codex"` together with `thread_id` to register Codex app-server delivery through this same tool.',
|
|
658
|
+
'Codex clients SHOULD prefer `register_codex_self` instead — it is the codex-specific convenience entry point. Do NOT pass `ui_pid` from codex agents: the launcher\'s `pre_register_codex_pane` pre-reg flow handles tmux pane binding, and supplying `ui_pid` from codex disables that auto-bind path.',
|
|
659
|
+
'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
|
|
660
|
+
'Do not treat the bare word "register" as a request for this tool unless the current conversation is already about cross-agent-teams registration.',
|
|
661
|
+
'When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.',
|
|
662
|
+
'`client` must describe the runtime behind `ui_pid`, not merely the current MCP caller. For example, if `ui_pid` points at an opencode process, pass `client="opencode"` even when the registration request is issued from Claude Code.',
|
|
663
|
+
'STRONGLY RECOMMENDED: pass `ui_pid` unless it is truly unobtainable. Without it, automatic runtime binding usually fails to converge and tmux-based cross-agent poke delivery stays off until a separate `bind_runtime_identity(...)` call. From Claude Code, `$PPID` inside a Bash tool call is the `claude` CLI pid; for Codex/opencode/other harnesses, discover the UI pid from the host harness. With `ui_pid` the daemon binds via verified pid → tty → pane evidence in one shot.',
|
|
664
|
+
'After registration, the daemon best-effort attempts runtime binding for recognized local clients so tmux-based poke delivery can come up without a second tool call.',
|
|
665
|
+
'If automatic runtime binding does not converge, call `bind_runtime_identity(...)` explicitly so the daemon can verify and persist your pane binding.',
|
|
666
|
+
'`detect_tmux_pane(...)` remains available as a debugging aid for ambiguous or missing matches, but it does not write registry state by itself.',
|
|
667
|
+
'When registration still has no usable `tmux_pane_id`, tmux-based poke delivery stays unavailable until automatic or explicit runtime binding succeeds.'
|
|
668
|
+
].join(' '),
|
|
669
|
+
inputSchema: registerAgentInputSchema
|
|
670
|
+
},
|
|
671
|
+
async (args: {
|
|
672
|
+
client: ClientKind
|
|
673
|
+
client_name?: string
|
|
674
|
+
model: string; name: string; role?: string; team?: string;
|
|
675
|
+
project_dir?: string;
|
|
676
|
+
ui_pid?: number;
|
|
677
|
+
channel_session_id?: string
|
|
678
|
+
thread_id?: string
|
|
679
|
+
ws_url?: string
|
|
680
|
+
auth_token_ref?: string
|
|
681
|
+
claude_ui_pid?: number
|
|
682
|
+
delivery?: { kind: string; [key: string]: unknown }
|
|
683
|
+
}) => {
|
|
684
|
+
return run(async () => executeRegister(registerAgentArgsSchema.parse(args)))
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
server.registerTool(
|
|
689
|
+
'register_claude_self',
|
|
690
|
+
{
|
|
691
|
+
title: 'Register Claude Code session',
|
|
692
|
+
description: [
|
|
693
|
+
'Register the current Claude Code MCP session as an agent.',
|
|
694
|
+
'Prefer this helper inside Claude Code when you want to avoid session-mismatch issues caused by external HTTP or curl registration.',
|
|
695
|
+
'This tool always writes on the caller\'s current MCP session, so follow-up tools like get_inbox use the same identity immediately.',
|
|
696
|
+
'AUTO-BIND: when `ui_pid` is supplied AND `channel_session_id` is omitted, the daemon best-effort looks up a live `__channel_proxy__` row whose `claude_ui_pid` matches the caller `ui_pid` and auto-binds `delivery.kind=\"claude-channel\"` to the proxy\'s current csid. On success the response includes `channel_session_id`. No match → delivery stays `none` (no error surfaced). This makes `ui_pid` sufficient for channel delivery — the LLM does not need to read or pass csid explicitly.',
|
|
697
|
+
'If `channel_session_id` is supplied explicitly, the explicit value wins and auto-bind is skipped (identical semantics to `bind_channel`).',
|
|
698
|
+
'When BOTH `ui_pid` AND `channel_session_id` are supplied, the daemon runs a consistency check: it looks up the live `__channel_proxy__` row matching the caller `ui_pid` and team, and if that proxy\'s persisted csid differs from the supplied `channel_session_id`, the call is rejected with `channel_session_id_ui_pid_mismatch` BEFORE any agent row is written. Prefer ui_pid-only registration (no `channel_session_id`) to avoid this class of stale-csid drift.',
|
|
699
|
+
'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
|
|
700
|
+
'Do not treat the bare word "register" as a request for this tool unless the current conversation is already about cross-agent-teams registration.',
|
|
701
|
+
'When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.',
|
|
702
|
+
'STRONGLY RECOMMENDED: pass `ui_pid` (the Claude Code CLI pid — obtainable as `$PPID` from a Bash tool call). Without it, both channel auto-bind AND automatic tmux runtime binding stay off until a separate `bind_runtime_identity(...)` call. With `ui_pid` the daemon can auto-bind the channel delivery AND verify pid → tty → pane evidence in one shot.',
|
|
703
|
+
'model is optional here; when omitted it falls back to a Claude-specific default.'
|
|
704
|
+
].join(' '),
|
|
705
|
+
inputSchema: registerClaudeSelfInputSchema
|
|
706
|
+
},
|
|
707
|
+
async (args: {
|
|
708
|
+
name: string
|
|
709
|
+
model?: string
|
|
710
|
+
role?: string
|
|
711
|
+
team?: string
|
|
712
|
+
project_dir?: string
|
|
713
|
+
ui_pid?: number
|
|
714
|
+
channel_session_id?: string
|
|
715
|
+
}) => run(async () => executeRegister({
|
|
716
|
+
client: 'claude-code',
|
|
717
|
+
name: args.name,
|
|
718
|
+
model: args.model ?? defaultClaudeSelfModel(getSessionClientInfo?.()),
|
|
719
|
+
role: args.role,
|
|
720
|
+
team: args.team,
|
|
721
|
+
project_dir: args.project_dir,
|
|
722
|
+
ui_pid: args.ui_pid,
|
|
723
|
+
channel_session_id: args.channel_session_id,
|
|
724
|
+
}))
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
server.registerTool(
|
|
728
|
+
'register_codex_self',
|
|
729
|
+
{
|
|
730
|
+
title: 'Register codex MCP session',
|
|
731
|
+
description: [
|
|
732
|
+
'Register the current codex MCP session as an agent bound to `codex-appserver` delivery.',
|
|
733
|
+
'Prefer this helper inside codex over the generic `register_agent` — it locks `client="codex"` and keeps the input surface minimal.',
|
|
734
|
+
'THREAD_ID: read `$CODEX_THREAD_ID` from your tool shell environment (codex 0.124.0+ exports it for every MCP tool subprocess) and pass its value as `thread_id`. When `thread_id` is omitted, the daemon returns the existing `thread_id_required` envelope listing resumable thread_ids as a candidate fallback.',
|
|
735
|
+
'DO NOT pass `ui_pid`: this tool\'s schema rejects it outright. UI pid discovery and tmux pane binding are handled automatically by the launcher\'s `pre_register_codex_pane` pre-reg flow — passing `ui_pid` would silently disable that auto-bind path.',
|
|
736
|
+
'The daemon connects to `ws_url` (default `ws://127.0.0.1:8799`, env override `CROSS_AGENT_TEAMS_CODEX_WS_URL`), runs the codex `initialize` + `thread/resume` handshake, and writes `delivery.kind="codex-appserver"`.',
|
|
737
|
+
'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
|
|
738
|
+
'When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.',
|
|
739
|
+
'model is optional here; when omitted it falls back to `gpt`.',
|
|
740
|
+
].join(' '),
|
|
741
|
+
inputSchema: registerCodexSelfInputSchema,
|
|
742
|
+
},
|
|
743
|
+
async (args: {
|
|
744
|
+
name: string
|
|
745
|
+
model?: string
|
|
746
|
+
role?: string
|
|
747
|
+
team?: string
|
|
748
|
+
project_dir?: string
|
|
749
|
+
thread_id?: string
|
|
750
|
+
ws_url?: string
|
|
751
|
+
auth_token_ref?: string
|
|
752
|
+
}) => run(async () => executeRegister({
|
|
753
|
+
client: 'codex',
|
|
754
|
+
name: args.name,
|
|
755
|
+
model: args.model ?? 'gpt',
|
|
756
|
+
role: args.role,
|
|
757
|
+
team: args.team,
|
|
758
|
+
project_dir: args.project_dir,
|
|
759
|
+
thread_id: args.thread_id,
|
|
760
|
+
// Ensure the codex-appserver path is always taken even when thread_id is
|
|
761
|
+
// omitted: callers of register_codex_self expect the thread_id_required
|
|
762
|
+
// candidate-list fallback, not a plain delivery=none registration.
|
|
763
|
+
// RegisterCodexSelfService resolves the empty string to env/default ws_url.
|
|
764
|
+
ws_url: args.ws_url ?? '',
|
|
765
|
+
auth_token_ref: args.auth_token_ref,
|
|
766
|
+
}))
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
server.registerTool(
|
|
770
|
+
'unregister_self',
|
|
771
|
+
{
|
|
772
|
+
title: 'Unregister current agent',
|
|
773
|
+
description: [
|
|
774
|
+
'Remove the caller session\'s current agent registration.',
|
|
775
|
+
'This tool only unregisters the currently bound agent identity; it does not delete other agents.',
|
|
776
|
+
'If the caller still owns any in-progress task, it returns `tasks_in_progress` and leaves all state unchanged.',
|
|
777
|
+
'On success it deletes the agent row, removes the caller\'s contract subscriptions, and immediately releases the current MCP session back to an unregistered state.'
|
|
778
|
+
].join(' '),
|
|
779
|
+
inputSchema: z.object({}).strict()
|
|
780
|
+
},
|
|
781
|
+
async () => {
|
|
782
|
+
const who = requireAgent()
|
|
783
|
+
if (typeof who !== 'string') return toText(who)
|
|
784
|
+
const result = await wrapStorage(() => unregisterSelfSvc.unregister({ caller: who }))
|
|
785
|
+
if (
|
|
786
|
+
typeof result === 'object' &&
|
|
787
|
+
result !== null &&
|
|
788
|
+
'ok' in result &&
|
|
789
|
+
result.ok === true &&
|
|
790
|
+
'agent_id' in result &&
|
|
791
|
+
typeof result.agent_id === 'string'
|
|
792
|
+
) {
|
|
793
|
+
releaseRegisteredState(result.agent_id)
|
|
794
|
+
return toText(result)
|
|
795
|
+
}
|
|
796
|
+
touchIfRegistered()
|
|
797
|
+
return toText(result)
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
// list_agents
|
|
802
|
+
server.registerTool(
|
|
803
|
+
'list_agents',
|
|
804
|
+
{
|
|
805
|
+
title: 'List agents',
|
|
806
|
+
description: 'List agents in the caller\'s team',
|
|
807
|
+
inputSchema: {}
|
|
808
|
+
},
|
|
809
|
+
async () => {
|
|
810
|
+
const who = requireAgent()
|
|
811
|
+
if (typeof who !== 'string') return toText(who)
|
|
812
|
+
const row = agents.findById(who)!
|
|
813
|
+
return run(() => ({
|
|
814
|
+
agents: agents.list({ team: row.team }).map(toPublicAgentRow),
|
|
815
|
+
}))
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
// send_message (by name)
|
|
820
|
+
server.registerTool(
|
|
821
|
+
'send_message',
|
|
822
|
+
{
|
|
823
|
+
title: 'Send message',
|
|
824
|
+
description: SEND_MESSAGE_DESC,
|
|
825
|
+
inputSchema: z.object({
|
|
826
|
+
to_agent_name: z.string().min(1),
|
|
827
|
+
to_team: z.string().min(1).optional(),
|
|
828
|
+
subject: z.string().optional(),
|
|
829
|
+
body: z.string().min(1),
|
|
830
|
+
auto_poke: z.boolean().optional(),
|
|
831
|
+
need_reply: z.boolean().optional()
|
|
832
|
+
}).strict()
|
|
833
|
+
},
|
|
834
|
+
async (args: {
|
|
835
|
+
to_agent_name: string
|
|
836
|
+
to_team?: string
|
|
837
|
+
subject?: string
|
|
838
|
+
body: string
|
|
839
|
+
auto_poke?: boolean
|
|
840
|
+
need_reply?: boolean
|
|
841
|
+
}) => {
|
|
842
|
+
const who = requireAgent()
|
|
843
|
+
if (typeof who !== 'string') return toText(who)
|
|
844
|
+
return run(() => sendSvc.send({ from: who, ...args }))
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
// send_message_by_id (by UUID)
|
|
849
|
+
server.registerTool(
|
|
850
|
+
'send_message_by_id',
|
|
851
|
+
{
|
|
852
|
+
title: 'Send message by id',
|
|
853
|
+
description: SEND_MESSAGE_BY_ID_DESC,
|
|
854
|
+
inputSchema: z.object({
|
|
855
|
+
to_agent_id: z.string().min(1),
|
|
856
|
+
subject: z.string().optional(),
|
|
857
|
+
body: z.string().min(1),
|
|
858
|
+
auto_poke: z.boolean().optional(),
|
|
859
|
+
need_reply: z.boolean().optional()
|
|
860
|
+
}).strict()
|
|
861
|
+
},
|
|
862
|
+
async (args: {
|
|
863
|
+
to_agent_id: string
|
|
864
|
+
subject?: string
|
|
865
|
+
body: string
|
|
866
|
+
auto_poke?: boolean
|
|
867
|
+
need_reply?: boolean
|
|
868
|
+
}) => {
|
|
869
|
+
const who = requireAgent()
|
|
870
|
+
if (typeof who !== 'string') return toText(who)
|
|
871
|
+
return run(() => sendSvc.send({ from: who, ...args }))
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
// broadcast
|
|
876
|
+
server.registerTool(
|
|
877
|
+
'broadcast',
|
|
878
|
+
{
|
|
879
|
+
title: 'Broadcast message',
|
|
880
|
+
description: BROADCAST_DESC,
|
|
881
|
+
inputSchema: {
|
|
882
|
+
subject: z.string().optional(),
|
|
883
|
+
body: z.string(),
|
|
884
|
+
auto_poke: z.boolean().optional()
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
async (args: { subject?: string; body: string; auto_poke?: boolean }) => {
|
|
888
|
+
const who = requireAgent()
|
|
889
|
+
if (typeof who !== 'string') return toText(who)
|
|
890
|
+
return run(() => broadcastSvc.broadcast({ from: who, ...args }))
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
// broadcast_to_role
|
|
895
|
+
server.registerTool(
|
|
896
|
+
'broadcast_to_role',
|
|
897
|
+
{
|
|
898
|
+
title: 'Broadcast to role',
|
|
899
|
+
description: BROADCAST_TO_ROLE_DESC,
|
|
900
|
+
inputSchema: z.object({
|
|
901
|
+
to_role: z.string().min(1),
|
|
902
|
+
subject: z.string().optional(),
|
|
903
|
+
body: z.string().min(1),
|
|
904
|
+
auto_poke: z.boolean().optional()
|
|
905
|
+
}).strict()
|
|
906
|
+
},
|
|
907
|
+
async (args: { to_role: string; subject?: string; body: string; auto_poke?: boolean }) => {
|
|
908
|
+
const who = requireAgent()
|
|
909
|
+
if (typeof who !== 'string') return toText(who)
|
|
910
|
+
return run(() => broadcastToRoleSvc.broadcast({ from: who, ...args }))
|
|
911
|
+
}
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
// get_inbox
|
|
915
|
+
server.registerTool(
|
|
916
|
+
'get_inbox',
|
|
917
|
+
{
|
|
918
|
+
title: 'Get inbox',
|
|
919
|
+
description: 'Return messages addressed to caller after since_event_id',
|
|
920
|
+
inputSchema: {
|
|
921
|
+
since_event_id: z.number().int().optional(),
|
|
922
|
+
limit: z.number().int().optional()
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
async (args: { since_event_id?: number; limit?: number }) => {
|
|
926
|
+
const who = requireAgent()
|
|
927
|
+
if (typeof who !== 'string') return toText(who)
|
|
928
|
+
return run(() => inboxSvc.get({ caller: who, ...args }))
|
|
929
|
+
}
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
// get_delivery_status
|
|
933
|
+
server.registerTool(
|
|
934
|
+
'get_delivery_status',
|
|
935
|
+
{
|
|
936
|
+
title: 'Get delivery status',
|
|
937
|
+
description: [
|
|
938
|
+
'Return wake-hint delivery status for a message sent by caller.',
|
|
939
|
+
'Status describes auto-poke delivery only; mailbox persistence is already complete.',
|
|
940
|
+
'Only the original sender can read a message delivery status.'
|
|
941
|
+
].join(' '),
|
|
942
|
+
inputSchema: {
|
|
943
|
+
message_id: z.string()
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
async (args: { message_id: string }) => {
|
|
947
|
+
const who = requireAgent()
|
|
948
|
+
if (typeof who !== 'string') return toText(who)
|
|
949
|
+
return run(() => deliveryStatusSvc.get({ caller: who, ...args }))
|
|
950
|
+
}
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
// task_add
|
|
954
|
+
server.registerTool(
|
|
955
|
+
'task_add',
|
|
956
|
+
{
|
|
957
|
+
title: 'Add task',
|
|
958
|
+
description: [
|
|
959
|
+
"Add a new task to the team's task list. Any team member can claim it via `task_claim`",
|
|
960
|
+
'on their next turn. The task will sit in the pending queue until someone pulls `task_list`.',
|
|
961
|
+
'`task_add` itself does not wake or target any specific agent; use normal mailbox messaging',
|
|
962
|
+
'when coordination is needed, then inspect that message with `get_delivery_status`.'
|
|
963
|
+
].join(' '),
|
|
964
|
+
inputSchema: {
|
|
965
|
+
title: z.string(),
|
|
966
|
+
description: z.string().optional(),
|
|
967
|
+
depends_on: z.array(z.string()).optional()
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
async (args: { title: string; description?: string; depends_on?: string[] }) => {
|
|
971
|
+
const who = requireAgent()
|
|
972
|
+
if (typeof who !== 'string') return toText(who)
|
|
973
|
+
return run(() => taskAddSvc.add({ caller: who, ...args }))
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
// task_claim
|
|
978
|
+
server.registerTool(
|
|
979
|
+
'task_claim',
|
|
980
|
+
{
|
|
981
|
+
title: 'Claim task',
|
|
982
|
+
description: 'Claim a pending task as caller',
|
|
983
|
+
inputSchema: { task_id: z.string() }
|
|
984
|
+
},
|
|
985
|
+
async (args: { task_id: string }) => {
|
|
986
|
+
const who = requireAgent()
|
|
987
|
+
if (typeof who !== 'string') return toText(who)
|
|
988
|
+
return run(() => taskClaimSvc.claim({ caller: who, task_id: args.task_id }))
|
|
989
|
+
}
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
// task_complete
|
|
993
|
+
server.registerTool(
|
|
994
|
+
'task_complete',
|
|
995
|
+
{
|
|
996
|
+
title: 'Complete task',
|
|
997
|
+
description: 'Mark the caller\'s in-progress task as completed',
|
|
998
|
+
inputSchema: {
|
|
999
|
+
task_id: z.string(),
|
|
1000
|
+
result: z.string().optional()
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
async (args: { task_id: string; result?: string }) => {
|
|
1004
|
+
const who = requireAgent()
|
|
1005
|
+
if (typeof who !== 'string') return toText(who)
|
|
1006
|
+
return run(() => taskCompleteSvc.complete({ caller: who, ...args }))
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
// task_list
|
|
1011
|
+
server.registerTool(
|
|
1012
|
+
'task_list',
|
|
1013
|
+
{
|
|
1014
|
+
title: 'List tasks',
|
|
1015
|
+
description: 'List tasks in the caller\'s team, optionally filtered by status',
|
|
1016
|
+
inputSchema: {
|
|
1017
|
+
status: z.enum(['pending', 'in_progress', 'completed']).optional()
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
async (args: { status?: 'pending' | 'in_progress' | 'completed' }) => {
|
|
1021
|
+
const who = requireAgent()
|
|
1022
|
+
if (typeof who !== 'string') return toText(who)
|
|
1023
|
+
return run(() => taskListSvc.list({ caller: who, status: args.status }))
|
|
1024
|
+
}
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
// register_contract
|
|
1028
|
+
server.registerTool(
|
|
1029
|
+
'register_contract',
|
|
1030
|
+
{
|
|
1031
|
+
title: 'Register contract',
|
|
1032
|
+
description: 'Register or upgrade a contract version',
|
|
1033
|
+
inputSchema: {
|
|
1034
|
+
name: z.string(),
|
|
1035
|
+
schema: z.record(z.unknown()),
|
|
1036
|
+
format: z.literal('jsonschema').optional(),
|
|
1037
|
+
note: z.string().optional()
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
async (args: { name: string; schema: Record<string, unknown>; format?: 'jsonschema'; note?: string }) => {
|
|
1041
|
+
const who = requireAgent()
|
|
1042
|
+
if (typeof who !== 'string') return toText(who)
|
|
1043
|
+
return run(() => {
|
|
1044
|
+
const res = regContractSvc.register({ caller: who, ...args })
|
|
1045
|
+
if ('version' in res && res._meta && fanout) {
|
|
1046
|
+
try {
|
|
1047
|
+
fanout.emitContractEvent(db, {
|
|
1048
|
+
to_team: res._meta.team,
|
|
1049
|
+
contract_name: res.name,
|
|
1050
|
+
version: res.version,
|
|
1051
|
+
event_id: res._meta.event_id,
|
|
1052
|
+
diff: res._meta.diff
|
|
1053
|
+
})
|
|
1054
|
+
} catch { /* push failure does not roll back event */ }
|
|
1055
|
+
}
|
|
1056
|
+
if ('version' in res) {
|
|
1057
|
+
const { _meta: _omit, ...publicRes } = res
|
|
1058
|
+
return publicRes
|
|
1059
|
+
}
|
|
1060
|
+
return res
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
// subscribe_contract
|
|
1066
|
+
server.registerTool(
|
|
1067
|
+
'subscribe_contract',
|
|
1068
|
+
{
|
|
1069
|
+
title: 'Subscribe contract',
|
|
1070
|
+
description: 'Subscribe the caller to a contract name\'s updates',
|
|
1071
|
+
inputSchema: { name: z.string() }
|
|
1072
|
+
},
|
|
1073
|
+
async (args: { name: string }) => {
|
|
1074
|
+
const who = requireAgent()
|
|
1075
|
+
if (typeof who !== 'string') return toText(who)
|
|
1076
|
+
return run(() => subContractSvc.subscribe({ caller: who, name: args.name }))
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
// get_contract
|
|
1081
|
+
server.registerTool(
|
|
1082
|
+
'get_contract',
|
|
1083
|
+
{
|
|
1084
|
+
title: 'Get contract',
|
|
1085
|
+
description: 'Fetch a contract version (latest by default)',
|
|
1086
|
+
inputSchema: {
|
|
1087
|
+
name: z.string(),
|
|
1088
|
+
version: z.number().int().optional()
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
async (args: { name: string; version?: number }) => {
|
|
1092
|
+
const who = requireAgent()
|
|
1093
|
+
if (typeof who !== 'string') return toText(who)
|
|
1094
|
+
return run(() => getContractSvc.get({ caller: who, ...args }))
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
// diff_contracts
|
|
1099
|
+
server.registerTool(
|
|
1100
|
+
'diff_contracts',
|
|
1101
|
+
{
|
|
1102
|
+
title: 'Diff contracts',
|
|
1103
|
+
description: 'Compute diff between two versions of a contract',
|
|
1104
|
+
inputSchema: {
|
|
1105
|
+
name: z.string(),
|
|
1106
|
+
from_version: z.number().int(),
|
|
1107
|
+
to_version: z.number().int()
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
async (args: { name: string; from_version: number; to_version: number }) => {
|
|
1111
|
+
const who = requireAgent()
|
|
1112
|
+
if (typeof who !== 'string') return toText(who)
|
|
1113
|
+
return run(() => diffContractsSvc.diff({ caller: who, ...args }))
|
|
1114
|
+
}
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
// bind_channel — self-binding: caller (Claude host) writes its own channel_session_id
|
|
1118
|
+
if (channelWakeFanout) {
|
|
1119
|
+
const bindSvc = new BindChannelService(db, channelWakeFanout)
|
|
1120
|
+
server.registerTool(
|
|
1121
|
+
'bind_channel',
|
|
1122
|
+
{
|
|
1123
|
+
title: 'Bind channel_session_id to caller',
|
|
1124
|
+
description: [
|
|
1125
|
+
'Low-level rebind tool for Claude channel delivery.',
|
|
1126
|
+
'Bind the caller session\'s agent row to a channel_session_id produced by the cross-agent-teams-mcp channel proxy.',
|
|
1127
|
+
'Most callers should prefer `register_agent({ client: "claude-code", channel_session_id, ... })` on the unified registration path.',
|
|
1128
|
+
'Call this when you need to rebind an already-registered row after the proxy announces a new csid.',
|
|
1129
|
+
'Rejects proxy callers (role=__channel_proxy__).',
|
|
1130
|
+
'Rejects unknown csid (no live proxy sink attached).'
|
|
1131
|
+
].join(' '),
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
channel_session_id: z.string().min(1)
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
async (args: { channel_session_id: string }) => {
|
|
1137
|
+
const who = requireAgent()
|
|
1138
|
+
if (typeof who !== 'string') return toText(who)
|
|
1139
|
+
return run(() => bindSvc.bind({
|
|
1140
|
+
callerAgentId: who,
|
|
1141
|
+
channel_session_id: args.channel_session_id
|
|
1142
|
+
}))
|
|
1143
|
+
}
|
|
1144
|
+
)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
server.registerTool(
|
|
1148
|
+
'bind_runtime_identity',
|
|
1149
|
+
{
|
|
1150
|
+
title: 'Bind runtime identity to caller',
|
|
1151
|
+
description: [
|
|
1152
|
+
'Bind the caller session\'s agent row to a verified tmux runtime identity.',
|
|
1153
|
+
'Pass `agent` to choose the built-in process matcher (`codex`, `claude-code`, `opencode`), or use `custom` together with `process_pattern`.',
|
|
1154
|
+
'Prefer passing `ui_pid` for the visible agent UI process; the daemon verifies pid → tty → pane before persisting `tmux_pane_id`.',
|
|
1155
|
+
'If `ui_pid` is unavailable, pass `ui_tty` together with `tmux_pane_id` for a weaker but still verified binding path.',
|
|
1156
|
+
'This tool writes registry state; `detect_tmux_pane` is for debugging only.'
|
|
1157
|
+
].join(' '),
|
|
1158
|
+
inputSchema: bindRuntimeIdentitySchema,
|
|
1159
|
+
},
|
|
1160
|
+
async (args: {
|
|
1161
|
+
agent: 'codex' | 'claude-code' | 'opencode' | 'custom'
|
|
1162
|
+
ui_pid?: number
|
|
1163
|
+
ui_tty?: string
|
|
1164
|
+
tmux_pane_id?: string
|
|
1165
|
+
process_pattern?: string
|
|
1166
|
+
}) => {
|
|
1167
|
+
const parsed = bindRuntimeIdentityArgsSchema.safeParse(args)
|
|
1168
|
+
if (!parsed.success) {
|
|
1169
|
+
return toText({
|
|
1170
|
+
error: 'invalid_arguments',
|
|
1171
|
+
detail: parsed.error.issues.map(issue => issue.message).join('; '),
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
const who = requireAgent()
|
|
1175
|
+
if (typeof who !== 'string') return toText(who)
|
|
1176
|
+
return run(() => bindRuntimeIdentitySvc.bind({
|
|
1177
|
+
callerAgentId: who,
|
|
1178
|
+
agent: parsed.data.agent,
|
|
1179
|
+
ui_pid: parsed.data.ui_pid,
|
|
1180
|
+
ui_tty: parsed.data.ui_tty,
|
|
1181
|
+
tmux_pane_id: parsed.data.tmux_pane_id,
|
|
1182
|
+
process_pattern: parsed.data.process_pattern,
|
|
1183
|
+
}))
|
|
1184
|
+
}
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
// subscribe_channel_wake — reserved for channel proxies (role=__channel_proxy__)
|
|
1188
|
+
if (channelWakeFanout) {
|
|
1189
|
+
const subscribeSvc = new SubscribeChannelWakeService(db, channelWakeFanout)
|
|
1190
|
+
server.registerTool(
|
|
1191
|
+
'subscribe_channel_wake',
|
|
1192
|
+
{
|
|
1193
|
+
title: 'Subscribe channel wake',
|
|
1194
|
+
description: [
|
|
1195
|
+
'Internal tool reserved for the cross-agent-teams-mcp channel proxy.',
|
|
1196
|
+
'Attaches the caller\'s MCP session notification sink to a channel_session_id so the',
|
|
1197
|
+
'daemon can emit notifications/channel_wake to it. Requires role=__channel_proxy__.'
|
|
1198
|
+
].join(' '),
|
|
1199
|
+
inputSchema: { channel_session_id: z.string().min(1) }
|
|
1200
|
+
},
|
|
1201
|
+
async (args: { channel_session_id: string }) => {
|
|
1202
|
+
const who = requireAgent()
|
|
1203
|
+
if (typeof who !== 'string') return toText(who)
|
|
1204
|
+
const sid = getSessionId?.()
|
|
1205
|
+
if (!sid) return toText({ error: 'unknown_session' })
|
|
1206
|
+
const sink = (payload: unknown) => {
|
|
1207
|
+
const t = getTransport?.()
|
|
1208
|
+
if (!t) return
|
|
1209
|
+
try {
|
|
1210
|
+
void Promise.resolve(t.send(payload as Record<string, unknown>)).catch(() => { /* best-effort */ })
|
|
1211
|
+
} catch { /* best-effort */ }
|
|
1212
|
+
}
|
|
1213
|
+
return run(() => subscribeSvc.subscribe({
|
|
1214
|
+
callerAgentId: who,
|
|
1215
|
+
channel_session_id: args.channel_session_id,
|
|
1216
|
+
sessionId: sid,
|
|
1217
|
+
sink
|
|
1218
|
+
}))
|
|
1219
|
+
}
|
|
1220
|
+
)
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// pending_contract_events
|
|
1224
|
+
server.registerTool(
|
|
1225
|
+
'pending_contract_events',
|
|
1226
|
+
{
|
|
1227
|
+
title: 'Pending contract events',
|
|
1228
|
+
description: 'Poll contract_registered events not yet seen',
|
|
1229
|
+
inputSchema: {
|
|
1230
|
+
since_event_id: z.number().int().optional(),
|
|
1231
|
+
limit: z.number().int().optional()
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
async (args: { since_event_id?: number; limit?: number }) => {
|
|
1235
|
+
const who = requireAgent()
|
|
1236
|
+
if (typeof who !== 'string') return toText(who)
|
|
1237
|
+
return run(() => pendingEventsSvc.poll({ caller: who, ...args }))
|
|
1238
|
+
}
|
|
1239
|
+
)
|
|
1240
|
+
}
|