mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 13:08:15 +02:00
- LSP (lsp.go) : un signal resté dans le tampon par une republication tardive faisait rendre « aucun diagnostic » à l'édition suivante, avant même que le serveur ait vu la nouvelle version. Le canal est vidé, une publication d'une version antérieure est ignorée, et check attend 350 ms de calme après la dernière publication (liste vide puis analyse : c'est l'analyse qui compte), toujours borné par lspDiagWait. Au passage : interblocage latent levé (writeMsg → markDead reprenait mu, déjà tenu par check) — dead devient atomique. - run_shell : ErrWaitDelay (commande finie, process laissé en arrière-plan sur les tubes) rend la sortie au lieu de « WaitDelay expired » qui l'effaçait (AJEAN 7403a8a0). - Clé de pilotage web : lue depuis un cache à chaque requête au lieu d'ouvrir la base sous dbMu (AJEAN #120). Lectures réussies seulement, invalidé par toute écriture du bucket d'état, par la date/taille du fichier (autre process) et au bout d'une seconde. - MCP : l'appel suit le contexte du tour (le bouton stop l'annule), délai par serveur (callTimeoutSec) et allongé par l'argument timeout de l'outil, plafonné à 30 min ; un délai dépassé n'est plus relancé (OpenFox 2ef584b8). - Recall : les blocs d'une compaction s'archivent en une seule transaction au lieu d'une par bloc (AJEAN #130). - Compactage en cours de tour : après un essai qui n'a rien réduit, pas de nouvel essai avant +5 % de la fenêtre (sauf à 90 %), pour les tâches et sous-agents aussi ; la cause d'un résumé raté est journalisée (AJEAN 68659e73). - Motifs de relance : un motif vide (qui correspondait à tout) est écarté, une surcharge sans motif utilisable retombe sur l'embarqué, et un changement de retry_patterns agit sans redémarrer (OpenFox 8e9b4869, c52c5bf7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
7.8 KiB
Go
264 lines
7.8 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Configuration des serveurs MCP (Model Context Protocol).
|
|
//
|
|
// loki peut se connecter à des serveurs MCP tiers pour enrichir la palette
|
|
// d'outils de l'IA (accès fichiers rapide type desktop-commander, bases de
|
|
// données, APIs métier…). Deux transports :
|
|
//
|
|
// - stdio : loki lance un process local (command + args) et parle en
|
|
// JSON-RPC sur stdin/stdout. C'est le cas de desktop-commander, du serveur
|
|
// filesystem officiel, etc. — lancés via npx/uvx/un binaire.
|
|
// - http : loki parle à un serveur MCP distant en Streamable HTTP (url +
|
|
// éventuels en-têtes d'auth).
|
|
//
|
|
// Le format du fichier mcp.json reprend celui de Claude Desktop (clé
|
|
// "mcpServers" indexée par nom) pour que les utilisateurs puissent copier-coller
|
|
// leurs configs existantes. On ajoute un champ "enabled" par serveur.
|
|
//
|
|
// IMPORTANT (sécurité) : un serveur MCP stdio exécute un process arbitraire sur
|
|
// la machine où tourne loki — même niveau de confiance que l'outil `bash` du
|
|
// mode agent. La configuration MCP est donc réservée au propriétaire local de la
|
|
// machine et ne doit JAMAIS être pilotable depuis le relais/accès distant.
|
|
|
|
// MCPServerConfig décrit un serveur MCP configuré. Un seul des deux transports
|
|
// est renseigné : Command => stdio, URL => http.
|
|
type MCPServerConfig struct {
|
|
// Transport stdio.
|
|
Command string `json:"command,omitempty"`
|
|
Args []string `json:"args,omitempty"`
|
|
Env map[string]string `json:"env,omitempty"`
|
|
|
|
// Transport http (Streamable HTTP).
|
|
URL string `json:"url,omitempty"`
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
|
|
// Enabled : le serveur n'est connecté et ses outils exposés que s'il est
|
|
// activé. Un serveur nouvellement ajouté est actif par défaut (voir
|
|
// UnmarshalJSON) pour coller à l'intuition « je l'ajoute, il marche ».
|
|
Enabled bool `json:"enabled"`
|
|
|
|
// DisabledTools : outils du serveur à NE PAS exposer à l'IA (par nom réel,
|
|
// non namespacé). Permet de garder un serveur connecté tout en masquant
|
|
// certains de ses outils. Vide = tous les outils exposés.
|
|
DisabledTools []string `json:"disabledTools,omitempty"`
|
|
|
|
// CallTimeoutSec : délai d'un appel d'outil de ce serveur, en secondes.
|
|
// 0 = défaut (mcpCallTimeout). Pour un serveur dont les outils sont longs
|
|
// par nature (build, scraping, requête lourde).
|
|
CallTimeoutSec int `json:"callTimeoutSec,omitempty"`
|
|
}
|
|
|
|
// ToolDisabled indique si un outil (nom réel) est masqué pour ce serveur.
|
|
func (c MCPServerConfig) ToolDisabled(tool string) bool {
|
|
for _, t := range c.DisabledTools {
|
|
if t == tool {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// enabledDefaultTrue est un alias utilisé pour appliquer enabled=true par défaut
|
|
// quand le champ est absent du JSON (compat configs Claude Desktop sans
|
|
// "enabled").
|
|
type mcpServerConfigAlias MCPServerConfig
|
|
|
|
// UnmarshalJSON applique enabled=true par défaut lorsque la clé est absente,
|
|
// pour rester compatible avec les fichiers mcp.json qui ne connaissent pas ce
|
|
// champ (Claude Desktop, etc.).
|
|
func (c *MCPServerConfig) UnmarshalJSON(b []byte) error {
|
|
// Sonde la présence de la clé "enabled".
|
|
var probe map[string]json.RawMessage
|
|
if err := json.Unmarshal(b, &probe); err != nil {
|
|
return err
|
|
}
|
|
alias := mcpServerConfigAlias{}
|
|
if err := json.Unmarshal(b, &alias); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := probe["enabled"]; !ok {
|
|
alias.Enabled = true
|
|
}
|
|
*c = MCPServerConfig(alias)
|
|
return nil
|
|
}
|
|
|
|
// Transport renvoie "stdio", "http" ou "" (mal configuré : ni command ni url).
|
|
func (c MCPServerConfig) Transport() string {
|
|
switch {
|
|
case strings.TrimSpace(c.Command) != "":
|
|
return "stdio"
|
|
case strings.TrimSpace(c.URL) != "":
|
|
return "http"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Validate vérifie qu'exactement un transport est renseigné.
|
|
func (c MCPServerConfig) Validate() error {
|
|
hasCmd := strings.TrimSpace(c.Command) != ""
|
|
hasURL := strings.TrimSpace(c.URL) != ""
|
|
switch {
|
|
case hasCmd && hasURL:
|
|
return fmt.Errorf("un serveur MCP ne peut avoir à la fois 'command' (stdio) et 'url' (http)")
|
|
case !hasCmd && !hasURL:
|
|
return fmt.Errorf("un serveur MCP doit avoir soit 'command' (stdio) soit 'url' (http)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mcpConfigMu sérialise les accès concurrents à la déclaration des serveurs
|
|
// MCP (l'UI web et les tours de chat peuvent lire/écrire en parallèle).
|
|
var mcpConfigMu sync.Mutex
|
|
|
|
// LoadMCPConfig lit les serveurs MCP déclarés. Aucun => map vide, pas d'erreur.
|
|
func LoadMCPConfig() (map[string]MCPServerConfig, error) {
|
|
mcpConfigMu.Lock()
|
|
defer mcpConfigMu.Unlock()
|
|
return loadMCPConfigLocked()
|
|
}
|
|
|
|
func loadMCPConfigLocked() (map[string]MCPServerConfig, error) {
|
|
servers := map[string]MCPServerConfig{}
|
|
getJSON(bkState, "mcp", &servers)
|
|
if servers == nil {
|
|
servers = map[string]MCPServerConfig{}
|
|
}
|
|
return servers, nil
|
|
}
|
|
|
|
func saveMCPConfigLocked(servers map[string]MCPServerConfig) error {
|
|
return putJSON(bkState, "mcp", servers)
|
|
}
|
|
|
|
// SetMCPServer ajoute ou remplace un serveur nommé, puis invalide le pool de
|
|
// sessions pour que le changement prenne effet au prochain tour.
|
|
func SetMCPServer(name string, cfg MCPServerConfig) error {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return fmt.Errorf("nom de serveur vide")
|
|
}
|
|
if strings.Contains(name, "__") {
|
|
return fmt.Errorf("le nom ne peut pas contenir '__' (réservé au namespacing des outils)")
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return err
|
|
}
|
|
mcpConfigMu.Lock()
|
|
servers, err := loadMCPConfigLocked()
|
|
if err != nil {
|
|
mcpConfigMu.Unlock()
|
|
return err
|
|
}
|
|
servers[name] = cfg
|
|
err = saveMCPConfigLocked(servers)
|
|
mcpConfigMu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpInvalidate(name)
|
|
return nil
|
|
}
|
|
|
|
// DeleteMCPServer retire un serveur et ferme sa session si ouverte.
|
|
func DeleteMCPServer(name string) error {
|
|
mcpConfigMu.Lock()
|
|
servers, err := loadMCPConfigLocked()
|
|
if err != nil {
|
|
mcpConfigMu.Unlock()
|
|
return err
|
|
}
|
|
if _, ok := servers[name]; !ok {
|
|
mcpConfigMu.Unlock()
|
|
return fmt.Errorf("serveur MCP inconnu: %s", name)
|
|
}
|
|
delete(servers, name)
|
|
err = saveMCPConfigLocked(servers)
|
|
mcpConfigMu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpInvalidate(name)
|
|
return nil
|
|
}
|
|
|
|
// SetMCPServerEnabled active/désactive un serveur existant.
|
|
func SetMCPServerEnabled(name string, on bool) error {
|
|
mcpConfigMu.Lock()
|
|
servers, err := loadMCPConfigLocked()
|
|
if err != nil {
|
|
mcpConfigMu.Unlock()
|
|
return err
|
|
}
|
|
cfg, ok := servers[name]
|
|
if !ok {
|
|
mcpConfigMu.Unlock()
|
|
return fmt.Errorf("serveur MCP inconnu: %s", name)
|
|
}
|
|
cfg.Enabled = on
|
|
servers[name] = cfg
|
|
err = saveMCPConfigLocked(servers)
|
|
mcpConfigMu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpInvalidate(name)
|
|
return nil
|
|
}
|
|
|
|
// SetMCPToolEnabled masque/démasque un outil précis d'un serveur (via sa liste
|
|
// DisabledTools), puis invalide la session pour recalculer les outils exposés.
|
|
func SetMCPToolEnabled(server, tool string, on bool) error {
|
|
mcpConfigMu.Lock()
|
|
servers, err := loadMCPConfigLocked()
|
|
if err != nil {
|
|
mcpConfigMu.Unlock()
|
|
return err
|
|
}
|
|
cfg, ok := servers[server]
|
|
if !ok {
|
|
mcpConfigMu.Unlock()
|
|
return fmt.Errorf("serveur MCP inconnu: %s", server)
|
|
}
|
|
// Reconstruit la liste sans l'outil concerné, puis l'ajoute si on désactive.
|
|
next := cfg.DisabledTools[:0:0]
|
|
for _, t := range cfg.DisabledTools {
|
|
if t != tool {
|
|
next = append(next, t)
|
|
}
|
|
}
|
|
if !on {
|
|
next = append(next, tool)
|
|
}
|
|
cfg.DisabledTools = next
|
|
servers[server] = cfg
|
|
err = saveMCPConfigLocked(servers)
|
|
mcpConfigMu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpInvalidate(server)
|
|
return nil
|
|
}
|
|
|
|
// sortedServerNames renvoie les noms triés, pour un ordre d'affichage/itération
|
|
// stable.
|
|
func sortedServerNames(servers map[string]MCPServerConfig) []string {
|
|
names := make([]string, 0, len(servers))
|
|
for n := range servers {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|