@kwonye/mcpx 0.1.99 → 0.1.101

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 (40) hide show
  1. package/dist/{cli-28g7x9rn.js → cli-6wh8qx2y.js} +9 -3
  2. package/dist/{cli-dy7x0zrg.js → cli-6zq2fd6h.js} +1 -1
  3. package/dist/{cli-b0xrqsjc.js → cli-9k3wmag6.js} +286 -363
  4. package/dist/{cli-ct7asw78.js → cli-aawpvhhe.js} +13 -10
  5. package/dist/{cli-1ewrsfj4.js → cli-hh7rthhx.js} +4 -1
  6. package/dist/{cli-7x2n23em.js → cli-qzzjkt0q.js} +1132 -723
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +352 -88
  9. package/dist/{config-yx5m60fv.js → config-fbgyqmv1.js} +2 -2
  10. package/dist/core/auth-probe.d.ts +6 -1
  11. package/dist/core/auth-probe.d.ts.map +1 -1
  12. package/dist/core/daemon.d.ts +1 -0
  13. package/dist/core/daemon.d.ts.map +1 -1
  14. package/dist/core/identifiers.d.ts +3 -0
  15. package/dist/core/identifiers.d.ts.map +1 -0
  16. package/dist/core/index.d.ts +2 -2
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +22 -6
  19. package/dist/core/marketplace.d.ts.map +1 -1
  20. package/dist/core/oauth.d.ts +77 -2
  21. package/dist/core/oauth.d.ts.map +1 -1
  22. package/dist/core/paths.d.ts +1 -0
  23. package/dist/core/paths.d.ts.map +1 -1
  24. package/dist/core/plugin-cache.d.ts.map +1 -1
  25. package/dist/core/plugin-host.d.ts.map +1 -1
  26. package/dist/core/plugin-lifecycle.d.ts.map +1 -1
  27. package/dist/core/plugin-manager.d.ts.map +1 -1
  28. package/dist/core/plugin-parse.d.ts.map +1 -1
  29. package/dist/core/plugin-projections.d.ts.map +1 -1
  30. package/dist/core/plugin-source.d.ts.map +1 -1
  31. package/dist/core/secrets.d.ts.map +1 -1
  32. package/dist/core/skill-projections.d.ts.map +1 -1
  33. package/dist/core/skills.d.ts.map +1 -1
  34. package/dist/core/update-manager.d.ts.map +1 -1
  35. package/dist/gateway/server.d.ts.map +1 -1
  36. package/dist/{marketplace-pwabb3ea.js → marketplace-zem0m4mw.js} +5 -5
  37. package/dist/{plugin-host-9xcpnehq.js → plugin-host-5jmyt0td.js} +13 -39
  38. package/dist/{plugin-manager-19q55n69.js → plugin-manager-rw05c2x3.js} +4 -4
  39. package/dist/{registry-yvbxcaq6.js → registry-04kfpjh0.js} +2 -2
  40. package/package.json +1 -1
@@ -1,735 +1,1066 @@
1
1
  import {
2
2
  refreshAutoUpdateMarketplaces
3
- } from "./cli-ct7asw78.js";
3
+ } from "./cli-aawpvhhe.js";
4
4
  import {
5
5
  APP_VERSION,
6
6
  getGatewayUrl,
7
+ mutateConfig,
7
8
  persistSyncState,
8
9
  syncAllClients
9
- } from "./cli-b0xrqsjc.js";
10
+ } from "./cli-9k3wmag6.js";
10
11
  import {
11
12
  loadConfig,
12
13
  loadMergedConfig,
13
14
  saveConfig
14
- } from "./cli-dy7x0zrg.js";
15
+ } from "./cli-6zq2fd6h.js";
15
16
  import {
16
17
  SecretsManager,
17
18
  UpstreamError,
18
19
  classifyUpstreamError,
19
20
  ensureGatewayToken
20
- } from "./cli-1ewrsfj4.js";
21
+ } from "./cli-hh7rthhx.js";
21
22
  import {
22
23
  ensureDir,
23
24
  ensureParentDir,
24
25
  getLogPath,
25
26
  getMarketplaceUpdateLockPath,
26
27
  getMarketplaceUpdateStatePath,
28
+ getOAuthLockPath,
27
29
  getPidPath,
28
30
  getStagedVersionPath,
29
31
  getUpdateLockPath,
30
32
  getUpdatesDir
31
- } from "./cli-28g7x9rn.js";
33
+ } from "./cli-6wh8qx2y.js";
32
34
  import {
33
35
  isServerEnabled
34
36
  } from "./cli-s4gspqdf.js";
35
37
 
