mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
MCP : catalogue embarqué de serveurs connus
Ajouter un serveur MCP demandait d'écrire soi-même `npx -y @scope/paquet …` dans la modale, en devinant le nom du paquet. Le catalogue liste une vingtaine de serveurs connus, commande déjà renseignée, classés par catégorie. - internal/loki/mcp_catalog.json est EMBARQUÉ dans le binaire (go:embed), pas interrogé sur le réseau : Loki tourne hors ligne, et rien de ce qui s'exécute sur la machine ne vient d'un annuaire distant. Pour proposer un serveur de plus : éditer le fichier et recompiler. - Choisir une entrée n'installe rien. Ça préremplit la modale d'ajout existante et l'utilisateur relit la commande avant d'enregistrer — un serveur stdio exécute un process arbitraire, au même titre que l'outil bash du mode agent. Aucune ligne de mcp_client.go n'est touchée : le catalogue s'arrête à l'affichage, tout le reste passe par l'API MCP déjà en place. - Une entrée dont le runtime manque (npx ou uvx absent) le signale dans la liste, plutôt que de laisser l'utilisateur découvrir l'échec au démarrage du serveur. - Les entrées qui réclament une clé d'API la rappellent avant l'enregistrement, au lieu d'enregistrer un serveur qui ne peut pas se connecter. - Les entrées Python publiées avant le SDK MCP 2.0 sont épinglées avec `uvx --with "mcp<2"` : sans cela elles plantent sur ImportError: McpError. - Le Dockerfile gagne uv/uvx (~35 Mo). Sans lui, 9 des 21 entrées s'affichent sans pouvoir démarrer dans le conteneur. Intensité du raisonnement (REASONING_EFFORT) L'interrupteur Raisonnement était binaire. llama-server accepte `reasoning_effort` dans /v1/chat/completions : `none` coupe le raisonnement, toute autre valeur est passée au gabarit jinja du modèle. - Nouvelle clé de preset REASONING_EFFORT — donc réglée par modèle, comme le reste du preset. Vide = on n'envoie rien et le gabarit garde son comportement. - Pas de repli à prévoir : un gabarit qui ne lit pas la valeur l'ignore sans erreur. En pratique seuls gpt-oss et apparentés changent de comportement, et le sous-titre du réglage le dit — promettre un effet universel serait faux. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e602adbbee
commit
847aad217b
15 files changed
+914
-4
No files matched your search
@@ -0,0 +1,81 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Le catalogue est embarqué : s'il ne décode pas, le binaire livré est cassé et
|
||||
// le panneau MCP part vide. Ce test attrape une virgule en trop au build.
|
||||
func TestMCPCatalogDecodes(t *testing.T) {
|
||||
entries, err := mcpCatalogEntries()
|
||||
if err != nil {
|
||||
t.Fatalf("catalogue illisible : %v", err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
t.Fatal("catalogue vide")
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for _, e := range entries {
|
||||
if e.ID == "" || e.Name == "" || e.Command == "" {
|
||||
t.Errorf("entrée incomplète : %+v", e)
|
||||
}
|
||||
if seen[e.ID] {
|
||||
t.Errorf("id dupliqué : %s", e.ID)
|
||||
}
|
||||
seen[e.ID] = true
|
||||
if e.Description == "" {
|
||||
t.Errorf("%s : description manquante", e.ID)
|
||||
}
|
||||
// Un secret annoncé doit exister comme variable d'environnement, sinon
|
||||
// l'UI met en avant un champ que l'utilisateur ne peut pas remplir.
|
||||
for _, s := range e.Secrets {
|
||||
if _, ok := e.Env[s]; !ok {
|
||||
t.Errorf("%s : secret %q absent de env", e.ID, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Les jetons {workspace} doivent disparaître : un serveur lancé avec un chemin
|
||||
// littéral « {workspace} » démarre puis échoue à la première lecture.
|
||||
func TestMCPCatalogExpandsWorkspaceToken(t *testing.T) {
|
||||
entries, err := MCPCatalog()
|
||||
if err != nil {
|
||||
t.Fatalf("MCPCatalog : %v", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
for _, a := range e.Args {
|
||||
if strings.Contains(a, "{workspace}") {
|
||||
t.Errorf("%s : jeton non résolu dans args : %s", e.ID, a)
|
||||
}
|
||||
}
|
||||
for k, v := range e.Env {
|
||||
if strings.Contains(v, "{workspace}") {
|
||||
t.Errorf("%s : jeton non résolu dans env[%s] : %s", e.ID, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReasoningEffortValue(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "",
|
||||
" ": "",
|
||||
"auto": "",
|
||||
"default": "",
|
||||
"off": "none",
|
||||
"none": "none",
|
||||
"false": "none",
|
||||
"0": "none",
|
||||
"low": "low",
|
||||
"MEDIUM": "medium",
|
||||
" High ": "high",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := reasoningEffortValue(in); got != want {
|
||||
t.Errorf("reasoningEffortValue(%q) = %q, attendu %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user