@desplega.ai/agent-swarm 1.2.1 → 1.9.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.
Files changed (119) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.env.docker.example +22 -1
  3. package/.env.example +17 -0
  4. package/.github/workflows/docker-publish.yml +92 -0
  5. package/CONTRIBUTING.md +270 -0
  6. package/DEPLOYMENT.md +391 -0
  7. package/Dockerfile.worker +29 -1
  8. package/FAQ.md +19 -0
  9. package/LICENSE +21 -0
  10. package/MCP.md +249 -0
  11. package/README.md +103 -207
  12. package/assets/agent-swarm-logo-orange.png +0 -0
  13. package/assets/agent-swarm-logo.png +0 -0
  14. package/docker-compose.example.yml +137 -0
  15. package/docker-entrypoint.sh +223 -7
  16. package/package.json +8 -3
  17. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  18. package/plugin/README.md +1 -0
  19. package/plugin/agents/.gitkeep +0 -0
  20. package/plugin/agents/codebase-analyzer.md +143 -0
  21. package/plugin/agents/codebase-locator.md +122 -0
  22. package/plugin/agents/codebase-pattern-finder.md +227 -0
  23. package/plugin/agents/web-search-researcher.md +109 -0
  24. package/plugin/commands/create-plan.md +415 -0
  25. package/plugin/commands/implement-plan.md +89 -0
  26. package/plugin/commands/research.md +200 -0
  27. package/plugin/commands/start-leader.md +101 -0
  28. package/plugin/commands/start-worker.md +56 -0
  29. package/plugin/commands/swarm-chat.md +78 -0
  30. package/plugin/commands/todos.md +66 -0
  31. package/plugin/commands/work-on-task.md +44 -0
  32. package/plugin/skills/.gitkeep +0 -0
  33. package/scripts/generate-mcp-docs.ts +415 -0
  34. package/slack-manifest.json +69 -0
  35. package/src/be/db.ts +1431 -25
  36. package/src/cli.tsx +135 -11
  37. package/src/commands/lead.ts +13 -0
  38. package/src/commands/runner.ts +255 -0
  39. package/src/commands/worker.ts +8 -220
  40. package/src/hooks/hook.ts +102 -14
  41. package/src/http.ts +361 -5
  42. package/src/prompts/base-prompt.ts +131 -0
  43. package/src/server.ts +56 -0
  44. package/src/slack/app.ts +73 -0
  45. package/src/slack/commands.ts +88 -0
  46. package/src/slack/handlers.ts +281 -0
  47. package/src/slack/index.ts +3 -0
  48. package/src/slack/responses.ts +175 -0
  49. package/src/slack/router.ts +170 -0
  50. package/src/slack/types.ts +20 -0
  51. package/src/slack/watcher.ts +119 -0
  52. package/src/tools/create-channel.ts +80 -0
  53. package/src/tools/get-tasks.ts +54 -21
  54. package/src/tools/join-swarm.ts +28 -4
  55. package/src/tools/list-channels.ts +37 -0
  56. package/src/tools/list-services.ts +110 -0
  57. package/src/tools/poll-task.ts +46 -3
  58. package/src/tools/post-message.ts +87 -0
  59. package/src/tools/read-messages.ts +192 -0
  60. package/src/tools/register-service.ts +118 -0
  61. package/src/tools/send-task.ts +80 -7
  62. package/src/tools/store-progress.ts +9 -3
  63. package/src/tools/task-action.ts +211 -0
  64. package/src/tools/unregister-service.ts +110 -0
  65. package/src/tools/update-profile.ts +105 -0
  66. package/src/tools/update-service-status.ts +118 -0
  67. package/src/types.ts +110 -3
  68. package/src/utils/pretty-print.ts +224 -0
  69. package/thoughts/shared/plans/.gitkeep +0 -0
  70. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  71. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  72. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  73. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  74. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  75. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  76. package/thoughts/shared/research/.gitkeep +0 -0
  77. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  78. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  79. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  80. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  81. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  82. package/tsconfig.json +3 -1
  83. package/ui/bun.lock +692 -0
  84. package/ui/index.html +22 -0
  85. package/ui/package.json +32 -0
  86. package/ui/pnpm-lock.yaml +3034 -0
  87. package/ui/postcss.config.js +6 -0
  88. package/ui/public/logo.png +0 -0
  89. package/ui/src/App.tsx +43 -0
  90. package/ui/src/components/ActivityFeed.tsx +415 -0
  91. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  92. package/ui/src/components/AgentsPanel.tsx +549 -0
  93. package/ui/src/components/ChatPanel.tsx +1820 -0
  94. package/ui/src/components/ConfigModal.tsx +232 -0
  95. package/ui/src/components/Dashboard.tsx +534 -0
  96. package/ui/src/components/Header.tsx +168 -0
  97. package/ui/src/components/ServicesPanel.tsx +612 -0
  98. package/ui/src/components/StatsBar.tsx +288 -0
  99. package/ui/src/components/StatusBadge.tsx +124 -0
  100. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  101. package/ui/src/components/TasksPanel.tsx +575 -0
  102. package/ui/src/hooks/queries.ts +170 -0
  103. package/ui/src/index.css +235 -0
  104. package/ui/src/lib/api.ts +161 -0
  105. package/ui/src/lib/config.ts +35 -0
  106. package/ui/src/lib/theme.ts +214 -0
  107. package/ui/src/lib/utils.ts +48 -0
  108. package/ui/src/main.tsx +32 -0
  109. package/ui/src/types/api.ts +164 -0
  110. package/ui/src/vite-env.d.ts +1 -0
  111. package/ui/tailwind.config.js +35 -0
  112. package/ui/tsconfig.json +31 -0
  113. package/ui/vite.config.ts +22 -0
  114. package/cc-plugin/README.md +0 -49
  115. package/cc-plugin/commands/setup-leader.md +0 -73
  116. package/cc-plugin/commands/start-worker.md +0 -64
  117. package/docker-compose.worker.yml +0 -35
  118. package/example-req-meta.json +0 -24
  119. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -0,0 +1,110 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, getAllServices } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ServiceSchema, ServiceStatusSchema } from "@/types";
