mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
feat(mcp): onglet Configuration — catalogue, toggles, test de connexion
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
91e0a9f0b2
commit
afe0a837d4
2 files changed
+161
-1
No files matched your search
@@ -428,6 +428,45 @@ export function downloadFile(path: string): void {
|
|||||||
link.remove();
|
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[]> {
|
export async function listSessions(): Promise<Session[]> {
|
||||||
const res = await fetch("/api/sessions");
|
const res = await fetch("/api/sessions");
|
||||||
return (await res.json()).sessions;
|
return (await res.json()).sessions;
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ import {
|
|||||||
deleteModel,
|
deleteModel,
|
||||||
getBenchScores,
|
getBenchScores,
|
||||||
getHardware,
|
getHardware,
|
||||||
|
listMcp,
|
||||||
pullModel,
|
pullModel,
|
||||||
runBench,
|
runBench,
|
||||||
|
testMcp,
|
||||||
|
updateMcp,
|
||||||
|
} from "../api/client";
|
||||||
|
import type {
|
||||||
|
AgentConfig,
|
||||||
|
BenchResult,
|
||||||
|
HardwareInfo,
|
||||||
|
McpServer,
|
||||||
} from "../api/client";
|
} from "../api/client";
|
||||||
import type { AgentConfig, BenchResult, HardwareInfo } from "../api/client";
|
|
||||||
import { DownloadIcon, RefreshIcon } from "../components/Icon";
|
import { DownloadIcon, RefreshIcon } from "../components/Icon";
|
||||||
|
|
||||||
const TOOL_DESC: Record<string, string> = {
|
const TOOL_DESC: Record<string, string> = {
|
||||||
@@ -466,6 +474,8 @@ export function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<McpCard />
|
||||||
|
|
||||||
<HardwareCard />
|
<HardwareCard />
|
||||||
|
|
||||||
<BenchCard />
|
<BenchCard />
|
||||||
@@ -498,6 +508,117 @@ export function SettingsView() {
|
|||||||
|
|
||||||
/** Benchmark : évalue le modèle sélectionné sur 5 mini-épreuves. */
|
/** 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. */
|
/** 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() {
|
function HardwareCard() {
|
||||||
const [hw, setHw] = useState<HardwareInfo | null>(null);
|
const [hw, setHw] = useState<HardwareInfo | null>(null);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user