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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ChannelWakeFanout } from '../daemon/channel-wake-fanout.js'
|
|
2
|
+
import { sendChannelWake } from '../daemon/channel-wake-send.js'
|
|
3
|
+
import type { ClientKind } from '../lib/client-kind.js'
|
|
4
|
+
import type { DeliverySpec } from '../lib/delivery-spec.js'
|
|
5
|
+
import {
|
|
6
|
+
dispatchCodexAppserverPoke,
|
|
7
|
+
type CodexAppserverDispatchResult,
|
|
8
|
+
} from './codex-appserver-dispatch.js'
|
|
9
|
+
|
|
10
|
+
export interface DispatchDeps {
|
|
11
|
+
channelWakeFanout?: ChannelWakeFanout
|
|
12
|
+
tmuxPoke: (args: { pane_id: string; content: string }) => Promise<TmuxPokeResult>
|
|
13
|
+
codexAppserverDispatch?: (args: {
|
|
14
|
+
delivery: Extract<DeliverySpec, { kind: 'codex-appserver' }>
|
|
15
|
+
content: string
|
|
16
|
+
}) => Promise<CodexAppserverDispatchResult>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type TmuxPokeResult =
|
|
20
|
+
| { ok: true; pane_tail_before: string; pane_tail_after: string }
|
|
21
|
+
| { error: string; detail?: unknown }
|
|
22
|
+
|
|
23
|
+
export interface TargetRow {
|
|
24
|
+
client: ClientKind | null
|
|
25
|
+
delivery: DeliverySpec
|
|
26
|
+
tmux_pane_id: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DispatchInput {
|
|
30
|
+
content: string
|
|
31
|
+
meta: Record<string, string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DispatchResult =
|
|
35
|
+
| {
|
|
36
|
+
ok: true
|
|
37
|
+
transport_used: 'claude-channel'
|
|
38
|
+
channel_session_id: string
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
ok: true
|
|
42
|
+
transport_used: 'tmux-poke'
|
|
43
|
+
pane_id: string
|
|
44
|
+
pane_tail_before: string
|
|
45
|
+
pane_tail_after: string
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
ok: true
|
|
49
|
+
transport_used: 'codex-appserver'
|
|
50
|
+
thread_id: string
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
error: string
|
|
54
|
+
detail?: unknown
|
|
55
|
+
transport_used?: 'tmux-poke' | 'codex-appserver'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function dispatchPoke(
|
|
59
|
+
deps: DispatchDeps,
|
|
60
|
+
target: TargetRow,
|
|
61
|
+
input: DispatchInput
|
|
62
|
+
): Promise<DispatchResult> {
|
|
63
|
+
const client = resolveClient(target)
|
|
64
|
+
if (client === 'claude-code') return dispatchClaude(deps, target, input)
|
|
65
|
+
if (client === 'codex') return dispatchCodex(deps, target, input)
|
|
66
|
+
return dispatchUnknown(deps, target, input)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveClient(target: TargetRow): ClientKind | null {
|
|
70
|
+
if (target.client) return target.client
|
|
71
|
+
if (target.delivery.kind === 'claude-channel') return 'claude-code'
|
|
72
|
+
if (target.delivery.kind === 'codex-appserver') return 'codex'
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function dispatchTmux(
|
|
77
|
+
deps: DispatchDeps,
|
|
78
|
+
paneId: string,
|
|
79
|
+
content: string
|
|
80
|
+
): Promise<DispatchResult> {
|
|
81
|
+
const tmuxResult = await deps.tmuxPoke({ pane_id: paneId, content })
|
|
82
|
+
if ('ok' in tmuxResult && tmuxResult.ok) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
transport_used: 'tmux-poke',
|
|
86
|
+
pane_id: paneId,
|
|
87
|
+
pane_tail_before: tmuxResult.pane_tail_before,
|
|
88
|
+
pane_tail_after: tmuxResult.pane_tail_after,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...(tmuxResult as { error: string; detail?: unknown }),
|
|
93
|
+
transport_used: 'tmux-poke',
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function dispatchClaude(
|
|
98
|
+
deps: DispatchDeps,
|
|
99
|
+
target: TargetRow,
|
|
100
|
+
input: DispatchInput
|
|
101
|
+
): Promise<DispatchResult> {
|
|
102
|
+
const paneId = target.tmux_pane_id
|
|
103
|
+
const channelSubscribed =
|
|
104
|
+
target.delivery.kind === 'claude-channel' &&
|
|
105
|
+
(deps.channelWakeFanout?.has(target.delivery.channel_session_id) ?? false)
|
|
106
|
+
|
|
107
|
+
if (target.delivery.kind === 'claude-channel' && channelSubscribed && deps.channelWakeFanout) {
|
|
108
|
+
const result = sendChannelWake(
|
|
109
|
+
deps.channelWakeFanout,
|
|
110
|
+
target.delivery.channel_session_id,
|
|
111
|
+
input
|
|
112
|
+
)
|
|
113
|
+
if (result.ok) {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
transport_used: 'claude-channel',
|
|
117
|
+
channel_session_id: target.delivery.channel_session_id,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (paneId) return dispatchTmux(deps, paneId, input.content)
|
|
123
|
+
return {
|
|
124
|
+
error: 'no_transport_available',
|
|
125
|
+
detail: {
|
|
126
|
+
channel_subscribed: channelSubscribed,
|
|
127
|
+
tmux_pane_set: false,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function dispatchCodex(
|
|
133
|
+
deps: DispatchDeps,
|
|
134
|
+
target: TargetRow,
|
|
135
|
+
input: DispatchInput
|
|
136
|
+
): Promise<DispatchResult> {
|
|
137
|
+
const paneId = target.tmux_pane_id
|
|
138
|
+
if (target.delivery.kind === 'codex-appserver') {
|
|
139
|
+
const result = await (deps.codexAppserverDispatch ?? dispatchCodexAppserverPoke)({
|
|
140
|
+
delivery: target.delivery,
|
|
141
|
+
content: input.content,
|
|
142
|
+
})
|
|
143
|
+
if ('ok' in result && result.ok) return result
|
|
144
|
+
if (paneId) return dispatchTmux(deps, paneId, input.content)
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
if (paneId) return dispatchTmux(deps, paneId, input.content)
|
|
148
|
+
return {
|
|
149
|
+
error: 'no_transport_available',
|
|
150
|
+
detail: {
|
|
151
|
+
codex_bound: false,
|
|
152
|
+
tmux_pane_set: false,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function dispatchUnknown(
|
|
158
|
+
deps: DispatchDeps,
|
|
159
|
+
target: TargetRow,
|
|
160
|
+
input: DispatchInput
|
|
161
|
+
): Promise<DispatchResult> {
|
|
162
|
+
const paneId = target.tmux_pane_id
|
|
163
|
+
if (paneId) return dispatchTmux(deps, paneId, input.content)
|
|
164
|
+
return {
|
|
165
|
+
error: 'no_transport_available',
|
|
166
|
+
detail: {
|
|
167
|
+
channel_subscribed: false,
|
|
168
|
+
tmux_pane_set: false,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'
|
|
2
|
+
import type Database from 'better-sqlite3'
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
6
|
+
import { echoSchema, echoHandler } from './echo.js'
|
|
7
|
+
import { registerBusinessTools, type AgentIdHolder } from './tools.js'
|
|
8
|
+
import type { SseFanout, SseSink } from '../daemon/sse-fanout.js'
|
|
9
|
+
import type { ChannelWakeFanout } from '../daemon/channel-wake-fanout.js'
|
|
10
|
+
|
|
11
|
+
interface Session {
|
|
12
|
+
transport: StreamableHTTPServerTransport
|
|
13
|
+
server: McpServer
|
|
14
|
+
sessionId: string
|
|
15
|
+
agentIdHolder: AgentIdHolder
|
|
16
|
+
clientInfo?: {
|
|
17
|
+
name?: string
|
|
18
|
+
version?: string
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mountMcp(
|
|
23
|
+
app: FastifyInstance,
|
|
24
|
+
db: Database.Database,
|
|
25
|
+
fanout: SseFanout,
|
|
26
|
+
channelWakeFanout?: ChannelWakeFanout
|
|
27
|
+
): void {
|
|
28
|
+
const sessions = new Map<string, Session>()
|
|
29
|
+
// Once register_agent succeeds for a session id, pin the owning Authorization hash.
|
|
30
|
+
// A later register_agent presenting a different Authorization triggers HTTP 409.
|
|
31
|
+
const sessionOwners = new Map<string, string>()
|
|
32
|
+
|
|
33
|
+
function createSession(): Session {
|
|
34
|
+
const server = new McpServer(
|
|
35
|
+
{ name: 'cross-agent-teams-mcp', version: '0.1.0' },
|
|
36
|
+
{
|
|
37
|
+
instructions:
|
|
38
|
+
'xats 是 cross-agent-teams 的缩写. 当用户或其他 agent 提到 xats 时, 指的是本 MCP server (cross-agent-teams-mcp) 及其跨 agent 协作工具 (register_agent, send_message, task_* 等). 用户说"注册到 xats"或"注册到 cross-agent-teams"时, 表示注册到本 MCP 服务, xats 和 cross-agent-teams 不是 team 名, 不要把它们传给 team. 不要把单独的常用词"注册"默认劫持为本工具; 只有当前上下文已经在讨论 cross-agent-teams 注册时才可续接. 注册时 (register_agent / register_claude_self) 如果用户未显式指定 team, 默认传 project_dir=当前工作目录 (cwd), daemon 会用它的 basename 作为 team 默认值; 都不传时回落 default.'
|
|
39
|
+
+ ' Codex clients: if your tool shell env has CODEX_THREAD_ID set (codex 0.124.0+ exports it), pass its value as `thread_id` and prefer `register_codex_self` over `register_agent`. Do NOT attempt to discover or pass `ui_pid` from codex — the launcher\'s `pre_register_codex_pane` pre-reg flow handles tmux pane binding automatically; supplying `ui_pid` manually disables that auto-bind path.'
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
const agentIdHolder: AgentIdHolder = { current: undefined }
|
|
43
|
+
server.registerTool('echo', { title: 'Echo', description: 'Return the input', inputSchema: echoSchema }, echoHandler as any)
|
|
44
|
+
|
|
45
|
+
let sessionIdForCaller: string | undefined
|
|
46
|
+
// `caller()` returns the session id before register_agent succeeds (to serve as
|
|
47
|
+
// a stable connection_id), and the bound agent_id after register succeeds.
|
|
48
|
+
const getCallerAgentId = (): string | undefined =>
|
|
49
|
+
agentIdHolder.current ?? sessionIdForCaller
|
|
50
|
+
|
|
51
|
+
const sink: SseSink = {
|
|
52
|
+
send(msg: Record<string, unknown>): void {
|
|
53
|
+
const payload = {
|
|
54
|
+
jsonrpc: '2.0' as const,
|
|
55
|
+
method: 'notifications/contract_event',
|
|
56
|
+
params: msg
|
|
57
|
+
}
|
|
58
|
+
void transport.send(payload).catch(() => { /* no active GET stream yet */ })
|
|
59
|
+
},
|
|
60
|
+
sendHeartbeat(): void {
|
|
61
|
+
void transport.send({
|
|
62
|
+
jsonrpc: '2.0' as const,
|
|
63
|
+
method: 'notifications/heartbeat',
|
|
64
|
+
params: {}
|
|
65
|
+
}).catch(() => { /* no active GET stream yet */ })
|
|
66
|
+
},
|
|
67
|
+
close(): void { /* transport.onclose handles lifecycle */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const onRegisterSuccess = (agent_id: string, team: string): void => {
|
|
71
|
+
// Detach any prior sink registered under this agent_id (e.g. from a previous
|
|
72
|
+
// session that reused the same identity) before attaching the new one.
|
|
73
|
+
try { fanout.detach(agent_id) } catch { /* ignore */ }
|
|
74
|
+
// If this session had previously bound a different agent_id (e.g. role change
|
|
75
|
+
// mid-session), detach that too.
|
|
76
|
+
if (agentIdHolder.current && agentIdHolder.current !== agent_id) {
|
|
77
|
+
try { fanout.detach(agentIdHolder.current) } catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
fanout.attach(agent_id, team, sink)
|
|
80
|
+
agentIdHolder.current = agent_id
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const onUnregisterSuccess = (agent_id: string): void => {
|
|
84
|
+
try { fanout.detach(agent_id) } catch { /* ignore */ }
|
|
85
|
+
if (sessionIdForCaller && channelWakeFanout) {
|
|
86
|
+
try { channelWakeFanout.detachBySession(sessionIdForCaller) } catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
if (agentIdHolder.current === agent_id) agentIdHolder.current = undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const transport = new StreamableHTTPServerTransport({
|
|
92
|
+
sessionIdGenerator: () => randomUUID(),
|
|
93
|
+
onsessioninitialized: (sid: string) => {
|
|
94
|
+
sessionIdForCaller = sid
|
|
95
|
+
sessions.set(sid, { transport, server, sessionId: sid, agentIdHolder, clientInfo: undefined })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
transport.onclose = () => {
|
|
99
|
+
if (agentIdHolder.current) {
|
|
100
|
+
try { fanout.detach(agentIdHolder.current) } catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
if (transport.sessionId && channelWakeFanout) {
|
|
103
|
+
try { channelWakeFanout.detachBySession(transport.sessionId) } catch { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
if (transport.sessionId) {
|
|
106
|
+
sessions.delete(transport.sessionId)
|
|
107
|
+
sessionOwners.delete(transport.sessionId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
registerBusinessTools(
|
|
111
|
+
server,
|
|
112
|
+
db,
|
|
113
|
+
getCallerAgentId,
|
|
114
|
+
fanout,
|
|
115
|
+
onRegisterSuccess,
|
|
116
|
+
() => sessionIdForCaller,
|
|
117
|
+
channelWakeFanout,
|
|
118
|
+
() => transport,
|
|
119
|
+
() => {
|
|
120
|
+
const sid = sessionIdForCaller
|
|
121
|
+
if (!sid) return undefined
|
|
122
|
+
return sessions.get(sid)?.clientInfo
|
|
123
|
+
},
|
|
124
|
+
onUnregisterSuccess
|
|
125
|
+
)
|
|
126
|
+
server.connect(transport)
|
|
127
|
+
return { transport, server, sessionId: '', agentIdHolder }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function authHashFor(req: FastifyRequest): string | null {
|
|
131
|
+
const raw = req.headers['authorization']
|
|
132
|
+
if (typeof raw !== 'string') return null
|
|
133
|
+
const trimmed = raw.trim()
|
|
134
|
+
if (trimmed.length === 0) return null
|
|
135
|
+
return createHash('sha256').update(trimmed).digest('hex')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ToolsCallBody {
|
|
139
|
+
method?: string
|
|
140
|
+
params?: { name?: string; arguments?: Record<string, unknown> }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
app.post('/mcp', async (req: FastifyRequest, reply: FastifyReply) => {
|
|
144
|
+
const sid = req.headers['mcp-session-id'] as string | undefined
|
|
145
|
+
const body = req.body as ToolsCallBody | undefined
|
|
146
|
+
const isInit = body?.method === 'initialize'
|
|
147
|
+
let session = sid ? sessions.get(sid) : undefined
|
|
148
|
+
if (!session && !isInit) { return reply.code(400).send({ error: 'unknown_session' }) }
|
|
149
|
+
|
|
150
|
+
// register_agent presenting a different Authorization header than the one that
|
|
151
|
+
// first claimed this session id -> agent_id_collision (HTTP 409). Absence of
|
|
152
|
+
// an Authorization header disables collision enforcement per spec.
|
|
153
|
+
if (session && body?.method === 'tools/call' && body.params?.name === 'register_agent') {
|
|
154
|
+
const authHash = authHashFor(req)
|
|
155
|
+
if (authHash !== null) {
|
|
156
|
+
const owner = sessionOwners.get(session.sessionId)
|
|
157
|
+
if (owner && owner !== authHash) {
|
|
158
|
+
return reply.code(409).send({ error: 'agent_id_collision' })
|
|
159
|
+
}
|
|
160
|
+
if (!owner) sessionOwners.set(session.sessionId, authHash)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Spoofed from_agent_id on tools/call -> 403. Compare against the session's
|
|
165
|
+
// currently bound agent_id (post register_agent), NOT the raw MCP session id.
|
|
166
|
+
if (session && body?.method === 'tools/call') {
|
|
167
|
+
const claimed = body.params?.arguments?.from_agent_id
|
|
168
|
+
if (typeof claimed === 'string') {
|
|
169
|
+
const current = session.agentIdHolder.current
|
|
170
|
+
if (current === undefined || claimed !== current) {
|
|
171
|
+
return reply.code(403).send({ error: 'identity_mismatch' })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!session) { session = createSession() }
|
|
177
|
+
if (body?.method === 'initialize') {
|
|
178
|
+
const params = body.params as { clientInfo?: { name?: unknown; version?: unknown } } | undefined
|
|
179
|
+
const clientInfo = params?.clientInfo
|
|
180
|
+
session.clientInfo = {
|
|
181
|
+
name: typeof clientInfo?.name === 'string' ? clientInfo.name : undefined,
|
|
182
|
+
version: typeof clientInfo?.version === 'string' ? clientInfo.version : undefined,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
await session.transport.handleRequest(req.raw, reply.raw, body)
|
|
186
|
+
return reply
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
app.get('/mcp', async (req: FastifyRequest, reply: FastifyReply) => {
|
|
190
|
+
const sid = req.headers['mcp-session-id'] as string | undefined
|
|
191
|
+
const session = sid ? sessions.get(sid) : undefined
|
|
192
|
+
if (!session) return reply.code(400).send({ error: 'unknown_session' })
|
|
193
|
+
await session.transport.handleRequest(req.raw, reply.raw)
|
|
194
|
+
return reply
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
app.delete('/mcp', async (req: FastifyRequest, reply: FastifyReply) => {
|
|
198
|
+
const sid = req.headers['mcp-session-id'] as string | undefined
|
|
199
|
+
const session = sid ? sessions.get(sid) : undefined
|
|
200
|
+
if (!session) return reply.code(400).send({ error: 'unknown_session' })
|
|
201
|
+
await session.transport.handleRequest(req.raw, reply.raw)
|
|
202
|
+
return reply
|
|
203
|
+
})
|
|
204
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3'
|
|
2
|
+
import type { AgentsRepo } from '../storage/agents-repo.js'
|
|
3
|
+
|
|
4
|
+
export type UnregisterSelfResult =
|
|
5
|
+
| { ok: true; team: string; name: string; agent_id: string }
|
|
6
|
+
| { error: 'unknown_agent' }
|
|
7
|
+
| { error: 'tasks_in_progress'; task_ids: string[] }
|
|
8
|
+
|
|
9
|
+
export class UnregisterSelfService {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly db: Database.Database,
|
|
12
|
+
private readonly agents: AgentsRepo
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
unregister(args: { caller: string }): UnregisterSelfResult {
|
|
16
|
+
const caller = this.agents.findById(args.caller)
|
|
17
|
+
if (!caller) return { error: 'unknown_agent' }
|
|
18
|
+
|
|
19
|
+
const task_ids = this.agents.listClaimedInProgressTaskIds({
|
|
20
|
+
agent_id: caller.agent_id,
|
|
21
|
+
team: caller.team,
|
|
22
|
+
})
|
|
23
|
+
if (task_ids.length > 0) {
|
|
24
|
+
return { error: 'tasks_in_progress', task_ids }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let removed = false
|
|
28
|
+
const tx = this.db.transaction(() => {
|
|
29
|
+
removed = this.agents.deleteById(caller.agent_id)
|
|
30
|
+
if (!removed) return
|
|
31
|
+
this.agents.deleteContractSubscriptions({
|
|
32
|
+
agent_id: caller.agent_id,
|
|
33
|
+
team: caller.team,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
tx()
|
|
37
|
+
|
|
38
|
+
if (!removed) return { error: 'unknown_agent' }
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
team: caller.team,
|
|
42
|
+
name: caller.name,
|
|
43
|
+
agent_id: caller.agent_id,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|