@kwonye/mcpx 0.1.93 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/adapters/claude-desktop.d.ts.map +1 -1
  2. package/dist/adapters/claude.d.ts +2 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/cline.d.ts +3 -1
  5. package/dist/adapters/cline.d.ts.map +1 -1
  6. package/dist/adapters/codex.d.ts +2 -1
  7. package/dist/adapters/codex.d.ts.map +1 -1
  8. package/dist/adapters/cursor.d.ts +2 -1
  9. package/dist/adapters/cursor.d.ts.map +1 -1
  10. package/dist/adapters/hermes.d.ts +11 -0
  11. package/dist/adapters/hermes.d.ts.map +1 -0
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/kiro.d.ts.map +1 -1
  14. package/dist/adapters/openclaw.d.ts +11 -0
  15. package/dist/adapters/openclaw.d.ts.map +1 -0
  16. package/dist/adapters/opencode.d.ts +2 -1
  17. package/dist/adapters/opencode.d.ts.map +1 -1
  18. package/dist/adapters/qwen.d.ts.map +1 -1
  19. package/dist/adapters/utils/index.d.ts +18 -4
  20. package/dist/adapters/utils/index.d.ts.map +1 -1
  21. package/dist/adapters/vscode.d.ts +2 -1
  22. package/dist/adapters/vscode.d.ts.map +1 -1
  23. package/dist/{cli-y7ba5qvr.js → cli-28fsxhe3.js} +430 -156
  24. package/dist/{cli-qvkpwy9w.js → cli-njtbb11m.js} +138 -43
  25. package/dist/{cli-8vxva0cb.js → cli-tk8bggg5.js} +81 -8
  26. package/dist/{cli-np0wwh6m.js → cli-v27bt25d.js} +157 -11
  27. package/dist/{cli-kwyp9ywh.js → cli-wxw1t3rr.js} +941 -303
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +388 -77
  30. package/dist/compat/hermes.d.ts +7 -0
  31. package/dist/compat/hermes.d.ts.map +1 -0
  32. package/dist/compat/index.d.ts.map +1 -1
  33. package/dist/compat/openclaw.d.ts +7 -0
  34. package/dist/compat/openclaw.d.ts.map +1 -0
  35. package/dist/{config-2s5nsm2g.js → config-5p08cb0c.js} +6 -2
  36. package/dist/core/add-command.d.ts.map +1 -1
  37. package/dist/core/config-repair.d.ts.map +1 -1
  38. package/dist/core/config-store.d.ts +33 -0
  39. package/dist/core/config-store.d.ts.map +1 -0
  40. package/dist/core/config.d.ts +13 -1
  41. package/dist/core/config.d.ts.map +1 -1
  42. package/dist/core/daemon.d.ts +5 -1
  43. package/dist/core/daemon.d.ts.map +1 -1
  44. package/dist/core/errors.d.ts +18 -0
  45. package/dist/core/errors.d.ts.map +1 -0
  46. package/dist/core/index.d.ts +6 -3
  47. package/dist/core/index.d.ts.map +1 -1
  48. package/dist/core/index.js +30 -6
  49. package/dist/core/managed-index-lock.d.ts +2 -0
  50. package/dist/core/managed-index-lock.d.ts.map +1 -0
  51. package/dist/core/oauth.d.ts.map +1 -1
  52. package/dist/core/plugin-host.d.ts +2 -0
  53. package/dist/core/plugin-host.d.ts.map +1 -0
  54. package/dist/core/plugin-manager.d.ts +19 -6
  55. package/dist/core/plugin-manager.d.ts.map +1 -1
  56. package/dist/core/plugin-parse.d.ts.map +1 -1
  57. package/dist/core/plugin-projections.d.ts +6 -2
  58. package/dist/core/plugin-projections.d.ts.map +1 -1
  59. package/dist/core/registry.d.ts +4 -1
  60. package/dist/core/registry.d.ts.map +1 -1
  61. package/dist/core/secrets.d.ts.map +1 -1
  62. package/dist/core/server-auth.d.ts.map +1 -1
  63. package/dist/core/skill-projections.d.ts +3 -0
  64. package/dist/core/skill-projections.d.ts.map +1 -0
  65. package/dist/core/status.d.ts +1 -0
  66. package/dist/core/status.d.ts.map +1 -1
  67. package/dist/core/sync.d.ts +19 -2
  68. package/dist/core/sync.d.ts.map +1 -1
  69. package/dist/core/update-manager.d.ts.map +1 -1
  70. package/dist/gateway/server.d.ts +4 -1
  71. package/dist/gateway/server.d.ts.map +1 -1
  72. package/dist/plugin-host-954a7tf7.js +129 -0
  73. package/dist/{plugin-manager-1sc4b6am.js → plugin-manager-m8ar2271.js} +12 -4
  74. package/dist/{registry-mztfy0cz.js → registry-n12thgf2.js} +1 -1
  75. package/dist/types.d.ts +5 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/version.d.ts +1 -1
  78. package/dist/version.d.ts.map +1 -1
  79. package/package.json +3 -2
  80. package/dist/cli-m2645syd.js +0 -2199
