@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.3.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.
package/guide.js ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * guide — Manuel intégré pour l'IA pilote d'orchestrator.
3
+ *
4
+ * Retourne des sections structurées (workflows, cheatsheet, pièges) pour que
5
+ * l'agent charge les bonnes pratiques en début de session, sans avoir à
6
+ * connaître les ~44 outils par cœur. Inspiré de gencodedoc_guide.
7
+ */
8
+
9
+ const SECTIONS = {
10
+ index: `# Guide Orchestrator — Index
11
+
12
+ Serveur MCP d'orchestration d'infrastructure (SSH/SFTP, fichiers, diff, sessions, snapshots).
13
+
14
+ ## Catégories d'outils
15
+ - **Serveurs** : server_add/list/remove, server_note_* (contexte du parc), infra_overview
16
+ - **Exécution** : task_exec (one-shot), task_exec_sequence, shell_* (sessions persistantes)
17
+ - **Fichiers** : file_read, file_write, file_edit (local + remote unifiés)
18
+ - **Comparaison** : diff_files, diff_folders, compare_all_sources
19
+ - **Versioning** : snapshot_create/list/diff/restore/delete
20
+ - **Monitoring** : get_system_resources, get_services_status, get_pm2_logs, get_docker_logs, tail_file
21
+ - **Transferts** : task_transfer, task_transfer_multi
22
+
23
+ ## Sections détaillées (guide section:<nom>)
24
+ - workflows : recettes copier-coller
25
+ - file-editing : éditer un fichier en sécurité
26
+ - sources : concept local vs remote
27
+ - cheatsheet : tableau outil → usage
28
+ - audit : audit complet du parc (recette copier-coller)
29
+ - security : politique de sécurité (blocklist + tunnels)
30
+ - pitfalls : pièges à éviter
31
+
32
+ Astuce : appelle 'infra_overview' en début de session pour situer le parc.`,
33
+
34
+ sources: `# Concept clé : les "sources" (local vs remote)
35
+
36
+ Tous les outils fichiers/diff/snapshot acceptent une source unifiée :
37
+
38
+ { type: 'local', path: '/chemin' } → PC hôte du MCP, SANS SSH
39
+ { type: 'remote', alias: 'vpsparfait', path: '/chemin' } → serveur distant via SFTP (pool SSH)
40
+
41
+ - 'local' = agit sur la machine où tourne le MCP (fs direct, rapide)
42
+ - 'remote' = agit sur un serveur configuré (server_list pour les alias)
43
+ - label optionnel : nom lisible pour un affichage clair quand les chemins varient
44
+
45
+ Puissance : on peut mixer (diff local↔remote, snapshot remote→restore local).`,
46
+
47
+ 'file-editing': `# Éditer un fichier en sécurité
48
+
49
+ ## Workflow recommandé
50
+ 1. file_read { source } → récupère 'content' + 'hash'
51
+ 2. file_edit { source, oldString, newString, expectedHash: <hash> }
52
+ - mode chirurgical : remplace un bout précis (économe en tokens)
53
+ - expectedHash : refuse l'édition si le fichier a changé entre-temps (HASH_MISMATCH)
54
+
55
+ ## Options utiles
56
+ - dryRun:true → prévisualise le diff SANS écrire (valider avant d'appliquer)
57
+ - backup:true → snapshot auto avant modif → undo via snapshot_restore
58
+ - replaceAll:true → remplace toutes les occurrences (sinon erreur si multiples)
59
+
60
+ ## Mode complet
61
+ file_edit { source, newContent: "<tout le fichier>" } → remplace tout.
62
+
63
+ ## Erreurs
64
+ - OLDSTRING_NOT_FOUND : le texte exact n'existe pas (vérifie espaces/indentation)
65
+ - MULTIPLE_MATCHES : oldString présent N fois → ajoute du contexte ou replaceAll
66
+ - HASH_MISMATCH : fichier modifié depuis la lecture → relire, réanalyser`,
67
+
68
+ workflows: `# Recettes
69
+
70
+ ## Éditer une config distante en sécurité
71
+ file_read {source:{type:'remote',alias:'vpsparfait',path:'/etc/nginx/nginx.conf'}}
72
+ → file_edit {source, oldString:'...', newString:'...', expectedHash, backup:true}
73
+
74
+ ## Détecter un drift de config sur le parc
75
+ compare_all_sources { sources:[
76
+ {type:'local', path:'/ref.conf', label:'ref'},
77
+ {type:'remote', alias:'vps1', path:'/etc/x.conf', label:'vps1'},
78
+ {type:'remote', alias:'vps2', path:'/etc/x.conf', label:'vps2'}
79
+ ]}
80
+ → drift:true + diff référence→divergences
81
+
82
+ ## Workflow multi-étapes avec état (build)
83
+ shell_create {alias:'vps1', workdir:'/app'} → sessionId
84
+ shell_exec {sessionId, command:'npm install'} # cd/env persistent
85
+ shell_exec {sessionId, command:'npm run build'}
86
+ shell_close {sessionId}
87
+
88
+ ## Sécuriser avant une modif risquée
89
+ snapshot_create {source:{type:'remote',alias:'vps1'}, paths:['/etc/nginx/'], tag:'before-fix'}
90
+ ... modifs ...
91
+ snapshot_restore {snapshotId, target:{type:'remote',alias:'vps1'}, dryRun:true} # preview
92
+ snapshot_restore {snapshotId, target:{type:'remote',alias:'vps1'}, dryRun:false, force:true}`,
93
+
94
+ cheatsheet: `# Cheatsheet
95
+
96
+ | Besoin | Outil |
97
+ |--------|-------|
98
+ | Lire un fichier | file_read |
99
+ | Modifier un bout précis | file_edit (oldString/newString) |
100
+ | Créer/écraser | file_write |
101
+ | Comparer 2 fichiers | diff_files |
102
+ | Comparer 2 dossiers | diff_folders |
103
+ | Vérifier cohérence parc | compare_all_sources |
104
+ | Commande one-shot | task_exec |
105
+ | Suite de commandes avec état | shell_create + shell_exec |
106
+ | Sauvegarder avant modif | snapshot_create / backup:true |
107
+ | Annuler une modif | snapshot_restore |
108
+ | Contexte d'un serveur | server_note_get |
109
+ | Vue du parc | infra_overview |
110
+ | CPU/RAM/Disque | get_system_resources |
111
+ | Logs PM2/Docker | get_pm2_logs / get_docker_logs |`,
112
+
113
+ audit: `# Audit complet du parc — recette copier-coller
114
+
115
+ ## 1. Vue d'ensemble
116
+ \`\`\`
117
+ infra_overview → liste des serveurs + rôles + services + warnings
118
+ \`\`\`
119
+
120
+ ## 2. Ressources de chaque serveur
121
+ \`\`\`
122
+ get_system_resources {alias:'vps_contabo'}
123
+ get_system_resources {alias:'vpsparfait_fkomp'}
124
+ get_system_resources {alias:'vpsparfait2_fkomp'}
125
+ get_system_resources {alias:'fkomprodmini1_prod'}
126
+ get_system_resources {alias:'fkomprodmini2_prod'}
127
+ get_system_resources {alias:'pc2'}
128
+ \`\`\`
129
+
130
+ ## 3. Services (systemd, Docker, PM2) par serveur
131
+ \`\`\`
132
+ get_services_status {alias:'vps_contabo'}
133
+ # ... idem pour chaque alias
134
+ \`\`\`
135
+
136
+ ## 4. Scan Nginx (domaines et proxy_pass)
137
+ \`\`\`
138
+ task_exec {alias:'<alias>', cmd: "for f in /etc/nginx/sites-enabled/*; do echo \"--- \$f ---\"; cat \"\$f\" 2>/dev/null | grep -E \"server_name|listen|proxy_pass|return \" | head -20; done"}
139
+ \`\`\`
140
+
141
+ ## 5. Ports ouverts
142
+ \`\`\`
143
+ task_exec {alias:'<alias>', cmd: "ss -tlnp 2>/dev/null | sort -n -k4"}
144
+ \`\`\`
145
+
146
+ ## 6. Contexte enregistré
147
+ \`\`\`
148
+ server_note_get {alias:'<alias>'} → avant d'intervenir sur un serveur
149
+ \`\`\`
150
+
151
+ ## 7. Notes de tout le parc
152
+ \`\`\`
153
+ server_note_list
154
+ \`\`\`
155
+
156
+ ## 8. Après une modification risquée, vérifier les drifts
157
+ \`\`\`
158
+ compare_all_sources {sources: [
159
+ {type:'remote', alias:'vps1', path:'/etc/nginx/nginx.conf', label:'vps1'},
160
+ {type:'remote', alias:'vps2', path:'/etc/nginx/nginx.conf', label:'vps2'}
161
+ ]}
162
+ \`\`\`
163
+
164
+ Astuce: lancer les appels des étapes 2-5 en parallèle pour un audit complet en ~30s.`,
165
+
166
+ security: `# Politique de sécurité
167
+
168
+ ## Command Blocklist
169
+ Les commandes dangereuses sont bloquées par défaut avant exécution SSH.
170
+
171
+ ## Gérer la blocklist
172
+ \`\`\`
173
+ policy_blocklist_list → lister les patterns
174
+ policy_blocklist_add {pattern:"rm -rf /"} → ajouter un pattern
175
+ policy_blocklist_remove {pattern:"rm -rf /"} → retirer un pattern
176
+ \`\`\`
177
+
178
+ ## Contournement conscient
179
+ \`\`\`
180
+ task_exec {alias:"vps", cmd:"...", skip_policy:true}
181
+ \`\`\`
182
+
183
+ ## Patterns supportés
184
+ - Littéral : "rm -rf /" bloque exactement cette commande
185
+ - Wildcard : "wget * | sh" bloque tout wget pipé vers sh
186
+ - Les patterns sont testés en regex insensible à la casse
187
+
188
+ La blocklist est stockée dans data/policies.json et persiste entre les redémarrages.
189
+
190
+ ## Tunnels SSH
191
+ \`\`\`
192
+ tunnel_create {name:"crm", type:"local", listen_port:8080, target:"127.0.0.1:3100", via:"vps_contabo"}
193
+ → http://localhost:8080 → vps_contabo:3100
194
+
195
+ tunnel_create {name:"proxy", type:"socks", listen_port:1080, via:"vpsparfait"}
196
+ → Proxy SOCKS5 sur localhost:1080 via vpsparfait
197
+
198
+ tunnel_create {name:"reverse", type:"remote", listen_port:9090, target:"127.0.0.1:3000", via:"vps_contabo", source:"fkomprodmini2"}
199
+ → fkomprodmini2:9090 → localhost:3000 (reverse)
200
+ \`\`\`
201
+
202
+ ## Allowlist de ports
203
+ Seuls les ports autorisés peuvent être utilisés pour les tunnels.
204
+ \`\`\`
205
+ tunnel_allowlist_add {port:3306} → ouvrir un port
206
+ tunnel_allowlist_remove {port:3306} → le fermer
207
+ tunnel_list → voir les ports autorisés
208
+ \`\`\`
209
+
210
+ ## Sécurité des tunnels
211
+ - Ports < 1024 bloqués (root requis)
212
+ - Allowlist obligatoire : un port doit être explicitement autorisé
213
+ - Les tunnels via tmux survivent au redémarrage du MCP
214
+ - Les tunnels locaux (source=null) meurent avec le MCP et sont auto-restaurés au redémarrage
215
+ - Warning émis à la création`,
216
+
217
+ pitfalls: `# Pièges à éviter
218
+
219
+ 1. **Toujours dryRun avant snapshot_restore réel** (écrase des fichiers).
220
+ 2. **expectedHash sur file_edit** : sans lui, pas de protection contre écrasement concurrent.
221
+ 3. **type:'local' agit sur l'hôte du MCP**, pas sur un serveur — vérifie bien le type.
222
+ 4. **Gros fichiers** : file_read avec offset/limit pour consulter sans tout charger.
223
+ 5. **Binaires** : détection auto (base64), mais file_edit ne s'applique qu'au texte.
224
+ 6. **shell_exec** : une commande à la fois par session ; ferme les sessions (shell_close).
225
+ 7. **task_exec ≠ shell_exec** : task_exec isole chaque commande (cd perdu), shell_exec persiste.
226
+ 8. **Consulte server_note_get avant d'intervenir** sur un serveur inconnu.`
227
+ };
228
+
229
+ export default {
230
+ get(section = 'index') {
231
+ return SECTIONS[section] || `Section '${section}' inconnue. Sections : ${Object.keys(SECTIONS).join(', ')}.`;
232
+ },
233
+ sections() {
234
+ return Object.keys(SECTIONS);
235
+ }
236
+ };
package/history.js CHANGED
@@ -14,7 +14,14 @@ async function readHistory() {
14
14
  }
