feat(mcp): onglet Configuration — catalogue, toggles, test de connexion

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-07-18 17:00:55 +02:00
1 parent 91e0a9f0b2
commit afe0a837d4
2 files changed
+161 -1

No files matched your search

+39
View File
@@ -428,6 +428,45 @@ export function downloadFile(path: string): void {
link.remove();
}
export interface McpServer {
id: string;
label: string;
description: string;
url_param: boolean;
env_params: string[];
enabled: boolean;
params: Record<string, string>;
state: "inactive" | "connected" | "error";
error: string | null;
tools: number;
}
export async function listMcp(): Promise<McpServer[]> {
const res = await fetch("/api/mcp");
return (await res.json()).servers;
}
export async function updateMcp(
id: string,
enabled: boolean,
params: Record<string, string>
): Promise<McpServer[]> {
const res = await fetch(`/api/mcp/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled, params }),
});
if (!res.ok) throw await apiError(res, "mise à jour MCP impossible");
return (await res.json()).servers;
}
export async function testMcp(
id: string
): Promise<{ ok: boolean; tools: string[]; error: string | null }> {
const res = await fetch(`/api/mcp/${id}/test`, { method: "POST" });
return res.json();
}
export async function listSessions(): Promise<Session[]> {
const res = await fetch("/api/sessions");
return (await res.json()).sessions;
+122 -1
View File
@@ -4,10 +4,18 @@ import {
deleteModel,
getBenchScores,
getHardware,
listMcp,
pullModel,
runBench,
testMcp,
updateMcp,
} from "../api/client";
import type {
AgentConfig,
BenchResult,
HardwareInfo,
McpServer,
} from "../api/client";
import type { AgentConfig, BenchResult, HardwareInfo } from "../api/client";
import { DownloadIcon, RefreshIcon } from "../components/Icon";
const TOOL_DESC: Record<string, string> = {
@@ -466,6 +474,8 @@ export function SettingsView() {
</div>
</Card>
<McpCard />
<HardwareCard />
<BenchCard />
@@ -498,6 +508,117 @@ export function SettingsView() {
/** Benchmark : évalue le modèle sélectionné sur 5 mini-épreuves. */
/** Panneau Matériel : GPU vu par Loki vs GPU réellement utilisé par Ollama. */
function McpCard() {
const [servers, setServers] = useState<McpServer[]>([]);
const [testing, setTesting] = useState<string | null>(null);
const [testResult, setTestResult] = useState<Record<string, string>>({});
useEffect(() => {
listMcp().then(setServers).catch(() => {});
}, []);
const toggle = async (s: McpServer) => {
setServers(await updateMcp(s.id, !s.enabled, s.params));
};
const setParam = async (s: McpServer, key: string, value: string) => {
setServers(await updateMcp(s.id, s.enabled, { ...s.params, [key]: value }));
};
const doTest = async (s: McpServer) => {
setTesting(s.id);
try {
const r = await testMcp(s.id);
setTestResult((m) => ({
...m,
[s.id]: r.ok
? `✓ ${r.tools.length} outil(s) : ${r.tools.join(", ").slice(0, 120)}`
: `✗ ${r.error ?? "échec"}`,
}));
} finally {
setTesting(null);
}
};
return (
<Card>
<div className="mb-1 font-pixel text-[11px] text-ink">SERVEURS MCP</div>
<div className="mb-3.5 text-[12px] text-muted-2">
Outils professionnels pour l'agent (navigateur, docs, web). Désactivé =
zéro coût.
</div>
<div className="flex flex-col gap-3">
{servers.map((s) => (
<div key={s.id} className="border-[3px] border-line bg-base p-3">
<div className="flex items-center gap-3">
<span
className={`h-[11px] w-[11px] flex-none border-2 border-line ${
s.state === "connected"
? "bg-ok"
: s.state === "error"
? "bg-warn"
: "bg-muted-2"
}`}
title={s.error ?? s.state}
/>
<div className="min-w-0 flex-1">
<div className="text-[14px] font-semibold text-ink">
{s.label}
{s.tools > 0 && (
<span className="ml-2 text-[11px] text-muted-2">
{s.tools} outil(s)
</span>
)}
</div>
<div className="truncate text-[12px] text-muted-2">
{s.description}
</div>
</div>
<button
onClick={() => doTest(s)}
disabled={testing === s.id}
className="border-2 border-line bg-card px-2 py-1 text-[11px] text-ink-2"
>
{testing === s.id ? "test…" : "Tester"}
</button>
<Toggle on={s.enabled} onClick={() => toggle(s)} />
</div>
{(s.url_param || s.env_params.length > 0) && (
<div className="mt-2 flex flex-col gap-1.5">
{s.url_param && (
<input
defaultValue={s.params.command ?? ""}
onBlur={(e) => setParam(s, "command", e.target.value)}
placeholder="commande stdio (ex. npx -y mon-mcp) ou URL"
className="border-2 border-line bg-card px-2 py-1 text-[12px] text-ink"
/>
)}
{s.env_params.map((k) => (
<input
key={k}
defaultValue={s.params[k] ?? ""}
onBlur={(e) => setParam(s, k, e.target.value)}
placeholder={k}
className="border-2 border-line bg-card px-2 py-1 text-[12px] text-ink"
/>
))}
</div>
)}
{s.error && (
<div className="mt-1.5 text-[12px] text-warn">{s.error}</div>
)}
{testResult[s.id] && (
<div className="mt-1.5 text-[12px] text-muted-2">
{testResult[s.id]}
</div>
)}
</div>
))}
</div>
</Card>
);
}
function HardwareCard() {
const [hw, setHw] = useState<HardwareInfo | null>(null);