@@ -1,19 +1,27 @@
1
1
  import {
2
+ APP_VERSION,
2
3
  getGatewayUrl,
4
+ persistSyncState,
3
5
  syncAllClients
4
- } from "./cli-kwyp9ywh.js";
6
+ } from "./cli-wxw1t3rr.js";
5
7
  import {
6
8
  loadConfig,
7
9
  loadMergedConfig,
8
- saveConfig
9
- } from "./cli-np0wwh6m.js";
10
+ loadProjectConfig,
11
+ saveConfig,
12
+ saveProjectConfig
13
+ } from "./cli-v27bt25d.js";
10
14
  import {
11
15
  SecretsManager,
16
+ UpstreamError,
17
+ classifyUpstreamError,
12
18
  ensureGatewayToken
13
- } from "./cli-8vxva0cb.js";
19
+ } from "./cli-tk8bggg5.js";
14
20
  import {
15
21
  ensureDir,
16
22
  ensureParentDir,
23
+ findProjectConfigPath,
24
+ getConfigPath,
17
25
  getLogPath,
18
26
  getPidPath,
19
27
  getStagedVersionPath,
@@ -24,6 +32,114 @@ import {
24
32
  isServerEnabled
25
33
  } from "./cli-s4gspqdf.js";
26
34
 
35
+ // src/core/config-store.ts
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+ var LOCK_STALE_MS = 5000;
39
+ var LOCK_RETRY_MS = 50;
40
+ var LOCK_MAX_RETRIES = 40;
41
+ var inProcessQueues = new Map;
42
+ function getLockPath(filePath) {
43
+ return `${filePath}.lock`;
44
+ }
45
+ function acquireLock(filePath) {
46
+ const lockPath = getLockPath(filePath);
47
+ try {
48
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
49
+ const fd = fs.openSync(lockPath, "wx");
50
+ fs.writeSync(fd, String(process.pid));
51
+ fs.closeSync(fd);
52
+ return true;
53
+ } catch (err) {
54
+ if (err.code !== "EEXIST") {
55
+ return false;
56
+ }
57
+ try {
58
+ const stat = fs.statSync(lockPath);
59
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
60
+ fs.unlinkSync(lockPath);
61
+ return acquireLock(filePath);
62
+ }
63
+ } catch {
64
+ return acquireLock(filePath);
65
+ }
66
+ return false;
67
+ }
68
+ }
69
+ function releaseLock(filePath) {
70
+ try {
71
+ fs.unlinkSync(getLockPath(filePath));
72
+ } catch {}
73
+ }
74
+ function mutateFile(filePath, load, save, fn) {
75
+ const prevQueue = inProcessQueues.get(filePath) ?? Promise.resolve();
76
+ const nextQueue = prevQueue.then(() => executeWithLock(filePath, load, save, fn), () => executeWithLock(filePath, load, save, fn));
77
+ inProcessQueues.set(filePath, nextQueue.catch(() => {
78
+ return;
79
+ }));
80
+ return nextQueue;
81
+ }
82
+ async function executeWithLock(filePath, load, save, fn) {
83
+ let acquired = false;
84
+ for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
85
+ acquired = acquireLock(filePath);
86
+ if (acquired)
87
+ break;
88
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
89
+ }
90
+ if (!acquired) {
91
+ throw new Error(`Could not acquire lock for ${filePath}.`);
92
+ }
93
+ try {
94
+ const data = load(filePath);
95
+ const result = await fn(data);
96
+ save(data, filePath);
97
+ return result;
98
+ } finally {
99
+ releaseLock(filePath);
100
+ }
101
+ }
102
+ function mutateConfig(fn, configPath) {
103
+ return mutateFile(configPath ?? getConfigPath(), loadConfig, saveConfig, fn);
104
+ }
105
+ function mutateProjectConfig(projectConfigPath, fn) {
106
+ return mutateFile(projectConfigPath, (p) => loadProjectConfig(p), saveProjectConfig, fn);
107
+ }
108
+ async function mutateActiveConfig(options, mutator) {
109
+ const globalPath = getConfigPath();
110
+ if (!options.global) {
111
+ const projectConfigPath = findProjectConfigPath();
112
+ if (options.local) {
113
+ const projectPath = projectConfigPath ? path.dirname(projectConfigPath) : process.cwd();
114
+ const localPath = path.join(projectPath, ".mcpx.json");
115
+ const globalConfig = loadConfig(globalPath);
116
+ const result2 = await mutateProjectConfig(localPath, async (projectConfig) => {
117
+ const mergedConfig = {
118
+ ...globalConfig,
119
+ servers: { ...globalConfig.servers, ...projectConfig.servers }
120
+ };
121
+ const mutatorResult = await mutator(mergedConfig, { type: "project", projectPath });
122
+ const localServers = {};
123
+ for (const [name, spec] of Object.entries(mergedConfig.servers)) {
124
+ if (!(name in globalConfig.servers)) {
125
+ localServers[name] = spec;
126
+ }
127
+ }
128
+ projectConfig.servers = localServers;
129
+ return mutatorResult;
130
+ });
131
+ return { type: "project", configPath: localPath, projectPath, result: result2 };
132
+ }
133
+ if (projectConfigPath) {
134
+ const projectPath = path.dirname(projectConfigPath);
135
+ const result2 = await mutateConfig((config) => mutator(config, { type: "project", projectPath }), globalPath);
136
+ return { type: "project", configPath: globalPath, projectPath, result: result2 };
137
+ }
138
+ }
139
+ const result = await mutateConfig((config) => mutator(config, { type: "global" }), globalPath);
140
+ return { type: "global", configPath: globalPath, result };
141
+ }
142
+
27
143
  // src/core/auth-probe.ts
28
144
  var JSON_RPC_VERSION = "2.0";
29
145
  var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
@@ -344,6 +460,7 @@ function listAuthBindings(spec) {
344
460
  value
345
461
  }));
346
462
  }
