@harbinger-ai/harbinger 0.1.2 → 0.1.4

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,9 +1,9 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
- import { motion } from "framer-motion";
5
- import { PlugIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, WrenchIcon, CheckIcon, XIcon } from "./icons.js";
6
- import { getMcpServers, getMcpStatus, getOwnMcpServerInfo, testMcpTool, reloadMcpClient } from "../../mcp/actions.js";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { PlugIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, WrenchIcon, CheckIcon, XIcon, SearchIcon, PlusIcon, TrashIcon, DownloadIcon } from "./icons.js";
6
+ import { getMcpServers, getMcpStatus, getOwnMcpServerInfo, testMcpTool, reloadMcpClient, getMcpRegistry, addMcpServer, removeMcpServer, toggleMcpServer } from "../../mcp/actions.js";
7
7
  const cardVariants = {
8
8
  hidden: { opacity: 0, y: 12 },
9
9
  visible: (i) => ({
@@ -12,6 +12,19 @@ const cardVariants = {
12
12
  transition: { delay: i * 0.05, duration: 0.3, ease: "easeOut" }
13
13
  })
14
14
  };
15
+ const CATEGORY_COLORS = {
16
+ search: { bg: "bg-blue-500/10", text: "text-blue-500", border: "border-blue-500/20" },
17
+ web: { bg: "bg-orange-500/10", text: "text-orange-500", border: "border-orange-500/20" },
18
+ dev: { bg: "bg-purple-500/10", text: "text-purple-500", border: "border-purple-500/20" },
19
+ communication: { bg: "bg-green-500/10", text: "text-green-500", border: "border-green-500/20" },
20
+ system: { bg: "bg-gray-500/10", text: "text-gray-500", border: "border-gray-500/20" },
21
+ database: { bg: "bg-cyan-500/10", text: "text-cyan-500", border: "border-cyan-500/20" },
22
+ monitoring: { bg: "bg-yellow-500/10", text: "text-yellow-500", border: "border-yellow-500/20" },
23
+ cloud: { bg: "bg-indigo-500/10", text: "text-indigo-500", border: "border-indigo-500/20" }
24
+ };
25
+ function getCatStyle(cat) {
26
+ return CATEGORY_COLORS[cat] || { bg: "bg-white/5", text: "text-muted-foreground", border: "border-white/10" };
27
+ }
15
28
  function SectionHeader({ label, count }) {
16
29
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-6 pb-2", children: [
17
30
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: label }),
@@ -39,9 +52,104 @@ function ItemCard({ name, description, badge, index = 0 }) {
39
52
  }
40
53
  );
41
54
  }
