@kwonye/mcpx 0.1.75 → 0.1.79

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 (48) hide show
  1. package/dist/adapters/claude-desktop.d.ts.map +1 -1
  2. package/dist/adapters/claude.d.ts.map +1 -1
  3. package/dist/adapters/cline.d.ts.map +1 -1
  4. package/dist/adapters/cursor.d.ts.map +1 -1
  5. package/dist/adapters/vscode.d.ts.map +1 -1
  6. package/dist/cli-7edpy4nk.js +514 -0
  7. package/dist/cli-fsf33sgp.js +105 -0
  8. package/dist/{cli-q8de7zb0.js → cli-s4gspqdf.js} +4 -1
  9. package/dist/cli-s4py8ckf.js +187 -0
  10. package/dist/{cli-e9hdd44n.js → cli-w4m7471h.js} +933 -542
  11. package/dist/{cli-fh6jh7fs.js → cli-ydr75jw4.js} +934 -543
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +381 -56
  14. package/dist/config-79j3jpra.js +20 -0
  15. package/dist/core/add-command.d.ts +7 -0
  16. package/dist/core/add-command.d.ts.map +1 -0
  17. package/dist/core/auth-probe.d.ts +1 -0
  18. package/dist/core/auth-probe.d.ts.map +1 -1
  19. package/dist/core/config-repair.d.ts +3 -0
  20. package/dist/core/config-repair.d.ts.map +1 -0
  21. package/dist/core/config.d.ts +19 -1
  22. package/dist/core/config.d.ts.map +1 -1
  23. package/dist/core/daemon.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +7 -2
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +50 -12
  27. package/dist/core/oauth.d.ts +12 -0
  28. package/dist/core/oauth.d.ts.map +1 -0
  29. package/dist/core/paths.d.ts +2 -0
  30. package/dist/core/paths.d.ts.map +1 -1
  31. package/dist/core/registry-client.d.ts +76 -0
  32. package/dist/core/registry-client.d.ts.map +1 -0
  33. package/dist/core/registry.d.ts +2 -0
  34. package/dist/core/registry.d.ts.map +1 -1
  35. package/dist/core/secrets.d.ts.map +1 -1
  36. package/dist/core/spawn-env.d.ts +2 -0
  37. package/dist/core/spawn-env.d.ts.map +1 -0
  38. package/dist/core/status.d.ts +6 -2
  39. package/dist/core/status.d.ts.map +1 -1
  40. package/dist/core/sync.d.ts.map +1 -1
  41. package/dist/gateway/server.d.ts +21 -0
  42. package/dist/gateway/server.d.ts.map +1 -1
  43. package/dist/registry-1s14wd58.js +24 -0
  44. package/dist/types.d.ts +13 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/package.json +2 -1
