From 5386f54922cae2c7a1ca0201bbb01a12b8d5d63b Mon Sep 17 00:00:00 2001 From: Arthur Ria Date: Mon, 24 Aug 2026 17:32:15 +0200 Subject: [PATCH] =?UTF-8?q?L2.2=20:=20rejette=20les=20param=C3=A8tres=20in?= =?UTF-8?q?connus=20et=20les=20requis=20manquants=20(D23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesure V1 (24/08/2026) : le SDK MCP ignore additionalProperties: false — read_recent_logs({lines: 5}) avec la clause sur le schéma répondait success: true, returnedLines: 100 (retombée silencieuse sur le défaut). La validation vit donc dans le wrapper tools/call de src/index.js, pilotée par les schémas de la table de routage (D22) : paramètre inconnu ou requis manquant -> erreur structurée nommant le fautif et les paramètres valides, avant tout dispatch. Les 23 schémas portent additionalProperties: false — inerte côté SDK, mais c'est le contrat que lisent les clients. Pas de renommage de paramètres (écarté, cf. ROADMAP). Mesures (via le protocole) : - read_recent_logs({"lines": 5}) -> "Paramètre(s) inconnu(s) pour read_recent_logs : lines. Paramètres valides : count, log_file." - read_recent_logs({"count": 5}) -> succès, returnedLines: 5 - boucle sur 22 outils avec {} (execute_command vérifié statiquement) : 22/22 répondent, aucun Unknown tool, les 11 outils à paramètres requis échouent avec le message actionnable - handshake : 23 outils, 6 resources Docs : D23 dans DECISIONS.md, L2.2 retirée de la ROADMAP. Co-Authored-By: Claude Fable 5 --- DECISIONS.md | 26 ++++++++++++++++++++++++++ ROADMAP.md | 12 ------------ src/index.js | 31 +++++++++++++++++++++++++++++++ src/tools/ad-tools.js | 5 +++++ src/tools/api-tools.js | 2 ++ src/tools/config-tools.js | 1 + src/tools/log-tools.js | 3 +++ src/tools/metadata-tools.js | 2 ++ src/tools/profile-tools.js | 3 +++ src/tools/wms-query-tools.js | 4 ++++ src/tools/workflow-tools.js | 3 +++ 11 files changed, 80 insertions(+), 12 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 223b978..5b92107 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -414,3 +414,29 @@ 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. + +--- + +## D23 — Le SDK ne valide pas les arguments : validation dans le wrapper + +**Piège mesuré (24/08/2026).** Le SDK MCP (`@modelcontextprotocol/sdk` 1.x) +ne valide **pas** les arguments d'appel contre l'`inputSchema` déclaré : +`additionalProperties: false` est ignoré, et un paramètre inconnu +(`read_recent_logs(lines: 60)`) retombe silencieusement sur les défauts +(`count = 100`) sans le moindre signal. + +**Décision.** Le wrapper `tools/call` de `src/index.js` valide chaque appel +contre le schéma de la table de routage (D22) avant le dispatch — schéma +déclaré = contrat appliqué, pour les 23 outils d'un coup : + +- **paramètre inconnu** → erreur structurée nommant le paramètre fautif **et** + les paramètres valides de l'outil ; +- **paramètre `required` manquant** → même forme d'erreur. + +Les 23 schémas portent aussi `additionalProperties: false` : inerte côté SDK, +mais c'est le contrat que lisent les clients. La validation reste volontairement +superficielle (noms et présence, pas les types) : le but est de supprimer le +silence, pas de réimplémenter JSON Schema. + +Le renommage des paramètres (`entity_type`/`query` uniformisés) a été **écarté** +au profit de cette validation — voir ROADMAP « Écarté ». diff --git a/ROADMAP.md b/ROADMAP.md index f6724a6..0ef98a2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,18 +17,6 @@ contient que ce qui reste à faire. La cause racine commune du lot (résolution `Name` -> `TableName` via l'API Metadata) est livrée — la règle et ses mesures vivent en **D21**. -### L2.2 — Rejeter les paramètres inconnus - -Le SDK MCP ignore silencieusement les paramètres non déclarés : un appel -`read_recent_logs(lines: 60)` retombe sur le défaut `count = 100` sans le -moindre signal, et l'appelant conclut à un paramètre ignoré. - -Ajouter `additionalProperties: false` aux 23 schémas d'outils. - -C'est le correctif retenu **à la place** d'une uniformisation des noms de -paramètres : renommer casse les usages existants pour un gain cosmétique, alors -que la cause réelle est l'absence de signal. - ### L2.3 — Arguments manquants : deux garde-fous Découverts lors de la révision du lot 1 (24/08/2026), en bouclant sur les 23 diff --git a/src/index.js b/src/index.js index 9383556..8d1f813 100644 --- a/src/index.js +++ b/src/index.js @@ -94,6 +94,36 @@ for (const { moduleName, module } of TOOL_MODULES) { } } +/** + * Valide les arguments d'un appel d'outil contre son inputSchema (D23). + * Le SDK MCP ne valide pas les schémas d'entrée — mesuré le 24/08/2026 : + * `additionalProperties: false` est ignoré et un paramètre inconnu retombe + * silencieusement sur les défauts. La validation vit donc ici, pilotée par la + * même table que tools/list : schéma déclaré = contrat appliqué. + */ +function validateToolArgs(definition, args) { + const schema = definition.inputSchema || {}; + const properties = schema.properties || {}; + const validNames = Object.keys(properties); + const validList = validNames.length > 0 ? validNames.join(', ') : '(aucun)'; + + const unknown = Object.keys(args || {}).filter(key => !(key in properties)); + if (unknown.length > 0) { + throw new Error( + `Paramètre(s) inconnu(s) pour ${definition.name} : ${unknown.join(', ')}. ` + + `Paramètres valides : ${validList}.` + ); + } + + const missing = (schema.required || []).filter(key => args?.[key] === undefined); + if (missing.length > 0) { + throw new Error( + `Paramètre(s) requis manquant(s) pour ${definition.name} : ${missing.join(', ')}. ` + + `Paramètres valides : ${validList}.` + ); + } +} + // Create MCP Server const server = new Server( { @@ -193,6 +223,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { `Unknown tool: ${name}. Available tools: ${Array.from(toolRegistry.keys()).join(', ')}` ); } + validateToolArgs(entry.definition, args); return await entry.module.executeTool(name, args); } catch (error) { console.error(`[Server] Error executing tool ${name}:`, error.message); diff --git a/src/tools/ad-tools.js b/src/tools/ad-tools.js index 7fd8eed..8383296 100644 --- a/src/tools/ad-tools.js +++ b/src/tools/ad-tools.js @@ -15,6 +15,7 @@ function listTools() { description: 'Get summary of Application Dictionary elements. Shows count of cached elements per type (Commands, Queries, Dialogs, Views, etc.). Only counts already-loaded types to avoid long waits.', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, }, @@ -23,6 +24,7 @@ function listTools() { description: 'Get all elements of a specific type from Application Dictionary. Supports: Command, Query, Dialog, View, Entity, Event, Hook, Report, Dashboard, and 11 other types (20 total). Elements are lazy-loaded and cached for 1 hour.', inputSchema: { type: 'object', + additionalProperties: false, properties: { element_type: { type: 'string', @@ -42,6 +44,7 @@ function listTools() { description: 'Search Application Dictionary elements by name, description, or code. Searches within a specific element type.', inputSchema: { type: 'object', + additionalProperties: false, properties: { element_type: { type: 'string', @@ -65,6 +68,7 @@ function listTools() { description: 'Get detailed information about a specific AD element by ID or name', inputSchema: { type: 'object', + additionalProperties: false, properties: { element_type: { type: 'string', @@ -83,6 +87,7 @@ function listTools() { description: 'List all available Application Dictionary element types', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, }, diff --git a/src/tools/api-tools.js b/src/tools/api-tools.js index 5fcd000..5cec3bd 100644 --- a/src/tools/api-tools.js +++ b/src/tools/api-tools.js @@ -16,6 +16,7 @@ function listTools() { description: 'Appelle l\'API Query du WMS pour interroger des entités (Containers, Stocks, Tasks, Products, etc.)', inputSchema: { type: 'object', + additionalProperties: false, properties: { entity_type: { type: 'string', @@ -44,6 +45,7 @@ function listTools() { description: 'Exécute une commande WMS (ATTENTION: peut modifier des données). Toujours récupérer la commande via get_ad_elements/get_ad_element_details avant d\'exécuter.', inputSchema: { type: 'object', + additionalProperties: false, properties: { command_name: { type: 'string', diff --git a/src/tools/config-tools.js b/src/tools/config-tools.js index f441a12..6187ea5 100644 --- a/src/tools/config-tools.js +++ b/src/tools/config-tools.js @@ -30,6 +30,7 @@ Examples: - get_system_parameters(search="CROSSDOCK") — parameters whose code/description matches`, inputSchema: { type: 'object', + additionalProperties: false, properties: { warehouse: { type: 'string', diff --git a/src/tools/log-tools.js b/src/tools/log-tools.js index 9d32d8a..30078f7 100644 --- a/src/tools/log-tools.js +++ b/src/tools/log-tools.js @@ -15,6 +15,7 @@ function listTools() { description: 'Lit les dernières lignes des fichiers de logs', inputSchema: { type: 'object', + additionalProperties: false, properties: { count: { type: 'number', @@ -33,6 +34,7 @@ function listTools() { description: 'Liste tous les fichiers de logs disponibles sous LOGS_PATH avec leur taille et date de modification', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, }, @@ -41,6 +43,7 @@ function listTools() { description: 'Recherche un mot-clé dans les fichiers de logs avec contexte', inputSchema: { type: 'object', + additionalProperties: false, properties: { keyword: { type: 'string', diff --git a/src/tools/metadata-tools.js b/src/tools/metadata-tools.js index 25304fc..7fe7d38 100644 --- a/src/tools/metadata-tools.js +++ b/src/tools/metadata-tools.js @@ -16,6 +16,7 @@ Use this to discover the exact field names and types for any entity before build - With entity_name (partial match ok, case-insensitive): returns field names + types for that entity`, inputSchema: { type: 'object', + additionalProperties: false, properties: { entity_name: { type: 'string', @@ -34,6 +35,7 @@ Examples: - generic_search() — list available search categories`, inputSchema: { type: 'object', + additionalProperties: false, properties: { query: { type: 'string', diff --git a/src/tools/profile-tools.js b/src/tools/profile-tools.js index 2e7d14a..4cd8466 100644 --- a/src/tools/profile-tools.js +++ b/src/tools/profile-tools.js @@ -16,6 +16,7 @@ function listTools() { description: 'List all WMS profiles configured in .env (AD, LIMAGRAIN, ...) with their host and tenant. Use this to see which WMS backends are available.', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, }, @@ -24,6 +25,7 @@ function listTools() { description: 'Return the currently active WMS profile (name, host, tenant, application). If no profile is active, returns an error explaining that switch_wms_profile must be called first.', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, }, @@ -32,6 +34,7 @@ function listTools() { description: 'Switch the active WMS profile. Resets the OAuth token and clears workflow/AD caches so the next API call targets the new backend. Use list_wms_profiles to see valid names.', inputSchema: { type: 'object', + additionalProperties: false, properties: { profile: { type: 'string', diff --git a/src/tools/wms-query-tools.js b/src/tools/wms-query-tools.js index 7c08f19..38b9dbb 100644 --- a/src/tools/wms-query-tools.js +++ b/src/tools/wms-query-tools.js @@ -24,6 +24,7 @@ IMPORTANT — before building a filter with a status/enum field: Never guess enum string values — they differ between Reading and Writing models.`, inputSchema: { type: 'object', + additionalProperties: false, properties: { entity_type: { type: 'string', @@ -52,6 +53,7 @@ Never guess enum string values — they differ between Reading and Writing model description: 'Get the schema/structure of a WMS entity by querying one sample record', inputSchema: { type: 'object', + additionalProperties: false, properties: { entity_type: { type: 'string', @@ -84,6 +86,7 @@ Verified values (curl-tested): (sur un emplacement: ajouter && z.LocationCode == "X")`, inputSchema: { type: 'object', + additionalProperties: false, properties: { entity_type: { type: 'string', @@ -102,6 +105,7 @@ Verified values (curl-tested): description: 'Search for a keyword across multiple WMS entities', inputSchema: { type: 'object', + additionalProperties: false, properties: { keyword: { type: 'string', diff --git a/src/tools/workflow-tools.js b/src/tools/workflow-tools.js index 2b12f76..3e82fcc 100644 --- a/src/tools/workflow-tools.js +++ b/src/tools/workflow-tools.js @@ -15,6 +15,7 @@ function listTools() { description: 'Search workflows by name, description, or code. Returns matching workflows with metadata. Workflows are lazy-loaded from API on first request and cached for 1 hour.', inputSchema: { type: 'object', + additionalProperties: false, properties: { query: { type: 'string', @@ -37,6 +38,7 @@ function listTools() { description: 'Get full details of a specific workflow by ID or code', inputSchema: { type: 'object', + additionalProperties: false, properties: { workflow_id: { type: 'string', @@ -51,6 +53,7 @@ function listTools() { description: 'List workflow groupings by applicationName. Workflows have no category field in the AD API — applicationName is the only grouping available, and all workflows of the active application share the same value.', inputSchema: { type: 'object', + additionalProperties: false, properties: {}, }, },