mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
MCP : délais de connexion et d'appel réglables dans l'interface
Repris d'AJEAN c05a0a9f et e44b6139. Les deux délais MCP, jusqu'ici codés en dur (20 s pour se connecter, 120 s pour un appel d'outil), deviennent des réglages globaux : clé bkState mcp_timeouts, bornes 1 à 3600 s, routes GET /api/mcp/timeouts et POST /api/mcp/timeouts/save. L'enregistrement ferme les sessions ouvertes pour que le nouveau délai de connexion s'applique dès la suivante. Un lancement npx/uvx garde au moins mcpConnectTimeoutCold. Un appel prend le réglage global, toujours remplacé par le callTimeoutSec du serveur, puis allongé par l'argument timeout de l'outil. Le plafond d'un appel suit la borne des réglages (1 h au lieu de 30 min) : un délai réglé à 3600 s n'est plus rogné en silence. Une valeur rangée hors bornes ou illisible retombe sur la borne ou le défaut. Le délai par serveur passe par /api/mcp/save (callTimeoutSec, 0 à 3600, vide = réglage global), revient dans /api/mcp et n'est plus perdu à l'enregistrement. L'interface gagne deux lignes dans la carte « Serveurs MCP », au style des réglages (sans bordure, un seul enregistrement par modification), et un champ « délai d'un appel d'outil » dans la fenêtre d'édition d'un serveur. index.html régénéré par tools/assemble-ui. Quand la façade tient les serveurs MCP (LOKI_MCP=rust), elle sert aussi les nouvelles routes : c'est son pool qu'il faut fermer quand les délais changent. Le journal de parité MCP couvre la lecture des délais, leur effet sur la connexion et l'appel, et les deux routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a45f19831
commit
2e28ca955e
13 files changed
+1007
-60
No files matched your search
@@ -37,11 +37,15 @@ var facadeMCPRoutes = map[string]bool{
|
||||
"/api/mcp/toggle": true,
|
||||
"/api/mcp/tool": true,
|
||||
"/api/mcp/test": true,
|
||||
// Les délais globaux : la façade tient le pool, c'est elle qui doit
|
||||
// fermer ses sessions quand ils changent.
|
||||
"/api/mcp/timeouts": true,
|
||||
"/api/mcp/timeouts/save": 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).
|
||||
// Délai large, simple garde-fou : la façade borne elle-même la connexion
|
||||
// (1 h au plus) et l'appel, nouvel essai compris (mcpCallTimeoutMax chacun).
|
||||
func facadeMCPJSON(path string, body any, out any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
@@ -53,7 +57,7 @@ func facadeMCPJSON(path string, body any, out any) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(internalTokenHeader, internalToken())
|
||||
resp, err := (&http.Client{Timeout: mcpConnectTimeoutCold + mcpCallTimeout}).Do(req)
|
||||
resp, err := (&http.Client{Timeout: mcpTimeoutMaxSec*time.Second + 2*mcpCallTimeoutMax}).Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,22 +74,27 @@ func TestMCPCallHonorsTurnCancel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMCPCallTimeoutFor(t *testing.T) {
|
||||
def := MCPTimeouts{}.normalized()
|
||||
cases := []struct {
|
||||
cfg MCPServerConfig
|
||||
t MCPTimeouts
|
||||
args map[string]any
|
||||
want time.Duration
|
||||
}{
|
||||
{MCPServerConfig{}, nil, mcpCallTimeout},
|
||||
{MCPServerConfig{CallTimeoutSec: 600}, nil, 10 * time.Minute},
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 300.0}, 330 * time.Second}, // secondes
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 200000.0}, 230 * time.Second}, // millisecondes
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 5000.0}, mcpCallTimeout}, // 5 s : le défaut reste
|
||||
{MCPServerConfig{}, map[string]any{"timeout": "400"}, 430 * time.Second}, // chaîne
|
||||
{MCPServerConfig{}, map[string]any{"timeout": 86400.0 * 1000}, mcpCallTimeoutMax}, // plafond
|
||||
{MCPServerConfig{CallTimeoutSec: 30}, nil, 30 * time.Second}, // plus court que le défaut
|
||||
{MCPServerConfig{}, def, nil, 120 * time.Second},
|
||||
{MCPServerConfig{CallTimeoutSec: 600}, def, nil, 10 * time.Minute},
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 300.0}, 330 * time.Second}, // secondes
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 200000.0}, 230 * time.Second}, // millisecondes
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 5000.0}, 120 * time.Second}, // 5 s : le défaut reste
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": "400"}, 430 * time.Second}, // chaîne
|
||||
{MCPServerConfig{}, def, map[string]any{"timeout": 86400.0 * 1000}, mcpCallTimeoutMax}, // plafond
|
||||
{MCPServerConfig{CallTimeoutSec: 30}, def, nil, 30 * time.Second}, // plus court que le défaut
|
||||
{MCPServerConfig{}, MCPTimeouts{Connect: 20, Call: 45}, nil, 45 * time.Second}, // réglage global
|
||||
{MCPServerConfig{CallTimeoutSec: 90}, MCPTimeouts{Connect: 20, Call: 45}, nil, 90 * time.Second},
|
||||
{MCPServerConfig{}, MCPTimeouts{Connect: 20, Call: 3600}, nil, time.Hour}, // pas rogné par le plafond
|
||||
}
|
||||
for i, c := range cases {
|
||||
if got := mcpCallTimeoutFor(c.cfg, c.args); got != c.want {
|
||||
if got := mcpCallTimeoutFor(c.cfg, c.t, c.args); got != c.want {
|
||||
t.Errorf("cas %d : %s, attendu %s", i, got, c.want)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-26
@@ -36,19 +36,17 @@ import (
|
||||
// pour router les appels sans avoir à re-parser des noms potentiellement
|
||||
// ambigus.
|
||||
|
||||
// Les délais de connexion et d'appel sont réglables (mcp_timeouts.go) :
|
||||
// 20 s et 120 s par défaut.
|
||||
const (
|
||||
// mcpConnectTimeout borne l'établissement d'une session (handshake initialize
|
||||
// + tools/list). Un serveur qui rame ne doit pas figer le tour de chat.
|
||||
mcpConnectTimeout = 20 * time.Second
|
||||
// mcpConnectTimeoutCold : npx et uvx TÉLÉCHARGENT le paquet du serveur au
|
||||
// premier lancement (le catalogue les utilise tous les deux). Sur un réseau
|
||||
// ou un disque lents, ça dépasse largement 20 s, et l'utilisateur voyait
|
||||
// « enregistré mais connexion échouée : context deadline exceeded » pour un
|
||||
// serveur parfaitement sain. Les lancements suivants profitent du cache et
|
||||
// retombent dans le régime normal.
|
||||
// C'est un minimum : un délai de connexion réglé plus long l'emporte.
|
||||
mcpConnectTimeoutCold = 3 * time.Minute
|
||||
// mcpCallTimeout borne un appel d'outil MCP.
|
||||
mcpCallTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
// mcpOutputCap borne la sortie d'un outil MCP renvoyée au modèle. L'amont
|
||||
@@ -194,11 +192,12 @@ func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
|
||||
m.mu.Unlock()
|
||||
|
||||
// Connexion hors-lock (peut être lente).
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg))
|
||||
timeouts := LoadMCPTimeouts()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg, timeouts))
|
||||
defer cancel()
|
||||
sess, err := mcpConnect(ctx, name, cfg)
|
||||
if err != nil {
|
||||
s.err = mcpFriendlyErr(err, cfg)
|
||||
s.err = mcpFriendlyErr(err, cfg, timeouts)
|
||||
} else {
|
||||
s.sess = sess
|
||||
lt, lerr := sess.ListTools(ctx, nil)
|
||||
@@ -391,7 +390,7 @@ func mcpCall(parent context.Context, name string, args map[string]any) string {
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
cfg, exists := servers[ref.server]
|
||||
timeout := mcpCallTimeoutFor(cfg, args)
|
||||
timeout := mcpCallTimeoutFor(cfg, LoadMCPTimeouts(), args)
|
||||
|
||||
result, err := mcpCallOnce(parent, ref, args, timeout)
|
||||
if err != nil {
|
||||
@@ -424,14 +423,16 @@ func mcpCall(parent context.Context, name string, args map[string]any) string {
|
||||
|
||||
// mcpCallTimeoutMax plafonne le délai d'un appel, quoi que demandent l'outil
|
||||
// ou la config : au-delà, c'est un job d'arrière-plan, pas un appel d'outil.
|
||||
const mcpCallTimeoutMax = 30 * time.Minute
|
||||
// Il suit la borne des réglages (1 h) : un délai réglé à 3600 s n'est pas
|
||||
// rogné en silence.
|
||||
const mcpCallTimeoutMax = mcpTimeoutMaxSec * time.Second
|
||||
|
||||
// mcpCallTimeoutFor : délai d'un appel. Défaut mcpCallTimeout, remplacé par
|
||||
// CallTimeoutSec du serveur ; un argument « timeout » de l'outil lui-même
|
||||
// mcpCallTimeoutFor : délai d'un appel. Le réglage global (t.Call), remplacé
|
||||
// par CallTimeoutSec du serveur ; un argument « timeout » de l'outil lui-même
|
||||
// (beaucoup en ont un : navigateur, build, requête) l'allonge de 30 s pour que
|
||||
// l'outil ait le temps de rendre SA propre erreur de délai.
|
||||
func mcpCallTimeoutFor(cfg MCPServerConfig, args map[string]any) time.Duration {
|
||||
d := mcpCallTimeout
|
||||
func mcpCallTimeoutFor(cfg MCPServerConfig, t MCPTimeouts, args map[string]any) time.Duration {
|
||||
d := t.call()
|
||||
if cfg.CallTimeoutSec > 0 {
|
||||
d = time.Duration(cfg.CallTimeoutSec) * time.Second
|
||||
}
|
||||
@@ -598,6 +599,8 @@ type MCPServerStatus struct {
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
// CallTimeoutSec : délai d'un appel propre au serveur (0 = réglage global).
|
||||
CallTimeoutSec int `json:"callTimeoutSec,omitempty"`
|
||||
}
|
||||
|
||||
// MCPStatus renvoie l'état de tous les serveurs configurés, en tentant de
|
||||
@@ -621,7 +624,9 @@ func MCPStatus() ([]MCPServerStatus, error) {
|
||||
URL: cfg.URL,
|
||||
Headers: cfg.Headers,
|
||||
Tools: []string{},
|
||||
Disabled: cfg.DisabledTools,
|
||||
|
||||
CallTimeoutSec: cfg.CallTimeoutSec,
|
||||
Disabled: cfg.DisabledTools,
|
||||
}
|
||||
if st.Disabled == nil {
|
||||
st.Disabled = []string{}
|
||||
@@ -643,29 +648,36 @@ func MCPStatus() ([]MCPServerStatus, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// mcpTimeoutFor choisit le délai de connexion d'un serveur : long pour un
|
||||
// lanceur qui télécharge son paquet à la volée (npx, uvx), court sinon.
|
||||
func mcpTimeoutFor(cfg MCPServerConfig) time.Duration {
|
||||
// mcpTimeoutFor choisit le délai de connexion d'un serveur : le réglage
|
||||
// global, mais au moins mcpConnectTimeoutCold pour un lanceur qui télécharge
|
||||
// son paquet à la volée (npx, uvx).
|
||||
func mcpTimeoutFor(cfg MCPServerConfig, t MCPTimeouts) time.Duration {
|
||||
if mcpColdLauncher(cfg) {
|
||||
return max(t.connect(), mcpConnectTimeoutCold)
|
||||
}
|
||||
return t.connect()
|
||||
}
|
||||
|
||||
// mcpColdLauncher : npx ou uvx, qui téléchargent le paquet au premier
|
||||
// lancement.
|
||||
func mcpColdLauncher(cfg MCPServerConfig) bool {
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
if base == "npx" || base == "uvx" {
|
||||
return mcpConnectTimeoutCold
|
||||
}
|
||||
return mcpConnectTimeout
|
||||
return base == "npx" || base == "uvx"
|
||||
}
|
||||
|
||||
// mcpFriendlyErr habille l'erreur de connexion la plus vécue : le délai
|
||||
// dépassé d'un npx/uvx en plein téléchargement. L'erreur brute (« context
|
||||
// deadline exceeded ») envoyait l'utilisateur chercher un bug de config alors
|
||||
// qu'il suffit souvent de réessayer une fois le paquet en cache.
|
||||
func mcpFriendlyErr(err error, cfg MCPServerConfig) error {
|
||||
func mcpFriendlyErr(err error, cfg MCPServerConfig, t MCPTimeouts) error {
|
||||
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
if base == "npx" || base == "uvx" {
|
||||
if mcpColdLauncher(cfg) {
|
||||
base := strings.ToLower(filepath.Base(cfg.Command))
|
||||
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
|
||||
return fmt.Errorf("délai dépassé — %s télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)", base)
|
||||
}
|
||||
return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s", mcpConnectTimeout)
|
||||
return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s (délai de connexion réglable dans les réglages MCP)", t.connect())
|
||||
}
|
||||
@@ -52,7 +52,7 @@ type MCPServerConfig struct {
|
||||
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
|
||||
// 0 = réglage global (MCPTimeouts.Call). Pour un serveur dont les outils sont longs
|
||||
// par nature (build, scraping, requête lourde).
|
||||
CallTimeoutSec int `json:"callTimeoutSec,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Délais MCP réglables depuis l'UI (repris d'AJEAN c05a0a9f) : connexion d'une
|
||||
// session et appel d'outil, en secondes, communs à tous les serveurs. Un
|
||||
// serveur peut remplacer le délai d'appel par le sien (CallTimeoutSec), et un
|
||||
// lancement npx/uvx garde au moins mcpConnectTimeoutCold pour se connecter.
|
||||
|
||||
// MCPTimeouts : les délais globaux, en secondes. Stockés dans bkState sous
|
||||
// la clé mcpTimeoutsKey ; zéro (ou clé absente) = valeur par défaut.
|
||||
type MCPTimeouts struct {
|
||||
// Connect borne l'établissement d'une session (handshake initialize +
|
||||
// tools/list). Un serveur qui rame ne doit pas figer le tour de chat.
|
||||
Connect int `json:"connect"`
|
||||
// Call borne un appel d'outil MCP.
|
||||
Call int `json:"call"`
|
||||
}
|
||||
|
||||
const (
|
||||
// Valeurs par défaut, en secondes (les anciennes constantes).
|
||||
mcpConnectTimeoutDefault = 20
|
||||
mcpCallTimeoutDefault = 120
|
||||
// mcpTimeoutMaxSec : borne haute des réglages (1 h), globaux comme par
|
||||
// serveur.
|
||||
mcpTimeoutMaxSec = 3600
|
||||
mcpTimeoutsKey = "mcp_timeouts"
|
||||
)
|
||||
|
||||
// LoadMCPTimeouts lit les délais globaux. Une valeur absente, nulle ou
|
||||
// illisible retombe sur son défaut ; une valeur hors bornes (base modifiée à
|
||||
// la main) est ramenée à mcpTimeoutMaxSec.
|
||||
func LoadMCPTimeouts() MCPTimeouts {
|
||||
var t MCPTimeouts
|
||||
if !getJSON(bkState, mcpTimeoutsKey, &t) {
|
||||
t = MCPTimeouts{}
|
||||
}
|
||||
return t.normalized()
|
||||
}
|
||||
|
||||
func (t MCPTimeouts) normalized() MCPTimeouts {
|
||||
fix := func(v, def int) int {
|
||||
switch {
|
||||
case v <= 0:
|
||||
return def
|
||||
case v > mcpTimeoutMaxSec:
|
||||
return mcpTimeoutMaxSec
|
||||
}
|
||||
return v
|
||||
}
|
||||
return MCPTimeouts{
|
||||
Connect: fix(t.Connect, mcpConnectTimeoutDefault),
|
||||
Call: fix(t.Call, mcpCallTimeoutDefault),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveMCPTimeouts enregistre les délais globaux (valeurs déjà validées par
|
||||
// l'appelant ; normalisées par sûreté).
|
||||
func SaveMCPTimeouts(t MCPTimeouts) error {
|
||||
mcpConfigMu.Lock()
|
||||
defer mcpConfigMu.Unlock()
|
||||
return putJSON(bkState, mcpTimeoutsKey, t.normalized())
|
||||
}
|
||||
|
||||
// connect et call : les délais en durées.
|
||||
func (t MCPTimeouts) connect() time.Duration { return time.Duration(t.Connect) * time.Second }
|
||||
func (t MCPTimeouts) call() time.Duration { return time.Duration(t.Call) * time.Second }
|
||||
|
||||
// mcpTimeoutsError : le refus d'un réglage hors bornes, nil sinon. Même texte
|
||||
// pour la façade (mcp/mod.rs).
|
||||
func mcpTimeoutsError(t MCPTimeouts) string {
|
||||
if t.Connect < 1 || t.Connect > mcpTimeoutMaxSec {
|
||||
return "connexion : 1 à 3600 s"
|
||||
}
|
||||
if t.Call < 1 || t.Call > mcpTimeoutMaxSec {
|
||||
return "appel d'outil : 1 à 3600 s"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// mcpCallTimeoutSecError : le refus d'un délai par serveur hors bornes.
|
||||
func mcpCallTimeoutSecError(sec int) string {
|
||||
if sec < 0 || sec > mcpTimeoutMaxSec {
|
||||
return "délai d'un appel : 0 à 3600 s (vide = réglage global)"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Une base vide donne les délais par défaut : 20 s de connexion, 120 s
|
||||
// d'appel.
|
||||
func TestMCPTimeoutsDefautsSansConfig(t *testing.T) {
|
||||
testHome(t)
|
||||
got := LoadMCPTimeouts()
|
||||
if got != (MCPTimeouts{Connect: 20, Call: 120}) {
|
||||
t.Fatalf("défauts = %+v", got)
|
||||
}
|
||||
if d := mcpTimeoutFor(MCPServerConfig{Command: "node"}, got); d != 20*time.Second {
|
||||
t.Errorf("connexion = %s", d)
|
||||
}
|
||||
if d := mcpCallTimeoutFor(MCPServerConfig{}, got, nil); d != 120*time.Second {
|
||||
t.Errorf("appel = %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
// Enregistrés puis relus ; une valeur nulle retombe sur son défaut, une
|
||||
// valeur trop grande (base modifiée à la main) sur la borne, une valeur
|
||||
// illisible sur les défauts.
|
||||
func TestMCPTimeoutsAllerRetour(t *testing.T) {
|
||||
testHome(t)
|
||||
if err := SaveMCPTimeouts(MCPTimeouts{Connect: 5, Call: 300}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := LoadMCPTimeouts(); got != (MCPTimeouts{Connect: 5, Call: 300}) {
|
||||
t.Fatalf("relecture = %+v", got)
|
||||
}
|
||||
for raw, want := range map[string]MCPTimeouts{
|
||||
`{"connect":0,"call":0}`: {20, 120},
|
||||
`{"connect":-4,"call":99999}`: {20, 3600},
|
||||
`{"connect":"x","call":50}`: {20, 120},
|
||||
`{"CONNECT":7}`: {7, 120},
|
||||
`null`: {20, 120},
|
||||
`{"connect":30,"call":40} reste`: {20, 120},
|
||||
} {
|
||||
if err := putStr(bkState, mcpTimeoutsKey, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := LoadMCPTimeouts(); got != want {
|
||||
t.Errorf("%s : %+v, attendu %+v", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Un lanceur npx/uvx garde au moins mcpConnectTimeoutCold ; un délai réglé
|
||||
// plus long l'emporte.
|
||||
func TestMCPTimeoutForLanceurFroid(t *testing.T) {
|
||||
npx := MCPServerConfig{Command: "npx"}
|
||||
if d := mcpTimeoutFor(npx, MCPTimeouts{Connect: 20, Call: 120}); d != mcpConnectTimeoutCold {
|
||||
t.Errorf("npx, 20 s : %s", d)
|
||||
}
|
||||
if d := mcpTimeoutFor(npx, MCPTimeouts{Connect: 600, Call: 120}); d != 10*time.Minute {
|
||||
t.Errorf("npx, 600 s : %s", d)
|
||||
}
|
||||
if d := mcpTimeoutFor(MCPServerConfig{URL: "http://x"}, MCPTimeouts{Connect: 45, Call: 120}); d != 45*time.Second {
|
||||
t.Errorf("http, 45 s : %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func mcpRoute(t *testing.T, h func(w *httptest.ResponseRecorder), want int) map[string]any {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
h(w)
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("réponse %q : %v", w.Body.String(), err)
|
||||
}
|
||||
if w.Code != want {
|
||||
t.Fatalf("code %d, attendu %d : %s", w.Code, want, w.Body.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Les routes des délais globaux : lecture, refus hors bornes, enregistrement
|
||||
// qui ferme les sessions ouvertes.
|
||||
func TestMCPTimeoutsRoutes(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(facadeMCPEnv, "")
|
||||
get := func() map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPTimeouts(w, httptest.NewRequest("GET", "/api/mcp/timeouts", nil))
|
||||
}, 200)
|
||||
}
|
||||
post := func(body string, want int) map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPTimeoutsSave(w, httptest.NewRequest("POST", "/api/mcp/timeouts/save", strings.NewReader(body)))
|
||||
}, want)
|
||||
}
|
||||
if v := get(); v["connect"] != 20.0 || v["call"] != 120.0 {
|
||||
t.Fatalf("GET : %v", v)
|
||||
}
|
||||
for body, msg := range map[string]string{
|
||||
`{"connect":0,"call":120}`: "connexion : 1 à 3600 s",
|
||||
`{"connect":20,"call":3601}`: "appel d'outil : 1 à 3600 s",
|
||||
`{"connect":20}`: "appel d'outil : 1 à 3600 s",
|
||||
`{"connect":"20","call":120}`: "json: cannot unmarshal string into Go struct field MCPTimeouts.connect of type int",
|
||||
} {
|
||||
if v := post(body, 400); v["error"] != msg {
|
||||
t.Errorf("%s : %v", body, v["error"])
|
||||
}
|
||||
}
|
||||
// Une session ouverte sur un serveur configuré : fermée à l'enregistrement.
|
||||
if err := SetMCPServer("lent", MCPServerConfig{URL: "http://127.0.0.1:1/mcp", Enabled: false}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fakeMCPSlow(t)
|
||||
if v := post(`{"connect":30,"call":600}`, 200); v["connect"] != 30.0 || v["call"] != 600.0 {
|
||||
t.Fatalf("POST : %v", v)
|
||||
}
|
||||
mcpMgr.mu.Lock()
|
||||
_, open := mcpMgr.sessions["lent"]
|
||||
mcpMgr.mu.Unlock()
|
||||
if open {
|
||||
t.Error("la session est restée ouverte")
|
||||
}
|
||||
if v := get(); v["connect"] != 30.0 || v["call"] != 600.0 {
|
||||
t.Fatalf("GET après enregistrement : %v", v)
|
||||
}
|
||||
if got := getStr(bkState, mcpTimeoutsKey); got != `{"connect":30,"call":600}` {
|
||||
t.Errorf("stocké : %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Le délai d'un serveur passe par /api/mcp/save, revient dans /api/mcp et
|
||||
// survit à un nouvel enregistrement sans lui (0 = réglage global).
|
||||
func TestMCPSaveCallTimeoutSec(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(facadeMCPEnv, "")
|
||||
t.Cleanup(mcpCloseAll)
|
||||
save := func(body string, want int) map[string]any {
|
||||
return mcpRoute(t, func(w *httptest.ResponseRecorder) {
|
||||
handleMCPSave(w, httptest.NewRequest("POST", "/api/mcp/save", strings.NewReader(body)))
|
||||
}, want)
|
||||
}
|
||||
v := save(`{"name":"long","url":"http://127.0.0.1:1/mcp","enabled":false,"callTimeoutSec":900}`, 200)
|
||||
srv := v["servers"].([]any)[0].(map[string]any)
|
||||
if srv["callTimeoutSec"] != 900.0 {
|
||||
t.Fatalf("état : %v", srv)
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
if servers["long"].CallTimeoutSec != 900 {
|
||||
t.Fatalf("config : %+v", servers["long"])
|
||||
}
|
||||
st, _ := MCPStatus()
|
||||
if len(st) != 1 || st[0].CallTimeoutSec != 900 {
|
||||
t.Fatalf("MCPStatus : %+v", st)
|
||||
}
|
||||
// Champ vidé dans l'UI : retour au réglage global, clé absente.
|
||||
v = save(`{"name":"long","url":"http://127.0.0.1:1/mcp","enabled":false}`, 200)
|
||||
if _, ok := v["servers"].([]any)[0].(map[string]any)["callTimeoutSec"]; ok {
|
||||
t.Fatalf("callTimeoutSec resté : %v", v)
|
||||
}
|
||||
for _, body := range []string{
|
||||
`{"name":"x","url":"http://h","callTimeoutSec":-1}`,
|
||||
`{"name":"x","url":"http://h","callTimeoutSec":3601}`,
|
||||
} {
|
||||
if v := save(body, 400); v["error"] != "délai d'un appel : 0 à 3600 s (vide = réglage global)" {
|
||||
t.Errorf("%s : %v", body, v["error"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +91,76 @@ func TestPariteMCP(t *testing.T) {
|
||||
}
|
||||
fx["prompt_line"] = lines
|
||||
timeouts := map[string]any{}
|
||||
def := MCPTimeouts{}.normalized()
|
||||
for _, cmd := range []string{"npx", "/usr/local/bin/NPX", "uvx.exe", "npx.cmd", "node", "", "uvx-x", "C:/bin/uvx.EXE"} {
|
||||
cfg := MCPServerConfig{Command: cmd}
|
||||
timeouts[cmd] = map[string]any{"secs": int(mcpTimeoutFor(cfg).Seconds()),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, cfg).Error()}
|
||||
timeouts[cmd] = map[string]any{"secs": int(mcpTimeoutFor(cfg, def).Seconds()),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, cfg, def).Error()}
|
||||
}
|
||||
fx["timeouts"] = timeouts
|
||||
fx["friendly_other"] = mcpFriendlyErr(errors.New("autre"), MCPServerConfig{Command: "npx"}).Error()
|
||||
fx["friendly_other"] = mcpFriendlyErr(errors.New("autre"), MCPServerConfig{Command: "npx"}, def).Error()
|
||||
// Délais globaux (mcp_timeouts) : lecture de la base, puis leur effet sur
|
||||
// la connexion et sur l'appel.
|
||||
var stored []any
|
||||
for _, raw := range []string{"", `{"connect":5,"call":300}`, `{"connect":0,"call":0}`, `{"connect":-4,"call":99999}`,
|
||||
`{"connect":"x","call":50}`, `{"CONNECT":7,"Call":8}`, `null`, `[1]`, `{"connect":30,"call":40} reste`,
|
||||
`{"connect":1.5,"call":60}`, `{"connect":3600,"call":3600,"autre":1}`} {
|
||||
_ = putStr(bkState, mcpTimeoutsKey, raw)
|
||||
stored = append(stored, map[string]any{"in": raw, "out": LoadMCPTimeouts()})
|
||||
}
|
||||
fx["timeouts_stored"] = stored
|
||||
var effects []any
|
||||
for _, tm := range []MCPTimeouts{{20, 120}, {5, 45}, {600, 3600}, {180, 1}} {
|
||||
for _, c := range []struct {
|
||||
cfg MCPServerConfig
|
||||
args string
|
||||
}{
|
||||
{MCPServerConfig{Command: "node"}, `{}`},
|
||||
{MCPServerConfig{Command: "npx"}, `{}`},
|
||||
{MCPServerConfig{URL: "http://h", CallTimeoutSec: 90}, `{}`},
|
||||
{MCPServerConfig{Command: "uvx", CallTimeoutSec: 3600}, `{"timeout":7200}`},
|
||||
{MCPServerConfig{URL: "http://h"}, `{"timeout":600}`},
|
||||
{MCPServerConfig{URL: "http://h"}, `{"timeout":99999999}`},
|
||||
} {
|
||||
var args map[string]any
|
||||
_ = json.Unmarshal([]byte(c.args), &args)
|
||||
cfgJSON, _ := json.Marshal(c.cfg)
|
||||
effects = append(effects, map[string]any{
|
||||
"timeouts": tm, "cfg": string(cfgJSON), "args": c.args,
|
||||
"connect": mcpTimeoutFor(c.cfg, tm).Milliseconds(),
|
||||
"err": mcpFriendlyErr(context.DeadlineExceeded, c.cfg, tm).Error(),
|
||||
"call": mcpCallTimeoutFor(c.cfg, tm, args).Milliseconds(),
|
||||
})
|
||||
}
|
||||
}
|
||||
fx["timeouts_effects"] = effects
|
||||
_ = putStr(bkState, mcpTimeoutsKey, "")
|
||||
var routes []any
|
||||
for _, rq := range [][2]string{
|
||||
{"/api/mcp/timeouts", ""},
|
||||
{"/api/mcp/timeouts/save", `{"connect":0,"call":120}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":20,"call":3601}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":20}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":"20","call":120}`},
|
||||
{"/api/mcp/timeouts/save", `{"connect":2.5,"call":120}`},
|
||||
{"/api/mcp/timeouts/save", `[]`},
|
||||
{"/api/mcp/timeouts/save", `pas du json`},
|
||||
{"/api/mcp/timeouts/save", `{"Connect":45,"CALL":900,"x":1}`},
|
||||
{"/api/mcp/timeouts", ""},
|
||||
{"/api/mcp/timeouts/save", `{"connect":3600,"call":1}`},
|
||||
{"/api/mcp/timeouts", ""},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
if rq[0] == "/api/mcp/timeouts" {
|
||||
handleMCPTimeouts(w, httptest.NewRequest("GET", rq[0], nil))
|
||||
} else {
|
||||
handleMCPTimeoutsSave(w, httptest.NewRequest("POST", rq[0], strings.NewReader(rq[1])))
|
||||
}
|
||||
routes = append(routes, map[string]any{"path": rq[0], "body": rq[1], "code": w.Code,
|
||||
"resp": strings.TrimSpace(w.Body.String()), "state": getStr(bkState, mcpTimeoutsKey)})
|
||||
}
|
||||
fx["timeouts_routes"] = routes
|
||||
_ = putStr(bkState, mcpTimeoutsKey, "")
|
||||
var flat []any
|
||||
for _, raw := range []string{
|
||||
`{"content":[{"type":"text","text":" un "},{"type":"text","text":"deux\n"}]}`,
|
||||
@@ -148,7 +211,7 @@ func TestPariteMCP(t *testing.T) {
|
||||
{Op: "set", Name: "a__b", Cfg: `{"command":"x"}`},
|
||||
{Op: "set", Name: "both", Cfg: `{"command":"x","url":"http://u"}`},
|
||||
{Op: "set", Name: "none", Cfg: `{"enabled":true}`},
|
||||
{Op: "set", Name: "web", Cfg: `{"url":"http://u/mcp","headers":{"X":"1"},"enabled":false}`},
|
||||
{Op: "set", Name: "web", Cfg: `{"url":"http://u/mcp","headers":{"X":"1"},"enabled":false,"callTimeoutSec":45}`},
|
||||
{Op: "enable", Name: "web", On: true},
|
||||
{Op: "enable", Name: "nope", On: true},
|
||||
{Op: "tool", Name: "fs", Tool: "write", On: false},
|
||||
@@ -285,7 +348,7 @@ func TestPariteMCP(t *testing.T) {
|
||||
}
|
||||
_ = SetMCPToolEnabled("alpha", "secret", false)
|
||||
_ = SetMCPServer("beta", MCPServerConfig{Command: "loki-mcp-absent-pour-test", Enabled: true})
|
||||
_ = SetMCPServer("gamma", MCPServerConfig{URL: ts.URL, Enabled: false})
|
||||
_ = SetMCPServer("gamma", MCPServerConfig{URL: ts.URL, Enabled: false, CallTimeoutSec: 300})
|
||||
raw := getStr(bkState, "mcp")
|
||||
raw = strings.Replace(raw, `}}`, `},"delta":{"enabled":true}}`, 1)
|
||||
_ = putStr(bkState, "mcp", raw)
|
||||
|
||||
@@ -2773,6 +2773,17 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<!-- Délais communs à tous les serveurs (AJEAN c05a0a9f, e44b6139),
|
||||
enregistrés à chaque modification. Un serveur peut remplacer le
|
||||
délai d'appel dans sa fenêtre d'édition. -->
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai de connexion<span class="pe-sub">pour joindre un serveur ; au moins 3 min au premier lancement npx/uvx</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-connect" class="pe-val" type="number" min="1" max="3600" step="1" value="20" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai d'un appel d'outil<span class="pe-sub">réglable aussi serveur par serveur</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-call" class="pe-val" type="number" min="1" max="3600" step="1" value="120" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
@@ -3292,6 +3303,10 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<textarea id="mcp-headers" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="Authorization: Bearer sk-…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="mcp-call-timeout">Délai d'un appel d'outil <span class="opt">(secondes, 1 à 3600 — vide = réglage global)</span></label>
|
||||
<input id="mcp-call-timeout" class="ctl" type="number" min="1" max="3600" step="1" inputmode="numeric" placeholder="réglage global">
|
||||
</div>
|
||||
<div id="mcp-modal-status" class="muted" style="font-size:12px;min-height:16px"></div>
|
||||
<div id="mcp-tools-section" style="display:none">
|
||||
<div class="subhead" style="margin-top:6px">Outils <span id="mcp-tools-count" class="muted" style="text-transform:none;letter-spacing:0;font-weight:400;font-family:var(--font)"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Décoche un outil pour le masquer à l'IA sans déconnecter le serveur. Utile pour ne garder que ce dont tu as besoin.</span></span></div>
|
||||
@@ -9502,7 +9517,47 @@ async function deleteExternal(){
|
||||
let mcpServers = [];
|
||||
let mcpEditing = null; // nom du serveur en cours d'édition ('' ou null = nouveau)
|
||||
|
||||
async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
||||
async function loadMCP(){
|
||||
renderMCP(await jget('/api/mcp'));
|
||||
loadMcpTimeouts();
|
||||
}
|
||||
|
||||
// ─── Délais globaux ──────────────────────────────────────────────────────────
|
||||
// Connexion et appel d'outil, communs à tous les serveurs (/api/mcp/timeouts).
|
||||
// Un seul enregistrement par modification : « change » part à la validation
|
||||
// ou à la perte du focus, pas à chaque frappe.
|
||||
let mcpTimeouts = {connect: 20, call: 120};
|
||||
|
||||
async function loadMcpTimeouts(){
|
||||
const r = await jget('/api/mcp/timeouts');
|
||||
if(!r || !r.ok) return;
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
document.getElementById('mcp-timeout-connect').value = r.connect;
|
||||
document.getElementById('mcp-timeout-call').value = r.call;
|
||||
mcpCallTimeoutPlaceholder();
|
||||
}
|
||||
|
||||
async function saveMcpTimeouts(){
|
||||
const connect = Number(document.getElementById('mcp-timeout-connect').value);
|
||||
const call = Number(document.getElementById('mcp-timeout-call').value);
|
||||
const ok = v => Number.isInteger(v) && v >= 1 && v <= 3600;
|
||||
if(!ok(connect) || !ok(call)){
|
||||
toast('délai invalide : de 1 à 3600 s');
|
||||
loadMcpTimeouts(); // revient aux valeurs enregistrées
|
||||
return;
|
||||
}
|
||||
const r = await jpost('/api/mcp/timeouts/save', {connect, call});
|
||||
if(!r || !r.ok){ toast('erreur : '+((r && r.error) || 'délais non enregistrés')); loadMcpTimeouts(); return; }
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
mcpCallTimeoutPlaceholder();
|
||||
toast('délais MCP enregistrés');
|
||||
}
|
||||
|
||||
// Le champ « délai d'un appel » de la fenêtre d'édition montre le réglage
|
||||
// global qu'il remplacerait.
|
||||
function mcpCallTimeoutPlaceholder(){
|
||||
document.getElementById('mcp-call-timeout').placeholder = 'réglage global ('+mcpTimeouts.call+' s)';
|
||||
}
|
||||
|
||||
function renderMCP(r){
|
||||
mcpServers = (r && r.servers) || [];
|
||||
@@ -9675,6 +9730,7 @@ function openMcpFromCatalog(e){
|
||||
Object.entries(e.env||{}).map(([k,v])=>k+'='+v).join('\n');
|
||||
document.getElementById('mcp-url').value = '';
|
||||
document.getElementById('mcp-headers').value = '';
|
||||
document.getElementById('mcp-call-timeout').value = '';
|
||||
document.getElementById('mcp-modal-title').textContent = e.name+' — depuis le catalogue';
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
@@ -9702,6 +9758,8 @@ function openMcp(name){
|
||||
document.getElementById('mcp-env').value = s && s.env ? Object.entries(s.env).map(([k,v])=>k+'='+v).join('\n') : '';
|
||||
document.getElementById('mcp-url').value = s ? (s.url||'') : '';
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
document.getElementById('mcp-call-timeout').value = s && s.callTimeoutSec ? s.callTimeoutSec : '';
|
||||
mcpCallTimeoutPlaceholder();
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
@@ -9780,6 +9838,13 @@ async function saveMcp(){
|
||||
body.headers = mcpParseKV(document.getElementById('mcp-headers').value, ':');
|
||||
body.command = ''; body.args = undefined; body.env = undefined;
|
||||
}
|
||||
// Vide = réglage global (0 côté serveur).
|
||||
const ct = document.getElementById('mcp-call-timeout').value.trim();
|
||||
if(ct){
|
||||
const n = Number(ct);
|
||||
if(!Number.isInteger(n) || n < 1 || n > 3600){ toast("délai d'un appel d'outil : de 1 à 3600 s, ou vide"); return; }
|
||||
body.callTimeoutSec = n;
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
|
||||
@@ -488,6 +488,17 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<!-- Délais communs à tous les serveurs (AJEAN c05a0a9f, e44b6139),
|
||||
enregistrés à chaque modification. Un serveur peut remplacer le
|
||||
délai d'appel dans sa fenêtre d'édition. -->
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai de connexion<span class="pe-sub">pour joindre un serveur ; au moins 3 min au premier lancement npx/uvx</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-connect" class="pe-val" type="number" min="1" max="3600" step="1" value="20" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Délai d'un appel d'outil<span class="pe-sub">réglable aussi serveur par serveur</span></span>
|
||||
<span class="pe-row-c"><input id="mcp-timeout-call" class="pe-val" type="number" min="1" max="3600" step="1" value="120" style="width:54px" onchange="saveMcpTimeouts()"><span class="pe-unit">s</span></span>
|
||||
</div>
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
@@ -1007,6 +1018,10 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<textarea id="mcp-headers" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="Authorization: Bearer sk-…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="mcp-call-timeout">Délai d'un appel d'outil <span class="opt">(secondes, 1 à 3600 — vide = réglage global)</span></label>
|
||||
<input id="mcp-call-timeout" class="ctl" type="number" min="1" max="3600" step="1" inputmode="numeric" placeholder="réglage global">
|
||||
</div>
|
||||
<div id="mcp-modal-status" class="muted" style="font-size:12px;min-height:16px"></div>
|
||||
<div id="mcp-tools-section" style="display:none">
|
||||
<div class="subhead" style="margin-top:6px">Outils <span id="mcp-tools-count" class="muted" style="text-transform:none;letter-spacing:0;font-weight:400;font-family:var(--font)"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Décoche un outil pour le masquer à l'IA sans déconnecter le serveur. Utile pour ne garder que ce dont tu as besoin.</span></span></div>
|
||||
|
||||
@@ -5,7 +5,47 @@
|
||||
let mcpServers = [];
|
||||
let mcpEditing = null; // nom du serveur en cours d'édition ('' ou null = nouveau)
|
||||
|
||||
async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
||||
async function loadMCP(){
|
||||
renderMCP(await jget('/api/mcp'));
|
||||
loadMcpTimeouts();
|
||||
}
|
||||
|
||||
// ─── Délais globaux ──────────────────────────────────────────────────────────
|
||||
// Connexion et appel d'outil, communs à tous les serveurs (/api/mcp/timeouts).
|
||||
// Un seul enregistrement par modification : « change » part à la validation
|
||||
// ou à la perte du focus, pas à chaque frappe.
|
||||
let mcpTimeouts = {connect: 20, call: 120};
|
||||
|
||||
async function loadMcpTimeouts(){
|
||||
const r = await jget('/api/mcp/timeouts');
|
||||
if(!r || !r.ok) return;
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
document.getElementById('mcp-timeout-connect').value = r.connect;
|
||||
document.getElementById('mcp-timeout-call').value = r.call;
|
||||
mcpCallTimeoutPlaceholder();
|
||||
}
|
||||
|
||||
async function saveMcpTimeouts(){
|
||||
const connect = Number(document.getElementById('mcp-timeout-connect').value);
|
||||
const call = Number(document.getElementById('mcp-timeout-call').value);
|
||||
const ok = v => Number.isInteger(v) && v >= 1 && v <= 3600;
|
||||
if(!ok(connect) || !ok(call)){
|
||||
toast('délai invalide : de 1 à 3600 s');
|
||||
loadMcpTimeouts(); // revient aux valeurs enregistrées
|
||||
return;
|
||||
}
|
||||
const r = await jpost('/api/mcp/timeouts/save', {connect, call});
|
||||
if(!r || !r.ok){ toast('erreur : '+((r && r.error) || 'délais non enregistrés')); loadMcpTimeouts(); return; }
|
||||
mcpTimeouts = {connect: r.connect, call: r.call};
|
||||
mcpCallTimeoutPlaceholder();
|
||||
toast('délais MCP enregistrés');
|
||||
}
|
||||
|
||||
// Le champ « délai d'un appel » de la fenêtre d'édition montre le réglage
|
||||
// global qu'il remplacerait.
|
||||
function mcpCallTimeoutPlaceholder(){
|
||||
document.getElementById('mcp-call-timeout').placeholder = 'réglage global ('+mcpTimeouts.call+' s)';
|
||||
}
|
||||
|
||||
function renderMCP(r){
|
||||
mcpServers = (r && r.servers) || [];
|
||||
@@ -178,6 +218,7 @@ function openMcpFromCatalog(e){
|
||||
Object.entries(e.env||{}).map(([k,v])=>k+'='+v).join('\n');
|
||||
document.getElementById('mcp-url').value = '';
|
||||
document.getElementById('mcp-headers').value = '';
|
||||
document.getElementById('mcp-call-timeout').value = '';
|
||||
document.getElementById('mcp-modal-title').textContent = e.name+' — depuis le catalogue';
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
@@ -205,6 +246,8 @@ function openMcp(name){
|
||||
document.getElementById('mcp-env').value = s && s.env ? Object.entries(s.env).map(([k,v])=>k+'='+v).join('\n') : '';
|
||||
document.getElementById('mcp-url').value = s ? (s.url||'') : '';
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
document.getElementById('mcp-call-timeout').value = s && s.callTimeoutSec ? s.callTimeoutSec : '';
|
||||
mcpCallTimeoutPlaceholder();
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
@@ -283,6 +326,13 @@ async function saveMcp(){
|
||||
body.headers = mcpParseKV(document.getElementById('mcp-headers').value, ':');
|
||||
body.command = ''; body.args = undefined; body.env = undefined;
|
||||
}
|
||||
// Vide = réglage global (0 côté serveur).
|
||||
const ct = document.getElementById('mcp-call-timeout').value.trim();
|
||||
if(ct){
|
||||
const n = Number(ct);
|
||||
if(!Number.isInteger(n) || n < 1 || n > 3600){ toast("délai d'un appel d'outil : de 1 à 3600 s, ou vide"); return; }
|
||||
body.callTimeoutSec = n;
|
||||
}
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
|
||||
@@ -1166,6 +1166,9 @@ type mcpSaveReq struct {
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
// CallTimeoutSec : délai d'un appel propre au serveur, en secondes
|
||||
// (0 ou absent = réglage global).
|
||||
CallTimeoutSec int `json:"callTimeoutSec"`
|
||||
}
|
||||
|
||||
// handleMCP renvoie l'état de tous les serveurs MCP configurés (connexion tentée
|
||||
@@ -1241,6 +1244,10 @@ func handleMCPSave(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if msg := mcpCallTimeoutSecError(req.CallTimeoutSec); msg != "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": msg})
|
||||
return
|
||||
}
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
@@ -1252,6 +1259,8 @@ func handleMCPSave(w http.ResponseWriter, r *http.Request) {
|
||||
URL: strings.TrimSpace(req.URL),
|
||||
Headers: req.Headers,
|
||||
Enabled: enabled,
|
||||
|
||||
CallTimeoutSec: req.CallTimeoutSec,
|
||||
}
|
||||
if err := SetMCPServer(req.Name, cfg); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
@@ -1334,6 +1343,36 @@ func handleMCPTest(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "servers": list})
|
||||
}
|
||||
|
||||
// handleMCPTimeouts renvoie les délais MCP globaux (secondes).
|
||||
func handleMCPTimeouts(w http.ResponseWriter, r *http.Request) {
|
||||
t := LoadMCPTimeouts()
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "connect": t.Connect, "call": t.Call})
|
||||
}
|
||||
|
||||
// handleMCPTimeoutsSave enregistre les délais MCP globaux (1 à 3600 s) et
|
||||
// ferme les sessions ouvertes, pour que le délai de connexion s'applique dès
|
||||
// la suivante.
|
||||
func handleMCPTimeoutsSave(w http.ResponseWriter, r *http.Request) {
|
||||
var req MCPTimeouts
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if msg := mcpTimeoutsError(req); msg != "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": msg})
|
||||
return
|
||||
}
|
||||
if err := SaveMCPTimeouts(req); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
for name := range servers {
|
||||
mcpInvalidate(name)
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "connect": req.Connect, "call": req.Call})
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -334,6 +334,8 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/mcp/toggle", handleMCPToggle)
|
||||
api("/api/mcp/tool", handleMCPTool)
|
||||
api("/api/mcp/test", handleMCPTest)
|
||||
api("/api/mcp/timeouts", handleMCPTimeouts)
|
||||
api("/api/mcp/timeouts/save", handleMCPTimeoutsSave)
|
||||
api("/api/memory", handleMemoryMode)
|
||||
// Tâches planifiées : l'IA exécute des consignes toute seule sur une fréquence
|
||||
// réglable (tasks.go). Le planificateur tourne dans CE process — celui qui
|
||||
|
||||
@@ -163,37 +163,37 @@
|
||||
{
|
||||
"op": "set",
|
||||
"name": "web",
|
||||
"cfg": "{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false}",
|
||||
"cfg": "{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false,\"callTimeoutSec\":45}",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":false,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "enable",
|
||||
"name": "web",
|
||||
"on": true,
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "enable",
|
||||
"name": "nope",
|
||||
"on": true,
|
||||
"error": "serveur MCP inconnu: nope",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "write",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "read",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\",\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"write\",\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
@@ -201,21 +201,21 @@
|
||||
"tool": "write",
|
||||
"on": true,
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "fs",
|
||||
"tool": "read",
|
||||
"error": "",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "tool",
|
||||
"name": "nope",
|
||||
"tool": "x",
|
||||
"error": "serveur MCP inconnu: nope",
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true}}"
|
||||
"state": "{\"fs\":{\"command\":\"npx\",\"args\":[\"-y\",\"@x/fs\",\"\\u003cw\\u003e\"],\"env\":{\"A\":\"\\u0026\"},\"enabled\":true,\"disabledTools\":[\"read\"]},\"web\":{\"url\":\"http://u/mcp\",\"headers\":{\"X\":\"1\"},\"enabled\":true,\"callTimeoutSec\":45}}"
|
||||
},
|
||||
{
|
||||
"op": "delete",
|
||||
@@ -382,13 +382,14 @@
|
||||
"connected": false,
|
||||
"tools": [],
|
||||
"disabled": [],
|
||||
"url": "{{URL}}"
|
||||
"url": "{{URL}}",
|
||||
"callTimeoutSec": 300
|
||||
}
|
||||
],
|
||||
"store": "{\"alpha\":{\"url\":\"{{URL}}\",\"enabled\":true,\"disabledTools\":[\"secret\"]},\"beta\":{\"command\":\"loki-mcp-absent-pour-test\",\"enabled\":true},\"gamma\":{\"url\":\"{{URL}}\",\"enabled\":false},\"delta\":{\"enabled\":true}}",
|
||||
"store": "{\"alpha\":{\"url\":\"{{URL}}\",\"enabled\":true,\"disabledTools\":[\"secret\"]},\"beta\":{\"command\":\"loki-mcp-absent-pour-test\",\"enabled\":true},\"gamma\":{\"url\":\"{{URL}}\",\"enabled\":false,\"callTimeoutSec\":300},\"delta\":{\"enabled\":true}}",
|
||||
"timeouts": {
|
||||
"": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"secs": 20
|
||||
},
|
||||
"/usr/local/bin/NPX": {
|
||||
@@ -400,7 +401,7 @@
|
||||
"secs": 180
|
||||
},
|
||||
"node": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"secs": 20
|
||||
},
|
||||
"npx": {
|
||||
@@ -412,7 +413,7 @@
|
||||
"secs": 180
|
||||
},
|
||||
"uvx-x": {
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s",
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"secs": 20
|
||||
},
|
||||
"uvx.exe": {
|
||||
@@ -420,6 +421,437 @@
|
||||
"secs": 180
|
||||
}
|
||||
},
|
||||
"timeouts_effects": [
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 120000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 120000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 20000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 20s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 45000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 45000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 5000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 5s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 5,
|
||||
"call": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 600000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 10m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 600,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 1000,
|
||||
"cfg": "{\"command\":\"node\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 1000,
|
||||
"cfg": "{\"command\":\"npx\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — npx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{}",
|
||||
"call": 90000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false,\"callTimeoutSec\":90}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":7200}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"command\":\"uvx\",\"enabled\":false,\"callTimeoutSec\":3600}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — uvx télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":600}",
|
||||
"call": 630000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": "{\"timeout\":99999999}",
|
||||
"call": 3600000,
|
||||
"cfg": "{\"url\":\"http://h\",\"enabled\":false}",
|
||||
"connect": 180000,
|
||||
"err": "délai dépassé — le serveur n'a pas répondu au handshake en 3m0s (délai de connexion réglable dans les réglages MCP)",
|
||||
"timeouts": {
|
||||
"connect": 180,
|
||||
"call": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timeouts_routes": [
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":120,\"connect\":20,\"ok\":true}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":0,\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"connexion : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":20,\"call\":3601}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"appel d'outil : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":20}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"appel d'outil : 1 à 3600 s\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":\"20\",\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal string into Go struct field MCPTimeouts.connect of type int\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":2.5,\"call\":120}",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal number 2.5 into Go struct field MCPTimeouts.connect of type int\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "[]",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"json: cannot unmarshal array into Go value of type loki.MCPTimeouts\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "pas du json",
|
||||
"code": 400,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"error\":\"invalid character 'p' looking for beginning of value\",\"ok\":false}",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"body": "{\"Connect\":45,\"CALL\":900,\"x\":1}",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"call\":900,\"connect\":45,\"ok\":true}",
|
||||
"state": "{\"connect\":45,\"call\":900}"
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":900,\"connect\":45,\"ok\":true}",
|
||||
"state": "{\"connect\":45,\"call\":900}"
|
||||
},
|
||||
{
|
||||
"body": "{\"connect\":3600,\"call\":1}",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts/save",
|
||||
"resp": "{\"call\":1,\"connect\":3600,\"ok\":true}",
|
||||
"state": "{\"connect\":3600,\"call\":1}"
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"code": 200,
|
||||
"path": "/api/mcp/timeouts",
|
||||
"resp": "{\"call\":1,\"connect\":3600,\"ok\":true}",
|
||||
"state": "{\"connect\":3600,\"call\":1}"
|
||||
}
|
||||
],
|
||||
"timeouts_stored": [
|
||||
{
|
||||
"in": "",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":5,\"call\":300}",
|
||||
"out": {
|
||||
"connect": 5,
|
||||
"call": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":0,\"call\":0}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":-4,\"call\":99999}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 3600
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":\"x\",\"call\":50}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"CONNECT\":7,\"Call\":8}",
|
||||
"out": {
|
||||
"connect": 7,
|
||||
"call": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "null",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "[1]",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":30,\"call\":40} reste",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":1.5,\"call\":60}",
|
||||
"out": {
|
||||
"connect": 20,
|
||||
"call": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "{\"connect\":3600,\"call\":3600,\"autre\":1}",
|
||||
"out": {
|
||||
"connect": 3600,
|
||||
"call": 3600
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_server": [
|
||||
{
|
||||
"desc": "[MCP: alpha] desc",
|
||||
|
||||
Reference in new issue
Block a user