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:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
const apiService = require('./api-service').getInstance();
|
||||
const constants = require('../config/constants');
|
||||
|
||||
/**
|
||||
* Build a LINQ select expression
|
||||
|
||||
Reference in New Issue
Block a user