Nettoyage du dépôt : doublons, code mort, secrets, build

Fichiers hors périmètre ou dupliqués :
- suppression des 5 .md dupliqués à la racine (copies md5-identiques de
  docs/api/ et docs/entities/)
- suppression de JANITOR_main.js / JANITOR_entities.json (application
  Electron sans lien avec le serveur MCP)
- suppression de temp/*.json (dumps de workflows versionnés par accident)
  et ajout de temp/ au .gitignore
- suppression de claude_desktop_config_ssh.json : mots de passe en clair et
  variables ORACLE_* d'une architecture abandonnée
- AD_API_TEST_RESULTS.md -> docs/ad-api-validation.md (credentials du
  snippet remplacés par des variables d'environnement)
- suppression d'IMPLEMENTATION_SUMMARY.md, doublon du précédent
- queries api.php -> docs/reference-queries-api.php (renommage seul)

Code mort :
- suppression de src/resources/documentation.js : la resource docs:// n'a
  jamais été branchée dans src/index.js
- suppression de src/config/constants.js : module entièrement inutilisé,
  requis par wms-query-service.js mais dont aucune constante n'était lue.
  Emporte RESOURCE_URIS.WORKFLOWS_CATEGORIES, URI déclarée jamais servie.
- log-service.js : suppression de findRecentErrors, readFullLog et
  getLogStats, exportées mais exposées par aucun outil MCP
- suppression de LOG_FILE_PATTERN (lue depuis .env, jamais appliquée : le
  scan filtre sur .log en dur), y compris dans .env.example
- log-service.js : préfixe [Logs] sur les messages, comme les autres modules

Secrets :
- test-ad-api.ps1 -> scripts/test-ad-api.ps1, credentials passés en
  paramètres ou par WMS_USERNAME / WMS_PASSWORD au lieu d'être en dur

Build et test :
- @yao-pkg/pkg en devDependency, cible node22-win-x64 : npm run build
  échouait faute de pkg, et node20 n'a pas de binaire prébuilt (bascule sur
  une compilation de Node qui échoue sans toolchain MSVC)
- index.js : le .env est lu à côté de l'exécutable quand le serveur est
  packagé. Avec un chemin statique, pkg embarquait le .env dans le snapshot,
  figeant les credentials dans le binaire.
- scripts/test-connection.js : npm test pointait sur un fichier absent.
  Smoke test en lecture seule (OAuth, QueryExecute, QueryScalarExecute,
  API AD), par profil ou sur tous.

Vérifié après nettoyage : 23 outils et 6 resources répondent au handshake
MCP, npm test passe 4/4 contre le WMS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-24 15:15:08 +02:00
parent b59cbb3546
commit 9b95e15cbc
28 changed files with 2172 additions and 11497 deletions
-83
View File
@@ -1,83 +0,0 @@
/**
* Constants for WMS MCP Server
*/
// WMS Entity Types available via Query API
// Based on queries api.php reference
const WMS_ENTITY_TYPES = [
// Master Data
'Products',
'Containers',
'Accounts',
'Suppliers',
'Kits',
'Aliases',
// Operations
'Tasks',
'Stocks',
'ProductLocations',
// Inbound
'InboundOrders',
'Receptions',
// Outbound
'OutboundOrders'
];
// Entity Categories for documentation
const ENTITY_CATEGORIES = {
'Master Data': ['Products', 'Containers', 'Accounts', 'Suppliers', 'Kits', 'Aliases'],
'Operations': ['Tasks', 'Stocks', 'ProductLocations'],
'Inbound': ['InboundOrders', 'Receptions'],
'Outbound': ['OutboundOrders']
};
// Common WMS Commands
// These can be used with call_command_api tool
const WMS_COMMANDS = {
'ProductRemove': 'Mecalux.ITSW.EasyWMS.Modules.MasterData.Contracts.Commands.ProductRemoveCommand',
'ProductUpdate': 'Mecalux.ITSW.EasyWMS.Modules.MasterData.Contracts.Commands.ProductUpdateCommand',
'ContainerCreate': 'Mecalux.ITSW.EasyWMS.Modules.MasterData.Contracts.Commands.ContainerCreateCommand',
'TaskCancel': 'Mecalux.ITSW.EasyWMS.Modules.Tasks.Contracts.Commands.TaskCancelCommand'
// Add more as needed
};
// MCP Resource URIs
const RESOURCE_URIS = {
WMS_ENTITIES: 'wms://entities',
ENTITY_SCHEMAS: 'wms://entity-schemas',
QUERY_EXAMPLES: 'wms://query-examples',
WORKFLOWS_OVERVIEW: 'workflows://overview',
WORKFLOWS_CATEGORIES: 'workflows://categories',
API_CATALOG: 'api://catalog',
LOGS_GUIDE: 'logs://guide'
};
// Log patterns for error detection
const LOG_ERROR_PATTERNS = [
'ERROR',
'EXCEPTION',
'FATAL',
'CRITICAL',
'FAILED',
'FAILURE',
'WARNING'
];
// Query limits
const QUERY_LIMITS = {
MAX_ROWS: parseInt(process.env.MAX_QUERY_ROWS) || 1000,
DEFAULT_LIMIT: 100,
TIMEOUT_MS: parseInt(process.env.QUERY_TIMEOUT) || 30000
};
module.exports = {
WMS_ENTITY_TYPES,
ENTITY_CATEGORIES,
WMS_COMMANDS,
RESOURCE_URIS,
LOG_ERROR_PATTERNS,
QUERY_LIMITS
};
+10 -3
View File
@@ -14,9 +14,16 @@ const path = require('path');
const originalStdoutWrite = process.stdout.write;
process.stdout.write = process.stderr.write.bind(process.stderr);
require('dotenv').config({
path: path.join(__dirname, '..', '.env')
});
// 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;
-193
View File
@@ -1,193 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
/**
* Resources MCP pour la documentation
* Permet à Claude d'accéder à la documentation structurée en fichiers Markdown
*/
// Chemin vers le dossier de documentation
const DOCS_PATH = process.env.DOCS_PATH || path.join(__dirname, '..', '..', 'docs');
/**
* Scanne récursivement un dossier pour trouver tous les fichiers .md
* @param {string} dir - Dossier à scanner
* @param {string} baseDir - Dossier de base pour les chemins relatifs
* @returns {Promise<Array>} - Liste des fichiers .md
*/
async function scanMarkdownFiles(dir, baseDir = dir) {
let files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Récursion dans les sous-dossiers
const subFiles = await scanMarkdownFiles(fullPath, baseDir);
files = files.concat(subFiles);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
// Fichier Markdown trouvé
const relativePath = path.relative(baseDir, fullPath);
files.push({
name: entry.name,
path: fullPath,
relativePath: relativePath.replace(/\\/g, '/'), // Normaliser les slashes
uri: `docs://${relativePath.replace(/\\/g, '/')}`,
});
}
}
} catch (err) {
// Dossier n'existe pas ou erreur de lecture
console.error(`Error scanning directory ${dir}:`, err.message);
}
return files;
}
/**
* Liste les resources disponibles pour la documentation
* @returns {Promise<Array>} - Liste des resources
*/
async function listResources() {
try {
// Scanner les fichiers Markdown
const mdFiles = await scanMarkdownFiles(DOCS_PATH);
const resources = [
{
uri: 'docs://index',
name: 'Documentation Index',
description: 'Sommaire de toute la documentation disponible',
mimeType: 'text/markdown',
},
];
// Ajouter chaque fichier .md comme resource
mdFiles.forEach((file) => {
resources.push({
uri: file.uri,
name: file.name.replace('.md', ''),
description: `Documentation: ${file.relativePath}`,
mimeType: 'text/markdown',
});
});
return resources;
} catch (err) {
console.error('Error listing documentation resources:', err);
return [
{
uri: 'docs://index',
name: 'Documentation Index',
description: 'Sommaire de toute la documentation disponible',
mimeType: 'text/markdown',
},
];
}
}
/**
* Génère un index/sommaire de la documentation
* @returns {Promise<string>} - Markdown avec le sommaire
*/
async function generateIndex() {
try {
const mdFiles = await scanMarkdownFiles(DOCS_PATH);
if (mdFiles.length === 0) {
return `# Documentation\n\n*Aucun fichier de documentation trouvé dans \`${DOCS_PATH}\`*\n\n` +
`Pour ajouter de la documentation :\n` +
`1. Créez un dossier \`docs\` à la racine du projet\n` +
`2. Ajoutez vos fichiers .md (organisation libre avec sous-dossiers)\n` +
`3. Redémarrez le serveur MCP\n`;
}
let markdown = '# Documentation WMS\n\n';
markdown += `**${mdFiles.length} fichiers de documentation disponibles**\n\n`;
markdown += `📁 Emplacement : \`${DOCS_PATH}\`\n\n`;
// Grouper par dossier
const grouped = {};
mdFiles.forEach((file) => {
const dir = path.dirname(file.relativePath);
const folder = dir === '.' ? '📄 Racine' : `📁 ${dir}`;
if (!grouped[folder]) {
grouped[folder] = [];
}
grouped[folder].push(file);
});
// Générer le sommaire
markdown += '## Sommaire\n\n';
Object.keys(grouped).sort().forEach((folder) => {
markdown += `### ${folder}\n\n`;
grouped[folder].forEach((file) => {
markdown += `- **${file.name.replace('.md', '')}** - \`${file.uri}\`\n`;
});
markdown += '\n';
});
markdown += '---\n\n';
markdown += '*Pour lire un fichier, demandez à Claude de lire la resource correspondante (par exemple: "Lis la documentation X")*\n';
return markdown;
} catch (err) {
return `# Erreur\n\nImpossible de générer l'index de documentation: ${err.message}`;
}
}
/**
* Lit un fichier de documentation
* @param {string} relativePath - Chemin relatif du fichier
* @returns {Promise<string>} - Contenu Markdown du fichier
*/
async function readDocFile(relativePath) {
try {
const filePath = path.join(DOCS_PATH, relativePath);
const content = await fs.readFile(filePath, 'utf8');
return content;
} catch (err) {
return `# Erreur\n\nImpossible de lire le fichier \`${relativePath}\`: ${err.message}`;
}
}
/**
* Lit une resource documentation selon son URI
* @param {string} uri - URI de la resource (format: docs://path/to/file.md)
* @returns {Promise<Object>} - Contenu de la resource
*/
async function readResource(uri) {
let content;
if (uri === 'docs://index') {
content = await generateIndex();
} else if (uri.startsWith('docs://')) {
// Extraire le chemin relatif de l'URI
const relativePath = uri.replace('docs://', '');
content = await readDocFile(relativePath);
} else {
throw new Error(`Unknown documentation resource: ${uri}`);
}
return {
contents: [
{
uri,
mimeType: 'text/markdown',
text: content,
},
],
};
}
module.exports = {
listResources,
readResource,
};
+4 -109
View File
@@ -20,8 +20,6 @@ const DEFAULT_LOG_PATHS = [
'\\\\{host}\\ProgramData\\Mecalux\\ETLLogs',
];
const LOG_FILE_PATTERN = process.env.LOG_FILE_PATTERN || '*.log';
/**
* Retourne les chemins de logs à scanner pour le profil actif.
* Substitue {host} par l'hostname du profil.
@@ -69,13 +67,13 @@ async function scanLogsRecursively(dir, fileList = []) {
});
} catch (statErr) {
// Ignorer les fichiers inaccessibles
console.error(`Cannot access file ${fullPath}: ${statErr.message}`);
console.error(`[Logs] Cannot access file ${fullPath}: ${statErr.message}`);
}
}
}
} catch (err) {
// Ne pas planter si un dossier n'existe pas ou n'est pas accessible
console.error(`Cannot scan directory ${dir}: ${err.message}`);
console.error(`[Logs] Cannot scan directory ${dir}: ${err.message}`);
}
return fileList;
@@ -163,7 +161,7 @@ async function resolveLogFilePath(logFile) {
try {
subEntries = await fs.readdir(basePath, { withFileTypes: true });
} catch (err) {
console.error(`Cannot read base log path ${basePath}: ${err.message}`);
console.error(`[Logs] Cannot read base log path ${basePath}: ${err.message}`);
}
const subDirs = subEntries.filter(e => e.isDirectory()).map(e => e.name);
@@ -286,7 +284,7 @@ async function searchLogs(keyword, maxResults = 50, contextLines = 2) {
}
} catch (readErr) {
// Ignorer les fichiers illisibles
console.error(`Cannot read file ${file.path}: ${readErr.message}`);
console.error(`[Logs] Cannot read file ${file.path}: ${readErr.message}`);
}
}
@@ -301,112 +299,9 @@ async function searchLogs(keyword, maxResults = 50, contextLines = 2) {
}
}
/**
* Recherche des erreurs dans les logs récents
* @param {number} maxResults - Nombre maximum de résultats
* @returns {Promise<Object>} - Erreurs trouvées
*/
async function findRecentErrors(maxResults = 20) {
const errorPatterns = ['error', 'exception', 'failed', 'fatal', 'critical'];
const allErrors = [];
try {
for (const pattern of errorPatterns) {
if (allErrors.length >= maxResults) break;
const results = await searchLogs(pattern, maxResults - allErrors.length, 1);
allErrors.push(...results.results);
}
// Dédupliquer par numéro de ligne et fichier
const unique = allErrors.filter(
(error, index, self) =>
index ===
self.findIndex(
(e) => e.fullPath === error.fullPath && e.lineNumber === error.lineNumber
)
);
return {
totalErrors: unique.length,
errors: unique.slice(0, maxResults),
};
} catch (err) {
throw new Error(`Failed to find errors: ${err.message}`);
}
}
/**
* Lit tout le contenu d'un fichier de log spécifique
* @param {string} logFilePath - Chemin complet du fichier
* @returns {Promise<Object>} - Contenu du fichier
*/
async function readFullLog(logFilePath) {
try {
const content = await fs.readFile(logFilePath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim() !== '');
return {
file: path.basename(logFilePath),
fullPath: logFilePath,
totalLines: lines.length,
content: lines,
};
} catch (err) {
throw new Error(`Failed to read log file: ${err.message}`);
}
}
/**
* Obtient des statistiques sur les logs
* @returns {Promise<Object>} - Statistiques
*/
async function getLogStats() {
try {
const files = await listLogFiles();
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
// Grouper par dossier
const byDirectory = {};
files.forEach(file => {
const dir = file.directory;
if (!byDirectory[dir]) {
byDirectory[dir] = {
directory: dir,
count: 0,
totalSize: 0,
files: [],
};
}
byDirectory[dir].count++;
byDirectory[dir].totalSize += file.size;
byDirectory[dir].files.push({
name: file.name,
sizeMB: (file.size / (1024 * 1024)).toFixed(2),
modified: file.modified.toISOString(),
});
});
return {
configuredPaths: getLogPaths(),
totalFiles: files.length,
totalSize,
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
oldestFile: files[files.length - 1]?.name,
newestFile: files[0]?.name,
byDirectory: Object.values(byDirectory).sort((a, b) => b.count - a.count),
};
} catch (err) {
throw new Error(`Failed to get log stats: ${err.message}`);
}
}
module.exports = {
listLogFiles,
findLatestLogFile,
readRecentLogs,
searchLogs,
findRecentErrors,
readFullLog,
getLogStats,
};
-1
View File
@@ -4,7 +4,6 @@
*/
const apiService = require('./api-service').getInstance();
const constants = require('../config/constants');
/**
* Build a LINQ select expression