36
- // src/core/auth-probe.ts
37
- var JSON_RPC_VERSION = "2.0";
38
- var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
39
- var AUTH_KEYWORDS = [
40
- "unauthorized",
41
- "unauthenticated",
42
- "forbidden",
43
- "authentication",
44
- "authorization",
45
- "api key",
46
- "api_key",
47
- "apikey",
48
- "credentials",
49
- "access denied",
50
- "auth required",
51
- "x-api-key",
52
- "invalid key",
53
- "missing header",
54
- "not authenticated",
55
- "auth token"
56
- ];
57
- function isAuthRelatedMessage(message) {
58
- const lower = message.toLowerCase();
59
- return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
38
+ // src/core/oauth.ts
39
+ import fs from "node:fs";
40
+ import path from "node:path";
41
+ import http from "node:http";
42
+ import crypto from "node:crypto";
43
+ import {
44
+ auth,
45
+ discoverOAuthServerInfo,
46
+ discoverOAuthProtectedResourceMetadata,
47
+ discoverAuthorizationServerMetadata,
48
+ refreshAuthorization
49
+ } from "@modelcontextprotocol/sdk/client/auth.js";
50
+ import { InvalidClientError, InvalidGrantError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
51
+ function oauthSecretName(serverName, suffix) {
52
+ const safe = serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_").slice(0, 32);
53
+ const hash = crypto.createHash("sha256").update(serverName).digest("hex").slice(0, 12);
54
+ return `oauth_${safe}_${hash}_${suffix}`;
60
55
  }
61
- function resolveHeaders(spec, secrets) {
62
- const headers = {
63
- "content-type": "application/json",
64
- accept: "application/json, text/event-stream"
56
+ function legacyOAuthSecretName(serverName, suffix) {
57
+ return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
58
+ }
59
+ function readJsonSecret(secrets, name) {
60
+ let raw = secrets.getSecret(name);
61
+ if (!raw) {
62
+ const match = name.match(/^oauth_(.+)_([0-9a-f]{12})_(client|tokens|verifier|discovery)$/);
63
+ if (match) {
64
+ const suffix = match[3];
65
+ const withoutHash = name.replace(/_[0-9a-f]{12}_/, "_");
66
+ raw = secrets.getSecret(withoutHash);
67
+ if (!raw) {}
68
+ }
69
+ }
70
+ if (!raw) {
71
+ return;
72
+ }
73
+ try {
74
+ return JSON.parse(raw);
75
+ } catch {
76
+ return;
77
+ }
78
+ }
79
+ function getOAuthSecretWithFallback(secrets, serverName, suffix) {
80
+ const name = oauthSecretName(serverName, suffix);
81
+ let raw = secrets.getSecret(name);
82
+ if (raw)
83
+ return raw;
84
+ return secrets.getSecret(legacyOAuthSecretName(serverName, suffix));
85
+ }
86
+ function writeJsonSecret(secrets, name, value) {
87
+ secrets.setSecret(name, JSON.stringify(value));
88
+ const match = name.match(/^oauth_(.+)_([0-9a-f]{12})_(client|tokens|verifier|discovery)$/);
89
+ if (match) {
90
+ const withoutHash = name.replace(/_[0-9a-f]{12}_/, "_");
91
+ secrets.setSecret(withoutHash, JSON.stringify(value));
92
+ }
93
+ }
94
+ function isOAuthReference(value) {
95
+ return value.startsWith("oauth://");
96
+ }
97
+ function oauthReferenceServerName(value) {
98
+ if (!isOAuthReference(value)) {
99
+ throw new Error(`Not an OAuth reference: ${value}`);
100
+ }
101
+ return decodeURIComponent(value.slice("oauth://".length));
102
+ }
103
+ var DEFAULT_OAUTH_SUPPORT_TIMEOUT_MS = 5000;
104
+ var OAUTH_SUPPORT_SUPPORTED_TTL_MS = 5 * 60000;
105
+ var OAUTH_SUPPORT_NEGATIVE_TTL_MS = 60000;
106
+ var oauthSupportCache = new Map;
107
+ function __resetOAuthSupportCache() {
108
+ oauthSupportCache.clear();
109
+ }
110
+ async function probeOAuthSupport(serverUrl, options = {}) {
111
+ const cached = oauthSupportCache.get(serverUrl);
112
+ if (!options.force && cached) {
113
+ const ttl = cached.probe.support === "supported" ? OAUTH_SUPPORT_SUPPORTED_TTL_MS : OAUTH_SUPPORT_NEGATIVE_TTL_MS;
114
+ if (Date.now() - cached.at < ttl) {
115
+ return cached.probe;
116
+ }
117
+ }
118
+ const timeoutMs = options.timeoutMs ?? DEFAULT_OAUTH_SUPPORT_TIMEOUT_MS;
119
+ let sawResponse = false;
120
+ const timeoutFetch = async (input, init) => {
121
+ const controller = new AbortController;
122
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
123
+ try {
124
+ const response = await fetch(input, { ...init, signal: controller.signal });
125
+ sawResponse = true;
126
+ return response;
127
+ } finally {
128
+ clearTimeout(timeout);
129
+ }
65
130
  };
66
- for (const [key, value] of Object.entries(spec.headers ?? {})) {
67
- headers[key] = secrets.resolveMaybeSecret(value);
131
+ let resourceMetadataFound = false;
132
+ let authorizationServerUrl;
133
+ try {
134
+ const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, timeoutFetch);
135
+ resourceMetadataFound = true;
136
+ authorizationServerUrl = resourceMetadata.authorization_servers?.[0];
137
+ } catch {}
138
+ let authServerMetadataFound = false;
139
+ if (!resourceMetadataFound) {
140
+ try {
141
+ const fallbackAuthServerUrl = new URL("/", serverUrl);
142
+ const metadata = await discoverAuthorizationServerMetadata(fallbackAuthServerUrl, { fetchFn: timeoutFetch });
143
+ authServerMetadataFound = metadata != null;
144
+ if (authServerMetadataFound) {
145
+ authorizationServerUrl = fallbackAuthServerUrl.toString();
146
+ }
147
+ } catch {}
68
148
  }
69
- return headers;
149
+ let probe;
150
+ if (resourceMetadataFound || authServerMetadataFound) {
151
+ probe = {
152
+ support: "supported",
153
+ authorizationServerUrl,
154
+ resourceMetadata: resourceMetadataFound,
155
+ authorizationServerMetadata: authServerMetadataFound
156
+ };
157
+ } else if (sawResponse) {
158
+ probe = { support: "unsupported", resourceMetadata: false, authorizationServerMetadata: false };
159
+ } else {
160
+ probe = {
161
+ support: "unknown",
162
+ resourceMetadata: false,
163
+ authorizationServerMetadata: false,
164
+ error: "Could not reach the server to check for OAuth support."
165
+ };
166
+ }
167
+ oauthSupportCache.set(serverUrl, { at: Date.now(), probe });
168
+ return probe;
70
169
  }
71
- function isOAuthLikelyChallenge(wwwAuthenticate) {
72
- if (!wwwAuthenticate) {
170
+ function tokensAreExpiring(tokens) {
171
+ if (!tokens.tokens.expires_in) {
73
172
  return false;
74
173
  }
75
- const lower = wwwAuthenticate.toLowerCase();
76
- return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
174
+ const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
175
+ return Date.now() >= expiresAt - 60000;
77
176
  }
78
- async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS) {
79
- const timeoutController = new AbortController;
80
- const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
81
- try {
82
- const headers = resolveHeaders(spec, secrets);
83
- const response = await fetch(spec.url, {
84
- method: "POST",
85
- headers,
86
- body: JSON.stringify({
87
- jsonrpc: JSON_RPC_VERSION,
88
- id: "mcpx-auth-probe",
89
- method: "tools/list",
90
- params: {}
91
- }),
92
- signal: timeoutController.signal
93
- });
94
- if (response.status === 401 || response.status === 403) {
95
- const wwwAuthenticate = response.headers.get("www-authenticate");
177
+
178
+ class McpxOAuthProvider {
179
+ #serverName;
180
+ #secrets;
181
+ #redirectUrlValue;
182
+ #stateValue;
183
+ constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
184
+ this.#serverName = serverName;
185
+ this.#secrets = secrets;
186
+ this.#redirectUrlValue = redirectUrl;
187
+ this.#stateValue = state;
188
+ }
189
+ get redirectUrl() {
190
+ return this.#redirectUrlValue;
191
+ }
192
+ get clientMetadata() {
193
+ if (!this.#redirectUrlValue) {
96
194
  return {
97
- authRequired: true,
98
- oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
99
- status: response.status,
100
- wwwAuthenticate: wwwAuthenticate ?? undefined
195
+ redirect_uris: [],
196
+ token_endpoint_auth_method: "none",
197
+ grant_types: ["authorization_code", "refresh_token"],
198
+ response_types: ["code"],
199
+ client_name: "mcpx"
101
200
  };
102
201
  }
103
- if (response.status === 200) {
104
- try {
105
- const body = await response.json();
106
- const err = body?.error;
107
- if (err && typeof err === "object" && !("result" in body)) {
108
- const message = typeof err.message === "string" ? err.message : "";
109
- if (message && isAuthRelatedMessage(message)) {
110
- return {
111
- authRequired: true,
112
- status: response.status,
113
- error: `JSON-RPC error: ${message}`
114
- };
115
- }
116
- }
117
- } catch {}
118
- }
119
- return {
120
- authRequired: false,
121
- status: response.status
122
- };
123
- } catch (error) {
124
- const isAbort = error.name === "AbortError";
125
202
  return {
126
- authRequired: false,
127
- error: isAbort ? `Auth probe timed out after ${timeoutMs}ms.` : error.message
203
+ redirect_uris: [this.#redirectUrlValue],
204
+ token_endpoint_auth_method: "none",
205
+ grant_types: ["authorization_code", "refresh_token"],
206
+ response_types: ["code"],
207
+ client_name: "mcpx"
128
208
  };
129
- } finally {
130
- clearTimeout(timeoutHandle);
131
209
  }
132
- }
133
-
134
- // src/core/registry-client.ts
135
- var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
136
- async function fetchRegistryServerDetail(name) {
137
- const encoded = encodeURIComponent(name);
138
- const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
139
- headers: { accept: "application/json" }
140
- });
141
- if (!response.ok) {
142
- throw new Error(`Registry API error: ${response.status}`);
210
+ state() {
211
+ return this.#stateValue;
143
212
  }
144
- const data = await response.json();
145
- return data.server;
146
- }
147
- var PACKAGE_PRIORITY = {
148
- npm: 1,
149
- pypi: 2,
150
- nuget: 3,
151
- oci: 4,
152
- mcpb: 5
153
- };
154
- var RUNTIME_HINT = {
155
- npm: "npx",
156
- pypi: "uvx",
157
- nuget: "dnx"
158
- };
159
- function selectBestPackage(packages = [], remotes = []) {
160
- const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
161
- if (stdioPkgs.length > 0) {
162
- return { kind: "package", package: stdioPkgs[0] };
213
+ clientInformation() {
214
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
163
215
  }
164
- const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
165
- if (httpRemotes.length > 0) {
166
- return { kind: "remote", remote: httpRemotes[0] };
216
+ saveClientInformation(clientInformation) {
217
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
167
218
  }
168
- if (packages.length > 0) {
169
- return { kind: "package", package: packages[0] };
219
+ tokens() {
220
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
170
221
  }
171
- if (remotes.length > 0) {
172
- return { kind: "remote", remote: remotes[0] };
222
+ saveTokens(tokens) {
223
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
224
+ tokens,
225
+ obtainedAt: Date.now()
226
+ });
173
227
  }
174
- throw new Error("Server has no packages or remotes");
175
- }
176
- function extractRequiredInputs(option) {
177
- const inputs = [];
178
- if (option.kind === "package" && option.package?.environmentVariables) {
179
- for (const env of option.package.environmentVariables) {
180
- if (env.isRequired && !env.default) {
181
- inputs.push({
182
- name: env.name,
183
- description: env.description,
184
- isSecret: env.isSecret ?? false,
185
- kind: "env"
186
- });
187
- }
188
- }
189
- for (const arg of option.package.packageArguments ?? []) {
190
- if (arg.isRequired && !arg.value && !arg.default) {
191
- inputs.push({
192
- name: arg.name ?? arg.valueHint ?? "arg",
193
- description: arg.description,
194
- isSecret: false,
195
- kind: "arg"
196
- });
197
- }
198
- }
228
+ redirectToAuthorization(_authorizationUrl) {}
229
+ saveCodeVerifier(codeVerifier) {
230
+ this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
199
231
  }
200
- if (option.kind === "remote" && option.remote?.headers) {
201
- for (const header of option.remote.headers) {
202
- if (header.isRequired && !header.default) {
203
- inputs.push({
204
- name: header.name,
205
- description: header.description,
206
- isSecret: header.isSecret ?? false,
207
- kind: "header"
208
- });
209
- }
232
+ codeVerifier() {
233
+ const verifier = getOAuthSecretWithFallback(this.#secrets, this.#serverName, "verifier");
234
+ if (!verifier) {
235
+ throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
210
236
  }
237
+ return verifier;
211
238
  }
212
- return inputs;
213
- }
214
- function mapRegistryToSpec(_name, option, resolvedValues) {
215
- if (option.kind === "remote") {
216
- const headers = {};
217
- for (const header of option.remote?.headers ?? []) {
218
- const value = resolvedValues[header.name] ?? header.default;
219
- if (value)
220
- headers[header.name] = value;
239
+ saveDiscoveryState(state) {
240
+ writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
241
+ }
242
+ discoveryState() {
243
+ return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
244
+ }
245
+ invalidateCredentials(scope) {
246
+ const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
247
+ for (const suffix of suffixes) {
248
+ this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
249
+ this.#secrets.removeSecret(legacyOAuthSecretName(this.#serverName, suffix));
221
250
  }
222
- return {
223
- transport: "http",
224
- url: option.remote.url,
225
- ...Object.keys(headers).length > 0 ? { headers } : {}
226
- };
227
251
  }
228
- const pkg = option.package;
229
- const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
230
- const args = [];
231
- const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
232
- args.push(identifier);
233
- for (const arg of pkg.packageArguments ?? []) {
234
- const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
235
- if (!value)
236
- continue;
237
- if (arg.type === "named" && arg.name) {
238
- args.push(arg.name, value);
239
- } else {
240
- args.push(value);
252
+ }
253
+
254
+ class InteractiveOAuthProvider extends McpxOAuthProvider {
255
+ #openUrl;
256
+ #cachedClientInfo;
257
+ #cachedCodeVerifier;
258
+ constructor(serverName, secrets, redirectUrl, state, openUrl) {
259
+ super(serverName, secrets, redirectUrl, state);
260
+ this.#openUrl = openUrl;
261
+ }
262
+ async redirectToAuthorization(authorizationUrl) {
263
+ await this.#openUrl(authorizationUrl.toString());
264
+ }
265
+ clientInformation() {
266
+ this.#cachedClientInfo ??= super.clientInformation();
267
+ return this.#cachedClientInfo;
268
+ }
269
+ saveClientInformation(clientInformation) {
270
+ super.saveClientInformation(clientInformation);
271
+ this.#cachedClientInfo = clientInformation;
272
+ }
273
+ codeVerifier() {
274
+ if (this.#cachedCodeVerifier !== undefined) {
275
+ return this.#cachedCodeVerifier;
241
276
  }
277
+ return super.codeVerifier();
242
278
  }
243
- const env = {};
244
- for (const envVar of pkg.environmentVariables ?? []) {
245
- const value = resolvedValues[envVar.name] ?? envVar.default;
246
- if (value)
247
- env[envVar.name] = value;
279
+ saveCodeVerifier(codeVerifier) {
280
+ super.saveCodeVerifier(codeVerifier);
281
+ this.#cachedCodeVerifier = codeVerifier;
282
+ }
283
+ invalidateCredentials(scope) {
284
+ super.invalidateCredentials(scope);
285
+ if (scope === "all" || scope === "client") {
286
+ this.#cachedClientInfo = undefined;
287
+ }
248
288
  }
249
- return {
250
- transport: "stdio",
251
- command: runtime,
252
- args,
253
- ...Object.keys(env).length > 0 ? { env } : {}
254
- };
255
289
  }
256
-
257
- // src/core/server-auth.ts
258
- function normalizeNamePart(value) {
259
- return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
290
+ function listen(server) {
291
+ return new Promise((resolve, reject) => {
292
+ server.once("error", reject);
293
+ server.listen(0, "127.0.0.1", () => {
294
+ const address = server.address();
295
+ if (!address || typeof address === "string") {
296
+ reject(new Error("Failed to start OAuth callback server."));
297
+ return;
298
+ }
299
+ resolve(address.port);
300
+ });
301
+ });
260
302
  }
261
- function requireNonEmpty(value, label) {
262
- const normalized = value?.trim();
263
- if (!normalized) {
264
- throw new Error(`${label} must be non-empty.`);
265
- }
266
- return normalized;
303
+ function closeServer(server) {
304
+ return new Promise((resolve) => {
305
+ server.close(() => resolve());
306
+ });
267
307
  }
268
- function toSecretRef(secretName) {
269
- return `secret://${secretName}`;
308
+ var OAUTH_LOGIN_TIMEOUT_MS = 120000;
309
+ function renderCallbackPage(kind, detail) {
310
+ const title = kind === "success" ? "Signed in" : "Sign-in failed";
311
+ const message = kind === "success" ? "You're signed in. You can close this window and return to mcpx." : detail ?? "Something went wrong. You can close this window and try again in mcpx.";
312
+ const icon = kind === "success" ? "&#10003;" : "&#10007;";
313
+ const closeScript = kind === "success" ? "<script>setTimeout(() => window.close(), 1500);</script>" : "";
314
+ return `<!doctype html>
315
+ <html>
316
+ <head>
317
+ <meta charset="utf-8">
318
+ <title>${title} — mcpx</title>
319
+ <style>
320
+ :root { color-scheme: light dark; }
321
+ body {
322
+ margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
323
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
324
+ background: #f5f5f7; color: #1d1d1f;
325
+ }
326
+ @media (prefers-color-scheme: dark) {
327
+ body { background: #1c1c1e; color: #f5f5f7; }
328
+ .card { background: #2c2c2e !important; box-shadow: none !important; }
329
+ }
330
+ .card {
331
+ background: #fff; border-radius: 16px; padding: 40px 48px; text-align: center;
332
+ box-shadow: 0 8px 30px rgba(0,0,0,0.12); max-width: 360px;
333
+ }
334
+ .icon {
335
+ width: 48px; height: 48px; border-radius: 50%; margin: 0 auto 16px;
336
+ display: flex; align-items: center; justify-content: center; font-size: 22px;
337
+ background: ${kind === "success" ? "#34c759" : "#ff3b30"}; color: #fff;
338
+ }
339
+ h1 { font-size: 17px; margin: 0 0 8px; }
340
+ p { font-size: 14px; margin: 0; opacity: 0.7; }
341
+ </style>
342
+ </head>
343
+ <body>
344
+ <div class="card">
345
+ <div class="icon">${icon}</div>
346
+ <h1>${title}</h1>
347
+ <p>${message}</p>
348
+ </div>
349
+ ${closeScript}
350
+ </body>
351
+ </html>`;
270
352
  }
271
- function secretRefName(ref) {
272
- if (!ref.startsWith("secret://")) {
273
- return null;
353
+
354
+ class OAuthCancelledError extends Error {
355
+ code = "oauth_cancelled";
356
+ constructor(serverName) {
357
+ super(`OAuth login for "${serverName}" was cancelled.`);
358
+ this.name = "OAuthCancelledError";
274
359
  }
275
- const name = ref.slice("secret://".length).trim();
276
- return name.length > 0 ? name : null;
277
360
  }
278
- function defaultAuthSecretName(serverName, target) {
279
- return `auth_${normalizeNamePart(serverName)}_${target.kind}_${normalizeNamePart(target.key)}`;
361
+
362
+ class OAuthLoginInProgressError extends Error {
363
+ code = "oauth_in_progress";
364
+ constructor(serverName, holderPid) {
365
+ super(holderPid ? `OAuth login for "${serverName}" is already in progress (pid ${holderPid}).` : `OAuth login for "${serverName}" is already in progress.`);
366
+ this.name = "OAuthLoginInProgressError";
367
+ }
280
368
  }
281
- function resolveAuthTarget(spec, header, env) {
282
- if (spec.transport === "http") {
283
- if (env) {
284
- throw new Error("--env is only valid for stdio upstreams.");
369
+ function waitForAuthorizationCode(server, serverName, expectedState, options = {}) {
370
+ const timeoutMs = options.timeoutMs ?? OAUTH_LOGIN_TIMEOUT_MS;
371
+ const { signal } = options;
372
+ return new Promise((resolve, reject) => {
373
+ let settled = false;
374
+ function settle(fn) {
375
+ if (settled) {
376
+ return;
377
+ }
378
+ settled = true;
379
+ clearTimeout(timeout);
380
+ signal?.removeEventListener("abort", onAbort);
381
+ fn();
285
382
  }
286
- return {
287
- kind: "header",
288
- key: requireNonEmpty(header ?? "Authorization", "Header name")
289
- };
383
+ function onAbort() {
384
+ settle(() => reject(new OAuthCancelledError(serverName)));
385
+ }
386
+ const timeout = setTimeout(() => {
387
+ settle(() => reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`)));
388
+ }, timeoutMs);
389
+ if (signal) {
390
+ if (signal.aborted) {
391
+ settle(() => reject(new OAuthCancelledError(serverName)));
392
+ return;
393
+ }
394
+ signal.addEventListener("abort", onAbort);
395
+ }
396
+ server.on("request", (request, response) => {
397
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
398
+ if (settled) {
399
+ response.statusCode = 404;
400
+ response.end();
401
+ return;
402
+ }
403
+ if (url.pathname === "/favicon.ico") {
404
+ response.statusCode = 204;
405
+ response.end();
406
+ return;
407
+ }
408
+ if (url.pathname !== "/callback") {
409
+ response.statusCode = 404;
410
+ response.end();
411
+ return;
412
+ }
413
+ const code = url.searchParams.get("code");
414
+ const state = url.searchParams.get("state");
415
+ const error = url.searchParams.get("error");
416
+ response.setHeader("content-type", "text/html; charset=utf-8");
417
+ if (error) {
418
+ response.statusCode = 400;
419
+ response.end(renderCallbackPage("error", `Authorization failed: ${error}`));
420
+ settle(() => reject(new Error(`OAuth authorization failed: ${error}`)));
421
+ return;
422
+ }
423
+ if (!code) {
424
+ response.statusCode = 400;
425
+ response.end(renderCallbackPage("error", "Missing authorization code."));
426
+ return;
427
+ }
428
+ if (state !== expectedState) {
429
+ response.statusCode = 400;
430
+ response.end(renderCallbackPage("error", "This sign-in link is no longer valid."));
431
+ settle(() => reject(new Error("OAuth state mismatch.")));
432
+ return;
433
+ }
434
+ response.end(renderCallbackPage("success"));
435
+ settle(() => resolve(code));
436
+ });
437
+ });
438
+ }
439
+ function bindOAuthReference(config, serverName) {
440
+ const spec = config.servers[serverName];
441
+ if (!spec) {
442
+ throw new Error(`Server "${serverName}" not found.`);
290
443
  }
291
- if (header) {
292
- throw new Error("--header is only valid for HTTP upstreams.");
444
+ if (spec.transport !== "http") {
445
+ throw new Error(`OAuth login only supports HTTP servers.`);
293
446
  }
294
- return {
295
- kind: "env",
296
- key: requireNonEmpty(env, "Env var name")
447
+ spec.headers = {
448
+ ...spec.headers ?? {},
449
+ Authorization: `oauth://${encodeURIComponent(serverName)}`
297
450
  };
298
451
  }
299
- function applyAuthReference(spec, target, secretRef) {
300
- if (target.kind === "header") {
301
- if (spec.transport !== "http") {
302
- throw new Error(`Cannot set header auth on ${spec.transport} upstream.`);
303
- }
304
- const headers = { ...spec.headers ?? {} };
305
- headers[target.key] = secretRef;
306
- spec.headers = headers;
307
- return;
308
- }
309
- if (spec.transport !== "stdio") {
310
- throw new Error(`Cannot set env auth on ${spec.transport} upstream.`);
452
+ var OAUTH_LOCK_STALE_MS = 5 * 60000;
453
+ function isProcessAlive(pid) {
454
+ try {
455
+ process.kill(pid, 0);
456
+ return true;
457
+ } catch {
458
+ return false;
311
459
  }
312
- const env = { ...spec.env ?? {} };
313
- env[target.key] = secretRef;
314
- spec.env = env;
315
460
  }
316
- function removeAuthReference(spec, target) {
317
- if (target.kind === "header") {
318
- if (spec.transport !== "http") {
319
- throw new Error(`Cannot remove header auth from ${spec.transport} upstream.`);
320
- }
321
- if (!spec.headers || !(target.key in spec.headers)) {
322
- return null;
461
+ function releaseOAuthLoginLock(lockPath) {
462
+ try {
463
+ fs.unlinkSync(lockPath);
464
+ } catch {}
465
+ }
466
+ function acquireOAuthLoginLock(serverName) {
467
+ const lockPath = getOAuthLockPath(serverName);
468
+ ensureDir(path.dirname(lockPath));
469
+ const tryCreate = () => {
470
+ try {
471
+ fs.writeFileSync(lockPath, `${process.pid}
472
+ `, { flag: "wx" });
473
+ return true;
474
+ } catch (err) {
475
+ if (err.code !== "EEXIST") {
476
+ throw err;
477
+ }
478
+ return false;
323
479
  }
324
- const previous2 = spec.headers[target.key] ?? null;
325
- const next2 = { ...spec.headers };
326
- delete next2[target.key];
327
- spec.headers = Object.keys(next2).length > 0 ? next2 : undefined;
328
- return previous2;
329
- }
330
- if (spec.transport !== "stdio") {
331
- throw new Error(`Cannot remove env auth from ${spec.transport} upstream.`);
480
+ };
481
+ if (tryCreate()) {
482
+ return () => releaseOAuthLoginLock(lockPath);
332
483
  }
333
- if (!spec.env || !(target.key in spec.env)) {
334
- return null;
484
+ let holderPid;
485
+ try {
486
+ const raw = fs.readFileSync(lockPath, "utf8").trim();
487
+ const pid = Number(raw);
488
+ const stat = fs.statSync(lockPath);
489
+ const stale = Date.now() - stat.mtimeMs > OAUTH_LOCK_STALE_MS;
490
+ const holderAlive = Number.isFinite(pid) && pid > 0 && isProcessAlive(pid);
491
+ if (!holderAlive || stale) {
492
+ fs.unlinkSync(lockPath);
493
+ if (tryCreate()) {
494
+ return () => releaseOAuthLoginLock(lockPath);
495
+ }
496
+ }
497
+ holderPid = holderAlive ? pid : undefined;
498
+ } catch {
499
+ if (tryCreate()) {
500
+ return () => releaseOAuthLoginLock(lockPath);
501
+ }
335
502
  }
336
- const previous = spec.env[target.key] ?? null;
337
- const next = { ...spec.env };
338
- delete next[target.key];
339
- spec.env = Object.keys(next).length > 0 ? next : undefined;
340
- return previous;
503
+ throw new OAuthLoginInProgressError(serverName, holderPid);
341
504
  }
342
- function listAuthBindings(spec) {
343
- if (spec.transport === "http") {
344
- return Object.entries(spec.headers ?? {}).map(([key, value]) => ({
345
- kind: "header",
346
- key,
347
- value
348
- }));
505
+ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver, options = {}) {
506
+ const { signal, onProgress } = options;
507
+ const timeoutMs = options.timeoutMs ?? OAUTH_LOGIN_TIMEOUT_MS;
508
+ if (signal?.aborted) {
509
+ throw new OAuthCancelledError(serverName);
349
510
  }
350
- return Object.entries(spec.env ?? {}).map(([key, value]) => ({
351
- kind: "env",
352
- key,
353
- value
354
- }));
355
- }
356
- var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
357
- function maybePrefixBearer(target, value, raw = false) {
358
- if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
359
- return value;
511
+ const releaseLock = acquireOAuthLoginLock(serverName);
512
+ try {
513
+ const oauthName = oauthSecretName(serverName, "tokens");
514
+ const clientName = oauthSecretName(serverName, "client");
515
+ const backupTokens = secrets.getSecret(oauthName);
516
+ const backupClient = secrets.getSecret(clientName);
517
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
518
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
519
+ const state = crypto.randomUUID();
520
+ let callbackServer;
521
+ let redirectUrl;
522
+ let codePromise;
523
+ if (codeReceiver) {
524
+ redirectUrl = codeReceiver.redirectUrl;
525
+ codePromise = codeReceiver.waitForCode(state, timeoutMs);
526
+ } else {
527
+ callbackServer = http.createServer();
528
+ const port = await listen(callbackServer);
529
+ redirectUrl = `http://127.0.0.1:${port}/callback`;
530
+ codePromise = waitForAuthorizationCode(callbackServer, serverName, state, { timeoutMs, signal });
531
+ }
532
+ codePromise.catch(() => {});
533
+ const wrappedOpenUrl = async (url) => {
534
+ onProgress?.({ phase: "awaiting-browser", authorizationUrl: url, expiresAt: Date.now() + timeoutMs });
535
+ await openUrl(url);
536
+ };
537
+ const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, wrappedOpenUrl);
538
+ try {
539
+ onProgress?.({ phase: "discovering" });
540
+ const initial = await auth(provider, {
541
+ serverUrl: spec.url
542
+ });
543
+ if (initial !== "REDIRECT") {
544
+ throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
545
+ }
546
+ if (signal?.aborted) {
547
+ throw new OAuthCancelledError(serverName);
548
+ }
549
+ const authorizationCode = await codePromise;
550
+ onProgress?.({ phase: "exchanging" });
551
+ const result = await auth(provider, {
552
+ serverUrl: spec.url,
553
+ authorizationCode
554
+ });
555
+ if (result !== "AUTHORIZED") {
556
+ throw new Error(`OAuth login did not authorize "${serverName}".`);
557
+ }
558
+ onProgress?.({ phase: "syncing" });
559
+ await mutateConfig((config2) => {
560
+ bindOAuthReference(config2, serverName);
561
+ }, configPath);
562
+ const config = loadConfig(configPath);
563
+ const summary = syncAllClients(config, secrets);
564
+ await mutateConfig((freshConfig) => {
565
+ persistSyncState(summary, freshConfig);
566
+ }, configPath);
567
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
568
+ return { serverName, authorized: true };
569
+ } catch (error) {
570
+ if (backupTokens)
571
+ secrets.setSecret(oauthName, backupTokens);
572
+ if (backupClient)
573
+ secrets.setSecret(clientName, backupClient);
574
+ new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
575
+ throw error;
576
+ } finally {
577
+ if (callbackServer) {
578
+ await closeServer(callbackServer);
579
+ }
580
+ }
581
+ } finally {
582
+ releaseLock();
360
583
  }
361
- if (/^\S+\s+\S+/.test(value)) {
362
- return value;
584
+ }
585
+ async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
586
+ const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
587
+ if (!stored) {
588
+ throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
363
589
  }
364
- if (TOKEN68.test(value)) {
365
- return `Bearer ${value}`;
590
+ if (options.forceRefresh || tokensAreExpiring(stored)) {
591
+ if (!stored.tokens.refresh_token) {
592
+ if (!options.forceRefresh) {
593
+ return stored.tokens.access_token;
594
+ }
595
+ throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
596
+ }
597
+ const provider = new McpxOAuthProvider(serverName, secrets);
598
+ const clientInformation = provider.clientInformation();
599
+ if (!clientInformation) {
600
+ throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
601
+ }
602
+ let serverInfo;
603
+ try {
604
+ const cachedDiscovery = provider.discoveryState();
605
+ serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
606
+ } catch (err) {
607
+ const message = err instanceof Error ? err.message : String(err);
608
+ if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
609
+ throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
610
+ }
611
+ throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
612
+ }
613
+ try {
614
+ const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
615
+ metadata: serverInfo.authorizationServerMetadata,
616
+ clientInformation,
617
+ refreshToken: stored.tokens.refresh_token,
618
+ resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
619
+ });
620
+ provider.saveTokens(refreshed);
621
+ return refreshed.access_token;
622
+ } catch (err) {
623
+ const message = err instanceof Error ? err.message : String(err);
624
+ if (err instanceof InvalidGrantError || err instanceof InvalidClientError || /invalid_grant|invalid_client|4\d{2}/i.test(message)) {
625
+ throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
626
+ }
627
+ if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
628
+ throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
629
+ }
630
+ throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
631
+ }
366
632
  }
367
- return value;
633
+ return stored.tokens.access_token;
368
634
  }
369
-
370
- // src/core/spawn-env.ts
371
- import os from "node:os";
372
- import path from "node:path";
373
- import fs from "node:fs";
374
- function existingNvmCurrentBin() {
375
- const nvmDir = process.env.NVM_DIR ?? path.join(os.homedir(), ".nvm");
376
- const current = path.join(nvmDir, "current", "bin");
377
- return fs.existsSync(current) ? current : null;
635
+ function oauthSecretNames(serverName) {
636
+ return {
637
+ client: oauthSecretName(serverName, "client"),
638
+ tokens: oauthSecretName(serverName, "tokens"),
639
+ verifier: oauthSecretName(serverName, "verifier"),
640
+ discovery: oauthSecretName(serverName, "discovery")
641
+ };
378
642
  }
379
- function buildEnrichedPath(current = process.env.PATH ?? "") {
643
+ function clearOAuthCredentials(serverName, secrets) {
644
+ const removed = [];
380
645
  const candidates = [
381
- ...current.split(":"),
382
- "/opt/homebrew/bin",
383
- "/usr/local/bin",
384
- path.join(os.homedir(), ".bun", "bin"),
385
- path.join(os.homedir(), ".local", "bin"),
386
- existingNvmCurrentBin()
387
- ].filter((entry) => Boolean(entry));
388
- const seen = new Set;
389
- const entries = [];
390
- for (const candidate of candidates) {
391
- if (!candidate || seen.has(candidate)) {
392
- continue;
393
- }
394
- seen.add(candidate);
395
- entries.push(candidate);
646
+ ...Object.values(oauthSecretNames(serverName)),
647
+ ...Object.values({
648
+ client: legacyOAuthSecretName(serverName, "client"),
649
+ tokens: legacyOAuthSecretName(serverName, "tokens"),
650
+ verifier: legacyOAuthSecretName(serverName, "verifier"),
651
+ discovery: legacyOAuthSecretName(serverName, "discovery")
652
+ })
653
+ ];
654
+ for (const name of candidates) {
655
+ if (secrets.getSecret(name) !== null) {
656
+ secrets.removeSecret(name);
657
+ removed.push(name);
658
+ }
659
+ }
660
+ return removed;
661
+ }
662
+ function getOAuthCredentialStatus(serverName, spec, secrets) {
663
+ const bound = spec.transport === "http" && Object.values(spec.headers ?? {}).some((value) => isOAuthReference(value) && oauthReferenceServerName(value) === serverName);
664
+ const clientInfo = readJsonSecret(secrets, oauthSecretName(serverName, "client"));
665
+ const clientRegistered = clientInfo != null;
666
+ const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
667
+ if (!stored) {
668
+ return { bound, signedIn: false, expired: false, hasRefreshToken: false, clientRegistered };
396
669
  }
397
- return entries.join(":");
670
+ const expiresAt = stored.tokens.expires_in ? stored.obtainedAt + stored.tokens.expires_in * 1000 : undefined;
671
+ const expired = expiresAt != null && Date.now() >= expiresAt;
672
+ return {
673
+ bound,
674
+ signedIn: true,
675
+ obtainedAt: stored.obtainedAt,
676
+ expiresAt,
677
+ expired,
678
+ hasRefreshToken: Boolean(stored.tokens.refresh_token),
679
+ clientRegistered
680
+ };
398
681
  }
399
682
 
400
- // src/core/oauth.ts
401
- import http from "node:http";
402
- import crypto from "node:crypto";
403
- import {
404
- auth,
405
- discoverOAuthServerInfo,
406
- refreshAuthorization
407
- } from "@modelcontextprotocol/sdk/client/auth.js";
408
- import { InvalidClientError, InvalidGrantError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
409
- function oauthSecretName(serverName, suffix) {
410
- return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
411
- }
412
- function readJsonSecret(secrets, name) {
413
- const raw = secrets.getSecret(name);
414
- if (!raw) {
415
- return;
416
- }
417
- try {
418
- return JSON.parse(raw);
419
- } catch {
420
- return;
421
- }
422
- }
423
- function writeJsonSecret(secrets, name, value) {
424
- secrets.setSecret(name, JSON.stringify(value));
425
- }
426
- function isOAuthReference(value) {
427
- return value.startsWith("oauth://");
683
+ // src/core/auth-probe.ts
684
+ var JSON_RPC_VERSION = "2.0";
685
+ var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
686
+ var AUTH_KEYWORDS = [
687
+ "unauthorized",
688
+ "unauthenticated",
689
+ "forbidden",
690
+ "authentication",
691
+ "authorization",
692
+ "api key",
693
+ "api_key",
694
+ "apikey",
695
+ "credentials",
696
+ "access denied",
697
+ "auth required",
698
+ "x-api-key",
699
+ "invalid key",
700
+ "missing header",
701
+ "not authenticated",
702
+ "auth token"
703
+ ];
704
+ function isAuthRelatedMessage(message) {
705
+ const lower = message.toLowerCase();
706
+ return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
428
707
  }
429
- function oauthReferenceServerName(value) {
430
- if (!isOAuthReference(value)) {
431
- throw new Error(`Not an OAuth reference: ${value}`);
708
+ function resolveHeaders(spec, secrets) {
709
+ const headers = {
710
+ "content-type": "application/json",
711
+ accept: "application/json, text/event-stream"
712
+ };
713
+ for (const [key, value] of Object.entries(spec.headers ?? {})) {
714
+ headers[key] = secrets.resolveMaybeSecret(value);
432
715
  }
433
- return decodeURIComponent(value.slice("oauth://".length));
716
+ return headers;
434
717
  }
435
- function tokensAreExpiring(tokens) {
436
- if (!tokens.tokens.expires_in) {
718
+ function isOAuthLikelyChallenge(wwwAuthenticate) {
719
+ if (!wwwAuthenticate) {
437
720
  return false;
438
721
  }
439
- const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
440
- return Date.now() >= expiresAt - 60000;
722
+ const lower = wwwAuthenticate.toLowerCase();
723
+ return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
441
724
  }
442
-
443
- class McpxOAuthProvider {
444
- #serverName;
445
- #secrets;
446
- #redirectUrlValue;
447
- #stateValue;
448
- constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
449
- this.#serverName = serverName;
450
- this.#secrets = secrets;
451
- this.#redirectUrlValue = redirectUrl;
452
- this.#stateValue = state;
453
- }
454
- get redirectUrl() {
455
- return this.#redirectUrlValue;
456
- }
457
- get clientMetadata() {
458
- if (!this.#redirectUrlValue) {
459
- return {
460
- redirect_uris: [],
461
- token_endpoint_auth_method: "none",
462
- grant_types: ["authorization_code", "refresh_token"],
463
- response_types: ["code"],
464
- client_name: "mcpx"
465
- };
725
+ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS, options = {}) {
726
+ const timeoutController = new AbortController;
727
+ const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
728
+ const discoverOAuth = options.discoverOAuth ?? true;
729
+ async function withOAuthSupport(result) {
730
+ if (!discoverOAuth) {
731
+ return result;
466
732
  }
733
+ const support = await probeOAuthSupport(spec.url).catch(() => {
734
+ return;
735
+ });
467
736
  return {
468
- redirect_uris: [this.#redirectUrlValue],
469
- token_endpoint_auth_method: "none",
470
- grant_types: ["authorization_code", "refresh_token"],
471
- response_types: ["code"],
472
- client_name: "mcpx"
737
+ ...result,
738
+ oauthSupport: support?.support,
739
+ oauthLikely: result.oauthLikely || support?.support === "supported"
473
740
  };
474
741
  }
475
- state() {
476
- return this.#stateValue;
477
- }
478
- clientInformation() {
479
- return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
480
- }
481
- saveClientInformation(clientInformation) {
482
- writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
483
- }
484
- tokens() {
485
- return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
486
- }
487
- saveTokens(tokens) {
488
- writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
489
- tokens,
490
- obtainedAt: Date.now()
742
+ try {
743
+ const headers = resolveHeaders(spec, secrets);
744
+ const response = await fetch(spec.url, {
745
+ method: "POST",
746
+ headers,
747
+ body: JSON.stringify({
748
+ jsonrpc: JSON_RPC_VERSION,
749
+ id: "mcpx-auth-probe",
750
+ method: "tools/list",
751
+ params: {}
752
+ }),
753
+ signal: timeoutController.signal
491
754
  });
492
- }
493
- redirectToAuthorization(_authorizationUrl) {}
494
- saveCodeVerifier(codeVerifier) {
495
- this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
496
- }
497
- codeVerifier() {
498
- const verifier = this.#secrets.getSecret(oauthSecretName(this.#serverName, "verifier"));
499
- if (!verifier) {
500
- throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
755
+ if (response.status === 401 || response.status === 403) {
756
+ const wwwAuthenticate = response.headers.get("www-authenticate");
757
+ return await withOAuthSupport({
758
+ authRequired: true,
759
+ oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
760
+ status: response.status,
761
+ wwwAuthenticate: wwwAuthenticate ?? undefined
762
+ });
501
763
  }
502
- return verifier;
503
- }
504
- saveDiscoveryState(state) {
505
- writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
506
- }
507
- discoveryState() {
508
- return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
509
- }
510
- invalidateCredentials(scope) {
511
- const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
512
- for (const suffix of suffixes) {
513
- this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
764
+ if (response.status === 200) {
765
+ try {
766
+ const body = await response.json();
767
+ const err = body?.error;
768
+ if (err && typeof err === "object" && !("result" in body)) {
769
+ const message = typeof err.message === "string" ? err.message : "";
770
+ if (message && isAuthRelatedMessage(message)) {
771
+ return await withOAuthSupport({
772
+ authRequired: true,
773
+ status: response.status,
774
+ error: `JSON-RPC error: ${message}`
775
+ });
776
+ }
777
+ }
778
+ } catch {}
514
779
  }
780
+ return {
781
+ authRequired: false,
782
+ status: response.status
783
+ };
784
+ } catch (error) {
785
+ const isAbort = error.name === "AbortError";
786
+ return {
787
+ authRequired: false,
788
+ error: isAbort ? `Auth probe timed out after ${timeoutMs}ms.` : error.message
789
+ };
790
+ } finally {
791
+ clearTimeout(timeoutHandle);
515
792
  }
516
793
  }
517
794
 
518
- class InteractiveOAuthProvider extends McpxOAuthProvider {
519
- #openUrl;
520
- #cachedClientInfo;
521
- #cachedCodeVerifier;
522
- constructor(serverName, secrets, redirectUrl, state, openUrl) {
523
- super(serverName, secrets, redirectUrl, state);
524
- this.#openUrl = openUrl;
525
- }
526
- async redirectToAuthorization(authorizationUrl) {
527
- await this.#openUrl(authorizationUrl.toString());
795
+ // src/core/registry-client.ts
796
+ var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
797
+ async function fetchRegistryServerDetail(name) {
798
+ const encoded = encodeURIComponent(name);
799
+ const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
800
+ headers: { accept: "application/json" }
801
+ });
802
+ if (!response.ok) {
803
+ throw new Error(`Registry API error: ${response.status}`);
528
804
  }
529
- clientInformation() {
530
- this.#cachedClientInfo ??= super.clientInformation();
531
- return this.#cachedClientInfo;
805
+ const data = await response.json();
806
+ return data.server;
807
+ }
808
+ var PACKAGE_PRIORITY = {
809
+ npm: 1,
810
+ pypi: 2,
811
+ nuget: 3,
812
+ oci: 4,
813
+ mcpb: 5
814
+ };
815
+ var RUNTIME_HINT = {
816
+ npm: "npx",
817
+ pypi: "uvx",
818
+ nuget: "dnx"
819
+ };
820
+ function selectBestPackage(packages = [], remotes = []) {
821
+ const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
822
+ if (stdioPkgs.length > 0) {
823
+ return { kind: "package", package: stdioPkgs[0] };
532
824
  }
533
- saveClientInformation(clientInformation) {
534
- super.saveClientInformation(clientInformation);
535
- this.#cachedClientInfo = clientInformation;
825
+ const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
826
+ if (httpRemotes.length > 0) {
827
+ return { kind: "remote", remote: httpRemotes[0] };
536
828
  }
537
- codeVerifier() {
538
- if (this.#cachedCodeVerifier !== undefined) {
539
- return this.#cachedCodeVerifier;
540
- }
541
- return super.codeVerifier();
829
+ if (packages.length > 0) {
830
+ return { kind: "package", package: packages[0] };
542
831
  }
543
- saveCodeVerifier(codeVerifier) {
544
- super.saveCodeVerifier(codeVerifier);
545
- this.#cachedCodeVerifier = codeVerifier;
832
+ if (remotes.length > 0) {
833
+ return { kind: "remote", remote: remotes[0] };
546
834
  }
547
- invalidateCredentials(scope) {
548
- super.invalidateCredentials(scope);
549
- if (scope === "all" || scope === "client") {
550
- this.#cachedClientInfo = undefined;
835
+ throw new Error("Server has no packages or remotes");
836
+ }
837
+ function extractRequiredInputs(option) {
838
+ const inputs = [];
839
+ if (option.kind === "package" && option.package?.environmentVariables) {
840
+ for (const env of option.package.environmentVariables) {
841
+ if (env.isRequired && !env.default) {
842
+ inputs.push({
843
+ name: env.name,
844
+ description: env.description,
845
+ isSecret: env.isSecret ?? false,
846
+ kind: "env"
847
+ });
848
+ }
849
+ }
850
+ for (const arg of option.package.packageArguments ?? []) {
851
+ if (arg.isRequired && !arg.value && !arg.default) {
852
+ inputs.push({
853
+ name: arg.name ?? arg.valueHint ?? "arg",
854
+ description: arg.description,
855
+ isSecret: false,
856
+ kind: "arg"
857
+ });
858
+ }
551
859
  }
552
860
  }
553
- }
554
- function listen(server) {
555
- return new Promise((resolve, reject) => {
556
- server.once("error", reject);
557
- server.listen(0, "127.0.0.1", () => {
558
- const address = server.address();
559
- if (!address || typeof address === "string") {
560
- reject(new Error("Failed to start OAuth callback server."));
561
- return;
861
+ if (option.kind === "remote" && option.remote?.headers) {
862
+ for (const header of option.remote.headers) {
863
+ if (header.isRequired && !header.default) {
864
+ inputs.push({
865
+ name: header.name,
866
+ description: header.description,
867
+ isSecret: header.isSecret ?? false,
868
+ kind: "header"
869
+ });
562
870
  }
563
- resolve(address.port);
564
- });
565
- });
871
+ }
872
+ }
873
+ return inputs;
566
874
  }
567
- function closeServer(server) {
568
- return new Promise((resolve) => {
569
- server.close(() => resolve());
570
- });
875
+ function mapRegistryToSpec(_name, option, resolvedValues) {
876
+ if (option.kind === "remote") {
877
+ const headers = {};
878
+ for (const header of option.remote?.headers ?? []) {
879
+ const value = resolvedValues[header.name] ?? header.default;
880
+ if (value)
881
+ headers[header.name] = value;
882
+ }
883
+ return {
884
+ transport: "http",
885
+ url: option.remote.url,
886
+ ...Object.keys(headers).length > 0 ? { headers } : {}
887
+ };
888
+ }
889
+ const pkg = option.package;
890
+ const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
891
+ const args = [];
892
+ const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
893
+ args.push(identifier);
894
+ for (const arg of pkg.packageArguments ?? []) {
895
+ const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
896
+ if (!value)
897
+ continue;
898
+ if (arg.type === "named" && arg.name) {
899
+ args.push(arg.name, value);
900
+ } else {
901
+ args.push(value);
902
+ }
903
+ }
904
+ const env = {};
905
+ for (const envVar of pkg.environmentVariables ?? []) {
906
+ const value = resolvedValues[envVar.name] ?? envVar.default;
907
+ if (value)
908
+ env[envVar.name] = value;
909
+ }
910
+ return {
911
+ transport: "stdio",
912
+ command: runtime,
913
+ args,
914
+ ...Object.keys(env).length > 0 ? { env } : {}
915
+ };
571
916
  }
572
- function waitForAuthorizationCode(server, expectedState, timeoutMs = 120000) {
573
- return new Promise((resolve, reject) => {
574
- const timeout = setTimeout(() => {
575
- reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
576
- }, timeoutMs);
577
- server.on("request", (request, response) => {
578
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
579
- const code = url.searchParams.get("code");
580
- const state = url.searchParams.get("state");
581
- const error = url.searchParams.get("error");
582
- response.setHeader("content-type", "text/html; charset=utf-8");
583
- if (error) {
584
- response.statusCode = 400;
585
- response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
586
- clearTimeout(timeout);
587
- reject(new Error(`OAuth authorization failed: ${error}`));
588
- return;
589
- }
590
- if (!code) {
591
- response.statusCode = 400;
592
- response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
593
- return;
594
- }
595
- if (state !== expectedState) {
596
- response.statusCode = 400;
597
- response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
598
- clearTimeout(timeout);
599
- reject(new Error("OAuth state mismatch."));
600
- return;
601
- }
602
- response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
603
- clearTimeout(timeout);
604
- resolve(code);
605
- });
606
- });
917
+
918
+ // src/core/server-auth.ts
919
+ function normalizeNamePart(value) {
920
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
607
921
  }
608
- function bindOAuthReference(config, serverName) {
609
- const spec = config.servers[serverName];
610
- if (!spec) {
611
- throw new Error(`Server "${serverName}" not found.`);
922
+ function requireNonEmpty(value, label) {
923
+ const normalized = value?.trim();
924
+ if (!normalized) {
925
+ throw new Error(`${label} must be non-empty.`);
612
926
  }
613
- if (spec.transport !== "http") {
614
- throw new Error(`OAuth login only supports HTTP servers.`);
927
+ return normalized;
928
+ }
929
+ function toSecretRef(secretName) {
930
+ return `secret://${secretName}`;
931
+ }
932
+ function secretRefName(ref) {
933
+ if (!ref.startsWith("secret://")) {
934
+ return null;
615
935
  }
616
- spec.headers = {
617
- ...spec.headers ?? {},
618
- Authorization: `oauth://${encodeURIComponent(serverName)}`
936
+ const name = ref.slice("secret://".length).trim();
937
+ return name.length > 0 ? name : null;
938
+ }
939
+ function defaultAuthSecretName(serverName, target) {
940
+ return `auth_${normalizeNamePart(serverName)}_${target.kind}_${normalizeNamePart(target.key)}`;
941
+ }
942
+ function resolveAuthTarget(spec, header, env) {
943
+ if (spec.transport === "http") {
944
+ if (env) {
945
+ throw new Error("--env is only valid for stdio upstreams.");
946
+ }
947
+ return {
948
+ kind: "header",
949
+ key: requireNonEmpty(header ?? "Authorization", "Header name")
950
+ };
951
+ }
952
+ if (header) {
953
+ throw new Error("--header is only valid for HTTP upstreams.");
954
+ }
955
+ return {
956
+ kind: "env",
957
+ key: requireNonEmpty(env, "Env var name")
619
958
  };
620
959
  }
621
- async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver) {
622
- const oauthName = oauthSecretName(serverName, "tokens");
623
- const clientName = oauthSecretName(serverName, "client");
624
- const backupTokens = secrets.getSecret(oauthName);
625
- const backupClient = secrets.getSecret(clientName);
626
- new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
627
- new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
628
- const state = crypto.randomUUID();
629
- let callbackServer;
630
- let redirectUrl;
631
- let codePromise;
632
- if (codeReceiver) {
633
- redirectUrl = codeReceiver.redirectUrl;
634
- codePromise = codeReceiver.waitForCode(state);
635
- } else {
636
- callbackServer = http.createServer();
637
- const port = await listen(callbackServer);
638
- redirectUrl = `http://127.0.0.1:${port}/callback`;
639
- codePromise = waitForAuthorizationCode(callbackServer, state);
960
+ function applyAuthReference(spec, target, secretRef) {
961
+ if (target.kind === "header") {
962
+ if (spec.transport !== "http") {
963
+ throw new Error(`Cannot set header auth on ${spec.transport} upstream.`);
964
+ }
965
+ const headers = { ...spec.headers ?? {} };
966
+ headers[target.key] = secretRef;
967
+ spec.headers = headers;
968
+ return;
640
969
  }
641
- codePromise.catch(() => {});
642
- const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, openUrl);
643
- try {
644
- const initial = await auth(provider, {
645
- serverUrl: spec.url
646
- });
647
- if (initial !== "REDIRECT") {
648
- throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
970
+ if (spec.transport !== "stdio") {
971
+ throw new Error(`Cannot set env auth on ${spec.transport} upstream.`);
972
+ }
973
+ const env = { ...spec.env ?? {} };
974
+ env[target.key] = secretRef;
975
+ spec.env = env;
976
+ }
977
+ function removeAuthReference(spec, target) {
978
+ if (target.kind === "header") {
979
+ if (spec.transport !== "http") {
980
+ throw new Error(`Cannot remove header auth from ${spec.transport} upstream.`);
649
981
  }
650
- const authorizationCode = await codePromise;
651
- const result = await auth(provider, {
652
- serverUrl: spec.url,
653
- authorizationCode
654
- });
655
- if (result !== "AUTHORIZED") {
656
- throw new Error(`OAuth login did not authorize "${serverName}".`);
657
- }
658
- const config = loadConfig(configPath);
659
- bindOAuthReference(config, serverName);
660
- saveConfig(config, configPath);
661
- const summary = syncAllClients(config, secrets);
662
- persistSyncState(summary, config);
663
- saveConfig(config, configPath);
664
- new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
665
- return { serverName, authorized: true };
666
- } catch (error) {
667
- if (backupTokens)
668
- secrets.setSecret(oauthName, backupTokens);
669
- if (backupClient)
670
- secrets.setSecret(clientName, backupClient);
671
- new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
672
- throw error;
673
- } finally {
674
- if (callbackServer) {
675
- await closeServer(callbackServer);
982
+ if (!spec.headers || !(target.key in spec.headers)) {
983
+ return null;
676
984
  }
985
+ const previous2 = spec.headers[target.key] ?? null;
986
+ const next2 = { ...spec.headers };
987
+ delete next2[target.key];
988
+ spec.headers = Object.keys(next2).length > 0 ? next2 : undefined;
989
+ return previous2;
990
+ }
991
+ if (spec.transport !== "stdio") {
992
+ throw new Error(`Cannot remove env auth from ${spec.transport} upstream.`);
677
993
  }
994
+ if (!spec.env || !(target.key in spec.env)) {
995
+ return null;
996
+ }
997
+ const previous = spec.env[target.key] ?? null;
998
+ const next = { ...spec.env };
999
+ delete next[target.key];
1000
+ spec.env = Object.keys(next).length > 0 ? next : undefined;
1001
+ return previous;
678
1002
  }
679
- async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
680
- const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
681
- if (!stored) {
682
- throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
1003
+ function listAuthBindings(spec) {
1004
+ if (spec.transport === "http") {
1005
+ return Object.entries(spec.headers ?? {}).map(([key, value]) => ({
1006
+ kind: "header",
1007
+ key,
1008
+ value
1009
+ }));
683
1010
  }
684
- if (options.forceRefresh || tokensAreExpiring(stored)) {
685
- if (!stored.tokens.refresh_token) {
686
- if (!options.forceRefresh) {
687
- return stored.tokens.access_token;
688
- }
689
- throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
690
- }
691
- const provider = new McpxOAuthProvider(serverName, secrets);
692
- const clientInformation = provider.clientInformation();
693
- if (!clientInformation) {
694
- throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
695
- }
696
- let serverInfo;
697
- try {
698
- const cachedDiscovery = provider.discoveryState();
699
- serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
700
- } catch (err) {
701
- const message = err instanceof Error ? err.message : String(err);
702
- if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
703
- throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
704
- }
705
- throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
706
- }
707
- try {
708
- const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
709
- metadata: serverInfo.authorizationServerMetadata,
710
- clientInformation,
711
- refreshToken: stored.tokens.refresh_token,
712
- resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
713
- });
714
- provider.saveTokens(refreshed);
715
- return refreshed.access_token;
716
- } catch (err) {
717
- const message = err instanceof Error ? err.message : String(err);
718
- if (err instanceof InvalidGrantError || err instanceof InvalidClientError || /invalid_grant|invalid_client|4\d{2}/i.test(message)) {
719
- throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
720
- }
721
- if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
722
- throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
723
- }
724
- throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
1011
+ return Object.entries(spec.env ?? {}).map(([key, value]) => ({
1012
+ kind: "env",
1013
+ key,
1014
+ value
1015
+ }));
1016
+ }
1017
+ var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
1018
+ function maybePrefixBearer(target, value, raw = false) {
1019
+ if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
1020
+ return value;
1021
+ }
1022
+ if (/^\S+\s+\S+/.test(value)) {
1023
+ return value;
1024
+ }
1025
+ if (TOKEN68.test(value)) {
1026
+ return `Bearer ${value}`;
1027
+ }
1028
+ return value;
1029
+ }
1030
+
1031
+ // src/core/spawn-env.ts
1032
+ import os from "node:os";
1033
+ import path2 from "node:path";
1034
+ import fs2 from "node:fs";
1035
+ function existingNvmCurrentBin() {
1036
+ const nvmDir = process.env.NVM_DIR ?? path2.join(os.homedir(), ".nvm");
1037
+ const current = path2.join(nvmDir, "current", "bin");
1038
+ return fs2.existsSync(current) ? current : null;
1039
+ }
1040
+ function buildEnrichedPath(current = process.env.PATH ?? "") {
1041
+ const candidates = [
1042
+ ...current.split(":"),
1043
+ "/opt/homebrew/bin",
1044
+ "/usr/local/bin",
1045
+ path2.join(os.homedir(), ".bun", "bin"),
1046
+ path2.join(os.homedir(), ".local", "bin"),
1047
+ existingNvmCurrentBin()
1048
+ ].filter((entry) => Boolean(entry));
1049
+ const seen = new Set;
1050
+ const entries = [];
1051
+ for (const candidate of candidates) {
1052
+ if (!candidate || seen.has(candidate)) {
1053
+ continue;
725
1054
  }
1055
+ seen.add(candidate);
1056
+ entries.push(candidate);
726
1057
  }
727
- return stored.tokens.access_token;
1058
+ return entries.join(":");
728
1059
  }
729
1060
 
730
1061
  // src/core/update.ts
731
- import fs2 from "node:fs";
732
- import path2 from "node:path";
1062
+ import fs3 from "node:fs";
1063
+ import path3 from "node:path";
733
1064
  import { execFileSync } from "node:child_process";
734
1065
  function compareVersions(a, b) {
735
1066
  const partsA = a.split(".").map(Number);
@@ -768,17 +1099,17 @@ async function checkForUpdates() {
768
1099
  }
769
1100
  function getStagedUpdate() {
770
1101
  const stagedPath = getStagedVersionPath();
771
- if (!fs2.existsSync(stagedPath)) {
1102
+ if (!fs3.existsSync(stagedPath)) {
772
1103
  return null;
773
1104
  }
774
1105
  try {
775
- const raw = fs2.readFileSync(stagedPath, "utf8");
1106
+ const raw = fs3.readFileSync(stagedPath, "utf8");
776
1107
  const info = JSON.parse(raw);
777
1108
  if (!info.version || !info.cliPath) {
778
1109
  return null;
779
1110
  }
780
- const absoluteCliPath = path2.isAbsolute(info.cliPath) ? info.cliPath : path2.join(getUpdatesDir(), info.cliPath);
781
- if (!fs2.existsSync(absoluteCliPath)) {
1111
+ const absoluteCliPath = path3.isAbsolute(info.cliPath) ? info.cliPath : path3.join(getUpdatesDir(), info.cliPath);
1112
+ if (!fs3.existsSync(absoluteCliPath)) {
782
1113
  return null;
783
1114
  }
784
1115
  return {
@@ -794,18 +1125,18 @@ function stageUpdate(version, cliPath) {
794
1125
  ensureDir(updatesDir);
795
1126
  const stagedInfo = {
796
1127
  version,
797
- cliPath: path2.isAbsolute(cliPath) ? path2.relative(updatesDir, cliPath) : cliPath,
1128
+ cliPath: path3.isAbsolute(cliPath) ? path3.relative(updatesDir, cliPath) : cliPath,
798
1129
  stagedAt: new Date().toISOString()
799
1130
  };
800
- fs2.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
1131
+ fs3.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
801
1132
  }
802
1133
  function shouldUseStagedCli(stagedVersion, currentVersion) {
803
1134
  return compareVersions(stagedVersion, currentVersion) > 0;
804
1135
  }
805
1136
  function clearStagedUpdate() {
806
1137
  const stagedPath = getStagedVersionPath();
807
- if (fs2.existsSync(stagedPath)) {
808
- fs2.unlinkSync(stagedPath);
1138
+ if (fs3.existsSync(stagedPath)) {
1139
+ fs3.unlinkSync(stagedPath);
809
1140
  }
810
1141
  }
811
1142
  function getStagedCliPath() {
@@ -814,31 +1145,33 @@ function getStagedCliPath() {
814
1145
  }
815
1146
  function removeOldVersions(keepCount = 2) {
816
1147
  const updatesDir = getUpdatesDir();
817
- if (!fs2.existsSync(updatesDir)) {
1148
+ if (!fs3.existsSync(updatesDir)) {
818
1149
  return;
819
1150
  }
820
- const entries = fs2.readdirSync(updatesDir, { withFileTypes: true });
1151
+ const entries = fs3.readdirSync(updatesDir, { withFileTypes: true });
821
1152
  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)));
822
1153
  if (versionDirs.length <= keepCount) {
823
1154
  return;
824
1155
  }
825
1156
  for (const oldVersion of versionDirs.slice(keepCount)) {
826
- const oldPath = path2.join(updatesDir, oldVersion);
1157
+ const oldPath = path3.join(updatesDir, oldVersion);
827
1158
  try {
828
- fs2.rmSync(oldPath, { recursive: true, force: true });
1159
+ fs3.rmSync(oldPath, { recursive: true, force: true });
829
1160
  } catch {}
830
1161
  }
831
1162
  }
832
1163
 
833
1164
  // src/core/update-manager.ts
834
- import fs3 from "node:fs";
835
- import path3 from "node:path";
836
- import { execSync, spawn } from "node:child_process";
1165
+ import fs4 from "node:fs";
1166
+ import path4 from "node:path";
1167
+ import { execFile, spawn } from "node:child_process";
1168
+ import { promisify } from "node:util";
1169
+ var execFileAsync = promisify(execFile);
837
1170
  function acquireLock() {
838
1171
  const lockPath = getUpdateLockPath();
839
1172
  try {
840
- ensureDir(path3.dirname(lockPath));
841
- fs3.writeFileSync(lockPath, `${process.pid}
1173
+ ensureDir(path4.dirname(lockPath));
1174
+ fs4.writeFileSync(lockPath, `${process.pid}
842
1175
  `, { flag: "wx" });
843
1176
  return true;
844
1177
  } catch {
@@ -848,25 +1181,25 @@ function acquireLock() {
848
1181
  function releaseLock() {
849
1182
  const lockPath = getUpdateLockPath();
850
1183
  try {
851
- fs3.unlinkSync(lockPath);
1184
+ fs4.unlinkSync(lockPath);
852
1185
  } catch {}
853
1186
  }
854
1187
  function isUpdateInProgress() {
855
1188
  const lockPath = getUpdateLockPath();
856
- if (!fs3.existsSync(lockPath)) {
1189
+ if (!fs4.existsSync(lockPath)) {
857
1190
  return false;
858
1191
  }
859
1192
  try {
860
- const raw = fs3.readFileSync(lockPath, "utf8").trim();
1193
+ const raw = fs4.readFileSync(lockPath, "utf8").trim();
861
1194
  const pid = Number(raw);
862
1195
  if (!Number.isFinite(pid) || pid <= 0) {
863
- fs3.unlinkSync(lockPath);
1196
+ fs4.unlinkSync(lockPath);
864
1197
  return false;
865
1198
  }
866
1199
  process.kill(pid, 0);
867
1200
  return true;
868
1201
  } catch {
869
- fs3.unlinkSync(lockPath);
1202
+ fs4.unlinkSync(lockPath);
870
1203
  return false;
871
1204
  }
872
1205
  }
@@ -876,9 +1209,12 @@ async function downloadAndStageUpdate() {
876
1209
  return { success: false, error: "No update available" };
877
1210
  }
878
1211
  const targetVersion = updateStatus.latestVersion;
1212
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(targetVersion)) {
1213
+ return { success: false, error: "Update returned an invalid version" };
1214
+ }
879
1215
  const updatesDir = getUpdatesDir();
880
- const versionDir = path3.join(updatesDir, `v${targetVersion}`);
881
- if (fs3.existsSync(versionDir)) {
1216
+ const versionDir = path4.join(updatesDir, `v${targetVersion}`);
1217
+ if (fs4.existsSync(versionDir)) {
882
1218
  const existingStaged = getStagedUpdate();
883
1219
  if (existingStaged?.version === targetVersion) {
884
1220
  return { success: false, error: `Version ${targetVersion} already staged` };
@@ -886,31 +1222,19 @@ async function downloadAndStageUpdate() {
886
1222
  }
887
1223
  try {
888
1224
  ensureDir(versionDir);
889
- execSync(`npm pack @kwonye/mcpx@${targetVersion} --json`, {
890
- cwd: versionDir,
891
- stdio: ["pipe", "pipe", "pipe"]
892
- });
893
- const tarballPath = path3.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
894
- if (!fs3.existsSync(tarballPath)) {
1225
+ await execFileAsync("npm", ["pack", `@kwonye/mcpx@${targetVersion}`, "--json"], { cwd: versionDir });
1226
+ const tarballPath = path4.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
1227
+ if (!fs4.existsSync(tarballPath)) {
895
1228
  throw new Error("Downloaded tarball not found");
896
1229
  }
897
- execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
898
- cwd: versionDir,
899
- stdio: ["pipe", "pipe", "pipe"]
900
- });
901
- execSync("npm install --production --ignore-scripts", {
902
- cwd: versionDir,
903
- stdio: ["pipe", "pipe", "pipe"]
904
- });
905
- const cliPath = path3.join(versionDir, "dist", "cli.js");
906
- if (!fs3.existsSync(cliPath)) {
1230
+ await execFileAsync("tar", ["-xzf", tarballPath, "--strip-components=1"], { cwd: versionDir });
1231
+ await execFileAsync("npm", ["install", "--production", "--ignore-scripts"], { cwd: versionDir });
1232
+ const cliPath = path4.join(versionDir, "dist", "cli.js");
1233
+ if (!fs4.existsSync(cliPath)) {
907
1234
  throw new Error("CLI entry point not found in downloaded package");
908
1235
  }
909
1236
  try {
910
- execSync(`${process.execPath} ${JSON.stringify(cliPath)} --version`, {
911
- stdio: ["ignore", "pipe", "pipe"],
912
- timeout: 1e4
913
- });
1237
+ await execFileAsync(process.execPath, [cliPath, "--version"], { timeout: 1e4 });
914
1238
  } catch (smokeTestError) {
915
1239
  throw new Error(`Downloaded CLI failed smoke test (--version): ${smokeTestError instanceof Error ? smokeTestError.message : String(smokeTestError)}`);
916
1240
  }
@@ -919,7 +1243,7 @@ async function downloadAndStageUpdate() {
919
1243
  return { success: true, version: targetVersion };
920
1244
  } catch (error) {
921
1245
  try {
922
- fs3.rmSync(versionDir, { recursive: true, force: true });
1246
+ fs4.rmSync(versionDir, { recursive: true, force: true });
923
1247
  } catch {}
924
1248
  return {
925
1249
  success: false,
@@ -950,9 +1274,10 @@ function startBackgroundUpdateCheck() {
950
1274
  }
951
1275
  });
952
1276
  child.unref();
953
- setTimeout(() => {
1277
+ const releaseTimer = setTimeout(() => {
954
1278
  releaseLock();
955
1279
  }, 60000);
1280
+ releaseTimer.unref?.();
956
1281
  }
957
1282
  async function runBackgroundUpdate() {
958
1283
  try {
@@ -1007,13 +1332,13 @@ function performRollback() {
1007
1332
  }
1008
1333
 
1009
1334
  // src/core/marketplace-updater.ts
1010
- import fs4 from "node:fs";
1335
+ import fs5 from "node:fs";
1011
1336
  var MARKETPLACE_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
1012
1337
  var INITIAL_DELAY_MS = 30000;
1013
1338
  var LOCK_STALE_MS = 30 * 60 * 1000;
1014
1339
  function readState() {
1015
1340
  try {
1016
- return JSON.parse(fs4.readFileSync(getMarketplaceUpdateStatePath(), "utf8"));
1341
+ return JSON.parse(fs5.readFileSync(getMarketplaceUpdateStatePath(), "utf8"));
1017
1342
  } catch {
1018
1343
  return {};
1019
1344
  }
@@ -1021,7 +1346,7 @@ function readState() {
1021
1346
  function writeState(state) {
1022
1347
  const target = getMarketplaceUpdateStatePath();
1023
1348
  ensureParentDir(target);
1024
- fs4.writeFileSync(target, JSON.stringify(state, null, 2), { mode: 384 });
1349
+ fs5.writeFileSync(target, JSON.stringify(state, null, 2), { mode: 384 });
1025
1350
  }
1026
1351
  function due(now = Date.now()) {
1027
1352
  const last = readState().lastRunAt;
@@ -1031,13 +1356,13 @@ function acquireLock2() {
1031
1356
  const target = getMarketplaceUpdateLockPath();
1032
1357
  ensureParentDir(target);
1033
1358
  try {
1034
- fs4.writeFileSync(target, `${process.pid}
1359
+ fs5.writeFileSync(target, `${process.pid}
1035
1360
  `, { flag: "wx", mode: 384 });
1036
1361
  return true;
1037
1362
  } catch {
1038
1363
  try {
1039
- if (Date.now() - fs4.statSync(target).mtimeMs > LOCK_STALE_MS) {
1040
- fs4.unlinkSync(target);
1364
+ if (Date.now() - fs5.statSync(target).mtimeMs > LOCK_STALE_MS) {
1365
+ fs5.unlinkSync(target);
1041
1366
  return acquireLock2();
1042
1367
  }
1043
1368
  } catch {
@@ -1048,7 +1373,7 @@ function acquireLock2() {
1048
1373
  }
1049
1374
  function releaseLock2() {
1050
1375
  try {
1051
- fs4.unlinkSync(getMarketplaceUpdateLockPath());
1376
+ fs5.unlinkSync(getMarketplaceUpdateLockPath());
1052
1377
  } catch {}
1053
1378
  }
1054
1379
  async function runMarketplaceAutoUpdate(options) {
@@ -1088,7 +1413,7 @@ function startMarketplaceAutoUpdater() {
1088
1413
  }
1089
1414
 
1090
1415
  // src/core/daemon.ts
1091
- import fs5 from "node:fs";
1416
+ import fs6 from "node:fs";
1092
1417
  import net from "node:net";
1093
1418
  import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
1094
1419
 
@@ -1258,6 +1583,29 @@ function parseNamespacedUri(uri) {
1258
1583
  async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
1259
1584
  return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
1260
1585
  }
1586
+ var ERROR_CODE_PRIORITY = [
1587
+ "auth_expired",
1588
+ "auth_required",
1589
+ "secret_missing",
1590
+ "unreachable",
1591
+ "timeout",
1592
+ "upstream_error"
1593
+ ];
1594
+ function pickPrimaryErrorCode(errors) {
1595
+ let best;
1596
+ let bestRank = Number.POSITIVE_INFINITY;
1597
+ for (const { code } of errors) {
1598
+ if (!code)
1599
+ continue;
1600
+ const rank = ERROR_CODE_PRIORITY.indexOf(code);
1601
+ const effectiveRank = rank === -1 ? ERROR_CODE_PRIORITY.length : rank;
1602
+ if (effectiveRank < bestRank) {
1603
+ bestRank = effectiveRank;
1604
+ best = code;
1605
+ }
1606
+ }
1607
+ return best;
1608
+ }
1261
1609
  async function getUpstreamTokenCounts(config, secrets, runtime) {
1262
1610
  if (!runtime.tokenCache) {
1263
1611
  runtime.tokenCache = new Map;
@@ -1265,7 +1613,11 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1265
1613
  const results = {};
1266
1614
  const upstreams = listUpstreams(config);
1267
1615
  for (const upstream of upstreams) {
1268
- const fingerprint = specFingerprint(upstream.spec);
1616
+ let recordError = function(label, error) {
1617
+ const message = error instanceof Error ? error.message : String(error);
1618
+ errors.push({ label, message, code: error instanceof UpstreamError ? error.code : undefined });
1619
+ };
1620
+ const fingerprint = specFingerprint(upstream.spec, secrets);
1269
1621
  const cached = runtime.tokenCache.get(upstream.name);
1270
1622
  if (cached?.fingerprint === fingerprint) {
1271
1623
  const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
@@ -1282,7 +1634,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1282
1634
  toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
1283
1635
  }
1284
1636
  } catch (error) {
1285
- errors.push(`tools/list: ${error instanceof Error ? error.message : String(error)}`);
1637
+ recordError("tools/list", error);
1286
1638
  }
1287
1639
  try {
1288
1640
  const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
@@ -1290,7 +1642,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1290
1642
  resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
1291
1643
  }
1292
1644
  } catch (error) {
1293
- errors.push(`resources/list: ${error instanceof Error ? error.message : String(error)}`);
1645
+ recordError("resources/list", error);
1294
1646
  }
1295
1647
  try {
1296
1648
  const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
@@ -1298,7 +1650,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1298
1650
  promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
1299
1651
  }
1300
1652
  } catch (error) {
1301
- errors.push(`prompts/list: ${error instanceof Error ? error.message : String(error)}`);
1653
+ recordError("prompts/list", error);
1302
1654
  }
1303
1655
  const total = toolsCount + resourcesCount + promptsCount;
1304
1656
  const countObj = {
@@ -1306,7 +1658,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
1306
1658
  resources: resourcesCount,
1307
1659
  prompts: promptsCount,
1308
1660
  total,
1309
- error: errors.length > 0 ? errors.join("; ") : undefined
1661
+ error: errors.length > 0 ? errors.map((e) => `${e.label}: ${e.message}`).join("; ") : undefined,
1662
+ errorCode: pickPrimaryErrorCode(errors)
1310
1663
  };
1311
1664
  if (errors.length === 0) {
1312
1665
  runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
@@ -1340,7 +1693,20 @@ function specFingerprint(spec, secrets) {
1340
1693
  if (!obj)
1341
1694
  return;
1342
1695
  for (const [key, value] of Object.entries(obj)) {
1343
- if (value.startsWith("secret://") || value.startsWith("oauth://")) {
1696
+ if (isOAuthReference(value)) {
1697
+ try {
1698
+ const serverName = oauthReferenceServerName(value);
1699
+ let storedTokens = secrets.getSecret(oauthSecretNames(serverName).tokens);
1700
+ if (!storedTokens) {
1701
+ const legacy = `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_tokens`;
1702
+ storedTokens = secrets.getSecret(legacy);
1703
+ }
1704
+ const obtainedAt = storedTokens ? JSON.parse(storedTokens).obtainedAt : undefined;
1705
+ resolvedMarkers[key] = obtainedAt != null ? `oauth:${serverName}:${obtainedAt}` : value;
1706
+ } catch {
1707
+ resolvedMarkers[key] = value;
1708
+ }
1709
+ } else if (value.startsWith("secret://")) {
1344
1710
  try {
1345
1711
  resolvedMarkers[key] = secrets.resolveMaybeSecret(value);
1346
1712
  } catch {
@@ -1355,6 +1721,9 @@ function specFingerprint(spec, secrets) {
1355
1721
  collect(spec.headers);
1356
1722
  return JSON.stringify({ spec, resolvedMarkers });
1357
1723
  }
1724
+ function passthroughCacheSuffix(passthroughAuthorizationHeader) {
1725
+ return `passthrough:${crypto2.createHash("sha256").update(passthroughAuthorizationHeader).digest("hex").slice(0, 16)}`;
1726
+ }
1358
1727
  var EXTRA_INHERITED_ENV = ["TMPDIR", "LANG"];
1359
1728
  function resolveStdioEnv(spec, secrets) {
1360
1729
  const env = getDefaultEnvironment();
@@ -1405,8 +1774,8 @@ async function closeUpstreamConnection(entry) {
1405
1774
  await connection.transport.close();
1406
1775
  } catch {}
1407
1776
  }
1408
- async function reconcileUpstreamConnections(config, runtime) {
1409
- const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
1777
+ async function reconcileUpstreamConnections(config, secrets, runtime) {
1778
+ const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec, secrets)]));
1410
1779
  const staleKeys = [];
1411
1780
  for (const [key, entry] of runtime.upstreamConnections.entries()) {
1412
1781
  const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
@@ -1425,7 +1794,7 @@ async function reconcileUpstreamConnections(config, runtime) {
1425
1794
  }
1426
1795
  }
1427
1796
  function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
1428
- const key = passthroughAuthorizationHeader ? `${upstreamName}:passthrough` : upstreamName;
1797
+ const key = passthroughAuthorizationHeader ? `${upstreamName}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstreamName;
1429
1798
  const existing = runtime.upstreamConnections.get(key);
1430
1799
  if (!existing) {
1431
1800
  return;
@@ -1435,8 +1804,8 @@ function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHead
1435
1804
  }
1436
1805
  async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
1437
1806
  const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
1438
- const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:passthrough` : upstream.name;
1439
- const fingerprint = specFingerprint(upstream.spec);
1807
+ const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstream.name;
1808
+ const fingerprint = specFingerprint(upstream.spec, secrets);
1440
1809
  if (!forceOAuthRefresh) {
1441
1810
  const existing = runtime.upstreamConnections.get(cacheKey);
1442
1811
  if (existing) {
@@ -1515,12 +1884,20 @@ async function callUpstreamOnce(upstream, method, params, secrets, runtime, pass
1515
1884
  throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
1516
1885
  }
1517
1886
  }
1887
+ function tokensMatch(candidate, expectedToken) {
1888
+ if (!candidate) {
1889
+ return false;
1890
+ }
1891
+ const candidateBuf = Buffer.from(candidate);
1892
+ const expectedBuf = Buffer.from(expectedToken);
1893
+ return candidateBuf.length === expectedBuf.length && crypto2.timingSafeEqual(candidateBuf, expectedBuf);
1894
+ }
1518
1895
  function authHeaderIsValid(request, expectedToken) {
1519
1896
  const localTokenHeader = request.headers["x-mcpx-local-token"];
1520
- if (typeof localTokenHeader === "string" && localTokenHeader === expectedToken) {
1897
+ if (typeof localTokenHeader === "string" && tokensMatch(localTokenHeader, expectedToken)) {
1521
1898
  return true;
1522
1899
  }
1523
- if (Array.isArray(localTokenHeader) && localTokenHeader.includes(expectedToken)) {
1900
+ if (Array.isArray(localTokenHeader) && localTokenHeader.some((value) => tokensMatch(value, expectedToken))) {
1524
1901
  return true;
1525
1902
  }
1526
1903
  const authHeader = request.headers.authorization;
@@ -1528,7 +1905,7 @@ function authHeaderIsValid(request, expectedToken) {
1528
1905
  return false;
1529
1906
  }
1530
1907
  const [scheme, token] = authHeader.split(" ");
1531
- return scheme === "Bearer" && token === expectedToken;
1908
+ return scheme === "Bearer" && tokensMatch(token, expectedToken);
1532
1909
  }
1533
1910
  function isAuthChallenge(error) {
1534
1911
  return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
@@ -1539,7 +1916,7 @@ function getClientAuthorizationForUpstream(request, expectedToken) {
1539
1916
  return;
1540
1917
  }
1541
1918
  const [scheme, token] = authHeader.split(" ");
1542
- if (scheme === "Bearer" && token === expectedToken) {
1919
+ if (scheme === "Bearer" && tokensMatch(token, expectedToken)) {
1543
1920
  return;
1544
1921
  }
1545
1922
  return authHeader;
@@ -1556,11 +1933,11 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
1556
1933
  runtime.tokenCache = new Map;
1557
1934
  }
1558
1935
  const cached = runtime.tokenCache.get(upstream.name);
1559
- const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
1936
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
1560
1937
  existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
1561
1938
  existing.total = existing.tools + existing.resources + existing.prompts;
1562
1939
  delete existing.error;
1563
- runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
1940
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
1564
1941
  for (const tool of result.tools ?? []) {
1565
1942
  const name = typeof tool.name === "string" ? tool.name : "tool";
1566
1943
  tools.push({
@@ -1594,11 +1971,11 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
1594
1971
  runtime.tokenCache = new Map;
1595
1972
  }
1596
1973
  const cached = runtime.tokenCache.get(upstream.name);
1597
- const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
1974
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
1598
1975
  existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
1599
1976
  existing.total = existing.tools + existing.resources + existing.prompts;
1600
1977
  delete existing.error;
1601
- runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
1978
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
1602
1979
  for (const resource of result.resources ?? []) {
1603
1980
  const originalUri = typeof resource.uri === "string" ? resource.uri : "";
1604
1981
  const originalName = typeof resource.name === "string" ? resource.name : originalUri;
@@ -1634,11 +2011,11 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
1634
2011
  runtime.tokenCache = new Map;
1635
2012
  }
1636
2013
  const cached = runtime.tokenCache.get(upstream.name);
1637
- const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
2014
+ const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
1638
2015
  existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
1639
2016
  existing.total = existing.tools + existing.resources + existing.prompts;
1640
2017
  delete existing.error;
1641
- runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
2018
+ runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
1642
2019
  for (const prompt of result.prompts ?? []) {
1643
2020
  const name = typeof prompt.name === "string" ? prompt.name : "prompt";
1644
2021
  prompts.push({
@@ -1799,7 +2176,7 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
1799
2176
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
1800
2177
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
1801
2178
  }
1802
- await reconcileUpstreamConnections(config, runtime);
2179
+ await reconcileUpstreamConnections(config, secrets, runtime);
1803
2180
  if (request.method === "custom/tokenCounts") {
1804
2181
  const result = await getUpstreamTokenCounts(config, secrets, runtime);
1805
2182
  return makeResult(id, result);
@@ -1862,12 +2239,36 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
1862
2239
  headers["mcp-protocol-version"] = protocolVersion;
1863
2240
  }
1864
2241
  for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
1865
- headers[key] = secrets.resolveMaybeSecret(value);
2242
+ if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2243
+ try {
2244
+ const accessToken = await getOAuthAccessToken(oauthReferenceServerName(value), upstream.spec, secrets);
2245
+ headers[key] = `Bearer ${accessToken}`;
2246
+ } catch {}
2247
+ } else {
2248
+ headers[key] = secrets.resolveMaybeSecret(value);
2249
+ }
2250
+ }
2251
+ const timeoutController = new AbortController;
2252
+ const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_CONNECT_TIMEOUT_MS);
2253
+ let upstreamResponse;
2254
+ try {
2255
+ upstreamResponse = await fetch(upstreamWellKnownUrl, {
2256
+ method: "GET",
2257
+ headers,
2258
+ signal: timeoutController.signal
2259
+ });
2260
+ } catch (error) {
2261
+ const isAbort = error.name === "AbortError";
2262
+ response.statusCode = 502;
2263
+ response.setHeader("content-type", "application/json");
2264
+ response.end(JSON.stringify({
2265
+ error: "upstream_unreachable",
2266
+ message: isAbort ? `Well-known request timed out after ${DEFAULT_CONNECT_TIMEOUT_MS}ms.` : error instanceof Error ? error.message : String(error)
2267
+ }));
2268
+ return true;
2269
+ } finally {
2270
+ clearTimeout(timeoutHandle);
1866
2271
  }
1867
- const upstreamResponse = await fetch(upstreamWellKnownUrl, {
1868
- method: "GET",
1869
- headers
1870
- });
1871
2272
  let bodyText = await upstreamResponse.text();
1872
2273
  response.statusCode = upstreamResponse.status;
1873
2274
  const contentType = upstreamResponse.headers.get("content-type");
@@ -1892,6 +2293,16 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
1892
2293
  response.end(bodyText);
1893
2294
  return true;
1894
2295
  }
2296
+ var SENSITIVE_HEADER_NAMES = new Set(["authorization", "x-mcpx-local-token"]);
2297
+ function redactSensitiveHeaders(headers) {
2298
+ const redacted = { ...headers };
2299
+ for (const name of Object.keys(redacted)) {
2300
+ if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
2301
+ redacted[name] = "[redacted]";
2302
+ }
2303
+ }
2304
+ return redacted;
2305
+ }
1895
2306
  function createGatewayServer(options) {
1896
2307
  const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
1897
2308
  const runtime = {
@@ -1905,7 +2316,7 @@ function createGatewayServer(options) {
1905
2316
  const upstreamFilter = getRequestedUpstream(requestUrl);
1906
2317
  if (debug) {
1907
2318
  console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
1908
- console.error(`[mcpx gateway] headers=${JSON.stringify(request.headers)}`);
2319
+ console.error(`[mcpx gateway] headers=${JSON.stringify(redactSensitiveHeaders(request.headers))}`);
1909
2320
  }
1910
2321
  if (await maybeHandleWellKnownOAuthRequest(request, response, requestUrl, options.secrets)) {
1911
2322
  if (debug) {
@@ -2081,11 +2492,18 @@ function createGatewayServer(options) {
2081
2492
  }
2082
2493
 
2083
2494
  // src/core/daemon.ts
2495
+ function buildDaemonChildEnv(baseEnv = process.env, electronRuntime = typeof process.versions.electron === "string") {
2496
+ return {
2497
+ ...baseEnv,
2498
+ MCPX_DAEMON_CHILD: "1",
2499
+ ...electronRuntime ? { ELECTRON_RUN_AS_NODE: "1" } : {}
2500
+ };
2501
+ }
2084
2502
  function readPidRecordFromFile(pidPath = getPidPath()) {
2085
- if (!fs5.existsSync(pidPath)) {
2503
+ if (!fs6.existsSync(pidPath)) {
2086
2504
  return null;
2087
2505
  }
2088
- const raw = fs5.readFileSync(pidPath, "utf8").trim();
2506
+ const raw = fs6.readFileSync(pidPath, "utf8").trim();
2089
2507
  if (!raw) {
2090
2508
  return null;
2091
2509
  }
@@ -2145,7 +2563,7 @@ async function resolveGatewayPort(config, secrets) {
2145
2563
  }
2146
2564
  throw new Error(`No available local port found near ${config.gateway.port}.`);
2147
2565
  }
2148
- async function waitForGatewayReady(port, token, timeoutMs = 5000) {
2566
+ async function waitForGatewayReady(port, token, timeoutMs = 15000) {
2149
2567
  const deadline = Date.now() + timeoutMs;
2150
2568
  let lastError;
2151
2569
  while (Date.now() < deadline) {
@@ -2193,18 +2611,12 @@ function getDaemonStatus(config) {
2193
2611
  async function startDaemon(config, cliPath, secrets) {
2194
2612
  const existingStatus = getDaemonStatus(config);
2195
2613
  if (existingStatus.running && existingStatus.pid) {
2196
- const portAvailable = await isPortAvailable(config.gateway.port);
2197
- if (!portAvailable) {
2198
- return {
2199
- started: false,
2200
- pid: existingStatus.pid,
2201
- port: config.gateway.port,
2202
- message: "mcpx daemon already running."
2203
- };
2204
- }
2205
- try {
2206
- fs5.unlinkSync(getPidPath());
2207
- } catch {}
2614
+ return {
2615
+ started: false,
2616
+ pid: existingStatus.pid,
2617
+ port: existingStatus.port ?? config.gateway.port,
2618
+ message: "mcpx daemon already running."
2619
+ };
2208
2620
  }
2209
2621
  const { port, fellBackFrom } = await resolveGatewayPort(config, secrets);
2210
2622
  if (fellBackFrom) {
@@ -2216,35 +2628,32 @@ async function startDaemon(config, cliPath, secrets) {
2216
2628
  const logPath = getLogPath();
2217
2629
  ensureParentDir(pidPath);
2218
2630
  ensureParentDir(logPath);
2219
- let logFd = fs5.openSync(logPath, "a", 384);
2220
- const logStat = fs5.fstatSync(logFd);
2631
+ let logFd = fs6.openSync(logPath, "a", 384);
2632
+ const logStat = fs6.fstatSync(logFd);
2221
2633
  if (logStat.size > 10 * 1024 * 1024) {
2222
- fs5.closeSync(logFd);
2634
+ fs6.closeSync(logFd);
2223
2635
  const log1 = `${logPath}.1`;
2224
2636
  const log2 = `${logPath}.2`;
2225
2637
  try {
2226
- if (fs5.existsSync(log2))
2227
- fs5.unlinkSync(log2);
2638
+ if (fs6.existsSync(log2))
2639
+ fs6.unlinkSync(log2);
2228
2640
  } catch {}
2229
2641
  try {
2230
- if (fs5.existsSync(log1))
2231
- fs5.renameSync(log1, log2);
2642
+ if (fs6.existsSync(log1))
2643
+ fs6.renameSync(log1, log2);
2232
2644
  } catch {}
2233
2645
  try {
2234
- fs5.renameSync(logPath, log1);
2646
+ fs6.renameSync(logPath, log1);
2235
2647
  } catch {}
2236
- logFd = fs5.openSync(logPath, "a", 384);
2648
+ logFd = fs6.openSync(logPath, "a", 384);
2237
2649
  }
2238
2650
  const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
2239
2651
  detached: true,
2240
2652
  stdio: ["ignore", logFd, logFd],
2241
- env: {
2242
- ...process.env,
2243
- MCPX_DAEMON_CHILD: "1"
2244
- }
2653
+ env: buildDaemonChildEnv()
2245
2654
  });
2246
2655
  child.unref();
2247
- fs5.writeFileSync(pidPath, `${child.pid}:${port}
2656
+ fs6.writeFileSync(pidPath, `${child.pid}:${port}
2248
2657
  `, { mode: 384 });
2249
2658
  startBackgroundUpdateCheck();
2250
2659
  await waitForGatewayReady(port, token);
@@ -2268,7 +2677,7 @@ function stopDaemon() {
2268
2677
  const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
2269
2678
  const looksLikeMcpxDaemon = cmd.includes("daemon run") && (cmd.includes("mcpx") || cmd.includes("cli.js"));
2270
2679
  if (!looksLikeMcpxDaemon) {
2271
- fs5.unlinkSync(pidPath);
2680
+ fs6.unlinkSync(pidPath);
2272
2681
  return {
2273
2682
  stopped: false,
2274
2683
  message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
@@ -2276,7 +2685,7 @@ function stopDaemon() {
2276
2685
  }
2277
2686
  } catch {
2278
2687
  try {
2279
- fs5.unlinkSync(pidPath);
2688
+ fs6.unlinkSync(pidPath);
2280
2689
  } catch {}
2281
2690
  return {
2282
2691
  stopped: false,
@@ -2287,7 +2696,7 @@ function stopDaemon() {
2287
2696
  process.kill(pid, "SIGTERM");
2288
2697
  } catch {}
2289
2698
  try {
2290
- fs5.unlinkSync(pidPath);
2699
+ fs6.unlinkSync(pidPath);
2291
2700
  } catch {}
2292
2701
  return {
2293
2702
  stopped: true,
@@ -2300,10 +2709,10 @@ async function restartDaemon(config, cliPath, secrets) {
2300
2709
  }
2301
2710
  function readDaemonLogs(maxLines = 200) {
2302
2711
  const logPath = getLogPath();
2303
- if (!fs5.existsSync(logPath)) {
2712
+ if (!fs6.existsSync(logPath)) {
2304
2713
  return "";
2305
2714
  }
2306
- const lines = fs5.readFileSync(logPath, "utf8").split(`
2715
+ const lines = fs6.readFileSync(logPath, "utf8").split(`
2307
2716
  `);
2308
2717
  return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
2309
2718
  `);
@@ -2448,4 +2857,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
2448
2857
  };
2449
2858
  }
2450
2859
 
2451
- export { probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, runMarketplaceAutoUpdate, startMarketplaceAutoUpdater, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
2860
+ export { isOAuthReference, oauthReferenceServerName, __resetOAuthSupportCache, probeOAuthSupport, OAUTH_LOGIN_TIMEOUT_MS, OAuthCancelledError, OAuthLoginInProgressError, runOAuthLogin, getOAuthAccessToken, oauthSecretNames, clearOAuthCredentials, getOAuthCredentialStatus, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, runMarketplaceAutoUpdate, startMarketplaceAutoUpdater, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };