@desplega.ai/agent-swarm 1.51.2 → 1.52.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.
- package/README.md +131 -0
- package/openapi.json +767 -4
- package/package.json +3 -1
- package/src/be/db.ts +669 -0
- package/src/be/migrations/019_skills.sql +65 -0
- package/src/be/migrations/020_approval_requests.sql +41 -0
- package/src/be/migrations/runner.ts +4 -4
- package/src/be/skill-parser.ts +70 -0
- package/src/be/skill-sync.ts +106 -0
- package/src/commands/runner.ts +299 -52
- package/src/http/agents.ts +29 -0
- package/src/http/approval-requests.ts +310 -0
- package/src/http/config.ts +3 -3
- package/src/http/index.ts +26 -2
- package/src/http/poll.ts +15 -0
- package/src/http/skills.ts +479 -0
- package/src/http/tasks.ts +94 -0
- package/src/linear/outbound.ts +12 -12
- package/src/prompts/base-prompt.ts +8 -0
- package/src/providers/claude-adapter.ts +19 -3
- package/src/scheduler/scheduler.ts +24 -1
- package/src/server.ts +29 -0
- package/src/slack/blocks.ts +1 -1
- package/src/tests/approval-requests.test.ts +948 -0
- package/src/tests/skill-parser.test.ts +178 -0
- package/src/tests/skill-sync.test.ts +171 -0
- package/src/tests/slack-blocks.test.ts +3 -2
- package/src/tests/structured-output.test.ts +1 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/tool-call-progress.test.ts +207 -0
- package/src/tests/tool-registrar-no-input.test.ts +114 -0
- package/src/tests/update-profile-auth.test.ts +1 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tools/request-human-input.ts +117 -0
- package/src/tools/skills/index.ts +11 -0
- package/src/tools/skills/skill-create.ts +105 -0
- package/src/tools/skills/skill-delete.ts +67 -0
- package/src/tools/skills/skill-get.ts +75 -0
- package/src/tools/skills/skill-install-remote.ts +152 -0
- package/src/tools/skills/skill-install.ts +101 -0
- package/src/tools/skills/skill-list.ts +77 -0
- package/src/tools/skills/skill-publish.ts +123 -0
- package/src/tools/skills/skill-search.ts +43 -0
- package/src/tools/skills/skill-sync-remote.ts +128 -0
- package/src/tools/skills/skill-uninstall.ts +60 -0
- package/src/tools/skills/skill-update.ts +128 -0
- package/src/tools/store-progress.ts +31 -0
- package/src/tools/templates.ts +28 -0
- package/src/tools/tool-config.ts +16 -0
- package/src/tools/utils.ts +9 -7
- package/src/types.ts +54 -0
- package/src/workflows/executors/human-in-the-loop.ts +273 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/recovery.ts +72 -0
- package/src/workflows/resume.ts +65 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { createToolRegistrar } from "../tools/utils";
|
|
5
|
+
|
|
6
|
+
describe("createToolRegistrar with no inputSchema", () => {
|
|
7
|
+
test("handler receives requestInfo from meta when no inputSchema is defined", async () => {
|
|
8
|
+
const server = new McpServer({ name: "test-no-input", version: "1.0.0" });
|
|
9
|
+
|
|
10
|
+
let receivedRequestInfo: unknown = null;
|
|
11
|
+
let receivedMeta: unknown = null;
|
|
12
|
+
|
|
13
|
+
createToolRegistrar(server)(
|
|
14
|
+
"test-no-input-tool",
|
|
15
|
+
{
|
|
16
|
+
title: "Test Tool",
|
|
17
|
+
description: "Tool with no inputSchema",
|
|
18
|
+
outputSchema: z.object({ ok: z.boolean() }),
|
|
19
|
+
},
|
|
20
|
+
async (requestInfo, meta) => {
|
|
21
|
+
receivedRequestInfo = requestInfo;
|
|
22
|
+
receivedMeta = meta;
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text" as const, text: "ok" }],
|
|
25
|
+
structuredContent: { ok: true },
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Access the internal registered tool handler directly
|
|
31
|
+
// The MCP SDK stores tools in _registeredTools and calls handler(extra) for no-inputSchema
|
|
32
|
+
const registeredTools = (server as Record<string, unknown>)._registeredTools as Record<
|
|
33
|
+
string,
|
|
34
|
+
{ handler: (extra: unknown) => Promise<unknown> }
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
const tool = registeredTools["test-no-input-tool"];
|
|
38
|
+
expect(tool).toBeDefined();
|
|
39
|
+
|
|
40
|
+
// Simulate how the MCP SDK calls the handler for tools without inputSchema:
|
|
41
|
+
// It calls handler(extra) with a single argument (the Meta/extra object)
|
|
42
|
+
const fakeExtra = {
|
|
43
|
+
sessionId: "test-session-123",
|
|
44
|
+
requestInfo: {
|
|
45
|
+
headers: {
|
|
46
|
+
"x-agent-id": "agent-abc",
|
|
47
|
+
"x-source-task-id": "task-xyz",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// This is the critical test: the handler should NOT throw
|
|
53
|
+
// Before the fix, this would throw "undefined is not an object (evaluating 'req.requestInfo')"
|
|
54
|
+
const result = await tool.handler(fakeExtra);
|
|
55
|
+
|
|
56
|
+
expect(receivedRequestInfo).toEqual({
|
|
57
|
+
sessionId: "test-session-123",
|
|
58
|
+
agentId: "agent-abc",
|
|
59
|
+
sourceTaskId: "task-xyz",
|
|
60
|
+
});
|
|
61
|
+
expect(receivedMeta).toBe(fakeExtra);
|
|
62
|
+
expect(result).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("handler with inputSchema still receives args correctly", async () => {
|
|
66
|
+
const server = new McpServer({ name: "test-with-input", version: "1.0.0" });
|
|
67
|
+
|
|
68
|
+
let receivedArgs: unknown = null;
|
|
69
|
+
let receivedRequestInfo: unknown = null;
|
|
70
|
+
|
|
71
|
+
createToolRegistrar(server)(
|
|
72
|
+
"test-with-input-tool",
|
|
73
|
+
{
|
|
74
|
+
title: "Test Tool With Input",
|
|
75
|
+
description: "Tool with inputSchema",
|
|
76
|
+
inputSchema: z.object({ name: z.string() }),
|
|
77
|
+
outputSchema: z.object({ greeting: z.string() }),
|
|
78
|
+
},
|
|
79
|
+
async (args, requestInfo, _meta) => {
|
|
80
|
+
receivedArgs = args;
|
|
81
|
+
receivedRequestInfo = requestInfo;
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text" as const, text: "ok" }],
|
|
84
|
+
structuredContent: { greeting: `Hello ${args.name}` },
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const registeredTools = (server as Record<string, unknown>)._registeredTools as Record<
|
|
90
|
+
string,
|
|
91
|
+
{ handler: (args: unknown, extra: unknown) => Promise<unknown> }
|
|
92
|
+
>;
|
|
93
|
+
|
|
94
|
+
const tool = registeredTools["test-with-input-tool"];
|
|
95
|
+
|
|
96
|
+
// MCP SDK calls handler(args, extra) when inputSchema is defined
|
|
97
|
+
const fakeExtra = {
|
|
98
|
+
sessionId: "test-session-456",
|
|
99
|
+
requestInfo: {
|
|
100
|
+
headers: { "x-agent-id": "agent-def" },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await tool.handler({ name: "World" }, fakeExtra);
|
|
105
|
+
|
|
106
|
+
expect(receivedArgs).toEqual({ name: "World" });
|
|
107
|
+
expect(receivedRequestInfo).toEqual({
|
|
108
|
+
sessionId: "test-session-456",
|
|
109
|
+
agentId: "agent-def",
|
|
110
|
+
sourceTaskId: undefined,
|
|
111
|
+
});
|
|
112
|
+
expect(result).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -28,6 +28,7 @@ async function callUpdateProfile(
|
|
|
28
28
|
callerAgentId: string | undefined,
|
|
29
29
|
args: Record<string, unknown>,
|
|
30
30
|
): Promise<{ structuredContent: StructuredContent }> {
|
|
31
|
+
// biome-ignore lint/complexity/noBannedTypes: accessing internal MCP SDK type for test
|
|
31
32
|
const tools = (server as unknown as { _registeredTools: Record<string, { handler: Function }> })
|
|
32
33
|
._registeredTools;
|
|
33
34
|
const handler = tools["update-profile"].handler;
|
|
@@ -690,7 +690,7 @@ describe("ValidateExecutor", () => {
|
|
|
690
690
|
// ─── Registry Wiring ─────────────────────────────────────────
|
|
691
691
|
|
|
692
692
|
describe("createExecutorRegistry", () => {
|
|
693
|
-
test("registers all
|
|
693
|
+
test("registers all 9 executors (7 instant + 2 async)", () => {
|
|
694
694
|
const registry = createExecutorRegistry(mockDeps);
|
|
695
695
|
const types = registry.types();
|
|
696
696
|
|
|
@@ -702,7 +702,8 @@ describe("createExecutorRegistry", () => {
|
|
|
702
702
|
expect(types).toContain("vcs");
|
|
703
703
|
expect(types).toContain("validate");
|
|
704
704
|
expect(types).toContain("agent-task");
|
|
705
|
-
expect(types).
|
|
705
|
+
expect(types).toContain("human-in-the-loop");
|
|
706
|
+
expect(types).toHaveLength(9);
|
|
706
707
|
});
|
|
707
708
|
|
|
708
709
|
test("instant executors have mode instant, async executors have mode async", () => {
|
|
@@ -720,6 +721,7 @@ describe("createExecutorRegistry", () => {
|
|
|
720
721
|
expect(registry.get(type).mode).toBe("instant");
|
|
721
722
|
}
|
|
722
723
|
expect(registry.get("agent-task").mode).toBe("async");
|
|
724
|
+
expect(registry.get("human-in-the-loop").mode).toBe("async");
|
|
723
725
|
});
|
|
724
726
|
|
|
725
727
|
test("get() retrieves the correct executor by type", () => {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createApprovalRequest, getAgentCurrentTask } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
+
|
|
6
|
+
const QuestionSchema = z.object({
|
|
7
|
+
id: z.string().describe("Unique ID for the question (used as key in responses)"),
|
|
8
|
+
type: z
|
|
9
|
+
.enum(["approval", "text", "single-select", "multi-select", "boolean"])
|
|
10
|
+
.describe("Question type"),
|
|
11
|
+
label: z.string().describe("The question text displayed to the user"),
|
|
12
|
+
required: z.boolean().optional().describe("Whether this question is required (default: true)"),
|
|
13
|
+
description: z.string().optional().describe("Optional help text"),
|
|
14
|
+
placeholder: z.string().optional().describe("Placeholder text (for text type)"),
|
|
15
|
+
multiline: z.boolean().optional().describe("Use textarea instead of input (for text type)"),
|
|
16
|
+
options: z
|
|
17
|
+
.array(
|
|
18
|
+
z.object({
|
|
19
|
+
value: z.string(),
|
|
20
|
+
label: z.string(),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Options (for single-select/multi-select types)"),
|
|
26
|
+
minSelections: z.number().int().min(0).optional().describe("Min selections (for multi-select)"),
|
|
27
|
+
maxSelections: z.number().int().min(1).optional().describe("Max selections (for multi-select)"),
|
|
28
|
+
defaultValue: z.boolean().optional().describe("Default value (for boolean type)"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const registerRequestHumanInputTool = (server: McpServer) => {
|
|
32
|
+
createToolRegistrar(server)(
|
|
33
|
+
"request-human-input",
|
|
34
|
+
{
|
|
35
|
+
title: "Request human input",
|
|
36
|
+
annotations: { destructiveHint: false },
|
|
37
|
+
description:
|
|
38
|
+
"Create an approval request that pauses until a human responds. " +
|
|
39
|
+
"Supports multiple question types: approval (yes/no), text, single-select, " +
|
|
40
|
+
"multi-select, and boolean. Returns the request ID and URL for the human to respond.",
|
|
41
|
+
inputSchema: z.object({
|
|
42
|
+
title: z.string().min(1).describe("Title of the approval request"),
|
|
43
|
+
questions: z.array(QuestionSchema).min(1).describe("Questions to ask the human"),
|
|
44
|
+
timeoutSeconds: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.min(1)
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Timeout in seconds (auto-rejects on timeout)"),
|
|
50
|
+
}),
|
|
51
|
+
outputSchema: z.object({
|
|
52
|
+
yourAgentId: z.string().uuid().optional(),
|
|
53
|
+
success: z.boolean(),
|
|
54
|
+
message: z.string(),
|
|
55
|
+
requestId: z.string().uuid().optional(),
|
|
56
|
+
url: z.string().optional(),
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
async ({ title, questions, timeoutSeconds }, requestInfo) => {
|
|
60
|
+
if (!requestInfo.agentId) {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
structuredContent: {
|
|
69
|
+
yourAgentId: requestInfo.agentId,
|
|
70
|
+
success: false,
|
|
71
|
+
message: 'Agent ID not found. The MCP client should define the "X-Agent-ID" header.',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Resolve sourceTaskId: prefer header, fall back to agent's current in-progress task.
|
|
77
|
+
// The X-Source-Task-Id header may be missing when the per-session MCP config wasn't
|
|
78
|
+
// created (e.g. .mcp.json not found, session resumed, or lead agent on a non-task trigger).
|
|
79
|
+
let sourceTaskId = requestInfo.sourceTaskId;
|
|
80
|
+
if (!sourceTaskId && requestInfo.agentId) {
|
|
81
|
+
const currentTask = getAgentCurrentTask(requestInfo.agentId);
|
|
82
|
+
if (currentTask) {
|
|
83
|
+
sourceTaskId = currentTask.id;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const id = crypto.randomUUID();
|
|
88
|
+
const request = createApprovalRequest({
|
|
89
|
+
id,
|
|
90
|
+
title,
|
|
91
|
+
questions,
|
|
92
|
+
approvers: { policy: "any" },
|
|
93
|
+
sourceTaskId: sourceTaskId ?? undefined,
|
|
94
|
+
timeoutSeconds,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const appUrl = process.env.APP_URL || "http://localhost:5274";
|
|
98
|
+
const url = `${appUrl}/approval-requests/${request.id}`;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `Created approval request "${id}". Human can respond at: ${url}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
structuredContent: {
|
|
108
|
+
yourAgentId: requestInfo.agentId,
|
|
109
|
+
success: true,
|
|
110
|
+
message: `Approval request created. Respond at: ${url}`,
|
|
111
|
+
requestId: request.id,
|
|
112
|
+
url,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { registerSkillCreateTool } from "./skill-create";
|
|
2
|
+
export { registerSkillDeleteTool } from "./skill-delete";
|
|
3
|
+
export { registerSkillGetTool } from "./skill-get";
|
|
4
|
+
export { registerSkillInstallTool } from "./skill-install";
|
|
5
|
+
export { registerSkillInstallRemoteTool } from "./skill-install-remote";
|
|
6
|
+
export { registerSkillListTool } from "./skill-list";
|
|
7
|
+
export { registerSkillPublishTool } from "./skill-publish";
|
|
8
|
+
export { registerSkillSearchTool } from "./skill-search";
|
|
9
|
+
export { registerSkillSyncRemoteTool } from "./skill-sync-remote";
|
|
10
|
+
export { registerSkillUninstallTool } from "./skill-uninstall";
|
|
11
|
+
export { registerSkillUpdateTool } from "./skill-update";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createSkill, getAgentById, installSkill } from "@/be/db";
|
|
4
|
+
import { parseSkillContent } from "@/be/skill-parser";
|
|
5
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
6
|
+
|
|
7
|
+
export const registerSkillCreateTool = (server: McpServer) => {
|
|
8
|
+
createToolRegistrar(server)(
|
|
9
|
+
"skill-create",
|
|
10
|
+
{
|
|
11
|
+
title: "Create Skill",
|
|
12
|
+
annotations: { destructiveHint: false },
|
|
13
|
+
description:
|
|
14
|
+
"Create a personal skill from SKILL.md content. Parses frontmatter for name, description, and metadata.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
content: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe("Full SKILL.md content (YAML frontmatter + markdown body)"),
|
|
20
|
+
scope: z
|
|
21
|
+
.enum(["agent", "swarm"])
|
|
22
|
+
.default("agent")
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Scope: agent (personal) or swarm (shared). Default: agent"),
|
|
25
|
+
}),
|
|
26
|
+
outputSchema: z.object({
|
|
27
|
+
yourAgentId: z.string().uuid().optional(),
|
|
28
|
+
success: z.boolean(),
|
|
29
|
+
message: z.string(),
|
|
30
|
+
skill: z.any().optional(),
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
async (args, requestInfo, _meta) => {
|
|
34
|
+
if (!requestInfo.agentId) {
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: "Agent ID not found." }],
|
|
37
|
+
structuredContent: { success: false, message: "Agent ID not found." },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const parsed = parseSkillContent(args.content);
|
|
43
|
+
|
|
44
|
+
// If swarm scope requested, only leads can create directly
|
|
45
|
+
if (args.scope === "swarm") {
|
|
46
|
+
const agent = getAgentById(requestInfo.agentId);
|
|
47
|
+
if (!agent?.isLead) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: 'Only lead agents can create swarm-scope skills directly. Use "skill-publish" to request approval.',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
structuredContent: {
|
|
56
|
+
yourAgentId: requestInfo.agentId,
|
|
57
|
+
success: false,
|
|
58
|
+
message: "Only lead agents can create swarm-scope skills directly.",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const skill = createSkill({
|
|
65
|
+
name: parsed.name,
|
|
66
|
+
description: parsed.description,
|
|
67
|
+
content: args.content,
|
|
68
|
+
type: "personal",
|
|
69
|
+
scope: args.scope ?? "agent",
|
|
70
|
+
ownerAgentId: requestInfo.agentId,
|
|
71
|
+
allowedTools: parsed.allowedTools,
|
|
72
|
+
model: parsed.model,
|
|
73
|
+
effort: parsed.effort,
|
|
74
|
+
context: parsed.context,
|
|
75
|
+
agent: parsed.agent,
|
|
76
|
+
disableModelInvocation: parsed.disableModelInvocation,
|
|
77
|
+
userInvocable: parsed.userInvocable,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Auto-install for the creating agent
|
|
81
|
+
installSkill(requestInfo.agentId, skill.id);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `Created skill "${skill.name}" (${skill.id})` }],
|
|
85
|
+
structuredContent: {
|
|
86
|
+
yourAgentId: requestInfo.agentId,
|
|
87
|
+
success: true,
|
|
88
|
+
message: `Created and installed skill "${skill.name}".`,
|
|
89
|
+
skill,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Failed to create skill: ${message}` }],
|
|
96
|
+
structuredContent: {
|
|
97
|
+
yourAgentId: requestInfo.agentId,
|
|
98
|
+
success: false,
|
|
99
|
+
message: `Failed: ${message}`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { deleteSkill, getAgentById, getSkillById } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
+
|
|
6
|
+
export const registerSkillDeleteTool = (server: McpServer) => {
|
|
7
|
+
createToolRegistrar(server)(
|
|
8
|
+
"skill-delete",
|
|
9
|
+
{
|
|
10
|
+
title: "Delete Skill",
|
|
11
|
+
annotations: { destructiveHint: true },
|
|
12
|
+
description: "Delete a skill. Only the owning agent or lead can delete.",
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
skillId: z.string().describe("ID of the skill to delete"),
|
|
15
|
+
}),
|
|
16
|
+
outputSchema: z.object({
|
|
17
|
+
yourAgentId: z.string().uuid().optional(),
|
|
18
|
+
success: z.boolean(),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
async (args, requestInfo, _meta) => {
|
|
23
|
+
if (!requestInfo.agentId) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: "Agent ID not found." }],
|
|
26
|
+
structuredContent: { success: false, message: "Agent ID not found." },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const existing = getSkillById(args.skillId);
|
|
31
|
+
if (!existing) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: "Skill not found." }],
|
|
34
|
+
structuredContent: {
|
|
35
|
+
yourAgentId: requestInfo.agentId,
|
|
36
|
+
success: false,
|
|
37
|
+
message: "Skill not found.",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const agent = getAgentById(requestInfo.agentId);
|
|
43
|
+
if (existing.ownerAgentId !== requestInfo.agentId && !agent?.isLead) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: "Only the owning agent or lead can delete this skill." }],
|
|
46
|
+
structuredContent: {
|
|
47
|
+
yourAgentId: requestInfo.agentId,
|
|
48
|
+
success: false,
|
|
49
|
+
message: "Permission denied.",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const deleted = deleteSkill(args.skillId);
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{ type: "text", text: deleted ? `Deleted skill "${existing.name}".` : "Delete failed." },
|
|
58
|
+
],
|
|
59
|
+
structuredContent: {
|
|
60
|
+
yourAgentId: requestInfo.agentId,
|
|
61
|
+
success: deleted,
|
|
62
|
+
message: deleted ? `Deleted skill "${existing.name}".` : "Delete failed.",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { getSkillById, getSkillByName } from "@/be/db";
|
|
4
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
+
|
|
6
|
+
export const registerSkillGetTool = (server: McpServer) => {
|
|
7
|
+
createToolRegistrar(server)(
|
|
8
|
+
"skill-get",
|
|
9
|
+
{
|
|
10
|
+
title: "Get Skill",
|
|
11
|
+
annotations: { destructiveHint: false },
|
|
12
|
+
description:
|
|
13
|
+
"Get full skill content by ID or name. Name resolution checks agent scope first, then swarm, then global.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
skillId: z.string().optional().describe("Skill ID"),
|
|
16
|
+
name: z.string().optional().describe("Skill name (resolved with precedence)"),
|
|
17
|
+
}),
|
|
18
|
+
outputSchema: z.object({
|
|
19
|
+
yourAgentId: z.string().uuid().optional(),
|
|
20
|
+
success: z.boolean(),
|
|
21
|
+
message: z.string(),
|
|
22
|
+
skill: z.any().optional(),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
async (args, requestInfo, _meta) => {
|
|
26
|
+
if (!args.skillId && !args.name) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: "Provide skillId or name." }],
|
|
29
|
+
structuredContent: {
|
|
30
|
+
yourAgentId: requestInfo.agentId,
|
|
31
|
+
success: false,
|
|
32
|
+
message: "Provide skillId or name.",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let skill = null;
|
|
38
|
+
|
|
39
|
+
if (args.skillId) {
|
|
40
|
+
skill = getSkillById(args.skillId);
|
|
41
|
+
} else if (args.name && requestInfo.agentId) {
|
|
42
|
+
// Precedence: agent (personal) → swarm → global
|
|
43
|
+
skill =
|
|
44
|
+
getSkillByName(args.name, "agent", requestInfo.agentId) ||
|
|
45
|
+
getSkillByName(args.name, "swarm") ||
|
|
46
|
+
getSkillByName(args.name, "global");
|
|
47
|
+
} else if (args.name) {
|
|
48
|
+
skill = getSkillByName(args.name, "swarm") || getSkillByName(args.name, "global");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!skill) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: "Skill not found." }],
|
|
54
|
+
structuredContent: {
|
|
55
|
+
yourAgentId: requestInfo.agentId,
|
|
56
|
+
success: false,
|
|
57
|
+
message: "Skill not found.",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{ type: "text", text: `Skill "${skill.name}" (${skill.id}):\n\n${skill.content}` },
|
|
65
|
+
],
|
|
66
|
+
structuredContent: {
|
|
67
|
+
yourAgentId: requestInfo.agentId,
|
|
68
|
+
success: true,
|
|
69
|
+
message: `Found skill "${skill.name}".`,
|
|
70
|
+
skill,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createSkill, getAgentById } from "@/be/db";
|
|
4
|
+
import { parseSkillContent } from "@/be/skill-parser";
|
|
5
|
+
import { createToolRegistrar } from "@/tools/utils";
|
|
6
|
+
|
|
7
|
+
export const registerSkillInstallRemoteTool = (server: McpServer) => {
|
|
8
|
+
createToolRegistrar(server)(
|
|
9
|
+
"skill-install-remote",
|
|
10
|
+
{
|
|
11
|
+
title: "Install Remote Skill",
|
|
12
|
+
annotations: { destructiveHint: false },
|
|
13
|
+
description:
|
|
14
|
+
"Fetch and install a remote skill from a GitHub repository. Fetches SKILL.md via GitHub raw content API.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
sourceRepo: z.string().describe('GitHub repo (e.g. "vercel-labs/skills")'),
|
|
17
|
+
sourcePath: z.string().optional().describe('Path within repo (e.g. "skills/nextjs")'),
|
|
18
|
+
scope: z
|
|
19
|
+
.enum(["global", "swarm"])
|
|
20
|
+
.default("global")
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Scope for the installed skill"),
|
|
23
|
+
isComplex: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.default(false)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("If true, registers for npx install (metadata only)"),
|
|
28
|
+
}),
|
|
29
|
+
outputSchema: z.object({
|
|
30
|
+
yourAgentId: z.string().uuid().optional(),
|
|
31
|
+
success: z.boolean(),
|
|
32
|
+
message: z.string(),
|
|
33
|
+
skill: z.any().optional(),
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
async (args, requestInfo, _meta) => {
|
|
37
|
+
if (!requestInfo.agentId) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: "Agent ID not found." }],
|
|
40
|
+
structuredContent: { success: false, message: "Agent ID not found." },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Only leads can install global/swarm remote skills
|
|
45
|
+
const agent = getAgentById(requestInfo.agentId);
|
|
46
|
+
if (!agent?.isLead) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: "Only lead agents can install remote skills." }],
|
|
49
|
+
structuredContent: {
|
|
50
|
+
yourAgentId: requestInfo.agentId,
|
|
51
|
+
success: false,
|
|
52
|
+
message: "Only lead agents can install remote skills.",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const branch = "main";
|
|
59
|
+
const filePath = args.sourcePath ? `${args.sourcePath}/SKILL.md` : "SKILL.md";
|
|
60
|
+
const rawUrl = `https://raw.githubusercontent.com/${args.sourceRepo}/${branch}/${filePath}`;
|
|
61
|
+
|
|
62
|
+
let content = "";
|
|
63
|
+
let sourceHash: string | null = null;
|
|
64
|
+
|
|
65
|
+
if (!args.isComplex) {
|
|
66
|
+
// Fetch SKILL.md content
|
|
67
|
+
const response = await fetch(rawUrl);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `Failed to fetch SKILL.md from ${rawUrl}: ${response.status}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
structuredContent: {
|
|
77
|
+
yourAgentId: requestInfo.agentId,
|
|
78
|
+
success: false,
|
|
79
|
+
message: `Failed to fetch: HTTP ${response.status}`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
content = await response.text();
|
|
84
|
+
sourceHash = new Bun.CryptoHasher("sha256").update(content).digest("hex");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let name: string;
|
|
88
|
+
let description: string;
|
|
89
|
+
let parsedMeta: Partial<ReturnType<typeof parseSkillContent>> = {};
|
|
90
|
+
|
|
91
|
+
if (content) {
|
|
92
|
+
const parsed = parseSkillContent(content);
|
|
93
|
+
name = parsed.name;
|
|
94
|
+
description = parsed.description;
|
|
95
|
+
parsedMeta = parsed;
|
|
96
|
+
} else {
|
|
97
|
+
// Complex skill — use repo/path as name
|
|
98
|
+
name = args.sourcePath
|
|
99
|
+
? args.sourcePath.split("/").pop() || args.sourceRepo
|
|
100
|
+
: args.sourceRepo.split("/").pop() || args.sourceRepo;
|
|
101
|
+
description = `Complex skill from ${args.sourceRepo}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const skill = createSkill({
|
|
105
|
+
name,
|
|
106
|
+
description,
|
|
107
|
+
content,
|
|
108
|
+
type: "remote",
|
|
109
|
+
scope: args.scope ?? "global",
|
|
110
|
+
sourceUrl: rawUrl,
|
|
111
|
+
sourceRepo: args.sourceRepo,
|
|
112
|
+
sourcePath: args.sourcePath,
|
|
113
|
+
sourceBranch: branch,
|
|
114
|
+
sourceHash: sourceHash ?? undefined,
|
|
115
|
+
isComplex: args.isComplex ?? false,
|
|
116
|
+
allowedTools: parsedMeta.allowedTools,
|
|
117
|
+
model: parsedMeta.model,
|
|
118
|
+
effort: parsedMeta.effort,
|
|
119
|
+
context: parsedMeta.context,
|
|
120
|
+
agent: parsedMeta.agent,
|
|
121
|
+
disableModelInvocation: parsedMeta.disableModelInvocation,
|
|
122
|
+
userInvocable: parsedMeta.userInvocable,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Installed remote skill "${skill.name}" from ${args.sourceRepo}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
structuredContent: {
|
|
133
|
+
yourAgentId: requestInfo.agentId,
|
|
134
|
+
success: true,
|
|
135
|
+
message: `Installed remote skill "${skill.name}".`,
|
|
136
|
+
skill,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: `Failed: ${message}` }],
|
|
143
|
+
structuredContent: {
|
|
144
|
+
yourAgentId: requestInfo.agentId,
|
|
145
|
+
success: false,
|
|
146
|
+
message: `Failed: ${message}`,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
};
|