diff --git a/CLAUDE.md b/CLAUDE.md index d1489f1..34bd113 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,10 +86,11 @@ docs/ ⚠️ utilise QueryType 1 : ne pas recopier (D3) ``` -**Routage.** `src/index.js` route les appels d'outils **par préfixe de nom** -(`name.startsWith('query_wms_')`, `name.includes('_logs')`, …). En ajoutant un -outil, vérifiez que son nom tombe dans la bonne branche — sinon il apparaîtra -dans `tools/list` mais renverra `Unknown tool`. +**Routage.** `src/index.js` construit au démarrage une **table nom d'outil → +module** depuis les `listTools()` des 8 modules de `src/tools/` ; `tools/list` +et le dispatch sont servis par cette même table, donc un outil listé est routé +par construction (D22). Deux modules déclarant le même nom font échouer le +serveur au démarrage. --- @@ -261,8 +262,9 @@ powershell -ExecutionPolicy Bypass -File scripts/test-ad-api.ps1 -WmsHost 10.255 1. Déclarer le schéma dans `listTools()` du module `src/tools/` concerné. 2. Traiter le cas dans son `executeTool()`. -3. **Vérifier le routage par préfixe** dans `src/index.js` — ou ajouter une - branche. +3. Rien à faire dans `src/index.js` pour un module existant : la table de + routage est construite depuis `listTools()` (D22). Un **nouveau module** + doit être ajouté à `TOOL_MODULES`. 4. Logger avec le préfixe du module. 5. Renvoyer les erreurs, ne pas les lever hors du wrapper. 6. Tester le handshake complet : @@ -279,12 +281,10 @@ Voir [ROADMAP.md](ROADMAP.md) : lots de correction planifiés, cause racine commune (résolution `Name` -> `TableName` des entités), et propositions explicitement écartées. -⚠️ Deux pièges connus et non encore corrigés, à garder en tête en attendant le -lot 1 : +⚠️ Piège connu et non encore corrigé, à garder en tête en attendant le lot 2 : - `entity_type` est interpolé sans validation dans `Context.{entity_type}`. Le nom attendu est le `TableName` de l'API Metadata, pas le nom d'entité de l'AD (`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un - HTTP 500 dont le détail est aujourd'hui perdu. -- `get_entity_metadata` et `list_log_files` sont listés dans `tools/list` mais - non routés dans `src/index.js` : ils renvoient `Unknown tool`. + HTTP 500 — dont le détail (erreur de compilation LINQ) remonte désormais dans + la réponse de l'outil (L1.1). diff --git a/DECISIONS.md b/DECISIONS.md index 0201779..cd548d6 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -350,3 +350,32 @@ Le fichier est retiré du répertoire de travail, **mais il reste dans l'historique git** (commit `b59cbb3`). Considérez ces mots de passe comme compromis et changez-les ; à défaut, réécrivez l'historique avant toute publication du dépôt. + +--- + +## D22 — Routage des outils par table explicite, plus par préfixe de nom + +**Piège.** Le handler `tools/call` de `src/index.js` routait par préfixe de nom +(`startsWith`, `includes`) dans une cascade de `else if`. Deux outils listés +dans `tools/list` n'atteignaient jamais leur module — reproduits le +24/08/2026 : + +| Outil | Cause | Erreur renvoyée | +|---|---|---| +| `get_entity_metadata` | capté par `startsWith('get_entity_')` (branche `wms-query-tools`, placée avant la sienne) | `Unknown WMS query tool: get_entity_metadata` | +| `list_log_files` | la branche logs testait `includes('_logs')`, or le nom contient `_log_files` | `Unknown tool: list_log_files` | + +Le routage par préfixe fait dépendre la joignabilité d'un outil de l'**ordre +des branches** et de conventions de nommage implicites : chaque ajout d'outil +pouvait en casser un autre silencieusement. + +**Décision.** Une table `nom d'outil → module` est construite au démarrage en +parcourant les `listTools()` des 8 modules de `src/tools/`. `tools/list` est +servi depuis cette même table et le dispatch est un lookup : un outil listé +est un outil routé, **par construction**. Deux modules déclarant le même nom +font échouer le serveur au démarrage (message nommant les deux modules) — +c'est un bug de développement, pas un cas d'exécution. + +La table ne présume rien de la signature des outils : `(name, args)` est +transmis tel quel au `executeTool()` du module. Ajouter un paramètre à un +outil ne la concerne pas. diff --git a/src/index.js b/src/index.js index 9b92fd1..9383556 100644 --- a/src/index.js +++ b/src/index.js @@ -62,6 +62,38 @@ const metadataTools = require('./tools/metadata-tools.js'); const configTools = require('./tools/config-tools.js'); const profileTools = require('./tools/profile-tools.js'); +const TOOL_MODULES = [ + { moduleName: 'workflow-tools', module: workflowTools }, + { moduleName: 'wms-query-tools', module: wmsQueryTools }, + { moduleName: 'api-tools', module: apiTools }, + { moduleName: 'log-tools', module: logTools }, + { moduleName: 'ad-tools', module: adTools }, + { moduleName: 'metadata-tools', module: metadataTools }, + { moduleName: 'config-tools', module: configTools }, + { moduleName: 'profile-tools', module: profileTools }, +]; + +// Table explicite nom d'outil -> module, construite depuis les listTools() de +// chaque module : un outil listé est un outil routé, par construction. Le +// routage par préfixe de nom laissait des outils listés mais injoignables +// (get_entity_metadata capté par la mauvaise branche, list_log_files capté +// par aucune). +// Un nom déclaré par deux modules est un bug de développement : on échoue au +// démarrage, pas à l'exécution. +const toolRegistry = new Map(); +for (const { moduleName, module } of TOOL_MODULES) { + for (const definition of module.listTools()) { + const existing = toolRegistry.get(definition.name); + if (existing) { + throw new Error( + `[Server] Duplicate tool name "${definition.name}" declared by both ` + + `${existing.moduleName} and ${moduleName} — rename one of them` + ); + } + toolRegistry.set(definition.name, { moduleName, module, definition }); + } +} + // Create MCP Server const server = new Server( { @@ -138,19 +170,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { * List all available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { - const allTools = [ - ...workflowTools.listTools(), - ...wmsQueryTools.listTools(), - ...apiTools.listTools(), - ...logTools.listTools(), - ...adTools.listTools(), - ...metadataTools.listTools(), - ...configTools.listTools(), - ...profileTools.listTools(), - ]; - + // Servi depuis la table de routage : la liste exposée et le dispatch ne + // peuvent pas diverger. return { - tools: allTools, + tools: Array.from(toolRegistry.values(), entry => entry.definition), }; }); @@ -164,37 +187,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { try { console.error(`[Server] Executing tool: ${name}`); - // Route to the appropriate handler based on tool name - if (name.startsWith('search_workflows') || - name.startsWith('get_workflow_') || - name.startsWith('list_workflow_')) { - return await workflowTools.executeTool(name, args); - } else if (name.startsWith('query_wms_') || - name.startsWith('count_wms_') || - name.startsWith('get_entity_') || - name.startsWith('search_wms_')) { - return await wmsQueryTools.executeTool(name, args); - } else if (name.startsWith('call_query_api') || - name.startsWith('execute_command')) { - return await apiTools.executeTool(name, args); - } else if (name.includes('_logs')) { - return await logTools.executeTool(name, args); - } else if (name.startsWith('get_application_') || - name.startsWith('get_ad_') || - name.startsWith('search_ad_') || - name.startsWith('list_ad_')) { - return await adTools.executeTool(name, args); - } else if (name === 'get_entity_metadata' || name === 'generic_search') { - return await metadataTools.executeTool(name, args); - } else if (name === 'get_system_parameters') { - return await configTools.executeTool(name, args); - } else if (name === 'list_wms_profiles' || - name === 'get_current_wms_profile' || - name === 'switch_wms_profile') { - return await profileTools.executeTool(name, args); - } else { - throw new Error(`Unknown tool: ${name}`); + const entry = toolRegistry.get(name); + if (!entry) { + throw new Error( + `Unknown tool: ${name}. Available tools: ${Array.from(toolRegistry.keys()).join(', ')}` + ); } + return await entry.module.executeTool(name, args); } catch (error) { console.error(`[Server] Error executing tool ${name}:`, error.message); return {