@desplega.ai/agent-swarm 1.52.1 → 1.53.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/openapi.json +1517 -488
- package/package.json +5 -2
- package/src/be/db.ts +530 -0
- package/src/be/events.ts +322 -0
- package/src/be/migrations/021_events.sql +24 -0
- package/src/be/migrations/022_context_usage.sql +34 -0
- package/src/be/migrations/023_mcp_servers.sql +44 -0
- package/src/commands/runner.ts +348 -1
- package/src/http/context.ts +118 -0
- package/src/http/events.ts +188 -0
- package/src/http/index.ts +6 -0
- package/src/http/mcp-servers.ts +364 -0
- package/src/http/tasks.ts +33 -0
- package/src/linear/outbound.ts +8 -1
- package/src/linear/sync.ts +3 -0
- package/src/oauth/ensure-token.ts +50 -0
- package/src/prompts/base-prompt.ts +7 -0
- package/src/providers/claude-adapter.ts +156 -15
- package/src/providers/pi-mono-adapter.ts +68 -0
- package/src/providers/pi-mono-extension.ts +56 -2
- package/src/providers/pi-mono-mcp-client.ts +10 -1
- package/src/providers/types.ts +14 -1
- package/src/server.ts +19 -0
- package/src/tests/context-window.test.ts +66 -0
- package/src/tests/ensure-token.test.ts +170 -0
- package/src/tests/events-db.test.ts +314 -0
- package/src/tests/events-http.test.ts +267 -0
- package/src/tests/prompt-template-remaining.test.ts +5 -5
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/vcs-tracking.test.ts +176 -0
- package/src/tests/workflow-executors.test.ts +8 -1
- package/src/tools/mcp-servers/index.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +138 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +72 -0
- package/src/tools/mcp-servers/mcp-server-get.ts +80 -0
- package/src/tools/mcp-servers/mcp-server-install.ts +110 -0
- package/src/tools/mcp-servers/mcp-server-list.ts +67 -0
- package/src/tools/mcp-servers/mcp-server-uninstall.ts +71 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +120 -0
- package/src/tools/tool-config.ts +9 -0
- package/src/types.ts +153 -0
- package/src/utils/context-window.ts +41 -0
- package/src/workflows/executors/base.ts +9 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createEvent,
|
|
5
|
+
createEventsBatch,
|
|
6
|
+
getEventCountsFiltered,
|
|
7
|
+
getEventsFiltered,
|
|
8
|
+
} from "../be/events";
|
|
9
|
+
import {
|
|
10
|
+
EventCategorySchema,
|
|
11
|
+
EventNameSchema,
|
|
12
|
+
EventSourceSchema,
|
|
13
|
+
EventStatusSchema,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import { route } from "./route-def";
|
|
16
|
+
import { json, jsonError } from "./utils";
|
|
17
|
+
|
|
18
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const eventBodySchema = z.object({
|
|
21
|
+
category: EventCategorySchema,
|
|
22
|
+
event: EventNameSchema,
|
|
23
|
+
status: EventStatusSchema.optional(),
|
|
24
|
+
source: EventSourceSchema,
|
|
25
|
+
agentId: z.string().optional(),
|
|
26
|
+
taskId: z.string().optional(),
|
|
27
|
+
sessionId: z.string().optional(),
|
|
28
|
+
parentEventId: z.string().optional(),
|
|
29
|
+
numericValue: z.number().optional(),
|
|
30
|
+
durationMs: z.number().int().optional(),
|
|
31
|
+
data: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const createEventRoute = route({
|
|
35
|
+
method: "post",
|
|
36
|
+
path: "/api/events",
|
|
37
|
+
pattern: ["api", "events"],
|
|
38
|
+
summary: "Store a single event",
|
|
39
|
+
tags: ["Events"],
|
|
40
|
+
body: eventBodySchema,
|
|
41
|
+
responses: {
|
|
42
|
+
201: { description: "Event stored" },
|
|
43
|
+
400: { description: "Validation error" },
|
|
44
|
+
},
|
|
45
|
+
auth: { apiKey: true },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const createEventsBatchRoute = route({
|
|
49
|
+
method: "post",
|
|
50
|
+
path: "/api/events/batch",
|
|
51
|
+
pattern: ["api", "events", "batch"],
|
|
52
|
+
summary: "Store multiple events in a batch",
|
|
53
|
+
tags: ["Events"],
|
|
54
|
+
body: z.object({
|
|
55
|
+
events: z.array(eventBodySchema).min(1).max(500),
|
|
56
|
+
}),
|
|
57
|
+
responses: {
|
|
58
|
+
201: { description: "Events stored" },
|
|
59
|
+
400: { description: "Validation error" },
|
|
60
|
+
},
|
|
61
|
+
auth: { apiKey: true },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const getEventsRoute = route({
|
|
65
|
+
method: "get",
|
|
66
|
+
path: "/api/events",
|
|
67
|
+
pattern: ["api", "events"],
|
|
68
|
+
summary: "Query events with filters",
|
|
69
|
+
tags: ["Events"],
|
|
70
|
+
query: z.object({
|
|
71
|
+
category: EventCategorySchema.optional(),
|
|
72
|
+
event: EventNameSchema.optional(),
|
|
73
|
+
status: EventStatusSchema.optional(),
|
|
74
|
+
source: EventSourceSchema.optional(),
|
|
75
|
+
agentId: z.string().optional(),
|
|
76
|
+
taskId: z.string().optional(),
|
|
77
|
+
sessionId: z.string().optional(),
|
|
78
|
+
since: z.string().optional(),
|
|
79
|
+
until: z.string().optional(),
|
|
80
|
+
limit: z.coerce.number().int().min(1).max(1000).optional(),
|
|
81
|
+
}),
|
|
82
|
+
responses: {
|
|
83
|
+
200: { description: "List of events" },
|
|
84
|
+
},
|
|
85
|
+
auth: { apiKey: true },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const getEventCountsRoute = route({
|
|
89
|
+
method: "get",
|
|
90
|
+
path: "/api/events/counts",
|
|
91
|
+
pattern: ["api", "events", "counts"],
|
|
92
|
+
summary: "Get event counts grouped by event name",
|
|
93
|
+
tags: ["Events"],
|
|
94
|
+
query: z.object({
|
|
95
|
+
category: EventCategorySchema.optional(),
|
|
96
|
+
source: EventSourceSchema.optional(),
|
|
97
|
+
agentId: z.string().optional(),
|
|
98
|
+
taskId: z.string().optional(),
|
|
99
|
+
sessionId: z.string().optional(),
|
|
100
|
+
since: z.string().optional(),
|
|
101
|
+
until: z.string().optional(),
|
|
102
|
+
}),
|
|
103
|
+
responses: {
|
|
104
|
+
200: { description: "Event counts" },
|
|
105
|
+
},
|
|
106
|
+
auth: { apiKey: true },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export async function handleEvents(
|
|
112
|
+
req: IncomingMessage,
|
|
113
|
+
res: ServerResponse,
|
|
114
|
+
pathSegments: string[],
|
|
115
|
+
queryParams: URLSearchParams,
|
|
116
|
+
_myAgentId: string | undefined,
|
|
117
|
+
): Promise<boolean> {
|
|
118
|
+
// Match batch BEFORE generic /api/events (POST)
|
|
119
|
+
if (createEventsBatchRoute.match(req.method, pathSegments)) {
|
|
120
|
+
const parsed = await createEventsBatchRoute.parse(req, res, pathSegments, queryParams);
|
|
121
|
+
if (!parsed) return true;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const count = createEventsBatch(parsed.body.events);
|
|
125
|
+
json(res, { success: true, count }, 201);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("[HTTP] Failed to create events batch:", error);
|
|
128
|
+
jsonError(res, "Failed to store events batch", 500);
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Match counts BEFORE generic /api/events (GET)
|
|
134
|
+
if (getEventCountsRoute.match(req.method, pathSegments)) {
|
|
135
|
+
const parsed = await getEventCountsRoute.parse(req, res, pathSegments, queryParams);
|
|
136
|
+
if (!parsed) return true;
|
|
137
|
+
|
|
138
|
+
const counts = getEventCountsFiltered({
|
|
139
|
+
category: parsed.query.category || undefined,
|
|
140
|
+
source: parsed.query.source || undefined,
|
|
141
|
+
agentId: parsed.query.agentId || undefined,
|
|
142
|
+
taskId: parsed.query.taskId || undefined,
|
|
143
|
+
sessionId: parsed.query.sessionId || undefined,
|
|
144
|
+
since: parsed.query.since || undefined,
|
|
145
|
+
until: parsed.query.until || undefined,
|
|
146
|
+
});
|
|
147
|
+
json(res, { counts });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// POST /api/events — single event
|
|
152
|
+
if (createEventRoute.match(req.method, pathSegments)) {
|
|
153
|
+
const parsed = await createEventRoute.parse(req, res, pathSegments, queryParams);
|
|
154
|
+
if (!parsed) return true;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const event = createEvent(parsed.body);
|
|
158
|
+
json(res, { success: true, event }, 201);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("[HTTP] Failed to create event:", error);
|
|
161
|
+
jsonError(res, "Failed to store event", 500);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// GET /api/events — filtered query
|
|
167
|
+
if (getEventsRoute.match(req.method, pathSegments)) {
|
|
168
|
+
const parsed = await getEventsRoute.parse(req, res, pathSegments, queryParams);
|
|
169
|
+
if (!parsed) return true;
|
|
170
|
+
|
|
171
|
+
const events = getEventsFiltered({
|
|
172
|
+
category: parsed.query.category || undefined,
|
|
173
|
+
event: parsed.query.event || undefined,
|
|
174
|
+
status: parsed.query.status || undefined,
|
|
175
|
+
source: parsed.query.source || undefined,
|
|
176
|
+
agentId: parsed.query.agentId || undefined,
|
|
177
|
+
taskId: parsed.query.taskId || undefined,
|
|
178
|
+
sessionId: parsed.query.sessionId || undefined,
|
|
179
|
+
since: parsed.query.since || undefined,
|
|
180
|
+
until: parsed.query.until || undefined,
|
|
181
|
+
limit: parsed.query.limit ?? 100,
|
|
182
|
+
});
|
|
183
|
+
json(res, { events });
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -19,11 +19,14 @@ import { handleActiveSessions } from "./active-sessions";
|
|
|
19
19
|
import { handleAgentRegister, handleAgentsRest } from "./agents";
|
|
20
20
|
import { handleApprovalRequests } from "./approval-requests";
|
|
21
21
|
import { handleConfig } from "./config";
|
|
22
|
+
import { handleContext } from "./context";
|
|
22
23
|
import { handleCore, loadGlobalConfigsIntoEnv } from "./core";
|
|
23
24
|
import { handleDbQuery } from "./db-query";
|
|
24
25
|
import { handleEcosystem } from "./ecosystem";
|
|
25
26
|
import { handleEpics } from "./epics";
|
|
27
|
+
import { handleEvents } from "./events";
|
|
26
28
|
import { handleMcp } from "./mcp";
|
|
29
|
+
import { handleMcpServers } from "./mcp-servers";
|
|
27
30
|
import { handleMemory } from "./memory";
|
|
28
31
|
import { handlePoll } from "./poll";
|
|
29
32
|
import { handlePromptTemplates } from "./prompt-templates";
|
|
@@ -102,6 +105,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
102
105
|
() => handleTrackers(req, res, pathSegments),
|
|
103
106
|
() => handleWebhooks(req, res, pathSegments),
|
|
104
107
|
() => handleAgentsRest(req, res, pathSegments, queryParams, myAgentId),
|
|
108
|
+
() => handleContext(req, res, pathSegments, queryParams, myAgentId),
|
|
105
109
|
() => handleTasks(req, res, pathSegments, queryParams, myAgentId),
|
|
106
110
|
() => handleStats(req, res, pathSegments, queryParams),
|
|
107
111
|
() => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
|
|
@@ -114,7 +118,9 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
114
118
|
() => handleDbQuery(req, res, pathSegments, queryParams),
|
|
115
119
|
() => handleRepos(req, res, pathSegments, queryParams),
|
|
116
120
|
() => handleSkills(req, res, pathSegments, queryParams, myAgentId),
|
|
121
|
+
() => handleMcpServers(req, res, pathSegments, queryParams),
|
|
117
122
|
() => handleMemory(req, res, pathSegments, myAgentId),
|
|
123
|
+
() => handleEvents(req, res, pathSegments, queryParams, myAgentId),
|
|
118
124
|
() => handleMcp(req, res, transports),
|
|
119
125
|
];
|
|
120
126
|
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createMcpServer,
|
|
5
|
+
deleteMcpServer,
|
|
6
|
+
getAgentMcpServers,
|
|
7
|
+
getMcpServerById,
|
|
8
|
+
getResolvedConfig,
|
|
9
|
+
installMcpServer,
|
|
10
|
+
listMcpServers,
|
|
11
|
+
uninstallMcpServer,
|
|
12
|
+
updateMcpServer,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import { route } from "./route-def";
|
|
15
|
+
import { json, jsonError } from "./utils";
|
|
16
|
+
|
|
17
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const listMcpServersRoute = route({
|
|
20
|
+
method: "get",
|
|
21
|
+
path: "/api/mcp-servers",
|
|
22
|
+
pattern: ["api", "mcp-servers"],
|
|
23
|
+
summary: "List MCP servers with optional filters",
|
|
24
|
+
tags: ["MCP Servers"],
|
|
25
|
+
auth: { apiKey: true },
|
|
26
|
+
query: z.object({
|
|
27
|
+
scope: z.string().optional(),
|
|
28
|
+
transport: z.string().optional(),
|
|
29
|
+
ownerAgentId: z.string().optional(),
|
|
30
|
+
enabled: z.string().optional(),
|
|
31
|
+
search: z.string().optional(),
|
|
32
|
+
}),
|
|
33
|
+
responses: {
|
|
34
|
+
200: { description: "MCP server list" },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const getMcpServerRoute = route({
|
|
39
|
+
method: "get",
|
|
40
|
+
path: "/api/mcp-servers/{id}",
|
|
41
|
+
pattern: ["api", "mcp-servers", null],
|
|
42
|
+
summary: "Get MCP server by ID",
|
|
43
|
+
tags: ["MCP Servers"],
|
|
44
|
+
auth: { apiKey: true },
|
|
45
|
+
params: z.object({ id: z.string() }),
|
|
46
|
+
responses: {
|
|
47
|
+
200: { description: "MCP server details" },
|
|
48
|
+
404: { description: "MCP server not found" },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const createMcpServerRoute = route({
|
|
53
|
+
method: "post",
|
|
54
|
+
path: "/api/mcp-servers",
|
|
55
|
+
pattern: ["api", "mcp-servers"],
|
|
56
|
+
summary: "Create a new MCP server",
|
|
57
|
+
tags: ["MCP Servers"],
|
|
58
|
+
auth: { apiKey: true },
|
|
59
|
+
body: z.object({
|
|
60
|
+
name: z.string().min(1),
|
|
61
|
+
transport: z.enum(["stdio", "http", "sse"]),
|
|
62
|
+
description: z.string().optional(),
|
|
63
|
+
scope: z.string().optional(),
|
|
64
|
+
ownerAgentId: z.string().optional(),
|
|
65
|
+
command: z.string().optional(),
|
|
66
|
+
args: z.string().optional(),
|
|
67
|
+
url: z.string().optional(),
|
|
68
|
+
headers: z.string().optional(),
|
|
69
|
+
envConfigKeys: z.string().optional(),
|
|
70
|
+
headerConfigKeys: z.string().optional(),
|
|
71
|
+
}),
|
|
72
|
+
responses: {
|
|
73
|
+
201: { description: "MCP server created" },
|
|
74
|
+
400: { description: "Validation error" },
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const updateMcpServerRoute = route({
|
|
79
|
+
method: "put",
|
|
80
|
+
path: "/api/mcp-servers/{id}",
|
|
81
|
+
pattern: ["api", "mcp-servers", null],
|
|
82
|
+
summary: "Update an MCP server",
|
|
83
|
+
tags: ["MCP Servers"],
|
|
84
|
+
auth: { apiKey: true },
|
|
85
|
+
params: z.object({ id: z.string() }),
|
|
86
|
+
body: z.record(z.string(), z.unknown()),
|
|
87
|
+
responses: {
|
|
88
|
+
200: { description: "MCP server updated" },
|
|
89
|
+
404: { description: "MCP server not found" },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const deleteMcpServerRoute = route({
|
|
94
|
+
method: "delete",
|
|
95
|
+
path: "/api/mcp-servers/{id}",
|
|
96
|
+
pattern: ["api", "mcp-servers", null],
|
|
97
|
+
summary: "Delete an MCP server",
|
|
98
|
+
tags: ["MCP Servers"],
|
|
99
|
+
auth: { apiKey: true },
|
|
100
|
+
params: z.object({ id: z.string() }),
|
|
101
|
+
responses: {
|
|
102
|
+
200: { description: "MCP server deleted" },
|
|
103
|
+
404: { description: "MCP server not found" },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const installMcpServerRoute = route({
|
|
108
|
+
method: "post",
|
|
109
|
+
path: "/api/mcp-servers/{id}/install",
|
|
110
|
+
pattern: ["api", "mcp-servers", null, "install"],
|
|
111
|
+
summary: "Install MCP server for an agent",
|
|
112
|
+
tags: ["MCP Servers"],
|
|
113
|
+
auth: { apiKey: true },
|
|
114
|
+
params: z.object({ id: z.string() }),
|
|
115
|
+
body: z.object({
|
|
116
|
+
agentId: z.string(),
|
|
117
|
+
}),
|
|
118
|
+
responses: {
|
|
119
|
+
200: { description: "MCP server installed" },
|
|
120
|
+
404: { description: "MCP server not found" },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const uninstallMcpServerRoute = route({
|
|
125
|
+
method: "delete",
|
|
126
|
+
path: "/api/mcp-servers/{id}/install/{agentId}",
|
|
127
|
+
pattern: ["api", "mcp-servers", null, "install", null],
|
|
128
|
+
summary: "Uninstall MCP server for an agent",
|
|
129
|
+
tags: ["MCP Servers"],
|
|
130
|
+
auth: { apiKey: true },
|
|
131
|
+
params: z.object({ id: z.string(), agentId: z.string() }),
|
|
132
|
+
responses: {
|
|
133
|
+
200: { description: "MCP server uninstalled" },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const getAgentMcpServersRoute = route({
|
|
138
|
+
method: "get",
|
|
139
|
+
path: "/api/agents/{id}/mcp-servers",
|
|
140
|
+
pattern: ["api", "agents", null, "mcp-servers"],
|
|
141
|
+
summary: "Get all MCP servers installed for an agent",
|
|
142
|
+
tags: ["MCP Servers"],
|
|
143
|
+
auth: { apiKey: true },
|
|
144
|
+
params: z.object({ id: z.string() }),
|
|
145
|
+
query: z.object({
|
|
146
|
+
resolveSecrets: z.string().optional(),
|
|
147
|
+
}),
|
|
148
|
+
responses: {
|
|
149
|
+
200: { description: "Agent MCP servers list" },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export async function handleMcpServers(
|
|
156
|
+
req: IncomingMessage,
|
|
157
|
+
res: ServerResponse,
|
|
158
|
+
pathSegments: string[],
|
|
159
|
+
queryParams: URLSearchParams,
|
|
160
|
+
): Promise<boolean> {
|
|
161
|
+
// GET /api/agents/:id/mcp-servers (must be before /api/mcp-servers routes)
|
|
162
|
+
if (getAgentMcpServersRoute.match(req.method, pathSegments)) {
|
|
163
|
+
const parsed = await getAgentMcpServersRoute.parse(req, res, pathSegments, queryParams);
|
|
164
|
+
if (!parsed) return true;
|
|
165
|
+
|
|
166
|
+
const servers = getAgentMcpServers(parsed.params.id);
|
|
167
|
+
const resolveSecrets = parsed.query.resolveSecrets === "true";
|
|
168
|
+
|
|
169
|
+
if (resolveSecrets) {
|
|
170
|
+
const configs = getResolvedConfig(parsed.params.id);
|
|
171
|
+
const configMap = new Map(configs.map((c) => [c.key, c.value]));
|
|
172
|
+
|
|
173
|
+
const serversWithSecrets = servers.map((server) => {
|
|
174
|
+
const resolvedEnv: Record<string, string> = {};
|
|
175
|
+
const resolvedHeaders: Record<string, string> = {};
|
|
176
|
+
|
|
177
|
+
// Resolve env config keys (JSON object: {"ENV_VAR": "config-key-name"})
|
|
178
|
+
if (server.envConfigKeys) {
|
|
179
|
+
try {
|
|
180
|
+
const mapping = JSON.parse(server.envConfigKeys) as Record<string, string>;
|
|
181
|
+
for (const [envVar, configKey] of Object.entries(mapping)) {
|
|
182
|
+
const value = configMap.get(configKey);
|
|
183
|
+
if (value !== undefined) {
|
|
184
|
+
resolvedEnv[envVar] = value;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Invalid JSON — skip resolution
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Resolve header config keys (JSON object: {"Header-Name": "config-key-name"})
|
|
193
|
+
if (server.headerConfigKeys) {
|
|
194
|
+
try {
|
|
195
|
+
const mapping = JSON.parse(server.headerConfigKeys) as Record<string, string>;
|
|
196
|
+
for (const [headerName, configKey] of Object.entries(mapping)) {
|
|
197
|
+
const value = configMap.get(configKey);
|
|
198
|
+
if (value !== undefined) {
|
|
199
|
+
resolvedHeaders[headerName] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Invalid JSON — skip resolution
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { ...server, resolvedEnv, resolvedHeaders };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
json(res, { servers: serversWithSecrets, total: serversWithSecrets.length });
|
|
211
|
+
} else {
|
|
212
|
+
json(res, { servers, total: servers.length });
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// POST /api/mcp-servers/:id/install
|
|
218
|
+
if (installMcpServerRoute.match(req.method, pathSegments)) {
|
|
219
|
+
const parsed = await installMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
220
|
+
if (!parsed) return true;
|
|
221
|
+
|
|
222
|
+
const server = getMcpServerById(parsed.params.id);
|
|
223
|
+
if (!server) {
|
|
224
|
+
jsonError(res, "MCP server not found", 404);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const agentMcpServer = installMcpServer(parsed.body.agentId, parsed.params.id);
|
|
230
|
+
json(res, { agentMcpServer });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
jsonError(res, err instanceof Error ? err.message : "Install failed", 400);
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// DELETE /api/mcp-servers/:id/install/:agentId
|
|
238
|
+
if (uninstallMcpServerRoute.match(req.method, pathSegments)) {
|
|
239
|
+
const parsed = await uninstallMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
240
|
+
if (!parsed) return true;
|
|
241
|
+
|
|
242
|
+
const removed = uninstallMcpServer(parsed.params.agentId, parsed.params.id);
|
|
243
|
+
json(res, { success: removed });
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// GET /api/mcp-servers
|
|
248
|
+
if (listMcpServersRoute.match(req.method, pathSegments)) {
|
|
249
|
+
const parsed = await listMcpServersRoute.parse(req, res, pathSegments, queryParams);
|
|
250
|
+
if (!parsed) return true;
|
|
251
|
+
|
|
252
|
+
const servers = listMcpServers({
|
|
253
|
+
scope: parsed.query.scope as "global" | "swarm" | "agent" | undefined,
|
|
254
|
+
transport: parsed.query.transport as "stdio" | "http" | "sse" | undefined,
|
|
255
|
+
ownerAgentId: parsed.query.ownerAgentId,
|
|
256
|
+
isEnabled: parsed.query.enabled !== undefined ? parsed.query.enabled === "true" : undefined,
|
|
257
|
+
search: parsed.query.search,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
json(res, { servers, total: servers.length });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// GET /api/mcp-servers/:id
|
|
265
|
+
if (getMcpServerRoute.match(req.method, pathSegments)) {
|
|
266
|
+
const parsed = await getMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
267
|
+
if (!parsed) return true;
|
|
268
|
+
|
|
269
|
+
const server = getMcpServerById(parsed.params.id);
|
|
270
|
+
if (!server) {
|
|
271
|
+
jsonError(res, "MCP server not found", 404);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
json(res, server);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// POST /api/mcp-servers
|
|
279
|
+
if (createMcpServerRoute.match(req.method, pathSegments)) {
|
|
280
|
+
const parsed = await createMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
281
|
+
if (!parsed) return true;
|
|
282
|
+
|
|
283
|
+
// Transport-specific validation
|
|
284
|
+
if (parsed.body.transport === "stdio" && !parsed.body.command) {
|
|
285
|
+
jsonError(res, "command is required for stdio transport", 400);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if ((parsed.body.transport === "http" || parsed.body.transport === "sse") && !parsed.body.url) {
|
|
289
|
+
jsonError(res, "url is required for http/sse transport", 400);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const server = createMcpServer({
|
|
295
|
+
name: parsed.body.name,
|
|
296
|
+
transport: parsed.body.transport,
|
|
297
|
+
description: parsed.body.description,
|
|
298
|
+
scope: parsed.body.scope as "global" | "swarm" | "agent" | undefined,
|
|
299
|
+
ownerAgentId: parsed.body.ownerAgentId,
|
|
300
|
+
command: parsed.body.command,
|
|
301
|
+
args: parsed.body.args,
|
|
302
|
+
url: parsed.body.url,
|
|
303
|
+
headers: parsed.body.headers,
|
|
304
|
+
envConfigKeys: parsed.body.envConfigKeys,
|
|
305
|
+
headerConfigKeys: parsed.body.headerConfigKeys,
|
|
306
|
+
});
|
|
307
|
+
json(res, { server }, 201);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
jsonError(res, err instanceof Error ? err.message : "Create failed", 400);
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// PUT /api/mcp-servers/:id
|
|
315
|
+
if (updateMcpServerRoute.match(req.method, pathSegments)) {
|
|
316
|
+
const parsed = await updateMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
317
|
+
if (!parsed) return true;
|
|
318
|
+
|
|
319
|
+
// Transport-specific validation on update (only if transport is being set)
|
|
320
|
+
const transport = parsed.body.transport as string | undefined;
|
|
321
|
+
if (transport === "stdio" && parsed.body.command === undefined) {
|
|
322
|
+
// Check if existing server already has a command
|
|
323
|
+
const existing = getMcpServerById(parsed.params.id);
|
|
324
|
+
if (existing && !existing.command && !parsed.body.command) {
|
|
325
|
+
jsonError(res, "command is required for stdio transport", 400);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if ((transport === "http" || transport === "sse") && parsed.body.url === undefined) {
|
|
330
|
+
const existing = getMcpServerById(parsed.params.id);
|
|
331
|
+
if (existing && !existing.url && !parsed.body.url) {
|
|
332
|
+
jsonError(res, "url is required for http/sse transport", 400);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const server = updateMcpServer(
|
|
338
|
+
parsed.params.id,
|
|
339
|
+
parsed.body as Parameters<typeof updateMcpServer>[1],
|
|
340
|
+
);
|
|
341
|
+
if (!server) {
|
|
342
|
+
jsonError(res, "MCP server not found", 404);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
json(res, { server });
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// DELETE /api/mcp-servers/:id
|
|
350
|
+
if (deleteMcpServerRoute.match(req.method, pathSegments)) {
|
|
351
|
+
const parsed = await deleteMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
352
|
+
if (!parsed) return true;
|
|
353
|
+
|
|
354
|
+
const deleted = deleteMcpServer(parsed.params.id);
|
|
355
|
+
if (!deleted) {
|
|
356
|
+
jsonError(res, "MCP server not found", 404);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
json(res, { success: true });
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return false;
|
|
364
|
+
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
updateAgentStatusFromCapacity,
|
|
18
18
|
updateTaskClaudeSessionId,
|
|
19
19
|
updateTaskProgress,
|
|
20
|
+
updateTaskVcs,
|
|
20
21
|
} from "../be/db";
|
|
21
22
|
import { route } from "./route-def";
|
|
22
23
|
import { json, jsonError } from "./utils";
|
|
@@ -187,6 +188,26 @@ const resumeTaskRoute = route({
|
|
|
187
188
|
},
|
|
188
189
|
});
|
|
189
190
|
|
|
191
|
+
const updateTaskVcsRoute = route({
|
|
192
|
+
method: "patch",
|
|
193
|
+
path: "/api/tasks/{id}/vcs",
|
|
194
|
+
pattern: ["api", "tasks", null, "vcs"],
|
|
195
|
+
summary: "Update VCS (PR/MR) info for a task",
|
|
196
|
+
tags: ["Tasks"],
|
|
197
|
+
params: z.object({ id: z.string() }),
|
|
198
|
+
body: z.object({
|
|
199
|
+
vcsProvider: z.enum(["github", "gitlab"]),
|
|
200
|
+
vcsRepo: z.string(),
|
|
201
|
+
vcsNumber: z.number().int().positive(),
|
|
202
|
+
vcsUrl: z.string().url(),
|
|
203
|
+
}),
|
|
204
|
+
responses: {
|
|
205
|
+
200: { description: "VCS info updated" },
|
|
206
|
+
404: { description: "Task not found" },
|
|
207
|
+
},
|
|
208
|
+
auth: { apiKey: true },
|
|
209
|
+
});
|
|
210
|
+
|
|
190
211
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
191
212
|
|
|
192
213
|
export async function handleTasks(
|
|
@@ -515,6 +536,18 @@ export async function handleTasks(
|
|
|
515
536
|
return true;
|
|
516
537
|
}
|
|
517
538
|
|
|
539
|
+
if (updateTaskVcsRoute.match(req.method, pathSegments)) {
|
|
540
|
+
const parsed = await updateTaskVcsRoute.parse(req, res, pathSegments, queryParams);
|
|
541
|
+
if (!parsed) return true;
|
|
542
|
+
const task = updateTaskVcs(parsed.params.id, parsed.body);
|
|
543
|
+
if (!task) {
|
|
544
|
+
jsonError(res, "Task not found", 404);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
json(res, task);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
518
551
|
if (resumeTaskRoute.match(req.method, pathSegments)) {
|
|
519
552
|
const parsed = await resumeTaskRoute.parse(req, res, pathSegments, queryParams);
|
|
520
553
|
if (!parsed) return true;
|
package/src/linear/outbound.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getTrackerSync, updateTrackerSync } from "../be/db-queries/tracker";
|
|
2
|
+
import { ensureToken } from "../oauth/ensure-token";
|
|
2
3
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
3
|
-
import { getLinearClient } from "./client";
|
|
4
|
+
import { getLinearClient, resetLinearClient } from "./client";
|
|
4
5
|
import { endAgentSession, postAgentSessionAction, taskSessionMap } from "./sync";
|
|
5
6
|
|
|
6
7
|
let subscribed = false;
|
|
@@ -85,6 +86,8 @@ async function handleTaskCompleted(data: unknown): Promise<void> {
|
|
|
85
86
|
} else {
|
|
86
87
|
// No session — fall back to issue comment
|
|
87
88
|
try {
|
|
89
|
+
await ensureToken("linear");
|
|
90
|
+
resetLinearClient(); // Clear cached client so it picks up refreshed token
|
|
88
91
|
const client = getLinearClient();
|
|
89
92
|
if (!client) {
|
|
90
93
|
console.log("[Linear Outbound] No Linear client available, skipping sync for", taskId);
|
|
@@ -133,6 +136,8 @@ async function handleTaskFailed(data: unknown): Promise<void> {
|
|
|
133
136
|
} else {
|
|
134
137
|
// No session — fall back to issue comment
|
|
135
138
|
try {
|
|
139
|
+
await ensureToken("linear");
|
|
140
|
+
resetLinearClient(); // Clear cached client so it picks up refreshed token
|
|
136
141
|
const client = getLinearClient();
|
|
137
142
|
if (!client) {
|
|
138
143
|
console.log("[Linear Outbound] No Linear client available, skipping sync for", taskId);
|
|
@@ -180,6 +185,8 @@ async function handleTaskCancelled(data: unknown): Promise<void> {
|
|
|
180
185
|
console.log(`[Linear Outbound] Posted cancellation to AgentSession for task ${taskId}`);
|
|
181
186
|
} else {
|
|
182
187
|
try {
|
|
188
|
+
await ensureToken("linear");
|
|
189
|
+
resetLinearClient(); // Clear cached client so it picks up refreshed token
|
|
183
190
|
const client = getLinearClient();
|
|
184
191
|
if (!client) {
|
|
185
192
|
console.log("[Linear Outbound] No Linear client available, skipping sync for", taskId);
|
package/src/linear/sync.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getTrackerSyncByExternalId,
|
|
7
7
|
updateTrackerSync,
|
|
8
8
|
} from "../be/db-queries/tracker";
|
|
9
|
+
import { ensureToken } from "../oauth/ensure-token";
|
|
9
10
|
import { resolveTemplate } from "../prompts/resolver";
|
|
10
11
|
// Side-effect import: registers all Linear event templates in the in-memory registry
|
|
11
12
|
import "./templates";
|
|
@@ -46,6 +47,7 @@ async function postAgentActivity(
|
|
|
46
47
|
| { type: "thought" | "response" | "error"; body: string }
|
|
47
48
|
| { type: "action"; action: string; parameter?: string; result?: string },
|
|
48
49
|
): Promise<boolean> {
|
|
50
|
+
await ensureToken("linear");
|
|
49
51
|
const tokens = getOAuthTokens("linear");
|
|
50
52
|
if (!tokens) {
|
|
51
53
|
console.log("[Linear Sync] No OAuth tokens, cannot post AgentSession activity");
|
|
@@ -96,6 +98,7 @@ async function updateAgentSession(
|
|
|
96
98
|
sessionId: string,
|
|
97
99
|
input: Record<string, unknown>,
|
|
98
100
|
): Promise<boolean> {
|
|
101
|
+
await ensureToken("linear");
|
|
99
102
|
const tokens = getOAuthTokens("linear");
|
|
100
103
|
if (!tokens) {
|
|
101
104
|
console.log("[Linear Sync] No OAuth tokens, cannot update AgentSession");
|