@kwonye/mcpx 0.1.74 → 0.1.77

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.
Files changed (47) hide show
  1. package/dist/adapters/claude-desktop.d.ts.map +1 -1
  2. package/dist/adapters/cline.d.ts.map +1 -1
  3. package/dist/adapters/cursor.d.ts.map +1 -1
  4. package/dist/adapters/kiro.d.ts +2 -1
  5. package/dist/adapters/kiro.d.ts.map +1 -1
  6. package/dist/adapters/qwen.d.ts +2 -1
  7. package/dist/adapters/qwen.d.ts.map +1 -1
  8. package/dist/adapters/utils/index.d.ts.map +1 -1
  9. package/dist/adapters/vscode.d.ts.map +1 -1
  10. package/dist/cli-7bsf6akp.js +202 -0
  11. package/dist/{cli-33pamqwn.js → cli-f16kphtq.js} +591 -456
  12. package/dist/cli-fsf33sgp.js +105 -0
  13. package/dist/{cli-ezvj44tr.js → cli-htm5cf82.js} +590 -455
  14. package/dist/{cli-q8de7zb0.js → cli-s4gspqdf.js} +4 -1
  15. package/dist/cli-s4py8ckf.js +187 -0
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +506 -85
  18. package/dist/config-kayej21q.js +20 -0
  19. package/dist/core/auth-probe.d.ts.map +1 -1
  20. package/dist/core/config.d.ts +19 -1
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/daemon.d.ts.map +1 -1
  23. package/dist/core/index.d.ts +5 -2
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +44 -12
  26. package/dist/core/paths.d.ts +3 -0
  27. package/dist/core/paths.d.ts.map +1 -1
  28. package/dist/core/proxy.d.ts +2 -0
  29. package/dist/core/proxy.d.ts.map +1 -0
  30. package/dist/core/registry-client.d.ts +76 -0
  31. package/dist/core/registry-client.d.ts.map +1 -0
  32. package/dist/core/registry.d.ts +2 -0
  33. package/dist/core/registry.d.ts.map +1 -1
  34. package/dist/core/secrets.d.ts.map +1 -1
  35. package/dist/core/skills.d.ts +6 -0
  36. package/dist/core/skills.d.ts.map +1 -0
  37. package/dist/core/status.d.ts +6 -2
  38. package/dist/core/status.d.ts.map +1 -1
  39. package/dist/core/sync.d.ts.map +1 -1
  40. package/dist/gateway/server.d.ts +17 -0
  41. package/dist/gateway/server.d.ts.map +1 -1
  42. package/dist/registry-1s14wd58.js +24 -0
  43. package/dist/types.d.ts +17 -0
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/version.d.ts +1 -1
  47. package/package.json +2 -1
@@ -1,328 +1,98 @@
1
1
  import {
2
- isServerEnabled,
3
- normalizeServerSpecEnabled
4
- } from "./cli-q8de7zb0.js";
2
+ loadMergedConfig,
3
+ saveConfig
4
+ } from "./cli-7bsf6akp.js";
5
+ import {
6
+ SecretsManager,
7
+ ensureGatewayToken,
8
+ getGatewayTokenSecretName
9
+ } from "./cli-s4py8ckf.js";
10
+ import {
11
+ ensureDir,
12
+ ensureParentDir,
13
+ getLogPath,
14
+ getManagedIndexPath,
15
+ getPidPath,
16
+ getSkillsDir,
17
+ getStagedVersionPath,
18
+ getUpdateLockPath,
19
+ getUpdatesDir,
20
+ homeDir,
21
+ readJsonFile,
22
+ sha256,
23
+ writeJsonAtomic
24
+ } from "./cli-fsf33sgp.js";
25
+ import {
26
+ isServerEnabled
27
+ } from "./cli-s4gspqdf.js";
5
28
 
6
- // src/core/paths.ts
7
- import os from "node:os";
8
- import path from "node:path";
29
+ // src/core/skills.ts
9
30
  import fs from "node:fs";
