@kwonye/mcpx 0.1.104 → 0.1.106
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/codex.d.ts +2 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/json.d.ts +26 -0
- package/dist/adapters/pi.d.ts +9 -0
- package/dist/chunk-0xt563m1.js +126 -0
- package/dist/{chunk-s9v1epnk.js → chunk-1mrkz29t.js} +71 -56
- package/dist/{chunk-31brdz5s.js → chunk-1v66g9d6.js} +4 -3
- package/dist/chunk-1yq3yb66.js +126 -0
- package/dist/chunk-27mq4agf.js +33 -0
- package/dist/{chunk-941dqsx2.js → chunk-4aafp444.js} +8175 -8066
- package/dist/chunk-4xym942z.js +36 -0
- package/dist/{chunk-tnhekcpv.js → chunk-56z18csx.js} +1 -1
- package/dist/{chunk-bnbz55g6.js → chunk-65kshh9w.js} +1 -1
- package/dist/{chunk-xydajvzg.js → chunk-7ky3rsw9.js} +1 -1
- package/dist/chunk-7rq88hb3.js +563 -0
- package/dist/chunk-8v13hqrp.js +825 -0
- package/dist/chunk-a2gen8bn.js +730 -0
- package/dist/{chunk-wq4zd3t7.js → chunk-brxwa30j.js} +130 -111
- package/dist/chunk-chaj8a8b.js +45 -0
- package/dist/chunk-e96643mj.js +26 -0
- package/dist/{chunk-02r7k4ax.js → chunk-ew06e41w.js} +12 -6
- package/dist/chunk-ezk8txcy.js +23 -0
- package/dist/{chunk-cqenqnxk.js → chunk-grkz21sq.js} +35 -3
- package/dist/{chunk-12vgqm89.js → chunk-gvjxpk98.js} +1 -1
- package/dist/chunk-h9kp47rp.js +18 -0
- package/dist/chunk-hahs24dv.js +127 -0
- package/dist/{chunk-z34m83m4.js → chunk-jv0s0cp6.js} +5 -3
- package/dist/{chunk-enzmq965.js → chunk-n0462d52.js} +1 -1
- package/dist/chunk-nh6hhv29.js +138 -0
- package/dist/chunk-p7yzyt87.js +19426 -0
- package/dist/chunk-q86c5s6r.js +7 -0
- package/dist/chunk-qf8bj9y7.js +254 -0
- package/dist/chunk-t20f49g3.js +30 -0
- package/dist/{chunk-b58zsdp4.js → chunk-v041118a.js} +17 -5
- package/dist/{chunk-91vcwk5m.js → chunk-v14bg5yp.js} +1 -1
- package/dist/chunk-wvzap0na.js +18166 -0
- package/dist/chunk-wztmp5b4.js +30479 -0
- package/dist/chunk-xvqr1q9b.js +14 -0
- package/dist/{chunk-0xhdyt4z.js → chunk-yj7mpczw.js} +17 -5
- package/dist/chunk-yr0txkw0.js +31 -0
- package/dist/cli.js +3977 -1045
- package/dist/core/adoption.d.ts +62 -0
- package/dist/core/analytics.d.ts +17 -0
- package/dist/core/consumers.d.ts +8 -0
- package/dist/core/control.d.ts +12 -0
- package/dist/core/daemon.d.ts +2 -2
- package/dist/core/file-state.d.ts +5 -0
- package/dist/core/harnesses.d.ts +25 -0
- package/dist/core/index.d.ts +17 -7
- package/dist/core/index.js +128 -62
- package/dist/core/managed-index-lock.d.ts +1 -1
- package/dist/core/management.d.ts +32 -0
- package/dist/core/native-document.d.ts +4 -0
- package/dist/core/operations.d.ts +52 -0
- package/dist/core/paths.d.ts +2 -0
- package/dist/core/plugin-manager.d.ts +4 -1
- package/dist/core/plugin-projections.d.ts +1 -0
- package/dist/core/runtime-entry.d.ts +6 -0
- package/dist/core/runtime-snapshot.d.ts +6 -0
- package/dist/core/secret-keychain.d.ts +31 -0
- package/dist/core/secrets.d.ts +12 -2
- package/dist/core/served-skills.d.ts +62 -0
- package/dist/core/share.d.ts +71 -0
- package/dist/core/skills.d.ts +7 -0
- package/dist/core/status.d.ts +1 -0
- package/dist/core/sync-plan.d.ts +14 -0
- package/dist/core/sync.d.ts +3 -0
- package/dist/core/telemetry.d.ts +2 -2
- package/dist/core/tui.d.ts +20 -0
- package/dist/core/verify-upstreams.d.ts +4 -0
- package/dist/core/work-limiter.d.ts +10 -0
- package/dist/gateway/server.d.ts +2 -0
- package/dist/types.d.ts +72 -1
- package/dist/util/fs.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/adapters/opencode.d.ts +0 -11
- package/dist/chunk-cz8cdmw1.js +0 -344
- package/dist/chunk-z1k63fa3.js +0 -3760
package/dist/chunk-z1k63fa3.js
DELETED
|
@@ -1,3760 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadConfig,
|
|
3
|
-
loadProjectConfig,
|
|
4
|
-
saveConfig,
|
|
5
|
-
saveProjectConfig
|
|
6
|
-
} from "./chunk-wq4zd3t7.js";
|
|
7
|
-
import {
|
|
8
|
-
SecretsManager,
|
|
9
|
-
ensureGatewayToken
|
|
10
|
-
} from "./chunk-cz8cdmw1.js";
|
|
11
|
-
import {
|
|
12
|
-
ensureDir,
|
|
13
|
-
findProjectConfigPath,
|
|
14
|
-
getConfigPath,
|
|
15
|
-
getManagedIndexPath,
|
|
16
|
-
getPluginCacheRoot,
|
|
17
|
-
getPluginDataRoot,
|
|
18
|
-
getSkillsDir,
|
|
19
|
-
homeDir,
|
|
20
|
-
readJsonFile,
|
|
21
|
-
sha256,
|
|
22
|
-
writeJsonAtomic
|
|
23
|
-
} from "./chunk-cqenqnxk.js";
|
|
24
|
-
import {
|
|
25
|
-
isServerEnabled
|
|
26
|
-
} from "./chunk-bjw71y54.js";
|
|
27
|
-
import {
|
|
28
|
-
__require
|
|
29
|
-
} from "./chunk-wj4tjacg.js";
|
|
30
|
-
|
|
31
|
-
// src/core/plugin-manager.ts
|
|
32
|
-
import fs15 from "node:fs";
|
|
33
|
-
import path21 from "node:path";
|
|
34
|
-
|
|
35
|
-
// src/core/plugin-cache.ts
|
|
36
|
-
import fs from "node:fs";
|
|
37
|
-
import path from "node:path";
|
|
38
|
-
import { execFile } from "node:child_process";
|
|
39
|
-
import { promisify } from "node:util";
|
|
40
|
-
var TMP_PREFIX = "mcpx-acquire-";
|
|
41
|
-
var execFileAsync = promisify(execFile);
|
|
42
|
-
|
|
43
|
-
class PluginCache {
|
|
44
|
-
cacheRoot;
|
|
45
|
-
constructor() {
|
|
46
|
-
this.cacheRoot = getPluginCacheRoot();
|
|
47
|
-
ensureDir(this.cacheRoot);
|
|
48
|
-
}
|
|
49
|
-
sourceDir(source) {
|
|
50
|
-
const key = source.type === "github" ? `github_${source.original.split("@")[0].replace(/[^a-zA-Z0-9_/-]/g, "_")}` : source.type === "npm" ? `npm_${source.original.replace(/^npm:/, "").split("@")[0].replace(/[^a-zA-Z0-9_/-]/g, "_")}` : source.type === "local" ? `local_${source.original.replace(/[^a-zA-Z0-9_/-]/g, "_")}` : source.type === "git-subdir" ? `git_subdir_${`${source.original}/${source.path ?? ""}`.replace(/[^a-zA-Z0-9_./@-]/g, "_")}` : `git_${source.original.replace(/[^a-zA-Z0-9_./@-]/g, "_")}`;
|
|
51
|
-
return path.join(this.cacheRoot, key);
|
|
52
|
-
}
|
|
53
|
-
refPath(srcDir) {
|
|
54
|
-
return path.join(srcDir, "ref.json");
|
|
55
|
-
}
|
|
56
|
-
shaDir(srcDir, sha) {
|
|
57
|
-
return path.join(srcDir, sha);
|
|
58
|
-
}
|
|
59
|
-
async resolveSha(source) {
|
|
60
|
-
const srcDir = this.sourceDir(source);
|
|
61
|
-
if (source.type === "github") {
|
|
62
|
-
const repo = source.original.replace(/^github\.com\//, "").split("@")[0];
|
|
63
|
-
const ref = source.ref || "HEAD";
|
|
64
|
-
const { stdout } = await execFileAsync("git", ["ls-remote", `https://github.com/${repo}.git`, ref], { timeout: 30000 });
|
|
65
|
-
const sha = stdout.split("\t")[0]?.trim();
|
|
66
|
-
if (!sha)
|
|
67
|
-
throw new Error(`Could not resolve ref ${ref} for ${repo}`);
|
|
68
|
-
return sha;
|
|
69
|
-
}
|
|
70
|
-
if (source.type === "git" || source.type === "git-subdir") {
|
|
71
|
-
const ref = source.ref || "HEAD";
|
|
72
|
-
const { stdout } = await execFileAsync("git", ["ls-remote", source.original, ref], { timeout: 30000 });
|
|
73
|
-
const sha = stdout.split("\t")[0]?.trim();
|
|
74
|
-
if (!sha)
|
|
75
|
-
throw new Error(`Could not resolve ref ${ref} for ${source.original}`);
|
|
76
|
-
return sha;
|
|
77
|
-
}
|
|
78
|
-
if (source.type === "local") {
|
|
79
|
-
const resolved = path.resolve(source.original);
|
|
80
|
-
if (!fs.existsSync(resolved)) {
|
|
81
|
-
throw new Error(`Local path not found: ${resolved}`);
|
|
82
|
-
}
|
|
83
|
-
return computeTreeHash(resolved);
|
|
84
|
-
}
|
|
85
|
-
if (source.type === "npm") {
|
|
86
|
-
throw new Error("npm source SHA resolution not yet implemented");
|
|
87
|
-
}
|
|
88
|
-
throw new Error(`Cannot resolve SHA for source type: ${source.type}`);
|
|
89
|
-
}
|
|
90
|
-
async fetch(source, pluginName) {
|
|
91
|
-
const sha = source.resolvedSha || await this.resolveSha(source);
|
|
92
|
-
const srcDir = this.sourceDir(source);
|
|
93
|
-
const destDir = this.shaDir(srcDir, sha);
|
|
94
|
-
if (fs.existsSync(destDir)) {
|
|
95
|
-
this.updateRef(srcDir, sha);
|
|
96
|
-
return { source: source.original, name: pluginName, sha, root: destDir };
|
|
97
|
-
}
|
|
98
|
-
ensureDir(srcDir);
|
|
99
|
-
if (source.type === "github" || source.type === "git" || source.type === "git-subdir") {
|
|
100
|
-
const remote = source.type === "github" ? `https://github.com/${source.original.replace(/^github\.com\//, "").split("@")[0]}.git` : source.original;
|
|
101
|
-
const target = source.resolvedSha || source.ref || "HEAD";
|
|
102
|
-
const tmpDir = fs.mkdtempSync(path.join(this.cacheRoot, TMP_PREFIX));
|
|
103
|
-
try {
|
|
104
|
-
await execFileAsync("git", ["init"], { cwd: tmpDir, timeout: 1e4 });
|
|
105
|
-
await execFileAsync("git", ["remote", "add", "origin", remote], { cwd: tmpDir, timeout: 1e4 });
|
|
106
|
-
await execFileAsync("git", ["fetch", "--depth", "1", "origin", target], {
|
|
107
|
-
cwd: tmpDir,
|
|
108
|
-
timeout: 60000
|
|
109
|
-
});
|
|
110
|
-
await execFileAsync("git", ["checkout", "--detach", "FETCH_HEAD"], { cwd: tmpDir, timeout: 1e4 });
|
|
111
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
112
|
-
cwd: tmpDir,
|
|
113
|
-
timeout: 1e4
|
|
114
|
-
});
|
|
115
|
-
const actualSha = stdout.trim();
|
|
116
|
-
fs.rmSync(path.join(tmpDir, ".git"), { recursive: true, force: true });
|
|
117
|
-
const finalDest = this.shaDir(srcDir, actualSha);
|
|
118
|
-
if (fs.existsSync(finalDest)) {
|
|
119
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
120
|
-
this.updateRef(srcDir, actualSha);
|
|
121
|
-
return { source: source.original, name: pluginName, sha: actualSha, root: finalDest };
|
|
122
|
-
}
|
|
123
|
-
if (source.type === "git-subdir") {
|
|
124
|
-
if (!source.path)
|
|
125
|
-
throw new Error("git-subdir source is missing a path");
|
|
126
|
-
const subdir = path.resolve(tmpDir, source.path);
|
|
127
|
-
if (!subdir.startsWith(path.resolve(tmpDir) + path.sep) || !fs.existsSync(subdir)) {
|
|
128
|
-
throw new Error(`Plugin subdirectory not found: ${source.path}`);
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
fs.renameSync(subdir, finalDest);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (!fs.existsSync(finalDest))
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
136
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
137
|
-
} else {
|
|
138
|
-
try {
|
|
139
|
-
fs.renameSync(tmpDir, finalDest);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
if (!fs.existsSync(finalDest))
|
|
142
|
-
throw error;
|
|
143
|
-
return { source: source.original, name: pluginName, sha: actualSha, root: finalDest };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
this.updateRef(srcDir, actualSha);
|
|
147
|
-
return { source: source.original, name: pluginName, sha: actualSha, root: finalDest };
|
|
148
|
-
} catch (e) {
|
|
149
|
-
if (fs.existsSync(tmpDir))
|
|
150
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
151
|
-
throw e;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (source.type === "local") {
|
|
155
|
-
const resolved = path.resolve(source.original);
|
|
156
|
-
if (!fs.existsSync(resolved)) {
|
|
157
|
-
throw new Error(`Local path not found: ${resolved}`);
|
|
158
|
-
}
|
|
159
|
-
copyDirSync(resolved, destDir);
|
|
160
|
-
this.updateRef(srcDir, sha);
|
|
161
|
-
return { source: source.original, name: pluginName, sha, root: destDir };
|
|
162
|
-
}
|
|
163
|
-
if (source.type === "npm") {
|
|
164
|
-
throw new Error("npm source fetch not yet implemented");
|
|
165
|
-
}
|
|
166
|
-
throw new Error(`Cannot fetch source type: ${source.type}`);
|
|
167
|
-
}
|
|
168
|
-
updateRef(srcDir, sha) {
|
|
169
|
-
const refs = {};
|
|
170
|
-
if (fs.existsSync(this.refPath(srcDir))) {
|
|
171
|
-
try {
|
|
172
|
-
const existing = JSON.parse(fs.readFileSync(this.refPath(srcDir), "utf8"));
|
|
173
|
-
Object.assign(refs, existing);
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
176
|
-
refs.latest = sha;
|
|
177
|
-
fs.writeFileSync(this.refPath(srcDir), JSON.stringify(refs, null, 2));
|
|
178
|
-
}
|
|
179
|
-
listCached() {
|
|
180
|
-
const results = [];
|
|
181
|
-
try {
|
|
182
|
-
const sourceDirs = fs.readdirSync(this.cacheRoot);
|
|
183
|
-
for (const dir of sourceDirs) {
|
|
184
|
-
const srcDir = path.join(this.cacheRoot, dir);
|
|
185
|
-
if (!fs.statSync(srcDir).isDirectory())
|
|
186
|
-
continue;
|
|
187
|
-
const entries = fs.readdirSync(srcDir);
|
|
188
|
-
for (const entry of entries) {
|
|
189
|
-
if (entry === "ref.json")
|
|
190
|
-
continue;
|
|
191
|
-
const shaDir = path.join(srcDir, entry);
|
|
192
|
-
if (!fs.statSync(shaDir).isDirectory())
|
|
193
|
-
continue;
|
|
194
|
-
const refs = {};
|
|
195
|
-
if (fs.existsSync(this.refPath(srcDir))) {
|
|
196
|
-
try {
|
|
197
|
-
Object.assign(refs, JSON.parse(fs.readFileSync(this.refPath(srcDir), "utf8")));
|
|
198
|
-
} catch {}
|
|
199
|
-
}
|
|
200
|
-
results.push({
|
|
201
|
-
source: dir,
|
|
202
|
-
name: entry,
|
|
203
|
-
sha: entry,
|
|
204
|
-
root: shaDir
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} catch {}
|
|
209
|
-
return results;
|
|
210
|
-
}
|
|
211
|
-
remove(source, sha) {
|
|
212
|
-
const srcDir = this.sourceDir(source);
|
|
213
|
-
const target = this.shaDir(srcDir, sha);
|
|
214
|
-
if (fs.existsSync(target)) {
|
|
215
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
removeAll(source) {
|
|
219
|
-
const srcDir = this.sourceDir(source);
|
|
220
|
-
if (fs.existsSync(srcDir)) {
|
|
221
|
-
fs.rmSync(srcDir, { recursive: true, force: true });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function computeTreeHash(dir) {
|
|
226
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
227
|
-
const crypto = __require("node:crypto");
|
|
228
|
-
const hash = crypto.createHash("sha256");
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
if (entry.name.startsWith(".git"))
|
|
231
|
-
continue;
|
|
232
|
-
if (entry.name === ".DS_Store")
|
|
233
|
-
continue;
|
|
234
|
-
if (entry.isDirectory()) {
|
|
235
|
-
hash.update(entry.name);
|
|
236
|
-
hash.update(computeTreeHash(path.join(dir, entry.name)));
|
|
237
|
-
} else if (entry.isFile()) {
|
|
238
|
-
hash.update(entry.name);
|
|
239
|
-
hash.update(fs.readFileSync(path.join(dir, entry.name)));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return hash.digest("hex");
|
|
243
|
-
}
|
|
244
|
-
function copyDirSync(src, dest) {
|
|
245
|
-
ensureDir(dest);
|
|
246
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
247
|
-
for (const entry of entries) {
|
|
248
|
-
if (entry.name.startsWith(".git"))
|
|
249
|
-
continue;
|
|
250
|
-
if (entry.name === ".DS_Store")
|
|
251
|
-
continue;
|
|
252
|
-
const srcPath = path.join(src, entry.name);
|
|
253
|
-
const destPath = path.join(dest, entry.name);
|
|
254
|
-
if (entry.isDirectory()) {
|
|
255
|
-
copyDirSync(srcPath, destPath);
|
|
256
|
-
} else if (entry.isFile()) {
|
|
257
|
-
fs.copyFileSync(srcPath, destPath);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/core/plugin-data.ts
|
|
263
|
-
import fs2 from "node:fs";
|
|
264
|
-
import path2 from "node:path";
|
|
265
|
-
class PluginDataManager {
|
|
266
|
-
dataRoot;
|
|
267
|
-
constructor() {
|
|
268
|
-
this.dataRoot = getPluginDataRoot();
|
|
269
|
-
ensureDir(this.dataRoot);
|
|
270
|
-
}
|
|
271
|
-
getPluginDataDir(pluginId) {
|
|
272
|
-
return path2.join(this.dataRoot, pluginId);
|
|
273
|
-
}
|
|
274
|
-
getPluginDataPath(pluginId) {
|
|
275
|
-
return path2.join(this.getPluginDataDir(pluginId), "data.json");
|
|
276
|
-
}
|
|
277
|
-
async loadData(pluginId) {
|
|
278
|
-
const dataPath = this.getPluginDataPath(pluginId);
|
|
279
|
-
if (!fs2.existsSync(dataPath)) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
try {
|
|
283
|
-
const data = readJsonFile(dataPath, null);
|
|
284
|
-
return data;
|
|
285
|
-
} catch {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
async saveData(pluginId, data) {
|
|
290
|
-
const existing = await this.loadData(pluginId) || {};
|
|
291
|
-
const merged = { ...existing, ...data };
|
|
292
|
-
const dataPath = this.getPluginDataPath(pluginId);
|
|
293
|
-
ensureDir(path2.dirname(dataPath));
|
|
294
|
-
writeJsonAtomic(dataPath, merged);
|
|
295
|
-
}
|
|
296
|
-
async updateDependency(pluginId, packageName, version) {
|
|
297
|
-
const data = await this.loadData(pluginId);
|
|
298
|
-
if (!data) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (!data.dependencies) {
|
|
302
|
-
data.dependencies = {};
|
|
303
|
-
}
|
|
304
|
-
data.dependencies[packageName] = version;
|
|
305
|
-
await this.saveData(pluginId, data);
|
|
306
|
-
}
|
|
307
|
-
async getDependencies(pluginId) {
|
|
308
|
-
const data = await this.loadData(pluginId);
|
|
309
|
-
return data?.dependencies || {};
|
|
310
|
-
}
|
|
311
|
-
async addLog(pluginId, level, message) {
|
|
312
|
-
const data = await this.loadData(pluginId);
|
|
313
|
-
if (!data) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (!data.logs) {
|
|
317
|
-
data.logs = [];
|
|
318
|
-
}
|
|
319
|
-
data.logs.push({
|
|
320
|
-
timestamp: new Date().toISOString(),
|
|
321
|
-
level,
|
|
322
|
-
message
|
|
323
|
-
});
|
|
324
|
-
if (data.logs.length > 1000) {
|
|
325
|
-
data.logs = data.logs.slice(-1000);
|
|
326
|
-
}
|
|
327
|
-
await this.saveData(pluginId, data);
|
|
328
|
-
}
|
|
329
|
-
async updateStatus(pluginId, status, error) {
|
|
330
|
-
const data = await this.loadData(pluginId);
|
|
331
|
-
if (!data) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
data.status = status;
|
|
335
|
-
if (error) {
|
|
336
|
-
data.error = error;
|
|
337
|
-
}
|
|
338
|
-
data.lastHealthCheck = new Date().toISOString();
|
|
339
|
-
await this.saveData(pluginId, data);
|
|
340
|
-
}
|
|
341
|
-
async removeData(pluginId) {
|
|
342
|
-
const dataDir = this.getPluginDataDir(pluginId);
|
|
343
|
-
if (fs2.existsSync(dataDir)) {
|
|
344
|
-
fs2.rmSync(dataDir, { recursive: true, force: true });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
async cleanupOrphanedData() {
|
|
348
|
-
if (!fs2.existsSync(this.dataRoot)) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
const pluginIds = fs2.readdirSync(this.dataRoot);
|
|
352
|
-
for (const pluginId of pluginIds) {
|
|
353
|
-
const pluginDir = path2.join(this.dataRoot, pluginId);
|
|
354
|
-
if (!fs2.statSync(pluginDir).isDirectory()) {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const dataPath = path2.join(pluginDir, "data.json");
|
|
358
|
-
if (!fs2.existsSync(dataPath)) {
|
|
359
|
-
fs2.rmSync(pluginDir, { recursive: true, force: true });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// src/core/plugin-lifecycle.ts
|
|
366
|
-
import fs3 from "node:fs";
|
|
367
|
-
import path3 from "node:path";
|
|
368
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
369
|
-
import { promisify as promisify2 } from "node:util";
|
|
370
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
371
|
-
|
|
372
|
-
class PluginLifecycle {
|
|
373
|
-
dataManager;
|
|
374
|
-
constructor() {
|
|
375
|
-
this.dataManager = new PluginDataManager;
|
|
376
|
-
}
|
|
377
|
-
async prepareDependencies(pluginId, pluginRoot) {
|
|
378
|
-
await this.dataManager.updateStatus(pluginId, "preparing");
|
|
379
|
-
const dataDir = path3.join(getPluginDataRoot(), pluginId);
|
|
380
|
-
ensureDir(dataDir);
|
|
381
|
-
const pkgJsonPath = path3.join(pluginRoot, "package.json");
|
|
382
|
-
if (fs3.existsSync(pkgJsonPath)) {
|
|
383
|
-
try {
|
|
384
|
-
const pkg = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
|
|
385
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
386
|
-
if (Object.keys(deps).length > 0) {
|
|
387
|
-
await execFileAsync2("npm", ["install", "--production", "--ignore-scripts"], {
|
|
388
|
-
cwd: pluginRoot,
|
|
389
|
-
timeout: 120000
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
} catch {}
|
|
393
|
-
}
|
|
394
|
-
const reqPath = path3.join(pluginRoot, "requirements.txt");
|
|
395
|
-
if (fs3.existsSync(reqPath)) {
|
|
396
|
-
try {
|
|
397
|
-
await execFileAsync2("pip", ["install", "-r", reqPath], {
|
|
398
|
-
timeout: 120000
|
|
399
|
-
});
|
|
400
|
-
} catch {}
|
|
401
|
-
}
|
|
402
|
-
await this.dataManager.updateStatus(pluginId, "healthy");
|
|
403
|
-
}
|
|
404
|
-
async removeData(pluginId) {
|
|
405
|
-
await this.dataManager.removeData(pluginId);
|
|
406
|
-
}
|
|
407
|
-
initData(pluginId) {
|
|
408
|
-
const dataDir = path3.join(getPluginDataRoot(), pluginId);
|
|
409
|
-
ensureDir(dataDir);
|
|
410
|
-
this.dataManager.saveData(pluginId, {
|
|
411
|
-
id: pluginId,
|
|
412
|
-
dependencies: {},
|
|
413
|
-
settings: {},
|
|
414
|
-
logs: [],
|
|
415
|
-
status: "healthy",
|
|
416
|
-
installedAt: new Date().toISOString()
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// src/core/plugin-source.ts
|
|
422
|
-
var GITHUB_RE = /^(?:github\.com\/)?([a-zA-Z0-9][\w.-]*\/[a-zA-Z0-9][\w.-]*?)(?:@([\w./-]+))?$/;
|
|
423
|
-
var GIT_URL_RE = /^https?:\/\/.*\.git(?:\?.*)?(?:\/\/.*)?$/;
|
|
424
|
-
var SCP_GIT_RE = /^[\w.-]+@[\w.-]+:[\w./-]+$/;
|
|
425
|
-
var NPM_RE = /^npm:(@?[\w.-]+(?:\/[\w.-]+)?)(?:@([\w.-]+))?$/;
|
|
426
|
-
var MARKETPLACE_RE = /^marketplace:([\w.-]+)$/;
|
|
427
|
-
function parseSource(source) {
|
|
428
|
-
const github = source.match(GITHUB_RE);
|
|
429
|
-
if (github) {
|
|
430
|
-
return {
|
|
431
|
-
type: "github",
|
|
432
|
-
original: source,
|
|
433
|
-
ref: github[2] || undefined
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
if (NPM_RE.test(source)) {
|
|
437
|
-
const npm = source.match(NPM_RE);
|
|
438
|
-
return {
|
|
439
|
-
type: "npm",
|
|
440
|
-
original: source,
|
|
441
|
-
ref: npm[2] || undefined
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
if (MARKETPLACE_RE.test(source)) {
|
|
445
|
-
return {
|
|
446
|
-
type: "marketplace",
|
|
447
|
-
original: source
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source === "." || source === "..") {
|
|
451
|
-
return {
|
|
452
|
-
type: "local",
|
|
453
|
-
original: source
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
if (GIT_URL_RE.test(source) || SCP_GIT_RE.test(source)) {
|
|
457
|
-
return {
|
|
458
|
-
type: "git",
|
|
459
|
-
original: source
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
throw new Error(`Unrecognized plugin source: ${source}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// src/core/plugin-parse.ts
|
|
466
|
-
import fs4 from "node:fs";
|
|
467
|
-
import path4 from "node:path";
|
|
468
|
-
import { z } from "zod";
|
|
469
|
-
var CLAUDE_PLUGIN_JSON_REL = path4.join(".claude-plugin", "plugin.json");
|
|
470
|
-
var CODEX_PLUGIN_JSON_REL = path4.join(".codex-plugin", "plugin.json");
|
|
471
|
-
var AGENT_PLUGIN_JSON_REL = "plugin.json";
|
|
472
|
-
var MCP_JSON_RELS = ["mcp.json", ".mcp.json"];
|
|
473
|
-
var HOOKS_JSON_REL = path4.join("hooks", "hooks.json");
|
|
474
|
-
var SKILLS_DIR = "skills";
|
|
475
|
-
var COMMANDS_DIR = "commands";
|
|
476
|
-
var AGENTS_DIR = "agents";
|
|
477
|
-
var manifestSchema = z.object({
|
|
478
|
-
name: z.string().optional(),
|
|
479
|
-
version: z.string().optional(),
|
|
480
|
-
description: z.string().optional(),
|
|
481
|
-
source: z.string().optional(),
|
|
482
|
-
icon: z.string().optional(),
|
|
483
|
-
assets: z.array(z.string()).optional(),
|
|
484
|
-
entry: z.string().optional(),
|
|
485
|
-
author: z.object({ name: z.string(), email: z.string().optional(), url: z.string().optional() }).optional(),
|
|
486
|
-
homepage: z.string().optional(),
|
|
487
|
-
repository: z.string().optional(),
|
|
488
|
-
license: z.string().optional(),
|
|
489
|
-
keywords: z.array(z.string()).optional(),
|
|
490
|
-
category: z.string().optional(),
|
|
491
|
-
skills: z.union([z.string(), z.array(z.string())]).optional(),
|
|
492
|
-
commands: z.union([z.string(), z.array(z.string())]).optional(),
|
|
493
|
-
agents: z.union([z.string(), z.array(z.string())]).optional(),
|
|
494
|
-
hooks: z.unknown().optional(),
|
|
495
|
-
mcpServers: z.unknown().optional(),
|
|
496
|
-
lspServers: z.unknown().optional(),
|
|
497
|
-
apps: z.unknown().optional(),
|
|
498
|
-
settings: z.unknown().optional(),
|
|
499
|
-
outputStyles: z.unknown().optional(),
|
|
500
|
-
dependencies: z.unknown().optional(),
|
|
501
|
-
interface: z.object({
|
|
502
|
-
displayName: z.string().optional(),
|
|
503
|
-
shortDescription: z.string().optional(),
|
|
504
|
-
category: z.string().optional()
|
|
505
|
-
}).passthrough().optional()
|
|
506
|
-
}).passthrough();
|
|
507
|
-
function manifestPath(pluginRoot) {
|
|
508
|
-
for (const rel of [AGENT_PLUGIN_JSON_REL, CLAUDE_PLUGIN_JSON_REL, CODEX_PLUGIN_JSON_REL]) {
|
|
509
|
-
const candidate = path4.join(pluginRoot, rel);
|
|
510
|
-
if (fs4.existsSync(candidate))
|
|
511
|
-
return candidate;
|
|
512
|
-
}
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
function readRawManifest(pluginRoot) {
|
|
516
|
-
const filePath = manifestPath(pluginRoot);
|
|
517
|
-
if (!filePath)
|
|
518
|
-
return null;
|
|
519
|
-
try {
|
|
520
|
-
return manifestSchema.parse(JSON.parse(fs4.readFileSync(filePath, "utf8")));
|
|
521
|
-
} catch {
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
function sanitizeComponentId(id) {
|
|
526
|
-
const stripped = id.replace(/[\/\\]/g, "_").replace(/^\.+/, "");
|
|
527
|
-
return stripped.length > 0 ? stripped : "unnamed";
|
|
528
|
-
}
|
|
529
|
-
function readManifest(pluginRoot) {
|
|
530
|
-
const raw = readRawManifest(pluginRoot);
|
|
531
|
-
if (!raw)
|
|
532
|
-
return null;
|
|
533
|
-
const declaredCapabilities = ["skills", "commands", "agents", "hooks", "mcpServers", "lspServers", "apps", "settings", "outputStyles", "dependencies"].filter((key) => raw[key] !== undefined);
|
|
534
|
-
return {
|
|
535
|
-
name: raw.name ?? path4.basename(pluginRoot),
|
|
536
|
-
version: raw.version ?? "0.0.0",
|
|
537
|
-
description: raw.description ?? raw.interface?.shortDescription,
|
|
538
|
-
source: raw.source,
|
|
539
|
-
icon: raw.icon,
|
|
540
|
-
assets: raw.assets,
|
|
541
|
-
entry: raw.entry,
|
|
542
|
-
author: raw.author,
|
|
543
|
-
homepage: raw.homepage,
|
|
544
|
-
repository: raw.repository,
|
|
545
|
-
license: raw.license,
|
|
546
|
-
keywords: raw.keywords,
|
|
547
|
-
displayName: raw.interface?.displayName,
|
|
548
|
-
category: raw.category ?? raw.interface?.category,
|
|
549
|
-
declaredCapabilities
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
function declaredPaths(raw, key, fallback) {
|
|
553
|
-
const value = raw?.[key];
|
|
554
|
-
if (typeof value === "string")
|
|
555
|
-
return [value];
|
|
556
|
-
if (Array.isArray(value))
|
|
557
|
-
return value;
|
|
558
|
-
return [fallback];
|
|
559
|
-
}
|
|
560
|
-
function resolveWithinRoot(pluginRoot, declared) {
|
|
561
|
-
const resolved = path4.resolve(pluginRoot, declared);
|
|
562
|
-
const root = path4.resolve(pluginRoot);
|
|
563
|
-
return resolved === root || resolved.startsWith(root + path4.sep) ? resolved : null;
|
|
564
|
-
}
|
|
565
|
-
function parseFrontmatter(content) {
|
|
566
|
-
const trimmed = content.trimStart();
|
|
567
|
-
if (!trimmed.startsWith("---")) {
|
|
568
|
-
return { frontmatter: {}, body: content };
|
|
569
|
-
}
|
|
570
|
-
const end = trimmed.indexOf("---", 3);
|
|
571
|
-
if (end === -1) {
|
|
572
|
-
return { frontmatter: {}, body: content };
|
|
573
|
-
}
|
|
574
|
-
const raw = trimmed.slice(3, end).trim();
|
|
575
|
-
const body = trimmed.slice(end + 3).trimStart();
|
|
576
|
-
const frontmatter = {};
|
|
577
|
-
for (const line of raw.split(`
|
|
578
|
-
`)) {
|
|
579
|
-
const sep = line.indexOf(":");
|
|
580
|
-
if (sep !== -1) {
|
|
581
|
-
const key = line.slice(0, sep).trim();
|
|
582
|
-
const val = line.slice(sep + 1).trim();
|
|
583
|
-
if (key === "name")
|
|
584
|
-
frontmatter.name = val;
|
|
585
|
-
if (key === "description")
|
|
586
|
-
frontmatter.description = val;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return { frontmatter, body };
|
|
590
|
-
}
|
|
591
|
-
function discoverSkills(pluginRoot, raw) {
|
|
592
|
-
const results = [];
|
|
593
|
-
for (const declared of declaredPaths(raw, "skills", SKILLS_DIR))
|
|
594
|
-
try {
|
|
595
|
-
const skillsDir = resolveWithinRoot(pluginRoot, declared);
|
|
596
|
-
if (!skillsDir || !fs4.existsSync(skillsDir))
|
|
597
|
-
continue;
|
|
598
|
-
const entries = fs4.readdirSync(skillsDir, { withFileTypes: true });
|
|
599
|
-
for (const entry of entries) {
|
|
600
|
-
if (!entry.isDirectory()) {
|
|
601
|
-
if (entry.name.endsWith(".md")) {
|
|
602
|
-
const skillPath = path4.join(skillsDir, entry.name);
|
|
603
|
-
const content = fs4.readFileSync(skillPath, "utf8");
|
|
604
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
605
|
-
results.push({
|
|
606
|
-
id: sanitizeComponentId(frontmatter.name ?? path4.basename(entry.name, ".md")),
|
|
607
|
-
type: "skills",
|
|
608
|
-
path: skillPath,
|
|
609
|
-
description: frontmatter.description
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
const skillMdPath = path4.join(skillsDir, entry.name, "SKILL.md");
|
|
615
|
-
if (fs4.existsSync(skillMdPath)) {
|
|
616
|
-
const content = fs4.readFileSync(skillMdPath, "utf8");
|
|
617
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
618
|
-
results.push({
|
|
619
|
-
id: sanitizeComponentId(frontmatter.name ?? entry.name),
|
|
620
|
-
type: "skills",
|
|
621
|
-
path: skillMdPath,
|
|
622
|
-
description: frontmatter.description
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
} catch {}
|
|
627
|
-
return results;
|
|
628
|
-
}
|
|
629
|
-
function discoverCommands(pluginRoot, raw) {
|
|
630
|
-
const results = [];
|
|
631
|
-
for (const declared of declaredPaths(raw, "commands", COMMANDS_DIR))
|
|
632
|
-
try {
|
|
633
|
-
const commandsDir = resolveWithinRoot(pluginRoot, declared);
|
|
634
|
-
if (!commandsDir || !fs4.existsSync(commandsDir))
|
|
635
|
-
continue;
|
|
636
|
-
const entries = fs4.readdirSync(commandsDir, { withFileTypes: true });
|
|
637
|
-
for (const entry of entries) {
|
|
638
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
639
|
-
const cmdPath = path4.join(commandsDir, entry.name);
|
|
640
|
-
const content = fs4.readFileSync(cmdPath, "utf8");
|
|
641
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
642
|
-
results.push({
|
|
643
|
-
id: sanitizeComponentId(frontmatter.name ?? path4.basename(entry.name, ".md")),
|
|
644
|
-
type: "commands",
|
|
645
|
-
path: cmdPath,
|
|
646
|
-
description: frontmatter.description
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
} catch {}
|
|
651
|
-
return results;
|
|
652
|
-
}
|
|
653
|
-
function discoverAgents(pluginRoot, raw) {
|
|
654
|
-
const results = [];
|
|
655
|
-
for (const declared of declaredPaths(raw, "agents", AGENTS_DIR))
|
|
656
|
-
try {
|
|
657
|
-
const agentsDir = resolveWithinRoot(pluginRoot, declared);
|
|
658
|
-
if (!agentsDir || !fs4.existsSync(agentsDir))
|
|
659
|
-
continue;
|
|
660
|
-
const entries = fs4.readdirSync(agentsDir, { withFileTypes: true });
|
|
661
|
-
for (const entry of entries) {
|
|
662
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
663
|
-
const agentPath = path4.join(agentsDir, entry.name);
|
|
664
|
-
const content = fs4.readFileSync(agentPath, "utf8");
|
|
665
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
666
|
-
results.push({
|
|
667
|
-
id: sanitizeComponentId(frontmatter.name ?? path4.basename(entry.name, ".md")),
|
|
668
|
-
type: "agents",
|
|
669
|
-
path: agentPath,
|
|
670
|
-
description: frontmatter.description
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} catch {}
|
|
675
|
-
return results;
|
|
676
|
-
}
|
|
677
|
-
function discoverHooks(pluginRoot, raw) {
|
|
678
|
-
const declared = typeof raw?.hooks === "string" ? raw.hooks : undefined;
|
|
679
|
-
const hooksPath = declared ? resolveWithinRoot(pluginRoot, declared) : [path4.join(pluginRoot, HOOKS_JSON_REL), path4.join(pluginRoot, "hooks.json")].find(fs4.existsSync) ?? null;
|
|
680
|
-
if (!hooksPath || !fs4.existsSync(hooksPath))
|
|
681
|
-
return [];
|
|
682
|
-
try {
|
|
683
|
-
const raw2 = JSON.parse(fs4.readFileSync(hooksPath, "utf8"));
|
|
684
|
-
const hooks = Array.isArray(raw2) ? raw2 : raw2.hooks ?? [];
|
|
685
|
-
return hooks.map((h, i) => ({
|
|
686
|
-
id: h.id ?? `hook-${i}`,
|
|
687
|
-
type: "hooks",
|
|
688
|
-
path: hooksPath,
|
|
689
|
-
description: h.description
|
|
690
|
-
}));
|
|
691
|
-
} catch {
|
|
692
|
-
return [];
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
function discoverMcpServers(pluginRoot, raw) {
|
|
696
|
-
const declared = typeof raw?.mcpServers === "string" ? raw.mcpServers : undefined;
|
|
697
|
-
const mcpPath = declared ? resolveWithinRoot(pluginRoot, declared) : MCP_JSON_RELS.map((rel) => path4.join(pluginRoot, rel)).find(fs4.existsSync) ?? null;
|
|
698
|
-
if (!mcpPath || !fs4.existsSync(mcpPath))
|
|
699
|
-
return [];
|
|
700
|
-
try {
|
|
701
|
-
const raw2 = JSON.parse(fs4.readFileSync(mcpPath, "utf8"));
|
|
702
|
-
const servers = raw2.mcpServers ?? {};
|
|
703
|
-
return Object.entries(servers).map(([id, s]) => {
|
|
704
|
-
const isHttp = s.type === "http" || typeof s.url === "string";
|
|
705
|
-
return {
|
|
706
|
-
id,
|
|
707
|
-
transport: isHttp ? "http" : "stdio",
|
|
708
|
-
command: s.command ?? "",
|
|
709
|
-
args: s.args,
|
|
710
|
-
env: s.env,
|
|
711
|
-
cwd: s.cwd,
|
|
712
|
-
url: isHttp ? s.url : undefined,
|
|
713
|
-
oauthResource: s.oauth_resource
|
|
714
|
-
};
|
|
715
|
-
});
|
|
716
|
-
} catch {
|
|
717
|
-
return [];
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
function discoverComponents(pluginRoot) {
|
|
721
|
-
const raw = readRawManifest(pluginRoot);
|
|
722
|
-
return {
|
|
723
|
-
skills: discoverSkills(pluginRoot, raw),
|
|
724
|
-
commands: discoverCommands(pluginRoot, raw),
|
|
725
|
-
agents: discoverAgents(pluginRoot, raw),
|
|
726
|
-
hooks: discoverHooks(pluginRoot, raw),
|
|
727
|
-
mcpServers: discoverMcpServers(pluginRoot, raw)
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
function hasManifest(pluginRoot) {
|
|
731
|
-
return manifestPath(pluginRoot) !== null;
|
|
732
|
-
}
|
|
733
|
-
function resolvePluginVars(value, pluginRoot, dataDir) {
|
|
734
|
-
return value.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginRoot).replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, dataDir).replace(/\$\{MCPX_PLUGIN_ROOT\}/g, pluginRoot).replace(/\$\{MCPX_PLUGIN_DATA\}/g, dataDir).replace(/\$\{DATA\}/g, dataDir).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, pluginRoot);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// src/core/config-store.ts
|
|
738
|
-
import fs5 from "node:fs";
|
|
739
|
-
import path5 from "node:path";
|
|
740
|
-
var LOCK_STALE_MS = 5000;
|
|
741
|
-
var LOCK_RETRY_MS = 50;
|
|
742
|
-
var LOCK_MAX_RETRIES = 40;
|
|
743
|
-
var inProcessQueues = new Map;
|
|
744
|
-
function getLockPath(filePath) {
|
|
745
|
-
return `${filePath}.lock`;
|
|
746
|
-
}
|
|
747
|
-
function acquireLock(filePath) {
|
|
748
|
-
const lockPath = getLockPath(filePath);
|
|
749
|
-
try {
|
|
750
|
-
fs5.mkdirSync(path5.dirname(lockPath), { recursive: true });
|
|
751
|
-
const fd = fs5.openSync(lockPath, "wx");
|
|
752
|
-
fs5.writeSync(fd, String(process.pid));
|
|
753
|
-
fs5.closeSync(fd);
|
|
754
|
-
return true;
|
|
755
|
-
} catch (err) {
|
|
756
|
-
if (err.code !== "EEXIST") {
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
try {
|
|
760
|
-
const stat = fs5.statSync(lockPath);
|
|
761
|
-
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
762
|
-
fs5.unlinkSync(lockPath);
|
|
763
|
-
return acquireLock(filePath);
|
|
764
|
-
}
|
|
765
|
-
} catch {
|
|
766
|
-
return acquireLock(filePath);
|
|
767
|
-
}
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
function releaseLock(filePath) {
|
|
772
|
-
try {
|
|
773
|
-
fs5.unlinkSync(getLockPath(filePath));
|
|
774
|
-
} catch {}
|
|
775
|
-
}
|
|
776
|
-
function mutateFile(filePath, load, save, fn) {
|
|
777
|
-
const prevQueue = inProcessQueues.get(filePath) ?? Promise.resolve();
|
|
778
|
-
const nextQueue = prevQueue.then(() => executeWithLock(filePath, load, save, fn), () => executeWithLock(filePath, load, save, fn));
|
|
779
|
-
inProcessQueues.set(filePath, nextQueue.catch(() => {
|
|
780
|
-
return;
|
|
781
|
-
}));
|
|
782
|
-
return nextQueue;
|
|
783
|
-
}
|
|
784
|
-
async function executeWithLock(filePath, load, save, fn) {
|
|
785
|
-
let acquired = false;
|
|
786
|
-
for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
787
|
-
acquired = acquireLock(filePath);
|
|
788
|
-
if (acquired)
|
|
789
|
-
break;
|
|
790
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
791
|
-
}
|
|
792
|
-
if (!acquired) {
|
|
793
|
-
throw new Error(`Could not acquire lock for ${filePath}.`);
|
|
794
|
-
}
|
|
795
|
-
try {
|
|
796
|
-
const data = load(filePath);
|
|
797
|
-
const result = await fn(data);
|
|
798
|
-
save(data, filePath);
|
|
799
|
-
return result;
|
|
800
|
-
} finally {
|
|
801
|
-
releaseLock(filePath);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function mutateConfig(fn, configPath) {
|
|
805
|
-
return mutateFile(configPath ?? getConfigPath(), loadConfig, saveConfig, fn);
|
|
806
|
-
}
|
|
807
|
-
function mutateProjectConfig(projectConfigPath, fn) {
|
|
808
|
-
return mutateFile(projectConfigPath, (p) => loadProjectConfig(p), saveProjectConfig, fn);
|
|
809
|
-
}
|
|
810
|
-
async function mutateActiveConfig(options, mutator) {
|
|
811
|
-
const globalPath = getConfigPath();
|
|
812
|
-
if (!options.global) {
|
|
813
|
-
const projectConfigPath = findProjectConfigPath();
|
|
814
|
-
if (options.local) {
|
|
815
|
-
const projectPath = projectConfigPath ? path5.dirname(projectConfigPath) : process.cwd();
|
|
816
|
-
const localPath = path5.join(projectPath, ".mcpx.json");
|
|
817
|
-
const globalConfig = loadConfig(globalPath);
|
|
818
|
-
const result2 = await mutateProjectConfig(localPath, async (projectConfig) => {
|
|
819
|
-
const mergedConfig = {
|
|
820
|
-
...globalConfig,
|
|
821
|
-
servers: { ...globalConfig.servers, ...projectConfig.servers }
|
|
822
|
-
};
|
|
823
|
-
const mutatorResult = await mutator(mergedConfig, { type: "project", projectPath });
|
|
824
|
-
const localServers = {};
|
|
825
|
-
for (const [name, spec] of Object.entries(mergedConfig.servers)) {
|
|
826
|
-
if (!(name in globalConfig.servers)) {
|
|
827
|
-
localServers[name] = spec;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
projectConfig.servers = localServers;
|
|
831
|
-
return mutatorResult;
|
|
832
|
-
});
|
|
833
|
-
return { type: "project", configPath: localPath, projectPath, result: result2 };
|
|
834
|
-
}
|
|
835
|
-
if (projectConfigPath) {
|
|
836
|
-
const projectPath = path5.dirname(projectConfigPath);
|
|
837
|
-
const result2 = await mutateConfig((config) => mutator(config, { type: "project", projectPath }), globalPath);
|
|
838
|
-
return { type: "project", configPath: globalPath, projectPath, result: result2 };
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
const result = await mutateConfig((config) => mutator(config, { type: "global" }), globalPath);
|
|
842
|
-
return { type: "global", configPath: globalPath, result };
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// src/adapters/utils/index.ts
|
|
846
|
-
function getManagedEntryNames(managedIndex, clientId) {
|
|
847
|
-
return Object.keys(managedIndex.managed[clientId]?.entries ?? {});
|
|
848
|
-
}
|
|
849
|
-
function isManagedEntry(managedIndex, clientId, entryName) {
|
|
850
|
-
return Boolean(managedIndex.managed[clientId]?.entries?.[entryName]);
|
|
851
|
-
}
|
|
852
|
-
function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryNames) {
|
|
853
|
-
const keep = new Set(keepEntryNames);
|
|
854
|
-
const managedNames = getManagedEntryNames(managedIndex, clientId);
|
|
855
|
-
for (const name of managedNames) {
|
|
856
|
-
if (!keep.has(name)) {
|
|
857
|
-
delete servers[name];
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
for (const name of Object.keys(servers)) {
|
|
861
|
-
if (!keep.has(name) && name.endsWith(" (mcpx)")) {
|
|
862
|
-
delete servers[name];
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue, expectedEntry) {
|
|
867
|
-
if (existingValue === undefined || existingValue === null) {
|
|
868
|
-
return null;
|
|
869
|
-
}
|
|
870
|
-
if (isManagedEntry(managedIndex, clientId, entryName)) {
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
if (expectedEntry && isManagedGatewayProjection(entryName) && matchesExpectedShape(existingValue, expectedEntry)) {
|
|
874
|
-
if (!managedIndex.managed[clientId]) {
|
|
875
|
-
managedIndex.managed[clientId] = { configPath: "", entries: {} };
|
|
876
|
-
}
|
|
877
|
-
managedIndex.managed[clientId].entries[entryName] = {
|
|
878
|
-
fingerprint: sha256(JSON.stringify(existingValue)),
|
|
879
|
-
lastSyncedAt: new Date().toISOString()
|
|
880
|
-
};
|
|
881
|
-
return null;
|
|
882
|
-
}
|
|
883
|
-
return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
|
|
884
|
-
}
|
|
885
|
-
function matchesExpectedShape(value, expected) {
|
|
886
|
-
if (!value || typeof value !== "object")
|
|
887
|
-
return false;
|
|
888
|
-
const v = value;
|
|
889
|
-
if (expected.url && v.url !== expected.url)
|
|
890
|
-
return false;
|
|
891
|
-
if (expected.command && v.command !== expected.command)
|
|
892
|
-
return false;
|
|
893
|
-
return true;
|
|
894
|
-
}
|
|
895
|
-
function setManagedEntries(managedIndex, clientId, configPath, entries) {
|
|
896
|
-
if (!managedIndex.managed[clientId]) {
|
|
897
|
-
managedIndex.managed[clientId] = {
|
|
898
|
-
configPath,
|
|
899
|
-
entries: {}
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
managedIndex.managed[clientId].configPath = configPath;
|
|
903
|
-
managedIndex.managed[clientId].entries = Object.fromEntries(Object.entries(entries).map(([entryName, serializedEntry]) => [entryName, {
|
|
904
|
-
fingerprint: sha256(serializedEntry),
|
|
905
|
-
lastSyncedAt: new Date().toISOString()
|
|
906
|
-
}]));
|
|
907
|
-
}
|
|
908
|
-
function okResult(clientId, configPath, message) {
|
|
909
|
-
return {
|
|
910
|
-
clientId,
|
|
911
|
-
status: "SYNCED",
|
|
912
|
-
configPath,
|
|
913
|
-
message
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
function errorResult(clientId, configPath, message) {
|
|
917
|
-
return {
|
|
918
|
-
clientId,
|
|
919
|
-
status: "ERROR",
|
|
920
|
-
configPath,
|
|
921
|
-
message
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
function managedGatewayEntryName(serverName) {
|
|
925
|
-
return `${serverName} (mcpx)`;
|
|
926
|
-
}
|
|
927
|
-
function isManagedGatewayProjection(entryName) {
|
|
928
|
-
return entryName.endsWith(" (mcpx)");
|
|
929
|
-
}
|
|
930
|
-
function sortRecord(record) {
|
|
931
|
-
if (!record || Object.keys(record).length === 0) {
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
935
|
-
}
|
|
936
|
-
function normalizeServerSpec(spec) {
|
|
937
|
-
if (spec.transport === "http") {
|
|
938
|
-
return {
|
|
939
|
-
transport: "http",
|
|
940
|
-
url: spec.url,
|
|
941
|
-
headers: sortRecord(spec.headers)
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
return {
|
|
945
|
-
transport: "stdio",
|
|
946
|
-
command: spec.command,
|
|
947
|
-
args: spec.args && spec.args.length > 0 ? [...spec.args] : undefined,
|
|
948
|
-
env: sortRecord(spec.env),
|
|
949
|
-
cwd: spec.cwd
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
function serializeServerSpec(spec) {
|
|
953
|
-
return JSON.stringify(normalizeServerSpec(spec));
|
|
954
|
-
}
|
|
955
|
-
function serverSpecsEqual(left, right) {
|
|
956
|
-
return serializeServerSpec(left) === serializeServerSpec(right);
|
|
957
|
-
}
|
|
958
|
-
function emptyImportScan(clientId, configPath) {
|
|
959
|
-
return {
|
|
960
|
-
clientId,
|
|
961
|
-
configPath,
|
|
962
|
-
candidates: [],
|
|
963
|
-
skipped: []
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
function buildImportSkip(clientId, sourceEntryName, reason, configPath, serverName = sourceEntryName) {
|
|
967
|
-
return {
|
|
968
|
-
clientId,
|
|
969
|
-
configPath,
|
|
970
|
-
sourceEntryName,
|
|
971
|
-
serverName,
|
|
972
|
-
reason
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
function removeSourceEntries(entries, names) {
|
|
976
|
-
for (const name of names ?? []) {
|
|
977
|
-
delete entries[name];
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
function purgeManagedFromDisabledArray(raw, managedNames) {
|
|
981
|
-
const existingDisabled = raw.disabledMcpServers ?? [];
|
|
982
|
-
raw.disabledMcpServers = existingDisabled.filter((name) => !managedNames.includes(name));
|
|
983
|
-
if (Array.isArray(raw.disabledMcpServers) && raw.disabledMcpServers.length === 0) {
|
|
984
|
-
delete raw.disabledMcpServers;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
function purgeManagedFromExcludedArray(raw, managedNames) {
|
|
988
|
-
const mcp = raw.mcp ?? {};
|
|
989
|
-
const existingExcluded = mcp.excluded ?? [];
|
|
990
|
-
mcp.excluded = existingExcluded.filter((name) => !managedNames.includes(name));
|
|
991
|
-
if (Array.isArray(mcp.excluded) && mcp.excluded.length === 0) {
|
|
992
|
-
delete mcp.excluded;
|
|
993
|
-
}
|
|
994
|
-
raw.mcp = Object.keys(mcp).length > 0 ? mcp : undefined;
|
|
995
|
-
}
|
|
996
|
-
function detectManagedEntryDrift(managedIndex, clientId, entryName, existingValue) {
|
|
997
|
-
const recorded = managedIndex.managed[clientId]?.entries?.[entryName];
|
|
998
|
-
if (!recorded || existingValue === undefined)
|
|
999
|
-
return false;
|
|
1000
|
-
const liveFingerprint = sha256(JSON.stringify(existingValue));
|
|
1001
|
-
return liveFingerprint !== recorded.fingerprint;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// src/adapters/claude.ts
|
|
1005
|
-
import fs8 from "node:fs";
|
|
1006
|
-
import path8 from "node:path";
|
|
1007
|
-
import { z as z4 } from "zod";
|
|
1008
|
-
|
|
1009
|
-
// src/core/plugin-projections.ts
|
|
1010
|
-
import fs6 from "node:fs";
|
|
1011
|
-
import path6 from "node:path";
|
|
1012
|
-
var OWNERSHIP_MANIFEST = "mcpx-plugins.json";
|
|
1013
|
-
function loadOwnership(targetDir) {
|
|
1014
|
-
const manifestPath2 = path6.join(targetDir, OWNERSHIP_MANIFEST);
|
|
1015
|
-
if (!fs6.existsSync(manifestPath2)) {
|
|
1016
|
-
return { version: 1, plugins: {} };
|
|
1017
|
-
}
|
|
1018
|
-
try {
|
|
1019
|
-
return JSON.parse(fs6.readFileSync(manifestPath2, "utf8"));
|
|
1020
|
-
} catch {
|
|
1021
|
-
console.error(`[mcpx] Warning: corrupt plugin ownership manifest at ${manifestPath2}, resetting`);
|
|
1022
|
-
return { version: 1, plugins: {} };
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function saveOwnership(targetDir, manifest) {
|
|
1026
|
-
ensureDir(targetDir);
|
|
1027
|
-
fs6.writeFileSync(path6.join(targetDir, OWNERSHIP_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
1028
|
-
}
|
|
1029
|
-
function recordOwned(targetDir, pluginId, paths) {
|
|
1030
|
-
const manifest = loadOwnership(targetDir);
|
|
1031
|
-
const previous = manifest.plugins[pluginId]?.paths ?? [];
|
|
1032
|
-
const next = new Set(paths);
|
|
1033
|
-
for (const stale of previous) {
|
|
1034
|
-
if (next.has(stale))
|
|
1035
|
-
continue;
|
|
1036
|
-
const stalePath = path6.join(targetDir, stale);
|
|
1037
|
-
try {
|
|
1038
|
-
assertWithinBase(targetDir, stalePath);
|
|
1039
|
-
fs6.rmSync(stalePath, { recursive: true, force: true });
|
|
1040
|
-
} catch {}
|
|
1041
|
-
}
|
|
1042
|
-
manifest.plugins[pluginId] = {
|
|
1043
|
-
pluginId,
|
|
1044
|
-
paths,
|
|
1045
|
-
installedAt: new Date().toISOString()
|
|
1046
|
-
};
|
|
1047
|
-
saveOwnership(targetDir, manifest);
|
|
1048
|
-
}
|
|
1049
|
-
function removeOwned(targetDir, pluginId) {
|
|
1050
|
-
const manifest = loadOwnership(targetDir);
|
|
1051
|
-
const entry = manifest.plugins[pluginId];
|
|
1052
|
-
if (entry) {
|
|
1053
|
-
delete manifest.plugins[pluginId];
|
|
1054
|
-
saveOwnership(targetDir, manifest);
|
|
1055
|
-
return entry.paths;
|
|
1056
|
-
}
|
|
1057
|
-
return [];
|
|
1058
|
-
}
|
|
1059
|
-
function prunePluginProjections(targetDir, pluginId) {
|
|
1060
|
-
const paths = removeOwned(targetDir, pluginId);
|
|
1061
|
-
for (const p of paths) {
|
|
1062
|
-
const fullPath = path6.join(targetDir, p);
|
|
1063
|
-
try {
|
|
1064
|
-
assertWithinBase(targetDir, fullPath);
|
|
1065
|
-
if (fs6.existsSync(fullPath)) {
|
|
1066
|
-
fs6.rmSync(fullPath, { recursive: true, force: true });
|
|
1067
|
-
}
|
|
1068
|
-
} catch {}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
function pruneAllPluginProjections(targetDir) {
|
|
1072
|
-
const manifest = loadOwnership(targetDir);
|
|
1073
|
-
for (const [pluginId] of Object.entries(manifest.plugins)) {
|
|
1074
|
-
prunePluginProjections(targetDir, pluginId);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
function pruneUnlistedPlugins(targetDir, keepPluginIds) {
|
|
1078
|
-
const manifest = loadOwnership(targetDir);
|
|
1079
|
-
const keep = new Set(keepPluginIds);
|
|
1080
|
-
for (const [pluginId] of Object.entries(manifest.plugins)) {
|
|
1081
|
-
if (!keep.has(pluginId)) {
|
|
1082
|
-
prunePluginProjections(targetDir, pluginId);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
function nsName(pluginName, id) {
|
|
1087
|
-
return `${pluginName}__${id}`;
|
|
1088
|
-
}
|
|
1089
|
-
function assertWithinBase(base, target) {
|
|
1090
|
-
const resolvedBase = path6.resolve(base);
|
|
1091
|
-
const resolvedTarget = path6.resolve(target);
|
|
1092
|
-
if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path6.sep)) {
|
|
1093
|
-
throw new Error(`Refusing to write outside projection base: ${target} (base: ${base})`);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
function isComponentApproved(plugin, component) {
|
|
1097
|
-
const approval = plugin.approvals?.[component];
|
|
1098
|
-
if (approval === false)
|
|
1099
|
-
return false;
|
|
1100
|
-
if (approval === true)
|
|
1101
|
-
return true;
|
|
1102
|
-
if (component === "hooks")
|
|
1103
|
-
return false;
|
|
1104
|
-
if (component === "mcpServers")
|
|
1105
|
-
return plugin.components.mcpServers;
|
|
1106
|
-
return plugin.components[component] ?? false;
|
|
1107
|
-
}
|
|
1108
|
-
function syncClaude(plugins) {
|
|
1109
|
-
const targetBase = path6.join(homeDir(), ".claude", "skills");
|
|
1110
|
-
const projectedDirs = [];
|
|
1111
|
-
const unsupported = [];
|
|
1112
|
-
const skippedUnapproved = [];
|
|
1113
|
-
for (const plugin of plugins) {
|
|
1114
|
-
if (!plugin.enabled)
|
|
1115
|
-
continue;
|
|
1116
|
-
if (!isComponentApproved(plugin, "skills")) {
|
|
1117
|
-
skippedUnapproved.push(`${plugin.pluginName}:skills`);
|
|
1118
|
-
continue;
|
|
1119
|
-
}
|
|
1120
|
-
const targetDir = path6.join(targetBase, plugin.pluginName);
|
|
1121
|
-
assertWithinBase(targetBase, targetDir);
|
|
1122
|
-
ensureDir(targetDir);
|
|
1123
|
-
const mcpJsonSrc = path6.join(plugin.pluginRoot, ".mcp.json");
|
|
1124
|
-
const mcpJsonDest = path6.join(targetDir, ".mcp.json");
|
|
1125
|
-
copyPluginDir(plugin.pluginRoot, targetDir, [".mcp.json", ".git"]);
|
|
1126
|
-
if (fs6.existsSync(mcpJsonDest)) {
|
|
1127
|
-
fs6.rmSync(mcpJsonDest, { recursive: true, force: true });
|
|
1128
|
-
}
|
|
1129
|
-
projectedDirs.push(targetDir);
|
|
1130
|
-
recordOwned(targetBase, plugin.pluginId, [plugin.pluginName]);
|
|
1131
|
-
}
|
|
1132
|
-
return {
|
|
1133
|
-
clientId: "claude",
|
|
1134
|
-
status: projectedDirs.length > 0 || plugins.length === 0 ? "SYNCED" : "SKIPPED",
|
|
1135
|
-
projectedDirs,
|
|
1136
|
-
unsupported: []
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
function syncClaudeDesktop(plugins) {
|
|
1140
|
-
const unsupported = ["skills", "hooks", "agents", "commands"];
|
|
1141
|
-
return {
|
|
1142
|
-
clientId: "claude-desktop",
|
|
1143
|
-
status: plugins.length > 0 ? "SYNCED" : "SKIPPED",
|
|
1144
|
-
projectedDirs: [],
|
|
1145
|
-
unsupported
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
function syncNamespacedComponents(config, plugins) {
|
|
1149
|
-
const projectedDirs = [];
|
|
1150
|
-
const errors = [];
|
|
1151
|
-
const syncSkills = (plugin) => {
|
|
1152
|
-
if (!config.skillsBase)
|
|
1153
|
-
return;
|
|
1154
|
-
if (!plugin.components.skills || !isComponentApproved(plugin, "skills")) {
|
|
1155
|
-
if (fs6.existsSync(config.skillsBase)) {
|
|
1156
|
-
recordOwned(config.skillsBase, plugin.pluginId, []);
|
|
1157
|
-
}
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
ensureDir(config.skillsBase);
|
|
1161
|
-
const owned = [];
|
|
1162
|
-
for (const skill of plugin.skills) {
|
|
1163
|
-
try {
|
|
1164
|
-
const name = nsName(plugin.pluginName, skill.id);
|
|
1165
|
-
const targetDir = path6.join(config.skillsBase, name);
|
|
1166
|
-
assertWithinBase(config.skillsBase, targetDir);
|
|
1167
|
-
ensureDir(targetDir);
|
|
1168
|
-
copyFileOrDir(skill.path, path6.join(targetDir, "SKILL.md"));
|
|
1169
|
-
owned.push(name);
|
|
1170
|
-
projectedDirs.push(targetDir);
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
recordOwned(config.skillsBase, plugin.pluginId, owned);
|
|
1176
|
-
};
|
|
1177
|
-
const syncCommands = (plugin) => {
|
|
1178
|
-
if (!config.commandsBase)
|
|
1179
|
-
return;
|
|
1180
|
-
if (!plugin.components.commands || !isComponentApproved(plugin, "commands")) {
|
|
1181
|
-
if (fs6.existsSync(config.commandsBase)) {
|
|
1182
|
-
recordOwned(config.commandsBase, plugin.pluginId, []);
|
|
1183
|
-
}
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
ensureDir(config.commandsBase);
|
|
1187
|
-
const owned = [];
|
|
1188
|
-
for (const command of plugin.commands) {
|
|
1189
|
-
try {
|
|
1190
|
-
const targetPath = path6.join(config.commandsBase, `${nsName(plugin.pluginName, command.id)}.md`);
|
|
1191
|
-
assertWithinBase(config.commandsBase, targetPath);
|
|
1192
|
-
copyFileOrDir(command.path, targetPath);
|
|
1193
|
-
owned.push(path6.basename(targetPath));
|
|
1194
|
-
projectedDirs.push(targetPath);
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
errors.push(`${plugin.pluginName}/${command.id}: ${error.message}`);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
recordOwned(config.commandsBase, plugin.pluginId, owned);
|
|
1200
|
-
};
|
|
1201
|
-
for (const plugin of plugins) {
|
|
1202
|
-
if (!plugin.enabled)
|
|
1203
|
-
continue;
|
|
1204
|
-
syncSkills(plugin);
|
|
1205
|
-
syncCommands(plugin);
|
|
1206
|
-
}
|
|
1207
|
-
return {
|
|
1208
|
-
clientId: config.clientId,
|
|
1209
|
-
status: "SYNCED",
|
|
1210
|
-
projectedDirs,
|
|
1211
|
-
unsupported: config.unsupported,
|
|
1212
|
-
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
function syncOpenCode(plugins) {
|
|
1216
|
-
const unsupported = ["skills", "hooks", "agents", "commands"];
|
|
1217
|
-
return {
|
|
1218
|
-
clientId: "opencode",
|
|
1219
|
-
status: plugins.length > 0 ? "SYNCED" : "SKIPPED",
|
|
1220
|
-
projectedDirs: [],
|
|
1221
|
-
unsupported
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
function syncPluginsToClient(clientId, plugins) {
|
|
1225
|
-
const syncMap = {
|
|
1226
|
-
claude: syncClaude,
|
|
1227
|
-
"claude-desktop": syncClaudeDesktop,
|
|
1228
|
-
codex: (plugins2) => syncNamespacedComponents({ clientId: "codex", skillsBase: path6.join(homeDir(), ".codex", "skills"), unsupported: ["hooks", "agents"] }, plugins2),
|
|
1229
|
-
cursor: (plugins2) => syncNamespacedComponents({ clientId: "cursor", skillsBase: path6.join(homeDir(), ".cursor", "skills"), commandsBase: path6.join(homeDir(), ".cursor", "commands"), unsupported: ["hooks", "agents"] }, plugins2),
|
|
1230
|
-
vscode: (plugins2) => syncNamespacedComponents({ clientId: "vscode", skillsBase: path6.join(homeDir(), ".copilot", "skills"), unsupported: ["hooks", "agents"] }, plugins2),
|
|
1231
|
-
qwen: (plugins2) => syncNamespacedComponents({ clientId: "qwen", skillsBase: path6.join(homeDir(), ".qwen", "skills"), unsupported: ["hooks", "agents", "commands"] }, plugins2),
|
|
1232
|
-
cline: (plugins2) => syncNamespacedComponents({ clientId: "cline", skillsBase: path6.join(homeDir(), ".config", "cline", "skills"), unsupported: ["hooks", "agents", "commands"] }, plugins2),
|
|
1233
|
-
kiro: (plugins2) => syncNamespacedComponents({ clientId: "kiro", skillsBase: path6.join(homeDir(), ".kiro", "skills"), unsupported: ["hooks", "agents", "commands"] }, plugins2),
|
|
1234
|
-
opencode: syncOpenCode
|
|
1235
|
-
};
|
|
1236
|
-
const syncFn = syncMap[clientId];
|
|
1237
|
-
if (!syncFn) {
|
|
1238
|
-
return { clientId, status: "SKIPPED", projectedDirs: [], unsupported: [] };
|
|
1239
|
-
}
|
|
1240
|
-
const targetBaseMap = {
|
|
1241
|
-
claude: path6.join(homeDir(), ".claude", "skills"),
|
|
1242
|
-
"claude-desktop": "",
|
|
1243
|
-
codex: path6.join(homeDir(), ".codex", "skills"),
|
|
1244
|
-
cursor: path6.join(homeDir(), ".cursor", "skills"),
|
|
1245
|
-
vscode: path6.join(homeDir(), ".copilot", "skills"),
|
|
1246
|
-
qwen: path6.join(homeDir(), ".qwen", "skills"),
|
|
1247
|
-
cline: path6.join(homeDir(), ".config", "cline", "skills"),
|
|
1248
|
-
kiro: path6.join(homeDir(), ".kiro", "skills"),
|
|
1249
|
-
opencode: ""
|
|
1250
|
-
};
|
|
1251
|
-
const target = targetBaseMap[clientId];
|
|
1252
|
-
if (target) {
|
|
1253
|
-
const enabledIds = plugins.filter((p) => p.enabled).map((p) => p.pluginId);
|
|
1254
|
-
pruneUnlistedPlugins(target, enabledIds);
|
|
1255
|
-
if (clientId === "cursor") {
|
|
1256
|
-
pruneUnlistedPlugins(path6.join(homeDir(), ".cursor", "commands"), enabledIds);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
return syncFn(plugins);
|
|
1260
|
-
}
|
|
1261
|
-
function copyPluginDir(src, dest, exclude = [], errors) {
|
|
1262
|
-
const excludeSet = new Set(exclude);
|
|
1263
|
-
ensureDir(dest);
|
|
1264
|
-
const entries = fs6.readdirSync(src, { withFileTypes: true });
|
|
1265
|
-
for (const entry of entries) {
|
|
1266
|
-
if (excludeSet.has(entry.name))
|
|
1267
|
-
continue;
|
|
1268
|
-
const srcPath = path6.join(src, entry.name);
|
|
1269
|
-
const destPath = path6.join(dest, entry.name);
|
|
1270
|
-
if (entry.isDirectory()) {
|
|
1271
|
-
copyPluginDir(srcPath, destPath, exclude, errors);
|
|
1272
|
-
} else if (entry.isFile()) {
|
|
1273
|
-
try {
|
|
1274
|
-
fs6.copyFileSync(srcPath, destPath);
|
|
1275
|
-
} catch (e) {
|
|
1276
|
-
errors?.push(`Failed to copy ${srcPath}: ${e.message}`);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
function copyFileOrDir(src, dest, errors) {
|
|
1282
|
-
try {
|
|
1283
|
-
ensureDir(path6.dirname(dest));
|
|
1284
|
-
if (fs6.statSync(src).isDirectory()) {
|
|
1285
|
-
copyPluginDir(src, dest, [], errors);
|
|
1286
|
-
} else {
|
|
1287
|
-
fs6.copyFileSync(src, dest);
|
|
1288
|
-
}
|
|
1289
|
-
} catch (e) {
|
|
1290
|
-
errors?.push(`Failed to copy ${src} -> ${dest}: ${e.message}`);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// src/core/skill-projections.ts
|
|
1295
|
-
import fs7 from "node:fs";
|
|
1296
|
-
import path7 from "node:path";
|
|
1297
|
-
import { z as z3 } from "zod";
|
|
1298
|
-
|
|
1299
|
-
// src/core/identifiers.ts
|
|
1300
|
-
import { z as z2 } from "zod";
|
|
1301
|
-
var skillIdSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
|
|
1302
|
-
function validateSkillId(id) {
|
|
1303
|
-
const result = skillIdSchema.safeParse(id);
|
|
1304
|
-
if (!result.success) {
|
|
1305
|
-
throw new Error(`Invalid skill id "${id}". Use letters, numbers, dots, underscores, and hyphens.`);
|
|
1306
|
-
}
|
|
1307
|
-
return id;
|
|
1308
|
-
}
|
|
1309
|
-
function normalizePluginName(name) {
|
|
1310
|
-
const normalized = name.replace(/[\\/]/g, "_").replace(/^\.+/, "").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1311
|
-
if (!normalized) {
|
|
1312
|
-
throw new Error(`Invalid plugin name "${name}"`);
|
|
1313
|
-
}
|
|
1314
|
-
return normalized;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// src/core/skill-projections.ts
|
|
1318
|
-
var OWNERSHIP_MANIFEST2 = "mcpx-skills.json";
|
|
1319
|
-
var ownershipSchema = z3.object({
|
|
1320
|
-
version: z3.literal(1),
|
|
1321
|
-
paths: z3.array(z3.string())
|
|
1322
|
-
});
|
|
1323
|
-
function assertWithinBase2(base, target) {
|
|
1324
|
-
const resolvedBase = path7.resolve(base);
|
|
1325
|
-
const resolvedTarget = path7.resolve(target);
|
|
1326
|
-
if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path7.sep)) {
|
|
1327
|
-
throw new Error(`Refusing to access path outside projection base: ${target}`);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
function loadOwnership2(targetDir) {
|
|
1331
|
-
const manifestPath2 = path7.join(targetDir, OWNERSHIP_MANIFEST2);
|
|
1332
|
-
if (!fs7.existsSync(manifestPath2)) {
|
|
1333
|
-
return { version: 1, paths: [] };
|
|
1334
|
-
}
|
|
1335
|
-
try {
|
|
1336
|
-
const parsed = ownershipSchema.safeParse(JSON.parse(fs7.readFileSync(manifestPath2, "utf8")));
|
|
1337
|
-
if (parsed.success)
|
|
1338
|
-
return parsed.data;
|
|
1339
|
-
} catch {
|
|
1340
|
-
console.error(`[mcpx] Warning: corrupt ownership manifest at ${manifestPath2}, resetting`);
|
|
1341
|
-
return { version: 1, paths: [] };
|
|
1342
|
-
}
|
|
1343
|
-
return { version: 1, paths: [] };
|
|
1344
|
-
}
|
|
1345
|
-
function saveOwnership2(targetDir, manifest) {
|
|
1346
|
-
ensureDir(targetDir);
|
|
1347
|
-
fs7.writeFileSync(path7.join(targetDir, OWNERSHIP_MANIFEST2), JSON.stringify(manifest, null, 2));
|
|
1348
|
-
}
|
|
1349
|
-
function projectSkillsToDir(targetDir, skills, layout) {
|
|
1350
|
-
const owned = loadOwnership2(targetDir);
|
|
1351
|
-
const newPaths = new Set(skills.map((s) => layout === "dir" ? `${s.id}/SKILL.md` : `${s.id}.md`));
|
|
1352
|
-
for (const ownedPath of owned.paths) {
|
|
1353
|
-
if (!newPaths.has(ownedPath)) {
|
|
1354
|
-
const fullPath = path7.join(targetDir, ownedPath);
|
|
1355
|
-
try {
|
|
1356
|
-
assertWithinBase2(targetDir, fullPath);
|
|
1357
|
-
if (fs7.existsSync(fullPath)) {
|
|
1358
|
-
fs7.rmSync(fullPath, { recursive: true, force: true });
|
|
1359
|
-
}
|
|
1360
|
-
if (layout === "dir") {
|
|
1361
|
-
const dirPath = path7.join(targetDir, path7.dirname(ownedPath));
|
|
1362
|
-
assertWithinBase2(targetDir, dirPath);
|
|
1363
|
-
try {
|
|
1364
|
-
const remaining = fs7.readdirSync(dirPath);
|
|
1365
|
-
if (remaining.length === 0) {
|
|
1366
|
-
fs7.rmSync(dirPath, { recursive: true, force: true });
|
|
1367
|
-
}
|
|
1368
|
-
} catch {}
|
|
1369
|
-
}
|
|
1370
|
-
} catch {
|
|
1371
|
-
console.error(`[mcpx] Warning: could not remove stale skill projection: ${fullPath}`);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
for (const skill of skills) {
|
|
1376
|
-
validateSkillId(skill.id);
|
|
1377
|
-
const filePath = layout === "dir" ? path7.join(targetDir, skill.id, "SKILL.md") : path7.join(targetDir, `${skill.id}.md`);
|
|
1378
|
-
assertWithinBase2(targetDir, filePath);
|
|
1379
|
-
ensureDir(path7.dirname(filePath));
|
|
1380
|
-
fs7.writeFileSync(filePath, skill.content, "utf-8");
|
|
1381
|
-
}
|
|
1382
|
-
saveOwnership2(targetDir, { version: 1, paths: Array.from(newPaths) });
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// src/adapters/claude.ts
|
|
1386
|
-
function normalizeProjectKey(p) {
|
|
1387
|
-
try {
|
|
1388
|
-
return fs8.realpathSync(path8.resolve(p));
|
|
1389
|
-
} catch {
|
|
1390
|
-
return path8.resolve(p);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
|
|
1394
|
-
const existingNonManaged = (project.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
|
|
1395
|
-
const combined = [...existingNonManaged, ...disabledForThisProject];
|
|
1396
|
-
if (combined.length > 0) {
|
|
1397
|
-
project.disabledMcpServers = combined;
|
|
1398
|
-
} else {
|
|
1399
|
-
delete project.disabledMcpServers;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
var stringMapSchema = z4.record(z4.string(), z4.string());
|
|
1403
|
-
var claudeEntrySchema = z4.object({
|
|
1404
|
-
type: z4.string().optional(),
|
|
1405
|
-
url: z4.string().min(1).optional(),
|
|
1406
|
-
headers: stringMapSchema.optional(),
|
|
1407
|
-
command: z4.string().min(1).optional(),
|
|
1408
|
-
args: z4.array(z4.string()).optional(),
|
|
1409
|
-
env: stringMapSchema.optional(),
|
|
1410
|
-
cwd: z4.string().min(1).optional(),
|
|
1411
|
-
disabled: z4.boolean().optional()
|
|
1412
|
-
}).passthrough();
|
|
1413
|
-
|
|
1414
|
-
class ClaudeAdapter {
|
|
1415
|
-
id = "claude";
|
|
1416
|
-
detectConfigPath() {
|
|
1417
|
-
return path8.join(homeDir(), ".claude.json");
|
|
1418
|
-
}
|
|
1419
|
-
supportsHttp() {
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
syncPlugins(plugins) {
|
|
1423
|
-
return syncPluginsToClient(this.id, plugins);
|
|
1424
|
-
}
|
|
1425
|
-
syncSkills(skills) {
|
|
1426
|
-
projectSkillsToDir(path8.join(homeDir(), ".claude", "skills"), skills, "dir");
|
|
1427
|
-
}
|
|
1428
|
-
scanForImports(_config, managedIndex) {
|
|
1429
|
-
const configPath = this.detectConfigPath();
|
|
1430
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1431
|
-
if (!configPath) {
|
|
1432
|
-
return result;
|
|
1433
|
-
}
|
|
1434
|
-
const raw = readJsonFile(configPath, {});
|
|
1435
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1436
|
-
const disabledMcpServers = new Set(raw.disabledMcpServers ?? []);
|
|
1437
|
-
const projects = raw.projects ?? {};
|
|
1438
|
-
for (const project of Object.values(projects)) {
|
|
1439
|
-
const projectDisabled = project?.disabledMcpServers ?? [];
|
|
1440
|
-
for (const name of projectDisabled) {
|
|
1441
|
-
disabledMcpServers.add(name);
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
1445
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1446
|
-
continue;
|
|
1447
|
-
}
|
|
1448
|
-
const parsed = claudeEntrySchema.safeParse(rawEntry);
|
|
1449
|
-
if (!parsed.success) {
|
|
1450
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1451
|
-
continue;
|
|
1452
|
-
}
|
|
1453
|
-
const entry = parsed.data;
|
|
1454
|
-
const isDisabled = entry.disabled === true || disabledMcpServers.has(name);
|
|
1455
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1456
|
-
result.candidates.push({
|
|
1457
|
-
clientId: this.id,
|
|
1458
|
-
configPath,
|
|
1459
|
-
sourceEntryName: name,
|
|
1460
|
-
serverName: name,
|
|
1461
|
-
spec: {
|
|
1462
|
-
transport: "http",
|
|
1463
|
-
url: entry.url,
|
|
1464
|
-
headers: entry.headers,
|
|
1465
|
-
enabled: !isDisabled
|
|
1466
|
-
}
|
|
1467
|
-
});
|
|
1468
|
-
continue;
|
|
1469
|
-
}
|
|
1470
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1471
|
-
result.candidates.push({
|
|
1472
|
-
clientId: this.id,
|
|
1473
|
-
configPath,
|
|
1474
|
-
sourceEntryName: name,
|
|
1475
|
-
serverName: name,
|
|
1476
|
-
spec: {
|
|
1477
|
-
transport: "stdio",
|
|
1478
|
-
command: entry.command,
|
|
1479
|
-
args: entry.args,
|
|
1480
|
-
env: entry.env,
|
|
1481
|
-
cwd: entry.cwd,
|
|
1482
|
-
enabled: !isDisabled
|
|
1483
|
-
}
|
|
1484
|
-
});
|
|
1485
|
-
continue;
|
|
1486
|
-
}
|
|
1487
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1488
|
-
}
|
|
1489
|
-
return result;
|
|
1490
|
-
}
|
|
1491
|
-
syncGateway(_config, options) {
|
|
1492
|
-
const configPath = this.detectConfigPath();
|
|
1493
|
-
if (!configPath) {
|
|
1494
|
-
return errorResult(this.id, undefined, "Unable to resolve Claude config path.");
|
|
1495
|
-
}
|
|
1496
|
-
try {
|
|
1497
|
-
const raw = readJsonFile(configPath, {});
|
|
1498
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1499
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1500
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1501
|
-
type: "http",
|
|
1502
|
-
url: entry.url,
|
|
1503
|
-
headers: entry.headers
|
|
1504
|
-
}]));
|
|
1505
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1506
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1507
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1508
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
1509
|
-
for (const name of enabledManagedNames) {
|
|
1510
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1511
|
-
if (topLevelConflict) {
|
|
1512
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
1516
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1517
|
-
topLevelServers[name] = entry;
|
|
1518
|
-
}
|
|
1519
|
-
raw.mcpServers = topLevelServers;
|
|
1520
|
-
purgeManagedFromDisabledArray(raw, managedNames);
|
|
1521
|
-
const scopeByNormalizedPath = new Map;
|
|
1522
|
-
for (const scope of options.projectScopes ?? []) {
|
|
1523
|
-
scopeByNormalizedPath.set(normalizeProjectKey(scope.path), scope.disabledServerNames);
|
|
1524
|
-
}
|
|
1525
|
-
const projects = raw.projects ?? {};
|
|
1526
|
-
for (const projectPath of Object.keys(projects)) {
|
|
1527
|
-
const project = projects[projectPath] ?? {};
|
|
1528
|
-
const disabledForThis = scopeByNormalizedPath.get(normalizeProjectKey(projectPath)) ?? [];
|
|
1529
|
-
syncProjectDisabledArray(project, managedNames, disabledForThis);
|
|
1530
|
-
projects[projectPath] = project;
|
|
1531
|
-
}
|
|
1532
|
-
for (const scope of options.projectScopes ?? []) {
|
|
1533
|
-
if (scope.disabledServerNames.length === 0)
|
|
1534
|
-
continue;
|
|
1535
|
-
const normalizedKey = normalizeProjectKey(scope.path);
|
|
1536
|
-
const alreadyExists = Object.keys(projects).some((p) => normalizeProjectKey(p) === normalizedKey);
|
|
1537
|
-
if (!alreadyExists) {
|
|
1538
|
-
projects[scope.path] = { disabledMcpServers: [...scope.disabledServerNames] };
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
raw.projects = projects;
|
|
1542
|
-
writeJsonAtomic(configPath, raw);
|
|
1543
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1544
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1545
|
-
} catch (error) {
|
|
1546
|
-
return errorResult(this.id, configPath, error.message);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// src/adapters/claude-desktop.ts
|
|
1552
|
-
import { execFileSync } from "node:child_process";
|
|
1553
|
-
import path9 from "node:path";
|
|
1554
|
-
import { z as z5 } from "zod";
|
|
1555
|
-
|
|
1556
|
-
// src/version.ts
|
|
1557
|
-
var APP_VERSION = "0.1.3";
|
|
1558
|
-
|
|
1559
|
-
// src/adapters/claude-desktop.ts
|
|
1560
|
-
var stringMapSchema2 = z5.record(z5.string(), z5.string());
|
|
1561
|
-
var claudeDesktopEntrySchema = z5.object({
|
|
1562
|
-
type: z5.string().optional(),
|
|
1563
|
-
url: z5.string().min(1).optional(),
|
|
1564
|
-
headers: stringMapSchema2.optional(),
|
|
1565
|
-
command: z5.string().min(1).optional(),
|
|
1566
|
-
args: z5.array(z5.string()).optional(),
|
|
1567
|
-
env: stringMapSchema2.optional(),
|
|
1568
|
-
cwd: z5.string().min(1).optional(),
|
|
1569
|
-
disabled: z5.boolean().optional()
|
|
1570
|
-
}).passthrough();
|
|
1571
|
-
function buildProxyEntry(entry) {
|
|
1572
|
-
const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
|
|
1573
|
-
try {
|
|
1574
|
-
const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1575
|
-
if (mcpxPath)
|
|
1576
|
-
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
1577
|
-
} catch {}
|
|
1578
|
-
return { command: "npx", args: ["-y", `@kwonye/mcpx@${APP_VERSION}`, "proxy", upstreamName] };
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
class ClaudeDesktopAdapter {
|
|
1582
|
-
id = "claude-desktop";
|
|
1583
|
-
detectConfigPath() {
|
|
1584
|
-
if (process.platform === "linux") {
|
|
1585
|
-
return path9.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
1586
|
-
}
|
|
1587
|
-
if (process.platform === "win32") {
|
|
1588
|
-
return path9.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
1589
|
-
}
|
|
1590
|
-
return path9.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1591
|
-
}
|
|
1592
|
-
supportsHttp() {
|
|
1593
|
-
return true;
|
|
1594
|
-
}
|
|
1595
|
-
syncPlugins(plugins) {
|
|
1596
|
-
return syncPluginsToClient(this.id, plugins);
|
|
1597
|
-
}
|
|
1598
|
-
syncSkills(skills) {
|
|
1599
|
-
const configPath = this.detectConfigPath();
|
|
1600
|
-
if (!configPath) {
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
projectSkillsToDir(path9.join(path9.dirname(configPath), "skills"), skills, "dir");
|
|
1604
|
-
}
|
|
1605
|
-
scanForImports(_config, managedIndex) {
|
|
1606
|
-
const configPath = this.detectConfigPath();
|
|
1607
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1608
|
-
if (!configPath) {
|
|
1609
|
-
return result;
|
|
1610
|
-
}
|
|
1611
|
-
const raw = readJsonFile(configPath, {});
|
|
1612
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1613
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
1614
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
const parsed = claudeDesktopEntrySchema.safeParse(rawEntry);
|
|
1618
|
-
if (!parsed.success) {
|
|
1619
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
const entry = parsed.data;
|
|
1623
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1624
|
-
result.candidates.push({
|
|
1625
|
-
clientId: this.id,
|
|
1626
|
-
configPath,
|
|
1627
|
-
sourceEntryName: name,
|
|
1628
|
-
serverName: name,
|
|
1629
|
-
spec: {
|
|
1630
|
-
transport: "http",
|
|
1631
|
-
url: entry.url,
|
|
1632
|
-
headers: entry.headers,
|
|
1633
|
-
enabled: entry.disabled !== true
|
|
1634
|
-
}
|
|
1635
|
-
});
|
|
1636
|
-
continue;
|
|
1637
|
-
}
|
|
1638
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1639
|
-
result.candidates.push({
|
|
1640
|
-
clientId: this.id,
|
|
1641
|
-
configPath,
|
|
1642
|
-
sourceEntryName: name,
|
|
1643
|
-
serverName: name,
|
|
1644
|
-
spec: {
|
|
1645
|
-
transport: "stdio",
|
|
1646
|
-
command: entry.command,
|
|
1647
|
-
args: entry.args,
|
|
1648
|
-
env: entry.env,
|
|
1649
|
-
cwd: entry.cwd,
|
|
1650
|
-
enabled: entry.disabled !== true
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
continue;
|
|
1654
|
-
}
|
|
1655
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1656
|
-
}
|
|
1657
|
-
return result;
|
|
1658
|
-
}
|
|
1659
|
-
syncGateway(_config, options) {
|
|
1660
|
-
const configPath = this.detectConfigPath();
|
|
1661
|
-
if (!configPath) {
|
|
1662
|
-
return errorResult(this.id, undefined, "Unable to resolve Claude Desktop config path.");
|
|
1663
|
-
}
|
|
1664
|
-
try {
|
|
1665
|
-
const raw = readJsonFile(configPath, {});
|
|
1666
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1667
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => {
|
|
1668
|
-
const { command, args } = buildProxyEntry(entry);
|
|
1669
|
-
return [entry.name, { command, args }];
|
|
1670
|
-
}));
|
|
1671
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1672
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1673
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1674
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
1675
|
-
for (const name of enabledManagedNames) {
|
|
1676
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1677
|
-
if (topLevelConflict) {
|
|
1678
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
1682
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1683
|
-
topLevelServers[name] = entry;
|
|
1684
|
-
}
|
|
1685
|
-
raw.mcpServers = topLevelServers;
|
|
1686
|
-
writeJsonAtomic(configPath, raw);
|
|
1687
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
|
|
1688
|
-
const { command, args } = buildProxyEntry(entry);
|
|
1689
|
-
return [entry.name, JSON.stringify({ command, args })];
|
|
1690
|
-
})));
|
|
1691
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1692
|
-
} catch (error) {
|
|
1693
|
-
return errorResult(this.id, configPath, error.message);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// src/adapters/codex.ts
|
|
1699
|
-
import path10 from "node:path";
|
|
1700
|
-
import fs9 from "node:fs";
|
|
1701
|
-
import { parse, stringify } from "@iarna/toml";
|
|
1702
|
-
import { z as z6 } from "zod";
|
|
1703
|
-
var stringMapSchema3 = z6.preprocess((value) => {
|
|
1704
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1705
|
-
return value;
|
|
1706
|
-
}
|
|
1707
|
-
return Object.fromEntries(Object.entries(value));
|
|
1708
|
-
}, z6.record(z6.string(), z6.string()));
|
|
1709
|
-
var codexEntrySchema = z6.object({
|
|
1710
|
-
enabled: z6.boolean().optional(),
|
|
1711
|
-
url: z6.string().min(1).optional(),
|
|
1712
|
-
headers: stringMapSchema3.optional(),
|
|
1713
|
-
http_headers: stringMapSchema3.optional(),
|
|
1714
|
-
command: z6.string().min(1).optional(),
|
|
1715
|
-
args: z6.array(z6.string()).optional(),
|
|
1716
|
-
env: stringMapSchema3.optional(),
|
|
1717
|
-
cwd: z6.string().min(1).optional()
|
|
1718
|
-
}).passthrough();
|
|
1719
|
-
|
|
1720
|
-
class CodexAdapter {
|
|
1721
|
-
id = "codex";
|
|
1722
|
-
detectConfigPath() {
|
|
1723
|
-
return path10.join(homeDir(), ".codex", "config.toml");
|
|
1724
|
-
}
|
|
1725
|
-
supportsHttp() {
|
|
1726
|
-
return true;
|
|
1727
|
-
}
|
|
1728
|
-
syncPlugins(plugins) {
|
|
1729
|
-
return syncPluginsToClient(this.id, plugins);
|
|
1730
|
-
}
|
|
1731
|
-
syncSkills(skills) {
|
|
1732
|
-
projectSkillsToDir(path10.join(homeDir(), ".codex", "skills"), skills, "dir");
|
|
1733
|
-
}
|
|
1734
|
-
scanForImports(_config, managedIndex) {
|
|
1735
|
-
const configPath = this.detectConfigPath();
|
|
1736
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1737
|
-
if (!configPath || !fs9.existsSync(configPath)) {
|
|
1738
|
-
return result;
|
|
1739
|
-
}
|
|
1740
|
-
const existing = fs9.readFileSync(configPath, "utf8");
|
|
1741
|
-
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
1742
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
1743
|
-
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
1744
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1745
|
-
continue;
|
|
1746
|
-
}
|
|
1747
|
-
const parsed = codexEntrySchema.safeParse(rawEntry);
|
|
1748
|
-
if (!parsed.success) {
|
|
1749
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
const entry = parsed.data;
|
|
1753
|
-
if (entry.url && !entry.command) {
|
|
1754
|
-
result.candidates.push({
|
|
1755
|
-
clientId: this.id,
|
|
1756
|
-
configPath,
|
|
1757
|
-
sourceEntryName: name,
|
|
1758
|
-
serverName: name,
|
|
1759
|
-
spec: {
|
|
1760
|
-
transport: "http",
|
|
1761
|
-
url: entry.url,
|
|
1762
|
-
headers: entry.http_headers ?? entry.headers,
|
|
1763
|
-
enabled: entry.enabled !== false
|
|
1764
|
-
}
|
|
1765
|
-
});
|
|
1766
|
-
continue;
|
|
1767
|
-
}
|
|
1768
|
-
if (entry.command && !entry.url) {
|
|
1769
|
-
result.candidates.push({
|
|
1770
|
-
clientId: this.id,
|
|
1771
|
-
configPath,
|
|
1772
|
-
sourceEntryName: name,
|
|
1773
|
-
serverName: name,
|
|
1774
|
-
spec: {
|
|
1775
|
-
transport: "stdio",
|
|
1776
|
-
command: entry.command,
|
|
1777
|
-
args: entry.args,
|
|
1778
|
-
env: entry.env,
|
|
1779
|
-
cwd: entry.cwd,
|
|
1780
|
-
enabled: entry.enabled !== false
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
1783
|
-
continue;
|
|
1784
|
-
}
|
|
1785
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1786
|
-
}
|
|
1787
|
-
return result;
|
|
1788
|
-
}
|
|
1789
|
-
syncGateway(_config, options) {
|
|
1790
|
-
const configPath = this.detectConfigPath();
|
|
1791
|
-
if (!configPath) {
|
|
1792
|
-
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
1793
|
-
}
|
|
1794
|
-
try {
|
|
1795
|
-
const existing = fs9.existsSync(configPath) ? fs9.readFileSync(configPath, "utf8") : "";
|
|
1796
|
-
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
1797
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
1798
|
-
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
1799
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1800
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1801
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1802
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1803
|
-
url: entry.url,
|
|
1804
|
-
http_headers: entry.headers
|
|
1805
|
-
}]));
|
|
1806
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcpServers[name]));
|
|
1807
|
-
for (const name of enabledManagedNames) {
|
|
1808
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
1809
|
-
if (conflict) {
|
|
1810
|
-
return errorResult(this.id, configPath, conflict);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, enabledManagedNames);
|
|
1814
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1815
|
-
mcpServers[name] = entry;
|
|
1816
|
-
}
|
|
1817
|
-
doc.mcp_servers = mcpServers;
|
|
1818
|
-
fs9.mkdirSync(path10.dirname(configPath), { recursive: true });
|
|
1819
|
-
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1820
|
-
fs9.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
1821
|
-
fs9.renameSync(tmpPath, configPath);
|
|
1822
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1823
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1824
|
-
} catch (error) {
|
|
1825
|
-
return errorResult(this.id, configPath, error.message);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
// src/adapters/cursor.ts
|
|
1831
|
-
import path11 from "node:path";
|
|
1832
|
-
import { z as z7 } from "zod";
|
|
1833
|
-
var stringMapSchema4 = z7.record(z7.string(), z7.string());
|
|
1834
|
-
var cursorEntrySchema = z7.object({
|
|
1835
|
-
type: z7.string().optional(),
|
|
1836
|
-
url: z7.string().min(1).optional(),
|
|
1837
|
-
headers: stringMapSchema4.optional(),
|
|
1838
|
-
command: z7.string().min(1).optional(),
|
|
1839
|
-
args: z7.array(z7.string()).optional(),
|
|
1840
|
-
env: stringMapSchema4.optional(),
|
|
1841
|
-
cwd: z7.string().min(1).optional(),
|
|
1842
|
-
disabled: z7.boolean().optional()
|
|
1843
|
-
}).passthrough();
|
|
1844
|
-
|
|
1845
|
-
class CursorAdapter {
|
|
1846
|
-
id = "cursor";
|
|
1847
|
-
detectConfigPath() {
|
|
1848
|
-
return path11.join(homeDir(), ".cursor", "mcp.json");
|
|
1849
|
-
}
|
|
1850
|
-
supportsHttp() {
|
|
1851
|
-
return true;
|
|
1852
|
-
}
|
|
1853
|
-
syncPlugins(plugins) {
|
|
1854
|
-
return syncPluginsToClient(this.id, plugins);
|
|
1855
|
-
}
|
|
1856
|
-
syncSkills(skills) {
|
|
1857
|
-
projectSkillsToDir(path11.join(homeDir(), ".cursor", "skills"), skills, "dir");
|
|
1858
|
-
}
|
|
1859
|
-
scanForImports(_config, managedIndex) {
|
|
1860
|
-
const configPath = this.detectConfigPath();
|
|
1861
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1862
|
-
if (!configPath) {
|
|
1863
|
-
return result;
|
|
1864
|
-
}
|
|
1865
|
-
const raw = readJsonFile(configPath, {});
|
|
1866
|
-
const servers = {
|
|
1867
|
-
...raw.mcpServers ?? {}
|
|
1868
|
-
};
|
|
1869
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1870
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1871
|
-
continue;
|
|
1872
|
-
}
|
|
1873
|
-
const parsed = cursorEntrySchema.safeParse(rawEntry);
|
|
1874
|
-
if (!parsed.success) {
|
|
1875
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1876
|
-
continue;
|
|
1877
|
-
}
|
|
1878
|
-
const entry = parsed.data;
|
|
1879
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1880
|
-
result.candidates.push({
|
|
1881
|
-
clientId: this.id,
|
|
1882
|
-
configPath,
|
|
1883
|
-
sourceEntryName: name,
|
|
1884
|
-
serverName: name,
|
|
1885
|
-
spec: {
|
|
1886
|
-
transport: "http",
|
|
1887
|
-
url: entry.url,
|
|
1888
|
-
headers: entry.headers,
|
|
1889
|
-
enabled: entry.disabled !== true
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
continue;
|
|
1893
|
-
}
|
|
1894
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1895
|
-
result.candidates.push({
|
|
1896
|
-
clientId: this.id,
|
|
1897
|
-
configPath,
|
|
1898
|
-
sourceEntryName: name,
|
|
1899
|
-
serverName: name,
|
|
1900
|
-
spec: {
|
|
1901
|
-
transport: "stdio",
|
|
1902
|
-
command: entry.command,
|
|
1903
|
-
args: entry.args,
|
|
1904
|
-
env: entry.env,
|
|
1905
|
-
cwd: entry.cwd,
|
|
1906
|
-
enabled: entry.disabled !== true
|
|
1907
|
-
}
|
|
1908
|
-
});
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1912
|
-
}
|
|
1913
|
-
return result;
|
|
1914
|
-
}
|
|
1915
|
-
syncGateway(_config, options) {
|
|
1916
|
-
const configPath = this.detectConfigPath();
|
|
1917
|
-
if (!configPath) {
|
|
1918
|
-
return errorResult(this.id, undefined, "Unable to resolve Cursor MCP config path.");
|
|
1919
|
-
}
|
|
1920
|
-
try {
|
|
1921
|
-
const raw = readJsonFile(configPath, {});
|
|
1922
|
-
const servers = {
|
|
1923
|
-
...raw.mcpServers ?? {}
|
|
1924
|
-
};
|
|
1925
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1926
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1927
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1928
|
-
type: "http",
|
|
1929
|
-
url: entry.url,
|
|
1930
|
-
headers: entry.headers
|
|
1931
|
-
}]));
|
|
1932
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1933
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
1934
|
-
for (const name of enabledManagedNames) {
|
|
1935
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1936
|
-
if (conflict) {
|
|
1937
|
-
return errorResult(this.id, configPath, conflict);
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1941
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1942
|
-
servers[name] = entry;
|
|
1943
|
-
}
|
|
1944
|
-
const next = {
|
|
1945
|
-
...raw,
|
|
1946
|
-
mcpServers: servers
|
|
1947
|
-
};
|
|
1948
|
-
writeJsonAtomic(configPath, next);
|
|
1949
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1950
|
-
entry.name,
|
|
1951
|
-
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1952
|
-
])));
|
|
1953
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
1954
|
-
} catch (error) {
|
|
1955
|
-
return errorResult(this.id, configPath, error.message);
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// src/adapters/cline.ts
|
|
1961
|
-
import path12 from "node:path";
|
|
1962
|
-
import fs10 from "node:fs";
|
|
1963
|
-
import { z as z8 } from "zod";
|
|
1964
|
-
var HTTP_TRANSPORT_TYPES = new Set(["http", "streamableHttp"]);
|
|
1965
|
-
var stringMapSchema5 = z8.record(z8.string(), z8.string());
|
|
1966
|
-
var clineEntrySchema = z8.object({
|
|
1967
|
-
type: z8.string().optional(),
|
|
1968
|
-
transportType: z8.string().optional(),
|
|
1969
|
-
url: z8.string().min(1).optional(),
|
|
1970
|
-
headers: stringMapSchema5.optional(),
|
|
1971
|
-
command: z8.string().min(1).optional(),
|
|
1972
|
-
args: z8.array(z8.string()).optional(),
|
|
1973
|
-
env: stringMapSchema5.optional(),
|
|
1974
|
-
cwd: z8.string().min(1).optional(),
|
|
1975
|
-
disabled: z8.boolean().optional()
|
|
1976
|
-
}).passthrough();
|
|
1977
|
-
function getCandidatePaths() {
|
|
1978
|
-
if (process.platform === "linux") {
|
|
1979
|
-
return [
|
|
1980
|
-
path12.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1981
|
-
path12.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1982
|
-
path12.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1983
|
-
];
|
|
1984
|
-
}
|
|
1985
|
-
if (process.platform === "win32") {
|
|
1986
|
-
return [
|
|
1987
|
-
path12.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1988
|
-
path12.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1989
|
-
path12.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1990
|
-
];
|
|
1991
|
-
}
|
|
1992
|
-
return [
|
|
1993
|
-
path12.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1994
|
-
path12.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
1995
|
-
path12.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
1996
|
-
];
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
class ClineAdapter {
|
|
2000
|
-
id = "cline";
|
|
2001
|
-
detectConfigPath() {
|
|
2002
|
-
const candidates = getCandidatePaths();
|
|
2003
|
-
for (const candidate of candidates) {
|
|
2004
|
-
if (fs10.existsSync(candidate)) {
|
|
2005
|
-
return candidate;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
return candidates[0] ?? null;
|
|
2009
|
-
}
|
|
2010
|
-
supportsHttp() {
|
|
2011
|
-
return true;
|
|
2012
|
-
}
|
|
2013
|
-
syncPlugins(plugins) {
|
|
2014
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2015
|
-
}
|
|
2016
|
-
syncSkills(skills) {
|
|
2017
|
-
projectSkillsToDir(path12.join(homeDir(), ".config", "cline", "skills"), skills, "dir");
|
|
2018
|
-
}
|
|
2019
|
-
scanForImports(_config, managedIndex) {
|
|
2020
|
-
const configPath = this.detectConfigPath();
|
|
2021
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2022
|
-
if (!configPath || !fs10.existsSync(configPath)) {
|
|
2023
|
-
return result;
|
|
2024
|
-
}
|
|
2025
|
-
const raw = readJsonFile(configPath, {});
|
|
2026
|
-
const servers = {
|
|
2027
|
-
...raw.mcpServers ?? {}
|
|
2028
|
-
};
|
|
2029
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
2030
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2031
|
-
continue;
|
|
2032
|
-
}
|
|
2033
|
-
const parsed = clineEntrySchema.safeParse(rawEntry);
|
|
2034
|
-
if (!parsed.success) {
|
|
2035
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2036
|
-
continue;
|
|
2037
|
-
}
|
|
2038
|
-
const entry = parsed.data;
|
|
2039
|
-
const transport = entry.type ?? entry.transportType;
|
|
2040
|
-
if ((transport === undefined || HTTP_TRANSPORT_TYPES.has(transport)) && entry.url && !entry.command) {
|
|
2041
|
-
result.candidates.push({
|
|
2042
|
-
clientId: this.id,
|
|
2043
|
-
configPath,
|
|
2044
|
-
sourceEntryName: name,
|
|
2045
|
-
serverName: name,
|
|
2046
|
-
spec: {
|
|
2047
|
-
transport: "http",
|
|
2048
|
-
url: entry.url,
|
|
2049
|
-
headers: entry.headers,
|
|
2050
|
-
enabled: entry.disabled !== true
|
|
2051
|
-
}
|
|
2052
|
-
});
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
if ((transport === undefined || transport === "stdio") && entry.command && !entry.url) {
|
|
2056
|
-
result.candidates.push({
|
|
2057
|
-
clientId: this.id,
|
|
2058
|
-
configPath,
|
|
2059
|
-
sourceEntryName: name,
|
|
2060
|
-
serverName: name,
|
|
2061
|
-
spec: {
|
|
2062
|
-
transport: "stdio",
|
|
2063
|
-
command: entry.command,
|
|
2064
|
-
args: entry.args,
|
|
2065
|
-
env: entry.env,
|
|
2066
|
-
cwd: entry.cwd,
|
|
2067
|
-
enabled: entry.disabled !== true
|
|
2068
|
-
}
|
|
2069
|
-
});
|
|
2070
|
-
continue;
|
|
2071
|
-
}
|
|
2072
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2073
|
-
}
|
|
2074
|
-
return result;
|
|
2075
|
-
}
|
|
2076
|
-
syncGateway(_config, options) {
|
|
2077
|
-
const configPath = this.detectConfigPath();
|
|
2078
|
-
if (!configPath) {
|
|
2079
|
-
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
2080
|
-
}
|
|
2081
|
-
if (!fs10.existsSync(configPath)) {
|
|
2082
|
-
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
2083
|
-
}
|
|
2084
|
-
try {
|
|
2085
|
-
const raw = readJsonFile(configPath, {});
|
|
2086
|
-
const servers = {
|
|
2087
|
-
...raw.mcpServers ?? {}
|
|
2088
|
-
};
|
|
2089
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
2090
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2091
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2092
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2093
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2094
|
-
type: "streamableHttp",
|
|
2095
|
-
url: entry.url,
|
|
2096
|
-
headers: entry.headers
|
|
2097
|
-
}]));
|
|
2098
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
2099
|
-
for (const name of enabledManagedNames) {
|
|
2100
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
2101
|
-
if (conflict) {
|
|
2102
|
-
return errorResult(this.id, configPath, conflict);
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
2106
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2107
|
-
servers[name] = entry;
|
|
2108
|
-
}
|
|
2109
|
-
const next = {
|
|
2110
|
-
...raw,
|
|
2111
|
-
mcpServers: servers
|
|
2112
|
-
};
|
|
2113
|
-
writeJsonAtomic(configPath, next);
|
|
2114
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
2115
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2116
|
-
} catch (error) {
|
|
2117
|
-
return errorResult(this.id, configPath, error.message);
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
// src/adapters/kiro.ts
|
|
2123
|
-
import path13 from "node:path";
|
|
2124
|
-
import { z as z9 } from "zod";
|
|
2125
|
-
var stringMapSchema6 = z9.record(z9.string(), z9.string());
|
|
2126
|
-
var kiroEntrySchema = z9.object({
|
|
2127
|
-
type: z9.string().optional(),
|
|
2128
|
-
url: z9.string().min(1).optional(),
|
|
2129
|
-
headers: stringMapSchema6.optional(),
|
|
2130
|
-
command: z9.string().min(1).optional(),
|
|
2131
|
-
args: z9.array(z9.string()).optional(),
|
|
2132
|
-
env: stringMapSchema6.optional(),
|
|
2133
|
-
cwd: z9.string().min(1).optional(),
|
|
2134
|
-
disabled: z9.boolean().optional()
|
|
2135
|
-
}).passthrough();
|
|
2136
|
-
|
|
2137
|
-
class KiroAdapter {
|
|
2138
|
-
id = "kiro";
|
|
2139
|
-
detectConfigPath() {
|
|
2140
|
-
return path13.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
2141
|
-
}
|
|
2142
|
-
supportsHttp() {
|
|
2143
|
-
return true;
|
|
2144
|
-
}
|
|
2145
|
-
syncPlugins(plugins) {
|
|
2146
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2147
|
-
}
|
|
2148
|
-
scanForImports(_config, managedIndex) {
|
|
2149
|
-
const configPath = this.detectConfigPath();
|
|
2150
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2151
|
-
if (!configPath) {
|
|
2152
|
-
return result;
|
|
2153
|
-
}
|
|
2154
|
-
const raw = readJsonFile(configPath, {});
|
|
2155
|
-
const servers = {
|
|
2156
|
-
...raw.mcpServers ?? {}
|
|
2157
|
-
};
|
|
2158
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
2159
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2160
|
-
continue;
|
|
2161
|
-
}
|
|
2162
|
-
const parsed = kiroEntrySchema.safeParse(rawEntry);
|
|
2163
|
-
if (!parsed.success) {
|
|
2164
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2165
|
-
continue;
|
|
2166
|
-
}
|
|
2167
|
-
const entry = parsed.data;
|
|
2168
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
2169
|
-
result.candidates.push({
|
|
2170
|
-
clientId: this.id,
|
|
2171
|
-
configPath,
|
|
2172
|
-
sourceEntryName: name,
|
|
2173
|
-
serverName: name,
|
|
2174
|
-
spec: {
|
|
2175
|
-
transport: "http",
|
|
2176
|
-
url: entry.url,
|
|
2177
|
-
headers: entry.headers,
|
|
2178
|
-
enabled: entry.disabled !== true
|
|
2179
|
-
}
|
|
2180
|
-
});
|
|
2181
|
-
continue;
|
|
2182
|
-
}
|
|
2183
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
2184
|
-
result.candidates.push({
|
|
2185
|
-
clientId: this.id,
|
|
2186
|
-
configPath,
|
|
2187
|
-
sourceEntryName: name,
|
|
2188
|
-
serverName: name,
|
|
2189
|
-
spec: {
|
|
2190
|
-
transport: "stdio",
|
|
2191
|
-
command: entry.command,
|
|
2192
|
-
args: entry.args,
|
|
2193
|
-
env: entry.env,
|
|
2194
|
-
cwd: entry.cwd,
|
|
2195
|
-
enabled: entry.disabled !== true
|
|
2196
|
-
}
|
|
2197
|
-
});
|
|
2198
|
-
continue;
|
|
2199
|
-
}
|
|
2200
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2201
|
-
}
|
|
2202
|
-
return result;
|
|
2203
|
-
}
|
|
2204
|
-
syncGateway(_config, options) {
|
|
2205
|
-
const configPath = this.detectConfigPath();
|
|
2206
|
-
if (!configPath) {
|
|
2207
|
-
return errorResult(this.id, undefined, "Unable to resolve Kiro MCP config path.");
|
|
2208
|
-
}
|
|
2209
|
-
try {
|
|
2210
|
-
const raw = readJsonFile(configPath, {});
|
|
2211
|
-
const servers = {
|
|
2212
|
-
...raw.mcpServers ?? {}
|
|
2213
|
-
};
|
|
2214
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
2215
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2216
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2217
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2218
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2219
|
-
url: entry.url,
|
|
2220
|
-
headers: entry.headers
|
|
2221
|
-
}]));
|
|
2222
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
2223
|
-
for (const name of enabledManagedNames) {
|
|
2224
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
2225
|
-
if (conflict) {
|
|
2226
|
-
return errorResult(this.id, configPath, conflict);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
2230
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2231
|
-
servers[name] = entry;
|
|
2232
|
-
}
|
|
2233
|
-
const next = {
|
|
2234
|
-
...raw,
|
|
2235
|
-
mcpServers: servers
|
|
2236
|
-
};
|
|
2237
|
-
writeJsonAtomic(configPath, next);
|
|
2238
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
2239
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2240
|
-
} catch (error) {
|
|
2241
|
-
return errorResult(this.id, configPath, error.message);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
syncSkills(skills) {
|
|
2245
|
-
projectSkillsToDir(path13.join(homeDir(), ".kiro", "skills"), skills, "flat");
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
// src/adapters/opencode.ts
|
|
2250
|
-
import path14 from "node:path";
|
|
2251
|
-
import { z as z10 } from "zod";
|
|
2252
|
-
var stringMapSchema7 = z10.record(z10.string(), z10.string());
|
|
2253
|
-
var openCodeEntrySchema = z10.object({
|
|
2254
|
-
type: z10.string().optional(),
|
|
2255
|
-
url: z10.string().min(1).optional(),
|
|
2256
|
-
headers: stringMapSchema7.optional(),
|
|
2257
|
-
command: z10.union([z10.string().min(1), z10.array(z10.string())]).optional(),
|
|
2258
|
-
args: z10.array(z10.string()).optional(),
|
|
2259
|
-
env: stringMapSchema7.optional(),
|
|
2260
|
-
environment: stringMapSchema7.optional(),
|
|
2261
|
-
cwd: z10.string().min(1).optional(),
|
|
2262
|
-
enabled: z10.boolean().optional()
|
|
2263
|
-
}).passthrough();
|
|
2264
|
-
|
|
2265
|
-
class OpenCodeAdapter {
|
|
2266
|
-
id = "opencode";
|
|
2267
|
-
detectConfigPath() {
|
|
2268
|
-
return path14.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
2269
|
-
}
|
|
2270
|
-
supportsHttp() {
|
|
2271
|
-
return true;
|
|
2272
|
-
}
|
|
2273
|
-
syncPlugins(plugins) {
|
|
2274
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2275
|
-
}
|
|
2276
|
-
syncSkills(skills) {
|
|
2277
|
-
const configPath = this.detectConfigPath();
|
|
2278
|
-
if (!configPath) {
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
|
-
projectSkillsToDir(path14.join(path14.dirname(configPath), "skills"), skills, "dir");
|
|
2282
|
-
}
|
|
2283
|
-
scanForImports(_config, managedIndex) {
|
|
2284
|
-
const configPath = this.detectConfigPath();
|
|
2285
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2286
|
-
if (!configPath) {
|
|
2287
|
-
return result;
|
|
2288
|
-
}
|
|
2289
|
-
const raw = readJsonFile(configPath, {});
|
|
2290
|
-
const mcp = {
|
|
2291
|
-
...raw.mcp ?? {}
|
|
2292
|
-
};
|
|
2293
|
-
for (const [name, rawEntry] of Object.entries(mcp)) {
|
|
2294
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2295
|
-
continue;
|
|
2296
|
-
}
|
|
2297
|
-
const parsed = openCodeEntrySchema.safeParse(rawEntry);
|
|
2298
|
-
if (!parsed.success) {
|
|
2299
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2300
|
-
continue;
|
|
2301
|
-
}
|
|
2302
|
-
const entry = parsed.data;
|
|
2303
|
-
if ((entry.type === undefined || entry.type === "remote" || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
2304
|
-
result.candidates.push({
|
|
2305
|
-
clientId: this.id,
|
|
2306
|
-
configPath,
|
|
2307
|
-
sourceEntryName: name,
|
|
2308
|
-
serverName: name,
|
|
2309
|
-
spec: {
|
|
2310
|
-
transport: "http",
|
|
2311
|
-
url: entry.url,
|
|
2312
|
-
headers: entry.headers,
|
|
2313
|
-
enabled: entry.enabled !== false
|
|
2314
|
-
}
|
|
2315
|
-
});
|
|
2316
|
-
continue;
|
|
2317
|
-
}
|
|
2318
|
-
if ((entry.type === undefined || entry.type === "local" || entry.type === "stdio") && entry.command && !entry.url) {
|
|
2319
|
-
const commandParts = Array.isArray(entry.command) ? entry.command : [entry.command, ...entry.args ?? []];
|
|
2320
|
-
if (commandParts.length === 0) {
|
|
2321
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2322
|
-
continue;
|
|
2323
|
-
}
|
|
2324
|
-
result.candidates.push({
|
|
2325
|
-
clientId: this.id,
|
|
2326
|
-
configPath,
|
|
2327
|
-
sourceEntryName: name,
|
|
2328
|
-
serverName: name,
|
|
2329
|
-
spec: {
|
|
2330
|
-
transport: "stdio",
|
|
2331
|
-
command: commandParts[0],
|
|
2332
|
-
args: commandParts.slice(1),
|
|
2333
|
-
env: entry.environment ?? entry.env,
|
|
2334
|
-
cwd: entry.cwd,
|
|
2335
|
-
enabled: entry.enabled !== false
|
|
2336
|
-
}
|
|
2337
|
-
});
|
|
2338
|
-
continue;
|
|
2339
|
-
}
|
|
2340
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2341
|
-
}
|
|
2342
|
-
return result;
|
|
2343
|
-
}
|
|
2344
|
-
syncGateway(_config, options) {
|
|
2345
|
-
const configPath = this.detectConfigPath();
|
|
2346
|
-
if (!configPath) {
|
|
2347
|
-
return errorResult(this.id, undefined, "Unable to resolve OpenCode config path.");
|
|
2348
|
-
}
|
|
2349
|
-
try {
|
|
2350
|
-
const raw = readJsonFile(configPath, {});
|
|
2351
|
-
const mcp = {
|
|
2352
|
-
...raw.mcp ?? {}
|
|
2353
|
-
};
|
|
2354
|
-
removeSourceEntries(mcp, options.sourceEntriesToRemove);
|
|
2355
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2356
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2357
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2358
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2359
|
-
type: "remote",
|
|
2360
|
-
url: entry.url,
|
|
2361
|
-
headers: entry.headers
|
|
2362
|
-
}]));
|
|
2363
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcp[name]));
|
|
2364
|
-
for (const name of enabledManagedNames) {
|
|
2365
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
|
|
2366
|
-
if (conflict) {
|
|
2367
|
-
return errorResult(this.id, configPath, conflict);
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcp, enabledManagedNames);
|
|
2371
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2372
|
-
mcp[name] = entry;
|
|
2373
|
-
}
|
|
2374
|
-
const next = {
|
|
2375
|
-
...raw,
|
|
2376
|
-
mcp
|
|
2377
|
-
};
|
|
2378
|
-
writeJsonAtomic(configPath, next);
|
|
2379
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
2380
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2381
|
-
} catch (error) {
|
|
2382
|
-
return errorResult(this.id, configPath, error.message);
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
// src/adapters/vscode.ts
|
|
2388
|
-
import path15 from "node:path";
|
|
2389
|
-
import { z as z11 } from "zod";
|
|
2390
|
-
var stringMapSchema8 = z11.record(z11.string(), z11.string());
|
|
2391
|
-
var vscodeEntrySchema = z11.object({
|
|
2392
|
-
type: z11.string().optional(),
|
|
2393
|
-
url: z11.string().min(1).optional(),
|
|
2394
|
-
headers: stringMapSchema8.optional(),
|
|
2395
|
-
command: z11.string().min(1).optional(),
|
|
2396
|
-
args: z11.array(z11.string()).optional(),
|
|
2397
|
-
env: stringMapSchema8.optional(),
|
|
2398
|
-
cwd: z11.string().min(1).optional(),
|
|
2399
|
-
disabled: z11.boolean().optional()
|
|
2400
|
-
}).passthrough();
|
|
2401
|
-
|
|
2402
|
-
class VsCodeAdapter {
|
|
2403
|
-
id = "vscode";
|
|
2404
|
-
detectConfigPath() {
|
|
2405
|
-
if (process.platform === "linux") {
|
|
2406
|
-
return path15.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
2407
|
-
}
|
|
2408
|
-
if (process.platform === "win32") {
|
|
2409
|
-
return path15.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
2410
|
-
}
|
|
2411
|
-
return path15.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
2412
|
-
}
|
|
2413
|
-
supportsHttp() {
|
|
2414
|
-
return true;
|
|
2415
|
-
}
|
|
2416
|
-
syncPlugins(plugins) {
|
|
2417
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2418
|
-
}
|
|
2419
|
-
syncSkills(skills) {
|
|
2420
|
-
projectSkillsToDir(path15.join(homeDir(), ".copilot", "skills"), skills, "dir");
|
|
2421
|
-
}
|
|
2422
|
-
scanForImports(_config, managedIndex) {
|
|
2423
|
-
const configPath = this.detectConfigPath();
|
|
2424
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2425
|
-
if (!configPath) {
|
|
2426
|
-
return result;
|
|
2427
|
-
}
|
|
2428
|
-
const raw = readJsonFile(configPath, {});
|
|
2429
|
-
const servers = {
|
|
2430
|
-
...raw.servers ?? {}
|
|
2431
|
-
};
|
|
2432
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
2433
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2434
|
-
continue;
|
|
2435
|
-
}
|
|
2436
|
-
const parsed = vscodeEntrySchema.safeParse(rawEntry);
|
|
2437
|
-
if (!parsed.success) {
|
|
2438
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2439
|
-
continue;
|
|
2440
|
-
}
|
|
2441
|
-
const entry = parsed.data;
|
|
2442
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
2443
|
-
result.candidates.push({
|
|
2444
|
-
clientId: this.id,
|
|
2445
|
-
configPath,
|
|
2446
|
-
sourceEntryName: name,
|
|
2447
|
-
serverName: name,
|
|
2448
|
-
spec: {
|
|
2449
|
-
transport: "http",
|
|
2450
|
-
url: entry.url,
|
|
2451
|
-
headers: entry.headers,
|
|
2452
|
-
enabled: entry.disabled !== true
|
|
2453
|
-
}
|
|
2454
|
-
});
|
|
2455
|
-
continue;
|
|
2456
|
-
}
|
|
2457
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
2458
|
-
result.candidates.push({
|
|
2459
|
-
clientId: this.id,
|
|
2460
|
-
configPath,
|
|
2461
|
-
sourceEntryName: name,
|
|
2462
|
-
serverName: name,
|
|
2463
|
-
spec: {
|
|
2464
|
-
transport: "stdio",
|
|
2465
|
-
command: entry.command,
|
|
2466
|
-
args: entry.args,
|
|
2467
|
-
env: entry.env,
|
|
2468
|
-
cwd: entry.cwd,
|
|
2469
|
-
enabled: entry.disabled !== true
|
|
2470
|
-
}
|
|
2471
|
-
});
|
|
2472
|
-
continue;
|
|
2473
|
-
}
|
|
2474
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2475
|
-
}
|
|
2476
|
-
return result;
|
|
2477
|
-
}
|
|
2478
|
-
syncGateway(_config, options) {
|
|
2479
|
-
const configPath = this.detectConfigPath();
|
|
2480
|
-
if (!configPath) {
|
|
2481
|
-
return errorResult(this.id, undefined, "Unable to resolve VS Code MCP config path.");
|
|
2482
|
-
}
|
|
2483
|
-
try {
|
|
2484
|
-
const raw = readJsonFile(configPath, {});
|
|
2485
|
-
const servers = {
|
|
2486
|
-
...raw.servers ?? {}
|
|
2487
|
-
};
|
|
2488
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
2489
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2490
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2491
|
-
type: "http",
|
|
2492
|
-
url: entry.url,
|
|
2493
|
-
headers: entry.headers
|
|
2494
|
-
}]));
|
|
2495
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2496
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
2497
|
-
for (const name of enabledManagedNames) {
|
|
2498
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
2499
|
-
if (conflict) {
|
|
2500
|
-
return errorResult(this.id, configPath, conflict);
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
2504
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2505
|
-
servers[name] = entry;
|
|
2506
|
-
}
|
|
2507
|
-
const next = {
|
|
2508
|
-
...raw,
|
|
2509
|
-
servers
|
|
2510
|
-
};
|
|
2511
|
-
writeJsonAtomic(configPath, next);
|
|
2512
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => [
|
|
2513
|
-
entry.name,
|
|
2514
|
-
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers })
|
|
2515
|
-
])));
|
|
2516
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2517
|
-
} catch (error) {
|
|
2518
|
-
return errorResult(this.id, configPath, error.message);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
// src/adapters/qwen.ts
|
|
2524
|
-
import path16 from "node:path";
|
|
2525
|
-
import { z as z12 } from "zod";
|
|
2526
|
-
var stringMapSchema9 = z12.record(z12.string(), z12.string());
|
|
2527
|
-
var qwenEntrySchema = z12.object({
|
|
2528
|
-
httpUrl: z12.string().min(1).optional(),
|
|
2529
|
-
url: z12.string().min(1).optional(),
|
|
2530
|
-
headers: stringMapSchema9.optional(),
|
|
2531
|
-
command: z12.string().min(1).optional(),
|
|
2532
|
-
args: z12.array(z12.string()).optional(),
|
|
2533
|
-
env: stringMapSchema9.optional(),
|
|
2534
|
-
cwd: z12.string().min(1).optional(),
|
|
2535
|
-
disabled: z12.boolean().optional()
|
|
2536
|
-
}).passthrough();
|
|
2537
|
-
|
|
2538
|
-
class QwenAdapter {
|
|
2539
|
-
id = "qwen";
|
|
2540
|
-
detectConfigPath() {
|
|
2541
|
-
return path16.join(homeDir(), ".qwen", "settings.json");
|
|
2542
|
-
}
|
|
2543
|
-
supportsHttp() {
|
|
2544
|
-
return true;
|
|
2545
|
-
}
|
|
2546
|
-
syncPlugins(plugins) {
|
|
2547
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2548
|
-
}
|
|
2549
|
-
scanForImports(_config, managedIndex) {
|
|
2550
|
-
const configPath = this.detectConfigPath();
|
|
2551
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2552
|
-
if (!configPath) {
|
|
2553
|
-
return result;
|
|
2554
|
-
}
|
|
2555
|
-
const raw = readJsonFile(configPath, {});
|
|
2556
|
-
const excludedSet = new Set(raw.mcp?.excluded ?? []);
|
|
2557
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
2558
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
2559
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2560
|
-
continue;
|
|
2561
|
-
}
|
|
2562
|
-
const parsed = qwenEntrySchema.safeParse(rawEntry);
|
|
2563
|
-
if (!parsed.success) {
|
|
2564
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2565
|
-
continue;
|
|
2566
|
-
}
|
|
2567
|
-
const entry = parsed.data;
|
|
2568
|
-
if (entry.httpUrl && !entry.command) {
|
|
2569
|
-
result.candidates.push({
|
|
2570
|
-
clientId: this.id,
|
|
2571
|
-
configPath,
|
|
2572
|
-
sourceEntryName: name,
|
|
2573
|
-
serverName: name,
|
|
2574
|
-
spec: {
|
|
2575
|
-
transport: "http",
|
|
2576
|
-
url: entry.httpUrl,
|
|
2577
|
-
headers: entry.headers,
|
|
2578
|
-
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
2579
|
-
}
|
|
2580
|
-
});
|
|
2581
|
-
continue;
|
|
2582
|
-
}
|
|
2583
|
-
if (entry.url && !entry.httpUrl && !entry.command) {
|
|
2584
|
-
result.skipped.push(buildImportSkip(this.id, name, "Legacy SSE transport is not supported; use Qwen's httpUrl field for MCP v2 streamable HTTP.", configPath));
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
if (entry.command && !entry.httpUrl) {
|
|
2588
|
-
result.candidates.push({
|
|
2589
|
-
clientId: this.id,
|
|
2590
|
-
configPath,
|
|
2591
|
-
sourceEntryName: name,
|
|
2592
|
-
serverName: name,
|
|
2593
|
-
spec: {
|
|
2594
|
-
transport: "stdio",
|
|
2595
|
-
command: entry.command,
|
|
2596
|
-
args: entry.args,
|
|
2597
|
-
env: entry.env,
|
|
2598
|
-
cwd: entry.cwd,
|
|
2599
|
-
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
2600
|
-
}
|
|
2601
|
-
});
|
|
2602
|
-
continue;
|
|
2603
|
-
}
|
|
2604
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2605
|
-
}
|
|
2606
|
-
return result;
|
|
2607
|
-
}
|
|
2608
|
-
syncGateway(_config, options) {
|
|
2609
|
-
const configPath = this.detectConfigPath();
|
|
2610
|
-
if (!configPath) {
|
|
2611
|
-
return errorResult(this.id, undefined, "Unable to resolve Qwen config path.");
|
|
2612
|
-
}
|
|
2613
|
-
try {
|
|
2614
|
-
const raw = readJsonFile(configPath, {});
|
|
2615
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2616
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2617
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2618
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2619
|
-
httpUrl: entry.url,
|
|
2620
|
-
headers: entry.headers
|
|
2621
|
-
}]));
|
|
2622
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
2623
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
2624
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
|
|
2625
|
-
for (const name of enabledManagedNames) {
|
|
2626
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
2627
|
-
if (topLevelConflict) {
|
|
2628
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
2632
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2633
|
-
topLevelServers[name] = entry;
|
|
2634
|
-
}
|
|
2635
|
-
raw.mcpServers = topLevelServers;
|
|
2636
|
-
purgeManagedFromExcludedArray(raw, managedNames);
|
|
2637
|
-
writeJsonAtomic(configPath, raw);
|
|
2638
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
2639
|
-
entry.name,
|
|
2640
|
-
JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
2641
|
-
])));
|
|
2642
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2643
|
-
} catch (error) {
|
|
2644
|
-
return errorResult(this.id, configPath, error.message);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
syncSkills(skills) {
|
|
2648
|
-
projectSkillsToDir(path16.join(homeDir(), ".qwen", "skills"), skills, "flat");
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// src/adapters/openclaw.ts
|
|
2653
|
-
import fs11 from "node:fs";
|
|
2654
|
-
import path17 from "node:path";
|
|
2655
|
-
import { z as z13 } from "zod";
|
|
2656
|
-
var stringMapSchema10 = z13.record(z13.string(), z13.string());
|
|
2657
|
-
var openclawEntrySchema = z13.object({
|
|
2658
|
-
url: z13.string().min(1).optional(),
|
|
2659
|
-
transport: z13.string().optional(),
|
|
2660
|
-
headers: stringMapSchema10.optional(),
|
|
2661
|
-
command: z13.string().min(1).optional(),
|
|
2662
|
-
args: z13.array(z13.string()).optional(),
|
|
2663
|
-
env: stringMapSchema10.optional(),
|
|
2664
|
-
cwd: z13.string().min(1).optional(),
|
|
2665
|
-
workingDirectory: z13.string().min(1).optional(),
|
|
2666
|
-
auth: z13.unknown().optional()
|
|
2667
|
-
}).passthrough();
|
|
2668
|
-
|
|
2669
|
-
class OpenClawAdapter {
|
|
2670
|
-
id = "openclaw";
|
|
2671
|
-
detectConfigPath() {
|
|
2672
|
-
return process.env.OPENCLAW_CONFIG_PATH ?? path17.join(homeDir(), ".openclaw", "openclaw.json");
|
|
2673
|
-
}
|
|
2674
|
-
supportsHttp() {
|
|
2675
|
-
return true;
|
|
2676
|
-
}
|
|
2677
|
-
syncPlugins(plugins) {
|
|
2678
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2679
|
-
}
|
|
2680
|
-
syncSkills(skills) {
|
|
2681
|
-
projectSkillsToDir(path17.join(homeDir(), ".openclaw", "skills"), skills, "dir");
|
|
2682
|
-
}
|
|
2683
|
-
scanForImports(_config, managedIndex) {
|
|
2684
|
-
const configPath = this.detectConfigPath();
|
|
2685
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2686
|
-
if (!configPath) {
|
|
2687
|
-
return result;
|
|
2688
|
-
}
|
|
2689
|
-
let raw;
|
|
2690
|
-
try {
|
|
2691
|
-
raw = JSON.parse(fs11.readFileSync(configPath, "utf8"));
|
|
2692
|
-
} catch {
|
|
2693
|
-
return result;
|
|
2694
|
-
}
|
|
2695
|
-
const mcpServers = {};
|
|
2696
|
-
const rootMcp = raw.mcp;
|
|
2697
|
-
if (rootMcp?.servers) {
|
|
2698
|
-
Object.assign(mcpServers, rootMcp.servers);
|
|
2699
|
-
}
|
|
2700
|
-
if (raw.mcpServers) {
|
|
2701
|
-
Object.assign(mcpServers, raw.mcpServers);
|
|
2702
|
-
}
|
|
2703
|
-
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
2704
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2705
|
-
continue;
|
|
2706
|
-
}
|
|
2707
|
-
const parsed = openclawEntrySchema.safeParse(rawEntry);
|
|
2708
|
-
if (!parsed.success) {
|
|
2709
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2710
|
-
continue;
|
|
2711
|
-
}
|
|
2712
|
-
const entry = parsed.data;
|
|
2713
|
-
if (entry.auth === "oauth") {
|
|
2714
|
-
result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
|
|
2715
|
-
continue;
|
|
2716
|
-
}
|
|
2717
|
-
if (entry.url && !entry.command) {
|
|
2718
|
-
result.candidates.push({
|
|
2719
|
-
clientId: this.id,
|
|
2720
|
-
configPath,
|
|
2721
|
-
sourceEntryName: name,
|
|
2722
|
-
serverName: name,
|
|
2723
|
-
spec: {
|
|
2724
|
-
transport: "http",
|
|
2725
|
-
url: entry.url,
|
|
2726
|
-
headers: entry.headers
|
|
2727
|
-
}
|
|
2728
|
-
});
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
|
-
if (entry.command && !entry.url) {
|
|
2732
|
-
result.candidates.push({
|
|
2733
|
-
clientId: this.id,
|
|
2734
|
-
configPath,
|
|
2735
|
-
sourceEntryName: name,
|
|
2736
|
-
serverName: name,
|
|
2737
|
-
spec: {
|
|
2738
|
-
transport: "stdio",
|
|
2739
|
-
command: entry.command,
|
|
2740
|
-
args: entry.args,
|
|
2741
|
-
env: entry.env,
|
|
2742
|
-
cwd: entry.workingDirectory ?? entry.cwd
|
|
2743
|
-
}
|
|
2744
|
-
});
|
|
2745
|
-
continue;
|
|
2746
|
-
}
|
|
2747
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2748
|
-
}
|
|
2749
|
-
return result;
|
|
2750
|
-
}
|
|
2751
|
-
syncGateway(_config, options) {
|
|
2752
|
-
const configPath = this.detectConfigPath();
|
|
2753
|
-
if (!configPath) {
|
|
2754
|
-
return errorResult(this.id, undefined, "Unable to resolve OpenClaw config path.");
|
|
2755
|
-
}
|
|
2756
|
-
try {
|
|
2757
|
-
let raw;
|
|
2758
|
-
if (fs11.existsSync(configPath)) {
|
|
2759
|
-
try {
|
|
2760
|
-
raw = JSON.parse(fs11.readFileSync(configPath, "utf8"));
|
|
2761
|
-
} catch {
|
|
2762
|
-
return errorResult(this.id, configPath, "OpenClaw config contains invalid JSON (possibly JSON5). Cannot safely write.");
|
|
2763
|
-
}
|
|
2764
|
-
} else {
|
|
2765
|
-
raw = {};
|
|
2766
|
-
}
|
|
2767
|
-
const rootMcp = raw.mcp ?? {};
|
|
2768
|
-
const servers = rootMcp.servers ?? {};
|
|
2769
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
2770
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
2771
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
2772
|
-
url: entry.url,
|
|
2773
|
-
transport: "streamable-http",
|
|
2774
|
-
headers: entry.headers
|
|
2775
|
-
}]));
|
|
2776
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
2777
|
-
const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
|
|
2778
|
-
for (const name of enabledManagedNames) {
|
|
2779
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
2780
|
-
if (conflict) {
|
|
2781
|
-
return errorResult(this.id, configPath, conflict);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
2785
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2786
|
-
servers[name] = entry;
|
|
2787
|
-
}
|
|
2788
|
-
rootMcp.servers = servers;
|
|
2789
|
-
raw.mcp = rootMcp;
|
|
2790
|
-
writeJsonAtomic(configPath, raw);
|
|
2791
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
2792
|
-
entry.name,
|
|
2793
|
-
JSON.stringify({ url: entry.url, transport: "streamable-http", headers: entry.headers, enabled: entry.enabled })
|
|
2794
|
-
])));
|
|
2795
|
-
return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
|
|
2796
|
-
} catch (error) {
|
|
2797
|
-
return errorResult(this.id, configPath, error.message);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
// src/adapters/hermes.ts
|
|
2803
|
-
import fs12 from "node:fs";
|
|
2804
|
-
import path18 from "node:path";
|
|
2805
|
-
import YAML from "yaml";
|
|
2806
|
-
import { z as z14 } from "zod";
|
|
2807
|
-
var stringMapSchema11 = z14.record(z14.string(), z14.string());
|
|
2808
|
-
var hermesEntrySchema = z14.object({
|
|
2809
|
-
url: z14.string().min(1).optional(),
|
|
2810
|
-
headers: stringMapSchema11.optional(),
|
|
2811
|
-
command: z14.string().min(1).optional(),
|
|
2812
|
-
args: z14.array(z14.string()).optional(),
|
|
2813
|
-
env: stringMapSchema11.optional(),
|
|
2814
|
-
auth: z14.unknown().optional(),
|
|
2815
|
-
enabled: z14.boolean().optional(),
|
|
2816
|
-
timeout: z14.number().optional(),
|
|
2817
|
-
tools: z14.array(z14.string()).optional()
|
|
2818
|
-
}).passthrough();
|
|
2819
|
-
|
|
2820
|
-
class HermesAdapter {
|
|
2821
|
-
id = "hermes";
|
|
2822
|
-
detectConfigPath() {
|
|
2823
|
-
return path18.join(homeDir(), ".hermes", "config.yaml");
|
|
2824
|
-
}
|
|
2825
|
-
supportsHttp() {
|
|
2826
|
-
return true;
|
|
2827
|
-
}
|
|
2828
|
-
syncPlugins(plugins) {
|
|
2829
|
-
return syncPluginsToClient(this.id, plugins);
|
|
2830
|
-
}
|
|
2831
|
-
syncSkills(skills) {
|
|
2832
|
-
projectSkillsToDir(path18.join(homeDir(), ".hermes", "skills"), skills, "dir");
|
|
2833
|
-
}
|
|
2834
|
-
scanForImports(_config, managedIndex) {
|
|
2835
|
-
const configPath = this.detectConfigPath();
|
|
2836
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
2837
|
-
if (!configPath || !fs12.existsSync(configPath)) {
|
|
2838
|
-
return result;
|
|
2839
|
-
}
|
|
2840
|
-
const existing = fs12.readFileSync(configPath, "utf8");
|
|
2841
|
-
const doc = YAML.parse(existing) ?? {};
|
|
2842
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
2843
|
-
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
2844
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
2845
|
-
continue;
|
|
2846
|
-
}
|
|
2847
|
-
const parsed = hermesEntrySchema.safeParse(rawEntry);
|
|
2848
|
-
if (!parsed.success) {
|
|
2849
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
2850
|
-
continue;
|
|
2851
|
-
}
|
|
2852
|
-
const entry = parsed.data;
|
|
2853
|
-
if (entry.auth === "oauth") {
|
|
2854
|
-
result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
|
|
2855
|
-
continue;
|
|
2856
|
-
}
|
|
2857
|
-
if (entry.url && !entry.command) {
|
|
2858
|
-
result.candidates.push({
|
|
2859
|
-
clientId: this.id,
|
|
2860
|
-
configPath,
|
|
2861
|
-
sourceEntryName: name,
|
|
2862
|
-
serverName: name,
|
|
2863
|
-
spec: {
|
|
2864
|
-
transport: "http",
|
|
2865
|
-
url: entry.url,
|
|
2866
|
-
headers: entry.headers,
|
|
2867
|
-
enabled: entry.enabled !== false
|
|
2868
|
-
}
|
|
2869
|
-
});
|
|
2870
|
-
continue;
|
|
2871
|
-
}
|
|
2872
|
-
if (entry.command && !entry.url) {
|
|
2873
|
-
result.candidates.push({
|
|
2874
|
-
clientId: this.id,
|
|
2875
|
-
configPath,
|
|
2876
|
-
sourceEntryName: name,
|
|
2877
|
-
serverName: name,
|
|
2878
|
-
spec: {
|
|
2879
|
-
transport: "stdio",
|
|
2880
|
-
command: entry.command,
|
|
2881
|
-
args: entry.args,
|
|
2882
|
-
env: entry.env,
|
|
2883
|
-
enabled: entry.enabled !== false
|
|
2884
|
-
}
|
|
2885
|
-
});
|
|
2886
|
-
continue;
|
|
2887
|
-
}
|
|
2888
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
2889
|
-
}
|
|
2890
|
-
return result;
|
|
2891
|
-
}
|
|
2892
|
-
syncGateway(_config, options) {
|
|
2893
|
-
const configPath = this.detectConfigPath();
|
|
2894
|
-
if (!configPath) {
|
|
2895
|
-
return errorResult(this.id, undefined, "Unable to resolve Hermes config.yaml path.");
|
|
2896
|
-
}
|
|
2897
|
-
try {
|
|
2898
|
-
const existing = fs12.existsSync(configPath) ? fs12.readFileSync(configPath, "utf8") : "";
|
|
2899
|
-
const doc = existing.trim().length > 0 ? YAML.parse(existing) ?? {} : {};
|
|
2900
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
2901
|
-
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
2902
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
2903
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
2904
|
-
url: entry.url,
|
|
2905
|
-
headers: entry.headers,
|
|
2906
|
-
enabled: entry.enabled
|
|
2907
|
-
}]));
|
|
2908
|
-
for (const name of managedNames) {
|
|
2909
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
2910
|
-
if (conflict) {
|
|
2911
|
-
return errorResult(this.id, configPath, conflict);
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
|
|
2915
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
2916
|
-
mcpServers[name] = entry;
|
|
2917
|
-
}
|
|
2918
|
-
doc.mcp_servers = mcpServers;
|
|
2919
|
-
fs12.mkdirSync(path18.dirname(configPath), { recursive: true });
|
|
2920
|
-
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
2921
|
-
fs12.writeFileSync(tmpPath, YAML.stringify(doc), { mode: 384 });
|
|
2922
|
-
fs12.renameSync(tmpPath, configPath);
|
|
2923
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
2924
|
-
return okResult(this.id, configPath);
|
|
2925
|
-
} catch (error) {
|
|
2926
|
-
return errorResult(this.id, configPath, error.message);
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
// src/adapters/index.ts
|
|
2932
|
-
function getAdapters() {
|
|
2933
|
-
return [
|
|
2934
|
-
new ClaudeAdapter,
|
|
2935
|
-
new ClaudeDesktopAdapter,
|
|
2936
|
-
new CodexAdapter,
|
|
2937
|
-
new CursorAdapter,
|
|
2938
|
-
new ClineAdapter,
|
|
2939
|
-
new OpenCodeAdapter,
|
|
2940
|
-
new KiroAdapter,
|
|
2941
|
-
new VsCodeAdapter,
|
|
2942
|
-
new QwenAdapter,
|
|
2943
|
-
new OpenClawAdapter,
|
|
2944
|
-
new HermesAdapter
|
|
2945
|
-
];
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
// src/core/managed-index.ts
|
|
2949
|
-
function defaultManagedIndex() {
|
|
2950
|
-
return {
|
|
2951
|
-
schemaVersion: 1,
|
|
2952
|
-
managed: {}
|
|
2953
|
-
};
|
|
2954
|
-
}
|
|
2955
|
-
function loadManagedIndex(filePath = getManagedIndexPath()) {
|
|
2956
|
-
const raw = readJsonFile(filePath, defaultManagedIndex());
|
|
2957
|
-
if (!raw || typeof raw !== "object") {
|
|
2958
|
-
return defaultManagedIndex();
|
|
2959
|
-
}
|
|
2960
|
-
if (raw.schemaVersion !== 1) {
|
|
2961
|
-
return defaultManagedIndex();
|
|
2962
|
-
}
|
|
2963
|
-
return raw;
|
|
2964
|
-
}
|
|
2965
|
-
function saveManagedIndex(index, filePath = getManagedIndexPath()) {
|
|
2966
|
-
writeJsonAtomic(filePath, index);
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
|
-
// src/core/skills.ts
|
|
2970
|
-
import fs13 from "node:fs";
|
|
2971
|
-
import path19 from "node:path";
|
|
2972
|
-
function listSkills() {
|
|
2973
|
-
const skillsDir = getSkillsDir();
|
|
2974
|
-
if (!fs13.existsSync(skillsDir)) {
|
|
2975
|
-
return [];
|
|
2976
|
-
}
|
|
2977
|
-
const files = fs13.readdirSync(skillsDir);
|
|
2978
|
-
const skills = [];
|
|
2979
|
-
for (const file of files) {
|
|
2980
|
-
if (file.endsWith(".md")) {
|
|
2981
|
-
const id = path19.basename(file, ".md");
|
|
2982
|
-
try {
|
|
2983
|
-
const content = fs13.readFileSync(path19.join(skillsDir, file), "utf-8");
|
|
2984
|
-
skills.push({ id, content });
|
|
2985
|
-
} catch {
|
|
2986
|
-
console.error(`[mcpx] Warning: could not read skill ${file}`);
|
|
2987
|
-
}
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
return skills;
|
|
2991
|
-
}
|
|
2992
|
-
function getSkill(id) {
|
|
2993
|
-
validateSkillId(id);
|
|
2994
|
-
const skillsDir = getSkillsDir();
|
|
2995
|
-
const filePath = path19.join(skillsDir, `${id}.md`);
|
|
2996
|
-
if (!fs13.existsSync(filePath)) {
|
|
2997
|
-
return null;
|
|
2998
|
-
}
|
|
2999
|
-
const content = fs13.readFileSync(filePath, "utf-8");
|
|
3000
|
-
return { id, content };
|
|
3001
|
-
}
|
|
3002
|
-
function saveSkill(id, content) {
|
|
3003
|
-
validateSkillId(id);
|
|
3004
|
-
const skillsDir = getSkillsDir();
|
|
3005
|
-
ensureDir(skillsDir);
|
|
3006
|
-
const filePath = path19.join(skillsDir, `${id}.md`);
|
|
3007
|
-
fs13.writeFileSync(filePath, content, "utf-8");
|
|
3008
|
-
}
|
|
3009
|
-
function deleteSkill(id) {
|
|
3010
|
-
validateSkillId(id);
|
|
3011
|
-
const skillsDir = getSkillsDir();
|
|
3012
|
-
const filePath = path19.join(skillsDir, `${id}.md`);
|
|
3013
|
-
if (fs13.existsSync(filePath)) {
|
|
3014
|
-
fs13.unlinkSync(filePath);
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
// src/core/managed-index-lock.ts
|
|
3019
|
-
import fs14 from "node:fs";
|
|
3020
|
-
import path20 from "node:path";
|
|
3021
|
-
function isStale(lockPath, maxAgeMs = 5000) {
|
|
3022
|
-
try {
|
|
3023
|
-
const stat = fs14.statSync(lockPath);
|
|
3024
|
-
return Date.now() - stat.mtimeMs > maxAgeMs;
|
|
3025
|
-
} catch {
|
|
3026
|
-
return true;
|
|
3027
|
-
}
|
|
3028
|
-
}
|
|
3029
|
-
function withManagedIndexLock(lockPath, fn) {
|
|
3030
|
-
fs14.mkdirSync(path20.dirname(lockPath), { recursive: true });
|
|
3031
|
-
const deadline = Date.now() + 5000;
|
|
3032
|
-
while (true) {
|
|
3033
|
-
try {
|
|
3034
|
-
fs14.writeFileSync(lockPath, `${process.pid}
|
|
3035
|
-
`, { flag: "wx" });
|
|
3036
|
-
break;
|
|
3037
|
-
} catch {
|
|
3038
|
-
if (isStale(lockPath)) {
|
|
3039
|
-
try {
|
|
3040
|
-
fs14.unlinkSync(lockPath);
|
|
3041
|
-
} catch {}
|
|
3042
|
-
continue;
|
|
3043
|
-
}
|
|
3044
|
-
if (Date.now() > deadline) {
|
|
3045
|
-
throw new Error(`Timed out waiting for managed-index lock: ${lockPath}`);
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
try {
|
|
3050
|
-
return fn();
|
|
3051
|
-
} finally {
|
|
3052
|
-
try {
|
|
3053
|
-
fs14.unlinkSync(lockPath);
|
|
3054
|
-
} catch {}
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
// src/core/sync.ts
|
|
3059
|
-
function getGatewayUrl(config) {
|
|
3060
|
-
return `http://127.0.0.1:${config.gateway.port}/mcp`;
|
|
3061
|
-
}
|
|
3062
|
-
function getGatewayInternalUrl(config, pathname) {
|
|
3063
|
-
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
3064
|
-
return `http://127.0.0.1:${config.gateway.port}${normalizedPath}`;
|
|
3065
|
-
}
|
|
3066
|
-
function buildProjectScopes(config) {
|
|
3067
|
-
const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
|
|
3068
|
-
const pluginDisabledByProject = new Map;
|
|
3069
|
-
for (const [, plugin] of Object.entries(config.plugins ?? {})) {
|
|
3070
|
-
if (!plugin.projectOverrides)
|
|
3071
|
-
continue;
|
|
3072
|
-
for (const [overridePath, override] of Object.entries(plugin.projectOverrides)) {
|
|
3073
|
-
const disabled = override.enabled === false || override.components?.mcpServers === false;
|
|
3074
|
-
if (!disabled)
|
|
3075
|
-
continue;
|
|
3076
|
-
if (!pluginDisabledByProject.has(overridePath)) {
|
|
3077
|
-
pluginDisabledByProject.set(overridePath, new Set);
|
|
3078
|
-
}
|
|
3079
|
-
for (const serverName of plugin.serverNames) {
|
|
3080
|
-
pluginDisabledByProject.get(overridePath).add(managedGatewayEntryName(serverName));
|
|
3081
|
-
}
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
return Object.values(config.projects ?? {}).map((project) => {
|
|
3085
|
-
const projectPath = project.path;
|
|
3086
|
-
const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
|
|
3087
|
-
const pluginDisabled = pluginDisabledByProject.get(projectPath);
|
|
3088
|
-
if (pluginDisabled) {
|
|
3089
|
-
for (const name of pluginDisabled) {
|
|
3090
|
-
if (!disabledServerNames.includes(name)) {
|
|
3091
|
-
disabledServerNames.push(name);
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
return { path: projectPath, disabledServerNames };
|
|
3096
|
-
});
|
|
3097
|
-
}
|
|
3098
|
-
function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
3099
|
-
return Object.entries(config.servers).map(([name, spec]) => ({
|
|
3100
|
-
name: `${name} (mcpx)`,
|
|
3101
|
-
url: `${gatewayUrl}?upstream=${encodeURIComponent(name)}`,
|
|
3102
|
-
headers: { Authorization: `Bearer ${localToken}` },
|
|
3103
|
-
enabled: isServerEnabled(spec)
|
|
3104
|
-
}));
|
|
3105
|
-
}
|
|
3106
|
-
function isClientInternalServerCommand(candidate) {
|
|
3107
|
-
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
3108
|
-
}
|
|
3109
|
-
function getFilteredAdapters(targetClients) {
|
|
3110
|
-
const adapters = getAdapters();
|
|
3111
|
-
if (targetClients && targetClients.length > 0) {
|
|
3112
|
-
return adapters.filter((a) => targetClients.includes(a.id));
|
|
3113
|
-
}
|
|
3114
|
-
return adapters;
|
|
3115
|
-
}
|
|
3116
|
-
function runImportPhase(adapters, config, managedIndex) {
|
|
3117
|
-
const imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
|
|
3118
|
-
const sourceEntriesToRemove = new Map;
|
|
3119
|
-
const phaseErrors = [];
|
|
3120
|
-
for (const adapter of adapters) {
|
|
3121
|
-
try {
|
|
3122
|
-
const scan = adapter.scanForImports(config, managedIndex);
|
|
3123
|
-
imports.skipped.push(...scan.skipped);
|
|
3124
|
-
for (const candidate of scan.candidates) {
|
|
3125
|
-
if (isClientInternalServerCommand(candidate)) {
|
|
3126
|
-
imports.skipped.push({
|
|
3127
|
-
clientId: candidate.clientId,
|
|
3128
|
-
configPath: candidate.configPath,
|
|
3129
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
3130
|
-
serverName: candidate.serverName,
|
|
3131
|
-
reason: "client-internal server"
|
|
3132
|
-
});
|
|
3133
|
-
continue;
|
|
3134
|
-
}
|
|
3135
|
-
const existing = config.servers[candidate.serverName];
|
|
3136
|
-
if (!existing) {
|
|
3137
|
-
config.servers[candidate.serverName] = candidate.spec;
|
|
3138
|
-
imports.imported.push({
|
|
3139
|
-
clientId: candidate.clientId,
|
|
3140
|
-
configPath: candidate.configPath,
|
|
3141
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
3142
|
-
serverName: candidate.serverName
|
|
3143
|
-
});
|
|
3144
|
-
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
3145
|
-
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
3146
|
-
}
|
|
3147
|
-
sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
|
|
3148
|
-
continue;
|
|
3149
|
-
}
|
|
3150
|
-
if (serverSpecsEqual(existing, candidate.spec)) {
|
|
3151
|
-
imports.duplicates.push({
|
|
3152
|
-
clientId: candidate.clientId,
|
|
3153
|
-
configPath: candidate.configPath,
|
|
3154
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
3155
|
-
serverName: candidate.serverName
|
|
3156
|
-
});
|
|
3157
|
-
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
3158
|
-
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
3159
|
-
}
|
|
3160
|
-
sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
|
|
3161
|
-
continue;
|
|
3162
|
-
}
|
|
3163
|
-
imports.conflicts.push({
|
|
3164
|
-
clientId: candidate.clientId,
|
|
3165
|
-
configPath: candidate.configPath,
|
|
3166
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
3167
|
-
serverName: candidate.serverName,
|
|
3168
|
-
message: `Server "${candidate.serverName}" already exists in mcpx with a different configuration.`
|
|
3169
|
-
});
|
|
3170
|
-
}
|
|
3171
|
-
} catch (error) {
|
|
3172
|
-
imports.errors.push({
|
|
3173
|
-
clientId: adapter.id,
|
|
3174
|
-
configPath: adapter.detectConfigPath() ?? undefined,
|
|
3175
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3176
|
-
});
|
|
3177
|
-
phaseErrors.push({
|
|
3178
|
-
clientId: adapter.id,
|
|
3179
|
-
phase: "import",
|
|
3180
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3181
|
-
});
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
return { imports, sourceEntriesToRemove, phaseErrors };
|
|
3185
|
-
}
|
|
3186
|
-
function runSkillsPhase(adapters, skills) {
|
|
3187
|
-
const phaseErrors = [];
|
|
3188
|
-
for (const adapter of adapters) {
|
|
3189
|
-
if (adapter.syncSkills) {
|
|
3190
|
-
try {
|
|
3191
|
-
adapter.syncSkills(skills);
|
|
3192
|
-
} catch (error) {
|
|
3193
|
-
phaseErrors.push({
|
|
3194
|
-
clientId: adapter.id,
|
|
3195
|
-
phase: "skills",
|
|
3196
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3197
|
-
});
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
return phaseErrors;
|
|
3202
|
-
}
|
|
3203
|
-
function runPluginsPhase(adapters, pluginInputs) {
|
|
3204
|
-
const pluginResults = [];
|
|
3205
|
-
const phaseErrors = [];
|
|
3206
|
-
for (const adapter of adapters) {
|
|
3207
|
-
if (adapter.syncPlugins) {
|
|
3208
|
-
try {
|
|
3209
|
-
const result = adapter.syncPlugins(pluginInputs);
|
|
3210
|
-
if (result)
|
|
3211
|
-
pluginResults.push(result);
|
|
3212
|
-
} catch (error) {
|
|
3213
|
-
phaseErrors.push({
|
|
3214
|
-
clientId: adapter.id,
|
|
3215
|
-
phase: "plugins",
|
|
3216
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3217
|
-
});
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
return { pluginResults, phaseErrors };
|
|
3222
|
-
}
|
|
3223
|
-
function runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, projectScopes) {
|
|
3224
|
-
const results = [];
|
|
3225
|
-
const phaseErrors = [];
|
|
3226
|
-
for (const adapter of adapters) {
|
|
3227
|
-
if (!adapter.supportsHttp()) {
|
|
3228
|
-
results.push({
|
|
3229
|
-
clientId: adapter.id,
|
|
3230
|
-
status: "UNSUPPORTED_HTTP",
|
|
3231
|
-
message: "Client adapter does not support MCP over HTTP."
|
|
3232
|
-
});
|
|
3233
|
-
continue;
|
|
3234
|
-
}
|
|
3235
|
-
try {
|
|
3236
|
-
const result = adapter.syncGateway(config, {
|
|
3237
|
-
managedEntries,
|
|
3238
|
-
managedIndex,
|
|
3239
|
-
managedIndexPath,
|
|
3240
|
-
sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
|
|
3241
|
-
projectScopes
|
|
3242
|
-
});
|
|
3243
|
-
results.push(result);
|
|
3244
|
-
} catch (error) {
|
|
3245
|
-
results.push({
|
|
3246
|
-
clientId: adapter.id,
|
|
3247
|
-
status: "ERROR",
|
|
3248
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3249
|
-
});
|
|
3250
|
-
phaseErrors.push({
|
|
3251
|
-
clientId: adapter.id,
|
|
3252
|
-
phase: "gateway",
|
|
3253
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3254
|
-
});
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
return { results, phaseErrors };
|
|
3258
|
-
}
|
|
3259
|
-
function syncAllClients(config, secrets, options) {
|
|
3260
|
-
const opts = Array.isArray(options) ? { targetClients: options, importScan: false } : { importScan: false, ...options };
|
|
3261
|
-
const managedIndexPath = getManagedIndexPath();
|
|
3262
|
-
return withManagedIndexLock(`${managedIndexPath}.lock`, () => {
|
|
3263
|
-
const managedIndex = loadManagedIndex(managedIndexPath);
|
|
3264
|
-
const adapters = getFilteredAdapters(opts.targetClients);
|
|
3265
|
-
const allPhaseErrors = [];
|
|
3266
|
-
let imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
|
|
3267
|
-
let sourceEntriesToRemove = new Map;
|
|
3268
|
-
if (opts.importScan) {
|
|
3269
|
-
const importResult = runImportPhase(adapters, config, managedIndex);
|
|
3270
|
-
imports = importResult.imports;
|
|
3271
|
-
sourceEntriesToRemove = importResult.sourceEntriesToRemove;
|
|
3272
|
-
allPhaseErrors.push(...importResult.phaseErrors ?? []);
|
|
3273
|
-
}
|
|
3274
|
-
const localToken = ensureGatewayToken(config, secrets);
|
|
3275
|
-
const gatewayUrl = getGatewayUrl(config);
|
|
3276
|
-
const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
|
|
3277
|
-
const gatewayResult = runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, buildProjectScopes(config));
|
|
3278
|
-
const results = gatewayResult.results;
|
|
3279
|
-
allPhaseErrors.push(...gatewayResult.phaseErrors ?? []);
|
|
3280
|
-
const skills = listSkills();
|
|
3281
|
-
const skillsErrors = runSkillsPhase(adapters, skills);
|
|
3282
|
-
allPhaseErrors.push(...skillsErrors ?? []);
|
|
3283
|
-
const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
|
|
3284
|
-
pluginId: p.id,
|
|
3285
|
-
pluginName: p.name,
|
|
3286
|
-
pluginRoot: p.root,
|
|
3287
|
-
components: p.components,
|
|
3288
|
-
approvals: p.approvals ?? {},
|
|
3289
|
-
enabled: p.enabled,
|
|
3290
|
-
serverNames: p.serverNames,
|
|
3291
|
-
skills: p.discovered.skills,
|
|
3292
|
-
commands: p.discovered.commands,
|
|
3293
|
-
agents: p.discovered.agents,
|
|
3294
|
-
hooks: p.discovered.hooks
|
|
3295
|
-
}));
|
|
3296
|
-
const pluginResult = runPluginsPhase(adapters, pluginInputs);
|
|
3297
|
-
const pluginResults = pluginResult.pluginResults;
|
|
3298
|
-
allPhaseErrors.push(...pluginResult.phaseErrors ?? []);
|
|
3299
|
-
saveManagedIndex(managedIndex, managedIndexPath);
|
|
3300
|
-
const clientStates = {};
|
|
3301
|
-
for (const result of results) {
|
|
3302
|
-
clientStates[result.clientId] = {
|
|
3303
|
-
status: result.status,
|
|
3304
|
-
configPath: result.configPath,
|
|
3305
|
-
message: result.message,
|
|
3306
|
-
lastSyncAt: new Date().toISOString()
|
|
3307
|
-
};
|
|
3308
|
-
}
|
|
3309
|
-
const hasErrors = results.some((r) => r.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
|
|
3310
|
-
return {
|
|
3311
|
-
gatewayUrl,
|
|
3312
|
-
imports,
|
|
3313
|
-
results,
|
|
3314
|
-
hasErrors,
|
|
3315
|
-
clientStates,
|
|
3316
|
-
phaseErrors: allPhaseErrors.length > 0 ? allPhaseErrors : undefined,
|
|
3317
|
-
pluginResults: pluginResults.length > 0 ? pluginResults : undefined
|
|
3318
|
-
};
|
|
3319
|
-
});
|
|
3320
|
-
}
|
|
3321
|
-
function persistSyncState(summary, config) {
|
|
3322
|
-
if (!summary.clientStates)
|
|
3323
|
-
return;
|
|
3324
|
-
for (const [clientId, state] of Object.entries(summary.clientStates)) {
|
|
3325
|
-
config.clients[clientId] = state;
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
// src/core/plugin-manager.ts
|
|
3330
|
-
class PluginManager {
|
|
3331
|
-
configPath;
|
|
3332
|
-
cache;
|
|
3333
|
-
dataManager;
|
|
3334
|
-
lifecycle;
|
|
3335
|
-
secrets;
|
|
3336
|
-
constructor(configPath) {
|
|
3337
|
-
this.configPath = configPath || getConfigPath();
|
|
3338
|
-
this.cache = new PluginCache;
|
|
3339
|
-
this.dataManager = new PluginDataManager;
|
|
3340
|
-
this.lifecycle = new PluginLifecycle;
|
|
3341
|
-
this.secrets = new SecretsManager;
|
|
3342
|
-
}
|
|
3343
|
-
async inspectSource(sourceStr) {
|
|
3344
|
-
const parsed = parseSource(sourceStr);
|
|
3345
|
-
return this.inspectResolvedSource(parsed, sourceStr);
|
|
3346
|
-
}
|
|
3347
|
-
async inspectResolvedSource(parsed, sourceLabel = parsed.original) {
|
|
3348
|
-
const sha = parsed.resolvedSha || await this.cache.resolveSha(parsed);
|
|
3349
|
-
parsed.resolvedSha = sha;
|
|
3350
|
-
const cached = await this.cache.fetch(parsed, "");
|
|
3351
|
-
let pluginRoot = cached.root;
|
|
3352
|
-
if (!hasManifest(pluginRoot)) {
|
|
3353
|
-
const entries = fs15.readdirSync(pluginRoot, { withFileTypes: true });
|
|
3354
|
-
for (const entry of entries) {
|
|
3355
|
-
if (entry.isDirectory()) {
|
|
3356
|
-
const candidate = path21.join(pluginRoot, entry.name);
|
|
3357
|
-
if (hasManifest(candidate)) {
|
|
3358
|
-
pluginRoot = candidate;
|
|
3359
|
-
break;
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
const manifest = readManifest(pluginRoot);
|
|
3365
|
-
const components = discoverComponents(pluginRoot);
|
|
3366
|
-
return { manifest, components, root: pluginRoot, source: sourceLabel, sha: cached.sha };
|
|
3367
|
-
}
|
|
3368
|
-
async installPlugin(sourceStr, options) {
|
|
3369
|
-
return this.installResolvedPlugin(parseSource(sourceStr), options, sourceStr);
|
|
3370
|
-
}
|
|
3371
|
-
async installResolvedPlugin(source, options, sourceLabel = source.original) {
|
|
3372
|
-
const info = await this.inspectResolvedSource(source, sourceLabel);
|
|
3373
|
-
const pluginName = normalizePluginName(options?.name || info.manifest?.name || path21.basename(info.root));
|
|
3374
|
-
const pluginId = `${pluginName}@${info.sha.slice(0, 8)}`;
|
|
3375
|
-
const dataDir = path21.join(getPluginDataRoot(), pluginId);
|
|
3376
|
-
ensureDir(dataDir);
|
|
3377
|
-
const enabled = options?.enabled ?? true;
|
|
3378
|
-
const discovered = info.components;
|
|
3379
|
-
const serverNames = discovered.mcpServers.map((s) => `${pluginName}__${s.id}`);
|
|
3380
|
-
const plugin = {
|
|
3381
|
-
id: pluginId,
|
|
3382
|
-
name: pluginName,
|
|
3383
|
-
source: sourceLabel,
|
|
3384
|
-
version: info.manifest?.version || "0.0.0",
|
|
3385
|
-
ref: source.ref || "latest",
|
|
3386
|
-
resolvedSha: info.sha,
|
|
3387
|
-
installedAt: new Date().toISOString(),
|
|
3388
|
-
root: info.root,
|
|
3389
|
-
dataDir,
|
|
3390
|
-
components: {
|
|
3391
|
-
mcpServers: options?.components?.mcpServers ?? discovered.mcpServers.length > 0,
|
|
3392
|
-
skills: options?.components?.skills ?? discovered.skills.length > 0,
|
|
3393
|
-
hooks: options?.components?.hooks ?? discovered.hooks.length > 0,
|
|
3394
|
-
agents: options?.components?.agents ?? discovered.agents.length > 0,
|
|
3395
|
-
commands: options?.components?.commands ?? discovered.commands.length > 0
|
|
3396
|
-
},
|
|
3397
|
-
discovered,
|
|
3398
|
-
enabled,
|
|
3399
|
-
status: "healthy",
|
|
3400
|
-
serverNames,
|
|
3401
|
-
projectedClients: [],
|
|
3402
|
-
approvals: options?.approvals || {},
|
|
3403
|
-
marketplace: options?.marketplace
|
|
3404
|
-
};
|
|
3405
|
-
await mutateConfig((config) => {
|
|
3406
|
-
if (!config.plugins)
|
|
3407
|
-
config.plugins = {};
|
|
3408
|
-
if (config.plugins[pluginId]) {
|
|
3409
|
-
throw new Error(`Plugin ${pluginId} is already installed`);
|
|
3410
|
-
}
|
|
3411
|
-
for (const existing of Object.values(config.plugins)) {
|
|
3412
|
-
for (const sn of serverNames) {
|
|
3413
|
-
if (existing.serverNames.includes(sn)) {
|
|
3414
|
-
throw new Error(`Server name collision: "${sn}" is already claimed by plugin "${existing.name}". Use --name to specify a different plugin name.`);
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
for (const sn of serverNames) {
|
|
3419
|
-
if (config.servers?.[sn]) {
|
|
3420
|
-
throw new Error(`Server name collision: "${sn}" is already registered as a server. Remove that server or use --name to choose a different plugin name.`);
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
config.plugins[pluginId] = plugin;
|
|
3424
|
-
for (const mcp of discovered.mcpServers) {
|
|
3425
|
-
const nsName2 = `${pluginName}__${mcp.id}`;
|
|
3426
|
-
const approved = plugin.approvals?.mcpServers !== false;
|
|
3427
|
-
config.servers[nsName2] = mcp.transport === "http" && mcp.url ? { transport: "http", url: new URL(mcp.url).toString(), enabled: enabled && approved } : {
|
|
3428
|
-
transport: "stdio",
|
|
3429
|
-
command: "npx",
|
|
3430
|
-
args: ["-y", "@kwonye/mcpx@latest", "plugin-host", pluginId, mcp.id],
|
|
3431
|
-
enabled: enabled && approved
|
|
3432
|
-
};
|
|
3433
|
-
}
|
|
3434
|
-
}, this.configPath);
|
|
3435
|
-
this.lifecycle.initData(pluginId);
|
|
3436
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3437
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3438
|
-
await mutateConfig((freshConfig) => {
|
|
3439
|
-
persistSyncState(summary, freshConfig);
|
|
3440
|
-
}, this.configPath);
|
|
3441
|
-
return plugin;
|
|
3442
|
-
}
|
|
3443
|
-
async updatePlugin(pluginId) {
|
|
3444
|
-
const current = loadConfig(this.configPath).plugins?.[pluginId];
|
|
3445
|
-
if (!current)
|
|
3446
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3447
|
-
const info = await this.inspectSource(current.source);
|
|
3448
|
-
return this.applyPluginUpdate(pluginId, info, undefined, true);
|
|
3449
|
-
}
|
|
3450
|
-
async updateResolvedPlugin(pluginId, source, sourceFingerprint, sync = true) {
|
|
3451
|
-
const info = await this.inspectResolvedSource(source);
|
|
3452
|
-
return this.applyPluginUpdate(pluginId, info, sourceFingerprint, sync);
|
|
3453
|
-
}
|
|
3454
|
-
async applyPluginUpdate(pluginId, info, sourceFingerprint, sync = true) {
|
|
3455
|
-
const currentSnapshot = loadConfig(this.configPath).plugins?.[pluginId];
|
|
3456
|
-
if (!currentSnapshot)
|
|
3457
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3458
|
-
const pluginName = normalizePluginName(info.manifest?.name || currentSnapshot.name);
|
|
3459
|
-
let updatedPlugin;
|
|
3460
|
-
await mutateConfig((config) => {
|
|
3461
|
-
if (!config.plugins)
|
|
3462
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3463
|
-
const current = config.plugins[pluginId];
|
|
3464
|
-
if (!current)
|
|
3465
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3466
|
-
for (const oldName of current.serverNames) {
|
|
3467
|
-
delete config.servers[oldName];
|
|
3468
|
-
}
|
|
3469
|
-
current.version = info.manifest?.version || current.version;
|
|
3470
|
-
current.resolvedSha = info.sha;
|
|
3471
|
-
current.root = info.root;
|
|
3472
|
-
current.discovered = info.components;
|
|
3473
|
-
current.serverNames = info.components.mcpServers.map((s) => `${pluginName}__${s.id}`);
|
|
3474
|
-
const requiresApproval = !current.marketplace && info.components.mcpServers.length > 0;
|
|
3475
|
-
const approved = !requiresApproval && current.approvals?.mcpServers !== false;
|
|
3476
|
-
for (const mcp of info.components.mcpServers) {
|
|
3477
|
-
const nsName2 = `${pluginName}__${mcp.id}`;
|
|
3478
|
-
config.servers[nsName2] = mcp.transport === "http" && mcp.url ? { transport: "http", url: new URL(mcp.url).toString(), enabled: current.enabled && approved } : {
|
|
3479
|
-
transport: "stdio",
|
|
3480
|
-
command: "npx",
|
|
3481
|
-
args: ["-y", "@kwonye/mcpx@latest", "plugin-host", pluginId, mcp.id],
|
|
3482
|
-
enabled: current.enabled && approved
|
|
3483
|
-
};
|
|
3484
|
-
}
|
|
3485
|
-
current.approvals = requiresApproval ? { mcpServers: false } : {};
|
|
3486
|
-
if (sourceFingerprint && current.marketplace)
|
|
3487
|
-
current.marketplace.sourceFingerprint = sourceFingerprint;
|
|
3488
|
-
delete current.updateError;
|
|
3489
|
-
config.plugins[pluginId] = current;
|
|
3490
|
-
updatedPlugin = current;
|
|
3491
|
-
}, this.configPath);
|
|
3492
|
-
if (sync) {
|
|
3493
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3494
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3495
|
-
await mutateConfig((freshConfig) => {
|
|
3496
|
-
persistSyncState(summary, freshConfig);
|
|
3497
|
-
}, this.configPath);
|
|
3498
|
-
}
|
|
3499
|
-
return updatedPlugin;
|
|
3500
|
-
}
|
|
3501
|
-
async uninstallPlugin(pluginId, keepData = false) {
|
|
3502
|
-
await mutateConfig((config) => {
|
|
3503
|
-
if (!config.plugins)
|
|
3504
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3505
|
-
const plugin = config.plugins[pluginId];
|
|
3506
|
-
if (!plugin)
|
|
3507
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3508
|
-
for (const name of plugin.serverNames) {
|
|
3509
|
-
delete config.servers[name];
|
|
3510
|
-
}
|
|
3511
|
-
delete config.plugins[pluginId];
|
|
3512
|
-
}, this.configPath);
|
|
3513
|
-
if (!keepData) {
|
|
3514
|
-
await this.lifecycle.removeData(pluginId);
|
|
3515
|
-
}
|
|
3516
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3517
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3518
|
-
await mutateConfig((freshConfig) => {
|
|
3519
|
-
persistSyncState(summary, freshConfig);
|
|
3520
|
-
}, this.configPath);
|
|
3521
|
-
}
|
|
3522
|
-
async enablePlugin(pluginId) {
|
|
3523
|
-
await mutateConfig((config) => {
|
|
3524
|
-
if (!config.plugins)
|
|
3525
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3526
|
-
const plugin = config.plugins[pluginId];
|
|
3527
|
-
if (!plugin)
|
|
3528
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3529
|
-
plugin.enabled = true;
|
|
3530
|
-
for (const name of plugin.serverNames) {
|
|
3531
|
-
const server = config.servers[name];
|
|
3532
|
-
if (server)
|
|
3533
|
-
server.enabled = plugin.approvals?.mcpServers !== false;
|
|
3534
|
-
}
|
|
3535
|
-
}, this.configPath);
|
|
3536
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3537
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3538
|
-
await mutateConfig((freshConfig) => {
|
|
3539
|
-
persistSyncState(summary, freshConfig);
|
|
3540
|
-
}, this.configPath);
|
|
3541
|
-
}
|
|
3542
|
-
async disablePlugin(pluginId) {
|
|
3543
|
-
await mutateConfig((config) => {
|
|
3544
|
-
if (!config.plugins)
|
|
3545
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3546
|
-
const plugin = config.plugins[pluginId];
|
|
3547
|
-
if (!plugin)
|
|
3548
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3549
|
-
plugin.enabled = false;
|
|
3550
|
-
for (const name of plugin.serverNames) {
|
|
3551
|
-
const server = config.servers[name];
|
|
3552
|
-
if (server)
|
|
3553
|
-
server.enabled = false;
|
|
3554
|
-
}
|
|
3555
|
-
}, this.configPath);
|
|
3556
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3557
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3558
|
-
await mutateConfig((freshConfig) => {
|
|
3559
|
-
persistSyncState(summary, freshConfig);
|
|
3560
|
-
}, this.configPath);
|
|
3561
|
-
}
|
|
3562
|
-
async approveComponent(pluginId, component) {
|
|
3563
|
-
await mutateConfig((config) => {
|
|
3564
|
-
if (!config.plugins)
|
|
3565
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3566
|
-
const plugin = config.plugins[pluginId];
|
|
3567
|
-
if (!plugin)
|
|
3568
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3569
|
-
if (!plugin.approvals)
|
|
3570
|
-
plugin.approvals = {};
|
|
3571
|
-
plugin.approvals[component] = true;
|
|
3572
|
-
if (component === "mcpServers") {
|
|
3573
|
-
for (const name of plugin.serverNames) {
|
|
3574
|
-
if (config.servers[name]) {
|
|
3575
|
-
config.servers[name].enabled = plugin.enabled;
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
}, this.configPath);
|
|
3580
|
-
const syncConfig = loadConfig(this.configPath);
|
|
3581
|
-
const summary = syncAllClients(syncConfig, this.secrets);
|
|
3582
|
-
await mutateConfig((freshConfig) => {
|
|
3583
|
-
persistSyncState(summary, freshConfig);
|
|
3584
|
-
}, this.configPath);
|
|
3585
|
-
}
|
|
3586
|
-
async getPluginStatus(pluginId) {
|
|
3587
|
-
const config = loadConfig(this.configPath);
|
|
3588
|
-
return config.plugins?.[pluginId] || null;
|
|
3589
|
-
}
|
|
3590
|
-
async listPlugins() {
|
|
3591
|
-
const config = loadConfig(this.configPath);
|
|
3592
|
-
return Object.values(config.plugins || {});
|
|
3593
|
-
}
|
|
3594
|
-
async preparePlugin(pluginId) {
|
|
3595
|
-
const config = loadConfig(this.configPath);
|
|
3596
|
-
if (!config.plugins)
|
|
3597
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3598
|
-
const resolvedId = resolvePluginId(config, pluginId);
|
|
3599
|
-
const plugin = config.plugins[resolvedId];
|
|
3600
|
-
if (!plugin)
|
|
3601
|
-
throw new Error(`Plugin ${pluginId} not found`);
|
|
3602
|
-
await this.lifecycle.prepareDependencies(resolvedId, plugin.root);
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
async function inspectPlugin(source) {
|
|
3606
|
-
const manager = new PluginManager;
|
|
3607
|
-
return manager.inspectSource(source);
|
|
3608
|
-
}
|
|
3609
|
-
async function installPlugin(source, options) {
|
|
3610
|
-
const manager = new PluginManager;
|
|
3611
|
-
return manager.installPlugin(source, options);
|
|
3612
|
-
}
|
|
3613
|
-
async function updatePlugin(nameOrId) {
|
|
3614
|
-
const manager = new PluginManager;
|
|
3615
|
-
const config = loadConfig(manager["configPath"]);
|
|
3616
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3617
|
-
if (config.plugins?.[id]?.marketplace) {
|
|
3618
|
-
const { updateMarketplaceInstalledPlugin } = await import("./chunk-02r7k4ax.js");
|
|
3619
|
-
return updateMarketplaceInstalledPlugin(id);
|
|
3620
|
-
}
|
|
3621
|
-
return manager.updatePlugin(id);
|
|
3622
|
-
}
|
|
3623
|
-
async function uninstallPlugin(nameOrId, keepData) {
|
|
3624
|
-
const manager = new PluginManager;
|
|
3625
|
-
const config = loadConfig(manager["configPath"]);
|
|
3626
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3627
|
-
await manager.uninstallPlugin(id, keepData);
|
|
3628
|
-
}
|
|
3629
|
-
async function enablePlugin(nameOrId) {
|
|
3630
|
-
const manager = new PluginManager;
|
|
3631
|
-
const config = loadConfig(manager["configPath"]);
|
|
3632
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3633
|
-
await manager.enablePlugin(id);
|
|
3634
|
-
}
|
|
3635
|
-
async function disablePlugin(nameOrId) {
|
|
3636
|
-
const manager = new PluginManager;
|
|
3637
|
-
const config = loadConfig(manager["configPath"]);
|
|
3638
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3639
|
-
await manager.disablePlugin(id);
|
|
3640
|
-
}
|
|
3641
|
-
async function setPluginProjectOverride(nameOrId, projectPath, override) {
|
|
3642
|
-
await mutateConfig((config) => {
|
|
3643
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3644
|
-
const plugin = config.plugins?.[id];
|
|
3645
|
-
if (!plugin)
|
|
3646
|
-
throw new Error(`Plugin ${id} not found`);
|
|
3647
|
-
if (!config.projects?.[projectPath]) {
|
|
3648
|
-
throw new Error(`Project "${projectPath}" is not registered. Run "mcpx project init" there first.`);
|
|
3649
|
-
}
|
|
3650
|
-
if (!plugin.projectOverrides)
|
|
3651
|
-
plugin.projectOverrides = {};
|
|
3652
|
-
plugin.projectOverrides[projectPath] = { ...plugin.projectOverrides[projectPath], ...override };
|
|
3653
|
-
});
|
|
3654
|
-
const secrets = new SecretsManager;
|
|
3655
|
-
const syncConfig = loadConfig();
|
|
3656
|
-
const summary = syncAllClients(syncConfig, secrets);
|
|
3657
|
-
await mutateConfig((freshConfig) => {
|
|
3658
|
-
persistSyncState(summary, freshConfig);
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
async function resetPluginProjectOverride(nameOrId, projectPath) {
|
|
3662
|
-
await mutateConfig((config) => {
|
|
3663
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3664
|
-
const plugin = config.plugins?.[id];
|
|
3665
|
-
if (!plugin)
|
|
3666
|
-
throw new Error(`Plugin ${id} not found`);
|
|
3667
|
-
if (!config.projects?.[projectPath]) {
|
|
3668
|
-
throw new Error(`Project "${projectPath}" is not registered. Run "mcpx project init" there first.`);
|
|
3669
|
-
}
|
|
3670
|
-
if (!plugin.projectOverrides?.[projectPath])
|
|
3671
|
-
return;
|
|
3672
|
-
delete plugin.projectOverrides[projectPath];
|
|
3673
|
-
if (Object.keys(plugin.projectOverrides).length === 0) {
|
|
3674
|
-
delete plugin.projectOverrides;
|
|
3675
|
-
}
|
|
3676
|
-
});
|
|
3677
|
-
const secrets = new SecretsManager;
|
|
3678
|
-
const syncConfig = loadConfig();
|
|
3679
|
-
const summary = syncAllClients(syncConfig, secrets);
|
|
3680
|
-
await mutateConfig((freshConfig) => {
|
|
3681
|
-
persistSyncState(summary, freshConfig);
|
|
3682
|
-
});
|
|
3683
|
-
}
|
|
3684
|
-
async function approvePluginComponent(nameOrId, component) {
|
|
3685
|
-
const manager = new PluginManager;
|
|
3686
|
-
const config = loadConfig(manager["configPath"]);
|
|
3687
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3688
|
-
await manager.approveComponent(id, component);
|
|
3689
|
-
}
|
|
3690
|
-
async function getPluginStatus(nameOrId) {
|
|
3691
|
-
const manager = new PluginManager;
|
|
3692
|
-
if (nameOrId) {
|
|
3693
|
-
const config = loadConfig(manager["configPath"]);
|
|
3694
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3695
|
-
return manager.getPluginStatus(id);
|
|
3696
|
-
}
|
|
3697
|
-
const plugins = await manager.listPlugins();
|
|
3698
|
-
return plugins[0] || null;
|
|
3699
|
-
}
|
|
3700
|
-
async function listPlugins() {
|
|
3701
|
-
const manager = new PluginManager;
|
|
3702
|
-
return manager.listPlugins();
|
|
3703
|
-
}
|
|
3704
|
-
function resolvePluginId(config, nameOrId) {
|
|
3705
|
-
const plugins = config.plugins ?? {};
|
|
3706
|
-
if (plugins[nameOrId])
|
|
3707
|
-
return nameOrId;
|
|
3708
|
-
const qualifiedMatches = Object.keys(plugins).filter((id) => {
|
|
3709
|
-
const provenance = plugins[id].marketplace;
|
|
3710
|
-
return provenance && `${provenance.pluginName}@${provenance.name}` === nameOrId;
|
|
3711
|
-
});
|
|
3712
|
-
if (qualifiedMatches.length === 1)
|
|
3713
|
-
return qualifiedMatches[0];
|
|
3714
|
-
const matches = Object.keys(plugins).filter((id) => plugins[id].name === nameOrId);
|
|
3715
|
-
if (matches.length === 1)
|
|
3716
|
-
return matches[0];
|
|
3717
|
-
if (matches.length === 0) {
|
|
3718
|
-
throw new Error(`Plugin "${nameOrId}" not found. Available: ${Object.values(plugins).map((p) => `${p.name} (${p.id})`).join(", ") || "none"}`);
|
|
3719
|
-
}
|
|
3720
|
-
throw new Error(`Multiple plugins match "${nameOrId}": ${matches.join(", ")}. Use the full id (<name>@<sha8>).`);
|
|
3721
|
-
}
|
|
3722
|
-
async function pluginConfigSet(nameOrId, key, value, projectPath) {
|
|
3723
|
-
const manager = new PluginManager;
|
|
3724
|
-
const configPath = manager["configPath"];
|
|
3725
|
-
await mutateConfig((config) => {
|
|
3726
|
-
const id = resolvePluginId(config, nameOrId);
|
|
3727
|
-
const plugin = config.plugins?.[id];
|
|
3728
|
-
if (!plugin)
|
|
3729
|
-
throw new Error(`Plugin ${id} not found`);
|
|
3730
|
-
if (projectPath) {
|
|
3731
|
-
if (!plugin.projectOverrides)
|
|
3732
|
-
plugin.projectOverrides = {};
|
|
3733
|
-
if (!plugin.projectOverrides[projectPath])
|
|
3734
|
-
plugin.projectOverrides[projectPath] = {};
|
|
3735
|
-
if (!plugin.projectOverrides[projectPath].config)
|
|
3736
|
-
plugin.projectOverrides[projectPath].config = {};
|
|
3737
|
-
plugin.projectOverrides[projectPath].config[key] = value;
|
|
3738
|
-
} else {
|
|
3739
|
-
if (!plugin.config)
|
|
3740
|
-
plugin.config = {};
|
|
3741
|
-
plugin.config[key] = value;
|
|
3742
|
-
}
|
|
3743
|
-
}, configPath);
|
|
3744
|
-
}
|
|
3745
|
-
async function pluginSync() {
|
|
3746
|
-
const config = loadConfig();
|
|
3747
|
-
const secrets = new SecretsManager;
|
|
3748
|
-
const summary = syncAllClients(config, secrets);
|
|
3749
|
-
await mutateConfig((freshConfig) => {
|
|
3750
|
-
persistSyncState(summary, freshConfig);
|
|
3751
|
-
});
|
|
3752
|
-
}
|
|
3753
|
-
async function preparePlugin(name) {
|
|
3754
|
-
const manager = new PluginManager;
|
|
3755
|
-
await manager.preparePlugin(name);
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
export { mutateConfig, mutateProjectConfig, mutateActiveConfig, listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, APP_VERSION, getAdapters, loadManagedIndex, getGatewayUrl, getGatewayInternalUrl, syncAllClients, persistSyncState, PluginCache, readManifest, discoverComponents, hasManifest, resolvePluginVars, PluginDataManager, PluginLifecycle, parseSource, PluginManager, inspectPlugin, installPlugin, updatePlugin, uninstallPlugin, enablePlugin, disablePlugin, setPluginProjectOverride, resetPluginProjectOverride, approvePluginComponent, getPluginStatus, listPlugins, resolvePluginId, pluginConfigSet, pluginSync, preparePlugin };
|
|
3759
|
-
|
|
3760
|
-
//# debugId=3872A3A17F73C93764756E2164756E21
|