apteva 0.2.11 → 0.3.7

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.
@@ -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 && servers.length > 0 && (
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
- {servers.map(server => {
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
- <h3 className="font-medium">{server.name}</h3>
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 Smithery in Settings to access cloud-based MCP servers.
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={() => quickAdd(s.name, s.pkg)}
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-3 py-1.5 rounded text-sm transition ${
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 Package
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-3 py-1.5 rounded text-sm transition ${
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
- Custom Command
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
+ }