@imenam/self-register-proxy 1.0.46 → 1.0.48
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/README.md +6 -0
- package/config.js +72 -72
- package/meta-config.js +336 -336
- package/package.json +2 -3
- package/routes/metaConfig.js +180 -180
- package/routes/registration.js +240 -240
- package/services/healthCheck.js +95 -95
- package/services/portManager.js +137 -137
- package/services/proxyRouter.js +99 -99
- package/services/registry.js +41 -41
- package/terminal-server.js +154 -154
package/meta-config.js
CHANGED
|
@@ -1,336 +1,336 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MetaConfigManager - Gestion des méta-configurations pour le proxy
|
|
3
|
-
*
|
|
4
|
-
* Permet de grouper les configurations de plusieurs MCPs en un seul profil
|
|
5
|
-
* applicable en un clic depuis le dashboard.
|
|
6
|
-
*
|
|
7
|
-
* Persistence : meta-configs.json dans le répertoire du proxy (__dirname)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
// Détermine le dossier de configuration
|
|
14
|
-
// Utilise PROXY_CONFIG_DIR si défini, sinon le dossier courant (__dirname)
|
|
15
|
-
const configDir = process.env.PROXY_CONFIG_DIR
|
|
16
|
-
? path.resolve(__dirname, process.env.PROXY_CONFIG_DIR)
|
|
17
|
-
: __dirname;
|
|
18
|
-
|
|
19
|
-
// Création du dossier s'il n'existe pas
|
|
20
|
-
if (!fs.existsSync(configDir)) {
|
|
21
|
-
try {
|
|
22
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.error(`[META-CONFIG] Erreur lors de la création du dossier ${configDir}:`, err.message);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const META_CONFIGS_FILE = path.join(configDir, 'meta-configs.json');
|
|
29
|
-
|
|
30
|
-
const DISCOVERY_TIMEOUT = 3000; // 3s timeout per MCP
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Structure du fichier meta-configs.json :
|
|
34
|
-
* {
|
|
35
|
-
* "metaConfigs": {
|
|
36
|
-
* "<id>": {
|
|
37
|
-
* "name": "string",
|
|
38
|
-
* "description": "string",
|
|
39
|
-
* "mappings": { "<mcpPath>": "<configId>", ... },
|
|
40
|
-
* "createdAt": "ISO8601"
|
|
41
|
-
* }
|
|
42
|
-
* },
|
|
43
|
-
* "activeMetaConfig": "<id>" | null
|
|
44
|
-
* }
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Charge les méta-configurations depuis le fichier JSON
|
|
49
|
-
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
50
|
-
*/
|
|
51
|
-
function loadMetaConfigs() {
|
|
52
|
-
try {
|
|
53
|
-
if (fs.existsSync(META_CONFIGS_FILE)) {
|
|
54
|
-
const raw = fs.readFileSync(META_CONFIGS_FILE, 'utf-8');
|
|
55
|
-
return JSON.parse(raw);
|
|
56
|
-
}
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error('[META-CONFIG] Error loading meta-configs:', err.message);
|
|
59
|
-
}
|
|
60
|
-
return { metaConfigs: {}, activeMetaConfig: null };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Sauvegarde les méta-configurations dans le fichier JSON
|
|
65
|
-
* @param {{ metaConfigs: Object, activeMetaConfig: string|null }} data
|
|
66
|
-
*/
|
|
67
|
-
function saveMetaConfigs(data) {
|
|
68
|
-
try {
|
|
69
|
-
fs.writeFileSync(META_CONFIGS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.error('[META-CONFIG] Error saving meta-configs:', err.message);
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Retourne toutes les méta-configurations
|
|
78
|
-
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
79
|
-
*/
|
|
80
|
-
function getAll() {
|
|
81
|
-
return loadMetaConfigs();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Retourne une méta-configuration par son ID
|
|
86
|
-
* @param {string} id
|
|
87
|
-
* @returns {Object|null}
|
|
88
|
-
*/
|
|
89
|
-
function getById(id) {
|
|
90
|
-
const data = loadMetaConfigs();
|
|
91
|
-
return data.metaConfigs[id] || null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Crée une nouvelle méta-configuration
|
|
96
|
-
* @param {string} id - Identifiant unique (slug)
|
|
97
|
-
* @param {string} name - Nom affiché
|
|
98
|
-
* @param {string} description - Description
|
|
99
|
-
* @param {Object} mappings - { mcpPath: configId, ... }
|
|
100
|
-
* @returns {Object} La méta-configuration créée
|
|
101
|
-
*/
|
|
102
|
-
function create(id, name, description, mappings) {
|
|
103
|
-
const data = loadMetaConfigs();
|
|
104
|
-
|
|
105
|
-
if (data.metaConfigs[id]) {
|
|
106
|
-
throw new Error(`Meta-config "${id}" already exists`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
data.metaConfigs[id] = {
|
|
110
|
-
name,
|
|
111
|
-
description: description || '',
|
|
112
|
-
mappings: mappings || {},
|
|
113
|
-
createdAt: new Date().toISOString()
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
saveMetaConfigs(data);
|
|
117
|
-
return data.metaConfigs[id];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Met à jour partiellement une méta-configuration
|
|
122
|
-
* @param {string} id
|
|
123
|
-
* @param {Object} updates - Champs à mettre à jour (name, description, mappings)
|
|
124
|
-
* @returns {Object} La méta-configuration mise à jour
|
|
125
|
-
*/
|
|
126
|
-
function update(id, updates) {
|
|
127
|
-
const data = loadMetaConfigs();
|
|
128
|
-
|
|
129
|
-
if (!data.metaConfigs[id]) {
|
|
130
|
-
throw new Error(`Meta-config "${id}" not found`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const config = data.metaConfigs[id];
|
|
134
|
-
if (updates.name !== undefined) config.name = updates.name;
|
|
135
|
-
if (updates.description !== undefined) config.description = updates.description;
|
|
136
|
-
if (updates.mappings !== undefined) config.mappings = updates.mappings;
|
|
137
|
-
|
|
138
|
-
saveMetaConfigs(data);
|
|
139
|
-
return config;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Supprime une méta-configuration
|
|
144
|
-
* @param {string} id
|
|
145
|
-
*/
|
|
146
|
-
function remove(id) {
|
|
147
|
-
const data = loadMetaConfigs();
|
|
148
|
-
|
|
149
|
-
if (!data.metaConfigs[id]) {
|
|
150
|
-
throw new Error(`Meta-config "${id}" not found`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
delete data.metaConfigs[id];
|
|
154
|
-
|
|
155
|
-
if (data.activeMetaConfig === id) {
|
|
156
|
-
data.activeMetaConfig = null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
saveMetaConfigs(data);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Définit la méta-configuration active
|
|
164
|
-
* @param {string|null} id
|
|
165
|
-
*/
|
|
166
|
-
function setActive(id) {
|
|
167
|
-
const data = loadMetaConfigs();
|
|
168
|
-
|
|
169
|
-
if (id !== null && !data.metaConfigs[id]) {
|
|
170
|
-
throw new Error(`Meta-config "${id}" not found`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
data.activeMetaConfig = id;
|
|
174
|
-
saveMetaConfigs(data);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Retourne l'ID de la méta-configuration active
|
|
179
|
-
* @returns {string|null}
|
|
180
|
-
*/
|
|
181
|
-
function getActive() {
|
|
182
|
-
const data = loadMetaConfigs();
|
|
183
|
-
return data.activeMetaConfig;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Découvre les configurations disponibles auprès de tous les MCPs enregistrés
|
|
188
|
-
* Interroge GET /api/configurations sur chaque MCP via son target direct
|
|
189
|
-
*
|
|
190
|
-
* @param {Map} routes - Le registry.routes du proxy
|
|
191
|
-
* @returns {Promise<Array<{ path: string, name: string, configurations: Array|null, error: string|null }>>}
|
|
192
|
-
*/
|
|
193
|
-
async function discoverConfigurations(routes) {
|
|
194
|
-
const results = [];
|
|
195
|
-
|
|
196
|
-
const promises = [];
|
|
197
|
-
for (const [mcpPath, route] of routes.entries()) {
|
|
198
|
-
const target = route.target || 'http://localhost';
|
|
199
|
-
const url = `${target}:${route.port}/api/configurations`;
|
|
200
|
-
|
|
201
|
-
promises.push(
|
|
202
|
-
(async () => {
|
|
203
|
-
try {
|
|
204
|
-
const controller = new AbortController();
|
|
205
|
-
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
206
|
-
|
|
207
|
-
const response = await fetch(url, {
|
|
208
|
-
method: 'GET',
|
|
209
|
-
signal: controller.signal
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
clearTimeout(timeoutId);
|
|
213
|
-
|
|
214
|
-
if (response.status === 200) {
|
|
215
|
-
const data = await response.json();
|
|
216
|
-
return {
|
|
217
|
-
path: mcpPath,
|
|
218
|
-
name: route.name,
|
|
219
|
-
configurations: data.configurations || [],
|
|
220
|
-
error: null
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
path: mcpPath,
|
|
226
|
-
name: route.name,
|
|
227
|
-
configurations: null,
|
|
228
|
-
error: `HTTP ${response.status}`
|
|
229
|
-
};
|
|
230
|
-
} catch (err) {
|
|
231
|
-
return {
|
|
232
|
-
path: mcpPath,
|
|
233
|
-
name: route.name,
|
|
234
|
-
configurations: null,
|
|
235
|
-
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
})()
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const settled = await Promise.all(promises);
|
|
243
|
-
results.push(...settled);
|
|
244
|
-
|
|
245
|
-
return results;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Applique une méta-configuration : envoie POST /api/configurations/apply à chaque MCP
|
|
250
|
-
*
|
|
251
|
-
* @param {string} metaConfigId - ID de la méta-configuration à appliquer
|
|
252
|
-
* @param {Map} routes - Le registry.routes du proxy
|
|
253
|
-
* @returns {Promise<{ success: boolean, results: Array<{ path: string, success: boolean, error?: string }> }>}
|
|
254
|
-
*/
|
|
255
|
-
async function applyMetaConfig(metaConfigId, routes) {
|
|
256
|
-
const config = getById(metaConfigId);
|
|
257
|
-
if (!config) {
|
|
258
|
-
throw new Error(`Meta-config "${metaConfigId}" not found`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const missingResults = [];
|
|
262
|
-
const promises = [];
|
|
263
|
-
|
|
264
|
-
for (const [mcpPath, configId] of Object.entries(config.mappings)) {
|
|
265
|
-
const route = routes.get(mcpPath);
|
|
266
|
-
if (!route) {
|
|
267
|
-
missingResults.push({
|
|
268
|
-
path: mcpPath,
|
|
269
|
-
success: false,
|
|
270
|
-
error: 'MCP not registered'
|
|
271
|
-
});
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const target = route.target || 'http://localhost';
|
|
276
|
-
const url = `${target}:${route.port}/api/configurations/apply`;
|
|
277
|
-
|
|
278
|
-
promises.push(
|
|
279
|
-
(async () => {
|
|
280
|
-
try {
|
|
281
|
-
const controller = new AbortController();
|
|
282
|
-
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
283
|
-
|
|
284
|
-
const response = await fetch(url, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: { 'Content-Type': 'application/json' },
|
|
287
|
-
body: JSON.stringify({ configId }),
|
|
288
|
-
signal: controller.signal
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
clearTimeout(timeoutId);
|
|
292
|
-
|
|
293
|
-
if (response.ok) {
|
|
294
|
-
const data = await response.json();
|
|
295
|
-
return { path: mcpPath, success: true, appliedConfig: data.appliedConfig || configId };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const errorData = await response.json().catch(() => ({}));
|
|
299
|
-
return { path: mcpPath, success: false, error: errorData.error || `HTTP ${response.status}` };
|
|
300
|
-
} catch (err) {
|
|
301
|
-
return {
|
|
302
|
-
path: mcpPath,
|
|
303
|
-
success: false,
|
|
304
|
-
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
})()
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const settled = await Promise.all(promises);
|
|
312
|
-
|
|
313
|
-
const allAppliedSuccess = settled.length === 0 || settled.every(r => r.success);
|
|
314
|
-
|
|
315
|
-
if (allAppliedSuccess) {
|
|
316
|
-
setActive(metaConfigId);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
success: allAppliedSuccess,
|
|
321
|
-
warnings: missingResults,
|
|
322
|
-
results: [...missingResults, ...settled]
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
module.exports = {
|
|
327
|
-
getAll,
|
|
328
|
-
getById,
|
|
329
|
-
create,
|
|
330
|
-
update,
|
|
331
|
-
remove,
|
|
332
|
-
setActive,
|
|
333
|
-
getActive,
|
|
334
|
-
discoverConfigurations,
|
|
335
|
-
applyMetaConfig
|
|
336
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* MetaConfigManager - Gestion des méta-configurations pour le proxy
|
|
3
|
+
*
|
|
4
|
+
* Permet de grouper les configurations de plusieurs MCPs en un seul profil
|
|
5
|
+
* applicable en un clic depuis le dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Persistence : meta-configs.json dans le répertoire du proxy (__dirname)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// Détermine le dossier de configuration
|
|
14
|
+
// Utilise PROXY_CONFIG_DIR si défini, sinon le dossier courant (__dirname)
|
|
15
|
+
const configDir = process.env.PROXY_CONFIG_DIR
|
|
16
|
+
? path.resolve(__dirname, process.env.PROXY_CONFIG_DIR)
|
|
17
|
+
: __dirname;
|
|
18
|
+
|
|
19
|
+
// Création du dossier s'il n'existe pas
|
|
20
|
+
if (!fs.existsSync(configDir)) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`[META-CONFIG] Erreur lors de la création du dossier ${configDir}:`, err.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const META_CONFIGS_FILE = path.join(configDir, 'meta-configs.json');
|
|
29
|
+
|
|
30
|
+
const DISCOVERY_TIMEOUT = 3000; // 3s timeout per MCP
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Structure du fichier meta-configs.json :
|
|
34
|
+
* {
|
|
35
|
+
* "metaConfigs": {
|
|
36
|
+
* "<id>": {
|
|
37
|
+
* "name": "string",
|
|
38
|
+
* "description": "string",
|
|
39
|
+
* "mappings": { "<mcpPath>": "<configId>", ... },
|
|
40
|
+
* "createdAt": "ISO8601"
|
|
41
|
+
* }
|
|
42
|
+
* },
|
|
43
|
+
* "activeMetaConfig": "<id>" | null
|
|
44
|
+
* }
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Charge les méta-configurations depuis le fichier JSON
|
|
49
|
+
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
50
|
+
*/
|
|
51
|
+
function loadMetaConfigs() {
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(META_CONFIGS_FILE)) {
|
|
54
|
+
const raw = fs.readFileSync(META_CONFIGS_FILE, 'utf-8');
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('[META-CONFIG] Error loading meta-configs:', err.message);
|
|
59
|
+
}
|
|
60
|
+
return { metaConfigs: {}, activeMetaConfig: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sauvegarde les méta-configurations dans le fichier JSON
|
|
65
|
+
* @param {{ metaConfigs: Object, activeMetaConfig: string|null }} data
|
|
66
|
+
*/
|
|
67
|
+
function saveMetaConfigs(data) {
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(META_CONFIGS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('[META-CONFIG] Error saving meta-configs:', err.message);
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retourne toutes les méta-configurations
|
|
78
|
+
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
79
|
+
*/
|
|
80
|
+
function getAll() {
|
|
81
|
+
return loadMetaConfigs();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retourne une méta-configuration par son ID
|
|
86
|
+
* @param {string} id
|
|
87
|
+
* @returns {Object|null}
|
|
88
|
+
*/
|
|
89
|
+
function getById(id) {
|
|
90
|
+
const data = loadMetaConfigs();
|
|
91
|
+
return data.metaConfigs[id] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Crée une nouvelle méta-configuration
|
|
96
|
+
* @param {string} id - Identifiant unique (slug)
|
|
97
|
+
* @param {string} name - Nom affiché
|
|
98
|
+
* @param {string} description - Description
|
|
99
|
+
* @param {Object} mappings - { mcpPath: configId, ... }
|
|
100
|
+
* @returns {Object} La méta-configuration créée
|
|
101
|
+
*/
|
|
102
|
+
function create(id, name, description, mappings) {
|
|
103
|
+
const data = loadMetaConfigs();
|
|
104
|
+
|
|
105
|
+
if (data.metaConfigs[id]) {
|
|
106
|
+
throw new Error(`Meta-config "${id}" already exists`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
data.metaConfigs[id] = {
|
|
110
|
+
name,
|
|
111
|
+
description: description || '',
|
|
112
|
+
mappings: mappings || {},
|
|
113
|
+
createdAt: new Date().toISOString()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
saveMetaConfigs(data);
|
|
117
|
+
return data.metaConfigs[id];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Met à jour partiellement une méta-configuration
|
|
122
|
+
* @param {string} id
|
|
123
|
+
* @param {Object} updates - Champs à mettre à jour (name, description, mappings)
|
|
124
|
+
* @returns {Object} La méta-configuration mise à jour
|
|
125
|
+
*/
|
|
126
|
+
function update(id, updates) {
|
|
127
|
+
const data = loadMetaConfigs();
|
|
128
|
+
|
|
129
|
+
if (!data.metaConfigs[id]) {
|
|
130
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const config = data.metaConfigs[id];
|
|
134
|
+
if (updates.name !== undefined) config.name = updates.name;
|
|
135
|
+
if (updates.description !== undefined) config.description = updates.description;
|
|
136
|
+
if (updates.mappings !== undefined) config.mappings = updates.mappings;
|
|
137
|
+
|
|
138
|
+
saveMetaConfigs(data);
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Supprime une méta-configuration
|
|
144
|
+
* @param {string} id
|
|
145
|
+
*/
|
|
146
|
+
function remove(id) {
|
|
147
|
+
const data = loadMetaConfigs();
|
|
148
|
+
|
|
149
|
+
if (!data.metaConfigs[id]) {
|
|
150
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
delete data.metaConfigs[id];
|
|
154
|
+
|
|
155
|
+
if (data.activeMetaConfig === id) {
|
|
156
|
+
data.activeMetaConfig = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
saveMetaConfigs(data);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Définit la méta-configuration active
|
|
164
|
+
* @param {string|null} id
|
|
165
|
+
*/
|
|
166
|
+
function setActive(id) {
|
|
167
|
+
const data = loadMetaConfigs();
|
|
168
|
+
|
|
169
|
+
if (id !== null && !data.metaConfigs[id]) {
|
|
170
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
data.activeMetaConfig = id;
|
|
174
|
+
saveMetaConfigs(data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Retourne l'ID de la méta-configuration active
|
|
179
|
+
* @returns {string|null}
|
|
180
|
+
*/
|
|
181
|
+
function getActive() {
|
|
182
|
+
const data = loadMetaConfigs();
|
|
183
|
+
return data.activeMetaConfig;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Découvre les configurations disponibles auprès de tous les MCPs enregistrés
|
|
188
|
+
* Interroge GET /api/configurations sur chaque MCP via son target direct
|
|
189
|
+
*
|
|
190
|
+
* @param {Map} routes - Le registry.routes du proxy
|
|
191
|
+
* @returns {Promise<Array<{ path: string, name: string, configurations: Array|null, error: string|null }>>}
|
|
192
|
+
*/
|
|
193
|
+
async function discoverConfigurations(routes) {
|
|
194
|
+
const results = [];
|
|
195
|
+
|
|
196
|
+
const promises = [];
|
|
197
|
+
for (const [mcpPath, route] of routes.entries()) {
|
|
198
|
+
const target = route.target || 'http://localhost';
|
|
199
|
+
const url = `${target}:${route.port}/api/configurations`;
|
|
200
|
+
|
|
201
|
+
promises.push(
|
|
202
|
+
(async () => {
|
|
203
|
+
try {
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
206
|
+
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
method: 'GET',
|
|
209
|
+
signal: controller.signal
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
|
|
214
|
+
if (response.status === 200) {
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
return {
|
|
217
|
+
path: mcpPath,
|
|
218
|
+
name: route.name,
|
|
219
|
+
configurations: data.configurations || [],
|
|
220
|
+
error: null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
path: mcpPath,
|
|
226
|
+
name: route.name,
|
|
227
|
+
configurations: null,
|
|
228
|
+
error: `HTTP ${response.status}`
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
path: mcpPath,
|
|
233
|
+
name: route.name,
|
|
234
|
+
configurations: null,
|
|
235
|
+
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
})()
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const settled = await Promise.all(promises);
|
|
243
|
+
results.push(...settled);
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Applique une méta-configuration : envoie POST /api/configurations/apply à chaque MCP
|
|
250
|
+
*
|
|
251
|
+
* @param {string} metaConfigId - ID de la méta-configuration à appliquer
|
|
252
|
+
* @param {Map} routes - Le registry.routes du proxy
|
|
253
|
+
* @returns {Promise<{ success: boolean, results: Array<{ path: string, success: boolean, error?: string }> }>}
|
|
254
|
+
*/
|
|
255
|
+
async function applyMetaConfig(metaConfigId, routes) {
|
|
256
|
+
const config = getById(metaConfigId);
|
|
257
|
+
if (!config) {
|
|
258
|
+
throw new Error(`Meta-config "${metaConfigId}" not found`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const missingResults = [];
|
|
262
|
+
const promises = [];
|
|
263
|
+
|
|
264
|
+
for (const [mcpPath, configId] of Object.entries(config.mappings)) {
|
|
265
|
+
const route = routes.get(mcpPath);
|
|
266
|
+
if (!route) {
|
|
267
|
+
missingResults.push({
|
|
268
|
+
path: mcpPath,
|
|
269
|
+
success: false,
|
|
270
|
+
error: 'MCP not registered'
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const target = route.target || 'http://localhost';
|
|
276
|
+
const url = `${target}:${route.port}/api/configurations/apply`;
|
|
277
|
+
|
|
278
|
+
promises.push(
|
|
279
|
+
(async () => {
|
|
280
|
+
try {
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
283
|
+
|
|
284
|
+
const response = await fetch(url, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json' },
|
|
287
|
+
body: JSON.stringify({ configId }),
|
|
288
|
+
signal: controller.signal
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
|
|
293
|
+
if (response.ok) {
|
|
294
|
+
const data = await response.json();
|
|
295
|
+
return { path: mcpPath, success: true, appliedConfig: data.appliedConfig || configId };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const errorData = await response.json().catch(() => ({}));
|
|
299
|
+
return { path: mcpPath, success: false, error: errorData.error || `HTTP ${response.status}` };
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return {
|
|
302
|
+
path: mcpPath,
|
|
303
|
+
success: false,
|
|
304
|
+
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
})()
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const settled = await Promise.all(promises);
|
|
312
|
+
|
|
313
|
+
const allAppliedSuccess = settled.length === 0 || settled.every(r => r.success);
|
|
314
|
+
|
|
315
|
+
if (allAppliedSuccess) {
|
|
316
|
+
setActive(metaConfigId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: allAppliedSuccess,
|
|
321
|
+
warnings: missingResults,
|
|
322
|
+
results: [...missingResults, ...settled]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
getAll,
|
|
328
|
+
getById,
|
|
329
|
+
create,
|
|
330
|
+
update,
|
|
331
|
+
remove,
|
|
332
|
+
setActive,
|
|
333
|
+
getActive,
|
|
334
|
+
discoverConfigurations,
|
|
335
|
+
applyMetaConfig
|
|
336
|
+
};
|