apteva 0.4.4 → 0.4.5

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.
@@ -0,0 +1,377 @@
1
+ import { spawn } from "bun";
2
+ import { json } from "./helpers";
3
+ import { McpServerDB, generateId, type McpServer } from "../../db";
4
+ import { getNextPort } from "../../server";
5
+ import {
6
+ startMcpProcess,
7
+ stopMcpProcess,
8
+ initializeMcpServer,
9
+ listMcpTools,
10
+ callMcpTool,
11
+ getMcpProcess,
12
+ getHttpMcpClient,
13
+ } from "../../mcp-client";
14
+
15
+ export async function handleMcpRoutes(
16
+ req: Request,
17
+ path: string,
18
+ method: string,
19
+ ): Promise<Response | null> {
20
+ // GET /api/mcp/servers - List MCP servers (optionally filtered by project)
21
+ if (path === "/api/mcp/servers" && method === "GET") {
22
+ const url = new URL(req.url);
23
+ const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
24
+ const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
25
+
26
+ let servers;
27
+ if (forAgent !== null) {
28
+ // Get servers available for an agent (global + agent's project)
29
+ servers = McpServerDB.findForAgent(forAgent || null);
30
+ } else if (projectFilter === "global") {
31
+ servers = McpServerDB.findGlobal();
32
+ } else if (projectFilter && projectFilter !== "all") {
33
+ servers = McpServerDB.findByProject(projectFilter);
34
+ } else {
35
+ servers = McpServerDB.findAll();
36
+ }
37
+ return json({ servers });
38
+ }
39
+
40
+ // GET /api/mcp/registry - Search MCP registry for available servers
41
+ if (path === "/api/mcp/registry" && method === "GET") {
42
+ const url = new URL(req.url);
43
+ const search = url.searchParams.get("search") || "";
44
+ const limit = url.searchParams.get("limit") || "20";
45
+
46
+ try {
47
+ const registryUrl = `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(search)}&limit=${limit}`;
48
+ const res = await fetch(registryUrl);
49
+ if (!res.ok) {
50
+ return json({ error: "Failed to fetch registry" }, 500);
51
+ }
52
+ const data = await res.json();
53
+
54
+ // Transform to simpler format - dedupe by name
55
+ const seen = new Set<string>();
56
+ const servers = (data.servers || [])
57
+ .map((item: any) => {
58
+ const s = item.server;
59
+ const pkg = s.packages?.find((p: any) => p.registryType === "npm");
60
+ const remote = s.remotes?.[0];
61
+
62
+ // Extract a short display name from the full name
63
+ const fullName = s.name || "";
64
+ const shortName = fullName.split("/").pop()?.replace(/-mcp$/, "").replace(/^mcp-/, "") || fullName;
65
+
66
+ return {
67
+ id: fullName,
68
+ name: shortName,
69
+ fullName: fullName,
70
+ description: s.description,
71
+ version: s.version,
72
+ repository: s.repository?.url,
73
+ npmPackage: pkg?.identifier || null,
74
+ remoteUrl: remote?.url || null,
75
+ transport: pkg?.transport?.type || (remote ? "http" : "stdio"),
76
+ };
77
+ })
78
+ .filter((s: any) => {
79
+ // Dedupe by fullName
80
+ if (seen.has(s.fullName)) return false;
81
+ seen.add(s.fullName);
82
+ // Only show servers with npm package or remote URL
83
+ return s.npmPackage || s.remoteUrl;
84
+ });
85
+
86
+ return json({ servers });
87
+ } catch (e) {
88
+ return json({ error: "Failed to search registry" }, 500);
89
+ }
90
+ }
91
+
92
+ // POST /api/mcp/servers - Create/install a new MCP server
93
+ if (path === "/api/mcp/servers" && method === "POST") {
94
+ try {
95
+ const body = await req.json();
96
+ const { name, type, package: pkg, pip_module, command, args, env, url, headers, source, project_id } = body;
97
+
98
+ if (!name) {
99
+ return json({ error: "Name is required" }, 400);
100
+ }
101
+
102
+ const server = McpServerDB.create({
103
+ id: generateId(),
104
+ name,
105
+ type: type || "npm",
106
+ package: pkg || null,
107
+ pip_module: pip_module || null,
108
+ command: command || null,
109
+ args: args || null,
110
+ env: env || {},
111
+ url: url || null,
112
+ headers: headers || {},
113
+ source: source || null,
114
+ project_id: project_id || null,
115
+ });
116
+
117
+ return json({ server }, 201);
118
+ } catch (e) {
119
+ console.error("Create MCP server error:", e);
120
+ return json({ error: "Invalid request body" }, 400);
121
+ }
122
+ }
123
+
124
+ // GET /api/mcp/servers/:id - Get a specific MCP server
125
+ const mcpServerMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)$/);
126
+ if (mcpServerMatch && method === "GET") {
127
+ const server = McpServerDB.findById(mcpServerMatch[1]);
128
+ if (!server) {
129
+ return json({ error: "MCP server not found" }, 404);
130
+ }
131
+ return json({ server });
132
+ }
133
+
134
+ // PUT /api/mcp/servers/:id - Update an MCP server
135
+ if (mcpServerMatch && method === "PUT") {
136
+ const server = McpServerDB.findById(mcpServerMatch[1]);
137
+ if (!server) {
138
+ return json({ error: "MCP server not found" }, 404);
139
+ }
140
+
141
+ try {
142
+ const body = await req.json();
143
+ const updates: Partial<McpServer> = {};
144
+
145
+ if (body.name !== undefined) updates.name = body.name;
146
+ if (body.type !== undefined) updates.type = body.type;
147
+ if (body.package !== undefined) updates.package = body.package;
148
+ if (body.pip_module !== undefined) updates.pip_module = body.pip_module;
149
+ if (body.command !== undefined) updates.command = body.command;
150
+ if (body.args !== undefined) updates.args = body.args;
151
+ if (body.env !== undefined) updates.env = body.env;
152
+ if (body.url !== undefined) updates.url = body.url;
153
+ if (body.headers !== undefined) updates.headers = body.headers;
154
+ if (body.project_id !== undefined) updates.project_id = body.project_id;
155
+
156
+ const updated = McpServerDB.update(mcpServerMatch[1], updates);
157
+ return json({ server: updated });
158
+ } catch (e) {
159
+ return json({ error: "Invalid request body" }, 400);
160
+ }
161
+ }
162
+
163
+ // DELETE /api/mcp/servers/:id - Delete an MCP server
164
+ if (mcpServerMatch && method === "DELETE") {
165
+ const server = McpServerDB.findById(mcpServerMatch[1]);
166
+ if (!server) {
167
+ return json({ error: "MCP server not found" }, 404);
168
+ }
169
+
170
+ // Stop if running
171
+ if (server.status === "running") {
172
+ // TODO: Stop the server process
173
+ }
174
+
175
+ McpServerDB.delete(mcpServerMatch[1]);
176
+ return json({ success: true });
177
+ }
178
+
179
+ // POST /api/mcp/servers/:id/start - Start an MCP server
180
+ const mcpStartMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/start$/);
181
+ if (mcpStartMatch && method === "POST") {
182
+ const server = McpServerDB.findById(mcpStartMatch[1]);
183
+ if (!server) {
184
+ return json({ error: "MCP server not found" }, 404);
185
+ }
186
+
187
+ if (server.status === "running") {
188
+ return json({ error: "MCP server already running" }, 400);
189
+ }
190
+
191
+ // Determine command to run
192
+ // Helper to substitute $ENV_VAR references with actual values
193
+ const substituteEnvVars = (str: string, env: Record<string, string>): string => {
194
+ return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
195
+ return env[varName] || '';
196
+ });
197
+ };
198
+
199
+ let cmd: string[];
200
+ const serverEnv = server.env || {};
201
+
202
+ if (server.command) {
203
+ // Custom command - substitute env vars in args
204
+ cmd = server.command.split(" ");
205
+ if (server.args) {
206
+ const substitutedArgs = substituteEnvVars(server.args, serverEnv);
207
+ cmd.push(...substitutedArgs.split(" "));
208
+ }
209
+ } else if (server.type === "pip" && server.package) {
210
+ // Python pip package - install first, then run module
211
+ const pipPackage = server.package;
212
+ const pipModule = server.pip_module || server.package.split("[")[0]; // Default: package name without extras
213
+
214
+ console.log(`Installing pip package: ${pipPackage}...`);
215
+ const installResult = spawn({
216
+ cmd: ["pip", "install", "--quiet", "--break-system-packages", pipPackage],
217
+ env: { ...process.env as Record<string, string>, ...serverEnv },
218
+ stdout: "pipe",
219
+ stderr: "pipe",
220
+ });
221
+
222
+ // Wait for installation to complete
223
+ const exitCode = await installResult.exited;
224
+ if (exitCode !== 0) {
225
+ const stderr = await new Response(installResult.stderr).text();
226
+ return json({ error: `Failed to install pip package: ${stderr || "unknown error"}` }, 500);
227
+ }
228
+
229
+ // Now run the module
230
+ cmd = ["python", "-m", pipModule];
231
+ if (server.args) {
232
+ const substitutedArgs = substituteEnvVars(server.args, serverEnv);
233
+ cmd.push(...substitutedArgs.split(" "));
234
+ }
235
+ } else if (server.package) {
236
+ // npm package - use npx
237
+ cmd = ["npx", "-y", server.package];
238
+ if (server.args) {
239
+ const substitutedArgs = substituteEnvVars(server.args, serverEnv);
240
+ cmd.push(...substitutedArgs.split(" "));
241
+ }
242
+ } else {
243
+ return json({ error: "No command or package specified" }, 400);
244
+ }
245
+
246
+ // Get a port for the HTTP proxy
247
+ const port = await getNextPort();
248
+
249
+ console.log(`Starting MCP server ${server.name}...`);
250
+ console.log(` Command: ${cmd.join(" ")}`);
251
+ console.log(` HTTP proxy: http://localhost:${port}/mcp`);
252
+
253
+ // Start the MCP process with stdio pipes + HTTP proxy
254
+ const result = await startMcpProcess(server.id, cmd, server.env || {}, port);
255
+
256
+ if (!result.success) {
257
+ console.error(`Failed to start MCP server: ${result.error}`);
258
+ return json({ error: `Failed to start: ${result.error}` }, 500);
259
+ }
260
+
261
+ // Update status with the HTTP proxy port
262
+ const updated = McpServerDB.setStatus(server.id, "running", port);
263
+
264
+ return json({
265
+ server: updated,
266
+ message: "MCP server started",
267
+ proxyUrl: `http://localhost:${port}/mcp`,
268
+ });
269
+ }
270
+
271
+ // POST /api/mcp/servers/:id/stop - Stop an MCP server
272
+ const mcpStopMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/stop$/);
273
+ if (mcpStopMatch && method === "POST") {
274
+ const server = McpServerDB.findById(mcpStopMatch[1]);
275
+ if (!server) {
276
+ return json({ error: "MCP server not found" }, 404);
277
+ }
278
+
279
+ // Stop the MCP process
280
+ stopMcpProcess(server.id);
281
+
282
+ const updated = McpServerDB.setStatus(server.id, "stopped");
283
+ return json({ server: updated, message: "MCP server stopped" });
284
+ }
285
+
286
+ // GET /api/mcp/servers/:id/tools - List tools from an MCP server
287
+ const mcpToolsMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/);
288
+ if (mcpToolsMatch && method === "GET") {
289
+ const server = McpServerDB.findById(mcpToolsMatch[1]);
290
+ if (!server) {
291
+ return json({ error: "MCP server not found" }, 404);
292
+ }
293
+
294
+ // HTTP servers use remote HTTP transport
295
+ if (server.type === "http" && server.url) {
296
+ try {
297
+ const httpClient = getHttpMcpClient(server.url, server.headers || {});
298
+ const serverInfo = await httpClient.initialize();
299
+ const tools = await httpClient.listTools();
300
+
301
+ return json({
302
+ serverInfo,
303
+ tools,
304
+ });
305
+ } catch (err) {
306
+ console.error(`Failed to list HTTP MCP tools: ${err}`);
307
+ return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
308
+ }
309
+ }
310
+
311
+ // Stdio servers require a running process
312
+ const mcpProcess = getMcpProcess(server.id);
313
+ if (!mcpProcess) {
314
+ return json({ error: "MCP server is not running" }, 400);
315
+ }
316
+
317
+ try {
318
+ const serverInfo = await initializeMcpServer(server.id);
319
+ const tools = await listMcpTools(server.id);
320
+
321
+ return json({
322
+ serverInfo,
323
+ tools,
324
+ });
325
+ } catch (err) {
326
+ console.error(`Failed to list MCP tools: ${err}`);
327
+ return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
328
+ }
329
+ }
330
+
331
+ // POST /api/mcp/servers/:id/tools/:toolName/call - Call a tool on an MCP server
332
+ const mcpToolCallMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools\/([^/]+)\/call$/);
333
+ if (mcpToolCallMatch && method === "POST") {
334
+ const server = McpServerDB.findById(mcpToolCallMatch[1]);
335
+ if (!server) {
336
+ return json({ error: "MCP server not found" }, 404);
337
+ }
338
+
339
+ const toolName = decodeURIComponent(mcpToolCallMatch[2]);
340
+
341
+ // HTTP servers use remote HTTP transport
342
+ if (server.type === "http" && server.url) {
343
+ try {
344
+ const body = await req.json();
345
+ const args = body.arguments || {};
346
+
347
+ const httpClient = getHttpMcpClient(server.url, server.headers || {});
348
+ const result = await httpClient.callTool(toolName, args);
349
+
350
+ return json({ result });
351
+ } catch (err) {
352
+ console.error(`Failed to call HTTP MCP tool: ${err}`);
353
+ return json({ error: `Failed to call tool: ${err}` }, 500);
354
+ }
355
+ }
356
+
357
+ // Stdio servers require a running process
358
+ const mcpProcess = getMcpProcess(server.id);
359
+ if (!mcpProcess) {
360
+ return json({ error: "MCP server is not running" }, 400);
361
+ }
362
+
363
+ try {
364
+ const body = await req.json();
365
+ const args = body.arguments || {};
366
+
367
+ const result = await callMcpTool(server.id, toolName, args);
368
+
369
+ return json({ result });
370
+ } catch (err) {
371
+ console.error(`Failed to call MCP tool: ${err}`);
372
+ return json({ error: `Failed to call tool: ${err}` }, 500);
373
+ }
374
+ }
375
+
376
+ return null;
377
+ }
@@ -0,0 +1,145 @@
1
+ import { json } from "./helpers";
2
+ import { META_AGENT_ENABLED, META_AGENT_ID, toApiAgent, startAgentProcess, setAgentStatus } from "./agent-utils";
3
+ import { AgentDB } from "../../db";
4
+ import { ProviderKeys, Onboarding, PROVIDERS } from "../../providers";
5
+ import { agentProcesses } from "../../server";
6
+
7
+ export async function handleMetaAgentRoutes(
8
+ req: Request,
9
+ path: string,
10
+ method: string,
11
+ ): Promise<Response | null> {
12
+ // GET /api/meta-agent/status - Get meta agent status and config
13
+ if (path === "/api/meta-agent/status" && method === "GET") {
14
+ if (!META_AGENT_ENABLED) {
15
+ return json({ enabled: false });
16
+ }
17
+
18
+ // Check if onboarding is complete
19
+ if (!Onboarding.isComplete()) {
20
+ return json({ enabled: true, available: false, reason: "onboarding_incomplete" });
21
+ }
22
+
23
+ // Get first configured provider
24
+ const configuredProviders = ProviderKeys.getConfiguredProviders();
25
+ if (configuredProviders.length === 0) {
26
+ return json({ enabled: true, available: false, reason: "no_provider" });
27
+ }
28
+
29
+ const providerId = configuredProviders[0] as keyof typeof PROVIDERS;
30
+ const provider = PROVIDERS[providerId];
31
+ if (!provider) {
32
+ return json({ enabled: true, available: false, reason: "invalid_provider" });
33
+ }
34
+
35
+ // Check if meta agent exists, create if not
36
+ let metaAgent = AgentDB.findById(META_AGENT_ID);
37
+ if (!metaAgent) {
38
+ // Find a recommended model or use first one
39
+ const defaultModel = provider.models.find((m: any) => m.recommended)?.value || provider.models[0]?.value;
40
+ if (!defaultModel) {
41
+ return json({ enabled: true, available: false, reason: "no_model" });
42
+ }
43
+
44
+ // Create the meta agent
45
+ metaAgent = AgentDB.create({
46
+ id: META_AGENT_ID,
47
+ name: "Apteva Assistant",
48
+ model: defaultModel,
49
+ provider: providerId,
50
+ system_prompt: `You are the Apteva Assistant, a helpful guide for users of the Apteva agent management platform.
51
+
52
+ You can help users with:
53
+ - Creating and configuring AI agents
54
+ - Setting up MCP servers for tool integrations
55
+ - Managing projects and organizing agents
56
+ - Explaining features like Memory, Tasks, Vision, Operator, Files, and Multi-Agent
57
+ - Troubleshooting common issues
58
+
59
+ Be concise, friendly, and helpful. When users ask about creating something, guide them step by step.
60
+ Keep responses short and actionable. Use markdown formatting when helpful.`,
61
+ features: {
62
+ memory: false,
63
+ tasks: false,
64
+ vision: false,
65
+ operator: false,
66
+ mcp: false,
67
+ realtime: false,
68
+ files: false,
69
+ agents: false,
70
+ },
71
+ mcp_servers: [],
72
+ skills: [],
73
+ project_id: null, // Meta agent belongs to no project
74
+ } as any);
75
+ }
76
+
77
+ // Return status
78
+ return json({
79
+ enabled: true,
80
+ available: true,
81
+ agent: {
82
+ id: metaAgent.id,
83
+ name: metaAgent.name,
84
+ status: metaAgent.status,
85
+ port: metaAgent.port,
86
+ provider: metaAgent.provider,
87
+ model: metaAgent.model,
88
+ },
89
+ });
90
+ }
91
+
92
+ // POST /api/meta-agent/start - Start the meta agent
93
+ if (path === "/api/meta-agent/start" && method === "POST") {
94
+ if (!META_AGENT_ENABLED) {
95
+ return json({ error: "Meta agent is not enabled" }, 400);
96
+ }
97
+
98
+ const metaAgent = AgentDB.findById(META_AGENT_ID);
99
+ if (!metaAgent) {
100
+ return json({ error: "Meta agent not found" }, 404);
101
+ }
102
+
103
+ if (metaAgent.status === "running") {
104
+ return json({ agent: toApiAgent(metaAgent), message: "Already running" });
105
+ }
106
+
107
+ // Start the agent using existing startAgentProcess function
108
+ const result = await startAgentProcess(metaAgent, { silent: true });
109
+ if (!result.success) {
110
+ return json({ error: result.error || "Failed to start meta agent" }, 500);
111
+ }
112
+
113
+ const updated = AgentDB.findById(META_AGENT_ID);
114
+ return json({ agent: updated ? toApiAgent(updated) : null });
115
+ }
116
+
117
+ // POST /api/meta-agent/stop - Stop the meta agent
118
+ if (path === "/api/meta-agent/stop" && method === "POST") {
119
+ if (!META_AGENT_ENABLED) {
120
+ return json({ error: "Meta agent is not enabled" }, 400);
121
+ }
122
+
123
+ const metaAgent = AgentDB.findById(META_AGENT_ID);
124
+ if (!metaAgent) {
125
+ return json({ error: "Meta agent not found" }, 404);
126
+ }
127
+
128
+ if (metaAgent.status === "stopped") {
129
+ return json({ agent: toApiAgent(metaAgent), message: "Already stopped" });
130
+ }
131
+
132
+ // Stop the agent
133
+ const proc = agentProcesses.get(META_AGENT_ID);
134
+ if (proc) {
135
+ proc.proc.kill(); // BUG FIX: was proc.kill() which would fail
136
+ agentProcesses.delete(META_AGENT_ID);
137
+ }
138
+ setAgentStatus(META_AGENT_ID, "stopped", "user_stopped");
139
+
140
+ const updated = AgentDB.findById(META_AGENT_ID);
141
+ return json({ agent: updated ? toApiAgent(updated) : null });
142
+ }
143
+
144
+ return null;
145
+ }
@@ -0,0 +1,95 @@
1
+ import { json } from "./helpers";
2
+ import { AgentDB, ProjectDB, type Project } from "../../db";
3
+ import { toApiAgent, toApiProject } from "./agent-utils";
4
+
5
+ export async function handleProjectRoutes(
6
+ req: Request,
7
+ path: string,
8
+ method: string,
9
+ authContext?: unknown,
10
+ ): Promise<Response | null> {
11
+ // GET /api/projects - List all projects
12
+ if (path === "/api/projects" && method === "GET") {
13
+ const projects = ProjectDB.findAll();
14
+ const agentCounts = ProjectDB.getAgentCounts();
15
+ return json({
16
+ projects: projects.map(p => ({
17
+ ...toApiProject(p),
18
+ agentCount: agentCounts.get(p.id) || 0,
19
+ })),
20
+ unassignedCount: agentCounts.get(null) || 0,
21
+ });
22
+ }
23
+
24
+ // POST /api/projects - Create a new project
25
+ if (path === "/api/projects" && method === "POST") {
26
+ try {
27
+ const body = await req.json();
28
+ const { name, description, color } = body;
29
+
30
+ if (!name) {
31
+ return json({ error: "Name is required" }, 400);
32
+ }
33
+
34
+ const project = ProjectDB.create({
35
+ name,
36
+ description: description || null,
37
+ color: color || "#6366f1",
38
+ });
39
+
40
+ return json({ project: toApiProject(project) }, 201);
41
+ } catch (e) {
42
+ console.error("Create project error:", e);
43
+ return json({ error: "Invalid request body" }, 400);
44
+ }
45
+ }
46
+
47
+ // GET /api/projects/:id - Get a specific project
48
+ const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
49
+ if (projectMatch && method === "GET") {
50
+ const project = ProjectDB.findById(projectMatch[1]);
51
+ if (!project) {
52
+ return json({ error: "Project not found" }, 404);
53
+ }
54
+ const agents = AgentDB.findByProject(project.id);
55
+ return json({
56
+ project: toApiProject(project),
57
+ agents: agents.map(toApiAgent),
58
+ });
59
+ }
60
+
61
+ // PUT /api/projects/:id - Update a project
62
+ if (projectMatch && method === "PUT") {
63
+ const project = ProjectDB.findById(projectMatch[1]);
64
+ if (!project) {
65
+ return json({ error: "Project not found" }, 404);
66
+ }
67
+
68
+ try {
69
+ const body = await req.json();
70
+ const updates: Partial<Project> = {};
71
+
72
+ if (body.name !== undefined) updates.name = body.name;
73
+ if (body.description !== undefined) updates.description = body.description;
74
+ if (body.color !== undefined) updates.color = body.color;
75
+
76
+ const updated = ProjectDB.update(projectMatch[1], updates);
77
+ return json({ project: updated ? toApiProject(updated) : null });
78
+ } catch (e) {
79
+ return json({ error: "Invalid request body" }, 400);
80
+ }
81
+ }
82
+
83
+ // DELETE /api/projects/:id - Delete a project
84
+ if (projectMatch && method === "DELETE") {
85
+ const project = ProjectDB.findById(projectMatch[1]);
86
+ if (!project) {
87
+ return json({ error: "Project not found" }, 404);
88
+ }
89
+
90
+ ProjectDB.delete(projectMatch[1]);
91
+ return json({ success: true });
92
+ }
93
+
94
+ return null;
95
+ }