@@ -1,238 +1,46 @@
1
1
  import {
2
- isServerEnabled,
3
- normalizeServerSpecEnabled
4
- } from "./cli-q8de7zb0.js";
5
-
6
- // src/core/paths.ts
7
- import os from "node:os";
8
- import path from "node:path";
9
- 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 getSkillsDir() {
32
- return path.join(getConfigRoot(), "mcpx", "skills");
33
- }
34
- function getManagedIndexPath() {
35
- return path.join(getDataRoot(), "mcpx", "managed-index.json");
36
- }
37
- function getSecretNamesPath() {
38
- return path.join(getDataRoot(), "mcpx", "secret-names.json");
39
- }
40
- function getRuntimeDir() {
41
- return path.join(getStateRoot(), "mcpx", "runtime");
42
- }
43
- function getLogDir() {
44
- return path.join(getStateRoot(), "mcpx", "logs");
45
- }
46
- function getPidPath() {
47
- return path.join(getRuntimeDir(), "daemon.pid");
48
- }
49
- function getLogPath() {
50
- return path.join(getLogDir(), "daemon.log");
51
- }
52
- function getUpdatesDir() {
53
- return path.join(getDataRoot(), "mcpx", "updates");
54
- }
55
- function getStagedVersionPath() {
56
- return path.join(getUpdatesDir(), "staged-version.json");
57
- }
58
- function getUpdateLockPath() {
59
- return path.join(getUpdatesDir(), ".update.lock");
60
- }
61
- function ensureDir(dirPath) {
62
- fs.mkdirSync(dirPath, { recursive: true });
63
- }
64
- function ensureParentDir(filePath) {
65
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
66
- }
67
-
68
- // src/core/config.ts
69
- import { z } from "zod";
70
-
71
- // src/util/fs.ts
72
- import fs2 from "node:fs";
73
- import path2 from "node:path";
74
- import crypto from "node:crypto";
75
- function readJsonFile(filePath, fallback) {
76
- if (!fs2.existsSync(filePath)) {
77
- return fallback;
78
- }
79
- try {
80
- const raw = fs2.readFileSync(filePath, "utf8");
81
- return JSON.parse(raw);
82
- } catch {
83
- return fallback;
84
- }
85
- }
86
- function writeJsonAtomic(filePath, data) {
87
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
88
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
89
- const content = `${JSON.stringify(data, null, 2)}
90
- `;
91
- fs2.writeFileSync(tempPath, content, { mode: 384 });
92
- fs2.renameSync(tempPath, filePath);
93
- }
94
- function sha256(value) {
95
- return crypto.createHash("sha256").update(value).digest("hex");
96
- }
97
-
98
- // src/core/config.ts
99
- var clientStateSchema = z.object({
100
- status: z.enum(["SYNCED", "UNSUPPORTED_HTTP", "ERROR", "SKIPPED"]),
101
- lastSyncAt: z.string().optional(),
102
- message: z.string().optional(),
103
- configPath: z.string().optional()
104
- });
105
- var httpServerSchema = z.object({
106
- transport: z.literal("http"),
107
- url: z.url(),
108
- headers: z.record(z.string(), z.string()).optional(),
109
- enabled: z.boolean().default(true)
110
- });
111
- var stdioServerSchema = z.object({
112
- transport: z.literal("stdio"),
113
- command: z.string().min(1),
114
- args: z.array(z.string()).optional(),
115
- env: z.record(z.string(), z.string()).optional(),
116
- cwd: z.string().min(1).optional(),
117
- enabled: z.boolean().default(true)
118
- });
119
- var serverSchema = z.discriminatedUnion("transport", [httpServerSchema, stdioServerSchema]);
120
- var configSchema = z.object({
121
- schemaVersion: z.literal(1).default(1),
122
- gateway: z.object({
123
- port: z.number().int().min(1).max(65535).default(37373),
124
- tokenRef: z.string().default("secret://local_gateway_token"),
125
- autoStart: z.boolean().default(true)
126
- }).default({
127
- port: 37373,
128
- tokenRef: "secret://local_gateway_token",
129
- autoStart: true
130
- }),
131
- servers: z.record(z.string(), serverSchema).default({}),
132
- clients: z.record(z.string(), clientStateSchema).default({})
133
- });
134
- function defaultConfig() {
135
- return {
136
- schemaVersion: 1,
137
- gateway: {
138
- port: 37373,
139
- tokenRef: "secret://local_gateway_token",
140
- autoStart: true
141
- },
142
- servers: {},
143
- clients: {}
144
- };
145
- }
146
- function loadConfig(configPath = getConfigPath()) {
147
- const raw = readJsonFile(configPath, defaultConfig());
148
- const parsed = configSchema.safeParse(raw);
149
- if (!parsed.success) {
150
- return defaultConfig();
151
- }
152
- const clientEntries = {};
153
- for (const [key, value] of Object.entries(parsed.data.clients)) {
154
- if (["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen"].includes(key)) {
155
- clientEntries[key] = value;
156
- }
157
- }
158
- const servers = Object.fromEntries(Object.entries(parsed.data.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
159
- return {
160
- schemaVersion: 1,
161
- gateway: parsed.data.gateway,
162
- servers,
163
- clients: clientEntries
164
- };
165
- }
166
- function saveConfig(config, configPath = getConfigPath()) {
167
- config.servers = Object.fromEntries(Object.entries(config.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
168
- writeJsonAtomic(configPath, config);
169
- }
170
-
171
- // src/core/registry.ts
172
- function validateServerName(name) {
173
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(name)) {
174
- throw new Error("Invalid server name. Use 1-63 chars: letters, numbers, '.', '-', '_' and start with alphanumeric.");
175
- }
176
- }
177
- function addServer(config, name, spec, force = false) {
178
- validateServerName(name);
179
- if (config.servers[name] && !force) {
180
- throw new Error(`Server "${name}" already exists. Use --force to overwrite.`);
181
- }
182
- config.servers[name] = normalizeServerSpecEnabled(spec);
183
- }
184
- function removeServer(config, name, force = false) {
185
- if (!config.servers[name] && !force) {
186
- throw new Error(`Server "${name}" does not exist.`);
187
- }
188
- delete config.servers[name];
189
- }
190
- function updateServer(config, name, spec) {
191
- validateServerName(name);
192
- if (!config.servers[name]) {
193
- throw new Error(`Server "${name}" does not exist.`);
194
- }
195
- config.servers[name] = normalizeServerSpecEnabled(spec);
196
- }
197
- function setServerEnabled(config, name, enabled) {
198
- const existing = config.servers[name];
199
- if (!existing) {
200
- throw new Error(`Server "${name}" does not exist.`);
201
- }
202
- config.servers[name] = {
203
- ...existing,
204
- enabled
205
- };
206
- }
207
- function getGatewayTokenSecretName(config) {
208
- if (!config.gateway.tokenRef.startsWith("secret://")) {
209
- return "local_gateway_token";
210
- }
211
- return config.gateway.tokenRef.slice("secret://".length) || "local_gateway_token";
212
- }
213
- function ensureGatewayToken(config, secrets) {
214
- const secretName = getGatewayTokenSecretName(config);
215
- const existing = secrets.getSecret(secretName);
216
- if (existing) {
217
- return existing;
218
- }
219
- return secrets.rotateLocalToken(secretName);
220
- }
2
+ loadConfig,
3
+ loadMergedConfig,
4
+ saveConfig
5
+ } from "./cli-7edpy4nk.js";
6
+ import {
7
+ SecretsManager,
8
+ ensureGatewayToken,
9
+ getGatewayTokenSecretName
10
+ } from "./cli-s4py8ckf.js";
11
+ import {
12
+ ensureDir,
13
+ ensureParentDir,
14
+ getLogPath,
15
+ getManagedIndexPath,
16
+ getPidPath,
17
+ getSkillsDir,
18
+ getStagedVersionPath,
19
+ getUpdateLockPath,
20
+ getUpdatesDir,
21
+ homeDir,
22
+ readJsonFile,
23
+ sha256,
24
+ writeJsonAtomic
25
+ } from "./cli-fsf33sgp.js";
26
+ import {
27
+ isServerEnabled
28
+ } from "./cli-s4gspqdf.js";
221
29
 
222
30
  // src/core/skills.ts
223
- import fs3 from "node:fs";
224
- import path3 from "node:path";
31
+ import fs from "node:fs";
32
+ import path from "node:path";
225
33
  function listSkills() {
226
34
  const skillsDir = getSkillsDir();
227
- if (!fs3.existsSync(skillsDir)) {
35
+ if (!fs.existsSync(skillsDir)) {
228
36
  return [];
229
37
  }
230
- const files = fs3.readdirSync(skillsDir);
38
+ const files = fs.readdirSync(skillsDir);
231
39
  const skills = [];
232
40
  for (const file of files) {
233
41
  if (file.endsWith(".md")) {
234
- const id = path3.basename(file, ".md");
235
- const content = fs3.readFileSync(path3.join(skillsDir, file), "utf-8");
42
+ const id = path.basename(file, ".md");
43
+ const content = fs.readFileSync(path.join(skillsDir, file), "utf-8");
236
44
  skills.push({ id, content });
237
45
  }
238
46
  }
@@ -240,134 +48,52 @@ function listSkills() {
240
48
  }
241
49
  function getSkill(id) {
242
50
  const skillsDir = getSkillsDir();
243
- const filePath = path3.join(skillsDir, `${id}.md`);
244
- if (!fs3.existsSync(filePath)) {
51
+ const filePath = path.join(skillsDir, `${id}.md`);
52
+ if (!fs.existsSync(filePath)) {
245
53
  return null;
246
54
  }
247
- const content = fs3.readFileSync(filePath, "utf-8");
55
+ const content = fs.readFileSync(filePath, "utf-8");
248
56
  return { id, content };
249
57
  }
250
58
  function saveSkill(id, content) {
251
59
  const skillsDir = getSkillsDir();
252
60
  ensureDir(skillsDir);
253
- const filePath = path3.join(skillsDir, `${id}.md`);
254
- fs3.writeFileSync(filePath, content, "utf-8");
61
+ const filePath = path.join(skillsDir, `${id}.md`);
62
+ fs.writeFileSync(filePath, content, "utf-8");
255
63
  }
256
64
  function deleteSkill(id) {
257
65
  const skillsDir = getSkillsDir();
258
- const filePath = path3.join(skillsDir, `${id}.md`);
259
- if (fs3.existsSync(filePath)) {
260
- fs3.unlinkSync(filePath);
261
- }
262
- }
263
-
264
- // src/core/secrets.ts
265
- import { execFileSync } from "node:child_process";
266
- import crypto2 from "node:crypto";
267
- import fs4 from "node:fs";
268
- function parseSecretRef(ref) {
269
- if (!ref.startsWith("secret://")) {
270
- return null;
271
- }
272
- const name = ref.slice("secret://".length);
273
- return name.length > 0 ? name : null;
274
- }
275
-
276
- class SecretsManager {
277
- service;
278
- nameIndexPath;
279
- constructor(service = "mcpx", nameIndexPath = getSecretNamesPath()) {
280
- this.service = service;
281
- this.nameIndexPath = nameIndexPath;
282
- }
283
- loadNameIndex() {
284
- return readJsonFile(this.nameIndexPath, { names: [] });
285
- }
286
- saveNameIndex(index) {
287
- const deduped = Array.from(new Set(index.names)).sort();
288
- writeJsonAtomic(this.nameIndexPath, { names: deduped });
289
- }
290
- trackSecretName(name) {
291
- const index = this.loadNameIndex();
292
- if (!index.names.includes(name)) {
293
- index.names.push(name);
294
- this.saveNameIndex(index);
295
- }
296
- }
297
- untrackSecretName(name) {
298
- const index = this.loadNameIndex();
299
- index.names = index.names.filter((entry) => entry !== name);
300
- this.saveNameIndex(index);
301
- }
302
- listSecretNames() {
303
- return this.loadNameIndex().names;
304
- }
305
- setSecret(name, value) {
306
- if (process.platform !== "darwin") {
307
- throw new Error("OS keychain integration is currently implemented for macOS only.");
308
- }
309
- execFileSync("security", ["add-generic-password", "-U", "-a", name, "-s", this.service, "-w", value], {
310
- stdio: "ignore"
311
- });
312
- this.trackSecretName(name);
313
- }
314
- removeSecret(name) {
315
- if (process.platform === "darwin") {
316
- try {
317
- execFileSync("security", ["delete-generic-password", "-a", name, "-s", this.service], {
318
- stdio: "ignore"
319
- });
320
- } catch {}
321
- }
322
- this.untrackSecretName(name);
323
- }
324
- getSecret(name) {
325
- const envOverride = process.env[`MCPX_SECRET_${name}`];
326
- if (envOverride && envOverride.length > 0) {
327
- return envOverride;
328
- }
329
- if (process.platform !== "darwin") {
330
- return null;
331
- }
332
- try {
333
- return execFileSync("security", ["find-generic-password", "-w", "-a", name, "-s", this.service], {
334
- encoding: "utf8"
335
- }).trim();
336
- } catch {
337
- return null;
338
- }
339
- }
340
- resolveMaybeSecret(value) {
341
- const secretName = parseSecretRef(value);
342
- if (!secretName) {
343
- return value;
344
- }
345
- const secret = this.getSecret(secretName);
346
- if (!secret) {
347
- throw new Error(`Secret not found: ${secretName}`);
348
- }
349
- return secret;
350
- }
351
- rotateLocalToken(secretName = "local_gateway_token") {
352
- const token = crypto2.randomBytes(32).toString("base64url");
353
- this.setSecret(secretName, token);
354
- return token;
66
+ const filePath = path.join(skillsDir, `${id}.md`);
67
+ if (fs.existsSync(filePath)) {
68
+ fs.unlinkSync(filePath);
355
69
  }
356
70
  }
357
- function readSecretValueFromStdin() {
358
- if (process.stdin.isTTY) {
359
- throw new Error("No value provided. Use --value or pipe a value via stdin.");
360
- }
361
- const content = fs4.readFileSync(process.stdin.fd, "utf8").trim();
362
- if (content.length === 0) {
363
- throw new Error("Empty secret value provided.");
364
- }
365
- return content;
366
- }
367
71
 
368
72
  // src/core/auth-probe.ts
369
73
  var JSON_RPC_VERSION = "2.0";
370
74
  var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
75
+ var AUTH_KEYWORDS = [
76
+ "unauthorized",
77
+ "unauthenticated",
78
+ "forbidden",
79
+ "authentication",
80
+ "authorization",
81
+ "api key",
82
+ "api_key",
83
+ "apikey",
84
+ "credentials",
85
+ "access denied",
86
+ "auth required",
87
+ "x-api-key",
88
+ "invalid key",
89
+ "missing header",
90
+ "not authenticated",
91
+ "auth token"
92
+ ];
93
+ function isAuthRelatedMessage(message) {
94
+ const lower = message.toLowerCase();
95
+ return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
96
+ }
371
97
  function resolveHeaders(spec, secrets) {
372
98
  const headers = {
373
99
  "content-type": "application/json",
@@ -378,6 +104,13 @@ function resolveHeaders(spec, secrets) {
378
104
  }
379
105
  return headers;
380
106
  }
107
+ function isOAuthLikelyChallenge(wwwAuthenticate) {
108
+ if (!wwwAuthenticate) {
109
+ return false;
110
+ }
111
+ const lower = wwwAuthenticate.toLowerCase();
112
+ return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
113
+ }
381
114
  async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS) {
382
115
  const timeoutController = new AbortController;
383
116
  const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
@@ -395,12 +128,30 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
395
128
  signal: timeoutController.signal
396
129
  });
397
130
  if (response.status === 401 || response.status === 403) {
131
+ const wwwAuthenticate = response.headers.get("www-authenticate");
398
132
  return {
399
133
  authRequired: true,
134
+ oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
400
135
  status: response.status,
401
- wwwAuthenticate: response.headers.get("www-authenticate") ?? undefined
136
+ wwwAuthenticate: wwwAuthenticate ?? undefined
402
137
  };
403
138
  }
139
+ if (response.status === 200) {
140
+ try {
141
+ const body = await response.json();
142
+ const err = body?.error;
143
+ if (err && typeof err === "object" && !("result" in body)) {
144
+ const message = typeof err.message === "string" ? err.message : "";
145
+ if (message && isAuthRelatedMessage(message)) {
146
+ return {
147
+ authRequired: true,
148
+ status: response.status,
149
+ error: `JSON-RPC error: ${message}`
150
+ };
151
+ }
152
+ }
153
+ } catch {}
154
+ }
404
155
  return {
405
156
  authRequired: false,
406
157
  status: response.status
@@ -416,9 +167,132 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
416
167
  }
417
168
  }
418
169
 
170
+ // src/core/registry-client.ts
171
+ var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
172
+ async function fetchRegistryServerDetail(name) {
173
+ const encoded = encodeURIComponent(name);
174
+ const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
175
+ headers: { accept: "application/json" }
176
+ });
177
+ if (!response.ok) {
178
+ throw new Error(`Registry API error: ${response.status}`);
179
+ }
180
+ const data = await response.json();
181
+ return data.server;
182
+ }
183
+ var PACKAGE_PRIORITY = {
184
+ npm: 1,
185
+ pypi: 2,
186
+ nuget: 3,
187
+ oci: 4,
188
+ mcpb: 5
189
+ };
190
+ var RUNTIME_HINT = {
191
+ npm: "npx",
192
+ pypi: "uvx",
193
+ nuget: "dnx"
194
+ };
195
+ function selectBestPackage(packages = [], remotes = []) {
196
+ const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
197
+ if (stdioPkgs.length > 0) {
198
+ return { kind: "package", package: stdioPkgs[0] };
199
+ }
200
+ const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
201
+ if (httpRemotes.length > 0) {
202
+ return { kind: "remote", remote: httpRemotes[0] };
203
+ }
204
+ if (packages.length > 0) {
205
+ return { kind: "package", package: packages[0] };
206
+ }
207
+ if (remotes.length > 0) {
208
+ return { kind: "remote", remote: remotes[0] };
209
+ }
210
+ throw new Error("Server has no packages or remotes");
211
+ }
212
+ function extractRequiredInputs(option) {
213
+ const inputs = [];
214
+ if (option.kind === "package" && option.package?.environmentVariables) {
215
+ for (const env of option.package.environmentVariables) {
216
+ if (env.isRequired && !env.default) {
217
+ inputs.push({
218
+ name: env.name,
219
+ description: env.description,
220
+ isSecret: env.isSecret ?? false,
221
+ kind: "env"
222
+ });
223
+ }
224
+ }
225
+ for (const arg of option.package.packageArguments ?? []) {
226
+ if (arg.isRequired && !arg.value && !arg.default) {
227
+ inputs.push({
228
+ name: arg.name ?? arg.valueHint ?? "arg",
229
+ description: arg.description,
230
+ isSecret: false,
231
+ kind: "arg"
232
+ });
233
+ }
234
+ }
235
+ }
236
+ if (option.kind === "remote" && option.remote?.headers) {
237
+ for (const header of option.remote.headers) {
238
+ if (header.isRequired && !header.default) {
239
+ inputs.push({
240
+ name: header.name,
241
+ description: header.description,
242
+ isSecret: header.isSecret ?? false,
243
+ kind: "header"
244
+ });
245
+ }
246
+ }
247
+ }
248
+ return inputs;
249
+ }
250
+ function mapRegistryToSpec(_name, option, resolvedValues) {
251
+ if (option.kind === "remote") {
252
+ const headers = {};
253
+ for (const header of option.remote?.headers ?? []) {
254
+ const value = resolvedValues[header.name] ?? header.default;
255
+ if (value)
256
+ headers[header.name] = value;
257
+ }
258
+ return {
259
+ transport: "http",
260
+ url: option.remote.url,
261
+ ...Object.keys(headers).length > 0 ? { headers } : {}
262
+ };
263
+ }
264
+ const pkg = option.package;
265
+ const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
266
+ const args = [];
267
+ const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
268
+ args.push(identifier);
269
+ for (const arg of pkg.packageArguments ?? []) {
270
+ const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
271
+ if (!value)
272
+ continue;
273
+ if (arg.type === "named" && arg.name) {
274
+ args.push(arg.name, value);
275
+ } else {
276
+ args.push(value);
277
+ }
278
+ }
279
+ const env = {};
280
+ for (const envVar of pkg.environmentVariables ?? []) {
281
+ const value = resolvedValues[envVar.name] ?? envVar.default;
282
+ if (value)
283
+ env[envVar.name] = value;
284
+ }
285
+ return {
286
+ transport: "stdio",
287
+ command: runtime,
288
+ args,
289
+ ...Object.keys(env).length > 0 ? { env } : {}
290
+ };
291
+ }
292
+
419
293
  // src/adapters/claude.ts
420
- import path4 from "node:path";
421
- import { z as z2 } from "zod";
294
+ import path2 from "node:path";
295
+ import { z } from "zod";
422
296
 
423
297
  // src/adapters/utils/index.ts
424
298
  function getManagedEntryNames(managedIndex, clientId) {
@@ -546,22 +420,22 @@ function syncMcpExcludedArray(raw, managedNames, managedEntries) {
546
420
  }
547
421
 
548
422
  // src/adapters/claude.ts
549
- var stringMapSchema = z2.record(z2.string(), z2.string());
550
- var claudeEntrySchema = z2.object({
551
- type: z2.string().optional(),
552
- url: z2.string().min(1).optional(),
423
+ var stringMapSchema = z.record(z.string(), z.string());
424
+ var claudeEntrySchema = z.object({
425
+ type: z.string().optional(),
426
+ url: z.string().min(1).optional(),
553
427
  headers: stringMapSchema.optional(),
554
- command: z2.string().min(1).optional(),
555
- args: z2.array(z2.string()).optional(),
428
+ command: z.string().min(1).optional(),
429
+ args: z.array(z.string()).optional(),
556
430
  env: stringMapSchema.optional(),
557
- cwd: z2.string().min(1).optional(),
558
- disabled: z2.boolean().optional()
431
+ cwd: z.string().min(1).optional(),
432
+ disabled: z.boolean().optional()
559
433
  }).passthrough();
560
434
 
561
435
  class ClaudeAdapter {
562
436
  id = "claude";
563
437
  detectConfigPath() {
564
- return path4.join(homeDir(), ".claude.json");
438
+ return path2.join(homeDir(), ".claude.json");
565
439
  }
566
440
  supportsHttp() {
567
441
  return true;
@@ -637,20 +511,22 @@ class ClaudeAdapter {
637
511
  try {
638
512
  const raw = readJsonFile(configPath, {});
639
513
  const managedNames = options.managedEntries.map((entry) => entry.name);
640
- const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
514
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
515
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
641
516
  type: "http",
642
517
  url: entry.url,
643
518
  headers: entry.headers
644
519
  }]));
520
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
645
521
  const topLevelServers = raw.mcpServers ?? {};
646
522
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
647
- for (const name of managedNames) {
523
+ for (const name of enabledManagedNames) {
648
524
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
649
525
  if (topLevelConflict) {
650
526
  return errorResult(this.id, configPath, topLevelConflict);
651
527
  }
652
528
  }
653
- pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, managedNames);
529
+ pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
654
530
  for (const [name, entry] of Object.entries(serverEntries)) {
655
531
  topLevelServers[name] = entry;
656
532
  }
@@ -673,24 +549,24 @@ class ClaudeAdapter {
673
549
  }
674
550
 
675
551
  // src/adapters/claude-desktop.ts
676
- import { execFileSync as execFileSync2 } from "node:child_process";
677
- import path5 from "node:path";
678
- import { z as z3 } from "zod";
679
- var stringMapSchema2 = z3.record(z3.string(), z3.string());
680
- var claudeDesktopEntrySchema = z3.object({
681
- type: z3.string().optional(),
682
- url: z3.string().min(1).optional(),
552
+ import { execFileSync } from "node:child_process";
553
+ import path3 from "node:path";
554
+ import { z as z2 } from "zod";
555
+ var stringMapSchema2 = z2.record(z2.string(), z2.string());
556
+ var claudeDesktopEntrySchema = z2.object({
557
+ type: z2.string().optional(),
558
+ url: z2.string().min(1).optional(),
683
559
  headers: stringMapSchema2.optional(),
684
- command: z3.string().min(1).optional(),
685
- args: z3.array(z3.string()).optional(),
560
+ command: z2.string().min(1).optional(),
561
+ args: z2.array(z2.string()).optional(),
686
562
  env: stringMapSchema2.optional(),
687
- cwd: z3.string().min(1).optional(),
688
- disabled: z3.boolean().optional()
563
+ cwd: z2.string().min(1).optional(),
564
+ disabled: z2.boolean().optional()
689
565
  }).passthrough();
690
566
  function buildProxyEntry(entry) {
691
567
  const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
692
568
  try {
693
- const mcpxPath = execFileSync2("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
569
+ const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
694
570
  if (mcpxPath)
695
571
  return { command: mcpxPath, args: ["proxy", upstreamName] };
696
572
  } catch {}
@@ -700,7 +576,13 @@ function buildProxyEntry(entry) {
700
576
  class ClaudeDesktopAdapter {
701
577
  id = "claude-desktop";
702
578
  detectConfigPath() {
703
- return path5.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
579
+ if (process.platform === "linux") {
580
+ return path3.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
581
+ }
582
+ if (process.platform === "win32") {
583
+ return path3.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
584
+ }
585
+ return path3.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
704
586
  }
705
587
  supportsHttp() {
706
588
  return true;
@@ -786,9 +668,9 @@ class ClaudeDesktopAdapter {
786
668
  }
787
669
  raw.mcpServers = topLevelServers;
788
670
  writeJsonAtomic(configPath, raw);
789
- setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => {
671
+ setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
790
672
  const { command, args } = buildProxyEntry(entry);
791
- return [entry.name, JSON.stringify({ command, args, enabled: entry.enabled })];
673
+ return [entry.name, JSON.stringify({ command, args })];
792
674
  })));
793
675
  return okResult(this.id, configPath);
794
676
  } catch (error) {
@@ -798,31 +680,31 @@ class ClaudeDesktopAdapter {
798
680
  }
799
681
 
800
682
  // src/adapters/codex.ts
801
- import path6 from "node:path";
802
- import fs5 from "node:fs";
683
+ import path4 from "node:path";
684
+ import fs2 from "node:fs";
803
685
  import { parse, stringify } from "@iarna/toml";
804
- import { z as z4 } from "zod";
805
- var stringMapSchema3 = z4.preprocess((value) => {
686
+ import { z as z3 } from "zod";
687
+ var stringMapSchema3 = z3.preprocess((value) => {
806
688
  if (!value || typeof value !== "object" || Array.isArray(value)) {
807
689
  return value;
808
690
  }
809
691
  return Object.fromEntries(Object.entries(value));
810
- }, z4.record(z4.string(), z4.string()));
811
- var codexEntrySchema = z4.object({
812
- enabled: z4.boolean().optional(),
813
- url: z4.string().min(1).optional(),
692
+ }, z3.record(z3.string(), z3.string()));
693
+ var codexEntrySchema = z3.object({
694
+ enabled: z3.boolean().optional(),
695
+ url: z3.string().min(1).optional(),
814
696
  headers: stringMapSchema3.optional(),
815
697
  http_headers: stringMapSchema3.optional(),
816
- command: z4.string().min(1).optional(),
817
- args: z4.array(z4.string()).optional(),
698
+ command: z3.string().min(1).optional(),
699
+ args: z3.array(z3.string()).optional(),
818
700
  env: stringMapSchema3.optional(),
819
- cwd: z4.string().min(1).optional()
701
+ cwd: z3.string().min(1).optional()
820
702
  }).passthrough();
821
703
 
822
704
  class CodexAdapter {
823
705
  id = "codex";
824
706
  detectConfigPath() {
825
- return path6.join(homeDir(), ".codex", "config.toml");
707
+ return path4.join(homeDir(), ".codex", "config.toml");
826
708
  }
827
709
  supportsHttp() {
828
710
  return true;
@@ -830,10 +712,10 @@ class CodexAdapter {
830
712
  scanForImports(_config, managedIndex) {
831
713
  const configPath = this.detectConfigPath();
832
714
  const result = emptyImportScan(this.id, configPath ?? undefined);
833
- if (!configPath || !fs5.existsSync(configPath)) {
715
+ if (!configPath || !fs2.existsSync(configPath)) {
834
716
  return result;
835
717
  }
836
- const existing = fs5.readFileSync(configPath, "utf8");
718
+ const existing = fs2.readFileSync(configPath, "utf8");
837
719
  const doc = existing.trim().length > 0 ? parse(existing) : {};
838
720
  const mcpServers = doc.mcp_servers ?? {};
839
721
  for (const [name, rawEntry] of Object.entries(mcpServers)) {
@@ -888,7 +770,7 @@ class CodexAdapter {
888
770
  return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
889
771
  }
890
772
  try {
891
- const existing = fs5.existsSync(configPath) ? fs5.readFileSync(configPath, "utf8") : "";
773
+ const existing = fs2.existsSync(configPath) ? fs2.readFileSync(configPath, "utf8") : "";
892
774
  const doc = existing.trim().length > 0 ? parse(existing) : {};
893
775
  const mcpServers = doc.mcp_servers ?? {};
894
776
  removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
@@ -909,10 +791,10 @@ class CodexAdapter {
909
791
  mcpServers[name] = entry;
910
792
  }
911
793
  doc.mcp_servers = mcpServers;
912
- fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
794
+ fs2.mkdirSync(path4.dirname(configPath), { recursive: true });
913
795
  const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
914
- fs5.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
915
- fs5.renameSync(tmpPath, configPath);
796
+ fs2.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
797
+ fs2.renameSync(tmpPath, configPath);
916
798
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
917
799
  return okResult(this.id, configPath);
918
800
  } catch (error) {
@@ -922,24 +804,30 @@ class CodexAdapter {
922
804
  }
923
805
 
924
806
  // src/adapters/cursor.ts
925
- import path7 from "node:path";
926
- import { z as z5 } from "zod";
927
- var stringMapSchema4 = z5.record(z5.string(), z5.string());
928
- var cursorEntrySchema = z5.object({
929
- type: z5.string().optional(),
930
- url: z5.string().min(1).optional(),
807
+ import path5 from "node:path";
808
+ import { z as z4 } from "zod";
809
+ var stringMapSchema4 = z4.record(z4.string(), z4.string());
810
+ var cursorEntrySchema = z4.object({
811
+ type: z4.string().optional(),
812
+ url: z4.string().min(1).optional(),
931
813
  headers: stringMapSchema4.optional(),
932
- command: z5.string().min(1).optional(),
933
- args: z5.array(z5.string()).optional(),
814
+ command: z4.string().min(1).optional(),
815
+ args: z4.array(z4.string()).optional(),
934
816
  env: stringMapSchema4.optional(),
935
- cwd: z5.string().min(1).optional(),
936
- disabled: z5.boolean().optional()
817
+ cwd: z4.string().min(1).optional(),
818
+ disabled: z4.boolean().optional()
937
819
  }).passthrough();
938
820
 
939
821
  class CursorAdapter {
940
822
  id = "cursor";
941
823
  detectConfigPath() {
942
- return path7.join(homeDir(), "Library", "Application Support", "Cursor", "User", "mcp.json");
824
+ if (process.platform === "linux") {
825
+ return path5.join(homeDir(), ".config", "Cursor", "User", "mcp.json");
826
+ }
827
+ if (process.platform === "win32") {
828
+ return path5.join(process.env.APPDATA ?? "", "Cursor", "User", "mcp.json");
829
+ }
830
+ return path5.join(homeDir(), "Library", "Application Support", "Cursor", "User", "mcp.json");
943
831
  }
944
832
  supportsHttp() {
945
833
  return true;
@@ -1045,24 +933,36 @@ class CursorAdapter {
1045
933
  }
1046
934
 
1047
935
  // src/adapters/cline.ts
1048
- import path8 from "node:path";
1049
- import fs6 from "node:fs";
1050
- import { z as z6 } from "zod";
1051
- var stringMapSchema5 = z6.record(z6.string(), z6.string());
1052
- var clineEntrySchema = z6.object({
1053
- transportType: z6.string().optional(),
1054
- url: z6.string().min(1).optional(),
936
+ import path6 from "node:path";
937
+ import fs3 from "node:fs";
938
+ import { z as z5 } from "zod";
939
+ var stringMapSchema5 = z5.record(z5.string(), z5.string());
940
+ var clineEntrySchema = z5.object({
941
+ transportType: z5.string().optional(),
942
+ url: z5.string().min(1).optional(),
1055
943
  headers: stringMapSchema5.optional(),
1056
- command: z6.string().min(1).optional(),
1057
- args: z6.array(z6.string()).optional(),
944
+ command: z5.string().min(1).optional(),
945
+ args: z5.array(z5.string()).optional(),
1058
946
  env: stringMapSchema5.optional(),
1059
- cwd: z6.string().min(1).optional(),
1060
- disabled: z6.boolean().optional()
947
+ cwd: z5.string().min(1).optional(),
948
+ disabled: z5.boolean().optional()
1061
949
  }).passthrough();
1062
950
  function getCandidatePaths() {
951
+ if (process.platform === "linux") {
952
+ return [
953
+ path6.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
954
+ path6.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
955
+ ];
956
+ }
957
+ if (process.platform === "win32") {
958
+ return [
959
+ path6.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
960
+ path6.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
961
+ ];
962
+ }
1063
963
  return [
1064
- path8.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1065
- path8.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
964
+ path6.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
965
+ path6.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
1066
966
  ];
1067
967
  }
1068
968
 
@@ -1071,7 +971,7 @@ class ClineAdapter {
1071
971
  detectConfigPath() {
1072
972
  const candidates = getCandidatePaths();
1073
973
  for (const candidate of candidates) {
1074
- if (fs6.existsSync(candidate)) {
974
+ if (fs3.existsSync(candidate)) {
1075
975
  return candidate;
1076
976
  }
1077
977
  }
@@ -1083,7 +983,7 @@ class ClineAdapter {
1083
983
  scanForImports(_config, managedIndex) {
1084
984
  const configPath = this.detectConfigPath();
1085
985
  const result = emptyImportScan(this.id, configPath ?? undefined);
1086
- if (!configPath || !fs6.existsSync(configPath)) {
986
+ if (!configPath || !fs3.existsSync(configPath)) {
1087
987
  return result;
1088
988
  }
1089
989
  const raw = readJsonFile(configPath, {});
@@ -1141,6 +1041,9 @@ class ClineAdapter {
1141
1041
  if (!configPath) {
1142
1042
  return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
1143
1043
  }
1044
+ if (!fs3.existsSync(configPath)) {
1045
+ return okResult(this.id, configPath, "Cline config not found; skipping sync.");
1046
+ }
1144
1047
  try {
1145
1048
  const raw = readJsonFile(configPath, {});
1146
1049
  const servers = {
@@ -1178,24 +1081,24 @@ class ClineAdapter {
1178
1081
  }
1179
1082
 
1180
1083
  // src/adapters/kiro.ts
1181
- import fs7 from "node:fs";
1182
- import path9 from "node:path";
1183
- import { z as z7 } from "zod";
1184
- var stringMapSchema6 = z7.record(z7.string(), z7.string());
1185
- var kiroEntrySchema = z7.object({
1186
- url: z7.string().min(1).optional(),
1084
+ import fs4 from "node:fs";
1085
+ import path7 from "node:path";
1086
+ import { z as z6 } from "zod";
1087
+ var stringMapSchema6 = z6.record(z6.string(), z6.string());
1088
+ var kiroEntrySchema = z6.object({
1089
+ url: z6.string().min(1).optional(),
1187
1090
  headers: stringMapSchema6.optional(),
1188
- command: z7.string().min(1).optional(),
1189
- args: z7.array(z7.string()).optional(),
1091
+ command: z6.string().min(1).optional(),
1092
+ args: z6.array(z6.string()).optional(),
1190
1093
  env: stringMapSchema6.optional(),
1191
- cwd: z7.string().min(1).optional(),
1192
- disabled: z7.boolean().optional()
1094
+ cwd: z6.string().min(1).optional(),
1095
+ disabled: z6.boolean().optional()
1193
1096
  }).passthrough();
1194
1097
 
1195
1098
  class KiroAdapter {
1196
1099
  id = "kiro";
1197
1100
  detectConfigPath() {
1198
- return path9.join(homeDir(), ".kiro", "settings", "mcp.json");
1101
+ return path7.join(homeDir(), ".kiro", "settings", "mcp.json");
1199
1102
  }
1200
1103
  supportsHttp() {
1201
1104
  return true;
@@ -1295,46 +1198,46 @@ class KiroAdapter {
1295
1198
  }
1296
1199
  }
1297
1200
  syncSkills(skills) {
1298
- const skillsDir = path9.join(homeDir(), ".kiro", "skills");
1299
- if (!fs7.existsSync(skillsDir)) {
1300
- fs7.mkdirSync(skillsDir, { recursive: true });
1201
+ const skillsDir = path7.join(homeDir(), ".kiro", "skills");
1202
+ if (!fs4.existsSync(skillsDir)) {
1203
+ fs4.mkdirSync(skillsDir, { recursive: true });
1301
1204
  }
1302
- const existingFiles = fs7.readdirSync(skillsDir);
1205
+ const existingFiles = fs4.readdirSync(skillsDir);
1303
1206
  const managedIds = new Set(skills.map((s) => s.id));
1304
1207
  for (const file of existingFiles) {
1305
1208
  if (file.endsWith(".md")) {
1306
- const id = path9.basename(file, ".md");
1209
+ const id = path7.basename(file, ".md");
1307
1210
  if (!managedIds.has(id)) {
1308
- fs7.unlinkSync(path9.join(skillsDir, file));
1211
+ fs4.unlinkSync(path7.join(skillsDir, file));
1309
1212
  }
1310
1213
  }
1311
1214
  }
1312
1215
  for (const skill of skills) {
1313
- const targetPath = path9.join(skillsDir, `${skill.id}.md`);
1314
- fs7.writeFileSync(targetPath, skill.content, "utf-8");
1216
+ const targetPath = path7.join(skillsDir, `${skill.id}.md`);
1217
+ fs4.writeFileSync(targetPath, skill.content, "utf-8");
1315
1218
  }
1316
1219
  }
1317
1220
  }
1318
1221
 
1319
1222
  // src/adapters/opencode.ts
1320
- import path10 from "node:path";
1321
- import { z as z8 } from "zod";
1322
- var stringMapSchema7 = z8.record(z8.string(), z8.string());
1323
- var openCodeEntrySchema = z8.object({
1324
- type: z8.string().optional(),
1325
- url: z8.string().min(1).optional(),
1223
+ import path8 from "node:path";
1224
+ import { z as z7 } from "zod";
1225
+ var stringMapSchema7 = z7.record(z7.string(), z7.string());
1226
+ var openCodeEntrySchema = z7.object({
1227
+ type: z7.string().optional(),
1228
+ url: z7.string().min(1).optional(),
1326
1229
  headers: stringMapSchema7.optional(),
1327
- command: z8.string().min(1).optional(),
1328
- args: z8.array(z8.string()).optional(),
1230
+ command: z7.string().min(1).optional(),
1231
+ args: z7.array(z7.string()).optional(),
1329
1232
  env: stringMapSchema7.optional(),
1330
- cwd: z8.string().min(1).optional(),
1331
- enabled: z8.boolean().optional()
1233
+ cwd: z7.string().min(1).optional(),
1234
+ enabled: z7.boolean().optional()
1332
1235
  }).passthrough();
1333
1236
 
1334
1237
  class OpenCodeAdapter {
1335
1238
  id = "opencode";
1336
1239
  detectConfigPath() {
1337
- return path10.join(homeDir(), ".config", "opencode", "opencode.json");
1240
+ return path8.join(homeDir(), ".config", "opencode", "opencode.json");
1338
1241
  }
1339
1242
  supportsHttp() {
1340
1243
  return true;
@@ -1437,24 +1340,30 @@ class OpenCodeAdapter {
1437
1340
  }
1438
1341
 
1439
1342
  // src/adapters/vscode.ts
1440
- import path11 from "node:path";
1441
- import { z as z9 } from "zod";
1442
- var stringMapSchema8 = z9.record(z9.string(), z9.string());
1443
- var vscodeEntrySchema = z9.object({
1444
- type: z9.string().optional(),
1445
- url: z9.string().min(1).optional(),
1343
+ import path9 from "node:path";
1344
+ import { z as z8 } from "zod";
1345
+ var stringMapSchema8 = z8.record(z8.string(), z8.string());
1346
+ var vscodeEntrySchema = z8.object({
1347
+ type: z8.string().optional(),
1348
+ url: z8.string().min(1).optional(),
1446
1349
  headers: stringMapSchema8.optional(),
1447
- command: z9.string().min(1).optional(),
1448
- args: z9.array(z9.string()).optional(),
1350
+ command: z8.string().min(1).optional(),
1351
+ args: z8.array(z8.string()).optional(),
1449
1352
  env: stringMapSchema8.optional(),
1450
- cwd: z9.string().min(1).optional(),
1451
- disabled: z9.boolean().optional()
1353
+ cwd: z8.string().min(1).optional(),
1354
+ disabled: z8.boolean().optional()
1452
1355
  }).passthrough();
1453
1356
 
1454
1357
  class VsCodeAdapter {
1455
1358
  id = "vscode";
1456
1359
  detectConfigPath() {
1457
- return path11.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1360
+ if (process.platform === "linux") {
1361
+ return path9.join(homeDir(), ".config", "Code", "User", "mcp.json");
1362
+ }
1363
+ if (process.platform === "win32") {
1364
+ return path9.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1365
+ }
1366
+ return path9.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1458
1367
  }
1459
1368
  supportsHttp() {
1460
1369
  return true;
@@ -1560,24 +1469,24 @@ class VsCodeAdapter {
1560
1469
  }
1561
1470
 
1562
1471
  // src/adapters/qwen.ts
1563
- import fs8 from "node:fs";
1564
- import path12 from "node:path";
1565
- import { z as z10 } from "zod";
1566
- var stringMapSchema9 = z10.record(z10.string(), z10.string());
1567
- var qwenEntrySchema = z10.object({
1568
- httpUrl: z10.string().min(1).optional(),
1472
+ import fs5 from "node:fs";
1473
+ import path10 from "node:path";
1474
+ import { z as z9 } from "zod";
1475
+ var stringMapSchema9 = z9.record(z9.string(), z9.string());
1476
+ var qwenEntrySchema = z9.object({
1477
+ httpUrl: z9.string().min(1).optional(),
1569
1478
  headers: stringMapSchema9.optional(),
1570
- command: z10.string().min(1).optional(),
1571
- args: z10.array(z10.string()).optional(),
1479
+ command: z9.string().min(1).optional(),
1480
+ args: z9.array(z9.string()).optional(),
1572
1481
  env: stringMapSchema9.optional(),
1573
- cwd: z10.string().min(1).optional(),
1574
- disabled: z10.boolean().optional()
1482
+ cwd: z9.string().min(1).optional(),
1483
+ disabled: z9.boolean().optional()
1575
1484
  }).passthrough();
1576
1485
 
1577
1486
  class QwenAdapter {
1578
1487
  id = "qwen";
1579
1488
  detectConfigPath() {
1580
- return path12.join(homeDir(), ".qwen", "settings.json");
1489
+ return path10.join(homeDir(), ".qwen", "settings.json");
1581
1490
  }
1582
1491
  supportsHttp() {
1583
1492
  return true;
@@ -1674,23 +1583,23 @@ class QwenAdapter {
1674
1583
  }
1675
1584
  }
1676
1585
  syncSkills(skills) {
1677
- const skillsDir = path12.join(homeDir(), ".qwen", "skills");
1678
- if (!fs8.existsSync(skillsDir)) {
1679
- fs8.mkdirSync(skillsDir, { recursive: true });
1586
+ const skillsDir = path10.join(homeDir(), ".qwen", "skills");
1587
+ if (!fs5.existsSync(skillsDir)) {
1588
+ fs5.mkdirSync(skillsDir, { recursive: true });
1680
1589
  }
1681
- const existingFiles = fs8.readdirSync(skillsDir);
1590
+ const existingFiles = fs5.readdirSync(skillsDir);
1682
1591
  const managedIds = new Set(skills.map((s) => s.id));
1683
1592
  for (const file of existingFiles) {
1684
1593
  if (file.endsWith(".md")) {
1685
- const id = path12.basename(file, ".md");
1594
+ const id = path10.basename(file, ".md");
1686
1595
  if (!managedIds.has(id)) {
1687
- fs8.unlinkSync(path12.join(skillsDir, file));
1596
+ fs5.unlinkSync(path10.join(skillsDir, file));
1688
1597
  }
1689
1598
  }
1690
1599
  }
1691
1600
  for (const skill of skills) {
1692
- const targetPath = path12.join(skillsDir, `${skill.id}.md`);
1693
- fs8.writeFileSync(targetPath, skill.content, "utf-8");
1601
+ const targetPath = path10.join(skillsDir, `${skill.id}.md`);
1602
+ fs5.writeFileSync(targetPath, skill.content, "utf-8");
1694
1603
  }
1695
1604
  }
1696
1605
  }
@@ -1746,6 +1655,9 @@ function buildManagedEntries(config, gatewayUrl, localToken) {
1746
1655
  enabled: isServerEnabled(spec)
1747
1656
  }));
1748
1657
  }
1658
+ function isClientInternalServerCommand(candidate) {
1659
+ return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
1660
+ }
1749
1661
  function syncAllClients(config, secrets, targetClients) {
1750
1662
  const adapters = getAdapters();
1751
1663
  const managedIndexPath = getManagedIndexPath();
@@ -1764,6 +1676,16 @@ function syncAllClients(config, secrets, targetClients) {
1764
1676
  const scan = adapter.scanForImports(config, managedIndex);
1765
1677
  imports.skipped.push(...scan.skipped);
1766
1678
  for (const candidate of scan.candidates) {
1679
+ if (isClientInternalServerCommand(candidate)) {
1680
+ imports.skipped.push({
1681
+ clientId: candidate.clientId,
1682
+ configPath: candidate.configPath,
1683
+ sourceEntryName: candidate.sourceEntryName,
1684
+ serverName: candidate.serverName,
1685
+ reason: "client-internal server"
1686
+ });
1687
+ continue;
1688
+ }
1767
1689
  const existing = config.servers[candidate.serverName];
1768
1690
  if (!existing) {
1769
1691
  config.servers[candidate.serverName] = candidate.spec;
@@ -1963,13 +1885,299 @@ function maybePrefixBearer(target, value, raw = false) {
1963
1885
  return `Bearer ${value}`;
1964
1886
  }
1965
1887
 
1888
+ // src/core/spawn-env.ts
1889
+ import os from "node:os";
1890
+ import path11 from "node:path";
1891
+ import fs6 from "node:fs";
1892
+ function existingNvmCurrentBin() {
1893
+ const nvmDir = process.env.NVM_DIR ?? path11.join(os.homedir(), ".nvm");
1894
+ const current = path11.join(nvmDir, "current", "bin");
1895
+ return fs6.existsSync(current) ? current : null;
1896
+ }
1897
+ function buildEnrichedPath(current = process.env.PATH ?? "") {
1898
+ const candidates = [
1899
+ ...current.split(":"),
1900
+ "/opt/homebrew/bin",
1901
+ "/usr/local/bin",
1902
+ path11.join(os.homedir(), ".bun", "bin"),
1903
+ path11.join(os.homedir(), ".local", "bin"),
1904
+ existingNvmCurrentBin()
1905
+ ].filter((entry) => Boolean(entry));
1906
+ const seen = new Set;
1907
+ const entries = [];
1908
+ for (const candidate of candidates) {
1909
+ if (!candidate || seen.has(candidate)) {
1910
+ continue;
1911
+ }
1912
+ seen.add(candidate);
1913
+ entries.push(candidate);
1914
+ }
1915
+ return entries.join(":");
1916
+ }
1917
+
1918
+ // src/core/oauth.ts
1919
+ import http from "node:http";
1920
+ import crypto from "node:crypto";
1921
+ import {
1922
+ auth,
1923
+ discoverOAuthServerInfo,
1924
+ refreshAuthorization
1925
+ } from "@modelcontextprotocol/sdk/client/auth.js";
1926
+ function oauthSecretName(serverName, suffix) {
1927
+ return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
1928
+ }
1929
+ function readJsonSecret(secrets, name) {
1930
+ const raw = secrets.getSecret(name);
1931
+ if (!raw) {
1932
+ return;
1933
+ }
1934
+ try {
1935
+ return JSON.parse(raw);
1936
+ } catch {
1937
+ return;
1938
+ }
1939
+ }
1940
+ function writeJsonSecret(secrets, name, value) {
1941
+ secrets.setSecret(name, JSON.stringify(value));
1942
+ }
1943
+ function isOAuthReference(value) {
1944
+ return value.startsWith("oauth://");
1945
+ }
1946
+ function oauthReferenceServerName(value) {
1947
+ if (!isOAuthReference(value)) {
1948
+ throw new Error(`Not an OAuth reference: ${value}`);
1949
+ }
1950
+ return decodeURIComponent(value.slice("oauth://".length));
1951
+ }
1952
+ function tokensAreExpiring(tokens) {
1953
+ if (!tokens.tokens.expires_in) {
1954
+ return false;
1955
+ }
1956
+ const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
1957
+ return Date.now() >= expiresAt - 60000;
1958
+ }
1959
+
1960
+ class McpxOAuthProvider {
1961
+ #serverName;
1962
+ #secrets;
1963
+ #redirectUrlValue;
1964
+ #stateValue;
1965
+ constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
1966
+ this.#serverName = serverName;
1967
+ this.#secrets = secrets;
1968
+ this.#redirectUrlValue = redirectUrl;
1969
+ this.#stateValue = state;
1970
+ }
1971
+ get redirectUrl() {
1972
+ return this.#redirectUrlValue;
1973
+ }
1974
+ get clientMetadata() {
1975
+ if (!this.#redirectUrlValue) {
1976
+ return {
1977
+ redirect_uris: [],
1978
+ token_endpoint_auth_method: "none",
1979
+ grant_types: ["authorization_code", "refresh_token"],
1980
+ response_types: ["code"],
1981
+ client_name: "mcpx"
1982
+ };
1983
+ }
1984
+ return {
1985
+ redirect_uris: [this.#redirectUrlValue],
1986
+ token_endpoint_auth_method: "none",
1987
+ grant_types: ["authorization_code", "refresh_token"],
1988
+ response_types: ["code"],
1989
+ client_name: "mcpx"
1990
+ };
1991
+ }
1992
+ state() {
1993
+ return this.#stateValue;
1994
+ }
1995
+ clientInformation() {
1996
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
1997
+ }
1998
+ saveClientInformation(clientInformation) {
1999
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
2000
+ }
2001
+ tokens() {
2002
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
2003
+ }
2004
+ saveTokens(tokens) {
2005
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
2006
+ tokens,
2007
+ obtainedAt: Date.now()
2008
+ });
2009
+ }
2010
+ redirectToAuthorization(_authorizationUrl) {}
2011
+ saveCodeVerifier(codeVerifier) {
2012
+ this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
2013
+ }
2014
+ codeVerifier() {
2015
+ const verifier = this.#secrets.getSecret(oauthSecretName(this.#serverName, "verifier"));
2016
+ if (!verifier) {
2017
+ throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
2018
+ }
2019
+ return verifier;
2020
+ }
2021
+ saveDiscoveryState(state) {
2022
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
2023
+ }
2024
+ discoveryState() {
2025
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
2026
+ }
2027
+ invalidateCredentials(scope) {
2028
+ const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
2029
+ for (const suffix of suffixes) {
2030
+ this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
2031
+ }
2032
+ }
2033
+ }
2034
+
2035
+ class InteractiveOAuthProvider extends McpxOAuthProvider {
2036
+ #openUrl;
2037
+ constructor(serverName, secrets, redirectUrl, state, openUrl) {
2038
+ super(serverName, secrets, redirectUrl, state);
2039
+ this.#openUrl = openUrl;
2040
+ }
2041
+ async redirectToAuthorization(authorizationUrl) {
2042
+ await this.#openUrl(authorizationUrl.toString());
2043
+ }
2044
+ }
2045
+ function listen(server) {
2046
+ return new Promise((resolve, reject) => {
2047
+ server.once("error", reject);
2048
+ server.listen(0, "127.0.0.1", () => {
2049
+ const address = server.address();
2050
+ if (!address || typeof address === "string") {
2051
+ reject(new Error("Failed to start OAuth callback server."));
2052
+ return;
2053
+ }
2054
+ resolve(address.port);
2055
+ });
2056
+ });
2057
+ }
2058
+ function closeServer(server) {
2059
+ return new Promise((resolve) => {
2060
+ server.close(() => resolve());
2061
+ });
2062
+ }
2063
+ function waitForAuthorizationCode(server, expectedState, timeoutMs = 120000) {
2064
+ return new Promise((resolve, reject) => {
2065
+ const timeout = setTimeout(() => {
2066
+ reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
2067
+ }, timeoutMs);
2068
+ server.on("request", (request, response) => {
2069
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
2070
+ const code = url.searchParams.get("code");
2071
+ const state = url.searchParams.get("state");
2072
+ const error = url.searchParams.get("error");
2073
+ response.setHeader("content-type", "text/html; charset=utf-8");
2074
+ if (error) {
2075
+ response.statusCode = 400;
2076
+ response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
2077
+ clearTimeout(timeout);
2078
+ reject(new Error(`OAuth authorization failed: ${error}`));
2079
+ return;
2080
+ }
2081
+ if (!code) {
2082
+ response.statusCode = 400;
2083
+ response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
2084
+ return;
2085
+ }
2086
+ if (state !== expectedState) {
2087
+ response.statusCode = 400;
2088
+ response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
2089
+ clearTimeout(timeout);
2090
+ reject(new Error("OAuth state mismatch."));
2091
+ return;
2092
+ }
2093
+ response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
2094
+ clearTimeout(timeout);
2095
+ resolve(code);
2096
+ });
2097
+ });
2098
+ }
2099
+ function bindOAuthReference(config, serverName) {
2100
+ const spec = config.servers[serverName];
2101
+ if (!spec) {
2102
+ throw new Error(`Server "${serverName}" not found.`);
2103
+ }
2104
+ if (spec.transport !== "http") {
2105
+ throw new Error(`OAuth login only supports HTTP servers.`);
2106
+ }
2107
+ spec.headers = {
2108
+ ...spec.headers ?? {},
2109
+ Authorization: `oauth://${encodeURIComponent(serverName)}`
2110
+ };
2111
+ }
2112
+ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath) {
2113
+ const state = crypto.randomUUID();
2114
+ const callbackServer = http.createServer();
2115
+ const port = await listen(callbackServer);
2116
+ const redirectUrl = `http://127.0.0.1:${port}/callback`;
2117
+ const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, openUrl);
2118
+ const codePromise = waitForAuthorizationCode(callbackServer, state);
2119
+ try {
2120
+ const initial = await auth(provider, {
2121
+ serverUrl: spec.url
2122
+ });
2123
+ if (initial !== "REDIRECT") {
2124
+ throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
2125
+ }
2126
+ const authorizationCode = await codePromise;
2127
+ const result = await auth(provider, {
2128
+ serverUrl: spec.url,
2129
+ authorizationCode
2130
+ });
2131
+ if (result !== "AUTHORIZED") {
2132
+ throw new Error(`OAuth login did not authorize "${serverName}".`);
2133
+ }
2134
+ const config = loadConfig(configPath);
2135
+ bindOAuthReference(config, serverName);
2136
+ saveConfig(config, configPath);
2137
+ syncAllClients(config, secrets);
2138
+ return { serverName, authorized: true };
2139
+ } finally {
2140
+ await closeServer(callbackServer);
2141
+ }
2142
+ }
2143
+ async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
2144
+ const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
2145
+ if (!stored) {
2146
+ throw new Error(`No OAuth tokens stored for "${serverName}". Run mcpx auth login ${serverName}.`);
2147
+ }
2148
+ if (!options.forceRefresh && !tokensAreExpiring(stored)) {
2149
+ return stored.tokens.access_token;
2150
+ }
2151
+ if (!stored.tokens.refresh_token) {
2152
+ if (!options.forceRefresh) {
2153
+ return stored.tokens.access_token;
2154
+ }
2155
+ throw new Error(`No OAuth refresh token stored for "${serverName}". Re-authentication required.`);
2156
+ }
2157
+ const provider = new McpxOAuthProvider(serverName, secrets);
2158
+ const clientInformation = provider.clientInformation();
2159
+ if (!clientInformation) {
2160
+ throw new Error(`No OAuth client information stored for "${serverName}". Re-authentication required.`);
2161
+ }
2162
+ const cachedDiscovery = provider.discoveryState();
2163
+ const serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
2164
+ const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
2165
+ metadata: serverInfo.authorizationServerMetadata,
2166
+ clientInformation,
2167
+ refreshToken: stored.tokens.refresh_token,
2168
+ resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
2169
+ });
2170
+ provider.saveTokens(refreshed);
2171
+ return refreshed.access_token;
2172
+ }
2173
+
1966
2174
  // src/version.ts
1967
- var APP_VERSION = "0.1.75";
2175
+ var APP_VERSION = "0.1.79";
1968
2176
 
1969
2177
  // src/core/update.ts
1970
- import fs9 from "node:fs";
1971
- import path13 from "node:path";
1972
- import { execFileSync as execFileSync3 } from "node:child_process";
2178
+ import fs7 from "node:fs";
2179
+ import path12 from "node:path";
2180
+ import { execFileSync as execFileSync2 } from "node:child_process";
1973
2181
  function compareVersions(a, b) {
1974
2182
  const partsA = a.split(".").map(Number);
1975
2183
  const partsB = b.split(".").map(Number);
@@ -1985,7 +2193,7 @@ function compareVersions(a, b) {
1985
2193
  }
1986
2194
  async function checkForUpdates() {
1987
2195
  try {
1988
- const output = execFileSync3("npm", ["view", "@kwonye/mcpx", "version"], {
2196
+ const output = execFileSync2("npm", ["view", "@kwonye/mcpx", "version"], {
1989
2197
  encoding: "utf8",
1990
2198
  stdio: ["pipe", "pipe", "pipe"]
1991
2199
  }).trim();
@@ -2007,17 +2215,17 @@ async function checkForUpdates() {
2007
2215
  }
2008
2216
  function getStagedUpdate() {
2009
2217
  const stagedPath = getStagedVersionPath();
2010
- if (!fs9.existsSync(stagedPath)) {
2218
+ if (!fs7.existsSync(stagedPath)) {
2011
2219
  return null;
2012
2220
  }
2013
2221
  try {
2014
- const raw = fs9.readFileSync(stagedPath, "utf8");
2222
+ const raw = fs7.readFileSync(stagedPath, "utf8");
2015
2223
  const info = JSON.parse(raw);
2016
2224
  if (!info.version || !info.cliPath) {
2017
2225
  return null;
2018
2226
  }
2019
- const absoluteCliPath = path13.isAbsolute(info.cliPath) ? info.cliPath : path13.join(getUpdatesDir(), info.cliPath);
2020
- if (!fs9.existsSync(absoluteCliPath)) {
2227
+ const absoluteCliPath = path12.isAbsolute(info.cliPath) ? info.cliPath : path12.join(getUpdatesDir(), info.cliPath);
2228
+ if (!fs7.existsSync(absoluteCliPath)) {
2021
2229
  return null;
2022
2230
  }
2023
2231
  return {
@@ -2033,15 +2241,15 @@ function stageUpdate(version, cliPath) {
2033
2241
  ensureDir(updatesDir);
2034
2242
  const stagedInfo = {
2035
2243
  version,
2036
- cliPath: path13.isAbsolute(cliPath) ? path13.relative(updatesDir, cliPath) : cliPath,
2244
+ cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
2037
2245
  stagedAt: new Date().toISOString()
2038
2246
  };
2039
- fs9.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
2247
+ fs7.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
2040
2248
  }
2041
2249
  function clearStagedUpdate() {
2042
2250
  const stagedPath = getStagedVersionPath();
2043
- if (fs9.existsSync(stagedPath)) {
2044
- fs9.unlinkSync(stagedPath);
2251
+ if (fs7.existsSync(stagedPath)) {
2252
+ fs7.unlinkSync(stagedPath);
2045
2253
  }
2046
2254
  }
2047
2255
  function getStagedCliPath() {
@@ -2050,31 +2258,31 @@ function getStagedCliPath() {
2050
2258
  }
2051
2259
  function removeOldVersions(keepCount = 2) {
2052
2260
  const updatesDir = getUpdatesDir();
2053
- if (!fs9.existsSync(updatesDir)) {
2261
+ if (!fs7.existsSync(updatesDir)) {
2054
2262
  return;
2055
2263
  }
2056
- const entries = fs9.readdirSync(updatesDir, { withFileTypes: true });
2264
+ const entries = fs7.readdirSync(updatesDir, { withFileTypes: true });
2057
2265
  const versionDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith("v")).map((entry) => entry.name).sort((a, b) => compareVersions(b.slice(1), a.slice(1)));
2058
2266
  if (versionDirs.length <= keepCount) {
2059
2267
  return;
2060
2268
  }
2061
2269
  for (const oldVersion of versionDirs.slice(keepCount)) {
2062
- const oldPath = path13.join(updatesDir, oldVersion);
2270
+ const oldPath = path12.join(updatesDir, oldVersion);
2063
2271
  try {
2064
- fs9.rmSync(oldPath, { recursive: true, force: true });
2272
+ fs7.rmSync(oldPath, { recursive: true, force: true });
2065
2273
  } catch {}
2066
2274
  }
2067
2275
  }
2068
2276
 
2069
2277
  // src/core/update-manager.ts
2070
- import fs10 from "node:fs";
2071
- import path14 from "node:path";
2278
+ import fs8 from "node:fs";
2279
+ import path13 from "node:path";
2072
2280
  import { execSync, spawn } from "node:child_process";
2073
2281
  function acquireLock() {
2074
2282
  const lockPath = getUpdateLockPath();
2075
2283
  try {
2076
- ensureDir(path14.dirname(lockPath));
2077
- fs10.writeFileSync(lockPath, `${process.pid}
2284
+ ensureDir(path13.dirname(lockPath));
2285
+ fs8.writeFileSync(lockPath, `${process.pid}
2078
2286
  `, { flag: "wx" });
2079
2287
  return true;
2080
2288
  } catch {
@@ -2084,25 +2292,25 @@ function acquireLock() {
2084
2292
  function releaseLock() {
2085
2293
  const lockPath = getUpdateLockPath();
2086
2294
  try {
2087
- fs10.unlinkSync(lockPath);
2295
+ fs8.unlinkSync(lockPath);
2088
2296
  } catch {}
2089
2297
  }
2090
2298
  function isUpdateInProgress() {
2091
2299
  const lockPath = getUpdateLockPath();
2092
- if (!fs10.existsSync(lockPath)) {
2300
+ if (!fs8.existsSync(lockPath)) {
2093
2301
  return false;
2094
2302
  }
2095
2303
  try {
2096
- const raw = fs10.readFileSync(lockPath, "utf8").trim();
2304
+ const raw = fs8.readFileSync(lockPath, "utf8").trim();
2097
2305
  const pid = Number(raw);
2098
2306
  if (!Number.isFinite(pid) || pid <= 0) {
2099
- fs10.unlinkSync(lockPath);
2307
+ fs8.unlinkSync(lockPath);
2100
2308
  return false;
2101
2309
  }
2102
2310
  process.kill(pid, 0);
2103
2311
  return true;
2104
2312
  } catch {
2105
- fs10.unlinkSync(lockPath);
2313
+ fs8.unlinkSync(lockPath);
2106
2314
  return false;
2107
2315
  }
2108
2316
  }
@@ -2113,8 +2321,8 @@ async function downloadAndStageUpdate() {
2113
2321
  }
2114
2322
  const targetVersion = updateStatus.latestVersion;
2115
2323
  const updatesDir = getUpdatesDir();
2116
- const versionDir = path14.join(updatesDir, `v${targetVersion}`);
2117
- if (fs10.existsSync(versionDir)) {
2324
+ const versionDir = path13.join(updatesDir, `v${targetVersion}`);
2325
+ if (fs8.existsSync(versionDir)) {
2118
2326
  const existingStaged = getStagedUpdate();
2119
2327
  if (existingStaged?.version === targetVersion) {
2120
2328
  return { success: false, error: `Version ${targetVersion} already staged` };
@@ -2126,8 +2334,8 @@ async function downloadAndStageUpdate() {
2126
2334
  cwd: versionDir,
2127
2335
  stdio: ["pipe", "pipe", "pipe"]
2128
2336
  });
2129
- const tarballPath = path14.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2130
- if (!fs10.existsSync(tarballPath)) {
2337
+ const tarballPath = path13.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2338
+ if (!fs8.existsSync(tarballPath)) {
2131
2339
  throw new Error("Downloaded tarball not found");
2132
2340
  }
2133
2341
  execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
@@ -2138,8 +2346,8 @@ async function downloadAndStageUpdate() {
2138
2346
  cwd: versionDir,
2139
2347
  stdio: ["pipe", "pipe", "pipe"]
2140
2348
  });
2141
- const cliPath = path14.join(versionDir, "dist", "cli.js");
2142
- if (!fs10.existsSync(cliPath)) {
2349
+ const cliPath = path13.join(versionDir, "dist", "cli.js");
2350
+ if (!fs8.existsSync(cliPath)) {
2143
2351
  throw new Error("CLI entry point not found in downloaded package");
2144
2352
  }
2145
2353
  stageUpdate(targetVersion, cliPath);
@@ -2147,7 +2355,7 @@ async function downloadAndStageUpdate() {
2147
2355
  return { success: true, version: targetVersion };
2148
2356
  } catch (error) {
2149
2357
  try {
2150
- fs10.rmSync(versionDir, { recursive: true, force: true });
2358
+ fs8.rmSync(versionDir, { recursive: true, force: true });
2151
2359
  } catch {}
2152
2360
  return {
2153
2361
  success: false,
@@ -2225,14 +2433,14 @@ function performRollback() {
2225
2433
  }
2226
2434
 
2227
2435
  // src/core/daemon.ts
2228
- import fs11 from "node:fs";
2436
+ import fs9 from "node:fs";
2229
2437
  import net from "node:net";
2230
2438
  import { spawn as spawn2 } from "node:child_process";
2231
2439
 
2232
2440
  // src/gateway/server.ts
2233
- import http from "node:http";
2234
- import { URL } from "node:url";
2235
- import crypto3 from "node:crypto";
2441
+ import http2 from "node:http";
2442
+ import { URL as URL2 } from "node:url";
2443
+ import crypto2 from "node:crypto";
2236
2444
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2237
2445
  import {
2238
2446
  StdioClientTransport,
@@ -2321,16 +2529,16 @@ function getWellKnownPrefix(pathname) {
2321
2529
  return null;
2322
2530
  }
2323
2531
  function getUpstreamPathSuffixForWellKnown(upstream) {
2324
- const upstreamUrl = new URL(upstream.spec.url);
2532
+ const upstreamUrl = new URL2(upstream.spec.url);
2325
2533
  if (upstreamUrl.pathname === "/") {
2326
2534
  return "";
2327
2535
  }
2328
2536
  return upstreamUrl.pathname.endsWith("/") ? upstreamUrl.pathname.slice(0, -1) : upstreamUrl.pathname;
2329
2537
  }
2330
2538
  function buildWellKnownUpstreamUrl(upstream, prefix) {
2331
- const upstreamUrl = new URL(upstream.spec.url);
2539
+ const upstreamUrl = new URL2(upstream.spec.url);
2332
2540
  const suffix = getUpstreamPathSuffixForWellKnown(upstream);
2333
- return new URL(`${prefix}${suffix}`, upstreamUrl.origin);
2541
+ return new URL2(`${prefix}${suffix}`, upstreamUrl.origin);
2334
2542
  }
2335
2543
  function getLocalOriginFromRequest(request) {
2336
2544
  return `http://${request.headers.host ?? "127.0.0.1"}`;
@@ -2346,7 +2554,7 @@ function appendUpstreamQuery(url, upstream) {
2346
2554
  if (!upstream) {
2347
2555
  return url;
2348
2556
  }
2349
- const parsed = new URL(url);
2557
+ const parsed = new URL2(url);
2350
2558
  parsed.searchParams.set("upstream", upstream);
2351
2559
  return parsed.toString();
2352
2560
  }
@@ -2411,6 +2619,62 @@ async function callUpstream(upstream, method, params, id, secrets, runtime, pass
2411
2619
  }
2412
2620
  return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
2413
2621
  }
2622
+ async function getUpstreamTokenCounts(config, secrets, runtime) {
2623
+ if (!runtime.tokenCache) {
2624
+ runtime.tokenCache = new Map;
2625
+ }
2626
+ const results = {};
2627
+ const upstreams = listUpstreams(config);
2628
+ for (const upstream of upstreams) {
2629
+ const fingerprint = specFingerprint(upstream.spec);
2630
+ const cached = runtime.tokenCache.get(upstream.name);
2631
+ if (cached?.fingerprint === fingerprint) {
2632
+ results[upstream.name] = cached.count;
2633
+ continue;
2634
+ }
2635
+ let toolsCount = 0;
2636
+ let resourcesCount = 0;
2637
+ let promptsCount = 0;
2638
+ const errors = [];
2639
+ try {
2640
+ const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
2641
+ if (toolsResult && toolsResult.tools) {
2642
+ toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
2643
+ }
2644
+ } catch (error) {
2645
+ errors.push(`tools/list: ${error instanceof Error ? error.message : String(error)}`);
2646
+ }
2647
+ try {
2648
+ const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
2649
+ if (resourcesResult && resourcesResult.resources) {
2650
+ resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
2651
+ }
2652
+ } catch (error) {
2653
+ errors.push(`resources/list: ${error instanceof Error ? error.message : String(error)}`);
2654
+ }
2655
+ try {
2656
+ const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
2657
+ if (promptsResult && promptsResult.prompts) {
2658
+ promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
2659
+ }
2660
+ } catch (error) {
2661
+ errors.push(`prompts/list: ${error instanceof Error ? error.message : String(error)}`);
2662
+ }
2663
+ const total = toolsCount + resourcesCount + promptsCount;
2664
+ const countObj = {
2665
+ tools: toolsCount,
2666
+ resources: resourcesCount,
2667
+ prompts: promptsCount,
2668
+ total,
2669
+ error: errors.length > 0 ? errors.join("; ") : undefined
2670
+ };
2671
+ if (errors.length === 0) {
2672
+ runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
2673
+ }
2674
+ results[upstream.name] = countObj;
2675
+ }
2676
+ return results;
2677
+ }
2414
2678
  function getConfiguredTimeoutMs() {
2415
2679
  const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
2416
2680
  return Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_UPSTREAM_TIMEOUT_MS;
@@ -2431,11 +2695,9 @@ function specFingerprint(spec) {
2431
2695
  return JSON.stringify(spec);
2432
2696
  }
2433
2697
  function resolveStdioEnv(spec, secrets) {
2434
- if (!spec.env || Object.keys(spec.env).length === 0) {
2435
- return;
2436
- }
2437
2698
  const env = getDefaultEnvironment();
2438
- for (const [key, value] of Object.entries(spec.env)) {
2699
+ env.PATH = buildEnrichedPath(env.PATH);
2700
+ for (const [key, value] of Object.entries(spec.env ?? {})) {
2439
2701
  env[key] = secrets.resolveMaybeSecret(value);
2440
2702
  }
2441
2703
  return env;
@@ -2544,12 +2806,21 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2544
2806
  }
2545
2807
  }
2546
2808
  async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
2809
+ return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
2810
+ }
2811
+ async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
2547
2812
  const headers = {
2548
2813
  "content-type": "application/json",
2549
2814
  accept: "application/json, text/event-stream"
2550
2815
  };
2551
2816
  for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2552
- headers[key] = secrets.resolveMaybeSecret(value);
2817
+ if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2818
+ const oauthServerName = oauthReferenceServerName(value);
2819
+ const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
2820
+ headers[key] = `Bearer ${accessToken}`;
2821
+ } else {
2822
+ headers[key] = secrets.resolveMaybeSecret(value);
2823
+ }
2553
2824
  }
2554
2825
  if (passthroughAuthorizationHeader) {
2555
2826
  headers.Authorization = passthroughAuthorizationHeader;
@@ -2581,6 +2852,9 @@ async function callHttpUpstream(upstream, method, params, id, secrets, passthrou
2581
2852
  }
2582
2853
  if (!response.ok) {
2583
2854
  const responseText = await response.text();
2855
+ if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
2856
+ return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
2857
+ }
2584
2858
  throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
2585
2859
  }
2586
2860
  let payload = null;
@@ -2719,6 +2993,15 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
2719
2993
  for (const upstream of upstreams) {
2720
2994
  try {
2721
2995
  const result = await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
2996
+ if (!runtime.tokenCache) {
2997
+ runtime.tokenCache = new Map;
2998
+ }
2999
+ const cached = runtime.tokenCache.get(upstream.name);
3000
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
3001
+ existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
3002
+ existing.total = existing.tools + existing.resources + existing.prompts;
3003
+ delete existing.error;
3004
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
2722
3005
  for (const tool of result.tools ?? []) {
2723
3006
  const name = typeof tool.name === "string" ? tool.name : "tool";
2724
3007
  tools.push({
@@ -2727,9 +3010,10 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
2727
3010
  });
2728
3011
  }
2729
3012
  } catch (error) {
2730
- if (upstreams.length === 1 && isAuthChallenge(error)) {
3013
+ if (upstreams.length === 1) {
2731
3014
  throw error;
2732
3015
  }
3016
+ runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
2733
3017
  }
2734
3018
  }
2735
3019
  return { tools };
@@ -2742,6 +3026,15 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
2742
3026
  for (const upstream of upstreams) {
2743
3027
  try {
2744
3028
  const result = await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
3029
+ if (!runtime.tokenCache) {
3030
+ runtime.tokenCache = new Map;
3031
+ }
3032
+ const cached = runtime.tokenCache.get(upstream.name);
3033
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
3034
+ existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
3035
+ existing.total = existing.tools + existing.resources + existing.prompts;
3036
+ delete existing.error;
3037
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
2745
3038
  for (const resource of result.resources ?? []) {
2746
3039
  const originalUri = typeof resource.uri === "string" ? resource.uri : "";
2747
3040
  const originalName = typeof resource.name === "string" ? resource.name : originalUri;
@@ -2752,9 +3045,10 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
2752
3045
  });
2753
3046
  }
2754
3047
  } catch (error) {
2755
- if (upstreams.length === 1 && isAuthChallenge(error)) {
3048
+ if (upstreams.length === 1) {
2756
3049
  throw error;
2757
3050
  }
3051
+ runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
2758
3052
  }
2759
3053
  }
2760
3054
  return { resources };
@@ -2767,6 +3061,15 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
2767
3061
  for (const upstream of upstreams) {
2768
3062
  try {
2769
3063
  const result = await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
3064
+ if (!runtime.tokenCache) {
3065
+ runtime.tokenCache = new Map;
3066
+ }
3067
+ const cached = runtime.tokenCache.get(upstream.name);
3068
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
3069
+ existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
3070
+ existing.total = existing.tools + existing.resources + existing.prompts;
3071
+ delete existing.error;
3072
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
2770
3073
  for (const prompt of result.prompts ?? []) {
2771
3074
  const name = typeof prompt.name === "string" ? prompt.name : "prompt";
2772
3075
  prompts.push({
@@ -2775,9 +3078,10 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
2775
3078
  });
2776
3079
  }
2777
3080
  } catch (error) {
2778
- if (upstreams.length === 1 && isAuthChallenge(error)) {
3081
+ if (upstreams.length === 1) {
2779
3082
  throw error;
2780
3083
  }
3084
+ runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
2781
3085
  }
2782
3086
  }
2783
3087
  return { prompts };
@@ -2908,11 +3212,20 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
2908
3212
  if (request.method === "ping") {
2909
3213
  return makeResult(id, { ok: true });
2910
3214
  }
2911
- const config = loadConfig();
3215
+ const config = loadMergedConfig();
2912
3216
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
2913
3217
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
2914
3218
  }
2915
3219
  await reconcileStdioConnections(config, runtime);
3220
+ if (request.method === "custom/tokenCounts") {
3221
+ const result = await getUpstreamTokenCounts(config, secrets, runtime);
3222
+ return makeResult(id, result);
3223
+ }
3224
+ if (request.method === "custom/refreshTokenCounts") {
3225
+ runtime.tokenCache?.clear();
3226
+ const result = await getUpstreamTokenCounts(config, secrets, runtime);
3227
+ return makeResult(id, result);
3228
+ }
2916
3229
  if (request.method === "tools/list") {
2917
3230
  const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
2918
3231
  return makeResult(id, result);
@@ -2942,7 +3255,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
2942
3255
  response.end(JSON.stringify({ error: "method_not_allowed" }));
2943
3256
  return true;
2944
3257
  }
2945
- const config = loadConfig();
3258
+ const config = loadMergedConfig();
2946
3259
  const requestedUpstream = getRequestedUpstream(requestUrl);
2947
3260
  const upstream = getScopedHttpUpstream(config, requestedUpstream);
2948
3261
  if (!upstream) {
@@ -2999,12 +3312,13 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
2999
3312
  function createGatewayServer(options) {
3000
3313
  const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
3001
3314
  const runtime = {
3002
- stdioConnections: new Map
3315
+ stdioConnections: new Map,
3316
+ upstreamErrors: new Map
3003
3317
  };
3004
- const server = http.createServer(async (request, response) => {
3005
- let requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
3318
+ const server = http2.createServer(async (request, response) => {
3319
+ let requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
3006
3320
  try {
3007
- requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
3321
+ requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
3008
3322
  const upstreamFilter = getRequestedUpstream(requestUrl);
3009
3323
  if (debug) {
3010
3324
  console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
@@ -3115,7 +3429,7 @@ function createGatewayServer(options) {
3115
3429
  response.setHeader("content-type", "application/json");
3116
3430
  }
3117
3431
  if (hasInitialize) {
3118
- const sessionId = crypto3.randomUUID();
3432
+ const sessionId = crypto2.randomUUID();
3119
3433
  response.setHeader("mcp-session-id", sessionId);
3120
3434
  response.setHeader("MCP-Session-Id", sessionId);
3121
3435
  }
@@ -3145,7 +3459,11 @@ function createGatewayServer(options) {
3145
3459
  const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
3146
3460
  response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
3147
3461
  }
3148
- response.end(error.bodyText.length > 0 ? error.bodyText : JSON.stringify({ error: "upstream_auth_required" }));
3462
+ response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
3463
+ status: error.status,
3464
+ body: error.bodyText,
3465
+ wwwAuthenticate: error.wwwAuthenticate
3466
+ })));
3149
3467
  if (debug) {
3150
3468
  console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
3151
3469
  }
@@ -3171,10 +3489,10 @@ function createGatewayServer(options) {
3171
3489
 
3172
3490
  // src/core/daemon.ts
3173
3491
  function readPidFromFile(pidPath = getPidPath()) {
3174
- if (!fs11.existsSync(pidPath)) {
3492
+ if (!fs9.existsSync(pidPath)) {
3175
3493
  return null;
3176
3494
  }
3177
- const raw = fs11.readFileSync(pidPath, "utf8").trim();
3495
+ const raw = fs9.readFileSync(pidPath, "utf8").trim();
3178
3496
  if (!raw) {
3179
3497
  return null;
3180
3498
  }
@@ -3221,6 +3539,29 @@ async function resolveGatewayPort(config) {
3221
3539
  }
3222
3540
  throw new Error(`No available local port found near ${config.gateway.port}.`);
3223
3541
  }
3542
+ async function waitForGatewayReady(port, token, timeoutMs = 5000) {
3543
+ const deadline = Date.now() + timeoutMs;
3544
+ let lastError;
3545
+ while (Date.now() < deadline) {
3546
+ try {
3547
+ const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
3548
+ method: "GET",
3549
+ headers: {
3550
+ Authorization: `Bearer ${token}`
3551
+ },
3552
+ signal: AbortSignal.timeout(500)
3553
+ });
3554
+ if (response.ok) {
3555
+ return;
3556
+ }
3557
+ lastError = new Error(`Gateway returned HTTP ${response.status}.`);
3558
+ } catch (error) {
3559
+ lastError = error;
3560
+ }
3561
+ await new Promise((resolve) => setTimeout(resolve, 100));
3562
+ }
3563
+ throw new Error(`Gateway did not become ready within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
3564
+ }
3224
3565
  function getDaemonStatus(config) {
3225
3566
  const pidPath = getPidPath();
3226
3567
  const pid = readPidFromFile(pidPath);
@@ -3253,16 +3594,16 @@ async function startDaemon(config, cliPath, secrets) {
3253
3594
  };
3254
3595
  }
3255
3596
  try {
3256
- fs11.unlinkSync(getPidPath());
3597
+ fs9.unlinkSync(getPidPath());
3257
3598
  } catch {}
3258
3599
  }
3259
3600
  const port = await resolveGatewayPort(config);
3260
- ensureGatewayToken(config, secrets);
3601
+ const token = ensureGatewayToken(config, secrets);
3261
3602
  const pidPath = getPidPath();
3262
3603
  const logPath = getLogPath();
3263
3604
  ensureParentDir(pidPath);
3264
3605
  ensureParentDir(logPath);
3265
- const logFd = fs11.openSync(logPath, "a", 384);
3606
+ const logFd = fs9.openSync(logPath, "a", 384);
3266
3607
  const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
3267
3608
  detached: true,
3268
3609
  stdio: ["ignore", logFd, logFd],
@@ -3272,9 +3613,10 @@ async function startDaemon(config, cliPath, secrets) {
3272
3613
  }
3273
3614
  });
3274
3615
  child.unref();
3275
- fs11.writeFileSync(pidPath, `${child.pid}
3616
+ fs9.writeFileSync(pidPath, `${child.pid}
3276
3617
  `, { mode: 384 });
3277
3618
  startBackgroundUpdateCheck();
3619
+ await waitForGatewayReady(port, token);
3278
3620
  return {
3279
3621
  started: true,
3280
3622
  pid: child.pid ?? -1,
@@ -3295,7 +3637,7 @@ function stopDaemon() {
3295
3637
  process.kill(pid, "SIGTERM");
3296
3638
  } catch {}
3297
3639
  try {
3298
- fs11.unlinkSync(pidPath);
3640
+ fs9.unlinkSync(pidPath);
3299
3641
  } catch {}
3300
3642
  return {
3301
3643
  stopped: true,
@@ -3308,10 +3650,10 @@ async function restartDaemon(config, cliPath, secrets) {
3308
3650
  }
3309
3651
  function readDaemonLogs(maxLines = 200) {
3310
3652
  const logPath = getLogPath();
3311
- if (!fs11.existsSync(logPath)) {
3653
+ if (!fs9.existsSync(logPath)) {
3312
3654
  return "";
3313
3655
  }
3314
- const lines = fs11.readFileSync(logPath, "utf8").split(`
3656
+ const lines = fs9.readFileSync(logPath, "utf8").split(`
3315
3657
  `);
3316
3658
  return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
3317
3659
  `);
@@ -3380,26 +3722,75 @@ function buildAuthBindings(spec) {
3380
3722
  secretName: secretRefName(binding.value) ?? undefined
3381
3723
  }));
3382
3724
  }
3383
- function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
3725
+ async function fetchTokenCounts(gatewayUrl, token) {
3726
+ try {
3727
+ const res = await fetch(gatewayUrl, {
3728
+ method: "POST",
3729
+ headers: {
3730
+ "content-type": "application/json",
3731
+ authorization: `Bearer ${token}`
3732
+ },
3733
+ body: JSON.stringify({
3734
+ jsonrpc: "2.0",
3735
+ id: "status-tokens",
3736
+ method: "custom/tokenCounts",
3737
+ params: {}
3738
+ }),
3739
+ signal: AbortSignal.timeout(4000)
3740
+ });
3741
+ if (!res.ok) {
3742
+ return {};
3743
+ }
3744
+ const data = await res.json();
3745
+ return data?.result ?? {};
3746
+ } catch (error) {
3747
+ return {};
3748
+ }
3749
+ }
3750
+ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
3751
+ const secrets = new SecretsManager;
3752
+ const token = ensureGatewayToken(config, secrets);
3753
+ const gatewayUrl = getGatewayUrl(config);
3754
+ let tokenCounts = {};
3755
+ if (daemon.running) {
3756
+ tokenCounts = await fetchTokenCounts(gatewayUrl, token);
3757
+ }
3758
+ const projectEntries = Object.values(config.projects ?? {});
3759
+ let totalGlobalTokens = 0;
3760
+ const totalProjectTokens = {};
3384
3761
  const serverNames = Object.keys(config.servers).sort((left, right) => left.localeCompare(right));
3385
3762
  const servers = serverNames.map((name) => {
3386
3763
  const spec = config.servers[name];
3387
- return {
3764
+ const tokenCount = tokenCounts[name];
3765
+ const serverEntry = {
3388
3766
  name,
3389
3767
  enabled: isServerEnabled(spec),
3390
3768
  transport: spec.transport,
3391
3769
  target: describeServerTarget(spec),
3392
3770
  authBindings: buildAuthBindings(spec),
3393
- clients: buildClientMappings(config, managedIndex, name)
3771
+ clients: buildClientMappings(config, managedIndex, name),
3772
+ tokenCount
3394
3773
  };
3774
+ if (serverEntry.enabled && tokenCount) {
3775
+ const matchingProject = projectEntries.find((p) => name.startsWith(`${p.name}.`));
3776
+ if (matchingProject) {
3777
+ totalProjectTokens[matchingProject.path] = (totalProjectTokens[matchingProject.path] ?? 0) + tokenCount.total;
3778
+ } else {
3779
+ totalGlobalTokens += tokenCount.total;
3780
+ }
3781
+ }
3782
+ return serverEntry;
3395
3783
  });
3396
3784
  return {
3397
- gatewayUrl: getGatewayUrl(config),
3785
+ gatewayUrl,
3398
3786
  daemon,
3399
3787
  upstreamCount: servers.length,
3400
3788
  servers,
3401
- clients: config.clients
3789
+ clients: config.clients,
3790
+ projects: config.projects,
3791
+ totalGlobalTokens,
3792
+ totalProjectTokens
3402
3793
  };
3403
3794
  }
3404
3795
 
3405
- export { getConfigPath, getManagedIndexPath, defaultConfig, loadConfig, saveConfig, addServer, removeServer, updateServer, setServerEnabled, ensureGatewayToken, listSkills, getSkill, saveSkill, deleteSkill, 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 };
3796
+ export { listSkills, getSkill, saveSkill, deleteSkill, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };