@kwonye/mcpx 0.1.94 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-desktop.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts +2 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts +3 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/cursor.d.ts +2 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/hermes.d.ts +11 -0
- package/dist/adapters/hermes.d.ts.map +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/openclaw.d.ts +11 -0
- package/dist/adapters/openclaw.d.ts.map +1 -0
- package/dist/adapters/opencode.d.ts +2 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/qwen.d.ts.map +1 -1
- package/dist/adapters/utils/index.d.ts +7 -1
- package/dist/adapters/utils/index.d.ts.map +1 -1
- package/dist/adapters/vscode.d.ts +2 -1
- package/dist/adapters/vscode.d.ts.map +1 -1
- package/dist/{cli-154shqmt.js → cli-28fsxhe3.js} +310 -94
- package/dist/{cli-9fmn9y72.js → cli-njtbb11m.js} +104 -28
- package/dist/{cli-csfsg1yf.js → cli-v27bt25d.js} +79 -7
- package/dist/{cli-n8vt375e.js → cli-wxw1t3rr.js} +714 -210
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +359 -72
- package/dist/compat/hermes.d.ts +7 -0
- package/dist/compat/hermes.d.ts.map +1 -0
- package/dist/compat/index.d.ts.map +1 -1
- package/dist/compat/openclaw.d.ts +7 -0
- package/dist/compat/openclaw.d.ts.map +1 -0
- package/dist/{config-dtc3c79c.js → config-5p08cb0c.js} +1 -1
- package/dist/core/add-command.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +30 -0
- package/dist/core/config-store.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts +5 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +15 -66
- package/dist/core/managed-index-lock.d.ts +2 -0
- package/dist/core/managed-index-lock.d.ts.map +1 -0
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/plugin-host.d.ts +2 -0
- package/dist/core/plugin-host.d.ts.map +1 -0
- package/dist/core/plugin-manager.d.ts +19 -6
- package/dist/core/plugin-manager.d.ts.map +1 -1
- package/dist/core/plugin-parse.d.ts.map +1 -1
- package/dist/core/plugin-projections.d.ts.map +1 -1
- package/dist/core/skill-projections.d.ts +3 -0
- package/dist/core/skill-projections.d.ts.map +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/core/update-manager.d.ts.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/plugin-host-954a7tf7.js +129 -0
- package/dist/{plugin-manager-1sjwfnah.js → plugin-manager-m8ar2271.js} +11 -3
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/cli-mxmgxdm2.js +0 -2257
package/dist/cli-mxmgxdm2.js
DELETED
|
@@ -1,2257 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getGatewayUrl,
|
|
3
|
-
persistSyncState,
|
|
4
|
-
syncAllClients
|
|
5
|
-
} from "./cli-n8vt375e.js";
|
|
6
|
-
import {
|
|
7
|
-
loadConfig,
|
|
8
|
-
loadMergedConfig,
|
|
9
|
-
saveConfig
|
|
10
|
-
} from "./cli-csfsg1yf.js";
|
|
11
|
-
import {
|
|
12
|
-
SecretsManager,
|
|
13
|
-
UpstreamError,
|
|
14
|
-
classifyUpstreamError,
|
|
15
|
-
ensureGatewayToken
|
|
16
|
-
} from "./cli-tk8bggg5.js";
|
|
17
|
-
import {
|
|
18
|
-
ensureDir,
|
|
19
|
-
ensureParentDir,
|
|
20
|
-
getLogPath,
|
|
21
|
-
getPidPath,
|
|
22
|
-
getStagedVersionPath,
|
|
23
|
-
getUpdateLockPath,
|
|
24
|
-
getUpdatesDir
|
|
25
|
-
} from "./cli-zjepj3sj.js";
|
|
26
|
-
import {
|
|
27
|
-
isServerEnabled
|
|
28
|
-
} from "./cli-s4gspqdf.js";
|
|
29
|
-
|
|
30
|
-
// src/core/auth-probe.ts
|
|
31
|
-
var JSON_RPC_VERSION = "2.0";
|
|
32
|
-
var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
|
|
33
|
-
var AUTH_KEYWORDS = [
|
|
34
|
-
"unauthorized",
|
|
35
|
-
"unauthenticated",
|
|
36
|
-
"forbidden",
|
|
37
|
-
"authentication",
|
|
38
|
-
"authorization",
|
|
39
|
-
"api key",
|
|
40
|
-
"api_key",
|
|
41
|
-
"apikey",
|
|
42
|
-
"credentials",
|
|
43
|
-
"access denied",
|
|
44
|
-
"auth required",
|
|
45
|
-
"x-api-key",
|
|
46
|
-
"invalid key",
|
|
47
|
-
"missing header",
|
|
48
|
-
"not authenticated",
|
|
49
|
-
"auth token"
|
|
50
|
-
];
|
|
51
|
-
function isAuthRelatedMessage(message) {
|
|
52
|
-
const lower = message.toLowerCase();
|
|
53
|
-
return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
|
|
54
|
-
}
|
|
55
|
-
function resolveHeaders(spec, secrets) {
|
|
56
|
-
const headers = {
|
|
57
|
-
"content-type": "application/json",
|
|
58
|
-
accept: "application/json, text/event-stream"
|
|
59
|
-
};
|
|
60
|
-
for (const [key, value] of Object.entries(spec.headers ?? {})) {
|
|
61
|
-
headers[key] = secrets.resolveMaybeSecret(value);
|
|
62
|
-
}
|
|
63
|
-
return headers;
|
|
64
|
-
}
|
|
65
|
-
function isOAuthLikelyChallenge(wwwAuthenticate) {
|
|
66
|
-
if (!wwwAuthenticate) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
const lower = wwwAuthenticate.toLowerCase();
|
|
70
|
-
return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
|
|
71
|
-
}
|
|
72
|
-
async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS) {
|
|
73
|
-
const timeoutController = new AbortController;
|
|
74
|
-
const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
75
|
-
try {
|
|
76
|
-
const headers = resolveHeaders(spec, secrets);
|
|
77
|
-
const response = await fetch(spec.url, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers,
|
|
80
|
-
body: JSON.stringify({
|
|
81
|
-
jsonrpc: JSON_RPC_VERSION,
|
|
82
|
-
id: "mcpx-auth-probe",
|
|
83
|
-
method: "tools/list",
|
|
84
|
-
params: {}
|
|
85
|
-
}),
|
|
86
|
-
signal: timeoutController.signal
|
|
87
|
-
});
|
|
88
|
-
if (response.status === 401 || response.status === 403) {
|
|
89
|
-
const wwwAuthenticate = response.headers.get("www-authenticate");
|
|
90
|
-
return {
|
|
91
|
-
authRequired: true,
|
|
92
|
-
oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
|
|
93
|
-
status: response.status,
|
|
94
|
-
wwwAuthenticate: wwwAuthenticate ?? undefined
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (response.status === 200) {
|
|
98
|
-
try {
|
|
99
|
-
const body = await response.json();
|
|
100
|
-
const err = body?.error;
|
|
101
|
-
if (err && typeof err === "object" && !("result" in body)) {
|
|
102
|
-
const message = typeof err.message === "string" ? err.message : "";
|
|
103
|
-
if (message && isAuthRelatedMessage(message)) {
|
|
104
|
-
return {
|
|
105
|
-
authRequired: true,
|
|
106
|
-
status: response.status,
|
|
107
|
-
error: `JSON-RPC error: ${message}`
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
authRequired: false,
|
|
115
|
-
status: response.status
|
|
116
|
-
};
|
|
117
|
-
} catch (error) {
|
|
118
|
-
const isAbort = error.name === "AbortError";
|
|
119
|
-
return {
|
|
120
|
-
authRequired: false,
|
|
121
|
-
error: isAbort ? `Auth probe timed out after ${timeoutMs}ms.` : error.message
|
|
122
|
-
};
|
|
123
|
-
} finally {
|
|
124
|
-
clearTimeout(timeoutHandle);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// src/core/registry-client.ts
|
|
129
|
-
var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
130
|
-
async function fetchRegistryServerDetail(name) {
|
|
131
|
-
const encoded = encodeURIComponent(name);
|
|
132
|
-
const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
|
|
133
|
-
headers: { accept: "application/json" }
|
|
134
|
-
});
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
throw new Error(`Registry API error: ${response.status}`);
|
|
137
|
-
}
|
|
138
|
-
const data = await response.json();
|
|
139
|
-
return data.server;
|
|
140
|
-
}
|
|
141
|
-
var PACKAGE_PRIORITY = {
|
|
142
|
-
npm: 1,
|
|
143
|
-
pypi: 2,
|
|
144
|
-
nuget: 3,
|
|
145
|
-
oci: 4,
|
|
146
|
-
mcpb: 5
|
|
147
|
-
};
|
|
148
|
-
var RUNTIME_HINT = {
|
|
149
|
-
npm: "npx",
|
|
150
|
-
pypi: "uvx",
|
|
151
|
-
nuget: "dnx"
|
|
152
|
-
};
|
|
153
|
-
function selectBestPackage(packages = [], remotes = []) {
|
|
154
|
-
const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
|
|
155
|
-
if (stdioPkgs.length > 0) {
|
|
156
|
-
return { kind: "package", package: stdioPkgs[0] };
|
|
157
|
-
}
|
|
158
|
-
const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
|
|
159
|
-
if (httpRemotes.length > 0) {
|
|
160
|
-
return { kind: "remote", remote: httpRemotes[0] };
|
|
161
|
-
}
|
|
162
|
-
if (packages.length > 0) {
|
|
163
|
-
return { kind: "package", package: packages[0] };
|
|
164
|
-
}
|
|
165
|
-
if (remotes.length > 0) {
|
|
166
|
-
return { kind: "remote", remote: remotes[0] };
|
|
167
|
-
}
|
|
168
|
-
throw new Error("Server has no packages or remotes");
|
|
169
|
-
}
|
|
170
|
-
function extractRequiredInputs(option) {
|
|
171
|
-
const inputs = [];
|
|
172
|
-
if (option.kind === "package" && option.package?.environmentVariables) {
|
|
173
|
-
for (const env of option.package.environmentVariables) {
|
|
174
|
-
if (env.isRequired && !env.default) {
|
|
175
|
-
inputs.push({
|
|
176
|
-
name: env.name,
|
|
177
|
-
description: env.description,
|
|
178
|
-
isSecret: env.isSecret ?? false,
|
|
179
|
-
kind: "env"
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
for (const arg of option.package.packageArguments ?? []) {
|
|
184
|
-
if (arg.isRequired && !arg.value && !arg.default) {
|
|
185
|
-
inputs.push({
|
|
186
|
-
name: arg.name ?? arg.valueHint ?? "arg",
|
|
187
|
-
description: arg.description,
|
|
188
|
-
isSecret: false,
|
|
189
|
-
kind: "arg"
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (option.kind === "remote" && option.remote?.headers) {
|
|
195
|
-
for (const header of option.remote.headers) {
|
|
196
|
-
if (header.isRequired && !header.default) {
|
|
197
|
-
inputs.push({
|
|
198
|
-
name: header.name,
|
|
199
|
-
description: header.description,
|
|
200
|
-
isSecret: header.isSecret ?? false,
|
|
201
|
-
kind: "header"
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return inputs;
|
|
207
|
-
}
|
|
208
|
-
function mapRegistryToSpec(_name, option, resolvedValues) {
|
|
209
|
-
if (option.kind === "remote") {
|
|
210
|
-
const headers = {};
|
|
211
|
-
for (const header of option.remote?.headers ?? []) {
|
|
212
|
-
const value = resolvedValues[header.name] ?? header.default;
|
|
213
|
-
if (value)
|
|
214
|
-
headers[header.name] = value;
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
transport: "http",
|
|
218
|
-
url: option.remote.url,
|
|
219
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
const pkg = option.package;
|
|
223
|
-
const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
|
|
224
|
-
const args = [];
|
|
225
|
-
const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
|
|
226
|
-
args.push(identifier);
|
|
227
|
-
for (const arg of pkg.packageArguments ?? []) {
|
|
228
|
-
const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
|
|
229
|
-
if (!value)
|
|
230
|
-
continue;
|
|
231
|
-
if (arg.type === "named" && arg.name) {
|
|
232
|
-
args.push(arg.name, value);
|
|
233
|
-
} else {
|
|
234
|
-
args.push(value);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const env = {};
|
|
238
|
-
for (const envVar of pkg.environmentVariables ?? []) {
|
|
239
|
-
const value = resolvedValues[envVar.name] ?? envVar.default;
|
|
240
|
-
if (value)
|
|
241
|
-
env[envVar.name] = value;
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
transport: "stdio",
|
|
245
|
-
command: runtime,
|
|
246
|
-
args,
|
|
247
|
-
...Object.keys(env).length > 0 ? { env } : {}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// src/core/server-auth.ts
|
|
252
|
-
function normalizeNamePart(value) {
|
|
253
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
254
|
-
}
|
|
255
|
-
function requireNonEmpty(value, label) {
|
|
256
|
-
const normalized = value?.trim();
|
|
257
|
-
if (!normalized) {
|
|
258
|
-
throw new Error(`${label} must be non-empty.`);
|
|
259
|
-
}
|
|
260
|
-
return normalized;
|
|
261
|
-
}
|
|
262
|
-
function toSecretRef(secretName) {
|
|
263
|
-
return `secret://${secretName}`;
|
|
264
|
-
}
|
|
265
|
-
function secretRefName(ref) {
|
|
266
|
-
if (!ref.startsWith("secret://")) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
const name = ref.slice("secret://".length).trim();
|
|
270
|
-
return name.length > 0 ? name : null;
|
|
271
|
-
}
|
|
272
|
-
function defaultAuthSecretName(serverName, target) {
|
|
273
|
-
return `auth_${normalizeNamePart(serverName)}_${target.kind}_${normalizeNamePart(target.key)}`;
|
|
274
|
-
}
|
|
275
|
-
function resolveAuthTarget(spec, header, env) {
|
|
276
|
-
if (spec.transport === "http") {
|
|
277
|
-
if (env) {
|
|
278
|
-
throw new Error("--env is only valid for stdio upstreams.");
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
kind: "header",
|
|
282
|
-
key: requireNonEmpty(header ?? "Authorization", "Header name")
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
if (header) {
|
|
286
|
-
throw new Error("--header is only valid for HTTP upstreams.");
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
kind: "env",
|
|
290
|
-
key: requireNonEmpty(env, "Env var name")
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function applyAuthReference(spec, target, secretRef) {
|
|
294
|
-
if (target.kind === "header") {
|
|
295
|
-
if (spec.transport !== "http") {
|
|
296
|
-
throw new Error(`Cannot set header auth on ${spec.transport} upstream.`);
|
|
297
|
-
}
|
|
298
|
-
const headers = { ...spec.headers ?? {} };
|
|
299
|
-
headers[target.key] = secretRef;
|
|
300
|
-
spec.headers = headers;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (spec.transport !== "stdio") {
|
|
304
|
-
throw new Error(`Cannot set env auth on ${spec.transport} upstream.`);
|
|
305
|
-
}
|
|
306
|
-
const env = { ...spec.env ?? {} };
|
|
307
|
-
env[target.key] = secretRef;
|
|
308
|
-
spec.env = env;
|
|
309
|
-
}
|
|
310
|
-
function removeAuthReference(spec, target) {
|
|
311
|
-
if (target.kind === "header") {
|
|
312
|
-
if (spec.transport !== "http") {
|
|
313
|
-
throw new Error(`Cannot remove header auth from ${spec.transport} upstream.`);
|
|
314
|
-
}
|
|
315
|
-
if (!spec.headers || !(target.key in spec.headers)) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
const previous2 = spec.headers[target.key] ?? null;
|
|
319
|
-
const next2 = { ...spec.headers };
|
|
320
|
-
delete next2[target.key];
|
|
321
|
-
spec.headers = Object.keys(next2).length > 0 ? next2 : undefined;
|
|
322
|
-
return previous2;
|
|
323
|
-
}
|
|
324
|
-
if (spec.transport !== "stdio") {
|
|
325
|
-
throw new Error(`Cannot remove env auth from ${spec.transport} upstream.`);
|
|
326
|
-
}
|
|
327
|
-
if (!spec.env || !(target.key in spec.env)) {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
const previous = spec.env[target.key] ?? null;
|
|
331
|
-
const next = { ...spec.env };
|
|
332
|
-
delete next[target.key];
|
|
333
|
-
spec.env = Object.keys(next).length > 0 ? next : undefined;
|
|
334
|
-
return previous;
|
|
335
|
-
}
|
|
336
|
-
function listAuthBindings(spec) {
|
|
337
|
-
if (spec.transport === "http") {
|
|
338
|
-
return Object.entries(spec.headers ?? {}).map(([key, value]) => ({
|
|
339
|
-
kind: "header",
|
|
340
|
-
key,
|
|
341
|
-
value
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
return Object.entries(spec.env ?? {}).map(([key, value]) => ({
|
|
345
|
-
kind: "env",
|
|
346
|
-
key,
|
|
347
|
-
value
|
|
348
|
-
}));
|
|
349
|
-
}
|
|
350
|
-
var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
351
|
-
function maybePrefixBearer(target, value, raw = false) {
|
|
352
|
-
if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
|
|
353
|
-
return value;
|
|
354
|
-
}
|
|
355
|
-
if (/^\S+\s+\S+/.test(value)) {
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
if (TOKEN68.test(value)) {
|
|
359
|
-
return `Bearer ${value}`;
|
|
360
|
-
}
|
|
361
|
-
return value;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// src/core/spawn-env.ts
|
|
365
|
-
import os from "node:os";
|
|
366
|
-
import path from "node:path";
|
|
367
|
-
import fs from "node:fs";
|
|
368
|
-
function existingNvmCurrentBin() {
|
|
369
|
-
const nvmDir = process.env.NVM_DIR ?? path.join(os.homedir(), ".nvm");
|
|
370
|
-
const current = path.join(nvmDir, "current", "bin");
|
|
371
|
-
return fs.existsSync(current) ? current : null;
|
|
372
|
-
}
|
|
373
|
-
function buildEnrichedPath(current = process.env.PATH ?? "") {
|
|
374
|
-
const candidates = [
|
|
375
|
-
...current.split(":"),
|
|
376
|
-
"/opt/homebrew/bin",
|
|
377
|
-
"/usr/local/bin",
|
|
378
|
-
path.join(os.homedir(), ".bun", "bin"),
|
|
379
|
-
path.join(os.homedir(), ".local", "bin"),
|
|
380
|
-
existingNvmCurrentBin()
|
|
381
|
-
].filter((entry) => Boolean(entry));
|
|
382
|
-
const seen = new Set;
|
|
383
|
-
const entries = [];
|
|
384
|
-
for (const candidate of candidates) {
|
|
385
|
-
if (!candidate || seen.has(candidate)) {
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
seen.add(candidate);
|
|
389
|
-
entries.push(candidate);
|
|
390
|
-
}
|
|
391
|
-
return entries.join(":");
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// src/core/oauth.ts
|
|
395
|
-
import http from "node:http";
|
|
396
|
-
import crypto from "node:crypto";
|
|
397
|
-
import {
|
|
398
|
-
auth,
|
|
399
|
-
discoverOAuthServerInfo,
|
|
400
|
-
refreshAuthorization
|
|
401
|
-
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
402
|
-
function oauthSecretName(serverName, suffix) {
|
|
403
|
-
return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
|
|
404
|
-
}
|
|
405
|
-
function readJsonSecret(secrets, name) {
|
|
406
|
-
const raw = secrets.getSecret(name);
|
|
407
|
-
if (!raw) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
return JSON.parse(raw);
|
|
412
|
-
} catch {
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function writeJsonSecret(secrets, name, value) {
|
|
417
|
-
secrets.setSecret(name, JSON.stringify(value));
|
|
418
|
-
}
|
|
419
|
-
function isOAuthReference(value) {
|
|
420
|
-
return value.startsWith("oauth://");
|
|
421
|
-
}
|
|
422
|
-
function oauthReferenceServerName(value) {
|
|
423
|
-
if (!isOAuthReference(value)) {
|
|
424
|
-
throw new Error(`Not an OAuth reference: ${value}`);
|
|
425
|
-
}
|
|
426
|
-
return decodeURIComponent(value.slice("oauth://".length));
|
|
427
|
-
}
|
|
428
|
-
function tokensAreExpiring(tokens) {
|
|
429
|
-
if (!tokens.tokens.expires_in) {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
|
|
433
|
-
return Date.now() >= expiresAt - 60000;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
class McpxOAuthProvider {
|
|
437
|
-
#serverName;
|
|
438
|
-
#secrets;
|
|
439
|
-
#redirectUrlValue;
|
|
440
|
-
#stateValue;
|
|
441
|
-
constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
|
|
442
|
-
this.#serverName = serverName;
|
|
443
|
-
this.#secrets = secrets;
|
|
444
|
-
this.#redirectUrlValue = redirectUrl;
|
|
445
|
-
this.#stateValue = state;
|
|
446
|
-
}
|
|
447
|
-
get redirectUrl() {
|
|
448
|
-
return this.#redirectUrlValue;
|
|
449
|
-
}
|
|
450
|
-
get clientMetadata() {
|
|
451
|
-
if (!this.#redirectUrlValue) {
|
|
452
|
-
return {
|
|
453
|
-
redirect_uris: [],
|
|
454
|
-
token_endpoint_auth_method: "none",
|
|
455
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
456
|
-
response_types: ["code"],
|
|
457
|
-
client_name: "mcpx"
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
redirect_uris: [this.#redirectUrlValue],
|
|
462
|
-
token_endpoint_auth_method: "none",
|
|
463
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
464
|
-
response_types: ["code"],
|
|
465
|
-
client_name: "mcpx"
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
state() {
|
|
469
|
-
return this.#stateValue;
|
|
470
|
-
}
|
|
471
|
-
clientInformation() {
|
|
472
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
|
|
473
|
-
}
|
|
474
|
-
saveClientInformation(clientInformation) {
|
|
475
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
|
|
476
|
-
}
|
|
477
|
-
tokens() {
|
|
478
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
|
|
479
|
-
}
|
|
480
|
-
saveTokens(tokens) {
|
|
481
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
482
|
-
tokens,
|
|
483
|
-
obtainedAt: Date.now()
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
redirectToAuthorization(_authorizationUrl) {}
|
|
487
|
-
saveCodeVerifier(codeVerifier) {
|
|
488
|
-
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
489
|
-
}
|
|
490
|
-
codeVerifier() {
|
|
491
|
-
const verifier = this.#secrets.getSecret(oauthSecretName(this.#serverName, "verifier"));
|
|
492
|
-
if (!verifier) {
|
|
493
|
-
throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
|
|
494
|
-
}
|
|
495
|
-
return verifier;
|
|
496
|
-
}
|
|
497
|
-
saveDiscoveryState(state) {
|
|
498
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
|
|
499
|
-
}
|
|
500
|
-
discoveryState() {
|
|
501
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
|
|
502
|
-
}
|
|
503
|
-
invalidateCredentials(scope) {
|
|
504
|
-
const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
|
|
505
|
-
for (const suffix of suffixes) {
|
|
506
|
-
this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
class InteractiveOAuthProvider extends McpxOAuthProvider {
|
|
512
|
-
#openUrl;
|
|
513
|
-
#cachedClientInfo;
|
|
514
|
-
#cachedCodeVerifier;
|
|
515
|
-
constructor(serverName, secrets, redirectUrl, state, openUrl) {
|
|
516
|
-
super(serverName, secrets, redirectUrl, state);
|
|
517
|
-
this.#openUrl = openUrl;
|
|
518
|
-
}
|
|
519
|
-
async redirectToAuthorization(authorizationUrl) {
|
|
520
|
-
await this.#openUrl(authorizationUrl.toString());
|
|
521
|
-
}
|
|
522
|
-
clientInformation() {
|
|
523
|
-
this.#cachedClientInfo ??= super.clientInformation();
|
|
524
|
-
return this.#cachedClientInfo;
|
|
525
|
-
}
|
|
526
|
-
saveClientInformation(clientInformation) {
|
|
527
|
-
super.saveClientInformation(clientInformation);
|
|
528
|
-
this.#cachedClientInfo = clientInformation;
|
|
529
|
-
}
|
|
530
|
-
codeVerifier() {
|
|
531
|
-
if (this.#cachedCodeVerifier !== undefined) {
|
|
532
|
-
return this.#cachedCodeVerifier;
|
|
533
|
-
}
|
|
534
|
-
return super.codeVerifier();
|
|
535
|
-
}
|
|
536
|
-
saveCodeVerifier(codeVerifier) {
|
|
537
|
-
super.saveCodeVerifier(codeVerifier);
|
|
538
|
-
this.#cachedCodeVerifier = codeVerifier;
|
|
539
|
-
}
|
|
540
|
-
invalidateCredentials(scope) {
|
|
541
|
-
super.invalidateCredentials(scope);
|
|
542
|
-
if (scope === "all" || scope === "client") {
|
|
543
|
-
this.#cachedClientInfo = undefined;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
function listen(server) {
|
|
548
|
-
return new Promise((resolve, reject) => {
|
|
549
|
-
server.once("error", reject);
|
|
550
|
-
server.listen(0, "127.0.0.1", () => {
|
|
551
|
-
const address = server.address();
|
|
552
|
-
if (!address || typeof address === "string") {
|
|
553
|
-
reject(new Error("Failed to start OAuth callback server."));
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
resolve(address.port);
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
function closeServer(server) {
|
|
561
|
-
return new Promise((resolve) => {
|
|
562
|
-
server.close(() => resolve());
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
function waitForAuthorizationCode(server, expectedState, timeoutMs = 120000) {
|
|
566
|
-
return new Promise((resolve, reject) => {
|
|
567
|
-
const timeout = setTimeout(() => {
|
|
568
|
-
reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
|
|
569
|
-
}, timeoutMs);
|
|
570
|
-
server.on("request", (request, response) => {
|
|
571
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
572
|
-
const code = url.searchParams.get("code");
|
|
573
|
-
const state = url.searchParams.get("state");
|
|
574
|
-
const error = url.searchParams.get("error");
|
|
575
|
-
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
576
|
-
if (error) {
|
|
577
|
-
response.statusCode = 400;
|
|
578
|
-
response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
|
|
579
|
-
clearTimeout(timeout);
|
|
580
|
-
reject(new Error(`OAuth authorization failed: ${error}`));
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (!code) {
|
|
584
|
-
response.statusCode = 400;
|
|
585
|
-
response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (state !== expectedState) {
|
|
589
|
-
response.statusCode = 400;
|
|
590
|
-
response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
|
|
591
|
-
clearTimeout(timeout);
|
|
592
|
-
reject(new Error("OAuth state mismatch."));
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
|
|
596
|
-
clearTimeout(timeout);
|
|
597
|
-
resolve(code);
|
|
598
|
-
});
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
function bindOAuthReference(config, serverName) {
|
|
602
|
-
const spec = config.servers[serverName];
|
|
603
|
-
if (!spec) {
|
|
604
|
-
throw new Error(`Server "${serverName}" not found.`);
|
|
605
|
-
}
|
|
606
|
-
if (spec.transport !== "http") {
|
|
607
|
-
throw new Error(`OAuth login only supports HTTP servers.`);
|
|
608
|
-
}
|
|
609
|
-
spec.headers = {
|
|
610
|
-
...spec.headers ?? {},
|
|
611
|
-
Authorization: `oauth://${encodeURIComponent(serverName)}`
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver) {
|
|
615
|
-
const oauthName = oauthSecretName(serverName, "tokens");
|
|
616
|
-
const clientName = oauthSecretName(serverName, "client");
|
|
617
|
-
const backupTokens = secrets.getSecret(oauthName);
|
|
618
|
-
const backupClient = secrets.getSecret(clientName);
|
|
619
|
-
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
620
|
-
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
|
|
621
|
-
const state = crypto.randomUUID();
|
|
622
|
-
let callbackServer;
|
|
623
|
-
let redirectUrl;
|
|
624
|
-
let codePromise;
|
|
625
|
-
if (codeReceiver) {
|
|
626
|
-
redirectUrl = codeReceiver.redirectUrl;
|
|
627
|
-
codePromise = codeReceiver.waitForCode(state);
|
|
628
|
-
} else {
|
|
629
|
-
callbackServer = http.createServer();
|
|
630
|
-
const port = await listen(callbackServer);
|
|
631
|
-
redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
632
|
-
codePromise = waitForAuthorizationCode(callbackServer, state);
|
|
633
|
-
}
|
|
634
|
-
const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, openUrl);
|
|
635
|
-
try {
|
|
636
|
-
const initial = await auth(provider, {
|
|
637
|
-
serverUrl: spec.url
|
|
638
|
-
});
|
|
639
|
-
if (initial !== "REDIRECT") {
|
|
640
|
-
throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
|
|
641
|
-
}
|
|
642
|
-
const authorizationCode = await codePromise;
|
|
643
|
-
const result = await auth(provider, {
|
|
644
|
-
serverUrl: spec.url,
|
|
645
|
-
authorizationCode
|
|
646
|
-
});
|
|
647
|
-
if (result !== "AUTHORIZED") {
|
|
648
|
-
throw new Error(`OAuth login did not authorize "${serverName}".`);
|
|
649
|
-
}
|
|
650
|
-
const config = loadConfig(configPath);
|
|
651
|
-
bindOAuthReference(config, serverName);
|
|
652
|
-
saveConfig(config, configPath);
|
|
653
|
-
const summary = syncAllClients(config, secrets);
|
|
654
|
-
persistSyncState(summary, config);
|
|
655
|
-
saveConfig(config, configPath);
|
|
656
|
-
return { serverName, authorized: true };
|
|
657
|
-
} catch (error) {
|
|
658
|
-
if (backupTokens)
|
|
659
|
-
secrets.setSecret(oauthName, backupTokens);
|
|
660
|
-
if (backupClient)
|
|
661
|
-
secrets.setSecret(clientName, backupClient);
|
|
662
|
-
throw error;
|
|
663
|
-
} finally {
|
|
664
|
-
if (callbackServer) {
|
|
665
|
-
await closeServer(callbackServer);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
670
|
-
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
671
|
-
if (!stored) {
|
|
672
|
-
throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
|
|
673
|
-
}
|
|
674
|
-
if (options.forceRefresh || tokensAreExpiring(stored)) {
|
|
675
|
-
if (!stored.tokens.refresh_token) {
|
|
676
|
-
if (!options.forceRefresh) {
|
|
677
|
-
return stored.tokens.access_token;
|
|
678
|
-
}
|
|
679
|
-
throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
|
|
680
|
-
}
|
|
681
|
-
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
682
|
-
const clientInformation = provider.clientInformation();
|
|
683
|
-
if (!clientInformation) {
|
|
684
|
-
throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
|
|
685
|
-
}
|
|
686
|
-
let serverInfo;
|
|
687
|
-
try {
|
|
688
|
-
const cachedDiscovery = provider.discoveryState();
|
|
689
|
-
serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
690
|
-
} catch (err) {
|
|
691
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
692
|
-
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
693
|
-
throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
|
|
694
|
-
}
|
|
695
|
-
throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
|
|
696
|
-
}
|
|
697
|
-
try {
|
|
698
|
-
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
699
|
-
metadata: serverInfo.authorizationServerMetadata,
|
|
700
|
-
clientInformation,
|
|
701
|
-
refreshToken: stored.tokens.refresh_token,
|
|
702
|
-
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
703
|
-
});
|
|
704
|
-
provider.saveTokens(refreshed);
|
|
705
|
-
return refreshed.access_token;
|
|
706
|
-
} catch (err) {
|
|
707
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
-
if (/invalid_grant|invalid_client|4\d{2}/i.test(message)) {
|
|
709
|
-
throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
|
|
710
|
-
}
|
|
711
|
-
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
712
|
-
throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
|
|
713
|
-
}
|
|
714
|
-
throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
return stored.tokens.access_token;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// src/version.ts
|
|
721
|
-
var APP_VERSION = "0.1.94";
|
|
722
|
-
|
|
723
|
-
// src/core/update.ts
|
|
724
|
-
import fs2 from "node:fs";
|
|
725
|
-
import path2 from "node:path";
|
|
726
|
-
import { execFileSync } from "node:child_process";
|
|
727
|
-
function compareVersions(a, b) {
|
|
728
|
-
const partsA = a.split(".").map(Number);
|
|
729
|
-
const partsB = b.split(".").map(Number);
|
|
730
|
-
for (let i = 0;i < 3; i += 1) {
|
|
731
|
-
const numA = partsA[i] ?? 0;
|
|
732
|
-
const numB = partsB[i] ?? 0;
|
|
733
|
-
if (numA > numB)
|
|
734
|
-
return 1;
|
|
735
|
-
if (numA < numB)
|
|
736
|
-
return -1;
|
|
737
|
-
}
|
|
738
|
-
return 0;
|
|
739
|
-
}
|
|
740
|
-
async function checkForUpdates() {
|
|
741
|
-
try {
|
|
742
|
-
const output = execFileSync("npm", ["view", "@kwonye/mcpx", "version"], {
|
|
743
|
-
encoding: "utf8",
|
|
744
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
745
|
-
}).trim();
|
|
746
|
-
const latestVersion = output;
|
|
747
|
-
const updateAvailable = compareVersions(latestVersion, APP_VERSION) > 0;
|
|
748
|
-
return {
|
|
749
|
-
currentVersion: APP_VERSION,
|
|
750
|
-
latestVersion,
|
|
751
|
-
updateAvailable
|
|
752
|
-
};
|
|
753
|
-
} catch (error) {
|
|
754
|
-
return {
|
|
755
|
-
currentVersion: APP_VERSION,
|
|
756
|
-
latestVersion: null,
|
|
757
|
-
updateAvailable: false,
|
|
758
|
-
error: error instanceof Error ? error.message : String(error)
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
function getStagedUpdate() {
|
|
763
|
-
const stagedPath = getStagedVersionPath();
|
|
764
|
-
if (!fs2.existsSync(stagedPath)) {
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
try {
|
|
768
|
-
const raw = fs2.readFileSync(stagedPath, "utf8");
|
|
769
|
-
const info = JSON.parse(raw);
|
|
770
|
-
if (!info.version || !info.cliPath) {
|
|
771
|
-
return null;
|
|
772
|
-
}
|
|
773
|
-
const absoluteCliPath = path2.isAbsolute(info.cliPath) ? info.cliPath : path2.join(getUpdatesDir(), info.cliPath);
|
|
774
|
-
if (!fs2.existsSync(absoluteCliPath)) {
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
return {
|
|
778
|
-
...info,
|
|
779
|
-
cliPath: absoluteCliPath
|
|
780
|
-
};
|
|
781
|
-
} catch {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
function stageUpdate(version, cliPath) {
|
|
786
|
-
const updatesDir = getUpdatesDir();
|
|
787
|
-
ensureDir(updatesDir);
|
|
788
|
-
const stagedInfo = {
|
|
789
|
-
version,
|
|
790
|
-
cliPath: path2.isAbsolute(cliPath) ? path2.relative(updatesDir, cliPath) : cliPath,
|
|
791
|
-
stagedAt: new Date().toISOString()
|
|
792
|
-
};
|
|
793
|
-
fs2.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
794
|
-
}
|
|
795
|
-
function shouldUseStagedCli(stagedVersion, currentVersion) {
|
|
796
|
-
return compareVersions(stagedVersion, currentVersion) > 0;
|
|
797
|
-
}
|
|
798
|
-
function clearStagedUpdate() {
|
|
799
|
-
const stagedPath = getStagedVersionPath();
|
|
800
|
-
if (fs2.existsSync(stagedPath)) {
|
|
801
|
-
fs2.unlinkSync(stagedPath);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function getStagedCliPath() {
|
|
805
|
-
const staged = getStagedUpdate();
|
|
806
|
-
return staged?.cliPath ?? null;
|
|
807
|
-
}
|
|
808
|
-
function removeOldVersions(keepCount = 2) {
|
|
809
|
-
const updatesDir = getUpdatesDir();
|
|
810
|
-
if (!fs2.existsSync(updatesDir)) {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
const entries = fs2.readdirSync(updatesDir, { withFileTypes: true });
|
|
814
|
-
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)));
|
|
815
|
-
if (versionDirs.length <= keepCount) {
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
819
|
-
const oldPath = path2.join(updatesDir, oldVersion);
|
|
820
|
-
try {
|
|
821
|
-
fs2.rmSync(oldPath, { recursive: true, force: true });
|
|
822
|
-
} catch {}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// src/core/update-manager.ts
|
|
827
|
-
import fs3 from "node:fs";
|
|
828
|
-
import path3 from "node:path";
|
|
829
|
-
import { execSync, spawn } from "node:child_process";
|
|
830
|
-
function acquireLock() {
|
|
831
|
-
const lockPath = getUpdateLockPath();
|
|
832
|
-
try {
|
|
833
|
-
ensureDir(path3.dirname(lockPath));
|
|
834
|
-
fs3.writeFileSync(lockPath, `${process.pid}
|
|
835
|
-
`, { flag: "wx" });
|
|
836
|
-
return true;
|
|
837
|
-
} catch {
|
|
838
|
-
return false;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function releaseLock() {
|
|
842
|
-
const lockPath = getUpdateLockPath();
|
|
843
|
-
try {
|
|
844
|
-
fs3.unlinkSync(lockPath);
|
|
845
|
-
} catch {}
|
|
846
|
-
}
|
|
847
|
-
function isUpdateInProgress() {
|
|
848
|
-
const lockPath = getUpdateLockPath();
|
|
849
|
-
if (!fs3.existsSync(lockPath)) {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
const raw = fs3.readFileSync(lockPath, "utf8").trim();
|
|
854
|
-
const pid = Number(raw);
|
|
855
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
856
|
-
fs3.unlinkSync(lockPath);
|
|
857
|
-
return false;
|
|
858
|
-
}
|
|
859
|
-
process.kill(pid, 0);
|
|
860
|
-
return true;
|
|
861
|
-
} catch {
|
|
862
|
-
fs3.unlinkSync(lockPath);
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
async function downloadAndStageUpdate() {
|
|
867
|
-
const updateStatus = await checkForUpdates();
|
|
868
|
-
if (!updateStatus.latestVersion || !updateStatus.updateAvailable) {
|
|
869
|
-
return { success: false, error: "No update available" };
|
|
870
|
-
}
|
|
871
|
-
const targetVersion = updateStatus.latestVersion;
|
|
872
|
-
const updatesDir = getUpdatesDir();
|
|
873
|
-
const versionDir = path3.join(updatesDir, `v${targetVersion}`);
|
|
874
|
-
if (fs3.existsSync(versionDir)) {
|
|
875
|
-
const existingStaged = getStagedUpdate();
|
|
876
|
-
if (existingStaged?.version === targetVersion) {
|
|
877
|
-
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
try {
|
|
881
|
-
ensureDir(versionDir);
|
|
882
|
-
execSync(`npm pack @kwonye/mcpx@${targetVersion} --json`, {
|
|
883
|
-
cwd: versionDir,
|
|
884
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
885
|
-
});
|
|
886
|
-
const tarballPath = path3.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
887
|
-
if (!fs3.existsSync(tarballPath)) {
|
|
888
|
-
throw new Error("Downloaded tarball not found");
|
|
889
|
-
}
|
|
890
|
-
execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
|
|
891
|
-
cwd: versionDir,
|
|
892
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
893
|
-
});
|
|
894
|
-
execSync("npm install --production --ignore-scripts", {
|
|
895
|
-
cwd: versionDir,
|
|
896
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
897
|
-
});
|
|
898
|
-
const cliPath = path3.join(versionDir, "dist", "cli.js");
|
|
899
|
-
if (!fs3.existsSync(cliPath)) {
|
|
900
|
-
throw new Error("CLI entry point not found in downloaded package");
|
|
901
|
-
}
|
|
902
|
-
stageUpdate(targetVersion, cliPath);
|
|
903
|
-
removeOldVersions(2);
|
|
904
|
-
return { success: true, version: targetVersion };
|
|
905
|
-
} catch (error) {
|
|
906
|
-
try {
|
|
907
|
-
fs3.rmSync(versionDir, { recursive: true, force: true });
|
|
908
|
-
} catch {}
|
|
909
|
-
return {
|
|
910
|
-
success: false,
|
|
911
|
-
error: error instanceof Error ? error.message : String(error)
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
function startBackgroundUpdateCheck() {
|
|
916
|
-
if (isUpdateInProgress()) {
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (!acquireLock()) {
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const child = spawn(process.execPath, [
|
|
923
|
-
process.argv[1] ?? "",
|
|
924
|
-
"update",
|
|
925
|
-
"--background"
|
|
926
|
-
], {
|
|
927
|
-
detached: true,
|
|
928
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
929
|
-
env: {
|
|
930
|
-
...process.env,
|
|
931
|
-
MCPX_UPDATE_CHILD: "1"
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
child.unref();
|
|
935
|
-
setTimeout(() => {
|
|
936
|
-
releaseLock();
|
|
937
|
-
}, 60000);
|
|
938
|
-
}
|
|
939
|
-
async function runBackgroundUpdate() {
|
|
940
|
-
try {
|
|
941
|
-
const result = await downloadAndStageUpdate();
|
|
942
|
-
if (result.success && result.version) {
|
|
943
|
-
console.error(`[update-manager] Update staged: v${result.version}. Will activate on next run.`);
|
|
944
|
-
} else if (result.error && result.error !== "No update available") {
|
|
945
|
-
console.error(`[update-manager] Update check failed: ${result.error}`);
|
|
946
|
-
}
|
|
947
|
-
} catch (error) {
|
|
948
|
-
console.error(`[update-manager] Background update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
949
|
-
} finally {
|
|
950
|
-
releaseLock();
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
async function performUpdate() {
|
|
954
|
-
if (isUpdateInProgress()) {
|
|
955
|
-
return { success: false, message: "Another update is already in progress." };
|
|
956
|
-
}
|
|
957
|
-
const result = await downloadAndStageUpdate();
|
|
958
|
-
if (result.success && result.version) {
|
|
959
|
-
return {
|
|
960
|
-
success: true,
|
|
961
|
-
message: `Update to v${result.version} ready! Will activate on next run.`
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
if (result.error === "No update available") {
|
|
965
|
-
return { success: true, message: "Already on latest version." };
|
|
966
|
-
}
|
|
967
|
-
return {
|
|
968
|
-
success: false,
|
|
969
|
-
message: result.error ?? "Update failed"
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
function performRollback() {
|
|
973
|
-
const currentStaged = getStagedUpdate();
|
|
974
|
-
if (!currentStaged) {
|
|
975
|
-
return { success: false, message: "No staged update to roll back." };
|
|
976
|
-
}
|
|
977
|
-
clearStagedUpdate();
|
|
978
|
-
return {
|
|
979
|
-
success: true,
|
|
980
|
-
message: `Rolled back from v${currentStaged.version}. Using installed version.`
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// src/core/daemon.ts
|
|
985
|
-
import fs4 from "node:fs";
|
|
986
|
-
import net from "node:net";
|
|
987
|
-
import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
|
|
988
|
-
|
|
989
|
-
// src/gateway/server.ts
|
|
990
|
-
import http2 from "node:http";
|
|
991
|
-
import { URL as URL2 } from "node:url";
|
|
992
|
-
import crypto2 from "node:crypto";
|
|
993
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
994
|
-
import {
|
|
995
|
-
StdioClientTransport,
|
|
996
|
-
getDefaultEnvironment
|
|
997
|
-
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
998
|
-
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
999
|
-
var SERVER_VERSION = APP_VERSION;
|
|
1000
|
-
var DEFAULT_UPSTREAM_TIMEOUT_MS = 60000;
|
|
1001
|
-
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
1002
|
-
var OAUTH_WELL_KNOWN_PREFIXES = [
|
|
1003
|
-
"/.well-known/oauth-protected-resource",
|
|
1004
|
-
"/.well-known/oauth-authorization-server",
|
|
1005
|
-
"/.well-known/openid-configuration"
|
|
1006
|
-
];
|
|
1007
|
-
function makeError(id, code, message, data) {
|
|
1008
|
-
return {
|
|
1009
|
-
jsonrpc: "2.0",
|
|
1010
|
-
id,
|
|
1011
|
-
error: {
|
|
1012
|
-
code,
|
|
1013
|
-
message,
|
|
1014
|
-
data
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
function makeResult(id, result) {
|
|
1019
|
-
return {
|
|
1020
|
-
jsonrpc: "2.0",
|
|
1021
|
-
id,
|
|
1022
|
-
result
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
function listUpstreams(config, upstreamFilter) {
|
|
1026
|
-
const all = Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => ({ name, spec }));
|
|
1027
|
-
if (!upstreamFilter) {
|
|
1028
|
-
return all;
|
|
1029
|
-
}
|
|
1030
|
-
return all.filter((upstream) => upstream.name === upstreamFilter);
|
|
1031
|
-
}
|
|
1032
|
-
function getSingleUpstream(config) {
|
|
1033
|
-
const upstreams = listUpstreams(config);
|
|
1034
|
-
if (upstreams.length !== 1) {
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
|
-
return upstreams[0] ?? null;
|
|
1038
|
-
}
|
|
1039
|
-
function getSingleHttpUpstream(config) {
|
|
1040
|
-
const upstream = getSingleUpstream(config);
|
|
1041
|
-
if (!upstream || upstream.spec.transport !== "http") {
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
return upstream;
|
|
1045
|
-
}
|
|
1046
|
-
function getScopedHttpUpstream(config, upstreamFilter) {
|
|
1047
|
-
if (upstreamFilter) {
|
|
1048
|
-
const selected = config.servers[upstreamFilter];
|
|
1049
|
-
if (!selected || !isServerEnabled(selected) || selected.transport !== "http") {
|
|
1050
|
-
return null;
|
|
1051
|
-
}
|
|
1052
|
-
return {
|
|
1053
|
-
name: upstreamFilter,
|
|
1054
|
-
spec: selected
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
return getSingleHttpUpstream(config);
|
|
1058
|
-
}
|
|
1059
|
-
function getWellKnownPrefix(pathname) {
|
|
1060
|
-
for (const prefix of OAUTH_WELL_KNOWN_PREFIXES) {
|
|
1061
|
-
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
1062
|
-
return prefix;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
|
-
function getUpstreamPathSuffixForWellKnown(upstream) {
|
|
1068
|
-
const upstreamUrl = new URL2(upstream.spec.url);
|
|
1069
|
-
if (upstreamUrl.pathname === "/") {
|
|
1070
|
-
return "";
|
|
1071
|
-
}
|
|
1072
|
-
return upstreamUrl.pathname.endsWith("/") ? upstreamUrl.pathname.slice(0, -1) : upstreamUrl.pathname;
|
|
1073
|
-
}
|
|
1074
|
-
function buildWellKnownUpstreamUrl(upstream, prefix) {
|
|
1075
|
-
const upstreamUrl = new URL2(upstream.spec.url);
|
|
1076
|
-
const suffix = getUpstreamPathSuffixForWellKnown(upstream);
|
|
1077
|
-
return new URL2(`${prefix}${suffix}`, upstreamUrl.origin);
|
|
1078
|
-
}
|
|
1079
|
-
function getLocalOriginFromRequest(request) {
|
|
1080
|
-
return `http://${request.headers.host ?? "127.0.0.1"}`;
|
|
1081
|
-
}
|
|
1082
|
-
function getRequestedUpstream(requestUrl) {
|
|
1083
|
-
const value = requestUrl.searchParams.get("upstream");
|
|
1084
|
-
if (!value) {
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
return value.trim() || undefined;
|
|
1088
|
-
}
|
|
1089
|
-
function appendUpstreamQuery(url, upstream) {
|
|
1090
|
-
if (!upstream) {
|
|
1091
|
-
return url;
|
|
1092
|
-
}
|
|
1093
|
-
const parsed = new URL2(url);
|
|
1094
|
-
parsed.searchParams.set("upstream", upstream);
|
|
1095
|
-
return parsed.toString();
|
|
1096
|
-
}
|
|
1097
|
-
function rewriteWwwAuthenticateResourceMetadata(headerValue, localResourceMetadataUrl) {
|
|
1098
|
-
if (headerValue.includes("resource_metadata=")) {
|
|
1099
|
-
return headerValue.replace(/resource_metadata="[^"]*"/, `resource_metadata="${localResourceMetadataUrl}"`);
|
|
1100
|
-
}
|
|
1101
|
-
return `${headerValue}, resource_metadata="${localResourceMetadataUrl}"`;
|
|
1102
|
-
}
|
|
1103
|
-
function isOAuthCapableServer(spec) {
|
|
1104
|
-
if (spec.transport !== "http") {
|
|
1105
|
-
return false;
|
|
1106
|
-
}
|
|
1107
|
-
const headers = spec.headers ?? {};
|
|
1108
|
-
for (const headerName of Object.keys(headers)) {
|
|
1109
|
-
if (headerName.toLowerCase() === "authorization") {
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
return false;
|
|
1114
|
-
}
|
|
1115
|
-
function splitNamespacedName(value) {
|
|
1116
|
-
const split = value.indexOf(".");
|
|
1117
|
-
if (split <= 0 || split >= value.length - 1) {
|
|
1118
|
-
return null;
|
|
1119
|
-
}
|
|
1120
|
-
return {
|
|
1121
|
-
serverName: value.slice(0, split),
|
|
1122
|
-
upstreamName: value.slice(split + 1)
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
function parseNamespacedUri(uri) {
|
|
1126
|
-
if (uri.startsWith("mcpx://")) {
|
|
1127
|
-
const rest = uri.slice("mcpx://".length);
|
|
1128
|
-
const slashIndex = rest.indexOf("/");
|
|
1129
|
-
if (slashIndex <= 0 || slashIndex >= rest.length - 1) {
|
|
1130
|
-
return null;
|
|
1131
|
-
}
|
|
1132
|
-
const serverName = rest.slice(0, slashIndex);
|
|
1133
|
-
const encoded = rest.slice(slashIndex + 1);
|
|
1134
|
-
try {
|
|
1135
|
-
return {
|
|
1136
|
-
serverName,
|
|
1137
|
-
upstreamUri: decodeURIComponent(encoded)
|
|
1138
|
-
};
|
|
1139
|
-
} catch {
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
const nameSplit = splitNamespacedName(uri);
|
|
1144
|
-
if (!nameSplit) {
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
return {
|
|
1148
|
-
serverName: nameSplit.serverName,
|
|
1149
|
-
upstreamUri: nameSplit.upstreamName
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
|
|
1153
|
-
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
|
|
1154
|
-
}
|
|
1155
|
-
async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
1156
|
-
if (!runtime.tokenCache) {
|
|
1157
|
-
runtime.tokenCache = new Map;
|
|
1158
|
-
}
|
|
1159
|
-
const results = {};
|
|
1160
|
-
const upstreams = listUpstreams(config);
|
|
1161
|
-
for (const upstream of upstreams) {
|
|
1162
|
-
const fingerprint = specFingerprint(upstream.spec);
|
|
1163
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1164
|
-
if (cached?.fingerprint === fingerprint) {
|
|
1165
|
-
const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
|
|
1166
|
-
results[upstream.name] = runtimeErr2 ? { ...cached.count, runtimeError: runtimeErr2.message, runtimeErrorCode: runtimeErr2.code } : cached.count;
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
let toolsCount = 0;
|
|
1170
|
-
let resourcesCount = 0;
|
|
1171
|
-
let promptsCount = 0;
|
|
1172
|
-
const errors = [];
|
|
1173
|
-
try {
|
|
1174
|
-
const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
|
|
1175
|
-
if (toolsResult && toolsResult.tools) {
|
|
1176
|
-
toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
|
|
1177
|
-
}
|
|
1178
|
-
} catch (error) {
|
|
1179
|
-
errors.push(`tools/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
1180
|
-
}
|
|
1181
|
-
try {
|
|
1182
|
-
const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
|
|
1183
|
-
if (resourcesResult && resourcesResult.resources) {
|
|
1184
|
-
resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
|
|
1185
|
-
}
|
|
1186
|
-
} catch (error) {
|
|
1187
|
-
errors.push(`resources/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
1188
|
-
}
|
|
1189
|
-
try {
|
|
1190
|
-
const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
|
|
1191
|
-
if (promptsResult && promptsResult.prompts) {
|
|
1192
|
-
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
1193
|
-
}
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
errors.push(`prompts/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
1196
|
-
}
|
|
1197
|
-
const total = toolsCount + resourcesCount + promptsCount;
|
|
1198
|
-
const countObj = {
|
|
1199
|
-
tools: toolsCount,
|
|
1200
|
-
resources: resourcesCount,
|
|
1201
|
-
prompts: promptsCount,
|
|
1202
|
-
total,
|
|
1203
|
-
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
1204
|
-
};
|
|
1205
|
-
if (errors.length === 0) {
|
|
1206
|
-
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
1207
|
-
}
|
|
1208
|
-
const runtimeErr = runtime.upstreamErrors?.get(upstream.name);
|
|
1209
|
-
results[upstream.name] = runtimeErr ? { ...countObj, runtimeError: runtimeErr.message, runtimeErrorCode: runtimeErr.code } : countObj;
|
|
1210
|
-
}
|
|
1211
|
-
return results;
|
|
1212
|
-
}
|
|
1213
|
-
function getConfiguredTimeoutMs() {
|
|
1214
|
-
const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
|
|
1215
|
-
return Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_UPSTREAM_TIMEOUT_MS;
|
|
1216
|
-
}
|
|
1217
|
-
function withTimeout(work, timeoutMs, timeoutMessage) {
|
|
1218
|
-
return new Promise((resolve, reject) => {
|
|
1219
|
-
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
1220
|
-
work.then((value) => {
|
|
1221
|
-
clearTimeout(timer);
|
|
1222
|
-
resolve(value);
|
|
1223
|
-
}, (error) => {
|
|
1224
|
-
clearTimeout(timer);
|
|
1225
|
-
reject(error);
|
|
1226
|
-
});
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
function specFingerprint(spec) {
|
|
1230
|
-
return JSON.stringify(spec);
|
|
1231
|
-
}
|
|
1232
|
-
function resolveStdioEnv(spec, secrets) {
|
|
1233
|
-
const env = getDefaultEnvironment();
|
|
1234
|
-
env.PATH = buildEnrichedPath(env.PATH);
|
|
1235
|
-
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
1236
|
-
env[key] = secrets.resolveMaybeSecret(value);
|
|
1237
|
-
}
|
|
1238
|
-
return env;
|
|
1239
|
-
}
|
|
1240
|
-
function buildStdioServerParameters(spec, secrets) {
|
|
1241
|
-
return {
|
|
1242
|
-
command: spec.command,
|
|
1243
|
-
args: spec.args ?? [],
|
|
1244
|
-
cwd: spec.cwd,
|
|
1245
|
-
env: resolveStdioEnv(spec, secrets)
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
async function buildHttpHeaders(upstream, secrets, options) {
|
|
1249
|
-
const headers = {};
|
|
1250
|
-
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
1251
|
-
if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
|
|
1252
|
-
const oauthServerName = oauthReferenceServerName(value);
|
|
1253
|
-
const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, {
|
|
1254
|
-
forceRefresh: options.forceOAuthRefresh
|
|
1255
|
-
});
|
|
1256
|
-
headers[key] = `Bearer ${accessToken}`;
|
|
1257
|
-
} else {
|
|
1258
|
-
headers[key] = secrets.resolveMaybeSecret(value);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (options.passthroughAuthorizationHeader) {
|
|
1262
|
-
headers.Authorization = options.passthroughAuthorizationHeader;
|
|
1263
|
-
}
|
|
1264
|
-
return headers;
|
|
1265
|
-
}
|
|
1266
|
-
async function closeUpstreamConnection(entry) {
|
|
1267
|
-
try {
|
|
1268
|
-
const connection = await entry.promise;
|
|
1269
|
-
await connection.transport.close();
|
|
1270
|
-
} catch {}
|
|
1271
|
-
}
|
|
1272
|
-
async function reconcileUpstreamConnections(config, runtime) {
|
|
1273
|
-
const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
|
|
1274
|
-
const staleKeys = [];
|
|
1275
|
-
for (const [key, entry] of runtime.upstreamConnections.entries()) {
|
|
1276
|
-
const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
|
1277
|
-
const expectedFingerprint = activeSpecs.get(upstreamName);
|
|
1278
|
-
if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
|
|
1279
|
-
staleKeys.push(key);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
for (const key of staleKeys) {
|
|
1283
|
-
const entry = runtime.upstreamConnections.get(key);
|
|
1284
|
-
if (!entry) {
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
runtime.upstreamConnections.delete(key);
|
|
1288
|
-
await closeUpstreamConnection(entry);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
|
|
1292
|
-
const key = passthroughAuthorizationHeader ? `${upstreamName}:passthrough` : upstreamName;
|
|
1293
|
-
const existing = runtime.upstreamConnections.get(key);
|
|
1294
|
-
if (!existing) {
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
runtime.upstreamConnections.delete(key);
|
|
1298
|
-
closeUpstreamConnection(existing);
|
|
1299
|
-
}
|
|
1300
|
-
async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
|
|
1301
|
-
const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
|
|
1302
|
-
const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:passthrough` : upstream.name;
|
|
1303
|
-
const fingerprint = specFingerprint(upstream.spec);
|
|
1304
|
-
if (!forceOAuthRefresh) {
|
|
1305
|
-
const existing = runtime.upstreamConnections.get(cacheKey);
|
|
1306
|
-
if (existing) {
|
|
1307
|
-
if (existing.fingerprint === fingerprint) {
|
|
1308
|
-
return existing.promise;
|
|
1309
|
-
}
|
|
1310
|
-
runtime.upstreamConnections.delete(cacheKey);
|
|
1311
|
-
closeUpstreamConnection(existing);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const promise = (async () => {
|
|
1315
|
-
let transport;
|
|
1316
|
-
if (upstream.spec.transport === "stdio") {
|
|
1317
|
-
transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
|
|
1318
|
-
} else {
|
|
1319
|
-
const headers = await buildHttpHeaders(upstream, secrets, { forceOAuthRefresh, passthroughAuthorizationHeader });
|
|
1320
|
-
const capturingFetch = async (url, init) => {
|
|
1321
|
-
const response = await fetch(url, init);
|
|
1322
|
-
const wwwAuth = response.headers.get("www-authenticate");
|
|
1323
|
-
if (wwwAuth) {
|
|
1324
|
-
if (!runtime.lastWwwAuthenticate) {
|
|
1325
|
-
runtime.lastWwwAuthenticate = new Map;
|
|
1326
|
-
}
|
|
1327
|
-
runtime.lastWwwAuthenticate.set(upstream.name, wwwAuth);
|
|
1328
|
-
}
|
|
1329
|
-
return response;
|
|
1330
|
-
};
|
|
1331
|
-
transport = new StreamableHTTPClientTransport(new URL2(upstream.spec.url), { requestInit: { headers }, fetch: capturingFetch });
|
|
1332
|
-
}
|
|
1333
|
-
const client = new Client({ name: "mcpx", version: SERVER_VERSION });
|
|
1334
|
-
await withTimeout(client.connect(transport), DEFAULT_CONNECT_TIMEOUT_MS, `Upstream ${upstream.name} failed to connect within ${DEFAULT_CONNECT_TIMEOUT_MS}ms.`);
|
|
1335
|
-
return { fingerprint, client, transport };
|
|
1336
|
-
})();
|
|
1337
|
-
runtime.upstreamConnections.set(cacheKey, { fingerprint, promise });
|
|
1338
|
-
try {
|
|
1339
|
-
return await promise;
|
|
1340
|
-
} catch (error) {
|
|
1341
|
-
runtime.upstreamConnections.delete(cacheKey);
|
|
1342
|
-
throw error;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
async function callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, forceOAuthRefresh) {
|
|
1346
|
-
const hasOAuth = upstream.spec.transport === "http" && Object.values(upstream.spec.headers ?? {}).some(isOAuthReference);
|
|
1347
|
-
try {
|
|
1348
|
-
const connection = await getUpstreamConnection(upstream, secrets, runtime, {
|
|
1349
|
-
forceOAuthRefresh,
|
|
1350
|
-
passthroughAuthorizationHeader
|
|
1351
|
-
});
|
|
1352
|
-
const timeoutMs = getConfiguredTimeoutMs();
|
|
1353
|
-
const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
|
|
1354
|
-
if (method === "tools/list") {
|
|
1355
|
-
return withTimeout(connection.client.listTools(params), timeoutMs, timeoutMessage);
|
|
1356
|
-
}
|
|
1357
|
-
if (method === "resources/list") {
|
|
1358
|
-
return withTimeout(connection.client.listResources(params), timeoutMs, timeoutMessage);
|
|
1359
|
-
}
|
|
1360
|
-
if (method === "prompts/list") {
|
|
1361
|
-
return withTimeout(connection.client.listPrompts(params), timeoutMs, timeoutMessage);
|
|
1362
|
-
}
|
|
1363
|
-
if (method === "tools/call") {
|
|
1364
|
-
return withTimeout(connection.client.callTool(params), timeoutMs, timeoutMessage);
|
|
1365
|
-
}
|
|
1366
|
-
if (method === "resources/read") {
|
|
1367
|
-
return withTimeout(connection.client.readResource(params), timeoutMs, timeoutMessage);
|
|
1368
|
-
}
|
|
1369
|
-
if (method === "prompts/get") {
|
|
1370
|
-
return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
|
|
1371
|
-
}
|
|
1372
|
-
throw new Error(`Unsupported method: ${method}`);
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
invalidateUpstreamConnection(upstream.name, passthroughAuthorizationHeader, runtime);
|
|
1375
|
-
if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
1376
|
-
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
|
|
1377
|
-
}
|
|
1378
|
-
const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
|
|
1379
|
-
throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
function authHeaderIsValid(request, expectedToken) {
|
|
1383
|
-
const localTokenHeader = request.headers["x-mcpx-local-token"];
|
|
1384
|
-
if (typeof localTokenHeader === "string" && localTokenHeader === expectedToken) {
|
|
1385
|
-
return true;
|
|
1386
|
-
}
|
|
1387
|
-
if (Array.isArray(localTokenHeader) && localTokenHeader.includes(expectedToken)) {
|
|
1388
|
-
return true;
|
|
1389
|
-
}
|
|
1390
|
-
const authHeader = request.headers.authorization;
|
|
1391
|
-
if (!authHeader) {
|
|
1392
|
-
return false;
|
|
1393
|
-
}
|
|
1394
|
-
const [scheme, token] = authHeader.split(" ");
|
|
1395
|
-
return scheme === "Bearer" && token === expectedToken;
|
|
1396
|
-
}
|
|
1397
|
-
function isAuthChallenge(error) {
|
|
1398
|
-
return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
|
|
1399
|
-
}
|
|
1400
|
-
function getClientAuthorizationForUpstream(request, expectedToken) {
|
|
1401
|
-
const authHeader = request.headers.authorization;
|
|
1402
|
-
if (!authHeader) {
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
const [scheme, token] = authHeader.split(" ");
|
|
1406
|
-
if (scheme === "Bearer" && token === expectedToken) {
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
return authHeader;
|
|
1410
|
-
}
|
|
1411
|
-
async function handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1412
|
-
const tools = [];
|
|
1413
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
1414
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
1415
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
1416
|
-
for (const upstream of upstreams) {
|
|
1417
|
-
try {
|
|
1418
|
-
const result = await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
1419
|
-
if (!runtime.tokenCache) {
|
|
1420
|
-
runtime.tokenCache = new Map;
|
|
1421
|
-
}
|
|
1422
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1423
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1424
|
-
existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
|
|
1425
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1426
|
-
delete existing.error;
|
|
1427
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
1428
|
-
for (const tool of result.tools ?? []) {
|
|
1429
|
-
const name = typeof tool.name === "string" ? tool.name : "tool";
|
|
1430
|
-
tools.push({
|
|
1431
|
-
...tool,
|
|
1432
|
-
name: flattenNames ? name : `${upstream.name}.${name}`
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
if (upstreams.length === 1) {
|
|
1437
|
-
throw error;
|
|
1438
|
-
}
|
|
1439
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1440
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
return { tools };
|
|
1444
|
-
}
|
|
1445
|
-
async function handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1446
|
-
const resources = [];
|
|
1447
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
1448
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
1449
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
1450
|
-
for (const upstream of upstreams) {
|
|
1451
|
-
try {
|
|
1452
|
-
const result = await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
1453
|
-
if (!runtime.tokenCache) {
|
|
1454
|
-
runtime.tokenCache = new Map;
|
|
1455
|
-
}
|
|
1456
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1457
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1458
|
-
existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
|
|
1459
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1460
|
-
delete existing.error;
|
|
1461
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
1462
|
-
for (const resource of result.resources ?? []) {
|
|
1463
|
-
const originalUri = typeof resource.uri === "string" ? resource.uri : "";
|
|
1464
|
-
const originalName = typeof resource.name === "string" ? resource.name : originalUri;
|
|
1465
|
-
resources.push({
|
|
1466
|
-
...resource,
|
|
1467
|
-
name: flattenNames ? originalName : `${upstream.name}.${originalName}`,
|
|
1468
|
-
uri: flattenNames ? originalUri : `mcpx://${upstream.name}/${encodeURIComponent(originalUri)}`
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
} catch (error) {
|
|
1472
|
-
if (upstreams.length === 1) {
|
|
1473
|
-
throw error;
|
|
1474
|
-
}
|
|
1475
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1476
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
return { resources };
|
|
1480
|
-
}
|
|
1481
|
-
async function handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1482
|
-
const prompts = [];
|
|
1483
|
-
const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
|
|
1484
|
-
const flattenNames = Boolean(flattenedUpstream);
|
|
1485
|
-
const upstreams = listUpstreams(config, upstreamFilter);
|
|
1486
|
-
for (const upstream of upstreams) {
|
|
1487
|
-
try {
|
|
1488
|
-
const result = await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
1489
|
-
if (!runtime.tokenCache) {
|
|
1490
|
-
runtime.tokenCache = new Map;
|
|
1491
|
-
}
|
|
1492
|
-
const cached = runtime.tokenCache.get(upstream.name);
|
|
1493
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1494
|
-
existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
|
|
1495
|
-
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1496
|
-
delete existing.error;
|
|
1497
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
1498
|
-
for (const prompt of result.prompts ?? []) {
|
|
1499
|
-
const name = typeof prompt.name === "string" ? prompt.name : "prompt";
|
|
1500
|
-
prompts.push({
|
|
1501
|
-
...prompt,
|
|
1502
|
-
name: flattenNames ? name : `${upstream.name}.${name}`
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1505
|
-
} catch (error) {
|
|
1506
|
-
if (upstreams.length === 1) {
|
|
1507
|
-
throw error;
|
|
1508
|
-
}
|
|
1509
|
-
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1510
|
-
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return { prompts };
|
|
1514
|
-
}
|
|
1515
|
-
async function routeNamespacedCall(config, method, params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1516
|
-
if (!params || typeof params !== "object") {
|
|
1517
|
-
return makeError(id, -32602, "Missing params object.");
|
|
1518
|
-
}
|
|
1519
|
-
const upstreamEntries = listUpstreams(config, upstreamFilter);
|
|
1520
|
-
const upstreams = new Map(upstreamEntries.map((entry) => [entry.name, entry]));
|
|
1521
|
-
const flattenedUpstream = upstreamEntries.length === 1 ? upstreamEntries[0] : null;
|
|
1522
|
-
if (method === "tools/call") {
|
|
1523
|
-
const toolName = typeof params.name === "string" ? params.name : "";
|
|
1524
|
-
const split = splitNamespacedName(toolName);
|
|
1525
|
-
if (split && upstreamFilter && split.serverName !== upstreamFilter) {
|
|
1526
|
-
return makeError(id, -32602, `Tool belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
1527
|
-
}
|
|
1528
|
-
let upstream2;
|
|
1529
|
-
let upstreamToolName = toolName;
|
|
1530
|
-
if (split && upstreams.has(split.serverName)) {
|
|
1531
|
-
upstream2 = upstreams.get(split.serverName);
|
|
1532
|
-
upstreamToolName = split.upstreamName;
|
|
1533
|
-
} else if (flattenedUpstream) {
|
|
1534
|
-
upstream2 = flattenedUpstream;
|
|
1535
|
-
}
|
|
1536
|
-
if (!upstream2) {
|
|
1537
|
-
return makeError(id, -32602, "Tool name must be namespaced as <server>.<tool>.");
|
|
1538
|
-
}
|
|
1539
|
-
const upstreamParams2 = {
|
|
1540
|
-
...params,
|
|
1541
|
-
name: upstreamToolName
|
|
1542
|
-
};
|
|
1543
|
-
try {
|
|
1544
|
-
const result = await callUpstream(upstream2, method, upstreamParams2, id, secrets, runtime, clientAuthorizationHeader);
|
|
1545
|
-
runtime.upstreamErrors?.delete(upstream2.name);
|
|
1546
|
-
return makeResult(id, result);
|
|
1547
|
-
} catch (error) {
|
|
1548
|
-
if (isAuthChallenge(error)) {
|
|
1549
|
-
throw error;
|
|
1550
|
-
}
|
|
1551
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1552
|
-
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1553
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
if (method === "prompts/get") {
|
|
1557
|
-
const promptName = typeof params.name === "string" ? params.name : "";
|
|
1558
|
-
const split = splitNamespacedName(promptName);
|
|
1559
|
-
if (split && upstreamFilter && split.serverName !== upstreamFilter) {
|
|
1560
|
-
return makeError(id, -32602, `Prompt belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
1561
|
-
}
|
|
1562
|
-
let upstream2;
|
|
1563
|
-
let upstreamPromptName = promptName;
|
|
1564
|
-
if (split && upstreams.has(split.serverName)) {
|
|
1565
|
-
upstream2 = upstreams.get(split.serverName);
|
|
1566
|
-
upstreamPromptName = split.upstreamName;
|
|
1567
|
-
} else if (flattenedUpstream) {
|
|
1568
|
-
upstream2 = flattenedUpstream;
|
|
1569
|
-
}
|
|
1570
|
-
if (!upstream2) {
|
|
1571
|
-
return makeError(id, -32602, "Prompt name must be namespaced as <server>.<prompt>.");
|
|
1572
|
-
}
|
|
1573
|
-
const upstreamParams2 = {
|
|
1574
|
-
...params,
|
|
1575
|
-
name: upstreamPromptName
|
|
1576
|
-
};
|
|
1577
|
-
try {
|
|
1578
|
-
const result = await callUpstream(upstream2, method, upstreamParams2, id, secrets, runtime, clientAuthorizationHeader);
|
|
1579
|
-
runtime.upstreamErrors?.delete(upstream2.name);
|
|
1580
|
-
return makeResult(id, result);
|
|
1581
|
-
} catch (error) {
|
|
1582
|
-
if (isAuthChallenge(error)) {
|
|
1583
|
-
throw error;
|
|
1584
|
-
}
|
|
1585
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1586
|
-
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1587
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
1591
|
-
const parsed = parseNamespacedUri(uri);
|
|
1592
|
-
if (parsed && upstreamFilter && parsed.serverName !== upstreamFilter) {
|
|
1593
|
-
return makeError(id, -32602, `Resource belongs to upstream ${parsed.serverName}, but request is scoped to ${upstreamFilter}.`);
|
|
1594
|
-
}
|
|
1595
|
-
let upstream;
|
|
1596
|
-
let upstreamUri = uri;
|
|
1597
|
-
if (parsed && upstreams.has(parsed.serverName)) {
|
|
1598
|
-
upstream = upstreams.get(parsed.serverName);
|
|
1599
|
-
upstreamUri = parsed.upstreamUri;
|
|
1600
|
-
} else if (flattenedUpstream) {
|
|
1601
|
-
upstream = flattenedUpstream;
|
|
1602
|
-
}
|
|
1603
|
-
if (!upstream) {
|
|
1604
|
-
return makeError(id, -32602, "Resource URI must be namespaced (mcpx://<server>/<encoded-uri>).", { uri });
|
|
1605
|
-
}
|
|
1606
|
-
const upstreamParams = {
|
|
1607
|
-
...params,
|
|
1608
|
-
uri: upstreamUri
|
|
1609
|
-
};
|
|
1610
|
-
try {
|
|
1611
|
-
const result = await callUpstream(upstream, method, upstreamParams, id, secrets, runtime, clientAuthorizationHeader);
|
|
1612
|
-
runtime.upstreamErrors?.delete(upstream.name);
|
|
1613
|
-
return makeResult(id, result);
|
|
1614
|
-
} catch (error) {
|
|
1615
|
-
if (isAuthChallenge(error)) {
|
|
1616
|
-
throw error;
|
|
1617
|
-
}
|
|
1618
|
-
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1619
|
-
runtime.upstreamErrors?.set(upstream.name, { code: errCode, message: error.message });
|
|
1620
|
-
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream.name });
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
1624
|
-
const id = request.id ?? null;
|
|
1625
|
-
if (!request.method || typeof request.method !== "string") {
|
|
1626
|
-
return makeError(id, -32600, "Invalid JSON-RPC request: missing method.");
|
|
1627
|
-
}
|
|
1628
|
-
if (request.method === "initialize") {
|
|
1629
|
-
const requestedProtocol = request.params?.protocolVersion;
|
|
1630
|
-
const protocolVersion = typeof requestedProtocol === "string" && requestedProtocol.length > 0 ? requestedProtocol : "2025-11-25";
|
|
1631
|
-
return makeResult(id, {
|
|
1632
|
-
protocolVersion,
|
|
1633
|
-
capabilities: {
|
|
1634
|
-
tools: {},
|
|
1635
|
-
resources: {},
|
|
1636
|
-
prompts: {}
|
|
1637
|
-
},
|
|
1638
|
-
serverInfo: {
|
|
1639
|
-
name: "mcpx",
|
|
1640
|
-
version: SERVER_VERSION
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
if (request.method === "notifications/initialized") {
|
|
1645
|
-
return null;
|
|
1646
|
-
}
|
|
1647
|
-
if (request.method === "ping") {
|
|
1648
|
-
return makeResult(id, { ok: true });
|
|
1649
|
-
}
|
|
1650
|
-
const config = loadMergedConfig();
|
|
1651
|
-
if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
|
|
1652
|
-
return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
|
|
1653
|
-
}
|
|
1654
|
-
await reconcileUpstreamConnections(config, runtime);
|
|
1655
|
-
if (request.method === "custom/tokenCounts") {
|
|
1656
|
-
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
1657
|
-
return makeResult(id, result);
|
|
1658
|
-
}
|
|
1659
|
-
if (request.method === "custom/refreshTokenCounts") {
|
|
1660
|
-
runtime.tokenCache?.clear();
|
|
1661
|
-
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
1662
|
-
return makeResult(id, result);
|
|
1663
|
-
}
|
|
1664
|
-
if (request.method === "tools/list") {
|
|
1665
|
-
const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1666
|
-
return makeResult(id, result);
|
|
1667
|
-
}
|
|
1668
|
-
if (request.method === "resources/list") {
|
|
1669
|
-
const result = await handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1670
|
-
return makeResult(id, result);
|
|
1671
|
-
}
|
|
1672
|
-
if (request.method === "prompts/list") {
|
|
1673
|
-
const result = await handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1674
|
-
return makeResult(id, result);
|
|
1675
|
-
}
|
|
1676
|
-
if (request.method === "tools/call" || request.method === "resources/read" || request.method === "prompts/get") {
|
|
1677
|
-
return routeNamespacedCall(config, request.method, request.params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1678
|
-
}
|
|
1679
|
-
return makeError(id, -32601, `Unsupported method: ${request.method}`);
|
|
1680
|
-
}
|
|
1681
|
-
async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, secrets) {
|
|
1682
|
-
const pathname = requestUrl.pathname;
|
|
1683
|
-
const wellKnownPrefix = getWellKnownPrefix(pathname);
|
|
1684
|
-
if (!wellKnownPrefix) {
|
|
1685
|
-
return false;
|
|
1686
|
-
}
|
|
1687
|
-
if (request.method !== "GET") {
|
|
1688
|
-
response.statusCode = 405;
|
|
1689
|
-
response.setHeader("content-type", "application/json");
|
|
1690
|
-
response.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
1691
|
-
return true;
|
|
1692
|
-
}
|
|
1693
|
-
const config = loadMergedConfig();
|
|
1694
|
-
const requestedUpstream = getRequestedUpstream(requestUrl);
|
|
1695
|
-
const upstream = getScopedHttpUpstream(config, requestedUpstream);
|
|
1696
|
-
if (!upstream) {
|
|
1697
|
-
response.statusCode = 404;
|
|
1698
|
-
response.setHeader("content-type", "application/json");
|
|
1699
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
1700
|
-
return true;
|
|
1701
|
-
}
|
|
1702
|
-
if (!isOAuthCapableServer(upstream.spec)) {
|
|
1703
|
-
response.statusCode = 404;
|
|
1704
|
-
response.setHeader("content-type", "application/json");
|
|
1705
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
1706
|
-
return true;
|
|
1707
|
-
}
|
|
1708
|
-
const upstreamWellKnownUrl = buildWellKnownUpstreamUrl(upstream, wellKnownPrefix);
|
|
1709
|
-
const headers = {
|
|
1710
|
-
accept: "application/json"
|
|
1711
|
-
};
|
|
1712
|
-
const protocolVersion = request.headers["mcp-protocol-version"];
|
|
1713
|
-
if (typeof protocolVersion === "string" && protocolVersion.length > 0) {
|
|
1714
|
-
headers["mcp-protocol-version"] = protocolVersion;
|
|
1715
|
-
}
|
|
1716
|
-
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
1717
|
-
headers[key] = secrets.resolveMaybeSecret(value);
|
|
1718
|
-
}
|
|
1719
|
-
const upstreamResponse = await fetch(upstreamWellKnownUrl, {
|
|
1720
|
-
method: "GET",
|
|
1721
|
-
headers
|
|
1722
|
-
});
|
|
1723
|
-
let bodyText = await upstreamResponse.text();
|
|
1724
|
-
response.statusCode = upstreamResponse.status;
|
|
1725
|
-
const contentType = upstreamResponse.headers.get("content-type");
|
|
1726
|
-
if (contentType) {
|
|
1727
|
-
response.setHeader("content-type", contentType);
|
|
1728
|
-
}
|
|
1729
|
-
const cacheControl = upstreamResponse.headers.get("cache-control");
|
|
1730
|
-
if (cacheControl) {
|
|
1731
|
-
response.setHeader("cache-control", cacheControl);
|
|
1732
|
-
}
|
|
1733
|
-
const wwwAuthenticate = upstreamResponse.headers.get("www-authenticate");
|
|
1734
|
-
if (wwwAuthenticate) {
|
|
1735
|
-
response.setHeader("www-authenticate", wwwAuthenticate);
|
|
1736
|
-
}
|
|
1737
|
-
if (wellKnownPrefix === "/.well-known/oauth-protected-resource" && contentType?.includes("application/json") && upstreamResponse.ok) {
|
|
1738
|
-
try {
|
|
1739
|
-
const parsed = JSON.parse(bodyText);
|
|
1740
|
-
parsed.resource = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/mcp`, requestedUpstream);
|
|
1741
|
-
bodyText = JSON.stringify(parsed);
|
|
1742
|
-
} catch {}
|
|
1743
|
-
}
|
|
1744
|
-
response.end(bodyText);
|
|
1745
|
-
return true;
|
|
1746
|
-
}
|
|
1747
|
-
function createGatewayServer(options) {
|
|
1748
|
-
const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
|
|
1749
|
-
const runtime = {
|
|
1750
|
-
upstreamConnections: new Map,
|
|
1751
|
-
upstreamErrors: new Map
|
|
1752
|
-
};
|
|
1753
|
-
const server = http2.createServer(async (request, response) => {
|
|
1754
|
-
let requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
1755
|
-
try {
|
|
1756
|
-
requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
1757
|
-
const upstreamFilter = getRequestedUpstream(requestUrl);
|
|
1758
|
-
if (debug) {
|
|
1759
|
-
console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
|
|
1760
|
-
console.error(`[mcpx gateway] headers=${JSON.stringify(request.headers)}`);
|
|
1761
|
-
}
|
|
1762
|
-
if (await maybeHandleWellKnownOAuthRequest(request, response, requestUrl, options.secrets)) {
|
|
1763
|
-
if (debug) {
|
|
1764
|
-
console.error(`[mcpx gateway] -> ${response.statusCode} (well-known oauth)`);
|
|
1765
|
-
}
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
if (requestUrl.pathname !== "/mcp") {
|
|
1769
|
-
response.statusCode = 404;
|
|
1770
|
-
response.setHeader("content-type", "application/json");
|
|
1771
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
1772
|
-
if (debug) {
|
|
1773
|
-
console.error(`[mcpx gateway] -> 404`);
|
|
1774
|
-
}
|
|
1775
|
-
return;
|
|
1776
|
-
}
|
|
1777
|
-
if (request.method === "GET") {
|
|
1778
|
-
if (!authHeaderIsValid(request, options.expectedToken)) {
|
|
1779
|
-
response.statusCode = 401;
|
|
1780
|
-
response.setHeader("content-type", "application/json");
|
|
1781
|
-
response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
|
|
1782
|
-
if (debug) {
|
|
1783
|
-
console.error(`[mcpx gateway] -> 401 (GET unauthorized)`);
|
|
1784
|
-
}
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
response.statusCode = 200;
|
|
1788
|
-
response.setHeader("content-type", "application/json");
|
|
1789
|
-
response.end(JSON.stringify({ ok: true, server: "mcpx" }));
|
|
1790
|
-
if (debug) {
|
|
1791
|
-
console.error(`[mcpx gateway] -> 200 (GET ok)`);
|
|
1792
|
-
}
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
if (request.method !== "POST") {
|
|
1796
|
-
response.statusCode = 405;
|
|
1797
|
-
response.end("Method Not Allowed");
|
|
1798
|
-
if (debug) {
|
|
1799
|
-
console.error(`[mcpx gateway] -> 405`);
|
|
1800
|
-
}
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
if (!authHeaderIsValid(request, options.expectedToken)) {
|
|
1804
|
-
response.statusCode = 401;
|
|
1805
|
-
response.setHeader("content-type", "application/json");
|
|
1806
|
-
response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
|
|
1807
|
-
if (debug) {
|
|
1808
|
-
console.error(`[mcpx gateway] -> 401 (POST unauthorized)`);
|
|
1809
|
-
}
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
let body = "";
|
|
1813
|
-
request.setEncoding("utf8");
|
|
1814
|
-
for await (const chunk of request) {
|
|
1815
|
-
body += chunk;
|
|
1816
|
-
if (body.length > 1e7) {
|
|
1817
|
-
response.statusCode = 413;
|
|
1818
|
-
response.end("Payload Too Large");
|
|
1819
|
-
if (debug) {
|
|
1820
|
-
console.error(`[mcpx gateway] -> 413`);
|
|
1821
|
-
}
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
const parsed = JSON.parse(body);
|
|
1826
|
-
const hasInitialize = Array.isArray(parsed) ? parsed.some((item) => item.method === "initialize") : parsed.method === "initialize";
|
|
1827
|
-
const responses = [];
|
|
1828
|
-
const clientAuthorizationHeader = getClientAuthorizationForUpstream(request, options.expectedToken);
|
|
1829
|
-
if (Array.isArray(parsed)) {
|
|
1830
|
-
for (const item of parsed) {
|
|
1831
|
-
if (debug) {
|
|
1832
|
-
console.error(`[mcpx gateway] rpc method=${item.method} id=${item.id ?? "null"}`);
|
|
1833
|
-
}
|
|
1834
|
-
const rpcResponse = await handleRequestObject(item, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1835
|
-
if (debug && rpcResponse?.error) {
|
|
1836
|
-
console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
|
|
1837
|
-
}
|
|
1838
|
-
if (rpcResponse) {
|
|
1839
|
-
responses.push(rpcResponse);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
} else {
|
|
1843
|
-
if (debug) {
|
|
1844
|
-
console.error(`[mcpx gateway] rpc method=${parsed.method} id=${parsed.id ?? "null"}`);
|
|
1845
|
-
if (parsed.method === "initialize") {
|
|
1846
|
-
console.error(`[mcpx gateway] rpc initialize params=${JSON.stringify(parsed.params ?? {})}`);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
const rpcResponse = await handleRequestObject(parsed, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
1850
|
-
if (debug && rpcResponse?.error) {
|
|
1851
|
-
console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
|
|
1852
|
-
}
|
|
1853
|
-
if (rpcResponse) {
|
|
1854
|
-
responses.push(rpcResponse);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
response.statusCode = 200;
|
|
1858
|
-
const acceptsSse = (request.headers.accept ?? "").includes("text/event-stream");
|
|
1859
|
-
if (acceptsSse) {
|
|
1860
|
-
response.setHeader("content-type", "text/event-stream");
|
|
1861
|
-
response.setHeader("cache-control", "no-cache");
|
|
1862
|
-
response.setHeader("connection", "keep-alive");
|
|
1863
|
-
} else {
|
|
1864
|
-
response.setHeader("content-type", "application/json");
|
|
1865
|
-
}
|
|
1866
|
-
if (hasInitialize) {
|
|
1867
|
-
const sessionId = crypto2.randomUUID();
|
|
1868
|
-
response.setHeader("mcp-session-id", sessionId);
|
|
1869
|
-
response.setHeader("MCP-Session-Id", sessionId);
|
|
1870
|
-
}
|
|
1871
|
-
if (debug) {
|
|
1872
|
-
console.error(`[mcpx gateway] -> 200 (rpc)`);
|
|
1873
|
-
}
|
|
1874
|
-
if (acceptsSse) {
|
|
1875
|
-
const payloads = responses.length > 0 ? responses : [];
|
|
1876
|
-
for (const payload of payloads) {
|
|
1877
|
-
response.write(`event: message
|
|
1878
|
-
`);
|
|
1879
|
-
response.write(`data: ${JSON.stringify(payload)}
|
|
1880
|
-
|
|
1881
|
-
`);
|
|
1882
|
-
}
|
|
1883
|
-
response.end();
|
|
1884
|
-
} else if (responses.length === 1) {
|
|
1885
|
-
response.end(JSON.stringify(responses[0]));
|
|
1886
|
-
} else {
|
|
1887
|
-
response.end(JSON.stringify(responses));
|
|
1888
|
-
}
|
|
1889
|
-
} catch (error) {
|
|
1890
|
-
if (isAuthChallenge(error)) {
|
|
1891
|
-
response.statusCode = error.status ?? 401;
|
|
1892
|
-
response.setHeader("content-type", "application/json");
|
|
1893
|
-
if (error.wwwAuthenticate) {
|
|
1894
|
-
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
1895
|
-
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
1896
|
-
}
|
|
1897
|
-
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
1898
|
-
mcpxCode: error.code,
|
|
1899
|
-
status: error.status,
|
|
1900
|
-
upstream: error.upstream
|
|
1901
|
-
})));
|
|
1902
|
-
if (debug) {
|
|
1903
|
-
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
1904
|
-
}
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
response.statusCode = 500;
|
|
1908
|
-
response.setHeader("content-type", "application/json");
|
|
1909
|
-
response.end(JSON.stringify(makeError(null, -32000, error.message)));
|
|
1910
|
-
if (debug) {
|
|
1911
|
-
console.error(`[mcpx gateway] -> 500 ${error.message}`);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
});
|
|
1915
|
-
server.on("close", () => {
|
|
1916
|
-
for (const entry of runtime.upstreamConnections.values()) {
|
|
1917
|
-
closeUpstreamConnection(entry);
|
|
1918
|
-
}
|
|
1919
|
-
runtime.upstreamConnections.clear();
|
|
1920
|
-
});
|
|
1921
|
-
server.listen(options.port, "127.0.0.1");
|
|
1922
|
-
return server;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
// src/core/daemon.ts
|
|
1926
|
-
function readPidFromFile(pidPath = getPidPath()) {
|
|
1927
|
-
if (!fs4.existsSync(pidPath)) {
|
|
1928
|
-
return null;
|
|
1929
|
-
}
|
|
1930
|
-
const raw = fs4.readFileSync(pidPath, "utf8").trim();
|
|
1931
|
-
if (!raw) {
|
|
1932
|
-
return null;
|
|
1933
|
-
}
|
|
1934
|
-
const pid = Number(raw);
|
|
1935
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
1936
|
-
return null;
|
|
1937
|
-
}
|
|
1938
|
-
return pid;
|
|
1939
|
-
}
|
|
1940
|
-
function processExists(pid) {
|
|
1941
|
-
try {
|
|
1942
|
-
process.kill(pid, 0);
|
|
1943
|
-
return true;
|
|
1944
|
-
} catch {
|
|
1945
|
-
return false;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
async function isPortAvailable(port) {
|
|
1949
|
-
return new Promise((resolve) => {
|
|
1950
|
-
const tester = net.createServer();
|
|
1951
|
-
tester.once("error", () => {
|
|
1952
|
-
resolve(false);
|
|
1953
|
-
});
|
|
1954
|
-
tester.once("listening", () => {
|
|
1955
|
-
tester.close(() => resolve(true));
|
|
1956
|
-
});
|
|
1957
|
-
tester.listen(port, "127.0.0.1");
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
async function resolveGatewayPort(config, secrets) {
|
|
1961
|
-
if (await isPortAvailable(config.gateway.port)) {
|
|
1962
|
-
return config.gateway.port;
|
|
1963
|
-
}
|
|
1964
|
-
for (let offset = 1;offset <= 20; offset += 1) {
|
|
1965
|
-
const candidate = config.gateway.port + offset;
|
|
1966
|
-
if (candidate > 65535) {
|
|
1967
|
-
break;
|
|
1968
|
-
}
|
|
1969
|
-
if (await isPortAvailable(candidate)) {
|
|
1970
|
-
const oldPort = config.gateway.port;
|
|
1971
|
-
config.gateway.port = candidate;
|
|
1972
|
-
saveConfig(config);
|
|
1973
|
-
if (secrets) {
|
|
1974
|
-
try {
|
|
1975
|
-
const summary = syncAllClients(config, secrets);
|
|
1976
|
-
persistSyncState(summary, config);
|
|
1977
|
-
saveConfig(config);
|
|
1978
|
-
} catch {}
|
|
1979
|
-
}
|
|
1980
|
-
return candidate;
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
1984
|
-
}
|
|
1985
|
-
async function waitForGatewayReady(port, token, timeoutMs = 5000) {
|
|
1986
|
-
const deadline = Date.now() + timeoutMs;
|
|
1987
|
-
let lastError;
|
|
1988
|
-
while (Date.now() < deadline) {
|
|
1989
|
-
try {
|
|
1990
|
-
const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
1991
|
-
method: "GET",
|
|
1992
|
-
headers: {
|
|
1993
|
-
Authorization: `Bearer ${token}`
|
|
1994
|
-
},
|
|
1995
|
-
signal: AbortSignal.timeout(500)
|
|
1996
|
-
});
|
|
1997
|
-
if (response.ok) {
|
|
1998
|
-
return;
|
|
1999
|
-
}
|
|
2000
|
-
lastError = new Error(`Gateway returned HTTP ${response.status}.`);
|
|
2001
|
-
} catch (error) {
|
|
2002
|
-
lastError = error;
|
|
2003
|
-
}
|
|
2004
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2005
|
-
}
|
|
2006
|
-
throw new Error(`Gateway did not become ready within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
2007
|
-
}
|
|
2008
|
-
function getDaemonStatus(config) {
|
|
2009
|
-
const pidPath = getPidPath();
|
|
2010
|
-
const pid = readPidFromFile(pidPath);
|
|
2011
|
-
if (!pid) {
|
|
2012
|
-
return {
|
|
2013
|
-
running: false,
|
|
2014
|
-
pidFile: pidPath,
|
|
2015
|
-
logFile: getLogPath(),
|
|
2016
|
-
port: config.gateway.port
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
return {
|
|
2020
|
-
running: processExists(pid),
|
|
2021
|
-
pid,
|
|
2022
|
-
pidFile: pidPath,
|
|
2023
|
-
logFile: getLogPath(),
|
|
2024
|
-
port: config.gateway.port
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
async function startDaemon(config, cliPath, secrets) {
|
|
2028
|
-
const existingStatus = getDaemonStatus(config);
|
|
2029
|
-
if (existingStatus.running && existingStatus.pid) {
|
|
2030
|
-
const portAvailable = await isPortAvailable(config.gateway.port);
|
|
2031
|
-
if (!portAvailable) {
|
|
2032
|
-
return {
|
|
2033
|
-
started: false,
|
|
2034
|
-
pid: existingStatus.pid,
|
|
2035
|
-
port: config.gateway.port,
|
|
2036
|
-
message: "mcpx daemon already running."
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
try {
|
|
2040
|
-
fs4.unlinkSync(getPidPath());
|
|
2041
|
-
} catch {}
|
|
2042
|
-
}
|
|
2043
|
-
const port = await resolveGatewayPort(config, secrets);
|
|
2044
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2045
|
-
const pidPath = getPidPath();
|
|
2046
|
-
const logPath = getLogPath();
|
|
2047
|
-
ensureParentDir(pidPath);
|
|
2048
|
-
ensureParentDir(logPath);
|
|
2049
|
-
const logFd = fs4.openSync(logPath, "a", 384);
|
|
2050
|
-
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
2051
|
-
detached: true,
|
|
2052
|
-
stdio: ["ignore", logFd, logFd],
|
|
2053
|
-
env: {
|
|
2054
|
-
...process.env,
|
|
2055
|
-
MCPX_DAEMON_CHILD: "1"
|
|
2056
|
-
}
|
|
2057
|
-
});
|
|
2058
|
-
child.unref();
|
|
2059
|
-
fs4.writeFileSync(pidPath, `${child.pid}
|
|
2060
|
-
`, { mode: 384 });
|
|
2061
|
-
startBackgroundUpdateCheck();
|
|
2062
|
-
await waitForGatewayReady(port, token);
|
|
2063
|
-
return {
|
|
2064
|
-
started: true,
|
|
2065
|
-
pid: child.pid ?? -1,
|
|
2066
|
-
port,
|
|
2067
|
-
message: "mcpx daemon started."
|
|
2068
|
-
};
|
|
2069
|
-
}
|
|
2070
|
-
function stopDaemon() {
|
|
2071
|
-
const pidPath = getPidPath();
|
|
2072
|
-
const pid = readPidFromFile(pidPath);
|
|
2073
|
-
if (!pid) {
|
|
2074
|
-
return {
|
|
2075
|
-
stopped: false,
|
|
2076
|
-
message: "mcpx daemon is not running."
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
|
-
try {
|
|
2080
|
-
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2081
|
-
if (!cmd.includes("mcpx") && !cmd.includes("daemon") && !cmd.includes("cli.js")) {
|
|
2082
|
-
fs4.unlinkSync(pidPath);
|
|
2083
|
-
return {
|
|
2084
|
-
stopped: false,
|
|
2085
|
-
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
} catch {
|
|
2089
|
-
try {
|
|
2090
|
-
fs4.unlinkSync(pidPath);
|
|
2091
|
-
} catch {}
|
|
2092
|
-
return {
|
|
2093
|
-
stopped: false,
|
|
2094
|
-
message: `PID ${pid} not found. Pidfile cleaned up.`
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
try {
|
|
2098
|
-
process.kill(pid, "SIGTERM");
|
|
2099
|
-
} catch {}
|
|
2100
|
-
try {
|
|
2101
|
-
fs4.unlinkSync(pidPath);
|
|
2102
|
-
} catch {}
|
|
2103
|
-
return {
|
|
2104
|
-
stopped: true,
|
|
2105
|
-
message: `Stopped mcpx daemon (pid ${pid}).`
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
async function restartDaemon(config, cliPath, secrets) {
|
|
2109
|
-
stopDaemon();
|
|
2110
|
-
return startDaemon(config, cliPath, secrets);
|
|
2111
|
-
}
|
|
2112
|
-
function readDaemonLogs(maxLines = 200) {
|
|
2113
|
-
const logPath = getLogPath();
|
|
2114
|
-
if (!fs4.existsSync(logPath)) {
|
|
2115
|
-
return "";
|
|
2116
|
-
}
|
|
2117
|
-
const lines = fs4.readFileSync(logPath, "utf8").split(`
|
|
2118
|
-
`);
|
|
2119
|
-
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
2120
|
-
`);
|
|
2121
|
-
}
|
|
2122
|
-
function runDaemonForeground(config, port, secrets) {
|
|
2123
|
-
return new Promise((resolve, reject) => {
|
|
2124
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2125
|
-
const server = createGatewayServer({
|
|
2126
|
-
port,
|
|
2127
|
-
expectedToken: token,
|
|
2128
|
-
secrets
|
|
2129
|
-
});
|
|
2130
|
-
const cleanup = () => {
|
|
2131
|
-
server.close(() => resolve());
|
|
2132
|
-
};
|
|
2133
|
-
process.on("SIGINT", cleanup);
|
|
2134
|
-
process.on("SIGTERM", cleanup);
|
|
2135
|
-
server.on("error", (error) => {
|
|
2136
|
-
reject(error);
|
|
2137
|
-
});
|
|
2138
|
-
});
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
// src/core/status.ts
|
|
2142
|
-
var STATUS_CLIENTS = ["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen"];
|
|
2143
|
-
function describeServerTarget(spec) {
|
|
2144
|
-
if (spec.transport === "http") {
|
|
2145
|
-
return spec.url;
|
|
2146
|
-
}
|
|
2147
|
-
const args = (spec.args ?? []).join(" ");
|
|
2148
|
-
const command = `${spec.command}${args ? ` ${args}` : ""}`;
|
|
2149
|
-
return spec.cwd ? `${command} (cwd: ${spec.cwd})` : command;
|
|
2150
|
-
}
|
|
2151
|
-
function deriveClientStatus(managed, configured) {
|
|
2152
|
-
if (configured) {
|
|
2153
|
-
return configured;
|
|
2154
|
-
}
|
|
2155
|
-
return managed ? "SYNCED" : "SKIPPED";
|
|
2156
|
-
}
|
|
2157
|
-
function buildClientMappings(config, managedIndex, serverName) {
|
|
2158
|
-
return STATUS_CLIENTS.map((clientId) => {
|
|
2159
|
-
const configState = config.clients[clientId];
|
|
2160
|
-
const managedClient = managedIndex.managed[clientId];
|
|
2161
|
-
const managedEntry = managedClient?.entries?.[`${serverName} (mcpx)`];
|
|
2162
|
-
const managed = Boolean(managedEntry);
|
|
2163
|
-
return {
|
|
2164
|
-
clientId,
|
|
2165
|
-
managed,
|
|
2166
|
-
status: deriveClientStatus(managed, configState?.status),
|
|
2167
|
-
configPath: managedClient?.configPath ?? configState?.configPath,
|
|
2168
|
-
message: configState?.message,
|
|
2169
|
-
lastSyncAt: managedEntry?.lastSyncedAt ?? configState?.lastSyncAt
|
|
2170
|
-
};
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
function buildAuthBindings(spec) {
|
|
2174
|
-
return listAuthBindings(spec).map((binding) => ({
|
|
2175
|
-
kind: binding.kind,
|
|
2176
|
-
key: binding.key,
|
|
2177
|
-
value: binding.value,
|
|
2178
|
-
secretName: secretRefName(binding.value) ?? undefined
|
|
2179
|
-
}));
|
|
2180
|
-
}
|
|
2181
|
-
async function fetchTokenCounts(gatewayUrl, token) {
|
|
2182
|
-
try {
|
|
2183
|
-
const res = await fetch(gatewayUrl, {
|
|
2184
|
-
method: "POST",
|
|
2185
|
-
headers: {
|
|
2186
|
-
"content-type": "application/json",
|
|
2187
|
-
authorization: `Bearer ${token}`
|
|
2188
|
-
},
|
|
2189
|
-
body: JSON.stringify({
|
|
2190
|
-
jsonrpc: "2.0",
|
|
2191
|
-
id: "status-tokens",
|
|
2192
|
-
method: "custom/tokenCounts",
|
|
2193
|
-
params: {}
|
|
2194
|
-
}),
|
|
2195
|
-
signal: AbortSignal.timeout(4000)
|
|
2196
|
-
});
|
|
2197
|
-
if (!res.ok) {
|
|
2198
|
-
return {};
|
|
2199
|
-
}
|
|
2200
|
-
const data = await res.json();
|
|
2201
|
-
return data?.result ?? {};
|
|
2202
|
-
} catch (error) {
|
|
2203
|
-
return {};
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
|
|
2207
|
-
const secrets = new SecretsManager;
|
|
2208
|
-
const token = ensureGatewayToken(config, secrets);
|
|
2209
|
-
const gatewayUrl = getGatewayUrl(config);
|
|
2210
|
-
let tokenCounts = {};
|
|
2211
|
-
if (daemon.running) {
|
|
2212
|
-
tokenCounts = await fetchTokenCounts(gatewayUrl, token);
|
|
2213
|
-
}
|
|
2214
|
-
const projectEntries = Object.values(config.projects ?? {});
|
|
2215
|
-
let totalGlobalTokens = 0;
|
|
2216
|
-
const totalProjectTokens = {};
|
|
2217
|
-
const serverNames = Object.keys(config.servers).sort((left, right) => left.localeCompare(right));
|
|
2218
|
-
const servers = serverNames.map((name) => {
|
|
2219
|
-
const spec = config.servers[name];
|
|
2220
|
-
let tokenCount = tokenCounts[name];
|
|
2221
|
-
if (tokenCount?.error && buildAuthBindings(spec).length > 0 && tokenCount.total > 0) {
|
|
2222
|
-
tokenCount = { ...tokenCount, error: undefined };
|
|
2223
|
-
}
|
|
2224
|
-
const health = tokenCount ? tokenCount.error || tokenCount.runtimeError ? "error" : "ok" : "unknown";
|
|
2225
|
-
const serverEntry = {
|
|
2226
|
-
name,
|
|
2227
|
-
enabled: isServerEnabled(spec),
|
|
2228
|
-
transport: spec.transport,
|
|
2229
|
-
target: describeServerTarget(spec),
|
|
2230
|
-
authBindings: buildAuthBindings(spec),
|
|
2231
|
-
clients: buildClientMappings(config, managedIndex, name),
|
|
2232
|
-
tokenCount,
|
|
2233
|
-
health
|
|
2234
|
-
};
|
|
2235
|
-
if (serverEntry.enabled && tokenCount) {
|
|
2236
|
-
totalGlobalTokens += tokenCount.total;
|
|
2237
|
-
for (const projectEntry of projectEntries) {
|
|
2238
|
-
if (!(projectEntry.disabledServers ?? []).includes(name)) {
|
|
2239
|
-
totalProjectTokens[projectEntry.path] = (totalProjectTokens[projectEntry.path] ?? 0) + tokenCount.total;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
return serverEntry;
|
|
2244
|
-
});
|
|
2245
|
-
return {
|
|
2246
|
-
gatewayUrl,
|
|
2247
|
-
daemon,
|
|
2248
|
-
upstreamCount: servers.length,
|
|
2249
|
-
servers,
|
|
2250
|
-
clients: config.clients,
|
|
2251
|
-
projects: config.projects,
|
|
2252
|
-
totalGlobalTokens,
|
|
2253
|
-
totalProjectTokens
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
export { probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|