From eaf5e313d98ab43259ccd5e1d4f38f3c112012cc Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 4 Oct 2026 01:34:36 +0200 Subject: [PATCH] =?UTF-8?q?Cache=20:=20corrections=20de=20relecture=20?= =?UTF-8?q?=E2=80=94=20la=20ligne=20MCP=20et=20les=20descriptions=20d'outi?= =?UTF-8?q?ls=20partagent=20un=20seul=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcpPromptLine relisait le préfixe « [MCP: ] » des descriptions en le réécrivant à la main : changer le format dans mcpTools aurait fait disparaître la ligne en silence. Un nom de serveur contenant « ] » y était aussi coupé. - mcpTagDescription pose le préfixe, mcpToolServer le relit : un seul endroit. - mcpToolServer retient la coupure dont la forme assainie est celle du nom d'outil — « a] b » reste « a] b ». - Les tests qui appellent EnabledTools isolent $LOKI_HOME : la configuration MCP de la machine de test (et ses connexions) ne fausse plus le cas « sans outil MCP » ni le budget du préambule. Co-Authored-By: Claude Opus 5.5 --- internal/loki/chat_prompt_size_test.go | 3 ++ internal/loki/mcp_client.go | 50 ++++++++++++++++++-------- internal/loki/mcp_prompt_line_test.go | 10 +++++- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/internal/loki/chat_prompt_size_test.go b/internal/loki/chat_prompt_size_test.go index dd8c6c3..c07988e 100644 --- a/internal/loki/chat_prompt_size_test.go +++ b/internal/loki/chat_prompt_size_test.go @@ -49,6 +49,9 @@ import ( const promptCharBudget = 11000 // ~2750 tokens, tout allumé func TestSystemPromptStaysLean(t *testing.T) { + // $LOKI_HOME isolé : les outils MCP configurés sur la machine de test (et + // leur ligne au préambule) ne comptent pas dans le budget de Loki. + testHome(t) caps := Caps{Agent: true, Internet: true, Mem: MemAlways} tools := EnabledTools(caps) sp := baseSystemPrompt(caps, tools) diff --git a/internal/loki/mcp_client.go b/internal/loki/mcp_client.go index b37606a..14a532b 100644 --- a/internal/loki/mcp_client.go +++ b/internal/loki/mcp_client.go @@ -307,7 +307,7 @@ func mcpTools() []Tool { } // Préfixe le serveur d'origine dans la description : aide le modèle à // choisir entre outils similaires de serveurs différents. - desc = "[MCP: " + name + "] " + desc + desc = mcpTagDescription(name, desc) out = append(out, Tool{ Type: "function", Function: ToolFunction{ @@ -449,6 +449,39 @@ func flattenMCPContent(res *mcpsdk.CallToolResult) string { return text } +// mcpTagDescription préfixe la description d'un outil MCP par le nom d'origine +// de son serveur, que le nom d'outil (assaini) ne garde pas. mcpToolServer le +// relit pour la ligne du préambule : ce format n'existe qu'ici, pour que les +// deux ne puissent pas diverger en silence. +func mcpTagDescription(server, desc string) string { + return "[MCP: " + server + "] " + desc +} + +// mcpToolServer retrouve le serveur d'origine d'un outil MCP annoncé, d'après +// le préfixe posé par mcpTagDescription. Le nom brut d'un serveur peut lui-même +// contenir « ] » : on retient la première coupure dont la forme assainie est +// bien celle qui figure dans le nom de l'outil. +func mcpToolServer(t Tool) (string, bool) { + name := t.Function.Name + if !strings.HasPrefix(name, "mcp__") { + return "", false + } + rest, ok := strings.CutPrefix(t.Function.Description, "[MCP: ") + if !ok { + return "", false + } + for i := 0; ; { + j := strings.Index(rest[i:], "] ") + if j < 0 { + return "", false + } + if server := rest[:i+j]; strings.HasPrefix(name, "mcp__"+mcpSanitize(server)+"__") { + return server, true + } + i += j + 1 + } +} + // mcpPromptLine renvoie une ligne système listant les serveurs MCP annoncés et // leur nombre d'outils, pour situer le modèle. Elle se DÉDUIT des outils // réellement envoyés ce tour-ci (EnabledTools), pas de l'état du pool. @@ -463,20 +496,9 @@ func flattenMCPContent(res *mcpsdk.CallToolResult) string { func mcpPromptLine(tools []Tool) string { counts := map[string]int{} for _, t := range tools { - if !strings.HasPrefix(t.Function.Name, "mcp__") { - continue + if server, ok := mcpToolServer(t); ok { + counts[server]++ } - // mcpTools préfixe chaque description par « [MCP: ] » : le nom - // d'origine du serveur, que le nom d'outil (assaini) ne garde pas. - rest, ok := strings.CutPrefix(t.Function.Description, "[MCP: ") - if !ok { - continue - } - server, _, ok := strings.Cut(rest, "] ") - if !ok { - continue - } - counts[server]++ } if len(counts) == 0 { return "" diff --git a/internal/loki/mcp_prompt_line_test.go b/internal/loki/mcp_prompt_line_test.go index a8211f9..2cb5af4 100644 --- a/internal/loki/mcp_prompt_line_test.go +++ b/internal/loki/mcp_prompt_line_test.go @@ -8,7 +8,7 @@ import ( func mcpTestTool(server, tool string) Tool { return Tool{Type: "function", Function: ToolFunction{ Name: mcpExposedName(server, tool), - Description: "[MCP: " + server + "] décrit " + tool, + Description: mcpTagDescription(server, "décrit "+tool), }} } @@ -32,6 +32,10 @@ func TestMCPPromptLineDeriveDesOutils(t *testing.T) { ""}, {"description vide après le préfixe", []Tool{{Type: "function", Function: ToolFunction{Name: "mcp__x__y", Description: "[MCP: x] "}}}, "x (1)"}, + {"nom de serveur contenant « ] »", []Tool{mcpTestTool("a] b", "t"), mcpTestTool("a] b", "u")}, + "a] b (2)"}, + {"préfixe d'un autre serveur que celui du nom d'outil", []Tool{{Type: "function", Function: ToolFunction{Name: "mcp__x__y", Description: "[MCP: z] y"}}}, + ""}, } for _, c := range cases { got := mcpPromptLine(c.tools) @@ -51,6 +55,10 @@ func TestMCPPromptLineDeriveDesOutils(t *testing.T) { // planner (aucun outil MCP) n'a pas de ligne MCP, le fil principal l'a dès le // premier tour. func TestBaseSystemPromptLigneMCPSuitLesOutils(t *testing.T) { + // $LOKI_HOME isolé : EnabledTools lit la configuration MCP, et celle de la + // machine de test (serveurs réels, connexions comprises) fausserait le cas + // « sans outil MCP ». + testHome(t) caps := Caps{Agent: true, Mem: MemOff} with := append(EnabledTools(caps), mcpTestTool("github", "issues")) if sp := baseSystemPrompt(caps, with); !strings.Contains(sp, "MCP servers connected") || !strings.Contains(sp, "github (1)") {