@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.
Files changed (41) hide show
  1. package/dist/adapters/claude.d.ts.map +1 -1
  2. package/dist/adapters/cline.d.ts.map +1 -1
  3. package/dist/{cli-fsf33sgp.js → cli-08mgt3c8.js} +4 -1
  4. package/dist/cli-0ztrmp65.js +514 -0
  5. package/dist/{cli-htm5cf82.js → cli-f2a6pkds.js} +492 -126
  6. package/dist/{cli-s4py8ckf.js → cli-ma89vavk.js} +42 -11
  7. package/dist/{cli-f16kphtq.js → cli-wcxyqz7g.js} +493 -127
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +34 -10
  10. package/dist/{config-kayej21q.js → config-p8tjffpa.js} +2 -2
  11. package/dist/core/add-command.d.ts +7 -0
  12. package/dist/core/add-command.d.ts.map +1 -0
  13. package/dist/core/auth-probe.d.ts +1 -0
  14. package/dist/core/auth-probe.d.ts.map +1 -1
  15. package/dist/core/config-repair.d.ts +3 -0
  16. package/dist/core/config-repair.d.ts.map +1 -0
  17. package/dist/core/config.d.ts.map +1 -1
  18. package/dist/core/daemon.d.ts.map +1 -1
  19. package/dist/core/index.d.ts +5 -2
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +25 -5
  22. package/dist/core/oauth.d.ts +12 -0
  23. package/dist/core/oauth.d.ts.map +1 -0
  24. package/dist/core/paths.d.ts +1 -0
  25. package/dist/core/paths.d.ts.map +1 -1
  26. package/dist/core/registry.d.ts +1 -0
  27. package/dist/core/registry.d.ts.map +1 -1
  28. package/dist/core/spawn-env.d.ts +2 -0
  29. package/dist/core/spawn-env.d.ts.map +1 -0
  30. package/dist/core/status.d.ts.map +1 -1
  31. package/dist/core/sync.d.ts.map +1 -1
  32. package/dist/core/update.d.ts +1 -0
  33. package/dist/core/update.d.ts.map +1 -1
  34. package/dist/gateway/server.d.ts +5 -1
  35. package/dist/gateway/server.d.ts.map +1 -1
  36. package/dist/{registry-1s14wd58.js → registry-b3cetq7x.js} +4 -2
  37. package/dist/types.d.ts +1 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/package.json +1 -1
  41. 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-7bsf6akp.js";