6
+
7
+ export const registerListServicesTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "list-services",
10
+ {
11
+ title: "List Services",
12
+ description:
13
+ "Query services registered by agents in the swarm. Use this to discover services exposed by other agents.",
14
+ inputSchema: z.object({
15
+ agentId: z.uuid().optional().describe("Filter by specific agent ID."),
16
+ name: z.string().optional().describe("Filter by service name (partial match)."),
17
+ status: ServiceStatusSchema.optional().describe("Filter by health status."),
18
+ includeOwn: z
19
+ .boolean()
20
+ .default(true)
21
+ .optional()
22
+ .describe("Include services registered by calling agent (default: true)."),
23
+ }),
24
+ outputSchema: z.object({
25
+ success: z.boolean(),
26
+ message: z.string(),
27
+ services: z.array(
28
+ ServiceSchema.extend({
29
+ agentName: z.string().optional(),
30
+ }),
31
+ ),
32
+ count: z.number(),
33
+ }),
34
+ },
35
+ async ({ agentId, name, status, includeOwn }, requestInfo, _meta) => {
36
+ if (!requestInfo.agentId) {
37
+ return {
38
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
39
+ structuredContent: {
40
+ success: false,
41
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
42
+ services: [],
43
+ count: 0,
44
+ },
45
+ };
46
+ }
47
+
48
+ try {
49
+ let services = getAllServices({
50
+ agentId,
51
+ name,
52
+ status,
53
+ });
54
+
55
+ // Filter out own services if requested
56
+ if (includeOwn === false) {
57
+ services = services.filter((s) => s.agentId !== requestInfo.agentId);
58
+ }
59
+
60
+ // Denormalize agent names
61
+ const servicesWithAgentNames = services.map((service) => {
62
+ const agent = getAgentById(service.agentId);
63
+ return {
64
+ ...service,
65
+ agentName: agent?.name,
66
+ };
67
+ });
68
+
69
+ const count = servicesWithAgentNames.length;
70
+ const statusSummary =
71
+ count === 0 ? "No services found." : `Found ${count} service${count === 1 ? "" : "s"}.`;
72
+
73
+ // Format for text output
74
+ const serviceList = servicesWithAgentNames
75
+ .map(
76
+ (s) => `- ${s.name} (${s.status}) by ${s.agentName ?? "unknown"}: ${s.url ?? "no URL"}`,
77
+ )
78
+ .join("\n");
79
+
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: count === 0 ? statusSummary : `${statusSummary}\n\n${serviceList}`,
85
+ },
86
+ ],
87
+ structuredContent: {
88
+ yourAgentId: requestInfo.agentId,
89
+ success: true,
90
+ message: statusSummary,
91
+ services: servicesWithAgentNames,
92
+ count,
93
+ },
94
+ };
95
+ } catch (error) {
96
+ const message = error instanceof Error ? error.message : "Unknown error";
97
+ return {
98
+ content: [{ type: "text", text: `Failed to list services: ${message}` }],
99
+ structuredContent: {
100
+ yourAgentId: requestInfo.agentId,
101
+ success: false,
102
+ message: `Failed to list services: ${message}`,
103
+ services: [],
104
+ count: 0,
105
+ },
106
+ };
107
+ }
108
+ },
109
+ );
110
+ };
@@ -1,7 +1,15 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { addMinutes } from "date-fns";
3
3
  import * as z from "zod";
4
- import { getAgentById, getDb, getPendingTaskForAgent, startTask, updateAgentStatus } from "@/be/db";
4
+ import {
5
+ getAgentById,
6
+ getDb,
7
+ getOfferedTasksForAgent,
8
+ getPendingTaskForAgent,
9
+ getUnassignedTasksCount,
10
+ startTask,
11
+ updateAgentStatus,
12
+ } from "@/be/db";
5
13
  import { createToolRegistrar } from "@/tools/utils";
6
14
  import { AgentTaskSchema } from "@/types";
7
15
 
@@ -14,12 +22,16 @@ export const registerPollTaskTool = (server: McpServer) => {
14
22
  {
15
23
  title: "Poll for a task",
16
24
  description:
17
- "Tool for an agent to poll for a new task assignment, to be used recursively until a task is assigned.",
25
+ "Poll for a new task assignment. Returns immediately if there are offered tasks awaiting accept/reject. Also returns count of unassigned tasks in the pool.",
18
26
  inputSchema: z.object({}),
19
27
  outputSchema: z.object({
20
28
  success: z.boolean(),
21
29
  message: z.string(),
22
30
  task: AgentTaskSchema.optional(),
31
+ offeredTasks: z
32
+ .array(AgentTaskSchema)
33
+ .describe("Tasks offered to you awaiting accept/reject."),
34
+ availableCount: z.number().describe("Count of unassigned tasks in the pool."),
23
35
  waitedForSeconds: z.number().describe("Seconds waited before receiving the task."),
24
36
  }),
25
37
  },
@@ -37,6 +49,8 @@ export const registerPollTaskTool = (server: McpServer) => {
37
49
  yourAgentId: requestInfo.agentId,
38
50
  success: false,
39
51
  message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
52
+ offeredTasks: [],
53
+ availableCount: 0,
40
54
  waitedForSeconds: 0,
41
55
  },
42
56
  };
