L2.2 : rejette les paramètres inconnus et les requis manquants (D23)

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 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-24 17:32:15 +02:00
parent cb625a7918
commit 5386f54922
11 changed files with 80 additions and 12 deletions
+26
View File
@@ -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é ».
-12
View File
@@ -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
+31
View File
@@ -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);
+5
View File
@@ -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: {},
},
},
+2
View File
@@ -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',
+1
View File
@@ -30,6 +30,7 @@ Examples:
- get_system_parameters(search="CROSSDOCK") — parameters whose code/description matches`,
inputSchema: {
type: 'object',
additionalProperties: false,
properties: {
warehouse: {
type: 'string',
+3
View File
@@ -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',
+2
View File
@@ -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',
+3
View File
@@ -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',
+4
View File
@@ -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',
+3
View File
@@ -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: {},
},
},