@kwonye/mcpx 0.1.93 → 0.1.94
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/cline.d.ts.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/utils/index.d.ts +11 -3
- package/dist/adapters/utils/index.d.ts.map +1 -1
- package/dist/{cli-y7ba5qvr.js → cli-154shqmt.js} +126 -68
- package/dist/{cli-qvkpwy9w.js → cli-9fmn9y72.js} +36 -17
- package/dist/{cli-np0wwh6m.js → cli-csfsg1yf.js} +78 -4
- package/dist/{cli-m2645syd.js → cli-mxmgxdm2.js} +127 -69
- package/dist/{cli-kwyp9ywh.js → cli-n8vt375e.js} +306 -172
- package/dist/{cli-8vxva0cb.js → cli-tk8bggg5.js} +81 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -21
- package/dist/{config-2s5nsm2g.js → config-dtc3c79c.js} +6 -2
- package/dist/core/config-repair.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +3 -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 +1 -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 +5 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +81 -6
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/plugin-manager.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/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/gateway/server.d.ts +4 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/{plugin-manager-1sc4b6am.js → plugin-manager-1sjwfnah.js} +4 -4
- package/dist/{registry-mztfy0cz.js → registry-n12thgf2.js} +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -317,6 +317,14 @@ function repairConfig(config) {
|
|
|
317
317
|
servers[name] = repaired;
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
+
const existingNames = new Set(Object.keys(servers));
|
|
321
|
+
if (config.projects) {
|
|
322
|
+
for (const project of Object.values(config.projects)) {
|
|
323
|
+
if (project.disabledServers) {
|
|
324
|
+
project.disabledServers = project.disabledServers.filter((s) => existingNames.has(s));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
320
328
|
return {
|
|
321
329
|
...config,
|
|
322
330
|
servers
|
|
@@ -324,6 +332,17 @@ function repairConfig(config) {
|
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
// src/core/config.ts
|
|
335
|
+
class ConfigLoadError extends Error {
|
|
336
|
+
configPath;
|
|
337
|
+
backupPath;
|
|
338
|
+
issues;
|
|
339
|
+
constructor(configPath, message, issues) {
|
|
340
|
+
super(message);
|
|
341
|
+
this.name = "ConfigLoadError";
|
|
342
|
+
this.configPath = configPath;
|
|
343
|
+
this.issues = issues;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
327
346
|
var clientStateSchema = z.object({
|
|
328
347
|
status: z.enum(["SYNCED", "UNSUPPORTED_HTTP", "ERROR", "SKIPPED"]),
|
|
329
348
|
lastSyncAt: z.string().optional(),
|
|
@@ -436,10 +455,30 @@ function defaultConfig() {
|
|
|
436
455
|
};
|
|
437
456
|
}
|
|
438
457
|
function loadConfig(configPath = getConfigPath()) {
|
|
439
|
-
|
|
458
|
+
if (!fs.existsSync(configPath)) {
|
|
459
|
+
return defaultConfig();
|
|
460
|
+
}
|
|
461
|
+
let raw;
|
|
462
|
+
try {
|
|
463
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
464
|
+
raw = JSON.parse(content);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
const backupPath = `${configPath}.invalid-${Date.now()}`;
|
|
467
|
+
try {
|
|
468
|
+
fs.copyFileSync(configPath, backupPath);
|
|
469
|
+
} catch {}
|
|
470
|
+
const message = `Failed to parse config file at ${configPath}.`;
|
|
471
|
+
throw new ConfigLoadError(configPath, `${message} Backed up to ${backupPath}.`, [`JSON parse error: ${err.message}`]);
|
|
472
|
+
}
|
|
440
473
|
const parsed = configSchema.safeParse(raw);
|
|
441
474
|
if (!parsed.success) {
|
|
442
|
-
|
|
475
|
+
const backupPath = `${configPath}.invalid-${Date.now()}`;
|
|
476
|
+
try {
|
|
477
|
+
fs.copyFileSync(configPath, backupPath);
|
|
478
|
+
} catch {}
|
|
479
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
480
|
+
const message = `Config file at ${configPath} has validation errors.`;
|
|
481
|
+
throw new ConfigLoadError(configPath, `${message} Backed up to ${backupPath}.`, issues);
|
|
443
482
|
}
|
|
444
483
|
const clientEntries = {};
|
|
445
484
|
for (const [key, value] of Object.entries(parsed.data.clients)) {
|
|
@@ -461,7 +500,18 @@ function saveConfig(config, configPath = getConfigPath()) {
|
|
|
461
500
|
config.servers = Object.fromEntries(Object.entries(config.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
|
|
462
501
|
writeJsonAtomic(configPath, config);
|
|
463
502
|
}
|
|
464
|
-
function loadProjectConfig(projectConfigPath) {
|
|
503
|
+
function loadProjectConfig(projectConfigPath, options) {
|
|
504
|
+
if (options?.strict) {
|
|
505
|
+
const raw2 = readJsonFile(projectConfigPath, null);
|
|
506
|
+
if (raw2 === null) {
|
|
507
|
+
return { servers: {} };
|
|
508
|
+
}
|
|
509
|
+
const parsed2 = projectConfigSchema.safeParse(raw2);
|
|
510
|
+
if (!parsed2.success) {
|
|
511
|
+
throw new ConfigLoadError(projectConfigPath, `Project config at ${projectConfigPath} has validation errors.`, parsed2.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`));
|
|
512
|
+
}
|
|
513
|
+
return parsed2.data;
|
|
514
|
+
}
|
|
465
515
|
const raw = readJsonFile(projectConfigPath, { servers: {} });
|
|
466
516
|
const parsed = projectConfigSchema.safeParse(raw);
|
|
467
517
|
if (!parsed.success) {
|
|
@@ -498,6 +548,30 @@ function loadMergedConfig(configPath = getConfigPath()) {
|
|
|
498
548
|
}
|
|
499
549
|
return config;
|
|
500
550
|
}
|
|
551
|
+
function migrateProjectServers(config, projectPath) {
|
|
552
|
+
const localConfigPath = path.join(projectPath, ".mcpx.json");
|
|
553
|
+
if (!fs.existsSync(localConfigPath)) {
|
|
554
|
+
return { migrated: [], conflicts: [] };
|
|
555
|
+
}
|
|
556
|
+
const migrated = [];
|
|
557
|
+
const conflicts = [];
|
|
558
|
+
try {
|
|
559
|
+
const localConfig = loadProjectConfig(localConfigPath);
|
|
560
|
+
for (const [serverName, spec] of Object.entries(localConfig.servers)) {
|
|
561
|
+
if (config.servers[serverName]) {
|
|
562
|
+
conflicts.push(serverName);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const normalized = normalizeServerSpecEnabled(spec);
|
|
566
|
+
if (normalized.transport === "stdio" && !normalized.cwd) {
|
|
567
|
+
normalized.cwd = projectPath;
|
|
568
|
+
}
|
|
569
|
+
config.servers[serverName] = normalized;
|
|
570
|
+
migrated.push(serverName);
|
|
571
|
+
}
|
|
572
|
+
} catch {}
|
|
573
|
+
return { migrated, conflicts };
|
|
574
|
+
}
|
|
501
575
|
function resolveActiveConfig(options = {}) {
|
|
502
576
|
const globalPath = getConfigPath();
|
|
503
577
|
const config = loadConfig(globalPath);
|
|
@@ -522,4 +596,4 @@ function resolveActiveConfig(options = {}) {
|
|
|
522
596
|
};
|
|
523
597
|
}
|
|
524
598
|
|
|
525
|
-
export { tokenizeCommandLine, parseCliAddCommand, defaultConfig, loadConfig, saveConfig, loadProjectConfig, saveProjectConfig, loadMergedConfig, resolveActiveConfig };
|
|
599
|
+
export { tokenizeCommandLine, parseCliAddCommand, ConfigLoadError, defaultConfig, loadConfig, saveConfig, loadProjectConfig, saveProjectConfig, loadMergedConfig, migrateProjectServers, resolveActiveConfig };
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getGatewayUrl,
|
|
3
|
+
persistSyncState,
|
|
3
4
|
syncAllClients
|
|
4
|
-
} from "./cli-
|
|
5
|
+
} from "./cli-n8vt375e.js";
|
|
5
6
|
import {
|
|
6
7
|
loadConfig,
|
|
7
8
|
loadMergedConfig,
|
|
8
9
|
saveConfig
|
|
9
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-csfsg1yf.js";
|
|
10
11
|
import {
|
|
11
12
|
SecretsManager,
|
|
13
|
+
UpstreamError,
|
|
14
|
+
classifyUpstreamError,
|
|
12
15
|
ensureGatewayToken
|
|
13
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-tk8bggg5.js";
|
|
14
17
|
import {
|
|
15
18
|
ensureDir,
|
|
16
19
|
ensureParentDir,
|
|
@@ -344,6 +347,7 @@ function listAuthBindings(spec) {
|
|
|
344
347
|
value
|
|
345
348
|
}));
|
|
346
349
|
}
|
|
350
|
+
var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
347
351
|
function maybePrefixBearer(target, value, raw = false) {
|
|
348
352
|
if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
|
|
349
353
|
return value;
|
|
@@ -351,7 +355,10 @@ function maybePrefixBearer(target, value, raw = false) {
|
|
|
351
355
|
if (/^\S+\s+\S+/.test(value)) {
|
|
352
356
|
return value;
|
|
353
357
|
}
|
|
354
|
-
|
|
358
|
+
if (TOKEN68.test(value)) {
|
|
359
|
+
return `Bearer ${value}`;
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
355
362
|
}
|
|
356
363
|
|
|
357
364
|
// src/core/spawn-env.ts
|
|
@@ -605,7 +612,12 @@ function bindOAuthReference(config, serverName) {
|
|
|
605
612
|
};
|
|
606
613
|
}
|
|
607
614
|
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver) {
|
|
608
|
-
|
|
615
|
+
const oauthName = oauthSecretName(serverName, "tokens");
|
|
616
|
+
const clientName = oauthSecretName(serverName, "client");
|
|
617
|
+
const backupTokens = secrets.getSecret(oauthName);
|
|
618
|
+
const backupClient = secrets.getSecret(clientName);
|
|
619
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
620
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
|
|
609
621
|
const state = crypto.randomUUID();
|
|
610
622
|
let callbackServer;
|
|
611
623
|
let redirectUrl;
|
|
@@ -638,8 +650,16 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
|
|
|
638
650
|
const config = loadConfig(configPath);
|
|
639
651
|
bindOAuthReference(config, serverName);
|
|
640
652
|
saveConfig(config, configPath);
|
|
641
|
-
syncAllClients(config, secrets);
|
|
653
|
+
const summary = syncAllClients(config, secrets);
|
|
654
|
+
persistSyncState(summary, config);
|
|
655
|
+
saveConfig(config, configPath);
|
|
642
656
|
return { serverName, authorized: true };
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (backupTokens)
|
|
659
|
+
secrets.setSecret(oauthName, backupTokens);
|
|
660
|
+
if (backupClient)
|
|
661
|
+
secrets.setSecret(clientName, backupClient);
|
|
662
|
+
throw error;
|
|
643
663
|
} finally {
|
|
644
664
|
if (callbackServer) {
|
|
645
665
|
await closeServer(callbackServer);
|
|
@@ -649,36 +669,56 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
|
|
|
649
669
|
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
650
670
|
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
651
671
|
if (!stored) {
|
|
652
|
-
throw new
|
|
653
|
-
}
|
|
654
|
-
if (!options.forceRefresh && !tokensAreExpiring(stored)) {
|
|
655
|
-
return stored.tokens.access_token;
|
|
672
|
+
throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
|
|
656
673
|
}
|
|
657
|
-
if (
|
|
658
|
-
if (!
|
|
659
|
-
|
|
674
|
+
if (options.forceRefresh || tokensAreExpiring(stored)) {
|
|
675
|
+
if (!stored.tokens.refresh_token) {
|
|
676
|
+
if (!options.forceRefresh) {
|
|
677
|
+
return stored.tokens.access_token;
|
|
678
|
+
}
|
|
679
|
+
throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
|
|
680
|
+
}
|
|
681
|
+
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
682
|
+
const clientInformation = provider.clientInformation();
|
|
683
|
+
if (!clientInformation) {
|
|
684
|
+
throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
|
|
685
|
+
}
|
|
686
|
+
let serverInfo;
|
|
687
|
+
try {
|
|
688
|
+
const cachedDiscovery = provider.discoveryState();
|
|
689
|
+
serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
692
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
693
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
|
|
694
|
+
}
|
|
695
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
699
|
+
metadata: serverInfo.authorizationServerMetadata,
|
|
700
|
+
clientInformation,
|
|
701
|
+
refreshToken: stored.tokens.refresh_token,
|
|
702
|
+
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
703
|
+
});
|
|
704
|
+
provider.saveTokens(refreshed);
|
|
705
|
+
return refreshed.access_token;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
if (/invalid_grant|invalid_client|4\d{2}/i.test(message)) {
|
|
709
|
+
throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
|
|
710
|
+
}
|
|
711
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
712
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
|
|
713
|
+
}
|
|
714
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
|
|
660
715
|
}
|
|
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
716
|
}
|
|
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;
|
|
717
|
+
return stored.tokens.access_token;
|
|
678
718
|
}
|
|
679
719
|
|
|
680
720
|
// src/version.ts
|
|
681
|
-
var APP_VERSION = "0.1.
|
|
721
|
+
var APP_VERSION = "0.1.94";
|
|
682
722
|
|
|
683
723
|
// src/core/update.ts
|
|
684
724
|
import fs2 from "node:fs";
|
|
@@ -944,7 +984,7 @@ function performRollback() {
|
|
|
944
984
|
// src/core/daemon.ts
|
|
945
985
|
import fs4 from "node:fs";
|
|
946
986
|
import net from "node:net";
|
|
947
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
987
|
+
import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
|
|
948
988
|
|
|
949
989
|
// src/gateway/server.ts
|
|
950
990
|
import http2 from "node:http";
|
|
@@ -964,19 +1004,6 @@ var OAUTH_WELL_KNOWN_PREFIXES = [
|
|
|
964
1004
|
"/.well-known/oauth-authorization-server",
|
|
965
1005
|
"/.well-known/openid-configuration"
|
|
966
1006
|
];
|
|
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
1007
|
function makeError(id, code, message, data) {
|
|
981
1008
|
return {
|
|
982
1009
|
jsonrpc: "2.0",
|
|
@@ -1135,8 +1162,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1135
1162
|
const fingerprint = specFingerprint(upstream.spec);
|
|
1136
1163
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
1137
1164
|
if (cached?.fingerprint === fingerprint) {
|
|
1138
|
-
const
|
|
1139
|
-
results[upstream.name] =
|
|
1165
|
+
const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
|
|
1166
|
+
results[upstream.name] = runtimeErr2 ? { ...cached.count, runtimeError: runtimeErr2.message, runtimeErrorCode: runtimeErr2.code } : cached.count;
|
|
1140
1167
|
continue;
|
|
1141
1168
|
}
|
|
1142
1169
|
let toolsCount = 0;
|
|
@@ -1178,8 +1205,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1178
1205
|
if (errors.length === 0) {
|
|
1179
1206
|
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
1180
1207
|
}
|
|
1181
|
-
const
|
|
1182
|
-
results[upstream.name] =
|
|
1208
|
+
const runtimeErr = runtime.upstreamErrors?.get(upstream.name);
|
|
1209
|
+
results[upstream.name] = runtimeErr ? { ...countObj, runtimeError: runtimeErr.message, runtimeErrorCode: runtimeErr.code } : countObj;
|
|
1183
1210
|
}
|
|
1184
1211
|
return results;
|
|
1185
1212
|
}
|
|
@@ -1348,11 +1375,8 @@ async function callUpstreamOnce(upstream, method, params, secrets, runtime, pass
|
|
|
1348
1375
|
if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
1349
1376
|
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
|
|
1350
1377
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
|
|
1354
|
-
}
|
|
1355
|
-
throw error;
|
|
1378
|
+
const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
|
|
1379
|
+
throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
|
|
1356
1380
|
}
|
|
1357
1381
|
}
|
|
1358
1382
|
function authHeaderIsValid(request, expectedToken) {
|
|
@@ -1371,7 +1395,7 @@ function authHeaderIsValid(request, expectedToken) {
|
|
|
1371
1395
|
return scheme === "Bearer" && token === expectedToken;
|
|
1372
1396
|
}
|
|
1373
1397
|
function isAuthChallenge(error) {
|
|
1374
|
-
return error instanceof
|
|
1398
|
+
return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
|
|
1375
1399
|
}
|
|
1376
1400
|
function getClientAuthorizationForUpstream(request, expectedToken) {
|
|
1377
1401
|
const authHeader = request.headers.authorization;
|
|
@@ -1412,7 +1436,8 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
1412
1436
|
if (upstreams.length === 1) {
|
|
1413
1437
|
throw error;
|
|
1414
1438
|
}
|
|
1415
|
-
|
|
1439
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1440
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1416
1441
|
}
|
|
1417
1442
|
}
|
|
1418
1443
|
return { tools };
|
|
@@ -1447,7 +1472,8 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
1447
1472
|
if (upstreams.length === 1) {
|
|
1448
1473
|
throw error;
|
|
1449
1474
|
}
|
|
1450
|
-
|
|
1475
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1476
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1451
1477
|
}
|
|
1452
1478
|
}
|
|
1453
1479
|
return { resources };
|
|
@@ -1480,7 +1506,8 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
1480
1506
|
if (upstreams.length === 1) {
|
|
1481
1507
|
throw error;
|
|
1482
1508
|
}
|
|
1483
|
-
|
|
1509
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1510
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1484
1511
|
}
|
|
1485
1512
|
}
|
|
1486
1513
|
return { prompts };
|
|
@@ -1521,8 +1548,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
1521
1548
|
if (isAuthChallenge(error)) {
|
|
1522
1549
|
throw error;
|
|
1523
1550
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1551
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1552
|
+
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1553
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
1526
1554
|
}
|
|
1527
1555
|
}
|
|
1528
1556
|
if (method === "prompts/get") {
|
|
@@ -1554,8 +1582,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
1554
1582
|
if (isAuthChallenge(error)) {
|
|
1555
1583
|
throw error;
|
|
1556
1584
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1585
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1586
|
+
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1587
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
1559
1588
|
}
|
|
1560
1589
|
}
|
|
1561
1590
|
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
@@ -1586,8 +1615,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
1586
1615
|
if (isAuthChallenge(error)) {
|
|
1587
1616
|
throw error;
|
|
1588
1617
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1618
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1619
|
+
runtime.upstreamErrors?.set(upstream.name, { code: errCode, message: error.message });
|
|
1620
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream.name });
|
|
1591
1621
|
}
|
|
1592
1622
|
}
|
|
1593
1623
|
async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
@@ -1858,16 +1888,16 @@ function createGatewayServer(options) {
|
|
|
1858
1888
|
}
|
|
1859
1889
|
} catch (error) {
|
|
1860
1890
|
if (isAuthChallenge(error)) {
|
|
1861
|
-
response.statusCode = error.status;
|
|
1891
|
+
response.statusCode = error.status ?? 401;
|
|
1862
1892
|
response.setHeader("content-type", "application/json");
|
|
1863
1893
|
if (error.wwwAuthenticate) {
|
|
1864
1894
|
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
1865
1895
|
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
1866
1896
|
}
|
|
1867
1897
|
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
1898
|
+
mcpxCode: error.code,
|
|
1868
1899
|
status: error.status,
|
|
1869
|
-
|
|
1870
|
-
wwwAuthenticate: error.wwwAuthenticate
|
|
1900
|
+
upstream: error.upstream
|
|
1871
1901
|
})));
|
|
1872
1902
|
if (debug) {
|
|
1873
1903
|
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
@@ -1927,7 +1957,7 @@ async function isPortAvailable(port) {
|
|
|
1927
1957
|
tester.listen(port, "127.0.0.1");
|
|
1928
1958
|
});
|
|
1929
1959
|
}
|
|
1930
|
-
async function resolveGatewayPort(config) {
|
|
1960
|
+
async function resolveGatewayPort(config, secrets) {
|
|
1931
1961
|
if (await isPortAvailable(config.gateway.port)) {
|
|
1932
1962
|
return config.gateway.port;
|
|
1933
1963
|
}
|
|
@@ -1937,8 +1967,16 @@ async function resolveGatewayPort(config) {
|
|
|
1937
1967
|
break;
|
|
1938
1968
|
}
|
|
1939
1969
|
if (await isPortAvailable(candidate)) {
|
|
1970
|
+
const oldPort = config.gateway.port;
|
|
1940
1971
|
config.gateway.port = candidate;
|
|
1941
1972
|
saveConfig(config);
|
|
1973
|
+
if (secrets) {
|
|
1974
|
+
try {
|
|
1975
|
+
const summary = syncAllClients(config, secrets);
|
|
1976
|
+
persistSyncState(summary, config);
|
|
1977
|
+
saveConfig(config);
|
|
1978
|
+
} catch {}
|
|
1979
|
+
}
|
|
1942
1980
|
return candidate;
|
|
1943
1981
|
}
|
|
1944
1982
|
}
|
|
@@ -2002,7 +2040,7 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
2002
2040
|
fs4.unlinkSync(getPidPath());
|
|
2003
2041
|
} catch {}
|
|
2004
2042
|
}
|
|
2005
|
-
const port = await resolveGatewayPort(config);
|
|
2043
|
+
const port = await resolveGatewayPort(config, secrets);
|
|
2006
2044
|
const token = ensureGatewayToken(config, secrets);
|
|
2007
2045
|
const pidPath = getPidPath();
|
|
2008
2046
|
const logPath = getLogPath();
|
|
@@ -2038,6 +2076,24 @@ function stopDaemon() {
|
|
|
2038
2076
|
message: "mcpx daemon is not running."
|
|
2039
2077
|
};
|
|
2040
2078
|
}
|
|
2079
|
+
try {
|
|
2080
|
+
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2081
|
+
if (!cmd.includes("mcpx") && !cmd.includes("daemon") && !cmd.includes("cli.js")) {
|
|
2082
|
+
fs4.unlinkSync(pidPath);
|
|
2083
|
+
return {
|
|
2084
|
+
stopped: false,
|
|
2085
|
+
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
} catch {
|
|
2089
|
+
try {
|
|
2090
|
+
fs4.unlinkSync(pidPath);
|
|
2091
|
+
} catch {}
|
|
2092
|
+
return {
|
|
2093
|
+
stopped: false,
|
|
2094
|
+
message: `PID ${pid} not found. Pidfile cleaned up.`
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2041
2097
|
try {
|
|
2042
2098
|
process.kill(pid, "SIGTERM");
|
|
2043
2099
|
} catch {}
|
|
@@ -2165,6 +2221,7 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
2165
2221
|
if (tokenCount?.error && buildAuthBindings(spec).length > 0 && tokenCount.total > 0) {
|
|
2166
2222
|
tokenCount = { ...tokenCount, error: undefined };
|
|
2167
2223
|
}
|
|
2224
|
+
const health = tokenCount ? tokenCount.error || tokenCount.runtimeError ? "error" : "ok" : "unknown";
|
|
2168
2225
|
const serverEntry = {
|
|
2169
2226
|
name,
|
|
2170
2227
|
enabled: isServerEnabled(spec),
|
|
@@ -2172,7 +2229,8 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
2172
2229
|
target: describeServerTarget(spec),
|
|
2173
2230
|
authBindings: buildAuthBindings(spec),
|
|
2174
2231
|
clients: buildClientMappings(config, managedIndex, name),
|
|
2175
|
-
tokenCount
|
|
2232
|
+
tokenCount,
|
|
2233
|
+
health
|
|
2176
2234
|
};
|
|
2177
2235
|
if (serverEntry.enabled && tokenCount) {
|
|
2178
2236
|
totalGlobalTokens += tokenCount.total;
|