apteva 0.2.7 → 0.2.9

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 (46) hide show
  1. package/dist/App.m4hg4bxq.js +218 -0
  2. package/dist/index.html +4 -2
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +688 -45
  9. package/src/integrations/composio.ts +437 -0
  10. package/src/integrations/index.ts +80 -0
  11. package/src/openapi.ts +1724 -0
  12. package/src/routes/api.ts +1476 -118
  13. package/src/routes/auth.ts +242 -0
  14. package/src/server.ts +121 -11
  15. package/src/web/App.tsx +64 -19
  16. package/src/web/components/agents/AgentCard.tsx +24 -22
  17. package/src/web/components/agents/AgentPanel.tsx +810 -45
  18. package/src/web/components/agents/AgentsView.tsx +81 -9
  19. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  20. package/src/web/components/api/ApiDocsPage.tsx +583 -0
  21. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  22. package/src/web/components/auth/LoginPage.tsx +91 -0
  23. package/src/web/components/auth/index.ts +2 -0
  24. package/src/web/components/common/Icons.tsx +56 -0
  25. package/src/web/components/common/Modal.tsx +184 -1
  26. package/src/web/components/dashboard/Dashboard.tsx +70 -22
  27. package/src/web/components/index.ts +3 -0
  28. package/src/web/components/layout/Header.tsx +135 -18
  29. package/src/web/components/layout/Sidebar.tsx +87 -43
  30. package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
  31. package/src/web/components/mcp/McpPage.tsx +451 -63
  32. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  33. package/src/web/components/settings/SettingsPage.tsx +340 -26
  34. package/src/web/components/tasks/TasksPage.tsx +22 -20
  35. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  36. package/src/web/context/AuthContext.tsx +230 -0
  37. package/src/web/context/ProjectContext.tsx +182 -0
  38. package/src/web/context/index.ts +5 -0
  39. package/src/web/hooks/useAgents.ts +18 -6
  40. package/src/web/hooks/useOnboarding.ts +20 -4
  41. package/src/web/hooks/useProviders.ts +15 -5
  42. package/src/web/icon.png +0 -0
  43. package/src/web/index.html +1 -1
  44. package/src/web/styles.css +12 -0
  45. package/src/web/types.ts +10 -1
  46. package/dist/App.3kb50qa3.js +0 -213
@@ -1,6 +1,9 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { McpIcon } from "../common/Icons";
3
+ import { useAuth } from "../../context";
4
+ import { useConfirm, useAlert } from "../common/Modal";
3
5
  import type { McpTool, McpToolCallResult } from "../../types";
6
+ import { IntegrationsPanel } from "./IntegrationsPanel";
4
7
 
5
8
  interface McpServer {
6
9
  id: string;
@@ -10,30 +13,38 @@ interface McpServer {
10
13
  command: string | null;
11
14
  args: string | null;
12
15
  env: Record<string, string>;
16
+ url: string | null;
17
+ headers: Record<string, string>;
13
18
  port: number | null;
14
19
  status: "stopped" | "running";
20
+ source: string | null; // "composio", "smithery", or null for local
15
21
  created_at: string;
16
22
  }
17
23
 
18
24
  interface RegistryServer {
25
+ id: string;
19
26
  name: string;
27
+ fullName: string;
20
28
  description: string;
21
- vendor: string;
22
- sourceUrl: string;
29
+ version?: string;
30
+ repository?: string;
23
31
  npmPackage: string | null;
24
- githubStars: number | null;
32
+ remoteUrl: string | null;
33
+ transport: string;
25
34
  }
26
35
 
27
36
  export function McpPage() {
37
+ const { authFetch } = useAuth();
28
38
  const [servers, setServers] = useState<McpServer[]>([]);
29
39
  const [loading, setLoading] = useState(true);
30
40
  const [showAdd, setShowAdd] = useState(false);
31
41
  const [selectedServer, setSelectedServer] = useState<McpServer | null>(null);
32
- const [activeTab, setActiveTab] = useState<"servers" | "registry">("servers");
42
+ const [activeTab, setActiveTab] = useState<"servers" | "hosted" | "registry">("servers");
43
+ const { confirm, ConfirmDialog } = useConfirm();
33
44
 
34
45
  const fetchServers = async () => {
35
46
  try {
36
- const res = await fetch("/api/mcp/servers");
47
+ const res = await authFetch("/api/mcp/servers");
37
48
  const data = await res.json();
38
49
  setServers(data.servers || []);
39
50
  } catch (e) {
@@ -44,11 +55,11 @@ export function McpPage() {
44
55
 
45
56
  useEffect(() => {
46
57
  fetchServers();
47
- }, []);
58
+ }, [authFetch]);
48
59
 
49
60
  const startServer = async (id: string) => {
50
61
  try {
51
- await fetch(`/api/mcp/servers/${id}/start`, { method: "POST" });
62
+ await authFetch(`/api/mcp/servers/${id}/start`, { method: "POST" });
52
63
  fetchServers();
53
64
  } catch (e) {
54
65
  console.error("Failed to start server:", e);
@@ -57,7 +68,7 @@ export function McpPage() {
57
68
 
58
69
  const stopServer = async (id: string) => {
59
70
  try {
60
- await fetch(`/api/mcp/servers/${id}/stop`, { method: "POST" });
71
+ await authFetch(`/api/mcp/servers/${id}/stop`, { method: "POST" });
61
72
  fetchServers();
62
73
  } catch (e) {
63
74
  console.error("Failed to stop server:", e);
@@ -65,9 +76,10 @@ export function McpPage() {
65
76
  };
66
77
 
67
78
  const deleteServer = async (id: string) => {
68
- if (!confirm("Delete this MCP server?")) return;
79
+ const confirmed = await confirm("Delete this MCP server?", { confirmText: "Delete", title: "Delete Server" });
80
+ if (!confirmed) return;
69
81
  try {
70
- await fetch(`/api/mcp/servers/${id}`, { method: "DELETE" });
82
+ await authFetch(`/api/mcp/servers/${id}`, { method: "DELETE" });
71
83
  if (selectedServer?.id === id) {
72
84
  setSelectedServer(null);
73
85
  }
@@ -78,6 +90,8 @@ export function McpPage() {
78
90
  };
79
91
 
80
92
  return (
93
+ <>
94
+ {ConfirmDialog}
81
95
  <div className="flex-1 overflow-auto p-6">
82
96
  <div className="max-w-6xl">
83
97
  {/* Header */}
@@ -110,6 +124,16 @@ export function McpPage() {
110
124
  >
111
125
  My Servers
112
126
  </button>
127
+ <button
128
+ onClick={() => setActiveTab("hosted")}
129
+ className={`px-4 py-2 rounded text-sm font-medium transition ${
130
+ activeTab === "hosted"
131
+ ? "bg-[#1a1a1a] text-white"
132
+ : "text-[#666] hover:text-[#888]"
133
+ }`}
134
+ >
135
+ Hosted Services
136
+ </button>
113
137
  <button
114
138
  onClick={() => setActiveTab("registry")}
115
139
  className={`px-4 py-2 rounded text-sm font-medium transition ${
@@ -161,17 +185,21 @@ export function McpPage() {
161
185
  <div className="flex gap-6">
162
186
  {/* Server List */}
163
187
  <div className={`space-y-3 ${selectedServer ? "w-1/2" : "w-full"}`}>
164
- {servers.map(server => (
165
- <McpServerCard
166
- key={server.id}
167
- server={server}
168
- selected={selectedServer?.id === server.id}
169
- onSelect={() => setSelectedServer(server.status === "running" ? server : null)}
170
- onStart={() => startServer(server.id)}
171
- onStop={() => stopServer(server.id)}
172
- onDelete={() => deleteServer(server.id)}
173
- />
174
- ))}
188
+ {servers.map(server => {
189
+ const isRemote = server.type === "http" && server.url;
190
+ const isAvailable = isRemote || server.status === "running";
191
+ return (
192
+ <McpServerCard
193
+ key={server.id}
194
+ server={server}
195
+ selected={selectedServer?.id === server.id}
196
+ onSelect={() => setSelectedServer(isAvailable ? server : null)}
197
+ onStart={() => startServer(server.id)}
198
+ onStop={() => stopServer(server.id)}
199
+ onDelete={() => deleteServer(server.id)}
200
+ />
201
+ );
202
+ })}
175
203
  </div>
176
204
 
177
205
  {/* Tools Panel */}
@@ -188,6 +216,11 @@ export function McpPage() {
188
216
  </>
189
217
  )}
190
218
 
219
+ {/* Hosted Services Tab */}
220
+ {activeTab === "hosted" && (
221
+ <HostedServices onServerAdded={fetchServers} />
222
+ )}
223
+
191
224
  {/* Browse Registry Tab */}
192
225
  {activeTab === "registry" && (
193
226
  <RegistryBrowser
@@ -232,6 +265,7 @@ export function McpPage() {
232
265
  />
233
266
  )}
234
267
  </div>
268
+ </>
235
269
  );
236
270
  }
237
271
 
@@ -250,28 +284,50 @@ function McpServerCard({
250
284
  onStop: () => void;
251
285
  onDelete: () => void;
252
286
  }) {
287
+ // Remote/hosted servers (http type with url) are always available
288
+ const isRemote = server.type === "http" && server.url;
289
+ const isAvailable = isRemote || server.status === "running";
290
+
291
+ // Determine what to show as the server info
292
+ const getServerInfo = () => {
293
+ if (isRemote) {
294
+ // Show source (composio, smithery) or just "remote"
295
+ const source = server.source || "remote";
296
+ return `${source} • http`;
297
+ }
298
+ return `${server.type} • ${server.package || server.command || "custom"}${
299
+ server.status === "running" && server.port ? ` • :${server.port}` : ""
300
+ }`;
301
+ };
302
+
253
303
  return (
254
304
  <div
255
305
  className={`bg-[#111] border rounded-lg p-4 cursor-pointer transition ${
256
306
  selected ? "border-[#f97316]" : "border-[#1a1a1a] hover:border-[#333]"
257
307
  }`}
258
- onClick={server.status === "running" ? onSelect : undefined}
308
+ onClick={isAvailable ? onSelect : undefined}
259
309
  >
260
310
  <div className="flex items-center justify-between">
261
311
  <div className="flex items-center gap-3">
262
312
  <div className={`w-2 h-2 rounded-full ${
263
- server.status === "running" ? "bg-green-400" : "bg-[#444]"
313
+ isAvailable ? "bg-green-400" : "bg-[#444]"
264
314
  }`} />
265
315
  <div>
266
316
  <h3 className="font-medium">{server.name}</h3>
267
- <p className="text-sm text-[#666]">
268
- {server.type} • {server.package || server.command || "custom"}
269
- {server.status === "running" && server.port && ` • :${server.port}`}
270
- </p>
317
+ <p className="text-sm text-[#666]">{getServerInfo()}</p>
271
318
  </div>
272
319
  </div>
273
320
  <div className="flex items-center gap-2">
274
- {server.status === "running" ? (
321
+ {isRemote ? (
322
+ // Remote servers: no start/stop, just delete
323
+ <button
324
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
325
+ className="text-sm text-[#666] hover:text-red-400 px-3 py-1 transition"
326
+ >
327
+ Remove
328
+ </button>
329
+ ) : server.status === "running" ? (
330
+ // Local running server: tools + stop + delete
275
331
  <>
276
332
  <button
277
333
  onClick={(e) => { e.stopPropagation(); onSelect(); }}
@@ -285,21 +341,30 @@ function McpServerCard({
285
341
  >
286
342
  Stop
287
343
  </button>
344
+ <button
345
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
346
+ className="text-sm text-[#666] hover:text-red-400 px-3 py-1 transition"
347
+ >
348
+ Delete
349
+ </button>
288
350
  </>
289
351
  ) : (
290
- <button
291
- onClick={(e) => { e.stopPropagation(); onStart(); }}
292
- className="text-sm text-[#666] hover:text-green-400 px-3 py-1 transition"
293
- >
294
- Start
295
- </button>
352
+ // Local stopped server: start + delete
353
+ <>
354
+ <button
355
+ onClick={(e) => { e.stopPropagation(); onStart(); }}
356
+ className="text-sm text-[#666] hover:text-green-400 px-3 py-1 transition"
357
+ >
358
+ Start
359
+ </button>
360
+ <button
361
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
362
+ className="text-sm text-[#666] hover:text-red-400 px-3 py-1 transition"
363
+ >
364
+ Delete
365
+ </button>
366
+ </>
296
367
  )}
297
- <button
298
- onClick={(e) => { e.stopPropagation(); onDelete(); }}
299
- className="text-sm text-[#666] hover:text-red-400 px-3 py-1 transition"
300
- >
301
- Delete
302
- </button>
303
368
  </div>
304
369
  </div>
305
370
  </div>
@@ -313,6 +378,7 @@ function ToolsPanel({
313
378
  server: McpServer;
314
379
  onClose: () => void;
315
380
  }) {
381
+ const { authFetch } = useAuth();
316
382
  const [tools, setTools] = useState<McpTool[]>([]);
317
383
  const [serverInfo, setServerInfo] = useState<{ name: string; version: string } | null>(null);
318
384
  const [loading, setLoading] = useState(true);
@@ -324,7 +390,7 @@ function ToolsPanel({
324
390
  setLoading(true);
325
391
  setError(null);
326
392
  try {
327
- const res = await fetch(`/api/mcp/servers/${server.id}/tools`);
393
+ const res = await authFetch(`/api/mcp/servers/${server.id}/tools`);
328
394
  const data = await res.json();
329
395
  if (!res.ok) {
330
396
  setError(data.error || "Failed to fetch tools");
@@ -340,7 +406,7 @@ function ToolsPanel({
340
406
  };
341
407
 
342
408
  fetchTools();
343
- }, [server.id]);
409
+ }, [server.id, authFetch]);
344
410
 
345
411
  return (
346
412
  <div className="bg-[#111] border border-[#1a1a1a] rounded-lg overflow-hidden">
@@ -414,6 +480,7 @@ function ToolTester({
414
480
  tool: McpTool;
415
481
  onBack: () => void;
416
482
  }) {
483
+ const { authFetch } = useAuth();
417
484
  const [args, setArgs] = useState<string>("{}");
418
485
  const [result, setResult] = useState<McpToolCallResult | null>(null);
419
486
  const [error, setError] = useState<string | null>(null);
@@ -451,7 +518,7 @@ function ToolTester({
451
518
 
452
519
  try {
453
520
  const parsedArgs = JSON.parse(args);
454
- const res = await fetch(`/api/mcp/servers/${serverId}/tools/${encodeURIComponent(tool.name)}/call`, {
521
+ const res = await authFetch(`/api/mcp/servers/${serverId}/tools/${encodeURIComponent(tool.name)}/call`, {
455
522
  method: "POST",
456
523
  headers: { "Content-Type": "application/json" },
457
524
  body: JSON.stringify({ arguments: parsedArgs }),
@@ -564,6 +631,7 @@ function RegistryBrowser({
564
631
  }: {
565
632
  onInstall: (server: RegistryServer) => void;
566
633
  }) {
634
+ const { authFetch } = useAuth();
567
635
  const [search, setSearch] = useState("");
568
636
  const [servers, setServers] = useState<RegistryServer[]>([]);
569
637
  const [loading, setLoading] = useState(false);
@@ -575,7 +643,7 @@ function RegistryBrowser({
575
643
  setLoading(true);
576
644
  setError(null);
577
645
  try {
578
- const res = await fetch(`/api/mcp/registry?search=${encodeURIComponent(query)}&limit=20`);
646
+ const res = await authFetch(`/api/mcp/registry?search=${encodeURIComponent(query)}&limit=20`);
579
647
  const data = await res.json();
580
648
  if (!res.ok) {
581
649
  setError(data.error || "Failed to search registry");
@@ -610,11 +678,11 @@ function RegistryBrowser({
610
678
  return;
611
679
  }
612
680
 
613
- setInstalling(server.name);
681
+ setInstalling(server.id);
614
682
  setError(null);
615
683
 
616
684
  try {
617
- const res = await fetch("/api/mcp/servers", {
685
+ const res = await authFetch("/api/mcp/servers", {
618
686
  method: "POST",
619
687
  headers: { "Content-Type": "application/json" },
620
688
  body: JSON.stringify({
@@ -676,7 +744,7 @@ function RegistryBrowser({
676
744
  <div className="grid gap-4 md:grid-cols-2">
677
745
  {servers.map((server) => (
678
746
  <div
679
- key={server.name}
747
+ key={server.id}
680
748
  className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 hover:border-[#333] transition"
681
749
  >
682
750
  <div className="flex items-start justify-between gap-3">
@@ -685,37 +753,37 @@ function RegistryBrowser({
685
753
  <p className="text-sm text-[#666] mt-1 line-clamp-2">
686
754
  {server.description || "No description"}
687
755
  </p>
688
- <div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
689
- {server.vendor && <span>by {server.vendor}</span>}
690
- {server.githubStars !== null && server.githubStars > 0 && (
691
- <span>★ {server.githubStars.toLocaleString()}</span>
692
- )}
756
+ <div className="flex items-center gap-2 mt-2 text-xs text-[#555]">
757
+ {server.version && <span>v{server.version}</span>}
758
+ <span className={`px-1.5 py-0.5 rounded ${
759
+ server.npmPackage ? "bg-green-500/10 text-green-400" : "bg-blue-500/10 text-blue-400"
760
+ }`}>
761
+ {server.npmPackage ? "npm" : "remote"}
762
+ </span>
693
763
  </div>
694
- {server.npmPackage && (
695
- <code className="text-xs text-[#666] bg-[#0a0a0a] px-2 py-0.5 rounded mt-2 inline-block truncate max-w-full">
696
- {server.npmPackage}
697
- </code>
698
- )}
764
+ <code className="text-xs text-[#555] bg-[#0a0a0a] px-2 py-0.5 rounded mt-2 inline-block truncate max-w-full">
765
+ {server.npmPackage || server.fullName}
766
+ </code>
699
767
  </div>
700
768
  <div className="flex-shrink-0">
701
769
  {server.npmPackage ? (
702
770
  <button
703
771
  onClick={() => installServer(server)}
704
- disabled={installing === server.name}
772
+ disabled={installing === server.id}
705
773
  className="text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-3 py-1.5 rounded transition disabled:opacity-50"
706
774
  >
707
- {installing === server.name ? "Adding..." : "Add"}
775
+ {installing === server.id ? "Adding..." : "Add"}
708
776
  </button>
709
- ) : (
777
+ ) : server.repository ? (
710
778
  <a
711
- href={server.sourceUrl}
779
+ href={server.repository}
712
780
  target="_blank"
713
781
  rel="noopener noreferrer"
714
782
  className="text-sm text-[#666] hover:text-[#f97316] transition"
715
783
  >
716
784
  View →
717
785
  </a>
718
- )}
786
+ ) : null}
719
787
  </div>
720
788
  </div>
721
789
  </div>
@@ -749,6 +817,325 @@ function RegistryBrowser({
749
817
  );
750
818
  }
751
819
 
820
+ // Hosted MCP Services (Composio, Smithery, etc.)
821
+ interface ComposioConfig {
822
+ id: string;
823
+ name: string;
824
+ toolkits: string[];
825
+ toolsCount: number;
826
+ mcpUrl: string;
827
+ createdAt?: string;
828
+ }
829
+
830
+ function HostedServices({ onServerAdded }: { onServerAdded?: () => void }) {
831
+ const { authFetch } = useAuth();
832
+ const [subTab, setSubTab] = useState<"configs" | "connect">("configs");
833
+ const [composioConnected, setComposioConnected] = useState(false);
834
+ const [smitheryConnected, setSmitheryConnected] = useState(false);
835
+ const [composioConfigs, setComposioConfigs] = useState<ComposioConfig[]>([]);
836
+ const [addedServers, setAddedServers] = useState<Set<string>>(new Set());
837
+ const [loading, setLoading] = useState(true);
838
+ const [loadingConfigs, setLoadingConfigs] = useState(false);
839
+ const [addingConfig, setAddingConfig] = useState<string | null>(null);
840
+ const { alert, AlertDialog } = useAlert();
841
+
842
+ const fetchStatus = async () => {
843
+ try {
844
+ const [providersRes, serversRes] = await Promise.all([
845
+ authFetch("/api/providers"),
846
+ authFetch("/api/mcp/servers"),
847
+ ]);
848
+ const providersData = await providersRes.json();
849
+ const serversData = await serversRes.json();
850
+
851
+ const providers = providersData.providers || [];
852
+ const servers = serversData.servers || [];
853
+
854
+ // Track which Composio config IDs are already added as servers
855
+ // Extract config ID from URLs like https://backend.composio.dev/v3/mcp/{configId}/mcp?user_id=...
856
+ const composioConfigIds = new Set(
857
+ servers
858
+ .filter((s: any) => s.source === "composio" && s.url)
859
+ .map((s: any) => {
860
+ const match = s.url.match(/\/v3\/mcp\/([^/]+)/);
861
+ return match ? match[1] : null;
862
+ })
863
+ .filter(Boolean)
864
+ );
865
+ setAddedServers(composioConfigIds);
866
+
867
+ const composio = providers.find((p: any) => p.id === "composio");
868
+ const smithery = providers.find((p: any) => p.id === "smithery");
869
+ setComposioConnected(composio?.hasKey || false);
870
+ setSmitheryConnected(smithery?.hasKey || false);
871
+
872
+ if (composio?.hasKey) {
873
+ fetchComposioConfigs();
874
+ }
875
+ } catch (e) {
876
+ console.error("Failed to fetch providers:", e);
877
+ }
878
+ setLoading(false);
879
+ };
880
+
881
+ const fetchComposioConfigs = async () => {
882
+ setLoadingConfigs(true);
883
+ try {
884
+ const res = await authFetch("/api/integrations/composio/configs");
885
+ const data = await res.json();
886
+ setComposioConfigs(data.configs || []);
887
+ } catch (e) {
888
+ console.error("Failed to fetch Composio configs:", e);
889
+ }
890
+ setLoadingConfigs(false);
891
+ };
892
+
893
+ const addComposioConfig = async (configId: string) => {
894
+ setAddingConfig(configId);
895
+ try {
896
+ const res = await authFetch(`/api/integrations/composio/configs/${configId}/add`, {
897
+ method: "POST",
898
+ });
899
+ if (res.ok) {
900
+ // Mark as added by config ID
901
+ setAddedServers(prev => new Set([...prev, configId]));
902
+ onServerAdded?.();
903
+ } else {
904
+ const data = await res.json();
905
+ await alert(data.error || "Failed to add config", { title: "Error", variant: "error" });
906
+ }
907
+ } catch (e) {
908
+ console.error("Failed to add config:", e);
909
+ }
910
+ setAddingConfig(null);
911
+ };
912
+
913
+ const isConfigAdded = (configId: string) => {
914
+ return addedServers.has(configId);
915
+ };
916
+
917
+ useEffect(() => {
918
+ fetchStatus();
919
+ }, [authFetch]);
920
+
921
+ if (loading) {
922
+ return <div className="text-center py-8 text-[#666]">Loading...</div>;
923
+ }
924
+
925
+ const hasAnyConnection = composioConnected || smitheryConnected;
926
+
927
+ if (!hasAnyConnection) {
928
+ return (
929
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-8 text-center">
930
+ <p className="text-[#888] mb-2">No hosted MCP services connected</p>
931
+ <p className="text-sm text-[#666] mb-4">
932
+ Connect Composio or Smithery in Settings to access cloud-based MCP servers.
933
+ </p>
934
+ <a
935
+ href="/settings"
936
+ className="inline-block bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] px-4 py-2 rounded text-sm font-medium transition"
937
+ >
938
+ Go to Settings →
939
+ </a>
940
+ </div>
941
+ );
942
+ }
943
+
944
+ return (
945
+ <>
946
+ {AlertDialog}
947
+ <div className="space-y-6">
948
+ {/* Sub-tabs for Composio */}
949
+ {composioConnected && (
950
+ <div className="flex gap-1 bg-[#0a0a0a] border border-[#222] rounded-lg p-1 w-fit">
951
+ <button
952
+ onClick={() => setSubTab("configs")}
953
+ className={`px-4 py-2 rounded text-sm font-medium transition ${
954
+ subTab === "configs"
955
+ ? "bg-[#1a1a1a] text-white"
956
+ : "text-[#666] hover:text-[#888]"
957
+ }`}
958
+ >
959
+ MCP Configs
960
+ </button>
961
+ <button
962
+ onClick={() => setSubTab("connect")}
963
+ className={`px-4 py-2 rounded text-sm font-medium transition ${
964
+ subTab === "connect"
965
+ ? "bg-[#1a1a1a] text-white"
966
+ : "text-[#666] hover:text-[#888]"
967
+ }`}
968
+ >
969
+ Connect Apps
970
+ </button>
971
+ </div>
972
+ )}
973
+
974
+ {/* Connect Apps Tab */}
975
+ {composioConnected && subTab === "connect" && (
976
+ <div>
977
+ <div className="flex items-center justify-between mb-4">
978
+ <div>
979
+ <h2 className="font-medium">Connect Apps via Composio</h2>
980
+ <p className="text-sm text-[#666] mt-1">
981
+ Connect your accounts to enable tools in MCP configs
982
+ </p>
983
+ </div>
984
+ </div>
985
+ <IntegrationsPanel
986
+ providerId="composio"
987
+ onConnectionComplete={() => {
988
+ // Refresh configs after connecting an app
989
+ fetchComposioConfigs();
990
+ }}
991
+ />
992
+ </div>
993
+ )}
994
+
995
+ {/* MCP Configs Tab */}
996
+ {composioConnected && subTab === "configs" && (
997
+ <div>
998
+ <div className="flex items-center justify-between mb-3">
999
+ <div className="flex items-center gap-2">
1000
+ <h2 className="font-medium">Composio MCP Configs</h2>
1001
+ <span className="text-xs text-green-400">Connected</span>
1002
+ </div>
1003
+ <div className="flex items-center gap-3">
1004
+ <button
1005
+ onClick={fetchComposioConfigs}
1006
+ disabled={loadingConfigs}
1007
+ className="text-xs text-[#666] hover:text-[#888] transition"
1008
+ >
1009
+ {loadingConfigs ? "Loading..." : "Refresh"}
1010
+ </button>
1011
+ <a
1012
+ href="https://app.composio.dev/mcp_configs"
1013
+ target="_blank"
1014
+ rel="noopener noreferrer"
1015
+ className="text-xs text-[#666] hover:text-[#f97316] transition"
1016
+ >
1017
+ Create Config →
1018
+ </a>
1019
+ </div>
1020
+ </div>
1021
+
1022
+ {loadingConfigs ? (
1023
+ <div className="text-center py-6 text-[#666]">Loading configs...</div>
1024
+ ) : composioConfigs.length === 0 ? (
1025
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 text-center">
1026
+ <p className="text-sm text-[#666]">No MCP configs found</p>
1027
+ <p className="text-xs text-[#555] mt-2">
1028
+ First <button onClick={() => setSubTab("connect")} className="text-[#f97316] hover:text-[#fb923c]">connect some apps</button>, then create a config.
1029
+ </p>
1030
+ <a
1031
+ href="https://app.composio.dev/mcp_configs"
1032
+ target="_blank"
1033
+ rel="noopener noreferrer"
1034
+ className="text-xs text-[#f97316] hover:text-[#fb923c] mt-2 inline-block"
1035
+ >
1036
+ Create in Composio →
1037
+ </a>
1038
+ </div>
1039
+ ) : (
1040
+ <div className="space-y-2">
1041
+ {composioConfigs.map((config) => {
1042
+ const added = isConfigAdded(config.id);
1043
+ const isAdding = addingConfig === config.id;
1044
+ return (
1045
+ <div
1046
+ key={config.id}
1047
+ className={`bg-[#111] border rounded-lg p-3 transition flex items-center justify-between ${
1048
+ added ? "border-green-500/30" : "border-[#1a1a1a] hover:border-[#333]"
1049
+ }`}
1050
+ >
1051
+ <div className="flex-1 min-w-0">
1052
+ <div className="flex items-center gap-2">
1053
+ <span className="font-medium text-sm">{config.name}</span>
1054
+ <span className="text-xs text-[#555]">{config.toolsCount} tools</span>
1055
+ {added && (
1056
+ <span className="text-xs text-green-400">Added</span>
1057
+ )}
1058
+ </div>
1059
+ {config.toolkits.length > 0 && (
1060
+ <div className="flex flex-wrap gap-1 mt-1">
1061
+ {config.toolkits.slice(0, 4).map((toolkit) => (
1062
+ <span
1063
+ key={toolkit}
1064
+ className="text-xs bg-[#1a1a1a] text-[#666] px-1.5 py-0.5 rounded"
1065
+ >
1066
+ {toolkit}
1067
+ </span>
1068
+ ))}
1069
+ {config.toolkits.length > 4 && (
1070
+ <span className="text-xs text-[#555]">+{config.toolkits.length - 4}</span>
1071
+ )}
1072
+ </div>
1073
+ )}
1074
+ </div>
1075
+ <div className="flex items-center gap-2 ml-3">
1076
+ {added ? (
1077
+ <span className="text-xs text-[#555] px-2 py-1">In Servers</span>
1078
+ ) : (
1079
+ <button
1080
+ onClick={() => addComposioConfig(config.id)}
1081
+ disabled={isAdding}
1082
+ className="text-xs bg-[#f97316] hover:bg-[#fb923c] text-black px-3 py-1 rounded font-medium transition disabled:opacity-50"
1083
+ >
1084
+ {isAdding ? "Adding..." : "Add"}
1085
+ </button>
1086
+ )}
1087
+ <a
1088
+ href={`https://app.composio.dev/mcp_configs/${config.id}`}
1089
+ target="_blank"
1090
+ rel="noopener noreferrer"
1091
+ className="text-xs text-[#666] hover:text-[#888] transition"
1092
+ >
1093
+ Edit
1094
+ </a>
1095
+ </div>
1096
+ </div>
1097
+ );
1098
+ })}
1099
+ </div>
1100
+ )}
1101
+ </div>
1102
+ )}
1103
+
1104
+ {/* Smithery - placeholder for when we have API support */}
1105
+ {smitheryConnected && (
1106
+ <div>
1107
+ <div className="flex items-center justify-between mb-3">
1108
+ <div className="flex items-center gap-2">
1109
+ <h2 className="font-medium">Smithery</h2>
1110
+ <span className="text-xs text-green-400">Connected</span>
1111
+ </div>
1112
+ <a
1113
+ href="https://smithery.ai/servers"
1114
+ target="_blank"
1115
+ rel="noopener noreferrer"
1116
+ className="text-xs text-[#666] hover:text-[#f97316] transition"
1117
+ >
1118
+ View Servers →
1119
+ </a>
1120
+ </div>
1121
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 text-center">
1122
+ <p className="text-sm text-[#666]">
1123
+ Smithery servers can be added from the Registry tab.
1124
+ </p>
1125
+ </div>
1126
+ </div>
1127
+ )}
1128
+
1129
+ <div className="p-3 bg-[#0a0a0a] border border-[#222] rounded text-xs text-[#666]">
1130
+ <strong className="text-[#888]">Tip:</strong> Connect apps first, then add MCP configs to make tools available to your agents.
1131
+ {" · "}
1132
+ <a href="/settings" className="text-[#f97316] hover:text-[#fb923c]">Add more providers in Settings</a>
1133
+ </div>
1134
+ </div>
1135
+ </>
1136
+ );
1137
+ }
1138
+
752
1139
  // Parse command and extract credential placeholders
753
1140
  function parseCommandForCredentials(cmd: string): {
754
1141
  cleanCommand: string;
@@ -810,6 +1197,7 @@ function AddServerModal({
810
1197
  onClose: () => void;
811
1198
  onAdded: () => void;
812
1199
  }) {
1200
+ const { authFetch } = useAuth();
813
1201
  const [mode, setMode] = useState<"npm" | "command">("npm");
814
1202
  const [name, setName] = useState("");
815
1203
  const [pkg, setPkg] = useState("");
@@ -941,7 +1329,7 @@ function AddServerModal({
941
1329
  body.env = env;
942
1330
  }
943
1331
 
944
- const res = await fetch("/api/mcp/servers", {
1332
+ const res = await authFetch("/api/mcp/servers", {
945
1333
  method: "POST",
946
1334
  headers: { "Content-Type": "application/json" },
947
1335
  body: JSON.stringify(body),