Files
Agentic-OS/frontend/app/mcp/page.tsx
nearxos 6185b9b85a Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 22:11:07 +03:00

200 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import AppShell from "@/components/AppShell";
import { EmptyState, LoadError, Modal, PageHeader, SkeletonRows, StatusBadge } from "@/components/ui";
import { useToast } from "@/components/toast";
import { api, fetcher } from "@/lib/api";
import { useAuth, roleAtLeast } from "@/lib/auth";
type Mcp = {
name: string;
status: string;
error: string | null;
source?: string;
tools: { name: string; description: string | null }[];
};
export default function McpPage() {
const router = useRouter();
const { user } = useAuth();
const isAdmin = roleAtLeast(user?.role, "admin");
const toast = useToast();
const {
data: mcps,
error: mcpError,
isLoading,
mutate,
} = useSWR<Mcp[]>("/api/mcp/servers", fetcher, { refreshInterval: 15000 });
const [expanded, setExpanded] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [developOpen, setDevelopOpen] = useState<string | null>(null);
const [developIssue, setDevelopIssue] = useState("");
const run = async (key: string, label: string, fn: () => Promise<void>) => {
setBusy(key);
try {
await fn();
toast.success(`${label} — status will refresh shortly.`);
mutate();
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : "Action failed");
} finally {
setBusy(null);
}
};
return (
<AppShell>
<PageHeader
title="MCP Servers"
subtitle="Device integrations — reload or upgrade when tools fail or repos change"
action={
isAdmin && (
<button
className="btn-ghost text-sm"
disabled={!!busy}
onClick={() =>
run("all", "Reload all queued", async () => {
await api("/api/mcp/reload-all", { method: "POST" });
})
}
>
{busy === "all" ? "Reloading…" : "Reload all"}
</button>
)
}
/>
{isLoading && !mcps ? (
<SkeletonRows rows={4} />
) : mcpError ? (
<LoadError message="Couldnt load MCP servers." onRetry={() => mutate()} />
) : (mcps || []).length === 0 ? (
<EmptyState
title="No MCP servers reported yet"
description="The worker clones and launches them on startup; check worker logs if this persists."
/>
) : (
<div className="space-y-4">
{(mcps || []).map((m) => (
<div key={m.name} className="card">
<div className="flex w-full flex-wrap items-center justify-between gap-3">
<button
className="flex flex-1 items-center gap-3 text-left"
onClick={() => setExpanded(expanded === m.name ? null : m.name)}
>
<span className="font-medium">{m.name}</span>
<span className="text-xs text-muted">
{m.tools?.length || 0} tools · {m.source || "git"}
</span>
</button>
<div className="flex items-center gap-2">
{isAdmin && (
<>
<button
className="btn-ghost btn-sm"
disabled={!!busy}
onClick={() =>
run(`reload-${m.name}`, "Reload queued", async () => {
await api(`/api/mcp/servers/${m.name}/reload`, { method: "POST" });
})
}
>
{busy === `reload-${m.name}` ? "…" : "Reload"}
</button>
<button
className="btn-ghost btn-sm text-accent2"
disabled={!!busy}
onClick={() =>
run(`upgrade-${m.name}`, "Upgrade queued", async () => {
await api(`/api/mcp/servers/${m.name}/upgrade`, { method: "POST" });
})
}
>
{busy === `upgrade-${m.name}` ? "…" : "Upgrade"}
</button>
<button
className="btn-ghost btn-sm text-accent"
disabled={!!busy}
onClick={() => {
setDevelopIssue("");
setDevelopOpen(m.name);
}}
>
Develop
</button>
</>
)}
<StatusBadge status={m.status} />
</div>
</div>
{m.error && <div className="mt-2 text-sm text-bad">{m.error}</div>}
{expanded === m.name && m.tools?.length > 0 && (
<div className="mt-4 grid grid-cols-1 gap-2 md:grid-cols-2">
{m.tools.map((t) => (
<div key={t.name} className="rounded-lg bg-panel2 px-3 py-2">
<div className="font-mono text-xs text-accent2">{t.name}</div>
{t.description && (
<div className="mt-1 line-clamp-2 text-xs text-muted">{t.description}</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
<Modal
open={!!developOpen}
onClose={() => setDevelopOpen(null)}
title={`Develop ${developOpen || ""}`}
>
<p className="text-sm text-muted">
Describe the missing tool or bug. A monitor task opens automatically so you can watch
analyze patch reload git push in the task timeline. Push to Gitea needs approval
unless auto-push is enabled.
</p>
<textarea
className="input mt-4 min-h-[120px]"
value={developIssue}
onChange={(e) => setDevelopIssue(e.target.value)}
placeholder="e.g. add pfsense_list_firewall_rules_by_interface or fix NameError in get_firewall_log"
/>
<div className="mt-4 flex justify-end gap-2">
<button className="btn-ghost" onClick={() => setDevelopOpen(null)}>
Cancel
</button>
<button
className="btn-primary"
disabled={!developIssue.trim() || !!busy}
onClick={() => {
const server = developOpen;
if (!server) return;
run(`develop-${server}`, "Develop started", async () => {
const res = await api<{ task_id?: number; summary?: string }>(
`/api/mcp/servers/${server}/develop`,
{
method: "POST",
body: JSON.stringify({ issue: developIssue.trim() }),
}
);
setDevelopOpen(null);
if (res.task_id) {
router.push(`/tasks/${res.task_id}`);
}
});
}}
>
{busy === `develop-${developOpen}` ? "Starting…" : "Run & monitor"}
</button>
</div>
</Modal>
</AppShell>
);
}