10
- function envOrDefault(value, fallback) {
11
- if (value && value.trim().length > 0) {
12
- return value;
13
- }
14
- return fallback;
15
- }
16
- function homeDir() {
17
- return process.env.HOME ?? os.homedir();
18
- }
19
- function getConfigRoot() {
20
- return envOrDefault(process.env.MCPX_CONFIG_HOME ?? process.env.XDG_CONFIG_HOME, path.join(homeDir(), ".config"));
21
- }
22
- function getDataRoot() {
23
- return envOrDefault(process.env.MCPX_DATA_HOME ?? process.env.XDG_DATA_HOME, path.join(homeDir(), ".local", "share"));
24
- }
25
- function getStateRoot() {
26
- return envOrDefault(process.env.MCPX_STATE_HOME ?? process.env.XDG_STATE_HOME, path.join(homeDir(), ".local", "state"));
27
- }
28
- function getConfigPath() {
29
- return path.join(getConfigRoot(), "mcpx", "config.json");
30
- }
31
- function getManagedIndexPath() {
32
- return path.join(getDataRoot(), "mcpx", "managed-index.json");
33
- }
34
- function getSecretNamesPath() {
35
- return path.join(getDataRoot(), "mcpx", "secret-names.json");
36
- }
37
- function getRuntimeDir() {
38
- return path.join(getStateRoot(), "mcpx", "runtime");
39
- }
40
- function getLogDir() {
41
- return path.join(getStateRoot(), "mcpx", "logs");
42
- }
43
- function getPidPath() {
44
- return path.join(getRuntimeDir(), "daemon.pid");
45
- }
46
- function getLogPath() {
47
- return path.join(getLogDir(), "daemon.log");
48
- }
49
- function getUpdatesDir() {
50
- return path.join(getDataRoot(), "mcpx", "updates");
51
- }
52
- function getStagedVersionPath() {
53
- return path.join(getUpdatesDir(), "staged-version.json");
54
- }
55
- function getUpdateLockPath() {
56
- return path.join(getUpdatesDir(), ".update.lock");
57
- }
58
- function ensureDir(dirPath) {
59
- fs.mkdirSync(dirPath, { recursive: true });
60
- }
61
- function ensureParentDir(filePath) {
62
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
- }
64
-
65
- // src/core/config.ts
66
- import { z } from "zod";
67
-
68
- // src/util/fs.ts
69
- import fs2 from "node:fs";
70
- import path2 from "node:path";
71
- import crypto from "node:crypto";
72
- function readJsonFile(filePath, fallback) {
73
- if (!fs2.existsSync(filePath)) {
74
- return fallback;
75
- }
76
- try {
77
- const raw = fs2.readFileSync(filePath, "utf8");
78
- return JSON.parse(raw);
79
- } catch {
80
- return fallback;
81
- }
82
- }
83
- function writeJsonAtomic(filePath, data) {
84
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
85
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
86
- const content = `${JSON.stringify(data, null, 2)}
87
- `;
88
- fs2.writeFileSync(tempPath, content, { mode: 384 });
89
- fs2.renameSync(tempPath, filePath);
90
- }
91
- function sha256(value) {
92
- return crypto.createHash("sha256").update(value).digest("hex");
93
- }
94
-
95
- // src/core/config.ts
96
- var clientStateSchema = z.object({
97
- status: z.enum(["SYNCED", "UNSUPPORTED_HTTP", "ERROR", "SKIPPED"]),
98
- lastSyncAt: z.string().optional(),
99
- message: z.string().optional(),
100
- configPath: z.string().optional()
101
- });
102
- var httpServerSchema = z.object({
103
- transport: z.literal("http"),
104
- url: z.url(),
105
- headers: z.record(z.string(), z.string()).optional(),
106
- enabled: z.boolean().default(true)
107
- });
108
- var stdioServerSchema = z.object({
109
- transport: z.literal("stdio"),
110
- command: z.string().min(1),
111
- args: z.array(z.string()).optional(),
112
- env: z.record(z.string(), z.string()).optional(),
113
- cwd: z.string().min(1).optional(),
114
- enabled: z.boolean().default(true)
115
- });
116
- var serverSchema = z.discriminatedUnion("transport", [httpServerSchema, stdioServerSchema]);
117
- var configSchema = z.object({
118
- schemaVersion: z.literal(1).default(1),
119
- gateway: z.object({
120
- port: z.number().int().min(1).max(65535).default(37373),
121
- tokenRef: z.string().default("secret://local_gateway_token"),
122
- autoStart: z.boolean().default(true)
123
- }).default({
124
- port: 37373,
125
- tokenRef: "secret://local_gateway_token",
126
- autoStart: true
127
- }),
128
- servers: z.record(z.string(), serverSchema).default({}),
129
- clients: z.record(z.string(), clientStateSchema).default({})
130
- });
131
- function defaultConfig() {
132
- return {
133
- schemaVersion: 1,
134
- gateway: {
135
- port: 37373,
136
- tokenRef: "secret://local_gateway_token",
137
- autoStart: true
138
- },
139
- servers: {},
140
- clients: {}
141
- };
142
- }
143
- function loadConfig(configPath = getConfigPath()) {
144
- const raw = readJsonFile(configPath, defaultConfig());
145
- const parsed = configSchema.safeParse(raw);
146
- if (!parsed.success) {
147
- return defaultConfig();
148
- }
149
- const clientEntries = {};
150
- for (const [key, value] of Object.entries(parsed.data.clients)) {
151
- if (["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen"].includes(key)) {
152
- clientEntries[key] = value;
153
- }
154
- }
155
- const servers = Object.fromEntries(Object.entries(parsed.data.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
156
- return {
157
- schemaVersion: 1,
158
- gateway: parsed.data.gateway,
159
- servers,
160
- clients: clientEntries
161
- };
162
- }
163
- function saveConfig(config, configPath = getConfigPath()) {
164
- config.servers = Object.fromEntries(Object.entries(config.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
165
- writeJsonAtomic(configPath, config);
166
- }
167
-
168
- // src/core/registry.ts
169
- function validateServerName(name) {
170
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(name)) {
171
- throw new Error("Invalid server name. Use 1-63 chars: letters, numbers, '.', '-', '_' and start with alphanumeric.");
172
- }
173
- }
174
- function addServer(config, name, spec, force = false) {
175
- validateServerName(name);
176
- if (config.servers[name] && !force) {
177
- throw new Error(`Server "${name}" already exists. Use --force to overwrite.`);
178
- }
179
- config.servers[name] = normalizeServerSpecEnabled(spec);
180
- }
181
- function removeServer(config, name, force = false) {
182
- if (!config.servers[name] && !force) {
183
- throw new Error(`Server "${name}" does not exist.`);
184
- }
185
- delete config.servers[name];
186
- }
187
- function updateServer(config, name, spec) {
188
- validateServerName(name);
189
- if (!config.servers[name]) {
190
- throw new Error(`Server "${name}" does not exist.`);
191
- }
192
- config.servers[name] = normalizeServerSpecEnabled(spec);
193
- }
194
- function setServerEnabled(config, name, enabled) {
195
- const existing = config.servers[name];
196
- if (!existing) {
197
- throw new Error(`Server "${name}" does not exist.`);
198
- }
199
- config.servers[name] = {
200
- ...existing,
201
- enabled
202
- };
203
- }
204
- function getGatewayTokenSecretName(config) {
205
- if (!config.gateway.tokenRef.startsWith("secret://")) {
206
- return "local_gateway_token";
207
- }
208
- return config.gateway.tokenRef.slice("secret://".length) || "local_gateway_token";
209
- }
210
- function ensureGatewayToken(config, secrets) {
211
- const secretName = getGatewayTokenSecretName(config);
212
- const existing = secrets.getSecret(secretName);
213
- if (existing) {
214
- return existing;
215
- }
216
- return secrets.rotateLocalToken(secretName);
217
- }
218
-
219
- // src/core/secrets.ts
220
- import { execFileSync } from "node:child_process";
221
- import crypto2 from "node:crypto";
222
- import fs3 from "node:fs";
223
- function parseSecretRef(ref) {
224
- if (!ref.startsWith("secret://")) {
31
+ import path from "node:path";
32
+ function listSkills() {
33
+ const skillsDir = getSkillsDir();
34
+ if (!fs.existsSync(skillsDir)) {
35
+ return [];
36
+ }
37
+ const files = fs.readdirSync(skillsDir);
38
+ const skills = [];
39
+ for (const file of files) {
40
+ if (file.endsWith(".md")) {
41
+ const id = path.basename(file, ".md");
42
+ const content = fs.readFileSync(path.join(skillsDir, file), "utf-8");
43
+ skills.push({ id, content });
44
+ }
45
+ }
46
+ return skills;
47
+ }
48
+ function getSkill(id) {
49
+ const skillsDir = getSkillsDir();
50
+ const filePath = path.join(skillsDir, `${id}.md`);
51
+ if (!fs.existsSync(filePath)) {
225
52
  return null;
226
53
  }
227
- const name = ref.slice("secret://".length);
228
- return name.length > 0 ? name : null;
54
+ const content = fs.readFileSync(filePath, "utf-8");
55
+ return { id, content };
229
56
  }
230
-
231
- class SecretsManager {
232
- service;
233
- nameIndexPath;
234
- constructor(service = "mcpx", nameIndexPath = getSecretNamesPath()) {
235
- this.service = service;
236
- this.nameIndexPath = nameIndexPath;
237
- }
238
- loadNameIndex() {
239
- return readJsonFile(this.nameIndexPath, { names: [] });
240
- }
241
- saveNameIndex(index) {
242
- const deduped = Array.from(new Set(index.names)).sort();
243
- writeJsonAtomic(this.nameIndexPath, { names: deduped });
244
- }
245
- trackSecretName(name) {
246
- const index = this.loadNameIndex();
247
- if (!index.names.includes(name)) {
248
- index.names.push(name);
249
- this.saveNameIndex(index);
250
- }
251
- }
252
- untrackSecretName(name) {
253
- const index = this.loadNameIndex();
254
- index.names = index.names.filter((entry) => entry !== name);
255
- this.saveNameIndex(index);
256
- }
257
- listSecretNames() {
258
- return this.loadNameIndex().names;
259
- }
260
- setSecret(name, value) {
261
- if (process.platform !== "darwin") {
262
- throw new Error("OS keychain integration is currently implemented for macOS only.");
263
- }
264
- execFileSync("security", ["add-generic-password", "-U", "-a", name, "-s", this.service, "-w", value], {
265
- stdio: "ignore"
266
- });
267
- this.trackSecretName(name);
268
- }
269
- removeSecret(name) {
270
- if (process.platform === "darwin") {
271
- try {
272
- execFileSync("security", ["delete-generic-password", "-a", name, "-s", this.service], {
273
- stdio: "ignore"
274
- });
275
- } catch {}
276
- }
277
- this.untrackSecretName(name);
278
- }
279
- getSecret(name) {
280
- const envOverride = process.env[`MCPX_SECRET_${name}`];
281
- if (envOverride && envOverride.length > 0) {
282
- return envOverride;
283
- }
284
- if (process.platform !== "darwin") {
285
- return null;
286
- }
287
- try {
288
- return execFileSync("security", ["find-generic-password", "-w", "-a", name, "-s", this.service], {
289
- encoding: "utf8"
290
- }).trim();
291
- } catch {
292
- return null;
293
- }
294
- }
295
- resolveMaybeSecret(value) {
296
- const secretName = parseSecretRef(value);
297
- if (!secretName) {
298
- return value;
299
- }
300
- const secret = this.getSecret(secretName);
301
- if (!secret) {
302
- throw new Error(`Secret not found: ${secretName}`);
303
- }
304
- return secret;
305
- }
306
- rotateLocalToken(secretName = "local_gateway_token") {
307
- const token = crypto2.randomBytes(32).toString("base64url");
308
- this.setSecret(secretName, token);
309
- return token;
310
- }
57
+ function saveSkill(id, content) {
58
+ const skillsDir = getSkillsDir();
59
+ ensureDir(skillsDir);
60
+ const filePath = path.join(skillsDir, `${id}.md`);
61
+ fs.writeFileSync(filePath, content, "utf-8");
311
62
  }
312
- function readSecretValueFromStdin() {
313
- if (process.stdin.isTTY) {
314
- throw new Error("No value provided. Use --value or pipe a value via stdin.");
315
- }
316
- const content = fs3.readFileSync(process.stdin.fd, "utf8").trim();
317
- if (content.length === 0) {
318
- throw new Error("Empty secret value provided.");
63
+ function deleteSkill(id) {
64
+ const skillsDir = getSkillsDir();
65
+ const filePath = path.join(skillsDir, `${id}.md`);
66
+ if (fs.existsSync(filePath)) {
67
+ fs.unlinkSync(filePath);
319
68
  }
320
- return content;
321
69
  }
322
70
 
323
71
  // src/core/auth-probe.ts
324
72
  var JSON_RPC_VERSION = "2.0";
325
73
  var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
74
+ var AUTH_KEYWORDS = [
75
+ "unauthorized",
76
+ "unauthenticated",
77
+ "forbidden",
78
+ "authentication",
79
+ "authorization",
80
+ "api key",
81
+ "api_key",
82
+ "apikey",
83
+ "credentials",
84
+ "access denied",
85
+ "auth required",
86
+ "x-api-key",
87
+ "invalid key",
88
+ "missing header",
89
+ "not authenticated",
90
+ "auth token"
91
+ ];
92
+ function isAuthRelatedMessage(message) {
93
+ const lower = message.toLowerCase();
94
+ return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
95
+ }
326
96
  function resolveHeaders(spec, secrets) {
327
97
  const headers = {
328
98
  "content-type": "application/json",
@@ -356,6 +126,22 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
356
126
  wwwAuthenticate: response.headers.get("www-authenticate") ?? undefined
357
127
  };
358
128
  }
129
+ if (response.status === 200) {
130
+ try {
131
+ const body = await response.json();
132
+ const err = body?.error;
133
+ if (err && typeof err === "object" && !("result" in body)) {
134
+ const message = typeof err.message === "string" ? err.message : "";
135
+ if (message && isAuthRelatedMessage(message)) {
136
+ return {
137
+ authRequired: true,
138
+ status: response.status,
139
+ error: `JSON-RPC error: ${message}`
140
+ };
141
+ }
142
+ }
143
+ } catch {}
144
+ }
359
145
  return {
360
146
  authRequired: false,
361
147
  status: response.status
@@ -371,9 +157,132 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
371
157
  }
372
158
  }
373
159
 
160
+ // src/core/registry-client.ts
161
+ var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
162
+ async function fetchRegistryServerDetail(name) {
163
+ const encoded = encodeURIComponent(name);
164
+ const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
165
+ headers: { accept: "application/json" }
166
+ });
167
+ if (!response.ok) {
168
+ throw new Error(`Registry API error: ${response.status}`);
169
+ }
170
+ const data = await response.json();
171
+ return data.server;
172
+ }
173
+ var PACKAGE_PRIORITY = {
174
+ npm: 1,
175
+ pypi: 2,
176
+ nuget: 3,
177
+ oci: 4,
178
+ mcpb: 5
179
+ };
180
+ var RUNTIME_HINT = {
181
+ npm: "npx",
182
+ pypi: "uvx",
183
+ nuget: "dnx"
184
+ };
185
+ function selectBestPackage(packages = [], remotes = []) {
186
+ const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
187
+ if (stdioPkgs.length > 0) {
188
+ return { kind: "package", package: stdioPkgs[0] };
189
+ }
190
+ const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
191
+ if (httpRemotes.length > 0) {
192
+ return { kind: "remote", remote: httpRemotes[0] };
193
+ }
194
+ if (packages.length > 0) {
195
+ return { kind: "package", package: packages[0] };
196
+ }
197
+ if (remotes.length > 0) {
198
+ return { kind: "remote", remote: remotes[0] };
199
+ }
200
+ throw new Error("Server has no packages or remotes");
201
+ }
202
+ function extractRequiredInputs(option) {
203
+ const inputs = [];
204
+ if (option.kind === "package" && option.package?.environmentVariables) {
205
+ for (const env of option.package.environmentVariables) {
206
+ if (env.isRequired && !env.default) {
207
+ inputs.push({
208
+ name: env.name,
209
+ description: env.description,
210
+ isSecret: env.isSecret ?? false,
211
+ kind: "env"
212
+ });
213
+ }
214
+ }
215
+ for (const arg of option.package.packageArguments ?? []) {
216
+ if (arg.isRequired && !arg.value && !arg.default) {
217
+ inputs.push({
218
+ name: arg.name ?? arg.valueHint ?? "arg",
219
+ description: arg.description,
220
+ isSecret: false,
221
+ kind: "arg"
222
+ });
223
+ }
224
+ }
225
+ }
226
+ if (option.kind === "remote" && option.remote?.headers) {
227
+ for (const header of option.remote.headers) {
228
+ if (header.isRequired && !header.default) {
229
+ inputs.push({
230
+ name: header.name,
231
+ description: header.description,
232
+ isSecret: header.isSecret ?? false,
233
+ kind: "header"
234
+ });
235
+ }
236
+ }
237
+ }
238
+ return inputs;
239
+ }
240
+ function mapRegistryToSpec(_name, option, resolvedValues) {
241
+ if (option.kind === "remote") {
242
+ const headers = {};
243
+ for (const header of option.remote?.headers ?? []) {
244
+ const value = resolvedValues[header.name] ?? header.default;
245
+ if (value)
246
+ headers[header.name] = value;
247
+ }
248
+ return {
249
+ transport: "http",
250
+ url: option.remote.url,
251
+ ...Object.keys(headers).length > 0 ? { headers } : {}
252
+ };
253
+ }
254
+ const pkg = option.package;
255
+ const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
256
+ const args = [];
257
+ const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
258
+ args.push(identifier);
259
+ for (const arg of pkg.packageArguments ?? []) {
260
+ const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
261
+ if (!value)
262
+ continue;
263
+ if (arg.type === "named" && arg.name) {
264
+ args.push(arg.name, value);
265
+ } else {
266
+ args.push(value);
267
+ }
268
+ }
269
+ const env = {};
270
+ for (const envVar of pkg.environmentVariables ?? []) {
271
+ const value = resolvedValues[envVar.name] ?? envVar.default;
272
+ if (value)
273
+ env[envVar.name] = value;
274
+ }
275
+ return {
276
+ transport: "stdio",
277
+ command: runtime,
278
+ args,
279
+ ...Object.keys(env).length > 0 ? { env } : {}
280
+ };
281
+ }
282
+
374
283
  // src/adapters/claude.ts
375
- import path3 from "node:path";
376
- import { z as z2 } from "zod";
284
+ import path2 from "node:path";
285
+ import { z } from "zod";
377
286
 
378
287
  // src/adapters/utils/index.ts
379
288
  function getManagedEntryNames(managedIndex, clientId) {
@@ -390,6 +299,11 @@ function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryName
390
299
  delete servers[name];
391
300
  }
392
301
  }
302
+ for (const name of Object.keys(servers)) {
303
+ if (!keep.has(name) && name.endsWith(" (mcpx)")) {
304
+ delete servers[name];
305
+ }
306
+ }
393
307
  }
394
308
  function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
395
309
  if (existingValue === undefined || existingValue === null) {
@@ -496,22 +410,22 @@ function syncMcpExcludedArray(raw, managedNames, managedEntries) {
496
410
  }
497
411
 
498
412
  // src/adapters/claude.ts
499
- var stringMapSchema = z2.record(z2.string(), z2.string());
500
- var claudeEntrySchema = z2.object({
501
- type: z2.string().optional(),
502
- url: z2.string().min(1).optional(),
413
+ var stringMapSchema = z.record(z.string(), z.string());
414
+ var claudeEntrySchema = z.object({
415
+ type: z.string().optional(),
416
+ url: z.string().min(1).optional(),
503
417
  headers: stringMapSchema.optional(),
504
- command: z2.string().min(1).optional(),
505
- args: z2.array(z2.string()).optional(),
418
+ command: z.string().min(1).optional(),
419
+ args: z.array(z.string()).optional(),
506
420
  env: stringMapSchema.optional(),
507
- cwd: z2.string().min(1).optional(),
508
- disabled: z2.boolean().optional()
421
+ cwd: z.string().min(1).optional(),
422
+ disabled: z.boolean().optional()
509
423
  }).passthrough();
510
424
 
511
425
  class ClaudeAdapter {
512
426
  id = "claude";
513
427
  detectConfigPath() {
514
- return path3.join(homeDir(), ".claude.json");
428
+ return path2.join(homeDir(), ".claude.json");
515
429
  }
516
430
  supportsHttp() {
517
431
  return true;
@@ -623,24 +537,40 @@ class ClaudeAdapter {
623
537
  }
624
538
 
625
539
  // src/adapters/claude-desktop.ts
626
- import path4 from "node:path";
627
- import { z as z3 } from "zod";
628
- var stringMapSchema2 = z3.record(z3.string(), z3.string());
629
- var claudeDesktopEntrySchema = z3.object({
630
- type: z3.string().optional(),
631
- url: z3.string().min(1).optional(),
540
+ import { execFileSync } from "node:child_process";
541
+ import path3 from "node:path";
542
+ import { z as z2 } from "zod";
543
+ var stringMapSchema2 = z2.record(z2.string(), z2.string());
544
+ var claudeDesktopEntrySchema = z2.object({
545
+ type: z2.string().optional(),
546
+ url: z2.string().min(1).optional(),
632
547
  headers: stringMapSchema2.optional(),
633
- command: z3.string().min(1).optional(),
634
- args: z3.array(z3.string()).optional(),
548
+ command: z2.string().min(1).optional(),
549
+ args: z2.array(z2.string()).optional(),
635
550
  env: stringMapSchema2.optional(),
636
- cwd: z3.string().min(1).optional(),
637
- disabled: z3.boolean().optional()
551
+ cwd: z2.string().min(1).optional(),
552
+ disabled: z2.boolean().optional()
638
553
  }).passthrough();
554
+ function buildProxyEntry(entry) {
555
+ const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
556
+ try {
557
+ const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
558
+ if (mcpxPath)
559
+ return { command: mcpxPath, args: ["proxy", upstreamName] };
560
+ } catch {}
561
+ return { command: "npx", args: ["-y", "@kwonye/mcpx@latest", "proxy", upstreamName] };
562
+ }
639
563
 
640
564
  class ClaudeDesktopAdapter {
641
565
  id = "claude-desktop";
642
566
  detectConfigPath() {
643
- return path4.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
567
+ if (process.platform === "linux") {
568
+ return path3.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
569
+ }
570
+ if (process.platform === "win32") {
571
+ return path3.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
572
+ }
573
+ return path3.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
644
574
  }
645
575
  supportsHttp() {
646
576
  return true;
@@ -663,7 +593,7 @@ class ClaudeDesktopAdapter {
663
593
  continue;
664
594
  }
665
595
  const entry = parsed.data;
666
- if ((entry.type === undefined || entry.type === "http") && entry.url && !entry.command) {
596
+ if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable_http") && entry.url && !entry.command) {
667
597
  result.candidates.push({
668
598
  clientId: this.id,
669
599
  configPath,
@@ -707,11 +637,10 @@ class ClaudeDesktopAdapter {
707
637
  try {
708
638
  const raw = readJsonFile(configPath, {});
709
639
  const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
710
- const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
711
- type: "http",
712
- url: entry.url,
713
- headers: entry.headers
714
- }]));
640
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => {
641
+ const { command, args } = buildProxyEntry(entry);
642
+ return [entry.name, { command, args }];
643
+ }));
715
644
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
716
645
  const topLevelServers = raw.mcpServers ?? {};
717
646
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
@@ -727,10 +656,10 @@ class ClaudeDesktopAdapter {
727
656
  }
728
657
  raw.mcpServers = topLevelServers;
729
658
  writeJsonAtomic(configPath, raw);
730
- setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
731
- entry.name,
732
- JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
733
- ])));
659
+ setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => {
660
+ const { command, args } = buildProxyEntry(entry);
661
+ return [entry.name, JSON.stringify({ command, args, enabled: entry.enabled })];
662
+ })));
734
663
  return okResult(this.id, configPath);
735
664
  } catch (error) {
736
665
  return errorResult(this.id, configPath, error.message);
@@ -739,31 +668,31 @@ class ClaudeDesktopAdapter {
739
668
  }
740
669
 
741
670
  // src/adapters/codex.ts
742
- import path5 from "node:path";
743
- import fs4 from "node:fs";
671
+ import path4 from "node:path";
672
+ import fs2 from "node:fs";
744
673
  import { parse, stringify } from "@iarna/toml";
745
- import { z as z4 } from "zod";
746
- var stringMapSchema3 = z4.preprocess((value) => {
674
+ import { z as z3 } from "zod";
675
+ var stringMapSchema3 = z3.preprocess((value) => {
747
676
  if (!value || typeof value !== "object" || Array.isArray(value)) {
748
677
  return value;
749
678
  }
750
679
  return Object.fromEntries(Object.entries(value));
751
- }, z4.record(z4.string(), z4.string()));
752
- var codexEntrySchema = z4.object({
753
- enabled: z4.boolean().optional(),
754
- url: z4.string().min(1).optional(),
680
+ }, z3.record(z3.string(), z3.string()));
681
+ var codexEntrySchema = z3.object({
682
+ enabled: z3.boolean().optional(),
683
+ url: z3.string().min(1).optional(),
755
684
  headers: stringMapSchema3.optional(),
756
685
  http_headers: stringMapSchema3.optional(),
757
- command: z4.string().min(1).optional(),
758
- args: z4.array(z4.string()).optional(),
686
+ command: z3.string().min(1).optional(),
687
+ args: z3.array(z3.string()).optional(),
759
688
  env: stringMapSchema3.optional(),
760
- cwd: z4.string().min(1).optional()
689
+ cwd: z3.string().min(1).optional()
761
690
  }).passthrough();
762
691
 
763
692
  class CodexAdapter {
764
693
  id = "codex";
765
694
  detectConfigPath() {
766
- return path5.join(homeDir(), ".codex", "config.toml");
695
+ return path4.join(homeDir(), ".codex", "config.toml");
767
696
  }
768
697
  supportsHttp() {
769
698
  return true;
@@ -771,10 +700,10 @@ class CodexAdapter {
771
700
  scanForImports(_config, managedIndex) {
772
701
  const configPath = this.detectConfigPath();
773
702
  const result = emptyImportScan(this.id, configPath ?? undefined);
774
- if (!configPath || !fs4.existsSync(configPath)) {
703
+ if (!configPath || !fs2.existsSync(configPath)) {
775
704
  return result;
776
705
  }
777
- const existing = fs4.readFileSync(configPath, "utf8");
706
+ const existing = fs2.readFileSync(configPath, "utf8");
778
707
  const doc = existing.trim().length > 0 ? parse(existing) : {};
779
708
  const mcpServers = doc.mcp_servers ?? {};
780
709
  for (const [name, rawEntry] of Object.entries(mcpServers)) {
@@ -829,7 +758,7 @@ class CodexAdapter {
829
758
  return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
830
759
  }
831
760
  try {
832
- const existing = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf8") : "";
761
+ const existing = fs2.existsSync(configPath) ? fs2.readFileSync(configPath, "utf8") : "";
833
762
  const doc = existing.trim().length > 0 ? parse(existing) : {};
834
763
  const mcpServers = doc.mcp_servers ?? {};
835
764
  removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
@@ -850,10 +779,10 @@ class CodexAdapter {
850
779
  mcpServers[name] = entry;
851
780
  }
852
781
  doc.mcp_servers = mcpServers;
853
- fs4.mkdirSync(path5.dirname(configPath), { recursive: true });
782
+ fs2.mkdirSync(path4.dirname(configPath), { recursive: true });
854
783
  const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
855
- fs4.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
856
- fs4.renameSync(tmpPath, configPath);
784
+ fs2.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
785
+ fs2.renameSync(tmpPath, configPath);
857
786
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
858
787
  return okResult(this.id, configPath);
859
788
  } catch (error) {
@@ -863,24 +792,30 @@ class CodexAdapter {
863
792
  }
864
793
 
865
794
  // src/adapters/cursor.ts
866
- import path6 from "node:path";
867
- import { z as z5 } from "zod";
868
- var stringMapSchema4 = z5.record(z5.string(), z5.string());
869
- var cursorEntrySchema = z5.object({
870
- type: z5.string().optional(),
871
- url: z5.string().min(1).optional(),
795
+ import path5 from "node:path";
796
+ import { z as z4 } from "zod";
797
+ var stringMapSchema4 = z4.record(z4.string(), z4.string());
798
+ var cursorEntrySchema = z4.object({
799
+ type: z4.string().optional(),
800
+ url: z4.string().min(1).optional(),
872
801
  headers: stringMapSchema4.optional(),
873
- command: z5.string().min(1).optional(),
874
- args: z5.array(z5.string()).optional(),
802
+ command: z4.string().min(1).optional(),
803
+ args: z4.array(z4.string()).optional(),
875
804
  env: stringMapSchema4.optional(),
876
- cwd: z5.string().min(1).optional(),
877
- disabled: z5.boolean().optional()
805
+ cwd: z4.string().min(1).optional(),
806
+ disabled: z4.boolean().optional()
878
807
  }).passthrough();
879
808
 
880
809
  class CursorAdapter {
881
810
  id = "cursor";
882
811
  detectConfigPath() {
883
- return path6.join(homeDir(), "Library", "Application Support", "Cursor", "User", "mcp.json");
812
+ if (process.platform === "linux") {
813
+ return path5.join(homeDir(), ".config", "Cursor", "User", "mcp.json");
814
+ }
815
+ if (process.platform === "win32") {
816
+ return path5.join(process.env.APPDATA ?? "", "Cursor", "User", "mcp.json");
817
+ }
818
+ return path5.join(homeDir(), "Library", "Application Support", "Cursor", "User", "mcp.json");
884
819
  }
885
820
  supportsHttp() {
886
821
  return true;
@@ -986,24 +921,36 @@ class CursorAdapter {
986
921
  }
987
922
 
988
923
  // src/adapters/cline.ts
989
- import path7 from "node:path";
990
- import fs5 from "node:fs";
991
- import { z as z6 } from "zod";
992
- var stringMapSchema5 = z6.record(z6.string(), z6.string());
993
- var clineEntrySchema = z6.object({
994
- transportType: z6.string().optional(),
995
- url: z6.string().min(1).optional(),
924
+ import path6 from "node:path";
925
+ import fs3 from "node:fs";
926
+ import { z as z5 } from "zod";
927
+ var stringMapSchema5 = z5.record(z5.string(), z5.string());
928
+ var clineEntrySchema = z5.object({
929
+ transportType: z5.string().optional(),
930
+ url: z5.string().min(1).optional(),
996
931
  headers: stringMapSchema5.optional(),
997
- command: z6.string().min(1).optional(),
998
- args: z6.array(z6.string()).optional(),
932
+ command: z5.string().min(1).optional(),
933
+ args: z5.array(z5.string()).optional(),
999
934
  env: stringMapSchema5.optional(),
1000
- cwd: z6.string().min(1).optional(),
1001
- disabled: z6.boolean().optional()
935
+ cwd: z5.string().min(1).optional(),
936
+ disabled: z5.boolean().optional()
1002
937
  }).passthrough();
1003
938
  function getCandidatePaths() {
939
+ if (process.platform === "linux") {
940
+ return [
941
+ path6.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
942
+ path6.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
943
+ ];
944
+ }
945
+ if (process.platform === "win32") {
946
+ return [
947
+ path6.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
948
+ path6.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
949
+ ];
950
+ }
1004
951
  return [
1005
- path7.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1006
- path7.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
952
+ path6.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
953
+ path6.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
1007
954
  ];
1008
955
  }
1009
956
 
@@ -1012,7 +959,7 @@ class ClineAdapter {
1012
959
  detectConfigPath() {
1013
960
  const candidates = getCandidatePaths();
1014
961
  for (const candidate of candidates) {
1015
- if (fs5.existsSync(candidate)) {
962
+ if (fs3.existsSync(candidate)) {
1016
963
  return candidate;
1017
964
  }
1018
965
  }
@@ -1024,7 +971,7 @@ class ClineAdapter {
1024
971
  scanForImports(_config, managedIndex) {
1025
972
  const configPath = this.detectConfigPath();
1026
973
  const result = emptyImportScan(this.id, configPath ?? undefined);
1027
- if (!configPath || !fs5.existsSync(configPath)) {
974
+ if (!configPath || !fs3.existsSync(configPath)) {
1028
975
  return result;
1029
976
  }
1030
977
  const raw = readJsonFile(configPath, {});
@@ -1119,23 +1066,24 @@ class ClineAdapter {
1119
1066
  }
1120
1067
 
1121
1068
  // src/adapters/kiro.ts
1122
- import path8 from "node:path";
1123
- import { z as z7 } from "zod";
1124
- var stringMapSchema6 = z7.record(z7.string(), z7.string());
1125
- var kiroEntrySchema = z7.object({
1126
- url: z7.string().min(1).optional(),
1069
+ import fs4 from "node:fs";
1070
+ import path7 from "node:path";
1071
+ import { z as z6 } from "zod";
1072
+ var stringMapSchema6 = z6.record(z6.string(), z6.string());
1073
+ var kiroEntrySchema = z6.object({
1074
+ url: z6.string().min(1).optional(),
1127
1075
  headers: stringMapSchema6.optional(),
1128
- command: z7.string().min(1).optional(),
1129
- args: z7.array(z7.string()).optional(),
1076
+ command: z6.string().min(1).optional(),
1077
+ args: z6.array(z6.string()).optional(),
1130
1078
  env: stringMapSchema6.optional(),
1131
- cwd: z7.string().min(1).optional(),
1132
- disabled: z7.boolean().optional()
1079
+ cwd: z6.string().min(1).optional(),
1080
+ disabled: z6.boolean().optional()
1133
1081
  }).passthrough();
1134
1082
 
1135
1083
  class KiroAdapter {
1136
1084
  id = "kiro";
1137
1085
  detectConfigPath() {
1138
- return path8.join(homeDir(), ".kiro", "settings", "mcp.json");
1086
+ return path7.join(homeDir(), ".kiro", "settings", "mcp.json");
1139
1087
  }
1140
1088
  supportsHttp() {
1141
1089
  return true;
@@ -1234,27 +1182,47 @@ class KiroAdapter {
1234
1182
  return errorResult(this.id, configPath, error.message);
1235
1183
  }
1236
1184
  }
1185
+ syncSkills(skills) {
1186
+ const skillsDir = path7.join(homeDir(), ".kiro", "skills");
1187
+ if (!fs4.existsSync(skillsDir)) {
1188
+ fs4.mkdirSync(skillsDir, { recursive: true });
1189
+ }
1190
+ const existingFiles = fs4.readdirSync(skillsDir);
1191
+ const managedIds = new Set(skills.map((s) => s.id));
1192
+ for (const file of existingFiles) {
1193
+ if (file.endsWith(".md")) {
1194
+ const id = path7.basename(file, ".md");
1195
+ if (!managedIds.has(id)) {
1196
+ fs4.unlinkSync(path7.join(skillsDir, file));
1197
+ }
1198
+ }
1199
+ }
1200
+ for (const skill of skills) {
1201
+ const targetPath = path7.join(skillsDir, `${skill.id}.md`);
1202
+ fs4.writeFileSync(targetPath, skill.content, "utf-8");
1203
+ }
1204
+ }
1237
1205
  }
1238
1206
 
1239
1207
  // src/adapters/opencode.ts
1240
- import path9 from "node:path";
1241
- import { z as z8 } from "zod";
1242
- var stringMapSchema7 = z8.record(z8.string(), z8.string());
1243
- var openCodeEntrySchema = z8.object({
1244
- type: z8.string().optional(),
1245
- url: z8.string().min(1).optional(),
1208
+ import path8 from "node:path";
1209
+ import { z as z7 } from "zod";
1210
+ var stringMapSchema7 = z7.record(z7.string(), z7.string());
1211
+ var openCodeEntrySchema = z7.object({
1212
+ type: z7.string().optional(),
1213
+ url: z7.string().min(1).optional(),
1246
1214
  headers: stringMapSchema7.optional(),
1247
- command: z8.string().min(1).optional(),
1248
- args: z8.array(z8.string()).optional(),
1215
+ command: z7.string().min(1).optional(),
1216
+ args: z7.array(z7.string()).optional(),
1249
1217
  env: stringMapSchema7.optional(),
1250
- cwd: z8.string().min(1).optional(),
1251
- enabled: z8.boolean().optional()
1218
+ cwd: z7.string().min(1).optional(),
1219
+ enabled: z7.boolean().optional()
1252
1220
  }).passthrough();
1253
1221
 
1254
1222
  class OpenCodeAdapter {
1255
1223
  id = "opencode";
1256
1224
  detectConfigPath() {
1257
- return path9.join(homeDir(), ".config", "opencode", "opencode.json");
1225
+ return path8.join(homeDir(), ".config", "opencode", "opencode.json");
1258
1226
  }
1259
1227
  supportsHttp() {
1260
1228
  return true;
@@ -1357,24 +1325,30 @@ class OpenCodeAdapter {
1357
1325
  }
1358
1326
 
1359
1327
  // src/adapters/vscode.ts
1360
- import path10 from "node:path";
1361
- import { z as z9 } from "zod";
1362
- var stringMapSchema8 = z9.record(z9.string(), z9.string());
1363
- var vscodeEntrySchema = z9.object({
1364
- type: z9.string().optional(),
1365
- url: z9.string().min(1).optional(),
1328
+ import path9 from "node:path";
1329
+ import { z as z8 } from "zod";
1330
+ var stringMapSchema8 = z8.record(z8.string(), z8.string());
1331
+ var vscodeEntrySchema = z8.object({
1332
+ type: z8.string().optional(),
1333
+ url: z8.string().min(1).optional(),
1366
1334
  headers: stringMapSchema8.optional(),
1367
- command: z9.string().min(1).optional(),
1368
- args: z9.array(z9.string()).optional(),
1335
+ command: z8.string().min(1).optional(),
1336
+ args: z8.array(z8.string()).optional(),
1369
1337
  env: stringMapSchema8.optional(),
1370
- cwd: z9.string().min(1).optional(),
1371
- disabled: z9.boolean().optional()
1338
+ cwd: z8.string().min(1).optional(),
1339
+ disabled: z8.boolean().optional()
1372
1340
  }).passthrough();
1373
1341
 
1374
1342
  class VsCodeAdapter {
1375
1343
  id = "vscode";
1376
1344
  detectConfigPath() {
1377
- return path10.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1345
+ if (process.platform === "linux") {
1346
+ return path9.join(homeDir(), ".config", "Code", "User", "mcp.json");
1347
+ }
1348
+ if (process.platform === "win32") {
1349
+ return path9.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1350
+ }
1351
+ return path9.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1378
1352
  }
1379
1353
  supportsHttp() {
1380
1354
  return true;
@@ -1480,23 +1454,24 @@ class VsCodeAdapter {
1480
1454
  }
1481
1455
 
1482
1456
  // src/adapters/qwen.ts
1483
- import path11 from "node:path";
1484
- import { z as z10 } from "zod";
1485
- var stringMapSchema9 = z10.record(z10.string(), z10.string());
1486
- var qwenEntrySchema = z10.object({
1487
- httpUrl: z10.string().min(1).optional(),
1457
+ import fs5 from "node:fs";
1458
+ import path10 from "node:path";
1459
+ import { z as z9 } from "zod";
1460
+ var stringMapSchema9 = z9.record(z9.string(), z9.string());
1461
+ var qwenEntrySchema = z9.object({
1462
+ httpUrl: z9.string().min(1).optional(),
1488
1463
  headers: stringMapSchema9.optional(),
1489
- command: z10.string().min(1).optional(),
1490
- args: z10.array(z10.string()).optional(),
1464
+ command: z9.string().min(1).optional(),
1465
+ args: z9.array(z9.string()).optional(),
1491
1466
  env: stringMapSchema9.optional(),
1492
- cwd: z10.string().min(1).optional(),
1493
- disabled: z10.boolean().optional()
1467
+ cwd: z9.string().min(1).optional(),
1468
+ disabled: z9.boolean().optional()
1494
1469
  }).passthrough();
1495
1470
 
1496
1471
  class QwenAdapter {
1497
1472
  id = "qwen";
1498
1473
  detectConfigPath() {
1499
- return path11.join(homeDir(), ".qwen", "settings.json");
1474
+ return path10.join(homeDir(), ".qwen", "settings.json");
1500
1475
  }
1501
1476
  supportsHttp() {
1502
1477
  return true;
@@ -1592,6 +1567,26 @@ class QwenAdapter {
1592
1567
  return errorResult(this.id, configPath, error.message);
1593
1568
  }
1594
1569
  }
1570
+ syncSkills(skills) {
1571
+ const skillsDir = path10.join(homeDir(), ".qwen", "skills");
1572
+ if (!fs5.existsSync(skillsDir)) {
1573
+ fs5.mkdirSync(skillsDir, { recursive: true });
1574
+ }
1575
+ const existingFiles = fs5.readdirSync(skillsDir);
1576
+ const managedIds = new Set(skills.map((s) => s.id));
1577
+ for (const file of existingFiles) {
1578
+ if (file.endsWith(".md")) {
1579
+ const id = path10.basename(file, ".md");
1580
+ if (!managedIds.has(id)) {
1581
+ fs5.unlinkSync(path10.join(skillsDir, file));
1582
+ }
1583
+ }
1584
+ }
1585
+ for (const skill of skills) {
1586
+ const targetPath = path10.join(skillsDir, `${skill.id}.md`);
1587
+ fs5.writeFileSync(targetPath, skill.content, "utf-8");
1588
+ }
1589
+ }
1595
1590
  }
1596
1591
 
1597
1592
  // src/adapters/index.ts
@@ -1710,8 +1705,14 @@ function syncAllClients(config, secrets, targetClients) {
1710
1705
  const localToken = ensureGatewayToken(config, secrets);
1711
1706
  const gatewayUrl = getGatewayUrl(config);
1712
1707
  const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
1708
+ const skills = listSkills();
1713
1709
  const results = [];
1714
1710
  for (const adapter of filteredAdapters) {
1711
+ if (adapter.syncSkills) {
1712
+ try {
1713
+ adapter.syncSkills(skills);
1714
+ } catch (error) {}
1715
+ }
1715
1716
  if (!adapter.supportsHttp()) {
1716
1717
  results.push({
1717
1718
  clientId: adapter.id,
@@ -1861,7 +1862,7 @@ var APP_VERSION = "0.1.3";
1861
1862
 
1862
1863
  // src/core/update.ts
1863
1864
  import fs6 from "node:fs";
1864
- import path12 from "node:path";
1865
+ import path11 from "node:path";
1865
1866
  import { execFileSync as execFileSync2 } from "node:child_process";
1866
1867
  function compareVersions(a, b) {
1867
1868
  const partsA = a.split(".").map(Number);
@@ -1909,7 +1910,7 @@ function getStagedUpdate() {
1909
1910
  if (!info.version || !info.cliPath) {
1910
1911
  return null;
1911
1912
  }
1912
- const absoluteCliPath = path12.isAbsolute(info.cliPath) ? info.cliPath : path12.join(getUpdatesDir(), info.cliPath);
1913
+ const absoluteCliPath = path11.isAbsolute(info.cliPath) ? info.cliPath : path11.join(getUpdatesDir(), info.cliPath);
1913
1914
  if (!fs6.existsSync(absoluteCliPath)) {
1914
1915
  return null;
1915
1916
  }
@@ -1926,7 +1927,7 @@ function stageUpdate(version, cliPath) {
1926
1927
  ensureDir(updatesDir);
1927
1928
  const stagedInfo = {
1928
1929
  version,
1929
- cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
1930
+ cliPath: path11.isAbsolute(cliPath) ? path11.relative(updatesDir, cliPath) : cliPath,
1930
1931
  stagedAt: new Date().toISOString()
1931
1932
  };
1932
1933
  fs6.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
@@ -1952,7 +1953,7 @@ function removeOldVersions(keepCount = 2) {
1952
1953
  return;
1953
1954
  }
1954
1955
  for (const oldVersion of versionDirs.slice(keepCount)) {
1955
- const oldPath = path12.join(updatesDir, oldVersion);
1956
+ const oldPath = path11.join(updatesDir, oldVersion);
1956
1957
  try {
1957
1958
  fs6.rmSync(oldPath, { recursive: true, force: true });
1958
1959
  } catch {}
@@ -1961,12 +1962,12 @@ function removeOldVersions(keepCount = 2) {
1961
1962
 
1962
1963
  // src/core/update-manager.ts
1963
1964
  import fs7 from "node:fs";
1964
- import path13 from "node:path";
1965
+ import path12 from "node:path";
1965
1966
  import { execSync, spawn } from "node:child_process";
1966
1967
  function acquireLock() {
1967
1968
  const lockPath = getUpdateLockPath();
1968
1969
  try {
1969
- ensureDir(path13.dirname(lockPath));
1970
+ ensureDir(path12.dirname(lockPath));
1970
1971
  fs7.writeFileSync(lockPath, `${process.pid}
1971
1972
  `, { flag: "wx" });
1972
1973
  return true;
@@ -2006,7 +2007,7 @@ async function downloadAndStageUpdate() {
2006
2007
  }
2007
2008
  const targetVersion = updateStatus.latestVersion;
2008
2009
  const updatesDir = getUpdatesDir();
2009
- const versionDir = path13.join(updatesDir, `v${targetVersion}`);
2010
+ const versionDir = path12.join(updatesDir, `v${targetVersion}`);
2010
2011
  if (fs7.existsSync(versionDir)) {
2011
2012
  const existingStaged = getStagedUpdate();
2012
2013
  if (existingStaged?.version === targetVersion) {
@@ -2019,7 +2020,7 @@ async function downloadAndStageUpdate() {
2019
2020
  cwd: versionDir,
2020
2021
  stdio: ["pipe", "pipe", "pipe"]
2021
2022
  });
2022
- const tarballPath = path13.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2023
+ const tarballPath = path12.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2023
2024
  if (!fs7.existsSync(tarballPath)) {
2024
2025
  throw new Error("Downloaded tarball not found");
2025
2026
  }
@@ -2031,7 +2032,7 @@ async function downloadAndStageUpdate() {
2031
2032
  cwd: versionDir,
2032
2033
  stdio: ["pipe", "pipe", "pipe"]
2033
2034
  });
2034
- const cliPath = path13.join(versionDir, "dist", "cli.js");
2035
+ const cliPath = path12.join(versionDir, "dist", "cli.js");
2035
2036
  if (!fs7.existsSync(cliPath)) {
2036
2037
  throw new Error("CLI entry point not found in downloaded package");
2037
2038
  }
@@ -2125,7 +2126,7 @@ import { spawn as spawn2 } from "node:child_process";
2125
2126
  // src/gateway/server.ts
2126
2127
  import http from "node:http";
2127
2128
  import { URL } from "node:url";
2128
- import crypto3 from "node:crypto";
2129
+ import crypto from "node:crypto";
2129
2130
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2130
2131
  import {
2131
2132
  StdioClientTransport,
@@ -2304,6 +2305,61 @@ async function callUpstream(upstream, method, params, id, secrets, runtime, pass
2304
2305
  }
2305
2306
  return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
2306
2307
  }
2308
+ async function getUpstreamTokenCounts(config, secrets, runtime) {
2309
+ if (!runtime.tokenCache) {
2310
+ runtime.tokenCache = new Map;
2311
+ }
2312
+ const results = {};
2313
+ const upstreams = listUpstreams(config);
2314
+ for (const upstream of upstreams) {
2315
+ if (runtime.tokenCache.has(upstream.name)) {
2316
+ results[upstream.name] = runtime.tokenCache.get(upstream.name);
2317
+ continue;
2318
+ }
2319
+ try {
2320
+ const prevTimeout = process.env.MCPX_UPSTREAM_TIMEOUT_MS;
2321
+ process.env.MCPX_UPSTREAM_TIMEOUT_MS = "3000";
2322
+ let toolsCount = 0;
2323
+ let resourcesCount = 0;
2324
+ let promptsCount = 0;
2325
+ try {
2326
+ const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
2327
+ if (toolsResult && toolsResult.tools) {
2328
+ toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
2329
+ }
2330
+ } catch (err) {}
2331
+ try {
2332
+ const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
2333
+ if (resourcesResult && resourcesResult.resources) {
2334
+ resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
2335
+ }
2336
+ } catch (err) {}
2337
+ try {
2338
+ const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
2339
+ if (promptsResult && promptsResult.prompts) {
2340
+ promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
2341
+ }
2342
+ } catch (err) {}
2343
+ if (prevTimeout !== undefined) {
2344
+ process.env.MCPX_UPSTREAM_TIMEOUT_MS = prevTimeout;
2345
+ } else {
2346
+ delete process.env.MCPX_UPSTREAM_TIMEOUT_MS;
2347
+ }
2348
+ const total = toolsCount + resourcesCount + promptsCount;
2349
+ const countObj = {
2350
+ tools: toolsCount,
2351
+ resources: resourcesCount,
2352
+ prompts: promptsCount,
2353
+ total
2354
+ };
2355
+ runtime.tokenCache.set(upstream.name, countObj);
2356
+ results[upstream.name] = countObj;
2357
+ } catch (error) {
2358
+ results[upstream.name] = { tools: 0, resources: 0, prompts: 0, total: 0 };
2359
+ }
2360
+ }
2361
+ return results;
2362
+ }
2307
2363
  function getConfiguredTimeoutMs() {
2308
2364
  const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
2309
2365
  return Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_UPSTREAM_TIMEOUT_MS;
@@ -2612,6 +2668,13 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
2612
2668
  for (const upstream of upstreams) {
2613
2669
  try {
2614
2670
  const result = await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
2671
+ if (!runtime.tokenCache) {
2672
+ runtime.tokenCache = new Map;
2673
+ }
2674
+ const existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
2675
+ existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
2676
+ existing.total = existing.tools + existing.resources + existing.prompts;
2677
+ runtime.tokenCache.set(upstream.name, existing);
2615
2678
  for (const tool of result.tools ?? []) {
2616
2679
  const name = typeof tool.name === "string" ? tool.name : "tool";
2617
2680
  tools.push({
@@ -2635,6 +2698,13 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
2635
2698
  for (const upstream of upstreams) {
2636
2699
  try {
2637
2700
  const result = await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
2701
+ if (!runtime.tokenCache) {
2702
+ runtime.tokenCache = new Map;
2703
+ }
2704
+ const existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
2705
+ existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
2706
+ existing.total = existing.tools + existing.resources + existing.prompts;
2707
+ runtime.tokenCache.set(upstream.name, existing);
2638
2708
  for (const resource of result.resources ?? []) {
2639
2709
  const originalUri = typeof resource.uri === "string" ? resource.uri : "";
2640
2710
  const originalName = typeof resource.name === "string" ? resource.name : originalUri;
@@ -2660,6 +2730,13 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
2660
2730
  for (const upstream of upstreams) {
2661
2731
  try {
2662
2732
  const result = await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
2733
+ if (!runtime.tokenCache) {
2734
+ runtime.tokenCache = new Map;
2735
+ }
2736
+ const existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
2737
+ existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
2738
+ existing.total = existing.tools + existing.resources + existing.prompts;
2739
+ runtime.tokenCache.set(upstream.name, existing);
2663
2740
  for (const prompt of result.prompts ?? []) {
2664
2741
  const name = typeof prompt.name === "string" ? prompt.name : "prompt";
2665
2742
  prompts.push({
@@ -2801,11 +2878,15 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
2801
2878
  if (request.method === "ping") {
2802
2879
  return makeResult(id, { ok: true });
2803
2880
  }
2804
- const config = loadConfig();
2881
+ const config = loadMergedConfig();
2805
2882
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
2806
2883
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
2807
2884
  }
2808
2885
  await reconcileStdioConnections(config, runtime);
2886
+ if (request.method === "custom/tokenCounts") {
2887
+ const result = await getUpstreamTokenCounts(config, secrets, runtime);
2888
+ return makeResult(id, result);
2889
+ }
2809
2890
  if (request.method === "tools/list") {
2810
2891
  const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
2811
2892
  return makeResult(id, result);
@@ -2835,7 +2916,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
2835
2916
  response.end(JSON.stringify({ error: "method_not_allowed" }));
2836
2917
  return true;
2837
2918
  }
2838
- const config = loadConfig();
2919
+ const config = loadMergedConfig();
2839
2920
  const requestedUpstream = getRequestedUpstream(requestUrl);
2840
2921
  const upstream = getScopedHttpUpstream(config, requestedUpstream);
2841
2922
  if (!upstream) {
@@ -3008,7 +3089,7 @@ function createGatewayServer(options) {
3008
3089
  response.setHeader("content-type", "application/json");
3009
3090
  }
3010
3091
  if (hasInitialize) {
3011
- const sessionId = crypto3.randomUUID();
3092
+ const sessionId = crypto.randomUUID();
3012
3093
  response.setHeader("mcp-session-id", sessionId);
3013
3094
  response.setHeader("MCP-Session-Id", sessionId);
3014
3095
  }
@@ -3136,12 +3217,18 @@ function getDaemonStatus(config) {
3136
3217
  async function startDaemon(config, cliPath, secrets) {
3137
3218
  const existingStatus = getDaemonStatus(config);
3138
3219
  if (existingStatus.running && existingStatus.pid) {
3139
- return {
3140
- started: false,
3141
- pid: existingStatus.pid,
3142
- port: config.gateway.port,
3143
- message: "mcpx daemon already running."
3144
- };
3220
+ const portAvailable = await isPortAvailable(config.gateway.port);
3221
+ if (!portAvailable) {
3222
+ return {
3223
+ started: false,
3224
+ pid: existingStatus.pid,
3225
+ port: config.gateway.port,
3226
+ message: "mcpx daemon already running."
3227
+ };
3228
+ }
3229
+ try {
3230
+ fs8.unlinkSync(getPidPath());
3231
+ } catch {}
3145
3232
  }
3146
3233
  const port = await resolveGatewayPort(config);
3147
3234
  ensureGatewayToken(config, secrets);
@@ -3267,26 +3354,74 @@ function buildAuthBindings(spec) {
3267
3354
  secretName: secretRefName(binding.value) ?? undefined
3268
3355
  }));
3269
3356
  }
3270
- function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
3357
+ async function fetchTokenCounts(gatewayUrl, token) {
3358
+ try {
3359
+ const res = await fetch(gatewayUrl, {
3360
+ method: "POST",
3361
+ headers: {
3362
+ "content-type": "application/json",
3363
+ authorization: `Bearer ${token}`
3364
+ },
3365
+ body: JSON.stringify({
3366
+ jsonrpc: "2.0",
3367
+ id: "status-tokens",
3368
+ method: "custom/tokenCounts",
3369
+ params: {}
3370
+ })
3371
+ });
3372
+ if (!res.ok) {
3373
+ return {};
3374
+ }
3375
+ const data = await res.json();
3376
+ return data?.result ?? {};
3377
+ } catch (error) {
3378
+ return {};
3379
+ }
3380
+ }
3381
+ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
3382
+ const secrets = new SecretsManager;
3383
+ const token = ensureGatewayToken(config, secrets);
3384
+ const gatewayUrl = getGatewayUrl(config);
3385
+ let tokenCounts = {};
3386
+ if (daemon.running) {
3387
+ tokenCounts = await fetchTokenCounts(gatewayUrl, token);
3388
+ }
3389
+ const projectEntries = Object.values(config.projects ?? {});
3390
+ let totalGlobalTokens = 0;
3391
+ const totalProjectTokens = {};
3271
3392
  const serverNames = Object.keys(config.servers).sort((left, right) => left.localeCompare(right));
3272
3393
  const servers = serverNames.map((name) => {
3273
3394
  const spec = config.servers[name];
3274
- return {
3395
+ const tokenCount = tokenCounts[name];
3396
+ const serverEntry = {
3275
3397
  name,
3276
3398
  enabled: isServerEnabled(spec),
3277
3399
  transport: spec.transport,
3278
3400
  target: describeServerTarget(spec),
3279
3401
  authBindings: buildAuthBindings(spec),
3280
- clients: buildClientMappings(config, managedIndex, name)
3402
+ clients: buildClientMappings(config, managedIndex, name),
3403
+ tokenCount
3281
3404
  };
3405
+ if (serverEntry.enabled && tokenCount) {
3406
+ const matchingProject = projectEntries.find((p) => name.startsWith(`${p.name}.`));
3407
+ if (matchingProject) {
3408
+ totalProjectTokens[matchingProject.path] = (totalProjectTokens[matchingProject.path] ?? 0) + tokenCount.total;
3409
+ } else {
3410
+ totalGlobalTokens += tokenCount.total;
3411
+ }
3412
+ }
3413
+ return serverEntry;
3282
3414
  });
3283
3415
  return {
3284
- gatewayUrl: getGatewayUrl(config),
3416
+ gatewayUrl,
3285
3417
  daemon,
3286
3418
  upstreamCount: servers.length,
3287
3419
  servers,
3288
- clients: config.clients
3420
+ clients: config.clients,
3421
+ projects: config.projects,
3422
+ totalGlobalTokens,
3423
+ totalProjectTokens
3289
3424
  };
3290
3425
  }
3291
3426
 
3292
- export { getConfigPath, getManagedIndexPath, defaultConfig, loadConfig, saveConfig, addServer, removeServer, updateServer, setServerEnabled, SecretsManager, readSecretValueFromStdin, probeHttpAuthRequirement, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
3427
+ export { listSkills, getSkill, saveSkill, deleteSkill, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };