@kwonye/mcpx 0.1.94 → 0.1.95
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/dist/adapters/claude-desktop.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts +2 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts +3 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/cursor.d.ts +2 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/hermes.d.ts +11 -0
- package/dist/adapters/hermes.d.ts.map +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/openclaw.d.ts +11 -0
- package/dist/adapters/openclaw.d.ts.map +1 -0
- package/dist/adapters/opencode.d.ts +2 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/qwen.d.ts.map +1 -1
- package/dist/adapters/utils/index.d.ts +7 -1
- package/dist/adapters/utils/index.d.ts.map +1 -1
- package/dist/adapters/vscode.d.ts +2 -1
- package/dist/adapters/vscode.d.ts.map +1 -1
- package/dist/{cli-154shqmt.js → cli-28fsxhe3.js} +310 -94
- package/dist/{cli-9fmn9y72.js → cli-njtbb11m.js} +104 -28
- package/dist/{cli-csfsg1yf.js → cli-v27bt25d.js} +79 -7
- package/dist/{cli-n8vt375e.js → cli-wxw1t3rr.js} +714 -210
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +359 -72
- package/dist/compat/hermes.d.ts +7 -0
- package/dist/compat/hermes.d.ts.map +1 -0
- package/dist/compat/index.d.ts.map +1 -1
- package/dist/compat/openclaw.d.ts +7 -0
- package/dist/compat/openclaw.d.ts.map +1 -0
- package/dist/{config-dtc3c79c.js → config-5p08cb0c.js} +1 -1
- package/dist/core/add-command.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +30 -0
- package/dist/core/config-store.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts +5 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +15 -66
- package/dist/core/managed-index-lock.d.ts +2 -0
- package/dist/core/managed-index-lock.d.ts.map +1 -0
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/plugin-host.d.ts +2 -0
- package/dist/core/plugin-host.d.ts.map +1 -0
- package/dist/core/plugin-manager.d.ts +19 -6
- package/dist/core/plugin-manager.d.ts.map +1 -1
- package/dist/core/plugin-parse.d.ts.map +1 -1
- package/dist/core/plugin-projections.d.ts.map +1 -1
- package/dist/core/skill-projections.d.ts +3 -0
- package/dist/core/skill-projections.d.ts.map +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/update-manager.d.ts.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/plugin-host-954a7tf7.js +129 -0
- package/dist/{plugin-manager-1sjwfnah.js → plugin-manager-m8ar2271.js} +11 -3
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/cli-mxmgxdm2.js +0 -2257
|
@@ -68,6 +68,7 @@ function loadOwnership(targetDir) {
|
|
|
68
68
|
try {
|
|
69
69
|
return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
|
|
70
70
|
} catch {
|
|
71
|
+
console.error(`[mcpx] Warning: corrupt plugin ownership manifest at ${manifestPath}, resetting`);
|
|
71
72
|
return { version: 1, plugins: {} };
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -123,6 +124,13 @@ function pruneUnlistedPlugins(targetDir, keepPluginIds) {
|
|
|
123
124
|
function nsName(pluginName, id) {
|
|
124
125
|
return `${pluginName}__${id}`;
|
|
125
126
|
}
|
|
127
|
+
function assertWithinBase(base, target) {
|
|
128
|
+
const resolvedBase = path2.resolve(base);
|
|
129
|
+
const resolvedTarget = path2.resolve(target);
|
|
130
|
+
if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path2.sep)) {
|
|
131
|
+
throw new Error(`Refusing to write outside projection base: ${target} (base: ${base})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
126
134
|
function isComponentApproved(plugin, component) {
|
|
127
135
|
const approval = plugin.approvals?.[component];
|
|
128
136
|
if (approval === false)
|
|
@@ -178,6 +186,7 @@ function syncCodex(plugins) {
|
|
|
178
186
|
const targetBase = path2.join(homeDir(), ".codex", "skills");
|
|
179
187
|
const projectedDirs = [];
|
|
180
188
|
const unsupported = ["hooks", "agents"];
|
|
189
|
+
const errors = [];
|
|
181
190
|
for (const plugin of plugins) {
|
|
182
191
|
if (!plugin.enabled)
|
|
183
192
|
continue;
|
|
@@ -185,11 +194,16 @@ function syncCodex(plugins) {
|
|
|
185
194
|
ensureDir(targetBase);
|
|
186
195
|
const owned = [];
|
|
187
196
|
for (const skill of plugin.skills) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
try {
|
|
198
|
+
const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
|
|
199
|
+
assertWithinBase(targetBase, targetDir);
|
|
200
|
+
ensureDir(targetDir);
|
|
201
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
202
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
203
|
+
projectedDirs.push(targetDir);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
206
|
+
}
|
|
193
207
|
}
|
|
194
208
|
recordOwned(targetBase, plugin.pluginId, owned);
|
|
195
209
|
}
|
|
@@ -198,7 +212,8 @@ function syncCodex(plugins) {
|
|
|
198
212
|
clientId: "codex",
|
|
199
213
|
status: "SYNCED",
|
|
200
214
|
projectedDirs,
|
|
201
|
-
unsupported
|
|
215
|
+
unsupported,
|
|
216
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
202
217
|
};
|
|
203
218
|
}
|
|
204
219
|
function syncCursor(plugins) {
|
|
@@ -206,28 +221,39 @@ function syncCursor(plugins) {
|
|
|
206
221
|
const commandsBase = path2.join(homeDir(), ".cursor", "commands");
|
|
207
222
|
const projectedDirs = [];
|
|
208
223
|
const unsupported = ["hooks", "agents"];
|
|
209
|
-
const
|
|
210
|
-
const ownedCommands = [];
|
|
224
|
+
const errors = [];
|
|
211
225
|
for (const plugin of plugins) {
|
|
212
226
|
if (!plugin.enabled)
|
|
213
227
|
continue;
|
|
228
|
+
const ownedSkills = [];
|
|
229
|
+
const ownedCommands = [];
|
|
214
230
|
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
215
231
|
ensureDir(skillsBase);
|
|
216
232
|
for (const skill of plugin.skills) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
233
|
+
try {
|
|
234
|
+
const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
|
|
235
|
+
assertWithinBase(skillsBase, targetDir);
|
|
236
|
+
ensureDir(targetDir);
|
|
237
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
238
|
+
ownedSkills.push(nsName(plugin.pluginName, skill.id));
|
|
239
|
+
projectedDirs.push(targetDir);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
242
|
+
}
|
|
222
243
|
}
|
|
223
244
|
}
|
|
224
245
|
if (plugin.components.commands && isComponentApproved(plugin, "commands")) {
|
|
225
246
|
ensureDir(commandsBase);
|
|
226
247
|
for (const cmd of plugin.commands) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
try {
|
|
249
|
+
const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
250
|
+
assertWithinBase(commandsBase, targetPath);
|
|
251
|
+
copyFileOrDir(cmd.path, targetPath);
|
|
252
|
+
ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
253
|
+
projectedDirs.push(targetPath);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
errors.push(`${plugin.pluginName}/${cmd.id}: ${error.message}`);
|
|
256
|
+
}
|
|
231
257
|
}
|
|
232
258
|
}
|
|
233
259
|
if (ownedSkills.length > 0)
|
|
@@ -239,13 +265,15 @@ function syncCursor(plugins) {
|
|
|
239
265
|
clientId: "cursor",
|
|
240
266
|
status: "SYNCED",
|
|
241
267
|
projectedDirs,
|
|
242
|
-
unsupported
|
|
268
|
+
unsupported,
|
|
269
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
243
270
|
};
|
|
244
271
|
}
|
|
245
272
|
function syncVsCode(plugins) {
|
|
246
273
|
const skillsBase = path2.join(homeDir(), ".copilot", "skills");
|
|
247
274
|
const projectedDirs = [];
|
|
248
275
|
const unsupported = ["hooks", "agents"];
|
|
276
|
+
const errors = [];
|
|
249
277
|
for (const plugin of plugins) {
|
|
250
278
|
if (!plugin.enabled)
|
|
251
279
|
continue;
|
|
@@ -253,11 +281,16 @@ function syncVsCode(plugins) {
|
|
|
253
281
|
ensureDir(skillsBase);
|
|
254
282
|
const owned = [];
|
|
255
283
|
for (const skill of plugin.skills) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
try {
|
|
285
|
+
const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
|
|
286
|
+
assertWithinBase(skillsBase, targetDir);
|
|
287
|
+
ensureDir(targetDir);
|
|
288
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
289
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
290
|
+
projectedDirs.push(targetDir);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
293
|
+
}
|
|
261
294
|
}
|
|
262
295
|
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
263
296
|
}
|
|
@@ -266,13 +299,15 @@ function syncVsCode(plugins) {
|
|
|
266
299
|
clientId: "vscode",
|
|
267
300
|
status: "SYNCED",
|
|
268
301
|
projectedDirs,
|
|
269
|
-
unsupported
|
|
302
|
+
unsupported,
|
|
303
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
270
304
|
};
|
|
271
305
|
}
|
|
272
306
|
function syncQwen(plugins) {
|
|
273
307
|
const targetBase = path2.join(homeDir(), ".qwen", "skills");
|
|
274
308
|
const projectedDirs = [];
|
|
275
309
|
const unsupported = ["hooks", "agents", "commands"];
|
|
310
|
+
const errors = [];
|
|
276
311
|
for (const plugin of plugins) {
|
|
277
312
|
if (!plugin.enabled)
|
|
278
313
|
continue;
|
|
@@ -280,11 +315,16 @@ function syncQwen(plugins) {
|
|
|
280
315
|
ensureDir(targetBase);
|
|
281
316
|
const owned = [];
|
|
282
317
|
for (const skill of plugin.skills) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
318
|
+
try {
|
|
319
|
+
const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
|
|
320
|
+
assertWithinBase(targetBase, targetDir);
|
|
321
|
+
ensureDir(targetDir);
|
|
322
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
323
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
324
|
+
projectedDirs.push(targetDir);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
327
|
+
}
|
|
288
328
|
}
|
|
289
329
|
recordOwned(targetBase, plugin.pluginId, owned);
|
|
290
330
|
}
|
|
@@ -293,13 +333,15 @@ function syncQwen(plugins) {
|
|
|
293
333
|
clientId: "qwen",
|
|
294
334
|
status: "SYNCED",
|
|
295
335
|
projectedDirs,
|
|
296
|
-
unsupported
|
|
336
|
+
unsupported,
|
|
337
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
297
338
|
};
|
|
298
339
|
}
|
|
299
340
|
function syncCline(plugins) {
|
|
300
341
|
const targetBase = path2.join(homeDir(), ".config", "cline", "skills");
|
|
301
342
|
const projectedDirs = [];
|
|
302
343
|
const unsupported = ["hooks", "agents", "commands"];
|
|
344
|
+
const errors = [];
|
|
303
345
|
for (const plugin of plugins) {
|
|
304
346
|
if (!plugin.enabled)
|
|
305
347
|
continue;
|
|
@@ -307,11 +349,16 @@ function syncCline(plugins) {
|
|
|
307
349
|
ensureDir(targetBase);
|
|
308
350
|
const owned = [];
|
|
309
351
|
for (const skill of plugin.skills) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
352
|
+
try {
|
|
353
|
+
const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
|
|
354
|
+
assertWithinBase(targetBase, targetDir);
|
|
355
|
+
ensureDir(targetDir);
|
|
356
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
357
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
358
|
+
projectedDirs.push(targetDir);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
361
|
+
}
|
|
315
362
|
}
|
|
316
363
|
recordOwned(targetBase, plugin.pluginId, owned);
|
|
317
364
|
}
|
|
@@ -320,13 +367,15 @@ function syncCline(plugins) {
|
|
|
320
367
|
clientId: "cline",
|
|
321
368
|
status: "SYNCED",
|
|
322
369
|
projectedDirs,
|
|
323
|
-
unsupported
|
|
370
|
+
unsupported,
|
|
371
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
324
372
|
};
|
|
325
373
|
}
|
|
326
374
|
function syncKiro(plugins) {
|
|
327
375
|
const targetBase = path2.join(homeDir(), ".kiro", "skills");
|
|
328
376
|
const projectedDirs = [];
|
|
329
377
|
const unsupported = ["hooks", "agents", "commands"];
|
|
378
|
+
const errors = [];
|
|
330
379
|
for (const plugin of plugins) {
|
|
331
380
|
if (!plugin.enabled)
|
|
332
381
|
continue;
|
|
@@ -334,11 +383,16 @@ function syncKiro(plugins) {
|
|
|
334
383
|
ensureDir(targetBase);
|
|
335
384
|
const owned = [];
|
|
336
385
|
for (const skill of plugin.skills) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
386
|
+
try {
|
|
387
|
+
const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
|
|
388
|
+
assertWithinBase(targetBase, targetDir);
|
|
389
|
+
ensureDir(targetDir);
|
|
390
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
391
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
392
|
+
projectedDirs.push(targetDir);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
395
|
+
}
|
|
342
396
|
}
|
|
343
397
|
recordOwned(targetBase, plugin.pluginId, owned);
|
|
344
398
|
}
|
|
@@ -347,6 +401,16 @@ function syncKiro(plugins) {
|
|
|
347
401
|
clientId: "kiro",
|
|
348
402
|
status: "SYNCED",
|
|
349
403
|
projectedDirs,
|
|
404
|
+
unsupported,
|
|
405
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function syncOpenCode(plugins) {
|
|
409
|
+
const unsupported = ["skills", "hooks", "agents", "commands"];
|
|
410
|
+
return {
|
|
411
|
+
clientId: "opencode",
|
|
412
|
+
status: plugins.length > 0 ? "SYNCED" : "SKIPPED",
|
|
413
|
+
projectedDirs: [],
|
|
350
414
|
unsupported
|
|
351
415
|
};
|
|
352
416
|
}
|
|
@@ -359,7 +423,8 @@ function syncPluginsToClient(clientId, plugins) {
|
|
|
359
423
|
vscode: syncVsCode,
|
|
360
424
|
qwen: syncQwen,
|
|
361
425
|
cline: syncCline,
|
|
362
|
-
kiro: syncKiro
|
|
426
|
+
kiro: syncKiro,
|
|
427
|
+
opencode: syncOpenCode
|
|
363
428
|
};
|
|
364
429
|
const syncFn = syncMap[clientId];
|
|
365
430
|
if (!syncFn) {
|
|
@@ -373,7 +438,8 @@ function syncPluginsToClient(clientId, plugins) {
|
|
|
373
438
|
vscode: path2.join(homeDir(), ".copilot", "skills"),
|
|
374
439
|
qwen: path2.join(homeDir(), ".qwen", "skills"),
|
|
375
440
|
cline: path2.join(homeDir(), ".config", "cline", "skills"),
|
|
376
|
-
kiro: path2.join(homeDir(), ".kiro", "skills")
|
|
441
|
+
kiro: path2.join(homeDir(), ".kiro", "skills"),
|
|
442
|
+
opencode: ""
|
|
377
443
|
};
|
|
378
444
|
const target = targetBaseMap[clientId];
|
|
379
445
|
if (target) {
|
|
@@ -382,7 +448,7 @@ function syncPluginsToClient(clientId, plugins) {
|
|
|
382
448
|
}
|
|
383
449
|
return syncFn(plugins);
|
|
384
450
|
}
|
|
385
|
-
function copyPluginDir(src, dest, exclude = []) {
|
|
451
|
+
function copyPluginDir(src, dest, exclude = [], errors) {
|
|
386
452
|
const excludeSet = new Set(exclude);
|
|
387
453
|
ensureDir(dest);
|
|
388
454
|
const entries = fs2.readdirSync(src, { withFileTypes: true });
|
|
@@ -392,29 +458,88 @@ function copyPluginDir(src, dest, exclude = []) {
|
|
|
392
458
|
const srcPath = path2.join(src, entry.name);
|
|
393
459
|
const destPath = path2.join(dest, entry.name);
|
|
394
460
|
if (entry.isDirectory()) {
|
|
395
|
-
copyPluginDir(srcPath, destPath, exclude);
|
|
461
|
+
copyPluginDir(srcPath, destPath, exclude, errors);
|
|
396
462
|
} else if (entry.isFile()) {
|
|
397
463
|
try {
|
|
398
464
|
fs2.copyFileSync(srcPath, destPath);
|
|
399
|
-
} catch {
|
|
465
|
+
} catch (e) {
|
|
466
|
+
errors?.push(`Failed to copy ${srcPath}: ${e.message}`);
|
|
467
|
+
}
|
|
400
468
|
}
|
|
401
469
|
}
|
|
402
470
|
}
|
|
403
|
-
function copyFileOrDir(src, dest) {
|
|
471
|
+
function copyFileOrDir(src, dest, errors) {
|
|
404
472
|
try {
|
|
405
473
|
ensureDir(path2.dirname(dest));
|
|
406
474
|
if (fs2.statSync(src).isDirectory()) {
|
|
407
|
-
copyPluginDir(src, dest);
|
|
475
|
+
copyPluginDir(src, dest, [], errors);
|
|
408
476
|
} else {
|
|
409
477
|
fs2.copyFileSync(src, dest);
|
|
410
478
|
}
|
|
411
|
-
} catch {
|
|
479
|
+
} catch (e) {
|
|
480
|
+
errors?.push(`Failed to copy ${src} -> ${dest}: ${e.message}`);
|
|
481
|
+
}
|
|
412
482
|
}
|
|
413
483
|
|
|
484
|
+
// src/version.ts
|
|
485
|
+
var APP_VERSION = "0.1.3";
|
|
486
|
+
|
|
414
487
|
// src/adapters/claude.ts
|
|
488
|
+
import fs4 from "node:fs";
|
|
489
|
+
import path4 from "node:path";
|
|
490
|
+
import { z } from "zod";
|
|
491
|
+
|
|
492
|
+
// src/core/skill-projections.ts
|
|
415
493
|
import fs3 from "node:fs";
|
|
416
494
|
import path3 from "node:path";
|
|
417
|
-
|
|
495
|
+
var OWNERSHIP_MANIFEST2 = "mcpx-skills.json";
|
|
496
|
+
function loadOwnership2(targetDir) {
|
|
497
|
+
const manifestPath = path3.join(targetDir, OWNERSHIP_MANIFEST2);
|
|
498
|
+
if (!fs3.existsSync(manifestPath)) {
|
|
499
|
+
return { version: 1, paths: [] };
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
|
|
503
|
+
} catch {
|
|
504
|
+
console.error(`[mcpx] Warning: corrupt ownership manifest at ${manifestPath}, resetting`);
|
|
505
|
+
return { version: 1, paths: [] };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function saveOwnership2(targetDir, manifest) {
|
|
509
|
+
ensureDir(targetDir);
|
|
510
|
+
fs3.writeFileSync(path3.join(targetDir, OWNERSHIP_MANIFEST2), JSON.stringify(manifest, null, 2));
|
|
511
|
+
}
|
|
512
|
+
function projectSkillsToDir(targetDir, skills, layout) {
|
|
513
|
+
const owned = loadOwnership2(targetDir);
|
|
514
|
+
const newPaths = new Set(skills.map((s) => layout === "dir" ? `${s.id}/SKILL.md` : `${s.id}.md`));
|
|
515
|
+
for (const ownedPath of owned.paths) {
|
|
516
|
+
if (!newPaths.has(ownedPath)) {
|
|
517
|
+
const fullPath = path3.join(targetDir, ownedPath);
|
|
518
|
+
try {
|
|
519
|
+
if (fs3.existsSync(fullPath)) {
|
|
520
|
+
fs3.rmSync(fullPath, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
if (layout === "dir") {
|
|
523
|
+
const dirPath = path3.join(targetDir, path3.dirname(ownedPath));
|
|
524
|
+
try {
|
|
525
|
+
const remaining = fs3.readdirSync(dirPath);
|
|
526
|
+
if (remaining.length === 0) {
|
|
527
|
+
fs3.rmSync(dirPath, { recursive: true, force: true });
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
console.error(`[mcpx] Warning: could not remove stale skill projection: ${fullPath}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const skill of skills) {
|
|
537
|
+
const filePath = layout === "dir" ? path3.join(targetDir, skill.id, "SKILL.md") : path3.join(targetDir, `${skill.id}.md`);
|
|
538
|
+
ensureDir(path3.dirname(filePath));
|
|
539
|
+
fs3.writeFileSync(filePath, skill.content, "utf-8");
|
|
540
|
+
}
|
|
541
|
+
saveOwnership2(targetDir, { version: 1, paths: Array.from(newPaths) });
|
|
542
|
+
}
|
|
418
543
|
|
|
419
544
|
// src/adapters/utils/index.ts
|
|
420
545
|
function getManagedEntryNames(managedIndex, clientId) {
|
|
@@ -437,15 +562,35 @@ function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryName
|
|
|
437
562
|
}
|
|
438
563
|
}
|
|
439
564
|
}
|
|
440
|
-
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
|
|
565
|
+
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue, expectedEntry) {
|
|
441
566
|
if (existingValue === undefined || existingValue === null) {
|
|
442
567
|
return null;
|
|
443
568
|
}
|
|
444
569
|
if (isManagedEntry(managedIndex, clientId, entryName)) {
|
|
445
570
|
return null;
|
|
446
571
|
}
|
|
572
|
+
if (expectedEntry && isManagedGatewayProjection(entryName) && matchesExpectedShape(existingValue, expectedEntry)) {
|
|
573
|
+
if (!managedIndex.managed[clientId]) {
|
|
574
|
+
managedIndex.managed[clientId] = { configPath: "", entries: {} };
|
|
575
|
+
}
|
|
576
|
+
managedIndex.managed[clientId].entries[entryName] = {
|
|
577
|
+
fingerprint: sha256(JSON.stringify(existingValue)),
|
|
578
|
+
lastSyncedAt: new Date().toISOString()
|
|
579
|
+
};
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
447
582
|
return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
|
|
448
583
|
}
|
|
584
|
+
function matchesExpectedShape(value, expected) {
|
|
585
|
+
if (!value || typeof value !== "object")
|
|
586
|
+
return false;
|
|
587
|
+
const v = value;
|
|
588
|
+
if (expected.url && v.url !== expected.url)
|
|
589
|
+
return false;
|
|
590
|
+
if (expected.command && v.command !== expected.command)
|
|
591
|
+
return false;
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
449
594
|
function setManagedEntries(managedIndex, clientId, configPath, entries) {
|
|
450
595
|
if (!managedIndex.managed[clientId]) {
|
|
451
596
|
managedIndex.managed[clientId] = {
|
|
@@ -547,13 +692,20 @@ function purgeManagedFromExcludedArray(raw, managedNames) {
|
|
|
547
692
|
}
|
|
548
693
|
raw.mcp = Object.keys(mcp).length > 0 ? mcp : undefined;
|
|
549
694
|
}
|
|
695
|
+
function detectManagedEntryDrift(managedIndex, clientId, entryName, existingValue) {
|
|
696
|
+
const recorded = managedIndex.managed[clientId]?.entries?.[entryName];
|
|
697
|
+
if (!recorded || existingValue === undefined)
|
|
698
|
+
return false;
|
|
699
|
+
const liveFingerprint = sha256(JSON.stringify(existingValue));
|
|
700
|
+
return liveFingerprint !== recorded.fingerprint;
|
|
701
|
+
}
|
|
550
702
|
|
|
551
703
|
// src/adapters/claude.ts
|
|
552
704
|
function normalizeProjectKey(p) {
|
|
553
705
|
try {
|
|
554
|
-
return
|
|
706
|
+
return fs4.realpathSync(path4.resolve(p));
|
|
555
707
|
} catch {
|
|
556
|
-
return
|
|
708
|
+
return path4.resolve(p);
|
|
557
709
|
}
|
|
558
710
|
}
|
|
559
711
|
function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
|
|
@@ -580,7 +732,7 @@ var claudeEntrySchema = z.object({
|
|
|
580
732
|
class ClaudeAdapter {
|
|
581
733
|
id = "claude";
|
|
582
734
|
detectConfigPath() {
|
|
583
|
-
return
|
|
735
|
+
return path4.join(homeDir(), ".claude.json");
|
|
584
736
|
}
|
|
585
737
|
supportsHttp() {
|
|
586
738
|
return true;
|
|
@@ -588,6 +740,9 @@ class ClaudeAdapter {
|
|
|
588
740
|
syncPlugins(plugins) {
|
|
589
741
|
return syncPluginsToClient(this.id, plugins);
|
|
590
742
|
}
|
|
743
|
+
syncSkills(skills) {
|
|
744
|
+
projectSkillsToDir(path4.join(homeDir(), ".claude", "skills"), skills, "dir");
|
|
745
|
+
}
|
|
591
746
|
scanForImports(_config, managedIndex) {
|
|
592
747
|
const configPath = this.detectConfigPath();
|
|
593
748
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -668,6 +823,7 @@ class ClaudeAdapter {
|
|
|
668
823
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
669
824
|
const topLevelServers = raw.mcpServers ?? {};
|
|
670
825
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
826
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
671
827
|
for (const name of enabledManagedNames) {
|
|
672
828
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
673
829
|
if (topLevelConflict) {
|
|
@@ -703,7 +859,7 @@ class ClaudeAdapter {
|
|
|
703
859
|
raw.projects = projects;
|
|
704
860
|
writeJsonAtomic(configPath, raw);
|
|
705
861
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
706
|
-
return okResult(this.id, configPath);
|
|
862
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
707
863
|
} catch (error) {
|
|
708
864
|
return errorResult(this.id, configPath, error.message);
|
|
709
865
|
}
|
|
@@ -712,7 +868,7 @@ class ClaudeAdapter {
|
|
|
712
868
|
|
|
713
869
|
// src/adapters/claude-desktop.ts
|
|
714
870
|
import { execFileSync } from "node:child_process";
|
|
715
|
-
import
|
|
871
|
+
import path5 from "node:path";
|
|
716
872
|
import { z as z2 } from "zod";
|
|
717
873
|
var stringMapSchema2 = z2.record(z2.string(), z2.string());
|
|
718
874
|
var claudeDesktopEntrySchema = z2.object({
|
|
@@ -732,19 +888,19 @@ function buildProxyEntry(entry) {
|
|
|
732
888
|
if (mcpxPath)
|
|
733
889
|
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
734
890
|
} catch {}
|
|
735
|
-
return { command: "npx", args: ["-y",
|
|
891
|
+
return { command: "npx", args: ["-y", `@kwonye/mcpx@${APP_VERSION}`, "proxy", upstreamName] };
|
|
736
892
|
}
|
|
737
893
|
|
|
738
894
|
class ClaudeDesktopAdapter {
|
|
739
895
|
id = "claude-desktop";
|
|
740
896
|
detectConfigPath() {
|
|
741
897
|
if (process.platform === "linux") {
|
|
742
|
-
return
|
|
898
|
+
return path5.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
743
899
|
}
|
|
744
900
|
if (process.platform === "win32") {
|
|
745
|
-
return
|
|
901
|
+
return path5.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
746
902
|
}
|
|
747
|
-
return
|
|
903
|
+
return path5.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
748
904
|
}
|
|
749
905
|
supportsHttp() {
|
|
750
906
|
return true;
|
|
@@ -821,6 +977,7 @@ class ClaudeDesktopAdapter {
|
|
|
821
977
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
822
978
|
const topLevelServers = raw.mcpServers ?? {};
|
|
823
979
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
980
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
824
981
|
for (const name of enabledManagedNames) {
|
|
825
982
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
826
983
|
if (topLevelConflict) {
|
|
@@ -837,7 +994,7 @@ class ClaudeDesktopAdapter {
|
|
|
837
994
|
const { command, args } = buildProxyEntry(entry);
|
|
838
995
|
return [entry.name, JSON.stringify({ command, args })];
|
|
839
996
|
})));
|
|
840
|
-
return okResult(this.id, configPath);
|
|
997
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
841
998
|
} catch (error) {
|
|
842
999
|
return errorResult(this.id, configPath, error.message);
|
|
843
1000
|
}
|
|
@@ -845,8 +1002,8 @@ class ClaudeDesktopAdapter {
|
|
|
845
1002
|
}
|
|
846
1003
|
|
|
847
1004
|
// src/adapters/codex.ts
|
|
848
|
-
import
|
|
849
|
-
import
|
|
1005
|
+
import path6 from "node:path";
|
|
1006
|
+
import fs5 from "node:fs";
|
|
850
1007
|
import { parse, stringify } from "@iarna/toml";
|
|
851
1008
|
import { z as z3 } from "zod";
|
|
852
1009
|
var stringMapSchema3 = z3.preprocess((value) => {
|
|
@@ -869,7 +1026,7 @@ var codexEntrySchema = z3.object({
|
|
|
869
1026
|
class CodexAdapter {
|
|
870
1027
|
id = "codex";
|
|
871
1028
|
detectConfigPath() {
|
|
872
|
-
return
|
|
1029
|
+
return path6.join(homeDir(), ".codex", "config.toml");
|
|
873
1030
|
}
|
|
874
1031
|
supportsHttp() {
|
|
875
1032
|
return true;
|
|
@@ -877,13 +1034,16 @@ class CodexAdapter {
|
|
|
877
1034
|
syncPlugins(plugins) {
|
|
878
1035
|
return syncPluginsToClient(this.id, plugins);
|
|
879
1036
|
}
|
|
1037
|
+
syncSkills(skills) {
|
|
1038
|
+
projectSkillsToDir(path6.join(homeDir(), ".codex", "skills"), skills, "dir");
|
|
1039
|
+
}
|
|
880
1040
|
scanForImports(_config, managedIndex) {
|
|
881
1041
|
const configPath = this.detectConfigPath();
|
|
882
1042
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
883
|
-
if (!configPath || !
|
|
1043
|
+
if (!configPath || !fs5.existsSync(configPath)) {
|
|
884
1044
|
return result;
|
|
885
1045
|
}
|
|
886
|
-
const existing =
|
|
1046
|
+
const existing = fs5.readFileSync(configPath, "utf8");
|
|
887
1047
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
888
1048
|
const mcpServers = doc.mcp_servers ?? {};
|
|
889
1049
|
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
@@ -938,32 +1098,35 @@ class CodexAdapter {
|
|
|
938
1098
|
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
939
1099
|
}
|
|
940
1100
|
try {
|
|
941
|
-
const existing =
|
|
1101
|
+
const existing = fs5.existsSync(configPath) ? fs5.readFileSync(configPath, "utf8") : "";
|
|
942
1102
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
943
1103
|
const mcpServers = doc.mcp_servers ?? {};
|
|
944
1104
|
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
945
1105
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
946
|
-
const
|
|
1106
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1107
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1108
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
947
1109
|
url: entry.url,
|
|
948
1110
|
http_headers: entry.headers
|
|
949
1111
|
}]));
|
|
950
|
-
|
|
1112
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcpServers[name]));
|
|
1113
|
+
for (const name of enabledManagedNames) {
|
|
951
1114
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
952
1115
|
if (conflict) {
|
|
953
1116
|
return errorResult(this.id, configPath, conflict);
|
|
954
1117
|
}
|
|
955
1118
|
}
|
|
956
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers,
|
|
1119
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, enabledManagedNames);
|
|
957
1120
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
958
1121
|
mcpServers[name] = entry;
|
|
959
1122
|
}
|
|
960
1123
|
doc.mcp_servers = mcpServers;
|
|
961
|
-
|
|
1124
|
+
fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
|
|
962
1125
|
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
963
|
-
|
|
964
|
-
|
|
1126
|
+
fs5.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
1127
|
+
fs5.renameSync(tmpPath, configPath);
|
|
965
1128
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
966
|
-
return okResult(this.id, configPath);
|
|
1129
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
967
1130
|
} catch (error) {
|
|
968
1131
|
return errorResult(this.id, configPath, error.message);
|
|
969
1132
|
}
|
|
@@ -971,7 +1134,7 @@ class CodexAdapter {
|
|
|
971
1134
|
}
|
|
972
1135
|
|
|
973
1136
|
// src/adapters/cursor.ts
|
|
974
|
-
import
|
|
1137
|
+
import path7 from "node:path";
|
|
975
1138
|
import { z as z4 } from "zod";
|
|
976
1139
|
var stringMapSchema4 = z4.record(z4.string(), z4.string());
|
|
977
1140
|
var cursorEntrySchema = z4.object({
|
|
@@ -988,7 +1151,7 @@ var cursorEntrySchema = z4.object({
|
|
|
988
1151
|
class CursorAdapter {
|
|
989
1152
|
id = "cursor";
|
|
990
1153
|
detectConfigPath() {
|
|
991
|
-
return
|
|
1154
|
+
return path7.join(homeDir(), ".cursor", "mcp.json");
|
|
992
1155
|
}
|
|
993
1156
|
supportsHttp() {
|
|
994
1157
|
return true;
|
|
@@ -996,6 +1159,9 @@ class CursorAdapter {
|
|
|
996
1159
|
syncPlugins(plugins) {
|
|
997
1160
|
return syncPluginsToClient(this.id, plugins);
|
|
998
1161
|
}
|
|
1162
|
+
syncSkills(skills) {
|
|
1163
|
+
projectSkillsToDir(path7.join(homeDir(), ".cursor", "skills"), skills, "dir");
|
|
1164
|
+
}
|
|
999
1165
|
scanForImports(_config, managedIndex) {
|
|
1000
1166
|
const configPath = this.detectConfigPath();
|
|
1001
1167
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1070,6 +1236,7 @@ class CursorAdapter {
|
|
|
1070
1236
|
headers: entry.headers
|
|
1071
1237
|
}]));
|
|
1072
1238
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1239
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1073
1240
|
for (const name of enabledManagedNames) {
|
|
1074
1241
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1075
1242
|
if (conflict) {
|
|
@@ -1089,7 +1256,7 @@ class CursorAdapter {
|
|
|
1089
1256
|
entry.name,
|
|
1090
1257
|
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1091
1258
|
])));
|
|
1092
|
-
return okResult(this.id, configPath);
|
|
1259
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1093
1260
|
} catch (error) {
|
|
1094
1261
|
return errorResult(this.id, configPath, error.message);
|
|
1095
1262
|
}
|
|
@@ -1097,8 +1264,8 @@ class CursorAdapter {
|
|
|
1097
1264
|
}
|
|
1098
1265
|
|
|
1099
1266
|
// src/adapters/cline.ts
|
|
1100
|
-
import
|
|
1101
|
-
import
|
|
1267
|
+
import path8 from "node:path";
|
|
1268
|
+
import fs6 from "node:fs";
|
|
1102
1269
|
import { z as z5 } from "zod";
|
|
1103
1270
|
var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
|
|
1104
1271
|
var stringMapSchema5 = z5.record(z5.string(), z5.string());
|
|
@@ -1116,22 +1283,22 @@ var clineEntrySchema = z5.object({
|
|
|
1116
1283
|
function getCandidatePaths() {
|
|
1117
1284
|
if (process.platform === "linux") {
|
|
1118
1285
|
return [
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1286
|
+
path8.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1287
|
+
path8.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1288
|
+
path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1122
1289
|
];
|
|
1123
1290
|
}
|
|
1124
1291
|
if (process.platform === "win32") {
|
|
1125
1292
|
return [
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1293
|
+
path8.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1294
|
+
path8.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1295
|
+
path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1129
1296
|
];
|
|
1130
1297
|
}
|
|
1131
1298
|
return [
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1299
|
+
path8.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1300
|
+
path8.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1301
|
+
path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1135
1302
|
];
|
|
1136
1303
|
}
|
|
1137
1304
|
|
|
@@ -1140,7 +1307,7 @@ class ClineAdapter {
|
|
|
1140
1307
|
detectConfigPath() {
|
|
1141
1308
|
const candidates = getCandidatePaths();
|
|
1142
1309
|
for (const candidate of candidates) {
|
|
1143
|
-
if (
|
|
1310
|
+
if (fs6.existsSync(candidate)) {
|
|
1144
1311
|
return candidate;
|
|
1145
1312
|
}
|
|
1146
1313
|
}
|
|
@@ -1149,10 +1316,16 @@ class ClineAdapter {
|
|
|
1149
1316
|
supportsHttp() {
|
|
1150
1317
|
return true;
|
|
1151
1318
|
}
|
|
1319
|
+
syncPlugins(plugins) {
|
|
1320
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1321
|
+
}
|
|
1322
|
+
syncSkills(skills) {
|
|
1323
|
+
projectSkillsToDir(path8.join(homeDir(), ".config", "cline", "skills"), skills, "dir");
|
|
1324
|
+
}
|
|
1152
1325
|
scanForImports(_config, managedIndex) {
|
|
1153
1326
|
const configPath = this.detectConfigPath();
|
|
1154
1327
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1155
|
-
if (!configPath || !
|
|
1328
|
+
if (!configPath || !fs6.existsSync(configPath)) {
|
|
1156
1329
|
return result;
|
|
1157
1330
|
}
|
|
1158
1331
|
const raw = readJsonFile(configPath, {});
|
|
@@ -1211,7 +1384,7 @@ class ClineAdapter {
|
|
|
1211
1384
|
if (!configPath) {
|
|
1212
1385
|
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1213
1386
|
}
|
|
1214
|
-
if (!
|
|
1387
|
+
if (!fs6.existsSync(configPath)) {
|
|
1215
1388
|
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1216
1389
|
}
|
|
1217
1390
|
try {
|
|
@@ -1221,18 +1394,21 @@ class ClineAdapter {
|
|
|
1221
1394
|
};
|
|
1222
1395
|
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1223
1396
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1224
|
-
const
|
|
1397
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1398
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1399
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1225
1400
|
type: "streamableHttp",
|
|
1226
1401
|
url: entry.url,
|
|
1227
1402
|
headers: entry.headers
|
|
1228
1403
|
}]));
|
|
1229
|
-
|
|
1404
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1405
|
+
for (const name of enabledManagedNames) {
|
|
1230
1406
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1231
1407
|
if (conflict) {
|
|
1232
1408
|
return errorResult(this.id, configPath, conflict);
|
|
1233
1409
|
}
|
|
1234
1410
|
}
|
|
1235
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers,
|
|
1411
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1236
1412
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1237
1413
|
servers[name] = entry;
|
|
1238
1414
|
}
|
|
@@ -1242,7 +1418,7 @@ class ClineAdapter {
|
|
|
1242
1418
|
};
|
|
1243
1419
|
writeJsonAtomic(configPath, next);
|
|
1244
1420
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1245
|
-
return okResult(this.id, configPath);
|
|
1421
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1246
1422
|
} catch (error) {
|
|
1247
1423
|
return errorResult(this.id, configPath, error.message);
|
|
1248
1424
|
}
|
|
@@ -1250,8 +1426,7 @@ class ClineAdapter {
|
|
|
1250
1426
|
}
|
|
1251
1427
|
|
|
1252
1428
|
// src/adapters/kiro.ts
|
|
1253
|
-
import
|
|
1254
|
-
import path8 from "node:path";
|
|
1429
|
+
import path9 from "node:path";
|
|
1255
1430
|
import { z as z6 } from "zod";
|
|
1256
1431
|
var stringMapSchema6 = z6.record(z6.string(), z6.string());
|
|
1257
1432
|
var kiroEntrySchema = z6.object({
|
|
@@ -1268,7 +1443,7 @@ var kiroEntrySchema = z6.object({
|
|
|
1268
1443
|
class KiroAdapter {
|
|
1269
1444
|
id = "kiro";
|
|
1270
1445
|
detectConfigPath() {
|
|
1271
|
-
return
|
|
1446
|
+
return path9.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
1272
1447
|
}
|
|
1273
1448
|
supportsHttp() {
|
|
1274
1449
|
return true;
|
|
@@ -1344,17 +1519,20 @@ class KiroAdapter {
|
|
|
1344
1519
|
};
|
|
1345
1520
|
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1346
1521
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1347
|
-
const
|
|
1522
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1523
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1524
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1348
1525
|
url: entry.url,
|
|
1349
1526
|
headers: entry.headers
|
|
1350
1527
|
}]));
|
|
1351
|
-
|
|
1528
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1529
|
+
for (const name of enabledManagedNames) {
|
|
1352
1530
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1353
1531
|
if (conflict) {
|
|
1354
1532
|
return errorResult(this.id, configPath, conflict);
|
|
1355
1533
|
}
|
|
1356
1534
|
}
|
|
1357
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers,
|
|
1535
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1358
1536
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1359
1537
|
servers[name] = entry;
|
|
1360
1538
|
}
|
|
@@ -1364,35 +1542,18 @@ class KiroAdapter {
|
|
|
1364
1542
|
};
|
|
1365
1543
|
writeJsonAtomic(configPath, next);
|
|
1366
1544
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1367
|
-
return okResult(this.id, configPath);
|
|
1545
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1368
1546
|
} catch (error) {
|
|
1369
1547
|
return errorResult(this.id, configPath, error.message);
|
|
1370
1548
|
}
|
|
1371
1549
|
}
|
|
1372
1550
|
syncSkills(skills) {
|
|
1373
|
-
|
|
1374
|
-
if (!fs6.existsSync(skillsDir)) {
|
|
1375
|
-
fs6.mkdirSync(skillsDir, { recursive: true });
|
|
1376
|
-
}
|
|
1377
|
-
const existingFiles = fs6.readdirSync(skillsDir);
|
|
1378
|
-
const managedIds = new Set(skills.map((s) => s.id));
|
|
1379
|
-
for (const file of existingFiles) {
|
|
1380
|
-
if (file.endsWith(".md")) {
|
|
1381
|
-
const id = path8.basename(file, ".md");
|
|
1382
|
-
if (!managedIds.has(id)) {
|
|
1383
|
-
fs6.unlinkSync(path8.join(skillsDir, file));
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
for (const skill of skills) {
|
|
1388
|
-
const targetPath = path8.join(skillsDir, `${skill.id}.md`);
|
|
1389
|
-
fs6.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1390
|
-
}
|
|
1551
|
+
projectSkillsToDir(path9.join(homeDir(), ".kiro", "skills"), skills, "flat");
|
|
1391
1552
|
}
|
|
1392
1553
|
}
|
|
1393
1554
|
|
|
1394
1555
|
// src/adapters/opencode.ts
|
|
1395
|
-
import
|
|
1556
|
+
import path10 from "node:path";
|
|
1396
1557
|
import { z as z7 } from "zod";
|
|
1397
1558
|
var stringMapSchema7 = z7.record(z7.string(), z7.string());
|
|
1398
1559
|
var openCodeEntrySchema = z7.object({
|
|
@@ -1410,11 +1571,14 @@ var openCodeEntrySchema = z7.object({
|
|
|
1410
1571
|
class OpenCodeAdapter {
|
|
1411
1572
|
id = "opencode";
|
|
1412
1573
|
detectConfigPath() {
|
|
1413
|
-
return
|
|
1574
|
+
return path10.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
1414
1575
|
}
|
|
1415
1576
|
supportsHttp() {
|
|
1416
1577
|
return true;
|
|
1417
1578
|
}
|
|
1579
|
+
syncPlugins(plugins) {
|
|
1580
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1581
|
+
}
|
|
1418
1582
|
scanForImports(_config, managedIndex) {
|
|
1419
1583
|
const configPath = this.detectConfigPath();
|
|
1420
1584
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1488,18 +1652,21 @@ class OpenCodeAdapter {
|
|
|
1488
1652
|
};
|
|
1489
1653
|
removeSourceEntries(mcp, options.sourceEntriesToRemove);
|
|
1490
1654
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1491
|
-
const
|
|
1655
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1656
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1657
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1492
1658
|
type: "remote",
|
|
1493
1659
|
url: entry.url,
|
|
1494
1660
|
headers: entry.headers
|
|
1495
1661
|
}]));
|
|
1496
|
-
|
|
1662
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcp[name]));
|
|
1663
|
+
for (const name of enabledManagedNames) {
|
|
1497
1664
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
|
|
1498
1665
|
if (conflict) {
|
|
1499
1666
|
return errorResult(this.id, configPath, conflict);
|
|
1500
1667
|
}
|
|
1501
1668
|
}
|
|
1502
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcp,
|
|
1669
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcp, enabledManagedNames);
|
|
1503
1670
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1504
1671
|
mcp[name] = entry;
|
|
1505
1672
|
}
|
|
@@ -1509,7 +1676,7 @@ class OpenCodeAdapter {
|
|
|
1509
1676
|
};
|
|
1510
1677
|
writeJsonAtomic(configPath, next);
|
|
1511
1678
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1512
|
-
return okResult(this.id, configPath);
|
|
1679
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1513
1680
|
} catch (error) {
|
|
1514
1681
|
return errorResult(this.id, configPath, error.message);
|
|
1515
1682
|
}
|
|
@@ -1517,7 +1684,7 @@ class OpenCodeAdapter {
|
|
|
1517
1684
|
}
|
|
1518
1685
|
|
|
1519
1686
|
// src/adapters/vscode.ts
|
|
1520
|
-
import
|
|
1687
|
+
import path11 from "node:path";
|
|
1521
1688
|
import { z as z8 } from "zod";
|
|
1522
1689
|
var stringMapSchema8 = z8.record(z8.string(), z8.string());
|
|
1523
1690
|
var vscodeEntrySchema = z8.object({
|
|
@@ -1535,12 +1702,12 @@ class VsCodeAdapter {
|
|
|
1535
1702
|
id = "vscode";
|
|
1536
1703
|
detectConfigPath() {
|
|
1537
1704
|
if (process.platform === "linux") {
|
|
1538
|
-
return
|
|
1705
|
+
return path11.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
1539
1706
|
}
|
|
1540
1707
|
if (process.platform === "win32") {
|
|
1541
|
-
return
|
|
1708
|
+
return path11.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
1542
1709
|
}
|
|
1543
|
-
return
|
|
1710
|
+
return path11.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
1544
1711
|
}
|
|
1545
1712
|
supportsHttp() {
|
|
1546
1713
|
return true;
|
|
@@ -1548,6 +1715,9 @@ class VsCodeAdapter {
|
|
|
1548
1715
|
syncPlugins(plugins) {
|
|
1549
1716
|
return syncPluginsToClient(this.id, plugins);
|
|
1550
1717
|
}
|
|
1718
|
+
syncSkills(skills) {
|
|
1719
|
+
projectSkillsToDir(path11.join(homeDir(), ".copilot", "skills"), skills, "dir");
|
|
1720
|
+
}
|
|
1551
1721
|
scanForImports(_config, managedIndex) {
|
|
1552
1722
|
const configPath = this.detectConfigPath();
|
|
1553
1723
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1622,6 +1792,7 @@ class VsCodeAdapter {
|
|
|
1622
1792
|
headers: entry.headers
|
|
1623
1793
|
}]));
|
|
1624
1794
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1795
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1625
1796
|
for (const name of enabledManagedNames) {
|
|
1626
1797
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1627
1798
|
if (conflict) {
|
|
@@ -1641,7 +1812,7 @@ class VsCodeAdapter {
|
|
|
1641
1812
|
entry.name,
|
|
1642
1813
|
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers })
|
|
1643
1814
|
])));
|
|
1644
|
-
return okResult(this.id, configPath);
|
|
1815
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1645
1816
|
} catch (error) {
|
|
1646
1817
|
return errorResult(this.id, configPath, error.message);
|
|
1647
1818
|
}
|
|
@@ -1649,8 +1820,7 @@ class VsCodeAdapter {
|
|
|
1649
1820
|
}
|
|
1650
1821
|
|
|
1651
1822
|
// src/adapters/qwen.ts
|
|
1652
|
-
import
|
|
1653
|
-
import path11 from "node:path";
|
|
1823
|
+
import path12 from "node:path";
|
|
1654
1824
|
import { z as z9 } from "zod";
|
|
1655
1825
|
var stringMapSchema9 = z9.record(z9.string(), z9.string());
|
|
1656
1826
|
var qwenEntrySchema = z9.object({
|
|
@@ -1667,7 +1837,7 @@ var qwenEntrySchema = z9.object({
|
|
|
1667
1837
|
class QwenAdapter {
|
|
1668
1838
|
id = "qwen";
|
|
1669
1839
|
detectConfigPath() {
|
|
1670
|
-
return
|
|
1840
|
+
return path12.join(homeDir(), ".qwen", "settings.json");
|
|
1671
1841
|
}
|
|
1672
1842
|
supportsHttp() {
|
|
1673
1843
|
return true;
|
|
@@ -1753,19 +1923,22 @@ class QwenAdapter {
|
|
|
1753
1923
|
try {
|
|
1754
1924
|
const raw = readJsonFile(configPath, {});
|
|
1755
1925
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1756
|
-
const
|
|
1926
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1927
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1928
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1757
1929
|
httpUrl: entry.url,
|
|
1758
1930
|
headers: entry.headers
|
|
1759
1931
|
}]));
|
|
1760
1932
|
const topLevelServers = raw.mcpServers ?? {};
|
|
1761
1933
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1762
|
-
|
|
1934
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
1935
|
+
for (const name of enabledManagedNames) {
|
|
1763
1936
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1764
1937
|
if (topLevelConflict) {
|
|
1765
1938
|
return errorResult(this.id, configPath, topLevelConflict);
|
|
1766
1939
|
}
|
|
1767
1940
|
}
|
|
1768
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers,
|
|
1941
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
1769
1942
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1770
1943
|
topLevelServers[name] = entry;
|
|
1771
1944
|
}
|
|
@@ -1776,29 +1949,291 @@ class QwenAdapter {
|
|
|
1776
1949
|
entry.name,
|
|
1777
1950
|
JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1778
1951
|
])));
|
|
1779
|
-
return okResult(this.id, configPath);
|
|
1952
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1780
1953
|
} catch (error) {
|
|
1781
1954
|
return errorResult(this.id, configPath, error.message);
|
|
1782
1955
|
}
|
|
1783
1956
|
}
|
|
1784
1957
|
syncSkills(skills) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1958
|
+
projectSkillsToDir(path12.join(homeDir(), ".qwen", "skills"), skills, "flat");
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// src/adapters/openclaw.ts
|
|
1963
|
+
import fs7 from "node:fs";
|
|
1964
|
+
import path13 from "node:path";
|
|
1965
|
+
import { z as z10 } from "zod";
|
|
1966
|
+
var stringMapSchema10 = z10.record(z10.string(), z10.string());
|
|
1967
|
+
var openclawEntrySchema = z10.object({
|
|
1968
|
+
url: z10.string().min(1).optional(),
|
|
1969
|
+
transport: z10.string().optional(),
|
|
1970
|
+
headers: stringMapSchema10.optional(),
|
|
1971
|
+
command: z10.string().min(1).optional(),
|
|
1972
|
+
args: z10.array(z10.string()).optional(),
|
|
1973
|
+
env: stringMapSchema10.optional(),
|
|
1974
|
+
cwd: z10.string().min(1).optional(),
|
|
1975
|
+
workingDirectory: z10.string().min(1).optional(),
|
|
1976
|
+
auth: z10.unknown().optional()
|
|
1977
|
+
}).passthrough();
|
|
1978
|
+
|
|
1979
|
+
class OpenClawAdapter {
|
|
1980
|
+
id = "openclaw";
|
|
1981
|
+
detectConfigPath() {
|
|
1982
|
+
return process.env.OPENCLAW_CONFIG_PATH ?? path13.join(homeDir(), ".openclaw", "openclaw.json");
|
|
1983
|
+
}
|
|
1984
|
+
supportsHttp() {
|
|
1985
|
+
return true;
|
|
1986
|
+
}
|
|
1987
|
+
syncPlugins(plugins) {
|
|
1988
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1989
|
+
}
|
|
1990
|
+
syncSkills(skills) {
|
|
1991
|
+
projectSkillsToDir(path13.join(homeDir(), ".openclaw", "skills"), skills, "dir");
|
|
1992
|
+
}
|
|
1993
|
+
scanForImports(_config, managedIndex) {
|
|
1994
|
+
const configPath = this.detectConfigPath();
|
|
1995
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1996
|
+
if (!configPath) {
|
|
1997
|
+
return result;
|
|
1998
|
+
}
|
|
1999
|
+
let raw;
|
|
2000
|
+
try {
|
|
2001
|
+
raw = JSON.parse(fs7.readFileSync(configPath, "utf8"));
|
|
2002
|
+
} catch {
|
|
2003
|
+
return result;
|
|
2004
|
+
}
|
|
2005
|
+
const mcpServers = {};
|
|
2006
|
+
const rootMcp = raw.mcp;
|
|
2007
|
+
if (rootMcp?.servers) {
|
|
2008
|
+
Object.assign(mcpServers, rootMcp.servers);
|
|
2009
|
+
}
|
|
2010
|
+
if (raw.mcpServers) {
|
|
2011
|
+
Object.assign(mcpServers, raw.mcpServers);
|
|
2012
|
+
}
|
|
2013
|
+
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
2014
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const parsed = openclawEntrySchema.safeParse(rawEntry);
|
|
2018
|
+
if (!parsed.success) {
|
|
2019
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
const entry = parsed.data;
|
|
2023
|
+
if (entry.auth === "oauth") {
|
|
2024
|
+
result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
if (entry.url && !entry.command) {
|
|
2028
|
+
result.candidates.push({
|
|
2029
|
+
clientId: this.id,
|
|
2030
|
+
configPath,
|
|
2031
|
+
sourceEntryName: name,
|
|
2032
|
+
serverName: name,
|
|
2033
|
+
spec: {
|
|
2034
|
+
transport: "http",
|
|
2035
|
+
url: entry.url,
|
|
2036
|
+
headers: entry.headers
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (entry.command && !entry.url) {
|
|
2042
|
+
result.candidates.push({
|
|
2043
|
+
clientId: this.id,
|
|
2044
|
+
configPath,
|
|
2045
|
+
sourceEntryName: name,
|
|
2046
|
+
serverName: name,
|
|
2047
|
+
spec: {
|
|
2048
|
+
transport: "stdio",
|
|
2049
|
+
command: entry.command,
|
|
2050
|
+
args: entry.args,
|
|
2051
|
+
env: entry.env,
|
|
2052
|
+
cwd: entry.workingDirectory ?? entry.cwd
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2058
|
+
}
|
|
2059
|
+
return result;
|
|
2060
|
+
}
|
|
2061
|
+
syncGateway(_config, options) {
|
|
2062
|
+
const configPath = this.detectConfigPath();
|
|
2063
|
+
if (!configPath) {
|
|
2064
|
+
return errorResult(this.id, undefined, "Unable to resolve OpenClaw config path.");
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
let raw;
|
|
2068
|
+
if (fs7.existsSync(configPath)) {
|
|
2069
|
+
try {
|
|
2070
|
+
raw = JSON.parse(fs7.readFileSync(configPath, "utf8"));
|
|
2071
|
+
} catch {
|
|
2072
|
+
return errorResult(this.id, configPath, "OpenClaw config contains invalid JSON (possibly JSON5). Cannot safely write.");
|
|
2073
|
+
}
|
|
2074
|
+
} else {
|
|
2075
|
+
raw = {};
|
|
2076
|
+
}
|
|
2077
|
+
const rootMcp = raw.mcp ?? {};
|
|
2078
|
+
const servers = rootMcp.servers ?? {};
|
|
2079
|
+
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
2080
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2081
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2082
|
+
url: entry.url,
|
|
2083
|
+
transport: "streamable-http",
|
|
2084
|
+
headers: entry.headers
|
|
2085
|
+
}]));
|
|
2086
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2087
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
2088
|
+
for (const name of enabledManagedNames) {
|
|
2089
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
2090
|
+
if (conflict) {
|
|
2091
|
+
return errorResult(this.id, configPath, conflict);
|
|
1796
2092
|
}
|
|
1797
2093
|
}
|
|
2094
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
2095
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2096
|
+
servers[name] = entry;
|
|
2097
|
+
}
|
|
2098
|
+
rootMcp.servers = servers;
|
|
2099
|
+
raw.mcp = rootMcp;
|
|
2100
|
+
writeJsonAtomic(configPath, raw);
|
|
2101
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
2102
|
+
entry.name,
|
|
2103
|
+
JSON.stringify({ url: entry.url, transport: "streamable-http", headers: entry.headers, enabled: entry.enabled })
|
|
2104
|
+
])));
|
|
2105
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
return errorResult(this.id, configPath, error.message);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/adapters/hermes.ts
|
|
2113
|
+
import fs8 from "node:fs";
|
|
2114
|
+
import path14 from "node:path";
|
|
2115
|
+
import YAML from "yaml";
|
|
2116
|
+
import { z as z11 } from "zod";
|
|
2117
|
+
var stringMapSchema11 = z11.record(z11.string(), z11.string());
|
|
2118
|
+
var hermesEntrySchema = z11.object({
|
|
2119
|
+
url: z11.string().min(1).optional(),
|
|
2120
|
+
headers: stringMapSchema11.optional(),
|
|
2121
|
+
command: z11.string().min(1).optional(),
|
|
2122
|
+
args: z11.array(z11.string()).optional(),
|
|
2123
|
+
env: stringMapSchema11.optional(),
|
|
2124
|
+
auth: z11.unknown().optional(),
|
|
2125
|
+
enabled: z11.boolean().optional(),
|
|
2126
|
+
timeout: z11.number().optional(),
|
|
2127
|
+
tools: z11.array(z11.string()).optional()
|
|
2128
|
+
}).passthrough();
|
|
2129
|
+
|
|
2130
|
+
class HermesAdapter {
|
|
2131
|
+
id = "hermes";
|
|
2132
|
+
detectConfigPath() {
|
|
2133
|
+
return path14.join(homeDir(), ".hermes", "config.yaml");
|
|
2134
|
+
}
|
|
2135
|
+
supportsHttp() {
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
syncPlugins(plugins) {
|
|
2139
|
+
return syncPluginsToClient(this.id, plugins);
|
|
2140
|
+
}
|
|
2141
|
+
syncSkills(skills) {
|
|
2142
|
+
projectSkillsToDir(path14.join(homeDir(), ".hermes", "skills"), skills, "dir");
|
|
2143
|
+
}
|
|
2144
|
+
scanForImports(_config, managedIndex) {
|
|
2145
|
+
const configPath = this.detectConfigPath();
|
|
2146
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2147
|
+
if (!configPath || !fs8.existsSync(configPath)) {
|
|
2148
|
+
return result;
|
|
2149
|
+
}
|
|
2150
|
+
const existing = fs8.readFileSync(configPath, "utf8");
|
|
2151
|
+
const doc = YAML.parse(existing) ?? {};
|
|
2152
|
+
const mcpServers = doc.mcp_servers ?? {};
|
|
2153
|
+
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
2154
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const parsed = hermesEntrySchema.safeParse(rawEntry);
|
|
2158
|
+
if (!parsed.success) {
|
|
2159
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
const entry = parsed.data;
|
|
2163
|
+
if (entry.auth === "oauth") {
|
|
2164
|
+
result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (entry.url && !entry.command) {
|
|
2168
|
+
result.candidates.push({
|
|
2169
|
+
clientId: this.id,
|
|
2170
|
+
configPath,
|
|
2171
|
+
sourceEntryName: name,
|
|
2172
|
+
serverName: name,
|
|
2173
|
+
spec: {
|
|
2174
|
+
transport: "http",
|
|
2175
|
+
url: entry.url,
|
|
2176
|
+
headers: entry.headers,
|
|
2177
|
+
enabled: entry.enabled !== false
|
|
2178
|
+
}
|
|
2179
|
+
});
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
if (entry.command && !entry.url) {
|
|
2183
|
+
result.candidates.push({
|
|
2184
|
+
clientId: this.id,
|
|
2185
|
+
configPath,
|
|
2186
|
+
sourceEntryName: name,
|
|
2187
|
+
serverName: name,
|
|
2188
|
+
spec: {
|
|
2189
|
+
transport: "stdio",
|
|
2190
|
+
command: entry.command,
|
|
2191
|
+
args: entry.args,
|
|
2192
|
+
env: entry.env,
|
|
2193
|
+
enabled: entry.enabled !== false
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1798
2199
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
2200
|
+
return result;
|
|
2201
|
+
}
|
|
2202
|
+
syncGateway(_config, options) {
|
|
2203
|
+
const configPath = this.detectConfigPath();
|
|
2204
|
+
if (!configPath) {
|
|
2205
|
+
return errorResult(this.id, undefined, "Unable to resolve Hermes config.yaml path.");
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const existing = fs8.existsSync(configPath) ? fs8.readFileSync(configPath, "utf8") : "";
|
|
2209
|
+
const doc = existing.trim().length > 0 ? YAML.parse(existing) ?? {} : {};
|
|
2210
|
+
const mcpServers = doc.mcp_servers ?? {};
|
|
2211
|
+
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
2212
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2213
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
2214
|
+
url: entry.url,
|
|
2215
|
+
headers: entry.headers,
|
|
2216
|
+
enabled: entry.enabled
|
|
2217
|
+
}]));
|
|
2218
|
+
for (const name of managedNames) {
|
|
2219
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
2220
|
+
if (conflict) {
|
|
2221
|
+
return errorResult(this.id, configPath, conflict);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
|
|
2225
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2226
|
+
mcpServers[name] = entry;
|
|
2227
|
+
}
|
|
2228
|
+
doc.mcp_servers = mcpServers;
|
|
2229
|
+
fs8.mkdirSync(path14.dirname(configPath), { recursive: true });
|
|
2230
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
2231
|
+
fs8.writeFileSync(tmpPath, YAML.stringify(doc), { mode: 384 });
|
|
2232
|
+
fs8.renameSync(tmpPath, configPath);
|
|
2233
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
2234
|
+
return okResult(this.id, configPath);
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
return errorResult(this.id, configPath, error.message);
|
|
1802
2237
|
}
|
|
1803
2238
|
}
|
|
1804
2239
|
}
|
|
@@ -1814,7 +2249,9 @@ function getAdapters() {
|
|
|
1814
2249
|
new OpenCodeAdapter,
|
|
1815
2250
|
new KiroAdapter,
|
|
1816
2251
|
new VsCodeAdapter,
|
|
1817
|
-
new QwenAdapter
|
|
2252
|
+
new QwenAdapter,
|
|
2253
|
+
new OpenClawAdapter,
|
|
2254
|
+
new HermesAdapter
|
|
1818
2255
|
];
|
|
1819
2256
|
}
|
|
1820
2257
|
|
|
@@ -1839,15 +2276,80 @@ function saveManagedIndex(index, filePath = getManagedIndexPath()) {
|
|
|
1839
2276
|
writeJsonAtomic(filePath, index);
|
|
1840
2277
|
}
|
|
1841
2278
|
|
|
2279
|
+
// src/core/managed-index-lock.ts
|
|
2280
|
+
import fs9 from "node:fs";
|
|
2281
|
+
import path15 from "node:path";
|
|
2282
|
+
function isStale(lockPath, maxAgeMs = 5000) {
|
|
2283
|
+
try {
|
|
2284
|
+
const stat = fs9.statSync(lockPath);
|
|
2285
|
+
return Date.now() - stat.mtimeMs > maxAgeMs;
|
|
2286
|
+
} catch {
|
|
2287
|
+
return true;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function withManagedIndexLock(lockPath, fn) {
|
|
2291
|
+
fs9.mkdirSync(path15.dirname(lockPath), { recursive: true });
|
|
2292
|
+
const deadline = Date.now() + 5000;
|
|
2293
|
+
while (true) {
|
|
2294
|
+
try {
|
|
2295
|
+
fs9.writeFileSync(lockPath, `${process.pid}
|
|
2296
|
+
`, { flag: "wx" });
|
|
2297
|
+
break;
|
|
2298
|
+
} catch {
|
|
2299
|
+
if (isStale(lockPath)) {
|
|
2300
|
+
try {
|
|
2301
|
+
fs9.unlinkSync(lockPath);
|
|
2302
|
+
} catch {}
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
if (Date.now() > deadline) {
|
|
2306
|
+
throw new Error(`Timed out waiting for managed-index lock: ${lockPath}`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
return fn();
|
|
2312
|
+
} finally {
|
|
2313
|
+
try {
|
|
2314
|
+
fs9.unlinkSync(lockPath);
|
|
2315
|
+
} catch {}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
1842
2319
|
// src/core/sync.ts
|
|
1843
2320
|
function getGatewayUrl(config) {
|
|
1844
2321
|
return `http://127.0.0.1:${config.gateway.port}/mcp`;
|
|
1845
2322
|
}
|
|
1846
2323
|
function buildProjectScopes(config) {
|
|
1847
2324
|
const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
|
|
2325
|
+
const pluginDisabledByProject = new Map;
|
|
2326
|
+
for (const [, plugin] of Object.entries(config.plugins ?? {})) {
|
|
2327
|
+
if (!plugin.projectOverrides)
|
|
2328
|
+
continue;
|
|
2329
|
+
for (const [overridePath, override] of Object.entries(plugin.projectOverrides)) {
|
|
2330
|
+
const disabled = override.enabled === false || override.components?.mcpServers === false;
|
|
2331
|
+
if (!disabled)
|
|
2332
|
+
continue;
|
|
2333
|
+
if (!pluginDisabledByProject.has(overridePath)) {
|
|
2334
|
+
pluginDisabledByProject.set(overridePath, new Set);
|
|
2335
|
+
}
|
|
2336
|
+
for (const serverName of plugin.serverNames) {
|
|
2337
|
+
pluginDisabledByProject.get(overridePath).add(managedGatewayEntryName(serverName));
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
1848
2341
|
return Object.values(config.projects ?? {}).map((project) => {
|
|
2342
|
+
const projectPath = project.path;
|
|
1849
2343
|
const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
|
|
1850
|
-
|
|
2344
|
+
const pluginDisabled = pluginDisabledByProject.get(projectPath);
|
|
2345
|
+
if (pluginDisabled) {
|
|
2346
|
+
for (const name of pluginDisabled) {
|
|
2347
|
+
if (!disabledServerNames.includes(name)) {
|
|
2348
|
+
disabledServerNames.push(name);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
return { path: projectPath, disabledServerNames };
|
|
1851
2353
|
});
|
|
1852
2354
|
}
|
|
1853
2355
|
function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
@@ -2014,62 +2516,64 @@ function runGatewayPhase(adapters, config, managedEntries, managedIndex, managed
|
|
|
2014
2516
|
function syncAllClients(config, secrets, options) {
|
|
2015
2517
|
const opts = Array.isArray(options) ? { targetClients: options, importScan: false } : { importScan: false, ...options };
|
|
2016
2518
|
const managedIndexPath = getManagedIndexPath();
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2519
|
+
return withManagedIndexLock(`${managedIndexPath}.lock`, () => {
|
|
2520
|
+
const managedIndex = loadManagedIndex(managedIndexPath);
|
|
2521
|
+
const adapters = getFilteredAdapters(opts.targetClients);
|
|
2522
|
+
const allPhaseErrors = [];
|
|
2523
|
+
let imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
|
|
2524
|
+
let sourceEntriesToRemove = new Map;
|
|
2525
|
+
if (opts.importScan) {
|
|
2526
|
+
const importResult = runImportPhase(adapters, config, managedIndex);
|
|
2527
|
+
imports = importResult.imports;
|
|
2528
|
+
sourceEntriesToRemove = importResult.sourceEntriesToRemove;
|
|
2529
|
+
allPhaseErrors.push(...importResult.phaseErrors ?? []);
|
|
2530
|
+
}
|
|
2531
|
+
const localToken = ensureGatewayToken(config, secrets);
|
|
2532
|
+
const gatewayUrl = getGatewayUrl(config);
|
|
2533
|
+
const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
|
|
2534
|
+
const gatewayResult = runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, buildProjectScopes(config));
|
|
2535
|
+
const results = gatewayResult.results;
|
|
2536
|
+
allPhaseErrors.push(...gatewayResult.phaseErrors ?? []);
|
|
2537
|
+
const skills = listSkills();
|
|
2538
|
+
const skillsErrors = runSkillsPhase(adapters, skills);
|
|
2539
|
+
allPhaseErrors.push(...skillsErrors ?? []);
|
|
2540
|
+
const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
|
|
2541
|
+
pluginId: p.id,
|
|
2542
|
+
pluginName: p.name,
|
|
2543
|
+
pluginRoot: p.root,
|
|
2544
|
+
components: p.components,
|
|
2545
|
+
approvals: p.approvals ?? {},
|
|
2546
|
+
enabled: p.enabled,
|
|
2547
|
+
serverNames: p.serverNames,
|
|
2548
|
+
skills: p.discovered.skills,
|
|
2549
|
+
commands: p.discovered.commands,
|
|
2550
|
+
agents: p.discovered.agents,
|
|
2551
|
+
hooks: p.discovered.hooks
|
|
2552
|
+
}));
|
|
2553
|
+
const pluginResult = runPluginsPhase(adapters, pluginInputs);
|
|
2554
|
+
const pluginResults = pluginResult.pluginResults;
|
|
2555
|
+
allPhaseErrors.push(...pluginResult.phaseErrors ?? []);
|
|
2556
|
+
saveManagedIndex(managedIndex, managedIndexPath);
|
|
2557
|
+
const clientStates = {};
|
|
2558
|
+
for (const result of results) {
|
|
2559
|
+
clientStates[result.clientId] = {
|
|
2560
|
+
status: result.status,
|
|
2561
|
+
configPath: result.configPath,
|
|
2562
|
+
message: result.message,
|
|
2563
|
+
lastSyncAt: new Date().toISOString()
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
const hasErrors = results.some((r) => r.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
|
|
2567
|
+
return {
|
|
2568
|
+
gatewayUrl,
|
|
2569
|
+
imports,
|
|
2570
|
+
results,
|
|
2571
|
+
hasErrors,
|
|
2572
|
+
clientStates,
|
|
2573
|
+
phaseErrors: allPhaseErrors.length > 0 ? allPhaseErrors : undefined,
|
|
2574
|
+
pluginResults: pluginResults.length > 0 ? pluginResults : undefined
|
|
2061
2575
|
};
|
|
2062
|
-
}
|
|
2063
|
-
const hasErrors = results.some((r) => r.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
|
|
2064
|
-
return {
|
|
2065
|
-
gatewayUrl,
|
|
2066
|
-
imports,
|
|
2067
|
-
results,
|
|
2068
|
-
hasErrors,
|
|
2069
|
-
clientStates,
|
|
2070
|
-
phaseErrors: allPhaseErrors.length > 0 ? allPhaseErrors : undefined,
|
|
2071
|
-
pluginResults: pluginResults.length > 0 ? pluginResults : undefined
|
|
2072
|
-
};
|
|
2576
|
+
});
|
|
2073
2577
|
}
|
|
2074
2578
|
function persistSyncState(summary, config) {
|
|
2075
2579
|
if (!summary.clientStates)
|
|
@@ -2079,4 +2583,4 @@ function persistSyncState(summary, config) {
|
|
|
2079
2583
|
}
|
|
2080
2584
|
}
|
|
2081
2585
|
|
|
2082
|
-
export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };
|
|
2586
|
+
export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, APP_VERSION, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };
|