42
- function ServerCard({ server, index = 0 }) {
55
+ function RegistryCard({ server, installed, onInstall, installing, index }) {
56
+ const [apiKey, setApiKey] = useState("");
57
+ const [showKeyInput, setShowKeyInput] = useState(false);
58
+ const cat = getCatStyle(server.category);
59
+ function handleAdd() {
60
+ if (server.requiresKey && !apiKey && !showKeyInput) {
61
+ setShowKeyInput(true);
62
+ return;
63
+ }
64
+ const config = {
65
+ name: server.name,
66
+ url: server.url || "",
67
+ transport: server.transport || "http",
68
+ enabled: true
69
+ };
70
+ if (apiKey && server.keyEnvVar) {
71
+ config.headers = { Authorization: `Bearer ${apiKey}` };
72
+ }
73
+ if (server.npmPackage) {
74
+ config.command = "npx";
75
+ config.args = ["-y", server.npmPackage];
76
+ config.transport = "stdio";
77
+ }
78
+ onInstall(config);
79
+ }
80
+ return /* @__PURE__ */ jsxs(
81
+ motion.div,
82
+ {
83
+ initial: { opacity: 0, y: 8 },
84
+ animate: { opacity: 1, y: 0 },
85
+ transition: { duration: 0.25, delay: index * 0.02 },
86
+ className: "group rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors overflow-hidden",
87
+ children: [
88
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3", children: [
89
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
90
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
91
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
92
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
93
+ ] }),
94
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
95
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium truncate", children: server.name }),
96
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground mt-0.5 line-clamp-1 font-mono", children: server.description })
97
+ ] }),
98
+ /* @__PURE__ */ jsx("span", { className: `shrink-0 inline-flex rounded-full px-2 py-0.5 text-[10px] font-mono font-medium border ${cat.bg} ${cat.text} ${cat.border}`, children: server.category }),
99
+ installed ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20 px-2 py-0.5 text-[10px] font-mono font-medium", children: [
100
+ /* @__PURE__ */ jsx(CheckIcon, { size: 10 }),
101
+ " added"
102
+ ] }) : /* @__PURE__ */ jsxs(
103
+ "button",
104
+ {
105
+ onClick: handleAdd,
106
+ disabled: installing,
107
+ className: "shrink-0 inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
108
+ children: [
109
+ installing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(DownloadIcon, { size: 12 }),
110
+ "Add"
111
+ ]
112
+ }
113
+ )
114
+ ] }),
115
+ showKeyInput && !installed && /* @__PURE__ */ jsxs("div", { className: "border-t border-white/[0.06] px-3 py-2.5 flex gap-2", children: [
116
+ /* @__PURE__ */ jsx(
117
+ "input",
118
+ {
119
+ type: "password",
120
+ value: apiKey,
121
+ onChange: (e) => setApiKey(e.target.value),
122
+ placeholder: `${server.keyEnvVar || "API_KEY"}...`,
123
+ className: "flex-1 text-xs border border-white/[0.06] rounded-md px-2.5 py-1.5 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors",
124
+ autoFocus: true
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsx(
128
+ "button",
129
+ {
130
+ onClick: handleAdd,
131
+ disabled: !apiKey,
132
+ className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
133
+ children: "Add"
134
+ }
135
+ )
136
+ ] })
137
+ ]
138
+ }
139
+ );
140
+ }
141
+ function ServerCard({ server, onToggle, onRemove, index = 0 }) {
43
142
  const [expanded, setExpanded] = useState(false);
143
+ const [confirming, setConfirming] = useState(false);
44
144
  const disabled = server.enabled === false;
145
+ function handleRemove() {
146
+ if (!confirming) {
147
+ setConfirming(true);
148
+ setTimeout(() => setConfirming(false), 3e3);
149
+ return;
150
+ }
151
+ onRemove(server.name);
152
+ }
45
153
  return /* @__PURE__ */ jsxs(
46
154
  motion.div,
47
155
  {
@@ -63,31 +171,161 @@ function ServerCard({ server, index = 0 }) {
63
171
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5 font-mono truncate", children: server.url })
64
172
  ] }),
65
173
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
66
- /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-mono font-medium ${server.transport === "http" ? "bg-blue-500/10 text-blue-400 border border-blue-500/20" : "bg-orange-500/10 text-orange-400 border border-orange-500/20"}`, children: server.transport || "http" }),
174
+ /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-mono font-medium ${server.transport === "http" ? "bg-blue-500/10 text-blue-400 border border-blue-500/20" : server.transport === "sse" ? "bg-orange-500/10 text-orange-400 border border-orange-500/20" : "bg-purple-500/10 text-purple-400 border border-purple-500/20"}`, children: server.transport || "http" }),
67
175
  /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-mono font-medium ${disabled ? "bg-white/[0.04] text-muted-foreground border border-white/[0.06]" : "bg-[--success]/10 text-[--success] border border-[--success]/20"}`, children: disabled ? "disabled" : "enabled" }),
68
176
  /* @__PURE__ */ jsx("span", { className: `transition-transform ${expanded ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: 14 }) })
69
177
  ] })
70
178
  ]
71
179
  }
72
180
  ),
