@desplega.ai/agent-swarm 1.0.1

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.
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import pkg from "../../package.json";
4
+ import type { Agent } from "../types";
5
+
6
+ // @ts-ignore
7
+ const SERVER_NAME = pkg.config?.name ?? "agent-swarm";
8
+
9
+ type McpServerConfig = {
10
+ url: string;
11
+ headers: {
12
+ Authorization: string;
13
+ "X-Agent-ID": string;
14
+ };
15
+ };
16
+
17
+ interface HookMessage {
18
+ hook_event_name: string;
19
+ session_id?: string;
20
+ transcript_path?: string;
21
+ permission_mode?: string;
22
+ cwd?: string;
23
+ source?: string;
24
+ trigger?: string;
25
+ custom_instructions?: string;
26
+ tool_name?: string;
27
+ tool_input?: Record<string, unknown>;
28
+ tool_response?: Record<string, unknown>;
29
+ tool_use_id?: string;
30
+ prompt?: string;
31
+ stop_hook_active?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Main hook handler - processes Claude Code hook events
36
+ */
37
+ export async function handleHook(): Promise<void> {
38
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
39
+
40
+ let mcpConfig: McpServerConfig | undefined;
41
+
42
+ try {
43
+ const mcpFile = Bun.file(`${projectDir}/.mcp.json`);
44
+ if (await mcpFile.exists()) {
45
+ const config = await mcpFile.json();
46
+ mcpConfig = config?.mcpServers?.[SERVER_NAME] as McpServerConfig;
47
+ }
48
+ } catch {
49
+ // No config found, proceed without MCP
50
+ }
51
+
52
+ let msg: HookMessage;
53
+ try {
54
+ msg = await Bun.stdin.json();
55
+ } catch {
56
+ // No stdin or invalid JSON - exit silently
57
+ return;
58
+ }
59
+
60
+ const getBaseUrl = (): string => {
61
+ if (!mcpConfig) return "";
62
+ try {
63
+ const url = new URL(mcpConfig.url);
64
+ return url.origin;
65
+ } catch {
66
+ return "";
67
+ }
68
+ };
69
+
70
+ const ping = async (): Promise<void> => {
71
+ if (!mcpConfig) return;
72
+
73
+ try {
74
+ await fetch(`${getBaseUrl()}/ping`, {
75
+ method: "POST",
76
+ headers: mcpConfig.headers,
77
+ });
78
+ } catch {
79
+ // Silently fail - server might not be running
80
+ }
81
+ };
82
+
83
+ const close = async (): Promise<void> => {
84
+ if (!mcpConfig) return;
85
+
86
+ try {
87
+ await fetch(`${getBaseUrl()}/close`, {
88
+ method: "POST",
89
+ headers: mcpConfig.headers,
90
+ });
91
+ } catch {
92
+ // Silently fail
93
+ }
94
+ };
95
+
96
+ const getAgentInfo = async (): Promise<Agent | undefined> => {
97
+ if (!mcpConfig) return;
98
+
99
+ try {
100
+ const resp = await fetch(`${getBaseUrl()}/me`, {
101
+ method: "GET",
102
+ headers: mcpConfig.headers,
103
+ });
104
+
105
+ if ([400, 404].includes(resp.status)) {
106
+ return;
107
+ }
108
+
109
+ return (await resp.json()) as Agent;
110
+ } catch {
111
+ // Silently fail
112
+ }
113
+
114
+ return;
115
+ };
116
+
117
+ // Ping the server to indicate activity
118
+ await ping();
119
+
120
+ // Get current agent info
121
+ const agentInfo = await getAgentInfo();
122
+
123
+ // Always output agent status
124
+ if (agentInfo) {
125
+ console.log(
126
+ `You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" with ID: ${agentInfo.id} (status: ${agentInfo.status}) as of ${new Date().toISOString()}.`,
127
+ );
128
+ } else {
129
+ console.log(
130
+ `You are not registered in the agent swarm yet. Use the join-swarm tool to register yourself, then check your status with my-agent-info.
131
+
132
+ If the ${SERVER_NAME} server is not running or disabled, disregard this message.`,
133
+ );
134
+ }
135
+
136
+ // Handle specific hook events
137
+ switch (msg.hook_event_name) {
138
+ case "SessionStart":
139
+ if (!agentInfo) break;
140
+
141
+ if (agentInfo.isLead) {
142
+ console.log(
143
+ `As the lead agent, you are responsible for coordinating the swarm to fulfill the user's request efficiently. Use the ${SERVER_NAME} tools to assign tasks to worker agents and monitor their progress.`,
144
+ );
145
+ } else {
146
+ console.log(
147
+ `As a worker agent, you should call the poll-task tool to wait for tasks assigned by the lead agent, unless specified otherwise.`,
148
+ );
149
+ }
150
+ break;
151
+
152
+ case "PreCompact":
153
+ // Covered by SessionStart hook
154
+ break;
155
+
156
+ case "PreToolUse":
157
+ // Nothing to do here for now
158
+ break;
159
+
160
+ case "PostToolUse":
161
+ if (agentInfo) {
162
+ if (agentInfo.isLead) {
163
+ if (msg.tool_name?.endsWith("send-task")) {
164
+ const maybeTaskId = (msg.tool_response as { task?: { id?: string } })
165
+ ?.task?.id;
166
+
167
+ console.log(
168
+ `Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
169
+ );
170
+ }
171
+ } else {
172
+ console.log(
173
+ `Remember to call store-progress periodically to update the lead agent on your progress.`,
174
+ );
175
+ }
176
+ }
177
+ break;
178
+
179
+ case "UserPromptSubmit":
180
+ // Nothing specific for now
181
+ break;
182
+
183
+ case "Stop":
184
+ // Mark the agent as offline
185
+ await close();
186
+ break;
187
+
188
+ default:
189
+ break;
190
+ }
191
+ }
192
+
193
+ // Run directly when executed as a script
194
+ const isMainModule = import.meta.main;
195
+ if (isMainModule) {
196
+ await handleHook();
197
+ process.exit(0);
198
+ }
package/src/http.ts ADDED
@@ -0,0 +1,262 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ createServer as createHttpServer,
4
+ type IncomingMessage,
5
+ type Server,
6
+ type ServerResponse,
7
+ } from "node:http";
8
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
10
+ import { createServer } from "@/server";
11
+ import { closeDb, getAgentById, getDb, updateAgentStatus } from "./be/db";
12
+ import type { AgentStatus } from "./types";
13
+
14
+ const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
15
+ const apiKey = process.env.API_KEY || "";
16
+
17
+ // Use globalThis to persist state across hot reloads
18
+ const globalState = globalThis as typeof globalThis & {
19
+ __httpServer?: Server<typeof IncomingMessage, typeof ServerResponse>;
20
+ __transports?: Record<string, StreamableHTTPServerTransport>;
21
+ __sigintRegistered?: boolean;
22
+ };
23
+
24
+ // Clean up previous server on hot reload
25
+ if (globalState.__httpServer) {
26
+ console.log("[HTTP] Hot reload detected, closing previous server...");
27
+ globalState.__httpServer.close();
28
+ }
29
+
30
+ const transports: Record<string, StreamableHTTPServerTransport> = globalState.__transports ?? {};
31
+
32
+ function setCorsHeaders(res: ServerResponse) {
33
+ res.setHeader("Access-Control-Allow-Origin", "*");
34
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
35
+ res.setHeader("Access-Control-Allow-Headers", "*");
36
+ res.setHeader("Access-Control-Expose-Headers", "*");
37
+ }
38
+
39
+ const httpServer = createHttpServer(async (req, res) => {
40
+ setCorsHeaders(res);
41
+
42
+ // Handle preflight
43
+ if (req.method === "OPTIONS") {
44
+ res.writeHead(204);
45
+ res.end();
46
+ return;
47
+ }
48
+
49
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
50
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
51
+
52
+ console.log(
53
+ `[HTTP] ${req.method} ${req.url} (sessionId=${sessionId || ""}, agentId=${myAgentId || ""})`,
54
+ );
55
+
56
+ if (req.url === "/health") {
57
+ // Read version from package.json
58
+ const version = (await Bun.file("package.json").json()).version;
59
+
60
+ res.writeHead(200, { "Content-Type": "application/json" });
61
+ res.end(
62
+ JSON.stringify({
63
+ status: "ok",
64
+ version,
65
+ }),
66
+ );
67
+
68
+ return;
69
+ }
70
+
71
+ // API key authentication (if API_KEY is configured)
72
+ if (apiKey) {
73
+ const authHeader = req.headers.authorization;
74
+ const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
75
+
76
+ if (providedKey !== apiKey) {
77
+ res.writeHead(401, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: "Unauthorized" }));
79
+ return;
80
+ }
81
+ }
82
+
83
+ if (req.method === "GET" && req.url === "/me") {
84
+ if (!myAgentId) {
85
+ res.writeHead(400, { "Content-Type": "application/json" });
86
+ res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
87
+ return;
88
+ }
89
+
90
+ const agent = getAgentById(myAgentId);
91
+
92
+ if (!agent) {
93
+ res.writeHead(404, { "Content-Type": "application/json" });
94
+ res.end(JSON.stringify({ error: "Agent not found" }));
95
+ return;
96
+ }
97
+
98
+ res.writeHead(200, { "Content-Type": "application/json" });
99
+ res.end(JSON.stringify(agent));
100
+ return;
101
+ }
102
+
103
+ if (req.method === "POST" && req.url === "/ping") {
104
+ if (!myAgentId) {
105
+ res.writeHead(400, { "Content-Type": "application/json" });
106
+ res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
107
+ return;
108
+ }
109
+
110
+ const tx = getDb().transaction(() => {
111
+ const agent = getAgentById(myAgentId);
112
+
113
+ if (!agent) {
114
+ res.writeHead(404, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ error: "Agent not found" }));
116
+ return;
117
+ }
118
+
119
+ let status: AgentStatus = "idle";
120
+
121
+ if (agent.status === "busy") {
122
+ status = "busy";
123
+ }
124
+
125
+ updateAgentStatus(agent.id, status);
126
+ });
127
+
128
+ tx();
129
+
130
+ res.writeHead(204);
131
+ res.end();
132
+ return;
133
+ }
134
+
135
+ if (req.method === "POST" && req.url === "/close") {
136
+ if (!myAgentId) {
137
+ res.writeHead(400, { "Content-Type": "application/json" });
138
+ res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
139
+ return;
140
+ }
141
+
142
+ const tx = getDb().transaction(() => {
143
+ const agent = getAgentById(myAgentId);
144
+
145
+ if (!agent) {
146
+ res.writeHead(404, { "Content-Type": "application/json" });
147
+ res.end(JSON.stringify({ error: "Agent not found" }));
148
+ return;
149
+ }
150
+
151
+ updateAgentStatus(agent.id, "offline");
152
+ });
153
+
154
+ tx();
155
+
156
+ res.writeHead(204);
157
+ res.end();
158
+ return;
159
+ }
160
+
161
+ if (req.url !== "/mcp") {
162
+ res.writeHead(404);
163
+ res.end("Not Found");
164
+ return;
165
+ }
166
+
167
+ if (req.method === "POST") {
168
+ const chunks: Buffer[] = [];
169
+ for await (const chunk of req) {
170
+ chunks.push(chunk);
171
+ }
172
+ const body = JSON.parse(Buffer.concat(chunks).toString());
173
+
174
+ let transport: StreamableHTTPServerTransport;
175
+
176
+ if (sessionId && transports[sessionId]) {
177
+ transport = transports[sessionId];
178
+ } else if (!sessionId && isInitializeRequest(body)) {
179
+ transport = new StreamableHTTPServerTransport({
180
+ sessionIdGenerator: () => randomUUID(),
181
+ onsessioninitialized: (id) => {
182
+ transports[id] = transport;
183
+ },
184
+ onsessionclosed: (id) => {
185
+ delete transports[id];
186
+ },
187
+ });
188
+
189
+ transport.onclose = () => {
190
+ if (transport.sessionId) {
191
+ delete transports[transport.sessionId];
192
+ }
193
+ };
194
+
195
+ const server = createServer();
196
+ await server.connect(transport);
197
+ } else {
198
+ res.writeHead(400, { "Content-Type": "application/json" });
199
+ res.end(
200
+ JSON.stringify({
201
+ jsonrpc: "2.0",
202
+ error: { code: -32000, message: "Invalid session" },
203
+ id: null,
204
+ }),
205
+ );
206
+ return;
207
+ }
208
+
209
+ await transport.handleRequest(req, res, body);
210
+ return;
211
+ }
212
+
213
+ if (req.method === "GET" || req.method === "DELETE") {
214
+ if (sessionId && transports[sessionId]) {
215
+ await transports[sessionId].handleRequest(req, res);
216
+ return;
217
+ }
218
+ res.writeHead(400);
219
+ res.end("Invalid session");
220
+ return;
221
+ }
222
+
223
+ res.writeHead(405);
224
+ res.end("Method not allowed");
225
+ });
226
+
227
+ // Store references in globalThis for hot reload persistence
228
+ globalState.__httpServer = httpServer;
229
+ globalState.__transports = transports;
230
+
231
+ function shutdown() {
232
+ console.log("Shutting down HTTP server...");
233
+
234
+ // Close all active transports (SSE connections, etc.)
235
+ for (const [id, transport] of Object.entries(transports)) {
236
+ console.log(`[HTTP] Closing transport ${id}`);
237
+ transport.close();
238
+ delete transports[id];
239
+ }
240
+
241
+ // Close all active connections forcefully
242
+ httpServer.closeAllConnections();
243
+ httpServer.close(() => {
244
+ closeDb();
245
+ console.log("MCP HTTP server closed, and database connection closed");
246
+ process.exit(0);
247
+ });
248
+ }
249
+
250
+ // Only register SIGINT handler once (avoid duplicates on hot reload)
251
+ if (!globalState.__sigintRegistered) {
252
+ globalState.__sigintRegistered = true;
253
+ process.on("SIGINT", shutdown);
254
+ }
255
+
256
+ httpServer
257
+ .listen(port, () => {
258
+ console.log(`MCP HTTP server running on http://localhost:${port}/mcp`);
259
+ })
260
+ .on("error", (err) => {
261
+ console.error("HTTP Server Error:", err);
262
+ });
package/src/server.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { initDb } from "./be/db";
3
+ import { registerGetSwarmTool } from "./tools/get-swarm";
4
+ import { registerGetTaskDetailsTool } from "./tools/get-task-details";
5
+ import { registerGetTasksTool } from "./tools/get-tasks";
6
+ import { registerJoinSwarmTool } from "./tools/join-swarm";
7
+ import { registerMyAgentInfoTool } from "./tools/my-agent-info";
8
+ import { registerPollTaskTool } from "./tools/poll-task";
9
+ import { registerSendTaskTool } from "./tools/send-task";
10
+ import { registerStoreProgressTool } from "./tools/store-progress";
11
+ import pkg from "../package.json";
12
+
13
+
14
+ export function createServer() {
15
+ // Initialize database with WAL mode
16
+ initDb();
17
+
18
+ const server = new McpServer(
19
+ {
20
+ name: pkg.name,
21
+ version: pkg.version,
22
+ description: pkg.description,
23
+ },
24
+ {
25
+ capabilities: {
26
+ logging: {},
27
+ },
28
+ },
29
+ );
30
+
31
+ registerJoinSwarmTool(server);
32
+ registerPollTaskTool(server);
33
+ registerGetSwarmTool(server);
34
+ registerGetTasksTool(server);
35
+ registerSendTaskTool(server);
36
+ registerGetTaskDetailsTool(server);
37
+ registerStoreProgressTool(server);
38
+ registerMyAgentInfoTool(server);
39
+
40
+ return server;
41
+ }
package/src/stdio.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "@/server";
3
+ import { closeDb } from "./be/db";
4
+
5
+ async function main() {
6
+ const server = createServer();
7
+ const transport = new StdioServerTransport();
8
+
9
+ await server.connect(transport);
10
+
11
+ await server.sendLoggingMessage({
12
+ level: "info",
13
+ data: "MCP server connected via stdio",
14
+ });
15
+ }
16
+
17
+ main()
18
+ .catch(console.error)
19
+ .finally(() => {
20
+ closeDb();
21
+ });
@@ -0,0 +1,37 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAllAgents } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentSchema } from "@/types";
6
+
7
+ export const registerGetSwarmTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "get-swarm",
10
+ {
11
+ title: "Get the agent swarm",
12
+ description: "Returns a list of agents in the swarm without their tasks.",
13
+ inputSchema: z.object({
14
+ a: z.string().optional(),
15
+ }),
16
+ outputSchema: z.object({
17
+ agents: z.array(AgentSchema),
18
+ }),
19
+ },
20
+ async (_input, requestInfo, _meta) => {
21
+ const agents = getAllAgents();
22
+
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: `Found ${agents.length} agent(s) in the swarm. Requested by session: ${requestInfo.sessionId}`,
28
+ },
29
+ ],
30
+ structuredContent: {
31
+ yourAgentId: requestInfo.agentId,
32
+ agents,
33
+ },
34
+ };
35
+ },
36
+ );
37
+ };
@@ -0,0 +1,48 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getTaskById } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentTaskSchema } from "@/types";
6
+
7
+ export const registerGetTaskDetailsTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "get-task-details",
10
+ {
11
+ title: "Get task details",
12
+ description:
13
+ "Returns detailed information about a specific task, including output and failure reason.",
14
+ inputSchema: z.object({
15
+ taskId: z.uuid().describe("The ID of the task to get details for."),
16
+ }),
17
+ outputSchema: z.object({
18
+ success: z.boolean(),
19
+ message: z.string(),
20
+ task: AgentTaskSchema.optional(),
21
+ }),
22
+ },
23
+ async ({ taskId }, requestInfo, _meta) => {
24
+ const task = getTaskById(taskId);
25
+
26
+ if (!task) {
27
+ return {
28
+ content: [{ type: "text", text: `Task with ID "${taskId}" not found.` }],
29
+ structuredContent: {
30
+ yourAgentId: requestInfo.agentId,
31
+ success: false,
32
+ message: `Task with ID "${taskId}" not found.`,
33
+ },
34
+ };
35
+ }
36
+
37
+ return {
38
+ content: [{ type: "text", text: `Task "${taskId}" details retrieved.` }],
39
+ structuredContent: {
40
+ yourAgentId: requestInfo.agentId,
41
+ success: true,
42
+ message: `Task "${taskId}" details retrieved.`,
43
+ task,
44
+ },
45
+ };
46
+ },
47
+ );
48
+ };
@@ -0,0 +1,78 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAllTasks } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentTaskStatusSchema } from "@/types";
6
+
7
+ const TaskSummarySchema = z.object({
8
+ id: z.string(),
9
+ task: z.string(),
10
+ status: AgentTaskStatusSchema,
11
+ createdAt: z.string(),
12
+ lastUpdatedAt: z.string(),
13
+ finishedAt: z.string().optional(),
14
+ progress: z.string().optional(),
15
+ });
16
+
17
+ export const registerGetTasksTool = (server: McpServer) => {
18
+ createToolRegistrar(server)(
19
+ "get-tasks",
20
+ {
21
+ title: "Get tasks",
22
+ description:
23
+ "Returns a list of tasks in the swarm, filtered by status and sorted by lastUpdatedAt desc. Defaults to in_progress tasks only. Does not return output or failure reason.",
24
+ inputSchema: z.object({
25
+ status: AgentTaskStatusSchema.optional().describe(
26
+ "Filter by task status. Defaults to 'in_progress'.",
27
+ ),
28
+ mineOnly: z
29
+ .boolean()
30
+ .optional()
31
+ .describe(
32
+ "If true, only return tasks assigned to your agent. Requires X-Agent-ID header.",
33
+ ),
34
+ }),
35
+ outputSchema: z.object({
36
+ tasks: z.array(TaskSummarySchema),
37
+ }),
38
+ },
39
+ async ({ status, mineOnly }, requestInfo, _meta) => {
40
+ const filterStatus = status ?? "in_progress";
41
+ let tasks = getAllTasks(filterStatus);
42
+
43
+ // Filter to only tasks assigned to this agent if mineOnly is true
44
+ if (mineOnly) {
45
+ if (!requestInfo.agentId) {
46
+ // No agent ID set, return empty list
47
+ tasks = [];
48
+ } else {
49
+ tasks = tasks.filter((t) => t.agentId === requestInfo.agentId);
50
+ }
51
+ }
52
+
53
+ const taskSummaries = tasks.map((t) => ({
54
+ id: t.id,
55
+ task: t.task,
56
+ status: t.status,
57
+ createdAt: t.createdAt,
58
+ lastUpdatedAt: t.lastUpdatedAt,
59
+ finishedAt: t.finishedAt,
60
+ progress: t.progress,
61
+ }));
62
+
63
+ const mineOnlyMsg = mineOnly ? " (mine only)" : "";
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `Found ${taskSummaries.length} task(s) with status '${filterStatus}'${mineOnlyMsg}.`,
69
+ },
70
+ ],
71
+ structuredContent: {
72
+ yourAgentId: requestInfo.agentId,
73
+ tasks: taskSummaries,
74
+ },
75
+ };
76
+ },
77
+ );
78
+ };