L1.2 : route les outils par table explicite nom -> module (D22)
Le routage par préfixe de nom laissait deux outils listés dans
tools/list mais injoignables : get_entity_metadata (capté par
startsWith('get_entity_') avant sa propre branche) et list_log_files
(aucune branche : le nom contient _log_files, pas _logs).
Une table nom d'outil -> module est construite au démarrage depuis les
listTools() des 8 modules de src/tools/. tools/list est servi depuis
cette même table et le dispatch devient 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 avec un message nommant les deux
modules. Le wrapper d'erreur du handler tools/call est inchangé, les
23 outils gardent leurs noms.
Vérifié contre le WMS réel : get_entity_metadata renvoie 232 entités,
list_log_files renvoie 19 fichiers, tools/list expose toujours 23
outils et chaque nom listé est traité par le executeTool() de son
module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,10 +86,11 @@ docs/
|
|||||||
⚠️ utilise QueryType 1 : ne pas recopier (D3)
|
⚠️ utilise QueryType 1 : ne pas recopier (D3)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Routage.** `src/index.js` route les appels d'outils **par préfixe de nom**
|
**Routage.** `src/index.js` construit au démarrage une **table nom d'outil →
|
||||||
(`name.startsWith('query_wms_')`, `name.includes('_logs')`, …). En ajoutant un
|
module** depuis les `listTools()` des 8 modules de `src/tools/` ; `tools/list`
|
||||||
outil, vérifiez que son nom tombe dans la bonne branche — sinon il apparaîtra
|
et le dispatch sont servis par cette même table, donc un outil listé est routé
|
||||||
dans `tools/list` mais renverra `Unknown tool`.
|
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é.
|
1. Déclarer le schéma dans `listTools()` du module `src/tools/` concerné.
|
||||||
2. Traiter le cas dans son `executeTool()`.
|
2. Traiter le cas dans son `executeTool()`.
|
||||||
3. **Vérifier le routage par préfixe** dans `src/index.js` — ou ajouter une
|
3. Rien à faire dans `src/index.js` pour un module existant : la table de
|
||||||
branche.
|
routage est construite depuis `listTools()` (D22). Un **nouveau module**
|
||||||
|
doit être ajouté à `TOOL_MODULES`.
|
||||||
4. Logger avec le préfixe du module.
|
4. Logger avec le préfixe du module.
|
||||||
5. Renvoyer les erreurs, ne pas les lever hors du wrapper.
|
5. Renvoyer les erreurs, ne pas les lever hors du wrapper.
|
||||||
6. Tester le handshake complet :
|
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
|
commune (résolution `Name` -> `TableName` des entités), et propositions
|
||||||
explicitement écartées.
|
explicitement écartées.
|
||||||
|
|
||||||
⚠️ Deux pièges connus et non encore corrigés, à garder en tête en attendant le
|
⚠️ Piège connu et non encore corrigé, à garder en tête en attendant le lot 2 :
|
||||||
lot 1 :
|
|
||||||
|
|
||||||
- `entity_type` est interpolé sans validation dans `Context.{entity_type}`. Le
|
- `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
|
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
|
(`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un
|
||||||
HTTP 500 dont le détail est aujourd'hui perdu.
|
HTTP 500 — dont le détail (erreur de compilation LINQ) remonte désormais dans
|
||||||
- `get_entity_metadata` et `list_log_files` sont listés dans `tools/list` mais
|
la réponse de l'outil (L1.1).
|
||||||
non routés dans `src/index.js` : ils renvoient `Unknown tool`.
|
|
||||||
|
|||||||
@@ -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
|
l'historique git** (commit `b59cbb3`). Considérez ces mots de passe comme
|
||||||
compromis et changez-les ; à défaut, réécrivez l'historique avant toute
|
compromis et changez-les ; à défaut, réécrivez l'historique avant toute
|
||||||
publication du dépôt.
|
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.
|
||||||
|
|||||||
+41
-42
@@ -62,6 +62,38 @@ const metadataTools = require('./tools/metadata-tools.js');
|
|||||||
const configTools = require('./tools/config-tools.js');
|
const configTools = require('./tools/config-tools.js');
|
||||||
const profileTools = require('./tools/profile-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
|
// Create MCP Server
|
||||||
const server = new Server(
|
const server = new Server(
|
||||||
{
|
{
|
||||||
@@ -138,19 +170,10 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|||||||
* List all available tools
|
* List all available tools
|
||||||
*/
|
*/
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
const allTools = [
|
// Servi depuis la table de routage : la liste exposée et le dispatch ne
|
||||||
...workflowTools.listTools(),
|
// peuvent pas diverger.
|
||||||
...wmsQueryTools.listTools(),
|
|
||||||
...apiTools.listTools(),
|
|
||||||
...logTools.listTools(),
|
|
||||||
...adTools.listTools(),
|
|
||||||
...metadataTools.listTools(),
|
|
||||||
...configTools.listTools(),
|
|
||||||
...profileTools.listTools(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools: allTools,
|
tools: Array.from(toolRegistry.values(), entry => entry.definition),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,37 +187,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
try {
|
try {
|
||||||
console.error(`[Server] Executing tool: ${name}`);
|
console.error(`[Server] Executing tool: ${name}`);
|
||||||
|
|
||||||
// Route to the appropriate handler based on tool name
|
const entry = toolRegistry.get(name);
|
||||||
if (name.startsWith('search_workflows') ||
|
if (!entry) {
|
||||||
name.startsWith('get_workflow_') ||
|
throw new Error(
|
||||||
name.startsWith('list_workflow_')) {
|
`Unknown tool: ${name}. Available tools: ${Array.from(toolRegistry.keys()).join(', ')}`
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
return await entry.module.executeTool(name, args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Server] Error executing tool ${name}:`, error.message);
|
console.error(`[Server] Error executing tool ${name}:`, error.message);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user