UNPKG

mcp-orchestrator-server

Version:

Serveur MCP pour l'orchestration de tâches distantes (SSH/SFTP) avec une file d'attente persistante.

806 lines (728 loc) 32.7 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; console.error("✅ McpServer importé"); import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; console.error("✅ StdioServerTransport importé"); import { z } from "zod"; console.error("✅ zod importé"); import queue from './queue.js'; console.error("✅ queue importé"); import servers from './servers.js'; console.error("✅ servers importé"); import sftp from './sftp.js'; console.error("✅ sftp importé"); import ssh from './ssh.js'; console.error("✅ ssh importé"); import history from './history.js'; console.error("✅ history importé"); import config from './config.js'; console.error("✅ config importé"); import apis from './apis.js'; console.error("✅ apis importé"); console.error("=== TOUS LES IMPORTS RÉUSSIS ==="); // ✅ INITIALISER EXPLICITEMENT ET ATTENDRE console.error("⏳ Initialisation de la queue..."); await queue.init(); console.error("✅ Queue initialisée"); const server = new McpServer({ name: "orchestrator", version: "8.0.0", // Version avec gestion du streaming description: "Serveur pour l'orchestration de tâches distantes avec exécution hybride et configuration flexible." }); console.error("✅ Serveur MCP créé"); // --- OUTILS DE GESTION DES SERVEURS --- console.error("✅ Serveur MCP créé"); server.registerTool( "server_add", { title: "Ajouter/Modifier un alias de serveur", description: "Enregistre ou met à jour les informations de connexion d'un serveur. Vous devez fournir soit un chemin de clé, soit un mot de passe.", inputSchema: z.object({ alias: z.string().describe("Nom court et unique pour le serveur (ex: vps_production)"), host: z.string().describe("Adresse IP ou nom d'hôte du serveur"), user: z.string().describe("Nom d'utilisateur pour la connexion"), keyPath: z.string().optional().describe("Chemin absolu vers la clé privée SSH."), password: z.string().optional().describe("Mot de passe pour la connexion.") }).refine(data => data.keyPath || data.password, { message: "Vous devez fournir au moins une méthode d'authentification ('keyPath' ou 'password')." }) }, async (params) => { try { const { alias, ...serverConfig } = params; const result = await servers.addServer(alias, serverConfig); return { content: [{ type: "text", text: result.message }] }; } catch (e) { const errorPayload = { toolName: "server_add", errorCode: "TOOL_EXECUTION_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); server.registerTool( "server_list", { title: "Lister les alias de serveurs", description: "Affiche la liste de tous les alias de serveurs configurés avec leurs détails.", inputSchema: z.object({}) }, async () => { const serverList = await servers.listServers(); return { content: [{ type: "text", text: JSON.stringify(serverList, null, 2) }] }; } ); server.registerTool( "server_remove", { title: "Supprimer un alias de serveur", description: "Supprime un alias de serveur de la configuration.", inputSchema: z.object({ alias: z.string().describe("Nom de l'alias à supprimer") }) }, async (params) => { try { const result = await servers.removeServer(params.alias); return { content: [{ type: "text", text: result.message }] }; } catch (e) { const errorPayload = { toolName: "server_remove", errorCode: "TOOL_EXECUTION_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); // --- OUTILS DE GESTION D'API --- server.registerTool( "api_add", { title: "Ajouter une API au catalogue", description: "Ajoute ou met à jour une API dans le catalogue de monitoring.", inputSchema: z.object({ alias: z.string().describe("Alias unique pour l'API."), url: z.string().url().describe("URL de base de l'API, incluant le port si nécessaire."), health_check_endpoint: z.string().optional().describe("Endpoint spécifique pour le test de santé (ex: /health)."), health_check_method: z.enum(['GET', 'POST']).optional().default('GET').describe("Méthode HTTP pour le test de santé."), auth_method: z.enum(['api_key', 'htpasswd', 'both', 'none']).optional().default('none').describe("Méthode d'authentification."), api_key: z.string().optional().describe("Clé API si nécessaire."), auth_header_name: z.string().optional().default('Authorization').describe("Nom du header pour la clé API."), auth_scheme: z.string().optional().default('Bearer').describe("Schéma d'authentification (ex: Bearer). Mettre à '' si non applicable."), htpasswd_user: z.string().optional().describe("Nom d'utilisateur pour l'authentification Basic (htpasswd)."), htpasswd_pass: z.string().optional().describe("Mot de passe pour l'authentification Basic (htpasswd)."), notes: z.string().optional().describe("Notes additionnelles.") }) }, async (params) => { try { const { alias, ...apiConfig } = params; const result = await apis.addApi(alias, apiConfig); return { content: [{ type: "text", text: result.message }] }; } catch (e) { return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true }; } } ); server.registerTool( "api_list", { title: "Lister les APIs du catalogue", description: "Affiche toutes les APIs configurées dans le catalogue.", inputSchema: z.object({}) }, async () => { const apiList = await apis.listApis(); return { content: [{ type: "text", text: JSON.stringify(apiList, null, 2) }] }; } ); server.registerTool( "api_remove", { title: "Supprimer une API du catalogue", description: "Supprime une API du catalogue en utilisant son alias.", inputSchema: z.object({ alias: z.string().describe("Alias de l'API à supprimer.") }) }, async (params) => { try { const result = await apis.removeApi(params.alias); return { content: [{ type: "text", text: result.message }] }; } catch (e) { return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true }; } } ); server.registerTool( "api_check", { title: "Vérifier la santé d'une API via son alias", description: "Lance un test de santé sur une API du catalogue.", inputSchema: z.object({ alias: z.string().describe("Alias de l'API à tester."), server_alias: z.string().describe("Alias du serveur depuis lequel lancer le test.") }) }, async (params) => { try { const apiConfig = await apis.getApi(params.alias); const endpoint = apiConfig.health_check_endpoint || ''; const url = `${apiConfig.url}${endpoint}`; const method = apiConfig.health_check_method || 'GET'; let curlCmd = `curl -X ${method} -o /dev/null -s -w '%{http_code}:%{time_total}'`; // Gérer l'authentification htpasswd if ((apiConfig.auth_method === 'htpasswd' || apiConfig.auth_method === 'both') && apiConfig.htpasswd_user && apiConfig.htpasswd_pass) { curlCmd += ` -u ${apiConfig.htpasswd_user}:${apiConfig.htpasswd_pass}`; } // Gérer l'authentification par clé API if ((apiConfig.auth_method === 'api_key' || apiConfig.auth_method === 'both') && apiConfig.api_key) { const scheme = apiConfig.auth_scheme ? `${apiConfig.auth_scheme} ` : ''; curlCmd += ` -H '${apiConfig.auth_header_name || 'Authorization'}: ${scheme}${apiConfig.api_key}'`; } curlCmd += ` ${url}`; const job = queue.addJob({ type: 'ssh', alias: params.server_alias, cmd: curlCmd }); ssh.executeCommand(job.id); const result = await waitForJobCompletion(job.id, config.syncTimeout); if (!result || result.status !== 'completed') { throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`); } const parsedOutput = ssh.parseApiHealth(result.output); return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] }; } catch (e) { return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true }; } } ); // --- NOUVEAUX OUTILS DE MONITORING --- server.registerTool( "get_system_resources", { title: "Obtenir les ressources système d'un VPS", description: "Récupère les métriques système vitales (CPU, RAM, Disque) d'un serveur.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible.") }) }, async (params) => { try { const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: "uptime && free -h && df -h /" }); ssh.executeCommand(job.id); const result = await waitForJobCompletion(job.id, config.syncTimeout); if (!result || result.status !== 'completed') { throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`); } const parsedOutput = ssh.parseSystemResources(result.output); return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] }; } catch (e) { const errorPayload = { toolName: "get_system_resources", errorCode: "MONITORING_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); server.registerTool( "get_services_status", { title: "Obtenir le statut des services d'un VPS", description: "Récupère le statut de tous les services connus (systemd, Docker, PM2) sur un serveur.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible.") }) }, async (params) => { try { const cmd = "systemctl --type=service --state=running --no-pager ; echo '---DOCKER---' ; docker ps --format '{{.Names}}: {{.Status}}' ; echo '---PM2---' ; pm2 list"; const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd }); ssh.executeCommand(job.id); const result = await waitForJobCompletion(job.id, config.syncTimeout); if (!result || result.status !== 'completed') { throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`); } const parsedOutput = ssh.parseServicesStatus(result.output); return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] }; } catch (e) { const errorPayload = { toolName: "get_services_status", errorCode: "MONITORING_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); server.registerTool( "check_api_health", { title: "Vérifier la santé d'une API", description: "Vérifie la disponibilité et le temps de réponse d'un endpoint HTTP/S.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur depuis lequel lancer le test."), url: z.string().url().describe("URL complète de l'endpoint à tester.") }) }, async (params) => { try { const cmd = `curl -o /dev/null -s -w '%{http_code}:%{time_total}' ${params.url}`; const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd }); ssh.executeCommand(job.id); const result = await waitForJobCompletion(job.id, config.syncTimeout); if (!result || result.status !== 'completed') { throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`); } const parsedOutput = ssh.parseApiHealth(result.output); return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] }; } catch (e) { const errorPayload = { toolName: "check_api_health", errorCode: "MONITORING_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); server.registerTool( "get_fail2ban_status", { title: "Obtenir le statut de Fail2Ban", description: "Récupère les informations du service Fail2Ban, pour toutes les jails ou une jail spécifique.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), jail: z.string().optional().describe("Nom d'une jail spécifique à inspecter (ex: sshd). Laissez vide pour un statut général.") }) }, async (params) => { try { const cmd = `sudo fail2ban-client status ${params.jail || ''}`.trim(); const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd }); ssh.executeCommand(job.id); const result = await waitForJobCompletion(job.id, config.syncTimeout); if (!result || result.status !== 'completed') { throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`); } // Pour cette version, nous retournons la sortie brute. return { content: [{ type: "text", text: result.output }] }; } catch (e) { const errorPayload = { toolName: "get_fail2ban_status", errorCode: "MONITORING_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); // --- LOGIQUE D'ATTENTE HYBRIDE --- async function waitForJobCompletion(jobId, timeout) { return new Promise((resolve) => { const startTime = Date.now(); const interval = setInterval(() => { const job = queue.getJob(jobId); if (job.status === 'completed' || job.status === 'failed') { clearInterval(interval); resolve(job); } else if (Date.now() - startTime > timeout) { clearInterval(interval); resolve(null); } }, 200); }); } // --- EXÉCUTION DE TÂCHES --- server.registerTool( "task_transfer", { title: "Transférer un fichier ou dossier (SFTP)", description: `Lance un transfert SFTP. Si la tâche prend moins de ${config.syncTimeout / 1000}s, le résultat est direct. Sinon, elle passe en arrière-plan.`, inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), direction: z.enum(['upload', 'download']), local: z.string().describe("Chemin absolu local."), remote: z.string().describe("Chemin absolu distant."), rappel: z.number().optional().describe("Définit un rappel en secondes.") }) }, async (params) => { const job = queue.addJob({ type: 'sftp', ...params, status: 'pending' }); history.logTask(job); sftp.executeTransfer(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `Résultat direct (tâche ${finalJob.id}): ${JSON.stringify(finalJob, null, 2)}` }] }; } else { return { content: [{ type: "text", text: `Tâche de transfert ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "task_exec", { title: "Exécuter une commande à distance (SSH)", description: `Exécute une commande SSH. Si la tâche prend moins de ${config.syncTimeout / 1000}s, le résultat est direct. Sinon, elle passe en arrière-plan.`, inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), cmd: z.string().describe("La commande complète à exécuter."), rappel: z.number().optional().describe("Définit un rappel en secondes.") }) }, async (params) => { const job = queue.addJob({ type: 'ssh', ...params, status: 'pending' }); history.logTask(job); ssh.executeCommand(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `Résultat direct (tâche ${finalJob.id}): ${finalJob.output || JSON.stringify(finalJob, null, 2)}` }] }; } else { return { content: [{ type: "text", text: `Tâche d'exécution ${job.id} initiée en arrière-plan.` }] }; } } ); // --- OUTILS DE SUIVI ET HISTORIQUE --- function formatJobForDisplay(job) { const displayJob = { ...job }; if (job.status === 'running' && job.reminderAt && new Date() > new Date(job.reminderAt)) { displayJob.reminder = "ATTENTION: Le temps de rappel est écoulé. La tâche est peut-être terminée ou bloquée."; } return displayJob; } server.registerTool( "task_queue", { title: "Voir la file d'attente des tâches", description: "Affiche le statut de toutes les tâches, avec des rappels pour les tâches longues.", inputSchema: z.object({}) }, async () => { const queueState = queue.getQueue(); const displayQueue = Object.values(queueState).map(formatJobForDisplay); return { content: [{ type: "text", text: JSON.stringify(displayQueue, null, 2) }] }; } ); server.registerTool( "task_status", { title: "Consulter une tâche par son ID", description: "Récupère les détails d'une seule tâche, avec un rappel si nécessaire.", inputSchema: z.object({ id: z.string().describe("L'ID de la tâche à consulter.") }) }, async (params) => { const job = queue.getJob(params.id); if (!job) return { content: [{ type: "text", text: `ERREUR: Tâche '${params.id}' introuvable.` }], isError: true }; const displayJob = formatJobForDisplay(job); return { content: [{ type: "text", text: JSON.stringify(displayJob, null, 2) }] }; } ); server.registerTool( "task_history", { title: "Consulter l'historique des tâches", description: "Affiche les dernières tâches lancées. Peut être filtré par alias.", inputSchema: z.object({ alias: z.string().optional().describe("Filtre l'historique pour ne montrer que les tâches d'un alias spécifique.") }) }, async (params) => { const historyLogs = await history.getHistory(params); return { content: [{ type: "text", text: JSON.stringify(historyLogs, null, 2) }] }; } ); // --- NOUVEAUX OUTILS POUR LES FONCTIONNALITÉS AVANCÉES --- server.registerTool( "task_transfer_multi", { title: "Transférer plusieurs fichiers/dossiers (SFTP)", description: "Lance des transferts SFTP multiples avec support de patterns glob (*, ?, []).", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), direction: z.enum(['upload', 'download']), files: z.array(z.object({ local: z.string().describe("Chemin local ou pattern glob (ex: /home/*.txt)"), remote: z.string().describe("Chemin distant") })).describe("Liste des fichiers à transférer"), rappel: z.number().optional().describe("Définit un rappel en secondes.") }) }, async (params) => { const job = queue.addJob({ type: 'sftp', ...params, status: 'pending', files: params.files }); history.logTask(job); sftp.executeMultiTransfer(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `Résultat transferts multiples (tâche ${finalJob.id}):\n${JSON.stringify(finalJob, null, 2)}` }] }; } else { return { content: [{ type: "text", text: `Tâche de transferts multiples ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "task_exec_interactive", { title: "Exécuter une commande interactive (SSH)", description: "Exécute une commande SSH avec gestion des prompts interactifs (yes/no, passwords, etc.).", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), cmd: z.string().describe("La commande à exécuter."), interactive: z.boolean().optional().default(true).describe("Mode interactif."), autoRespond: z.boolean().optional().default(true).describe("Répondre automatiquement aux prompts standards."), responses: z.record(z.string()).optional().describe("Réponses personnalisées aux prompts (clé: pattern, valeur: réponse)."), timeout: z.number().optional().describe("Timeout personnalisé en secondes. Défaut 2 minutes."), rappel: z.number().optional().describe("Définit un rappel en secondes.") }) }, async (params) => { const job = queue.addJob({ type: 'ssh', ...params, status: 'pending', interactive: params.interactive, autoRespond: params.autoRespond }); history.logTask(job); ssh.executeCommand(job.id); const finalJob = await waitForJobCompletion(job.id, params.timeout || config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `Résultat commande interactive (tâche ${finalJob.id}):\n${finalJob.output || JSON.stringify(finalJob, null, 2)}` }] }; } else { return { content: [{ type: "text", text: `Tâche interactive ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "task_exec_sequence", { title: "Exécuter une séquence de commandes (SSH)", description: "Exécute plusieurs commandes SSH en séquence sur le même serveur.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), commands: z.array(z.union([ z.string(), z.object({ command: z.string(), timeout: z.number().optional(), continueOnError: z.boolean().optional() }) ])).min(1).describe("Liste des commandes à exécuter en séquence (minimum 1)."), continueOnError: z.boolean().optional().default(false).describe("Continuer même si une commande échoue."), rappel: z.number().optional().describe("Définit un rappel en secondes.") }) }, async (params) => { const job = queue.addJob({ type: 'ssh_sequence', ...params, status: 'pending' }); history.logTask(job); ssh.executeCommandSequence(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout * params.commands.length); if (finalJob) { return { content: [{ type: "text", text: `Résultat séquence (tâche ${finalJob.id}):\n${JSON.stringify(finalJob, null, 2)}` }] }; } else { return { content: [{ type: "text", text: `Séquence de commandes ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "pool_stats", { title: "Statistiques du pool de connexions SSH", description: "Affiche les statistiques du pool de connexions SSH persistantes.", inputSchema: z.object({}) }, async () => { const stats = ssh.getPoolStats(); return { content: [{ type: "text", text: `Pool de connexions SSH:\n${JSON.stringify(stats, null, 2)}` }] }; } ); server.registerTool( "queue_stats", { title: "Statistiques de la queue", description: "Affiche les statistiques détaillées de la queue de tâches.", inputSchema: z.object({}) }, async () => { const stats = queue.getStats(); const crashed = queue.getCrashedJobs(); const result = { stats, crashedJobs: crashed.length, canRetry: crashed.map(j => ({ id: j.id, type: j.type, crashedAt: j.crashedAt })) }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } ); server.registerTool( "task_retry", { title: "Réessayer une tâche échouée", description: "Relance une tâche qui a échoué ou crashé.", inputSchema: z.object({ id: z.string().describe("L'ID de la tâche à réessayer.") }) }, async (params) => { try { const newJob = await queue.retryJob(params.id); // Relancer selon le type if (newJob.type === 'sftp') { sftp.executeTransfer(newJob.id); } else if (newJob.type === 'ssh') { ssh.executeCommand(newJob.id); } else if (newJob.type === 'ssh_sequence') { ssh.executeCommandSequence(newJob.id); } return { content: [{ type: "text", text: `Tâche ${params.id} relancée avec le nouvel ID: ${newJob.id}` }] }; } catch (e) { const errorPayload = { toolName: "task_retry", errorCode: "TOOL_EXECUTION_ERROR", errorMessage: e.message }; return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true }; } } ); server.registerTool( "task_logs", { title: "Consulter les logs système", description: "Affiche les logs du système MCP.", inputSchema: z.object({ level: z.enum(['error', 'warn', 'info', 'debug']).optional().describe("Filtrer par niveau de log."), search: z.string().optional().describe("Rechercher dans les messages."), limit: z.number().optional().default(50).describe("Nombre de logs à afficher.") }) }, async (params) => { const logs = queue.getLogs({ level: params.level, search: params.search }).slice(-params.limit); return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] }; } ); // --- NOUVEAUX OUTILS DE LOGS (v8) --- server.registerTool( "get_pm2_logs", { title: "Récupérer les logs PM2", description: "Raccourci pour récupérer les logs PM2 d'une application spécifique ou de toutes les apps.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), app: z.string().optional().describe("Nom de l'application PM2 (optionnel, toutes par défaut)."), lines: z.number().optional().default(100).describe("Nombre de lignes à récupérer."), errors: z.boolean().optional().default(false).describe("Récupérer uniquement les erreurs (stderr).") }) }, async (params) => { let cmd = 'pm2 logs'; if (params.app) cmd += ` ${params.app}`; if (params.errors) cmd += ' --err'; cmd += ` --lines ${params.lines} --nostream`; const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false }); history.logTask(job); ssh.executeCommand(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `📋 Logs PM2${params.app ? ` (${params.app})` : ''} - ${finalJob.lineCount || 0} lignes:\n\n${finalJob.output || '(vide)'}` }] }; } else { return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "get_docker_logs", { title: "Récupérer les logs Docker", description: "Raccourci pour récupérer les logs d'un container Docker.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), container: z.string().describe("Nom ou ID du container Docker."), lines: z.number().optional().default(100).describe("Nombre de lignes à récupérer."), since: z.string().optional().describe("Logs depuis (ex: '5m', '1h', '2024-01-01')."), timestamps: z.boolean().optional().default(false).describe("Afficher les timestamps.") }) }, async (params) => { let cmd = `docker logs --tail ${params.lines}`; if (params.since) cmd += ` --since ${params.since}`; if (params.timestamps) cmd += ' --timestamps'; cmd += ` ${params.container}`; const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false }); history.logTask(job); ssh.executeCommand(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `🐳 Logs Docker (${params.container}) - ${finalJob.lineCount || 0} lignes:\n\n${finalJob.output || '(vide)'}` }] }; } else { return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] }; } } ); server.registerTool( "tail_file", { title: "Afficher les dernières lignes d'un fichier", description: "Équivalent de tail -n pour afficher les dernières lignes d'un fichier distant.", inputSchema: z.object({ alias: z.string().describe("Alias du serveur cible."), filepath: z.string().describe("Chemin absolu du fichier à lire."), lines: z.number().optional().default(50).describe("Nombre de lignes à afficher.") }) }, async (params) => { const cmd = `tail -n ${params.lines} ${params.filepath}`; const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false }); history.logTask(job); ssh.executeCommand(job.id); const finalJob = await waitForJobCompletion(job.id, config.syncTimeout); if (finalJob) { return { content: [{ type: "text", text: `📄 Contenu de ${params.filepath} (${finalJob.lineCount || 0} lignes):\n\n${finalJob.output || '(vide)'}` }] }; } else { return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] }; } } ); // --- DÉMARRAGE DU SERVEUR --- async function main() { console.error("🔌 Connexion du transport stdio..."); const transport = new StdioServerTransport(); await server.connect(transport); console.error("🚀 Serveur connecté et prêt !"); } main().catch((error) => { console.error("Erreur fatale du serveur:", error); process.exit(1); });