apteva 0.2.10 → 0.3.6
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.mvbdnw89.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +11 -3
- package/src/auth/middleware.ts +1 -0
- package/src/binary.ts +7 -5
- package/src/crypto.ts +4 -0
- package/src/db.ts +437 -14
- package/src/integrations/skillsmp.ts +318 -0
- package/src/providers.ts +21 -0
- package/src/routes/api.ts +836 -16
- package/src/server.ts +58 -7
- package/src/web/App.tsx +24 -8
- package/src/web/components/agents/AgentCard.tsx +36 -11
- package/src/web/components/agents/AgentPanel.tsx +333 -24
- package/src/web/components/agents/AgentsView.tsx +1 -1
- package/src/web/components/agents/CreateAgentModal.tsx +169 -23
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +4 -2
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/McpPage.tsx +602 -19
- package/src/web/components/meta-agent/MetaAgent.tsx +222 -0
- package/src/web/components/settings/SettingsPage.tsx +212 -150
- package/src/web/components/skills/SkillsPage.tsx +871 -0
- package/src/web/context/AuthContext.tsx +5 -0
- package/src/web/context/ProjectContext.tsx +26 -4
- package/src/web/types.ts +48 -3
- package/dist/App.44ge5b89.js +0 -218
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { McpIcon } from "../common/Icons";
|
|
3
|
-
import { useAuth } from "../../context";
|
|
3
|
+
import { useAuth, useProjects } from "../../context";
|
|
4
4
|
import { useConfirm, useAlert } from "../common/Modal";
|
|
5
|
+
import { Select } from "../common/Select";
|
|
5
6
|
import type { McpTool, McpToolCallResult } from "../../types";
|
|
6
7
|
import { IntegrationsPanel } from "./IntegrationsPanel";
|
|
7
8
|
|
|
8
9
|
interface McpServer {
|
|
9
10
|
id: string;
|
|
10
11
|
name: string;
|
|
11
|
-
type: "npm" | "github" | "http" | "custom";
|
|
12
|
+
type: "npm" | "pip" | "github" | "http" | "custom";
|
|
12
13
|
package: string | null;
|
|
14
|
+
pip_module: string | null; // For pip type: module to run (e.g., "late.mcp")
|
|
13
15
|
command: string | null;
|
|
14
16
|
args: string | null;
|
|
15
17
|
env: Record<string, string>;
|
|
@@ -18,6 +20,7 @@ interface McpServer {
|
|
|
18
20
|
port: number | null;
|
|
19
21
|
status: "stopped" | "running";
|
|
20
22
|
source: string | null; // "composio", "smithery", or null for local
|
|
23
|
+
project_id: string | null; // null = global
|
|
21
24
|
created_at: string;
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -35,13 +38,17 @@ interface RegistryServer {
|
|
|
35
38
|
|
|
36
39
|
export function McpPage() {
|
|
37
40
|
const { authFetch } = useAuth();
|
|
41
|
+
const { projects, currentProjectId } = useProjects();
|
|
38
42
|
const [servers, setServers] = useState<McpServer[]>([]);
|
|
39
43
|
const [loading, setLoading] = useState(true);
|
|
40
44
|
const [showAdd, setShowAdd] = useState(false);
|
|
45
|
+
const [editingServer, setEditingServer] = useState<McpServer | null>(null);
|
|
41
46
|
const [selectedServer, setSelectedServer] = useState<McpServer | null>(null);
|
|
42
47
|
const [activeTab, setActiveTab] = useState<"servers" | "hosted" | "registry">("servers");
|
|
43
48
|
const { confirm, ConfirmDialog } = useConfirm();
|
|
44
49
|
|
|
50
|
+
const hasProjects = projects.length > 0;
|
|
51
|
+
|
|
45
52
|
const fetchServers = async () => {
|
|
46
53
|
try {
|
|
47
54
|
const res = await authFetch("/api/mcp/servers");
|
|
@@ -57,6 +64,15 @@ export function McpPage() {
|
|
|
57
64
|
fetchServers();
|
|
58
65
|
}, [authFetch]);
|
|
59
66
|
|
|
67
|
+
// Filter servers based on global project selector
|
|
68
|
+
// When a project is selected, show global + that project's servers
|
|
69
|
+
const filteredServers = servers.filter(server => {
|
|
70
|
+
if (!currentProjectId) return true; // "All Projects" - show everything
|
|
71
|
+
if (currentProjectId === "unassigned") return server.project_id === null; // Only global
|
|
72
|
+
// Project selected: show global + project-specific
|
|
73
|
+
return server.project_id === null || server.project_id === currentProjectId;
|
|
74
|
+
});
|
|
75
|
+
|
|
60
76
|
const startServer = async (id: string) => {
|
|
61
77
|
try {
|
|
62
78
|
await authFetch(`/api/mcp/servers/${id}/start`, { method: "POST" });
|
|
@@ -89,6 +105,33 @@ export function McpPage() {
|
|
|
89
105
|
}
|
|
90
106
|
};
|
|
91
107
|
|
|
108
|
+
const renameServer = async (id: string, newName: string) => {
|
|
109
|
+
try {
|
|
110
|
+
await authFetch(`/api/mcp/servers/${id}`, {
|
|
111
|
+
method: "PUT",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ name: newName }),
|
|
114
|
+
});
|
|
115
|
+
fetchServers();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error("Failed to rename server:", e);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const updateServer = async (id: string, updates: Partial<McpServer>) => {
|
|
122
|
+
try {
|
|
123
|
+
await authFetch(`/api/mcp/servers/${id}`, {
|
|
124
|
+
method: "PUT",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify(updates),
|
|
127
|
+
});
|
|
128
|
+
fetchServers();
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error("Failed to update server:", e);
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
92
135
|
return (
|
|
93
136
|
<>
|
|
94
137
|
{ConfirmDialog}
|
|
@@ -155,7 +198,7 @@ export function McpPage() {
|
|
|
155
198
|
)}
|
|
156
199
|
|
|
157
200
|
{/* Empty State */}
|
|
158
|
-
{!loading && servers.length === 0 && (
|
|
201
|
+
{!loading && filteredServers.length === 0 && servers.length === 0 && (
|
|
159
202
|
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-8 text-center">
|
|
160
203
|
<McpIcon className="w-12 h-12 text-[#333] mx-auto mb-4" />
|
|
161
204
|
<h3 className="text-lg font-medium mb-2">No MCP servers configured</h3>
|
|
@@ -180,23 +223,35 @@ export function McpPage() {
|
|
|
180
223
|
</div>
|
|
181
224
|
)}
|
|
182
225
|
|
|
226
|
+
{/* Empty filter state */}
|
|
227
|
+
{!loading && filteredServers.length === 0 && servers.length > 0 && (
|
|
228
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-6 text-center">
|
|
229
|
+
<p className="text-[#666]">No servers match this filter.</p>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
183
233
|
{/* Main content with server list and tools panel */}
|
|
184
|
-
{!loading &&
|
|
234
|
+
{!loading && filteredServers.length > 0 && (
|
|
185
235
|
<div className="flex gap-6">
|
|
186
236
|
{/* Server List */}
|
|
187
237
|
<div className={`space-y-3 ${selectedServer ? "w-1/2" : "w-full"}`}>
|
|
188
|
-
{
|
|
238
|
+
{filteredServers.map(server => {
|
|
189
239
|
const isRemote = server.type === "http" && server.url;
|
|
190
240
|
const isAvailable = isRemote || server.status === "running";
|
|
241
|
+
const project = hasProjects && server.project_id
|
|
242
|
+
? projects.find(p => p.id === server.project_id)
|
|
243
|
+
: null;
|
|
191
244
|
return (
|
|
192
245
|
<McpServerCard
|
|
193
246
|
key={server.id}
|
|
194
247
|
server={server}
|
|
248
|
+
project={project}
|
|
195
249
|
selected={selectedServer?.id === server.id}
|
|
196
250
|
onSelect={() => setSelectedServer(isAvailable ? server : null)}
|
|
197
251
|
onStart={() => startServer(server.id)}
|
|
198
252
|
onStop={() => stopServer(server.id)}
|
|
199
253
|
onDelete={() => deleteServer(server.id)}
|
|
254
|
+
onEdit={() => setEditingServer(server)}
|
|
200
255
|
/>
|
|
201
256
|
);
|
|
202
257
|
})}
|
|
@@ -262,6 +317,20 @@ export function McpPage() {
|
|
|
262
317
|
setShowAdd(false);
|
|
263
318
|
fetchServers();
|
|
264
319
|
}}
|
|
320
|
+
projects={hasProjects ? projects : undefined}
|
|
321
|
+
defaultProjectId={currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{editingServer && (
|
|
326
|
+
<EditServerModal
|
|
327
|
+
server={editingServer}
|
|
328
|
+
projects={hasProjects ? projects : undefined}
|
|
329
|
+
onClose={() => setEditingServer(null)}
|
|
330
|
+
onSaved={() => {
|
|
331
|
+
setEditingServer(null);
|
|
332
|
+
fetchServers();
|
|
333
|
+
}}
|
|
265
334
|
/>
|
|
266
335
|
)}
|
|
267
336
|
</div>
|
|
@@ -271,18 +340,22 @@ export function McpPage() {
|
|
|
271
340
|
|
|
272
341
|
function McpServerCard({
|
|
273
342
|
server,
|
|
343
|
+
project,
|
|
274
344
|
selected,
|
|
275
345
|
onSelect,
|
|
276
346
|
onStart,
|
|
277
347
|
onStop,
|
|
278
348
|
onDelete,
|
|
349
|
+
onEdit,
|
|
279
350
|
}: {
|
|
280
351
|
server: McpServer;
|
|
352
|
+
project?: { id: string; name: string; color: string } | null;
|
|
281
353
|
selected: boolean;
|
|
282
354
|
onSelect: () => void;
|
|
283
355
|
onStart: () => void;
|
|
284
356
|
onStop: () => void;
|
|
285
357
|
onDelete: () => void;
|
|
358
|
+
onEdit: () => void;
|
|
286
359
|
}) {
|
|
287
360
|
// Remote/hosted servers (http type with url) are always available
|
|
288
361
|
const isRemote = server.type === "http" && server.url;
|
|
@@ -300,6 +373,28 @@ function McpServerCard({
|
|
|
300
373
|
}`;
|
|
301
374
|
};
|
|
302
375
|
|
|
376
|
+
// Scope badge: Global or Project name
|
|
377
|
+
const getScopeBadge = () => {
|
|
378
|
+
if (project) {
|
|
379
|
+
return (
|
|
380
|
+
<span
|
|
381
|
+
className="text-xs px-1.5 py-0.5 rounded"
|
|
382
|
+
style={{ backgroundColor: `${project.color}20`, color: project.color }}
|
|
383
|
+
>
|
|
384
|
+
{project.name}
|
|
385
|
+
</span>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (server.project_id === null) {
|
|
389
|
+
return (
|
|
390
|
+
<span className="text-xs text-[#666] bg-[#1a1a1a] px-1.5 py-0.5 rounded">
|
|
391
|
+
Global
|
|
392
|
+
</span>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
};
|
|
397
|
+
|
|
303
398
|
return (
|
|
304
399
|
<div
|
|
305
400
|
className={`bg-[#111] border rounded-lg p-4 cursor-pointer transition ${
|
|
@@ -313,11 +408,21 @@ function McpServerCard({
|
|
|
313
408
|
isAvailable ? "bg-green-400" : "bg-[#444]"
|
|
314
409
|
}`} />
|
|
315
410
|
<div>
|
|
316
|
-
<
|
|
411
|
+
<div className="flex items-center gap-2">
|
|
412
|
+
<h3 className="font-medium">{server.name}</h3>
|
|
413
|
+
{getScopeBadge()}
|
|
414
|
+
</div>
|
|
317
415
|
<p className="text-sm text-[#666]">{getServerInfo()}</p>
|
|
318
416
|
</div>
|
|
319
417
|
</div>
|
|
320
418
|
<div className="flex items-center gap-2">
|
|
419
|
+
<button
|
|
420
|
+
onClick={(e) => { e.stopPropagation(); onEdit(); }}
|
|
421
|
+
className="text-sm text-[#666] hover:text-[#888] px-3 py-1 transition"
|
|
422
|
+
title="Edit server settings"
|
|
423
|
+
>
|
|
424
|
+
Edit
|
|
425
|
+
</button>
|
|
321
426
|
{isRemote ? (
|
|
322
427
|
// Remote servers: no start/stop, just delete
|
|
323
428
|
<button
|
|
@@ -832,6 +937,7 @@ function HostedServices({ onServerAdded }: { onServerAdded?: () => void }) {
|
|
|
832
937
|
const [subTab, setSubTab] = useState<"configs" | "connect">("configs");
|
|
833
938
|
const [composioConnected, setComposioConnected] = useState(false);
|
|
834
939
|
const [smitheryConnected, setSmitheryConnected] = useState(false);
|
|
940
|
+
const [agentDojoConnected, setAgentDojoConnected] = useState(false);
|
|
835
941
|
const [composioConfigs, setComposioConfigs] = useState<ComposioConfig[]>([]);
|
|
836
942
|
const [addedServers, setAddedServers] = useState<Set<string>>(new Set());
|
|
837
943
|
const [loading, setLoading] = useState(true);
|
|
@@ -866,8 +972,10 @@ function HostedServices({ onServerAdded }: { onServerAdded?: () => void }) {
|
|
|
866
972
|
|
|
867
973
|
const composio = providers.find((p: any) => p.id === "composio");
|
|
868
974
|
const smithery = providers.find((p: any) => p.id === "smithery");
|
|
975
|
+
const agentdojo = providers.find((p: any) => p.id === "agentdojo");
|
|
869
976
|
setComposioConnected(composio?.hasKey || false);
|
|
870
977
|
setSmitheryConnected(smithery?.hasKey || false);
|
|
978
|
+
setAgentDojoConnected(agentdojo?.hasKey || false);
|
|
871
979
|
|
|
872
980
|
if (composio?.hasKey) {
|
|
873
981
|
fetchComposioConfigs();
|
|
@@ -922,14 +1030,14 @@ function HostedServices({ onServerAdded }: { onServerAdded?: () => void }) {
|
|
|
922
1030
|
return <div className="text-center py-8 text-[#666]">Loading...</div>;
|
|
923
1031
|
}
|
|
924
1032
|
|
|
925
|
-
const hasAnyConnection = composioConnected || smitheryConnected;
|
|
1033
|
+
const hasAnyConnection = composioConnected || smitheryConnected || agentDojoConnected;
|
|
926
1034
|
|
|
927
1035
|
if (!hasAnyConnection) {
|
|
928
1036
|
return (
|
|
929
1037
|
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-8 text-center">
|
|
930
1038
|
<p className="text-[#888] mb-2">No hosted MCP services connected</p>
|
|
931
1039
|
<p className="text-sm text-[#666] mb-4">
|
|
932
|
-
Connect Composio or
|
|
1040
|
+
Connect Composio, Smithery, or AgentDojo in Settings to access cloud-based MCP servers.
|
|
933
1041
|
</p>
|
|
934
1042
|
<a
|
|
935
1043
|
href="/settings"
|
|
@@ -1126,6 +1234,31 @@ function HostedServices({ onServerAdded }: { onServerAdded?: () => void }) {
|
|
|
1126
1234
|
</div>
|
|
1127
1235
|
)}
|
|
1128
1236
|
|
|
1237
|
+
{/* AgentDojo - hosted MCP tools */}
|
|
1238
|
+
{agentDojoConnected && (
|
|
1239
|
+
<div>
|
|
1240
|
+
<div className="flex items-center justify-between mb-3">
|
|
1241
|
+
<div className="flex items-center gap-2">
|
|
1242
|
+
<h2 className="font-medium">AgentDojo</h2>
|
|
1243
|
+
<span className="text-xs text-green-400">Connected</span>
|
|
1244
|
+
</div>
|
|
1245
|
+
<a
|
|
1246
|
+
href="https://agentdojo.com/tools"
|
|
1247
|
+
target="_blank"
|
|
1248
|
+
rel="noopener noreferrer"
|
|
1249
|
+
className="text-xs text-[#666] hover:text-[#f97316] transition"
|
|
1250
|
+
>
|
|
1251
|
+
Browse Tools →
|
|
1252
|
+
</a>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 text-center">
|
|
1255
|
+
<p className="text-sm text-[#666]">
|
|
1256
|
+
AgentDojo integration coming soon. Browse available tools on their platform.
|
|
1257
|
+
</p>
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
)}
|
|
1261
|
+
|
|
1129
1262
|
<div className="p-3 bg-[#0a0a0a] border border-[#222] rounded text-xs text-[#666]">
|
|
1130
1263
|
<strong className="text-[#888]">Tip:</strong> Connect apps first, then add MCP configs to make tools available to your agents.
|
|
1131
1264
|
{" · "}
|
|
@@ -1193,19 +1326,30 @@ function parseCommandForCredentials(cmd: string): {
|
|
|
1193
1326
|
function AddServerModal({
|
|
1194
1327
|
onClose,
|
|
1195
1328
|
onAdded,
|
|
1329
|
+
projects,
|
|
1330
|
+
defaultProjectId,
|
|
1196
1331
|
}: {
|
|
1197
1332
|
onClose: () => void;
|
|
1198
1333
|
onAdded: () => void;
|
|
1334
|
+
projects?: Array<{ id: string; name: string; color: string }>;
|
|
1335
|
+
defaultProjectId?: string | null;
|
|
1199
1336
|
}) {
|
|
1200
1337
|
const { authFetch } = useAuth();
|
|
1201
|
-
const [mode, setMode] = useState<"npm" | "command">("npm");
|
|
1338
|
+
const [mode, setMode] = useState<"npm" | "pip" | "command" | "http">("npm");
|
|
1202
1339
|
const [name, setName] = useState("");
|
|
1203
1340
|
const [pkg, setPkg] = useState("");
|
|
1341
|
+
const [pipModule, setPipModule] = useState("");
|
|
1204
1342
|
const [command, setCommand] = useState("");
|
|
1343
|
+
const [url, setUrl] = useState("");
|
|
1344
|
+
const [username, setUsername] = useState("");
|
|
1345
|
+
const [password, setPassword] = useState("");
|
|
1205
1346
|
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([]);
|
|
1347
|
+
const [projectId, setProjectId] = useState<string | null>(defaultProjectId || null);
|
|
1206
1348
|
const [saving, setSaving] = useState(false);
|
|
1207
1349
|
const [error, setError] = useState<string | null>(null);
|
|
1208
1350
|
|
|
1351
|
+
const hasProjects = projects && projects.length > 0;
|
|
1352
|
+
|
|
1209
1353
|
const addEnvVar = () => {
|
|
1210
1354
|
setEnvVars([...envVars, { key: "", value: "" }]);
|
|
1211
1355
|
};
|
|
@@ -1295,11 +1439,21 @@ function AddServerModal({
|
|
|
1295
1439
|
return;
|
|
1296
1440
|
}
|
|
1297
1441
|
|
|
1442
|
+
if (mode === "pip" && !pkg) {
|
|
1443
|
+
setError("pip package is required");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1298
1447
|
if (mode === "command" && !command) {
|
|
1299
1448
|
setError("Command is required");
|
|
1300
1449
|
return;
|
|
1301
1450
|
}
|
|
1302
1451
|
|
|
1452
|
+
if (mode === "http" && !url) {
|
|
1453
|
+
setError("URL is required");
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1303
1457
|
setSaving(true);
|
|
1304
1458
|
setError(null);
|
|
1305
1459
|
|
|
@@ -1317,6 +1471,25 @@ function AddServerModal({
|
|
|
1317
1471
|
if (mode === "npm") {
|
|
1318
1472
|
body.type = "npm";
|
|
1319
1473
|
body.package = pkg;
|
|
1474
|
+
} else if (mode === "pip") {
|
|
1475
|
+
body.type = "pip";
|
|
1476
|
+
body.package = pkg;
|
|
1477
|
+
if (pipModule) {
|
|
1478
|
+
body.pip_module = pipModule;
|
|
1479
|
+
}
|
|
1480
|
+
} else if (mode === "http") {
|
|
1481
|
+
body.type = "http";
|
|
1482
|
+
body.url = url;
|
|
1483
|
+
// Build headers with Basic Auth if credentials provided
|
|
1484
|
+
const headers: Record<string, string> = {
|
|
1485
|
+
"Content-Type": "application/json",
|
|
1486
|
+
};
|
|
1487
|
+
if (username && password) {
|
|
1488
|
+
// Base64 encode username:password for Basic Auth
|
|
1489
|
+
const credentials = btoa(`${username}:${password}`);
|
|
1490
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
1491
|
+
}
|
|
1492
|
+
body.headers = headers;
|
|
1320
1493
|
} else {
|
|
1321
1494
|
// Parse command into parts
|
|
1322
1495
|
const parts = command.trim().split(/\s+/);
|
|
@@ -1329,6 +1502,11 @@ function AddServerModal({
|
|
|
1329
1502
|
body.env = env;
|
|
1330
1503
|
}
|
|
1331
1504
|
|
|
1505
|
+
// Add project_id if selected
|
|
1506
|
+
if (projectId) {
|
|
1507
|
+
body.project_id = projectId;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1332
1510
|
const res = await authFetch("/api/mcp/servers", {
|
|
1333
1511
|
method: "POST",
|
|
1334
1512
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1371,14 +1549,24 @@ function AddServerModal({
|
|
|
1371
1549
|
<p className="text-sm text-[#666] mb-2">Quick add:</p>
|
|
1372
1550
|
<div className="flex flex-wrap gap-2">
|
|
1373
1551
|
{[
|
|
1374
|
-
{ name: "filesystem", pkg: "@modelcontextprotocol/server-filesystem" },
|
|
1375
|
-
{ name: "fetch", pkg: "@modelcontextprotocol/server-fetch" },
|
|
1376
|
-
{ name: "memory", pkg: "@modelcontextprotocol/server-memory" },
|
|
1377
|
-
{ name: "github", pkg: "@modelcontextprotocol/server-github" },
|
|
1552
|
+
{ name: "filesystem", pkg: "@modelcontextprotocol/server-filesystem", type: "npm" as const },
|
|
1553
|
+
{ name: "fetch", pkg: "@modelcontextprotocol/server-fetch", type: "npm" as const },
|
|
1554
|
+
{ name: "memory", pkg: "@modelcontextprotocol/server-memory", type: "npm" as const },
|
|
1555
|
+
{ name: "github", pkg: "@modelcontextprotocol/server-github", type: "npm" as const },
|
|
1556
|
+
{ name: "time", pkg: "mcp-server-time", module: "mcp_server_time", type: "pip" as const },
|
|
1378
1557
|
].map(s => (
|
|
1379
1558
|
<button
|
|
1380
1559
|
key={s.name}
|
|
1381
|
-
onClick={() =>
|
|
1560
|
+
onClick={() => {
|
|
1561
|
+
setMode(s.type);
|
|
1562
|
+
setName(s.name);
|
|
1563
|
+
setPkg(s.pkg);
|
|
1564
|
+
if (s.type === "pip" && "module" in s) {
|
|
1565
|
+
setPipModule(s.module || "");
|
|
1566
|
+
} else {
|
|
1567
|
+
setPipModule("");
|
|
1568
|
+
}
|
|
1569
|
+
}}
|
|
1382
1570
|
className="text-sm bg-[#1a1a1a] hover:bg-[#222] px-3 py-1 rounded transition"
|
|
1383
1571
|
>
|
|
1384
1572
|
{s.name}
|
|
@@ -1391,23 +1579,43 @@ function AddServerModal({
|
|
|
1391
1579
|
<div className="flex gap-1 bg-[#0a0a0a] border border-[#222] rounded p-1">
|
|
1392
1580
|
<button
|
|
1393
1581
|
onClick={() => setMode("npm")}
|
|
1394
|
-
className={`flex-1 px-
|
|
1582
|
+
className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
|
|
1395
1583
|
mode === "npm"
|
|
1396
1584
|
? "bg-[#1a1a1a] text-white"
|
|
1397
1585
|
: "text-[#666] hover:text-[#888]"
|
|
1398
1586
|
}`}
|
|
1399
1587
|
>
|
|
1400
|
-
npm
|
|
1588
|
+
npm
|
|
1589
|
+
</button>
|
|
1590
|
+
<button
|
|
1591
|
+
onClick={() => setMode("pip")}
|
|
1592
|
+
className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
|
|
1593
|
+
mode === "pip"
|
|
1594
|
+
? "bg-[#1a1a1a] text-white"
|
|
1595
|
+
: "text-[#666] hover:text-[#888]"
|
|
1596
|
+
}`}
|
|
1597
|
+
>
|
|
1598
|
+
pip
|
|
1401
1599
|
</button>
|
|
1402
1600
|
<button
|
|
1403
1601
|
onClick={() => setMode("command")}
|
|
1404
|
-
className={`flex-1 px-
|
|
1602
|
+
className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
|
|
1405
1603
|
mode === "command"
|
|
1406
1604
|
? "bg-[#1a1a1a] text-white"
|
|
1407
1605
|
: "text-[#666] hover:text-[#888]"
|
|
1408
1606
|
}`}
|
|
1409
1607
|
>
|
|
1410
|
-
|
|
1608
|
+
Command
|
|
1609
|
+
</button>
|
|
1610
|
+
<button
|
|
1611
|
+
onClick={() => setMode("http")}
|
|
1612
|
+
className={`flex-1 px-2 py-1.5 rounded text-sm transition ${
|
|
1613
|
+
mode === "http"
|
|
1614
|
+
? "bg-[#1a1a1a] text-white"
|
|
1615
|
+
: "text-[#666] hover:text-[#888]"
|
|
1616
|
+
}`}
|
|
1617
|
+
>
|
|
1618
|
+
HTTP
|
|
1411
1619
|
</button>
|
|
1412
1620
|
</div>
|
|
1413
1621
|
|
|
@@ -1423,6 +1631,25 @@ function AddServerModal({
|
|
|
1423
1631
|
/>
|
|
1424
1632
|
</div>
|
|
1425
1633
|
|
|
1634
|
+
{/* Project Scope - only show when projects exist */}
|
|
1635
|
+
{hasProjects && (
|
|
1636
|
+
<div>
|
|
1637
|
+
<label className="block text-sm text-[#666] mb-1">Scope</label>
|
|
1638
|
+
<Select
|
|
1639
|
+
value={projectId || ""}
|
|
1640
|
+
onChange={(value) => setProjectId(value || null)}
|
|
1641
|
+
options={[
|
|
1642
|
+
{ value: "", label: "Global (all projects)" },
|
|
1643
|
+
...projects!.map(p => ({ value: p.id, label: p.name }))
|
|
1644
|
+
]}
|
|
1645
|
+
placeholder="Select scope..."
|
|
1646
|
+
/>
|
|
1647
|
+
<p className="text-xs text-[#555] mt-1">
|
|
1648
|
+
Global servers are available to all agents. Project-scoped servers are only available to agents in that project.
|
|
1649
|
+
</p>
|
|
1650
|
+
</div>
|
|
1651
|
+
)}
|
|
1652
|
+
|
|
1426
1653
|
{/* npm Package */}
|
|
1427
1654
|
{mode === "npm" && (
|
|
1428
1655
|
<div>
|
|
@@ -1440,6 +1667,45 @@ function AddServerModal({
|
|
|
1440
1667
|
</div>
|
|
1441
1668
|
)}
|
|
1442
1669
|
|
|
1670
|
+
{/* pip Package (Python) */}
|
|
1671
|
+
{mode === "pip" && (
|
|
1672
|
+
<div className="space-y-4">
|
|
1673
|
+
<div>
|
|
1674
|
+
<label className="block text-sm text-[#666] mb-1">pip Package</label>
|
|
1675
|
+
<input
|
|
1676
|
+
type="text"
|
|
1677
|
+
value={pkg}
|
|
1678
|
+
onChange={e => {
|
|
1679
|
+
setPkg(e.target.value);
|
|
1680
|
+
// Auto-set module from package name
|
|
1681
|
+
if (!pipModule && e.target.value) {
|
|
1682
|
+
const basePkg = e.target.value.split("[")[0].replace(/-/g, ".");
|
|
1683
|
+
setPipModule(basePkg);
|
|
1684
|
+
}
|
|
1685
|
+
}}
|
|
1686
|
+
placeholder="e.g., late-sdk[mcp]"
|
|
1687
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
1688
|
+
/>
|
|
1689
|
+
<p className="text-xs text-[#555] mt-1">
|
|
1690
|
+
Python package with extras, e.g., late-sdk[mcp] or mcp-server-time
|
|
1691
|
+
</p>
|
|
1692
|
+
</div>
|
|
1693
|
+
<div>
|
|
1694
|
+
<label className="block text-sm text-[#666] mb-1">Module (optional)</label>
|
|
1695
|
+
<input
|
|
1696
|
+
type="text"
|
|
1697
|
+
value={pipModule}
|
|
1698
|
+
onChange={e => setPipModule(e.target.value)}
|
|
1699
|
+
placeholder="e.g., late.mcp"
|
|
1700
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[#f97316]"
|
|
1701
|
+
/>
|
|
1702
|
+
<p className="text-xs text-[#555] mt-1">
|
|
1703
|
+
Python module to run with -m. Auto-detected from package name if not specified.
|
|
1704
|
+
</p>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
)}
|
|
1708
|
+
|
|
1443
1709
|
{/* Custom Command */}
|
|
1444
1710
|
{mode === "command" && (
|
|
1445
1711
|
<div>
|
|
@@ -1457,6 +1723,49 @@ function AddServerModal({
|
|
|
1457
1723
|
</div>
|
|
1458
1724
|
)}
|
|
1459
1725
|
|
|
1726
|
+
{/* HTTP Endpoint */}
|
|
1727
|
+
{mode === "http" && (
|
|
1728
|
+
<div className="space-y-4">
|
|
1729
|
+
<div>
|
|
1730
|
+
<label className="block text-sm text-[#666] mb-1">URL</label>
|
|
1731
|
+
<input
|
|
1732
|
+
type="text"
|
|
1733
|
+
value={url}
|
|
1734
|
+
onChange={e => setUrl(e.target.value)}
|
|
1735
|
+
placeholder="e.g., https://example.com/wp-json/mcp/v1/messages"
|
|
1736
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[#f97316]"
|
|
1737
|
+
/>
|
|
1738
|
+
</div>
|
|
1739
|
+
<div className="p-3 bg-[#0a0a0a] border border-[#222] rounded">
|
|
1740
|
+
<p className="text-xs text-[#666] mb-3">
|
|
1741
|
+
Optional: Basic Auth credentials (will be encoded and stored securely)
|
|
1742
|
+
</p>
|
|
1743
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1744
|
+
<div>
|
|
1745
|
+
<label className="block text-xs text-[#555] mb-1">Username</label>
|
|
1746
|
+
<input
|
|
1747
|
+
type="text"
|
|
1748
|
+
value={username}
|
|
1749
|
+
onChange={e => setUsername(e.target.value)}
|
|
1750
|
+
placeholder="username"
|
|
1751
|
+
className="w-full bg-[#111] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
|
|
1752
|
+
/>
|
|
1753
|
+
</div>
|
|
1754
|
+
<div>
|
|
1755
|
+
<label className="block text-xs text-[#555] mb-1">Password</label>
|
|
1756
|
+
<input
|
|
1757
|
+
type="password"
|
|
1758
|
+
value={password}
|
|
1759
|
+
onChange={e => setPassword(e.target.value)}
|
|
1760
|
+
placeholder="password or app key"
|
|
1761
|
+
className="w-full bg-[#111] border border-[#333] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316]"
|
|
1762
|
+
/>
|
|
1763
|
+
</div>
|
|
1764
|
+
</div>
|
|
1765
|
+
</div>
|
|
1766
|
+
</div>
|
|
1767
|
+
)}
|
|
1768
|
+
|
|
1460
1769
|
{/* Environment Variables / Credentials */}
|
|
1461
1770
|
<div>
|
|
1462
1771
|
<div className="flex items-center justify-between mb-2">
|
|
@@ -1520,7 +1829,7 @@ function AddServerModal({
|
|
|
1520
1829
|
</button>
|
|
1521
1830
|
<button
|
|
1522
1831
|
onClick={handleAdd}
|
|
1523
|
-
disabled={saving || !name || (mode === "npm" ? !pkg : !command)}
|
|
1832
|
+
disabled={saving || !name || (mode === "npm" ? !pkg : mode === "pip" ? !pkg : mode === "http" ? !url : !command)}
|
|
1524
1833
|
className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] text-black rounded font-medium transition disabled:opacity-50"
|
|
1525
1834
|
>
|
|
1526
1835
|
{saving ? "Adding..." : "Add Server"}
|
|
@@ -1530,3 +1839,277 @@ function AddServerModal({
|
|
|
1530
1839
|
</div>
|
|
1531
1840
|
);
|
|
1532
1841
|
}
|
|
1842
|
+
|
|
1843
|
+
function EditServerModal({
|
|
1844
|
+
server,
|
|
1845
|
+
projects,
|
|
1846
|
+
onClose,
|
|
1847
|
+
onSaved,
|
|
1848
|
+
}: {
|
|
1849
|
+
server: McpServer;
|
|
1850
|
+
projects?: Array<{ id: string; name: string; color: string }>;
|
|
1851
|
+
onClose: () => void;
|
|
1852
|
+
onSaved: () => void;
|
|
1853
|
+
}) {
|
|
1854
|
+
const { authFetch } = useAuth();
|
|
1855
|
+
const [name, setName] = useState(server.name);
|
|
1856
|
+
const [pkg, setPkg] = useState(server.package || "");
|
|
1857
|
+
const [command, setCommand] = useState(server.command || "");
|
|
1858
|
+
const [args, setArgs] = useState(server.args || "");
|
|
1859
|
+
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>(() => {
|
|
1860
|
+
// Convert env object to array format
|
|
1861
|
+
return Object.entries(server.env || {}).map(([key, value]) => ({ key, value }));
|
|
1862
|
+
});
|
|
1863
|
+
const [projectId, setProjectId] = useState<string | null>(server.project_id);
|
|
1864
|
+
const [saving, setSaving] = useState(false);
|
|
1865
|
+
const [error, setError] = useState<string | null>(null);
|
|
1866
|
+
|
|
1867
|
+
const hasProjects = projects && projects.length > 0;
|
|
1868
|
+
const isRemote = server.type === "http" && server.url;
|
|
1869
|
+
|
|
1870
|
+
const addEnvVar = () => {
|
|
1871
|
+
setEnvVars([...envVars, { key: "", value: "" }]);
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
|
|
1875
|
+
const updated = [...envVars];
|
|
1876
|
+
updated[index][field] = value;
|
|
1877
|
+
setEnvVars(updated);
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const removeEnvVar = (index: number) => {
|
|
1881
|
+
setEnvVars(envVars.filter((_, i) => i !== index));
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
const handleSave = async () => {
|
|
1885
|
+
if (!name.trim()) {
|
|
1886
|
+
setError("Name is required");
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
setSaving(true);
|
|
1891
|
+
setError(null);
|
|
1892
|
+
|
|
1893
|
+
// Build env object from envVars array
|
|
1894
|
+
const env: Record<string, string> = {};
|
|
1895
|
+
for (const { key, value } of envVars) {
|
|
1896
|
+
if (key.trim()) {
|
|
1897
|
+
env[key.trim()] = value;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
try {
|
|
1902
|
+
const updates: Record<string, unknown> = {
|
|
1903
|
+
name: name.trim(),
|
|
1904
|
+
env,
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
// Only include fields that are relevant to the server type
|
|
1908
|
+
if (!isRemote) {
|
|
1909
|
+
if (server.type === "npm" && pkg.trim()) {
|
|
1910
|
+
updates.package = pkg.trim();
|
|
1911
|
+
}
|
|
1912
|
+
if (server.type === "custom") {
|
|
1913
|
+
if (command.trim()) updates.command = command.trim();
|
|
1914
|
+
if (args.trim()) updates.args = args.trim();
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Include project_id update
|
|
1919
|
+
updates.project_id = projectId;
|
|
1920
|
+
|
|
1921
|
+
const res = await authFetch(`/api/mcp/servers/${server.id}`, {
|
|
1922
|
+
method: "PUT",
|
|
1923
|
+
headers: { "Content-Type": "application/json" },
|
|
1924
|
+
body: JSON.stringify(updates),
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
if (!res.ok) {
|
|
1928
|
+
const data = await res.json();
|
|
1929
|
+
setError(data.error || "Failed to save changes");
|
|
1930
|
+
setSaving(false);
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// If server was running, restart it to apply new env vars
|
|
1935
|
+
if (server.status === "running" && !isRemote) {
|
|
1936
|
+
try {
|
|
1937
|
+
// Stop the server
|
|
1938
|
+
await authFetch(`/api/mcp/servers/${server.id}/stop`, { method: "POST" });
|
|
1939
|
+
// Start it again
|
|
1940
|
+
await authFetch(`/api/mcp/servers/${server.id}/start`, { method: "POST" });
|
|
1941
|
+
} catch (e) {
|
|
1942
|
+
console.error("Failed to restart server:", e);
|
|
1943
|
+
// Don't fail the save, just log the error
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
onSaved();
|
|
1948
|
+
} catch (e) {
|
|
1949
|
+
setError("Failed to save changes");
|
|
1950
|
+
setSaving(false);
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
return (
|
|
1955
|
+
<div className="fixed inset-0 bg-black/50 backdrop-blur-[2px] z-50 flex items-center justify-center p-4">
|
|
1956
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
|
1957
|
+
<div className="p-4 border-b border-[#1a1a1a] flex items-center justify-between sticky top-0 bg-[#111]">
|
|
1958
|
+
<h2 className="text-lg font-semibold">Edit MCP Server</h2>
|
|
1959
|
+
<button onClick={onClose} className="text-[#666] hover:text-[#888]">
|
|
1960
|
+
✕
|
|
1961
|
+
</button>
|
|
1962
|
+
</div>
|
|
1963
|
+
|
|
1964
|
+
<div className="p-4 space-y-4">
|
|
1965
|
+
{/* Server Type Info */}
|
|
1966
|
+
<div className="text-sm text-[#666] bg-[#0a0a0a] border border-[#222] rounded p-3">
|
|
1967
|
+
Type: <span className="text-[#888]">{server.type}</span>
|
|
1968
|
+
{server.package && <> • Package: <span className="text-[#888] font-mono">{server.package}</span></>}
|
|
1969
|
+
{server.command && <> • Command: <span className="text-[#888] font-mono">{server.command}</span></>}
|
|
1970
|
+
{isRemote && server.url && <> • URL: <span className="text-[#888] font-mono text-xs">{server.url}</span></>}
|
|
1971
|
+
</div>
|
|
1972
|
+
|
|
1973
|
+
{/* Name */}
|
|
1974
|
+
<div>
|
|
1975
|
+
<label className="block text-sm text-[#666] mb-1">Name</label>
|
|
1976
|
+
<input
|
|
1977
|
+
type="text"
|
|
1978
|
+
value={name}
|
|
1979
|
+
onChange={e => setName(e.target.value)}
|
|
1980
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
1981
|
+
/>
|
|
1982
|
+
</div>
|
|
1983
|
+
|
|
1984
|
+
{/* Project Scope */}
|
|
1985
|
+
{hasProjects && (
|
|
1986
|
+
<div>
|
|
1987
|
+
<label className="block text-sm text-[#666] mb-1">Scope</label>
|
|
1988
|
+
<Select
|
|
1989
|
+
value={projectId || ""}
|
|
1990
|
+
onChange={(value) => setProjectId(value || null)}
|
|
1991
|
+
options={[
|
|
1992
|
+
{ value: "", label: "Global (all projects)" },
|
|
1993
|
+
...projects!.map(p => ({ value: p.id, label: p.name }))
|
|
1994
|
+
]}
|
|
1995
|
+
placeholder="Select scope..."
|
|
1996
|
+
/>
|
|
1997
|
+
</div>
|
|
1998
|
+
)}
|
|
1999
|
+
|
|
2000
|
+
{/* Package (for npm type) */}
|
|
2001
|
+
{server.type === "npm" && (
|
|
2002
|
+
<div>
|
|
2003
|
+
<label className="block text-sm text-[#666] mb-1">npm Package</label>
|
|
2004
|
+
<input
|
|
2005
|
+
type="text"
|
|
2006
|
+
value={pkg}
|
|
2007
|
+
onChange={e => setPkg(e.target.value)}
|
|
2008
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[#f97316]"
|
|
2009
|
+
/>
|
|
2010
|
+
</div>
|
|
2011
|
+
)}
|
|
2012
|
+
|
|
2013
|
+
{/* Command & Args (for custom type) */}
|
|
2014
|
+
{server.type === "custom" && (
|
|
2015
|
+
<>
|
|
2016
|
+
<div>
|
|
2017
|
+
<label className="block text-sm text-[#666] mb-1">Command</label>
|
|
2018
|
+
<input
|
|
2019
|
+
type="text"
|
|
2020
|
+
value={command}
|
|
2021
|
+
onChange={e => setCommand(e.target.value)}
|
|
2022
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[#f97316]"
|
|
2023
|
+
/>
|
|
2024
|
+
</div>
|
|
2025
|
+
<div>
|
|
2026
|
+
<label className="block text-sm text-[#666] mb-1">Arguments</label>
|
|
2027
|
+
<input
|
|
2028
|
+
type="text"
|
|
2029
|
+
value={args}
|
|
2030
|
+
onChange={e => setArgs(e.target.value)}
|
|
2031
|
+
placeholder="e.g., --token $TOKEN --verbose"
|
|
2032
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 font-mono text-sm focus:outline-none focus:border-[#f97316]"
|
|
2033
|
+
/>
|
|
2034
|
+
</div>
|
|
2035
|
+
</>
|
|
2036
|
+
)}
|
|
2037
|
+
|
|
2038
|
+
{/* Environment Variables */}
|
|
2039
|
+
{!isRemote && (
|
|
2040
|
+
<div>
|
|
2041
|
+
<div className="flex items-center justify-between mb-2">
|
|
2042
|
+
<label className="text-sm text-[#666]">
|
|
2043
|
+
Environment Variables / Credentials
|
|
2044
|
+
</label>
|
|
2045
|
+
<button
|
|
2046
|
+
onClick={addEnvVar}
|
|
2047
|
+
className="text-xs text-[#f97316] hover:text-[#fb923c] transition"
|
|
2048
|
+
>
|
|
2049
|
+
+ Add Variable
|
|
2050
|
+
</button>
|
|
2051
|
+
</div>
|
|
2052
|
+
|
|
2053
|
+
{envVars.length === 0 && (
|
|
2054
|
+
<p className="text-xs text-[#555] bg-[#0a0a0a] border border-[#222] rounded p-3">
|
|
2055
|
+
No environment variables configured.
|
|
2056
|
+
</p>
|
|
2057
|
+
)}
|
|
2058
|
+
|
|
2059
|
+
{envVars.length > 0 && (
|
|
2060
|
+
<div className="space-y-2">
|
|
2061
|
+
{envVars.map((env, index) => (
|
|
2062
|
+
<div key={index} className="flex gap-2">
|
|
2063
|
+
<input
|
|
2064
|
+
type="text"
|
|
2065
|
+
value={env.key}
|
|
2066
|
+
onChange={e => updateEnvVar(index, "key", e.target.value)}
|
|
2067
|
+
placeholder="KEY"
|
|
2068
|
+
className="w-1/3 bg-[#0a0a0a] border border-[#333] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[#f97316]"
|
|
2069
|
+
/>
|
|
2070
|
+
<input
|
|
2071
|
+
type="password"
|
|
2072
|
+
value={env.value}
|
|
2073
|
+
onChange={e => updateEnvVar(index, "value", e.target.value)}
|
|
2074
|
+
placeholder="value"
|
|
2075
|
+
className="flex-1 bg-[#0a0a0a] border border-[#333] rounded px-2 py-1.5 text-sm font-mono focus:outline-none focus:border-[#f97316]"
|
|
2076
|
+
/>
|
|
2077
|
+
<button
|
|
2078
|
+
onClick={() => removeEnvVar(index)}
|
|
2079
|
+
className="text-[#666] hover:text-red-400 px-2 transition"
|
|
2080
|
+
>
|
|
2081
|
+
✕
|
|
2082
|
+
</button>
|
|
2083
|
+
</div>
|
|
2084
|
+
))}
|
|
2085
|
+
</div>
|
|
2086
|
+
)}
|
|
2087
|
+
|
|
2088
|
+
<p className="text-xs text-[#555] mt-2">
|
|
2089
|
+
{server.status === "running" ? "Server will be automatically restarted to apply changes." : "Changes will take effect when the server is started."}
|
|
2090
|
+
</p>
|
|
2091
|
+
</div>
|
|
2092
|
+
)}
|
|
2093
|
+
|
|
2094
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
2095
|
+
</div>
|
|
2096
|
+
|
|
2097
|
+
<div className="p-4 border-t border-[#1a1a1a] flex justify-end gap-2 sticky bottom-0 bg-[#111]">
|
|
2098
|
+
<button
|
|
2099
|
+
onClick={onClose}
|
|
2100
|
+
className="px-4 py-2 border border-[#333] hover:border-[#666] rounded transition"
|
|
2101
|
+
>
|
|
2102
|
+
Cancel
|
|
2103
|
+
</button>
|
|
2104
|
+
<button
|
|
2105
|
+
onClick={handleSave}
|
|
2106
|
+
disabled={saving || !name.trim()}
|
|
2107
|
+
className="px-4 py-2 bg-[#f97316] hover:bg-[#fb923c] text-black rounded font-medium transition disabled:opacity-50"
|
|
2108
|
+
>
|
|
2109
|
+
{saving ? "Saving..." : "Save Changes"}
|
|
2110
|
+
</button>
|
|
2111
|
+
</div>
|
|
2112
|
+
</div>
|
|
2113
|
+
</div>
|
|
2114
|
+
);
|
|
2115
|
+
}
|