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,157 @@
|
||||
package loki
|
||||
|
||||
// Catalogue MCP embarqué.
|
||||
//
|
||||
// Ajouter un serveur MCP demandait jusqu'ici d'écrire soi-même la commande et
|
||||
// ses arguments (`npx -y @scope/paquet …`) dans la modale. Le catalogue fournit
|
||||
// une liste de serveurs connus, déjà renseignés : un clic préremplit la modale,
|
||||
// l'utilisateur relit la commande et enregistre.
|
||||
//
|
||||
// Le catalogue est un JSON EMBARQUÉ dans le binaire (go:embed), pas un appel à
|
||||
// un annuaire distant : cohérent avec un Loki qui tourne hors ligne, et rien de
|
||||
// ce qui s'exécute sur la machine ne vient du réseau. Pour proposer un serveur
|
||||
// de plus : éditer mcp_catalog.json et recompiler.
|
||||
//
|
||||
// SÉCURITÉ : le catalogue ne fait qu'AFFICHER une commande. Il n'installe ni
|
||||
// n'exécute rien tout seul — c'est l'enregistrement du serveur par
|
||||
// l'utilisateur, via la modale existante, qui décide. Même niveau de confiance
|
||||
// que l'outil `bash` du mode agent (voir l'en-tête de mcp_config.go).
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed mcp_catalog.json
|
||||
var mcpCatalogRaw []byte
|
||||
|
||||
// MCPCatalogEntry décrit un serveur proposé par le catalogue. Les champs de
|
||||
// transport reprennent ceux de MCPServerConfig : l'UI peut les recopier tels
|
||||
// quels dans la modale d'ajout.
|
||||
type MCPCatalogEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
|
||||
// Runtime : binaire nécessaire pour lancer le serveur ("node" → npx,
|
||||
// "python" → uvx). Sert à signaler une entrée non installable ici.
|
||||
Runtime string `json:"runtime,omitempty"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Secrets : variables d'environnement que l'utilisateur DOIT renseigner
|
||||
// (jetons, clés d'API). L'UI les met en évidence ; elles sont volontairement
|
||||
// vides dans le catalogue.
|
||||
Secrets []string `json:"secrets,omitempty"`
|
||||
|
||||
// Renseignés à la lecture, pas dans le fichier.
|
||||
Installed bool `json:"installed"`
|
||||
RuntimeOK bool `json:"runtimeOk"`
|
||||
}
|
||||
|
||||
type mcpCatalogFile struct {
|
||||
Version int `json:"version"`
|
||||
Servers []MCPCatalogEntry `json:"servers"`
|
||||
}
|
||||
|
||||
var (
|
||||
mcpCatalogOnce sync.Once
|
||||
mcpCatalogParsed []MCPCatalogEntry
|
||||
mcpCatalogParseEr error
|
||||
)
|
||||
|
||||
// mcpCatalogEntries décode le JSON embarqué une seule fois. Le contenu est figé
|
||||
// dans le binaire : inutile de le relire à chaque appel.
|
||||
func mcpCatalogEntries() ([]MCPCatalogEntry, error) {
|
||||
mcpCatalogOnce.Do(func() {
|
||||
var f mcpCatalogFile
|
||||
if err := json.Unmarshal(mcpCatalogRaw, &f); err != nil {
|
||||
mcpCatalogParseEr = fmt.Errorf("catalogue MCP illisible : %w", err)
|
||||
return
|
||||
}
|
||||
mcpCatalogParsed = f.Servers
|
||||
})
|
||||
return mcpCatalogParsed, mcpCatalogParseEr
|
||||
}
|
||||
|
||||
// mcpRuntimeAvailable indique si le binaire qui lance ce type de serveur est
|
||||
// présent. L'image conteneur embarque Node ; uv/uvx peut manquer.
|
||||
func mcpRuntimeAvailable(runtime string) bool {
|
||||
switch runtime {
|
||||
case "node":
|
||||
return lookPathOK("npx")
|
||||
case "python":
|
||||
return lookPathOK("uvx")
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func lookPathOK(bin string) bool {
|
||||
_, err := exec.LookPath(bin)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// MCPCatalog renvoie les entrées du catalogue, chemins résolus et marquées
|
||||
// « déjà installée » (un serveur configuré porte le même nom que l'entrée).
|
||||
func MCPCatalog() ([]MCPCatalogEntry, error) {
|
||||
entries, err := mcpCatalogEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configured, err := LoadMCPConfig()
|
||||
if err != nil {
|
||||
// Une config MCP illisible ne doit pas vider le catalogue : on affiche
|
||||
// les entrées sans savoir lesquelles sont déjà là.
|
||||
configured = map[string]MCPServerConfig{}
|
||||
}
|
||||
|
||||
out := make([]MCPCatalogEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
e.Args = expandCatalogSlice(e.Args)
|
||||
e.Env = expandCatalogMap(e.Env)
|
||||
_, e.Installed = configured[e.ID]
|
||||
e.RuntimeOK = mcpRuntimeAvailable(e.Runtime)
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// expandCatalogToken remplace les jetons de chemin du catalogue. Seul
|
||||
// {workspace} existe : c'est le dossier de travail de l'agent, le seul chemin
|
||||
// qu'un serveur du catalogue a une raison légitime de recevoir par défaut.
|
||||
func expandCatalogToken(s string) string {
|
||||
if !strings.Contains(s, "{workspace}") {
|
||||
return s
|
||||
}
|
||||
return strings.ReplaceAll(s, "{workspace}", workspaceDir())
|
||||
}
|
||||
|
||||
func expandCatalogSlice(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return in
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
for i, s := range in {
|
||||
out[i] = expandCatalogToken(s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func expandCatalogMap(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return in
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for k, v := range in {
|
||||
out[k] = expandCatalogToken(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user