#!/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 + _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();