Files
Loki/internal/loki/facade_mcp.go
T
Claude 869bd18bff Lot 36 : appels d'outils MCP du tour de la façade par son pool
Quand la façade tient les serveurs MCP (LOKI_MCP=rust), un appel mcp__*
d'un de ses tours partait au Go (/internal/v1/tool/exec), qui le renvoyait
aussitôt à la façade (/internal/facade/v1/mcp/call) : deux allers-retours
pour revenir au même pool. Le Go n'ajoute rien au résultat d'un outil MCP.

ChatShared porte désormais le pool (le même que celui des routes, créé une
fois) ; GoTurnHost::exec_tool l'appelle directement — même résultat que la
route interne, reconnexion comprise. Une reconnexion annonce la version du
Go (/internal/v1/auth), lue une fois et retenue. Sans pool tenu, rien ne
change. Le Go garde la route interne pour les tours qu'il exécute encore.

Test : un tour de la façade contre un faux serveur MCP stdio — rien par le
Go, résultats tels que le modèle les reçoit, erreur après reconnexion,
version du Go annoncée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-16 03:47:58 +00:00

107 lines
3.2 KiB
Go

package loki
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
// facade_mcp.go — serveurs MCP tenus par la façade Rust (LOKI_MCP=rust, lot 20
// de la migration ; expérimental, le Go reste le défaut) : configuration,
// catalogue, état et pool de sessions (rust/loki-rs/src/mcp).
//
// Un seul pool : celui de la façade. Le Go lui demande les outils MCP à
// annoncer au modèle, et leurs appels pour les tours qu'il exécute encore
// (sous-agents, tâches IA, discussion rendue au Go) via
// /internal/facade/v1/mcp/* ; son propre pool reste vide. Les tours de la
// façade appellent son pool sans passer par ici (lot 36).
const facadeMCPEnv = "LOKI_MCP"
// facadeMCP : la façade tient-elle les serveurs MCP ?
func facadeMCP() bool {
return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeMCPEnv)), "rust") &&
facadeAddr() != "" && internalToken() != ""
}
// facadeMCPRoutes : les routes que la façade sert alors (mcp/mod.rs).
var facadeMCPRoutes = map[string]bool{
"/api/mcp": true,
"/api/mcp/catalog": true,
"/api/mcp/save": true,
"/api/mcp/delete": true,
"/api/mcp/toggle": true,
"/api/mcp/tool": true,
"/api/mcp/test": true,
}
// facadeMCPJSON appelle l'API interne MCP de la façade et décode sa réponse.
// Délai large : un appel d'outil MCP peut durer (mcpCallTimeout), une
// première connexion npx/uvx aussi (mcpConnectTimeoutCold).
func facadeMCPJSON(path string, body any, out any) error {
b, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, "http://"+facadeAddr()+path, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set(internalTokenHeader, internalToken())
resp, err := (&http.Client{Timeout: mcpConnectTimeoutCold + mcpCallTimeout}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("façade : %s", resp.Status)
}
return json.NewDecoder(resp.Body).Decode(out)
}
// facadeMCPTools : les outils MCP à annoncer, tenus par la façade.
func facadeMCPTools() []Tool {
var out struct {
Tools []Tool `json:"tools"`
}
if err := facadeMCPJSON("/internal/facade/v1/mcp/tools", map[string]any{}, &out); err != nil {
fmt.Fprintf(os.Stderr, "[mcp] outils demandés à la façade : %v\n", err)
return nil
}
return out.Tools
}
// facadeMCPCall : un outil MCP exécuté par la façade.
func facadeMCPCall(name string, args map[string]any) string {
if args == nil {
args = map[string]any{}
}
var out struct {
Result string `json:"result"`
}
if err := facadeMCPJSON("/internal/facade/v1/mcp/call", map[string]any{"name": name, "args": args}, &out); err != nil {
return "[erreur MCP] façade injoignable : " + err.Error()
}
return out.Result
}
// facadeMCPPrewarm : la façade ouvre ses sessions en tâche de fond.
func facadeMCPPrewarm() {
go func() {
var out map[string]any
// La façade démarre le Go puis attend qu'il réponde : elle peut ne pas
// écouter encore au tout premier instant.
for i := 0; i < 10; i++ {
if facadeMCPJSON("/internal/facade/v1/mcp/prewarm", map[string]any{}, &out) == nil {
return
}
time.Sleep(time.Second)
}
}()
}