@kwonye/mcpx 0.1.77 → 0.1.80
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.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/{cli-fsf33sgp.js → cli-08mgt3c8.js} +4 -1
- package/dist/cli-0ztrmp65.js +514 -0
- package/dist/{cli-htm5cf82.js → cli-f2a6pkds.js} +492 -126
- package/dist/{cli-s4py8ckf.js → cli-ma89vavk.js} +42 -11
- package/dist/{cli-f16kphtq.js → cli-wcxyqz7g.js} +493 -127
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +34 -10
- package/dist/{config-kayej21q.js → config-p8tjffpa.js} +2 -2
- package/dist/core/add-command.d.ts +7 -0
- package/dist/core/add-command.d.ts.map +1 -0
- package/dist/core/auth-probe.d.ts +1 -0
- package/dist/core/auth-probe.d.ts.map +1 -1
- package/dist/core/config-repair.d.ts +3 -0
- package/dist/core/config-repair.d.ts.map +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +25 -5
- package/dist/core/oauth.d.ts +12 -0
- package/dist/core/oauth.d.ts.map +1 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/registry.d.ts +1 -0
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/spawn-env.d.ts +2 -0
- package/dist/core/spawn-env.d.ts.map +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/gateway/server.d.ts +5 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/{registry-1s14wd58.js → registry-b3cetq7x.js} +4 -2
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/cli-7bsf6akp.js +0 -202
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
loadConfig,
|
|
2
3
|
loadMergedConfig,
|
|
3
4
|
saveConfig
|
|
4
|
-
} from "./cli-
|
|
5
|
+
} from "./cli-0ztrmp65.js";
|
|
5
6
|
import {
|
|
6
7
|
SecretsManager,
|
|
7
|
-
ensureGatewayToken
|
|
8
|
-
|
|
9
|
-
} from "./cli-s4py8ckf.js";
|
|
8
|
+
ensureGatewayToken
|
|
9
|
+
} from "./cli-ma89vavk.js";
|
|
10
10
|
import {
|
|
11
11
|
ensureDir,
|
|
12
12
|
ensureParentDir,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
readJsonFile,
|
|
22
22
|
sha256,
|
|
23
23
|
writeJsonAtomic
|
|
24
|
-
} from "./cli-
|
|
24
|
+
} from "./cli-08mgt3c8.js";
|
|
25
25
|
import {
|
|
26
26
|
isServerEnabled
|
|
27
27
|
} from "./cli-s4gspqdf.js";
|
|
@@ -103,6 +103,13 @@ function resolveHeaders(spec, secrets) {
|
|
|
103
103
|
}
|
|
104
104
|
return headers;
|
|
105
105
|
}
|
|
106
|
+
function isOAuthLikelyChallenge(wwwAuthenticate) {
|
|
107
|
+
if (!wwwAuthenticate) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const lower = wwwAuthenticate.toLowerCase();
|
|
111
|
+
return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
|
|
112
|
+
}
|
|
106
113
|
async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS) {
|
|
107
114
|
const timeoutController = new AbortController;
|
|
108
115
|
const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
@@ -120,10 +127,12 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
|
|
|
120
127
|
signal: timeoutController.signal
|
|
121
128
|
});
|
|
122
129
|
if (response.status === 401 || response.status === 403) {
|
|
130
|
+
const wwwAuthenticate = response.headers.get("www-authenticate");
|
|
123
131
|
return {
|
|
124
132
|
authRequired: true,
|
|
133
|
+
oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
|
|
125
134
|
status: response.status,
|
|
126
|
-
wwwAuthenticate:
|
|
135
|
+
wwwAuthenticate: wwwAuthenticate ?? undefined
|
|
127
136
|
};
|
|
128
137
|
}
|
|
129
138
|
if (response.status === 200) {
|
|
@@ -501,20 +510,22 @@ class ClaudeAdapter {
|
|
|
501
510
|
try {
|
|
502
511
|
const raw = readJsonFile(configPath, {});
|
|
503
512
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
504
|
-
const
|
|
513
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
514
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
505
515
|
type: "http",
|
|
506
516
|
url: entry.url,
|
|
507
517
|
headers: entry.headers
|
|
508
518
|
}]));
|
|
519
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
509
520
|
const topLevelServers = raw.mcpServers ?? {};
|
|
510
521
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
511
|
-
for (const name of
|
|
522
|
+
for (const name of enabledManagedNames) {
|
|
512
523
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
513
524
|
if (topLevelConflict) {
|
|
514
525
|
return errorResult(this.id, configPath, topLevelConflict);
|
|
515
526
|
}
|
|
516
527
|
}
|
|
517
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers,
|
|
528
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
518
529
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
519
530
|
topLevelServers[name] = entry;
|
|
520
531
|
}
|
|
@@ -656,9 +667,9 @@ class ClaudeDesktopAdapter {
|
|
|
656
667
|
}
|
|
657
668
|
raw.mcpServers = topLevelServers;
|
|
658
669
|
writeJsonAtomic(configPath, raw);
|
|
659
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(
|
|
670
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
|
|
660
671
|
const { command, args } = buildProxyEntry(entry);
|
|
661
|
-
return [entry.name, JSON.stringify({ command, args
|
|
672
|
+
return [entry.name, JSON.stringify({ command, args })];
|
|
662
673
|
})));
|
|
663
674
|
return okResult(this.id, configPath);
|
|
664
675
|
} catch (error) {
|
|
@@ -1029,6 +1040,9 @@ class ClineAdapter {
|
|
|
1029
1040
|
if (!configPath) {
|
|
1030
1041
|
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1031
1042
|
}
|
|
1043
|
+
if (!fs3.existsSync(configPath)) {
|
|
1044
|
+
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1045
|
+
}
|
|
1032
1046
|
try {
|
|
1033
1047
|
const raw = readJsonFile(configPath, {});
|
|
1034
1048
|
const servers = {
|
|
@@ -1640,6 +1654,9 @@ function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
|
1640
1654
|
enabled: isServerEnabled(spec)
|
|
1641
1655
|
}));
|
|
1642
1656
|
}
|
|
1657
|
+
function isClientInternalServerCommand(candidate) {
|
|
1658
|
+
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
1659
|
+
}
|
|
1643
1660
|
function syncAllClients(config, secrets, targetClients) {
|
|
1644
1661
|
const adapters = getAdapters();
|
|
1645
1662
|
const managedIndexPath = getManagedIndexPath();
|
|
@@ -1658,6 +1675,16 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1658
1675
|
const scan = adapter.scanForImports(config, managedIndex);
|
|
1659
1676
|
imports.skipped.push(...scan.skipped);
|
|
1660
1677
|
for (const candidate of scan.candidates) {
|
|
1678
|
+
if (isClientInternalServerCommand(candidate)) {
|
|
1679
|
+
imports.skipped.push({
|
|
1680
|
+
clientId: candidate.clientId,
|
|
1681
|
+
configPath: candidate.configPath,
|
|
1682
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1683
|
+
serverName: candidate.serverName,
|
|
1684
|
+
reason: "client-internal server"
|
|
1685
|
+
});
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1661
1688
|
const existing = config.servers[candidate.serverName];
|
|
1662
1689
|
if (!existing) {
|
|
1663
1690
|
config.servers[candidate.serverName] = candidate.spec;
|
|
@@ -1857,12 +1884,298 @@ function maybePrefixBearer(target, value, raw = false) {
|
|
|
1857
1884
|
return `Bearer ${value}`;
|
|
1858
1885
|
}
|
|
1859
1886
|
|
|
1887
|
+
// src/core/spawn-env.ts
|
|
1888
|
+
import os from "node:os";
|
|
1889
|
+
import path11 from "node:path";
|
|
1890
|
+
import fs6 from "node:fs";
|
|
1891
|
+
function existingNvmCurrentBin() {
|
|
1892
|
+
const nvmDir = process.env.NVM_DIR ?? path11.join(os.homedir(), ".nvm");
|
|
1893
|
+
const current = path11.join(nvmDir, "current", "bin");
|
|
1894
|
+
return fs6.existsSync(current) ? current : null;
|
|
1895
|
+
}
|
|
1896
|
+
function buildEnrichedPath(current = process.env.PATH ?? "") {
|
|
1897
|
+
const candidates = [
|
|
1898
|
+
...current.split(":"),
|
|
1899
|
+
"/opt/homebrew/bin",
|
|
1900
|
+
"/usr/local/bin",
|
|
1901
|
+
path11.join(os.homedir(), ".bun", "bin"),
|
|
1902
|
+
path11.join(os.homedir(), ".local", "bin"),
|
|
1903
|
+
existingNvmCurrentBin()
|
|
1904
|
+
].filter((entry) => Boolean(entry));
|
|
1905
|
+
const seen = new Set;
|
|
1906
|
+
const entries = [];
|
|
1907
|
+
for (const candidate of candidates) {
|
|
1908
|
+
if (!candidate || seen.has(candidate)) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
seen.add(candidate);
|
|
1912
|
+
entries.push(candidate);
|
|
1913
|
+
}
|
|
1914
|
+
return entries.join(":");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/core/oauth.ts
|
|
1918
|
+
import http from "node:http";
|
|
1919
|
+
import crypto from "node:crypto";
|
|
1920
|
+
import {
|
|
1921
|
+
auth,
|
|
1922
|
+
discoverOAuthServerInfo,
|
|
1923
|
+
refreshAuthorization
|
|
1924
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
1925
|
+
function oauthSecretName(serverName, suffix) {
|
|
1926
|
+
return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
|
|
1927
|
+
}
|
|
1928
|
+
function readJsonSecret(secrets, name) {
|
|
1929
|
+
const raw = secrets.getSecret(name);
|
|
1930
|
+
if (!raw) {
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
return JSON.parse(raw);
|
|
1935
|
+
} catch {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function writeJsonSecret(secrets, name, value) {
|
|
1940
|
+
secrets.setSecret(name, JSON.stringify(value));
|
|
1941
|
+
}
|
|
1942
|
+
function isOAuthReference(value) {
|
|
1943
|
+
return value.startsWith("oauth://");
|
|
1944
|
+
}
|
|
1945
|
+
function oauthReferenceServerName(value) {
|
|
1946
|
+
if (!isOAuthReference(value)) {
|
|
1947
|
+
throw new Error(`Not an OAuth reference: ${value}`);
|
|
1948
|
+
}
|
|
1949
|
+
return decodeURIComponent(value.slice("oauth://".length));
|
|
1950
|
+
}
|
|
1951
|
+
function tokensAreExpiring(tokens) {
|
|
1952
|
+
if (!tokens.tokens.expires_in) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
|
|
1956
|
+
return Date.now() >= expiresAt - 60000;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
class McpxOAuthProvider {
|
|
1960
|
+
#serverName;
|
|
1961
|
+
#secrets;
|
|
1962
|
+
#redirectUrlValue;
|
|
1963
|
+
#stateValue;
|
|
1964
|
+
constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
|
|
1965
|
+
this.#serverName = serverName;
|
|
1966
|
+
this.#secrets = secrets;
|
|
1967
|
+
this.#redirectUrlValue = redirectUrl;
|
|
1968
|
+
this.#stateValue = state;
|
|
1969
|
+
}
|
|
1970
|
+
get redirectUrl() {
|
|
1971
|
+
return this.#redirectUrlValue;
|
|
1972
|
+
}
|
|
1973
|
+
get clientMetadata() {
|
|
1974
|
+
if (!this.#redirectUrlValue) {
|
|
1975
|
+
return {
|
|
1976
|
+
redirect_uris: [],
|
|
1977
|
+
token_endpoint_auth_method: "none",
|
|
1978
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1979
|
+
response_types: ["code"],
|
|
1980
|
+
client_name: "mcpx"
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
redirect_uris: [this.#redirectUrlValue],
|
|
1985
|
+
token_endpoint_auth_method: "none",
|
|
1986
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1987
|
+
response_types: ["code"],
|
|
1988
|
+
client_name: "mcpx"
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
state() {
|
|
1992
|
+
return this.#stateValue;
|
|
1993
|
+
}
|
|
1994
|
+
clientInformation() {
|
|
1995
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
|
|
1996
|
+
}
|
|
1997
|
+
saveClientInformation(clientInformation) {
|
|
1998
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
|
|
1999
|
+
}
|
|
2000
|
+
tokens() {
|
|
2001
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
|
|
2002
|
+
}
|
|
2003
|
+
saveTokens(tokens) {
|
|
2004
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
2005
|
+
tokens,
|
|
2006
|
+
obtainedAt: Date.now()
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
redirectToAuthorization(_authorizationUrl) {}
|
|
2010
|
+
saveCodeVerifier(codeVerifier) {
|
|
2011
|
+
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
2012
|
+
}
|
|
2013
|
+
codeVerifier() {
|
|
2014
|
+
const verifier = this.#secrets.getSecret(oauthSecretName(this.#serverName, "verifier"));
|
|
2015
|
+
if (!verifier) {
|
|
2016
|
+
throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
|
|
2017
|
+
}
|
|
2018
|
+
return verifier;
|
|
2019
|
+
}
|
|
2020
|
+
saveDiscoveryState(state) {
|
|
2021
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
|
|
2022
|
+
}
|
|
2023
|
+
discoveryState() {
|
|
2024
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
|
|
2025
|
+
}
|
|
2026
|
+
invalidateCredentials(scope) {
|
|
2027
|
+
const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
|
|
2028
|
+
for (const suffix of suffixes) {
|
|
2029
|
+
this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
class InteractiveOAuthProvider extends McpxOAuthProvider {
|
|
2035
|
+
#openUrl;
|
|
2036
|
+
constructor(serverName, secrets, redirectUrl, state, openUrl) {
|
|
2037
|
+
super(serverName, secrets, redirectUrl, state);
|
|
2038
|
+
this.#openUrl = openUrl;
|
|
2039
|
+
}
|
|
2040
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
2041
|
+
await this.#openUrl(authorizationUrl.toString());
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function listen(server) {
|
|
2045
|
+
return new Promise((resolve, reject) => {
|
|
2046
|
+
server.once("error", reject);
|
|
2047
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2048
|
+
const address = server.address();
|
|
2049
|
+
if (!address || typeof address === "string") {
|
|
2050
|
+
reject(new Error("Failed to start OAuth callback server."));
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
resolve(address.port);
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
function closeServer(server) {
|
|
2058
|
+
return new Promise((resolve) => {
|
|
2059
|
+
server.close(() => resolve());
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
function waitForAuthorizationCode(server, expectedState, timeoutMs = 120000) {
|
|
2063
|
+
return new Promise((resolve, reject) => {
|
|
2064
|
+
const timeout = setTimeout(() => {
|
|
2065
|
+
reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
|
|
2066
|
+
}, timeoutMs);
|
|
2067
|
+
server.on("request", (request, response) => {
|
|
2068
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
2069
|
+
const code = url.searchParams.get("code");
|
|
2070
|
+
const state = url.searchParams.get("state");
|
|
2071
|
+
const error = url.searchParams.get("error");
|
|
2072
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
2073
|
+
if (error) {
|
|
2074
|
+
response.statusCode = 400;
|
|
2075
|
+
response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
|
|
2076
|
+
clearTimeout(timeout);
|
|
2077
|
+
reject(new Error(`OAuth authorization failed: ${error}`));
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
if (!code) {
|
|
2081
|
+
response.statusCode = 400;
|
|
2082
|
+
response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (state !== expectedState) {
|
|
2086
|
+
response.statusCode = 400;
|
|
2087
|
+
response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
|
|
2088
|
+
clearTimeout(timeout);
|
|
2089
|
+
reject(new Error("OAuth state mismatch."));
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
|
|
2093
|
+
clearTimeout(timeout);
|
|
2094
|
+
resolve(code);
|
|
2095
|
+
});
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
function bindOAuthReference(config, serverName) {
|
|
2099
|
+
const spec = config.servers[serverName];
|
|
2100
|
+
if (!spec) {
|
|
2101
|
+
throw new Error(`Server "${serverName}" not found.`);
|
|
2102
|
+
}
|
|
2103
|
+
if (spec.transport !== "http") {
|
|
2104
|
+
throw new Error(`OAuth login only supports HTTP servers.`);
|
|
2105
|
+
}
|
|
2106
|
+
spec.headers = {
|
|
2107
|
+
...spec.headers ?? {},
|
|
2108
|
+
Authorization: `oauth://${encodeURIComponent(serverName)}`
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath) {
|
|
2112
|
+
const state = crypto.randomUUID();
|
|
2113
|
+
const callbackServer = http.createServer();
|
|
2114
|
+
const port = await listen(callbackServer);
|
|
2115
|
+
const redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
2116
|
+
const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, openUrl);
|
|
2117
|
+
const codePromise = waitForAuthorizationCode(callbackServer, state);
|
|
2118
|
+
try {
|
|
2119
|
+
const initial = await auth(provider, {
|
|
2120
|
+
serverUrl: spec.url
|
|
2121
|
+
});
|
|
2122
|
+
if (initial !== "REDIRECT") {
|
|
2123
|
+
throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
|
|
2124
|
+
}
|
|
2125
|
+
const authorizationCode = await codePromise;
|
|
2126
|
+
const result = await auth(provider, {
|
|
2127
|
+
serverUrl: spec.url,
|
|
2128
|
+
authorizationCode
|
|
2129
|
+
});
|
|
2130
|
+
if (result !== "AUTHORIZED") {
|
|
2131
|
+
throw new Error(`OAuth login did not authorize "${serverName}".`);
|
|
2132
|
+
}
|
|
2133
|
+
const config = loadConfig(configPath);
|
|
2134
|
+
bindOAuthReference(config, serverName);
|
|
2135
|
+
saveConfig(config, configPath);
|
|
2136
|
+
syncAllClients(config, secrets);
|
|
2137
|
+
return { serverName, authorized: true };
|
|
2138
|
+
} finally {
|
|
2139
|
+
await closeServer(callbackServer);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
2143
|
+
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
2144
|
+
if (!stored) {
|
|
2145
|
+
throw new Error(`No OAuth tokens stored for "${serverName}". Run mcpx auth login ${serverName}.`);
|
|
2146
|
+
}
|
|
2147
|
+
if (!options.forceRefresh && !tokensAreExpiring(stored)) {
|
|
2148
|
+
return stored.tokens.access_token;
|
|
2149
|
+
}
|
|
2150
|
+
if (!stored.tokens.refresh_token) {
|
|
2151
|
+
if (!options.forceRefresh) {
|
|
2152
|
+
return stored.tokens.access_token;
|
|
2153
|
+
}
|
|
2154
|
+
throw new Error(`No OAuth refresh token stored for "${serverName}". Re-authentication required.`);
|
|
2155
|
+
}
|
|
2156
|
+
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
2157
|
+
const clientInformation = provider.clientInformation();
|
|
2158
|
+
if (!clientInformation) {
|
|
2159
|
+
throw new Error(`No OAuth client information stored for "${serverName}". Re-authentication required.`);
|
|
2160
|
+
}
|
|
2161
|
+
const cachedDiscovery = provider.discoveryState();
|
|
2162
|
+
const serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
2163
|
+
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
2164
|
+
metadata: serverInfo.authorizationServerMetadata,
|
|
2165
|
+
clientInformation,
|
|
2166
|
+
refreshToken: stored.tokens.refresh_token,
|
|
2167
|
+
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
2168
|
+
});
|
|
2169
|
+
provider.saveTokens(refreshed);
|
|
2170
|
+
return refreshed.access_token;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1860
2173
|
// src/version.ts
|
|
1861
2174
|
var APP_VERSION = "0.1.3";
|
|
1862
2175
|
|
|
1863
2176
|
// src/core/update.ts
|
|
1864
|
-
import
|
|
1865
|
-
import
|
|
2177
|
+
import fs7 from "node:fs";
|
|
2178
|
+
import path12 from "node:path";
|
|
1866
2179
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1867
2180
|
function compareVersions(a, b) {
|
|
1868
2181
|
const partsA = a.split(".").map(Number);
|
|
@@ -1901,17 +2214,17 @@ async function checkForUpdates() {
|
|
|
1901
2214
|
}
|
|
1902
2215
|
function getStagedUpdate() {
|
|
1903
2216
|
const stagedPath = getStagedVersionPath();
|
|
1904
|
-
if (!
|
|
2217
|
+
if (!fs7.existsSync(stagedPath)) {
|
|
1905
2218
|
return null;
|
|
1906
2219
|
}
|
|
1907
2220
|
try {
|
|
1908
|
-
const raw =
|
|
2221
|
+
const raw = fs7.readFileSync(stagedPath, "utf8");
|
|
1909
2222
|
const info = JSON.parse(raw);
|
|
1910
2223
|
if (!info.version || !info.cliPath) {
|
|
1911
2224
|
return null;
|
|
1912
2225
|
}
|
|
1913
|
-
const absoluteCliPath =
|
|
1914
|
-
if (!
|
|
2226
|
+
const absoluteCliPath = path12.isAbsolute(info.cliPath) ? info.cliPath : path12.join(getUpdatesDir(), info.cliPath);
|
|
2227
|
+
if (!fs7.existsSync(absoluteCliPath)) {
|
|
1915
2228
|
return null;
|
|
1916
2229
|
}
|
|
1917
2230
|
return {
|
|
@@ -1927,15 +2240,18 @@ function stageUpdate(version, cliPath) {
|
|
|
1927
2240
|
ensureDir(updatesDir);
|
|
1928
2241
|
const stagedInfo = {
|
|
1929
2242
|
version,
|
|
1930
|
-
cliPath:
|
|
2243
|
+
cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
|
|
1931
2244
|
stagedAt: new Date().toISOString()
|
|
1932
2245
|
};
|
|
1933
|
-
|
|
2246
|
+
fs7.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
2247
|
+
}
|
|
2248
|
+
function shouldUseStagedCli(stagedVersion, currentVersion) {
|
|
2249
|
+
return compareVersions(stagedVersion, currentVersion) > 0;
|
|
1934
2250
|
}
|
|
1935
2251
|
function clearStagedUpdate() {
|
|
1936
2252
|
const stagedPath = getStagedVersionPath();
|
|
1937
|
-
if (
|
|
1938
|
-
|
|
2253
|
+
if (fs7.existsSync(stagedPath)) {
|
|
2254
|
+
fs7.unlinkSync(stagedPath);
|
|
1939
2255
|
}
|
|
1940
2256
|
}
|
|
1941
2257
|
function getStagedCliPath() {
|
|
@@ -1944,31 +2260,31 @@ function getStagedCliPath() {
|
|
|
1944
2260
|
}
|
|
1945
2261
|
function removeOldVersions(keepCount = 2) {
|
|
1946
2262
|
const updatesDir = getUpdatesDir();
|
|
1947
|
-
if (!
|
|
2263
|
+
if (!fs7.existsSync(updatesDir)) {
|
|
1948
2264
|
return;
|
|
1949
2265
|
}
|
|
1950
|
-
const entries =
|
|
2266
|
+
const entries = fs7.readdirSync(updatesDir, { withFileTypes: true });
|
|
1951
2267
|
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)));
|
|
1952
2268
|
if (versionDirs.length <= keepCount) {
|
|
1953
2269
|
return;
|
|
1954
2270
|
}
|
|
1955
2271
|
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
1956
|
-
const oldPath =
|
|
2272
|
+
const oldPath = path12.join(updatesDir, oldVersion);
|
|
1957
2273
|
try {
|
|
1958
|
-
|
|
2274
|
+
fs7.rmSync(oldPath, { recursive: true, force: true });
|
|
1959
2275
|
} catch {}
|
|
1960
2276
|
}
|
|
1961
2277
|
}
|
|
1962
2278
|
|
|
1963
2279
|
// src/core/update-manager.ts
|
|
1964
|
-
import
|
|
1965
|
-
import
|
|
2280
|
+
import fs8 from "node:fs";
|
|
2281
|
+
import path13 from "node:path";
|
|
1966
2282
|
import { execSync, spawn } from "node:child_process";
|
|
1967
2283
|
function acquireLock() {
|
|
1968
2284
|
const lockPath = getUpdateLockPath();
|
|
1969
2285
|
try {
|
|
1970
|
-
ensureDir(
|
|
1971
|
-
|
|
2286
|
+
ensureDir(path13.dirname(lockPath));
|
|
2287
|
+
fs8.writeFileSync(lockPath, `${process.pid}
|
|
1972
2288
|
`, { flag: "wx" });
|
|
1973
2289
|
return true;
|
|
1974
2290
|
} catch {
|
|
@@ -1978,25 +2294,25 @@ function acquireLock() {
|
|
|
1978
2294
|
function releaseLock() {
|
|
1979
2295
|
const lockPath = getUpdateLockPath();
|
|
1980
2296
|
try {
|
|
1981
|
-
|
|
2297
|
+
fs8.unlinkSync(lockPath);
|
|
1982
2298
|
} catch {}
|
|
1983
2299
|
}
|
|
1984
2300
|
function isUpdateInProgress() {
|
|
1985
2301
|
const lockPath = getUpdateLockPath();
|
|
1986
|
-
if (!
|
|
2302
|
+
if (!fs8.existsSync(lockPath)) {
|
|
1987
2303
|
return false;
|
|
1988
2304
|
}
|
|
1989
2305
|
try {
|
|
1990
|
-
const raw =
|
|
2306
|
+
const raw = fs8.readFileSync(lockPath, "utf8").trim();
|
|
1991
2307
|
const pid = Number(raw);
|
|
1992
2308
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
1993
|
-
|
|
2309
|
+
fs8.unlinkSync(lockPath);
|
|
1994
2310
|
return false;
|
|
1995
2311
|
}
|
|
1996
2312
|
process.kill(pid, 0);
|
|
1997
2313
|
return true;
|
|
1998
2314
|
} catch {
|
|
1999
|
-
|
|
2315
|
+
fs8.unlinkSync(lockPath);
|
|
2000
2316
|
return false;
|
|
2001
2317
|
}
|
|
2002
2318
|
}
|
|
@@ -2007,8 +2323,8 @@ async function downloadAndStageUpdate() {
|
|
|
2007
2323
|
}
|
|
2008
2324
|
const targetVersion = updateStatus.latestVersion;
|
|
2009
2325
|
const updatesDir = getUpdatesDir();
|
|
2010
|
-
const versionDir =
|
|
2011
|
-
if (
|
|
2326
|
+
const versionDir = path13.join(updatesDir, `v${targetVersion}`);
|
|
2327
|
+
if (fs8.existsSync(versionDir)) {
|
|
2012
2328
|
const existingStaged = getStagedUpdate();
|
|
2013
2329
|
if (existingStaged?.version === targetVersion) {
|
|
2014
2330
|
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
@@ -2020,8 +2336,8 @@ async function downloadAndStageUpdate() {
|
|
|
2020
2336
|
cwd: versionDir,
|
|
2021
2337
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2022
2338
|
});
|
|
2023
|
-
const tarballPath =
|
|
2024
|
-
if (!
|
|
2339
|
+
const tarballPath = path13.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
2340
|
+
if (!fs8.existsSync(tarballPath)) {
|
|
2025
2341
|
throw new Error("Downloaded tarball not found");
|
|
2026
2342
|
}
|
|
2027
2343
|
execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
|
|
@@ -2032,8 +2348,8 @@ async function downloadAndStageUpdate() {
|
|
|
2032
2348
|
cwd: versionDir,
|
|
2033
2349
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2034
2350
|
});
|
|
2035
|
-
const cliPath =
|
|
2036
|
-
if (!
|
|
2351
|
+
const cliPath = path13.join(versionDir, "dist", "cli.js");
|
|
2352
|
+
if (!fs8.existsSync(cliPath)) {
|
|
2037
2353
|
throw new Error("CLI entry point not found in downloaded package");
|
|
2038
2354
|
}
|
|
2039
2355
|
stageUpdate(targetVersion, cliPath);
|
|
@@ -2041,7 +2357,7 @@ async function downloadAndStageUpdate() {
|
|
|
2041
2357
|
return { success: true, version: targetVersion };
|
|
2042
2358
|
} catch (error) {
|
|
2043
2359
|
try {
|
|
2044
|
-
|
|
2360
|
+
fs8.rmSync(versionDir, { recursive: true, force: true });
|
|
2045
2361
|
} catch {}
|
|
2046
2362
|
return {
|
|
2047
2363
|
success: false,
|
|
@@ -2119,14 +2435,14 @@ function performRollback() {
|
|
|
2119
2435
|
}
|
|
2120
2436
|
|
|
2121
2437
|
// src/core/daemon.ts
|
|
2122
|
-
import
|
|
2438
|
+
import fs9 from "node:fs";
|
|
2123
2439
|
import net from "node:net";
|
|
2124
2440
|
import { spawn as spawn2 } from "node:child_process";
|
|
2125
2441
|
|
|
2126
2442
|
// src/gateway/server.ts
|
|
2127
|
-
import
|
|
2128
|
-
import { URL } from "node:url";
|
|
2129
|
-
import
|
|
2443
|
+
import http2 from "node:http";
|
|
2444
|
+
import { URL as URL2 } from "node:url";
|
|
2445
|
+
import crypto2 from "node:crypto";
|
|
2130
2446
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2131
2447
|
import {
|
|
2132
2448
|
StdioClientTransport,
|
|
@@ -2215,16 +2531,16 @@ function getWellKnownPrefix(pathname) {
|
|
|
2215
2531
|
return null;
|
|
2216
2532
|
}
|
|
2217
2533
|
function getUpstreamPathSuffixForWellKnown(upstream) {
|
|
2218
|
-
const upstreamUrl = new
|
|
2534
|
+
const upstreamUrl = new URL2(upstream.spec.url);
|
|
2219
2535
|
if (upstreamUrl.pathname === "/") {
|
|
2220
2536
|
return "";
|
|
2221
2537
|
}
|
|
2222
2538
|
return upstreamUrl.pathname.endsWith("/") ? upstreamUrl.pathname.slice(0, -1) : upstreamUrl.pathname;
|
|
2223
2539
|
}
|
|
2224
2540
|
function buildWellKnownUpstreamUrl(upstream, prefix) {
|
|
2225
|
-
const upstreamUrl = new
|
|
2541
|
+
const upstreamUrl = new URL2(upstream.spec.url);
|
|
2226
2542
|
const suffix = getUpstreamPathSuffixForWellKnown(upstream);
|
|
2227
|
-
return new
|
|
2543
|
+
return new URL2(`${prefix}${suffix}`, upstreamUrl.origin);
|
|
2228
2544
|
}
|
|
2229
2545
|
function getLocalOriginFromRequest(request) {
|
|
2230
2546
|
return `http://${request.headers.host ?? "127.0.0.1"}`;
|
|
@@ -2240,7 +2556,7 @@ function appendUpstreamQuery(url, upstream) {
|
|
|
2240
2556
|
if (!upstream) {
|
|
2241
2557
|
return url;
|
|
2242
2558
|
}
|
|
2243
|
-
const parsed = new
|
|
2559
|
+
const parsed = new URL2(url);
|
|
2244
2560
|
parsed.searchParams.set("upstream", upstream);
|
|
2245
2561
|
return parsed.toString();
|
|
2246
2562
|
}
|
|
@@ -2312,51 +2628,52 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
2312
2628
|
const results = {};
|
|
2313
2629
|
const upstreams = listUpstreams(config);
|
|
2314
2630
|
for (const upstream of upstreams) {
|
|
2315
|
-
|
|
2316
|
-
|
|
2631
|
+
const fingerprint = specFingerprint(upstream.spec);
|
|
2632
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
2633
|
+
if (cached?.fingerprint === fingerprint) {
|
|
2634
|
+
results[upstream.name] = cached.count;
|
|
2317
2635
|
continue;
|
|
2318
2636
|
}
|
|
2637
|
+
let toolsCount = 0;
|
|
2638
|
+
let resourcesCount = 0;
|
|
2639
|
+
let promptsCount = 0;
|
|
2640
|
+
const errors = [];
|
|
2319
2641
|
try {
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
2341
|
-
}
|
|
2342
|
-
} catch (err) {}
|
|
2343
|
-
if (prevTimeout !== undefined) {
|
|
2344
|
-
process.env.MCPX_UPSTREAM_TIMEOUT_MS = prevTimeout;
|
|
2345
|
-
} else {
|
|
2346
|
-
delete process.env.MCPX_UPSTREAM_TIMEOUT_MS;
|
|
2347
|
-
}
|
|
2348
|
-
const total = toolsCount + resourcesCount + promptsCount;
|
|
2349
|
-
const countObj = {
|
|
2350
|
-
tools: toolsCount,
|
|
2351
|
-
resources: resourcesCount,
|
|
2352
|
-
prompts: promptsCount,
|
|
2353
|
-
total
|
|
2354
|
-
};
|
|
2355
|
-
runtime.tokenCache.set(upstream.name, countObj);
|
|
2356
|
-
results[upstream.name] = countObj;
|
|
2642
|
+
const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
|
|
2643
|
+
if (toolsResult && toolsResult.tools) {
|
|
2644
|
+
toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
|
|
2645
|
+
}
|
|
2646
|
+
} catch (error) {
|
|
2647
|
+
errors.push(`tools/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
|
|
2651
|
+
if (resourcesResult && resourcesResult.resources) {
|
|
2652
|
+
resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
|
|
2653
|
+
}
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
errors.push(`resources/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2656
|
+
}
|
|
2657
|
+
try {
|
|
2658
|
+
const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
|
|
2659
|
+
if (promptsResult && promptsResult.prompts) {
|
|
2660
|
+
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
2661
|
+
}
|
|
2357
2662
|
} catch (error) {
|
|
2358
|
-
|
|
2663
|
+
errors.push(`prompts/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2664
|
+
}
|
|
2665
|
+
const total = toolsCount + resourcesCount + promptsCount;
|
|
2666
|
+
const countObj = {
|
|
2667
|
+
tools: toolsCount,
|
|
2668
|
+
resources: resourcesCount,
|
|
2669
|
+
prompts: promptsCount,
|
|
2670
|
+
total,
|
|
2671
|
+
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
2672
|
+
};
|
|
2673
|
+
if (errors.length === 0) {
|
|
2674
|
+
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
2359
2675
|
}
|
|
2676
|
+
results[upstream.name] = countObj;
|
|
2360
2677
|
}
|
|
2361
2678
|
return results;
|
|
2362
2679
|
}
|
|
@@ -2380,11 +2697,9 @@ function specFingerprint(spec) {
|
|
|
2380
2697
|
return JSON.stringify(spec);
|
|
2381
2698
|
}
|
|
2382
2699
|
function resolveStdioEnv(spec, secrets) {
|
|
2383
|
-
if (!spec.env || Object.keys(spec.env).length === 0) {
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
2700
|
const env = getDefaultEnvironment();
|
|
2387
|
-
|
|
2701
|
+
env.PATH = buildEnrichedPath(env.PATH);
|
|
2702
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
2388
2703
|
env[key] = secrets.resolveMaybeSecret(value);
|
|
2389
2704
|
}
|
|
2390
2705
|
return env;
|
|
@@ -2493,12 +2808,21 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
|
|
|
2493
2808
|
}
|
|
2494
2809
|
}
|
|
2495
2810
|
async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
|
|
2811
|
+
return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
|
|
2812
|
+
}
|
|
2813
|
+
async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
|
|
2496
2814
|
const headers = {
|
|
2497
2815
|
"content-type": "application/json",
|
|
2498
2816
|
accept: "application/json, text/event-stream"
|
|
2499
2817
|
};
|
|
2500
2818
|
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
2501
|
-
|
|
2819
|
+
if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
|
|
2820
|
+
const oauthServerName = oauthReferenceServerName(value);
|
|
2821
|
+
const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
|
|
2822
|
+
headers[key] = `Bearer ${accessToken}`;
|
|
2823
|
+
} else {
|
|
2824
|
+
headers[key] = secrets.resolveMaybeSecret(value);
|
|
2825
|
+
}
|
|
2502
2826
|
}
|
|
2503
2827
|
if (passthroughAuthorizationHeader) {
|
|
2504
2828
|
headers.Authorization = passthroughAuthorizationHeader;
|
|
@@ -2530,6 +2854,9 @@ async function callHttpUpstream(upstream, method, params, id, secrets, passthrou
|
|
|
2530
2854
|
}
|
|
2531
2855
|
if (!response.ok) {
|
|
2532
2856
|
const responseText = await response.text();
|
|
2857
|
+
if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
|
|
2858
|
+
return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
|
|
2859
|
+
}
|
|
2533
2860
|
throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
|
|
2534
2861
|
}
|
|
2535
2862
|
let payload = null;
|
|
@@ -2671,10 +2998,12 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
2671
2998
|
if (!runtime.tokenCache) {
|
|
2672
2999
|
runtime.tokenCache = new Map;
|
|
2673
3000
|
}
|
|
2674
|
-
const
|
|
3001
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3002
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
2675
3003
|
existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
|
|
2676
3004
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
2677
|
-
|
|
3005
|
+
delete existing.error;
|
|
3006
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2678
3007
|
for (const tool of result.tools ?? []) {
|
|
2679
3008
|
const name = typeof tool.name === "string" ? tool.name : "tool";
|
|
2680
3009
|
tools.push({
|
|
@@ -2683,9 +3012,10 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
2683
3012
|
});
|
|
2684
3013
|
}
|
|
2685
3014
|
} catch (error) {
|
|
2686
|
-
if (upstreams.length === 1
|
|
3015
|
+
if (upstreams.length === 1) {
|
|
2687
3016
|
throw error;
|
|
2688
3017
|
}
|
|
3018
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2689
3019
|
}
|
|
2690
3020
|
}
|
|
2691
3021
|
return { tools };
|
|
@@ -2701,10 +3031,12 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
2701
3031
|
if (!runtime.tokenCache) {
|
|
2702
3032
|
runtime.tokenCache = new Map;
|
|
2703
3033
|
}
|
|
2704
|
-
const
|
|
3034
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3035
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
2705
3036
|
existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
|
|
2706
3037
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
2707
|
-
|
|
3038
|
+
delete existing.error;
|
|
3039
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2708
3040
|
for (const resource of result.resources ?? []) {
|
|
2709
3041
|
const originalUri = typeof resource.uri === "string" ? resource.uri : "";
|
|
2710
3042
|
const originalName = typeof resource.name === "string" ? resource.name : originalUri;
|
|
@@ -2715,9 +3047,10 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
2715
3047
|
});
|
|
2716
3048
|
}
|
|
2717
3049
|
} catch (error) {
|
|
2718
|
-
if (upstreams.length === 1
|
|
3050
|
+
if (upstreams.length === 1) {
|
|
2719
3051
|
throw error;
|
|
2720
3052
|
}
|
|
3053
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2721
3054
|
}
|
|
2722
3055
|
}
|
|
2723
3056
|
return { resources };
|
|
@@ -2733,10 +3066,12 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
2733
3066
|
if (!runtime.tokenCache) {
|
|
2734
3067
|
runtime.tokenCache = new Map;
|
|
2735
3068
|
}
|
|
2736
|
-
const
|
|
3069
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3070
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
2737
3071
|
existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
|
|
2738
3072
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
2739
|
-
|
|
3073
|
+
delete existing.error;
|
|
3074
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2740
3075
|
for (const prompt of result.prompts ?? []) {
|
|
2741
3076
|
const name = typeof prompt.name === "string" ? prompt.name : "prompt";
|
|
2742
3077
|
prompts.push({
|
|
@@ -2745,9 +3080,10 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
2745
3080
|
});
|
|
2746
3081
|
}
|
|
2747
3082
|
} catch (error) {
|
|
2748
|
-
if (upstreams.length === 1
|
|
3083
|
+
if (upstreams.length === 1) {
|
|
2749
3084
|
throw error;
|
|
2750
3085
|
}
|
|
3086
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2751
3087
|
}
|
|
2752
3088
|
}
|
|
2753
3089
|
return { prompts };
|
|
@@ -2887,6 +3223,11 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
|
|
|
2887
3223
|
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
2888
3224
|
return makeResult(id, result);
|
|
2889
3225
|
}
|
|
3226
|
+
if (request.method === "custom/refreshTokenCounts") {
|
|
3227
|
+
runtime.tokenCache?.clear();
|
|
3228
|
+
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
3229
|
+
return makeResult(id, result);
|
|
3230
|
+
}
|
|
2890
3231
|
if (request.method === "tools/list") {
|
|
2891
3232
|
const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2892
3233
|
return makeResult(id, result);
|
|
@@ -2973,12 +3314,13 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
|
|
|
2973
3314
|
function createGatewayServer(options) {
|
|
2974
3315
|
const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
|
|
2975
3316
|
const runtime = {
|
|
2976
|
-
stdioConnections: new Map
|
|
3317
|
+
stdioConnections: new Map,
|
|
3318
|
+
upstreamErrors: new Map
|
|
2977
3319
|
};
|
|
2978
|
-
const server =
|
|
2979
|
-
let requestUrl = new
|
|
3320
|
+
const server = http2.createServer(async (request, response) => {
|
|
3321
|
+
let requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
2980
3322
|
try {
|
|
2981
|
-
requestUrl = new
|
|
3323
|
+
requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
2982
3324
|
const upstreamFilter = getRequestedUpstream(requestUrl);
|
|
2983
3325
|
if (debug) {
|
|
2984
3326
|
console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
|
|
@@ -3089,7 +3431,7 @@ function createGatewayServer(options) {
|
|
|
3089
3431
|
response.setHeader("content-type", "application/json");
|
|
3090
3432
|
}
|
|
3091
3433
|
if (hasInitialize) {
|
|
3092
|
-
const sessionId =
|
|
3434
|
+
const sessionId = crypto2.randomUUID();
|
|
3093
3435
|
response.setHeader("mcp-session-id", sessionId);
|
|
3094
3436
|
response.setHeader("MCP-Session-Id", sessionId);
|
|
3095
3437
|
}
|
|
@@ -3119,7 +3461,11 @@ function createGatewayServer(options) {
|
|
|
3119
3461
|
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
3120
3462
|
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
3121
3463
|
}
|
|
3122
|
-
response.end(
|
|
3464
|
+
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
3465
|
+
status: error.status,
|
|
3466
|
+
body: error.bodyText,
|
|
3467
|
+
wwwAuthenticate: error.wwwAuthenticate
|
|
3468
|
+
})));
|
|
3123
3469
|
if (debug) {
|
|
3124
3470
|
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
3125
3471
|
}
|
|
@@ -3145,10 +3491,10 @@ function createGatewayServer(options) {
|
|
|
3145
3491
|
|
|
3146
3492
|
// src/core/daemon.ts
|
|
3147
3493
|
function readPidFromFile(pidPath = getPidPath()) {
|
|
3148
|
-
if (!
|
|
3494
|
+
if (!fs9.existsSync(pidPath)) {
|
|
3149
3495
|
return null;
|
|
3150
3496
|
}
|
|
3151
|
-
const raw =
|
|
3497
|
+
const raw = fs9.readFileSync(pidPath, "utf8").trim();
|
|
3152
3498
|
if (!raw) {
|
|
3153
3499
|
return null;
|
|
3154
3500
|
}
|
|
@@ -3195,6 +3541,29 @@ async function resolveGatewayPort(config) {
|
|
|
3195
3541
|
}
|
|
3196
3542
|
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
3197
3543
|
}
|
|
3544
|
+
async function waitForGatewayReady(port, token, timeoutMs = 5000) {
|
|
3545
|
+
const deadline = Date.now() + timeoutMs;
|
|
3546
|
+
let lastError;
|
|
3547
|
+
while (Date.now() < deadline) {
|
|
3548
|
+
try {
|
|
3549
|
+
const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
3550
|
+
method: "GET",
|
|
3551
|
+
headers: {
|
|
3552
|
+
Authorization: `Bearer ${token}`
|
|
3553
|
+
},
|
|
3554
|
+
signal: AbortSignal.timeout(500)
|
|
3555
|
+
});
|
|
3556
|
+
if (response.ok) {
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
lastError = new Error(`Gateway returned HTTP ${response.status}.`);
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
lastError = error;
|
|
3562
|
+
}
|
|
3563
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3564
|
+
}
|
|
3565
|
+
throw new Error(`Gateway did not become ready within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
3566
|
+
}
|
|
3198
3567
|
function getDaemonStatus(config) {
|
|
3199
3568
|
const pidPath = getPidPath();
|
|
3200
3569
|
const pid = readPidFromFile(pidPath);
|
|
@@ -3227,16 +3596,16 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3227
3596
|
};
|
|
3228
3597
|
}
|
|
3229
3598
|
try {
|
|
3230
|
-
|
|
3599
|
+
fs9.unlinkSync(getPidPath());
|
|
3231
3600
|
} catch {}
|
|
3232
3601
|
}
|
|
3233
3602
|
const port = await resolveGatewayPort(config);
|
|
3234
|
-
ensureGatewayToken(config, secrets);
|
|
3603
|
+
const token = ensureGatewayToken(config, secrets);
|
|
3235
3604
|
const pidPath = getPidPath();
|
|
3236
3605
|
const logPath = getLogPath();
|
|
3237
3606
|
ensureParentDir(pidPath);
|
|
3238
3607
|
ensureParentDir(logPath);
|
|
3239
|
-
const logFd =
|
|
3608
|
+
const logFd = fs9.openSync(logPath, "a", 384);
|
|
3240
3609
|
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
3241
3610
|
detached: true,
|
|
3242
3611
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -3246,9 +3615,10 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3246
3615
|
}
|
|
3247
3616
|
});
|
|
3248
3617
|
child.unref();
|
|
3249
|
-
|
|
3618
|
+
fs9.writeFileSync(pidPath, `${child.pid}
|
|
3250
3619
|
`, { mode: 384 });
|
|
3251
3620
|
startBackgroundUpdateCheck();
|
|
3621
|
+
await waitForGatewayReady(port, token);
|
|
3252
3622
|
return {
|
|
3253
3623
|
started: true,
|
|
3254
3624
|
pid: child.pid ?? -1,
|
|
@@ -3269,7 +3639,7 @@ function stopDaemon() {
|
|
|
3269
3639
|
process.kill(pid, "SIGTERM");
|
|
3270
3640
|
} catch {}
|
|
3271
3641
|
try {
|
|
3272
|
-
|
|
3642
|
+
fs9.unlinkSync(pidPath);
|
|
3273
3643
|
} catch {}
|
|
3274
3644
|
return {
|
|
3275
3645
|
stopped: true,
|
|
@@ -3282,22 +3652,17 @@ async function restartDaemon(config, cliPath, secrets) {
|
|
|
3282
3652
|
}
|
|
3283
3653
|
function readDaemonLogs(maxLines = 200) {
|
|
3284
3654
|
const logPath = getLogPath();
|
|
3285
|
-
if (!
|
|
3655
|
+
if (!fs9.existsSync(logPath)) {
|
|
3286
3656
|
return "";
|
|
3287
3657
|
}
|
|
3288
|
-
const lines =
|
|
3658
|
+
const lines = fs9.readFileSync(logPath, "utf8").split(`
|
|
3289
3659
|
`);
|
|
3290
3660
|
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
3291
3661
|
`);
|
|
3292
3662
|
}
|
|
3293
3663
|
function runDaemonForeground(config, port, secrets) {
|
|
3294
3664
|
return new Promise((resolve, reject) => {
|
|
3295
|
-
const
|
|
3296
|
-
const token = secrets.getSecret(tokenName);
|
|
3297
|
-
if (!token) {
|
|
3298
|
-
reject(new Error(`Local gateway token not found: ${tokenName}`));
|
|
3299
|
-
return;
|
|
3300
|
-
}
|
|
3665
|
+
const token = ensureGatewayToken(config, secrets);
|
|
3301
3666
|
const server = createGatewayServer({
|
|
3302
3667
|
port,
|
|
3303
3668
|
expectedToken: token,
|
|
@@ -3367,7 +3732,8 @@ async function fetchTokenCounts(gatewayUrl, token) {
|
|
|
3367
3732
|
id: "status-tokens",
|
|
3368
3733
|
method: "custom/tokenCounts",
|
|
3369
3734
|
params: {}
|
|
3370
|
-
})
|
|
3735
|
+
}),
|
|
3736
|
+
signal: AbortSignal.timeout(4000)
|
|
3371
3737
|
});
|
|
3372
3738
|
if (!res.ok) {
|
|
3373
3739
|
return {};
|
|
@@ -3424,4 +3790,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
3424
3790
|
};
|
|
3425
3791
|
}
|
|
3426
3792
|
|
|
3427
|
-
export { listSkills, getSkill, saveSkill, deleteSkill, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|
|
3793
|
+
export { listSkills, getSkill, saveSkill, deleteSkill, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|