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/db.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { mkdirSync, existsSync } from "fs";
|
|
4
|
-
import { encryptObject, decryptObject } from "./crypto";
|
|
4
|
+
import { encrypt, decrypt, encryptObject, decryptObject } from "./crypto";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
5
6
|
|
|
6
7
|
// Types
|
|
7
8
|
export interface AgentFeatures {
|
|
@@ -37,6 +38,7 @@ export interface Agent {
|
|
|
37
38
|
features: AgentFeatures;
|
|
38
39
|
mcp_servers: string[]; // Array of MCP server IDs
|
|
39
40
|
project_id: string | null; // Optional project grouping
|
|
41
|
+
api_key_encrypted: string | null; // Encrypted API key for agent authentication
|
|
40
42
|
created_at: string;
|
|
41
43
|
updated_at: string;
|
|
42
44
|
}
|
|
@@ -70,6 +72,7 @@ export interface AgentRow {
|
|
|
70
72
|
features: string | null;
|
|
71
73
|
mcp_servers: string | null;
|
|
72
74
|
project_id: string | null;
|
|
75
|
+
api_key_encrypted: string | null;
|
|
73
76
|
created_at: string;
|
|
74
77
|
updated_at: string;
|
|
75
78
|
}
|
|
@@ -107,8 +110,11 @@ export interface McpServer {
|
|
|
107
110
|
command: string | null;
|
|
108
111
|
args: string | null;
|
|
109
112
|
env: Record<string, string>;
|
|
113
|
+
url: string | null; // For http type: the remote server URL
|
|
114
|
+
headers: Record<string, string>; // For http type: auth headers
|
|
110
115
|
port: number | null;
|
|
111
116
|
status: "stopped" | "running";
|
|
117
|
+
source: string | null; // e.g., "composio", "smithery", null for local
|
|
112
118
|
created_at: string;
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -120,8 +126,11 @@ export interface McpServerRow {
|
|
|
120
126
|
command: string | null;
|
|
121
127
|
args: string | null;
|
|
122
128
|
env: string | null;
|
|
129
|
+
url: string | null;
|
|
130
|
+
headers: string | null;
|
|
123
131
|
port: number | null;
|
|
124
132
|
status: string;
|
|
133
|
+
source: string | null;
|
|
125
134
|
created_at: string;
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -348,6 +357,20 @@ function runMigrations() {
|
|
|
348
357
|
CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_id);
|
|
349
358
|
`,
|
|
350
359
|
},
|
|
360
|
+
{
|
|
361
|
+
name: "014_add_mcp_server_url_headers",
|
|
362
|
+
sql: `
|
|
363
|
+
ALTER TABLE mcp_servers ADD COLUMN url TEXT;
|
|
364
|
+
ALTER TABLE mcp_servers ADD COLUMN headers TEXT DEFAULT '{}';
|
|
365
|
+
ALTER TABLE mcp_servers ADD COLUMN source TEXT;
|
|
366
|
+
`,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "015_add_agent_api_key",
|
|
370
|
+
sql: `
|
|
371
|
+
ALTER TABLE agents ADD COLUMN api_key_encrypted TEXT;
|
|
372
|
+
`,
|
|
373
|
+
},
|
|
351
374
|
];
|
|
352
375
|
|
|
353
376
|
// Check which migrations have been applied
|
|
@@ -418,18 +441,39 @@ function runSchemaUpgrades() {
|
|
|
418
441
|
}
|
|
419
442
|
}
|
|
420
443
|
|
|
444
|
+
// Generate a unique API key for an agent
|
|
445
|
+
function generateAgentApiKey(agentId: string): string {
|
|
446
|
+
const randomPart = randomBytes(24).toString("hex");
|
|
447
|
+
return `agt_${randomPart}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
421
450
|
// Agent CRUD operations
|
|
422
451
|
export const AgentDB = {
|
|
423
|
-
//
|
|
424
|
-
|
|
452
|
+
// Get the next available port for a new agent (starting from 4100)
|
|
453
|
+
getNextAvailablePort(): number {
|
|
454
|
+
const BASE_PORT = 4100;
|
|
455
|
+
const row = db.query("SELECT MAX(port) as max_port FROM agents").get() as { max_port: number | null };
|
|
456
|
+
if (row.max_port === null) {
|
|
457
|
+
return BASE_PORT;
|
|
458
|
+
}
|
|
459
|
+
return row.max_port + 1;
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
// Create a new agent with a permanently assigned port and API key
|
|
463
|
+
create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "api_key_encrypted"> & { port?: number }): Agent {
|
|
425
464
|
const now = new Date().toISOString();
|
|
426
465
|
const featuresJson = JSON.stringify(agent.features || DEFAULT_FEATURES);
|
|
427
466
|
const mcpServersJson = JSON.stringify(agent.mcp_servers || []);
|
|
467
|
+
// Assign port permanently at creation time
|
|
468
|
+
const port = agent.port ?? this.getNextAvailablePort();
|
|
469
|
+
// Generate and encrypt API key
|
|
470
|
+
const apiKey = generateAgentApiKey(agent.id);
|
|
471
|
+
const apiKeyEncrypted = encrypt(apiKey);
|
|
428
472
|
const stmt = db.prepare(`
|
|
429
|
-
INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, project_id, status, port, created_at, updated_at)
|
|
430
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'stopped',
|
|
473
|
+
INSERT INTO agents (id, name, model, provider, system_prompt, features, mcp_servers, project_id, status, port, api_key_encrypted, created_at, updated_at)
|
|
474
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'stopped', ?, ?, ?, ?)
|
|
431
475
|
`);
|
|
432
|
-
stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, agent.project_id || null, now, now);
|
|
476
|
+
stmt.run(agent.id, agent.name, agent.model, agent.provider, agent.system_prompt, featuresJson, mcpServersJson, agent.project_id || null, port, apiKeyEncrypted, now, now);
|
|
433
477
|
return this.findById(agent.id)!;
|
|
434
478
|
},
|
|
435
479
|
|
|
@@ -523,14 +567,14 @@ export const AgentDB = {
|
|
|
523
567
|
return result.changes > 0;
|
|
524
568
|
},
|
|
525
569
|
|
|
526
|
-
// Set agent status
|
|
527
|
-
setStatus(id: string, status: "stopped" | "running"
|
|
528
|
-
return this.update(id, { status
|
|
570
|
+
// Set agent status (port is permanently assigned, don't change it)
|
|
571
|
+
setStatus(id: string, status: "stopped" | "running"): Agent | null {
|
|
572
|
+
return this.update(id, { status });
|
|
529
573
|
},
|
|
530
574
|
|
|
531
|
-
// Reset all agents to stopped (on server restart)
|
|
575
|
+
// Reset all agents to stopped (on server restart) - keep ports as they're permanent
|
|
532
576
|
resetAllStatus(): void {
|
|
533
|
-
db.run("UPDATE agents SET status = 'stopped'
|
|
577
|
+
db.run("UPDATE agents SET status = 'stopped'");
|
|
534
578
|
},
|
|
535
579
|
|
|
536
580
|
// Count agents
|
|
@@ -544,6 +588,54 @@ export const AgentDB = {
|
|
|
544
588
|
const row = db.query("SELECT COUNT(*) as count FROM agents WHERE status = 'running'").get() as { count: number };
|
|
545
589
|
return row.count;
|
|
546
590
|
},
|
|
591
|
+
|
|
592
|
+
// Get decrypted API key for an agent
|
|
593
|
+
getApiKey(id: string): string | null {
|
|
594
|
+
const agent = this.findById(id);
|
|
595
|
+
if (!agent || !agent.api_key_encrypted) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
return decrypt(agent.api_key_encrypted);
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Regenerate API key for an agent
|
|
606
|
+
regenerateApiKey(id: string): string | null {
|
|
607
|
+
const agent = this.findById(id);
|
|
608
|
+
if (!agent) return null;
|
|
609
|
+
|
|
610
|
+
const newApiKey = generateAgentApiKey(id);
|
|
611
|
+
const encrypted = encrypt(newApiKey);
|
|
612
|
+
const now = new Date().toISOString();
|
|
613
|
+
|
|
614
|
+
db.run(
|
|
615
|
+
"UPDATE agents SET api_key_encrypted = ?, updated_at = ? WHERE id = ?",
|
|
616
|
+
[encrypted, now, id]
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
return newApiKey;
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
// Ensure agent has an API key (for migration of existing agents)
|
|
623
|
+
ensureApiKey(id: string): string | null {
|
|
624
|
+
const agent = this.findById(id);
|
|
625
|
+
if (!agent) return null;
|
|
626
|
+
|
|
627
|
+
// If agent already has a key, return it
|
|
628
|
+
if (agent.api_key_encrypted) {
|
|
629
|
+
try {
|
|
630
|
+
return decrypt(agent.api_key_encrypted);
|
|
631
|
+
} catch {
|
|
632
|
+
// Key is corrupted, regenerate
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Generate new key for agents without one
|
|
637
|
+
return this.regenerateApiKey(id);
|
|
638
|
+
},
|
|
547
639
|
};
|
|
548
640
|
|
|
549
641
|
// Project CRUD operations
|
|
@@ -736,6 +828,7 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
736
828
|
features,
|
|
737
829
|
mcp_servers,
|
|
738
830
|
project_id: row.project_id,
|
|
831
|
+
api_key_encrypted: row.api_key_encrypted,
|
|
739
832
|
created_at: row.created_at,
|
|
740
833
|
updated_at: row.updated_at,
|
|
741
834
|
};
|
|
@@ -826,13 +919,17 @@ function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
|
|
|
826
919
|
export const McpServerDB = {
|
|
827
920
|
create(server: Omit<McpServer, "created_at" | "status" | "port">): McpServer {
|
|
828
921
|
const now = new Date().toISOString();
|
|
829
|
-
// Encrypt env vars (credentials) before storing
|
|
922
|
+
// Encrypt env vars and headers (credentials) before storing
|
|
830
923
|
const envEncrypted = encryptObject(server.env || {});
|
|
924
|
+
const headersEncrypted = encryptObject(server.headers || {});
|
|
831
925
|
const stmt = db.prepare(`
|
|
832
|
-
INSERT INTO mcp_servers (id, name, type, package, command, args, env, status, port, created_at)
|
|
833
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?)
|
|
926
|
+
INSERT INTO mcp_servers (id, name, type, package, command, args, env, url, headers, source, status, port, created_at)
|
|
927
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?)
|
|
834
928
|
`);
|
|
835
|
-
stmt.run(
|
|
929
|
+
stmt.run(
|
|
930
|
+
server.id, server.name, server.type, server.package, server.command, server.args,
|
|
931
|
+
envEncrypted, server.url || null, headersEncrypted, server.source || null, now
|
|
932
|
+
);
|
|
836
933
|
return this.findById(server.id)!;
|
|
837
934
|
},
|
|
838
935
|
|
|
@@ -883,6 +980,19 @@ export const McpServerDB = {
|
|
|
883
980
|
// Encrypt env vars (credentials) before storing
|
|
884
981
|
values.push(encryptObject(updates.env));
|
|
885
982
|
}
|
|
983
|
+
if (updates.url !== undefined) {
|
|
984
|
+
fields.push("url = ?");
|
|
985
|
+
values.push(updates.url);
|
|
986
|
+
}
|
|
987
|
+
if (updates.headers !== undefined) {
|
|
988
|
+
fields.push("headers = ?");
|
|
989
|
+
// Encrypt headers (may contain auth tokens) before storing
|
|
990
|
+
values.push(encryptObject(updates.headers));
|
|
991
|
+
}
|
|
992
|
+
if (updates.source !== undefined) {
|
|
993
|
+
fields.push("source = ?");
|
|
994
|
+
values.push(updates.source);
|
|
995
|
+
}
|
|
886
996
|
if (updates.port !== undefined) {
|
|
887
997
|
fields.push("port = ?");
|
|
888
998
|
values.push(updates.port);
|
|
@@ -921,8 +1031,9 @@ export const McpServerDB = {
|
|
|
921
1031
|
|
|
922
1032
|
// Helper to convert DB row to McpServer type
|
|
923
1033
|
function rowToMcpServer(row: McpServerRow): McpServer {
|
|
924
|
-
// Decrypt env vars (handles both encrypted and legacy unencrypted data)
|
|
1034
|
+
// Decrypt env vars and headers (handles both encrypted and legacy unencrypted data)
|
|
925
1035
|
const env = row.env ? decryptObject(row.env) : {};
|
|
1036
|
+
const headers = row.headers ? decryptObject(row.headers) : {};
|
|
926
1037
|
return {
|
|
927
1038
|
id: row.id,
|
|
928
1039
|
name: row.name,
|
|
@@ -931,8 +1042,11 @@ function rowToMcpServer(row: McpServerRow): McpServer {
|
|
|
931
1042
|
command: row.command,
|
|
932
1043
|
args: row.args,
|
|
933
1044
|
env,
|
|
1045
|
+
url: row.url,
|
|
1046
|
+
headers,
|
|
934
1047
|
port: row.port,
|
|
935
1048
|
status: row.status as "stopped" | "running",
|
|
1049
|
+
source: row.source,
|
|
936
1050
|
created_at: row.created_at,
|
|
937
1051
|
};
|
|
938
1052
|
}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// Composio Integration Provider
|
|
2
|
+
// https://docs.composio.dev/api-reference
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
IntegrationProvider,
|
|
6
|
+
IntegrationApp,
|
|
7
|
+
ConnectedAccount,
|
|
8
|
+
ConnectionRequest,
|
|
9
|
+
ConnectionCredentials,
|
|
10
|
+
} from "./index";
|
|
11
|
+
|
|
12
|
+
const COMPOSIO_API_BASE = "https://backend.composio.dev";
|
|
13
|
+
|
|
14
|
+
export const ComposioProvider: IntegrationProvider = {
|
|
15
|
+
id: "composio",
|
|
16
|
+
name: "Composio",
|
|
17
|
+
|
|
18
|
+
async listApps(apiKey: string): Promise<IntegrationApp[]> {
|
|
19
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/toolkits`, {
|
|
20
|
+
headers: {
|
|
21
|
+
"x-api-key": apiKey,
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
console.error("Composio listApps error:", res.status, await res.text());
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
const items = data.items || data.toolkits || data || [];
|
|
33
|
+
|
|
34
|
+
return items.map((item: any) => ({
|
|
35
|
+
id: item.slug || item.key || item.name,
|
|
36
|
+
name: item.name || item.slug,
|
|
37
|
+
slug: item.slug || item.key || item.name?.toLowerCase(),
|
|
38
|
+
description: item.meta?.description || item.description || null,
|
|
39
|
+
logo: item.meta?.logo || item.logo || null,
|
|
40
|
+
categories: (item.meta?.categories || item.categories || []).map((c: any) =>
|
|
41
|
+
typeof c === "string" ? c : c.name || c.id
|
|
42
|
+
),
|
|
43
|
+
authSchemes: item.auth_schemes || item.authSchemes || ["OAUTH2"],
|
|
44
|
+
}));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async listConnectedAccounts(apiKey: string, userId: string): Promise<ConnectedAccount[]> {
|
|
48
|
+
console.log(`Fetching connected accounts for user: ${userId}`);
|
|
49
|
+
const res = await fetch(
|
|
50
|
+
`${COMPOSIO_API_BASE}/api/v3/connected_accounts?user_id=${encodeURIComponent(userId)}&limit=100`,
|
|
51
|
+
{
|
|
52
|
+
headers: {
|
|
53
|
+
"x-api-key": apiKey,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
console.error("Composio listConnectedAccounts error:", res.status, await res.text());
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const items = data.items || data.connections || data || [];
|
|
66
|
+
console.log(`Found ${items.length} connected accounts`);
|
|
67
|
+
if (items.length > 0) {
|
|
68
|
+
console.log(`Sample account:`, JSON.stringify(items[0], null, 2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return items.map((item: any) => ({
|
|
72
|
+
id: item.id,
|
|
73
|
+
appId: item.toolkit?.slug || item.toolkit_slug || item.appId || item.app_id,
|
|
74
|
+
appName: item.toolkit?.name || item.toolkit_name || item.appName || item.toolkit?.slug,
|
|
75
|
+
status: mapStatus(item.status),
|
|
76
|
+
createdAt: item.created_at || item.createdAt || new Date().toISOString(),
|
|
77
|
+
metadata: {
|
|
78
|
+
entityId: item.entity_id || item.user_id,
|
|
79
|
+
integrationId: item.auth_config?.id,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async initiateConnection(
|
|
85
|
+
apiKey: string,
|
|
86
|
+
userId: string,
|
|
87
|
+
appSlug: string,
|
|
88
|
+
redirectUrl: string,
|
|
89
|
+
credentials?: ConnectionCredentials
|
|
90
|
+
): Promise<ConnectionRequest> {
|
|
91
|
+
const isApiKeyAuth = credentials?.authScheme === "API_KEY" && credentials?.apiKey;
|
|
92
|
+
|
|
93
|
+
console.log(`Initiating ${isApiKeyAuth ? "API_KEY" : "OAuth"} connection for ${appSlug}`);
|
|
94
|
+
|
|
95
|
+
// Step 1: Get toolkit info to find the API key field name
|
|
96
|
+
let apiKeyFieldName = "api_key"; // default
|
|
97
|
+
try {
|
|
98
|
+
const toolkitRes = await fetch(`${COMPOSIO_API_BASE}/api/v3/toolkits/${appSlug}`, {
|
|
99
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
100
|
+
});
|
|
101
|
+
if (toolkitRes.ok) {
|
|
102
|
+
const toolkitData = await toolkitRes.json();
|
|
103
|
+
// Find the API_KEY auth config details
|
|
104
|
+
const apiKeyConfig = toolkitData.auth_config_details?.find(
|
|
105
|
+
(c: any) => c.mode === "API_KEY"
|
|
106
|
+
);
|
|
107
|
+
if (apiKeyConfig?.fields?.connected_account_initiation?.required?.[0]?.name) {
|
|
108
|
+
apiKeyFieldName = apiKeyConfig.fields.connected_account_initiation.required[0].name;
|
|
109
|
+
}
|
|
110
|
+
console.log(`Toolkit ${appSlug} API key field: ${apiKeyFieldName}`);
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error(`Failed to get toolkit info:`, e);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Step 2: Get existing auth configs for this toolkit
|
|
117
|
+
const configsRes = await fetch(`${COMPOSIO_API_BASE}/api/v3/auth_configs?toolkit=${appSlug}`, {
|
|
118
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let authConfigId: string | null = null;
|
|
122
|
+
|
|
123
|
+
if (configsRes.ok) {
|
|
124
|
+
const configsData = await configsRes.json();
|
|
125
|
+
const allConfigs = configsData.items || [];
|
|
126
|
+
|
|
127
|
+
// Filter to configs for this toolkit
|
|
128
|
+
const configs = allConfigs.filter((c: any) => {
|
|
129
|
+
const toolkit = c.toolkit?.slug || c.toolkit_slug || "";
|
|
130
|
+
return toolkit.toLowerCase() === appSlug.toLowerCase();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(`Found ${configs.length} auth configs for ${appSlug}`);
|
|
134
|
+
|
|
135
|
+
if (isApiKeyAuth) {
|
|
136
|
+
const apiKeyConfig = configs.find((c: any) => c.auth_scheme === "API_KEY");
|
|
137
|
+
if (apiKeyConfig) {
|
|
138
|
+
authConfigId = apiKeyConfig.id;
|
|
139
|
+
console.log(`Using existing API_KEY config: ${authConfigId}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const oauthConfig = configs.find((c: any) =>
|
|
143
|
+
c.auth_scheme === "OAUTH2" || c.is_composio_managed
|
|
144
|
+
);
|
|
145
|
+
if (oauthConfig) {
|
|
146
|
+
authConfigId = oauthConfig.id;
|
|
147
|
+
console.log(`Using existing OAuth config: ${authConfigId}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 3: Create auth config if not found
|
|
153
|
+
if (!authConfigId) {
|
|
154
|
+
console.log(`Creating new auth config for ${appSlug}...`);
|
|
155
|
+
|
|
156
|
+
const createBody = isApiKeyAuth
|
|
157
|
+
? {
|
|
158
|
+
toolkit: { slug: appSlug },
|
|
159
|
+
auth_config: {
|
|
160
|
+
type: "use_custom_auth",
|
|
161
|
+
authScheme: "API_KEY",
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
toolkit: { slug: appSlug },
|
|
166
|
+
auth_config: {
|
|
167
|
+
type: "use_composio_managed_auth",
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const createRes = await fetch(`${COMPOSIO_API_BASE}/api/v3/auth_configs`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify(createBody),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (createRes.ok) {
|
|
178
|
+
const createData = await createRes.json();
|
|
179
|
+
authConfigId = createData.auth_config?.id;
|
|
180
|
+
console.log(`Created auth config: ${authConfigId}`);
|
|
181
|
+
} else {
|
|
182
|
+
const errText = await createRes.text();
|
|
183
|
+
console.error(`Failed to create auth config:`, errText);
|
|
184
|
+
throw new Error(`Failed to create auth config: ${errText}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!authConfigId) {
|
|
189
|
+
throw new Error(`Could not find or create auth configuration for ${appSlug}.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Step 4: Create connected account
|
|
193
|
+
const connectionBody: any = {
|
|
194
|
+
auth_config: { id: authConfigId },
|
|
195
|
+
connection: {
|
|
196
|
+
user_id: userId,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (isApiKeyAuth && credentials?.apiKey) {
|
|
201
|
+
connectionBody.connection.state = {
|
|
202
|
+
authScheme: "API_KEY",
|
|
203
|
+
val: {
|
|
204
|
+
[apiKeyFieldName]: credentials.apiKey,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
} else {
|
|
208
|
+
connectionBody.connection.callback_url = redirectUrl;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`Creating connected account...`);
|
|
212
|
+
|
|
213
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/connected_accounts`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify(connectionBody),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
const text = await res.text();
|
|
221
|
+
console.error("Composio connection error:", res.status, text);
|
|
222
|
+
throw new Error(`Failed to create connection: ${text}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const data = await res.json();
|
|
226
|
+
const status = (data.status || "").toLowerCase();
|
|
227
|
+
|
|
228
|
+
console.log(`Connection created: ${data.id}, status: ${status}`);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
redirectUrl: isApiKeyAuth ? null : (data.redirect_url || data.redirectUrl),
|
|
232
|
+
connectionId: data.id,
|
|
233
|
+
status: (status === "active" || status === "connected") ? "active" : "pending",
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async getConnectionStatus(apiKey: string, connectionId: string): Promise<ConnectedAccount | null> {
|
|
238
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/connected_accounts/${connectionId}`, {
|
|
239
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
if (res.status === 404) return null;
|
|
244
|
+
console.error("Composio getConnectionStatus error:", res.status, await res.text());
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const item = await res.json();
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
id: item.id,
|
|
252
|
+
appId: item.toolkit_slug || item.appId,
|
|
253
|
+
appName: item.toolkit_name || item.appName || item.toolkit_slug,
|
|
254
|
+
status: mapStatus(item.status),
|
|
255
|
+
createdAt: item.created_at || item.createdAt,
|
|
256
|
+
metadata: {
|
|
257
|
+
entityId: item.entity_id,
|
|
258
|
+
integrationId: item.integration_id,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async disconnect(apiKey: string, connectionId: string): Promise<boolean> {
|
|
264
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/connected_accounts/${connectionId}`, {
|
|
265
|
+
method: "DELETE",
|
|
266
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return res.ok;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// MCP Server types
|
|
274
|
+
export interface McpServer {
|
|
275
|
+
id: string;
|
|
276
|
+
name: string;
|
|
277
|
+
authConfigIds: string[];
|
|
278
|
+
mcpUrl: string;
|
|
279
|
+
toolkits: string[];
|
|
280
|
+
toolkitIcons: Record<string, string>;
|
|
281
|
+
allowedTools: string[];
|
|
282
|
+
createdAt: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function listMcpServers(apiKey: string): Promise<McpServer[]> {
|
|
286
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/mcp/servers`, {
|
|
287
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
console.error("Failed to list MCP servers:", await res.text());
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const data = await res.json();
|
|
296
|
+
const items = data.items || [];
|
|
297
|
+
|
|
298
|
+
return items.map((item: any) => ({
|
|
299
|
+
id: item.id,
|
|
300
|
+
name: item.name,
|
|
301
|
+
authConfigIds: item.auth_config_ids || [],
|
|
302
|
+
mcpUrl: item.mcp_url,
|
|
303
|
+
toolkits: item.toolkits || [],
|
|
304
|
+
toolkitIcons: item.toolkit_icons || {},
|
|
305
|
+
allowedTools: item.allowed_tools || [],
|
|
306
|
+
createdAt: item.created_at,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function createMcpServer(
|
|
311
|
+
apiKey: string,
|
|
312
|
+
name: string,
|
|
313
|
+
authConfigIds: string[],
|
|
314
|
+
allowedTools?: string[]
|
|
315
|
+
): Promise<McpServer | null> {
|
|
316
|
+
// Use auth_config_ids - Composio includes all tools by default when allowed_tools is not provided
|
|
317
|
+
const body: any = {
|
|
318
|
+
name,
|
|
319
|
+
auth_config_ids: authConfigIds,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Only set allowed_tools if explicitly provided to restrict tools
|
|
323
|
+
// If not provided, Composio enables all tools by default
|
|
324
|
+
if (allowedTools?.length) {
|
|
325
|
+
body.allowed_tools = allowedTools;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/mcp/servers`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify(body),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (!res.ok) {
|
|
335
|
+
const errText = await res.text();
|
|
336
|
+
console.error("Failed to create MCP server:", errText);
|
|
337
|
+
throw new Error(`Failed to create MCP server: ${errText}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const item = await res.json();
|
|
341
|
+
return {
|
|
342
|
+
id: item.id,
|
|
343
|
+
name: item.name,
|
|
344
|
+
authConfigIds: item.auth_config_ids || [],
|
|
345
|
+
mcpUrl: item.mcp_url,
|
|
346
|
+
toolkits: item.toolkits || [],
|
|
347
|
+
toolkitIcons: item.toolkit_icons || {},
|
|
348
|
+
allowedTools: item.allowed_tools || [],
|
|
349
|
+
createdAt: item.created_at,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function deleteMcpServer(apiKey: string, serverId: string): Promise<boolean> {
|
|
354
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/mcp/servers/${serverId}`, {
|
|
355
|
+
method: "DELETE",
|
|
356
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
357
|
+
});
|
|
358
|
+
return res.ok;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Create a server instance for a user
|
|
362
|
+
export async function createMcpServerInstance(
|
|
363
|
+
apiKey: string,
|
|
364
|
+
serverId: string,
|
|
365
|
+
userId: string
|
|
366
|
+
): Promise<{ id: string; instanceId: string } | null> {
|
|
367
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/mcp/servers/${serverId}/instances`, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
370
|
+
body: JSON.stringify({ user_id: userId }),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!res.ok) {
|
|
374
|
+
const errText = await res.text();
|
|
375
|
+
console.error("Failed to create MCP server instance:", errText);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const data = await res.json();
|
|
380
|
+
return {
|
|
381
|
+
id: data.id,
|
|
382
|
+
instanceId: data.instance_id,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Get user_id from connected accounts for a specific auth config
|
|
387
|
+
export async function getUserIdForAuthConfig(
|
|
388
|
+
apiKey: string,
|
|
389
|
+
authConfigId: string
|
|
390
|
+
): Promise<string | null> {
|
|
391
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/connected_accounts?limit=100`, {
|
|
392
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (!res.ok) return null;
|
|
396
|
+
|
|
397
|
+
const data = await res.json();
|
|
398
|
+
const items = data.items || [];
|
|
399
|
+
|
|
400
|
+
// Find an active connected account for this auth config
|
|
401
|
+
const account = items.find((item: any) =>
|
|
402
|
+
item.auth_config?.id === authConfigId && item.status === "ACTIVE"
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return account?.user_id || null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Get auth config ID for a connected account's toolkit
|
|
409
|
+
export async function getAuthConfigForToolkit(
|
|
410
|
+
apiKey: string,
|
|
411
|
+
toolkitSlug: string,
|
|
412
|
+
authScheme: "API_KEY" | "OAUTH2" = "API_KEY"
|
|
413
|
+
): Promise<string | null> {
|
|
414
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/auth_configs?toolkit=${toolkitSlug}`, {
|
|
415
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (!res.ok) return null;
|
|
419
|
+
|
|
420
|
+
const data = await res.json();
|
|
421
|
+
const configs = (data.items || []).filter((c: any) => {
|
|
422
|
+
const toolkit = c.toolkit?.slug || "";
|
|
423
|
+
return toolkit.toLowerCase() === toolkitSlug.toLowerCase();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const config = configs.find((c: any) => c.auth_scheme === authScheme);
|
|
427
|
+
return config?.id || null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function mapStatus(status: string): ConnectedAccount["status"] {
|
|
431
|
+
const s = (status || "").toLowerCase();
|
|
432
|
+
if (s === "active" || s === "connected") return "active";
|
|
433
|
+
if (s === "pending" || s === "initiated") return "pending";
|
|
434
|
+
if (s === "failed" || s === "error") return "failed";
|
|
435
|
+
if (s === "expired") return "expired";
|
|
436
|
+
return "pending";
|
|
437
|
+
}
|