@kwonye/mcpx 0.1.90 → 0.1.93
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 +2 -1
- 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.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/kiro.d.ts +2 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/qwen.d.ts +2 -1
- package/dist/adapters/qwen.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-qfvawh8e.js → cli-8vxva0cb.js} +1 -1
- package/dist/cli-kwyp9ywh.js +1948 -0
- package/dist/{cli-mzgjjdjt.js → cli-m2645syd.js} +58 -1630
- package/dist/{cli-btx8jwra.js → cli-np0wwh6m.js} +58 -4
- package/dist/cli-qvkpwy9w.js +904 -0
- package/dist/{cli-cvve840g.js → cli-y7ba5qvr.js} +57 -1629
- package/dist/{cli-zbbeadz8.js → cli-zjepj3sj.js} +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +143 -15
- package/dist/{config-nkgc4c4n.js → config-2s5nsm2g.js} +2 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/index.d.ts +9 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +58 -12
- package/dist/core/paths.d.ts +4 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/plugin-cache.d.ts +21 -0
- package/dist/core/plugin-cache.d.ts.map +1 -0
- package/dist/core/plugin-data.d.ts +29 -0
- package/dist/core/plugin-data.d.ts.map +1 -0
- package/dist/core/plugin-lifecycle.d.ts +8 -0
- package/dist/core/plugin-lifecycle.d.ts.map +1 -0
- package/dist/core/plugin-manager.d.ts +52 -0
- package/dist/core/plugin-manager.d.ts.map +1 -0
- package/dist/core/plugin-parse.d.ts +6 -0
- package/dist/core/plugin-parse.d.ts.map +1 -0
- package/dist/core/plugin-projections.d.ts +5 -0
- package/dist/core/plugin-projections.d.ts.map +1 -0
- package/dist/core/plugin-source.d.ts +5 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/plugin-manager-1sc4b6am.js +31 -0
- package/dist/{registry-vykv33et.js → registry-mztfy0cz.js} +2 -2
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,1948 @@
|
|
|
1
|
+
import {
|
|
2
|
+
saveConfig
|
|
3
|
+
} from "./cli-np0wwh6m.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureGatewayToken
|
|
6
|
+
} from "./cli-8vxva0cb.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureDir,
|
|
9
|
+
getManagedIndexPath,
|
|
10
|
+
getSkillsDir,
|
|
11
|
+
homeDir,
|
|
12
|
+
readJsonFile,
|
|
13
|
+
sha256,
|
|
14
|
+
writeJsonAtomic
|
|
15
|
+
} from "./cli-zjepj3sj.js";
|
|
16
|
+
import {
|
|
17
|
+
isServerEnabled
|
|
18
|
+
} from "./cli-s4gspqdf.js";
|
|
19
|
+
|
|
20
|
+
// src/core/skills.ts
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
function listSkills() {
|
|
24
|
+
const skillsDir = getSkillsDir();
|
|
25
|
+
if (!fs.existsSync(skillsDir)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const files = fs.readdirSync(skillsDir);
|
|
29
|
+
const skills = [];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
if (file.endsWith(".md")) {
|
|
32
|
+
const id = path.basename(file, ".md");
|
|
33
|
+
const content = fs.readFileSync(path.join(skillsDir, file), "utf-8");
|
|
34
|
+
skills.push({ id, content });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return skills;
|
|
38
|
+
}
|
|
39
|
+
function getSkill(id) {
|
|
40
|
+
const skillsDir = getSkillsDir();
|
|
41
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
46
|
+
return { id, content };
|
|
47
|
+
}
|
|
48
|
+
function saveSkill(id, content) {
|
|
49
|
+
const skillsDir = getSkillsDir();
|
|
50
|
+
ensureDir(skillsDir);
|
|
51
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
52
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
function deleteSkill(id) {
|
|
55
|
+
const skillsDir = getSkillsDir();
|
|
56
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
57
|
+
if (fs.existsSync(filePath)) {
|
|
58
|
+
fs.unlinkSync(filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/plugin-projections.ts
|
|
63
|
+
import fs2 from "node:fs";
|
|
64
|
+
import path2 from "node:path";
|
|
65
|
+
var OWNERSHIP_MANIFEST = "mcpx-plugins.json";
|
|
66
|
+
function loadOwnership(targetDir) {
|
|
67
|
+
const manifestPath = path2.join(targetDir, OWNERSHIP_MANIFEST);
|
|
68
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
69
|
+
return { version: 1, plugins: {} };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return { version: 1, plugins: {} };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function saveOwnership(targetDir, manifest) {
|
|
78
|
+
fs2.writeFileSync(path2.join(targetDir, OWNERSHIP_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
79
|
+
}
|
|
80
|
+
function recordOwned(targetDir, pluginId, paths) {
|
|
81
|
+
ensureDir(targetDir);
|
|
82
|
+
const manifest = loadOwnership(targetDir);
|
|
83
|
+
manifest.plugins[pluginId] = {
|
|
84
|
+
pluginId,
|
|
85
|
+
paths,
|
|
86
|
+
installedAt: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
saveOwnership(targetDir, manifest);
|
|
89
|
+
}
|
|
90
|
+
function removeOwned(targetDir, pluginId) {
|
|
91
|
+
const manifest = loadOwnership(targetDir);
|
|
92
|
+
const entry = manifest.plugins[pluginId];
|
|
93
|
+
if (entry) {
|
|
94
|
+
delete manifest.plugins[pluginId];
|
|
95
|
+
saveOwnership(targetDir, manifest);
|
|
96
|
+
return entry.paths;
|
|
97
|
+
}
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
function prunePluginProjections(targetDir, pluginId) {
|
|
101
|
+
const paths = removeOwned(targetDir, pluginId);
|
|
102
|
+
for (const p of paths) {
|
|
103
|
+
const fullPath = path2.join(targetDir, p);
|
|
104
|
+
try {
|
|
105
|
+
if (fs2.existsSync(fullPath)) {
|
|
106
|
+
fs2.rmSync(fullPath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function pruneAllPluginProjections(targetDir) {
|
|
112
|
+
const manifest = loadOwnership(targetDir);
|
|
113
|
+
for (const [pluginId] of Object.entries(manifest.plugins)) {
|
|
114
|
+
prunePluginProjections(targetDir, pluginId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function nsName(pluginName, id) {
|
|
118
|
+
return `${pluginName}__${id}`;
|
|
119
|
+
}
|
|
120
|
+
function syncClaude(plugins) {
|
|
121
|
+
const targetBase = path2.join(homeDir(), ".claude", "skills");
|
|
122
|
+
const projectedDirs = [];
|
|
123
|
+
const unsupported = [];
|
|
124
|
+
for (const plugin of plugins) {
|
|
125
|
+
if (!plugin.enabled)
|
|
126
|
+
continue;
|
|
127
|
+
const targetDir = path2.join(targetBase, plugin.pluginName);
|
|
128
|
+
ensureDir(targetDir);
|
|
129
|
+
const mcpJsonSrc = path2.join(plugin.pluginRoot, ".mcp.json");
|
|
130
|
+
const mcpJsonDest = path2.join(targetDir, ".mcp.json");
|
|
131
|
+
copyPluginDir(plugin.pluginRoot, targetDir, [".mcp.json", ".git"]);
|
|
132
|
+
if (fs2.existsSync(mcpJsonDest)) {
|
|
133
|
+
fs2.rmSync(mcpJsonDest, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
projectedDirs.push(targetDir);
|
|
136
|
+
recordOwned(targetBase, plugin.pluginId, [plugin.pluginName]);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
clientId: "claude",
|
|
140
|
+
status: projectedDirs.length > 0 || plugins.length === 0 ? "SYNCED" : "SKIPPED",
|
|
141
|
+
projectedDirs,
|
|
142
|
+
unsupported: []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function syncClaudeDesktop(plugins) {
|
|
146
|
+
const unsupported = ["skills", "hooks", "agents", "commands"];
|
|
147
|
+
return {
|
|
148
|
+
clientId: "claude-desktop",
|
|
149
|
+
status: plugins.length > 0 ? "SYNCED" : "SKIPPED",
|
|
150
|
+
projectedDirs: [],
|
|
151
|
+
unsupported
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function syncCodex(plugins) {
|
|
155
|
+
const targetBase = path2.join(homeDir(), ".codex", "skills");
|
|
156
|
+
const projectedDirs = [];
|
|
157
|
+
const unsupported = ["hooks", "agents"];
|
|
158
|
+
for (const plugin of plugins) {
|
|
159
|
+
if (!plugin.enabled || !plugin.components.skills)
|
|
160
|
+
continue;
|
|
161
|
+
ensureDir(targetBase);
|
|
162
|
+
const owned = [];
|
|
163
|
+
for (const skill of plugin.skills) {
|
|
164
|
+
const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
|
|
165
|
+
ensureDir(targetDir);
|
|
166
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
167
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
168
|
+
projectedDirs.push(targetDir);
|
|
169
|
+
}
|
|
170
|
+
recordOwned(targetBase, plugin.pluginId, owned);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
clientId: "codex",
|
|
174
|
+
status: "SYNCED",
|
|
175
|
+
projectedDirs,
|
|
176
|
+
unsupported
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function syncCursor(plugins) {
|
|
180
|
+
const skillsBase = path2.join(homeDir(), ".cursor", "skills");
|
|
181
|
+
const commandsBase = path2.join(homeDir(), ".cursor", "commands");
|
|
182
|
+
const projectedDirs = [];
|
|
183
|
+
const unsupported = ["hooks", "agents"];
|
|
184
|
+
const ownedSkills = [];
|
|
185
|
+
const ownedCommands = [];
|
|
186
|
+
for (const plugin of plugins) {
|
|
187
|
+
if (!plugin.enabled)
|
|
188
|
+
continue;
|
|
189
|
+
if (plugin.components.skills) {
|
|
190
|
+
ensureDir(skillsBase);
|
|
191
|
+
for (const skill of plugin.skills) {
|
|
192
|
+
const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
|
|
193
|
+
ensureDir(targetDir);
|
|
194
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
195
|
+
ownedSkills.push(nsName(plugin.pluginName, skill.id));
|
|
196
|
+
projectedDirs.push(targetDir);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (plugin.components.commands) {
|
|
200
|
+
ensureDir(commandsBase);
|
|
201
|
+
for (const cmd of plugin.commands) {
|
|
202
|
+
const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
203
|
+
copyFileOrDir(cmd.path, targetPath);
|
|
204
|
+
ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
205
|
+
projectedDirs.push(targetPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (ownedSkills.length > 0)
|
|
209
|
+
recordOwned(skillsBase, plugin.pluginId, ownedSkills);
|
|
210
|
+
if (ownedCommands.length > 0)
|
|
211
|
+
recordOwned(commandsBase, plugin.pluginId, ownedCommands);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
clientId: "cursor",
|
|
215
|
+
status: "SYNCED",
|
|
216
|
+
projectedDirs,
|
|
217
|
+
unsupported
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function syncVsCode(plugins) {
|
|
221
|
+
const skillsBase = path2.join(homeDir(), ".copilot", "skills");
|
|
222
|
+
const projectedDirs = [];
|
|
223
|
+
const unsupported = ["hooks", "agents"];
|
|
224
|
+
for (const plugin of plugins) {
|
|
225
|
+
if (!plugin.enabled || !plugin.components.skills)
|
|
226
|
+
continue;
|
|
227
|
+
ensureDir(skillsBase);
|
|
228
|
+
const owned = [];
|
|
229
|
+
for (const skill of plugin.skills) {
|
|
230
|
+
const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
|
|
231
|
+
ensureDir(targetDir);
|
|
232
|
+
copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
|
|
233
|
+
owned.push(nsName(plugin.pluginName, skill.id));
|
|
234
|
+
projectedDirs.push(targetDir);
|
|
235
|
+
}
|
|
236
|
+
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
clientId: "vscode",
|
|
240
|
+
status: "SYNCED",
|
|
241
|
+
projectedDirs,
|
|
242
|
+
unsupported
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function syncKiro(plugins) {
|
|
246
|
+
const skillsBase = path2.join(homeDir(), ".kiro", "skills");
|
|
247
|
+
const projectedDirs = [];
|
|
248
|
+
const unsupported = ["hooks", "agents", "commands"];
|
|
249
|
+
for (const plugin of plugins) {
|
|
250
|
+
if (!plugin.enabled || !plugin.components.skills)
|
|
251
|
+
continue;
|
|
252
|
+
ensureDir(skillsBase);
|
|
253
|
+
const owned = [];
|
|
254
|
+
for (const skill of plugin.skills) {
|
|
255
|
+
const targetPath = path2.join(skillsBase, `${nsName(plugin.pluginName, skill.id)}.md`);
|
|
256
|
+
copyFileOrDir(skill.path, targetPath);
|
|
257
|
+
owned.push(`${nsName(plugin.pluginName, skill.id)}.md`);
|
|
258
|
+
projectedDirs.push(targetPath);
|
|
259
|
+
}
|
|
260
|
+
recordOwned(skillsBase, plugin.pluginId, owned);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
clientId: "kiro",
|
|
264
|
+
status: "SYNCED",
|
|
265
|
+
projectedDirs,
|
|
266
|
+
unsupported
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function syncQwen(plugins) {
|
|
270
|
+
const skillsBase = path2.join(homeDir(), ".qwen", "skills");
|
|
271
|
+
const commandsBase = path2.join(homeDir(), ".qwen", "commands");
|
|
272
|
+
const projectedDirs = [];
|
|
273
|
+
const unsupported = ["hooks", "agents"];
|
|
274
|
+
const ownedSkills = [];
|
|
275
|
+
const ownedCommands = [];
|
|
276
|
+
for (const plugin of plugins) {
|
|
277
|
+
if (!plugin.enabled)
|
|
278
|
+
continue;
|
|
279
|
+
if (plugin.components.skills) {
|
|
280
|
+
ensureDir(skillsBase);
|
|
281
|
+
for (const skill of plugin.skills) {
|
|
282
|
+
const targetPath = path2.join(skillsBase, `${nsName(plugin.pluginName, skill.id)}.md`);
|
|
283
|
+
copyFileOrDir(skill.path, targetPath);
|
|
284
|
+
ownedSkills.push(`${nsName(plugin.pluginName, skill.id)}.md`);
|
|
285
|
+
projectedDirs.push(targetPath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (plugin.components.commands) {
|
|
289
|
+
ensureDir(commandsBase);
|
|
290
|
+
for (const cmd of plugin.commands) {
|
|
291
|
+
const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
292
|
+
copyFileOrDir(cmd.path, targetPath);
|
|
293
|
+
ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
|
|
294
|
+
projectedDirs.push(targetPath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (ownedSkills.length > 0)
|
|
298
|
+
recordOwned(skillsBase, plugin.pluginId, ownedSkills);
|
|
299
|
+
if (ownedCommands.length > 0)
|
|
300
|
+
recordOwned(commandsBase, plugin.pluginId, ownedCommands);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
clientId: "qwen",
|
|
304
|
+
status: "SYNCED",
|
|
305
|
+
projectedDirs,
|
|
306
|
+
unsupported
|
|
307
|
+
};
|
|
308
|
+
}
|
|
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
|
+
function syncPluginsToClient(clientId, plugins) {
|
|
319
|
+
const syncer = CLIENT_SYNCERS[clientId];
|
|
320
|
+
if (!syncer) {
|
|
321
|
+
return {
|
|
322
|
+
clientId,
|
|
323
|
+
status: "SKIPPED",
|
|
324
|
+
projectedDirs: [],
|
|
325
|
+
unsupported: ["mcpServers", "skills", "hooks", "agents", "commands"],
|
|
326
|
+
error: `No plugin sync implementation for ${clientId}`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return syncer(plugins);
|
|
330
|
+
}
|
|
331
|
+
function copyPluginDir(src, dest, exclude = []) {
|
|
332
|
+
ensureDir(dest);
|
|
333
|
+
const entries = fs2.readdirSync(src, { withFileTypes: true });
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (exclude.includes(entry.name))
|
|
336
|
+
continue;
|
|
337
|
+
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin")
|
|
338
|
+
continue;
|
|
339
|
+
const srcPath = path2.join(src, entry.name);
|
|
340
|
+
const destPath = path2.join(dest, entry.name);
|
|
341
|
+
if (entry.isDirectory()) {
|
|
342
|
+
copyPluginDir(srcPath, destPath, exclude);
|
|
343
|
+
} else {
|
|
344
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
345
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function copyFileOrDir(src, dest) {
|
|
350
|
+
ensureDir(path2.dirname(dest));
|
|
351
|
+
if (fs2.statSync(src).isDirectory()) {
|
|
352
|
+
copyPluginDir(src, dest);
|
|
353
|
+
} else {
|
|
354
|
+
fs2.copyFileSync(src, dest);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/adapters/claude.ts
|
|
359
|
+
import fs3 from "node:fs";
|
|
360
|
+
import path3 from "node:path";
|
|
361
|
+
import { z } from "zod";
|
|
362
|
+
|
|
363
|
+
// src/adapters/utils/index.ts
|
|
364
|
+
function getManagedEntryNames(managedIndex, clientId) {
|
|
365
|
+
return Object.keys(managedIndex.managed[clientId]?.entries ?? {});
|
|
366
|
+
}
|
|
367
|
+
function isManagedEntry(managedIndex, clientId, entryName) {
|
|
368
|
+
return Boolean(managedIndex.managed[clientId]?.entries?.[entryName]);
|
|
369
|
+
}
|
|
370
|
+
function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryNames) {
|
|
371
|
+
const keep = new Set(keepEntryNames);
|
|
372
|
+
const managedNames = getManagedEntryNames(managedIndex, clientId);
|
|
373
|
+
for (const name of managedNames) {
|
|
374
|
+
if (!keep.has(name)) {
|
|
375
|
+
delete servers[name];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const name of Object.keys(servers)) {
|
|
379
|
+
if (!keep.has(name) && name.endsWith(" (mcpx)")) {
|
|
380
|
+
delete servers[name];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
|
|
385
|
+
if (existingValue === undefined || existingValue === null) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (isManagedEntry(managedIndex, clientId, entryName)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
|
|
392
|
+
}
|
|
393
|
+
function setManagedEntries(managedIndex, clientId, configPath, entries) {
|
|
394
|
+
if (!managedIndex.managed[clientId]) {
|
|
395
|
+
managedIndex.managed[clientId] = {
|
|
396
|
+
configPath,
|
|
397
|
+
entries: {}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
managedIndex.managed[clientId].configPath = configPath;
|
|
401
|
+
managedIndex.managed[clientId].entries = Object.fromEntries(Object.entries(entries).map(([entryName, serializedEntry]) => [entryName, {
|
|
402
|
+
fingerprint: sha256(serializedEntry),
|
|
403
|
+
lastSyncedAt: new Date().toISOString()
|
|
404
|
+
}]));
|
|
405
|
+
}
|
|
406
|
+
function okResult(clientId, configPath, message) {
|
|
407
|
+
return {
|
|
408
|
+
clientId,
|
|
409
|
+
status: "SYNCED",
|
|
410
|
+
configPath,
|
|
411
|
+
message
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function errorResult(clientId, configPath, message) {
|
|
415
|
+
return {
|
|
416
|
+
clientId,
|
|
417
|
+
status: "ERROR",
|
|
418
|
+
configPath,
|
|
419
|
+
message
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function managedGatewayEntryName(serverName) {
|
|
423
|
+
return `${serverName} (mcpx)`;
|
|
424
|
+
}
|
|
425
|
+
function isManagedGatewayProjection(entryName) {
|
|
426
|
+
return entryName.endsWith(" (mcpx)");
|
|
427
|
+
}
|
|
428
|
+
function sortRecord(record) {
|
|
429
|
+
if (!record || Object.keys(record).length === 0) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
433
|
+
}
|
|
434
|
+
function normalizeServerSpec(spec) {
|
|
435
|
+
if (spec.transport === "http") {
|
|
436
|
+
return {
|
|
437
|
+
transport: "http",
|
|
438
|
+
url: spec.url,
|
|
439
|
+
headers: sortRecord(spec.headers)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
transport: "stdio",
|
|
444
|
+
command: spec.command,
|
|
445
|
+
args: spec.args && spec.args.length > 0 ? [...spec.args] : undefined,
|
|
446
|
+
env: sortRecord(spec.env),
|
|
447
|
+
cwd: spec.cwd
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function serializeServerSpec(spec) {
|
|
451
|
+
return JSON.stringify(normalizeServerSpec(spec));
|
|
452
|
+
}
|
|
453
|
+
function serverSpecsEqual(left, right) {
|
|
454
|
+
return serializeServerSpec(left) === serializeServerSpec(right);
|
|
455
|
+
}
|
|
456
|
+
function emptyImportScan(clientId, configPath) {
|
|
457
|
+
return {
|
|
458
|
+
clientId,
|
|
459
|
+
configPath,
|
|
460
|
+
candidates: [],
|
|
461
|
+
skipped: []
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function buildImportSkip(clientId, sourceEntryName, reason, configPath, serverName = sourceEntryName) {
|
|
465
|
+
return {
|
|
466
|
+
clientId,
|
|
467
|
+
configPath,
|
|
468
|
+
sourceEntryName,
|
|
469
|
+
serverName,
|
|
470
|
+
reason
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function removeSourceEntries(entries, names) {
|
|
474
|
+
for (const name of names ?? []) {
|
|
475
|
+
delete entries[name];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function syncDisabledMcpServersArray(raw, managedNames, managedEntries) {
|
|
479
|
+
const existingDisabled = (raw.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
|
|
480
|
+
const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
|
|
481
|
+
raw.disabledMcpServers = [...existingDisabled, ...disabledManaged];
|
|
482
|
+
}
|
|
483
|
+
function syncMcpExcludedArray(raw, managedNames, managedEntries) {
|
|
484
|
+
const mcp = raw.mcp ?? {};
|
|
485
|
+
const existingExcluded = (mcp.excluded ?? []).filter((name) => !managedNames.includes(name));
|
|
486
|
+
const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
|
|
487
|
+
mcp.excluded = [...existingExcluded, ...disabledManaged];
|
|
488
|
+
raw.mcp = mcp;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/adapters/claude.ts
|
|
492
|
+
function normalizeProjectKey(p) {
|
|
493
|
+
try {
|
|
494
|
+
return fs3.realpathSync(path3.resolve(p));
|
|
495
|
+
} catch {
|
|
496
|
+
return path3.resolve(p);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
|
|
500
|
+
const existingNonManaged = (project.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
|
|
501
|
+
const combined = [...existingNonManaged, ...disabledForThisProject];
|
|
502
|
+
if (combined.length > 0) {
|
|
503
|
+
project.disabledMcpServers = combined;
|
|
504
|
+
} else {
|
|
505
|
+
delete project.disabledMcpServers;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
var stringMapSchema = z.record(z.string(), z.string());
|
|
509
|
+
var claudeEntrySchema = z.object({
|
|
510
|
+
type: z.string().optional(),
|
|
511
|
+
url: z.string().min(1).optional(),
|
|
512
|
+
headers: stringMapSchema.optional(),
|
|
513
|
+
command: z.string().min(1).optional(),
|
|
514
|
+
args: z.array(z.string()).optional(),
|
|
515
|
+
env: stringMapSchema.optional(),
|
|
516
|
+
cwd: z.string().min(1).optional(),
|
|
517
|
+
disabled: z.boolean().optional()
|
|
518
|
+
}).passthrough();
|
|
519
|
+
|
|
520
|
+
class ClaudeAdapter {
|
|
521
|
+
id = "claude";
|
|
522
|
+
detectConfigPath() {
|
|
523
|
+
return path3.join(homeDir(), ".claude.json");
|
|
524
|
+
}
|
|
525
|
+
supportsHttp() {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
syncPlugins(plugins) {
|
|
529
|
+
return syncPluginsToClient(this.id, plugins);
|
|
530
|
+
}
|
|
531
|
+
scanForImports(_config, managedIndex) {
|
|
532
|
+
const configPath = this.detectConfigPath();
|
|
533
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
534
|
+
if (!configPath) {
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
const raw = readJsonFile(configPath, {});
|
|
538
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
539
|
+
const disabledMcpServers = new Set(raw.disabledMcpServers ?? []);
|
|
540
|
+
const projects = raw.projects ?? {};
|
|
541
|
+
for (const project of Object.values(projects)) {
|
|
542
|
+
const projectDisabled = project?.disabledMcpServers ?? [];
|
|
543
|
+
for (const name of projectDisabled) {
|
|
544
|
+
disabledMcpServers.add(name);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
548
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const parsed = claudeEntrySchema.safeParse(rawEntry);
|
|
552
|
+
if (!parsed.success) {
|
|
553
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const entry = parsed.data;
|
|
557
|
+
const isDisabled = entry.disabled === true || disabledMcpServers.has(name);
|
|
558
|
+
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
559
|
+
result.candidates.push({
|
|
560
|
+
clientId: this.id,
|
|
561
|
+
configPath,
|
|
562
|
+
sourceEntryName: name,
|
|
563
|
+
serverName: name,
|
|
564
|
+
spec: {
|
|
565
|
+
transport: "http",
|
|
566
|
+
url: entry.url,
|
|
567
|
+
headers: entry.headers,
|
|
568
|
+
enabled: !isDisabled
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
574
|
+
result.candidates.push({
|
|
575
|
+
clientId: this.id,
|
|
576
|
+
configPath,
|
|
577
|
+
sourceEntryName: name,
|
|
578
|
+
serverName: name,
|
|
579
|
+
spec: {
|
|
580
|
+
transport: "stdio",
|
|
581
|
+
command: entry.command,
|
|
582
|
+
args: entry.args,
|
|
583
|
+
env: entry.env,
|
|
584
|
+
cwd: entry.cwd,
|
|
585
|
+
enabled: !isDisabled
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
syncGateway(_config, options) {
|
|
595
|
+
const configPath = this.detectConfigPath();
|
|
596
|
+
if (!configPath) {
|
|
597
|
+
return errorResult(this.id, undefined, "Unable to resolve Claude config path.");
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const raw = readJsonFile(configPath, {});
|
|
601
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
602
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
603
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
604
|
+
type: "http",
|
|
605
|
+
url: entry.url,
|
|
606
|
+
headers: entry.headers
|
|
607
|
+
}]));
|
|
608
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
609
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
610
|
+
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
611
|
+
for (const name of enabledManagedNames) {
|
|
612
|
+
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
613
|
+
if (topLevelConflict) {
|
|
614
|
+
return errorResult(this.id, configPath, topLevelConflict);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
618
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
619
|
+
topLevelServers[name] = entry;
|
|
620
|
+
}
|
|
621
|
+
raw.mcpServers = topLevelServers;
|
|
622
|
+
syncDisabledMcpServersArray(raw, managedNames, options.managedEntries);
|
|
623
|
+
const scopeByNormalizedPath = new Map;
|
|
624
|
+
for (const scope of options.projectScopes ?? []) {
|
|
625
|
+
scopeByNormalizedPath.set(normalizeProjectKey(scope.path), scope.disabledServerNames);
|
|
626
|
+
}
|
|
627
|
+
const projects = raw.projects ?? {};
|
|
628
|
+
for (const projectPath of Object.keys(projects)) {
|
|
629
|
+
const project = projects[projectPath] ?? {};
|
|
630
|
+
const disabledForThis = scopeByNormalizedPath.get(normalizeProjectKey(projectPath)) ?? [];
|
|
631
|
+
syncProjectDisabledArray(project, managedNames, disabledForThis);
|
|
632
|
+
projects[projectPath] = project;
|
|
633
|
+
}
|
|
634
|
+
for (const scope of options.projectScopes ?? []) {
|
|
635
|
+
if (scope.disabledServerNames.length === 0)
|
|
636
|
+
continue;
|
|
637
|
+
const normalizedKey = normalizeProjectKey(scope.path);
|
|
638
|
+
const alreadyExists = Object.keys(projects).some((p) => normalizeProjectKey(p) === normalizedKey);
|
|
639
|
+
if (!alreadyExists) {
|
|
640
|
+
projects[scope.path] = { disabledMcpServers: [...scope.disabledServerNames] };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
raw.projects = projects;
|
|
644
|
+
writeJsonAtomic(configPath, raw);
|
|
645
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
646
|
+
return okResult(this.id, configPath);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return errorResult(this.id, configPath, error.message);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/adapters/claude-desktop.ts
|
|
654
|
+
import { execFileSync } from "node:child_process";
|
|
655
|
+
import path4 from "node:path";
|
|
656
|
+
import { z as z2 } from "zod";
|
|
657
|
+
var stringMapSchema2 = z2.record(z2.string(), z2.string());
|
|
658
|
+
var claudeDesktopEntrySchema = z2.object({
|
|
659
|
+
type: z2.string().optional(),
|
|
660
|
+
url: z2.string().min(1).optional(),
|
|
661
|
+
headers: stringMapSchema2.optional(),
|
|
662
|
+
command: z2.string().min(1).optional(),
|
|
663
|
+
args: z2.array(z2.string()).optional(),
|
|
664
|
+
env: stringMapSchema2.optional(),
|
|
665
|
+
cwd: z2.string().min(1).optional(),
|
|
666
|
+
disabled: z2.boolean().optional()
|
|
667
|
+
}).passthrough();
|
|
668
|
+
function buildProxyEntry(entry) {
|
|
669
|
+
const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
|
|
670
|
+
try {
|
|
671
|
+
const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
672
|
+
if (mcpxPath)
|
|
673
|
+
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
674
|
+
} catch {}
|
|
675
|
+
return { command: "npx", args: ["-y", "@kwonye/mcpx@latest", "proxy", upstreamName] };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
class ClaudeDesktopAdapter {
|
|
679
|
+
id = "claude-desktop";
|
|
680
|
+
detectConfigPath() {
|
|
681
|
+
if (process.platform === "linux") {
|
|
682
|
+
return path4.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
683
|
+
}
|
|
684
|
+
if (process.platform === "win32") {
|
|
685
|
+
return path4.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
686
|
+
}
|
|
687
|
+
return path4.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
688
|
+
}
|
|
689
|
+
supportsHttp() {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
syncPlugins(plugins) {
|
|
693
|
+
return syncPluginsToClient(this.id, plugins);
|
|
694
|
+
}
|
|
695
|
+
scanForImports(_config, managedIndex) {
|
|
696
|
+
const configPath = this.detectConfigPath();
|
|
697
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
698
|
+
if (!configPath) {
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
const raw = readJsonFile(configPath, {});
|
|
702
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
703
|
+
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
704
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const parsed = claudeDesktopEntrySchema.safeParse(rawEntry);
|
|
708
|
+
if (!parsed.success) {
|
|
709
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const entry = parsed.data;
|
|
713
|
+
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
714
|
+
result.candidates.push({
|
|
715
|
+
clientId: this.id,
|
|
716
|
+
configPath,
|
|
717
|
+
sourceEntryName: name,
|
|
718
|
+
serverName: name,
|
|
719
|
+
spec: {
|
|
720
|
+
transport: "http",
|
|
721
|
+
url: entry.url,
|
|
722
|
+
headers: entry.headers,
|
|
723
|
+
enabled: entry.disabled !== true
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
729
|
+
result.candidates.push({
|
|
730
|
+
clientId: this.id,
|
|
731
|
+
configPath,
|
|
732
|
+
sourceEntryName: name,
|
|
733
|
+
serverName: name,
|
|
734
|
+
spec: {
|
|
735
|
+
transport: "stdio",
|
|
736
|
+
command: entry.command,
|
|
737
|
+
args: entry.args,
|
|
738
|
+
env: entry.env,
|
|
739
|
+
cwd: entry.cwd,
|
|
740
|
+
enabled: entry.disabled !== true
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
syncGateway(_config, options) {
|
|
750
|
+
const configPath = this.detectConfigPath();
|
|
751
|
+
if (!configPath) {
|
|
752
|
+
return errorResult(this.id, undefined, "Unable to resolve Claude Desktop config path.");
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const raw = readJsonFile(configPath, {});
|
|
756
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
757
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => {
|
|
758
|
+
const { command, args } = buildProxyEntry(entry);
|
|
759
|
+
return [entry.name, { command, args }];
|
|
760
|
+
}));
|
|
761
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
762
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
763
|
+
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
764
|
+
for (const name of enabledManagedNames) {
|
|
765
|
+
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
766
|
+
if (topLevelConflict) {
|
|
767
|
+
return errorResult(this.id, configPath, topLevelConflict);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
771
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
772
|
+
topLevelServers[name] = entry;
|
|
773
|
+
}
|
|
774
|
+
raw.mcpServers = topLevelServers;
|
|
775
|
+
writeJsonAtomic(configPath, raw);
|
|
776
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
|
|
777
|
+
const { command, args } = buildProxyEntry(entry);
|
|
778
|
+
return [entry.name, JSON.stringify({ command, args })];
|
|
779
|
+
})));
|
|
780
|
+
return okResult(this.id, configPath);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
return errorResult(this.id, configPath, error.message);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/adapters/codex.ts
|
|
788
|
+
import path5 from "node:path";
|
|
789
|
+
import fs4 from "node:fs";
|
|
790
|
+
import { parse, stringify } from "@iarna/toml";
|
|
791
|
+
import { z as z3 } from "zod";
|
|
792
|
+
var stringMapSchema3 = z3.preprocess((value) => {
|
|
793
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
return Object.fromEntries(Object.entries(value));
|
|
797
|
+
}, z3.record(z3.string(), z3.string()));
|
|
798
|
+
var codexEntrySchema = z3.object({
|
|
799
|
+
enabled: z3.boolean().optional(),
|
|
800
|
+
url: z3.string().min(1).optional(),
|
|
801
|
+
headers: stringMapSchema3.optional(),
|
|
802
|
+
http_headers: stringMapSchema3.optional(),
|
|
803
|
+
command: z3.string().min(1).optional(),
|
|
804
|
+
args: z3.array(z3.string()).optional(),
|
|
805
|
+
env: stringMapSchema3.optional(),
|
|
806
|
+
cwd: z3.string().min(1).optional()
|
|
807
|
+
}).passthrough();
|
|
808
|
+
|
|
809
|
+
class CodexAdapter {
|
|
810
|
+
id = "codex";
|
|
811
|
+
detectConfigPath() {
|
|
812
|
+
return path5.join(homeDir(), ".codex", "config.toml");
|
|
813
|
+
}
|
|
814
|
+
supportsHttp() {
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
syncPlugins(plugins) {
|
|
818
|
+
return syncPluginsToClient(this.id, plugins);
|
|
819
|
+
}
|
|
820
|
+
scanForImports(_config, managedIndex) {
|
|
821
|
+
const configPath = this.detectConfigPath();
|
|
822
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
823
|
+
if (!configPath || !fs4.existsSync(configPath)) {
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
const existing = fs4.readFileSync(configPath, "utf8");
|
|
827
|
+
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
828
|
+
const mcpServers = doc.mcp_servers ?? {};
|
|
829
|
+
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
830
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const parsed = codexEntrySchema.safeParse(rawEntry);
|
|
834
|
+
if (!parsed.success) {
|
|
835
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const entry = parsed.data;
|
|
839
|
+
if (entry.url && !entry.command) {
|
|
840
|
+
result.candidates.push({
|
|
841
|
+
clientId: this.id,
|
|
842
|
+
configPath,
|
|
843
|
+
sourceEntryName: name,
|
|
844
|
+
serverName: name,
|
|
845
|
+
spec: {
|
|
846
|
+
transport: "http",
|
|
847
|
+
url: entry.url,
|
|
848
|
+
headers: entry.http_headers ?? entry.headers,
|
|
849
|
+
enabled: entry.enabled !== false
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (entry.command && !entry.url) {
|
|
855
|
+
result.candidates.push({
|
|
856
|
+
clientId: this.id,
|
|
857
|
+
configPath,
|
|
858
|
+
sourceEntryName: name,
|
|
859
|
+
serverName: name,
|
|
860
|
+
spec: {
|
|
861
|
+
transport: "stdio",
|
|
862
|
+
command: entry.command,
|
|
863
|
+
args: entry.args,
|
|
864
|
+
env: entry.env,
|
|
865
|
+
cwd: entry.cwd,
|
|
866
|
+
enabled: entry.enabled !== false
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
872
|
+
}
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
syncGateway(_config, options) {
|
|
876
|
+
const configPath = this.detectConfigPath();
|
|
877
|
+
if (!configPath) {
|
|
878
|
+
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const existing = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf8") : "";
|
|
882
|
+
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
883
|
+
const mcpServers = doc.mcp_servers ?? {};
|
|
884
|
+
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
885
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
886
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
887
|
+
enabled: entry.enabled,
|
|
888
|
+
url: entry.url,
|
|
889
|
+
http_headers: entry.headers
|
|
890
|
+
}]));
|
|
891
|
+
for (const name of managedNames) {
|
|
892
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
893
|
+
if (conflict) {
|
|
894
|
+
return errorResult(this.id, configPath, conflict);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
|
|
898
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
899
|
+
mcpServers[name] = entry;
|
|
900
|
+
}
|
|
901
|
+
doc.mcp_servers = mcpServers;
|
|
902
|
+
fs4.mkdirSync(path5.dirname(configPath), { recursive: true });
|
|
903
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
904
|
+
fs4.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
905
|
+
fs4.renameSync(tmpPath, configPath);
|
|
906
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
907
|
+
return okResult(this.id, configPath);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
return errorResult(this.id, configPath, error.message);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/adapters/cursor.ts
|
|
915
|
+
import path6 from "node:path";
|
|
916
|
+
import { z as z4 } from "zod";
|
|
917
|
+
var stringMapSchema4 = z4.record(z4.string(), z4.string());
|
|
918
|
+
var cursorEntrySchema = z4.object({
|
|
919
|
+
type: z4.string().optional(),
|
|
920
|
+
url: z4.string().min(1).optional(),
|
|
921
|
+
headers: stringMapSchema4.optional(),
|
|
922
|
+
command: z4.string().min(1).optional(),
|
|
923
|
+
args: z4.array(z4.string()).optional(),
|
|
924
|
+
env: stringMapSchema4.optional(),
|
|
925
|
+
cwd: z4.string().min(1).optional(),
|
|
926
|
+
disabled: z4.boolean().optional()
|
|
927
|
+
}).passthrough();
|
|
928
|
+
|
|
929
|
+
class CursorAdapter {
|
|
930
|
+
id = "cursor";
|
|
931
|
+
detectConfigPath() {
|
|
932
|
+
return path6.join(homeDir(), ".cursor", "mcp.json");
|
|
933
|
+
}
|
|
934
|
+
supportsHttp() {
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
syncPlugins(plugins) {
|
|
938
|
+
return syncPluginsToClient(this.id, plugins);
|
|
939
|
+
}
|
|
940
|
+
scanForImports(_config, managedIndex) {
|
|
941
|
+
const configPath = this.detectConfigPath();
|
|
942
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
943
|
+
if (!configPath) {
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
const raw = readJsonFile(configPath, {});
|
|
947
|
+
const servers = {
|
|
948
|
+
...raw.mcpServers ?? {}
|
|
949
|
+
};
|
|
950
|
+
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
951
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const parsed = cursorEntrySchema.safeParse(rawEntry);
|
|
955
|
+
if (!parsed.success) {
|
|
956
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const entry = parsed.data;
|
|
960
|
+
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
961
|
+
result.candidates.push({
|
|
962
|
+
clientId: this.id,
|
|
963
|
+
configPath,
|
|
964
|
+
sourceEntryName: name,
|
|
965
|
+
serverName: name,
|
|
966
|
+
spec: {
|
|
967
|
+
transport: "http",
|
|
968
|
+
url: entry.url,
|
|
969
|
+
headers: entry.headers,
|
|
970
|
+
enabled: entry.disabled !== true
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
976
|
+
result.candidates.push({
|
|
977
|
+
clientId: this.id,
|
|
978
|
+
configPath,
|
|
979
|
+
sourceEntryName: name,
|
|
980
|
+
serverName: name,
|
|
981
|
+
spec: {
|
|
982
|
+
transport: "stdio",
|
|
983
|
+
command: entry.command,
|
|
984
|
+
args: entry.args,
|
|
985
|
+
env: entry.env,
|
|
986
|
+
cwd: entry.cwd,
|
|
987
|
+
enabled: entry.disabled !== true
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
993
|
+
}
|
|
994
|
+
return result;
|
|
995
|
+
}
|
|
996
|
+
syncGateway(_config, options) {
|
|
997
|
+
const configPath = this.detectConfigPath();
|
|
998
|
+
if (!configPath) {
|
|
999
|
+
return errorResult(this.id, undefined, "Unable to resolve Cursor MCP config path.");
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
const raw = readJsonFile(configPath, {});
|
|
1003
|
+
const servers = {
|
|
1004
|
+
...raw.mcpServers ?? {}
|
|
1005
|
+
};
|
|
1006
|
+
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1007
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1008
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1009
|
+
type: "http",
|
|
1010
|
+
url: entry.url,
|
|
1011
|
+
headers: entry.headers
|
|
1012
|
+
}]));
|
|
1013
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1014
|
+
for (const name of enabledManagedNames) {
|
|
1015
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1016
|
+
if (conflict) {
|
|
1017
|
+
return errorResult(this.id, configPath, conflict);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1021
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1022
|
+
servers[name] = entry;
|
|
1023
|
+
}
|
|
1024
|
+
const next = {
|
|
1025
|
+
...raw,
|
|
1026
|
+
mcpServers: servers
|
|
1027
|
+
};
|
|
1028
|
+
writeJsonAtomic(configPath, next);
|
|
1029
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1030
|
+
entry.name,
|
|
1031
|
+
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1032
|
+
])));
|
|
1033
|
+
return okResult(this.id, configPath);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
return errorResult(this.id, configPath, error.message);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/adapters/cline.ts
|
|
1041
|
+
import path7 from "node:path";
|
|
1042
|
+
import fs5 from "node:fs";
|
|
1043
|
+
import { z as z5 } from "zod";
|
|
1044
|
+
var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
|
|
1045
|
+
var stringMapSchema5 = z5.record(z5.string(), z5.string());
|
|
1046
|
+
var clineEntrySchema = z5.object({
|
|
1047
|
+
type: z5.string().optional(),
|
|
1048
|
+
transportType: z5.string().optional(),
|
|
1049
|
+
url: z5.string().min(1).optional(),
|
|
1050
|
+
headers: stringMapSchema5.optional(),
|
|
1051
|
+
command: z5.string().min(1).optional(),
|
|
1052
|
+
args: z5.array(z5.string()).optional(),
|
|
1053
|
+
env: stringMapSchema5.optional(),
|
|
1054
|
+
cwd: z5.string().min(1).optional(),
|
|
1055
|
+
disabled: z5.boolean().optional()
|
|
1056
|
+
}).passthrough();
|
|
1057
|
+
function getCandidatePaths() {
|
|
1058
|
+
if (process.platform === "linux") {
|
|
1059
|
+
return [
|
|
1060
|
+
path7.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1061
|
+
path7.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1062
|
+
path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1063
|
+
];
|
|
1064
|
+
}
|
|
1065
|
+
if (process.platform === "win32") {
|
|
1066
|
+
return [
|
|
1067
|
+
path7.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1068
|
+
path7.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1069
|
+
path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1070
|
+
];
|
|
1071
|
+
}
|
|
1072
|
+
return [
|
|
1073
|
+
path7.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1074
|
+
path7.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1075
|
+
path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1076
|
+
];
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
class ClineAdapter {
|
|
1080
|
+
id = "cline";
|
|
1081
|
+
detectConfigPath() {
|
|
1082
|
+
const candidates = getCandidatePaths();
|
|
1083
|
+
for (const candidate of candidates) {
|
|
1084
|
+
if (fs5.existsSync(candidate)) {
|
|
1085
|
+
return candidate;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return candidates[0] ?? null;
|
|
1089
|
+
}
|
|
1090
|
+
supportsHttp() {
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
scanForImports(_config, managedIndex) {
|
|
1094
|
+
const configPath = this.detectConfigPath();
|
|
1095
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1096
|
+
if (!configPath || !fs5.existsSync(configPath)) {
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
const raw = readJsonFile(configPath, {});
|
|
1100
|
+
const servers = {
|
|
1101
|
+
...raw.mcpServers ?? {}
|
|
1102
|
+
};
|
|
1103
|
+
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1104
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const parsed = clineEntrySchema.safeParse(rawEntry);
|
|
1108
|
+
if (!parsed.success) {
|
|
1109
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const entry = parsed.data;
|
|
1113
|
+
const transport = entry.type ?? entry.transportType;
|
|
1114
|
+
if ((transport === undefined || HTTP_TRANSPORT_TYPES.has(transport)) && entry.url && !entry.command) {
|
|
1115
|
+
result.candidates.push({
|
|
1116
|
+
clientId: this.id,
|
|
1117
|
+
configPath,
|
|
1118
|
+
sourceEntryName: name,
|
|
1119
|
+
serverName: name,
|
|
1120
|
+
spec: {
|
|
1121
|
+
transport: "http",
|
|
1122
|
+
url: entry.url,
|
|
1123
|
+
headers: entry.headers,
|
|
1124
|
+
enabled: entry.disabled !== true
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if ((transport === undefined || transport === "stdio") && entry.command && !entry.url) {
|
|
1130
|
+
result.candidates.push({
|
|
1131
|
+
clientId: this.id,
|
|
1132
|
+
configPath,
|
|
1133
|
+
sourceEntryName: name,
|
|
1134
|
+
serverName: name,
|
|
1135
|
+
spec: {
|
|
1136
|
+
transport: "stdio",
|
|
1137
|
+
command: entry.command,
|
|
1138
|
+
args: entry.args,
|
|
1139
|
+
env: entry.env,
|
|
1140
|
+
cwd: entry.cwd,
|
|
1141
|
+
enabled: entry.disabled !== true
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1147
|
+
}
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
syncGateway(_config, options) {
|
|
1151
|
+
const configPath = this.detectConfigPath();
|
|
1152
|
+
if (!configPath) {
|
|
1153
|
+
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1154
|
+
}
|
|
1155
|
+
if (!fs5.existsSync(configPath)) {
|
|
1156
|
+
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const raw = readJsonFile(configPath, {});
|
|
1160
|
+
const servers = {
|
|
1161
|
+
...raw.mcpServers ?? {}
|
|
1162
|
+
};
|
|
1163
|
+
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1164
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1165
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1166
|
+
type: "streamableHttp",
|
|
1167
|
+
url: entry.url,
|
|
1168
|
+
headers: entry.headers,
|
|
1169
|
+
disabled: !entry.enabled
|
|
1170
|
+
}]));
|
|
1171
|
+
for (const name of managedNames) {
|
|
1172
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1173
|
+
if (conflict) {
|
|
1174
|
+
return errorResult(this.id, configPath, conflict);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
|
|
1178
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1179
|
+
servers[name] = entry;
|
|
1180
|
+
}
|
|
1181
|
+
const next = {
|
|
1182
|
+
...raw,
|
|
1183
|
+
mcpServers: servers
|
|
1184
|
+
};
|
|
1185
|
+
writeJsonAtomic(configPath, next);
|
|
1186
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1187
|
+
return okResult(this.id, configPath);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
return errorResult(this.id, configPath, error.message);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/adapters/kiro.ts
|
|
1195
|
+
import fs6 from "node:fs";
|
|
1196
|
+
import path8 from "node:path";
|
|
1197
|
+
import { z as z6 } from "zod";
|
|
1198
|
+
var stringMapSchema6 = z6.record(z6.string(), z6.string());
|
|
1199
|
+
var kiroEntrySchema = z6.object({
|
|
1200
|
+
type: z6.string().optional(),
|
|
1201
|
+
url: z6.string().min(1).optional(),
|
|
1202
|
+
headers: stringMapSchema6.optional(),
|
|
1203
|
+
command: z6.string().min(1).optional(),
|
|
1204
|
+
args: z6.array(z6.string()).optional(),
|
|
1205
|
+
env: stringMapSchema6.optional(),
|
|
1206
|
+
cwd: z6.string().min(1).optional(),
|
|
1207
|
+
disabled: z6.boolean().optional()
|
|
1208
|
+
}).passthrough();
|
|
1209
|
+
|
|
1210
|
+
class KiroAdapter {
|
|
1211
|
+
id = "kiro";
|
|
1212
|
+
detectConfigPath() {
|
|
1213
|
+
return path8.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
1214
|
+
}
|
|
1215
|
+
supportsHttp() {
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
syncPlugins(plugins) {
|
|
1219
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1220
|
+
}
|
|
1221
|
+
scanForImports(_config, managedIndex) {
|
|
1222
|
+
const configPath = this.detectConfigPath();
|
|
1223
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1224
|
+
if (!configPath) {
|
|
1225
|
+
return result;
|
|
1226
|
+
}
|
|
1227
|
+
const raw = readJsonFile(configPath, {});
|
|
1228
|
+
const servers = {
|
|
1229
|
+
...raw.mcpServers ?? {}
|
|
1230
|
+
};
|
|
1231
|
+
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1232
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const parsed = kiroEntrySchema.safeParse(rawEntry);
|
|
1236
|
+
if (!parsed.success) {
|
|
1237
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const entry = parsed.data;
|
|
1241
|
+
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http" || entry.type === "sse") && entry.url && !entry.command) {
|
|
1242
|
+
result.candidates.push({
|
|
1243
|
+
clientId: this.id,
|
|
1244
|
+
configPath,
|
|
1245
|
+
sourceEntryName: name,
|
|
1246
|
+
serverName: name,
|
|
1247
|
+
spec: {
|
|
1248
|
+
transport: "http",
|
|
1249
|
+
url: entry.url,
|
|
1250
|
+
headers: entry.headers,
|
|
1251
|
+
enabled: entry.disabled !== true
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1257
|
+
result.candidates.push({
|
|
1258
|
+
clientId: this.id,
|
|
1259
|
+
configPath,
|
|
1260
|
+
sourceEntryName: name,
|
|
1261
|
+
serverName: name,
|
|
1262
|
+
spec: {
|
|
1263
|
+
transport: "stdio",
|
|
1264
|
+
command: entry.command,
|
|
1265
|
+
args: entry.args,
|
|
1266
|
+
env: entry.env,
|
|
1267
|
+
cwd: entry.cwd,
|
|
1268
|
+
enabled: entry.disabled !== true
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1274
|
+
}
|
|
1275
|
+
return result;
|
|
1276
|
+
}
|
|
1277
|
+
syncGateway(_config, options) {
|
|
1278
|
+
const configPath = this.detectConfigPath();
|
|
1279
|
+
if (!configPath) {
|
|
1280
|
+
return errorResult(this.id, undefined, "Unable to resolve Kiro MCP config path.");
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
const raw = readJsonFile(configPath, {});
|
|
1284
|
+
const servers = {
|
|
1285
|
+
...raw.mcpServers ?? {}
|
|
1286
|
+
};
|
|
1287
|
+
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1288
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1289
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1290
|
+
url: entry.url,
|
|
1291
|
+
headers: entry.headers,
|
|
1292
|
+
disabled: !entry.enabled
|
|
1293
|
+
}]));
|
|
1294
|
+
for (const name of managedNames) {
|
|
1295
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1296
|
+
if (conflict) {
|
|
1297
|
+
return errorResult(this.id, configPath, conflict);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
|
|
1301
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1302
|
+
servers[name] = entry;
|
|
1303
|
+
}
|
|
1304
|
+
const next = {
|
|
1305
|
+
...raw,
|
|
1306
|
+
mcpServers: servers
|
|
1307
|
+
};
|
|
1308
|
+
writeJsonAtomic(configPath, next);
|
|
1309
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1310
|
+
return okResult(this.id, configPath);
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
return errorResult(this.id, configPath, error.message);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
syncSkills(skills) {
|
|
1316
|
+
const skillsDir = path8.join(homeDir(), ".kiro", "skills");
|
|
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
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/adapters/opencode.ts
|
|
1338
|
+
import path9 from "node:path";
|
|
1339
|
+
import { z as z7 } from "zod";
|
|
1340
|
+
var stringMapSchema7 = z7.record(z7.string(), z7.string());
|
|
1341
|
+
var openCodeEntrySchema = z7.object({
|
|
1342
|
+
type: z7.string().optional(),
|
|
1343
|
+
url: z7.string().min(1).optional(),
|
|
1344
|
+
headers: stringMapSchema7.optional(),
|
|
1345
|
+
command: z7.union([z7.string().min(1), z7.array(z7.string())]).optional(),
|
|
1346
|
+
args: z7.array(z7.string()).optional(),
|
|
1347
|
+
env: stringMapSchema7.optional(),
|
|
1348
|
+
environment: stringMapSchema7.optional(),
|
|
1349
|
+
cwd: z7.string().min(1).optional(),
|
|
1350
|
+
enabled: z7.boolean().optional()
|
|
1351
|
+
}).passthrough();
|
|
1352
|
+
|
|
1353
|
+
class OpenCodeAdapter {
|
|
1354
|
+
id = "opencode";
|
|
1355
|
+
detectConfigPath() {
|
|
1356
|
+
return path9.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
1357
|
+
}
|
|
1358
|
+
supportsHttp() {
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
scanForImports(_config, managedIndex) {
|
|
1362
|
+
const configPath = this.detectConfigPath();
|
|
1363
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1364
|
+
if (!configPath) {
|
|
1365
|
+
return result;
|
|
1366
|
+
}
|
|
1367
|
+
const raw = readJsonFile(configPath, {});
|
|
1368
|
+
const mcp = {
|
|
1369
|
+
...raw.mcp ?? {}
|
|
1370
|
+
};
|
|
1371
|
+
for (const [name, rawEntry] of Object.entries(mcp)) {
|
|
1372
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const parsed = openCodeEntrySchema.safeParse(rawEntry);
|
|
1376
|
+
if (!parsed.success) {
|
|
1377
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const entry = parsed.data;
|
|
1381
|
+
if ((entry.type === undefined || entry.type === "remote" || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1382
|
+
result.candidates.push({
|
|
1383
|
+
clientId: this.id,
|
|
1384
|
+
configPath,
|
|
1385
|
+
sourceEntryName: name,
|
|
1386
|
+
serverName: name,
|
|
1387
|
+
spec: {
|
|
1388
|
+
transport: "http",
|
|
1389
|
+
url: entry.url,
|
|
1390
|
+
headers: entry.headers,
|
|
1391
|
+
enabled: entry.enabled !== false
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if ((entry.type === undefined || entry.type === "local" || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1397
|
+
const commandParts = Array.isArray(entry.command) ? entry.command : [entry.command, ...entry.args ?? []];
|
|
1398
|
+
if (commandParts.length === 0) {
|
|
1399
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
result.candidates.push({
|
|
1403
|
+
clientId: this.id,
|
|
1404
|
+
configPath,
|
|
1405
|
+
sourceEntryName: name,
|
|
1406
|
+
serverName: name,
|
|
1407
|
+
spec: {
|
|
1408
|
+
transport: "stdio",
|
|
1409
|
+
command: commandParts[0],
|
|
1410
|
+
args: commandParts.slice(1),
|
|
1411
|
+
env: entry.environment ?? entry.env,
|
|
1412
|
+
cwd: entry.cwd,
|
|
1413
|
+
enabled: entry.enabled !== false
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1419
|
+
}
|
|
1420
|
+
return result;
|
|
1421
|
+
}
|
|
1422
|
+
syncGateway(_config, options) {
|
|
1423
|
+
const configPath = this.detectConfigPath();
|
|
1424
|
+
if (!configPath) {
|
|
1425
|
+
return errorResult(this.id, undefined, "Unable to resolve OpenCode config path.");
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = readJsonFile(configPath, {});
|
|
1429
|
+
const mcp = {
|
|
1430
|
+
...raw.mcp ?? {}
|
|
1431
|
+
};
|
|
1432
|
+
removeSourceEntries(mcp, options.sourceEntriesToRemove);
|
|
1433
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1434
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1435
|
+
type: "remote",
|
|
1436
|
+
url: entry.url,
|
|
1437
|
+
headers: entry.headers,
|
|
1438
|
+
enabled: entry.enabled
|
|
1439
|
+
}]));
|
|
1440
|
+
for (const name of managedNames) {
|
|
1441
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
|
|
1442
|
+
if (conflict) {
|
|
1443
|
+
return errorResult(this.id, configPath, conflict);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, mcp, managedNames);
|
|
1447
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1448
|
+
mcp[name] = entry;
|
|
1449
|
+
}
|
|
1450
|
+
const next = {
|
|
1451
|
+
...raw,
|
|
1452
|
+
mcp
|
|
1453
|
+
};
|
|
1454
|
+
writeJsonAtomic(configPath, next);
|
|
1455
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1456
|
+
return okResult(this.id, configPath);
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
return errorResult(this.id, configPath, error.message);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/adapters/vscode.ts
|
|
1464
|
+
import path10 from "node:path";
|
|
1465
|
+
import { z as z8 } from "zod";
|
|
1466
|
+
var stringMapSchema8 = z8.record(z8.string(), z8.string());
|
|
1467
|
+
var vscodeEntrySchema = z8.object({
|
|
1468
|
+
type: z8.string().optional(),
|
|
1469
|
+
url: z8.string().min(1).optional(),
|
|
1470
|
+
headers: stringMapSchema8.optional(),
|
|
1471
|
+
command: z8.string().min(1).optional(),
|
|
1472
|
+
args: z8.array(z8.string()).optional(),
|
|
1473
|
+
env: stringMapSchema8.optional(),
|
|
1474
|
+
cwd: z8.string().min(1).optional(),
|
|
1475
|
+
disabled: z8.boolean().optional()
|
|
1476
|
+
}).passthrough();
|
|
1477
|
+
|
|
1478
|
+
class VsCodeAdapter {
|
|
1479
|
+
id = "vscode";
|
|
1480
|
+
detectConfigPath() {
|
|
1481
|
+
if (process.platform === "linux") {
|
|
1482
|
+
return path10.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
1483
|
+
}
|
|
1484
|
+
if (process.platform === "win32") {
|
|
1485
|
+
return path10.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
1486
|
+
}
|
|
1487
|
+
return path10.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
1488
|
+
}
|
|
1489
|
+
supportsHttp() {
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
syncPlugins(plugins) {
|
|
1493
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1494
|
+
}
|
|
1495
|
+
scanForImports(_config, managedIndex) {
|
|
1496
|
+
const configPath = this.detectConfigPath();
|
|
1497
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1498
|
+
if (!configPath) {
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
const raw = readJsonFile(configPath, {});
|
|
1502
|
+
const servers = {
|
|
1503
|
+
...raw.servers ?? {}
|
|
1504
|
+
};
|
|
1505
|
+
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1506
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
const parsed = vscodeEntrySchema.safeParse(rawEntry);
|
|
1510
|
+
if (!parsed.success) {
|
|
1511
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
const entry = parsed.data;
|
|
1515
|
+
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1516
|
+
result.candidates.push({
|
|
1517
|
+
clientId: this.id,
|
|
1518
|
+
configPath,
|
|
1519
|
+
sourceEntryName: name,
|
|
1520
|
+
serverName: name,
|
|
1521
|
+
spec: {
|
|
1522
|
+
transport: "http",
|
|
1523
|
+
url: entry.url,
|
|
1524
|
+
headers: entry.headers,
|
|
1525
|
+
enabled: entry.disabled !== true
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1531
|
+
result.candidates.push({
|
|
1532
|
+
clientId: this.id,
|
|
1533
|
+
configPath,
|
|
1534
|
+
sourceEntryName: name,
|
|
1535
|
+
serverName: name,
|
|
1536
|
+
spec: {
|
|
1537
|
+
transport: "stdio",
|
|
1538
|
+
command: entry.command,
|
|
1539
|
+
args: entry.args,
|
|
1540
|
+
env: entry.env,
|
|
1541
|
+
cwd: entry.cwd,
|
|
1542
|
+
enabled: entry.disabled !== true
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1548
|
+
}
|
|
1549
|
+
return result;
|
|
1550
|
+
}
|
|
1551
|
+
syncGateway(_config, options) {
|
|
1552
|
+
const configPath = this.detectConfigPath();
|
|
1553
|
+
if (!configPath) {
|
|
1554
|
+
return errorResult(this.id, undefined, "Unable to resolve VS Code MCP config path.");
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const raw = readJsonFile(configPath, {});
|
|
1558
|
+
const servers = {
|
|
1559
|
+
...raw.servers ?? {}
|
|
1560
|
+
};
|
|
1561
|
+
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1562
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1563
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1564
|
+
type: "http",
|
|
1565
|
+
url: entry.url,
|
|
1566
|
+
headers: entry.headers
|
|
1567
|
+
}]));
|
|
1568
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1569
|
+
for (const name of enabledManagedNames) {
|
|
1570
|
+
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1571
|
+
if (conflict) {
|
|
1572
|
+
return errorResult(this.id, configPath, conflict);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1576
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1577
|
+
servers[name] = entry;
|
|
1578
|
+
}
|
|
1579
|
+
const next = {
|
|
1580
|
+
...raw,
|
|
1581
|
+
servers
|
|
1582
|
+
};
|
|
1583
|
+
writeJsonAtomic(configPath, next);
|
|
1584
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1585
|
+
entry.name,
|
|
1586
|
+
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1587
|
+
])));
|
|
1588
|
+
return okResult(this.id, configPath);
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
return errorResult(this.id, configPath, error.message);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/adapters/qwen.ts
|
|
1596
|
+
import fs7 from "node:fs";
|
|
1597
|
+
import path11 from "node:path";
|
|
1598
|
+
import { z as z9 } from "zod";
|
|
1599
|
+
var stringMapSchema9 = z9.record(z9.string(), z9.string());
|
|
1600
|
+
var qwenEntrySchema = z9.object({
|
|
1601
|
+
httpUrl: z9.string().min(1).optional(),
|
|
1602
|
+
url: z9.string().min(1).optional(),
|
|
1603
|
+
headers: stringMapSchema9.optional(),
|
|
1604
|
+
command: z9.string().min(1).optional(),
|
|
1605
|
+
args: z9.array(z9.string()).optional(),
|
|
1606
|
+
env: stringMapSchema9.optional(),
|
|
1607
|
+
cwd: z9.string().min(1).optional(),
|
|
1608
|
+
disabled: z9.boolean().optional()
|
|
1609
|
+
}).passthrough();
|
|
1610
|
+
|
|
1611
|
+
class QwenAdapter {
|
|
1612
|
+
id = "qwen";
|
|
1613
|
+
detectConfigPath() {
|
|
1614
|
+
return path11.join(homeDir(), ".qwen", "settings.json");
|
|
1615
|
+
}
|
|
1616
|
+
supportsHttp() {
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
syncPlugins(plugins) {
|
|
1620
|
+
return syncPluginsToClient(this.id, plugins);
|
|
1621
|
+
}
|
|
1622
|
+
scanForImports(_config, managedIndex) {
|
|
1623
|
+
const configPath = this.detectConfigPath();
|
|
1624
|
+
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1625
|
+
if (!configPath) {
|
|
1626
|
+
return result;
|
|
1627
|
+
}
|
|
1628
|
+
const raw = readJsonFile(configPath, {});
|
|
1629
|
+
const excludedSet = new Set(raw.mcp?.excluded ?? []);
|
|
1630
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
1631
|
+
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
1632
|
+
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
const parsed = qwenEntrySchema.safeParse(rawEntry);
|
|
1636
|
+
if (!parsed.success) {
|
|
1637
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
const entry = parsed.data;
|
|
1641
|
+
if (entry.httpUrl && !entry.command) {
|
|
1642
|
+
result.candidates.push({
|
|
1643
|
+
clientId: this.id,
|
|
1644
|
+
configPath,
|
|
1645
|
+
sourceEntryName: name,
|
|
1646
|
+
serverName: name,
|
|
1647
|
+
spec: {
|
|
1648
|
+
transport: "http",
|
|
1649
|
+
url: entry.httpUrl,
|
|
1650
|
+
headers: entry.headers,
|
|
1651
|
+
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (entry.url && !entry.httpUrl && !entry.command) {
|
|
1657
|
+
result.candidates.push({
|
|
1658
|
+
clientId: this.id,
|
|
1659
|
+
configPath,
|
|
1660
|
+
sourceEntryName: name,
|
|
1661
|
+
serverName: name,
|
|
1662
|
+
spec: {
|
|
1663
|
+
transport: "http",
|
|
1664
|
+
url: entry.url,
|
|
1665
|
+
headers: entry.headers,
|
|
1666
|
+
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (entry.command && !entry.httpUrl) {
|
|
1672
|
+
result.candidates.push({
|
|
1673
|
+
clientId: this.id,
|
|
1674
|
+
configPath,
|
|
1675
|
+
sourceEntryName: name,
|
|
1676
|
+
serverName: name,
|
|
1677
|
+
spec: {
|
|
1678
|
+
transport: "stdio",
|
|
1679
|
+
command: entry.command,
|
|
1680
|
+
args: entry.args,
|
|
1681
|
+
env: entry.env,
|
|
1682
|
+
cwd: entry.cwd,
|
|
1683
|
+
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1689
|
+
}
|
|
1690
|
+
return result;
|
|
1691
|
+
}
|
|
1692
|
+
syncGateway(_config, options) {
|
|
1693
|
+
const configPath = this.detectConfigPath();
|
|
1694
|
+
if (!configPath) {
|
|
1695
|
+
return errorResult(this.id, undefined, "Unable to resolve Qwen config path.");
|
|
1696
|
+
}
|
|
1697
|
+
try {
|
|
1698
|
+
const raw = readJsonFile(configPath, {});
|
|
1699
|
+
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1700
|
+
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1701
|
+
httpUrl: entry.url,
|
|
1702
|
+
headers: entry.headers
|
|
1703
|
+
}]));
|
|
1704
|
+
const topLevelServers = raw.mcpServers ?? {};
|
|
1705
|
+
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1706
|
+
for (const name of managedNames) {
|
|
1707
|
+
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1708
|
+
if (topLevelConflict) {
|
|
1709
|
+
return errorResult(this.id, configPath, topLevelConflict);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, managedNames);
|
|
1713
|
+
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1714
|
+
topLevelServers[name] = entry;
|
|
1715
|
+
}
|
|
1716
|
+
raw.mcpServers = topLevelServers;
|
|
1717
|
+
syncMcpExcludedArray(raw, managedNames, options.managedEntries);
|
|
1718
|
+
writeJsonAtomic(configPath, raw);
|
|
1719
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1720
|
+
entry.name,
|
|
1721
|
+
JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1722
|
+
])));
|
|
1723
|
+
return okResult(this.id, configPath);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
return errorResult(this.id, configPath, error.message);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
syncSkills(skills) {
|
|
1729
|
+
const skillsDir = path11.join(homeDir(), ".qwen", "skills");
|
|
1730
|
+
if (!fs7.existsSync(skillsDir)) {
|
|
1731
|
+
fs7.mkdirSync(skillsDir, { recursive: true });
|
|
1732
|
+
}
|
|
1733
|
+
const existingFiles = fs7.readdirSync(skillsDir);
|
|
1734
|
+
const managedIds = new Set(skills.map((s) => s.id));
|
|
1735
|
+
for (const file of existingFiles) {
|
|
1736
|
+
if (file.endsWith(".md")) {
|
|
1737
|
+
const id = path11.basename(file, ".md");
|
|
1738
|
+
if (!managedIds.has(id)) {
|
|
1739
|
+
fs7.unlinkSync(path11.join(skillsDir, file));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
for (const skill of skills) {
|
|
1744
|
+
const targetPath = path11.join(skillsDir, `${skill.id}.md`);
|
|
1745
|
+
fs7.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/adapters/index.ts
|
|
1751
|
+
function getAdapters() {
|
|
1752
|
+
return [
|
|
1753
|
+
new ClaudeAdapter,
|
|
1754
|
+
new ClaudeDesktopAdapter,
|
|
1755
|
+
new CodexAdapter,
|
|
1756
|
+
new CursorAdapter,
|
|
1757
|
+
new ClineAdapter,
|
|
1758
|
+
new OpenCodeAdapter,
|
|
1759
|
+
new KiroAdapter,
|
|
1760
|
+
new VsCodeAdapter,
|
|
1761
|
+
new QwenAdapter
|
|
1762
|
+
];
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// src/core/managed-index.ts
|
|
1766
|
+
function defaultManagedIndex() {
|
|
1767
|
+
return {
|
|
1768
|
+
schemaVersion: 1,
|
|
1769
|
+
managed: {}
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function loadManagedIndex(filePath = getManagedIndexPath()) {
|
|
1773
|
+
const raw = readJsonFile(filePath, defaultManagedIndex());
|
|
1774
|
+
if (!raw || typeof raw !== "object") {
|
|
1775
|
+
return defaultManagedIndex();
|
|
1776
|
+
}
|
|
1777
|
+
if (raw.schemaVersion !== 1) {
|
|
1778
|
+
return defaultManagedIndex();
|
|
1779
|
+
}
|
|
1780
|
+
return raw;
|
|
1781
|
+
}
|
|
1782
|
+
function saveManagedIndex(index, filePath = getManagedIndexPath()) {
|
|
1783
|
+
writeJsonAtomic(filePath, index);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/core/sync.ts
|
|
1787
|
+
function getGatewayUrl(config) {
|
|
1788
|
+
return `http://127.0.0.1:${config.gateway.port}/mcp`;
|
|
1789
|
+
}
|
|
1790
|
+
function buildProjectScopes(config) {
|
|
1791
|
+
const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
|
|
1792
|
+
return Object.values(config.projects ?? {}).map((project) => {
|
|
1793
|
+
const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
|
|
1794
|
+
return { path: project.path, disabledServerNames };
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
1798
|
+
const entries = Object.entries(config.servers);
|
|
1799
|
+
return entries.map(([name, spec]) => ({
|
|
1800
|
+
name: `${name} (mcpx)`,
|
|
1801
|
+
url: `${gatewayUrl}?upstream=${encodeURIComponent(name)}`,
|
|
1802
|
+
headers: {
|
|
1803
|
+
Authorization: `Bearer ${localToken}`
|
|
1804
|
+
},
|
|
1805
|
+
enabled: isServerEnabled(spec)
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
function isClientInternalServerCommand(candidate) {
|
|
1809
|
+
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
1810
|
+
}
|
|
1811
|
+
function syncAllClients(config, secrets, targetClients) {
|
|
1812
|
+
const adapters = getAdapters();
|
|
1813
|
+
const managedIndexPath = getManagedIndexPath();
|
|
1814
|
+
const managedIndex = loadManagedIndex(managedIndexPath);
|
|
1815
|
+
const filteredAdapters = targetClients && targetClients.length > 0 ? adapters.filter((adapter) => targetClients.includes(adapter.id)) : adapters;
|
|
1816
|
+
const imports = {
|
|
1817
|
+
imported: [],
|
|
1818
|
+
duplicates: [],
|
|
1819
|
+
skipped: [],
|
|
1820
|
+
conflicts: [],
|
|
1821
|
+
errors: []
|
|
1822
|
+
};
|
|
1823
|
+
const sourceEntriesToRemove = new Map;
|
|
1824
|
+
for (const adapter of filteredAdapters) {
|
|
1825
|
+
try {
|
|
1826
|
+
const scan = adapter.scanForImports(config, managedIndex);
|
|
1827
|
+
imports.skipped.push(...scan.skipped);
|
|
1828
|
+
for (const candidate of scan.candidates) {
|
|
1829
|
+
if (isClientInternalServerCommand(candidate)) {
|
|
1830
|
+
imports.skipped.push({
|
|
1831
|
+
clientId: candidate.clientId,
|
|
1832
|
+
configPath: candidate.configPath,
|
|
1833
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1834
|
+
serverName: candidate.serverName,
|
|
1835
|
+
reason: "client-internal server"
|
|
1836
|
+
});
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
const existing = config.servers[candidate.serverName];
|
|
1840
|
+
if (!existing) {
|
|
1841
|
+
config.servers[candidate.serverName] = candidate.spec;
|
|
1842
|
+
imports.imported.push({
|
|
1843
|
+
clientId: candidate.clientId,
|
|
1844
|
+
configPath: candidate.configPath,
|
|
1845
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1846
|
+
serverName: candidate.serverName
|
|
1847
|
+
});
|
|
1848
|
+
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1849
|
+
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1850
|
+
}
|
|
1851
|
+
sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (serverSpecsEqual(existing, candidate.spec)) {
|
|
1855
|
+
imports.duplicates.push({
|
|
1856
|
+
clientId: candidate.clientId,
|
|
1857
|
+
configPath: candidate.configPath,
|
|
1858
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1859
|
+
serverName: candidate.serverName
|
|
1860
|
+
});
|
|
1861
|
+
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1862
|
+
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1863
|
+
}
|
|
1864
|
+
sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
imports.conflicts.push({
|
|
1868
|
+
clientId: candidate.clientId,
|
|
1869
|
+
configPath: candidate.configPath,
|
|
1870
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1871
|
+
serverName: candidate.serverName,
|
|
1872
|
+
message: `Server "${candidate.serverName}" already exists in mcpx with a different configuration.`
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
imports.errors.push({
|
|
1877
|
+
clientId: adapter.id,
|
|
1878
|
+
configPath: adapter.detectConfigPath() ?? undefined,
|
|
1879
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
const localToken = ensureGatewayToken(config, secrets);
|
|
1884
|
+
const gatewayUrl = getGatewayUrl(config);
|
|
1885
|
+
const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
|
|
1886
|
+
const skills = listSkills();
|
|
1887
|
+
const results = [];
|
|
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) {
|
|
1902
|
+
if (adapter.syncSkills) {
|
|
1903
|
+
try {
|
|
1904
|
+
adapter.syncSkills(skills);
|
|
1905
|
+
} catch (error) {}
|
|
1906
|
+
}
|
|
1907
|
+
if (adapter.syncPlugins && pluginInputs.length > 0) {
|
|
1908
|
+
try {
|
|
1909
|
+
adapter.syncPlugins(pluginInputs);
|
|
1910
|
+
} catch (error) {}
|
|
1911
|
+
}
|
|
1912
|
+
if (!adapter.supportsHttp()) {
|
|
1913
|
+
results.push({
|
|
1914
|
+
clientId: adapter.id,
|
|
1915
|
+
status: "UNSUPPORTED_HTTP",
|
|
1916
|
+
message: "Client adapter does not support MCP over HTTP."
|
|
1917
|
+
});
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
const result = adapter.syncGateway(config, {
|
|
1921
|
+
managedEntries,
|
|
1922
|
+
managedIndex,
|
|
1923
|
+
managedIndexPath,
|
|
1924
|
+
sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
|
|
1925
|
+
projectScopes: buildProjectScopes(config)
|
|
1926
|
+
});
|
|
1927
|
+
results.push(result);
|
|
1928
|
+
}
|
|
1929
|
+
for (const result of results) {
|
|
1930
|
+
config.clients[result.clientId] = {
|
|
1931
|
+
status: result.status,
|
|
1932
|
+
configPath: result.configPath,
|
|
1933
|
+
message: result.message,
|
|
1934
|
+
lastSyncAt: new Date().toISOString()
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
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
|
+
}
|
|
1947
|
+
|
|
1948
|
+
export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients };
|