@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,102 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createAgent, getAllAgents, getDb } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentSchema } from "@/types";
6
+
7
+ export const registerJoinSwarmTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "join-swarm",
10
+ {
11
+ title: "Join the agent swarm",
12
+ description: "Tool for an agent to join the swarm of agents.",
13
+ inputSchema: z.object({
14
+ lead: z.boolean().default(false).describe("Whether this agent should be the lead."),
15
+ name: z.string().min(1).describe("The name of the agent joining the swarm."),
16
+ }),
17
+ outputSchema: z.object({
18
+ success: z.boolean(),
19
+ message: z.string(),
20
+ agent: AgentSchema.optional(),
21
+ }),
22
+ },
23
+ async ({ lead, name }, requestInfo, _meta) => {
24
+ // Check if agent ID is set
25
+ if (!requestInfo.agentId) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
31
+ },
32
+ ],
33
+ structuredContent: {
34
+ yourAgentId: requestInfo.agentId,
35
+ success: false,
36
+ message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
37
+ },
38
+ };
39
+ }
40
+
41
+ const agentId = requestInfo.agentId;
42
+
43
+ try {
44
+ const agentTx = getDb().transaction(() => {
45
+ const agents = getAllAgents();
46
+
47
+ const existingAgent = agents.find((agent) => agent.name === name);
48
+ const existingLead = agents.find((agent) => agent.isLead);
49
+
50
+ if (existingAgent) {
51
+ throw new Error(`Agent with name "${name}" already exists.`);
52
+ }
53
+
54
+ // If lead is true, demote e
55
+ if (lead && existingLead) {
56
+ throw new Error(
57
+ `Lead agent "${existingLead.name}" already exists. Only one lead agent is allowed.`,
58
+ );
59
+ }
60
+
61
+ return createAgent({
62
+ id: agentId,
63
+ name,
64
+ isLead: lead,
65
+ status: "idle",
66
+ });
67
+ });
68
+
69
+ const agent = agentTx();
70
+
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Successfully joined swarm as agent "${agent.name}" (ID: ${agent.id}).`,
76
+ },
77
+ ],
78
+ structuredContent: {
79
+ yourAgentId: requestInfo.agentId,
80
+ success: true,
81
+ message: `Successfully joined swarm as agent "${agent.name}" (ID: ${agent.id}).`,
82
+ agent,
83
+ },
84
+ };
85
+ } catch (error) {
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: `Failed to join swarm: ${(error as Error).message}`,
91
+ },
92
+ ],
93
+ structuredContent: {
94
+ yourAgentId: requestInfo.agentId,
95
+ success: false,
96
+ message: `Failed to join swarm: ${(error as Error).message}`,
97
+ },
98
+ };
99
+ }
100
+ },
101
+ );
102
+ };
@@ -0,0 +1,61 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+
6
+ export const registerMyAgentInfoTool = (server: McpServer) => {
7
+ createToolRegistrar(server)(
8
+ "my-agent-info",
9
+ {
10
+ title: "Get your agent info",
11
+ description: "Returns your agent ID based on the X-Agent-ID header.",
12
+ inputSchema: z.object({}),
13
+ outputSchema: z.object({
14
+ success: z.boolean(),
15
+ message: z.string(),
16
+ agentId: z.string().optional(),
17
+ }),
18
+ },
19
+ async (_input, requestInfo, _meta) => {
20
+ if (!requestInfo.agentId) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
26
+ },
27
+ ],
28
+ structuredContent: {
29
+ yourAgentId: requestInfo.agentId,
30
+ success: false,
31
+ message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
32
+ },
33
+ };
34
+ }
35
+
36
+ const maybeAgent = getAgentById(requestInfo.agentId);
37
+
38
+ let registeredMessage =
39
+ " You are not registered as an agent, use the 'join-swarm' tool to register, use a nice name related to the project you are working on if not provided by the user.";
40
+
41
+ if (maybeAgent) {
42
+ registeredMessage = ` You are registered as agent "${maybeAgent.name}".`;
43
+ }
44
+
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: `Your agent ID is: ${requestInfo.agentId}.${registeredMessage}`,
50
+ },
51
+ ],
52
+ structuredContent: {
53
+ yourAgentId: requestInfo.agentId,
54
+ yourAgentInfo: maybeAgent,
55
+ success: true,
56
+ message: `Your agent ID is: ${requestInfo.agentId}.${registeredMessage}`,
57
+ },
58
+ };
59
+ },
60
+ );
61
+ };
@@ -0,0 +1,109 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { addMinutes } from "date-fns";
3
+ import * as z from "zod";
4
+ import { getDb, getPendingTaskForAgent, startTask } from "@/be/db";
5
+ import { createToolRegistrar } from "@/tools/utils";
6
+ import { AgentTaskSchema } from "@/types";
7
+
8
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
9
+ const MAX_POLL_DURATION_MS = 1 * 60 * 1000;
10
+
11
+ export const registerPollTaskTool = (server: McpServer) => {
12
+ createToolRegistrar(server)(
13
+ "poll-task",
14
+ {
15
+ title: "Poll for a task",
16
+ description:
17
+ "Tool for an agent to poll for a new task assignment, to be used recursively until a task is assigned.",
18
+ inputSchema: z.object({}),
19
+ outputSchema: z.object({
20
+ success: z.boolean(),
21
+ message: z.string(),
22
+ task: AgentTaskSchema.optional(),
23
+ waitedForSeconds: z.number().describe("Seconds waited before receiving the task."),
24
+ }),
25
+ },
26
+ async (_input, requestInfo, meta) => {
27
+ // Check if agent ID is set
28
+ if (!requestInfo.agentId) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
34
+ },
35
+ ],
36
+ structuredContent: {
37
+ yourAgentId: requestInfo.agentId,
38
+ success: false,
39
+ message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
40
+ waitedForSeconds: 0,
41
+ },
42
+ };
43
+ }
44
+
45
+ const agentId = requestInfo.agentId;
46
+ const now = new Date();
47
+ const maxTime = addMinutes(now, MAX_POLL_DURATION_MS / 60000);
48
+
49
+ // Poll for pending tasks
50
+ while (new Date() < maxTime) {
51
+ // Fetch and update in a single transaction to avoid race conditions
52
+ const startedTask = getDb().transaction(() => {
53
+ const pendingTask = getPendingTaskForAgent(agentId);
54
+ if (!pendingTask) return null;
55
+ return startTask(pendingTask.id);
56
+ })();
57
+
58
+ if (startedTask) {
59
+ const waitedFor = Math.round((Date.now() - now.getTime()) / 1000);
60
+
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: `Task "${startedTask.id}" assigned and started.`,
66
+ },
67
+ ],
68
+ structuredContent: {
69
+ yourAgentId: requestInfo.agentId,
70
+ success: true,
71
+ message: `Task "${startedTask.id}" assigned and started.`,
72
+ task: startedTask,
73
+ waitedForSeconds: waitedFor,
74
+ },
75
+ };
76
+ }
77
+
78
+ await meta.sendNotification({
79
+ method: "notifications/message",
80
+ params: {
81
+ level: "info",
82
+ data: `Polling for task assignment...`,
83
+ },
84
+ });
85
+
86
+ // Wait for a short period before polling again
87
+ await new Promise((resolve) => setTimeout(resolve, DEFAULT_POLL_INTERVAL_MS));
88
+ }
89
+
90
+ const waitedForSeconds = Math.round((Date.now() - now.getTime()) / 1000);
91
+
92
+ // If no task was found within the time limit
93
+ return {
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: `No task assigned within the polling duration.`,
98
+ },
99
+ ],
100
+ structuredContent: {
101
+ yourAgentId: requestInfo.agentId,
102
+ success: false,
103
+ message: `No task assigned within the polling duration, please keep polling until a task is assigned.`,
104
+ waitedForSeconds,
105
+ },
106
+ };
107
+ },
108
+ );
109
+ };
@@ -0,0 +1,62 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createTask, getAgentById, getDb, updateAgentStatus } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentTaskSchema } from "@/types";
6
+
7
+ export const registerSendTaskTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "send-task",
10
+ {
11
+ title: "Send a task",
12
+ description: "Sends a task to a specific agent in the swarm.",
13
+ inputSchema: z.object({
14
+ agentId: z.uuid().describe("The ID of the agent to send the task to."),
15
+ task: z.string().min(1).describe("The task description to send."),
16
+ }),
17
+ outputSchema: z.object({
18
+ success: z.boolean(),
19
+ message: z.string(),
20
+ task: AgentTaskSchema.optional(),
21
+ }),
22
+ },
23
+ async ({ agentId, task }, requestInfo, _meta) => {
24
+ const txn = getDb().transaction(() => {
25
+ const agent = getAgentById(agentId);
26
+
27
+ if (!agent) {
28
+ return {
29
+ success: false,
30
+ message: `Agent with ID "${agentId}" not found.`,
31
+ };
32
+ }
33
+
34
+ if (agent.status !== "idle") {
35
+ return {
36
+ success: false,
37
+ message: `Agent "${agent.name}" is not idle (status: ${agent.status}). Cannot assign task.`,
38
+ };
39
+ }
40
+
41
+ const newTask = createTask(agentId, task);
42
+ updateAgentStatus(agentId, "busy");
43
+
44
+ return {
45
+ success: true,
46
+ message: `Task "${newTask.id}" sent to agent "${agent.name}".`,
47
+ task: newTask,
48
+ };
49
+ });
50
+
51
+ const result = txn();
52
+
53
+ return {
54
+ content: [{ type: "text", text: result.message }],
55
+ structuredContent: {
56
+ yourAgentId: requestInfo.agentId,
57
+ ...result,
58
+ },
59
+ };
60
+ },
61
+ );
62
+ };
@@ -0,0 +1,94 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import {
4
+ completeTask,
5
+ failTask,
6
+ getDb,
7
+ getTaskById,
8
+ updateAgentStatus,
9
+ updateTaskProgress,
10
+ } from "@/be/db";
11
+ import { createToolRegistrar } from "@/tools/utils";
12
+ import { AgentTaskSchema } from "@/types";
13
+
14
+ export const registerStoreProgressTool = (server: McpServer) => {
15
+ createToolRegistrar(server)(
16
+ "store-progress",
17
+ {
18
+ title: "Store task progress",
19
+ description:
20
+ "Stores the progress of a specific task. Can also mark task as completed or failed, which will set the agent back to idle.",
21
+ inputSchema: z.object({
22
+ taskId: z.uuid().describe("The ID of the task to update progress for."),
23
+ progress: z.string().optional().describe("The progress update to store."),
24
+ status: z
25
+ .enum(["completed", "failed"])
26
+ .optional()
27
+ .describe("Set to 'completed' or 'failed' to finish the task."),
28
+ output: z.string().optional().describe("The output of the task (used when completing)."),
29
+ failureReason: z
30
+ .string()
31
+ .optional()
32
+ .describe("The reason for failure (used when failing)."),
33
+ }),
34
+ outputSchema: z.object({
35
+ success: z.boolean(),
36
+ message: z.string(),
37
+ task: AgentTaskSchema.optional(),
38
+ }),
39
+ },
40
+ async ({ taskId, progress, status, output, failureReason }, requestInfo, _meta) => {
41
+ const txn = getDb().transaction(() => {
42
+ const existingTask = getTaskById(taskId);
43
+
44
+ if (!existingTask) {
45
+ return {
46
+ success: false,
47
+ message: `Task with ID "${taskId}" not found.`,
48
+ };
49
+ }
50
+
51
+ let updatedTask = existingTask;
52
+
53
+ // Update progress if provided
54
+ if (progress) {
55
+ const result = updateTaskProgress(taskId, progress);
56
+ if (result) updatedTask = result;
57
+ }
58
+
59
+ // Handle status change
60
+ if (status === "completed") {
61
+ const result = completeTask(taskId, output);
62
+ if (result) {
63
+ updatedTask = result;
64
+ updateAgentStatus(existingTask.agentId, "idle");
65
+ }
66
+ } else if (status === "failed") {
67
+ const result = failTask(taskId, failureReason ?? "Unknown failure");
68
+ if (result) {
69
+ updatedTask = result;
70
+ updateAgentStatus(existingTask.agentId, "idle");
71
+ }
72
+ }
73
+
74
+ return {
75
+ success: true,
76
+ message: status
77
+ ? `Task "${taskId}" marked as ${status}.`
78
+ : `Progress stored for task "${taskId}".`,
79
+ task: updatedTask,
80
+ };
81
+ });
82
+
83
+ const result = txn();
84
+
85
+ return {
86
+ content: [{ type: "text", text: result.message }],
87
+ structuredContent: {
88
+ yourAgentId: requestInfo.agentId,
89
+ ...result,
90
+ },
91
+ };
92
+ },
93
+ );
94
+ };
@@ -0,0 +1,115 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type {
3
+ AnySchema,
4
+ SchemaOutput,
5
+ ShapeOutput,
6
+ ZodRawShapeCompat,
7
+ } from "@modelcontextprotocol/sdk/server/zod-compat.js";
8
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
9
+ import type {
10
+ CallToolResult,
11
+ ServerNotification,
12
+ ServerRequest,
13
+ ToolAnnotations,
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+
16
+ type Meta = RequestHandlerExtra<ServerRequest, ServerNotification>;
17
+
18
+ export type RequestInfo = {
19
+ sessionId: string | undefined;
20
+ agentId: string | undefined;
21
+ };
22
+
23
+ export const getRequestInfo = (req: Meta): RequestInfo => {
24
+ const agentIdHeader = req.requestInfo?.headers?.["x-agent-id"];
25
+
26
+ let agentId: string | undefined;
27
+
28
+ if (Array.isArray(agentIdHeader)) {
29
+ agentId = agentIdHeader?.[0];
30
+ } else if (typeof agentIdHeader === "string") {
31
+ agentId = agentIdHeader;
32
+ }
33
+
34
+ return {
35
+ sessionId: req.sessionId || undefined,
36
+ agentId,
37
+ };
38
+ };
39
+
40
+ // Infer the input type from the schema
41
+ type InferInput<Args extends undefined | ZodRawShapeCompat | AnySchema> =
42
+ Args extends ZodRawShapeCompat
43
+ ? ShapeOutput<Args>
44
+ : Args extends AnySchema
45
+ ? SchemaOutput<Args>
46
+ : undefined;
47
+
48
+ // Callback type with requestInfo injected as second parameter
49
+ type ToolCallbackWithInfo<Args extends undefined | ZodRawShapeCompat | AnySchema = undefined> =
50
+ Args extends undefined
51
+ ? (requestInfo: RequestInfo, meta: Meta) => CallToolResult | Promise<CallToolResult>
52
+ : (
53
+ args: InferInput<Args>,
54
+ requestInfo: RequestInfo,
55
+ meta: Meta,
56
+ ) => CallToolResult | Promise<CallToolResult>;
57
+
58
+ type ToolConfig<
59
+ InputArgs extends undefined | ZodRawShapeCompat | AnySchema,
60
+ OutputArgs extends ZodRawShapeCompat | AnySchema,
61
+ > = {
62
+ title?: string;
63
+ description?: string;
64
+ inputSchema?: InputArgs;
65
+ outputSchema?: OutputArgs;
66
+ annotations?: ToolAnnotations;
67
+ _meta?: Record<string, unknown>;
68
+ };
69
+
70
+ /**
71
+ * Creates a tool registration helper that automatically extracts request info
72
+ * and passes it as the second parameter to the callback.
73
+ *
74
+ * @example
75
+ * const registerTool = createToolRegistrar(server);
76
+ *
77
+ * registerTool(
78
+ * "my-tool",
79
+ * { inputSchema: z.object({ name: z.string() }) },
80
+ * async ({ name }, requestInfo, meta) => {
81
+ * // requestInfo.sessionId and requestInfo.agentId are available
82
+ * return { content: [{ type: "text", text: `Hello ${name}` }] };
83
+ * }
84
+ * );
85
+ */
86
+ export const createToolRegistrar = (server: McpServer) => {
87
+ return <
88
+ OutputArgs extends ZodRawShapeCompat | AnySchema,
89
+ InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined,
90
+ >(
91
+ name: string,
92
+ config: ToolConfig<InputArgs, OutputArgs>,
93
+ cb: ToolCallbackWithInfo<InputArgs>,
94
+ ) => {
95
+ return server.registerTool(name, config, ((args: InferInput<InputArgs>, meta: Meta) => {
96
+ const requestInfo = getRequestInfo(meta);
97
+
98
+ // Handle zero-argument case
99
+ if (config.inputSchema === undefined) {
100
+ return (
101
+ cb as (requestInfo: RequestInfo, meta: Meta) => CallToolResult | Promise<CallToolResult>
102
+ )(requestInfo, meta);
103
+ }
104
+
105
+ // Handle with-arguments case
106
+ return (
107
+ cb as (
108
+ args: InferInput<InputArgs>,
109
+ requestInfo: RequestInfo,
110
+ meta: Meta,
111
+ ) => CallToolResult | Promise<CallToolResult>
112
+ )(args, requestInfo, meta);
113
+ }) as Parameters<typeof server.registerTool>[2]);
114
+ };
115
+ };
package/src/types.ts ADDED
@@ -0,0 +1,42 @@
1
+ import * as z from "zod";
2
+
3
+ export const AgentTaskStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]);
4
+
5
+ export const AgentTaskSchema = z.object({
6
+ id: z.uuid(),
7
+ agentId: z.uuid(),
8
+ task: z.string().min(1),
9
+ status: AgentTaskStatusSchema,
10
+
11
+ createdAt: z.iso.datetime().default(() => new Date().toISOString()),
12
+ lastUpdatedAt: z.iso.datetime().default(() => new Date().toISOString()),
13
+
14
+ finishedAt: z.iso.datetime().optional(),
15
+
16
+ failureReason: z.string().optional(),
17
+ output: z.string().optional(),
18
+ progress: z.string().optional(),
19
+ });
20
+
21
+ export const AgentStatusSchema = z.enum(["idle", "busy", "offline"]);
22
+
23
+ export const AgentSchema = z.object({
24
+ id: z.uuid(),
25
+ name: z.string().min(1),
26
+ isLead: z.boolean().default(false),
27
+ status: AgentStatusSchema,
28
+
29
+ createdAt: z.iso.datetime().default(() => new Date().toISOString()),
30
+ lastUpdatedAt: z.iso.datetime().default(() => new Date().toISOString()),
31
+ });
32
+
33
+ export const AgentWithTasksSchema = AgentSchema.extend({
34
+ tasks: z.array(AgentTaskSchema).default([]),
35
+ });
36
+
37
+ export type AgentTaskStatus = z.infer<typeof AgentTaskStatusSchema>;
38
+ export type AgentTask = z.infer<typeof AgentTaskSchema>;
39
+
40
+ export type AgentStatus = z.infer<typeof AgentStatusSchema>;
41
+ export type Agent = z.infer<typeof AgentSchema>;
42
+ export type AgentWithTasks = z.infer<typeof AgentWithTasksSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+
15
+ // Path aliases
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@/*": ["src/*"]
19
+ },
20
+ "verbatimModuleSyntax": true,
21
+ "noEmit": true,
22
+
23
+ // Best practices
24
+ "strict": true,
25
+ "skipLibCheck": true,
26
+ "noFallthroughCasesInSwitch": true,
27
+ "noUncheckedIndexedAccess": true,
28
+ "noImplicitOverride": true,
29
+
30
+ // Some stricter flags (disabled by default)
31
+ "noUnusedLocals": false,
32
+ "noUnusedParameters": false,
33
+ "noPropertyAccessFromIndexSignature": false
34
+ }
35
+ }