|
| 1 | +/** |
| 2 | + * Built-in Servers page. |
| 3 | + * |
| 4 | + * Home for the MCP servers McpMux ships itself — distinct from the servers the |
| 5 | + * user installs (those live under "My Servers"). Each built-in server can be |
| 6 | + * enabled/disabled; when on, its tools appear to connected MCP clients |
| 7 | + * alongside the user's own tools. |
| 8 | + * |
| 9 | + * Today there's one concrete built-in server — "Tool Optimization" (the |
| 10 | + * `mcpmux_*` self-management toolset). Memory / Skills / Plugins are scaffolded |
| 11 | + * as "coming soon" so the shape of the framework is visible. Per-workspace |
| 12 | + * enable/disable + per-tool toggles land on top of this in a later pass. |
| 13 | + */ |
| 14 | + |
| 15 | +import { useEffect, useState } from 'react'; |
| 16 | +import { |
| 17 | + Card, |
| 18 | + CardHeader, |
| 19 | + CardTitle, |
| 20 | + CardDescription, |
| 21 | + CardContent, |
| 22 | + Switch, |
| 23 | + useToast, |
| 24 | + ToastContainer, |
| 25 | +} from '@mcpmux/ui'; |
| 26 | +import { |
| 27 | + Sparkles, |
| 28 | + Brain, |
| 29 | + BookOpen, |
| 30 | + Puzzle, |
| 31 | + Wrench, |
| 32 | + Eye, |
| 33 | + Pencil, |
| 34 | + Boxes, |
| 35 | +} from 'lucide-react'; |
| 36 | +import { getMetaToolsEnabled, setMetaToolsEnabled } from '@/lib/api/metaTools'; |
| 37 | +import { MetaToolAuditLog, MetaToolGrantsPanel } from '@/features/metaTools'; |
| 38 | + |
| 39 | +/** A tool the Tool Optimization server exposes — display metadata only. */ |
| 40 | +interface BuiltinTool { |
| 41 | + name: string; |
| 42 | + description: string; |
| 43 | + write: boolean; |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * The `mcpmux_*` tools, mirrored from the gateway's meta-tool registry. Kept |
| 48 | + * here for display; the gateway is the source of truth for what's actually |
| 49 | + * advertised. Writes require a native approval before they run. |
| 50 | + */ |
| 51 | +const TOOL_OPTIMIZATION_TOOLS: BuiltinTool[] = [ |
| 52 | + { |
| 53 | + name: 'mcpmux_list_all_tools', |
| 54 | + description: 'Browse every tool available in the resolved Space, unfiltered.', |
| 55 | + write: false, |
| 56 | + }, |
| 57 | + { |
| 58 | + name: 'mcpmux_list_feature_sets', |
| 59 | + description: 'See the feature sets defined in the Space.', |
| 60 | + write: false, |
| 61 | + }, |
| 62 | + { |
| 63 | + name: 'mcpmux_create_feature_set', |
| 64 | + description: 'Build a focused feature set from chosen tools.', |
| 65 | + write: true, |
| 66 | + }, |
| 67 | + { |
| 68 | + name: 'mcpmux_bind_current_workspace', |
| 69 | + description: 'Map the current folder to a feature set so it persists.', |
| 70 | + write: true, |
| 71 | + }, |
| 72 | +]; |
| 73 | + |
| 74 | +interface ComingSoonServer { |
| 75 | + id: string; |
| 76 | + name: string; |
| 77 | + description: string; |
| 78 | + icon: React.ReactNode; |
| 79 | +} |
| 80 | + |
| 81 | +const COMING_SOON: ComingSoonServer[] = [ |
| 82 | + { |
| 83 | + id: 'memory', |
| 84 | + name: 'Memory', |
| 85 | + description: 'Persistent notes and recall the AI can read and write across sessions.', |
| 86 | + icon: <Brain className="h-5 w-5" />, |
| 87 | + }, |
| 88 | + { |
| 89 | + id: 'skills', |
| 90 | + name: 'Skills', |
| 91 | + description: 'Search a catalog of skills and pull the ones you want into a workspace.', |
| 92 | + icon: <BookOpen className="h-5 w-5" />, |
| 93 | + }, |
| 94 | + { |
| 95 | + id: 'plugins', |
| 96 | + name: 'Plugins', |
| 97 | + description: 'Extend McpMux with community plugins exposed as first-class tools.', |
| 98 | + icon: <Puzzle className="h-5 w-5" />, |
| 99 | + }, |
| 100 | +]; |
| 101 | + |
| 102 | +export function BuiltinServersPage() { |
| 103 | + const { toasts, success, error, dismiss } = useToast(); |
| 104 | + |
| 105 | + const [enabled, setEnabled] = useState(true); |
| 106 | + const [loading, setLoading] = useState(true); |
| 107 | + |
| 108 | + useEffect(() => { |
| 109 | + getMetaToolsEnabled() |
| 110 | + .then(setEnabled) |
| 111 | + .catch((e) => console.error('Failed to load meta_tools_enabled', e)) |
| 112 | + .finally(() => setLoading(false)); |
| 113 | + }, []); |
| 114 | + |
| 115 | + // Live-sync if the switch is flipped elsewhere (the gateway forwards |
| 116 | + // `meta-tools-changed` after a toggle); keep this page honest without a |
| 117 | + // manual refresh. |
| 118 | + useEffect(() => { |
| 119 | + let unlisten: (() => void) | undefined; |
| 120 | + void import('@tauri-apps/api/event').then(({ listen }) => { |
| 121 | + void listen<{ enabled: boolean }>('meta-tools-changed', (e) => { |
| 122 | + setEnabled(e.payload.enabled); |
| 123 | + }).then((fn) => { |
| 124 | + unlisten = fn; |
| 125 | + }); |
| 126 | + }); |
| 127 | + return () => unlisten?.(); |
| 128 | + }, []); |
| 129 | + |
| 130 | + const handleToggle = async (next: boolean) => { |
| 131 | + const previous = enabled; |
| 132 | + setEnabled(next); |
| 133 | + try { |
| 134 | + await setMetaToolsEnabled(next); |
| 135 | + success( |
| 136 | + next ? 'Tool Optimization enabled' : 'Tool Optimization disabled', |
| 137 | + next |
| 138 | + ? 'Connected clients now see the mcpmux_* tools.' |
| 139 | + : 'The mcpmux_* tools are hidden from connected clients.' |
| 140 | + ); |
| 141 | + } catch (e) { |
| 142 | + setEnabled(previous); |
| 143 | + error('Failed to save', e instanceof Error ? e.message : String(e)); |
| 144 | + } |
| 145 | + }; |
| 146 | + |
| 147 | + return ( |
| 148 | + <div className="flex h-full flex-col" data-testid="builtin-servers-page"> |
| 149 | + <header className="flex-shrink-0 border-b border-[rgb(var(--border-subtle))] p-8"> |
| 150 | + <div className="mx-auto max-w-[2000px]"> |
| 151 | + <h1 className="text-3xl font-bold">Built-in Servers</h1> |
| 152 | + <p className="mt-2 max-w-2xl text-base text-[rgb(var(--muted))]"> |
| 153 | + MCP servers that ship with McpMux — separate from the ones you install under{' '} |
| 154 | + <span className="font-medium text-[rgb(var(--foreground))]">My Servers</span>. Turn a |
| 155 | + server on and its tools appear to every connected client alongside your own. |
| 156 | + </p> |
| 157 | + </div> |
| 158 | + </header> |
| 159 | + |
| 160 | + <div className="flex-1 overflow-auto p-8"> |
| 161 | + <div className="mx-auto max-w-3xl space-y-6"> |
| 162 | + {/* Tool Optimization — the mcpmux_* self-management server */} |
| 163 | + <Card data-testid="builtin-server-tool-optimization"> |
| 164 | + <CardHeader> |
| 165 | + <div className="flex items-start justify-between gap-4"> |
| 166 | + <div className="min-w-0"> |
| 167 | + <CardTitle className="flex items-center gap-2"> |
| 168 | + <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-500/10 text-primary-500"> |
| 169 | + <Sparkles className="h-5 w-5" /> |
| 170 | + </span> |
| 171 | + Tool Optimization |
| 172 | + <span className="rounded-md border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--muted))]"> |
| 173 | + Built-in |
| 174 | + </span> |
| 175 | + </CardTitle> |
| 176 | + <CardDescription className="mt-2"> |
| 177 | + Lets the AI keep its own toolset lean: it can browse every available tool, |
| 178 | + assemble a focused feature set, and pin it to the current folder — each with |
| 179 | + your approval. Reads are silent; writes pop a native approval dialog. |
| 180 | + </CardDescription> |
| 181 | + </div> |
| 182 | + <Switch |
| 183 | + checked={enabled} |
| 184 | + onCheckedChange={handleToggle} |
| 185 | + disabled={loading} |
| 186 | + data-testid="meta-tools-enabled-switch" |
| 187 | + /> |
| 188 | + </div> |
| 189 | + </CardHeader> |
| 190 | + <CardContent className="space-y-6"> |
| 191 | + {/* Tools this server exposes */} |
| 192 | + <div> |
| 193 | + <div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[rgb(var(--muted))]"> |
| 194 | + <Wrench className="h-3.5 w-3.5" /> |
| 195 | + Tools ({TOOL_OPTIMIZATION_TOOLS.length}) |
| 196 | + </div> |
| 197 | + <div |
| 198 | + className={`overflow-hidden rounded-xl border border-[rgb(var(--border))] transition-opacity ${ |
| 199 | + enabled ? '' : 'opacity-50' |
| 200 | + }`} |
| 201 | + > |
| 202 | + <div className="divide-y divide-[rgb(var(--border-subtle))]"> |
| 203 | + {TOOL_OPTIMIZATION_TOOLS.map((t) => ( |
| 204 | + <div key={t.name} className="flex items-start gap-3 px-4 py-2.5"> |
| 205 | + {t.write ? ( |
| 206 | + <Pencil className="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500" /> |
| 207 | + ) : ( |
| 208 | + <Eye className="mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-500" /> |
| 209 | + )} |
| 210 | + <div className="min-w-0 flex-1"> |
| 211 | + <div className="flex items-center gap-2"> |
| 212 | + <span className="truncate font-mono text-sm">{t.name}</span> |
| 213 | + <span |
| 214 | + className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${ |
| 215 | + t.write |
| 216 | + ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' |
| 217 | + : 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' |
| 218 | + }`} |
| 219 | + > |
| 220 | + {t.write ? 'write · approval' : 'read'} |
| 221 | + </span> |
| 222 | + </div> |
| 223 | + <p className="mt-0.5 text-xs text-[rgb(var(--muted))]">{t.description}</p> |
| 224 | + </div> |
| 225 | + </div> |
| 226 | + ))} |
| 227 | + </div> |
| 228 | + </div> |
| 229 | + </div> |
| 230 | + |
| 231 | + <MetaToolGrantsPanel /> |
| 232 | + <MetaToolAuditLog /> |
| 233 | + </CardContent> |
| 234 | + </Card> |
| 235 | + |
| 236 | + {/* Framework preview — servers that slot into this same shell later. */} |
| 237 | + <div> |
| 238 | + <div className="mb-3 flex items-center gap-2"> |
| 239 | + <Boxes className="h-4 w-4 text-[rgb(var(--muted))]" /> |
| 240 | + <h2 className="text-sm font-semibold text-[rgb(var(--foreground))]"> |
| 241 | + More built-in servers |
| 242 | + </h2> |
| 243 | + <span className="text-xs text-[rgb(var(--muted))]">coming soon</span> |
| 244 | + </div> |
| 245 | + <div className="grid gap-4 sm:grid-cols-3"> |
| 246 | + {COMING_SOON.map((s) => ( |
| 247 | + <div |
| 248 | + key={s.id} |
| 249 | + className="rounded-xl border border-dashed border-[rgb(var(--border))] bg-[rgb(var(--surface))] p-4 opacity-75" |
| 250 | + data-testid={`builtin-server-coming-soon-${s.id}`} |
| 251 | + > |
| 252 | + <div className="mb-2 flex items-center justify-between"> |
| 253 | + <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-[rgb(var(--background))] text-[rgb(var(--muted))]"> |
| 254 | + {s.icon} |
| 255 | + </span> |
| 256 | + <span className="rounded-md bg-[rgb(var(--background))] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--muted))]"> |
| 257 | + Soon |
| 258 | + </span> |
| 259 | + </div> |
| 260 | + <div className="text-sm font-semibold">{s.name}</div> |
| 261 | + <p className="mt-1 text-xs text-[rgb(var(--muted))]">{s.description}</p> |
| 262 | + </div> |
| 263 | + ))} |
| 264 | + </div> |
| 265 | + </div> |
| 266 | + </div> |
| 267 | + </div> |
| 268 | + |
| 269 | + <ToastContainer toasts={toasts} onClose={dismiss} /> |
| 270 | + </div> |
| 271 | + ); |
| 272 | +} |
0 commit comments