463
+ var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
347
464
  function maybePrefixBearer(target, value, raw = false) {
348
465
  if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
349
466
  return value;
@@ -351,25 +468,28 @@ function maybePrefixBearer(target, value, raw = false) {
351
468
  if (/^\S+\s+\S+/.test(value)) {
352
469
  return value;
353
470
  }
354
- return `Bearer ${value}`;
471
+ if (TOKEN68.test(value)) {
472
+ return `Bearer ${value}`;
473
+ }
474
+ return value;
355
475
  }
356
476
 
357
477
  // src/core/spawn-env.ts
358
478
  import os from "node:os";
359
- import path from "node:path";
360
- import fs from "node:fs";
479
+ import path2 from "node:path";
480
+ import fs2 from "node:fs";
361
481
  function existingNvmCurrentBin() {
362
- const nvmDir = process.env.NVM_DIR ?? path.join(os.homedir(), ".nvm");
363
- const current = path.join(nvmDir, "current", "bin");
364
- return fs.existsSync(current) ? current : null;
482
+ const nvmDir = process.env.NVM_DIR ?? path2.join(os.homedir(), ".nvm");
483
+ const current = path2.join(nvmDir, "current", "bin");
484
+ return fs2.existsSync(current) ? current : null;
365
485
  }
366
486
  function buildEnrichedPath(current = process.env.PATH ?? "") {
367
487
  const candidates = [
368
488
  ...current.split(":"),
369
489
  "/opt/homebrew/bin",
370
490
  "/usr/local/bin",
371
- path.join(os.homedir(), ".bun", "bin"),
372
- path.join(os.homedir(), ".local", "bin"),
491
+ path2.join(os.homedir(), ".bun", "bin"),
492
+ path2.join(os.homedir(), ".local", "bin"),
373
493
  existingNvmCurrentBin()
374
494
  ].filter((entry) => Boolean(entry));
375
495
  const seen = new Set;
@@ -605,7 +725,12 @@ function bindOAuthReference(config, serverName) {
605
725
  };
606
726
  }
607
727
  async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver) {
608
- new McpxOAuthProvider(serverName, secrets).invalidateCredentials("all");
728
+ const oauthName = oauthSecretName(serverName, "tokens");
729
+ const clientName = oauthSecretName(serverName, "client");
730
+ const backupTokens = secrets.getSecret(oauthName);
731
+ const backupClient = secrets.getSecret(clientName);
732
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
733
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
609
734
  const state = crypto.randomUUID();
610
735
  let callbackServer;
611
736
  let redirectUrl;
@@ -638,8 +763,18 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
638
763
  const config = loadConfig(configPath);
639
764
  bindOAuthReference(config, serverName);
640
765
  saveConfig(config, configPath);
641
- syncAllClients(config, secrets);
766
+ const summary = syncAllClients(config, secrets);
767
+ persistSyncState(summary, config);
768
+ saveConfig(config, configPath);
769
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
642
770
  return { serverName, authorized: true };
771
+ } catch (error) {
772
+ if (backupTokens)
773
+ secrets.setSecret(oauthName, backupTokens);
774
+ if (backupClient)
775
+ secrets.setSecret(clientName, backupClient);
776
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
777
+ throw error;
643
778
  } finally {
644
779
  if (callbackServer) {
645
780
  await closeServer(callbackServer);
@@ -649,40 +784,57 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
649
784
  async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
650
785
  const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
651
786
  if (!stored) {
652
- throw new Error(`No OAuth tokens stored for "${serverName}". Run mcpx auth login ${serverName}.`);
787
+ throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
653
788
  }
654
- if (!options.forceRefresh && !tokensAreExpiring(stored)) {
655
- return stored.tokens.access_token;
656
- }
657
- if (!stored.tokens.refresh_token) {
658
- if (!options.forceRefresh) {
659
- return stored.tokens.access_token;
789
+ if (options.forceRefresh || tokensAreExpiring(stored)) {
790
+ if (!stored.tokens.refresh_token) {
791
+ if (!options.forceRefresh) {
792
+ return stored.tokens.access_token;
793
+ }
794
+ throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
795
+ }
796
+ const provider = new McpxOAuthProvider(serverName, secrets);
797
+ const clientInformation = provider.clientInformation();
798
+ if (!clientInformation) {
799
+ throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
800
+ }
801
+ let serverInfo;
802
+ try {
803
+ const cachedDiscovery = provider.discoveryState();
804
+ serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
805
+ } catch (err) {
806
+ const message = err instanceof Error ? err.message : String(err);
807
+ if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
808
+ throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
809
+ }
810
+ throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
811
+ }
812
+ try {
813
+ const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
814
+ metadata: serverInfo.authorizationServerMetadata,
815
+ clientInformation,
816
+ refreshToken: stored.tokens.refresh_token,
817
+ resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
818
+ });
819
+ provider.saveTokens(refreshed);
820
+ return refreshed.access_token;
821
+ } catch (err) {
822
+ const message = err instanceof Error ? err.message : String(err);
823
+ if (/invalid_grant|invalid_client|4\d{2}/i.test(message)) {
824
+ throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
825
+ }
826
+ if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
827
+ throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
828
+ }
829
+ throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
660
830
  }
661
- throw new Error(`No OAuth refresh token stored for "${serverName}". Re-authentication required.`);
662
- }
663
- const provider = new McpxOAuthProvider(serverName, secrets);
664
- const clientInformation = provider.clientInformation();
665
- if (!clientInformation) {
666
- throw new Error(`No OAuth client information stored for "${serverName}". Re-authentication required.`);
667
831
  }
668
- const cachedDiscovery = provider.discoveryState();
669
- const serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
670
- const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
671
- metadata: serverInfo.authorizationServerMetadata,
672
- clientInformation,
673
- refreshToken: stored.tokens.refresh_token,
674
- resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
675
- });
676
- provider.saveTokens(refreshed);
677
- return refreshed.access_token;
832
+ return stored.tokens.access_token;
678
833
  }
679
834
 
680
- // src/version.ts
681
- var APP_VERSION = "0.1.3";
682
-
683
835
  // src/core/update.ts
684
- import fs2 from "node:fs";
685
- import path2 from "node:path";
836
+ import fs3 from "node:fs";
837
+ import path3 from "node:path";
686
838
  import { execFileSync } from "node:child_process";
