apteva 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/App.ggy88vnx.js +213 -0
  2. package/dist/index.html +1 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +6 -6
  5. package/src/binary.ts +271 -1
  6. package/src/crypto.ts +53 -0
  7. package/src/db.ts +492 -3
  8. package/src/mcp-client.ts +599 -0
  9. package/src/providers.ts +31 -0
  10. package/src/routes/api.ts +786 -63
  11. package/src/server.ts +122 -5
  12. package/src/web/App.tsx +36 -1
  13. package/src/web/components/agents/AgentCard.tsx +22 -1
  14. package/src/web/components/agents/AgentPanel.tsx +381 -0
  15. package/src/web/components/agents/AgentsView.tsx +27 -10
  16. package/src/web/components/agents/CreateAgentModal.tsx +7 -7
  17. package/src/web/components/agents/index.ts +1 -1
  18. package/src/web/components/common/Icons.tsx +8 -0
  19. package/src/web/components/common/Modal.tsx +2 -2
  20. package/src/web/components/common/Select.tsx +1 -1
  21. package/src/web/components/common/index.ts +1 -0
  22. package/src/web/components/dashboard/Dashboard.tsx +74 -25
  23. package/src/web/components/index.ts +5 -2
  24. package/src/web/components/layout/Sidebar.tsx +22 -2
  25. package/src/web/components/mcp/McpPage.tsx +1144 -0
  26. package/src/web/components/mcp/index.ts +1 -0
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
  28. package/src/web/components/settings/SettingsPage.tsx +312 -82
  29. package/src/web/components/tasks/TasksPage.tsx +129 -0
  30. package/src/web/components/tasks/index.ts +1 -0
  31. package/src/web/components/telemetry/TelemetryPage.tsx +316 -0
  32. package/src/web/hooks/useAgents.ts +23 -0
  33. package/src/web/styles.css +18 -0
  34. package/src/web/types.ts +75 -1
  35. package/dist/App.wfhmfhx7.js +0 -213
  36. package/src/web/components/agents/ChatPanel.tsx +0 -63
@@ -0,0 +1,599 @@
1
+ // MCP Client for communicating with MCP servers
2
+ // Supports both stdio (subprocess) and HTTP transports
3
+ // Includes HTTP proxy to expose stdio servers over HTTP for agents
4
+
5
+ import { spawn, serve, type Subprocess, type Server } from "bun";
6
+
7
+ export interface McpTool {
8
+ name: string;
9
+ description?: string;
10
+ inputSchema: Record<string, unknown>;
11
+ }
12
+
13
+ export interface McpToolCallResult {
14
+ content: McpContentBlock[];
15
+ isError?: boolean;
16
+ }
17
+
18
+ export interface McpContentBlock {
19
+ type: "text" | "image" | "resource";
20
+ text?: string;
21
+ data?: string; // base64 for images
22
+ mimeType?: string;
23
+ }
24
+
25
+ interface JsonRpcRequest {
26
+ jsonrpc: "2.0";
27
+ id: number;
28
+ method: string;
29
+ params?: unknown;
30
+ }
31
+
32
+ interface JsonRpcResponse {
33
+ jsonrpc: "2.0";
34
+ id: number;
35
+ result?: unknown;
36
+ error?: {
37
+ code: number;
38
+ message: string;
39
+ data?: unknown;
40
+ };
41
+ }
42
+
43
+ interface InitializeResult {
44
+ protocolVersion: string;
45
+ capabilities: {
46
+ tools?: { listChanged?: boolean };
47
+ resources?: { subscribe?: boolean; listChanged?: boolean };
48
+ prompts?: { listChanged?: boolean };
49
+ };
50
+ serverInfo: {
51
+ name: string;
52
+ version: string;
53
+ };
54
+ instructions?: string;
55
+ }
56
+
57
+ interface ToolsListResult {
58
+ tools: McpTool[];
59
+ nextCursor?: string;
60
+ }
61
+
62
+ const MCP_PROTOCOL_VERSION = "2024-11-05";
63
+
64
+ // ============ Stdio MCP Client ============
65
+
66
+ interface McpProcess {
67
+ proc: Subprocess;
68
+ initialized: boolean;
69
+ serverInfo: { name: string; version: string } | null;
70
+ requestId: number;
71
+ buffer: string;
72
+ httpServer?: Server; // HTTP proxy server
73
+ httpPort?: number; // Port the HTTP proxy is running on
74
+ }
75
+
76
+ // Store running MCP processes
77
+ const mcpProcesses = new Map<string, McpProcess>();
78
+
79
+ // Mutex for serializing stdio requests (one at a time per process)
80
+ const requestLocks = new Map<string, Promise<void>>();
81
+
82
+ async function withLock<T>(serverId: string, fn: () => Promise<T>): Promise<T> {
83
+ // Wait for any pending request to complete
84
+ while (requestLocks.has(serverId)) {
85
+ await requestLocks.get(serverId);
86
+ }
87
+
88
+ let resolve: () => void;
89
+ const lockPromise = new Promise<void>(r => { resolve = r; });
90
+ requestLocks.set(serverId, lockPromise);
91
+
92
+ try {
93
+ return await fn();
94
+ } finally {
95
+ requestLocks.delete(serverId);
96
+ resolve!();
97
+ }
98
+ }
99
+
100
+ export function getMcpProcess(serverId: string): McpProcess | undefined {
101
+ return mcpProcesses.get(serverId);
102
+ }
103
+
104
+ export async function startMcpProcess(
105
+ serverId: string,
106
+ command: string[],
107
+ env: Record<string, string> = {},
108
+ httpPort?: number
109
+ ): Promise<{ success: boolean; error?: string; port?: number }> {
110
+ // Stop existing process if any
111
+ stopMcpProcess(serverId);
112
+
113
+ try {
114
+ const proc = spawn({
115
+ cmd: command,
116
+ env: { ...process.env as Record<string, string>, ...env },
117
+ stdin: "pipe",
118
+ stdout: "pipe",
119
+ stderr: "pipe",
120
+ });
121
+
122
+ const entry: McpProcess = {
123
+ proc,
124
+ initialized: false,
125
+ serverInfo: null,
126
+ requestId: 0,
127
+ buffer: "",
128
+ };
129
+
130
+ mcpProcesses.set(serverId, entry);
131
+
132
+ // Give it a moment to start
133
+ await new Promise(resolve => setTimeout(resolve, 500));
134
+
135
+ // Check if process is still running
136
+ if (proc.exitCode !== null) {
137
+ const stderr = await new Response(proc.stderr).text();
138
+ mcpProcesses.delete(serverId);
139
+ return { success: false, error: `Process exited: ${stderr || "unknown error"}` };
140
+ }
141
+
142
+ // Start HTTP proxy server if port specified
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);
152
+ }
153
+ }
154
+
155
+ return { success: true, port: httpPort };
156
+ } catch (err) {
157
+ return { success: false, error: String(err) };
158
+ }
159
+ }
160
+
161
+ // Start HTTP proxy server that forwards requests to stdio process
162
+ function startHttpProxy(serverId: string, port: number): Server {
163
+ return serve({
164
+ port,
165
+ async fetch(req) {
166
+ const url = new URL(req.url);
167
+
168
+ // CORS headers
169
+ const corsHeaders = {
170
+ "Access-Control-Allow-Origin": "*",
171
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
172
+ "Access-Control-Allow-Headers": "Content-Type, Mcp-Session-Id",
173
+ };
174
+
175
+ // Handle CORS preflight
176
+ if (req.method === "OPTIONS") {
177
+ return new Response(null, { headers: corsHeaders });
178
+ }
179
+
180
+ // Only accept POST to /mcp
181
+ if (req.method !== "POST" || (url.pathname !== "/mcp" && url.pathname !== "/")) {
182
+ return new Response(JSON.stringify({ error: "Not found" }), {
183
+ status: 404,
184
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
185
+ });
186
+ }
187
+
188
+ try {
189
+ const body = await req.json() as JsonRpcRequest;
190
+
191
+ // Forward to stdio process with lock to serialize requests
192
+ const result = await withLock(serverId, async () => {
193
+ return await sendRequestRaw(serverId, body.method, body.params, body.id);
194
+ });
195
+
196
+ return new Response(JSON.stringify(result), {
197
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
198
+ });
199
+ } catch (err) {
200
+ const errorResponse: JsonRpcResponse = {
201
+ jsonrpc: "2.0",
202
+ id: 0,
203
+ error: {
204
+ code: -32603,
205
+ message: err instanceof Error ? err.message : String(err),
206
+ },
207
+ };
208
+ return new Response(JSON.stringify(errorResponse), {
209
+ status: 500,
210
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
211
+ });
212
+ }
213
+ },
214
+ });
215
+ }
216
+
217
+ // Raw request that returns full JSON-RPC response (for proxy use)
218
+ async function sendRequestRaw(
219
+ serverId: string,
220
+ method: string,
221
+ params?: unknown,
222
+ requestId?: number
223
+ ): Promise<JsonRpcResponse> {
224
+ const entry = mcpProcesses.get(serverId);
225
+ if (!entry) {
226
+ throw new Error("MCP process not running");
227
+ }
228
+
229
+ const id = requestId ?? ++entry.requestId;
230
+ const request: JsonRpcRequest = {
231
+ jsonrpc: "2.0",
232
+ id,
233
+ method,
234
+ params,
235
+ };
236
+
237
+ const requestLine = JSON.stringify(request) + "\n";
238
+
239
+ // Write to stdin
240
+ entry.proc.stdin.write(requestLine);
241
+ entry.proc.stdin.flush();
242
+
243
+ // Read response from stdout with timeout
244
+ return await readJsonRpcResponse(entry, id, 30000);
245
+ }
246
+
247
+ export function stopMcpProcess(serverId: string): void {
248
+ const entry = mcpProcesses.get(serverId);
249
+ if (entry) {
250
+ // Stop HTTP proxy server first
251
+ if (entry.httpServer) {
252
+ try {
253
+ entry.httpServer.stop();
254
+ console.log(`[MCP] HTTP proxy for ${serverId} stopped`);
255
+ } catch {
256
+ // Ignore stop errors
257
+ }
258
+ }
259
+ // Kill the stdio process
260
+ try {
261
+ entry.proc.kill();
262
+ } catch {
263
+ // Ignore kill errors
264
+ }
265
+ mcpProcesses.delete(serverId);
266
+ }
267
+ }
268
+
269
+ // Get the HTTP proxy URL for an MCP server
270
+ export function getMcpProxyUrl(serverId: string): string | null {
271
+ const entry = mcpProcesses.get(serverId);
272
+ if (entry?.httpPort) {
273
+ return `http://localhost:${entry.httpPort}/mcp`;
274
+ }
275
+ return null;
276
+ }
277
+
278
+ async function sendRequest(serverId: string, method: string, params?: unknown): Promise<unknown> {
279
+ const entry = mcpProcesses.get(serverId);
280
+ if (!entry) {
281
+ throw new Error("MCP process not running");
282
+ }
283
+
284
+ const id = ++entry.requestId;
285
+ const request: JsonRpcRequest = {
286
+ jsonrpc: "2.0",
287
+ id,
288
+ method,
289
+ params,
290
+ };
291
+
292
+ const requestLine = JSON.stringify(request) + "\n";
293
+
294
+ // Write to stdin (Bun's FileSink has write() directly)
295
+ entry.proc.stdin.write(requestLine);
296
+ entry.proc.stdin.flush();
297
+
298
+ // Read response from stdout with timeout
299
+ const response = await readJsonRpcResponse(entry, id, 30000);
300
+
301
+ if (response.error) {
302
+ throw new Error(`MCP Error ${response.error.code}: ${response.error.message}`);
303
+ }
304
+
305
+ return response.result;
306
+ }
307
+
308
+ async function readJsonRpcResponse(
309
+ entry: McpProcess,
310
+ expectedId: number,
311
+ timeoutMs: number
312
+ ): Promise<JsonRpcResponse> {
313
+ const decoder = new TextDecoder();
314
+ const startTime = Date.now();
315
+
316
+ // Initialize buffer if not exists
317
+ if (!entry.buffer) {
318
+ entry.buffer = "";
319
+ }
320
+
321
+ while (Date.now() - startTime < timeoutMs) {
322
+ // Check buffer first for complete lines
323
+ const lines = entry.buffer.split("\n");
324
+ entry.buffer = lines.pop() || ""; // Keep incomplete line in buffer
325
+
326
+ for (const line of lines) {
327
+ if (!line.trim()) continue;
328
+
329
+ try {
330
+ const response = JSON.parse(line) as JsonRpcResponse;
331
+ if (response.id === expectedId) {
332
+ return response;
333
+ }
334
+ // Ignore responses with different IDs (could be notifications)
335
+ } catch {
336
+ // Not valid JSON, continue
337
+ }
338
+ }
339
+
340
+ // Read more data from stdout
341
+ const reader = entry.proc.stdout.getReader();
342
+ try {
343
+ const { value, done } = await Promise.race([
344
+ reader.read(),
345
+ new Promise<{ value: undefined; done: true }>((resolve) =>
346
+ setTimeout(() => resolve({ value: undefined, done: true }), Math.min(1000, timeoutMs - (Date.now() - startTime)))
347
+ ),
348
+ ]);
349
+
350
+ if (value) {
351
+ entry.buffer += decoder.decode(value, { stream: true });
352
+ }
353
+
354
+ if (done && !value) {
355
+ // Process might have exited
356
+ await new Promise(r => setTimeout(r, 100));
357
+ }
358
+ } finally {
359
+ reader.releaseLock();
360
+ }
361
+ }
362
+
363
+ throw new Error("Timeout waiting for MCP response");
364
+ }
365
+
366
+ async function sendNotification(serverId: string, method: string, params?: unknown): Promise<void> {
367
+ const entry = mcpProcesses.get(serverId);
368
+ if (!entry) return;
369
+
370
+ const notification = {
371
+ jsonrpc: "2.0",
372
+ method,
373
+ params,
374
+ };
375
+
376
+ const notificationLine = JSON.stringify(notification) + "\n";
377
+
378
+ try {
379
+ entry.proc.stdin.write(notificationLine);
380
+ entry.proc.stdin.flush();
381
+ } catch {
382
+ // Ignore notification errors
383
+ }
384
+ }
385
+
386
+ export async function initializeMcpServer(serverId: string): Promise<{ name: string; version: string }> {
387
+ const entry = mcpProcesses.get(serverId);
388
+ if (!entry) {
389
+ throw new Error("MCP process not running");
390
+ }
391
+
392
+ if (entry.initialized && entry.serverInfo) {
393
+ return entry.serverInfo;
394
+ }
395
+
396
+ const result = await sendRequest(serverId, "initialize", {
397
+ protocolVersion: MCP_PROTOCOL_VERSION,
398
+ capabilities: {
399
+ roots: { listChanged: true },
400
+ },
401
+ clientInfo: {
402
+ name: "apteva",
403
+ version: "1.0.0",
404
+ },
405
+ }) as InitializeResult;
406
+
407
+ entry.serverInfo = result.serverInfo;
408
+ entry.initialized = true;
409
+
410
+ // Send initialized notification
411
+ await sendNotification(serverId, "notifications/initialized");
412
+
413
+ return entry.serverInfo;
414
+ }
415
+
416
+ export async function listMcpTools(serverId: string): Promise<McpTool[]> {
417
+ const entry = mcpProcesses.get(serverId);
418
+ if (!entry) {
419
+ throw new Error("MCP process not running");
420
+ }
421
+
422
+ if (!entry.initialized) {
423
+ await initializeMcpServer(serverId);
424
+ }
425
+
426
+ const result = await sendRequest(serverId, "tools/list") as ToolsListResult;
427
+ return result.tools || [];
428
+ }
429
+
430
+ export async function callMcpTool(
431
+ serverId: string,
432
+ toolName: string,
433
+ args: Record<string, unknown> = {}
434
+ ): Promise<McpToolCallResult> {
435
+ const entry = mcpProcesses.get(serverId);
436
+ if (!entry) {
437
+ throw new Error("MCP process not running");
438
+ }
439
+
440
+ if (!entry.initialized) {
441
+ await initializeMcpServer(serverId);
442
+ }
443
+
444
+ const result = await sendRequest(serverId, "tools/call", {
445
+ name: toolName,
446
+ arguments: args,
447
+ }) as McpToolCallResult;
448
+
449
+ return result;
450
+ }
451
+
452
+ // ============ HTTP MCP Client (for remote servers) ============
453
+
454
+ export class HttpMcpClient {
455
+ private url: string;
456
+ private headers: Record<string, string>;
457
+ private sessionId: string | null = null;
458
+ private initialized = false;
459
+ private requestId = 0;
460
+ private serverInfo: { name: string; version: string } | null = null;
461
+
462
+ constructor(url: string, headers: Record<string, string> = {}) {
463
+ this.url = url;
464
+ this.headers = headers;
465
+ }
466
+
467
+ private nextId(): number {
468
+ return ++this.requestId;
469
+ }
470
+
471
+ private async doRequest(method: string, params?: unknown): Promise<unknown> {
472
+ const request: JsonRpcRequest = {
473
+ jsonrpc: "2.0",
474
+ id: this.nextId(),
475
+ method,
476
+ params,
477
+ };
478
+
479
+ const headers: Record<string, string> = {
480
+ "Content-Type": "application/json",
481
+ "Accept": "application/json, text/event-stream",
482
+ ...this.headers,
483
+ };
484
+
485
+ if (this.sessionId) {
486
+ headers["Mcp-Session-Id"] = this.sessionId;
487
+ }
488
+
489
+ const response = await fetch(this.url, {
490
+ method: "POST",
491
+ headers,
492
+ body: JSON.stringify(request),
493
+ signal: AbortSignal.timeout(30000),
494
+ });
495
+
496
+ if (!response.ok) {
497
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
498
+ }
499
+
500
+ const newSessionId = response.headers.get("Mcp-Session-Id");
501
+ if (newSessionId) {
502
+ this.sessionId = newSessionId;
503
+ }
504
+
505
+ const contentType = response.headers.get("Content-Type") || "";
506
+
507
+ if (contentType.includes("text/event-stream")) {
508
+ return this.handleSSEResponse(response);
509
+ }
510
+
511
+ const data = await response.json() as JsonRpcResponse;
512
+
513
+ if (data.error) {
514
+ throw new Error(`MCP Error ${data.error.code}: ${data.error.message}`);
515
+ }
516
+
517
+ return data.result;
518
+ }
519
+
520
+ private async handleSSEResponse(response: Response): Promise<unknown> {
521
+ const text = await response.text();
522
+ const lines = text.split("\n");
523
+
524
+ for (const line of lines) {
525
+ if (line.startsWith("data: ")) {
526
+ const data = JSON.parse(line.slice(6)) as JsonRpcResponse;
527
+ if (data.error) {
528
+ throw new Error(`MCP Error ${data.error.code}: ${data.error.message}`);
529
+ }
530
+ return data.result;
531
+ }
532
+ }
533
+
534
+ throw new Error("No data in SSE response");
535
+ }
536
+
537
+ async initialize(): Promise<{ name: string; version: string }> {
538
+ if (this.initialized && this.serverInfo) {
539
+ return this.serverInfo;
540
+ }
541
+
542
+ const result = await this.doRequest("initialize", {
543
+ protocolVersion: MCP_PROTOCOL_VERSION,
544
+ capabilities: {
545
+ roots: { listChanged: true },
546
+ },
547
+ clientInfo: {
548
+ name: "apteva",
549
+ version: "1.0.0",
550
+ },
551
+ }) as InitializeResult;
552
+
553
+ this.serverInfo = result.serverInfo;
554
+ this.initialized = true;
555
+
556
+ return this.serverInfo;
557
+ }
558
+
559
+ async listTools(): Promise<McpTool[]> {
560
+ if (!this.initialized) {
561
+ await this.initialize();
562
+ }
563
+
564
+ const result = await this.doRequest("tools/list") as ToolsListResult;
565
+ return result.tools || [];
566
+ }
567
+
568
+ async callTool(name: string, args: Record<string, unknown> = {}): Promise<McpToolCallResult> {
569
+ if (!this.initialized) {
570
+ await this.initialize();
571
+ }
572
+
573
+ const result = await this.doRequest("tools/call", {
574
+ name,
575
+ arguments: args,
576
+ }) as McpToolCallResult;
577
+
578
+ return result;
579
+ }
580
+ }
581
+
582
+ // Cache for HTTP MCP clients
583
+ const httpClientCache = new Map<string, HttpMcpClient>();
584
+
585
+ export function getHttpMcpClient(url: string, headers: Record<string, string> = {}): HttpMcpClient {
586
+ const cacheKey = `${url}:${JSON.stringify(headers)}`;
587
+
588
+ let client = httpClientCache.get(cacheKey);
589
+ if (!client) {
590
+ client = new HttpMcpClient(url, headers);
591
+ httpClientCache.set(cacheKey, client);
592
+ }
593
+
594
+ return client;
595
+ }
596
+
597
+ export function clearHttpMcpClientCache(): void {
598
+ httpClientCache.clear();
599
+ }
package/src/providers.ts CHANGED
@@ -7,6 +7,7 @@ export const PROVIDERS = {
7
7
  id: "anthropic",
8
8
  name: "Anthropic",
9
9
  displayName: "Anthropic",
10
+ type: "llm" as const,
10
11
  envVar: "ANTHROPIC_API_KEY",
11
12
  docsUrl: "https://console.anthropic.com/settings/keys",
12
13
  testEndpoint: "https://api.anthropic.com/v1/messages",
@@ -19,6 +20,7 @@ export const PROVIDERS = {
19
20
  id: "openai",
20
21
  name: "OpenAI",
21
22
  displayName: "OpenAI",
23
+ type: "llm" as const,
22
24
  envVar: "OPENAI_API_KEY",
23
25
  docsUrl: "https://platform.openai.com/api-keys",
24
26
  testEndpoint: "https://api.openai.com/v1/models",
@@ -31,6 +33,7 @@ export const PROVIDERS = {
31
33
  id: "groq",
32
34
  name: "Groq",
33
35
  displayName: "Groq",
36
+ type: "llm" as const,
34
37
  envVar: "GROQ_API_KEY",
35
38
  docsUrl: "https://console.groq.com/keys",
36
39
  testEndpoint: "https://api.groq.com/openai/v1/models",
@@ -43,6 +46,7 @@ export const PROVIDERS = {
43
46
  id: "gemini",
44
47
  name: "Google",
45
48
  displayName: "Google Gemini",
49
+ type: "llm" as const,
46
50
  envVar: "GEMINI_API_KEY",
47
51
  docsUrl: "https://aistudio.google.com/app/apikey",
48
52
  testEndpoint: "https://generativelanguage.googleapis.com/v1/models",
@@ -55,6 +59,7 @@ export const PROVIDERS = {
55
59
  id: "xai",
56
60
  name: "xAI",
57
61
  displayName: "xAI Grok",
62
+ type: "llm" as const,
58
63
  envVar: "XAI_API_KEY",
59
64
  docsUrl: "https://console.x.ai/",
60
65
  testEndpoint: "https://api.x.ai/v1/models",
@@ -67,6 +72,7 @@ export const PROVIDERS = {
67
72
  id: "together",
68
73
  name: "Together",
69
74
  displayName: "Together AI",
75
+ type: "llm" as const,
70
76
  envVar: "TOGETHER_API_KEY",
71
77
  docsUrl: "https://api.together.xyz/settings/api-keys",
72
78
  testEndpoint: "https://api.together.xyz/v1/models",
@@ -79,6 +85,7 @@ export const PROVIDERS = {
79
85
  id: "fireworks",
80
86
  name: "Fireworks",
81
87
  displayName: "Fireworks AI",
88
+ type: "llm" as const,
82
89
  envVar: "FIREWORKS_API_KEY",
83
90
  docsUrl: "https://fireworks.ai/api-keys",
84
91
  testEndpoint: "https://api.fireworks.ai/inference/v1/models",
@@ -91,6 +98,7 @@ export const PROVIDERS = {
91
98
  id: "moonshot",
92
99
  name: "Moonshot",
93
100
  displayName: "Moonshot AI",
101
+ type: "llm" as const,
94
102
  envVar: "MOONSHOT_API_KEY",
95
103
  docsUrl: "https://platform.moonshot.cn/console/api-keys",
96
104
  testEndpoint: "https://api.moonshot.cn/v1/models",
@@ -99,6 +107,27 @@ export const PROVIDERS = {
99
107
  { value: "moonshot-v1-32k", label: "Kimi 32K (Fast)" },
100
108
  ],
101
109
  },
110
+ // MCP Integrations
111
+ composio: {
112
+ id: "composio",
113
+ name: "Composio",
114
+ displayName: "Composio",
115
+ type: "integration" as const,
116
+ envVar: "COMPOSIO_API_KEY",
117
+ docsUrl: "https://app.composio.dev/settings",
118
+ description: "500+ app integrations via MCP gateway",
119
+ models: [],
120
+ },
121
+ smithery: {
122
+ id: "smithery",
123
+ name: "Smithery",
124
+ displayName: "Smithery",
125
+ type: "integration" as const,
126
+ envVar: "SMITHERY_API_KEY",
127
+ docsUrl: "https://smithery.ai/settings",
128
+ description: "MCP server registry and hosting",
129
+ models: [],
130
+ },
102
131
  } as const;
103
132
 
104
133
  export type ProviderId = keyof typeof PROVIDERS;
@@ -228,7 +257,9 @@ export function getProvidersWithStatus() {
228
257
  return Object.values(PROVIDERS).map(provider => ({
229
258
  id: provider.id,
230
259
  name: provider.displayName,
260
+ type: provider.type,
231
261
  docsUrl: provider.docsUrl,
262
+ description: "description" in provider ? provider.description : undefined,
232
263
  models: provider.models,
233
264
  hasKey: configuredProviders.has(provider.id),
234
265
  keyHint: keyStatuses.get(provider.id)?.key_hint || null,