@kwonye/mcpx 0.1.102 → 0.1.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{marketplace-zem0m4mw.js → chunk-02r7k4ax.js} +20 -17
- package/dist/{plugin-host-5jmyt0td.js → chunk-0xhdyt4z.js} +8 -5
- package/dist/chunk-12vgqm89.js +31 -0
- package/dist/{config-fbgyqmv1.js → chunk-31brdz5s.js} +14 -11
- package/dist/chunk-91vcwk5m.js +21 -0
- package/dist/chunk-941dqsx2.js +10432 -0
- package/dist/{plugin-manager-rw05c2x3.js → chunk-b58zsdp4.js} +23 -20
- package/dist/{cli-s4gspqdf.js → chunk-bjw71y54.js} +3 -4
- package/dist/chunk-bnbz55g6.js +38 -0
- package/dist/chunk-c7zg7ey3.js +17 -0
- package/dist/{cli-6wh8qx2y.js → chunk-cqenqnxk.js} +6 -1
- package/dist/{cli-hh7rthhx.js → chunk-cz8cdmw1.js} +7 -4
- package/dist/chunk-enzmq965.js +40 -0
- package/dist/{cli-aawpvhhe.js → chunk-s9v1epnk.js} +7 -5
- package/dist/chunk-tnhekcpv.js +1487 -0
- package/dist/chunk-wj4tjacg.js +52 -0
- package/dist/{cli-6zq2fd6h.js → chunk-wq4zd3t7.js} +4 -2
- package/dist/chunk-xydajvzg.js +38 -0
- package/dist/{cli-9k3wmag6.js → chunk-z1k63fa3.js} +18 -21
- package/dist/{registry-04kfpjh0.js → chunk-z34m83m4.js} +16 -13
- package/dist/cli.js +34124 -994
- package/dist/compat/claude.d.ts +1 -1
- package/dist/compat/qwen.d.ts +2 -2
- package/dist/core/error-reporting.d.ts +6 -0
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.js +172 -125
- package/dist/core/mcp-v2.d.ts +11 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/sync.d.ts +1 -0
- package/dist/core/telemetry.d.ts +203 -0
- package/dist/gateway/server.d.ts +9 -3
- package/dist/types.js +6 -3
- package/package.json +8 -3
- package/dist/adapters/claude-desktop.d.ts.map +0 -1
- package/dist/adapters/claude.d.ts.map +0 -1
- package/dist/adapters/cline.d.ts.map +0 -1
- package/dist/adapters/codex.d.ts.map +0 -1
- package/dist/adapters/cursor.d.ts.map +0 -1
- package/dist/adapters/hermes.d.ts.map +0 -1
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/kiro.d.ts.map +0 -1
- package/dist/adapters/openclaw.d.ts.map +0 -1
- package/dist/adapters/opencode.d.ts.map +0 -1
- package/dist/adapters/qwen.d.ts.map +0 -1
- package/dist/adapters/utils/index.d.ts.map +0 -1
- package/dist/adapters/vscode.d.ts.map +0 -1
- package/dist/cli-qzzjkt0q.js +0 -2860
- package/dist/cli.d.ts.map +0 -1
- package/dist/compat/claude.d.ts.map +0 -1
- package/dist/compat/codex.d.ts.map +0 -1
- package/dist/compat/hermes.d.ts.map +0 -1
- package/dist/compat/index.d.ts.map +0 -1
- package/dist/compat/openclaw.d.ts.map +0 -1
- package/dist/compat/qwen.d.ts.map +0 -1
- package/dist/compat/unsupported.d.ts.map +0 -1
- package/dist/compat/vscode.d.ts.map +0 -1
- package/dist/core/add-command.d.ts.map +0 -1
- package/dist/core/auth-probe.d.ts.map +0 -1
- package/dist/core/config-repair.d.ts.map +0 -1
- package/dist/core/config-store.d.ts.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/daemon.d.ts.map +0 -1
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/identifiers.d.ts.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/managed-index-lock.d.ts.map +0 -1
- package/dist/core/managed-index.d.ts.map +0 -1
- package/dist/core/marketplace-defaults.d.ts.map +0 -1
- package/dist/core/marketplace-updater.d.ts.map +0 -1
- package/dist/core/marketplace.d.ts.map +0 -1
- package/dist/core/oauth.d.ts.map +0 -1
- package/dist/core/paths.d.ts.map +0 -1
- package/dist/core/plugin-cache.d.ts.map +0 -1
- package/dist/core/plugin-data.d.ts.map +0 -1
- package/dist/core/plugin-host.d.ts.map +0 -1
- package/dist/core/plugin-lifecycle.d.ts.map +0 -1
- package/dist/core/plugin-manager.d.ts.map +0 -1
- package/dist/core/plugin-parse.d.ts.map +0 -1
- package/dist/core/plugin-projections.d.ts.map +0 -1
- package/dist/core/plugin-source.d.ts.map +0 -1
- package/dist/core/proxy.d.ts.map +0 -1
- package/dist/core/registry-client.d.ts.map +0 -1
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/secrets.d.ts.map +0 -1
- package/dist/core/server-auth.d.ts.map +0 -1
- package/dist/core/skill-projections.d.ts.map +0 -1
- package/dist/core/skills.d.ts.map +0 -1
- package/dist/core/spawn-env.d.ts.map +0 -1
- package/dist/core/status.d.ts.map +0 -1
- package/dist/core/sync.d.ts.map +0 -1
- package/dist/core/update-manager.d.ts.map +0 -1
- package/dist/core/update.d.ts.map +0 -1
- package/dist/gateway/server.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/util/fs.d.ts.map +0 -1
- package/dist/version.d.ts.map +0 -1
package/dist/cli-qzzjkt0q.js
DELETED
|
@@ -1,2860 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
refreshAutoUpdateMarketplaces
|
|
3
|
-
} from "./cli-aawpvhhe.js";
|
|
4
|
-
import {
|
|
5
|
-
APP_VERSION,
|
|
6
|
-
getGatewayUrl,
|
|
7
|
-
mutateConfig,
|
|
8
|
-
persistSyncState,
|
|
9
|
-
syncAllClients
|
|
10
|
-
} from "./cli-9k3wmag6.js";
|
|
11
|
-
import {
|
|
12
|
-
loadConfig,
|
|
13
|
-
loadMergedConfig,
|
|
14
|
-
saveConfig
|
|
15
|
-
} from "./cli-6zq2fd6h.js";
|
|
16
|
-
import {
|
|
17
|
-
SecretsManager,
|
|
18
|
-
UpstreamError,
|
|
19
|
-
classifyUpstreamError,
|
|
20
|
-
ensureGatewayToken
|
|
21
|
-
} from "./cli-hh7rthhx.js";
|
|
22
|
-
import {
|
|
23
|
-
ensureDir,
|
|
24
|
-
ensureParentDir,
|
|
25
|
-
getLogPath,
|
|
26
|
-
getMarketplaceUpdateLockPath,
|
|
27
|
-
getMarketplaceUpdateStatePath,
|
|
28
|
-
getOAuthLockPath,
|
|
29
|
-
getPidPath,
|
|
30
|
-
getStagedVersionPath,
|
|
31
|
-
getUpdateLockPath,
|
|
32
|
-
getUpdatesDir
|
|
33
|
-
} from "./cli-6wh8qx2y.js";
|
|
34
|
-
import {
|
|
35
|
-
isServerEnabled
|
|
36
|
-
} from "./cli-s4gspqdf.js";
|
|
37
|
-
|
|
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}`;
|
|
55
|
-
}
|
|
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
|
-
}
|
|
130
|
-
};
|
|
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 {}
|
|
148
|
-
}
|
|
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;
|
|
169
|
-
}
|
|
170
|
-
function tokensAreExpiring(tokens) {
|
|
171
|
-
if (!tokens.tokens.expires_in) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
|
|
175
|
-
return Date.now() >= expiresAt - 60000;
|
|
176
|
-
}
|
|
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) {
|
|
194
|
-
return {
|
|
195
|
-
redirect_uris: [],
|
|
196
|
-
token_endpoint_auth_method: "none",
|
|
197
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
198
|
-
response_types: ["code"],
|
|
199
|
-
client_name: "mcpx"
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
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"
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
state() {
|
|
211
|
-
return this.#stateValue;
|
|
212
|
-
}
|
|
213
|
-
clientInformation() {
|
|
214
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
|
|
215
|
-
}
|
|
216
|
-
saveClientInformation(clientInformation) {
|
|
217
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
|
|
218
|
-
}
|
|
219
|
-
tokens() {
|
|
220
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
|
|
221
|
-
}
|
|
222
|
-
saveTokens(tokens) {
|
|
223
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
224
|
-
tokens,
|
|
225
|
-
obtainedAt: Date.now()
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
redirectToAuthorization(_authorizationUrl) {}
|
|
229
|
-
saveCodeVerifier(codeVerifier) {
|
|
230
|
-
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
231
|
-
}
|
|
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}".`);
|
|
236
|
-
}
|
|
237
|
-
return verifier;
|
|
238
|
-
}
|
|
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));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
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;
|
|
276
|
-
}
|
|
277
|
-
return super.codeVerifier();
|
|
278
|
-
}
|
|
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
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
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
|
-
});
|
|
302
|
-
}
|
|
303
|
-
function closeServer(server) {
|
|
304
|
-
return new Promise((resolve) => {
|
|
305
|
-
server.close(() => resolve());
|
|
306
|
-
});
|
|
307
|
-
}
|
|
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" ? "✓" : "✗";
|
|
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>`;
|
|
352
|
-
}
|
|
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";
|
|
359
|
-
}
|
|
360
|
-
}
|
|
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
|
-
}
|
|
368
|
-
}
|
|
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();
|
|
382
|
-
}
|
|
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.`);
|
|
443
|
-
}
|
|
444
|
-
if (spec.transport !== "http") {
|
|
445
|
-
throw new Error(`OAuth login only supports HTTP servers.`);
|
|
446
|
-
}
|
|
447
|
-
spec.headers = {
|
|
448
|
-
...spec.headers ?? {},
|
|
449
|
-
Authorization: `oauth://${encodeURIComponent(serverName)}`
|
|
450
|
-
};
|
|
451
|
-
}
|
|
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;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
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;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
if (tryCreate()) {
|
|
482
|
-
return () => releaseOAuthLoginLock(lockPath);
|
|
483
|
-
}
|
|
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
|
-
}
|
|
502
|
-
}
|
|
503
|
-
throw new OAuthLoginInProgressError(serverName, holderPid);
|
|
504
|
-
}
|
|
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);
|
|
510
|
-
}
|
|
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();
|
|
583
|
-
}
|
|
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}".`);
|
|
589
|
-
}
|
|
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
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return stored.tokens.access_token;
|
|
634
|
-
}
|
|
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
|
-
};
|
|
642
|
-
}
|
|
643
|
-
function clearOAuthCredentials(serverName, secrets) {
|
|
644
|
-
const removed = [];
|
|
645
|
-
const candidates = [
|
|
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 };
|
|
669
|
-
}
|
|
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
|
-
};
|
|
681
|
-
}
|
|
682
|
-
|
|
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));
|
|
707
|
-
}
|
|
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);
|
|
715
|
-
}
|
|
716
|
-
return headers;
|
|
717
|
-
}
|
|
718
|
-
function isOAuthLikelyChallenge(wwwAuthenticate) {
|
|
719
|
-
if (!wwwAuthenticate) {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
const lower = wwwAuthenticate.toLowerCase();
|
|
723
|
-
return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
|
|
724
|
-
}
|
|
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;
|
|
732
|
-
}
|
|
733
|
-
const support = await probeOAuthSupport(spec.url).catch(() => {
|
|
734
|
-
return;
|
|
735
|
-
});
|
|
736
|
-
return {
|
|
737
|
-
...result,
|
|
738
|
-
oauthSupport: support?.support,
|
|
739
|
-
oauthLikely: result.oauthLikely || support?.support === "supported"
|
|
740
|
-
};
|
|
741
|
-
}
|
|
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
|
|
754
|
-
});
|
|
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
|
-
});
|
|
763
|
-
}
|
|
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 {}
|
|
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);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
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}`);
|
|
804
|
-
}
|
|
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] };
|
|
824
|
-
}
|
|
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] };
|
|
828
|
-
}
|
|
829
|
-
if (packages.length > 0) {
|
|
830
|
-
return { kind: "package", package: packages[0] };
|
|
831
|
-
}
|
|
832
|
-
if (remotes.length > 0) {
|
|
833
|
-
return { kind: "remote", remote: remotes[0] };
|
|
834
|
-
}
|
|
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
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
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
|
-
});
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
return inputs;
|
|
874
|
-
}
|
|
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
|
-
};
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// src/core/server-auth.ts
|
|
919
|
-
function normalizeNamePart(value) {
|
|
920
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
921
|
-
}
|
|
922
|
-
function requireNonEmpty(value, label) {
|
|
923
|
-
const normalized = value?.trim();
|
|
924
|
-
if (!normalized) {
|
|
925
|
-
throw new Error(`${label} must be non-empty.`);
|
|
926
|
-
}
|
|
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;
|
|
935
|
-
}
|
|
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")
|
|
958
|
-
};
|
|
959
|
-
}
|
|
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;
|
|
969
|
-
}
|
|
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.`);
|
|
981
|
-
}
|
|
982
|
-
if (!spec.headers || !(target.key in spec.headers)) {
|
|
983
|
-
return null;
|
|
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.`);
|
|
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;
|
|
1002
|
-
}
|
|
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
|
-
}));
|
|
1010
|
-
}
|
|
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;
|
|
1054
|
-
}
|
|
1055
|
-
seen.add(candidate);
|
|
1056
|
-
entries.push(candidate);
|
|
1057
|
-
}
|
|
1058
|
-
return entries.join(":");
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// src/core/update.ts
|
|
1062
|
-
import fs3 from "node:fs";
|
|
1063
|
-
import path3 from "node:path";
|
|
1064
|
-
import { execFileSync } from "node:child_process";
|
|
1065
|
-
function compareVersions(a, b) {
|
|
1066
|
-
const partsA = a.split(".").map(Number);
|
|
1067
|
-
const partsB = b.split(".").map(Number);
|
|
1068
|
-
for (let i = 0;i < 3; i += 1) {
|
|
1069
|
-
const numA = partsA[i] ?? 0;
|
|
1070
|
-
const numB = partsB[i] ?? 0;
|
|
1071
|
-
if (numA > numB)
|
|
1072
|
-
return 1;
|
|
1073
|
-
if (numA < numB)
|
|
1074
|
-
return -1;
|
|
1075
|
-
}
|
|
1076
|
-
return 0;
|
|
1077
|
-
}
|
|
1078
|
-
async function checkForUpdates() {
|
|
1079
|
-
try {
|
|
1080
|
-
const output = execFileSync("npm", ["view", "@kwonye/mcpx", "version"], {
|
|
1081
|
-
encoding: "utf8",
|
|
1082
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1083
|
-
}).trim();
|
|
1084
|
-
const latestVersion = output;
|
|
1085
|
-
const updateAvailable = compareVersions(latestVersion, APP_VERSION) > 0;
|
|
1086
|
-
return {
|
|
1087
|
-
currentVersion: APP_VERSION,
|
|
1088
|
-
latestVersion,
|
|
1089
|
-
updateAvailable
|
|
1090
|
-
};
|
|
1091
|
-
} catch (error) {
|
|
1092
|
-
return {
|
|
1093
|
-
currentVersion: APP_VERSION,
|
|
1094
|
-
latestVersion: null,
|
|
1095
|
-
updateAvailable: false,
|
|
1096
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
function getStagedUpdate() {
|
|
1101
|
-
const stagedPath = getStagedVersionPath();
|
|
1102
|
-
if (!fs3.existsSync(stagedPath)) {
|
|
1103
|
-
return null;
|
|
1104
|
-
}
|
|
1105
|
-
try {
|
|
1106
|
-
const raw = fs3.readFileSync(stagedPath, "utf8");
|
|
1107
|
-
const info = JSON.parse(raw);
|
|
1108
|
-
if (!info.version || !info.cliPath) {
|
|
1109
|
-
return null;
|
|
1110
|
-
}
|
|
1111
|
-
const absoluteCliPath = path3.isAbsolute(info.cliPath) ? info.cliPath : path3.join(getUpdatesDir(), info.cliPath);
|
|
1112
|
-
if (!fs3.existsSync(absoluteCliPath)) {
|
|
1113
|
-
return null;
|
|
1114
|
-
}
|
|
1115
|
-
return {
|
|
1116
|
-
...info,
|
|
1117
|
-
cliPath: absoluteCliPath
|
|
1118
|
-
};
|
|
1119
|
-
} catch {
|
|
1120
|
-
return null;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
function stageUpdate(version, cliPath) {
|
|
1124
|
-
const updatesDir = getUpdatesDir();
|
|
1125
|
-
ensureDir(updatesDir);
|
|
1126
|
-
const stagedInfo = {
|
|
1127
|
-
version,
|
|
1128
|
-
cliPath: path3.isAbsolute(cliPath) ? path3.relative(updatesDir, cliPath) : cliPath,
|
|
1129
|
-
stagedAt: new Date().toISOString()
|
|
1130
|
-
};
|
|
1131
|
-
fs3.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
1132
|
-
}
|
|
1133
|
-
function shouldUseStagedCli(stagedVersion, currentVersion) {
|
|
1134
|
-
return compareVersions(stagedVersion, currentVersion) > 0;
|
|
1135
|
-
}
|
|
1136
|
-
function clearStagedUpdate() {
|
|
1137
|
-
const stagedPath = getStagedVersionPath();
|
|
1138
|
-
if (fs3.existsSync(stagedPath)) {
|
|
1139
|
-
fs3.unlinkSync(stagedPath);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
function getStagedCliPath() {
|
|
1143
|
-
const staged = getStagedUpdate();
|
|
1144
|
-
return staged?.cliPath ?? null;
|
|
1145
|
-
}
|
|
1146
|
-
function removeOldVersions(keepCount = 2) {
|
|
1147
|
-
const updatesDir = getUpdatesDir();
|
|
1148
|
-
if (!fs3.existsSync(updatesDir)) {
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const entries = fs3.readdirSync(updatesDir, { withFileTypes: true });
|
|
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)));
|
|
1153
|
-
if (versionDirs.length <= keepCount) {
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
1157
|
-
const oldPath = path3.join(updatesDir, oldVersion);
|
|
1158
|
-
try {
|
|
1159
|
-
fs3.rmSync(oldPath, { recursive: true, force: true });
|
|
1160
|
-
} catch {}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// src/core/update-manager.ts
|
|
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);
|
|
1170
|
-
function acquireLock() {
|
|
1171
|
-
const lockPath = getUpdateLockPath();
|
|
1172
|
-
try {
|
|
1173
|
-
ensureDir(path4.dirname(lockPath));
|
|
1174
|
-
fs4.writeFileSync(lockPath, `${process.pid}
|
|
1175
|
-
`, { flag: "wx" });
|
|
1176
|
-
return true;
|
|
1177
|
-
} catch {
|
|
1178
|
-
return false;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
function releaseLock() {
|
|
1182
|
-
const lockPath = getUpdateLockPath();
|
|
1183
|
-
try {
|
|
1184
|
-
fs4.unlinkSync(lockPath);
|
|
1185
|
-
} catch {}
|
|
1186
|
-
}
|
|
1187
|
-
function isUpdateInProgress() {
|
|
1188
|
-
const lockPath = getUpdateLockPath();
|
|
1189
|
-
if (!fs4.existsSync(lockPath)) {
|
|
1190
|
-
return false;
|
|
1191
|
-
}
|
|
1192
|
-
try {
|
|
1193
|
-
const raw = fs4.readFileSync(lockPath, "utf8").trim();
|
|
1194
|
-
const pid = Number(raw);
|
|
1195
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
1196
|
-
fs4.unlinkSync(lockPath);
|
|
1197
|
-
return false;
|
|
1198
|
-
}
|
|
1199
|
-
process.kill(pid, 0);
|
|
1200
|
-
return true;
|
|
1201
|
-
} catch {
|
|
1202
|
-
fs4.unlinkSync(lockPath);
|
|
1203
|
-
return false;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
async function downloadAndStageUpdate() {
|
|
1207
|
-
const updateStatus = await checkForUpdates();
|
|
1208
|
-
if (!updateStatus.latestVersion || !updateStatus.updateAvailable) {
|
|
1209
|
-
return { success: false, error: "No update available" };
|
|
1210
|
-
}
|
|
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
|
-
}
|
|
1215
|
-
const updatesDir = getUpdatesDir();
|
|
1216
|
-
const versionDir = path4.join(updatesDir, `v${targetVersion}`);
|
|
1217
|
-
if (fs4.existsSync(versionDir)) {
|
|
1218
|
-
const existingStaged = getStagedUpdate();
|
|
1219
|
-
if (existingStaged?.version === targetVersion) {
|
|
1220
|
-
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
try {
|
|
1224
|
-
ensureDir(versionDir);
|
|
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)) {
|
|
1228
|
-
throw new Error("Downloaded tarball not found");
|
|
1229
|
-
}
|
|
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)) {
|
|
1234
|
-
throw new Error("CLI entry point not found in downloaded package");
|
|
1235
|
-
}
|
|
1236
|
-
try {
|
|
1237
|
-
await execFileAsync(process.execPath, [cliPath, "--version"], { timeout: 1e4 });
|
|
1238
|
-
} catch (smokeTestError) {
|
|
1239
|
-
throw new Error(`Downloaded CLI failed smoke test (--version): ${smokeTestError instanceof Error ? smokeTestError.message : String(smokeTestError)}`);
|
|
1240
|
-
}
|
|
1241
|
-
stageUpdate(targetVersion, cliPath);
|
|
1242
|
-
removeOldVersions(2);
|
|
1243
|
-
return { success: true, version: targetVersion };
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
try {
|
|
1246
|
-
fs4.rmSync(versionDir, { recursive: true, force: true });
|
|
1247
|
-
} catch {}
|
|
1248
|
-
return {
|
|
1249
|
-
success: false,
|
|
1250
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
function startBackgroundUpdateCheck() {
|
|
1255
|
-
if (process.env.MCPX_NO_UPDATE === "1") {
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
if (isUpdateInProgress()) {
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
if (!acquireLock()) {
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
const child = spawn(process.execPath, [
|
|
1265
|
-
process.argv[1] ?? "",
|
|
1266
|
-
"update",
|
|
1267
|
-
"--background"
|
|
1268
|
-
], {
|
|
1269
|
-
detached: true,
|
|
1270
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
1271
|
-
env: {
|
|
1272
|
-
...process.env,
|
|
1273
|
-
MCPX_UPDATE_CHILD: "1"
|
|
1274
|
-
}
|
|
1275
|
-
});
|
|
1276
|
-
child.unref();
|
|
1277
|
-
const releaseTimer = setTimeout(() => {
|
|
1278
|
-
releaseLock();
|
|
1279
|
-
}, 60000);
|
|
1280
|
-
releaseTimer.unref?.();
|
|
1281
|
-
}
|
|
1282
|
-
async function runBackgroundUpdate() {
|
|
1283
|
-
try {
|
|
1284
|
-
const result = await downloadAndStageUpdate();
|
|
1285
|
-
if (result.success && result.version) {
|
|
1286
|
-
console.error(`[update-manager] Update staged: v${result.version}. Will activate on next run.`);
|
|
1287
|
-
} else if (result.error && result.error !== "No update available") {
|
|
1288
|
-
console.error(`[update-manager] Update check failed: ${result.error}`);
|
|
1289
|
-
}
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
console.error(`[update-manager] Background update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1292
|
-
} finally {
|
|
1293
|
-
releaseLock();
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
async function performUpdate() {
|
|
1297
|
-
if (isUpdateInProgress()) {
|
|
1298
|
-
return { success: false, message: "Another update is already in progress." };
|
|
1299
|
-
}
|
|
1300
|
-
if (!acquireLock()) {
|
|
1301
|
-
return { success: false, message: "Another update is already in progress." };
|
|
1302
|
-
}
|
|
1303
|
-
try {
|
|
1304
|
-
const result = await downloadAndStageUpdate();
|
|
1305
|
-
if (result.success && result.version) {
|
|
1306
|
-
return {
|
|
1307
|
-
success: true,
|
|
1308
|
-
message: `Update to v${result.version} ready! Will activate on next run.`
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
if (result.error === "No update available") {
|
|
1312
|
-
return { success: true, message: "Already on latest version." };
|
|
1313
|
-
}
|
|
1314
|
-
return {
|
|
1315
|
-
success: false,
|
|
1316
|
-
message: result.error ?? "Update failed"
|
|
1317
|
-
};
|
|
1318
|
-
} finally {
|
|
1319
|
-
releaseLock();
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
function performRollback() {
|
|
1323
|
-
const currentStaged = getStagedUpdate();
|
|
1324
|
-
if (!currentStaged) {
|
|
1325
|
-
return { success: false, message: "No staged update to roll back." };
|
|
1326
|
-
}
|
|
1327
|
-
clearStagedUpdate();
|
|
1328
|
-
return {
|
|
1329
|
-
success: true,
|
|
1330
|
-
message: `Rolled back from v${currentStaged.version}. Using installed version.`
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
// src/core/marketplace-updater.ts
|
|
1335
|
-
import fs5 from "node:fs";
|
|
1336
|
-
var MARKETPLACE_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
1337
|
-
var INITIAL_DELAY_MS = 30000;
|
|
1338
|
-
var LOCK_STALE_MS = 30 * 60 * 1000;
|
|
1339
|
-
function readState() {
|
|
1340
|
-
try {
|
|
1341
|
-
return JSON.parse(fs5.readFileSync(getMarketplaceUpdateStatePath(), "utf8"));
|
|
1342
|
-
} catch {
|
|
1343
|
-
return {};
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
function writeState(state) {
|
|
1347
|
-
const target = getMarketplaceUpdateStatePath();
|
|
1348
|
-
ensureParentDir(target);
|
|
1349
|
-
fs5.writeFileSync(target, JSON.stringify(state, null, 2), { mode: 384 });
|
|
1350
|
-
}
|
|
1351
|
-
function due(now = Date.now()) {
|
|
1352
|
-
const last = readState().lastRunAt;
|
|
1353
|
-
return !last || !Number.isFinite(Date.parse(last)) || now - Date.parse(last) >= MARKETPLACE_UPDATE_INTERVAL_MS;
|
|
1354
|
-
}
|
|
1355
|
-
function acquireLock2() {
|
|
1356
|
-
const target = getMarketplaceUpdateLockPath();
|
|
1357
|
-
ensureParentDir(target);
|
|
1358
|
-
try {
|
|
1359
|
-
fs5.writeFileSync(target, `${process.pid}
|
|
1360
|
-
`, { flag: "wx", mode: 384 });
|
|
1361
|
-
return true;
|
|
1362
|
-
} catch {
|
|
1363
|
-
try {
|
|
1364
|
-
if (Date.now() - fs5.statSync(target).mtimeMs > LOCK_STALE_MS) {
|
|
1365
|
-
fs5.unlinkSync(target);
|
|
1366
|
-
return acquireLock2();
|
|
1367
|
-
}
|
|
1368
|
-
} catch {
|
|
1369
|
-
return acquireLock2();
|
|
1370
|
-
}
|
|
1371
|
-
return false;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
function releaseLock2() {
|
|
1375
|
-
try {
|
|
1376
|
-
fs5.unlinkSync(getMarketplaceUpdateLockPath());
|
|
1377
|
-
} catch {}
|
|
1378
|
-
}
|
|
1379
|
-
async function runMarketplaceAutoUpdate(options) {
|
|
1380
|
-
if (process.env.MCPX_NO_UPDATE === "1")
|
|
1381
|
-
return { checked: [], errors: [], skipped: true };
|
|
1382
|
-
if (!options?.force && !due())
|
|
1383
|
-
return { checked: [], errors: [], skipped: true };
|
|
1384
|
-
if (!acquireLock2())
|
|
1385
|
-
return { checked: [], errors: [], skipped: true };
|
|
1386
|
-
try {
|
|
1387
|
-
const result = await refreshAutoUpdateMarketplaces();
|
|
1388
|
-
writeState({ lastRunAt: new Date().toISOString() });
|
|
1389
|
-
return result;
|
|
1390
|
-
} finally {
|
|
1391
|
-
releaseLock2();
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
function startMarketplaceAutoUpdater() {
|
|
1395
|
-
if (process.env.MCPX_NO_UPDATE === "1")
|
|
1396
|
-
return () => {};
|
|
1397
|
-
let stopped = false;
|
|
1398
|
-
const run = () => {
|
|
1399
|
-
if (!stopped)
|
|
1400
|
-
runMarketplaceAutoUpdate().catch(() => {
|
|
1401
|
-
return;
|
|
1402
|
-
});
|
|
1403
|
-
};
|
|
1404
|
-
const initial = setTimeout(run, INITIAL_DELAY_MS);
|
|
1405
|
-
initial.unref?.();
|
|
1406
|
-
const interval = setInterval(run, MARKETPLACE_UPDATE_INTERVAL_MS);
|
|
1407
|
-
interval.unref?.();
|
|
1408
|
-
return () => {
|
|
1409
|
-
stopped = true;
|
|
1410
|
-
clearTimeout(initial);
|
|
1411
|
-
clearInterval(interval);
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
// src/core/daemon.ts
|
|
1416
|
-
import fs6 from "node:fs";
|
|
1417
|
-
import net from "node:net";
|
|
1418
|
-
import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
|
|
1419
|
-
|
|
1420
|
-
// src/gateway/server.ts
|
|
1421
|
-
import http2 from "node:http";
|
|
1422
|
-
import { URL as URL2 } from "node:url";
|
|
1423
|
-
import crypto2 from "node:crypto";
|
|
1424
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1425
|
-
import {
|
|
1426
|
-
StdioClientTransport,
|
|
1427
|
-
getDefaultEnvironment
|
|
1428
|
-
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1429
|
-
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1430
|
-
var SERVER_VERSION = APP_VERSION;
|
|
1431
|
-
var DEFAULT_UPSTREAM_TIMEOUT_MS = 60000;
|
|
1432
|
-
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
1433
|
-
var OAUTH_WELL_KNOWN_PREFIXES = [
|
|
1434
|
-
"/.well-known/oauth-protected-resource",
|
|
1435
|
-
"/.well-known/oauth-authorization-server",
|
|
1436
|
-
"/.well-known/openid-configuration"
|
|
1437
|
-
];
|
|
1438
|
-
function makeError(id, code, message, data) {
|
|
1439
|
-
return {
|
|
1440
|
-
jsonrpc: "2.0",
|
|
1441
|
-
id,
|
|
1442
|
-
error: {
|
|
1443
|
-
code,
|
|
1444
|
-
message,
|
|
1445
|
-
data
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
|
-
function makeResult(id, result) {
|
|
1450
|
-
return {
|
|
1451
|
-
jsonrpc: "2.0",
|
|
1452
|
-
id,
|
|
1453
|
-
result
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
function listUpstreams(config, upstreamFilter) {
|
|
1457
|
-
const all = Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => ({ name, spec }));
|
|
1458
|
-
if (!upstreamFilter) {
|
|
1459
|
-
return all;
|
|
1460
|
-
}
|
|
1461
|
-
return all.filter((upstream) => upstream.name === upstreamFilter);
|
|
1462
|
-
}
|
|
1463
|
-
function getSingleUpstream(config) {
|
|
1464
|
-
const upstreams = listUpstreams(config);
|
|
1465
|
-
if (upstreams.length !== 1) {
|
|
1466
|
-
return null;
|
|
1467
|
-
}
|
|
1468
|
-
return upstreams[0] ?? null;
|
|
1469
|
-
}
|
|
1470
|
-
function getSingleHttpUpstream(config) {
|
|
1471
|
-
const upstream = getSingleUpstream(config);
|
|
1472
|
-
if (!upstream || upstream.spec.transport !== "http") {
|
|
1473
|
-
return null;
|
|
1474
|
-
}
|
|
1475
|
-
return upstream;
|
|
1476
|
-
}
|
|
1477
|
-
function getScopedHttpUpstream(config, upstreamFilter) {
|
|
1478
|
-
if (upstreamFilter) {
|
|
1479
|
-
const selected = config.servers[upstreamFilter];
|
|
1480
|
-
if (!selected || !isServerEnabled(selected) || selected.transport !== "http") {
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
|
-
return {
|
|
1484
|
-
name: upstreamFilter,
|
|
1485
|
-
spec: selected
|
|
1486
|
-
};
|
|
1487
|
-
}
|
|
1488
|
-
return getSingleHttpUpstream(config);
|
|
1489
|
-
}
|
|
1490
|
-
function getWellKnownPrefix(pathname) {
|
|
1491
|
-
for (const prefix of OAUTH_WELL_KNOWN_PREFIXES) {
|
|
1492
|
-
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
1493
|
-
return prefix;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
return null;
|
|
1497
|
-
}
|
|
1498
|
-
function getUpstreamPathSuffixForWellKnown(upstream) {
|
|
1499
|
-
const upstreamUrl = new URL2(upstream.spec.url);
|
|
1500
|
-
if (upstreamUrl.pathname === "/") {
|
|
1501
|
-
return "";
|
|
1502
|
-
}
|
|
1503
|
-
return upstreamUrl.pathname.endsWith("/") ? upstreamUrl.pathname.slice(0, -1) : upstreamUrl.pathname;
|
|
1504
|
-
}
|
|
1505
|
-
function buildWellKnownUpstreamUrl(upstream, prefix) {
|
|
1506
|
-
const upstreamUrl = new URL2(upstream.spec.url);
|
|
1507
|
-
const suffix = getUpstreamPathSuffixForWellKnown(upstream);
|
|
1508
|
-
return new URL2(`${prefix}${suffix}`, upstreamUrl.origin);
|
|
1509
|
-
}
|
|
1510
|
-
function getLocalOriginFromRequest(request) {
|
|
1511
|
-
return `http://${request.headers.host ?? "127.0.0.1"}`;
|
|
1512
|
-
}
|
|
1513
|
-
function getRequestedUpstream(requestUrl) {
|
|
1514
|
-
const value = requestUrl.searchParams.get("upstream");
|
|
1515
|
-
if (!value) {
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
|
-
return value.trim() || undefined;
|
|
1519
|
-
}
|
|
1520
|
-
function appendUpstreamQuery(url, upstream) {
|
|
1521
|
-
if (!upstream) {
|
|
1522
|
-
return url;
|
|
1523
|
-
}
|
|
1524
|
-
const parsed = new URL2(url);
|
|
1525
|
-
parsed.searchParams.set("upstream", upstream);
|
|
1526
|
-
return parsed.toString();
|
|
1527
|
-
}
|
|
1528
|
-
function rewriteWwwAuthenticateResourceMetadata(headerValue, localResourceMetadataUrl) {
|
|
1529
|
-
if (headerValue.includes("resource_metadata=")) {
|
|
1530
|
-
return headerValue.replace(/resource_metadata="[^"]*"/, `resource_metadata="${localResourceMetadataUrl}"`);
|
|
1531
|
-
}
|
|
1532
|
-
return `${headerValue}, resource_metadata="${localResourceMetadataUrl}"`;
|
|
1533
|
-
}
|
|
1534
|
-
function isOAuthCapableServer(spec) {
|
|
1535
|
-
if (spec.transport !== "http") {
|
|
1536
|
-
return false;
|
|
1537
|
-
}
|
|
1538
|
-
const headers = spec.headers ?? {};
|
|
1539
|
-
for (const headerName of Object.keys(headers)) {
|
|
1540
|
-
if (headerName.toLowerCase() === "authorization") {
|
|
1541
|
-
return true;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return false;
|
|
1545
|
-
}
|
|
1546
|
-
function splitNamespacedName(value) {
|
|
1547
|
-
const split = value.indexOf(".");
|
|
1548
|
-
if (split <= 0 || split >= value.length - 1) {
|
|
1549
|
-
return null;
|
|
1550
|
-
}
|
|
1551
|
-
return {
|
|
1552
|
-
serverName: value.slice(0, split),
|
|
1553
|
-
upstreamName: value.slice(split + 1)
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
function parseNamespacedUri(uri) {
|
|
1557
|
-
if (uri.startsWith("mcpx://")) {
|
|
1558
|
-
const rest = uri.slice("mcpx://".length);
|
|
1559
|
-
const slashIndex = rest.indexOf("/");
|
|
1560
|
-
if (slashIndex <= 0 || slashIndex >= rest.length - 1) {
|
|
1561
|
-
return null;
|
|
1562
|
-
}
|
|
1563
|
-
const serverName = rest.slice(0, slashIndex);
|
|
1564
|
-
const encoded = rest.slice(slashIndex + 1);
|
|
1565
|
-
try {
|
|
1566
|
-
return {
|
|
1567
|
-
serverName,
|
|
1568
|
-
upstreamUri: decodeURIComponent(encoded)
|
|
1569
|
-
};
|
|
1570
|
-
} catch {
|
|
1571
|
-
return null;
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
const nameSplit = splitNamespacedName(uri);
|
|
1575
|
-
if (!nameSplit) {
|
|
1576
|
-
return null;
|
|
1577
|
-
}
|
|
1578
|
-
return {
|
|
1579
|
-
serverName: nameSplit.serverName,
|
|
1580
|
-
upstreamUri: nameSplit.upstreamName
|
|
1581
|
-
};
|
|
1582
|
-
}
|
|
1583
|
-
async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
|
|
1584
|
-
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
|
|
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
|
-
}
|
|
1609
|
-
async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
1610
|
-
if (!runtime.tokenCache) {
|
|
1611
|
-
runtime.tokenCache = new Map;
|
|
1612
|
-
}
|
|
1613
|
-
const results = {};
|
|
1614
|
-
const upstreams = listUpstreams(config);
|
|
1615
|
-
for (const upstream of upstreams) {
|
|
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);
|
|
1621
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1622
|
-
if (cached?.fingerprint === fingerprint) {
|
|
1623
|
-
const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
|
|
1624
|
-
results[upstream.name] = runtimeErr2 ? { ...cached.count, runtimeError: runtimeErr2.message, runtimeErrorCode: runtimeErr2.code } : cached.count;
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
let toolsCount = 0;
|
|
1628
|
-
let resourcesCount = 0;
|
|
1629
|
-
let promptsCount = 0;
|
|
1630
|
-
const errors = [];
|
|
1631
|
-
try {
|
|
1632
|
-
const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
|
|
1633
|
-
if (toolsResult && toolsResult.tools) {
|
|
1634
|
-
toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
|
|
1635
|
-
}
|
|
1636
|
-
} catch (error) {
|
|
1637
|
-
recordError("tools/list", error);
|
|
1638
|
-
}
|
|
1639
|
-
try {
|
|
1640
|
-
const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
|
|
1641
|
-
if (resourcesResult && resourcesResult.resources) {
|
|
1642
|
-
resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
|
|
1643
|
-
}
|
|
1644
|
-
} catch (error) {
|
|
1645
|
-
recordError("resources/list", error);
|
|
1646
|
-
}
|
|
1647
|
-
try {
|
|
1648
|
-
const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
|
|
1649
|
-
if (promptsResult && promptsResult.prompts) {
|
|
1650
|
-
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
1651
|
-
}
|
|
1652
|
-
} catch (error) {
|
|
1653
|
-
recordError("prompts/list", error);
|
|
1654
|
-
}
|
|
1655
|
-
const total = toolsCount + resourcesCount + promptsCount;
|
|
1656
|
-
const countObj = {
|
|
1657
|
-
tools: toolsCount,
|
|
1658
|
-
resources: resourcesCount,
|
|
1659
|
-
prompts: promptsCount,
|
|
1660
|
-
total,
|
|
1661
|
-
error: errors.length > 0 ? errors.map((e) => `${e.label}: ${e.message}`).join("; ") : undefined,
|
|
1662
|
-
errorCode: pickPrimaryErrorCode(errors)
|
|
1663
|
-
};
|
|
1664
|
-
if (errors.length === 0) {
|
|
1665
|
-
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
1666
|
-
}
|
|
1667
|
-
const runtimeErr = runtime.upstreamErrors?.get(upstream.name);
|
|
1668
|
-
results[upstream.name] = runtimeErr ? { ...countObj, runtimeError: runtimeErr.message, runtimeErrorCode: runtimeErr.code } : countObj;
|
|
1669
|
-
}
|
|
1670
|
-
return results;
|
|
1671
|
-
}
|
|
1672
|
-
function getConfiguredTimeoutMs() {
|
|
1673
|
-
const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
|
|
1674
|
-
return Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_UPSTREAM_TIMEOUT_MS;
|
|
1675
|
-
}
|
|
1676
|
-
function withTimeout(work, timeoutMs, timeoutMessage) {
|
|
1677
|
-
return new Promise((resolve, reject) => {
|
|
1678
|
-
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
1679
|
-
work.then((value) => {
|
|
1680
|
-
clearTimeout(timer);
|
|
1681
|
-
resolve(value);
|
|
1682
|
-
}, (error) => {
|
|
1683
|
-
clearTimeout(timer);
|
|
1684
|
-
reject(error);
|
|
1685
|
-
});
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
function specFingerprint(spec, secrets) {
|
|
1689
|
-
if (!secrets)
|
|
1690
|
-
return JSON.stringify(spec);
|
|
1691
|
-
const resolvedMarkers = {};
|
|
1692
|
-
const collect = (obj) => {
|
|
1693
|
-
if (!obj)
|
|
1694
|
-
return;
|
|
1695
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
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://")) {
|
|
1710
|
-
try {
|
|
1711
|
-
resolvedMarkers[key] = secrets.resolveMaybeSecret(value);
|
|
1712
|
-
} catch {
|
|
1713
|
-
resolvedMarkers[key] = value;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
if (spec.transport === "stdio")
|
|
1719
|
-
collect(spec.env);
|
|
1720
|
-
if (spec.transport === "http")
|
|
1721
|
-
collect(spec.headers);
|
|
1722
|
-
return JSON.stringify({ spec, resolvedMarkers });
|
|
1723
|
-
}
|
|
1724
|
-
function passthroughCacheSuffix(passthroughAuthorizationHeader) {
|
|
1725
|
-
return `passthrough:${crypto2.createHash("sha256").update(passthroughAuthorizationHeader).digest("hex").slice(0, 16)}`;
|
|
1726
|
-
}
|
|
1727
|
-
var EXTRA_INHERITED_ENV = ["TMPDIR", "LANG"];
|
|
1728
|
-
function resolveStdioEnv(spec, secrets) {
|
|
1729
|
-
const env = getDefaultEnvironment();
|
|
1730
|
-
for (const key of EXTRA_INHERITED_ENV) {
|
|
1731
|
-
const value = process.env[key];
|
|
1732
|
-
if (value)
|
|
1733
|
-
env[key] = value;
|
|
1734
|
-
}
|
|
1735
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
1736
|
-
if (key.startsWith("LC_") && value)
|
|
1737
|
-
env[key] = value;
|
|
1738
|
-
}
|
|
1739
|
-
env.PATH = buildEnrichedPath(env.PATH);
|
|
1740
|
-
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
1741
|
-
env[key] = secrets.resolveMaybeSecret(value);
|
|
1742
|
-
}
|
|
1743
|
-
return env;
|
|
1744
|
-
}
|
|
1745
|
-
function buildStdioServerParameters(spec, secrets) {
|
|
1746
|
-
return {
|
|
1747
|
-
command: spec.command,
|
|
1748
|
-
args: spec.args ?? [],
|
|
1749
|
-
cwd: spec.cwd,
|
|
1750
|
-
env: resolveStdioEnv(spec, secrets)
|
|
1751
|
-
};
|
|
1752
|
-
}
|
|
1753
|
-
async function buildHttpHeaders(upstream, secrets, options) {
|
|
1754
|
-
const headers = {};
|
|
1755
|
-
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
1756
|
-
if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
|
|
1757
|
-
const oauthServerName = oauthReferenceServerName(value);
|
|
1758
|
-
const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, {
|
|
1759
|
-
forceRefresh: options.forceOAuthRefresh
|
|
1760
|
-
});
|
|
1761
|
-
headers[key] = `Bearer ${accessToken}`;
|
|
1762
|
-
} else {
|
|
1763
|
-
headers[key] = secrets.resolveMaybeSecret(value);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
if (options.passthroughAuthorizationHeader) {
|
|
1767
|
-
headers.Authorization = options.passthroughAuthorizationHeader;
|
|
1768
|
-
}
|
|
1769
|
-
return headers;
|
|
1770
|
-
}
|
|
1771
|
-
async function closeUpstreamConnection(entry) {
|
|
1772
|
-
try {
|
|
1773
|
-
const connection = await entry.promise;
|
|
1774
|
-
await connection.transport.close();
|
|
1775
|
-
} catch {}
|
|
1776
|
-
}
|
|
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)]));
|
|
1779
|
-
const staleKeys = [];
|
|
1780
|
-
for (const [key, entry] of runtime.upstreamConnections.entries()) {
|
|
1781
|
-
const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
|
1782
|
-
const expectedFingerprint = activeSpecs.get(upstreamName);
|
|
1783
|
-
if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
|
|
1784
|
-
staleKeys.push(key);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
for (const key of staleKeys) {
|
|
1788
|
-
const entry = runtime.upstreamConnections.get(key);
|
|
1789
|
-
if (!entry) {
|
|
1790
|
-
continue;
|
|
1791
|
-
}
|
|
1792
|
-
runtime.upstreamConnections.delete(key);
|
|
1793
|
-
await closeUpstreamConnection(entry);
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
|
|
1797
|
-
const key = passthroughAuthorizationHeader ? `${upstreamName}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstreamName;
|
|
1798
|
-
const existing = runtime.upstreamConnections.get(key);
|
|
1799
|
-
if (!existing) {
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
runtime.upstreamConnections.delete(key);
|
|
1803
|
-
closeUpstreamConnection(existing);
|
|
1804
|
-
}
|
|
1805
|
-
async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
|
|
1806
|
-
const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
|
|
1807
|
-
const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstream.name;
|
|
1808
|
-
const fingerprint = specFingerprint(upstream.spec, secrets);
|
|
1809
|
-
if (!forceOAuthRefresh) {
|
|
1810
|
-
const existing = runtime.upstreamConnections.get(cacheKey);
|
|
1811
|
-
if (existing) {
|
|
1812
|
-
if (existing.fingerprint === fingerprint) {
|
|
1813
|
-
return existing.promise;
|
|
1814
|
-
}
|
|
1815
|
-
runtime.upstreamConnections.delete(cacheKey);
|
|
1816
|
-
closeUpstreamConnection(existing);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
const promise = (async () => {
|
|
1820
|
-
let transport;
|
|
1821
|
-
if (upstream.spec.transport === "stdio") {
|
|
1822
|
-
transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
|
|
1823
|
-
} else {
|
|
1824
|
-
const headers = await buildHttpHeaders(upstream, secrets, { forceOAuthRefresh, passthroughAuthorizationHeader });
|
|
1825
|
-
const capturingFetch = async (url, init) => {
|
|
1826
|
-
const response = await fetch(url, init);
|
|
1827
|
-
const wwwAuth = response.headers.get("www-authenticate");
|
|
1828
|
-
if (wwwAuth) {
|
|
1829
|
-
if (!runtime.lastWwwAuthenticate) {
|
|
1830
|
-
runtime.lastWwwAuthenticate = new Map;
|
|
1831
|
-
}
|
|
1832
|
-
runtime.lastWwwAuthenticate.set(upstream.name, wwwAuth);
|
|
1833
|
-
}
|
|
1834
|
-
return response;
|
|
1835
|
-
};
|
|
1836
|
-
transport = new StreamableHTTPClientTransport(new URL2(upstream.spec.url), { requestInit: { headers }, fetch: capturingFetch });
|
|
1837
|
-
}
|
|
1838
|
-
const client = new Client({ name: "mcpx", version: SERVER_VERSION });
|
|
1839
|
-
await withTimeout(client.connect(transport), DEFAULT_CONNECT_TIMEOUT_MS, `Upstream ${upstream.name} failed to connect within ${DEFAULT_CONNECT_TIMEOUT_MS}ms.`);
|
|
1840
|
-
return { fingerprint, client, transport };
|
|
1841
|
-
})();
|
|
1842
|
-
runtime.upstreamConnections.set(cacheKey, { fingerprint, promise });
|
|
1843
|
-
try {
|
|
1844
|
-
return await promise;
|
|
1845
|
-
} catch (error) {
|
|
1846
|
-
runtime.upstreamConnections.delete(cacheKey);
|
|
1847
|
-
throw error;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
async function callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, forceOAuthRefresh) {
|
|
1851
|
-
const hasOAuth = upstream.spec.transport === "http" && Object.values(upstream.spec.headers ?? {}).some(isOAuthReference);
|
|
1852
|
-
try {
|
|
1853
|
-
const connection = await getUpstreamConnection(upstream, secrets, runtime, {
|
|
1854
|
-
forceOAuthRefresh,
|
|
1855
|
-
passthroughAuthorizationHeader
|
|
1856
|
-
});
|
|
1857
|
-
const timeoutMs = getConfiguredTimeoutMs();
|
|
1858
|
-
const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
|
|
1859
|
-
if (method === "tools/list") {
|
|
1860
|
-
return withTimeout(connection.client.listTools(params), timeoutMs, timeoutMessage);
|
|
1861
|
-
}
|
|
1862
|
-
if (method === "resources/list") {
|
|
1863
|
-
return withTimeout(connection.client.listResources(params), timeoutMs, timeoutMessage);
|
|
1864
|
-
}
|
|
1865
|
-
if (method === "prompts/list") {
|
|
1866
|
-
return withTimeout(connection.client.listPrompts(params), timeoutMs, timeoutMessage);
|
|
1867
|
-
}
|
|
1868
|
-
if (method === "tools/call") {
|
|
1869
|
-
return withTimeout(connection.client.callTool(params), timeoutMs, timeoutMessage);
|
|
1870
|
-
}
|
|
1871
|
-
if (method === "resources/read") {
|
|
1872
|
-
return withTimeout(connection.client.readResource(params), timeoutMs, timeoutMessage);
|
|
1873
|
-
}
|
|
1874
|
-
if (method === "prompts/get") {
|
|
1875
|
-
return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
|
|
1876
|
-
}
|
|
1877
|
-
throw new Error(`Unsupported method: ${method}`);
|
|
1878
|
-
} catch (error) {
|
|
1879
|
-
invalidateUpstreamConnection(upstream.name, passthroughAuthorizationHeader, runtime);
|
|
1880
|
-
if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
1881
|
-
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
|
|
1882
|
-
}
|
|
1883
|
-
const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
|
|
1884
|
-
throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
|
|
1885
|
-
}
|
|
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
|
-
}
|
|
1895
|
-
function authHeaderIsValid(request, expectedToken) {
|
|
1896
|
-
const localTokenHeader = request.headers["x-mcpx-local-token"];
|
|
1897
|
-
if (typeof localTokenHeader === "string" && tokensMatch(localTokenHeader, expectedToken)) {
|
|
1898
|
-
return true;
|
|
1899
|
-
}
|
|
1900
|
-
if (Array.isArray(localTokenHeader) && localTokenHeader.some((value) => tokensMatch(value, expectedToken))) {
|
|
1901
|
-
return true;
|
|
1902
|
-
}
|
|
1903
|
-
const authHeader = request.headers.authorization;
|
|
1904
|
-
if (!authHeader) {
|
|
1905
|
-
return false;
|
|
1906
|
-
}
|
|
1907
|
-
const [scheme, token] = authHeader.split(" ");
|
|
1908
|
-
return scheme === "Bearer" && tokensMatch(token, expectedToken);
|
|
1909
|
-
}
|
|
1910
|
-
function isAuthChallenge(error) {
|
|
1911
|
-
return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
|
|
1912
|
-
}
|
|
1913
|
-
function getClientAuthorizationForUpstream(request, expectedToken) {
|
|
1914
|
-
const authHeader = request.headers.authorization;
|
|
1915
|
-
if (!authHeader) {
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
const [scheme, token] = authHeader.split(" ");
|
|
1919
|
-
if (scheme === "Bearer" && tokensMatch(token, expectedToken)) {
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
return authHeader;
|
|
1923
|
-
}
|
|
1924
|
-
async function handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1925
|
-
const tools = [];
|
|
1926
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
1927
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
1928
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
1929
|
-
for (const upstream of upstreams) {
|
|
1930
|
-
try {
|
|
1931
|
-
const result = await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
1932
|
-
if (!runtime.tokenCache) {
|
|
1933
|
-
runtime.tokenCache = new Map;
|
|
1934
|
-
}
|
|
1935
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1936
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1937
|
-
existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
|
|
1938
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1939
|
-
delete existing.error;
|
|
1940
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
1941
|
-
for (const tool of result.tools ?? []) {
|
|
1942
|
-
const name = typeof tool.name === "string" ? tool.name : "tool";
|
|
1943
|
-
tools.push({
|
|
1944
|
-
...tool,
|
|
1945
|
-
name: flattenNames ? name : `${upstream.name}.${name}`
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
} catch (error) {
|
|
1949
|
-
if (upstreams.length === 1) {
|
|
1950
|
-
throw error;
|
|
1951
|
-
}
|
|
1952
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1953
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
|
|
1957
|
-
return {
|
|
1958
|
-
tools,
|
|
1959
|
-
...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
async function handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1963
|
-
const resources = [];
|
|
1964
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
1965
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
1966
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
1967
|
-
for (const upstream of upstreams) {
|
|
1968
|
-
try {
|
|
1969
|
-
const result = await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
1970
|
-
if (!runtime.tokenCache) {
|
|
1971
|
-
runtime.tokenCache = new Map;
|
|
1972
|
-
}
|
|
1973
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1974
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1975
|
-
existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
|
|
1976
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1977
|
-
delete existing.error;
|
|
1978
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
1979
|
-
for (const resource of result.resources ?? []) {
|
|
1980
|
-
const originalUri = typeof resource.uri === "string" ? resource.uri : "";
|
|
1981
|
-
const originalName = typeof resource.name === "string" ? resource.name : originalUri;
|
|
1982
|
-
resources.push({
|
|
1983
|
-
...resource,
|
|
1984
|
-
name: flattenNames ? originalName : `${upstream.name}.${originalName}`,
|
|
1985
|
-
uri: flattenNames ? originalUri : `mcpx://${upstream.name}/${encodeURIComponent(originalUri)}`
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
} catch (error) {
|
|
1989
|
-
if (upstreams.length === 1) {
|
|
1990
|
-
throw error;
|
|
1991
|
-
}
|
|
1992
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1993
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
|
|
1997
|
-
return {
|
|
1998
|
-
resources,
|
|
1999
|
-
...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
async function handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
2003
|
-
const prompts = [];
|
|
2004
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
2005
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
2006
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
2007
|
-
for (const upstream of upstreams) {
|
|
2008
|
-
try {
|
|
2009
|
-
const result = await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
2010
|
-
if (!runtime.tokenCache) {
|
|
2011
|
-
runtime.tokenCache = new Map;
|
|
2012
|
-
}
|
|
2013
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
2014
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
2015
|
-
existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
|
|
2016
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
2017
|
-
delete existing.error;
|
|
2018
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
2019
|
-
for (const prompt of result.prompts ?? []) {
|
|
2020
|
-
const name = typeof prompt.name === "string" ? prompt.name : "prompt";
|
|
2021
|
-
prompts.push({
|
|
2022
|
-
...prompt,
|
|
2023
|
-
name: flattenNames ? name : `${upstream.name}.${name}`
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
} catch (error) {
|
|
2027
|
-
if (upstreams.length === 1) {
|
|
2028
|
-
throw error;
|
|
2029
|
-
}
|
|
2030
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
2031
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
const failedUpstreams = upstreams.filter((u) => runtime.upstreamErrors?.has(u.name)).map((u) => ({ name: u.name, ...runtime.upstreamErrors.get(u.name) }));
|
|
2035
|
-
return {
|
|
2036
|
-
prompts,
|
|
2037
|
-
...failedUpstreams.length > 0 ? { _meta: { mcpxUpstreamErrors: failedUpstreams } } : {}
|
|
2038
|
-
};
|
|
2039
|
-
}
|
|
2040
|
-
async function routeNamespacedCall(config, method, params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
2041
|
-
if (!params || typeof params !== "object") {
|
|
2042
|
-
return makeError(id, -32602, "Missing params object.");
|
|
2043
|
-
}
|
|
2044
|
-
const upstreamEntries = listUpstreams(config, upstreamFilter);
|
|
2045
|
-
const upstreams = new Map(upstreamEntries.map((entry) => [entry.name, entry]));
|
|
2046
|
-
const flattenedUpstream = upstreamEntries.length === 1 ? upstreamEntries[0] : null;
|
|
2047
|
-
if (method === "tools/call") {
|
|
2048
|
-
const toolName = typeof params.name === "string" ? params.name : "";
|
|
2049
|
-
const split = splitNamespacedName(toolName);
|
|
2050
|
-
if (split && upstreamFilter && split.serverName !== upstreamFilter) {
|
|
2051
|
-
return makeError(id, -32602, `Tool belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
2052
|
-
}
|
|
2053
|
-
let upstream2;
|
|
2054
|
-
let upstreamToolName = toolName;
|
|
2055
|
-
if (split && upstreams.has(split.serverName)) {
|
|
2056
|
-
upstream2 = upstreams.get(split.serverName);
|
|
2057
|
-
upstreamToolName = split.upstreamName;
|
|
2058
|
-
} else if (flattenedUpstream) {
|
|
2059
|
-
upstream2 = flattenedUpstream;
|
|
2060
|
-
}
|
|
2061
|
-
if (!upstream2) {
|
|
2062
|
-
return makeError(id, -32602, "Tool name must be namespaced as <server>.<tool>.");
|
|
2063
|
-
}
|
|
2064
|
-
const upstreamParams2 = {
|
|
2065
|
-
...params,
|
|
2066
|
-
name: upstreamToolName
|
|
2067
|
-
};
|
|
2068
|
-
try {
|
|
2069
|
-
const result = await callUpstream(upstream2, method, upstreamParams2, id, secrets, runtime, clientAuthorizationHeader);
|
|
2070
|
-
runtime.upstreamErrors?.delete(upstream2.name);
|
|
2071
|
-
return makeResult(id, result);
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
if (isAuthChallenge(error)) {
|
|
2074
|
-
throw error;
|
|
2075
|
-
}
|
|
2076
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
2077
|
-
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
2078
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
if (method === "prompts/get") {
|
|
2082
|
-
const promptName = typeof params.name === "string" ? params.name : "";
|
|
2083
|
-
const split = splitNamespacedName(promptName);
|
|
2084
|
-
if (split && upstreamFilter && split.serverName !== upstreamFilter) {
|
|
2085
|
-
return makeError(id, -32602, `Prompt belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
2086
|
-
}
|
|
2087
|
-
let upstream2;
|
|
2088
|
-
let upstreamPromptName = promptName;
|
|
2089
|
-
if (split && upstreams.has(split.serverName)) {
|
|
2090
|
-
upstream2 = upstreams.get(split.serverName);
|
|
2091
|
-
upstreamPromptName = split.upstreamName;
|
|
2092
|
-
} else if (flattenedUpstream) {
|
|
2093
|
-
upstream2 = flattenedUpstream;
|
|
2094
|
-
}
|
|
2095
|
-
if (!upstream2) {
|
|
2096
|
-
return makeError(id, -32602, "Prompt name must be namespaced as <server>.<prompt>.");
|
|
2097
|
-
}
|
|
2098
|
-
const upstreamParams2 = {
|
|
2099
|
-
...params,
|
|
2100
|
-
name: upstreamPromptName
|
|
2101
|
-
};
|
|
2102
|
-
try {
|
|
2103
|
-
const result = await callUpstream(upstream2, method, upstreamParams2, id, secrets, runtime, clientAuthorizationHeader);
|
|
2104
|
-
runtime.upstreamErrors?.delete(upstream2.name);
|
|
2105
|
-
return makeResult(id, result);
|
|
2106
|
-
} catch (error) {
|
|
2107
|
-
if (isAuthChallenge(error)) {
|
|
2108
|
-
throw error;
|
|
2109
|
-
}
|
|
2110
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
2111
|
-
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
2112
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
2116
|
-
const parsed = parseNamespacedUri(uri);
|
|
2117
|
-
if (parsed && upstreamFilter && parsed.serverName !== upstreamFilter) {
|
|
2118
|
-
return makeError(id, -32602, `Resource belongs to upstream ${parsed.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
2119
|
-
}
|
|
2120
|
-
let upstream;
|
|
2121
|
-
let upstreamUri = uri;
|
|
2122
|
-
if (parsed && upstreams.has(parsed.serverName)) {
|
|
2123
|
-
upstream = upstreams.get(parsed.serverName);
|
|
2124
|
-
upstreamUri = parsed.upstreamUri;
|
|
2125
|
-
} else if (flattenedUpstream) {
|
|
2126
|
-
upstream = flattenedUpstream;
|
|
2127
|
-
}
|
|
2128
|
-
if (!upstream) {
|
|
2129
|
-
return makeError(id, -32602, "Resource URI must be namespaced (mcpx://<server>/<encoded-uri>).", { uri });
|
|
2130
|
-
}
|
|
2131
|
-
const upstreamParams = {
|
|
2132
|
-
...params,
|
|
2133
|
-
uri: upstreamUri
|
|
2134
|
-
};
|
|
2135
|
-
try {
|
|
2136
|
-
const result = await callUpstream(upstream, method, upstreamParams, id, secrets, runtime, clientAuthorizationHeader);
|
|
2137
|
-
runtime.upstreamErrors?.delete(upstream.name);
|
|
2138
|
-
return makeResult(id, result);
|
|
2139
|
-
} catch (error) {
|
|
2140
|
-
if (isAuthChallenge(error)) {
|
|
2141
|
-
throw error;
|
|
2142
|
-
}
|
|
2143
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
2144
|
-
runtime.upstreamErrors?.set(upstream.name, { code: errCode, message: error.message });
|
|
2145
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream.name });
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
2149
|
-
const id = request.id ?? null;
|
|
2150
|
-
if (!request.method || typeof request.method !== "string") {
|
|
2151
|
-
return makeError(id, -32600, "Invalid JSON-RPC request: missing method.");
|
|
2152
|
-
}
|
|
2153
|
-
if (request.method === "initialize") {
|
|
2154
|
-
const requestedProtocol = request.params?.protocolVersion;
|
|
2155
|
-
const protocolVersion = typeof requestedProtocol === "string" && requestedProtocol.length > 0 ? requestedProtocol : "2025-11-25";
|
|
2156
|
-
return makeResult(id, {
|
|
2157
|
-
protocolVersion,
|
|
2158
|
-
capabilities: {
|
|
2159
|
-
tools: {},
|
|
2160
|
-
resources: {},
|
|
2161
|
-
prompts: {}
|
|
2162
|
-
},
|
|
2163
|
-
serverInfo: {
|
|
2164
|
-
name: "mcpx",
|
|
2165
|
-
version: SERVER_VERSION
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2168
|
-
}
|
|
2169
|
-
if (request.method === "notifications/initialized") {
|
|
2170
|
-
return null;
|
|
2171
|
-
}
|
|
2172
|
-
if (request.method === "ping") {
|
|
2173
|
-
return makeResult(id, { ok: true });
|
|
2174
|
-
}
|
|
2175
|
-
const config = loadMergedConfig();
|
|
2176
|
-
if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
|
|
2177
|
-
return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
|
|
2178
|
-
}
|
|
2179
|
-
await reconcileUpstreamConnections(config, secrets, runtime);
|
|
2180
|
-
if (request.method === "custom/tokenCounts") {
|
|
2181
|
-
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
2182
|
-
return makeResult(id, result);
|
|
2183
|
-
}
|
|
2184
|
-
if (request.method === "custom/refreshTokenCounts") {
|
|
2185
|
-
runtime.tokenCache?.clear();
|
|
2186
|
-
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
2187
|
-
return makeResult(id, result);
|
|
2188
|
-
}
|
|
2189
|
-
if (request.method === "tools/list") {
|
|
2190
|
-
const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2191
|
-
return makeResult(id, result);
|
|
2192
|
-
}
|
|
2193
|
-
if (request.method === "resources/list") {
|
|
2194
|
-
const result = await handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2195
|
-
return makeResult(id, result);
|
|
2196
|
-
}
|
|
2197
|
-
if (request.method === "prompts/list") {
|
|
2198
|
-
const result = await handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2199
|
-
return makeResult(id, result);
|
|
2200
|
-
}
|
|
2201
|
-
if (request.method === "tools/call" || request.method === "resources/read" || request.method === "prompts/get") {
|
|
2202
|
-
return routeNamespacedCall(config, request.method, request.params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2203
|
-
}
|
|
2204
|
-
return makeError(id, -32601, `Unsupported method: ${request.method}`);
|
|
2205
|
-
}
|
|
2206
|
-
async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, secrets) {
|
|
2207
|
-
const pathname = requestUrl.pathname;
|
|
2208
|
-
const wellKnownPrefix = getWellKnownPrefix(pathname);
|
|
2209
|
-
if (!wellKnownPrefix) {
|
|
2210
|
-
return false;
|
|
2211
|
-
}
|
|
2212
|
-
if (request.method !== "GET") {
|
|
2213
|
-
response.statusCode = 405;
|
|
2214
|
-
response.setHeader("content-type", "application/json");
|
|
2215
|
-
response.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
2216
|
-
return true;
|
|
2217
|
-
}
|
|
2218
|
-
const config = loadMergedConfig();
|
|
2219
|
-
const requestedUpstream = getRequestedUpstream(requestUrl);
|
|
2220
|
-
const upstream = getScopedHttpUpstream(config, requestedUpstream);
|
|
2221
|
-
if (!upstream) {
|
|
2222
|
-
response.statusCode = 404;
|
|
2223
|
-
response.setHeader("content-type", "application/json");
|
|
2224
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
2225
|
-
return true;
|
|
2226
|
-
}
|
|
2227
|
-
if (!isOAuthCapableServer(upstream.spec)) {
|
|
2228
|
-
response.statusCode = 404;
|
|
2229
|
-
response.setHeader("content-type", "application/json");
|
|
2230
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
2231
|
-
return true;
|
|
2232
|
-
}
|
|
2233
|
-
const upstreamWellKnownUrl = buildWellKnownUpstreamUrl(upstream, wellKnownPrefix);
|
|
2234
|
-
const headers = {
|
|
2235
|
-
accept: "application/json"
|
|
2236
|
-
};
|
|
2237
|
-
const protocolVersion = request.headers["mcp-protocol-version"];
|
|
2238
|
-
if (typeof protocolVersion === "string" && protocolVersion.length > 0) {
|
|
2239
|
-
headers["mcp-protocol-version"] = protocolVersion;
|
|
2240
|
-
}
|
|
2241
|
-
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
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);
|
|
2271
|
-
}
|
|
2272
|
-
let bodyText = await upstreamResponse.text();
|
|
2273
|
-
response.statusCode = upstreamResponse.status;
|
|
2274
|
-
const contentType = upstreamResponse.headers.get("content-type");
|
|
2275
|
-
if (contentType) {
|
|
2276
|
-
response.setHeader("content-type", contentType);
|
|
2277
|
-
}
|
|
2278
|
-
const cacheControl = upstreamResponse.headers.get("cache-control");
|
|
2279
|
-
if (cacheControl) {
|
|
2280
|
-
response.setHeader("cache-control", cacheControl);
|
|
2281
|
-
}
|
|
2282
|
-
const wwwAuthenticate = upstreamResponse.headers.get("www-authenticate");
|
|
2283
|
-
if (wwwAuthenticate) {
|
|
2284
|
-
response.setHeader("www-authenticate", wwwAuthenticate);
|
|
2285
|
-
}
|
|
2286
|
-
if (wellKnownPrefix === "/.well-known/oauth-protected-resource" && contentType?.includes("application/json") && upstreamResponse.ok) {
|
|
2287
|
-
try {
|
|
2288
|
-
const parsed = JSON.parse(bodyText);
|
|
2289
|
-
parsed.resource = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/mcp`, requestedUpstream);
|
|
2290
|
-
bodyText = JSON.stringify(parsed);
|
|
2291
|
-
} catch {}
|
|
2292
|
-
}
|
|
2293
|
-
response.end(bodyText);
|
|
2294
|
-
return true;
|
|
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
|
-
}
|
|
2306
|
-
function createGatewayServer(options) {
|
|
2307
|
-
const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
|
|
2308
|
-
const runtime = {
|
|
2309
|
-
upstreamConnections: new Map,
|
|
2310
|
-
upstreamErrors: new Map
|
|
2311
|
-
};
|
|
2312
|
-
const server = http2.createServer(async (request, response) => {
|
|
2313
|
-
let requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
2314
|
-
try {
|
|
2315
|
-
requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
2316
|
-
const upstreamFilter = getRequestedUpstream(requestUrl);
|
|
2317
|
-
if (debug) {
|
|
2318
|
-
console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
|
|
2319
|
-
console.error(`[mcpx gateway] headers=${JSON.stringify(redactSensitiveHeaders(request.headers))}`);
|
|
2320
|
-
}
|
|
2321
|
-
if (await maybeHandleWellKnownOAuthRequest(request, response, requestUrl, options.secrets)) {
|
|
2322
|
-
if (debug) {
|
|
2323
|
-
console.error(`[mcpx gateway] -> ${response.statusCode} (well-known oauth)`);
|
|
2324
|
-
}
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
if (requestUrl.pathname !== "/mcp") {
|
|
2328
|
-
response.statusCode = 404;
|
|
2329
|
-
response.setHeader("content-type", "application/json");
|
|
2330
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
2331
|
-
if (debug) {
|
|
2332
|
-
console.error(`[mcpx gateway] -> 404`);
|
|
2333
|
-
}
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (request.method === "GET") {
|
|
2337
|
-
if (!authHeaderIsValid(request, options.expectedToken)) {
|
|
2338
|
-
response.statusCode = 401;
|
|
2339
|
-
response.setHeader("content-type", "application/json");
|
|
2340
|
-
response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
|
|
2341
|
-
if (debug) {
|
|
2342
|
-
console.error(`[mcpx gateway] -> 401 (GET unauthorized)`);
|
|
2343
|
-
}
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
const wantsStream = (request.headers.accept ?? "").includes("text/event-stream");
|
|
2347
|
-
if (wantsStream && request.headers["mcp-session-id"]) {
|
|
2348
|
-
response.statusCode = 405;
|
|
2349
|
-
response.setHeader("content-type", "application/json");
|
|
2350
|
-
response.end(JSON.stringify({ error: "get_stream_not_supported", message: "mcpx does not support server-initiated SSE streams over GET; use POST for all requests." }));
|
|
2351
|
-
if (debug) {
|
|
2352
|
-
console.error(`[mcpx gateway] -> 405 (GET stream not supported)`);
|
|
2353
|
-
}
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
response.statusCode = 200;
|
|
2357
|
-
response.setHeader("content-type", "application/json");
|
|
2358
|
-
response.end(JSON.stringify({ ok: true, server: "mcpx" }));
|
|
2359
|
-
if (debug) {
|
|
2360
|
-
console.error(`[mcpx gateway] -> 200 (GET ok)`);
|
|
2361
|
-
}
|
|
2362
|
-
return;
|
|
2363
|
-
}
|
|
2364
|
-
if (request.method !== "POST") {
|
|
2365
|
-
response.statusCode = 405;
|
|
2366
|
-
response.end("Method Not Allowed");
|
|
2367
|
-
if (debug) {
|
|
2368
|
-
console.error(`[mcpx gateway] -> 405`);
|
|
2369
|
-
}
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
if (!authHeaderIsValid(request, options.expectedToken)) {
|
|
2373
|
-
response.statusCode = 401;
|
|
2374
|
-
response.setHeader("content-type", "application/json");
|
|
2375
|
-
response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
|
|
2376
|
-
if (debug) {
|
|
2377
|
-
console.error(`[mcpx gateway] -> 401 (POST unauthorized)`);
|
|
2378
|
-
}
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
let body = "";
|
|
2382
|
-
request.setEncoding("utf8");
|
|
2383
|
-
for await (const chunk of request) {
|
|
2384
|
-
body += chunk;
|
|
2385
|
-
if (body.length > 1e7) {
|
|
2386
|
-
response.statusCode = 413;
|
|
2387
|
-
response.end("Payload Too Large");
|
|
2388
|
-
if (debug) {
|
|
2389
|
-
console.error(`[mcpx gateway] -> 413`);
|
|
2390
|
-
}
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
const parsed = JSON.parse(body);
|
|
2395
|
-
const hasInitialize = Array.isArray(parsed) ? parsed.some((item) => item.method === "initialize") : parsed.method === "initialize";
|
|
2396
|
-
const responses = [];
|
|
2397
|
-
const clientAuthorizationHeader = getClientAuthorizationForUpstream(request, options.expectedToken);
|
|
2398
|
-
if (Array.isArray(parsed)) {
|
|
2399
|
-
for (const item of parsed) {
|
|
2400
|
-
if (debug) {
|
|
2401
|
-
console.error(`[mcpx gateway] rpc method=${item.method} id=${item.id ?? "null"}`);
|
|
2402
|
-
}
|
|
2403
|
-
const rpcResponse = await handleRequestObject(item, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2404
|
-
if (debug && rpcResponse?.error) {
|
|
2405
|
-
console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
|
|
2406
|
-
}
|
|
2407
|
-
if (rpcResponse) {
|
|
2408
|
-
responses.push(rpcResponse);
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
} else {
|
|
2412
|
-
if (debug) {
|
|
2413
|
-
console.error(`[mcpx gateway] rpc method=${parsed.method} id=${parsed.id ?? "null"}`);
|
|
2414
|
-
if (parsed.method === "initialize") {
|
|
2415
|
-
console.error(`[mcpx gateway] rpc initialize params=${JSON.stringify(parsed.params ?? {})}`);
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
const rpcResponse = await handleRequestObject(parsed, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2419
|
-
if (debug && rpcResponse?.error) {
|
|
2420
|
-
console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
|
|
2421
|
-
}
|
|
2422
|
-
if (rpcResponse) {
|
|
2423
|
-
responses.push(rpcResponse);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
response.statusCode = 200;
|
|
2427
|
-
const acceptsSse = (request.headers.accept ?? "").includes("text/event-stream");
|
|
2428
|
-
if (acceptsSse) {
|
|
2429
|
-
response.setHeader("content-type", "text/event-stream");
|
|
2430
|
-
response.setHeader("cache-control", "no-cache");
|
|
2431
|
-
response.setHeader("connection", "keep-alive");
|
|
2432
|
-
} else {
|
|
2433
|
-
response.setHeader("content-type", "application/json");
|
|
2434
|
-
}
|
|
2435
|
-
if (hasInitialize) {
|
|
2436
|
-
const sessionId = crypto2.randomUUID();
|
|
2437
|
-
response.setHeader("mcp-session-id", sessionId);
|
|
2438
|
-
response.setHeader("MCP-Session-Id", sessionId);
|
|
2439
|
-
}
|
|
2440
|
-
if (debug) {
|
|
2441
|
-
console.error(`[mcpx gateway] -> 200 (rpc)`);
|
|
2442
|
-
}
|
|
2443
|
-
if (acceptsSse) {
|
|
2444
|
-
const payloads = responses.length > 0 ? responses : [];
|
|
2445
|
-
for (const payload of payloads) {
|
|
2446
|
-
response.write(`event: message
|
|
2447
|
-
`);
|
|
2448
|
-
response.write(`data: ${JSON.stringify(payload)}
|
|
2449
|
-
|
|
2450
|
-
`);
|
|
2451
|
-
}
|
|
2452
|
-
response.end();
|
|
2453
|
-
} else if (responses.length === 1) {
|
|
2454
|
-
response.end(JSON.stringify(responses[0]));
|
|
2455
|
-
} else {
|
|
2456
|
-
response.end(JSON.stringify(responses));
|
|
2457
|
-
}
|
|
2458
|
-
} catch (error) {
|
|
2459
|
-
if (isAuthChallenge(error)) {
|
|
2460
|
-
response.statusCode = error.status ?? 401;
|
|
2461
|
-
response.setHeader("content-type", "application/json");
|
|
2462
|
-
if (error.wwwAuthenticate) {
|
|
2463
|
-
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
2464
|
-
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
2465
|
-
}
|
|
2466
|
-
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
2467
|
-
mcpxCode: error.code,
|
|
2468
|
-
status: error.status,
|
|
2469
|
-
upstream: error.upstream
|
|
2470
|
-
})));
|
|
2471
|
-
if (debug) {
|
|
2472
|
-
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
2473
|
-
}
|
|
2474
|
-
return;
|
|
2475
|
-
}
|
|
2476
|
-
response.statusCode = 500;
|
|
2477
|
-
response.setHeader("content-type", "application/json");
|
|
2478
|
-
response.end(JSON.stringify(makeError(null, -32000, error.message)));
|
|
2479
|
-
if (debug) {
|
|
2480
|
-
console.error(`[mcpx gateway] -> 500 ${error.message}`);
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
});
|
|
2484
|
-
server.on("close", () => {
|
|
2485
|
-
for (const entry of runtime.upstreamConnections.values()) {
|
|
2486
|
-
closeUpstreamConnection(entry);
|
|
2487
|
-
}
|
|
2488
|
-
runtime.upstreamConnections.clear();
|
|
2489
|
-
});
|
|
2490
|
-
server.listen(options.port, "127.0.0.1");
|
|
2491
|
-
return server;
|
|
2492
|
-
}
|
|
2493
|
-
|
|
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
|
-
}
|
|
2502
|
-
function readPidRecordFromFile(pidPath = getPidPath()) {
|
|
2503
|
-
if (!fs6.existsSync(pidPath)) {
|
|
2504
|
-
return null;
|
|
2505
|
-
}
|
|
2506
|
-
const raw = fs6.readFileSync(pidPath, "utf8").trim();
|
|
2507
|
-
if (!raw) {
|
|
2508
|
-
return null;
|
|
2509
|
-
}
|
|
2510
|
-
const [pidRaw, portRaw] = raw.split(":");
|
|
2511
|
-
const pid = Number(pidRaw);
|
|
2512
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2513
|
-
return null;
|
|
2514
|
-
}
|
|
2515
|
-
const port = portRaw ? Number(portRaw) : null;
|
|
2516
|
-
return { pid, port: Number.isFinite(port) && port > 0 ? port : null };
|
|
2517
|
-
}
|
|
2518
|
-
function readPidFromFile(pidPath = getPidPath()) {
|
|
2519
|
-
return readPidRecordFromFile(pidPath)?.pid ?? null;
|
|
2520
|
-
}
|
|
2521
|
-
function processExists(pid) {
|
|
2522
|
-
try {
|
|
2523
|
-
process.kill(pid, 0);
|
|
2524
|
-
return true;
|
|
2525
|
-
} catch {
|
|
2526
|
-
return false;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
async function isPortAvailable(port) {
|
|
2530
|
-
return new Promise((resolve) => {
|
|
2531
|
-
const tester = net.createServer();
|
|
2532
|
-
tester.once("error", () => {
|
|
2533
|
-
resolve(false);
|
|
2534
|
-
});
|
|
2535
|
-
tester.once("listening", () => {
|
|
2536
|
-
tester.close(() => resolve(true));
|
|
2537
|
-
});
|
|
2538
|
-
tester.listen(port, "127.0.0.1");
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
async function resolveGatewayPort(config, secrets) {
|
|
2542
|
-
if (await isPortAvailable(config.gateway.port)) {
|
|
2543
|
-
return { port: config.gateway.port };
|
|
2544
|
-
}
|
|
2545
|
-
for (let offset = 1;offset <= 20; offset += 1) {
|
|
2546
|
-
const candidate = config.gateway.port + offset;
|
|
2547
|
-
if (candidate > 65535) {
|
|
2548
|
-
break;
|
|
2549
|
-
}
|
|
2550
|
-
if (await isPortAvailable(candidate)) {
|
|
2551
|
-
const oldPort = config.gateway.port;
|
|
2552
|
-
config.gateway.port = candidate;
|
|
2553
|
-
saveConfig(config);
|
|
2554
|
-
if (secrets) {
|
|
2555
|
-
try {
|
|
2556
|
-
const summary = syncAllClients(config, secrets);
|
|
2557
|
-
persistSyncState(summary, config);
|
|
2558
|
-
saveConfig(config);
|
|
2559
|
-
} catch {}
|
|
2560
|
-
}
|
|
2561
|
-
return { port: candidate, fellBackFrom: oldPort };
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
2565
|
-
}
|
|
2566
|
-
async function waitForGatewayReady(port, token, timeoutMs = 15000) {
|
|
2567
|
-
const deadline = Date.now() + timeoutMs;
|
|
2568
|
-
let lastError;
|
|
2569
|
-
while (Date.now() < deadline) {
|
|
2570
|
-
try {
|
|
2571
|
-
const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
2572
|
-
method: "GET",
|
|
2573
|
-
headers: {
|
|
2574
|
-
Authorization: `Bearer ${token}`
|
|
2575
|
-
},
|
|
2576
|
-
signal: AbortSignal.timeout(500)
|
|
2577
|
-
});
|
|
2578
|
-
if (response.ok) {
|
|
2579
|
-
return;
|
|
2580
|
-
}
|
|
2581
|
-
lastError = new Error(`Gateway returned HTTP ${response.status}.`);
|
|
2582
|
-
} catch (error) {
|
|
2583
|
-
lastError = error;
|
|
2584
|
-
}
|
|
2585
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2586
|
-
}
|
|
2587
|
-
throw new Error(`Gateway did not become ready within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
2588
|
-
}
|
|
2589
|
-
function getDaemonStatus(config) {
|
|
2590
|
-
const pidPath = getPidPath();
|
|
2591
|
-
const record = readPidRecordFromFile(pidPath);
|
|
2592
|
-
if (!record) {
|
|
2593
|
-
return {
|
|
2594
|
-
running: false,
|
|
2595
|
-
pidFile: pidPath,
|
|
2596
|
-
logFile: getLogPath(),
|
|
2597
|
-
port: config.gateway.port
|
|
2598
|
-
};
|
|
2599
|
-
}
|
|
2600
|
-
const running = processExists(record.pid);
|
|
2601
|
-
const port = record.port ?? config.gateway.port;
|
|
2602
|
-
return {
|
|
2603
|
-
running,
|
|
2604
|
-
pid: record.pid,
|
|
2605
|
-
pidFile: pidPath,
|
|
2606
|
-
logFile: getLogPath(),
|
|
2607
|
-
port,
|
|
2608
|
-
portMismatch: running && record.port !== null && record.port !== config.gateway.port
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
async function startDaemon(config, cliPath, secrets) {
|
|
2612
|
-
const existingStatus = getDaemonStatus(config);
|
|
2613
|
-
if (existingStatus.running && existingStatus.pid) {
|
|
2614
|
-
return {
|
|
2615
|
-
started: false,
|
|
2616
|
-
pid: existingStatus.pid,
|
|
2617
|
-
port: existingStatus.port ?? config.gateway.port,
|
|
2618
|
-
message: "mcpx daemon already running."
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
const { port, fellBackFrom } = await resolveGatewayPort(config, secrets);
|
|
2622
|
-
if (fellBackFrom) {
|
|
2623
|
-
process.stderr.write(`mcpx: port ${fellBackFrom} was unavailable; gateway started on ${port} instead. All client configs were re-synced to the new port.
|
|
2624
|
-
`);
|
|
2625
|
-
}
|
|
2626
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2627
|
-
const pidPath = getPidPath();
|
|
2628
|
-
const logPath = getLogPath();
|
|
2629
|
-
ensureParentDir(pidPath);
|
|
2630
|
-
ensureParentDir(logPath);
|
|
2631
|
-
let logFd = fs6.openSync(logPath, "a", 384);
|
|
2632
|
-
const logStat = fs6.fstatSync(logFd);
|
|
2633
|
-
if (logStat.size > 10 * 1024 * 1024) {
|
|
2634
|
-
fs6.closeSync(logFd);
|
|
2635
|
-
const log1 = `${logPath}.1`;
|
|
2636
|
-
const log2 = `${logPath}.2`;
|
|
2637
|
-
try {
|
|
2638
|
-
if (fs6.existsSync(log2))
|
|
2639
|
-
fs6.unlinkSync(log2);
|
|
2640
|
-
} catch {}
|
|
2641
|
-
try {
|
|
2642
|
-
if (fs6.existsSync(log1))
|
|
2643
|
-
fs6.renameSync(log1, log2);
|
|
2644
|
-
} catch {}
|
|
2645
|
-
try {
|
|
2646
|
-
fs6.renameSync(logPath, log1);
|
|
2647
|
-
} catch {}
|
|
2648
|
-
logFd = fs6.openSync(logPath, "a", 384);
|
|
2649
|
-
}
|
|
2650
|
-
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
2651
|
-
detached: true,
|
|
2652
|
-
stdio: ["ignore", logFd, logFd],
|
|
2653
|
-
env: buildDaemonChildEnv()
|
|
2654
|
-
});
|
|
2655
|
-
child.unref();
|
|
2656
|
-
fs6.writeFileSync(pidPath, `${child.pid}:${port}
|
|
2657
|
-
`, { mode: 384 });
|
|
2658
|
-
startBackgroundUpdateCheck();
|
|
2659
|
-
await waitForGatewayReady(port, token);
|
|
2660
|
-
return {
|
|
2661
|
-
started: true,
|
|
2662
|
-
pid: child.pid ?? -1,
|
|
2663
|
-
port,
|
|
2664
|
-
message: "mcpx daemon started."
|
|
2665
|
-
};
|
|
2666
|
-
}
|
|
2667
|
-
function stopDaemon() {
|
|
2668
|
-
const pidPath = getPidPath();
|
|
2669
|
-
const pid = readPidFromFile(pidPath);
|
|
2670
|
-
if (!pid) {
|
|
2671
|
-
return {
|
|
2672
|
-
stopped: false,
|
|
2673
|
-
message: "mcpx daemon is not running."
|
|
2674
|
-
};
|
|
2675
|
-
}
|
|
2676
|
-
try {
|
|
2677
|
-
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2678
|
-
const looksLikeMcpxDaemon = cmd.includes("daemon run") && (cmd.includes("mcpx") || cmd.includes("cli.js"));
|
|
2679
|
-
if (!looksLikeMcpxDaemon) {
|
|
2680
|
-
fs6.unlinkSync(pidPath);
|
|
2681
|
-
return {
|
|
2682
|
-
stopped: false,
|
|
2683
|
-
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
2684
|
-
};
|
|
2685
|
-
}
|
|
2686
|
-
} catch {
|
|
2687
|
-
try {
|
|
2688
|
-
fs6.unlinkSync(pidPath);
|
|
2689
|
-
} catch {}
|
|
2690
|
-
return {
|
|
2691
|
-
stopped: false,
|
|
2692
|
-
message: `PID ${pid} not found. Pidfile cleaned up.`
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
|
-
try {
|
|
2696
|
-
process.kill(pid, "SIGTERM");
|
|
2697
|
-
} catch {}
|
|
2698
|
-
try {
|
|
2699
|
-
fs6.unlinkSync(pidPath);
|
|
2700
|
-
} catch {}
|
|
2701
|
-
return {
|
|
2702
|
-
stopped: true,
|
|
2703
|
-
message: `Stopped mcpx daemon (pid ${pid}).`
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
async function restartDaemon(config, cliPath, secrets) {
|
|
2707
|
-
stopDaemon();
|
|
2708
|
-
return startDaemon(config, cliPath, secrets);
|
|
2709
|
-
}
|
|
2710
|
-
function readDaemonLogs(maxLines = 200) {
|
|
2711
|
-
const logPath = getLogPath();
|
|
2712
|
-
if (!fs6.existsSync(logPath)) {
|
|
2713
|
-
return "";
|
|
2714
|
-
}
|
|
2715
|
-
const lines = fs6.readFileSync(logPath, "utf8").split(`
|
|
2716
|
-
`);
|
|
2717
|
-
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
2718
|
-
`);
|
|
2719
|
-
}
|
|
2720
|
-
function runDaemonForeground(config, port, secrets) {
|
|
2721
|
-
return new Promise((resolve, reject) => {
|
|
2722
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2723
|
-
const server = createGatewayServer({
|
|
2724
|
-
port,
|
|
2725
|
-
expectedToken: token,
|
|
2726
|
-
secrets
|
|
2727
|
-
});
|
|
2728
|
-
const stopMarketplaceUpdater = startMarketplaceAutoUpdater();
|
|
2729
|
-
const cleanup = () => {
|
|
2730
|
-
stopMarketplaceUpdater();
|
|
2731
|
-
server.close(() => resolve());
|
|
2732
|
-
};
|
|
2733
|
-
process.on("SIGINT", cleanup);
|
|
2734
|
-
process.on("SIGTERM", cleanup);
|
|
2735
|
-
server.on("error", (error) => {
|
|
2736
|
-
reject(error);
|
|
2737
|
-
});
|
|
2738
|
-
});
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
// src/core/status.ts
|
|
2742
|
-
var STATUS_CLIENTS = ["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen", "openclaw", "hermes"];
|
|
2743
|
-
function describeServerTarget(spec) {
|
|
2744
|
-
if (spec.transport === "http") {
|
|
2745
|
-
return spec.url;
|
|
2746
|
-
}
|
|
2747
|
-
const args = (spec.args ?? []).join(" ");
|
|
2748
|
-
const command = `${spec.command}${args ? ` ${args}` : ""}`;
|
|
2749
|
-
return spec.cwd ? `${command} (cwd: ${spec.cwd})` : command;
|
|
2750
|
-
}
|
|
2751
|
-
function deriveClientStatus(managed, configured) {
|
|
2752
|
-
if (configured) {
|
|
2753
|
-
return configured;
|
|
2754
|
-
}
|
|
2755
|
-
return managed ? "SYNCED" : "SKIPPED";
|
|
2756
|
-
}
|
|
2757
|
-
function buildClientMappings(config, managedIndex, serverName) {
|
|
2758
|
-
return STATUS_CLIENTS.map((clientId) => {
|
|
2759
|
-
const configState = config.clients[clientId];
|
|
2760
|
-
const managedClient = managedIndex.managed[clientId];
|
|
2761
|
-
const managedEntry = managedClient?.entries?.[`${serverName} (mcpx)`];
|
|
2762
|
-
const managed = Boolean(managedEntry);
|
|
2763
|
-
return {
|
|
2764
|
-
clientId,
|
|
2765
|
-
managed,
|
|
2766
|
-
status: deriveClientStatus(managed, configState?.status),
|
|
2767
|
-
configPath: managedClient?.configPath ?? configState?.configPath,
|
|
2768
|
-
message: configState?.message,
|
|
2769
|
-
lastSyncAt: managedEntry?.lastSyncedAt ?? configState?.lastSyncAt
|
|
2770
|
-
};
|
|
2771
|
-
});
|
|
2772
|
-
}
|
|
2773
|
-
function buildAuthBindings(spec) {
|
|
2774
|
-
return listAuthBindings(spec).map((binding) => ({
|
|
2775
|
-
kind: binding.kind,
|
|
2776
|
-
key: binding.key,
|
|
2777
|
-
value: binding.value,
|
|
2778
|
-
secretName: secretRefName(binding.value) ?? undefined
|
|
2779
|
-
}));
|
|
2780
|
-
}
|
|
2781
|
-
async function fetchTokenCounts(gatewayUrl, token) {
|
|
2782
|
-
try {
|
|
2783
|
-
const res = await fetch(gatewayUrl, {
|
|
2784
|
-
method: "POST",
|
|
2785
|
-
headers: {
|
|
2786
|
-
"content-type": "application/json",
|
|
2787
|
-
authorization: `Bearer ${token}`
|
|
2788
|
-
},
|
|
2789
|
-
body: JSON.stringify({
|
|
2790
|
-
jsonrpc: "2.0",
|
|
2791
|
-
id: "status-tokens",
|
|
2792
|
-
method: "custom/tokenCounts",
|
|
2793
|
-
params: {}
|
|
2794
|
-
}),
|
|
2795
|
-
signal: AbortSignal.timeout(65000)
|
|
2796
|
-
});
|
|
2797
|
-
if (!res.ok) {
|
|
2798
|
-
return { counts: {}, fetchError: `HTTP ${res.status}` };
|
|
2799
|
-
}
|
|
2800
|
-
const data = await res.json();
|
|
2801
|
-
return { counts: data?.result ?? {} };
|
|
2802
|
-
} catch (error) {
|
|
2803
|
-
return { counts: {}, fetchError: error instanceof Error ? error.message : String(error) };
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
|
|
2807
|
-
const secrets = new SecretsManager;
|
|
2808
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2809
|
-
const gatewayUrl = getGatewayUrl(config);
|
|
2810
|
-
let tokenCounts = {};
|
|
2811
|
-
let tokenCountFetchError;
|
|
2812
|
-
if (daemon.running) {
|
|
2813
|
-
const result = await fetchTokenCounts(gatewayUrl, token);
|
|
2814
|
-
tokenCounts = result.counts;
|
|
2815
|
-
tokenCountFetchError = result.fetchError;
|
|
2816
|
-
}
|
|
2817
|
-
const projectEntries = Object.values(config.projects ?? {});
|
|
2818
|
-
let totalGlobalTokens = 0;
|
|
2819
|
-
const totalProjectTokens = {};
|
|
2820
|
-
const serverNames = Object.keys(config.servers).sort((left, right) => left.localeCompare(right));
|
|
2821
|
-
const servers = serverNames.map((name) => {
|
|
2822
|
-
const spec = config.servers[name];
|
|
2823
|
-
let tokenCount = tokenCounts[name];
|
|
2824
|
-
if (tokenCount?.error && buildAuthBindings(spec).length > 0 && tokenCount.total > 0) {
|
|
2825
|
-
tokenCount = { ...tokenCount, error: undefined };
|
|
2826
|
-
}
|
|
2827
|
-
const health = tokenCount ? tokenCount.error || tokenCount.runtimeError ? "error" : "ok" : "unknown";
|
|
2828
|
-
const serverEntry = {
|
|
2829
|
-
name,
|
|
2830
|
-
enabled: isServerEnabled(spec),
|
|
2831
|
-
transport: spec.transport,
|
|
2832
|
-
target: describeServerTarget(spec),
|
|
2833
|
-
authBindings: buildAuthBindings(spec),
|
|
2834
|
-
clients: buildClientMappings(config, managedIndex, name),
|
|
2835
|
-
tokenCount,
|
|
2836
|
-
health
|
|
2837
|
-
};
|
|
2838
|
-
if (serverEntry.enabled && tokenCount) {
|
|
2839
|
-
totalGlobalTokens += tokenCount.total;
|
|
2840
|
-
for (const projectEntry of projectEntries) {
|
|
2841
|
-
if (!(projectEntry.disabledServers ?? []).includes(name)) {
|
|
2842
|
-
totalProjectTokens[projectEntry.path] = (totalProjectTokens[projectEntry.path] ?? 0) + tokenCount.total;
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
return serverEntry;
|
|
2847
|
-
});
|
|
2848
|
-
return {
|
|
2849
|
-
gatewayUrl,
|
|
2850
|
-
daemon,
|
|
2851
|
-
upstreamCount: servers.length,
|
|
2852
|
-
servers,
|
|
2853
|
-
clients: config.clients,
|
|
2854
|
-
projects: config.projects,
|
|
2855
|
-
totalGlobalTokens,
|
|
2856
|
-
totalProjectTokens
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
|
|
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 };
|