5
+ } from "./cli-0ztrmp65.js";
5
6
  import {
6
7
  SecretsManager,
7
- ensureGatewayToken,
8
- getGatewayTokenSecretName
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-fsf33sgp.js";
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: response.headers.get("www-authenticate") ?? undefined
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 serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
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 managedNames) {
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, managedNames);
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(options.managedEntries.map((entry) => {
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, enabled: entry.enabled })];
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 fs6 from "node:fs";
1865
- import path11 from "node:path";
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 (!fs6.existsSync(stagedPath)) {
2217
+ if (!fs7.existsSync(stagedPath)) {
1905
2218
  return null;
1906
2219
  }
1907
2220
  try {
1908
- const raw = fs6.readFileSync(stagedPath, "utf8");
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 = path11.isAbsolute(info.cliPath) ? info.cliPath : path11.join(getUpdatesDir(), info.cliPath);
1914
- if (!fs6.existsSync(absoluteCliPath)) {
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: path11.isAbsolute(cliPath) ? path11.relative(updatesDir, cliPath) : cliPath,
2243
+ cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
1931
2244
  stagedAt: new Date().toISOString()
1932
2245
  };
1933
- fs6.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
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 (fs6.existsSync(stagedPath)) {
1938
- fs6.unlinkSync(stagedPath);
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 (!fs6.existsSync(updatesDir)) {
2263
+ if (!fs7.existsSync(updatesDir)) {
1948
2264
  return;
1949
2265
  }
1950
- const entries = fs6.readdirSync(updatesDir, { withFileTypes: true });
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 = path11.join(updatesDir, oldVersion);
2272
+ const oldPath = path12.join(updatesDir, oldVersion);
1957
2273
  try {
1958
- fs6.rmSync(oldPath, { recursive: true, force: true });
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 fs7 from "node:fs";
1965
- import path12 from "node:path";
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(path12.dirname(lockPath));
1971
- fs7.writeFileSync(lockPath, `${process.pid}
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
- fs7.unlinkSync(lockPath);
2297
+ fs8.unlinkSync(lockPath);
1982
2298
  } catch {}
1983
2299
  }
1984
2300
  function isUpdateInProgress() {
1985
2301
  const lockPath = getUpdateLockPath();
1986
- if (!fs7.existsSync(lockPath)) {
2302
+ if (!fs8.existsSync(lockPath)) {
1987
2303
  return false;
1988
2304
  }
1989
2305
  try {
1990
- const raw = fs7.readFileSync(lockPath, "utf8").trim();
2306
+ const raw = fs8.readFileSync(lockPath, "utf8").trim();
1991
2307
  const pid = Number(raw);
1992
2308
  if (!Number.isFinite(pid) || pid <= 0) {
1993
- fs7.unlinkSync(lockPath);
2309
+ fs8.unlinkSync(lockPath);
1994
2310
  return false;
1995
2311
  }
1996
2312
  process.kill(pid, 0);
1997
2313
  return true;
1998
2314
  } catch {
1999
- fs7.unlinkSync(lockPath);
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 = path12.join(updatesDir, `v${targetVersion}`);
2011
- if (fs7.existsSync(versionDir)) {
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 = path12.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2024
- if (!fs7.existsSync(tarballPath)) {
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 = path12.join(versionDir, "dist", "cli.js");
2036
- if (!fs7.existsSync(cliPath)) {
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
- fs7.rmSync(versionDir, { recursive: true, force: true });
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 fs8 from "node:fs";
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 http from "node:http";
2128
- import { URL } from "node:url";
2129
- import crypto from "node:crypto";
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 URL(upstream.spec.url);
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 URL(upstream.spec.url);
2541
+ const upstreamUrl = new URL2(upstream.spec.url);
2226
2542
  const suffix = getUpstreamPathSuffixForWellKnown(upstream);
2227
- return new URL(`${prefix}${suffix}`, upstreamUrl.origin);
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 URL(url);
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
- if (runtime.tokenCache.has(upstream.name)) {
2316
- results[upstream.name] = runtime.tokenCache.get(upstream.name);
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 prevTimeout = process.env.MCPX_UPSTREAM_TIMEOUT_MS;
2321
- process.env.MCPX_UPSTREAM_TIMEOUT_MS = "3000";
2322
- let toolsCount = 0;
2323
- let resourcesCount = 0;
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;
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
- results[upstream.name] = { tools: 0, resources: 0, prompts: 0, total: 0 };
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
- for (const [key, value] of Object.entries(spec.env)) {
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
- headers[key] = secrets.resolveMaybeSecret(value);
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 existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
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
- runtime.tokenCache.set(upstream.name, existing);
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 && isAuthChallenge(error)) {
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 existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
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
- runtime.tokenCache.set(upstream.name, existing);
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 && isAuthChallenge(error)) {
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 existing = runtime.tokenCache.get(upstream.name) ?? { tools: 0, resources: 0, prompts: 0, total: 0 };
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
- runtime.tokenCache.set(upstream.name, existing);
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 && isAuthChallenge(error)) {
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 = http.createServer(async (request, response) => {
2979
- let requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
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 URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
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 = crypto.randomUUID();
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(error.bodyText.length > 0 ? error.bodyText : JSON.stringify({ error: "upstream_auth_required" }));
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 (!fs8.existsSync(pidPath)) {
3494
+ if (!fs9.existsSync(pidPath)) {
3149
3495
  return null;
3150
3496
  }
3151
- const raw = fs8.readFileSync(pidPath, "utf8").trim();
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
- fs8.unlinkSync(getPidPath());
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 = fs8.openSync(logPath, "a", 384);
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
- fs8.writeFileSync(pidPath, `${child.pid}
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
- fs8.unlinkSync(pidPath);
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 (!fs8.existsSync(logPath)) {
3655
+ if (!fs9.existsSync(logPath)) {
3286
3656
  return "";
3287
3657
  }
3288
- const lines = fs8.readFileSync(logPath, "utf8").split(`
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 tokenName = getGatewayTokenSecretName(config);
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 };