73
- expanded && /* @__PURE__ */ jsx("div", { className: "border-t border-white/[0.06] px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
74
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
75
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "URL" }),
76
- /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-foreground/80", children: server.url })
77
- ] }),
78
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
79
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "Transport" }),
80
- /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-foreground/80", children: server.transport || "http" })
81
- ] }),
82
- server.headers && Object.keys(server.headers).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
83
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "Headers" }),
84
- /* @__PURE__ */ jsx("pre", { className: "text-[11px] bg-black/30 rounded-md p-2.5 mt-1 font-mono overflow-auto max-h-24 text-foreground/80 border border-white/[0.04]", children: JSON.stringify(server.headers, null, 2) })
85
- ] })
86
- ] }) })
181
+ /* @__PURE__ */ jsx(AnimatePresence, { children: expanded && /* @__PURE__ */ jsx(
182
+ motion.div,
183
+ {
184
+ initial: { height: 0, opacity: 0 },
185
+ animate: { height: "auto", opacity: 1 },
186
+ exit: { height: 0, opacity: 0 },
187
+ transition: { duration: 0.2 },
188
+ className: "overflow-hidden",
189
+ children: /* @__PURE__ */ jsxs("div", { className: "border-t border-white/[0.06] px-4 py-3", children: [
190
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
191
+ server.url && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
192
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider w-16 shrink-0", children: "URL" }),
193
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-foreground/80", children: server.url })
194
+ ] }),
195
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
196
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider w-16 shrink-0", children: "Transport" }),
197
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-foreground/80", children: server.transport || "http" })
198
+ ] }),
199
+ server.headers && Object.keys(server.headers).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
200
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "Headers" }),
201
+ /* @__PURE__ */ jsx("pre", { className: "text-[11px] bg-black/30 rounded-md p-2.5 mt-1 font-mono overflow-auto max-h-24 text-foreground/80 border border-white/[0.04]", children: JSON.stringify(server.headers, null, 2) })
202
+ ] })
203
+ ] }),
204
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-3", children: [
205
+ /* @__PURE__ */ jsx(
206
+ "button",
207
+ {
208
+ onClick: (e) => {
209
+ e.stopPropagation();
210
+ onToggle(server.name);
211
+ },
212
+ className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors",
213
+ children: disabled ? "Enable" : "Disable"
214
+ }
215
+ ),
216
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
217
+ /* @__PURE__ */ jsx(
218
+ "button",
219
+ {
220
+ onClick: (e) => {
221
+ e.stopPropagation();
222
+ handleRemove();
223
+ },
224
+ className: `text-xs font-mono transition-colors ${confirming ? "text-[--destructive]" : "text-muted-foreground hover:text-[--destructive]"}`,
225
+ children: confirming ? "Confirm remove" : "Remove"
226
+ }
227
+ )
228
+ ] })
229
+ ] })
230
+ }
231
+ ) })
87
232
  ]
88
233
  }
89
234
  );
90
235
  }
