@fkom13/mcp-sftp-orchestrator 6.0.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/.env.example +22 -0
- package/README.md +102 -0
- package/apis.js +88 -0
- package/config.js +34 -0
- package/history.js +44 -0
- package/package.json +40 -0
- package/publish1github.md +216 -0
- package/queue.js +377 -0
- package/server.js +773 -0
- package/servers.js +58 -0
- package/sftp.js +248 -0
- package/ssh.js +492 -0
- package/sshPool.js +272 -0
- package/test_features.js +112 -0
package/server.js
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import queue from './queue.js';
|
|
6
|
+
import servers from './servers.js';
|
|
7
|
+
import sftp from './sftp.js';
|
|
8
|
+
import ssh from './ssh.js';
|
|
9
|
+
import history from './history.js';
|
|
10
|
+
import config from './config.js';
|
|
11
|
+
import apis from './apis.js';
|
|
12
|
+
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "orchestrator",
|
|
15
|
+
version: "8.0.0", // Version avec gestion du streaming
|
|
16
|
+
description: "Serveur pour l'orchestration de tâches distantes avec exécution hybride et configuration flexible."
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// --- OUTILS DE GESTION DES SERVEURS ---
|
|
20
|
+
server.registerTool(
|
|
21
|
+
"server_add",
|
|
22
|
+
{
|
|
23
|
+
title: "Ajouter/Modifier un alias de serveur",
|
|
24
|
+
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.",
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
alias: z.string().describe("Nom court et unique pour le serveur (ex: vps_production)"),
|
|
27
|
+
host: z.string().describe("Adresse IP ou nom d'hôte du serveur"),
|
|
28
|
+
user: z.string().describe("Nom d'utilisateur pour la connexion"),
|
|
29
|
+
keyPath: z.string().optional().describe("Chemin absolu vers la clé privée SSH."),
|
|
30
|
+
password: z.string().optional().describe("Mot de passe pour la connexion.")
|
|
31
|
+
}).refine(data => data.keyPath || data.password, {
|
|
32
|
+
message: "Vous devez fournir au moins une méthode d'authentification ('keyPath' ou 'password')."
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
async (params) => {
|
|
36
|
+
try {
|
|
37
|
+
const { alias, ...serverConfig } = params;
|
|
38
|
+
const result = await servers.addServer(alias, serverConfig);
|
|
39
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
const errorPayload = {
|
|
42
|
+
toolName: "server_add",
|
|
43
|
+
errorCode: "TOOL_EXECUTION_ERROR",
|
|
44
|
+
errorMessage: e.message
|
|
45
|
+
};
|
|
46
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
server.registerTool(
|
|
52
|
+
"server_list",
|
|
53
|
+
{
|
|
54
|
+
title: "Lister les alias de serveurs",
|
|
55
|
+
description: "Affiche la liste de tous les alias de serveurs configurés avec leurs détails.",
|
|
56
|
+
inputSchema: {}
|
|
57
|
+
},
|
|
58
|
+
async () => {
|
|
59
|
+
const serverList = await servers.listServers();
|
|
60
|
+
return { content: [{ type: "text", text: JSON.stringify(serverList, null, 2) }] };
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.registerTool(
|
|
65
|
+
"server_remove",
|
|
66
|
+
{
|
|
67
|
+
title: "Supprimer un alias de serveur",
|
|
68
|
+
description: "Supprime un alias de serveur de la configuration.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
alias: z.string().describe("Nom de l'alias à supprimer")
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async (params) => {
|
|
74
|
+
try {
|
|
75
|
+
const result = await servers.removeServer(params.alias);
|
|
76
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const errorPayload = {
|
|
79
|
+
toolName: "server_remove",
|
|
80
|
+
errorCode: "TOOL_EXECUTION_ERROR",
|
|
81
|
+
errorMessage: e.message
|
|
82
|
+
};
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// --- OUTILS DE GESTION D'API ---
|
|
89
|
+
server.registerTool(
|
|
90
|
+
"api_add",
|
|
91
|
+
{
|
|
92
|
+
title: "Ajouter une API au catalogue",
|
|
93
|
+
description: "Ajoute ou met à jour une API dans le catalogue de monitoring.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
alias: z.string().describe("Alias unique pour l'API."),
|
|
96
|
+
url: z.string().url().describe("URL de base de l'API, incluant le port si nécessaire."),
|
|
97
|
+
health_check_endpoint: z.string().optional().describe("Endpoint spécifique pour le test de santé (ex: /health)."),
|
|
98
|
+
health_check_method: z.enum(['GET', 'POST']).optional().default('GET').describe("Méthode HTTP pour le test de santé."),
|
|
99
|
+
auth_method: z.enum(['api_key', 'htpasswd', 'both', 'none']).optional().default('none').describe("Méthode d'authentification."),
|
|
100
|
+
api_key: z.string().optional().describe("Clé API si nécessaire."),
|
|
101
|
+
auth_header_name: z.string().optional().default('Authorization').describe("Nom du header pour la clé API."),
|
|
102
|
+
auth_scheme: z.string().optional().default('Bearer').describe("Schéma d'authentification (ex: Bearer). Mettre à '' si non applicable."),
|
|
103
|
+
htpasswd_user: z.string().optional().describe("Nom d'utilisateur pour l'authentification Basic (htpasswd)."),
|
|
104
|
+
htpasswd_pass: z.string().optional().describe("Mot de passe pour l'authentification Basic (htpasswd)."),
|
|
105
|
+
notes: z.string().optional().describe("Notes additionnelles.")
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async (params) => {
|
|
109
|
+
try {
|
|
110
|
+
const { alias, ...apiConfig } = params;
|
|
111
|
+
const result = await apis.addApi(alias, apiConfig);
|
|
112
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
server.registerTool(
|
|
120
|
+
"api_list",
|
|
121
|
+
{
|
|
122
|
+
title: "Lister les APIs du catalogue",
|
|
123
|
+
description: "Affiche toutes les APIs configurées dans le catalogue.",
|
|
124
|
+
inputSchema: {}
|
|
125
|
+
},
|
|
126
|
+
async () => {
|
|
127
|
+
const apiList = await apis.listApis();
|
|
128
|
+
return { content: [{ type: "text", text: JSON.stringify(apiList, null, 2) }] };
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.registerTool(
|
|
133
|
+
"api_remove",
|
|
134
|
+
{
|
|
135
|
+
title: "Supprimer une API du catalogue",
|
|
136
|
+
description: "Supprime une API du catalogue en utilisant son alias.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
alias: z.string().describe("Alias de l'API à supprimer.")
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async (params) => {
|
|
142
|
+
try {
|
|
143
|
+
const result = await apis.removeApi(params.alias);
|
|
144
|
+
return { content: [{ type: "text", text: result.message }] };
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
server.registerTool(
|
|
152
|
+
"api_check",
|
|
153
|
+
{
|
|
154
|
+
title: "Vérifier la santé d'une API via son alias",
|
|
155
|
+
description: "Lance un test de santé sur une API du catalogue.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
alias: z.string().describe("Alias de l'API à tester."),
|
|
158
|
+
server_alias: z.string().describe("Alias du serveur depuis lequel lancer le test.")
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async (params) => {
|
|
162
|
+
try {
|
|
163
|
+
const apiConfig = await apis.getApi(params.alias);
|
|
164
|
+
const endpoint = apiConfig.health_check_endpoint || '';
|
|
165
|
+
const url = `${apiConfig.url}${endpoint}`;
|
|
166
|
+
const method = apiConfig.health_check_method || 'GET';
|
|
167
|
+
|
|
168
|
+
let curlCmd = `curl -X ${method} -o /dev/null -s -w '%{http_code}:%{time_total}'`;
|
|
169
|
+
|
|
170
|
+
// Gérer l'authentification htpasswd
|
|
171
|
+
if ((apiConfig.auth_method === 'htpasswd' || apiConfig.auth_method === 'both') && apiConfig.htpasswd_user && apiConfig.htpasswd_pass) {
|
|
172
|
+
curlCmd += ` -u ${apiConfig.htpasswd_user}:${apiConfig.htpasswd_pass}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Gérer l'authentification par clé API
|
|
176
|
+
if ((apiConfig.auth_method === 'api_key' || apiConfig.auth_method === 'both') && apiConfig.api_key) {
|
|
177
|
+
const scheme = apiConfig.auth_scheme ? `${apiConfig.auth_scheme} ` : '';
|
|
178
|
+
curlCmd += ` -H '${apiConfig.auth_header_name || 'Authorization'}: ${scheme}${apiConfig.api_key}'`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
curlCmd += ` ${url}`;
|
|
182
|
+
|
|
183
|
+
const job = queue.addJob({ type: 'ssh', alias: params.server_alias, cmd: curlCmd });
|
|
184
|
+
ssh.executeCommand(job.id);
|
|
185
|
+
const result = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
186
|
+
|
|
187
|
+
if (!result || result.status !== 'completed') {
|
|
188
|
+
throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const parsedOutput = ssh.parseApiHealth(result.output);
|
|
192
|
+
return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] };
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return { content: [{ type: "text", text: `ERREUR: ${e.message}` }], isError: true };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// --- NOUVEAUX OUTILS DE MONITORING ---
|
|
200
|
+
server.registerTool(
|
|
201
|
+
"get_system_resources",
|
|
202
|
+
{
|
|
203
|
+
title: "Obtenir les ressources système d'un VPS",
|
|
204
|
+
description: "Récupère les métriques système vitales (CPU, RAM, Disque) d'un serveur.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
alias: z.string().describe("Alias du serveur cible.")
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async (params) => {
|
|
210
|
+
try {
|
|
211
|
+
const job = queue.addJob({
|
|
212
|
+
type: 'ssh',
|
|
213
|
+
alias: params.alias,
|
|
214
|
+
cmd: "uptime && free -h && df -h /"
|
|
215
|
+
});
|
|
216
|
+
ssh.executeCommand(job.id);
|
|
217
|
+
const result = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
218
|
+
|
|
219
|
+
if (!result || result.status !== 'completed') {
|
|
220
|
+
throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const parsedOutput = ssh.parseSystemResources(result.output);
|
|
224
|
+
return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] };
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const errorPayload = {
|
|
227
|
+
toolName: "get_system_resources",
|
|
228
|
+
errorCode: "MONITORING_ERROR",
|
|
229
|
+
errorMessage: e.message
|
|
230
|
+
};
|
|
231
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.registerTool(
|
|
237
|
+
"get_services_status",
|
|
238
|
+
{
|
|
239
|
+
title: "Obtenir le statut des services d'un VPS",
|
|
240
|
+
description: "Récupère le statut de tous les services connus (systemd, Docker, PM2) sur un serveur.",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
alias: z.string().describe("Alias du serveur cible.")
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async (params) => {
|
|
246
|
+
try {
|
|
247
|
+
const cmd = "systemctl --type=service --state=running --no-pager ; echo '---DOCKER---' ; docker ps --format '{{.Names}}: {{.Status}}' ; echo '---PM2---' ; pm2 list";
|
|
248
|
+
const job = queue.addJob({
|
|
249
|
+
type: 'ssh',
|
|
250
|
+
alias: params.alias,
|
|
251
|
+
cmd: cmd
|
|
252
|
+
});
|
|
253
|
+
ssh.executeCommand(job.id);
|
|
254
|
+
const result = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
255
|
+
|
|
256
|
+
if (!result || result.status !== 'completed') {
|
|
257
|
+
throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const parsedOutput = ssh.parseServicesStatus(result.output);
|
|
261
|
+
return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] };
|
|
262
|
+
} catch (e) {
|
|
263
|
+
const errorPayload = {
|
|
264
|
+
toolName: "get_services_status",
|
|
265
|
+
errorCode: "MONITORING_ERROR",
|
|
266
|
+
errorMessage: e.message
|
|
267
|
+
};
|
|
268
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
server.registerTool(
|
|
274
|
+
"check_api_health",
|
|
275
|
+
{
|
|
276
|
+
title: "Vérifier la santé d'une API",
|
|
277
|
+
description: "Vérifie la disponibilité et le temps de réponse d'un endpoint HTTP/S.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
alias: z.string().describe("Alias du serveur depuis lequel lancer le test."),
|
|
280
|
+
url: z.string().url().describe("URL complète de l'endpoint à tester.")
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
async (params) => {
|
|
284
|
+
try {
|
|
285
|
+
const cmd = `curl -o /dev/null -s -w '%{http_code}:%{time_total}' ${params.url}`;
|
|
286
|
+
const job = queue.addJob({
|
|
287
|
+
type: 'ssh',
|
|
288
|
+
alias: params.alias,
|
|
289
|
+
cmd: cmd
|
|
290
|
+
});
|
|
291
|
+
ssh.executeCommand(job.id);
|
|
292
|
+
const result = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
293
|
+
|
|
294
|
+
if (!result || result.status !== 'completed') {
|
|
295
|
+
throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const parsedOutput = ssh.parseApiHealth(result.output);
|
|
299
|
+
return { content: [{ type: "text", text: JSON.stringify(parsedOutput, null, 2) }] };
|
|
300
|
+
} catch (e) {
|
|
301
|
+
const errorPayload = {
|
|
302
|
+
toolName: "check_api_health",
|
|
303
|
+
errorCode: "MONITORING_ERROR",
|
|
304
|
+
errorMessage: e.message
|
|
305
|
+
};
|
|
306
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
server.registerTool(
|
|
312
|
+
"get_fail2ban_status",
|
|
313
|
+
{
|
|
314
|
+
title: "Obtenir le statut de Fail2Ban",
|
|
315
|
+
description: "Récupère les informations du service Fail2Ban, pour toutes les jails ou une jail spécifique.",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
318
|
+
jail: z.string().optional().describe("Nom d'une jail spécifique à inspecter (ex: sshd). Laissez vide pour un statut général.")
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
async (params) => {
|
|
322
|
+
try {
|
|
323
|
+
const cmd = `sudo fail2ban-client status ${params.jail || ''}`.trim();
|
|
324
|
+
const job = queue.addJob({
|
|
325
|
+
type: 'ssh',
|
|
326
|
+
alias: params.alias,
|
|
327
|
+
cmd: cmd
|
|
328
|
+
});
|
|
329
|
+
ssh.executeCommand(job.id);
|
|
330
|
+
const result = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
331
|
+
|
|
332
|
+
if (!result || result.status !== 'completed') {
|
|
333
|
+
throw new Error(result ? result.error : `Timeout de la commande de monitoring pour ${params.alias}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Pour cette version, nous retournons la sortie brute.
|
|
337
|
+
return { content: [{ type: "text", text: result.output }] };
|
|
338
|
+
} catch (e) {
|
|
339
|
+
const errorPayload = {
|
|
340
|
+
toolName: "get_fail2ban_status",
|
|
341
|
+
errorCode: "MONITORING_ERROR",
|
|
342
|
+
errorMessage: e.message
|
|
343
|
+
};
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
// --- LOGIQUE D'ATTENTE HYBRIDE ---
|
|
351
|
+
async function waitForJobCompletion(jobId, timeout) {
|
|
352
|
+
return new Promise((resolve) => {
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
const interval = setInterval(() => {
|
|
355
|
+
const job = queue.getJob(jobId);
|
|
356
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
357
|
+
clearInterval(interval);
|
|
358
|
+
resolve(job);
|
|
359
|
+
} else if (Date.now() - startTime > timeout) {
|
|
360
|
+
clearInterval(interval);
|
|
361
|
+
resolve(null);
|
|
362
|
+
}
|
|
363
|
+
}, 200);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- EXÉCUTION DE TÂCHES ---
|
|
368
|
+
server.registerTool(
|
|
369
|
+
"task_transfer",
|
|
370
|
+
{
|
|
371
|
+
title: "Transférer un fichier ou dossier (SFTP)",
|
|
372
|
+
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.`,
|
|
373
|
+
inputSchema: {
|
|
374
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
375
|
+
direction: z.enum(['upload', 'download']),
|
|
376
|
+
local: z.string().describe("Chemin absolu local."),
|
|
377
|
+
remote: z.string().describe("Chemin absolu distant."),
|
|
378
|
+
rappel: z.number().optional().describe("Définit un rappel en secondes.")
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
async (params) => {
|
|
382
|
+
const job = queue.addJob({ type: 'sftp', ...params, status: 'pending' });
|
|
383
|
+
history.logTask(job);
|
|
384
|
+
sftp.executeTransfer(job.id);
|
|
385
|
+
|
|
386
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
387
|
+
if (finalJob) {
|
|
388
|
+
return { content: [{ type: "text", text: `Résultat direct (tâche ${finalJob.id}):
|
|
389
|
+
${JSON.stringify(finalJob, null, 2)}` }] };
|
|
390
|
+
} else {
|
|
391
|
+
return { content: [{ type: "text", text: `Tâche de transfert ${job.id} initiée en arrière-plan.` }] };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
server.registerTool(
|
|
397
|
+
"task_exec",
|
|
398
|
+
{
|
|
399
|
+
title: "Exécuter une commande à distance (SSH)",
|
|
400
|
+
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.`,
|
|
401
|
+
inputSchema: {
|
|
402
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
403
|
+
cmd: z.string().describe("La commande complète à exécuter."),
|
|
404
|
+
rappel: z.number().optional().describe("Définit un rappel en secondes.")
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async (params) => {
|
|
408
|
+
const job = queue.addJob({ type: 'ssh', ...params, status: 'pending' });
|
|
409
|
+
history.logTask(job);
|
|
410
|
+
ssh.executeCommand(job.id);
|
|
411
|
+
|
|
412
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
413
|
+
if (finalJob) {
|
|
414
|
+
return { content: [{ type: "text", text: `Résultat direct (tâche ${finalJob.id}):
|
|
415
|
+
${finalJob.output || JSON.stringify(finalJob, null, 2)}` }] };
|
|
416
|
+
} else {
|
|
417
|
+
return { content: [{ type: "text", text: `Tâche d'exécution ${job.id} initiée en arrière-plan.` }] };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// --- OUTILS DE SUIVI ET HISTORIQUE ---
|
|
423
|
+
function formatJobForDisplay(job) {
|
|
424
|
+
const displayJob = { ...job };
|
|
425
|
+
if (job.status === 'running' && job.reminderAt && new Date() > new Date(job.reminderAt)) {
|
|
426
|
+
displayJob.reminder = "ATTENTION: Le temps de rappel est écoulé. La tâche est peut-être terminée ou bloquée.";
|
|
427
|
+
}
|
|
428
|
+
return displayJob;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
server.registerTool(
|
|
433
|
+
"task_queue",
|
|
434
|
+
{
|
|
435
|
+
title: "Voir la file d'attente des tâches",
|
|
436
|
+
description: "Affiche le statut de toutes les tâches, avec des rappels pour les tâches longues.",
|
|
437
|
+
inputSchema: {}
|
|
438
|
+
},
|
|
439
|
+
async () => {
|
|
440
|
+
const queueState = queue.getQueue();
|
|
441
|
+
const displayQueue = Object.values(queueState).map(formatJobForDisplay);
|
|
442
|
+
return { content: [{ type: "text", text: JSON.stringify(displayQueue, null, 2) }] };
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
server.registerTool(
|
|
447
|
+
"task_status",
|
|
448
|
+
{
|
|
449
|
+
title: "Consulter une tâche par son ID",
|
|
450
|
+
description: "Récupère les détails d'une seule tâche, avec un rappel si nécessaire.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
id: z.string().describe("L'ID de la tâche à consulter.")
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
async (params) => {
|
|
456
|
+
const job = queue.getJob(params.id);
|
|
457
|
+
if (!job) return { content: [{ type: "text", text: `ERREUR: Tâche '${params.id}' introuvable.` }], isError: true };
|
|
458
|
+
|
|
459
|
+
const displayJob = formatJobForDisplay(job);
|
|
460
|
+
return { content: [{ type: "text", text: JSON.stringify(displayJob, null, 2) }] };
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
server.registerTool(
|
|
465
|
+
"task_history",
|
|
466
|
+
{
|
|
467
|
+
title: "Consulter l'historique des tâches",
|
|
468
|
+
description: "Affiche les dernières tâches lancées. Peut être filtré par alias.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
alias: z.string().optional().describe("Filtre l'historique pour ne montrer que les tâches d'un alias spécifique.")
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
async (params) => {
|
|
474
|
+
const historyLogs = await history.getHistory(params);
|
|
475
|
+
return { content: [{ type: "text", text: JSON.stringify(historyLogs, null, 2) }] };
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// --- NOUVEAUX OUTILS POUR LES FONCTIONNALITÉS AVANCÉES ---
|
|
480
|
+
|
|
481
|
+
server.registerTool(
|
|
482
|
+
"task_transfer_multi",
|
|
483
|
+
{
|
|
484
|
+
title: "Transférer plusieurs fichiers/dossiers (SFTP)",
|
|
485
|
+
description: "Lance des transferts SFTP multiples avec support de patterns glob (*, ?, []).",
|
|
486
|
+
inputSchema: {
|
|
487
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
488
|
+
direction: z.enum(['upload', 'download']),
|
|
489
|
+
files: z.array(z.object({
|
|
490
|
+
local: z.string().describe("Chemin local ou pattern glob (ex: /home/*.txt)"),
|
|
491
|
+
remote: z.string().describe("Chemin distant")
|
|
492
|
+
})).describe("Liste des fichiers à transférer"),
|
|
493
|
+
rappel: z.number().optional().describe("Définit un rappel en secondes.")
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
async (params) => {
|
|
497
|
+
const job = queue.addJob({
|
|
498
|
+
type: 'sftp',
|
|
499
|
+
...params,
|
|
500
|
+
status: 'pending',
|
|
501
|
+
files: params.files
|
|
502
|
+
});
|
|
503
|
+
history.logTask(job);
|
|
504
|
+
sftp.executeMultiTransfer(job.id);
|
|
505
|
+
|
|
506
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
507
|
+
if (finalJob) {
|
|
508
|
+
return { content: [{ type: "text", text: `Résultat transferts multiples (tâche ${finalJob.id}):\n${JSON.stringify(finalJob, null, 2)}` }] };
|
|
509
|
+
} else {
|
|
510
|
+
return { content: [{ type: "text", text: `Tâche de transferts multiples ${job.id} initiée en arrière-plan.` }] };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
server.registerTool(
|
|
516
|
+
"task_exec_interactive",
|
|
517
|
+
{
|
|
518
|
+
title: "Exécuter une commande interactive (SSH)",
|
|
519
|
+
description: "Exécute une commande SSH avec gestion des prompts interactifs (yes/no, passwords, etc.).",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
522
|
+
cmd: z.string().describe("La commande à exécuter."),
|
|
523
|
+
interactive: z.boolean().optional().default(true).describe("Mode interactif."),
|
|
524
|
+
autoRespond: z.boolean().optional().default(true).describe("Répondre automatiquement aux prompts standards."),
|
|
525
|
+
responses: z.record(z.string()).optional().describe("Réponses personnalisées aux prompts (clé: pattern, valeur: réponse)."),
|
|
526
|
+
timeout: z.number().optional().describe("Timeout personnalisé en secondes. Défaut 2 minutes."),
|
|
527
|
+
rappel: z.number().optional().describe("Définit un rappel en secondes.")
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
async (params) => {
|
|
531
|
+
const job = queue.addJob({
|
|
532
|
+
type: 'ssh',
|
|
533
|
+
...params,
|
|
534
|
+
status: 'pending',
|
|
535
|
+
interactive: params.interactive,
|
|
536
|
+
autoRespond: params.autoRespond
|
|
537
|
+
});
|
|
538
|
+
history.logTask(job);
|
|
539
|
+
ssh.executeCommand(job.id);
|
|
540
|
+
|
|
541
|
+
const finalJob = await waitForJobCompletion(job.id, params.timeout || config.syncTimeout);
|
|
542
|
+
if (finalJob) {
|
|
543
|
+
return { content: [{ type: "text", text: `Résultat commande interactive (tâche ${finalJob.id}):\n${finalJob.output || JSON.stringify(finalJob, null, 2)}` }] };
|
|
544
|
+
} else {
|
|
545
|
+
return { content: [{ type: "text", text: `Tâche interactive ${job.id} initiée en arrière-plan.` }] };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
server.registerTool(
|
|
551
|
+
"task_exec_sequence",
|
|
552
|
+
{
|
|
553
|
+
title: "Exécuter une séquence de commandes (SSH)",
|
|
554
|
+
description: "Exécute plusieurs commandes SSH en séquence sur le même serveur.",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
557
|
+
commands: z.array(z.union([
|
|
558
|
+
z.string(),
|
|
559
|
+
z.object({
|
|
560
|
+
command: z.string(),
|
|
561
|
+
timeout: z.number().optional(),
|
|
562
|
+
continueOnError: z.boolean().optional()
|
|
563
|
+
})
|
|
564
|
+
])).describe("Liste des commandes à exécuter en séquence."),
|
|
565
|
+
continueOnError: z.boolean().optional().default(false).describe("Continuer même si une commande échoue."),
|
|
566
|
+
rappel: z.number().optional().describe("Définit un rappel en secondes.")
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
async (params) => {
|
|
570
|
+
const job = queue.addJob({
|
|
571
|
+
type: 'ssh_sequence',
|
|
572
|
+
...params,
|
|
573
|
+
status: 'pending'
|
|
574
|
+
});
|
|
575
|
+
history.logTask(job);
|
|
576
|
+
ssh.executeCommandSequence(job.id);
|
|
577
|
+
|
|
578
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout * params.commands.length);
|
|
579
|
+
if (finalJob) {
|
|
580
|
+
return { content: [{ type: "text", text: `Résultat séquence (tâche ${finalJob.id}):\n${JSON.stringify(finalJob, null, 2)}` }] };
|
|
581
|
+
} else {
|
|
582
|
+
return { content: [{ type: "text", text: `Séquence de commandes ${job.id} initiée en arrière-plan.` }] };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
server.registerTool(
|
|
588
|
+
"pool_stats",
|
|
589
|
+
{
|
|
590
|
+
title: "Statistiques du pool de connexions SSH",
|
|
591
|
+
description: "Affiche les statistiques du pool de connexions SSH persistantes.",
|
|
592
|
+
inputSchema: {}
|
|
593
|
+
},
|
|
594
|
+
async () => {
|
|
595
|
+
const stats = ssh.getPoolStats();
|
|
596
|
+
return { content: [{ type: "text", text: `Pool de connexions SSH:\n${JSON.stringify(stats, null, 2)}` }] };
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
server.registerTool(
|
|
601
|
+
"queue_stats",
|
|
602
|
+
{
|
|
603
|
+
title: "Statistiques de la queue",
|
|
604
|
+
description: "Affiche les statistiques détaillées de la queue de tâches.",
|
|
605
|
+
inputSchema: {}
|
|
606
|
+
},
|
|
607
|
+
async () => {
|
|
608
|
+
const stats = queue.getStats();
|
|
609
|
+
const crashed = queue.getCrashedJobs();
|
|
610
|
+
const result = {
|
|
611
|
+
stats,
|
|
612
|
+
crashedJobs: crashed.length,
|
|
613
|
+
canRetry: crashed.map(j => ({ id: j.id, type: j.type, crashedAt: j.crashedAt }))
|
|
614
|
+
};
|
|
615
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
server.registerTool(
|
|
620
|
+
"task_retry",
|
|
621
|
+
{
|
|
622
|
+
title: "Réessayer une tâche échouée",
|
|
623
|
+
description: "Relance une tâche qui a échoué ou crashé.",
|
|
624
|
+
inputSchema: {
|
|
625
|
+
id: z.string().describe("L'ID de la tâche à réessayer.")
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
async (params) => {
|
|
629
|
+
try {
|
|
630
|
+
const newJob = await queue.retryJob(params.id);
|
|
631
|
+
|
|
632
|
+
// Relancer selon le type
|
|
633
|
+
if (newJob.type === 'sftp') {
|
|
634
|
+
sftp.executeTransfer(newJob.id);
|
|
635
|
+
} else if (newJob.type === 'ssh') {
|
|
636
|
+
ssh.executeCommand(newJob.id);
|
|
637
|
+
} else if (newJob.type === 'ssh_sequence') {
|
|
638
|
+
ssh.executeCommandSequence(newJob.id);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return { content: [{ type: "text", text: `Tâche ${params.id} relancée avec le nouvel ID: ${newJob.id}` }] };
|
|
642
|
+
} catch (e) {
|
|
643
|
+
const errorPayload = {
|
|
644
|
+
toolName: "task_retry",
|
|
645
|
+
errorCode: "TOOL_EXECUTION_ERROR",
|
|
646
|
+
errorMessage: e.message
|
|
647
|
+
};
|
|
648
|
+
return { content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }], isError: true };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
server.registerTool(
|
|
654
|
+
"task_logs",
|
|
655
|
+
{
|
|
656
|
+
title: "Consulter les logs système",
|
|
657
|
+
description: "Affiche les logs du système MCP.",
|
|
658
|
+
inputSchema: {
|
|
659
|
+
level: z.enum(['error', 'warn', 'info', 'debug']).optional().describe("Filtrer par niveau de log."),
|
|
660
|
+
search: z.string().optional().describe("Rechercher dans les messages."),
|
|
661
|
+
limit: z.number().optional().default(50).describe("Nombre de logs à afficher.")
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
async (params) => {
|
|
665
|
+
const logs = queue.getLogs({
|
|
666
|
+
level: params.level,
|
|
667
|
+
search: params.search
|
|
668
|
+
}).slice(-params.limit);
|
|
669
|
+
return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
// --- NOUVEAUX OUTILS DE LOGS (v8) ---
|
|
674
|
+
|
|
675
|
+
server.registerTool(
|
|
676
|
+
"get_pm2_logs",
|
|
677
|
+
{
|
|
678
|
+
title: "Récupérer les logs PM2",
|
|
679
|
+
description: "Raccourci pour récupérer les logs PM2 d'une application spécifique ou de toutes les apps.",
|
|
680
|
+
inputSchema: {
|
|
681
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
682
|
+
app: z.string().optional().describe("Nom de l'application PM2 (optionnel, toutes par défaut)."),
|
|
683
|
+
lines: z.number().optional().default(100).describe("Nombre de lignes à récupérer."),
|
|
684
|
+
errors: z.boolean().optional().default(false).describe("Récupérer uniquement les erreurs (stderr).")
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
async (params) => {
|
|
688
|
+
let cmd = 'pm2 logs';
|
|
689
|
+
if (params.app) cmd += ` ${params.app}`;
|
|
690
|
+
if (params.errors) cmd += ' --err';
|
|
691
|
+
cmd += ` --lines ${params.lines} --nostream`;
|
|
692
|
+
|
|
693
|
+
const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false });
|
|
694
|
+
history.logTask(job);
|
|
695
|
+
ssh.executeCommand(job.id);
|
|
696
|
+
|
|
697
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
698
|
+
if (finalJob) {
|
|
699
|
+
return { content: [{ type: "text", text: `📋 Logs PM2${params.app ? ` (${params.app})` : ''} - ${finalJob.lineCount || 0} lignes:\n\n${finalJob.output || '(vide)'}` }] };
|
|
700
|
+
} else {
|
|
701
|
+
return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
server.registerTool(
|
|
707
|
+
"get_docker_logs",
|
|
708
|
+
{
|
|
709
|
+
title: "Récupérer les logs Docker",
|
|
710
|
+
description: "Raccourci pour récupérer les logs d'un container Docker.",
|
|
711
|
+
inputSchema: {
|
|
712
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
713
|
+
container: z.string().describe("Nom ou ID du container Docker."),
|
|
714
|
+
lines: z.number().optional().default(100).describe("Nombre de lignes à récupérer."),
|
|
715
|
+
since: z.string().optional().describe("Logs depuis (ex: '5m', '1h', '2024-01-01')."),
|
|
716
|
+
timestamps: z.boolean().optional().default(false).describe("Afficher les timestamps.")
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
async (params) => {
|
|
720
|
+
let cmd = `docker logs --tail ${params.lines}`;
|
|
721
|
+
if (params.since) cmd += ` --since ${params.since}`;
|
|
722
|
+
if (params.timestamps) cmd += ' --timestamps';
|
|
723
|
+
cmd += ` ${params.container}`;
|
|
724
|
+
|
|
725
|
+
const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false });
|
|
726
|
+
history.logTask(job);
|
|
727
|
+
ssh.executeCommand(job.id);
|
|
728
|
+
|
|
729
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
730
|
+
if (finalJob) {
|
|
731
|
+
return { content: [{ type: "text", text: `🐳 Logs Docker (${params.container}) - ${finalJob.lineCount || 0} lignes:\n\n${finalJob.output || '(vide)'}` }] };
|
|
732
|
+
} else {
|
|
733
|
+
return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
server.registerTool(
|
|
739
|
+
"tail_file",
|
|
740
|
+
{
|
|
741
|
+
title: "Afficher les dernières lignes d'un fichier",
|
|
742
|
+
description: "Équivalent de tail -n pour afficher les dernières lignes d'un fichier distant.",
|
|
743
|
+
inputSchema: {
|
|
744
|
+
alias: z.string().describe("Alias du serveur cible."),
|
|
745
|
+
filepath: z.string().describe("Chemin absolu du fichier à lire."),
|
|
746
|
+
lines: z.number().optional().default(50).describe("Nombre de lignes à afficher.")
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
async (params) => {
|
|
750
|
+
const cmd = `tail -n ${params.lines} ${params.filepath}`;
|
|
751
|
+
const job = queue.addJob({ type: 'ssh', alias: params.alias, cmd: cmd, streaming: false });
|
|
752
|
+
history.logTask(job);
|
|
753
|
+
ssh.executeCommand(job.id);
|
|
754
|
+
|
|
755
|
+
const finalJob = await waitForJobCompletion(job.id, config.syncTimeout);
|
|
756
|
+
if (finalJob) {
|
|
757
|
+
return { content: [{ type: "text", text: `📄 Contenu de ${params.filepath} (${finalJob.lineCount || 0} lignes):\n\n${finalJob.output || '(vide)'}` }] };
|
|
758
|
+
} else {
|
|
759
|
+
return { content: [{ type: "text", text: `Tâche ${job.id} initiée en arrière-plan.` }] };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// --- DÉMARRAGE DU SERVEUR ---
|
|
765
|
+
async function main() {
|
|
766
|
+
const transport = new StdioServerTransport();
|
|
767
|
+
await server.connect(transport);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
main().catch((error) => {
|
|
771
|
+
console.error("Erreur fatale du serveur:", error);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
});
|