687
839
  function compareVersions(a, b) {
688
840
  const partsA = a.split(".").map(Number);
@@ -721,17 +873,17 @@ async function checkForUpdates() {
721
873
  }
722
874
  function getStagedUpdate() {
723
875
  const stagedPath = getStagedVersionPath();
724
- if (!fs2.existsSync(stagedPath)) {
876
+ if (!fs3.existsSync(stagedPath)) {
725
877
  return null;
726
878
  }
727
879
  try {
728
- const raw = fs2.readFileSync(stagedPath, "utf8");
880
+ const raw = fs3.readFileSync(stagedPath, "utf8");
729
881
  const info = JSON.parse(raw);
730
882
  if (!info.version || !info.cliPath) {
731
883
  return null;
732
884
  }
733
- const absoluteCliPath = path2.isAbsolute(info.cliPath) ? info.cliPath : path2.join(getUpdatesDir(), info.cliPath);
734
- if (!fs2.existsSync(absoluteCliPath)) {
885
+ const absoluteCliPath = path3.isAbsolute(info.cliPath) ? info.cliPath : path3.join(getUpdatesDir(), info.cliPath);
886
+ if (!fs3.existsSync(absoluteCliPath)) {
735
887
  return null;
736
888
  }
737
889
  return {
@@ -747,18 +899,18 @@ function stageUpdate(version, cliPath) {
747
899
  ensureDir(updatesDir);
748
900
  const stagedInfo = {
749
901
  version,
750
- cliPath: path2.isAbsolute(cliPath) ? path2.relative(updatesDir, cliPath) : cliPath,
902
+ cliPath: path3.isAbsolute(cliPath) ? path3.relative(updatesDir, cliPath) : cliPath,
751
903
  stagedAt: new Date().toISOString()
752
904
  };
753
- fs2.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
905
+ fs3.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
754
906
  }
755
907
  function shouldUseStagedCli(stagedVersion, currentVersion) {
756
908
  return compareVersions(stagedVersion, currentVersion) > 0;
757
909
  }
758
910
  function clearStagedUpdate() {
759
911
  const stagedPath = getStagedVersionPath();
760
- if (fs2.existsSync(stagedPath)) {
761
- fs2.unlinkSync(stagedPath);
912
+ if (fs3.existsSync(stagedPath)) {
913
+ fs3.unlinkSync(stagedPath);
762
914
  }
763
915
  }
764
916
  function getStagedCliPath() {
@@ -767,59 +919,59 @@ function getStagedCliPath() {
767
919
  }
768
920
  function removeOldVersions(keepCount = 2) {
769
921
  const updatesDir = getUpdatesDir();
770
- if (!fs2.existsSync(updatesDir)) {
922
+ if (!fs3.existsSync(updatesDir)) {
771
923
  return;
772
924
  }
773
- const entries = fs2.readdirSync(updatesDir, { withFileTypes: true });
925
+ const entries = fs3.readdirSync(updatesDir, { withFileTypes: true });
774
926
  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)));
775
927
  if (versionDirs.length <= keepCount) {
776
928
  return;
777
929
  }
778
930
  for (const oldVersion of versionDirs.slice(keepCount)) {
779
- const oldPath = path2.join(updatesDir, oldVersion);
931
+ const oldPath = path3.join(updatesDir, oldVersion);
780
932
  try {
781
- fs2.rmSync(oldPath, { recursive: true, force: true });
933
+ fs3.rmSync(oldPath, { recursive: true, force: true });
782
934
  } catch {}
783
935
  }
784
936
  }
785
937
 
786
938
  // src/core/update-manager.ts
787
- import fs3 from "node:fs";
788
- import path3 from "node:path";
939
+ import fs4 from "node:fs";
940
+ import path4 from "node:path";
789
941
  import { execSync, spawn } from "node:child_process";
790
- function acquireLock() {
942
+ function acquireLock2() {
791
943
  const lockPath = getUpdateLockPath();
792
944
  try {
793
- ensureDir(path3.dirname(lockPath));
794
- fs3.writeFileSync(lockPath, `${process.pid}
945
+ ensureDir(path4.dirname(lockPath));
946
+ fs4.writeFileSync(lockPath, `${process.pid}
795
947
  `, { flag: "wx" });
796
948
  return true;
797
949
  } catch {
798
950
  return false;
799
951
  }
800
952
  }
801
- function releaseLock() {
953
+ function releaseLock2() {
802
954
  const lockPath = getUpdateLockPath();
803
955
  try {
804
- fs3.unlinkSync(lockPath);
956
+ fs4.unlinkSync(lockPath);
805
957
  } catch {}
806
958
  }
807
959
  function isUpdateInProgress() {
808
960
  const lockPath = getUpdateLockPath();
809
- if (!fs3.existsSync(lockPath)) {
961
+ if (!fs4.existsSync(lockPath)) {
810
962
  return false;
811
963
  }
812
964
  try {
813
- const raw = fs3.readFileSync(lockPath, "utf8").trim();
965
+ const raw = fs4.readFileSync(lockPath, "utf8").trim();
814
966
  const pid = Number(raw);
815
967
  if (!Number.isFinite(pid) || pid <= 0) {
816
- fs3.unlinkSync(lockPath);
968
+ fs4.unlinkSync(lockPath);
817
969
  return false;
818
970
  }
819
971
  process.kill(pid, 0);
820
972
  return true;
821
973
  } catch {
822
- fs3.unlinkSync(lockPath);
974
+ fs4.unlinkSync(lockPath);
823
975
  return false;
824
976
  }
825
977
  }
@@ -830,8 +982,8 @@ async function downloadAndStageUpdate() {
830
982
  }
831
983
  const targetVersion = updateStatus.latestVersion;
832
984
  const updatesDir = getUpdatesDir();
833
- const versionDir = path3.join(updatesDir, `v${targetVersion}`);
834
- if (fs3.existsSync(versionDir)) {
985
+ const versionDir = path4.join(updatesDir, `v${targetVersion}`);
986
+ if (fs4.existsSync(versionDir)) {
835
987
  const existingStaged = getStagedUpdate();
836
988
  if (existingStaged?.version === targetVersion) {
837
989
  return { success: false, error: `Version ${targetVersion} already staged` };
@@ -843,8 +995,8 @@ async function downloadAndStageUpdate() {
843
995
  cwd: versionDir,
844
996
  stdio: ["pipe", "pipe", "pipe"]
845
997
  });
846
- const tarballPath = path3.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
847
- if (!fs3.existsSync(tarballPath)) {
998
+ const tarballPath = path4.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
999
+ if (!fs4.existsSync(tarballPath)) {
848
1000
  throw new Error("Downloaded tarball not found");
849
1001
  }
850
1002
  execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
@@ -855,16 +1007,24 @@ async function downloadAndStageUpdate() {
855
1007
  cwd: versionDir,
856
1008
  stdio: ["pipe", "pipe", "pipe"]
857
1009
  });
858
- const cliPath = path3.join(versionDir, "dist", "cli.js");
859
- if (!fs3.existsSync(cliPath)) {
1010
+ const cliPath = path4.join(versionDir, "dist", "cli.js");
1011
+ if (!fs4.existsSync(cliPath)) {
860
1012
  throw new Error("CLI entry point not found in downloaded package");
861
1013
  }
1014
+ try {
1015
+ execSync(`${process.execPath} ${JSON.stringify(cliPath)} --version`, {
1016
+ stdio: ["ignore", "pipe", "pipe"],
1017
+ timeout: 1e4
1018
+ });
1019
+ } catch (smokeTestError) {
1020
+ throw new Error(`Downloaded CLI failed smoke test (--version): ${smokeTestError instanceof Error ? smokeTestError.message : String(smokeTestError)}`);
1021
+ }
862
1022
  stageUpdate(targetVersion, cliPath);
863
1023
  removeOldVersions(2);
864
1024
  return { success: true, version: targetVersion };
865
1025
  } catch (error) {
866
1026
  try {
867
- fs3.rmSync(versionDir, { recursive: true, force: true });
1027
+ fs4.rmSync(versionDir, { recursive: true, force: true });
868
1028
  } catch {}
869
1029
  return {
870
1030
  success: false,
@@ -873,10 +1033,13 @@ async function downloadAndStageUpdate() {
873
1033
  }
874
1034
  }
875
1035
  function startBackgroundUpdateCheck() {
1036
+ if (process.env.MCPX_NO_UPDATE === "1") {
1037
+ return;
1038
+ }
876
1039
  if (isUpdateInProgress()) {
877
1040
  return;
878
1041
  }
879
- if (!acquireLock()) {
1042
+ if (!acquireLock2()) {
880
1043
  return;
881
1044
  }
882
1045
  const child = spawn(process.execPath, [
@@ -893,7 +1056,7 @@ function startBackgroundUpdateCheck() {
893
1056
  });
894
1057
  child.unref();
895
1058
  setTimeout(() => {
896
- releaseLock();
1059
+ releaseLock2();
897
1060
  }, 60000);
898
1061
  }
899
1062
  async function runBackgroundUpdate() {
@@ -907,27 +1070,34 @@ async function runBackgroundUpdate() {
907
1070
  } catch (error) {
908
1071
  console.error(`[update-manager] Background update failed: ${error instanceof Error ? error.message : String(error)}`);
909
1072
  } finally {
910
- releaseLock();
1073
+ releaseLock2();
911
1074
  }
912
1075
  }
913
1076
  async function performUpdate() {
914
1077
  if (isUpdateInProgress()) {
915
1078
  return { success: false, message: "Another update is already in progress." };
916
1079
  }
917
- const result = await downloadAndStageUpdate();
918
- if (result.success && result.version) {
1080
+ if (!acquireLock2()) {
1081
+ return { success: false, message: "Another update is already in progress." };
1082
+ }
1083
+ try {
1084
+ const result = await downloadAndStageUpdate();
1085
+ if (result.success && result.version) {
1086
+ return {
1087
+ success: true,
1088
+ message: `Update to v${result.version} ready! Will activate on next run.`
1089
+ };
1090
+ }
1091
+ if (result.error === "No update available") {
1092
+ return { success: true, message: "Already on latest version." };
1093
+ }
919
1094
  return {
920
- success: true,
921
- message: `Update to v${result.version} ready! Will activate on next run.`
1095
+ success: false,
1096
+ message: result.error ?? "Update failed"
922
1097
  };
1098
+ } finally {
1099
+ releaseLock2();
923
1100
  }
924
- if (result.error === "No update available") {
925
- return { success: true, message: "Already on latest version." };
926
- }
927
- return {
928
- success: false,
929
- message: result.error ?? "Update failed"
930
- };
931
1101
  }
932
1102
  function performRollback() {
933
1103
  const currentStaged = getStagedUpdate();
@@ -942,9 +1112,9 @@ function performRollback() {
942
1112
  }
943
1113
 
944
1114
  // src/core/daemon.ts
945
- import fs4 from "node:fs";
1115
+ import fs5 from "node:fs";
946
1116
  import net from "node:net";
947
- import { spawn as spawn2 } from "node:child_process";
1117
+ import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
948
1118
 
949
1119
  // src/gateway/server.ts
950
1120
  import http2 from "node:http";
@@ -964,19 +1134,6 @@ var OAUTH_WELL_KNOWN_PREFIXES = [
964
1134
  "/.well-known/oauth-authorization-server",
965
1135
  "/.well-known/openid-configuration"
966
1136
  ];
967
-
968
- class UpstreamHttpError extends Error {
969
- status;
970
- wwwAuthenticate;
971
- bodyText;
972
- constructor(upstreamName, status, bodyText, wwwAuthenticate) {
973
- super(`Upstream ${upstreamName} returned HTTP ${status}: ${bodyText.slice(0, 400)}`);
974
- this.name = "UpstreamHttpError";
975
- this.status = status;
976
- this.bodyText = bodyText;
977
- this.wwwAuthenticate = wwwAuthenticate;
978
- }
979
- }
980
1137
  function makeError(id, code, message, data) {
981
1138
  return {
982
1139
  jsonrpc: "2.0",
@@ -1135,8 +1292,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1135
1292
  const fingerprint = specFingerprint(upstream.spec);
1136
1293
  const cached = runtime.tokenCache.get(upstream.name);
1137
1294
  if (cached?.fingerprint === fingerprint) {
1138
- const runtimeError2 = runtime.upstreamErrors?.get(upstream.name);
1139
- results[upstream.name] = runtimeError2 ? { ...cached.count, runtimeError: runtimeError2 } : cached.count;
1295
+ const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
1296
+ results[upstream.name] = runtimeErr2 ? { ...cached.count, runtimeError: runtimeErr2.message, runtimeErrorCode: runtimeErr2.code } : cached.count;
1140
1297
  continue;
1141
1298
  }
1142
1299
  let toolsCount = 0;
@@ -1178,8 +1335,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1178
1335
  if (errors.length === 0) {
1179
1336
  runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
1180
1337
  }
1181
- const runtimeError = runtime.upstreamErrors?.get(upstream.name);
1182
- results[upstream.name] = runtimeError ? { ...countObj, runtimeError } : countObj;
1338
+ const runtimeErr = runtime.upstreamErrors?.get(upstream.name);
1339
+ results[upstream.name] = runtimeErr ? { ...countObj, runtimeError: runtimeErr.message, runtimeErrorCode: runtimeErr.code } : countObj;
1183
1340
  }
1184
1341
  return results;
1185
1342
  }
@@ -1199,11 +1356,41 @@ function withTimeout(work, timeoutMs, timeoutMessage) {
1199
1356
  });
1200
1357
  });
1201
1358
  }
1202
- function specFingerprint(spec) {
1203
- return JSON.stringify(spec);
1359
+ function specFingerprint(spec, secrets) {
1360
+ if (!secrets)
1361
+ return JSON.stringify(spec);
1362
+ const resolvedMarkers = {};
1363
+ const collect = (obj) => {
1364
+ if (!obj)
1365
+ return;
1366
+ for (const [key, value] of Object.entries(obj)) {
1367
+ if (value.startsWith("secret://") || value.startsWith("oauth://")) {
1368
+ try {
1369
+ resolvedMarkers[key] = secrets.resolveMaybeSecret(value);
1370
+ } catch {
1371
+ resolvedMarkers[key] = value;
1372
+ }
1373
+ }
1374
+ }
1375
+ };
1376
+ if (spec.transport === "stdio")
1377
+ collect(spec.env);
1378
+ if (spec.transport === "http")
1379
+ collect(spec.headers);
1380
+ return JSON.stringify({ spec, resolvedMarkers });
1204
1381
  }
1382
+ var EXTRA_INHERITED_ENV = ["TMPDIR", "LANG"];
1205
1383
  function resolveStdioEnv(spec, secrets) {
1206
1384
  const env = getDefaultEnvironment();
1385
+ for (const key of EXTRA_INHERITED_ENV) {
1386
+ const value = process.env[key];
1387
+ if (value)
1388
+ env[key] = value;
1389
+ }
1390
+ for (const [key, value] of Object.entries(process.env)) {
1391
+ if (key.startsWith("LC_") && value)
1392
+ env[key] = value;
1393
+ }
1207
1394
  env.PATH = buildEnrichedPath(env.PATH);
1208
1395
  for (const [key, value] of Object.entries(spec.env ?? {})) {
1209
1396
  env[key] = secrets.resolveMaybeSecret(value);
@@ -1348,11 +1535,8 @@ async function callUpstreamOnce(upstream, method, params, secrets, runtime, pass
1348
1535
  if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
1349
1536
  return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
1350
1537
  }
1351
- if (upstream.spec.transport === "http" && error instanceof StreamableHTTPError && error.code) {
1352
- const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
1353
- throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
1354
- }
1355
- throw error;
1538
+ const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
1539
+ throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
1356
1540
  }
1357
1541
  }
1358
1542
  function authHeaderIsValid(request, expectedToken) {
@@ -1371,7 +1555,7 @@ function authHeaderIsValid(request, expectedToken) {
1371
1555
  return scheme === "Bearer" && token === expectedToken;
1372
1556
  }
1373
1557
  function isAuthChallenge(error) {
1374
- return error instanceof UpstreamHttpError && (error.status === 401 || error.status === 403);
1558
+ return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
1375
1559
  }
1376
1560
  function getClientAuthorizationForUpstream(request, expectedToken) {
1377
1561
  const authHeader = request.headers.authorization;
@@ -1412,10 +1596,15 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
1412
1596
  if (upstreams.length === 1) {
1413
1597
  throw error;
1414
1598
  }
1415
- runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
1599
+ const code = error instanceof UpstreamError ? error.code : "upstream_error";
1600
+ runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
1416
1601
  }
1417
1602
  }
1418
- return { tools };
1603
+ const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
1604
+ return {
1605
+ tools,
1606
+ ...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
1607
+ };
1419
1608
  }
1420
1609
  async function handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
1421
1610
  const resources = [];
@@ -1447,10 +1636,15 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
1447
1636
  if (upstreams.length === 1) {
1448
1637
  throw error;
1449
1638
  }
1450
- runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
1639
+ const code = error instanceof UpstreamError ? error.code : "upstream_error";
1640
+ runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
1451
1641
  }
1452
1642
  }
1453
- return { resources };
1643
+ const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
1644
+ return {
1645
+ resources,
1646
+ ...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
1647
+ };
1454
1648
  }
1455
1649
  async function handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
1456
1650
  const prompts = [];
@@ -1480,10 +1674,15 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
1480
1674
  if (upstreams.length === 1) {
1481
1675
  throw error;
1482
1676
  }
1483
- runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
1677
+ const code = error instanceof UpstreamError ? error.code : "upstream_error";
1678
+ runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
1484
1679
  }
1485
1680
  }
1486
- return { prompts };
1681
+ const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
1682
+ return {
1683
+ prompts,
1684
+ ...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
1685
+ };
1487
1686
  }
1488
1687
  async function routeNamespacedCall(config, method, params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
1489
1688
  if (!params || typeof params !== "object") {
@@ -1521,8 +1720,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
1521
1720
  if (isAuthChallenge(error)) {
1522
1721
  throw error;
1523
1722
  }
1524
- runtime.upstreamErrors?.set(upstream2.name, error.message);
1525
- return makeError(id, -32000, error.message);
1723
+ const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
1724
+ runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
1725
+ return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
1526
1726
  }
1527
1727
  }
1528
1728
  if (method === "prompts/get") {
@@ -1554,8 +1754,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
1554
1754
  if (isAuthChallenge(error)) {
1555
1755
  throw error;
1556
1756
  }
1557
- runtime.upstreamErrors?.set(upstream2.name, error.message);
1558
- return makeError(id, -32000, error.message);
1757
+ const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
1758
+ runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
1759
+ return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
1559
1760
  }
1560
1761
  }
1561
1762
  const uri = typeof params.uri === "string" ? params.uri : "";
@@ -1586,8 +1787,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
1586
1787
  if (isAuthChallenge(error)) {
1587
1788
  throw error;
1588
1789
  }
1589
- runtime.upstreamErrors?.set(upstream.name, error.message);
1590
- return makeError(id, -32000, error.message);
1790
+ const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
1791
+ runtime.upstreamErrors?.set(upstream.name, { code: errCode, message: error.message });
1792
+ return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream.name });
1591
1793
  }
1592
1794
  }
1593
1795
  async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
@@ -1754,6 +1956,16 @@ function createGatewayServer(options) {
1754
1956
  }
1755
1957
  return;
1756
1958
  }
1959
+ const wantsStream = (request.headers.accept ?? "").includes("text/event-stream");
1960
+ if (wantsStream && request.headers["mcp-session-id"]) {
1961
+ response.statusCode = 405;
1962
+ response.setHeader("content-type", "application/json");
1963
+ response.end(JSON.stringify({ error: "get_stream_not_supported", message: "mcpx does not support server-initiated SSE streams over GET; use POST for all requests." }));
1964
+ if (debug) {
1965
+ console.error(`[mcpx gateway] -> 405 (GET stream not supported)`);
1966
+ }
1967
+ return;
1968
+ }
1757
1969
  response.statusCode = 200;
1758
1970
  response.setHeader("content-type", "application/json");
1759
1971
  response.end(JSON.stringify({ ok: true, server: "mcpx" }));
@@ -1858,16 +2070,16 @@ function createGatewayServer(options) {
1858
2070
  }
1859
2071
  } catch (error) {
1860
2072
  if (isAuthChallenge(error)) {
1861
- response.statusCode = error.status;
2073
+ response.statusCode = error.status ?? 401;
1862
2074
  response.setHeader("content-type", "application/json");
1863
2075
  if (error.wwwAuthenticate) {
1864
2076
  const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
1865
2077
  response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
1866
2078
  }
1867
2079
  response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
2080
+ mcpxCode: error.code,
1868
2081
  status: error.status,
1869
- body: error.bodyText,
1870
- wwwAuthenticate: error.wwwAuthenticate
2082
+ upstream: error.upstream
1871
2083
  })));
1872
2084
  if (debug) {
1873
2085
  console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
@@ -1893,19 +2105,24 @@ function createGatewayServer(options) {
1893
2105
  }
1894
2106
 
1895
2107
  // src/core/daemon.ts
1896
- function readPidFromFile(pidPath = getPidPath()) {
1897
- if (!fs4.existsSync(pidPath)) {
2108
+ function readPidRecordFromFile(pidPath = getPidPath()) {
2109
+ if (!fs5.existsSync(pidPath)) {
1898
2110
  return null;
1899
2111
  }
1900
- const raw = fs4.readFileSync(pidPath, "utf8").trim();
2112
+ const raw = fs5.readFileSync(pidPath, "utf8").trim();
1901
2113
  if (!raw) {
1902
2114
  return null;
1903
2115
  }
1904
- const pid = Number(raw);
2116
+ const [pidRaw, portRaw] = raw.split(":");
2117
+ const pid = Number(pidRaw);
1905
2118
  if (!Number.isFinite(pid) || pid <= 0) {
1906
2119
  return null;
1907
2120
  }
1908
- return pid;
2121
+ const port = portRaw ? Number(portRaw) : null;
2122
+ return { pid, port: Number.isFinite(port) && port > 0 ? port : null };
2123
+ }
2124
+ function readPidFromFile(pidPath = getPidPath()) {
2125
+ return readPidRecordFromFile(pidPath)?.pid ?? null;
1909
2126
  }
1910
2127
  function processExists(pid) {
1911
2128
  try {
@@ -1927,9 +2144,9 @@ async function isPortAvailable(port) {
1927
2144
  tester.listen(port, "127.0.0.1");
1928
2145
  });
1929
2146
  }
1930
- async function resolveGatewayPort(config) {
2147
+ async function resolveGatewayPort(config, secrets) {
1931
2148
  if (await isPortAvailable(config.gateway.port)) {
1932
- return config.gateway.port;
2149
+ return { port: config.gateway.port };
1933
2150
  }
1934
2151
  for (let offset = 1;offset <= 20; offset += 1) {
1935
2152
  const candidate = config.gateway.port + offset;
@@ -1937,9 +2154,17 @@ async function resolveGatewayPort(config) {
1937
2154
  break;
1938
2155
  }
1939
2156
  if (await isPortAvailable(candidate)) {
2157
+ const oldPort = config.gateway.port;
1940
2158
  config.gateway.port = candidate;
1941
2159
  saveConfig(config);
1942
- return candidate;
2160
+ if (secrets) {
2161
+ try {
2162
+ const summary = syncAllClients(config, secrets);
2163
+ persistSyncState(summary, config);
2164
+ saveConfig(config);
2165
+ } catch {}
2166
+ }
2167
+ return { port: candidate, fellBackFrom: oldPort };
1943
2168
  }
1944
2169
  }
1945
2170
  throw new Error(`No available local port found near ${config.gateway.port}.`);
@@ -1969,8 +2194,8 @@ async function waitForGatewayReady(port, token, timeoutMs = 5000) {
1969
2194
  }
1970
2195
  function getDaemonStatus(config) {
1971
2196
  const pidPath = getPidPath();
1972
- const pid = readPidFromFile(pidPath);
1973
- if (!pid) {
2197
+ const record = readPidRecordFromFile(pidPath);
2198
+ if (!record) {
1974
2199
  return {
1975
2200
  running: false,
1976
2201
  pidFile: pidPath,
@@ -1978,12 +2203,15 @@ function getDaemonStatus(config) {
1978
2203
  port: config.gateway.port
1979
2204
  };
1980
2205
  }
2206
+ const running = processExists(record.pid);
2207
+ const port = record.port ?? config.gateway.port;
1981
2208
  return {
1982
- running: processExists(pid),
1983
- pid,
2209
+ running,
2210
+ pid: record.pid,
1984
2211
  pidFile: pidPath,
1985
2212
  logFile: getLogPath(),
1986
- port: config.gateway.port
2213
+ port,
2214
+ portMismatch: running && record.port !== null && record.port !== config.gateway.port
1987
2215
  };
1988
2216
  }
1989
2217
  async function startDaemon(config, cliPath, secrets) {
@@ -1999,16 +2227,38 @@ async function startDaemon(config, cliPath, secrets) {
1999
2227
  };
2000
2228
  }
2001
2229
  try {
2002
- fs4.unlinkSync(getPidPath());
2230
+ fs5.unlinkSync(getPidPath());
2003
2231
  } catch {}
2004
2232
  }
2005
- const port = await resolveGatewayPort(config);
2233
+ const { port, fellBackFrom } = await resolveGatewayPort(config, secrets);
2234
+ if (fellBackFrom) {
2235
+ process.stderr.write(`mcpx: port ${fellBackFrom} was unavailable; gateway started on ${port} instead. All client configs were re-synced to the new port.
2236
+ `);
2237
+ }
2006
2238
  const token = ensureGatewayToken(config, secrets);
2007
2239
  const pidPath = getPidPath();
2008
2240
  const logPath = getLogPath();
2009
2241
  ensureParentDir(pidPath);
2010
2242
  ensureParentDir(logPath);
2011
- const logFd = fs4.openSync(logPath, "a", 384);
2243
+ let logFd = fs5.openSync(logPath, "a", 384);
2244
+ const logStat = fs5.fstatSync(logFd);
2245
+ if (logStat.size > 10 * 1024 * 1024) {
2246
+ fs5.closeSync(logFd);
2247
+ const log1 = `${logPath}.1`;
2248
+ const log2 = `${logPath}.2`;
2249
+ try {
2250
+ if (fs5.existsSync(log2))
2251
+ fs5.unlinkSync(log2);
2252
+ } catch {}
2253
+ try {
2254
+ if (fs5.existsSync(log1))
2255
+ fs5.renameSync(log1, log2);
2256
+ } catch {}
2257
+ try {
2258
+ fs5.renameSync(logPath, log1);
2259
+ } catch {}
2260
+ logFd = fs5.openSync(logPath, "a", 384);
2261
+ }
2012
2262
  const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
2013
2263
  detached: true,
2014
2264
  stdio: ["ignore", logFd, logFd],
@@ -2018,7 +2268,7 @@ async function startDaemon(config, cliPath, secrets) {
2018
2268
  }
2019
2269
  });
2020
2270
  child.unref();
2021
- fs4.writeFileSync(pidPath, `${child.pid}
2271
+ fs5.writeFileSync(pidPath, `${child.pid}:${port}
2022
2272
  `, { mode: 384 });
2023
2273
  startBackgroundUpdateCheck();
2024
2274
  await waitForGatewayReady(port, token);
@@ -2038,11 +2288,30 @@ function stopDaemon() {
2038
2288
  message: "mcpx daemon is not running."
2039
2289
  };
2040
2290
  }
2291
+ try {
2292
+ const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
2293
+ const looksLikeMcpxDaemon = cmd.includes("daemon run") && (cmd.includes("mcpx") || cmd.includes("cli.js"));
2294
+ if (!looksLikeMcpxDaemon) {
2295
+ fs5.unlinkSync(pidPath);
2296
+ return {
2297
+ stopped: false,
2298
+ message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
2299
+ };
2300
+ }
2301
+ } catch {
2302
+ try {
2303
+ fs5.unlinkSync(pidPath);
2304
+ } catch {}
2305
+ return {
2306
+ stopped: false,
2307
+ message: `PID ${pid} not found. Pidfile cleaned up.`
2308
+ };
2309
+ }
2041
2310
  try {
2042
2311
  process.kill(pid, "SIGTERM");
2043
2312
  } catch {}
2044
2313
  try {
2045
- fs4.unlinkSync(pidPath);
2314
+ fs5.unlinkSync(pidPath);
2046
2315
  } catch {}
2047
2316
  return {
2048
2317
  stopped: true,
@@ -2055,10 +2324,10 @@ async function restartDaemon(config, cliPath, secrets) {
2055
2324
  }
2056
2325
  function readDaemonLogs(maxLines = 200) {
2057
2326
  const logPath = getLogPath();
2058
- if (!fs4.existsSync(logPath)) {
2327
+ if (!fs5.existsSync(logPath)) {
2059
2328
  return "";
2060
2329
  }
2061
- const lines = fs4.readFileSync(logPath, "utf8").split(`
2330
+ const lines = fs5.readFileSync(logPath, "utf8").split(`
2062
2331
  `);
2063
2332
  return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
2064
2333
  `);
@@ -2083,7 +2352,7 @@ function runDaemonForeground(config, port, secrets) {
2083
2352
  }
2084
2353
 
2085
2354
  // src/core/status.ts
2086
- var STATUS_CLIENTS = ["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen"];
2355
+ var STATUS_CLIENTS = ["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen", "openclaw", "hermes"];
2087
2356
  function describeServerTarget(spec) {
2088
2357
  if (spec.transport === "http") {
2089
2358
  return spec.url;
@@ -2136,15 +2405,15 @@ async function fetchTokenCounts(gatewayUrl, token) {
2136
2405
  method: "custom/tokenCounts",
2137
2406
  params: {}
2138
2407
  }),
2139
- signal: AbortSignal.timeout(4000)
2408
+ signal: AbortSignal.timeout(65000)
2140
2409
  });
2141
2410
  if (!res.ok) {
2142
- return {};
2411
+ return { counts: {}, fetchError: `HTTP ${res.status}` };
2143
2412
  }
2144
2413
  const data = await res.json();
2145
- return data?.result ?? {};
2414
+ return { counts: data?.result ?? {} };
2146
2415
  } catch (error) {
2147
- return {};
2416
+ return { counts: {}, fetchError: error instanceof Error ? error.message : String(error) };
2148
2417
  }
2149
2418
  }
2150
2419
  async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
@@ -2152,8 +2421,11 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
2152
2421
  const token = ensureGatewayToken(config, secrets);
2153
2422
  const gatewayUrl = getGatewayUrl(config);
2154
2423
  let tokenCounts = {};
2424
+ let tokenCountFetchError;
2155
2425
  if (daemon.running) {
2156
- tokenCounts = await fetchTokenCounts(gatewayUrl, token);
2426
+ const result = await fetchTokenCounts(gatewayUrl, token);
2427
+ tokenCounts = result.counts;
2428
+ tokenCountFetchError = result.fetchError;
2157
2429
  }
2158
2430
  const projectEntries = Object.values(config.projects ?? {});
2159
2431
  let totalGlobalTokens = 0;
@@ -2165,6 +2437,7 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
2165
2437
  if (tokenCount?.error && buildAuthBindings(spec).length > 0 && tokenCount.total > 0) {
2166
2438
  tokenCount = { ...tokenCount, error: undefined };
2167
2439
  }
2440
+ const health = tokenCount ? tokenCount.error || tokenCount.runtimeError ? "error" : "ok" : "unknown";
2168
2441
  const serverEntry = {
2169
2442
  name,
2170
2443
  enabled: isServerEnabled(spec),
@@ -2172,7 +2445,8 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
2172
2445
  target: describeServerTarget(spec),
2173
2446
  authBindings: buildAuthBindings(spec),
2174
2447
  clients: buildClientMappings(config, managedIndex, name),
2175
- tokenCount
2448
+ tokenCount,
2449
+ health
2176
2450
  };
2177
2451
  if (serverEntry.enabled && tokenCount) {
2178
2452
  totalGlobalTokens += tokenCount.total;
@@ -2196,4 +2470,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
2196
2470
  };
2197
2471
  }
2198
2472
 
2199
- export { probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
2473
+ export { mutateConfig, mutateProjectConfig, mutateActiveConfig, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };