@desplega.ai/agent-swarm 1.2.0 → 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.
- package/.claude/settings.local.json +20 -1
- package/.dockerignore +3 -0
- package/.env.docker.example +22 -1
- package/.env.example +17 -0
- package/.github/workflows/docker-publish.yml +92 -0
- package/CONTRIBUTING.md +270 -0
- package/DEPLOYMENT.md +391 -0
- package/Dockerfile.worker +29 -1
- package/FAQ.md +19 -0
- package/LICENSE +21 -0
- package/MCP.md +249 -0
- package/README.md +105 -185
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.png +0 -0
- package/deploy/docker-push.ts +30 -0
- package/docker-compose.example.yml +137 -0
- package/docker-entrypoint.sh +223 -7
- package/package.json +13 -4
- package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/codebase-analyzer.md +143 -0
- package/plugin/agents/codebase-locator.md +122 -0
- package/plugin/agents/codebase-pattern-finder.md +227 -0
- package/plugin/agents/web-search-researcher.md +109 -0
- package/plugin/commands/create-plan.md +415 -0
- package/plugin/commands/implement-plan.md +89 -0
- package/plugin/commands/research.md +200 -0
- package/plugin/commands/start-leader.md +101 -0
- package/plugin/commands/start-worker.md +56 -0
- package/plugin/commands/swarm-chat.md +78 -0
- package/plugin/commands/todos.md +66 -0
- package/plugin/commands/work-on-task.md +44 -0
- package/plugin/skills/.gitkeep +0 -0
- package/scripts/generate-mcp-docs.ts +415 -0
- package/slack-manifest.json +69 -0
- package/src/be/db.ts +1431 -25
- package/src/cli.tsx +135 -11
- package/src/commands/lead.ts +13 -0
- package/src/commands/runner.ts +255 -0
- package/src/commands/setup.tsx +5 -5
- package/src/commands/worker.ts +8 -220
- package/src/hooks/hook.ts +108 -14
- package/src/http.ts +361 -5
- package/src/prompts/base-prompt.ts +131 -0
- package/src/server.ts +56 -0
- package/src/slack/app.ts +73 -0
- package/src/slack/commands.ts +88 -0
- package/src/slack/handlers.ts +281 -0
- package/src/slack/index.ts +3 -0
- package/src/slack/responses.ts +175 -0
- package/src/slack/router.ts +170 -0
- package/src/slack/types.ts +20 -0
- package/src/slack/watcher.ts +119 -0
- package/src/tools/create-channel.ts +80 -0
- package/src/tools/get-tasks.ts +54 -21
- package/src/tools/join-swarm.ts +28 -4
- package/src/tools/list-channels.ts +37 -0
- package/src/tools/list-services.ts +110 -0
- package/src/tools/poll-task.ts +47 -3
- package/src/tools/post-message.ts +87 -0
- package/src/tools/read-messages.ts +192 -0
- package/src/tools/register-service.ts +118 -0
- package/src/tools/send-task.ts +80 -7
- package/src/tools/store-progress.ts +9 -3
- package/src/tools/task-action.ts +211 -0
- package/src/tools/unregister-service.ts +110 -0
- package/src/tools/update-profile.ts +105 -0
- package/src/tools/update-service-status.ts +118 -0
- package/src/types.ts +110 -3
- package/src/utils/pretty-print.ts +224 -0
- package/thoughts/shared/plans/.gitkeep +0 -0
- package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
- package/tsconfig.json +3 -1
- package/ui/bun.lock +692 -0
- package/ui/index.html +22 -0
- package/ui/package.json +32 -0
- package/ui/pnpm-lock.yaml +3034 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +43 -0
- package/ui/src/components/ActivityFeed.tsx +415 -0
- package/ui/src/components/AgentDetailPanel.tsx +534 -0
- package/ui/src/components/AgentsPanel.tsx +549 -0
- package/ui/src/components/ChatPanel.tsx +1820 -0
- package/ui/src/components/ConfigModal.tsx +232 -0
- package/ui/src/components/Dashboard.tsx +534 -0
- package/ui/src/components/Header.tsx +168 -0
- package/ui/src/components/ServicesPanel.tsx +612 -0
- package/ui/src/components/StatsBar.tsx +288 -0
- package/ui/src/components/StatusBadge.tsx +124 -0
- package/ui/src/components/TaskDetailPanel.tsx +807 -0
- package/ui/src/components/TasksPanel.tsx +575 -0
- package/ui/src/hooks/queries.ts +170 -0
- package/ui/src/index.css +235 -0
- package/ui/src/lib/api.ts +161 -0
- package/ui/src/lib/config.ts +35 -0
- package/ui/src/lib/theme.ts +214 -0
- package/ui/src/lib/utils.ts +48 -0
- package/ui/src/main.tsx +32 -0
- package/ui/src/types/api.ts +164 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tailwind.config.js +35 -0
- package/ui/tsconfig.json +31 -0
- package/ui/vite.config.ts +22 -0
- package/cc-plugin/README.md +0 -49
- package/cc-plugin/commands/setup-leader.md +0 -73
- package/cc-plugin/commands/start-worker.md +0 -64
- package/docker-compose.worker.yml +0 -35
- package/example-req-meta.json +0 -24
- /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
|
+
};
|
package/src/tools/poll-task.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
"
|
|
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
|
};
|
|
@@ -68,6 +107,7 @@ export const registerPollTaskTool = (server: McpServer) => {
|
|
|
68
107
|
while (new Date() < maxTime) {
|
|
69
108
|
// Fetch and update in a single transaction to avoid race conditions
|
|
70
109
|
const startedTask = getDb().transaction(() => {
|
|
110
|
+
// biome-ignore lint/style/noNonNullAssertion: agent existence verified above
|
|
71
111
|
const agentNow = getAgentById(agentId)!;
|
|
72
112
|
|
|
73
113
|
if (agentNow.status !== "busy") {
|
|
@@ -102,6 +142,8 @@ export const registerPollTaskTool = (server: McpServer) => {
|
|
|
102
142
|
success: true,
|
|
103
143
|
message: `Task "${startedTask.id}" assigned and started.`,
|
|
104
144
|
task: startedTask,
|
|
145
|
+
offeredTasks: [],
|
|
146
|
+
availableCount: getUnassignedTasksCount(),
|
|
105
147
|
waitedForSeconds: waitedFor,
|
|
106
148
|
},
|
|
107
149
|
};
|
|
@@ -126,13 +168,15 @@ export const registerPollTaskTool = (server: McpServer) => {
|
|
|
126
168
|
content: [
|
|
127
169
|
{
|
|
128
170
|
type: "text",
|
|
129
|
-
text: `No task assigned within the polling duration.`,
|
|
171
|
+
text: `No task assigned within the polling duration. ${availableCount} unassigned task(s) available in pool.`,
|
|
130
172
|
},
|
|
131
173
|
],
|
|
132
174
|
structuredContent: {
|
|
133
175
|
yourAgentId: requestInfo.agentId,
|
|
134
176
|
success: false,
|
|
135
177
|
message: `No task assigned within the polling duration, please keep polling until a task is assigned.`,
|
|
178
|
+
offeredTasks: [],
|
|
179
|
+
availableCount: getUnassignedTasksCount(),
|
|
136
180
|
waitedForSeconds,
|
|
137
181
|
},
|
|
138
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
|
+
};
|