apteva 0.2.8 → 0.2.10
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/dist/App.44ge5b89.js +218 -0
- package/dist/index.html +2 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/binary.ts +36 -36
- package/src/db.ts +130 -16
- package/src/integrations/composio.ts +437 -0
- package/src/integrations/index.ts +80 -0
- package/src/openapi.ts +1724 -0
- package/src/routes/api.ts +599 -107
- package/src/server.ts +82 -8
- package/src/web/App.tsx +3 -0
- package/src/web/components/agents/AgentPanel.tsx +84 -38
- package/src/web/components/api/ApiDocsPage.tsx +583 -0
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +183 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
- package/src/web/components/mcp/McpPage.tsx +242 -83
- package/src/web/components/settings/SettingsPage.tsx +24 -9
- package/src/web/components/tasks/TasksPage.tsx +1 -1
- package/src/web/index.html +1 -1
- package/src/web/types.ts +4 -1
- package/dist/App.hzbfeg94.js +0 -217
package/src/routes/api.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "bun";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { mkdirSync, existsSync, rmSync } from "fs";
|
|
5
|
-
import { agentProcesses, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
|
|
5
|
+
import { agentProcesses, agentsStarting, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR, telemetryBroadcaster, type TelemetryEvent } from "../server";
|
|
6
6
|
import { AgentDB, McpServerDB, TelemetryDB, UserDB, ProjectDB, generateId, type Agent, type AgentFeatures, type McpServer, type Project } from "../db";
|
|
7
7
|
import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
|
|
8
8
|
import { createUser, hashPassword, validatePassword } from "../auth";
|
|
@@ -23,7 +23,14 @@ import {
|
|
|
23
23
|
callMcpTool,
|
|
24
24
|
getMcpProcess,
|
|
25
25
|
getMcpProxyUrl,
|
|
26
|
+
getHttpMcpClient,
|
|
26
27
|
} from "../mcp-client";
|
|
28
|
+
import { openApiSpec } from "../openapi";
|
|
29
|
+
import { getProvider, getProviderIds, registerProvider } from "../integrations";
|
|
30
|
+
import { ComposioProvider } from "../integrations/composio";
|
|
31
|
+
|
|
32
|
+
// Register integration providers
|
|
33
|
+
registerProvider(ComposioProvider);
|
|
27
34
|
|
|
28
35
|
// Data directory for agent instances (in ~/.apteva/agents/)
|
|
29
36
|
const AGENTS_DATA_DIR = process.env.DATA_DIR
|
|
@@ -43,6 +50,7 @@ function debug(...args: unknown[]) {
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
// Wait for agent to be healthy (with timeout)
|
|
53
|
+
// Note: /health endpoint is whitelisted in agent, no auth needed
|
|
46
54
|
async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
|
|
47
55
|
for (let i = 0; i < maxAttempts; i++) {
|
|
48
56
|
try {
|
|
@@ -58,41 +66,56 @@ async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200)
|
|
|
58
66
|
return false;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
// Make authenticated request to agent
|
|
70
|
+
async function agentFetch(
|
|
71
|
+
agentId: string,
|
|
72
|
+
port: number,
|
|
73
|
+
endpoint: string,
|
|
74
|
+
options: RequestInit = {}
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
const apiKey = AgentDB.getApiKey(agentId);
|
|
77
|
+
const headers: Record<string, string> = {
|
|
78
|
+
...(options.headers as Record<string, string> || {}),
|
|
79
|
+
};
|
|
80
|
+
if (apiKey) {
|
|
81
|
+
headers["X-API-Key"] = apiKey;
|
|
82
|
+
}
|
|
83
|
+
return fetch(`http://localhost:${port}${endpoint}`, {
|
|
84
|
+
...options,
|
|
85
|
+
headers,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
// Build agent config from apteva agent data
|
|
62
90
|
// Note: POST /config expects flat structure WITHOUT "agent" wrapper
|
|
63
91
|
function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
64
92
|
const features = agent.features;
|
|
65
93
|
|
|
66
94
|
// Get MCP server details for the agent's selected servers
|
|
67
|
-
// Supports both local servers and Composio configs (prefixed with "composio:")
|
|
68
95
|
const mcpServers: Array<{ name: string; type: "http"; url: string; headers: Record<string, string>; enabled: boolean }> = [];
|
|
69
96
|
|
|
70
97
|
for (const id of agent.mcp_servers || []) {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
headers: {},
|
|
93
|
-
enabled: true,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
98
|
+
const server = McpServerDB.findById(id);
|
|
99
|
+
if (!server) continue;
|
|
100
|
+
|
|
101
|
+
if (server.type === "http" && server.url) {
|
|
102
|
+
// Remote HTTP server (Composio, Smithery, or custom)
|
|
103
|
+
mcpServers.push({
|
|
104
|
+
name: server.name,
|
|
105
|
+
type: "http",
|
|
106
|
+
url: server.url,
|
|
107
|
+
headers: server.headers || {},
|
|
108
|
+
enabled: true,
|
|
109
|
+
});
|
|
110
|
+
} else if (server.status === "running" && server.port) {
|
|
111
|
+
// Local MCP server (npm, github, custom)
|
|
112
|
+
mcpServers.push({
|
|
113
|
+
name: server.name,
|
|
114
|
+
type: "http",
|
|
115
|
+
url: `http://localhost:${server.port}/mcp`,
|
|
116
|
+
headers: {},
|
|
117
|
+
enabled: true,
|
|
118
|
+
});
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
|
|
@@ -196,9 +219,10 @@ function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
|
196
219
|
}
|
|
197
220
|
|
|
198
221
|
// Push config to running agent
|
|
199
|
-
|
|
222
|
+
// Push config to running agent (with authentication)
|
|
223
|
+
async function pushConfigToAgent(agentId: string, port: number, config: any): Promise<{ success: boolean; error?: string }> {
|
|
200
224
|
try {
|
|
201
|
-
const res = await
|
|
225
|
+
const res = await agentFetch(agentId, port, "/config", {
|
|
202
226
|
method: "POST",
|
|
203
227
|
headers: { "Content-Type": "application/json" },
|
|
204
228
|
body: JSON.stringify(config),
|
|
@@ -217,38 +241,85 @@ async function pushConfigToAgent(port: number, config: any): Promise<{ success:
|
|
|
217
241
|
// Exported helper to start an agent process (used by API route and auto-restart)
|
|
218
242
|
export async function startAgentProcess(
|
|
219
243
|
agent: Agent,
|
|
220
|
-
options: { silent?: boolean } = {}
|
|
244
|
+
options: { silent?: boolean; cleanData?: boolean } = {}
|
|
221
245
|
): Promise<{ success: boolean; port?: number; error?: string }> {
|
|
222
|
-
const { silent = false } = options;
|
|
246
|
+
const { silent = false, cleanData = false } = options;
|
|
223
247
|
|
|
224
248
|
// Check if binary exists
|
|
225
249
|
if (!binaryExists(BIN_DIR)) {
|
|
226
250
|
return { success: false, error: "Agent binary not available" };
|
|
227
251
|
}
|
|
228
252
|
|
|
229
|
-
// Check if already running
|
|
253
|
+
// Check if already running (process map)
|
|
230
254
|
if (agentProcesses.has(agent.id)) {
|
|
231
255
|
return { success: false, error: "Agent already running" };
|
|
232
256
|
}
|
|
233
257
|
|
|
258
|
+
// Check if already being started (race condition prevention)
|
|
259
|
+
if (agentsStarting.has(agent.id)) {
|
|
260
|
+
return { success: false, error: "Agent is already starting" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Mark as starting
|
|
264
|
+
agentsStarting.add(agent.id);
|
|
265
|
+
|
|
234
266
|
// Get the API key for the agent's provider
|
|
235
267
|
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
236
268
|
if (!providerKey) {
|
|
269
|
+
agentsStarting.delete(agent.id);
|
|
237
270
|
return { success: false, error: `No API key for provider: ${agent.provider}` };
|
|
238
271
|
}
|
|
239
272
|
|
|
240
273
|
// Get provider config for env var name
|
|
241
274
|
const providerConfig = PROVIDERS[agent.provider as ProviderId];
|
|
242
275
|
if (!providerConfig) {
|
|
276
|
+
agentsStarting.delete(agent.id);
|
|
243
277
|
return { success: false, error: `Unknown provider: ${agent.provider}` };
|
|
244
278
|
}
|
|
245
279
|
|
|
246
|
-
//
|
|
247
|
-
const port =
|
|
280
|
+
// Use agent's permanently assigned port
|
|
281
|
+
const port = agent.port;
|
|
282
|
+
if (!port) {
|
|
283
|
+
agentsStarting.delete(agent.id);
|
|
284
|
+
return { success: false, error: "Agent has no assigned port" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Get or create API key for the agent
|
|
288
|
+
const agentApiKey = AgentDB.ensureApiKey(agent.id);
|
|
289
|
+
if (!agentApiKey) {
|
|
290
|
+
agentsStarting.delete(agent.id);
|
|
291
|
+
return { success: false, error: "Failed to get/create agent API key" };
|
|
292
|
+
}
|
|
248
293
|
|
|
249
294
|
try {
|
|
250
|
-
//
|
|
295
|
+
// Check if something is already running on this port (orphaned process)
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
|
|
298
|
+
if (res.ok) {
|
|
299
|
+
// Something is running - try to shut it down
|
|
300
|
+
if (!silent) {
|
|
301
|
+
console.log(` Port ${port} in use, stopping orphaned process...`);
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(1000) });
|
|
305
|
+
await new Promise(r => setTimeout(r, 500)); // Wait for shutdown
|
|
306
|
+
} catch {
|
|
307
|
+
// Shutdown failed - process might not support it
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Port is free - good
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Handle data directory
|
|
251
315
|
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
316
|
+
if (cleanData && existsSync(agentDataDir)) {
|
|
317
|
+
// Clean old data if requested
|
|
318
|
+
rmSync(agentDataDir, { recursive: true, force: true });
|
|
319
|
+
if (!silent) {
|
|
320
|
+
console.log(` Cleaned old data directory`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
252
323
|
if (!existsSync(agentDataDir)) {
|
|
253
324
|
mkdirSync(agentDataDir, { recursive: true });
|
|
254
325
|
}
|
|
@@ -259,11 +330,12 @@ export async function startAgentProcess(
|
|
|
259
330
|
console.log(` Data dir: ${agentDataDir}`);
|
|
260
331
|
}
|
|
261
332
|
|
|
262
|
-
// Build environment with provider key
|
|
333
|
+
// Build environment with provider key and agent API key
|
|
263
334
|
const env: Record<string, string> = {
|
|
264
335
|
...process.env as Record<string, string>,
|
|
265
336
|
PORT: String(port),
|
|
266
337
|
DATA_DIR: agentDataDir,
|
|
338
|
+
AGENT_API_KEY: agentApiKey,
|
|
267
339
|
[providerConfig.envVar]: providerKey,
|
|
268
340
|
};
|
|
269
341
|
|
|
@@ -274,7 +346,8 @@ export async function startAgentProcess(
|
|
|
274
346
|
stderr: "ignore",
|
|
275
347
|
});
|
|
276
348
|
|
|
277
|
-
|
|
349
|
+
// Store process with port for tracking
|
|
350
|
+
agentProcesses.set(agent.id, { proc, port });
|
|
278
351
|
|
|
279
352
|
// Wait for agent to be healthy
|
|
280
353
|
if (!silent) {
|
|
@@ -287,6 +360,7 @@ export async function startAgentProcess(
|
|
|
287
360
|
}
|
|
288
361
|
proc.kill();
|
|
289
362
|
agentProcesses.delete(agent.id);
|
|
363
|
+
agentsStarting.delete(agent.id);
|
|
290
364
|
return { success: false, error: "Health check timeout" };
|
|
291
365
|
}
|
|
292
366
|
|
|
@@ -295,7 +369,7 @@ export async function startAgentProcess(
|
|
|
295
369
|
console.log(` Pushing configuration...`);
|
|
296
370
|
}
|
|
297
371
|
const config = buildAgentConfig(agent, providerKey);
|
|
298
|
-
const configResult = await pushConfigToAgent(port, config);
|
|
372
|
+
const configResult = await pushConfigToAgent(agent.id, port, config);
|
|
299
373
|
if (!configResult.success) {
|
|
300
374
|
if (!silent) {
|
|
301
375
|
console.error(` Failed to configure agent: ${configResult.error}`);
|
|
@@ -305,15 +379,17 @@ export async function startAgentProcess(
|
|
|
305
379
|
console.log(` Configuration applied successfully`);
|
|
306
380
|
}
|
|
307
381
|
|
|
308
|
-
// Update status in database
|
|
309
|
-
AgentDB.setStatus(agent.id, "running"
|
|
382
|
+
// Update status in database (port is already set, just update status)
|
|
383
|
+
AgentDB.setStatus(agent.id, "running");
|
|
310
384
|
|
|
311
385
|
if (!silent) {
|
|
312
386
|
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
313
387
|
}
|
|
314
388
|
|
|
389
|
+
agentsStarting.delete(agent.id);
|
|
315
390
|
return { success: true, port };
|
|
316
391
|
} catch (err) {
|
|
392
|
+
agentsStarting.delete(agent.id);
|
|
317
393
|
if (!silent) {
|
|
318
394
|
console.error(`Failed to start agent: ${err}`);
|
|
319
395
|
}
|
|
@@ -379,6 +455,11 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
379
455
|
});
|
|
380
456
|
}
|
|
381
457
|
|
|
458
|
+
// GET /api/openapi - OpenAPI spec (no auth required)
|
|
459
|
+
if (path === "/api/openapi" && method === "GET") {
|
|
460
|
+
return json(openApiSpec);
|
|
461
|
+
}
|
|
462
|
+
|
|
382
463
|
// GET /api/agents - List all agents
|
|
383
464
|
if (path === "/api/agents" && method === "GET") {
|
|
384
465
|
const agents = AgentDB.findAll();
|
|
@@ -452,7 +533,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
452
533
|
const providerKey = ProviderKeys.getDecrypted(updated.provider);
|
|
453
534
|
if (providerKey) {
|
|
454
535
|
const config = buildAgentConfig(updated, providerKey);
|
|
455
|
-
const configResult = await pushConfigToAgent(updated.port, config);
|
|
536
|
+
const configResult = await pushConfigToAgent(updated.id, updated.port, config);
|
|
456
537
|
if (!configResult.success) {
|
|
457
538
|
console.error(`Failed to push config to running agent: ${configResult.error}`);
|
|
458
539
|
}
|
|
@@ -474,9 +555,9 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
474
555
|
}
|
|
475
556
|
|
|
476
557
|
// Stop the agent if running
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
proc.kill();
|
|
558
|
+
const agentProc = agentProcesses.get(agentId);
|
|
559
|
+
if (agentProc) {
|
|
560
|
+
agentProc.proc.kill();
|
|
480
561
|
agentProcesses.delete(agentId);
|
|
481
562
|
}
|
|
482
563
|
|
|
@@ -498,6 +579,46 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
498
579
|
return json({ success: true });
|
|
499
580
|
}
|
|
500
581
|
|
|
582
|
+
// GET /api/agents/:id/api-key - Get the agent's API key (masked)
|
|
583
|
+
const apiKeyGetMatch = path.match(/^\/api\/agents\/([^/]+)\/api-key$/);
|
|
584
|
+
if (apiKeyGetMatch && method === "GET") {
|
|
585
|
+
const agent = AgentDB.findById(apiKeyGetMatch[1]);
|
|
586
|
+
if (!agent) {
|
|
587
|
+
return json({ error: "Agent not found" }, 404);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const apiKey = AgentDB.getApiKey(agent.id);
|
|
591
|
+
if (!apiKey) {
|
|
592
|
+
return json({ error: "No API key found for this agent" }, 404);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Return masked key (show only first 8 chars)
|
|
596
|
+
const masked = apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length - 4);
|
|
597
|
+
return json({
|
|
598
|
+
apiKey: masked,
|
|
599
|
+
hasKey: true,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// POST /api/agents/:id/api-key - Regenerate the agent's API key
|
|
604
|
+
if (apiKeyGetMatch && method === "POST") {
|
|
605
|
+
const agent = AgentDB.findById(apiKeyGetMatch[1]);
|
|
606
|
+
if (!agent) {
|
|
607
|
+
return json({ error: "Agent not found" }, 404);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const newKey = AgentDB.regenerateApiKey(agent.id);
|
|
611
|
+
if (!newKey) {
|
|
612
|
+
return json({ error: "Failed to regenerate API key" }, 500);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Return the full new key (only time it's fully visible)
|
|
616
|
+
return json({
|
|
617
|
+
apiKey: newKey,
|
|
618
|
+
message: "API key regenerated. This is the only time the full key will be shown.",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
501
622
|
// POST /api/agents/:id/start - Start an agent
|
|
502
623
|
const startMatch = path.match(/^\/api\/agents\/([^/]+)\/start$/);
|
|
503
624
|
if (startMatch && method === "POST") {
|
|
@@ -523,10 +644,10 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
523
644
|
return json({ error: "Agent not found" }, 404);
|
|
524
645
|
}
|
|
525
646
|
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
console.log(`Stopping agent ${agent.name} (pid: ${proc.pid})...`);
|
|
529
|
-
proc.kill();
|
|
647
|
+
const agentProc = agentProcesses.get(agent.id);
|
|
648
|
+
if (agentProc) {
|
|
649
|
+
console.log(`Stopping agent ${agent.name} (pid: ${agentProc.proc.pid})...`);
|
|
650
|
+
agentProc.proc.kill();
|
|
530
651
|
agentProcesses.delete(agent.id);
|
|
531
652
|
}
|
|
532
653
|
|
|
@@ -549,13 +670,10 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
549
670
|
try {
|
|
550
671
|
const body = await req.json();
|
|
551
672
|
|
|
552
|
-
// Proxy to the agent's /chat endpoint
|
|
553
|
-
const
|
|
554
|
-
const response = await fetch(agentUrl, {
|
|
673
|
+
// Proxy to the agent's /chat endpoint with authentication
|
|
674
|
+
const response = await agentFetch(agent.id, agent.port, "/chat", {
|
|
555
675
|
method: "POST",
|
|
556
|
-
headers: {
|
|
557
|
-
"Content-Type": "application/json",
|
|
558
|
-
},
|
|
676
|
+
headers: { "Content-Type": "application/json" },
|
|
559
677
|
body: JSON.stringify(body),
|
|
560
678
|
});
|
|
561
679
|
|
|
@@ -595,8 +713,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
595
713
|
}
|
|
596
714
|
|
|
597
715
|
try {
|
|
598
|
-
const
|
|
599
|
-
const response = await fetch(agentUrl, {
|
|
716
|
+
const response = await agentFetch(agent.id, agent.port, "/threads", {
|
|
600
717
|
method: "GET",
|
|
601
718
|
headers: { "Accept": "application/json" },
|
|
602
719
|
});
|
|
@@ -627,8 +744,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
627
744
|
|
|
628
745
|
try {
|
|
629
746
|
const body = await req.json().catch(() => ({}));
|
|
630
|
-
const
|
|
631
|
-
const response = await fetch(agentUrl, {
|
|
747
|
+
const response = await agentFetch(agent.id, agent.port, "/threads", {
|
|
632
748
|
method: "POST",
|
|
633
749
|
headers: { "Content-Type": "application/json" },
|
|
634
750
|
body: JSON.stringify(body),
|
|
@@ -661,8 +777,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
661
777
|
|
|
662
778
|
try {
|
|
663
779
|
const threadId = threadDetailMatch[2];
|
|
664
|
-
const
|
|
665
|
-
const response = await fetch(agentUrl, {
|
|
780
|
+
const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
|
|
666
781
|
method: "GET",
|
|
667
782
|
headers: { "Accept": "application/json" },
|
|
668
783
|
});
|
|
@@ -693,8 +808,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
693
808
|
|
|
694
809
|
try {
|
|
695
810
|
const threadId = threadDetailMatch[2];
|
|
696
|
-
const
|
|
697
|
-
const response = await fetch(agentUrl, {
|
|
811
|
+
const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}`, {
|
|
698
812
|
method: "DELETE",
|
|
699
813
|
});
|
|
700
814
|
|
|
@@ -724,8 +838,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
724
838
|
|
|
725
839
|
try {
|
|
726
840
|
const threadId = threadMessagesMatch[2];
|
|
727
|
-
const
|
|
728
|
-
const response = await fetch(agentUrl, {
|
|
841
|
+
const response = await agentFetch(agent.id, agent.port, `/threads/${threadId}/messages`, {
|
|
729
842
|
method: "GET",
|
|
730
843
|
headers: { "Accept": "application/json" },
|
|
731
844
|
});
|
|
@@ -760,8 +873,8 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
760
873
|
try {
|
|
761
874
|
const url = new URL(req.url);
|
|
762
875
|
const threadId = url.searchParams.get("thread_id") || "";
|
|
763
|
-
const
|
|
764
|
-
const response = await
|
|
876
|
+
const endpoint = `/memories${threadId ? `?thread_id=${threadId}` : ""}`;
|
|
877
|
+
const response = await agentFetch(agent.id, agent.port, endpoint, {
|
|
765
878
|
method: "GET",
|
|
766
879
|
headers: { "Accept": "application/json" },
|
|
767
880
|
});
|
|
@@ -791,8 +904,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
791
904
|
}
|
|
792
905
|
|
|
793
906
|
try {
|
|
794
|
-
const
|
|
795
|
-
const response = await fetch(agentUrl, { method: "DELETE" });
|
|
907
|
+
const response = await agentFetch(agent.id, agent.port, "/memories", { method: "DELETE" });
|
|
796
908
|
|
|
797
909
|
if (!response.ok) {
|
|
798
910
|
const errorText = await response.text();
|
|
@@ -820,8 +932,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
820
932
|
|
|
821
933
|
try {
|
|
822
934
|
const memoryId = memoryDeleteMatch[2];
|
|
823
|
-
const
|
|
824
|
-
const response = await fetch(agentUrl, { method: "DELETE" });
|
|
935
|
+
const response = await agentFetch(agent.id, agent.port, `/memories/${memoryId}`, { method: "DELETE" });
|
|
825
936
|
|
|
826
937
|
if (!response.ok) {
|
|
827
938
|
const errorText = await response.text();
|
|
@@ -855,8 +966,8 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
855
966
|
if (url.searchParams.get("thread_id")) params.set("thread_id", url.searchParams.get("thread_id")!);
|
|
856
967
|
if (url.searchParams.get("limit")) params.set("limit", url.searchParams.get("limit")!);
|
|
857
968
|
|
|
858
|
-
const
|
|
859
|
-
const response = await
|
|
969
|
+
const endpoint = `/files${params.toString() ? `?${params}` : ""}`;
|
|
970
|
+
const response = await agentFetch(agent.id, agent.port, endpoint, {
|
|
860
971
|
method: "GET",
|
|
861
972
|
headers: { "Accept": "application/json" },
|
|
862
973
|
});
|
|
@@ -888,8 +999,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
888
999
|
|
|
889
1000
|
try {
|
|
890
1001
|
const fileId = fileGetMatch[2];
|
|
891
|
-
const
|
|
892
|
-
const response = await fetch(agentUrl, {
|
|
1002
|
+
const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
|
|
893
1003
|
method: "GET",
|
|
894
1004
|
headers: { "Accept": "application/json" },
|
|
895
1005
|
});
|
|
@@ -920,8 +1030,9 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
920
1030
|
|
|
921
1031
|
try {
|
|
922
1032
|
const fileId = fileGetMatch[2];
|
|
923
|
-
const
|
|
924
|
-
|
|
1033
|
+
const response = await agentFetch(agent.id, agent.port, `/files/${fileId}`, {
|
|
1034
|
+
method: "DELETE",
|
|
1035
|
+
});
|
|
925
1036
|
|
|
926
1037
|
if (!response.ok) {
|
|
927
1038
|
const errorText = await response.text();
|
|
@@ -949,8 +1060,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
949
1060
|
|
|
950
1061
|
try {
|
|
951
1062
|
const fileId = fileDownloadMatch[2];
|
|
952
|
-
const
|
|
953
|
-
const response = await fetch(agentUrl);
|
|
1063
|
+
const response = await agentFetch(agent.id, agent.port, `/files/${fileId}/download`);
|
|
954
1064
|
|
|
955
1065
|
if (!response.ok) {
|
|
956
1066
|
const errorText = await response.text();
|
|
@@ -987,8 +1097,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
987
1097
|
}
|
|
988
1098
|
|
|
989
1099
|
try {
|
|
990
|
-
const
|
|
991
|
-
const response = await fetch(agentUrl, {
|
|
1100
|
+
const response = await agentFetch(agent.id, agent.port, "/discovery/agents", {
|
|
992
1101
|
method: "GET",
|
|
993
1102
|
headers: { "Accept": "application/json" },
|
|
994
1103
|
});
|
|
@@ -1385,13 +1494,19 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1385
1494
|
|
|
1386
1495
|
// POST /api/version/update - Download/install latest agent binary
|
|
1387
1496
|
if (path === "/api/version/update" && method === "POST") {
|
|
1388
|
-
//
|
|
1497
|
+
// Get all running agents to restart later
|
|
1389
1498
|
const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
);
|
|
1499
|
+
const agentsToRestart = runningAgents.map(a => a.id);
|
|
1500
|
+
|
|
1501
|
+
// Stop all running agents
|
|
1502
|
+
for (const agent of runningAgents) {
|
|
1503
|
+
const agentProc = agentProcesses.get(agent.id);
|
|
1504
|
+
if (agentProc) {
|
|
1505
|
+
console.log(`Stopping agent ${agent.name} for update...`);
|
|
1506
|
+
agentProc.proc.kill();
|
|
1507
|
+
agentProcesses.delete(agent.id);
|
|
1508
|
+
}
|
|
1509
|
+
AgentDB.setStatus(agent.id, "stopped");
|
|
1395
1510
|
}
|
|
1396
1511
|
|
|
1397
1512
|
// Try npm install first, fall back to direct download
|
|
@@ -1401,10 +1516,31 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1401
1516
|
result = await downloadLatestBinary(BIN_DIR);
|
|
1402
1517
|
}
|
|
1403
1518
|
|
|
1404
|
-
if (result.success) {
|
|
1405
|
-
return json({ success:
|
|
1519
|
+
if (!result.success) {
|
|
1520
|
+
return json({ success: false, error: result.error }, 500);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Restart agents that were running
|
|
1524
|
+
const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
|
|
1525
|
+
for (const agentId of agentsToRestart) {
|
|
1526
|
+
const agent = AgentDB.findById(agentId);
|
|
1527
|
+
if (agent) {
|
|
1528
|
+
console.log(`Restarting agent ${agent.name} after update...`);
|
|
1529
|
+
const startResult = await startAgentProcess(agent);
|
|
1530
|
+
restartResults.push({
|
|
1531
|
+
id: agent.id,
|
|
1532
|
+
name: agent.name,
|
|
1533
|
+
success: startResult.success,
|
|
1534
|
+
error: startResult.error,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1406
1537
|
}
|
|
1407
|
-
|
|
1538
|
+
|
|
1539
|
+
return json({
|
|
1540
|
+
success: true,
|
|
1541
|
+
version: result.version,
|
|
1542
|
+
restarted: restartResults,
|
|
1543
|
+
});
|
|
1408
1544
|
}
|
|
1409
1545
|
|
|
1410
1546
|
// GET /api/health - Health check
|
|
@@ -1429,10 +1565,10 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1429
1565
|
|
|
1430
1566
|
// ==================== TASKS ====================
|
|
1431
1567
|
|
|
1432
|
-
// Helper to fetch from a running agent
|
|
1433
|
-
async function fetchFromAgent(port: number, endpoint: string): Promise<any> {
|
|
1568
|
+
// Helper to fetch from a running agent (with authentication)
|
|
1569
|
+
async function fetchFromAgent(agentId: string, port: number, endpoint: string): Promise<any> {
|
|
1434
1570
|
try {
|
|
1435
|
-
const response = await
|
|
1571
|
+
const response = await agentFetch(agentId, port, endpoint, {
|
|
1436
1572
|
headers: { "Accept": "application/json" },
|
|
1437
1573
|
});
|
|
1438
1574
|
if (response.ok) {
|
|
@@ -1453,7 +1589,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1453
1589
|
const allTasks: any[] = [];
|
|
1454
1590
|
|
|
1455
1591
|
for (const agent of runningAgents) {
|
|
1456
|
-
const data = await fetchFromAgent(agent.port!, `/tasks?status=${status}`);
|
|
1592
|
+
const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
|
|
1457
1593
|
if (data?.tasks) {
|
|
1458
1594
|
// Add agent info to each task
|
|
1459
1595
|
for (const task of data.tasks) {
|
|
@@ -1489,7 +1625,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1489
1625
|
const url = new URL(req.url);
|
|
1490
1626
|
const status = url.searchParams.get("status") || "all";
|
|
1491
1627
|
|
|
1492
|
-
const data = await fetchFromAgent(agent.port, `/tasks?status=${status}`);
|
|
1628
|
+
const data = await fetchFromAgent(agent.id, agent.port, `/tasks?status=${status}`);
|
|
1493
1629
|
if (!data) {
|
|
1494
1630
|
return json({ error: "Failed to fetch tasks from agent" }, 500);
|
|
1495
1631
|
}
|
|
@@ -1508,7 +1644,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1508
1644
|
let runningTasks = 0;
|
|
1509
1645
|
|
|
1510
1646
|
for (const agent of runningAgents) {
|
|
1511
|
-
const data = await fetchFromAgent(agent.port!, "/tasks?status=all");
|
|
1647
|
+
const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
|
|
1512
1648
|
if (data?.tasks) {
|
|
1513
1649
|
totalTasks += data.tasks.length;
|
|
1514
1650
|
for (const task of data.tasks) {
|
|
@@ -1633,7 +1769,161 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1633
1769
|
}
|
|
1634
1770
|
}
|
|
1635
1771
|
|
|
1636
|
-
// ============
|
|
1772
|
+
// ============ Generic Integration Providers ============
|
|
1773
|
+
// These endpoints work with any registered provider (composio, smithery, etc.)
|
|
1774
|
+
|
|
1775
|
+
// GET /api/integrations/providers - List available integration providers
|
|
1776
|
+
if (path === "/api/integrations/providers" && method === "GET") {
|
|
1777
|
+
const providerIds = getProviderIds();
|
|
1778
|
+
const providers = providerIds.map(id => {
|
|
1779
|
+
const provider = getProvider(id);
|
|
1780
|
+
const hasKey = !!ProviderKeys.getDecrypted(id);
|
|
1781
|
+
return {
|
|
1782
|
+
id,
|
|
1783
|
+
name: provider?.name || id,
|
|
1784
|
+
connected: hasKey,
|
|
1785
|
+
};
|
|
1786
|
+
});
|
|
1787
|
+
return json({ providers });
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// GET /api/integrations/:provider/apps - List available apps from a provider
|
|
1791
|
+
const appsMatch = path.match(/^\/api\/integrations\/([^/]+)\/apps$/);
|
|
1792
|
+
if (appsMatch && method === "GET") {
|
|
1793
|
+
const providerId = appsMatch[1];
|
|
1794
|
+
const provider = getProvider(providerId);
|
|
1795
|
+
if (!provider) {
|
|
1796
|
+
return json({ error: `Unknown provider: ${providerId}` }, 404);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const apiKey = ProviderKeys.getDecrypted(providerId);
|
|
1800
|
+
if (!apiKey) {
|
|
1801
|
+
return json({ error: `${provider.name} API key not configured`, apps: [] }, 200);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
try {
|
|
1805
|
+
const apps = await provider.listApps(apiKey);
|
|
1806
|
+
return json({ apps });
|
|
1807
|
+
} catch (e) {
|
|
1808
|
+
console.error(`Failed to list apps from ${providerId}:`, e);
|
|
1809
|
+
return json({ error: "Failed to fetch apps" }, 500);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// GET /api/integrations/:provider/connected - List user's connected accounts
|
|
1814
|
+
const connectedMatch = path.match(/^\/api\/integrations\/([^/]+)\/connected$/);
|
|
1815
|
+
if (connectedMatch && method === "GET") {
|
|
1816
|
+
const providerId = connectedMatch[1];
|
|
1817
|
+
const provider = getProvider(providerId);
|
|
1818
|
+
if (!provider) {
|
|
1819
|
+
return json({ error: `Unknown provider: ${providerId}` }, 404);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const apiKey = ProviderKeys.getDecrypted(providerId);
|
|
1823
|
+
if (!apiKey) {
|
|
1824
|
+
return json({ error: `${provider.name} API key not configured`, accounts: [] }, 200);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Use Apteva user ID as the entity ID for the provider
|
|
1828
|
+
const userId = user?.id || "default";
|
|
1829
|
+
|
|
1830
|
+
try {
|
|
1831
|
+
const accounts = await provider.listConnectedAccounts(apiKey, userId);
|
|
1832
|
+
return json({ accounts });
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
console.error(`Failed to list connected accounts from ${providerId}:`, e);
|
|
1835
|
+
return json({ error: "Failed to fetch connected accounts" }, 500);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// POST /api/integrations/:provider/connect - Initiate connection (OAuth or API Key)
|
|
1840
|
+
const connectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connect$/);
|
|
1841
|
+
if (connectMatch && method === "POST") {
|
|
1842
|
+
const providerId = connectMatch[1];
|
|
1843
|
+
const provider = getProvider(providerId);
|
|
1844
|
+
if (!provider) {
|
|
1845
|
+
return json({ error: `Unknown provider: ${providerId}` }, 404);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
const apiKey = ProviderKeys.getDecrypted(providerId);
|
|
1849
|
+
if (!apiKey) {
|
|
1850
|
+
return json({ error: `${provider.name} API key not configured` }, 401);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
try {
|
|
1854
|
+
const body = await req.json();
|
|
1855
|
+
const { appSlug, redirectUrl, credentials } = body;
|
|
1856
|
+
|
|
1857
|
+
if (!appSlug) {
|
|
1858
|
+
return json({ error: "appSlug is required" }, 400);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Use Apteva user ID as the entity ID
|
|
1862
|
+
const userId = user?.id || "default";
|
|
1863
|
+
|
|
1864
|
+
// Default redirect URL back to our integrations page
|
|
1865
|
+
const callbackUrl = redirectUrl || `http://localhost:${process.env.PORT || 4280}/mcp?tab=hosted&connected=${appSlug}`;
|
|
1866
|
+
|
|
1867
|
+
const result = await provider.initiateConnection(apiKey, userId, appSlug, callbackUrl, credentials);
|
|
1868
|
+
return json(result);
|
|
1869
|
+
} catch (e) {
|
|
1870
|
+
console.error(`Failed to initiate connection for ${providerId}:`, e);
|
|
1871
|
+
return json({ error: `Failed to initiate connection: ${e}` }, 500);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// GET /api/integrations/:provider/connection/:id - Check connection status
|
|
1876
|
+
const connectionStatusMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
|
|
1877
|
+
if (connectionStatusMatch && method === "GET") {
|
|
1878
|
+
const providerId = connectionStatusMatch[1];
|
|
1879
|
+
const connectionId = connectionStatusMatch[2];
|
|
1880
|
+
const provider = getProvider(providerId);
|
|
1881
|
+
if (!provider) {
|
|
1882
|
+
return json({ error: `Unknown provider: ${providerId}` }, 404);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const apiKey = ProviderKeys.getDecrypted(providerId);
|
|
1886
|
+
if (!apiKey) {
|
|
1887
|
+
return json({ error: `${provider.name} API key not configured` }, 401);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
try {
|
|
1891
|
+
const connection = await provider.getConnectionStatus(apiKey, connectionId);
|
|
1892
|
+
if (!connection) {
|
|
1893
|
+
return json({ error: "Connection not found" }, 404);
|
|
1894
|
+
}
|
|
1895
|
+
return json({ connection });
|
|
1896
|
+
} catch (e) {
|
|
1897
|
+
console.error(`Failed to get connection status:`, e);
|
|
1898
|
+
return json({ error: "Failed to get connection status" }, 500);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// DELETE /api/integrations/:provider/connection/:id - Disconnect/revoke
|
|
1903
|
+
const disconnectMatch = path.match(/^\/api\/integrations\/([^/]+)\/connection\/([^/]+)$/);
|
|
1904
|
+
if (disconnectMatch && method === "DELETE") {
|
|
1905
|
+
const providerId = disconnectMatch[1];
|
|
1906
|
+
const connectionId = disconnectMatch[2];
|
|
1907
|
+
const provider = getProvider(providerId);
|
|
1908
|
+
if (!provider) {
|
|
1909
|
+
return json({ error: `Unknown provider: ${providerId}` }, 404);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const apiKey = ProviderKeys.getDecrypted(providerId);
|
|
1913
|
+
if (!apiKey) {
|
|
1914
|
+
return json({ error: `${provider.name} API key not configured` }, 401);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
try {
|
|
1918
|
+
const success = await provider.disconnect(apiKey, connectionId);
|
|
1919
|
+
return json({ success });
|
|
1920
|
+
} catch (e) {
|
|
1921
|
+
console.error(`Failed to disconnect:`, e);
|
|
1922
|
+
return json({ error: "Failed to disconnect" }, 500);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// ============ Composio-Specific Routes (MCP Configs) ============
|
|
1637
1927
|
|
|
1638
1928
|
// GET /api/integrations/composio/configs - List Composio MCP configs
|
|
1639
1929
|
if (path === "/api/integrations/composio/configs" && method === "GET") {
|
|
@@ -1658,15 +1948,13 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1658
1948
|
|
|
1659
1949
|
const data = await res.json();
|
|
1660
1950
|
|
|
1661
|
-
// Transform to our format
|
|
1951
|
+
// Transform to our format (no user_id in URLs - that's provided when adding)
|
|
1662
1952
|
const configs = (data.items || data.servers || []).map((item: any) => ({
|
|
1663
1953
|
id: item.id,
|
|
1664
1954
|
name: item.name || item.id,
|
|
1665
1955
|
toolkits: item.toolkits || item.apps || [],
|
|
1666
1956
|
toolsCount: item.toolsCount || item.tools?.length || 0,
|
|
1667
1957
|
createdAt: item.createdAt || item.created_at,
|
|
1668
|
-
// Build the MCP URL for this config
|
|
1669
|
-
mcpUrl: `https://backend.composio.dev/v3/mcp/${item.id}`,
|
|
1670
1958
|
}));
|
|
1671
1959
|
|
|
1672
1960
|
return json({ configs });
|
|
@@ -1704,19 +1992,187 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1704
1992
|
name: data.name || data.id,
|
|
1705
1993
|
toolkits: data.toolkits || data.apps || [],
|
|
1706
1994
|
tools: data.tools || [],
|
|
1707
|
-
|
|
1708
|
-
}
|
|
1995
|
+
},
|
|
1709
1996
|
});
|
|
1710
1997
|
} catch (e) {
|
|
1711
1998
|
return json({ error: "Failed to fetch config" }, 500);
|
|
1712
1999
|
}
|
|
1713
2000
|
}
|
|
1714
2001
|
|
|
2002
|
+
// POST /api/integrations/composio/configs/:id/add - Add a Composio config as an MCP server
|
|
2003
|
+
// Fetches the mcp_url directly from Composio API
|
|
2004
|
+
const composioAddMatch = path.match(/^\/api\/integrations\/composio\/configs\/([^/]+)\/add$/);
|
|
2005
|
+
if (composioAddMatch && method === "POST") {
|
|
2006
|
+
const configId = composioAddMatch[1];
|
|
2007
|
+
const apiKey = ProviderKeys.getDecrypted("composio");
|
|
2008
|
+
if (!apiKey) {
|
|
2009
|
+
return json({ error: "Composio API key not configured" }, 401);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
try {
|
|
2013
|
+
// Fetch config details from Composio to get the name and mcp_url
|
|
2014
|
+
const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${configId}`, {
|
|
2015
|
+
headers: {
|
|
2016
|
+
"x-api-key": apiKey,
|
|
2017
|
+
"Content-Type": "application/json",
|
|
2018
|
+
},
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
if (!res.ok) {
|
|
2022
|
+
const errText = await res.text();
|
|
2023
|
+
console.error("Failed to fetch Composio MCP config:", errText);
|
|
2024
|
+
return json({ error: "Failed to fetch MCP config from Composio" }, 400);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
const data = await res.json();
|
|
2028
|
+
const configName = data.name || `composio-${configId.slice(0, 8)}`;
|
|
2029
|
+
const mcpUrl = data.mcp_url;
|
|
2030
|
+
const authConfigIds = data.auth_config_ids || [];
|
|
2031
|
+
const serverInstanceCount = data.server_instance_count || 0;
|
|
2032
|
+
|
|
2033
|
+
if (!mcpUrl) {
|
|
2034
|
+
return json({ error: "MCP config does not have a URL" }, 400);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Get user_id from connected accounts for this auth config
|
|
2038
|
+
const { createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
|
|
2039
|
+
let userId: string | null = null;
|
|
2040
|
+
|
|
2041
|
+
if (authConfigIds.length > 0) {
|
|
2042
|
+
userId = await getUserIdForAuthConfig(apiKey, authConfigIds[0]);
|
|
2043
|
+
|
|
2044
|
+
// Create server instance if none exists
|
|
2045
|
+
if (serverInstanceCount === 0 && userId) {
|
|
2046
|
+
const instance = await createMcpServerInstance(apiKey, configId, userId);
|
|
2047
|
+
if (instance) {
|
|
2048
|
+
console.log(`Created server instance for user ${userId} on server ${configId}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Append user_id to mcp_url for authentication
|
|
2054
|
+
const mcpUrlWithUser = userId
|
|
2055
|
+
? `${mcpUrl}?user_id=${encodeURIComponent(userId)}`
|
|
2056
|
+
: mcpUrl;
|
|
2057
|
+
|
|
2058
|
+
// Check if already exists (match by config ID in URL)
|
|
2059
|
+
const existing = McpServerDB.findAll().find(
|
|
2060
|
+
s => s.source === "composio" && s.url?.includes(configId)
|
|
2061
|
+
);
|
|
2062
|
+
if (existing) {
|
|
2063
|
+
return json({ server: existing, message: "Server already exists" });
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// Create the MCP server entry with user_id in URL
|
|
2067
|
+
const server = McpServerDB.create({
|
|
2068
|
+
id: generateId(),
|
|
2069
|
+
name: configName,
|
|
2070
|
+
type: "http",
|
|
2071
|
+
package: null,
|
|
2072
|
+
command: null,
|
|
2073
|
+
args: null,
|
|
2074
|
+
env: {},
|
|
2075
|
+
url: mcpUrlWithUser,
|
|
2076
|
+
headers: { "x-api-key": apiKey },
|
|
2077
|
+
source: "composio",
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
return json({ server, message: "Server added successfully" });
|
|
2081
|
+
} catch (e) {
|
|
2082
|
+
console.error("Failed to add Composio config:", e);
|
|
2083
|
+
return json({ error: "Failed to add Composio config" }, 500);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// POST /api/integrations/composio/configs - Create a new MCP config from connected app
|
|
2088
|
+
if (path === "/api/integrations/composio/configs" && method === "POST") {
|
|
2089
|
+
const apiKey = ProviderKeys.getDecrypted("composio");
|
|
2090
|
+
if (!apiKey) {
|
|
2091
|
+
return json({ error: "Composio API key not configured" }, 401);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
try {
|
|
2095
|
+
const body = await req.json();
|
|
2096
|
+
const { name, toolkitSlug, authConfigId } = body;
|
|
2097
|
+
|
|
2098
|
+
if (!name || !toolkitSlug) {
|
|
2099
|
+
return json({ error: "name and toolkitSlug are required" }, 400);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// If authConfigId not provided, find it from the toolkit
|
|
2103
|
+
let configId = authConfigId;
|
|
2104
|
+
if (!configId) {
|
|
2105
|
+
const { getAuthConfigForToolkit } = await import("../integrations/composio");
|
|
2106
|
+
configId = await getAuthConfigForToolkit(apiKey, toolkitSlug);
|
|
2107
|
+
if (!configId) {
|
|
2108
|
+
return json({ error: `No auth config found for ${toolkitSlug}. Make sure you have connected this app first.` }, 400);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Create MCP server in Composio
|
|
2113
|
+
const { createMcpServer, createMcpServerInstance, getUserIdForAuthConfig } = await import("../integrations/composio");
|
|
2114
|
+
const mcpServer = await createMcpServer(apiKey, name, [configId]);
|
|
2115
|
+
|
|
2116
|
+
if (!mcpServer) {
|
|
2117
|
+
return json({ error: "Failed to create MCP config" }, 500);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// Create server instance for the user who has the connected account
|
|
2121
|
+
const userId = await getUserIdForAuthConfig(apiKey, configId);
|
|
2122
|
+
if (userId) {
|
|
2123
|
+
const instance = await createMcpServerInstance(apiKey, mcpServer.id, userId);
|
|
2124
|
+
if (!instance) {
|
|
2125
|
+
console.warn(`Created MCP server but failed to create instance for user ${userId}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Append user_id to mcp_url for authentication
|
|
2130
|
+
const mcpUrlWithUser = userId
|
|
2131
|
+
? `${mcpServer.mcpUrl}?user_id=${encodeURIComponent(userId)}`
|
|
2132
|
+
: mcpServer.mcpUrl;
|
|
2133
|
+
|
|
2134
|
+
return json({
|
|
2135
|
+
config: {
|
|
2136
|
+
id: mcpServer.id,
|
|
2137
|
+
name: mcpServer.name,
|
|
2138
|
+
toolkits: mcpServer.toolkits,
|
|
2139
|
+
mcpUrl: mcpUrlWithUser,
|
|
2140
|
+
allowedTools: mcpServer.allowedTools,
|
|
2141
|
+
userId,
|
|
2142
|
+
},
|
|
2143
|
+
}, 201);
|
|
2144
|
+
} catch (e: any) {
|
|
2145
|
+
console.error("Failed to create Composio MCP config:", e);
|
|
2146
|
+
return json({ error: e.message || "Failed to create MCP config" }, 500);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// DELETE /api/integrations/composio/configs/:id - Delete a Composio MCP config
|
|
2151
|
+
if (composioConfigMatch && method === "DELETE") {
|
|
2152
|
+
const configId = composioConfigMatch[1];
|
|
2153
|
+
const apiKey = ProviderKeys.getDecrypted("composio");
|
|
2154
|
+
if (!apiKey) {
|
|
2155
|
+
return json({ error: "Composio API key not configured" }, 401);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
try {
|
|
2159
|
+
const { deleteMcpServer } = await import("../integrations/composio");
|
|
2160
|
+
const success = await deleteMcpServer(apiKey, configId);
|
|
2161
|
+
if (!success) {
|
|
2162
|
+
return json({ error: "Failed to delete MCP config" }, 500);
|
|
2163
|
+
}
|
|
2164
|
+
return json({ success: true });
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
console.error("Failed to delete Composio config:", e);
|
|
2167
|
+
return json({ error: "Failed to delete MCP config" }, 500);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1715
2171
|
// POST /api/mcp/servers - Create/install a new MCP server
|
|
1716
2172
|
if (path === "/api/mcp/servers" && method === "POST") {
|
|
1717
2173
|
try {
|
|
1718
2174
|
const body = await req.json();
|
|
1719
|
-
const { name, type, package: pkg, command, args, env } = body;
|
|
2175
|
+
const { name, type, package: pkg, command, args, env, url, headers, source } = body;
|
|
1720
2176
|
|
|
1721
2177
|
if (!name) {
|
|
1722
2178
|
return json({ error: "Name is required" }, 400);
|
|
@@ -1730,6 +2186,9 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1730
2186
|
command: command || null,
|
|
1731
2187
|
args: args || null,
|
|
1732
2188
|
env: env || {},
|
|
2189
|
+
url: url || null,
|
|
2190
|
+
headers: headers || {},
|
|
2191
|
+
source: source || null,
|
|
1733
2192
|
});
|
|
1734
2193
|
|
|
1735
2194
|
return json({ server }, 201);
|
|
@@ -1832,7 +2291,7 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1832
2291
|
}
|
|
1833
2292
|
|
|
1834
2293
|
// Get a port for the HTTP proxy
|
|
1835
|
-
const port = getNextPort();
|
|
2294
|
+
const port = await getNextPort();
|
|
1836
2295
|
|
|
1837
2296
|
console.log(`Starting MCP server ${server.name}...`);
|
|
1838
2297
|
console.log(` Command: ${cmd.join(" ")}`);
|
|
@@ -1879,7 +2338,24 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1879
2338
|
return json({ error: "MCP server not found" }, 404);
|
|
1880
2339
|
}
|
|
1881
2340
|
|
|
1882
|
-
//
|
|
2341
|
+
// HTTP servers use remote HTTP transport
|
|
2342
|
+
if (server.type === "http" && server.url) {
|
|
2343
|
+
try {
|
|
2344
|
+
const httpClient = getHttpMcpClient(server.url, server.headers || {});
|
|
2345
|
+
const serverInfo = await httpClient.initialize();
|
|
2346
|
+
const tools = await httpClient.listTools();
|
|
2347
|
+
|
|
2348
|
+
return json({
|
|
2349
|
+
serverInfo,
|
|
2350
|
+
tools,
|
|
2351
|
+
});
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
console.error(`Failed to list HTTP MCP tools: ${err}`);
|
|
2354
|
+
return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// Stdio servers require a running process
|
|
1883
2359
|
const mcpProcess = getMcpProcess(server.id);
|
|
1884
2360
|
if (!mcpProcess) {
|
|
1885
2361
|
return json({ error: "MCP server is not running" }, 400);
|
|
@@ -1907,14 +2383,30 @@ export async function handleApiRequest(req: Request, path: string, authContext?:
|
|
|
1907
2383
|
return json({ error: "MCP server not found" }, 404);
|
|
1908
2384
|
}
|
|
1909
2385
|
|
|
1910
|
-
|
|
2386
|
+
const toolName = decodeURIComponent(mcpToolCallMatch[2]);
|
|
2387
|
+
|
|
2388
|
+
// HTTP servers use remote HTTP transport
|
|
2389
|
+
if (server.type === "http" && server.url) {
|
|
2390
|
+
try {
|
|
2391
|
+
const body = await req.json();
|
|
2392
|
+
const args = body.arguments || {};
|
|
2393
|
+
|
|
2394
|
+
const httpClient = getHttpMcpClient(server.url, server.headers || {});
|
|
2395
|
+
const result = await httpClient.callTool(toolName, args);
|
|
2396
|
+
|
|
2397
|
+
return json({ result });
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
console.error(`Failed to call HTTP MCP tool: ${err}`);
|
|
2400
|
+
return json({ error: `Failed to call tool: ${err}` }, 500);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// Stdio servers require a running process
|
|
1911
2405
|
const mcpProcess = getMcpProcess(server.id);
|
|
1912
2406
|
if (!mcpProcess) {
|
|
1913
2407
|
return json({ error: "MCP server is not running" }, 400);
|
|
1914
2408
|
}
|
|
1915
2409
|
|
|
1916
|
-
const toolName = decodeURIComponent(mcpToolCallMatch[2]);
|
|
1917
|
-
|
|
1918
2410
|
try {
|
|
1919
2411
|
const body = await req.json();
|
|
1920
2412
|
const args = body.arguments || {};
|