apteva 0.4.12 → 0.4.15

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
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
2
2
  import { join } from "path";
3
3
  import { mkdirSync, existsSync } from "fs";
4
4
  import { encrypt, decrypt, encryptObject, decryptObject } from "./crypto";
5
- import { randomBytes } from "crypto";
5
+ import { randomBytes, createHash } from "crypto";
6
6
 
7
7
  // Types
8
8
  export type AgentMode = "coordinator" | "worker";
@@ -520,6 +520,146 @@ function runMigrations() {
520
520
  CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
521
521
  `,
522
522
  },
523
+ {
524
+ name: "023_create_test_cases",
525
+ sql: `
526
+ CREATE TABLE IF NOT EXISTS test_cases (
527
+ id TEXT PRIMARY KEY,
528
+ name TEXT NOT NULL,
529
+ description TEXT,
530
+ agent_id TEXT NOT NULL,
531
+ input_message TEXT NOT NULL,
532
+ eval_criteria TEXT NOT NULL,
533
+ timeout_ms INTEGER DEFAULT 60000,
534
+ project_id TEXT,
535
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
536
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
537
+ );
538
+ CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
539
+ CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
540
+ `,
541
+ },
542
+ {
543
+ name: "025_create_api_keys",
544
+ sql: `
545
+ CREATE TABLE IF NOT EXISTS api_keys (
546
+ id TEXT PRIMARY KEY,
547
+ name TEXT NOT NULL,
548
+ key_hash TEXT NOT NULL UNIQUE,
549
+ key_prefix TEXT NOT NULL,
550
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
551
+ expires_at TEXT,
552
+ last_used_at TEXT,
553
+ is_active INTEGER DEFAULT 1,
554
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
555
+ );
556
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash) WHERE is_active = 1;
557
+ CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
558
+ `,
559
+ },
560
+ {
561
+ name: "026_behavior_tests",
562
+ sql: `
563
+ -- Recreate test_cases with nullable agent_id and input_message
564
+ CREATE TABLE IF NOT EXISTS test_cases_new (
565
+ id TEXT PRIMARY KEY,
566
+ name TEXT NOT NULL,
567
+ description TEXT,
568
+ behavior TEXT,
569
+ agent_id TEXT,
570
+ input_message TEXT,
571
+ eval_criteria TEXT NOT NULL DEFAULT '',
572
+ timeout_ms INTEGER DEFAULT 300000,
573
+ project_id TEXT,
574
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
575
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
576
+ );
577
+ INSERT OR IGNORE INTO test_cases_new (id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
578
+ SELECT id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
579
+ DROP TABLE IF EXISTS test_cases;
580
+ ALTER TABLE test_cases_new RENAME TO test_cases;
581
+ CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
582
+ CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
583
+
584
+ -- Add planner columns to test_runs
585
+ ALTER TABLE test_runs ADD COLUMN generated_message TEXT;
586
+ ALTER TABLE test_runs ADD COLUMN selected_agent_id TEXT;
587
+ ALTER TABLE test_runs ADD COLUMN selected_agent_name TEXT;
588
+ ALTER TABLE test_runs ADD COLUMN planner_reasoning TEXT;
589
+ `,
590
+ },
591
+ {
592
+ name: "027_fix_test_cases_nullable",
593
+ sql: `
594
+ -- Recreate test_cases with nullable agent_id and input_message
595
+ CREATE TABLE IF NOT EXISTS test_cases_new (
596
+ id TEXT PRIMARY KEY,
597
+ name TEXT NOT NULL,
598
+ description TEXT,
599
+ behavior TEXT,
600
+ agent_id TEXT,
601
+ input_message TEXT,
602
+ eval_criteria TEXT NOT NULL DEFAULT '',
603
+ timeout_ms INTEGER DEFAULT 300000,
604
+ project_id TEXT,
605
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
606
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
607
+ );
608
+ INSERT OR IGNORE INTO test_cases_new (id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
609
+ SELECT id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
610
+ DROP TABLE IF EXISTS test_cases;
611
+ ALTER TABLE test_cases_new RENAME TO test_cases;
612
+ CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
613
+ CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
614
+ `,
615
+ },
616
+ {
617
+ name: "028_add_test_run_score",
618
+ sql: `ALTER TABLE test_runs ADD COLUMN score INTEGER;`,
619
+ },
620
+ {
621
+ name: "024_create_test_runs",
622
+ sql: `
623
+ CREATE TABLE IF NOT EXISTS test_runs (
624
+ id TEXT PRIMARY KEY,
625
+ test_case_id TEXT NOT NULL,
626
+ status TEXT NOT NULL DEFAULT 'running',
627
+ agent_response TEXT,
628
+ judge_reasoning TEXT,
629
+ duration_ms INTEGER,
630
+ error TEXT,
631
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
632
+ FOREIGN KEY (test_case_id) REFERENCES test_cases(id) ON DELETE CASCADE
633
+ );
634
+ CREATE INDEX IF NOT EXISTS idx_test_runs_test_case ON test_runs(test_case_id);
635
+ CREATE INDEX IF NOT EXISTS idx_test_runs_status ON test_runs(status);
636
+ `,
637
+ },
638
+ {
639
+ name: "029_fix_provider_keys_unique_constraint",
640
+ sql: `
641
+ -- Recreate provider_keys table without UNIQUE constraint on provider_id alone
642
+ -- This allows multiple keys per provider (one per project)
643
+ CREATE TABLE IF NOT EXISTS provider_keys_new (
644
+ id TEXT PRIMARY KEY,
645
+ provider_id TEXT NOT NULL,
646
+ encrypted_key TEXT NOT NULL,
647
+ key_hint TEXT,
648
+ is_valid INTEGER DEFAULT 1,
649
+ last_tested_at TEXT,
650
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
651
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
652
+ name TEXT
653
+ );
654
+ INSERT OR IGNORE INTO provider_keys_new (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name)
655
+ SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name FROM provider_keys;
656
+ DROP TABLE IF EXISTS provider_keys;
657
+ ALTER TABLE provider_keys_new RENAME TO provider_keys;
658
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
659
+ CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
660
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
661
+ `,
662
+ },
523
663
  ];
524
664
 
525
665
  // Check which migrations have been applied
@@ -599,6 +739,19 @@ function runSchemaUpgrades() {
599
739
  console.log(`[db] Migrated ${existingUsers.length} user(s). Usernames derived from email addresses.`);
600
740
  }
601
741
  }
742
+
743
+ // Assign permanent ports to MCP servers that don't have one yet
744
+ // (HTTP-type servers don't need a local proxy port)
745
+ const mcpWithoutPort = db.query("SELECT id FROM mcp_servers WHERE port IS NULL AND type != 'http'").all() as { id: string }[];
746
+ if (mcpWithoutPort.length > 0) {
747
+ const MCP_BASE_PORT = 4500;
748
+ const maxRow = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
749
+ let nextPort = maxRow.max_port !== null ? maxRow.max_port + 1 : MCP_BASE_PORT;
750
+ for (const row of mcpWithoutPort) {
751
+ db.run("UPDATE mcp_servers SET port = ? WHERE id = ?", [nextPort, row.id]);
752
+ nextPort++;
753
+ }
754
+ }
602
755
  }
603
756
 
604
757
  // Generate a unique API key for an agent
@@ -1149,18 +1302,31 @@ function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
1149
1302
 
1150
1303
  // MCP Server operations
1151
1304
  export const McpServerDB = {
1305
+ // Get the next available port for a new MCP server (starting from 4500)
1306
+ getNextAvailablePort(): number {
1307
+ const BASE_PORT = 4500;
1308
+ const row = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
1309
+ if (row.max_port === null) {
1310
+ return BASE_PORT;
1311
+ }
1312
+ return row.max_port + 1;
1313
+ },
1314
+
1152
1315
  create(server: Omit<McpServer, "created_at" | "status" | "port">): McpServer {
1153
1316
  const now = new Date().toISOString();
1154
1317
  // Encrypt env vars and headers (credentials) before storing
1155
1318
  const envEncrypted = encryptObject(server.env || {});
1156
1319
  const headersEncrypted = encryptObject(server.headers || {});
1320
+ // Assign port permanently at creation time (like agents)
1321
+ // HTTP-type servers don't need a local proxy port
1322
+ const port = server.type === "http" ? null : this.getNextAvailablePort();
1157
1323
  const stmt = db.prepare(`
1158
1324
  INSERT INTO mcp_servers (id, name, type, package, pip_module, command, args, env, url, headers, source, project_id, status, port, created_at)
1159
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', NULL, ?)
1325
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', ?, ?)
1160
1326
  `);
1161
1327
  stmt.run(
1162
1328
  server.id, server.name, server.type, server.package, server.pip_module || null, server.command, server.args,
1163
- envEncrypted, server.url || null, headersEncrypted, server.source || null, server.project_id || null, now
1329
+ envEncrypted, server.url || null, headersEncrypted, server.source || null, server.project_id || null, port, now
1164
1330
  );
1165
1331
  return this.findById(server.id)!;
1166
1332
  },
@@ -1251,7 +1417,12 @@ export const McpServerDB = {
1251
1417
  },
1252
1418
 
1253
1419
  setStatus(id: string, status: "stopped" | "running", port?: number): McpServer | null {
1254
- return this.update(id, { status, port: port ?? null });
1420
+ // Port is permanently assigned only update if explicitly provided
1421
+ const updates: Partial<Omit<McpServer, "id" | "created_at">> = { status };
1422
+ if (port !== undefined) {
1423
+ updates.port = port;
1424
+ }
1425
+ return this.update(id, updates);
1255
1426
  },
1256
1427
 
1257
1428
  delete(id: string): boolean {
@@ -1260,7 +1431,8 @@ export const McpServerDB = {
1260
1431
  },
1261
1432
 
1262
1433
  resetAllStatus(): void {
1263
- db.run("UPDATE mcp_servers SET status = 'stopped', port = NULL");
1434
+ // Keep ports as they're permanently assigned (like agents)
1435
+ db.run("UPDATE mcp_servers SET status = 'stopped'");
1264
1436
  },
1265
1437
 
1266
1438
  count(): number {
@@ -1896,6 +2068,131 @@ function rowToSession(row: SessionRow): Session {
1896
2068
  };
1897
2069
  }
1898
2070
 
2071
+ // API Key types
2072
+ export interface ApiKey {
2073
+ id: string;
2074
+ name: string;
2075
+ key_hash: string;
2076
+ key_prefix: string;
2077
+ user_id: string;
2078
+ expires_at: string | null;
2079
+ last_used_at: string | null;
2080
+ is_active: boolean;
2081
+ created_at: string;
2082
+ }
2083
+
2084
+ interface ApiKeyRow {
2085
+ id: string;
2086
+ name: string;
2087
+ key_hash: string;
2088
+ key_prefix: string;
2089
+ user_id: string;
2090
+ expires_at: string | null;
2091
+ last_used_at: string | null;
2092
+ is_active: number;
2093
+ created_at: string;
2094
+ }
2095
+
2096
+ function rowToApiKey(row: ApiKeyRow): ApiKey {
2097
+ return {
2098
+ id: row.id,
2099
+ name: row.name,
2100
+ key_hash: row.key_hash,
2101
+ key_prefix: row.key_prefix,
2102
+ user_id: row.user_id,
2103
+ expires_at: row.expires_at,
2104
+ last_used_at: row.last_used_at,
2105
+ is_active: row.is_active === 1,
2106
+ created_at: row.created_at,
2107
+ };
2108
+ }
2109
+
2110
+ // API Key operations
2111
+ export const ApiKeyDB = {
2112
+ // Create a new API key (returns the raw key only at creation time)
2113
+ create(data: { name: string; user_id: string; expires_at?: string | null }): { apiKey: ApiKey; rawKey: string } {
2114
+ const id = generateId();
2115
+ const rawKey = `apt_${randomBytes(24).toString("hex")}`;
2116
+ const keyHash = createHash("sha256").update(rawKey).digest("hex");
2117
+ const keyPrefix = rawKey.slice(0, 10);
2118
+ const now = new Date().toISOString();
2119
+
2120
+ db.run(
2121
+ `INSERT INTO api_keys (id, name, key_hash, key_prefix, user_id, expires_at, created_at)
2122
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2123
+ [id, data.name, keyHash, keyPrefix, data.user_id, data.expires_at || null, now]
2124
+ );
2125
+
2126
+ return { apiKey: this.findById(id)!, rawKey };
2127
+ },
2128
+
2129
+ // Find by ID
2130
+ findById(id: string): ApiKey | null {
2131
+ const row = db.query("SELECT * FROM api_keys WHERE id = ?").get(id) as ApiKeyRow | null;
2132
+ return row ? rowToApiKey(row) : null;
2133
+ },
2134
+
2135
+ // Validate a raw key - returns the API key record and user if valid
2136
+ validate(rawKey: string): { apiKey: ApiKey; user: User } | null {
2137
+ const keyHash = createHash("sha256").update(rawKey).digest("hex");
2138
+ const row = db.query(
2139
+ "SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1"
2140
+ ).get(keyHash) as ApiKeyRow | null;
2141
+
2142
+ if (!row) return null;
2143
+
2144
+ const apiKey = rowToApiKey(row);
2145
+
2146
+ // Check expiration
2147
+ if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
2148
+ return null;
2149
+ }
2150
+
2151
+ // Load the user
2152
+ const user = UserDB.findById(apiKey.user_id);
2153
+ if (!user) return null;
2154
+
2155
+ // Update last_used_at
2156
+ db.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [new Date().toISOString(), apiKey.id]);
2157
+
2158
+ return { apiKey, user };
2159
+ },
2160
+
2161
+ // List all keys for a user (does not expose hash)
2162
+ findByUser(userId: string): ApiKey[] {
2163
+ const rows = db.query(
2164
+ "SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC"
2165
+ ).all(userId) as ApiKeyRow[];
2166
+ return rows.map(rowToApiKey);
2167
+ },
2168
+
2169
+ // Revoke a key
2170
+ revoke(id: string, userId: string): boolean {
2171
+ const result = db.run(
2172
+ "UPDATE api_keys SET is_active = 0 WHERE id = ? AND user_id = ?",
2173
+ [id, userId]
2174
+ );
2175
+ return result.changes > 0;
2176
+ },
2177
+
2178
+ // Delete a key
2179
+ delete(id: string, userId: string): boolean {
2180
+ const result = db.run(
2181
+ "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
2182
+ [id, userId]
2183
+ );
2184
+ return result.changes > 0;
2185
+ },
2186
+
2187
+ // Count active keys for a user
2188
+ countByUser(userId: string): number {
2189
+ const row = db.query(
2190
+ "SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND is_active = 1"
2191
+ ).get(userId) as { count: number };
2192
+ return row.count;
2193
+ },
2194
+ };
2195
+
1899
2196
  // Skill operations
1900
2197
  export const SkillDB = {
1901
2198
  // Create a new skill
@@ -12,44 +12,104 @@ import type {
12
12
  // AgentDojo MCP API base URL
13
13
  const AGENTDOJO_API_BASE = process.env.AGENTDOJO_API_BASE || "https://api.agentdojo.dev";
14
14
 
15
+ // Map MCP API provider_type to IntegrationApp authSchemes
16
+ function mapAuthSchemes(providerType: string): string[] {
17
+ switch (providerType) {
18
+ case "oauth": return ["OAUTH2"];
19
+ case "api_key": return ["API_KEY"];
20
+ case "basic_auth": return ["BASIC"];
21
+ case "none": return ["NONE"];
22
+ default: return ["API_KEY"];
23
+ }
24
+ }
25
+
15
26
  export const AgentDojoProvider: IntegrationProvider = {
16
27
  id: "agentdojo",
17
28
  name: "AgentDojo",
18
29
 
19
- // List available toolkits as "apps"
30
+ // List toolkits + providers from MCP API, merge so we get both
31
+ // no-auth toolkits AND OAuth/API key providers
20
32
  async listApps(apiKey: string): Promise<IntegrationApp[]> {
21
- const res = await fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=true`, {
22
- headers: {
23
- "X-API-Key": apiKey,
24
- "Content-Type": "application/json",
25
- },
33
+ const headers = { "X-API-Key": apiKey, "Content-Type": "application/json" };
34
+
35
+ // Fetch both in parallel
36
+ const [toolkitsRes, providersRes] = await Promise.all([
37
+ fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=true`, { headers }).catch(() => null),
38
+ fetch(`${AGENTDOJO_API_BASE}/providers?is_active=true`, { headers }).catch(() => null),
39
+ ]);
40
+
41
+ // Parse toolkits
42
+ let toolkits: any[] = [];
43
+ if (toolkitsRes?.ok) {
44
+ const data = await toolkitsRes.json();
45
+ toolkits = data.toolkits || data.data || [];
46
+ } else if (toolkitsRes) {
47
+ console.error("AgentDojo listApps toolkits error:", toolkitsRes.status);
48
+ }
49
+
50
+ // Parse providers (for auth type info)
51
+ let providers: any[] = [];
52
+ if (providersRes?.ok) {
53
+ const data = await providersRes.json();
54
+ providers = data.providers || data.data || [];
55
+ } else if (providersRes) {
56
+ console.error("AgentDojo listApps providers error:", providersRes.status);
57
+ }
58
+
59
+ // Index providers by name for quick lookup
60
+ const providerByName = new Map<string, any>();
61
+ for (const p of providers) {
62
+ providerByName.set(p.name, p);
63
+ if (p.display_name) providerByName.set(p.display_name.toLowerCase(), p);
64
+ }
65
+
66
+ // Map toolkits to apps, enriching auth info from providers
67
+ const apps: IntegrationApp[] = toolkits.map((toolkit: any) => {
68
+ const name = toolkit.name || toolkit.slug;
69
+ // Try to find matching provider for this toolkit
70
+ const provider = providerByName.get(name) || providerByName.get(name?.toLowerCase());
71
+
72
+ let authSchemes: string[];
73
+ if (provider) {
74
+ authSchemes = mapAuthSchemes(provider.provider_type);
75
+ } else if (toolkit.requires_auth) {
76
+ authSchemes = ["API_KEY"]; // Default if no provider found but auth required
77
+ } else {
78
+ authSchemes = ["NONE"];
79
+ }
80
+
81
+ return {
82
+ id: String(toolkit.id),
83
+ name: toolkit.display_name || toolkit.name,
84
+ slug: name,
85
+ description: toolkit.description || null,
86
+ logo: provider?.favicon || toolkit.icon_url || null,
87
+ categories: [],
88
+ authSchemes,
89
+ };
26
90
  });
27
91
 
28
- if (!res.ok) {
29
- console.error("AgentDojo listApps error:", res.status, await res.text());
30
- return [];
92
+ // Also add any providers that don't match a toolkit (standalone OAuth providers)
93
+ const toolkitNames = new Set(toolkits.map((t: any) => t.name));
94
+ for (const p of providers) {
95
+ if (!toolkitNames.has(p.name)) {
96
+ apps.push({
97
+ id: String(p.id),
98
+ name: p.display_name || p.name,
99
+ slug: p.name,
100
+ description: p.description || null,
101
+ logo: p.favicon || p.icon_url || null,
102
+ categories: [],
103
+ authSchemes: mapAuthSchemes(p.provider_type),
104
+ });
105
+ }
31
106
  }
32
107
 
33
- const data = await res.json();
34
- const toolkits = data.toolkits || [];
35
-
36
- return toolkits.map((toolkit: any) => ({
37
- id: toolkit.id,
38
- name: toolkit.display_name || toolkit.name,
39
- slug: toolkit.name,
40
- description: toolkit.description || null,
41
- logo: toolkit.icon_url || null,
42
- categories: [],
43
- // If toolkit requires auth, it needs API_KEY connection
44
- authSchemes: toolkit.requires_auth ? ["API_KEY"] : ["NONE"],
45
- toolsCount: toolkit.tools_count || toolkit.tools?.length || 0,
46
- tools: toolkit.tools || [],
47
- }));
108
+ return apps;
48
109
  },
49
110
 
50
- // List user's credentials (stored locally, validated against toolkits)
111
+ // List user's credentials
51
112
  async listConnectedAccounts(apiKey: string, userId: string): Promise<ConnectedAccount[]> {
52
- // Get list of credentials from our credentials API
53
113
  const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
54
114
  headers: {
55
115
  "X-API-Key": apiKey,
@@ -66,18 +126,19 @@ export const AgentDojoProvider: IntegrationProvider = {
66
126
  const credentials = data.data || data.credentials || [];
67
127
 
68
128
  return credentials.map((cred: any) => ({
69
- id: cred.id,
70
- appId: cred.provider_id || cred.toolkit_id || cred.provider_name,
71
- appName: cred.provider_name || cred.toolkit_name || cred.provider_id,
72
- status: cred.is_valid !== false ? "active" : "failed",
129
+ id: String(cred.id),
130
+ appId: String(cred.provider_id || cred.toolkit_id || cred.provider_name),
131
+ appName: cred.provider_name || cred.name || cred.toolkit_name || String(cred.provider_id),
132
+ status: (cred.status === "active" || cred.is_valid !== false) ? "active" as const : "failed" as const,
73
133
  createdAt: cred.created_at || new Date().toISOString(),
74
134
  metadata: {
75
135
  keyHint: cred.key_hint,
136
+ credentialType: cred.credential_type,
76
137
  },
77
138
  }));
78
139
  },
79
140
 
80
- // Store credentials for a toolkit
141
+ // Initiate connection OAuth (popup redirect) or API key (direct store)
81
142
  async initiateConnection(
82
143
  apiKey: string,
83
144
  userId: string,
@@ -85,11 +146,41 @@ export const AgentDojoProvider: IntegrationProvider = {
85
146
  redirectUrl: string,
86
147
  credentials?: ConnectionCredentials
87
148
  ): Promise<ConnectionRequest> {
149
+ // OAuth flow: no credentials provided, or explicit OAUTH2 scheme
88
150
  if (!credentials?.apiKey) {
89
- throw new Error("API key is required for AgentDojo connections");
151
+ // Init OAuth via MCP API
152
+ const res = await fetch(`${AGENTDOJO_API_BASE}/oauth/init`, {
153
+ method: "POST",
154
+ headers: {
155
+ "X-API-Key": apiKey,
156
+ "Content-Type": "application/json",
157
+ },
158
+ body: JSON.stringify({
159
+ provider_name: appSlug,
160
+ }),
161
+ });
162
+
163
+ if (!res.ok) {
164
+ const text = await res.text();
165
+ console.error("AgentDojo OAuth init error:", res.status, text);
166
+ throw new Error(`Failed to initiate OAuth: ${text}`);
167
+ }
168
+
169
+ const data = await res.json();
170
+ const flowData = data.data || data;
171
+
172
+ if (!flowData.auth_url) {
173
+ throw new Error("No auth URL returned from OAuth init");
174
+ }
175
+
176
+ return {
177
+ redirectUrl: flowData.auth_url,
178
+ connectionId: flowData.state_token || flowData.state || flowData.flow_id,
179
+ status: "pending",
180
+ };
90
181
  }
91
182
 
92
- // Store the credential
183
+ // API key flow: store credential directly
93
184
  const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
94
185
  method: "POST",
95
186
  headers: {
@@ -99,26 +190,61 @@ export const AgentDojoProvider: IntegrationProvider = {
99
190
  body: JSON.stringify({
100
191
  provider_id: appSlug,
101
192
  provider_name: appSlug,
102
- api_key: credentials.apiKey,
193
+ credential_data: { api_key: credentials.apiKey },
103
194
  }),
104
195
  });
105
196
 
106
197
  if (!res.ok) {
107
198
  const text = await res.text();
108
- console.error("AgentDojo initiateConnection error:", res.status, text);
199
+ console.error("AgentDojo credential create error:", res.status, text);
109
200
  throw new Error(`Failed to store credentials: ${text}`);
110
201
  }
111
202
 
112
203
  const data = await res.json();
113
204
 
114
205
  return {
115
- redirectUrl: null, // No OAuth redirect
116
- connectionId: data.data?.id || data.id,
117
- status: "active", // Credentials are immediately active
206
+ redirectUrl: null,
207
+ connectionId: String(data.data?.id || data.id),
208
+ status: "active",
118
209
  };
119
210
  },
120
211
 
212
+ // Check connection/OAuth flow status
121
213
  async getConnectionStatus(apiKey: string, connectionId: string): Promise<ConnectedAccount | null> {
214
+ // First try OAuth status poll (connectionId = state_token)
215
+ const oauthRes = await fetch(`${AGENTDOJO_API_BASE}/oauth/status?state=${encodeURIComponent(connectionId)}`, {
216
+ headers: {
217
+ "X-API-Key": apiKey,
218
+ "Content-Type": "application/json",
219
+ },
220
+ });
221
+
222
+ if (oauthRes.ok) {
223
+ const oauthData = await oauthRes.json();
224
+ const status = oauthData.data?.status || oauthData.status;
225
+
226
+ if (status === "completed") {
227
+ return {
228
+ id: connectionId,
229
+ appId: oauthData.data?.provider_name || oauthData.data?.provider_id || "",
230
+ appName: oauthData.data?.provider_name || "",
231
+ status: "active",
232
+ createdAt: new Date().toISOString(),
233
+ };
234
+ } else if (status === "failed") {
235
+ return {
236
+ id: connectionId,
237
+ appId: oauthData.data?.provider_name || "",
238
+ appName: oauthData.data?.provider_name || "",
239
+ status: "failed",
240
+ createdAt: new Date().toISOString(),
241
+ };
242
+ } else if (status === "pending") {
243
+ return null; // Still waiting
244
+ }
245
+ }
246
+
247
+ // Fallback: check credentials list directly (for API key connections)
122
248
  const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
123
249
  headers: {
124
250
  "X-API-Key": apiKey,
@@ -130,14 +256,14 @@ export const AgentDojoProvider: IntegrationProvider = {
130
256
 
131
257
  const data = await res.json();
132
258
  const credentials = data.data || data.credentials || [];
133
- const cred = credentials.find((c: any) => c.id === connectionId);
259
+ const cred = credentials.find((c: any) => String(c.id) === connectionId);
134
260
 
135
261
  if (!cred) return null;
136
262
 
137
263
  return {
138
- id: cred.id,
139
- appId: cred.provider_id || cred.toolkit_id,
140
- appName: cred.provider_name || cred.toolkit_name,
264
+ id: String(cred.id),
265
+ appId: String(cred.provider_id || cred.toolkit_id),
266
+ appName: cred.provider_name || cred.name || String(cred.provider_id),
141
267
  status: "active",
142
268
  createdAt: cred.created_at || new Date().toISOString(),
143
269
  };
package/src/mcp-client.ts CHANGED
@@ -139,16 +139,22 @@ export async function startMcpProcess(
139
139
  return { success: false, error: `Process exited: ${stderr || "unknown error"}` };
140
140
  }
141
141
 
142
- // Start HTTP proxy server if port specified
142
+ // Start HTTP proxy server if port specified (retry once if port busy from previous process)
143
143
  if (httpPort) {
144
- try {
145
- const httpServer = startHttpProxy(serverId, httpPort);
146
- entry.httpServer = httpServer;
147
- entry.httpPort = httpPort;
148
- console.log(`[MCP] HTTP proxy for ${serverId} started on port ${httpPort}`);
149
- } catch (err) {
150
- // HTTP proxy failed, but stdio process is running - still usable
151
- console.error(`[MCP] Failed to start HTTP proxy for ${serverId}:`, err);
144
+ for (let attempt = 0; attempt < 3; attempt++) {
145
+ try {
146
+ const httpServer = startHttpProxy(serverId, httpPort);
147
+ entry.httpServer = httpServer;
148
+ entry.httpPort = httpPort;
149
+ console.log(`[MCP] HTTP proxy for ${serverId} started on port ${httpPort}`);
150
+ break;
151
+ } catch (err: any) {
152
+ if (err?.code === "EADDRINUSE" && attempt < 2) {
153
+ await new Promise(r => setTimeout(r, 1000));
154
+ continue;
155
+ }
156
+ console.error(`[MCP] Failed to start HTTP proxy for ${serverId}:`, err);
157
+ }
152
158
  }
153
159
  }
154
160