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.
- package/dist/App.m4hg4bxq.js +218 -0
- package/dist/index.html +4 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +688 -45
- package/src/integrations/composio.ts +437 -0
- package/src/integrations/index.ts +80 -0
- package/src/openapi.ts +1724 -0
- package/src/routes/api.ts +1476 -118
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +121 -11
- package/src/web/App.tsx +64 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- package/src/web/components/agents/AgentPanel.tsx +810 -45
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/api/ApiDocsPage.tsx +583 -0
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +56 -0
- package/src/web/components/common/Modal.tsx +184 -1
- package/src/web/components/dashboard/Dashboard.tsx +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- package/src/web/components/layout/Sidebar.tsx +87 -43
- package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
- package/src/web/components/mcp/McpPage.tsx +451 -63
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +340 -26
- package/src/web/components/tasks/TasksPage.tsx +22 -20
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/index.html +1 -1
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +10 -1
- 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
|
-
|
|
22
|
-
|
|
29
|
+
version?: string;
|
|
30
|
+
repository?: string;
|
|
23
31
|
npmPackage: string | null;
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
79
|
+
const confirmed = await confirm("Delete this MCP server?", { confirmText: "Delete", title: "Delete Server" });
|
|
80
|
+
if (!confirmed) return;
|
|
69
81
|
try {
|
|
70
|
-
await
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
681
|
+
setInstalling(server.id);
|
|
614
682
|
setError(null);
|
|
615
683
|
|
|
616
684
|
try {
|
|
617
|
-
const res = await
|
|
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.
|
|
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-
|
|
689
|
-
{server.
|
|
690
|
-
{
|
|
691
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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.
|
|
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.
|
|
775
|
+
{installing === server.id ? "Adding..." : "Add"}
|
|
708
776
|
</button>
|
|
709
|
-
) : (
|
|
777
|
+
) : server.repository ? (
|
|
710
778
|
<a
|
|
711
|
-
href={server.
|
|
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
|
|
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),
|