@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
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
- import { createTask, getAgentById, getDb } from "@/be/db";
3
+ import { createTaskExtended, getAgentById, getDb } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
  import { AgentTaskSchema } from "@/types";
6
6
 
@@ -9,10 +9,35 @@ export const registerSendTaskTool = (server: McpServer) => {
9
9
  "send-task",
10
10
  {
11
11
  title: "Send a task",
12
- description: "Sends a task to a specific agent in the swarm.",
12
+ description:
13
+ "Sends a task to a specific agent, creates an unassigned task for the pool, or offers a task for acceptance.",
13
14
  inputSchema: z.object({
14
- agentId: z.uuid().describe("The ID of the agent to send the task to."),
15
+ agentId: z
16
+ .uuid()
17
+ .optional()
18
+ .describe("The agent to assign/offer task to. Omit to create unassigned task for pool."),
15
19
  task: z.string().min(1).describe("The task description to send."),
20
+ offerMode: z
21
+ .boolean()
22
+ .default(false)
23
+ .describe("If true, offer the task instead of direct assign (agent must accept/reject)."),
24
+ taskType: z
25
+ .string()
26
+ .max(50)
27
+ .optional()
28
+ .describe("Task type (e.g., 'bug', 'feature', 'review')."),
29
+ tags: z
30
+ .array(z.string())
31
+ .optional()
32
+ .describe("Tags for filtering (e.g., ['urgent', 'frontend'])."),
33
+ priority: z
34
+ .number()
35
+ .int()
36
+ .min(0)
37
+ .max(100)
38
+ .optional()
39
+ .describe("Priority 0-100 (default: 50)."),
40
+ dependsOn: z.array(z.uuid()).optional().describe("Task IDs this task depends on."),
16
41
  }),
17
42
  outputSchema: z.object({
18
43
  success: z.boolean(),
@@ -20,7 +45,11 @@ export const registerSendTaskTool = (server: McpServer) => {
20
45
  task: AgentTaskSchema.optional(),
21
46
  }),
22
47
  },
23
- async ({ agentId, task }, requestInfo, _meta) => {
48
+ async (
49
+ { agentId, task, offerMode, taskType, tags, priority, dependsOn },
50
+ requestInfo,
51
+ _meta,
52
+ ) => {
24
53
  if (!requestInfo.agentId) {
25
54
  return {
26
55
  content: [
@@ -54,6 +83,23 @@ export const registerSendTaskTool = (server: McpServer) => {
54
83
  }
55
84
 
56
85
  const txn = getDb().transaction(() => {
86
+ // If no agentId, create an unassigned task for the pool
87
+ if (!agentId) {
88
+ const newTask = createTaskExtended(task, {
89
+ creatorAgentId: requestInfo.agentId,
90
+ taskType,
91
+ tags,
92
+ priority,
93
+ dependsOn,
94
+ });
95
+
96
+ return {
97
+ success: true,
98
+ message: `Created unassigned task "${newTask.id}" in the pool.`,
99
+ task: newTask,
100
+ };
101
+ }
102
+
57
103
  const agent = getAgentById(agentId);
58
104
 
59
105
  if (!agent) {
@@ -70,14 +116,41 @@ export const registerSendTaskTool = (server: McpServer) => {
70
116
  };
71
117
  }
72
118
 
73
- if (agent.status !== "idle") {
119
+ // For direct assignment (not offer), check if agent is idle
120
+ if (!offerMode && agent.status !== "idle") {
74
121
  return {
75
122
  success: false,
76
- message: `Agent "${agent.name}" is not idle (status: ${agent.status}). Cannot assign task.`,
123
+ message: `Agent "${agent.name}" is not idle (status: ${agent.status}). Cannot assign task directly. Use offerMode: true to offer the task instead.`,
124
+ };
125
+ }
126
+
127
+ if (offerMode) {
128
+ // Offer the task to the agent (they must accept/reject)
129
+ const newTask = createTaskExtended(task, {
130
+ offeredTo: agentId,
131
+ creatorAgentId: requestInfo.agentId,
132
+ taskType,
133
+ tags,
134
+ priority,
135
+ dependsOn,
136
+ });
137
+
138
+ return {
139
+ success: true,
140
+ message: `Task "${newTask.id}" offered to agent "${agent.name}". They must accept or reject it.`,
141
+ task: newTask,
77
142
  };
78
143
  }
79
144
 
80
- const newTask = createTask(agentId, task);
145
+ // Direct assignment
146
+ const newTask = createTaskExtended(task, {
147
+ agentId,
148
+ creatorAgentId: requestInfo.agentId,
149
+ taskType,
150
+ tags,
151
+ priority,
152
+ dependsOn,
153
+ });
81
154
 
82
155
  return {
83
156
  success: true,
@@ -87,17 +87,23 @@ export const registerStoreProgressTool = (server: McpServer) => {
87
87
  const result = completeTask(taskId, output);
88
88
  if (result) {
89
89
  updatedTask = result;
90
- updateAgentStatus(existingTask.agentId, "idle");
90
+ if (existingTask.agentId) {
91
+ updateAgentStatus(existingTask.agentId, "idle");
92
+ }
91
93
  }
92
94
  } else if (status === "failed") {
93
95
  const result = failTask(taskId, failureReason ?? "Unknown failure");
94
96
  if (result) {
95
97
  updatedTask = result;
96
- updateAgentStatus(existingTask.agentId, "idle");
98
+ if (existingTask.agentId) {
99
+ updateAgentStatus(existingTask.agentId, "idle");
100
+ }
97
101
  }
98
102
  } else {
99
103
  // Keep it busy if just updating progress
100
- updateAgentStatus(existingTask.agentId, "busy");
104
+ if (existingTask.agentId) {
105
+ updateAgentStatus(existingTask.agentId, "busy");
106
+ }
101
107
  }
102
108
 
103
109
  return {
@@ -0,0 +1,211 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import {
4
+ acceptTask,
5
+ claimTask,
6
+ createTaskExtended,
7
+ getDb,
8
+ getTaskById,
9
+ rejectTask,
10
+ releaseTask,
11
+ } from "@/be/db";
12
+ import { createToolRegistrar } from "@/tools/utils";
13
+ import { AgentTaskSchema } from "@/types";
14
+
15
+ const TaskActionSchema = z.enum(["create", "claim", "release", "accept", "reject"]);
16
+
17
+ export const registerTaskActionTool = (server: McpServer) => {
18
+ createToolRegistrar(server)(
19
+ "task-action",
20
+ {
21
+ title: "Task Pool Actions",
22
+ description:
23
+ "Perform task pool operations: create unassigned tasks, claim/release tasks from pool, accept/reject offered tasks.",
24
+ inputSchema: z.object({
25
+ action: TaskActionSchema.describe(
26
+ "The action to perform: 'create' creates an unassigned task, 'claim' takes a task from pool, 'release' returns task to pool, 'accept' accepts offered task, 'reject' declines offered task.",
27
+ ),
28
+ // For 'create' action:
29
+ task: z.string().min(1).optional().describe("Task description (required for 'create')."),
30
+ taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature')."),
31
+ tags: z
32
+ .array(z.string())
33
+ .optional()
34
+ .describe("Tags for filtering (e.g., ['urgent', 'frontend'])."),
35
+ priority: z
36
+ .number()
37
+ .int()
38
+ .min(0)
39
+ .max(100)
40
+ .optional()
41
+ .describe("Priority 0-100, default 50."),
42
+ dependsOn: z.array(z.uuid()).optional().describe("Task IDs this task depends on."),
43
+ // For claim/release/accept/reject actions:
44
+ taskId: z.uuid().optional().describe("Task ID (required for claim/release/accept/reject)."),
45
+ // For 'reject' action:
46
+ reason: z.string().optional().describe("Reason for rejection (optional for 'reject')."),
47
+ }),
48
+ outputSchema: z.object({
49
+ success: z.boolean(),
50
+ message: z.string(),
51
+ task: AgentTaskSchema.optional(),
52
+ }),
53
+ },
54
+ async (input, requestInfo, _meta) => {
55
+ const { action, task, taskType, tags, priority, dependsOn, taskId, reason } = input;
56
+
57
+ if (!requestInfo.agentId) {
58
+ return {
59
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
60
+ structuredContent: {
61
+ success: false,
62
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
63
+ },
64
+ };
65
+ }
66
+
67
+ const agentId = requestInfo.agentId;
68
+
69
+ const txn = getDb().transaction(() => {
70
+ switch (action) {
71
+ case "create": {
72
+ if (!task) {
73
+ return {
74
+ success: false,
75
+ message: "Task description is required for 'create' action.",
76
+ };
77
+ }
78
+ const newTask = createTaskExtended(task, {
79
+ creatorAgentId: agentId,
80
+ taskType,
81
+ tags,
82
+ priority,
83
+ dependsOn,
84
+ });
85
+ return {
86
+ success: true,
87
+ message: `Created unassigned task "${newTask.id}".`,
88
+ task: newTask,
89
+ };
90
+ }
91
+
92
+ case "claim": {
93
+ if (!taskId) {
94
+ return { success: false, message: "Task ID is required for 'claim' action." };
95
+ }
96
+ const existingTask = getTaskById(taskId);
97
+ if (!existingTask) {
98
+ return { success: false, message: `Task "${taskId}" not found.` };
99
+ }
100
+ if (existingTask.status !== "unassigned") {
101
+ return {
102
+ success: false,
103
+ message: `Task "${taskId}" is not unassigned (status: ${existingTask.status}).`,
104
+ };
105
+ }
106
+ const claimedTask = claimTask(taskId, agentId);
107
+ if (!claimedTask) {
108
+ return { success: false, message: `Failed to claim task "${taskId}".` };
109
+ }
110
+ return {
111
+ success: true,
112
+ message: `Claimed task "${taskId}".`,
113
+ task: claimedTask,
114
+ };
115
+ }
116
+
117
+ case "release": {
118
+ if (!taskId) {
119
+ return { success: false, message: "Task ID is required for 'release' action." };
120
+ }
121
+ const existingTask = getTaskById(taskId);
122
+ if (!existingTask) {
123
+ return { success: false, message: `Task "${taskId}" not found.` };
124
+ }
125
+ if (existingTask.agentId !== agentId) {
126
+ return { success: false, message: `Task "${taskId}" is not assigned to you.` };
127
+ }
128
+ if (existingTask.status !== "pending") {
129
+ return {
130
+ success: false,
131
+ message: `Cannot release task in status "${existingTask.status}". Only 'pending' tasks can be released.`,
132
+ };
133
+ }
134
+ const releasedTask = releaseTask(taskId);
135
+ if (!releasedTask) {
136
+ return { success: false, message: `Failed to release task "${taskId}".` };
137
+ }
138
+ return {
139
+ success: true,
140
+ message: `Released task "${taskId}" back to pool.`,
141
+ task: releasedTask,
142
+ };
143
+ }
144
+
145
+ case "accept": {
146
+ if (!taskId) {
147
+ return { success: false, message: "Task ID is required for 'accept' action." };
148
+ }
149
+ const existingTask = getTaskById(taskId);
150
+ if (!existingTask) {
151
+ return { success: false, message: `Task "${taskId}" not found.` };
152
+ }
153
+ if (existingTask.status !== "offered") {
154
+ return { success: false, message: `Task "${taskId}" is not offered.` };
155
+ }
156
+ if (existingTask.offeredTo !== agentId) {
157
+ return { success: false, message: `Task "${taskId}" was not offered to you.` };
158
+ }
159
+ const acceptedTask = acceptTask(taskId, agentId);
160
+ if (!acceptedTask) {
161
+ return { success: false, message: `Failed to accept task "${taskId}".` };
162
+ }
163
+ return {
164
+ success: true,
165
+ message: `Accepted task "${taskId}".`,
166
+ task: acceptedTask,
167
+ };
168
+ }
169
+
170
+ case "reject": {
171
+ if (!taskId) {
172
+ return { success: false, message: "Task ID is required for 'reject' action." };
173
+ }
174
+ const existingTask = getTaskById(taskId);
175
+ if (!existingTask) {
176
+ return { success: false, message: `Task "${taskId}" not found.` };
177
+ }
178
+ if (existingTask.status !== "offered") {
179
+ return { success: false, message: `Task "${taskId}" is not offered.` };
180
+ }
181
+ if (existingTask.offeredTo !== agentId) {
182
+ return { success: false, message: `Task "${taskId}" was not offered to you.` };
183
+ }
184
+ const rejectedTask = rejectTask(taskId, agentId, reason);
185
+ if (!rejectedTask) {
186
+ return { success: false, message: `Failed to reject task "${taskId}".` };
187
+ }
188
+ return {
189
+ success: true,
190
+ message: `Rejected task "${taskId}". Task returned to pool.`,
191
+ task: rejectedTask,
192
+ };
193
+ }
194
+
195
+ default:
196
+ return { success: false, message: `Unknown action: ${action}` };
197
+ }
198
+ });
199
+
200
+ const result = txn();
201
+
202
+ return {
203
+ content: [{ type: "text", text: result.message }],
204
+ structuredContent: {
205
+ yourAgentId: agentId,
206
+ ...result,
207
+ },
208
+ };
209
+ },
210
+ );
211
+ };
@@ -0,0 +1,110 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { deleteService, getServiceByAgentAndName, getServiceById } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+
6
+ export const registerUnregisterServiceTool = (server: McpServer) => {
7
+ createToolRegistrar(server)(
8
+ "unregister-service",
9
+ {
10
+ title: "Unregister Service",
11
+ description:
12
+ "Remove a service from the registry. Use this after stopping a PM2 process. You can only unregister your own services.",
13
+ inputSchema: z.object({
14
+ serviceId: z.uuid().optional().describe("Service ID to unregister."),
15
+ name: z
16
+ .string()
17
+ .optional()
18
+ .describe("Service name to unregister (alternative to serviceId)."),
19
+ }),
20
+ outputSchema: z.object({
21
+ success: z.boolean(),
22
+ message: z.string(),
23
+ }),
24
+ },
25
+ async ({ serviceId, name }, requestInfo, _meta) => {
26
+ if (!requestInfo.agentId) {
27
+ return {
28
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
29
+ structuredContent: {
30
+ success: false,
31
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
32
+ },
33
+ };
34
+ }
35
+
36
+ if (!serviceId && !name) {
37
+ return {
38
+ content: [{ type: "text", text: "Either serviceId or name is required." }],
39
+ structuredContent: {
40
+ yourAgentId: requestInfo.agentId,
41
+ success: false,
42
+ message: "Either serviceId or name is required.",
43
+ },
44
+ };
45
+ }
46
+
47
+ try {
48
+ // Find the service
49
+ let service = serviceId ? getServiceById(serviceId) : null;
50
+ if (!service && name) {
51
+ service = getServiceByAgentAndName(requestInfo.agentId, name);
52
+ }
53
+
54
+ if (!service) {
55
+ return {
56
+ content: [{ type: "text", text: "Service not found." }],
57
+ structuredContent: {
58
+ yourAgentId: requestInfo.agentId,
59
+ success: false,
60
+ message: "Service not found.",
61
+ },
62
+ };
63
+ }
64
+
65
+ // Check ownership
66
+ if (service.agentId !== requestInfo.agentId) {
67
+ return {
68
+ content: [{ type: "text", text: "You can only unregister your own services." }],
69
+ structuredContent: {
70
+ yourAgentId: requestInfo.agentId,
71
+ success: false,
72
+ message: "You can only unregister your own services.",
73
+ },
74
+ };
75
+ }
76
+
77
+ const deleted = deleteService(service.id);
78
+ if (!deleted) {
79
+ return {
80
+ content: [{ type: "text", text: "Failed to unregister service." }],
81
+ structuredContent: {
82
+ yourAgentId: requestInfo.agentId,
83
+ success: false,
84
+ message: "Failed to unregister service.",
85
+ },
86
+ };
87
+ }
88
+
89
+ return {
90
+ content: [{ type: "text", text: `Unregistered service "${service.name}".` }],
91
+ structuredContent: {
92
+ yourAgentId: requestInfo.agentId,
93
+ success: true,
94
+ message: `Unregistered service "${service.name}".`,
95
+ },
96
+ };
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : "Unknown error";
99
+ return {
100
+ content: [{ type: "text", text: `Failed to unregister service: ${message}` }],
101
+ structuredContent: {
102
+ yourAgentId: requestInfo.agentId,
103
+ success: false,
104
+ message: `Failed to unregister service: ${message}`,
105
+ },
106
+ };
107
+ }
108
+ },
109
+ );
110
+ };
@@ -0,0 +1,105 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { updateAgentProfile } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { AgentSchema } from "@/types";
6
+
7
+ export const registerUpdateProfileTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "update-profile",
10
+ {
11
+ title: "Update Profile",
12
+ description:
13
+ "Updates the calling agent's profile information (description, role, capabilities).",
14
+ inputSchema: z.object({
15
+ description: z.string().optional().describe("Agent description."),
16
+ role: z
17
+ .string()
18
+ .max(100)
19
+ .optional()
20
+ .describe("Agent role (free-form, e.g., 'frontend dev', 'code reviewer')."),
21
+ capabilities: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("List of capabilities (e.g., ['typescript', 'react', 'testing'])."),
25
+ }),
26
+ outputSchema: z.object({
27
+ success: z.boolean(),
28
+ message: z.string(),
29
+ agent: AgentSchema.optional(),
30
+ }),
31
+ },
32
+ async ({ description, role, capabilities }, requestInfo, _meta) => {
33
+ if (!requestInfo.agentId) {
34
+ return {
35
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
36
+ structuredContent: {
37
+ success: false,
38
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
39
+ },
40
+ };
41
+ }
42
+
43
+ // At least one field must be provided
44
+ if (description === undefined && role === undefined && capabilities === undefined) {
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: "At least one field (description, role, or capabilities) must be provided.",
50
+ },
51
+ ],
52
+ structuredContent: {
53
+ yourAgentId: requestInfo.agentId,
54
+ success: false,
55
+ message: "At least one field (description, role, or capabilities) must be provided.",
56
+ },
57
+ };
58
+ }
59
+
60
+ try {
61
+ const agent = updateAgentProfile(requestInfo.agentId, {
62
+ description,
63
+ role,
64
+ capabilities,
65
+ });
66
+
67
+ if (!agent) {
68
+ return {
69
+ content: [{ type: "text", text: "Agent not found." }],
70
+ structuredContent: {
71
+ yourAgentId: requestInfo.agentId,
72
+ success: false,
73
+ message: "Agent not found.",
74
+ },
75
+ };
76
+ }
77
+
78
+ const updatedFields: string[] = [];
79
+ if (description !== undefined) updatedFields.push("description");
80
+ if (role !== undefined) updatedFields.push("role");
81
+ if (capabilities !== undefined) updatedFields.push("capabilities");
82
+
83
+ return {
84
+ content: [{ type: "text", text: `Updated profile: ${updatedFields.join(", ")}.` }],
85
+ structuredContent: {
86
+ yourAgentId: requestInfo.agentId,
87
+ success: true,
88
+ message: `Updated profile: ${updatedFields.join(", ")}.`,
89
+ agent,
90
+ },
91
+ };
92
+ } catch (error) {
93
+ const message = error instanceof Error ? error.message : "Unknown error";
94
+ return {
95
+ content: [{ type: "text", text: `Failed to update profile: ${message}` }],
96
+ structuredContent: {
97
+ yourAgentId: requestInfo.agentId,
98
+ success: false,
99
+ message: `Failed to update profile: ${message}`,
100
+ },
101
+ };
102
+ }
103
+ },
104
+ );
105
+ };
@@ -0,0 +1,118 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getServiceByAgentAndName, getServiceById, updateServiceStatus } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { ServiceSchema, ServiceStatusSchema } from "@/types";
6
+
7
+ export const registerUpdateServiceStatusTool = (server: McpServer) => {
8
+ createToolRegistrar(server)(
9
+ "update-service-status",
10
+ {
11
+ title: "Update Service Status",
12
+ description:
13
+ "Update the health status of a registered service. Use this after a service becomes healthy or needs to be marked as stopped/unhealthy.",
14
+ inputSchema: z.object({
15
+ serviceId: z.uuid().optional().describe("Service ID to update."),
16
+ name: z.string().optional().describe("Service name to update (alternative to serviceId)."),
17
+ status: ServiceStatusSchema.describe(
18
+ "New status: 'starting', 'healthy', 'unhealthy', or 'stopped'.",
19
+ ),
20
+ }),
21
+ outputSchema: z.object({
22
+ success: z.boolean(),
23
+ message: z.string(),
24
+ service: ServiceSchema.optional(),
25
+ }),
26
+ },
27
+ async ({ serviceId, name, status }, requestInfo, _meta) => {
28
+ if (!requestInfo.agentId) {
29
+ return {
30
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
31
+ structuredContent: {
32
+ success: false,
33
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
34
+ },
35
+ };
36
+ }
37
+
38
+ if (!serviceId && !name) {
39
+ return {
40
+ content: [{ type: "text", text: "Either serviceId or name is required." }],
41
+ structuredContent: {
42
+ yourAgentId: requestInfo.agentId,
43
+ success: false,
44
+ message: "Either serviceId or name is required.",
45
+ },
46
+ };
47
+ }
48
+
49
+ try {
50
+ // Find the service
51
+ let service = serviceId ? getServiceById(serviceId) : null;
52
+ if (!service && name) {
53
+ service = getServiceByAgentAndName(requestInfo.agentId, name);
54
+ }
55
+
56
+ if (!service) {
57
+ return {
58
+ content: [{ type: "text", text: "Service not found." }],
59
+ structuredContent: {
60
+ yourAgentId: requestInfo.agentId,
61
+ success: false,
62
+ message: "Service not found.",
63
+ },
64
+ };
65
+ }
66
+
67
+ // Check ownership
68
+ if (service.agentId !== requestInfo.agentId) {
69
+ return {
70
+ content: [{ type: "text", text: "You can only update status of your own services." }],
71
+ structuredContent: {
72
+ yourAgentId: requestInfo.agentId,
73
+ success: false,
74
+ message: "You can only update status of your own services.",
75
+ },
76
+ };
77
+ }
78
+
79
+ const updated = updateServiceStatus(service.id, status);
80
+ if (!updated) {
81
+ return {
82
+ content: [{ type: "text", text: "Failed to update service status." }],
83
+ structuredContent: {
84
+ yourAgentId: requestInfo.agentId,
85
+ success: false,
86
+ message: "Failed to update service status.",
87
+ },
88
+ };
89
+ }
90
+
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Updated service "${service.name}" status to "${status}".`,
96
+ },
97
+ ],
98
+ structuredContent: {
99
+ yourAgentId: requestInfo.agentId,
100
+ success: true,
101
+ message: `Updated service "${service.name}" status to "${status}".`,
102
+ service: updated,
103
+ },
104
+ };
105
+ } catch (error) {
106
+ const message = error instanceof Error ? error.message : "Unknown error";
107
+ return {
108
+ content: [{ type: "text", text: `Failed to update service status: ${message}` }],
109
+ structuredContent: {
110
+ yourAgentId: requestInfo.agentId,
111
+ success: false,
112
+ message: `Failed to update service status: ${message}`,
113
+ },
114
+ };
115
+ }
116
+ },
117
+ );
118
+ };