15
15
 
16
16
  async function writeHistory(history) {
17
- await fs.writeFile(HISTORY_FILE_PATH, JSON.stringify(history, null, 2));
17
+ try {
18
+ await fs.writeFile(HISTORY_FILE_PATH, JSON.stringify(history, null, 2));
19
+ } catch (error) {
20
+ // Silently fail or log if possible, but don't crash
21
+ if (process.env.MCP_DEBUG === 'true') {
22
+ console.error(`[ERROR] Failed to write history: ${error.message}`);
23
+ }
24
+ }
18
25
  }
19
26
 
20
27
  async function logTask(jobDetails) {
package/notes.js ADDED
@@ -0,0 +1,87 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import config from './config.js';
4
+
5
+ /**
6
+ * notes — Mémoire contextuelle par serveur (protocole/particularités).
7
+ *
8
+ * Permet à l'IA pilote de ne pas se perdre dans un parc de nombreux serveurs :
9
+ * chaque serveur peut porter une description, la liste de ses services critiques,
10
+ * des avertissements (ex: "RAM 1Go, ne pas rebuild en prod"), des conventions
11
+ * (chemins standards), et la trace de la dernière intervention.
12
+ *
13
+ * Stockage : <dataDir>/server_notes.json (même pattern que servers.json).
14
+ */
15
+
16
+ const NOTES_FILE = path.join(config.dataDir, 'server_notes.json');
17
+
18
+ async function readAll() {
19
+ try {
20
+ const data = await fs.readFile(NOTES_FILE, 'utf-8');
21
+ return JSON.parse(data);
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ async function writeAll(notes) {
28
+ await fs.mkdir(path.dirname(NOTES_FILE), { recursive: true });
29
+ await fs.writeFile(NOTES_FILE, JSON.stringify(notes, null, 2));
30
+ }
31
+
32
+ export default {
33
+ /**
34
+ * Définit/met à jour la note d'un serveur (merge partiel : seuls les champs
35
+ * fournis sont modifiés). Champs libres :
36
+ * description, services[], warnings[], conventions[], note (texte libre)
37
+ * `last_intervention` est mis à jour automatiquement (timestamp + résumé).
38
+ */
39
+ async set(alias, fields = {}) {
40
+ const notes = await readAll();
41
+ const existing = notes[alias] || {};
42
+ const merged = { ...existing };
43
+
44
+ // Champs structurés (remplacés si fournis)
45
+ for (const key of ['description', 'services', 'warnings', 'conventions', 'note']) {
46
+ if (fields[key] !== undefined) merged[key] = fields[key];
47
+ }
48
+ // Trace d'intervention
49
+ if (fields.intervention) {
50
+ merged.last_intervention = {
51
+ at: new Date().toISOString(),
52
+ summary: fields.intervention
53
+ };
54
+ }
55
+ merged.updatedAt = new Date().toISOString();
56
+
57
+ notes[alias] = merged;
58
+ await writeAll(notes);
59
+ return { alias, note: merged };
60
+ },
61
+
62
+ /**
63
+ * Récupère la note d'un serveur. Retourne null si absente.
64
+ */
65
+ async get(alias) {
66
+ const notes = await readAll();
67
+ return notes[alias] || null;
68
+ },
69
+
70
+ /**
71
+ * Liste toutes les notes (vue d'ensemble du parc).
72
+ */
73
+ async list() {
74
+ return await readAll();
75
+ },
76
+
77
+ /**
78
+ * Supprime la note d'un serveur.
79
+ */
80
+ async remove(alias) {
81
+ const notes = await readAll();
82
+ if (!notes[alias]) return { removed: false, alias };
83
+ delete notes[alias];
84
+ await writeAll(notes);
85
+ return { removed: true, alias };
86
+ }
87
+ };
package/package.json CHANGED
@@ -1,32 +1,15 @@
1
1
  {
2
2
  "name": "@fkom13/mcp-sftp-orchestrator",
3
- "version": "6.0.0",
3
+ "version": "11.3.0",
4
4
  "description": "Un serveur MCP stdio pour l'orchestration de tâches distantes avec exécution hybride (synchrone/asynchrone) et configuration flexible.",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
- "bin": {
8
- "mcp-sftp-orchestrator": "server.js"
9
- },
7
+ "mcpName": "io.github.fkom13/mcp-sftp-orchestrator",
10
8
  "scripts": {
11
- "test": "node test_features.js"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/fkom13/mcp-sftp-orchestrator.git"
16
- },
17
- "keywords": [
18
- "mcp",
19
- "orchestrator",
20
- "ssh",
21
- "sftp",
22
- "automation"
23
- ],
24
- "author": "fkom13",
25
- "license": "MIT",
26
- "bugs": {
27
- "url": "https://github.com/fkom13/mcp-sftp-orchestrator/issues"
9
+ "start": "node server.js",
10
+ "test": "node test_mcp.js && node test_features.js",
11
+ "test:integration": "node test_features.js"
28
12
  },
29
- "homepage": "https://github.com/fkom13/mcp-sftp-orchestrator#readme",
30
13
  "dependencies": {
31
14
  "@modelcontextprotocol/sdk": "^1.17.0",
32
15
  "dotenv": "^16.4.5",
package/policies.js ADDED
@@ -0,0 +1,76 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import config from './config.js';
4
+
5
+ const POLICIES_PATH = path.join(config.dataDir, 'policies.json');
6
+
7
+ const DEFAULT_POLICIES = {
8
+ command_blocklist: [
9
+ "rm -rf /",
10
+ "rm -rf /*",
11
+ "mkfs*",
12
+ ":(){:|:&};:",
13
+ "dd if=/dev/zero",
14
+ "> /dev/sda",
15
+ "chmod -R 000 /",
16
+ "mv / /dev/null",
17
+ "wget -O- http://* | sh",
18
+ "curl http://* | sh",
19
+ "eval $(curl",
20
+ "eval $(wget",
21
+ "git clone http://* | sh"
22
+ ]
23
+ };
24
+
25
+ async function loadPolicies() {
26
+ try {
27
+ await fs.access(POLICIES_PATH);
28
+ const data = await fs.readFile(POLICIES_PATH, 'utf-8');
29
+ return JSON.parse(data);
30
+ } catch {
31
+ await fs.writeFile(POLICIES_PATH, JSON.stringify(DEFAULT_POLICIES, null, 2));
32
+ return { ...DEFAULT_POLICIES };
33
+ }
34
+ }
35
+
36
+ async function savePolicies(policies) {
37
+ await fs.writeFile(POLICIES_PATH, JSON.stringify(policies, null, 2));
38
+ }
39
+
40
+ function checkCommand(cmd, blocklist) {
41
+ const cmdTrimmed = cmd.trim();
42
+ for (const pattern of blocklist) {
43
+ const regex = pattern.includes('*')
44
+ ? new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i')
45
+ : new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
46
+ if (regex.test(cmdTrimmed)) {
47
+ return { blocked: true, pattern };
48
+ }
49
+ }
50
+ return { blocked: false };
51
+ }
52
+
53
+ export default {
54
+ async list() {
55
+ return await loadPolicies();
56
+ },
57
+ async add(pattern) {
58
+ const policies = await loadPolicies();
59
+ if (!policies.command_blocklist.includes(pattern)) {
60
+ policies.command_blocklist.push(pattern);
61
+ await savePolicies(policies);
62
+ }
63
+ return policies;
64
+ },
65
+ async remove(pattern) {
66
+ const policies = await loadPolicies();
67
+ policies.command_blocklist = policies.command_blocklist.filter(p => p !== pattern);
68
+ await savePolicies(policies);
69
+ return policies;
70
+ },
71
+ async check(cmd) {
72
+ const policies = await loadPolicies();
73
+ return checkCommand(cmd, policies.command_blocklist);
74
+ },
75
+ checkCommand
76
+ };