236
+ function AddServerForm({ onAdd }) {
237
+ const [name, setName] = useState("");
238
+ const [url, setUrl] = useState("");
239
+ const [transport, setTransport] = useState("http");
240
+ const [headers, setHeaders] = useState("");
241
+ const [loading, setLoading] = useState(false);
242
+ const [result, setResult] = useState(null);
243
+ async function handleAdd() {
244
+ if (!name || !url) return;
245
+ setLoading(true);
246
+ setResult(null);
247
+ let headerObj = {};
248
+ if (headers) {
249
+ try {
250
+ headerObj = JSON.parse(headers);
251
+ } catch {
252
+ setResult({ error: "Invalid JSON headers" });
253
+ setLoading(false);
254
+ return;
255
+ }
256
+ }
257
+ const res = await onAdd({ name, url, transport, headers: headerObj, enabled: true });
258
+ setResult(res);
259
+ if (!res.error) {
260
+ setName("");
261
+ setUrl("");
262
+ setHeaders("");
263
+ }
264
+ setLoading(false);
265
+ }
266
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-white/[0.06] bg-[--card] p-4 mb-4", children: [
267
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
268
+ /* @__PURE__ */ jsx(PlusIcon, { size: 14 }),
269
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium text-[--cyan]", children: "Add Custom Server" })
270
+ ] }),
271
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3", children: [
272
+ /* @__PURE__ */ jsx(
273
+ "input",
274
+ {
275
+ placeholder: "Server name",
276
+ value: name,
277
+ onChange: (e) => setName(e.target.value),
278
+ className: "text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
279
+ }
280
+ ),
281
+ /* @__PURE__ */ jsx(
282
+ "input",
283
+ {
284
+ placeholder: "https://mcp.example.com/sse",
285
+ value: url,
286
+ onChange: (e) => setUrl(e.target.value),
287
+ className: "text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
288
+ }
289
+ ),
290
+ /* @__PURE__ */ jsxs(
291
+ "select",
292
+ {
293
+ value: transport,
294
+ onChange: (e) => setTransport(e.target.value),
295
+ className: "text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors appearance-none",
296
+ children: [
297
+ /* @__PURE__ */ jsx("option", { value: "http", children: "HTTP" }),
298
+ /* @__PURE__ */ jsx("option", { value: "sse", children: "SSE" })
299
+ ]
300
+ }
301
+ ),
302
+ /* @__PURE__ */ jsx(
303
+ "input",
304
+ {
305
+ placeholder: '{"Authorization": "Bearer ..."}',
306
+ value: headers,
307
+ onChange: (e) => setHeaders(e.target.value),
308
+ className: "text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
309
+ }
310
+ )
311
+ ] }),
312
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
313
+ /* @__PURE__ */ jsxs(
314
+ "button",
315
+ {
316
+ onClick: handleAdd,
317
+ disabled: loading || !name || !url,
318
+ className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
319
+ children: [
320
+ loading ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(PlusIcon, { size: 12 }),
321
+ " Add Server"
322
+ ]
323
+ }
324
+ ),
325
+ result && /* @__PURE__ */ jsx("p", { className: `text-xs font-mono ${result.error ? "text-[--destructive]" : "text-green-500"}`, children: result.error || "Added successfully" })
326
+ ] })
327
+ ] });
328
+ }
91
329
  function ToolCard({ tool, index = 0 }) {
92
330
  const [testing, setTesting] = useState(false);
93
331
  const [result, setResult] = useState(null);
@@ -156,16 +394,22 @@ function McpPage() {
156
394
  const [servers, setServers] = useState([]);
157
395
  const [status, setStatus] = useState(null);
158
396
  const [serverInfo, setServerInfo] = useState(null);
397
+ const [registry, setRegistry] = useState({ servers: [], categories: [] });
398
+ const [search, setSearch] = useState("");
399
+ const [installing, setInstalling] = useState(false);
400
+ const [tab, setTab] = useState("servers");
159
401
  async function load() {
160
402
  try {
161
- const [s, st, info] = await Promise.all([
403
+ const [s, st, info, reg] = await Promise.all([
162
404
  getMcpServers(),
163
405
  getMcpStatus(),
164
- getOwnMcpServerInfo()
406
+ getOwnMcpServerInfo(),
407
+ getMcpRegistry()
165
408
  ]);
166
409
  setServers(s);
167
410
  setStatus(st);
168
411
  setServerInfo(info);
412
+ setRegistry(reg);
169
413
  } catch {
170
414
  }
171
415
  setLoading(false);
@@ -182,11 +426,41 @@ function McpPage() {
182
426
  }
183
427
  setReloading(false);
184
428
  }
429
+ async function handleAddFromRegistry(config) {
430
+ setInstalling(true);
431
+ await addMcpServer(config);
432
+ await load();
433
+ setInstalling(false);
434
+ }
435
+ async function handleAddCustom(config) {
436
+ const result = await addMcpServer(config);
437
+ if (!result.error) await load();
438
+ return result;
439
+ }
440
+ async function handleToggle(name) {
441
+ await toggleMcpServer(name);
442
+ await load();
443
+ }
444
+ async function handleRemove(name) {
445
+ await removeMcpServer(name);
446
+ await load();
447
+ }
185
448
  if (loading) {
186
449
  return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-shimmer rounded-lg border border-white/[0.06] bg-[--card]" }, i)) });
187
450
  }
188
451
  const enabledServers = servers.filter((s) => s.enabled !== false);
189
452
  const disabledServers = servers.filter((s) => s.enabled === false);
453
+ const serverNames = new Set(servers.map((s) => s.name));
454
+ const filteredRegistry = registry.servers.filter((s) => {
455
+ if (!search) return true;
456
+ const q = search.toLowerCase();
457
+ return s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.category.toLowerCase().includes(q);
458
+ });
459
+ const tabs = [
460
+ { id: "servers", label: "SERVERS", count: servers.length },
461
+ { id: "hub", label: "MCP HUB", count: registry.servers.length },
462
+ { id: "tools", label: "TOOLS", count: status?.toolCount || 0 }
463
+ ];
190
464
  return /* @__PURE__ */ jsxs(Fragment, { children: [
191
465
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-3 mb-6", children: [
192
466
  /* @__PURE__ */ jsx(StatsCard, { label: "External Tools", value: status?.toolCount || 0 }),
@@ -214,44 +488,98 @@ function McpPage() {
214
488
  }
215
489
  )
216
490
  ] }),
217
- serverInfo && /* @__PURE__ */ jsxs(Fragment, { children: [
218
- /* @__PURE__ */ jsx(SectionHeader, { label: "Server \u2014 Exposed to External Clients", count: serverInfo.tools.length + serverInfo.resources.length }),
219
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
220
- /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-md bg-[--card] border border-white/[0.06] px-2.5 py-1 text-[11px] font-mono text-muted-foreground", children: "/api/mcp" }),
221
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-[--success]" })
222
- ] }),
223
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 mb-2", children: [
224
- serverInfo.tools.map((t, i) => /* @__PURE__ */ jsx(ItemCard, { name: t.name, description: t.description, badge: "tool", index: i }, t.name)),
225
- serverInfo.resources.map((r, i) => /* @__PURE__ */ jsx(ItemCard, { name: r.uri, description: r.description, badge: "resource", index: serverInfo.tools.length + i }, r.uri)),
226
- serverInfo.prompts.map((p, i) => /* @__PURE__ */ jsx(ItemCard, { name: p.name, description: p.description, badge: "prompt", index: serverInfo.tools.length + serverInfo.resources.length + i }, p.name))
227
- ] })
228
- ] }),
229
- /* @__PURE__ */ jsx(SectionHeader, { label: "Client \u2014 External MCP Servers", count: servers.length }),
230
- servers.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-lg border border-white/[0.06] bg-[--card]", children: [
231
- /* @__PURE__ */ jsx("div", { className: "rounded-full bg-[--cyan]/10 p-4 mb-4", children: /* @__PURE__ */ jsx(PlugIcon, { size: 24, className: "text-[--cyan]" }) }),
232
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1 text-foreground", children: "No external MCP servers configured" }),
233
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
234
- "Add servers by editing ",
235
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[--cyan]", children: "config/MCP_SERVERS.json" }),
236
- " in your project."
237
- ] })
238
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
239
- enabledServers.length > 0 && enabledServers.map((s, i) => /* @__PURE__ */ jsx(ServerCard, { server: s, index: i }, `enabled-${i}`)),
240
- disabledServers.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
241
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2 pb-1", children: [
242
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "Disabled" }),
243
- /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-mono text-muted-foreground", children: [
491
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b border-white/[0.06] mb-4", children: tabs.map((t) => /* @__PURE__ */ jsxs(
492
+ "button",
493
+ {
494
+ onClick: () => setTab(t.id),
495
+ className: `px-4 py-2.5 text-[11px] font-mono font-medium uppercase tracking-wider border-b-2 transition-colors ${tab === t.id ? "border-[--cyan] text-[--cyan]" : "border-transparent text-muted-foreground hover:text-foreground"}`,
496
+ children: [
497
+ t.label,
498
+ " ",
499
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
244
500
  "(",
245
- disabledServers.length,
501
+ t.count,
246
502
  ")"
247
503
  ] })