@@ -59,6 +73,31 @@ export const registerPollTaskTool = (server: McpServer) => {
59
73
  yourAgentId: requestInfo.agentId,
60
74
  success: false,
61
75
  message: `Agent with ID "${agentId}" not found in the swarm.`,
76
+ offeredTasks: [],
77
+ availableCount: 0,
78
+ waitedForSeconds: 0,
79
+ },
80
+ };
81
+ }
82
+
83
+ // Check for offered tasks first - these need immediate attention
84
+ const offeredTasks = getOfferedTasksForAgent(agentId);
85
+ const availableCount = getUnassignedTasksCount();
86
+
87
+ if (offeredTasks.length > 0) {
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: `You have ${offeredTasks.length} task(s) offered to you awaiting accept/reject. Use task-action with action='accept' or 'reject'.`,
93
+ },
94
+ ],
95
+ structuredContent: {
96
+ yourAgentId: requestInfo.agentId,
97
+ success: true,
98
+ message: `You have ${offeredTasks.length} task(s) offered to you awaiting accept/reject.`,
99
+ offeredTasks,
100
+ availableCount,
62
101
  waitedForSeconds: 0,
63
102
  },
64
103
  };
@@ -103,6 +142,8 @@ export const registerPollTaskTool = (server: McpServer) => {
103
142
  success: true,
104
143
  message: `Task "${startedTask.id}" assigned and started.`,
105
144
  task: startedTask,
145
+ offeredTasks: [],
146
+ availableCount: getUnassignedTasksCount(),
106
147
  waitedForSeconds: waitedFor,
107
148
  },
108
149
  };
@@ -127,13 +168,15 @@ export const registerPollTaskTool = (server: McpServer) => {
127
168
  content: [
128
169
  {
129
170
  type: "text",
130
- text: `No task assigned within the polling duration.`,
171
+ text: `No task assigned within the polling duration. ${availableCount} unassigned task(s) available in pool.`,
131
172
  },
132
173
  ],
133
174
  structuredContent: {
134
175
  yourAgentId: requestInfo.agentId,
135
176
  success: false,
136
177
  message: `No task assigned within the polling duration, please keep polling until a task is assigned.`,
178
+ offeredTasks: [],
179
+ availableCount: getUnassignedTasksCount(),
137
180
  waitedForSeconds,
138
181
  },
139
182
  };
