v0.5.0 : intégration MCP (Model Context Protocol)

Jean peut désormais se connecter à des serveurs MCP tiers pour enrichir la
palette d'outils de l'IA (accès fichiers, bases de données, APIs métier…).

- Transports stdio (process local npx/uvx/binaire) et http (Streamable HTTP
  distant, en-têtes d'auth possibles), via le SDK officiel go-sdk.
- Outils namespacés mcp__<serveur>__<outil>, exposés seulement en mode agent
  (même niveau de confiance que le shell : exécution de code sur l'hôte).
- Sessions persistantes (connexion paresseuse + cachée, reconnexion auto).
- Gestion complète dans l'UI web : ajout/édition de serveurs, activation par
  serveur ET par outil (pour maîtriser le contexte), état de connexion en direct.
- Config dans mcp.json (format Claude Desktop mcpServers, copiable).
This commit is contained in:
nathaninline committed 2026-07-24 12:49:56 +02:00
1 parent e8f566ebc0
commit 8e57685b19
19 files changed
+1560 -19

No files matched your search

+129
View File
@@ -483,6 +483,135 @@ func handleInternet(w http.ResponseWriter, r *http.Request) {
})
}
// ─── Serveurs MCP ────────────────────────────────────────────────────────────
//
// Gestion des serveurs MCP depuis l'UI. Même modèle de protection que le reste
// de /api/* : la clé web est le seul garde. Un serveur MCP stdio exécute du code
// arbitraire sur l'hôte, au même titre que l'outil `bash` du mode agent — donc
// réservé de fait au propriétaire de la machine qui détient la clé.
// mcpSaveReq est le payload d'ajout/édition d'un serveur MCP.
type mcpSaveReq struct {
Name string `json:"name"`
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Enabled *bool `json:"enabled"`
}
// handleMCP renvoie l'état de tous les serveurs MCP configurés (connexion tentée
// pour ceux activés → l'UI voit l'état réel + les outils découverts).
func handleMCP(w http.ResponseWriter, r *http.Request) {
list, err := MCPStatus()
if err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
}
// handleMCPSave ajoute ou remplace un serveur MCP.
func handleMCPSave(w http.ResponseWriter, r *http.Request) {
var req mcpSaveReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
cfg := MCPServerConfig{
Command: strings.TrimSpace(req.Command),
Args: req.Args,
Env: req.Env,
URL: strings.TrimSpace(req.URL),
Headers: req.Headers,
Enabled: enabled,
}
if err := SetMCPServer(req.Name, cfg); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
// Renvoie l'état à jour (avec tentative de connexion) pour un retour immédiat.
list, _ := MCPStatus()
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
}
// handleMCPDelete retire un serveur MCP.
func handleMCPDelete(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if err := DeleteMCPServer(req.Name); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true})
}
// handleMCPToggle active/désactive un serveur MCP.
func handleMCPToggle(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
On bool `json:"on"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if err := SetMCPServerEnabled(req.Name, req.On); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
list, _ := MCPStatus()
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
}
// handleMCPTool masque/démasque un outil précis d'un serveur.
func handleMCPTool(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Tool string `json:"tool"`
On bool `json:"on"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if err := SetMCPToolEnabled(req.Name, req.Tool, req.On); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
list, _ := MCPStatus()
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
}
// handleMCPTest force une reconnexion d'un serveur et renvoie son état (bouton
// « tester/rafraîchir » de l'UI).
func handleMCPTest(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
mcpInvalidate(req.Name)
list, err := MCPStatus()
if err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
}
// handleMem / handleMemSave / handleMemDelete : éditeur web des pages mémoire
// (MEMORY/<nom>.md). Payload partagé saveReq (name/old/content) ; "name" = nom
// de fichier de la page.