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/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
- // Create a new agent
424
- create(agent: Omit<Agent, "created_at" | "updated_at" | "status" | "port">): Agent {
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', NULL, ?, ?)
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", port?: number): Agent | null {
528
- return this.update(id, { status, port: port ?? null });
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', port = NULL");
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(server.id, server.name, server.type, server.package, server.command, server.args, envEncrypted, now);
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
+ }