@@ -0,0 +1,87 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getChannelById, getChannelByName, postMessage, updateReadState } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ChannelMessageSchema } from "@/types";
6
+
7
+ export const registerPostMessageTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "post-message",
10
+ {
11
+ title: "Post Message",
12
+ description: "Posts a message to a channel for cross-agent communication.",
13
+ inputSchema: z.object({
14
+ channel: z.string().default("general").describe("Channel name (default: 'general')."),
15
+ content: z.string().min(1).max(4000).describe("Message content."),
16
+ replyTo: z.uuid().optional().describe("Message ID to reply to (for threading)."),
17
+ mentions: z
18
+ .array(z.uuid())
19
+ .optional()
20
+ .describe("Agent IDs to @mention (they'll see it in unread)."),
21
+ }),
22
+ outputSchema: z.object({
23
+ success: z.boolean(),
24
+ message: z.string(),
25
+ posted: ChannelMessageSchema.optional(),
26
+ }),
27
+ },
28
+ async ({ channel, content, replyTo, mentions }, requestInfo, _meta) => {
29
+ if (!requestInfo.agentId) {
30
+ return {
31
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
32
+ structuredContent: {
33
+ success: false,
34
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
35
+ },
36
+ };
37
+ }
38
+
39
+ // Find channel by name or ID
40
+ let targetChannel = getChannelByName(channel);
41
+ if (!targetChannel) {
42
+ targetChannel = getChannelById(channel);
43
+ }
44
+
45
+ if (!targetChannel) {
46
+ return {
47
+ content: [{ type: "text", text: `Channel "${channel}" not found.` }],
48
+ structuredContent: {
49
+ yourAgentId: requestInfo.agentId,
50
+ success: false,
51
+ message: `Channel "${channel}" not found.`,
52
+ },
53
+ };
54
+ }
55
+
56
+ try {
57
+ const posted = postMessage(targetChannel.id, requestInfo.agentId, content, {
58
+ replyToId: replyTo,
59
+ mentions,
60
+ });
61
+
62
+ // Auto-mark channel as read after posting (so you don't see your own message as unread)
63
+ updateReadState(requestInfo.agentId, targetChannel.id);
64
+
65
+ return {
66
+ content: [{ type: "text", text: `Posted message to #${targetChannel.name}.` }],
67
+ structuredContent: {
68
+ yourAgentId: requestInfo.agentId,
69
+ success: true,
70
+ message: `Posted message to #${targetChannel.name}.`,
71
+ posted,
72
+ },
73
+ };
74
+ } catch (error) {
75
+ const message = error instanceof Error ? error.message : "Unknown error";
76
+ return {
77
+ content: [{ type: "text", text: `Failed to post message: ${message}` }],
78
+ structuredContent: {
79
+ yourAgentId: requestInfo.agentId,
80
+ success: false,
81
+ message: `Failed to post message: ${message}`,
82
+ },
83
+ };
84
+ }
85
+ },
86
+ );
87
+ };
@@ -0,0 +1,192 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import {
4
+ getAllChannels,
5
+ getChannelById,
6
+ getChannelByName,
7
+ getChannelMessages,
8
+ getMentionsForAgent,
9
+ getUnreadMessages,
10
+ updateReadState,
11
+ } from "@/be/db";
12
+ import { createToolRegistrar } from "@/tools/utils";
13
+ import type { ChannelMessage } from "@/types";
14
+ import { ChannelMessageSchema } from "@/types";
15
+
16
+ export const registerReadMessagesTool = (server: McpServer) => {
17
+ createToolRegistrar(server)(
18
+ "read-messages",
19
+ {
20
+ title: "Read Messages",
21
+ description:
22
+ "Reads messages from a channel. If no channel is specified, returns unread messages from ALL channels. Supports filtering by unread, mentions, and time range. Automatically marks messages as read.",
23
+ inputSchema: z.object({
24
+ channel: z
25
+ .string()
26
+ .optional()
27
+ .describe("Channel name or ID. If omitted, returns unread messages from all channels."),
28
+ limit: z
29
+ .number()
30
+ .int()
31
+ .min(1)
32
+ .default(20)
33
+ .describe("Max messages to return per channel (default: 20)."),
34
+ since: z.iso.datetime().optional().describe("Only messages after this ISO timestamp."),
35
+ unreadOnly: z.boolean().default(false).describe("Only return unread messages."),
36
+ mentionsOnly: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe("Only return messages that @mention you."),
40
+ markAsRead: z
41
+ .boolean()
42
+ .default(true)
43
+ .describe("Update your read position after fetching (default: true)."),
44
+ }),
45
+ outputSchema: z.object({
46
+ success: z.boolean(),
47
+ message: z.string(),
48
+ channelName: z.string().optional(),
49
+ messages: z.array(ChannelMessageSchema),
50
+ unreadCount: z.number().optional(),
51
+ totalUnreadCount: z.number().optional(),
52
+ }),
53
+ },
54
+ async ({ channel, limit, since, unreadOnly, mentionsOnly, markAsRead }, requestInfo, _meta) => {
55
+ if (!requestInfo.agentId) {
56
+ return {
57
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
58
+ structuredContent: {
59
+ success: false,
60
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
61
+ messages: [],
62
+ },
63
+ };
64
+ }
65
+
66
+ try {
67
+ // If no channel specified, get unread messages from all channels
68
+ if (!channel) {
69
+ const allChannels = getAllChannels();
70
+ let allMessages: ReturnType<typeof getUnreadMessages> = [];
71
+ let totalUnreadCount = 0;
72
+
73
+ for (const ch of allChannels) {
74
+ const unreadMessages = getUnreadMessages(requestInfo.agentId, ch.id);
75
+ totalUnreadCount += unreadMessages.length;
76
+
77
+ // Add channel name to messages for context
78
+ const messagesWithChannel = unreadMessages.slice(-limit).map((msg) => ({
79
+ ...msg,
80
+ agentName: msg.agentName ? `${msg.agentName} in #${ch.name}` : `#${ch.name}`,
81
+ }));
82
+ allMessages = allMessages.concat(messagesWithChannel);
83
+
84
+ // Update read state if requested
85
+ if (markAsRead && unreadMessages.length > 0) {
86
+ updateReadState(requestInfo.agentId, ch.id);
87
+ }
88
+ }
89
+
90
+ // Sort by createdAt and limit
91
+ allMessages.sort(
92
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
93
+ );
94
+
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: `Found ${allMessages.length} unread message(s) across ${allChannels.length} channel(s).`,
100
+ },
101
+ ],
102
+ structuredContent: {
103
+ yourAgentId: requestInfo.agentId,
104
+ success: true,
105
+ message: `Found ${allMessages.length} unread message(s) across all channels.`,
106
+ messages: allMessages,
107
+ totalUnreadCount,
108
+ },
109
+ };
110
+ }
111
+
112
+ // Find channel by name or ID
113
+ let targetChannel = getChannelByName(channel);
114
+ if (!targetChannel) {
115
+ targetChannel = getChannelById(channel);
116
+ }
117
+
118
+ if (!targetChannel) {
119
+ return {
120
+ content: [{ type: "text", text: `Channel "${channel}" not found.` }],
121
+ structuredContent: {
122
+ yourAgentId: requestInfo.agentId,
123
+ success: false,
124
+ message: `Channel "${channel}" not found.`,
125
+ messages: [],
126
+ },
127
+ };
128
+ }
129
+
130
+ let messages: ChannelMessage[] = [];
131
+
132
+ if (mentionsOnly) {
133
+ // Get messages that mention this agent
134
+ messages = getMentionsForAgent(requestInfo.agentId, {
135
+ unreadOnly,
136
+ channelId: targetChannel.id,
137
+ });
138
+ } else if (unreadOnly) {
139
+ // Get unread messages only
140
+ messages = getUnreadMessages(requestInfo.agentId, targetChannel.id);
141
+ } else {
142
+ // Get regular messages with filters
143
+ messages = getChannelMessages(targetChannel.id, {
144
+ limit,
145
+ since,
146
+ });
147
+ }
148
+
149
+ // Apply limit if not already applied (unreadOnly and mentionsOnly don't limit)
150
+ if ((unreadOnly || mentionsOnly) && messages.length > limit) {
151
+ messages = messages.slice(-limit); // Keep most recent
152
+ }
153
+
154
+ // Update read state if requested
155
+ if (markAsRead && messages.length > 0) {
156
+ updateReadState(requestInfo.agentId, targetChannel.id);
157
+ }
158
+
159
+ // Get unread count for context
160
+ const allUnread = getUnreadMessages(requestInfo.agentId, targetChannel.id);
161
+
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: `Found ${messages.length} message(s) in #${targetChannel.name}${unreadOnly ? " (unread)" : ""}${mentionsOnly ? " (mentions)" : ""}.`,
167
+ },
168
+ ],
169
+ structuredContent: {
170
+ yourAgentId: requestInfo.agentId,
171
+ success: true,
172
+ message: `Found ${messages.length} message(s) in #${targetChannel.name}.`,
173
+ channelName: targetChannel.name,
174
+ messages,
175
+ unreadCount: allUnread.length,
176
+ },
177
+ };
178
+ } catch (error) {
179
+ const message = error instanceof Error ? error.message : "Unknown error";
180
+ return {
181
+ content: [{ type: "text", text: `Failed to read messages: ${message}` }],
182
+ structuredContent: {
183
+ yourAgentId: requestInfo.agentId,
184
+ success: false,
185
+ message: `Failed to read messages: ${message}`,
186
+ messages: [],
187
+ },
188
+ };
189
+ }
190
+ },
191
+ );
192
+ };
@@ -0,0 +1,118 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, upsertService } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ServiceSchema } from "@/types";
6
+
7
+ const SWARM_URL = process.env.SWARM_URL ?? "localhost";
8
+
9
+ export const registerRegisterServiceTool = (server: McpServer) => {
10
+ createToolRegistrar(server)(
11
+ "register-service",
12
+ {
13
+ title: "Register Service",
14
+ description:
15
+ "Register a background service (e.g., PM2 process) for discovery by other agents. The service URL is automatically derived from your agent ID (https://{AGENT_ID}.{SWARM_URL}). Each agent can only run one service on port 3000.",
16
+ inputSchema: z.object({
17
+ script: z.string().min(1).describe("Path to the script to run (required for PM2 restart)."),
18
+ description: z.string().optional().describe("What this service does."),
19
+ healthCheckPath: z
20
+ .string()
21
+ .optional()
22
+ .describe("Health check endpoint path (default: /health)."),
23
+ cwd: z.string().optional().describe("Working directory for the script."),
24
+ interpreter: z
25
+ .string()
26
+ .optional()
27
+ .describe(
28
+ "Interpreter to use (e.g., 'node', 'bun'). Auto-detected from extension if not set.",
29
+ ),
30
+ args: z.array(z.string()).optional().describe("Command line arguments for the script."),
31
+ env: z
32
+ .record(z.string(), z.string())
33
+ .optional()
34
+ .describe("Environment variables for the process."),
35
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata."),
36
+ }),
37
+ outputSchema: z.object({
38
+ success: z.boolean(),
39
+ message: z.string(),
40
+ service: ServiceSchema.optional(),
41
+ }),
42
+ },
43
+ async (
44
+ { script, description, healthCheckPath, cwd, interpreter, args, env, metadata },
45
+ requestInfo,
46
+ _meta,
47
+ ) => {
48
+ if (!requestInfo.agentId) {
49
+ return {
50
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
51
+ structuredContent: {
52
+ success: false,
53
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
54
+ },
55
+ };
56
+ }
57
+
58
+ try {
59
+ // Look up the agent to get its name
60
+ const agent = getAgentById(requestInfo.agentId);
61
+ if (!agent) {
62
+ return {
63
+ content: [{ type: "text", text: "Agent not found. Join the swarm first." }],
64
+ structuredContent: {
65
+ yourAgentId: requestInfo.agentId,
66
+ success: false,
67
+ message: "Agent not found. Join the swarm first.",
68
+ },
69
+ };
70
+ }
71
+
72
+ // Service name uses agent ID (stable, URL-safe) for subdomain
73
+ const serviceName = agent.id;
74
+ const servicePort = 3000; // Fixed port - only one service per worker
75
+ const url = `https://${serviceName}.${SWARM_URL}`;
76
+
77
+ // Upsert: create or update if exists
78
+ const service = upsertService(requestInfo.agentId, serviceName, {
79
+ script,
80
+ port: servicePort,
81
+ description,
82
+ url,
83
+ healthCheckPath: healthCheckPath ?? "/health",
84
+ cwd,
85
+ interpreter,
86
+ args,
87
+ env,
88
+ metadata,
89
+ });
90
+
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Registered service "${serviceName}" at ${url}. Status: ${service.status}. Use update-service-status to mark as healthy.`,
96
+ },
97
+ ],
98
+ structuredContent: {
99
+ yourAgentId: requestInfo.agentId,
100
+ success: true,
101
+ message: `Registered service "${serviceName}" at ${url}.`,
102
+ service,
103
+ },
104
+ };
105
+ } catch (error) {
106
+ const message = error instanceof Error ? error.message : "Unknown error";
107
+ return {
108
+ content: [{ type: "text", text: `Failed to register service: ${message}` }],
109
+ structuredContent: {
110
+ yourAgentId: requestInfo.agentId,
111
+ success: false,
112
+ message: `Failed to register service: ${message}`,
113
+ },
114
+ };
115
+ }
116
+ },
117
+ );
118
+ };