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