504
+ ]
505
+ },
506
+ t.id
507
+ )) }),
508
+ /* @__PURE__ */ jsxs(AnimatePresence, { mode: "wait", children: [
509
+ tab === "servers" && /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, children: [
510
+ serverInfo && /* @__PURE__ */ jsxs(Fragment, { children: [
511
+ /* @__PURE__ */ jsx(SectionHeader, { label: "Server \u2014 Exposed to External Clients", count: serverInfo.tools.length + serverInfo.resources.length }),
512
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
513
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-md bg-[--card] border border-white/[0.06] px-2.5 py-1 text-[11px] font-mono text-muted-foreground", children: "/api/mcp" }),
514
+ /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-[--success]" })
515
+ ] }),
516
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 mb-2", children: [
517
+ serverInfo.tools.map((t, i) => /* @__PURE__ */ jsx(ItemCard, { name: t.name, description: t.description, badge: "tool", index: i }, t.name)),
518
+ serverInfo.resources.map((r, i) => /* @__PURE__ */ jsx(ItemCard, { name: r.uri, description: r.description, badge: "resource", index: serverInfo.tools.length + i }, r.uri)),
519
+ serverInfo.prompts.map((p, i) => /* @__PURE__ */ jsx(ItemCard, { name: p.name, description: p.description, badge: "prompt", index: serverInfo.tools.length + serverInfo.resources.length + i }, p.name))
520
+ ] })
248
521
  ] }),
