Files
mcp-wms-api/src/index.js
T
Arthur Ria 5386f54922 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>
2026-08-24 17:32:15 +02:00

321 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* WMS MCP Server
* Main entry point for the Model Context Protocol server
*
* Architecture: 100% API-based (no direct database access)
* Data Access: All data retrieved via WMS REST APIs
*/
const path = require('path');
// Silence dotenv output to stdout (MCP requires stdout for JSON only)
const originalStdoutWrite = process.stdout.write;
process.stdout.write = process.stderr.write.bind(process.stderr);
// Resolve .env:
// - packaged (.exe built with pkg): next to the executable, so the deployed
// server can be reconfigured without a rebuild and no credential is ever
// baked into the binary snapshot.
// - from sources: project root.
const ENV_PATH = process.pkg
? path.join(path.dirname(process.execPath), '.env')
: path.join(__dirname, '..', '.env');
require('dotenv').config({ path: ENV_PATH });
// Restore stdout
process.stdout.write = originalStdoutWrite;
// Load WMS profiles from .env before any service is instantiated.
// Services register onSwitch listeners at module load, so they must see
// profile-manager in its final state — but since loadProfiles() mutates
// the manager's internal state, it's safe to call it after requires too.
const profileManager = require('./config/profile-manager');
profileManager.loadProfiles();
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} = require('@modelcontextprotocol/sdk/types.js');
// Import resources
const wmsEntitiesResources = require('./resources/wms-entities.js');
const entitySchemasResources = require('./resources/entity-schemas.js');
const queryExamplesResources = require('./resources/query-examples.js');
const workflowsResources = require('./resources/workflows.js');
const apisResources = require('./resources/apis.js');
const logsResources = require('./resources/logs.js');
// Import tools
const workflowTools = require('./tools/workflow-tools.js');
const wmsQueryTools = require('./tools/wms-query-tools.js');
const apiTools = require('./tools/api-tools.js');
const logTools = require('./tools/log-tools.js');
const adTools = require('./tools/ad-tools.js');
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 });
}
}
/**
* 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(
{
name: 'wms-mcp-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
/**
* Handler: resources/list
* List all available resources
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const allResources = [
...wmsEntitiesResources.listResources(),
...entitySchemasResources.listResources(),
...queryExamplesResources.listResources(),
...workflowsResources.listResources(),
...apisResources.listResources(),
...logsResources.listResources(),
];
return {
resources: allResources,
};
});
/**
* Handler: resources/read
* Read a specific resource
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
// Route to the appropriate handler based on URI
if (uri.startsWith('wms://entities')) {
return await wmsEntitiesResources.readResource(uri);
} else if (uri.startsWith('wms://entity-schemas')) {
return await entitySchemasResources.readResource(uri);
} else if (uri.startsWith('wms://query-examples')) {
return await queryExamplesResources.readResource(uri);
} else if (uri.startsWith('workflows://')) {
return await workflowsResources.readResource(uri);
} else if (uri.startsWith('api://')) {
return await apisResources.readResource(uri);
} else if (uri.startsWith('logs://')) {
return await logsResources.readResource(uri);
} else {
throw new Error(`Unknown resource URI: ${uri}`);
}
} catch (error) {
console.error(`[Server] Error reading resource ${uri}:`, error.message);
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: `Error reading resource: ${error.message}`,
},
],
};
}
});
/**
* Handler: tools/list
* List all available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
// Servi depuis la table de routage : la liste exposée et le dispatch ne
// peuvent pas diverger.
return {
tools: Array.from(toolRegistry.values(), entry => entry.definition),
};
});
/**
* Handler: tools/call
* Execute a specific tool
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
console.error(`[Server] Executing tool: ${name}`);
const entry = toolRegistry.get(name);
if (!entry) {
throw new Error(
`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);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
success: false,
error: error.message,
tool: name,
},
null,
2
),
},
],
isError: true,
};
}
});
/**
* Start the server
*/
async function main() {
try {
console.error('[Server] Starting WMS MCP Server...');
console.error('[Server] Architecture: 100% API-based (no direct database access)');
// Validate shared environment variables
const requiredSharedEnvVars = ['WMS_API_AUTH', 'WMS_PROFILES'];
const missingShared = requiredSharedEnvVars.filter(v => !process.env[v]);
if (missingShared.length > 0) {
console.error('[Server] WARNING: Missing shared env vars:', missingShared.join(', '));
}
// Validate that at least one profile is loaded
const profiles = profileManager.listProfiles();
if (profiles.length === 0) {
console.error('[Server] WARNING: No WMS profile loaded. Define WMS_PROFILES + <NAME>_HOST/USERNAME/PASSWORD/TENANT in .env');
} else {
console.error(`[Server] Profiles available: ${profiles.join(', ')}`);
const current = profileManager.getCurrentName();
if (current) {
console.error(`[Server] Active profile: ${current}`);
} else {
console.error('[Server] No active profile — Claude must call switch_wms_profile before any API call');
}
}
// Create stdio transport
const transport = new StdioServerTransport();
// Connect server
await server.connect(transport);
console.error('[Server] WMS MCP Server running on stdio');
console.error('[Server] Server name: wms-mcp-server');
console.error('[Server] Version: 1.0.0');
console.error('[Server] Ready to accept requests from Claude Desktop');
} catch (error) {
console.error('[Server] Fatal error starting server:', error.message);
process.exit(1);
}
}
/**
* Handle graceful shutdown
*/
process.on('SIGINT', async () => {
console.error('\n[Server] Shutting down WMS MCP Server (SIGINT)...');
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('\n[Server] Shutting down WMS MCP Server (SIGTERM)...');
process.exit(0);
});
// Handle uncaught errors
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error.message);
console.error(error.stack);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise);
console.error('[Server] Reason:', reason);
});
// Start the server
main();