@kwonye/mcpx 0.1.93 → 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 +18 -4
- 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-y7ba5qvr.js → cli-28fsxhe3.js} +430 -156
- package/dist/{cli-qvkpwy9w.js → cli-njtbb11m.js} +138 -43
- package/dist/{cli-8vxva0cb.js → cli-tk8bggg5.js} +81 -8
- package/dist/{cli-np0wwh6m.js → cli-v27bt25d.js} +157 -11
- package/dist/{cli-kwyp9ywh.js → cli-wxw1t3rr.js} +941 -303
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +388 -77
- 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-2s5nsm2g.js → config-5p08cb0c.js} +6 -2
- package/dist/core/add-command.d.ts.map +1 -1
- package/dist/core/config-repair.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +33 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config.d.ts +13 -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/errors.d.ts +18 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +30 -6
- 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 +6 -2
- package/dist/core/plugin-projections.d.ts.map +1 -1
- package/dist/core/registry.d.ts +4 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/secrets.d.ts.map +1 -1
- package/dist/core/server-auth.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 +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts +19 -2
- 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 +4 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/plugin-host-954a7tf7.js +129 -0
- package/dist/{plugin-manager-1sc4b6am.js → plugin-manager-m8ar2271.js} +12 -4
- package/dist/{registry-mztfy0cz.js → registry-n12thgf2.js} +1 -1
- package/dist/types.d.ts +5 -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-m2645syd.js +0 -2199
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
saveConfig
|
|
3
|
-
} from "./cli-np0wwh6m.js";
|
|
4
1
|
import {
|
|
5
2
|
ensureGatewayToken
|
|
6
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-tk8bggg5.js";
|
|
7
4
|
import {
|
|
8
5
|
ensureDir,
|
|
9
6
|
getManagedIndexPath,
|
|
@@ -71,14 +68,15 @@ function loadOwnership(targetDir) {
|
|
|
71
68
|
try {
|
|
72
69
|
return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
|
|
73
70
|
} catch {
|
|
71
|
+
console.error(`[mcpx] Warning: corrupt plugin ownership manifest at ${manifestPath}, resetting`);
|
|
74
72
|
return { version: 1, plugins: {} };
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
75
|
function saveOwnership(targetDir, manifest) {
|
|
76
|
+
ensureDir(targetDir);
|
|
78
77
|
fs2.writeFileSync(path2.join(targetDir, OWNERSHIP_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
79
78
|
}
|
|
80
79
|
function recordOwned(targetDir, pluginId, paths) {
|
|
81
|
-
ensureDir(targetDir);
|
|
82
80
|
const manifest = loadOwnership(targetDir);
|
|
83
81
|
manifest.plugins[pluginId] = {
|
|
84
82
|
pluginId,
|
|
@@ -114,16 +112,49 @@ function pruneAllPluginProjections(targetDir) {
|
|
|
114
112
|
prunePluginProjections(targetDir, pluginId);
|
|
115
113
|
}
|
|
116
114
|
}
|
|
115
|
+
function pruneUnlistedPlugins(targetDir, keepPluginIds) {
|
|
116
|
+
const manifest = loadOwnership(targetDir);
|
|
117
|
+
const keep = new Set(keepPluginIds);
|
|
118
|
+
for (const [pluginId] of Object.entries(manifest.plugins)) {
|
|
119
|
+
if (!keep.has(pluginId)) {
|
|
120
|
+
prunePluginProjections(targetDir, pluginId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
function nsName(pluginName, id) {
|
|
118
125
|
return `${pluginName}__${id}`;
|
|
119
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
|
+
}
|
|
134
|
+
function isComponentApproved(plugin, component) {
|
|
135
|
+
const approval = plugin.approvals?.[component];
|
|
136
|
+
if (approval === false)
|
|
137
|
+
return false;
|
|
138
|
+
if (approval === true)
|
|
139
|
+
return true;
|
|
140
|
+
if (component === "hooks")
|
|
141
|
+
return false;
|
|
142
|
+
if (component === "mcpServers")
|
|
143
|
+
return plugin.components.mcpServers;
|
|
144
|
+
return plugin.components[component] ?? false;
|
|
145
|
+
}
|
|
120
146
|
function syncClaude(plugins) {
|
|
121
147
|
const targetBase = path2.join(homeDir(), ".claude", "skills");
|
|
122
148
|
const projectedDirs = [];
|
|
123
149
|
const unsupported = [];
|
|
150
|
+
const skippedUnapproved = [];
|
|
124
151
|
for (const plugin of plugins) {
|
|
125
152
|
if (!plugin.enabled)
|
|
126
153
|
continue;
|
|
154
|
+
if (!isComponentApproved(plugin, "skills")) {
|
|
155
|
+
skippedUnapproved.push(`${plugin.pluginName}:skills`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
127
158
|
const targetDir = path2.join(targetBase, plugin.pluginName);
|
|
128
159
|
ensureDir(targetDir);
|
|
129
160
|
const mcpJsonSrc = path2.join(plugin.pluginRoot, ".mcp.json");
|
|
@@ -155,25 +186,34 @@ function syncCodex(plugins) {
|
|
|
155
186
|
const targetBase = path2.join(homeDir(), ".codex", "skills");
|
|
156
187
|
const projectedDirs = [];
|
|
157
188
|
const unsupported = ["hooks", "agents"];
|
|
189
|
+
const errors = [];
|
|
158
190
|
for (const plugin of plugins) {
|
|
159
|
-
if (!plugin.enabled
|
|
191
|
+
if (!plugin.enabled)
|
|
160
192
|
continue;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
193
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
194
|
+
ensureDir(targetBase);
|
|
195
|
+
const owned = [];
|
|
196
|
+
for (const skill of plugin.skills) {
|
|
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
|
+
}
|
|
207
|
+
}
|
|
208
|
+
recordOwned(targetBase, plugin.pluginId, owned);
|
|
169
209
|
}
|
|
170
|
-
recordOwned(targetBase, plugin.pluginId, owned);
|
|
171
210
|
}
|
|
172
211
|
return {
|
|
173
212
|
clientId: "codex",
|
|
174
213
|
status: "SYNCED",
|
|
175
214
|
projectedDirs,
|
|
176
|
-
unsupported
|
|
215
|
+
unsupported,
|
|
216
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
177
217
|
};
|
|
178
218
|
}
|
|
179
219
|
function syncCursor(plugins) {
|
|
@@ -181,28 +221,39 @@ function syncCursor(plugins) {
|
|
|
181
221
|
const commandsBase = path2.join(homeDir(), ".cursor", "commands");
|
|
182
222
|
const projectedDirs = [];
|
|
183
223
|
const unsupported = ["hooks", "agents"];
|
|
184
|
-
const
|
|
185
|
-
const ownedCommands = [];
|
|
224
|
+
const errors = [];
|
|
186
225
|
for (const plugin of plugins) {
|
|
187
226
|
if (!plugin.enabled)
|
|
188
227
|
continue;
|
|
189
|
-
|
|
228
|
+
const ownedSkills = [];
|
|
229
|
+
const ownedCommands = [];
|
|
230
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
190
231
|
ensureDir(skillsBase);
|
|
191
232
|
for (const skill of plugin.skills) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
243
|
}
|
|
198
244
|
}
|
|
199
|
-
if (plugin.components.commands) {
|
|
245
|
+
if (plugin.components.commands && isComponentApproved(plugin, "commands")) {
|
|
200
246
|
ensureDir(commandsBase);
|
|
201
247
|
for (const cmd of plugin.commands) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
}
|
|
206
257
|
}
|
|
207
258
|
}
|
|
208
259
|
if (ownedSkills.length > 0)
|
|
@@ -214,151 +265,281 @@ function syncCursor(plugins) {
|
|
|
214
265
|
clientId: "cursor",
|
|
215
266
|
status: "SYNCED",
|
|
216
267
|
projectedDirs,
|
|
217
|
-
unsupported
|
|
268
|
+
unsupported,
|
|
269
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
218
270
|
};
|
|
219
271
|
}
|
|
220
272
|
function syncVsCode(plugins) {
|
|
221
273
|
const skillsBase = path2.join(homeDir(), ".copilot", "skills");
|
|
222
274
|
const projectedDirs = [];
|
|
223
275
|
const unsupported = ["hooks", "agents"];
|
|
276
|
+
const errors = [];
|
|
224
277
|
for (const plugin of plugins) {
|
|
225
|
-
if (!plugin.enabled
|
|
278
|
+
if (!plugin.enabled)
|
|
226
279
|
continue;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
280
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
281
|
+
ensureDir(skillsBase);
|
|
282
|
+
const owned = [];
|
|
283
|
+
for (const skill of plugin.skills) {
|
|
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
|
+
}
|
|
294
|
+
}
|
|
295
|
+
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
235
296
|
}
|
|
236
|
-
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
237
297
|
}
|
|
238
298
|
return {
|
|
239
299
|
clientId: "vscode",
|
|
240
300
|
status: "SYNCED",
|
|
241
301
|
projectedDirs,
|
|
242
|
-
unsupported
|
|
302
|
+
unsupported,
|
|
303
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
243
304
|
};
|
|
244
305
|
}
|
|
245
|
-
function
|
|
246
|
-
const
|
|
306
|
+
function syncQwen(plugins) {
|
|
307
|
+
const targetBase = path2.join(homeDir(), ".qwen", "skills");
|
|
247
308
|
const projectedDirs = [];
|
|
248
309
|
const unsupported = ["hooks", "agents", "commands"];
|
|
310
|
+
const errors = [];
|
|
249
311
|
for (const plugin of plugins) {
|
|
250
|
-
if (!plugin.enabled
|
|
312
|
+
if (!plugin.enabled)
|
|
251
313
|
continue;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
314
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
315
|
+
ensureDir(targetBase);
|
|
316
|
+
const owned = [];
|
|
317
|
+
for (const skill of plugin.skills) {
|
|
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
|
+
}
|
|
328
|
+
}
|
|
329
|
+
recordOwned(targetBase, plugin.pluginId, owned);
|
|
259
330
|
}
|
|
260
|
-
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
261
331
|
}
|
|
262
332
|
return {
|
|
263
|
-
clientId: "
|
|
333
|
+
clientId: "qwen",
|
|
264
334
|
status: "SYNCED",
|
|
265
335
|
projectedDirs,
|
|
266
|
-
unsupported
|
|
336
|
+
unsupported,
|
|
337
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
267
338
|
};
|
|
268
339
|
}
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
const commandsBase = path2.join(homeDir(), ".qwen", "commands");
|
|
340
|
+
function syncCline(plugins) {
|
|
341
|
+
const targetBase = path2.join(homeDir(), ".config", "cline", "skills");
|
|
272
342
|
const projectedDirs = [];
|
|
273
|
-
const unsupported = ["hooks", "agents"];
|
|
274
|
-
const
|
|
275
|
-
const ownedCommands = [];
|
|
343
|
+
const unsupported = ["hooks", "agents", "commands"];
|
|
344
|
+
const errors = [];
|
|
276
345
|
for (const plugin of plugins) {
|
|
277
346
|
if (!plugin.enabled)
|
|
278
347
|
continue;
|
|
279
|
-
if (plugin.components.skills) {
|
|
280
|
-
ensureDir(
|
|
348
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
349
|
+
ensureDir(targetBase);
|
|
350
|
+
const owned = [];
|
|
281
351
|
for (const skill of plugin.skills) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
362
|
}
|
|
363
|
+
recordOwned(targetBase, plugin.pluginId, owned);
|
|
287
364
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
clientId: "cline",
|
|
368
|
+
status: "SYNCED",
|
|
369
|
+
projectedDirs,
|
|
370
|
+
unsupported,
|
|
371
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function syncKiro(plugins) {
|
|
375
|
+
const targetBase = path2.join(homeDir(), ".kiro", "skills");
|
|
376
|
+
const projectedDirs = [];
|
|
377
|
+
const unsupported = ["hooks", "agents", "commands"];
|
|
378
|
+
const errors = [];
|
|
379
|
+
for (const plugin of plugins) {
|
|
380
|
+
if (!plugin.enabled)
|
|
381
|
+
continue;
|
|
382
|
+
if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
|
|
383
|
+
ensureDir(targetBase);
|
|
384
|
+
const owned = [];
|
|
385
|
+
for (const skill of plugin.skills) {
|
|
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
|
+
}
|
|
295
396
|
}
|
|
397
|
+
recordOwned(targetBase, plugin.pluginId, owned);
|
|
296
398
|
}
|
|
297
|
-
if (ownedSkills.length > 0)
|
|
298
|
-
recordOwned(skillsBase, plugin.pluginId, ownedSkills);
|
|
299
|
-
if (ownedCommands.length > 0)
|
|
300
|
-
recordOwned(commandsBase, plugin.pluginId, ownedCommands);
|
|
301
399
|
}
|
|
302
400
|
return {
|
|
303
|
-
clientId: "
|
|
401
|
+
clientId: "kiro",
|
|
304
402
|
status: "SYNCED",
|
|
305
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: [],
|
|
306
414
|
unsupported
|
|
307
415
|
};
|
|
308
416
|
}
|
|
309
|
-
var CLIENT_SYNCERS = {
|
|
310
|
-
claude: syncClaude,
|
|
311
|
-
"claude-desktop": syncClaudeDesktop,
|
|
312
|
-
codex: syncCodex,
|
|
313
|
-
cursor: syncCursor,
|
|
314
|
-
vscode: syncVsCode,
|
|
315
|
-
kiro: syncKiro,
|
|
316
|
-
qwen: syncQwen
|
|
317
|
-
};
|
|
318
417
|
function syncPluginsToClient(clientId, plugins) {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
418
|
+
const syncMap = {
|
|
419
|
+
claude: syncClaude,
|
|
420
|
+
"claude-desktop": syncClaudeDesktop,
|
|
421
|
+
codex: syncCodex,
|
|
422
|
+
cursor: syncCursor,
|
|
423
|
+
vscode: syncVsCode,
|
|
424
|
+
qwen: syncQwen,
|
|
425
|
+
cline: syncCline,
|
|
426
|
+
kiro: syncKiro,
|
|
427
|
+
opencode: syncOpenCode
|
|
428
|
+
};
|
|
429
|
+
const syncFn = syncMap[clientId];
|
|
430
|
+
if (!syncFn) {
|
|
431
|
+
return { clientId, status: "SKIPPED", projectedDirs: [], unsupported: [] };
|
|
432
|
+
}
|
|
433
|
+
const targetBaseMap = {
|
|
434
|
+
claude: path2.join(homeDir(), ".claude", "skills"),
|
|
435
|
+
"claude-desktop": "",
|
|
436
|
+
codex: path2.join(homeDir(), ".codex", "skills"),
|
|
437
|
+
cursor: path2.join(homeDir(), ".cursor"),
|
|
438
|
+
vscode: path2.join(homeDir(), ".copilot", "skills"),
|
|
439
|
+
qwen: path2.join(homeDir(), ".qwen", "skills"),
|
|
440
|
+
cline: path2.join(homeDir(), ".config", "cline", "skills"),
|
|
441
|
+
kiro: path2.join(homeDir(), ".kiro", "skills"),
|
|
442
|
+
opencode: ""
|
|
443
|
+
};
|
|
444
|
+
const target = targetBaseMap[clientId];
|
|
445
|
+
if (target) {
|
|
446
|
+
const enabledIds = plugins.filter((p) => p.enabled).map((p) => p.pluginId);
|
|
447
|
+
pruneUnlistedPlugins(target, enabledIds);
|
|
328
448
|
}
|
|
329
|
-
return
|
|
449
|
+
return syncFn(plugins);
|
|
330
450
|
}
|
|
331
|
-
function copyPluginDir(src, dest, exclude = []) {
|
|
451
|
+
function copyPluginDir(src, dest, exclude = [], errors) {
|
|
452
|
+
const excludeSet = new Set(exclude);
|
|
332
453
|
ensureDir(dest);
|
|
333
454
|
const entries = fs2.readdirSync(src, { withFileTypes: true });
|
|
334
455
|
for (const entry of entries) {
|
|
335
|
-
if (
|
|
336
|
-
continue;
|
|
337
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin")
|
|
456
|
+
if (excludeSet.has(entry.name))
|
|
338
457
|
continue;
|
|
339
458
|
const srcPath = path2.join(src, entry.name);
|
|
340
459
|
const destPath = path2.join(dest, entry.name);
|
|
341
460
|
if (entry.isDirectory()) {
|
|
342
|
-
copyPluginDir(srcPath, destPath, exclude);
|
|
343
|
-
} else {
|
|
344
|
-
|
|
345
|
-
|
|
461
|
+
copyPluginDir(srcPath, destPath, exclude, errors);
|
|
462
|
+
} else if (entry.isFile()) {
|
|
463
|
+
try {
|
|
464
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
errors?.push(`Failed to copy ${srcPath}: ${e.message}`);
|
|
467
|
+
}
|
|
346
468
|
}
|
|
347
469
|
}
|
|
348
470
|
}
|
|
349
|
-
function copyFileOrDir(src, dest) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
471
|
+
function copyFileOrDir(src, dest, errors) {
|
|
472
|
+
try {
|
|
473
|
+
ensureDir(path2.dirname(dest));
|
|
474
|
+
if (fs2.statSync(src).isDirectory()) {
|
|
475
|
+
copyPluginDir(src, dest, [], errors);
|
|
476
|
+
} else {
|
|
477
|
+
fs2.copyFileSync(src, dest);
|
|
478
|
+
}
|
|
479
|
+
} catch (e) {
|
|
480
|
+
errors?.push(`Failed to copy ${src} -> ${dest}: ${e.message}`);
|
|
355
481
|
}
|
|
356
482
|
}
|
|
357
483
|
|
|
484
|
+
// src/version.ts
|
|
485
|
+
var APP_VERSION = "0.1.3";
|
|
486
|
+
|
|
358
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
|
|
359
493
|
import fs3 from "node:fs";
|
|
360
494
|
import path3 from "node:path";
|
|
361
|
-
|
|
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
|
+
}
|
|
362
543
|
|
|
363
544
|
// src/adapters/utils/index.ts
|
|
364
545
|
function getManagedEntryNames(managedIndex, clientId) {
|
|
@@ -381,15 +562,35 @@ function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryName
|
|
|
381
562
|
}
|
|
382
563
|
}
|
|
383
564
|
}
|
|
384
|
-
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
|
|
565
|
+
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue, expectedEntry) {
|
|
385
566
|
if (existingValue === undefined || existingValue === null) {
|
|
386
567
|
return null;
|
|
387
568
|
}
|
|
388
569
|
if (isManagedEntry(managedIndex, clientId, entryName)) {
|
|
389
570
|
return null;
|
|
390
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
|
+
}
|
|
391
582
|
return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
|
|
392
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
|
+
}
|
|
393
594
|
function setManagedEntries(managedIndex, clientId, configPath, entries) {
|
|
394
595
|
if (!managedIndex.managed[clientId]) {
|
|
395
596
|
managedIndex.managed[clientId] = {
|
|
@@ -475,25 +676,36 @@ function removeSourceEntries(entries, names) {
|
|
|
475
676
|
delete entries[name];
|
|
476
677
|
}
|
|
477
678
|
}
|
|
478
|
-
function
|
|
479
|
-
const existingDisabled =
|
|
480
|
-
|
|
481
|
-
raw.disabledMcpServers
|
|
679
|
+
function purgeManagedFromDisabledArray(raw, managedNames) {
|
|
680
|
+
const existingDisabled = raw.disabledMcpServers ?? [];
|
|
681
|
+
raw.disabledMcpServers = existingDisabled.filter((name) => !managedNames.includes(name));
|
|
682
|
+
if (Array.isArray(raw.disabledMcpServers) && raw.disabledMcpServers.length === 0) {
|
|
683
|
+
delete raw.disabledMcpServers;
|
|
684
|
+
}
|
|
482
685
|
}
|
|
483
|
-
function
|
|
686
|
+
function purgeManagedFromExcludedArray(raw, managedNames) {
|
|
484
687
|
const mcp = raw.mcp ?? {};
|
|
485
|
-
const existingExcluded =
|
|
486
|
-
|
|
487
|
-
mcp.excluded
|
|
488
|
-
|
|
688
|
+
const existingExcluded = mcp.excluded ?? [];
|
|
689
|
+
mcp.excluded = existingExcluded.filter((name) => !managedNames.includes(name));
|
|
690
|
+
if (Array.isArray(mcp.excluded) && mcp.excluded.length === 0) {
|
|
691
|
+
delete mcp.excluded;
|
|
692
|
+
}
|
|
693
|
+
raw.mcp = Object.keys(mcp).length > 0 ? mcp : undefined;
|
|
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;
|
|
489
701
|
}
|
|
490
702
|
|
|
491
703
|
// src/adapters/claude.ts
|
|
492
704
|
function normalizeProjectKey(p) {
|
|
493
705
|
try {
|
|
494
|
-
return
|
|
706
|
+
return fs4.realpathSync(path4.resolve(p));
|
|
495
707
|
} catch {
|
|
496
|
-
return
|
|
708
|
+
return path4.resolve(p);
|
|
497
709
|
}
|
|
498
710
|
}
|
|
499
711
|
function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
|
|
@@ -520,7 +732,7 @@ var claudeEntrySchema = z.object({
|
|
|
520
732
|
class ClaudeAdapter {
|
|
521
733
|
id = "claude";
|
|
522
734
|
detectConfigPath() {
|
|
523
|
-
return
|
|
735
|
+
return path4.join(homeDir(), ".claude.json");
|
|
524
736
|
}
|
|
525
737
|
supportsHttp() {
|
|
526
738
|
return true;
|
|
@@ -528,6 +740,9 @@ class ClaudeAdapter {
|
|
|
528
740
|
syncPlugins(plugins) {
|
|
529
741
|
return syncPluginsToClient(this.id, plugins);
|
|
530
742
|
}
|
|
743
|
+
syncSkills(skills) {
|
|
744
|
+
projectSkillsToDir(path4.join(homeDir(), ".claude", "skills"), skills, "dir");
|
|
745
|
+
}
|
|
531
746
|
scanForImports(_config, managedIndex) {
|
|
532
747
|
const configPath = this.detectConfigPath();
|
|
533
748
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -608,6 +823,7 @@ class ClaudeAdapter {
|
|
|
608
823
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
609
824
|
const topLevelServers = raw.mcpServers ?? {};
|
|
610
825
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
826
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
611
827
|
for (const name of enabledManagedNames) {
|
|
612
828
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
613
829
|
if (topLevelConflict) {
|
|
@@ -619,7 +835,7 @@ class ClaudeAdapter {
|
|
|
619
835
|
topLevelServers[name] = entry;
|
|
620
836
|
}
|
|
621
837
|
raw.mcpServers = topLevelServers;
|
|
622
|
-
|
|
838
|
+
purgeManagedFromDisabledArray(raw, managedNames);
|
|
623
839
|
const scopeByNormalizedPath = new Map;
|
|
624
840
|
for (const scope of options.projectScopes ?? []) {
|
|
625
841
|
scopeByNormalizedPath.set(normalizeProjectKey(scope.path), scope.disabledServerNames);
|
|
@@ -643,7 +859,7 @@ class ClaudeAdapter {
|
|
|
643
859
|
raw.projects = projects;
|
|
644
860
|
writeJsonAtomic(configPath, raw);
|
|
645
861
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
646
|
-
return okResult(this.id, configPath);
|
|
862
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
647
863
|
} catch (error) {
|
|
648
864
|
return errorResult(this.id, configPath, error.message);
|
|
649
865
|
}
|
|
@@ -652,7 +868,7 @@ class ClaudeAdapter {
|
|
|
652
868
|
|
|
653
869
|
// src/adapters/claude-desktop.ts
|
|
654
870
|
import { execFileSync } from "node:child_process";
|
|
655
|
-
import
|
|
871
|
+
import path5 from "node:path";
|
|
656
872
|
import { z as z2 } from "zod";
|
|
657
873
|
var stringMapSchema2 = z2.record(z2.string(), z2.string());
|
|
658
874
|
var claudeDesktopEntrySchema = z2.object({
|
|
@@ -672,19 +888,19 @@ function buildProxyEntry(entry) {
|
|
|
672
888
|
if (mcpxPath)
|
|
673
889
|
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
674
890
|
} catch {}
|
|
675
|
-
return { command: "npx", args: ["-y",
|
|
891
|
+
return { command: "npx", args: ["-y", `@kwonye/mcpx@${APP_VERSION}`, "proxy", upstreamName] };
|
|
676
892
|
}
|
|
677
893
|
|
|
678
894
|
class ClaudeDesktopAdapter {
|
|
679
895
|
id = "claude-desktop";
|
|
680
896
|
detectConfigPath() {
|
|
681
897
|
if (process.platform === "linux") {
|
|
682
|
-
return
|
|
898
|
+
return path5.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
683
899
|
}
|
|
684
900
|
if (process.platform === "win32") {
|
|
685
|
-
return
|
|
901
|
+
return path5.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
686
902
|
}
|
|
687
|
-
return
|
|
903
|
+
return path5.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
688
904
|
}
|
|
689
905
|
supportsHttp() {
|
|
690
906
|
return true;
|
|
@@ -761,6 +977,7 @@ class ClaudeDesktopAdapter {
|
|
|
761
977
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
762
978
|
const topLevelServers = raw.mcpServers ?? {};
|
|
763
979
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
980
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
764
981
|
for (const name of enabledManagedNames) {
|
|
765
982
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
766
983
|
if (topLevelConflict) {
|
|
@@ -777,7 +994,7 @@ class ClaudeDesktopAdapter {
|
|
|
777
994
|
const { command, args } = buildProxyEntry(entry);
|
|
778
995
|
return [entry.name, JSON.stringify({ command, args })];
|
|
779
996
|
})));
|
|
780
|
-
return okResult(this.id, configPath);
|
|
997
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
781
998
|
} catch (error) {
|
|
782
999
|
return errorResult(this.id, configPath, error.message);
|
|
783
1000
|
}
|
|
@@ -785,8 +1002,8 @@ class ClaudeDesktopAdapter {
|
|
|
785
1002
|
}
|
|
786
1003
|
|
|
787
1004
|
// src/adapters/codex.ts
|
|
788
|
-
import
|
|
789
|
-
import
|
|
1005
|
+
import path6 from "node:path";
|
|
1006
|
+
import fs5 from "node:fs";
|
|
790
1007
|
import { parse, stringify } from "@iarna/toml";
|
|
791
1008
|
import { z as z3 } from "zod";
|
|
792
1009
|
var stringMapSchema3 = z3.preprocess((value) => {
|
|
@@ -809,7 +1026,7 @@ var codexEntrySchema = z3.object({
|
|
|
809
1026
|
class CodexAdapter {
|
|
810
1027
|
id = "codex";
|
|
811
1028
|
detectConfigPath() {
|
|
812
|
-
return
|
|
1029
|
+
return path6.join(homeDir(), ".codex", "config.toml");
|
|
813
1030
|
}
|
|
814
1031
|
supportsHttp() {
|
|
815
1032
|
return true;
|
|
@@ -817,13 +1034,16 @@ class CodexAdapter {
|
|
|
817
1034
|
syncPlugins(plugins) {
|
|
818
1035
|
return syncPluginsToClient(this.id, plugins);
|
|
819
1036
|
}
|
|
1037
|
+
syncSkills(skills) {
|
|
1038
|
+
projectSkillsToDir(path6.join(homeDir(), ".codex", "skills"), skills, "dir");
|
|
1039
|
+
}
|
|
820
1040
|
scanForImports(_config, managedIndex) {
|
|
821
1041
|
const configPath = this.detectConfigPath();
|
|
822
1042
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
823
|
-
if (!configPath || !
|
|
1043
|
+
if (!configPath || !fs5.existsSync(configPath)) {
|
|
824
1044
|
return result;
|
|
825
1045
|
}
|
|
826
|
-
const existing =
|
|
1046
|
+
const existing = fs5.readFileSync(configPath, "utf8");
|
|
827
1047
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
828
1048
|
const mcpServers = doc.mcp_servers ?? {};
|
|
829
1049
|
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
@@ -878,33 +1098,35 @@ class CodexAdapter {
|
|
|
878
1098
|
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
879
1099
|
}
|
|
880
1100
|
try {
|
|
881
|
-
const existing =
|
|
1101
|
+
const existing = fs5.existsSync(configPath) ? fs5.readFileSync(configPath, "utf8") : "";
|
|
882
1102
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
883
1103
|
const mcpServers = doc.mcp_servers ?? {};
|
|
884
1104
|
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
885
1105
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
886
|
-
const
|
|
887
|
-
|
|
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, {
|
|
888
1109
|
url: entry.url,
|
|
889
1110
|
http_headers: entry.headers
|
|
890
1111
|
}]));
|
|
891
|
-
|
|
1112
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcpServers[name]));
|
|
1113
|
+
for (const name of enabledManagedNames) {
|
|
892
1114
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
893
1115
|
if (conflict) {
|
|
894
1116
|
return errorResult(this.id, configPath, conflict);
|
|
895
1117
|
}
|
|
896
1118
|
}
|
|
897
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers,
|
|
1119
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, enabledManagedNames);
|
|
898
1120
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
899
1121
|
mcpServers[name] = entry;
|
|
900
1122
|
}
|
|
901
1123
|
doc.mcp_servers = mcpServers;
|
|
902
|
-
|
|
1124
|
+
fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
|
|
903
1125
|
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
904
|
-
|
|
905
|
-
|
|
1126
|
+
fs5.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
1127
|
+
fs5.renameSync(tmpPath, configPath);
|
|
906
1128
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
907
|
-
return okResult(this.id, configPath);
|
|
1129
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
908
1130
|
} catch (error) {
|
|
909
1131
|
return errorResult(this.id, configPath, error.message);
|
|
910
1132
|
}
|
|
@@ -912,7 +1134,7 @@ class CodexAdapter {
|
|
|
912
1134
|
}
|
|
913
1135
|
|
|
914
1136
|
// src/adapters/cursor.ts
|
|
915
|
-
import
|
|
1137
|
+
import path7 from "node:path";
|
|
916
1138
|
import { z as z4 } from "zod";
|
|
917
1139
|
var stringMapSchema4 = z4.record(z4.string(), z4.string());
|
|
918
1140
|
var cursorEntrySchema = z4.object({
|
|
@@ -929,7 +1151,7 @@ var cursorEntrySchema = z4.object({
|
|
|
929
1151
|
class CursorAdapter {
|
|
930
1152
|
id = "cursor";
|
|
931
1153
|
detectConfigPath() {
|
|
932
|
-
return
|
|
1154
|
+
return path7.join(homeDir(), ".cursor", "mcp.json");
|
|
933
1155
|
}
|
|
934
1156
|
supportsHttp() {
|
|
935
1157
|
return true;
|
|
@@ -937,6 +1159,9 @@ class CursorAdapter {
|
|
|
937
1159
|
syncPlugins(plugins) {
|
|
938
1160
|
return syncPluginsToClient(this.id, plugins);
|
|
939
1161
|
}
|
|
1162
|
+
syncSkills(skills) {
|
|
1163
|
+
projectSkillsToDir(path7.join(homeDir(), ".cursor", "skills"), skills, "dir");
|
|
1164
|
+
}
|
|
940
1165
|
scanForImports(_config, managedIndex) {
|
|
941
1166
|
const configPath = this.detectConfigPath();
|
|
942
1167
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1011,6 +1236,7 @@ class CursorAdapter {
|
|
|
1011
1236
|
headers: entry.headers
|
|
1012
1237
|
}]));
|
|
1013
1238
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1239
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1014
1240
|
for (const name of enabledManagedNames) {
|
|
1015
1241
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1016
1242
|
if (conflict) {
|
|
@@ -1030,7 +1256,7 @@ class CursorAdapter {
|
|
|
1030
1256
|
entry.name,
|
|
1031
1257
|
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1032
1258
|
])));
|
|
1033
|
-
return okResult(this.id, configPath);
|
|
1259
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1034
1260
|
} catch (error) {
|
|
1035
1261
|
return errorResult(this.id, configPath, error.message);
|
|
1036
1262
|
}
|
|
@@ -1038,8 +1264,8 @@ class CursorAdapter {
|
|
|
1038
1264
|
}
|
|
1039
1265
|
|
|
1040
1266
|
// src/adapters/cline.ts
|
|
1041
|
-
import
|
|
1042
|
-
import
|
|
1267
|
+
import path8 from "node:path";
|
|
1268
|
+
import fs6 from "node:fs";
|
|
1043
1269
|
import { z as z5 } from "zod";
|
|
1044
1270
|
var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
|
|
1045
1271
|
var stringMapSchema5 = z5.record(z5.string(), z5.string());
|
|
@@ -1057,22 +1283,22 @@ var clineEntrySchema = z5.object({
|
|
|
1057
1283
|
function getCandidatePaths() {
|
|
1058
1284
|
if (process.platform === "linux") {
|
|
1059
1285
|
return [
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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")
|
|
1063
1289
|
];
|
|
1064
1290
|
}
|
|
1065
1291
|
if (process.platform === "win32") {
|
|
1066
1292
|
return [
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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")
|
|
1070
1296
|
];
|
|
1071
1297
|
}
|
|
1072
1298
|
return [
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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")
|
|
1076
1302
|
];
|
|
1077
1303
|
}
|
|
1078
1304
|
|
|
@@ -1081,7 +1307,7 @@ class ClineAdapter {
|
|
|
1081
1307
|
detectConfigPath() {
|
|
1082
1308
|
const candidates = getCandidatePaths();
|
|
1083
1309
|
for (const candidate of candidates) {
|
|
1084
|
-
if (
|
|
1310
|
+
if (fs6.existsSync(candidate)) {
|
|
1085
1311
|
return candidate;
|
|
1086
1312
|
}
|
|
1087
1313
|
}
|
|
@@ -1090,10 +1316,16 @@ class ClineAdapter {
|
|
|
1090
1316
|
supportsHttp() {
|
|
1091
1317
|
return true;
|
|
1092
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
|
+
}
|
|
1093
1325
|
scanForImports(_config, managedIndex) {
|
|
1094
1326
|
const configPath = this.detectConfigPath();
|
|
1095
1327
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1096
|
-
if (!configPath || !
|
|
1328
|
+
if (!configPath || !fs6.existsSync(configPath)) {
|
|
1097
1329
|
return result;
|
|
1098
1330
|
}
|
|
1099
1331
|
const raw = readJsonFile(configPath, {});
|
|
@@ -1152,7 +1384,7 @@ class ClineAdapter {
|
|
|
1152
1384
|
if (!configPath) {
|
|
1153
1385
|
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1154
1386
|
}
|
|
1155
|
-
if (!
|
|
1387
|
+
if (!fs6.existsSync(configPath)) {
|
|
1156
1388
|
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1157
1389
|
}
|
|
1158
1390
|
try {
|
|
@@ -1162,19 +1394,21 @@ class ClineAdapter {
|
|
|
1162
1394
|
};
|
|
1163
1395
|
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1164
1396
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1165
|
-
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, {
|
|
1166
1400
|
type: "streamableHttp",
|
|
1167
1401
|
url: entry.url,
|
|
1168
|
-
headers: entry.headers
|
|
1169
|
-
disabled: !entry.enabled
|
|
1402
|
+
headers: entry.headers
|
|
1170
1403
|
}]));
|
|
1171
|
-
|
|
1404
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1405
|
+
for (const name of enabledManagedNames) {
|
|
1172
1406
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1173
1407
|
if (conflict) {
|
|
1174
1408
|
return errorResult(this.id, configPath, conflict);
|
|
1175
1409
|
}
|
|
1176
1410
|
}
|
|
1177
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers,
|
|
1411
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1178
1412
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1179
1413
|
servers[name] = entry;
|
|
1180
1414
|
}
|
|
@@ -1184,7 +1418,7 @@ class ClineAdapter {
|
|
|
1184
1418
|
};
|
|
1185
1419
|
writeJsonAtomic(configPath, next);
|
|
1186
1420
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1187
|
-
return okResult(this.id, configPath);
|
|
1421
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1188
1422
|
} catch (error) {
|
|
1189
1423
|
return errorResult(this.id, configPath, error.message);
|
|
1190
1424
|
}
|
|
@@ -1192,8 +1426,7 @@ class ClineAdapter {
|
|
|
1192
1426
|
}
|
|
1193
1427
|
|
|
1194
1428
|
// src/adapters/kiro.ts
|
|
1195
|
-
import
|
|
1196
|
-
import path8 from "node:path";
|
|
1429
|
+
import path9 from "node:path";
|
|
1197
1430
|
import { z as z6 } from "zod";
|
|
1198
1431
|
var stringMapSchema6 = z6.record(z6.string(), z6.string());
|
|
1199
1432
|
var kiroEntrySchema = z6.object({
|
|
@@ -1210,7 +1443,7 @@ var kiroEntrySchema = z6.object({
|
|
|
1210
1443
|
class KiroAdapter {
|
|
1211
1444
|
id = "kiro";
|
|
1212
1445
|
detectConfigPath() {
|
|
1213
|
-
return
|
|
1446
|
+
return path9.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
1214
1447
|
}
|
|
1215
1448
|
supportsHttp() {
|
|
1216
1449
|
return true;
|
|
@@ -1286,18 +1519,20 @@ class KiroAdapter {
|
|
|
1286
1519
|
};
|
|
1287
1520
|
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1288
1521
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1289
|
-
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, {
|
|
1290
1525
|
url: entry.url,
|
|
1291
|
-
headers: entry.headers
|
|
1292
|
-
disabled: !entry.enabled
|
|
1526
|
+
headers: entry.headers
|
|
1293
1527
|
}]));
|
|
1294
|
-
|
|
1528
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1529
|
+
for (const name of enabledManagedNames) {
|
|
1295
1530
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1296
1531
|
if (conflict) {
|
|
1297
1532
|
return errorResult(this.id, configPath, conflict);
|
|
1298
1533
|
}
|
|
1299
1534
|
}
|
|
1300
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers,
|
|
1535
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1301
1536
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1302
1537
|
servers[name] = entry;
|
|
1303
1538
|
}
|
|
@@ -1307,35 +1542,18 @@ class KiroAdapter {
|
|
|
1307
1542
|
};
|
|
1308
1543
|
writeJsonAtomic(configPath, next);
|
|
1309
1544
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1310
|
-
return okResult(this.id, configPath);
|
|
1545
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1311
1546
|
} catch (error) {
|
|
1312
1547
|
return errorResult(this.id, configPath, error.message);
|
|
1313
1548
|
}
|
|
1314
1549
|
}
|
|
1315
1550
|
syncSkills(skills) {
|
|
1316
|
-
|
|
1317
|
-
if (!fs6.existsSync(skillsDir)) {
|
|
1318
|
-
fs6.mkdirSync(skillsDir, { recursive: true });
|
|
1319
|
-
}
|
|
1320
|
-
const existingFiles = fs6.readdirSync(skillsDir);
|
|
1321
|
-
const managedIds = new Set(skills.map((s) => s.id));
|
|
1322
|
-
for (const file of existingFiles) {
|
|
1323
|
-
if (file.endsWith(".md")) {
|
|
1324
|
-
const id = path8.basename(file, ".md");
|
|
1325
|
-
if (!managedIds.has(id)) {
|
|
1326
|
-
fs6.unlinkSync(path8.join(skillsDir, file));
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
for (const skill of skills) {
|
|
1331
|
-
const targetPath = path8.join(skillsDir, `${skill.id}.md`);
|
|
1332
|
-
fs6.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1333
|
-
}
|
|
1551
|
+
projectSkillsToDir(path9.join(homeDir(), ".kiro", "skills"), skills, "flat");
|
|
1334
1552
|
}
|
|
1335
1553
|
}
|
|
1336
1554
|
|
|
1337
1555
|
// src/adapters/opencode.ts
|
|
1338
|
-
import
|
|
1556
|
+
import path10 from "node:path";
|
|
1339
1557
|
import { z as z7 } from "zod";
|
|
1340
1558
|
var stringMapSchema7 = z7.record(z7.string(), z7.string());
|
|
1341
1559
|
var openCodeEntrySchema = z7.object({
|
|
@@ -1353,11 +1571,14 @@ var openCodeEntrySchema = z7.object({
|
|
|
1353
1571
|
class OpenCodeAdapter {
|
|
1354
1572
|
id = "opencode";
|
|
1355
1573
|
detectConfigPath() {
|
|
1356
|
-
return
|
|
1574
|
+
return path10.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
1357
1575
|
}
|
|
1358
1576
|
supportsHttp() {
|
|
1359
1577
|
return true;
|
|
1360
1578
|
}
|
|
1579
|
+
syncPlugins(plugins) {
|
|
1580
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1581
|
+
}
|
|
1361
1582
|
scanForImports(_config, managedIndex) {
|
|
1362
1583
|
const configPath = this.detectConfigPath();
|
|
1363
1584
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1431,19 +1652,21 @@ class OpenCodeAdapter {
|
|
|
1431
1652
|
};
|
|
1432
1653
|
removeSourceEntries(mcp, options.sourceEntriesToRemove);
|
|
1433
1654
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1434
|
-
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, {
|
|
1435
1658
|
type: "remote",
|
|
1436
1659
|
url: entry.url,
|
|
1437
|
-
headers: entry.headers
|
|
1438
|
-
enabled: entry.enabled
|
|
1660
|
+
headers: entry.headers
|
|
1439
1661
|
}]));
|
|
1440
|
-
|
|
1662
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcp[name]));
|
|
1663
|
+
for (const name of enabledManagedNames) {
|
|
1441
1664
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
|
|
1442
1665
|
if (conflict) {
|
|
1443
1666
|
return errorResult(this.id, configPath, conflict);
|
|
1444
1667
|
}
|
|
1445
1668
|
}
|
|
1446
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcp,
|
|
1669
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcp, enabledManagedNames);
|
|
1447
1670
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1448
1671
|
mcp[name] = entry;
|
|
1449
1672
|
}
|
|
@@ -1453,7 +1676,7 @@ class OpenCodeAdapter {
|
|
|
1453
1676
|
};
|
|
1454
1677
|
writeJsonAtomic(configPath, next);
|
|
1455
1678
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1456
|
-
return okResult(this.id, configPath);
|
|
1679
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1457
1680
|
} catch (error) {
|
|
1458
1681
|
return errorResult(this.id, configPath, error.message);
|
|
1459
1682
|
}
|
|
@@ -1461,7 +1684,7 @@ class OpenCodeAdapter {
|
|
|
1461
1684
|
}
|
|
1462
1685
|
|
|
1463
1686
|
// src/adapters/vscode.ts
|
|
1464
|
-
import
|
|
1687
|
+
import path11 from "node:path";
|
|
1465
1688
|
import { z as z8 } from "zod";
|
|
1466
1689
|
var stringMapSchema8 = z8.record(z8.string(), z8.string());
|
|
1467
1690
|
var vscodeEntrySchema = z8.object({
|
|
@@ -1479,12 +1702,12 @@ class VsCodeAdapter {
|
|
|
1479
1702
|
id = "vscode";
|
|
1480
1703
|
detectConfigPath() {
|
|
1481
1704
|
if (process.platform === "linux") {
|
|
1482
|
-
return
|
|
1705
|
+
return path11.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
1483
1706
|
}
|
|
1484
1707
|
if (process.platform === "win32") {
|
|
1485
|
-
return
|
|
1708
|
+
return path11.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
1486
1709
|
}
|
|
1487
|
-
return
|
|
1710
|
+
return path11.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
1488
1711
|
}
|
|
1489
1712
|
supportsHttp() {
|
|
1490
1713
|
return true;
|
|
@@ -1492,6 +1715,9 @@ class VsCodeAdapter {
|
|
|
1492
1715
|
syncPlugins(plugins) {
|
|
1493
1716
|
return syncPluginsToClient(this.id, plugins);
|
|
1494
1717
|
}
|
|
1718
|
+
syncSkills(skills) {
|
|
1719
|
+
projectSkillsToDir(path11.join(homeDir(), ".copilot", "skills"), skills, "dir");
|
|
1720
|
+
}
|
|
1495
1721
|
scanForImports(_config, managedIndex) {
|
|
1496
1722
|
const configPath = this.detectConfigPath();
|
|
1497
1723
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
@@ -1566,6 +1792,7 @@ class VsCodeAdapter {
|
|
|
1566
1792
|
headers: entry.headers
|
|
1567
1793
|
}]));
|
|
1568
1794
|
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1795
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1569
1796
|
for (const name of enabledManagedNames) {
|
|
1570
1797
|
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1571
1798
|
if (conflict) {
|
|
@@ -1581,11 +1808,11 @@ class VsCodeAdapter {
|
|
|
1581
1808
|
servers
|
|
1582
1809
|
};
|
|
1583
1810
|
writeJsonAtomic(configPath, next);
|
|
1584
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(
|
|
1811
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => [
|
|
1585
1812
|
entry.name,
|
|
1586
|
-
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers
|
|
1813
|
+
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers })
|
|
1587
1814
|
])));
|
|
1588
|
-
return okResult(this.id, configPath);
|
|
1815
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1589
1816
|
} catch (error) {
|
|
1590
1817
|
return errorResult(this.id, configPath, error.message);
|
|
1591
1818
|
}
|
|
@@ -1593,8 +1820,7 @@ class VsCodeAdapter {
|
|
|
1593
1820
|
}
|
|
1594
1821
|
|
|
1595
1822
|
// src/adapters/qwen.ts
|
|
1596
|
-
import
|
|
1597
|
-
import path11 from "node:path";
|
|
1823
|
+
import path12 from "node:path";
|
|
1598
1824
|
import { z as z9 } from "zod";
|
|
1599
1825
|
var stringMapSchema9 = z9.record(z9.string(), z9.string());
|
|
1600
1826
|
var qwenEntrySchema = z9.object({
|
|
@@ -1611,7 +1837,7 @@ var qwenEntrySchema = z9.object({
|
|
|
1611
1837
|
class QwenAdapter {
|
|
1612
1838
|
id = "qwen";
|
|
1613
1839
|
detectConfigPath() {
|
|
1614
|
-
return
|
|
1840
|
+
return path12.join(homeDir(), ".qwen", "settings.json");
|
|
1615
1841
|
}
|
|
1616
1842
|
supportsHttp() {
|
|
1617
1843
|
return true;
|
|
@@ -1697,52 +1923,317 @@ class QwenAdapter {
|
|
|
1697
1923
|
try {
|
|
1698
1924
|
const raw = readJsonFile(configPath, {});
|
|
1699
1925
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1700
|
-
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, {
|
|
1701
1929
|
httpUrl: entry.url,
|
|
1702
1930
|
headers: entry.headers
|
|
1703
1931
|
}]));
|
|
1704
1932
|
const topLevelServers = raw.mcpServers ?? {};
|
|
1705
1933
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1706
|
-
|
|
1934
|
+
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
1935
|
+
for (const name of enabledManagedNames) {
|
|
1707
1936
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1708
1937
|
if (topLevelConflict) {
|
|
1709
1938
|
return errorResult(this.id, configPath, topLevelConflict);
|
|
1710
1939
|
}
|
|
1711
1940
|
}
|
|
1712
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers,
|
|
1941
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
1713
1942
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1714
1943
|
topLevelServers[name] = entry;
|
|
1715
1944
|
}
|
|
1716
1945
|
raw.mcpServers = topLevelServers;
|
|
1717
|
-
|
|
1946
|
+
purgeManagedFromExcludedArray(raw, managedNames);
|
|
1718
1947
|
writeJsonAtomic(configPath, raw);
|
|
1719
1948
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1720
1949
|
entry.name,
|
|
1721
1950
|
JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1722
1951
|
])));
|
|
1723
|
-
return okResult(this.id, configPath);
|
|
1952
|
+
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1724
1953
|
} catch (error) {
|
|
1725
1954
|
return errorResult(this.id, configPath, error.message);
|
|
1726
1955
|
}
|
|
1727
1956
|
}
|
|
1728
1957
|
syncSkills(skills) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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.");
|
|
1740
2073
|
}
|
|
2074
|
+
} else {
|
|
2075
|
+
raw = {};
|
|
1741
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);
|
|
2092
|
+
}
|
|
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);
|
|
1742
2108
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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));
|
|
2199
|
+
}
|
|
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);
|
|
1746
2237
|
}
|
|
1747
2238
|
}
|
|
1748
2239
|
}
|
|
@@ -1758,7 +2249,9 @@ function getAdapters() {
|
|
|
1758
2249
|
new OpenCodeAdapter,
|
|
1759
2250
|
new KiroAdapter,
|
|
1760
2251
|
new VsCodeAdapter,
|
|
1761
|
-
new QwenAdapter
|
|
2252
|
+
new QwenAdapter,
|
|
2253
|
+
new OpenClawAdapter,
|
|
2254
|
+
new HermesAdapter
|
|
1762
2255
|
];
|
|
1763
2256
|
}
|
|
1764
2257
|
|
|
@@ -1783,45 +2276,105 @@ function saveManagedIndex(index, filePath = getManagedIndexPath()) {
|
|
|
1783
2276
|
writeJsonAtomic(filePath, index);
|
|
1784
2277
|
}
|
|
1785
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
|
+
|
|
1786
2319
|
// src/core/sync.ts
|
|
1787
2320
|
function getGatewayUrl(config) {
|
|
1788
2321
|
return `http://127.0.0.1:${config.gateway.port}/mcp`;
|
|
1789
2322
|
}
|
|
1790
2323
|
function buildProjectScopes(config) {
|
|
1791
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
|
+
}
|
|
1792
2341
|
return Object.values(config.projects ?? {}).map((project) => {
|
|
2342
|
+
const projectPath = project.path;
|
|
1793
2343
|
const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
|
|
1794
|
-
|
|
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 };
|
|
1795
2353
|
});
|
|
1796
2354
|
}
|
|
1797
2355
|
function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
1798
|
-
|
|
1799
|
-
return entries.map(([name, spec]) => ({
|
|
2356
|
+
return Object.entries(config.servers).map(([name, spec]) => ({
|
|
1800
2357
|
name: `${name} (mcpx)`,
|
|
1801
2358
|
url: `${gatewayUrl}?upstream=${encodeURIComponent(name)}`,
|
|
1802
|
-
headers: {
|
|
1803
|
-
Authorization: `Bearer ${localToken}`
|
|
1804
|
-
},
|
|
2359
|
+
headers: { Authorization: `Bearer ${localToken}` },
|
|
1805
2360
|
enabled: isServerEnabled(spec)
|
|
1806
2361
|
}));
|
|
1807
2362
|
}
|
|
1808
2363
|
function isClientInternalServerCommand(candidate) {
|
|
1809
2364
|
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
1810
2365
|
}
|
|
1811
|
-
function
|
|
2366
|
+
function getFilteredAdapters(targetClients) {
|
|
1812
2367
|
const adapters = getAdapters();
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
conflicts: [],
|
|
1821
|
-
errors: []
|
|
1822
|
-
};
|
|
2368
|
+
if (targetClients && targetClients.length > 0) {
|
|
2369
|
+
return adapters.filter((a) => targetClients.includes(a.id));
|
|
2370
|
+
}
|
|
2371
|
+
return adapters;
|
|
2372
|
+
}
|
|
2373
|
+
function runImportPhase(adapters, config, managedIndex) {
|
|
2374
|
+
const imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
|
|
1823
2375
|
const sourceEntriesToRemove = new Map;
|
|
1824
|
-
|
|
2376
|
+
const phaseErrors = [];
|
|
2377
|
+
for (const adapter of adapters) {
|
|
1825
2378
|
try {
|
|
1826
2379
|
const scan = adapter.scanForImports(config, managedIndex);
|
|
1827
2380
|
imports.skipped.push(...scan.skipped);
|
|
@@ -1848,7 +2401,7 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1848
2401
|
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1849
2402
|
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1850
2403
|
}
|
|
1851
|
-
sourceEntriesToRemove.get(candidate.clientId)
|
|
2404
|
+
sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
|
|
1852
2405
|
continue;
|
|
1853
2406
|
}
|
|
1854
2407
|
if (serverSpecsEqual(existing, candidate.spec)) {
|
|
@@ -1861,7 +2414,7 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1861
2414
|
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1862
2415
|
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1863
2416
|
}
|
|
1864
|
-
sourceEntriesToRemove.get(candidate.clientId)
|
|
2417
|
+
sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
|
|
1865
2418
|
continue;
|
|
1866
2419
|
}
|
|
1867
2420
|
imports.conflicts.push({
|
|
@@ -1878,37 +2431,56 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1878
2431
|
configPath: adapter.detectConfigPath() ?? undefined,
|
|
1879
2432
|
message: error instanceof Error ? error.message : String(error)
|
|
1880
2433
|
});
|
|
2434
|
+
phaseErrors.push({
|
|
2435
|
+
clientId: adapter.id,
|
|
2436
|
+
phase: "import",
|
|
2437
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2438
|
+
});
|
|
1881
2439
|
}
|
|
1882
2440
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
const
|
|
1888
|
-
const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
|
|
1889
|
-
pluginId: p.id,
|
|
1890
|
-
pluginName: p.name,
|
|
1891
|
-
pluginRoot: p.root,
|
|
1892
|
-
components: p.components,
|
|
1893
|
-
approvals: p.approvals ?? {},
|
|
1894
|
-
enabled: p.enabled,
|
|
1895
|
-
serverNames: p.serverNames,
|
|
1896
|
-
skills: p.discovered.skills,
|
|
1897
|
-
commands: p.discovered.commands,
|
|
1898
|
-
agents: p.discovered.agents,
|
|
1899
|
-
hooks: p.discovered.hooks
|
|
1900
|
-
}));
|
|
1901
|
-
for (const adapter of filteredAdapters) {
|
|
2441
|
+
return { imports, sourceEntriesToRemove, phaseErrors };
|
|
2442
|
+
}
|
|
2443
|
+
function runSkillsPhase(adapters, skills) {
|
|
2444
|
+
const phaseErrors = [];
|
|
2445
|
+
for (const adapter of adapters) {
|
|
1902
2446
|
if (adapter.syncSkills) {
|
|
1903
2447
|
try {
|
|
1904
2448
|
adapter.syncSkills(skills);
|
|
1905
|
-
} catch (error) {
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
phaseErrors.push({
|
|
2451
|
+
clientId: adapter.id,
|
|
2452
|
+
phase: "skills",
|
|
2453
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
1906
2456
|
}
|
|
1907
|
-
|
|
2457
|
+
}
|
|
2458
|
+
return phaseErrors;
|
|
2459
|
+
}
|
|
2460
|
+
function runPluginsPhase(adapters, pluginInputs) {
|
|
2461
|
+
const pluginResults = [];
|
|
2462
|
+
const phaseErrors = [];
|
|
2463
|
+
for (const adapter of adapters) {
|
|
2464
|
+
if (adapter.syncPlugins) {
|
|
1908
2465
|
try {
|
|
1909
|
-
adapter.syncPlugins(pluginInputs);
|
|
1910
|
-
|
|
2466
|
+
const result = adapter.syncPlugins(pluginInputs);
|
|
2467
|
+
if (result)
|
|
2468
|
+
pluginResults.push(result);
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
phaseErrors.push({
|
|
2471
|
+
clientId: adapter.id,
|
|
2472
|
+
phase: "plugins",
|
|
2473
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
1911
2476
|
}
|
|
2477
|
+
}
|
|
2478
|
+
return { pluginResults, phaseErrors };
|
|
2479
|
+
}
|
|
2480
|
+
function runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, projectScopes) {
|
|
2481
|
+
const results = [];
|
|
2482
|
+
const phaseErrors = [];
|
|
2483
|
+
for (const adapter of adapters) {
|
|
1912
2484
|
if (!adapter.supportsHttp()) {
|
|
1913
2485
|
results.push({
|
|
1914
2486
|
clientId: adapter.id,
|
|
@@ -1917,32 +2489,98 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1917
2489
|
});
|
|
1918
2490
|
continue;
|
|
1919
2491
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2492
|
+
try {
|
|
2493
|
+
const result = adapter.syncGateway(config, {
|
|
2494
|
+
managedEntries,
|
|
2495
|
+
managedIndex,
|
|
2496
|
+
managedIndexPath,
|
|
2497
|
+
sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
|
|
2498
|
+
projectScopes
|
|
2499
|
+
});
|
|
2500
|
+
results.push(result);
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
results.push({
|
|
2503
|
+
clientId: adapter.id,
|
|
2504
|
+
status: "ERROR",
|
|
2505
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2506
|
+
});
|
|
2507
|
+
phaseErrors.push({
|
|
2508
|
+
clientId: adapter.id,
|
|
2509
|
+
phase: "gateway",
|
|
2510
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return { results, phaseErrors };
|
|
2515
|
+
}
|
|
2516
|
+
function syncAllClients(config, secrets, options) {
|
|
2517
|
+
const opts = Array.isArray(options) ? { targetClients: options, importScan: false } : { importScan: false, ...options };
|
|
2518
|
+
const managedIndexPath = getManagedIndexPath();
|
|
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
|
|
1935
2575
|
};
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
function persistSyncState(summary, config) {
|
|
2579
|
+
if (!summary.clientStates)
|
|
2580
|
+
return;
|
|
2581
|
+
for (const [clientId, state] of Object.entries(summary.clientStates)) {
|
|
2582
|
+
config.clients[clientId] = state;
|
|
1936
2583
|
}
|
|
1937
|
-
saveManagedIndex(managedIndex, managedIndexPath);
|
|
1938
|
-
saveConfig(config);
|
|
1939
|
-
const hasErrors = results.some((result) => result.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
|
|
1940
|
-
return {
|
|
1941
|
-
gatewayUrl,
|
|
1942
|
-
imports,
|
|
1943
|
-
results,
|
|
1944
|
-
hasErrors
|
|
1945
|
-
};
|
|
1946
2584
|
}
|
|
1947
2585
|
|
|
1948
|
-
export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients };
|
|
2586
|
+
export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, APP_VERSION, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };
|