249
- disabledServers.map((s, i) => /* @__PURE__ */ jsx(ServerCard, { server: s, index: i }, `disabled-${i}`))
250
- ] })
251
- ] }),
252
- status?.tools?.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
253
- /* @__PURE__ */ jsx(SectionHeader, { label: "Loaded External Tools", count: status.tools.length }),
254
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: status.tools.map((t, i) => /* @__PURE__ */ jsx(ToolCard, { tool: t, index: i }, t.name)) })
522
+ /* @__PURE__ */ jsx(SectionHeader, { label: "Client \u2014 External MCP Servers", count: servers.length }),
523
+ /* @__PURE__ */ jsx(AddServerForm, { onAdd: handleAddCustom }),
524
+ servers.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-lg border border-white/[0.06] bg-[--card]", children: [
525
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-[--cyan]/10 p-4 mb-4", children: /* @__PURE__ */ jsx(PlugIcon, { size: 24, className: "text-[--cyan]" }) }),
526
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1 text-foreground", children: "No external MCP servers configured" }),
527
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
528
+ "Add servers from the ",
529
+ /* @__PURE__ */ jsx("button", { onClick: () => setTab("hub"), className: "text-[--cyan] hover:underline", children: "MCP Hub" }),
530
+ " or add a custom server above."
531
+ ] })
532
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
533
+ enabledServers.map((s, i) => /* @__PURE__ */ jsx(ServerCard, { server: s, onToggle: handleToggle, onRemove: handleRemove, index: i }, `enabled-${s.name}-${i}`)),
534
+ disabledServers.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
535
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2 pb-1", children: [
536
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-muted-foreground uppercase tracking-wider", children: "Disabled" }),
537
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-mono text-muted-foreground", children: [
538
+ "(",
539
+ disabledServers.length,
540
+ ")"
541
+ ] })
542
+ ] }),
543
+ disabledServers.map((s, i) => /* @__PURE__ */ jsx(ServerCard, { server: s, onToggle: handleToggle, onRemove: handleRemove, index: i }, `disabled-${s.name}-${i}`))
544
+ ] })
545
+ ] })
546
+ ] }, "servers"),
547
+ tab === "hub" && /* @__PURE__ */ jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, children: [
548
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-4", children: [
549
+ /* @__PURE__ */ jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground", children: /* @__PURE__ */ jsx(SearchIcon, { size: 14 }) }),
550
+ /* @__PURE__ */ jsx(
551
+ "input",
552
+ {
553
+ placeholder: "Search MCP servers...",
554
+ value: search,
555
+ onChange: (e) => setSearch(e.target.value),
556
+ className: "w-full text-sm border border-white/[0.06] rounded-md pl-9 pr-3 py-2 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
557
+ }
558
+ )
559
+ ] }),
560
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
561
+ filteredRegistry.map((s, i) => /* @__PURE__ */ jsx(
562
+ RegistryCard,
563
+ {
564
+ server: s,
565
+ installed: serverNames.has(s.name),
566
+ onInstall: handleAddFromRegistry,
567
+ installing,
568
+ index: i
569
+ },
570
+ s.id
571
+ )),
572
+ filteredRegistry.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center py-12 text-center", children: [
573
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4", children: /* @__PURE__ */ jsx(SearchIcon, { size: 24 }) }),
574
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono text-muted-foreground", children: "No servers match your search." })
575
+ ] })
576
+ ] })
577
+ ] }, "hub"),
578
+ tab === "tools" && /* @__PURE__ */ jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, children: status?.tools?.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: status.tools.map((t, i) => /* @__PURE__ */ jsx(ToolCard, { tool: t, index: i }, t.name)) }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
579
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4", children: /* @__PURE__ */ jsx(WrenchIcon, { size: 24 }) }),
580
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium mb-1", children: "No tools loaded" }),
581
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground font-mono", children: "Add MCP servers to load external tools." })
582
+ ] }) }, "tools")
255
583
  ] })
256
584
  ] });
257
585
  }