@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.
- package/dist/adapters/claude-desktop.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts +2 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts +3 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/cursor.d.ts +2 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/hermes.d.ts +11 -0
- package/dist/adapters/hermes.d.ts.map +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/openclaw.d.ts +11 -0
- package/dist/adapters/openclaw.d.ts.map +1 -0
- package/dist/adapters/opencode.d.ts +2 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/qwen.d.ts.map +1 -1
- package/dist/adapters/utils/index.d.ts +18 -4
- package/dist/adapters/utils/index.d.ts.map +1 -1
- package/dist/adapters/vscode.d.ts +2 -1
- package/dist/adapters/vscode.d.ts.map +1 -1
- package/dist/{cli-y7ba5qvr.js → cli-28fsxhe3.js} +430 -156
- package/dist/{cli-qvkpwy9w.js → cli-njtbb11m.js} +138 -43
- package/dist/{cli-8vxva0cb.js → cli-tk8bggg5.js} +81 -8
- package/dist/{cli-np0wwh6m.js → cli-v27bt25d.js} +157 -11
- package/dist/{cli-kwyp9ywh.js → cli-wxw1t3rr.js} +941 -303
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +388 -77
- package/dist/compat/hermes.d.ts +7 -0
- package/dist/compat/hermes.d.ts.map +1 -0
- package/dist/compat/index.d.ts.map +1 -1
- package/dist/compat/openclaw.d.ts +7 -0
- package/dist/compat/openclaw.d.ts.map +1 -0
- package/dist/{config-2s5nsm2g.js → config-5p08cb0c.js} +6 -2
- package/dist/core/add-command.d.ts.map +1 -1
- package/dist/core/config-repair.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +33 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts +5 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/errors.d.ts +18 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +30 -6
- package/dist/core/managed-index-lock.d.ts +2 -0
- package/dist/core/managed-index-lock.d.ts.map +1 -0
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/plugin-host.d.ts +2 -0
- package/dist/core/plugin-host.d.ts.map +1 -0
- package/dist/core/plugin-manager.d.ts +19 -6
- package/dist/core/plugin-manager.d.ts.map +1 -1
- package/dist/core/plugin-parse.d.ts.map +1 -1
- package/dist/core/plugin-projections.d.ts +6 -2
- package/dist/core/plugin-projections.d.ts.map +1 -1
- package/dist/core/registry.d.ts +4 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/secrets.d.ts.map +1 -1
- package/dist/core/server-auth.d.ts.map +1 -1
- package/dist/core/skill-projections.d.ts +3 -0
- package/dist/core/skill-projections.d.ts.map +1 -0
- package/dist/core/status.d.ts +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts +19 -2
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/update-manager.d.ts.map +1 -1
- package/dist/gateway/server.d.ts +4 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/plugin-host-954a7tf7.js +129 -0
- package/dist/{plugin-manager-1sc4b6am.js → plugin-manager-m8ar2271.js} +12 -4
- package/dist/{registry-mztfy0cz.js → registry-n12thgf2.js} +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +3 -2
- 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-
|
|
6
|
+
} from "./cli-wxw1t3rr.js";
|
|
5
7
|
import {
|
|
6
8
|
loadConfig,
|
|
7
9
|
loadMergedConfig,
|
|
8
|
-
|
|
9
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
360
|
-
import
|
|
479
|
+
import path2 from "node:path";
|
|
480
|
+
import fs2 from "node:fs";
|
|
361
481
|
function existingNvmCurrentBin() {
|
|
362
|
-
const nvmDir = process.env.NVM_DIR ??
|
|
363
|
-
const current =
|
|
364
|
-
return
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
|
787
|
+
throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
|
|
653
788
|
}
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
|
685
|
-
import
|
|
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 (!
|
|
876
|
+
if (!fs3.existsSync(stagedPath)) {
|
|
725
877
|
return null;
|
|
726
878
|
}
|
|
727
879
|
try {
|
|
728
|
-
const raw =
|
|
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 =
|
|
734
|
-
if (!
|
|
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:
|
|
902
|
+
cliPath: path3.isAbsolute(cliPath) ? path3.relative(updatesDir, cliPath) : cliPath,
|
|
751
903
|
stagedAt: new Date().toISOString()
|
|
752
904
|
};
|
|
753
|
-
|
|
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 (
|
|
761
|
-
|
|
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 (!
|
|
922
|
+
if (!fs3.existsSync(updatesDir)) {
|
|
771
923
|
return;
|
|
772
924
|
}
|
|
773
|
-
const entries =
|
|
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 =
|
|
931
|
+
const oldPath = path3.join(updatesDir, oldVersion);
|
|
780
932
|
try {
|
|
781
|
-
|
|
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
|
|
788
|
-
import
|
|
939
|
+
import fs4 from "node:fs";
|
|
940
|
+
import path4 from "node:path";
|
|
789
941
|
import { execSync, spawn } from "node:child_process";
|
|
790
|
-
function
|
|
942
|
+
function acquireLock2() {
|
|
791
943
|
const lockPath = getUpdateLockPath();
|
|
792
944
|
try {
|
|
793
|
-
ensureDir(
|
|
794
|
-
|
|
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
|
|
953
|
+
function releaseLock2() {
|
|
802
954
|
const lockPath = getUpdateLockPath();
|
|
803
955
|
try {
|
|
804
|
-
|
|
956
|
+
fs4.unlinkSync(lockPath);
|
|
805
957
|
} catch {}
|
|
806
958
|
}
|
|
807
959
|
function isUpdateInProgress() {
|
|
808
960
|
const lockPath = getUpdateLockPath();
|
|
809
|
-
if (!
|
|
961
|
+
if (!fs4.existsSync(lockPath)) {
|
|
810
962
|
return false;
|
|
811
963
|
}
|
|
812
964
|
try {
|
|
813
|
-
const raw =
|
|
965
|
+
const raw = fs4.readFileSync(lockPath, "utf8").trim();
|
|
814
966
|
const pid = Number(raw);
|
|
815
967
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
816
|
-
|
|
968
|
+
fs4.unlinkSync(lockPath);
|
|
817
969
|
return false;
|
|
818
970
|
}
|
|
819
971
|
process.kill(pid, 0);
|
|
820
972
|
return true;
|
|
821
973
|
} catch {
|
|
822
|
-
|
|
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 =
|
|
834
|
-
if (
|
|
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 =
|
|
847
|
-
if (!
|
|
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 =
|
|
859
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
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:
|
|
921
|
-
message:
|
|
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
|
|
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
|
|
1139
|
-
results[upstream.name] =
|
|
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
|
|
1182
|
-
results[upstream.name] =
|
|
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
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
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
|
-
|
|
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
|
|
1897
|
-
if (!
|
|
2108
|
+
function readPidRecordFromFile(pidPath = getPidPath()) {
|
|
2109
|
+
if (!fs5.existsSync(pidPath)) {
|
|
1898
2110
|
return null;
|
|
1899
2111
|
}
|
|
1900
|
-
const raw =
|
|
2112
|
+
const raw = fs5.readFileSync(pidPath, "utf8").trim();
|
|
1901
2113
|
if (!raw) {
|
|
1902
2114
|
return null;
|
|
1903
2115
|
}
|
|
1904
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1973
|
-
if (!
|
|
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
|
|
1983
|
-
pid,
|
|
2209
|
+
running,
|
|
2210
|
+
pid: record.pid,
|
|
1984
2211
|
pidFile: pidPath,
|
|
1985
2212
|
logFile: getLogPath(),
|
|
1986
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2327
|
+
if (!fs5.existsSync(logPath)) {
|
|
2059
2328
|
return "";
|
|
2060
2329
|
}
|
|
2061
|
-
const lines =
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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 };
|