@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import sourceAdapter from './sourceAdapter.js';
3
+ import fileOps from './fileOps.js';
4
+
5
+ /**
6
+ * compareEngine — Comparaison d'un même fichier (ou de fichiers équivalents à
7
+ * des emplacements différents) sur plusieurs sources.
8
+ * Détecte les "drifts" de configuration entre serveurs (et localhost).
9
+ *
10
+ * Regroupe les sources par hash : chaque groupe = ensemble de sources ayant
11
+ * un contenu identique. Plus d'un groupe → drift détecté.
12
+ *
13
+ * Chaque source peut porter :
14
+ * - un path différent d'une source à l'autre (ex: .bashrc ici, .zshrc là)
15
+ * - un label personnalisé (source.label) pour un affichage clair
16
+ * Quand un drift est détecté, on peut générer le DIFF réel entre la version
17
+ * de référence (groupe majoritaire) et chaque version divergente.
18
+ */
19
+
20
+ export default {
21
+ /**
22
+ * Compare le contenu d'un fichier sur plusieurs sources.
23
+ * sources = [{ type, path, alias?, label? }, ...] (min 2, local et/ou remote)
24
+ * options = { includeContent: bool (défaut false), includeDiff: bool (défaut true) }
25
+ *
26
+ * Retourne :
27
+ * {
28
+ * drift: bool, // true si plus d'une version distincte
29
+ * reference: { label, sources } | null, // groupe majoritaire (référence)
30
+ * groups: [{ hash, size, sources:[{server,path,label}], content?, isReference }],
31
+ * drifts: [{ from, to, added, removed, diff }], // diff référence → chaque divergence
32
+ * errors: [{ source:{server,path,label}, error }],
33
+ * stats: { totalSources, successfulReads, uniqueVersions, unreadable }
34
+ * }
35
+ */
36
+ async compareSources(sources, options = {}) {
37
+ const includeContent = options.includeContent === true;
38
+ const includeDiff = options.includeDiff !== false; // défaut true
39
+ const results = [];
40
+ const errors = [];
41
+
42
+ // Lecture de chaque source (séquentiel : simple et robuste ; le pool gère le reste)
43
+ for (const source of sources) {
44
+ const origin = sourceAdapter.describe(source);
45
+ try {
46
+ const file = await fileOps.readFile(source);
47
+ results.push({ origin, hash: file.hash, size: file.size, content: file.content });
48
+ } catch (e) {
49
+ errors.push({ source: origin, error: e.message });
50
+ }
51
+ }
52
+
53
+ // Regroupement par hash (on garde le contenu en interne pour le diff)
54
+ const groupsMap = new Map();
55
+ for (const r of results) {
56
+ if (!groupsMap.has(r.hash)) {
57
+ groupsMap.set(r.hash, {
58
+ hash: r.hash,
59
+ size: r.size,
60
+ sources: [],
61
+ _content: r.content // interne, retiré à la fin si includeContent=false
62
+ });
63
+ }
64
+ groupsMap.get(r.hash).sources.push(r.origin);
65
+ }
66
+
67
+ const groups = Array.from(groupsMap.values())
68
+ // le groupe le plus répandu en premier (majorité = référence probable)
69
+ .sort((a, b) => b.sources.length - a.sources.length);
70
+
71
+ const drift = groups.length > 1;
72
+
73
+ // Marque le groupe de référence (majoritaire)
74
+ groups.forEach((g, i) => { g.isReference = i === 0; });
75
+ const reference = groups.length > 0
76
+ ? { label: groups[0].sources[0].label, sources: groups[0].sources }
77
+ : null;
78
+
79
+ // Génère les diffs référence → chaque groupe divergent
80
+ const drifts = [];
81
+ if (drift && includeDiff) {
82
+ const ref = groups[0];
83
+ const refLabel = ref.sources[0].label;
84
+ for (let i = 1; i < groups.length; i++) {
85
+ const g = groups[i];
86
+ const gLabel = g.sources[0].label;
87
+ try {
88
+ const patch = createTwoFilesPatch(
89
+ refLabel, gLabel,
90
+ ref._content, g._content,
91
+ 'référence', 'divergent'
92
+ );
93
+ let added = 0, removed = 0;
94
+ for (const line of patch.split('\n')) {
95
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
96
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++;
97
+ }
98
+ drifts.push({
99
+ from: refLabel,
100
+ to: gLabel,
101
+ affectedSources: g.sources.map(s => s.label),
102
+ added,
103
+ removed,
104
+ diff: patch
105
+ });
106
+ } catch (e) {
107
+ drifts.push({ from: refLabel, to: gLabel, note: 'diff indisponible (binaire ?)' });
108
+ }
109
+ }
110
+ }
111
+
112
+ // Nettoie le contenu interne (ou l'expose si demandé)
113
+ for (const g of groups) {
114
+ if (includeContent) g.content = g._content;
115
+ delete g._content;
116
+ }
117
+
118
+ return {
119
+ drift,
120
+ reference,
121
+ groups,
122
+ drifts,
123
+ errors,
124
+ stats: {
125
+ totalSources: sources.length,
126
+ successfulReads: results.length,
127
+ uniqueVersions: groups.length,
128
+ unreadable: errors.length
129
+ }
130
+ };
131
+ }
132
+ };
package/config.js CHANGED
@@ -21,13 +21,49 @@ const args = process.argv.slice(2).reduce((acc, arg) => {
21
21
  }, {});
22
22
 
23
23
  const config = {
24
- // La hiérarchie reste la même : les arguments en ligne de commande écrasent le .env, qui écrase le défaut.
24
+ version: '11.8.0',
25
25
  dataDir: args['data-dir'] || process.env.MCP_DATA_DIR || path.join(os.homedir(), '.config', 'mcp-orchestrator'),
26
- syncTimeout: parseInt(args['sync-timeout'] || process.env.MCP_SYNC_TIMEOUT_S || '10', 10) * 1000,
27
- defaultCommandTimeout: parseInt(process.env.MCP_DEFAULT_CMD_TIMEOUT_MS || '300000', 10),
28
- interactiveCommandTimeout: parseInt(process.env.MCP_INTERACTIVE_CMD_TIMEOUT_MS || '120000', 10)
26
+
27
+ // Timeouts en millisecondes (plus cohérent)
28
+ syncTimeout: parseInt(args['sync-timeout'] || process.env.MCP_SYNC_TIMEOUT_S || '120', 10) * 1000,
29
+ defaultCommandTimeout: parseInt(process.env.MCP_DEFAULT_CMD_TIMEOUT_S || '600', 10) * 1000,
30
+ interactiveCommandTimeout: parseInt(process.env.MCP_INTERACTIVE_CMD_TIMEOUT_S || '300', 10) * 1000,
31
+ maxWaitTimeout: parseInt(process.env.MCP_MAX_WAIT_TIMEOUT_S || '600', 10) * 1000,
32
+
33
+ // Configuration Pool SSH
34
+ maxConnectionsPerServer: parseInt(process.env.MAX_CONNECTIONS_PER_SERVER || '5', 10),
35
+ minConnectionsPerServer: parseInt(process.env.MIN_CONNECTIONS_PER_SERVER || '1', 10),
36
+ idleTimeout: parseInt(process.env.IDLE_TIMEOUT || '300000', 10),
37
+ keepAliveInterval: parseInt(process.env.KEEP_ALIVE_INTERVAL || '30000', 10),
38
+
39
+ // Sécurité
40
+ allowedRoots: (process.env.MCP_ALLOWED_ROOTS || '').split(',').map(s => s.trim()).filter(Boolean),
41
+ // Mode lecture seule : refuse écritures / exec mutantes (exploration safe)
42
+ readOnly: process.env.MCP_READONLY === 'true' || process.env.MCP_READONLY === '1',
43
+ // Réponses compactes (moins de tokens agent)
44
+ compact: process.env.MCP_COMPACT === 'true' || process.env.MCP_COMPACT === '1',
45
+
46
+ // Configuration Queue
47
+ maxQueueSize: parseInt(process.env.MAX_QUEUE_SIZE || '1000', 10),
48
+ saveInterval: parseInt(process.env.SAVE_INTERVAL || '5000', 10),
49
+ historyRetention: parseInt(process.env.HISTORY_RETENTION || '2678400000', 10), // 31 jours
29
50
  };
30
51
 
52
+ // Validation
53
+ if (config.syncTimeout < 1000) {
54
+ if (process.env.MCP_DEBUG === 'true') {
55
+ console.error('⚠️ syncTimeout trop court, minimum 1000ms');
56
+ }
57
+ config.syncTimeout = 1000;
58
+ }
59
+
60
+ if (config.maxConnectionsPerServer < config.minConnectionsPerServer) {
61
+ if (process.env.MCP_DEBUG === 'true') {
62
+ console.error('⚠️ maxConnections < minConnections, ajustement automatique');
63
+ }
64
+ config.maxConnectionsPerServer = config.minConnectionsPerServer;
65
+ }
66
+
31
67
  // Créer le dossier de données s'il n'existe pas
32
68
  fs.mkdirSync(config.dataDir, { recursive: true });
