mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 15:38:12 +02:00
Lot 20 : serveurs MCP tenus par la façade (LOKI_MCP=rust)
La façade tient les serveurs MCP quand LOKI_MCP=rust (expérimental ; le Go reste le défaut) : configuration (state/mcp), catalogue embarqué (le même fichier que le Go, copié dans l'étape Rust de l'image), état et routes /api/mcp*. Un client MCP en Rust (mcp/client.rs) parle JSON-RPC sur stdio et en Streamable HTTP (réponses JSON ou SSE, session suivie, en-têtes configurés, ping du serveur servi). Le pool suit mcp_client.go : session gardée entre les tours, connexions parallèles et partagées, session invalidée pendant sa connexion jamais republiée, délais npx/uvx, appel borné avec une reconnexion, sortie plafonnée à la fenêtre. Un seul pool, celui de la façade : le Go, qui exécute encore les outils de l'agent, lui demande les outils MCP, leurs appels et le pré-chauffage (/internal/facade/v1/mcp/*, facade_mcp.go). Parité : parity_mcp_test.go tire du Go les fonctions pures, seize mutations de configuration, et un vrai serveur du SDK MCP Go (onze outils) : ce qu'il met sur le fil et ce que le client Go en tire (état, outils annoncés, appels). tests/mcp.rs le rejoue contre un faux serveur qui resert ces réponses, et vérifie stdio, reconnexion, routes, catalogue et session invalidée de bout en bout ; TestFacadeMCP côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
20 files changed
+3670
-2
No files matched your search
@@ -0,0 +1,105 @@
|
||||
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, qui exécute encore les outils de
|
||||
// l'agent, lui demande les outils MCP à annoncer au modèle et leurs appels
|
||||
// (/internal/facade/v1/mcp/*), quelle que soit la conversation qui tourne ;
|
||||
// son propre pool reste vide.
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in new issue
Block a user