33
69
 
package/diagnose.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.error("🔍 Diagnostic MCP Orchestrator\n");
4
+
5
+ // 1. Tester les imports
6
+ console.error("1️⃣ Test des imports...");
7
+ try {
8
+ await import("@modelcontextprotocol/sdk/server/mcp.js");
9
+ console.error(" ✅ @modelcontextprotocol/sdk");
10
+ } catch (e) {
11
+ console.error(" ❌ @modelcontextprotocol/sdk:", e.message);
12
+ process.exit(1);
13
+ }
14
+
15
+ try {
16
+ await import("zod");
17
+ console.error(" ✅ zod");
18
+ } catch (e) {
19
+ console.error(" ❌ zod:", e.message);
20
+ process.exit(1);
21
+ }
22
+
23
+ // 2. Tester config
24
+ console.error("\n2️⃣ Test de config...");
25
+ try {
26
+ const config = await import("./config.js");
27
+ console.error(" ✅ config.dataDir:", config.default.dataDir);
28
+ console.error(" ✅ config.syncTimeout:", config.default.syncTimeout);
29
+ } catch (e) {
30
+ console.error(" ❌ config:", e.message);
31
+ process.exit(1);
32
+ }
33
+
34
+ // 3. Tester queue
35
+ console.error("\n3️⃣ Test de queue...");
36
+ try {
37
+ const queue = await import("./queue.js");
38
+ console.error(" ✅ queue importée");
39
+ const stats = queue.default.getStats();
40
+ console.error(" ✅ queue.getStats():", JSON.stringify(stats));
41
+ } catch (e) {
42
+ console.error(" ❌ queue:", e.message);
43
+ console.error(e.stack);
44
+ process.exit(1);
45
+ }
46
+
47
+ // 4. Tester servers
48
+ console.error("\n4️⃣ Test de servers...");
49
+ try {
50
+ const servers = await import("./servers.js");
51
+ const list = await servers.default.listServers();
52
+ console.error(" ✅ servers.listServers():", Object.keys(list).length, "serveurs");
53
+ } catch (e) {
54
+ console.error(" ❌ servers:", e.message);
55
+ process.exit(1);
56
+ }
57
+
58
+ // 5. Tester un simple serveur MCP
59
+ console.error("\n5️⃣ Test d'un serveur MCP minimal...");
60
+ try {
61
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
62
+ const { z } = await import("zod");
63
+
64
+ const server = new McpServer({
65
+ name: "test",
66
+ version: "1.0.0",
67
+ description: "Test"
68
+ });
69
+
70
+ server.registerTool(
71
+ "test",
72
+ {
73
+ title: "Test",
74
+ description: "Test",
75
+ inputSchema: z.object({})
76
+ },
77
+ async () => ({ content: [{ type: "text", text: "OK" }] })
78
+ );
79
+
80
+ console.error(" ✅ Serveur MCP minimal fonctionne");
81
+ } catch (e) {
82
+ console.error(" ❌ Serveur MCP:", e.message);
83
+ console.error(e.stack);
84
+ process.exit(1);
85
+ }
86
+
87
+ console.error("\n✅ TOUS LES TESTS PASSENT");
88
+ console.error("\n📋 Prochaine étape: vérifier server.js ligne par ligne");
89
+ process.exit(0);
package/diffEngine.js ADDED
@@ -0,0 +1,141 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import sourceAdapter from './sourceAdapter.js';
3
+ import fileOps from './fileOps.js';
4
+
5
+ /**
6
+ * diffEngine — Comparaison fichier à fichier et dossier à dossier.
7
+ * Fonctionne pour n'importe quelle combinaison local/remote via sourceAdapter.
8
+ * Chaque résultat indique clairement l'origine (serveur + chemin) des sources.
9
+ */
10
+
11
+ function countAddRemove(patch) {
12
+ let added = 0, removed = 0;
13
+ for (const line of patch.split('\n')) {
14
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
15
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++;
16
+ }
17
+ return { added, removed };
18
+ }
19
+
20
+ export default {
21
+ /**
22
+ * Compare deux fichiers. Retourne :
23
+ * { identical, source1, source2, hash1, hash2, diff?, added?, removed? }
24
+ * source1/source2 = { server, path, label } (origine claire).
25
+ */
26
+ async diffFiles(s1, s2) {
27
+ const f1 = await fileOps.readFile(s1);
28
+ const f2 = await fileOps.readFile(s2);
29
+
30
+ const origin1 = sourceAdapter.describe(s1);
31
+ const origin2 = sourceAdapter.describe(s2);
32
+
33
+ if (f1.hash === f2.hash) {
34
+ return {
35
+ identical: true,
36
+ source1: origin1,
37
+ source2: origin2,
38
+ hash: f1.hash,
39
+ stats: { size1: f1.size, size2: f2.size }
40
+ };
41
+ }
42
+
43
+ const patch = createTwoFilesPatch(
44
+ origin1.label, origin2.label,
45
+ f1.content, f2.content,
46
+ 'source1', 'source2'
47
+ );
48
+ const { added, removed } = countAddRemove(patch);
49
+
50
+ return {
51
+ identical: false,
52
+ source1: origin1,
53
+ source2: origin2,
54
+ hash1: f1.hash,
55
+ hash2: f2.hash,
56
+ added,
57
+ removed,
58
+ diff: patch,
59
+ stats: { size1: f1.size, size2: f2.size }
60
+ };
61
+ },
62
+
63
+ /**
64
+ * Compare deux dossiers (arborescences). Retourne :
65
+ * { source1, source2, only_in_source1[], only_in_source2[],
66
+ * identical[], modified[{path, hash1, hash2, added, removed, diff?}], stats }
67
+ * options = { recursive, compareContent, ignorePatterns, includeDiff }
68
+ * - compareContent (défaut true) : compare le contenu des fichiers communs par hash
69
+ * - includeDiff (défaut false) : inclut le diff unifié pour chaque fichier modifié
70
+ */
71
+ async diffFolders(s1, s2, options = {}) {
72
+ const recursive = options.recursive !== false;
73
+ const compareContent = options.compareContent !== false;
74
+ const includeDiff = options.includeDiff === true;
75
+ const ignorePatterns = options.ignorePatterns || [];
76
+
77
+ const origin1 = sourceAdapter.describe(s1);
78
+ const origin2 = sourceAdapter.describe(s2);
79
+
80
+ const list1 = await sourceAdapter.listFilesRecursive(s1, { recursive, ignorePatterns });
81
+ const list2 = await sourceAdapter.listFilesRecursive(s2, { recursive, ignorePatterns });
82
+
83
+ const set2 = new Set(list2);
84
+ const set1 = new Set(list1);
85
+
86
+ const only_in_source1 = list1.filter(p => !set2.has(p));
87
+ const only_in_source2 = list2.filter(p => !set1.has(p));
88
+ const common = list1.filter(p => set2.has(p));
89
+
90
+ const identical = [];
91
+ const modified = [];
92
+
93
+ if (compareContent && common.length > 0) {
94
+ // Lit les fichiers communs des deux côtés (batch par source pour perf remote)
95
+ const hashes1 = await sourceAdapter.readManyHashes(s1, s1.path, common);
96
+ const hashes2 = await sourceAdapter.readManyHashes(s2, s2.path, common);
97
+
98
+ for (const rel of common) {
99
+ const h1 = hashes1.get(rel);
100
+ const h2 = hashes2.get(rel);
101
+ if (h1.hash === h2.hash) {
102
+ identical.push(rel);
103
+ } else {
104
+ const entry = { path: rel, hash1: h1.hash, hash2: h2.hash };
105
+ // diff textuel (best-effort ; ignore si binaire)
106
+ try {
107
+ const patch = createTwoFilesPatch(
108
+ `${origin1.label}/${rel}`, `${origin2.label}/${rel}`,
109
+ h1.content.toString('utf8'), h2.content.toString('utf8'),
110
+ 'source1', 'source2'
111
+ );
112
+ const { added, removed } = countAddRemove(patch);
113
+ entry.added = added;
114
+ entry.removed = removed;
115
+ if (includeDiff) entry.diff = patch;
116
+ } catch (e) {
117
+ entry.note = 'diff indisponible (binaire ?)';
118
+ }
119
+ modified.push(entry);
120
+ }
121
+ }
122
+ }
123
+
124
+ return {
125
+ source1: origin1,
126
+ source2: origin2,
127
+ only_in_source1,
128
+ only_in_source2,
129
+ identical,
130
+ modified,
131
+ stats: {
132
+ total1: list1.length,
133
+ total2: list2.length,
134
+ onlyIn1: only_in_source1.length,
135
+ onlyIn2: only_in_source2.length,
136
+ identical: identical.length,
137
+ modified: modified.length
138
+ }
139
+ };
140
+ }
141
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * diffFormatter — Rendu lisible des diffs pour les clients MCP.
3
+ *
4
+ * Les outils renvoient des métadonnées JSON (hash, applied, added/removed...)
5
+ * dont l'IA a besoin de façon fiable. Mais un diff unifié brut dans un champ
6
+ * JSON est illisible (\n échappés). Ce module produit :
7
+ * - un bloc markdown ```diff (coloré dans les clients, "comme les vrais tools")
8
+ * - un résumé concis (+X / -Y lignes)
9
+ *
10
+ * Convention de sortie des outils : content = [ blocMarkdown, blocJSON ].
11
+ */
12
+
13
+ // Nettoie un patch unifié (createTwoFilesPatch) : retire le bruit d'en-tête
14
+ // (Index:/===) tout en gardant ---/+++/@@ et le corps.
15
+ function cleanPatch(patch) {
16
+ return patch
17
+ .split('\n')
18
+ .filter(line => !line.startsWith('Index: ') && !/^=+$/.test(line))
19
+ .join('\n')
20
+ .trim();
21
+ }
22
+
23
+ // Compte les lignes ajoutées/supprimées (hors en-têtes +++/---)
24
+ function countAddRemove(patch) {
25
+ let added = 0, removed = 0;
26
+ for (const line of patch.split('\n')) {
27
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
28
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++;
29
+ }
30
+ return { added, removed };
31
+ }
32
+
33
+ // Barre visuelle proportionnelle (ex: +++++-- )
34
+ function bar(added, removed, width = 20) {
35
+ const total = added + removed;
36
+ if (total === 0) return '';
37
+ const plus = Math.round((added / total) * width);
38
+ const minus = width - plus;
39
+ return '+'.repeat(plus) + '-'.repeat(minus);
40
+ }
41
+
42
+ export default {
43
+ countAddRemove,
44
+
45
+ /**
46
+ * Formate un patch unifié en markdown.
47
+ * opts = { title }
48
+ * Retourne { markdown, summary, added, removed }.
49
+ */
50
+ format(patch, opts = {}) {
51
+ const cleaned = cleanPatch(patch);
52
+ const { added, removed } = countAddRemove(patch);
53
+ const summary = `+${added} / -${removed} ligne${(added + removed) > 1 ? 's' : ''}`;
54
+
55
+ let markdown = '';
56
+ if (opts.title) markdown += `### ${opts.title}\n`;
57
+ markdown += `**${summary}** \`${bar(added, removed)}\`\n\n`;
58
+ markdown += '```diff\n' + cleaned + '\n```';
59
+
60
+ return { markdown, summary, added, removed };
61
+ },
62
+
63
+ /**
64
+ * Construit une réponse MCP à deux blocs : markdown lisible + JSON metadata.
65
+ * - markdown : string déjà formatée (via format())
66
+ * - meta : objet metadata (sera JSON.stringify)
67
+ * Le champ 'diff' brut est retiré du JSON (déjà rendu en markdown) pour
68
+ * éviter la duplication et économiser des tokens.
69
+ */
70
+ response(markdown, meta) {
71
+ const clean = { ...meta };
72
+ delete clean.diff; // le diff est dans le bloc markdown
73
+ return {
74
+ content: [
75
+ { type: "text", text: markdown },
76
+ { type: "text", text: JSON.stringify(clean, null, 2) }
77
+ ]
78
+ };
79
+ },
80
+
81
+ /**
82
+ * Résumé d'un diff de dossiers/snapshots (listes de fichiers) en markdown.
83
+ * groups = { added:[], removed:[], modified:[{path, added, removed}], identical? }
84
+ */
85
+ formatFileList(groups, opts = {}) {
86
+ const lines = [];
87
+ if (opts.title) lines.push(`### ${opts.title}`);
88
+ const a = groups.added?.length || 0;
89
+ const r = groups.removed?.length || 0;
90
+ const m = groups.modified?.length || 0;
91
+ lines.push(`**${m} modifié(s), ${a} ajouté(s), ${r} supprimé(s)**\n`);
92
+
93
+ if (m > 0) {
94
+ lines.push('**Modifiés :**');
95
+ for (const f of groups.modified) {
96
+ const p = f.path || f;
97
+ const delta = (f.added !== undefined) ? ` (+${f.added}/-${f.removed})` : '';
98
+ lines.push(`- \`${p}\`${delta}`);
99
+ }
100
+ lines.push('');
101
+ }
102
+ if (a > 0) {
103
+ lines.push('**Ajoutés :**');
104
+ for (const p of groups.added) lines.push(`- \`${p}\``);
105
+ lines.push('');
106
+ }
107
+ if (r > 0) {
108
+ lines.push('**Supprimés :**');
109
+ for (const p of groups.removed) lines.push(`- \`${p}\``);
110
+ lines.push('');
111
+ }
112
+ return lines.join('\n').trim();
113
+ }
114
+ };