@kwonye/mcpx 0.1.75 → 0.1.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-desktop.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/vscode.d.ts.map +1 -1
- package/dist/cli-7edpy4nk.js +514 -0
- package/dist/cli-fsf33sgp.js +105 -0
- package/dist/{cli-q8de7zb0.js → cli-s4gspqdf.js} +4 -1
- package/dist/cli-s4py8ckf.js +187 -0
- package/dist/{cli-e9hdd44n.js → cli-w4m7471h.js} +933 -542
- package/dist/{cli-fh6jh7fs.js → cli-ydr75jw4.js} +934 -543
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +381 -56
- package/dist/config-79j3jpra.js +20 -0
- package/dist/core/add-command.d.ts +7 -0
- package/dist/core/add-command.d.ts.map +1 -0
- package/dist/core/auth-probe.d.ts +1 -0
- package/dist/core/auth-probe.d.ts.map +1 -1
- package/dist/core/config-repair.d.ts +3 -0
- package/dist/core/config-repair.d.ts.map +1 -0
- package/dist/core/config.d.ts +19 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +50 -12
- package/dist/core/oauth.d.ts +12 -0
- package/dist/core/oauth.d.ts.map +1 -0
- package/dist/core/paths.d.ts +2 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/registry-client.d.ts +76 -0
- package/dist/core/registry-client.d.ts.map +1 -0
- package/dist/core/registry.d.ts +2 -0
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/secrets.d.ts.map +1 -1
- package/dist/core/spawn-env.d.ts +2 -0
- package/dist/core/spawn-env.d.ts.map +1 -0
- package/dist/core/status.d.ts +6 -2
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/gateway/server.d.ts +21 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/registry-1s14wd58.js +24 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
|
@@ -1,238 +1,46 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return path.join(getConfigRoot(), "mcpx", "config.json");
|
|
30
|
-
}
|
|
31
|
-
function getSkillsDir() {
|
|
32
|
-
return path.join(getConfigRoot(), "mcpx", "skills");
|
|
33
|
-
}
|
|
34
|
-
function getManagedIndexPath() {
|
|
35
|
-
return path.join(getDataRoot(), "mcpx", "managed-index.json");
|
|
36
|
-
}
|
|
37
|
-
function getSecretNamesPath() {
|
|
38
|
-
return path.join(getDataRoot(), "mcpx", "secret-names.json");
|
|
39
|
-
}
|
|
40
|
-
function getRuntimeDir() {
|
|
41
|
-
return path.join(getStateRoot(), "mcpx", "runtime");
|
|
42
|
-
}
|
|
43
|
-
function getLogDir() {
|
|
44
|
-
return path.join(getStateRoot(), "mcpx", "logs");
|
|
45
|
-
}
|
|
46
|
-
function getPidPath() {
|
|
47
|
-
return path.join(getRuntimeDir(), "daemon.pid");
|
|
48
|
-
}
|
|
49
|
-
function getLogPath() {
|
|
50
|
-
return path.join(getLogDir(), "daemon.log");
|
|
51
|
-
}
|
|
52
|
-
function getUpdatesDir() {
|
|
53
|
-
return path.join(getDataRoot(), "mcpx", "updates");
|
|
54
|
-
}
|
|
55
|
-
function getStagedVersionPath() {
|
|
56
|
-
return path.join(getUpdatesDir(), "staged-version.json");
|
|
57
|
-
}
|
|
58
|
-
function getUpdateLockPath() {
|
|
59
|
-
return path.join(getUpdatesDir(), ".update.lock");
|
|
60
|
-
}
|
|
61
|
-
function ensureDir(dirPath) {
|
|
62
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
function ensureParentDir(filePath) {
|
|
65
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// src/core/config.ts
|
|
69
|
-
import { z } from "zod";
|
|
70
|
-
|
|
71
|
-
// src/util/fs.ts
|
|
72
|
-
import fs2 from "node:fs";
|
|
73
|
-
import path2 from "node:path";
|
|
74
|
-
import crypto from "node:crypto";
|
|
75
|
-
function readJsonFile(filePath, fallback) {
|
|
76
|
-
if (!fs2.existsSync(filePath)) {
|
|
77
|
-
return fallback;
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const raw = fs2.readFileSync(filePath, "utf8");
|
|
81
|
-
return JSON.parse(raw);
|
|
82
|
-
} catch {
|
|
83
|
-
return fallback;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function writeJsonAtomic(filePath, data) {
|
|
87
|
-
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
88
|
-
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
89
|
-
const content = `${JSON.stringify(data, null, 2)}
|
|
90
|
-
`;
|
|
91
|
-
fs2.writeFileSync(tempPath, content, { mode: 384 });
|
|
92
|
-
fs2.renameSync(tempPath, filePath);
|
|
93
|
-
}
|
|
94
|
-
function sha256(value) {
|
|
95
|
-
return crypto.createHash("sha256").update(value).digest("hex");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/core/config.ts
|
|
99
|
-
var clientStateSchema = z.object({
|
|
100
|
-
status: z.enum(["SYNCED", "UNSUPPORTED_HTTP", "ERROR", "SKIPPED"]),
|
|
101
|
-
lastSyncAt: z.string().optional(),
|
|
102
|
-
message: z.string().optional(),
|
|
103
|
-
configPath: z.string().optional()
|
|
104
|
-
});
|
|
105
|
-
var httpServerSchema = z.object({
|
|
106
|
-
transport: z.literal("http"),
|
|
107
|
-
url: z.url(),
|
|
108
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
109
|
-
enabled: z.boolean().default(true)
|
|
110
|
-
});
|
|
111
|
-
var stdioServerSchema = z.object({
|
|
112
|
-
transport: z.literal("stdio"),
|
|
113
|
-
command: z.string().min(1),
|
|
114
|
-
args: z.array(z.string()).optional(),
|
|
115
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
116
|
-
cwd: z.string().min(1).optional(),
|
|
117
|
-
enabled: z.boolean().default(true)
|
|
118
|
-
});
|
|
119
|
-
var serverSchema = z.discriminatedUnion("transport", [httpServerSchema, stdioServerSchema]);
|
|
120
|
-
var configSchema = z.object({
|
|
121
|
-
schemaVersion: z.literal(1).default(1),
|
|
122
|
-
gateway: z.object({
|
|
123
|
-
port: z.number().int().min(1).max(65535).default(37373),
|
|
124
|
-
tokenRef: z.string().default("secret://local_gateway_token"),
|
|
125
|
-
autoStart: z.boolean().default(true)
|
|
126
|
-
}).default({
|
|
127
|
-
port: 37373,
|
|
128
|
-
tokenRef: "secret://local_gateway_token",
|
|
129
|
-
autoStart: true
|
|
130
|
-
}),
|
|
131
|
-
servers: z.record(z.string(), serverSchema).default({}),
|
|
132
|
-
clients: z.record(z.string(), clientStateSchema).default({})
|
|
133
|
-
});
|
|
134
|
-
function defaultConfig() {
|
|
135
|
-
return {
|
|
136
|
-
schemaVersion: 1,
|
|
137
|
-
gateway: {
|
|
138
|
-
port: 37373,
|
|
139
|
-
tokenRef: "secret://local_gateway_token",
|
|
140
|
-
autoStart: true
|
|
141
|
-
},
|
|
142
|
-
servers: {},
|
|
143
|
-
clients: {}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function loadConfig(configPath = getConfigPath()) {
|
|
147
|
-
const raw = readJsonFile(configPath, defaultConfig());
|
|
148
|
-
const parsed = configSchema.safeParse(raw);
|
|
149
|
-
if (!parsed.success) {
|
|
150
|
-
return defaultConfig();
|
|
151
|
-
}
|
|
152
|
-
const clientEntries = {};
|
|
153
|
-
for (const [key, value] of Object.entries(parsed.data.clients)) {
|
|
154
|
-
if (["claude", "claude-desktop", "codex", "cursor", "cline", "opencode", "kiro", "vscode", "qwen"].includes(key)) {
|
|
155
|
-
clientEntries[key] = value;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const servers = Object.fromEntries(Object.entries(parsed.data.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
|
|
159
|
-
return {
|
|
160
|
-
schemaVersion: 1,
|
|
161
|
-
gateway: parsed.data.gateway,
|
|
162
|
-
servers,
|
|
163
|
-
clients: clientEntries
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function saveConfig(config, configPath = getConfigPath()) {
|
|
167
|
-
config.servers = Object.fromEntries(Object.entries(config.servers).map(([name, spec]) => [name, normalizeServerSpecEnabled(spec)]));
|
|
168
|
-
writeJsonAtomic(configPath, config);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// src/core/registry.ts
|
|
172
|
-
function validateServerName(name) {
|
|
173
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(name)) {
|
|
174
|
-
throw new Error("Invalid server name. Use 1-63 chars: letters, numbers, '.', '-', '_' and start with alphanumeric.");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function addServer(config, name, spec, force = false) {
|
|
178
|
-
validateServerName(name);
|
|
179
|
-
if (config.servers[name] && !force) {
|
|
180
|
-
throw new Error(`Server "${name}" already exists. Use --force to overwrite.`);
|
|
181
|
-
}
|
|
182
|
-
config.servers[name] = normalizeServerSpecEnabled(spec);
|
|
183
|
-
}
|
|
184
|
-
function removeServer(config, name, force = false) {
|
|
185
|
-
if (!config.servers[name] && !force) {
|
|
186
|
-
throw new Error(`Server "${name}" does not exist.`);
|
|
187
|
-
}
|
|
188
|
-
delete config.servers[name];
|
|
189
|
-
}
|
|
190
|
-
function updateServer(config, name, spec) {
|
|
191
|
-
validateServerName(name);
|
|
192
|
-
if (!config.servers[name]) {
|
|
193
|
-
throw new Error(`Server "${name}" does not exist.`);
|
|
194
|
-
}
|
|
195
|
-
config.servers[name] = normalizeServerSpecEnabled(spec);
|
|
196
|
-
}
|
|
197
|
-
function setServerEnabled(config, name, enabled) {
|
|
198
|
-
const existing = config.servers[name];
|
|
199
|
-
if (!existing) {
|
|
200
|
-
throw new Error(`Server "${name}" does not exist.`);
|
|
201
|
-
}
|
|
202
|
-
config.servers[name] = {
|
|
203
|
-
...existing,
|
|
204
|
-
enabled
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
function getGatewayTokenSecretName(config) {
|
|
208
|
-
if (!config.gateway.tokenRef.startsWith("secret://")) {
|
|
209
|
-
return "local_gateway_token";
|
|
210
|
-
}
|
|
211
|
-
return config.gateway.tokenRef.slice("secret://".length) || "local_gateway_token";
|
|
212
|
-
}
|
|
213
|
-
function ensureGatewayToken(config, secrets) {
|
|
214
|
-
const secretName = getGatewayTokenSecretName(config);
|
|
215
|
-
const existing = secrets.getSecret(secretName);
|
|
216
|
-
if (existing) {
|
|
217
|
-
return existing;
|
|
218
|
-
}
|
|
219
|
-
return secrets.rotateLocalToken(secretName);
|
|
220
|
-
}
|
|
2
|
+
loadConfig,
|
|
3
|
+
loadMergedConfig,
|
|
4
|
+
saveConfig
|
|
5
|
+
} from "./cli-7edpy4nk.js";
|
|
6
|
+
import {
|
|
7
|
+
SecretsManager,
|
|
8
|
+
ensureGatewayToken,
|
|
9
|
+
getGatewayTokenSecretName
|
|
10
|
+
} from "./cli-s4py8ckf.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureDir,
|
|
13
|
+
ensureParentDir,
|
|
14
|
+
getLogPath,
|
|
15
|
+
getManagedIndexPath,
|
|
16
|
+
getPidPath,
|
|
17
|
+
getSkillsDir,
|
|
18
|
+
getStagedVersionPath,
|
|
19
|
+
getUpdateLockPath,
|
|
20
|
+
getUpdatesDir,
|
|
21
|
+
homeDir,
|
|
22
|
+
readJsonFile,
|
|
23
|
+
sha256,
|
|
24
|
+
writeJsonAtomic
|
|
25
|
+
} from "./cli-fsf33sgp.js";
|
|
26
|
+
import {
|
|
27
|
+
isServerEnabled
|
|
28
|
+
} from "./cli-s4gspqdf.js";
|
|
221
29
|
|
|
222
30
|
// src/core/skills.ts
|
|
223
|
-
import
|
|
224
|
-
import
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
225
33
|
function listSkills() {
|
|
226
34
|
const skillsDir = getSkillsDir();
|
|
227
|
-
if (!
|
|
35
|
+
if (!fs.existsSync(skillsDir)) {
|
|
228
36
|
return [];
|
|
229
37
|
}
|
|
230
|
-
const files =
|
|
38
|
+
const files = fs.readdirSync(skillsDir);
|
|
231
39
|
const skills = [];
|
|
232
40
|
for (const file of files) {
|
|
233
41
|
if (file.endsWith(".md")) {
|
|
234
|
-
const id =
|
|
235
|
-
const content =
|
|
42
|
+
const id = path.basename(file, ".md");
|
|
43
|
+
const content = fs.readFileSync(path.join(skillsDir, file), "utf-8");
|
|
236
44
|
skills.push({ id, content });
|
|
237
45
|
}
|
|
238
46
|
}
|
|
@@ -240,134 +48,52 @@ function listSkills() {
|
|
|
240
48
|
}
|
|
241
49
|
function getSkill(id) {
|
|
242
50
|
const skillsDir = getSkillsDir();
|
|
243
|
-
const filePath =
|
|
244
|
-
if (!
|
|
51
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
52
|
+
if (!fs.existsSync(filePath)) {
|
|
245
53
|
return null;
|
|
246
54
|
}
|
|
247
|
-
const content =
|
|
55
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
248
56
|
return { id, content };
|
|
249
57
|
}
|
|
250
58
|
function saveSkill(id, content) {
|
|
251
59
|
const skillsDir = getSkillsDir();
|
|
252
60
|
ensureDir(skillsDir);
|
|
253
|
-
const filePath =
|
|
254
|
-
|
|
61
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
62
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
255
63
|
}
|
|
256
64
|
function deleteSkill(id) {
|
|
257
65
|
const skillsDir = getSkillsDir();
|
|
258
|
-
const filePath =
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// src/core/secrets.ts
|
|
265
|
-
import { execFileSync } from "node:child_process";
|
|
266
|
-
import crypto2 from "node:crypto";
|
|
267
|
-
import fs4 from "node:fs";
|
|
268
|
-
function parseSecretRef(ref) {
|
|
269
|
-
if (!ref.startsWith("secret://")) {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
const name = ref.slice("secret://".length);
|
|
273
|
-
return name.length > 0 ? name : null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
class SecretsManager {
|
|
277
|
-
service;
|
|
278
|
-
nameIndexPath;
|
|
279
|
-
constructor(service = "mcpx", nameIndexPath = getSecretNamesPath()) {
|
|
280
|
-
this.service = service;
|
|
281
|
-
this.nameIndexPath = nameIndexPath;
|
|
282
|
-
}
|
|
283
|
-
loadNameIndex() {
|
|
284
|
-
return readJsonFile(this.nameIndexPath, { names: [] });
|
|
285
|
-
}
|
|
286
|
-
saveNameIndex(index) {
|
|
287
|
-
const deduped = Array.from(new Set(index.names)).sort();
|
|
288
|
-
writeJsonAtomic(this.nameIndexPath, { names: deduped });
|
|
289
|
-
}
|
|
290
|
-
trackSecretName(name) {
|
|
291
|
-
const index = this.loadNameIndex();
|
|
292
|
-
if (!index.names.includes(name)) {
|
|
293
|
-
index.names.push(name);
|
|
294
|
-
this.saveNameIndex(index);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
untrackSecretName(name) {
|
|
298
|
-
const index = this.loadNameIndex();
|
|
299
|
-
index.names = index.names.filter((entry) => entry !== name);
|
|
300
|
-
this.saveNameIndex(index);
|
|
301
|
-
}
|
|
302
|
-
listSecretNames() {
|
|
303
|
-
return this.loadNameIndex().names;
|
|
304
|
-
}
|
|
305
|
-
setSecret(name, value) {
|
|
306
|
-
if (process.platform !== "darwin") {
|
|
307
|
-
throw new Error("OS keychain integration is currently implemented for macOS only.");
|
|
308
|
-
}
|
|
309
|
-
execFileSync("security", ["add-generic-password", "-U", "-a", name, "-s", this.service, "-w", value], {
|
|
310
|
-
stdio: "ignore"
|
|
311
|
-
});
|
|
312
|
-
this.trackSecretName(name);
|
|
313
|
-
}
|
|
314
|
-
removeSecret(name) {
|
|
315
|
-
if (process.platform === "darwin") {
|
|
316
|
-
try {
|
|
317
|
-
execFileSync("security", ["delete-generic-password", "-a", name, "-s", this.service], {
|
|
318
|
-
stdio: "ignore"
|
|
319
|
-
});
|
|
320
|
-
} catch {}
|
|
321
|
-
}
|
|
322
|
-
this.untrackSecretName(name);
|
|
323
|
-
}
|
|
324
|
-
getSecret(name) {
|
|
325
|
-
const envOverride = process.env[`MCPX_SECRET_${name}`];
|
|
326
|
-
if (envOverride && envOverride.length > 0) {
|
|
327
|
-
return envOverride;
|
|
328
|
-
}
|
|
329
|
-
if (process.platform !== "darwin") {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
try {
|
|
333
|
-
return execFileSync("security", ["find-generic-password", "-w", "-a", name, "-s", this.service], {
|
|
334
|
-
encoding: "utf8"
|
|
335
|
-
}).trim();
|
|
336
|
-
} catch {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
resolveMaybeSecret(value) {
|
|
341
|
-
const secretName = parseSecretRef(value);
|
|
342
|
-
if (!secretName) {
|
|
343
|
-
return value;
|
|
344
|
-
}
|
|
345
|
-
const secret = this.getSecret(secretName);
|
|
346
|
-
if (!secret) {
|
|
347
|
-
throw new Error(`Secret not found: ${secretName}`);
|
|
348
|
-
}
|
|
349
|
-
return secret;
|
|
350
|
-
}
|
|
351
|
-
rotateLocalToken(secretName = "local_gateway_token") {
|
|
352
|
-
const token = crypto2.randomBytes(32).toString("base64url");
|
|
353
|
-
this.setSecret(secretName, token);
|
|
354
|
-
return token;
|
|
66
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
67
|
+
if (fs.existsSync(filePath)) {
|
|
68
|
+
fs.unlinkSync(filePath);
|
|
355
69
|
}
|
|
356
70
|
}
|
|
357
|
-
function readSecretValueFromStdin() {
|
|
358
|
-
if (process.stdin.isTTY) {
|
|
359
|
-
throw new Error("No value provided. Use --value or pipe a value via stdin.");
|
|
360
|
-
}
|
|
361
|
-
const content = fs4.readFileSync(process.stdin.fd, "utf8").trim();
|
|
362
|
-
if (content.length === 0) {
|
|
363
|
-
throw new Error("Empty secret value provided.");
|
|
364
|
-
}
|
|
365
|
-
return content;
|
|
366
|
-
}
|
|
367
71
|
|
|
368
72
|
// src/core/auth-probe.ts
|
|
369
73
|
var JSON_RPC_VERSION = "2.0";
|
|
370
74
|
var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
|
|
75
|
+
var AUTH_KEYWORDS = [
|
|
76
|
+
"unauthorized",
|
|
77
|
+
"unauthenticated",
|
|
78
|
+
"forbidden",
|
|
79
|
+
"authentication",
|
|
80
|
+
"authorization",
|
|
81
|
+
"api key",
|
|
82
|
+
"api_key",
|
|
83
|
+
"apikey",
|
|
84
|
+
"credentials",
|
|
85
|
+
"access denied",
|
|
86
|
+
"auth required",
|
|
87
|
+
"x-api-key",
|
|
88
|
+
"invalid key",
|
|
89
|
+
"missing header",
|
|
90
|
+
"not authenticated",
|
|
91
|
+
"auth token"
|
|
92
|
+
];
|
|
93
|
+
function isAuthRelatedMessage(message) {
|
|
94
|
+
const lower = message.toLowerCase();
|
|
95
|
+
return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
|
|
96
|
+
}
|
|
371
97
|
function resolveHeaders(spec, secrets) {
|
|
372
98
|
const headers = {
|
|
373
99
|
"content-type": "application/json",
|
|
@@ -378,6 +104,13 @@ function resolveHeaders(spec, secrets) {
|
|
|
378
104
|
}
|
|
379
105
|
return headers;
|
|
380
106
|
}
|
|
107
|
+
function isOAuthLikelyChallenge(wwwAuthenticate) {
|
|
108
|
+
if (!wwwAuthenticate) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const lower = wwwAuthenticate.toLowerCase();
|
|
112
|
+
return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
|
|
113
|
+
}
|
|
381
114
|
async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS) {
|
|
382
115
|
const timeoutController = new AbortController;
|
|
383
116
|
const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
@@ -395,12 +128,30 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
|
|
|
395
128
|
signal: timeoutController.signal
|
|
396
129
|
});
|
|
397
130
|
if (response.status === 401 || response.status === 403) {
|
|
131
|
+
const wwwAuthenticate = response.headers.get("www-authenticate");
|
|
398
132
|
return {
|
|
399
133
|
authRequired: true,
|
|
134
|
+
oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
|
|
400
135
|
status: response.status,
|
|
401
|
-
wwwAuthenticate:
|
|
136
|
+
wwwAuthenticate: wwwAuthenticate ?? undefined
|
|
402
137
|
};
|
|
403
138
|
}
|
|
139
|
+
if (response.status === 200) {
|
|
140
|
+
try {
|
|
141
|
+
const body = await response.json();
|
|
142
|
+
const err = body?.error;
|
|
143
|
+
if (err && typeof err === "object" && !("result" in body)) {
|
|
144
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
145
|
+
if (message && isAuthRelatedMessage(message)) {
|
|
146
|
+
return {
|
|
147
|
+
authRequired: true,
|
|
148
|
+
status: response.status,
|
|
149
|
+
error: `JSON-RPC error: ${message}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
404
155
|
return {
|
|
405
156
|
authRequired: false,
|
|
406
157
|
status: response.status
|
|
@@ -416,9 +167,132 @@ async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_
|
|
|
416
167
|
}
|
|
417
168
|
}
|
|
418
169
|
|
|
170
|
+
// src/core/registry-client.ts
|
|
171
|
+
var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
172
|
+
async function fetchRegistryServerDetail(name) {
|
|
173
|
+
const encoded = encodeURIComponent(name);
|
|
174
|
+
const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
|
|
175
|
+
headers: { accept: "application/json" }
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Registry API error: ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
const data = await response.json();
|
|
181
|
+
return data.server;
|
|
182
|
+
}
|
|
183
|
+
var PACKAGE_PRIORITY = {
|
|
184
|
+
npm: 1,
|
|
185
|
+
pypi: 2,
|
|
186
|
+
nuget: 3,
|
|
187
|
+
oci: 4,
|
|
188
|
+
mcpb: 5
|
|
189
|
+
};
|
|
190
|
+
var RUNTIME_HINT = {
|
|
191
|
+
npm: "npx",
|
|
192
|
+
pypi: "uvx",
|
|
193
|
+
nuget: "dnx"
|
|
194
|
+
};
|
|
195
|
+
function selectBestPackage(packages = [], remotes = []) {
|
|
196
|
+
const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
|
|
197
|
+
if (stdioPkgs.length > 0) {
|
|
198
|
+
return { kind: "package", package: stdioPkgs[0] };
|
|
199
|
+
}
|
|
200
|
+
const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
|
|
201
|
+
if (httpRemotes.length > 0) {
|
|
202
|
+
return { kind: "remote", remote: httpRemotes[0] };
|
|
203
|
+
}
|
|
204
|
+
if (packages.length > 0) {
|
|
205
|
+
return { kind: "package", package: packages[0] };
|
|
206
|
+
}
|
|
207
|
+
if (remotes.length > 0) {
|
|
208
|
+
return { kind: "remote", remote: remotes[0] };
|
|
209
|
+
}
|
|
210
|
+
throw new Error("Server has no packages or remotes");
|
|
211
|
+
}
|
|
212
|
+
function extractRequiredInputs(option) {
|
|
213
|
+
const inputs = [];
|
|
214
|
+
if (option.kind === "package" && option.package?.environmentVariables) {
|
|
215
|
+
for (const env of option.package.environmentVariables) {
|
|
216
|
+
if (env.isRequired && !env.default) {
|
|
217
|
+
inputs.push({
|
|
218
|
+
name: env.name,
|
|
219
|
+
description: env.description,
|
|
220
|
+
isSecret: env.isSecret ?? false,
|
|
221
|
+
kind: "env"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const arg of option.package.packageArguments ?? []) {
|
|
226
|
+
if (arg.isRequired && !arg.value && !arg.default) {
|
|
227
|
+
inputs.push({
|
|
228
|
+
name: arg.name ?? arg.valueHint ?? "arg",
|
|
229
|
+
description: arg.description,
|
|
230
|
+
isSecret: false,
|
|
231
|
+
kind: "arg"
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (option.kind === "remote" && option.remote?.headers) {
|
|
237
|
+
for (const header of option.remote.headers) {
|
|
238
|
+
if (header.isRequired && !header.default) {
|
|
239
|
+
inputs.push({
|
|
240
|
+
name: header.name,
|
|
241
|
+
description: header.description,
|
|
242
|
+
isSecret: header.isSecret ?? false,
|
|
243
|
+
kind: "header"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return inputs;
|
|
249
|
+
}
|
|
250
|
+
function mapRegistryToSpec(_name, option, resolvedValues) {
|
|
251
|
+
if (option.kind === "remote") {
|
|
252
|
+
const headers = {};
|
|
253
|
+
for (const header of option.remote?.headers ?? []) {
|
|
254
|
+
const value = resolvedValues[header.name] ?? header.default;
|
|
255
|
+
if (value)
|
|
256
|
+
headers[header.name] = value;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
transport: "http",
|
|
260
|
+
url: option.remote.url,
|
|
261
|
+
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const pkg = option.package;
|
|
265
|
+
const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
|
|
266
|
+
const args = [];
|
|
267
|
+
const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
|
|
268
|
+
args.push(identifier);
|
|
269
|
+
for (const arg of pkg.packageArguments ?? []) {
|
|
270
|
+
const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
|
|
271
|
+
if (!value)
|
|
272
|
+
continue;
|
|
273
|
+
if (arg.type === "named" && arg.name) {
|
|
274
|
+
args.push(arg.name, value);
|
|
275
|
+
} else {
|
|
276
|
+
args.push(value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const env = {};
|
|
280
|
+
for (const envVar of pkg.environmentVariables ?? []) {
|
|
281
|
+
const value = resolvedValues[envVar.name] ?? envVar.default;
|
|
282
|
+
if (value)
|
|
283
|
+
env[envVar.name] = value;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
transport: "stdio",
|
|
287
|
+
command: runtime,
|
|
288
|
+
args,
|
|
289
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
419
293
|
// src/adapters/claude.ts
|
|
420
|
-
import
|
|
421
|
-
import { z
|
|
294
|
+
import path2 from "node:path";
|
|
295
|
+
import { z } from "zod";
|
|
422
296
|
|
|
423
297
|
// src/adapters/utils/index.ts
|
|
424
298
|
function getManagedEntryNames(managedIndex, clientId) {
|
|
@@ -546,22 +420,22 @@ function syncMcpExcludedArray(raw, managedNames, managedEntries) {
|
|
|
546
420
|
}
|
|
547
421
|
|
|
548
422
|
// src/adapters/claude.ts
|
|
549
|
-
var stringMapSchema =
|
|
550
|
-
var claudeEntrySchema =
|
|
551
|
-
type:
|
|
552
|
-
url:
|
|
423
|
+
var stringMapSchema = z.record(z.string(), z.string());
|
|
424
|
+
var claudeEntrySchema = z.object({
|
|
425
|
+
type: z.string().optional(),
|
|
426
|
+
url: z.string().min(1).optional(),
|
|
553
427
|
headers: stringMapSchema.optional(),
|
|
554
|
-
command:
|
|
555
|
-
args:
|
|
428
|
+
command: z.string().min(1).optional(),
|
|
429
|
+
args: z.array(z.string()).optional(),
|
|
556
430
|
env: stringMapSchema.optional(),
|
|
557
|
-
cwd:
|
|
558
|
-
disabled:
|
|
431
|
+
cwd: z.string().min(1).optional(),
|
|
432
|
+
disabled: z.boolean().optional()
|
|
559
433
|
}).passthrough();
|
|
560
434
|
|
|
561
435
|
class ClaudeAdapter {
|
|
562
436
|
id = "claude";
|
|
563
437
|
detectConfigPath() {
|
|
564
|
-
return
|
|
438
|
+
return path2.join(homeDir(), ".claude.json");
|
|
565
439
|
}
|
|
566
440
|
supportsHttp() {
|
|
567
441
|
return true;
|
|
@@ -637,20 +511,22 @@ class ClaudeAdapter {
|
|
|
637
511
|
try {
|
|
638
512
|
const raw = readJsonFile(configPath, {});
|
|
639
513
|
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
640
|
-
const
|
|
514
|
+
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
515
|
+
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
641
516
|
type: "http",
|
|
642
517
|
url: entry.url,
|
|
643
518
|
headers: entry.headers
|
|
644
519
|
}]));
|
|
520
|
+
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
645
521
|
const topLevelServers = raw.mcpServers ?? {};
|
|
646
522
|
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
647
|
-
for (const name of
|
|
523
|
+
for (const name of enabledManagedNames) {
|
|
648
524
|
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
649
525
|
if (topLevelConflict) {
|
|
650
526
|
return errorResult(this.id, configPath, topLevelConflict);
|
|
651
527
|
}
|
|
652
528
|
}
|
|
653
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers,
|
|
529
|
+
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
654
530
|
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
655
531
|
topLevelServers[name] = entry;
|
|
656
532
|
}
|
|
@@ -673,24 +549,24 @@ class ClaudeAdapter {
|
|
|
673
549
|
}
|
|
674
550
|
|
|
675
551
|
// src/adapters/claude-desktop.ts
|
|
676
|
-
import { execFileSync
|
|
677
|
-
import
|
|
678
|
-
import { z as
|
|
679
|
-
var stringMapSchema2 =
|
|
680
|
-
var claudeDesktopEntrySchema =
|
|
681
|
-
type:
|
|
682
|
-
url:
|
|
552
|
+
import { execFileSync } from "node:child_process";
|
|
553
|
+
import path3 from "node:path";
|
|
554
|
+
import { z as z2 } from "zod";
|
|
555
|
+
var stringMapSchema2 = z2.record(z2.string(), z2.string());
|
|
556
|
+
var claudeDesktopEntrySchema = z2.object({
|
|
557
|
+
type: z2.string().optional(),
|
|
558
|
+
url: z2.string().min(1).optional(),
|
|
683
559
|
headers: stringMapSchema2.optional(),
|
|
684
|
-
command:
|
|
685
|
-
args:
|
|
560
|
+
command: z2.string().min(1).optional(),
|
|
561
|
+
args: z2.array(z2.string()).optional(),
|
|
686
562
|
env: stringMapSchema2.optional(),
|
|
687
|
-
cwd:
|
|
688
|
-
disabled:
|
|
563
|
+
cwd: z2.string().min(1).optional(),
|
|
564
|
+
disabled: z2.boolean().optional()
|
|
689
565
|
}).passthrough();
|
|
690
566
|
function buildProxyEntry(entry) {
|
|
691
567
|
const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
|
|
692
568
|
try {
|
|
693
|
-
const mcpxPath =
|
|
569
|
+
const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
694
570
|
if (mcpxPath)
|
|
695
571
|
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
696
572
|
} catch {}
|
|
@@ -700,7 +576,13 @@ function buildProxyEntry(entry) {
|
|
|
700
576
|
class ClaudeDesktopAdapter {
|
|
701
577
|
id = "claude-desktop";
|
|
702
578
|
detectConfigPath() {
|
|
703
|
-
|
|
579
|
+
if (process.platform === "linux") {
|
|
580
|
+
return path3.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
581
|
+
}
|
|
582
|
+
if (process.platform === "win32") {
|
|
583
|
+
return path3.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
584
|
+
}
|
|
585
|
+
return path3.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
704
586
|
}
|
|
705
587
|
supportsHttp() {
|
|
706
588
|
return true;
|
|
@@ -786,9 +668,9 @@ class ClaudeDesktopAdapter {
|
|
|
786
668
|
}
|
|
787
669
|
raw.mcpServers = topLevelServers;
|
|
788
670
|
writeJsonAtomic(configPath, raw);
|
|
789
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(
|
|
671
|
+
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
|
|
790
672
|
const { command, args } = buildProxyEntry(entry);
|
|
791
|
-
return [entry.name, JSON.stringify({ command, args
|
|
673
|
+
return [entry.name, JSON.stringify({ command, args })];
|
|
792
674
|
})));
|
|
793
675
|
return okResult(this.id, configPath);
|
|
794
676
|
} catch (error) {
|
|
@@ -798,31 +680,31 @@ class ClaudeDesktopAdapter {
|
|
|
798
680
|
}
|
|
799
681
|
|
|
800
682
|
// src/adapters/codex.ts
|
|
801
|
-
import
|
|
802
|
-
import
|
|
683
|
+
import path4 from "node:path";
|
|
684
|
+
import fs2 from "node:fs";
|
|
803
685
|
import { parse, stringify } from "@iarna/toml";
|
|
804
|
-
import { z as
|
|
805
|
-
var stringMapSchema3 =
|
|
686
|
+
import { z as z3 } from "zod";
|
|
687
|
+
var stringMapSchema3 = z3.preprocess((value) => {
|
|
806
688
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
807
689
|
return value;
|
|
808
690
|
}
|
|
809
691
|
return Object.fromEntries(Object.entries(value));
|
|
810
|
-
},
|
|
811
|
-
var codexEntrySchema =
|
|
812
|
-
enabled:
|
|
813
|
-
url:
|
|
692
|
+
}, z3.record(z3.string(), z3.string()));
|
|
693
|
+
var codexEntrySchema = z3.object({
|
|
694
|
+
enabled: z3.boolean().optional(),
|
|
695
|
+
url: z3.string().min(1).optional(),
|
|
814
696
|
headers: stringMapSchema3.optional(),
|
|
815
697
|
http_headers: stringMapSchema3.optional(),
|
|
816
|
-
command:
|
|
817
|
-
args:
|
|
698
|
+
command: z3.string().min(1).optional(),
|
|
699
|
+
args: z3.array(z3.string()).optional(),
|
|
818
700
|
env: stringMapSchema3.optional(),
|
|
819
|
-
cwd:
|
|
701
|
+
cwd: z3.string().min(1).optional()
|
|
820
702
|
}).passthrough();
|
|
821
703
|
|
|
822
704
|
class CodexAdapter {
|
|
823
705
|
id = "codex";
|
|
824
706
|
detectConfigPath() {
|
|
825
|
-
return
|
|
707
|
+
return path4.join(homeDir(), ".codex", "config.toml");
|
|
826
708
|
}
|
|
827
709
|
supportsHttp() {
|
|
828
710
|
return true;
|
|
@@ -830,10 +712,10 @@ class CodexAdapter {
|
|
|
830
712
|
scanForImports(_config, managedIndex) {
|
|
831
713
|
const configPath = this.detectConfigPath();
|
|
832
714
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
833
|
-
if (!configPath || !
|
|
715
|
+
if (!configPath || !fs2.existsSync(configPath)) {
|
|
834
716
|
return result;
|
|
835
717
|
}
|
|
836
|
-
const existing =
|
|
718
|
+
const existing = fs2.readFileSync(configPath, "utf8");
|
|
837
719
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
838
720
|
const mcpServers = doc.mcp_servers ?? {};
|
|
839
721
|
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
@@ -888,7 +770,7 @@ class CodexAdapter {
|
|
|
888
770
|
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
889
771
|
}
|
|
890
772
|
try {
|
|
891
|
-
const existing =
|
|
773
|
+
const existing = fs2.existsSync(configPath) ? fs2.readFileSync(configPath, "utf8") : "";
|
|
892
774
|
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
893
775
|
const mcpServers = doc.mcp_servers ?? {};
|
|
894
776
|
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
@@ -909,10 +791,10 @@ class CodexAdapter {
|
|
|
909
791
|
mcpServers[name] = entry;
|
|
910
792
|
}
|
|
911
793
|
doc.mcp_servers = mcpServers;
|
|
912
|
-
|
|
794
|
+
fs2.mkdirSync(path4.dirname(configPath), { recursive: true });
|
|
913
795
|
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
914
|
-
|
|
915
|
-
|
|
796
|
+
fs2.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
797
|
+
fs2.renameSync(tmpPath, configPath);
|
|
916
798
|
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
917
799
|
return okResult(this.id, configPath);
|
|
918
800
|
} catch (error) {
|
|
@@ -922,24 +804,30 @@ class CodexAdapter {
|
|
|
922
804
|
}
|
|
923
805
|
|
|
924
806
|
// src/adapters/cursor.ts
|
|
925
|
-
import
|
|
926
|
-
import { z as
|
|
927
|
-
var stringMapSchema4 =
|
|
928
|
-
var cursorEntrySchema =
|
|
929
|
-
type:
|
|
930
|
-
url:
|
|
807
|
+
import path5 from "node:path";
|
|
808
|
+
import { z as z4 } from "zod";
|
|
809
|
+
var stringMapSchema4 = z4.record(z4.string(), z4.string());
|
|
810
|
+
var cursorEntrySchema = z4.object({
|
|
811
|
+
type: z4.string().optional(),
|
|
812
|
+
url: z4.string().min(1).optional(),
|
|
931
813
|
headers: stringMapSchema4.optional(),
|
|
932
|
-
command:
|
|
933
|
-
args:
|
|
814
|
+
command: z4.string().min(1).optional(),
|
|
815
|
+
args: z4.array(z4.string()).optional(),
|
|
934
816
|
env: stringMapSchema4.optional(),
|
|
935
|
-
cwd:
|
|
936
|
-
disabled:
|
|
817
|
+
cwd: z4.string().min(1).optional(),
|
|
818
|
+
disabled: z4.boolean().optional()
|
|
937
819
|
}).passthrough();
|
|
938
820
|
|
|
939
821
|
class CursorAdapter {
|
|
940
822
|
id = "cursor";
|
|
941
823
|
detectConfigPath() {
|
|
942
|
-
|
|
824
|
+
if (process.platform === "linux") {
|
|
825
|
+
return path5.join(homeDir(), ".config", "Cursor", "User", "mcp.json");
|
|
826
|
+
}
|
|
827
|
+
if (process.platform === "win32") {
|
|
828
|
+
return path5.join(process.env.APPDATA ?? "", "Cursor", "User", "mcp.json");
|
|
829
|
+
}
|
|
830
|
+
return path5.join(homeDir(), "Library", "Application Support", "Cursor", "User", "mcp.json");
|
|
943
831
|
}
|
|
944
832
|
supportsHttp() {
|
|
945
833
|
return true;
|
|
@@ -1045,24 +933,36 @@ class CursorAdapter {
|
|
|
1045
933
|
}
|
|
1046
934
|
|
|
1047
935
|
// src/adapters/cline.ts
|
|
1048
|
-
import
|
|
1049
|
-
import
|
|
1050
|
-
import { z as
|
|
1051
|
-
var stringMapSchema5 =
|
|
1052
|
-
var clineEntrySchema =
|
|
1053
|
-
transportType:
|
|
1054
|
-
url:
|
|
936
|
+
import path6 from "node:path";
|
|
937
|
+
import fs3 from "node:fs";
|
|
938
|
+
import { z as z5 } from "zod";
|
|
939
|
+
var stringMapSchema5 = z5.record(z5.string(), z5.string());
|
|
940
|
+
var clineEntrySchema = z5.object({
|
|
941
|
+
transportType: z5.string().optional(),
|
|
942
|
+
url: z5.string().min(1).optional(),
|
|
1055
943
|
headers: stringMapSchema5.optional(),
|
|
1056
|
-
command:
|
|
1057
|
-
args:
|
|
944
|
+
command: z5.string().min(1).optional(),
|
|
945
|
+
args: z5.array(z5.string()).optional(),
|
|
1058
946
|
env: stringMapSchema5.optional(),
|
|
1059
|
-
cwd:
|
|
1060
|
-
disabled:
|
|
947
|
+
cwd: z5.string().min(1).optional(),
|
|
948
|
+
disabled: z5.boolean().optional()
|
|
1061
949
|
}).passthrough();
|
|
1062
950
|
function getCandidatePaths() {
|
|
951
|
+
if (process.platform === "linux") {
|
|
952
|
+
return [
|
|
953
|
+
path6.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
954
|
+
path6.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
955
|
+
];
|
|
956
|
+
}
|
|
957
|
+
if (process.platform === "win32") {
|
|
958
|
+
return [
|
|
959
|
+
path6.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
960
|
+
path6.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
961
|
+
];
|
|
962
|
+
}
|
|
1063
963
|
return [
|
|
1064
|
-
|
|
1065
|
-
|
|
964
|
+
path6.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
965
|
+
path6.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
1066
966
|
];
|
|
1067
967
|
}
|
|
1068
968
|
|
|
@@ -1071,7 +971,7 @@ class ClineAdapter {
|
|
|
1071
971
|
detectConfigPath() {
|
|
1072
972
|
const candidates = getCandidatePaths();
|
|
1073
973
|
for (const candidate of candidates) {
|
|
1074
|
-
if (
|
|
974
|
+
if (fs3.existsSync(candidate)) {
|
|
1075
975
|
return candidate;
|
|
1076
976
|
}
|
|
1077
977
|
}
|
|
@@ -1083,7 +983,7 @@ class ClineAdapter {
|
|
|
1083
983
|
scanForImports(_config, managedIndex) {
|
|
1084
984
|
const configPath = this.detectConfigPath();
|
|
1085
985
|
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1086
|
-
if (!configPath || !
|
|
986
|
+
if (!configPath || !fs3.existsSync(configPath)) {
|
|
1087
987
|
return result;
|
|
1088
988
|
}
|
|
1089
989
|
const raw = readJsonFile(configPath, {});
|
|
@@ -1141,6 +1041,9 @@ class ClineAdapter {
|
|
|
1141
1041
|
if (!configPath) {
|
|
1142
1042
|
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1143
1043
|
}
|
|
1044
|
+
if (!fs3.existsSync(configPath)) {
|
|
1045
|
+
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1046
|
+
}
|
|
1144
1047
|
try {
|
|
1145
1048
|
const raw = readJsonFile(configPath, {});
|
|
1146
1049
|
const servers = {
|
|
@@ -1178,24 +1081,24 @@ class ClineAdapter {
|
|
|
1178
1081
|
}
|
|
1179
1082
|
|
|
1180
1083
|
// src/adapters/kiro.ts
|
|
1181
|
-
import
|
|
1182
|
-
import
|
|
1183
|
-
import { z as
|
|
1184
|
-
var stringMapSchema6 =
|
|
1185
|
-
var kiroEntrySchema =
|
|
1186
|
-
url:
|
|
1084
|
+
import fs4 from "node:fs";
|
|
1085
|
+
import path7 from "node:path";
|
|
1086
|
+
import { z as z6 } from "zod";
|
|
1087
|
+
var stringMapSchema6 = z6.record(z6.string(), z6.string());
|
|
1088
|
+
var kiroEntrySchema = z6.object({
|
|
1089
|
+
url: z6.string().min(1).optional(),
|
|
1187
1090
|
headers: stringMapSchema6.optional(),
|
|
1188
|
-
command:
|
|
1189
|
-
args:
|
|
1091
|
+
command: z6.string().min(1).optional(),
|
|
1092
|
+
args: z6.array(z6.string()).optional(),
|
|
1190
1093
|
env: stringMapSchema6.optional(),
|
|
1191
|
-
cwd:
|
|
1192
|
-
disabled:
|
|
1094
|
+
cwd: z6.string().min(1).optional(),
|
|
1095
|
+
disabled: z6.boolean().optional()
|
|
1193
1096
|
}).passthrough();
|
|
1194
1097
|
|
|
1195
1098
|
class KiroAdapter {
|
|
1196
1099
|
id = "kiro";
|
|
1197
1100
|
detectConfigPath() {
|
|
1198
|
-
return
|
|
1101
|
+
return path7.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
1199
1102
|
}
|
|
1200
1103
|
supportsHttp() {
|
|
1201
1104
|
return true;
|
|
@@ -1295,46 +1198,46 @@ class KiroAdapter {
|
|
|
1295
1198
|
}
|
|
1296
1199
|
}
|
|
1297
1200
|
syncSkills(skills) {
|
|
1298
|
-
const skillsDir =
|
|
1299
|
-
if (!
|
|
1300
|
-
|
|
1201
|
+
const skillsDir = path7.join(homeDir(), ".kiro", "skills");
|
|
1202
|
+
if (!fs4.existsSync(skillsDir)) {
|
|
1203
|
+
fs4.mkdirSync(skillsDir, { recursive: true });
|
|
1301
1204
|
}
|
|
1302
|
-
const existingFiles =
|
|
1205
|
+
const existingFiles = fs4.readdirSync(skillsDir);
|
|
1303
1206
|
const managedIds = new Set(skills.map((s) => s.id));
|
|
1304
1207
|
for (const file of existingFiles) {
|
|
1305
1208
|
if (file.endsWith(".md")) {
|
|
1306
|
-
const id =
|
|
1209
|
+
const id = path7.basename(file, ".md");
|
|
1307
1210
|
if (!managedIds.has(id)) {
|
|
1308
|
-
|
|
1211
|
+
fs4.unlinkSync(path7.join(skillsDir, file));
|
|
1309
1212
|
}
|
|
1310
1213
|
}
|
|
1311
1214
|
}
|
|
1312
1215
|
for (const skill of skills) {
|
|
1313
|
-
const targetPath =
|
|
1314
|
-
|
|
1216
|
+
const targetPath = path7.join(skillsDir, `${skill.id}.md`);
|
|
1217
|
+
fs4.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1315
1218
|
}
|
|
1316
1219
|
}
|
|
1317
1220
|
}
|
|
1318
1221
|
|
|
1319
1222
|
// src/adapters/opencode.ts
|
|
1320
|
-
import
|
|
1321
|
-
import { z as
|
|
1322
|
-
var stringMapSchema7 =
|
|
1323
|
-
var openCodeEntrySchema =
|
|
1324
|
-
type:
|
|
1325
|
-
url:
|
|
1223
|
+
import path8 from "node:path";
|
|
1224
|
+
import { z as z7 } from "zod";
|
|
1225
|
+
var stringMapSchema7 = z7.record(z7.string(), z7.string());
|
|
1226
|
+
var openCodeEntrySchema = z7.object({
|
|
1227
|
+
type: z7.string().optional(),
|
|
1228
|
+
url: z7.string().min(1).optional(),
|
|
1326
1229
|
headers: stringMapSchema7.optional(),
|
|
1327
|
-
command:
|
|
1328
|
-
args:
|
|
1230
|
+
command: z7.string().min(1).optional(),
|
|
1231
|
+
args: z7.array(z7.string()).optional(),
|
|
1329
1232
|
env: stringMapSchema7.optional(),
|
|
1330
|
-
cwd:
|
|
1331
|
-
enabled:
|
|
1233
|
+
cwd: z7.string().min(1).optional(),
|
|
1234
|
+
enabled: z7.boolean().optional()
|
|
1332
1235
|
}).passthrough();
|
|
1333
1236
|
|
|
1334
1237
|
class OpenCodeAdapter {
|
|
1335
1238
|
id = "opencode";
|
|
1336
1239
|
detectConfigPath() {
|
|
1337
|
-
return
|
|
1240
|
+
return path8.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
1338
1241
|
}
|
|
1339
1242
|
supportsHttp() {
|
|
1340
1243
|
return true;
|
|
@@ -1437,24 +1340,30 @@ class OpenCodeAdapter {
|
|
|
1437
1340
|
}
|
|
1438
1341
|
|
|
1439
1342
|
// src/adapters/vscode.ts
|
|
1440
|
-
import
|
|
1441
|
-
import { z as
|
|
1442
|
-
var stringMapSchema8 =
|
|
1443
|
-
var vscodeEntrySchema =
|
|
1444
|
-
type:
|
|
1445
|
-
url:
|
|
1343
|
+
import path9 from "node:path";
|
|
1344
|
+
import { z as z8 } from "zod";
|
|
1345
|
+
var stringMapSchema8 = z8.record(z8.string(), z8.string());
|
|
1346
|
+
var vscodeEntrySchema = z8.object({
|
|
1347
|
+
type: z8.string().optional(),
|
|
1348
|
+
url: z8.string().min(1).optional(),
|
|
1446
1349
|
headers: stringMapSchema8.optional(),
|
|
1447
|
-
command:
|
|
1448
|
-
args:
|
|
1350
|
+
command: z8.string().min(1).optional(),
|
|
1351
|
+
args: z8.array(z8.string()).optional(),
|
|
1449
1352
|
env: stringMapSchema8.optional(),
|
|
1450
|
-
cwd:
|
|
1451
|
-
disabled:
|
|
1353
|
+
cwd: z8.string().min(1).optional(),
|
|
1354
|
+
disabled: z8.boolean().optional()
|
|
1452
1355
|
}).passthrough();
|
|
1453
1356
|
|
|
1454
1357
|
class VsCodeAdapter {
|
|
1455
1358
|
id = "vscode";
|
|
1456
1359
|
detectConfigPath() {
|
|
1457
|
-
|
|
1360
|
+
if (process.platform === "linux") {
|
|
1361
|
+
return path9.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
1362
|
+
}
|
|
1363
|
+
if (process.platform === "win32") {
|
|
1364
|
+
return path9.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
1365
|
+
}
|
|
1366
|
+
return path9.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
1458
1367
|
}
|
|
1459
1368
|
supportsHttp() {
|
|
1460
1369
|
return true;
|
|
@@ -1560,24 +1469,24 @@ class VsCodeAdapter {
|
|
|
1560
1469
|
}
|
|
1561
1470
|
|
|
1562
1471
|
// src/adapters/qwen.ts
|
|
1563
|
-
import
|
|
1564
|
-
import
|
|
1565
|
-
import { z as
|
|
1566
|
-
var stringMapSchema9 =
|
|
1567
|
-
var qwenEntrySchema =
|
|
1568
|
-
httpUrl:
|
|
1472
|
+
import fs5 from "node:fs";
|
|
1473
|
+
import path10 from "node:path";
|
|
1474
|
+
import { z as z9 } from "zod";
|
|
1475
|
+
var stringMapSchema9 = z9.record(z9.string(), z9.string());
|
|
1476
|
+
var qwenEntrySchema = z9.object({
|
|
1477
|
+
httpUrl: z9.string().min(1).optional(),
|
|
1569
1478
|
headers: stringMapSchema9.optional(),
|
|
1570
|
-
command:
|
|
1571
|
-
args:
|
|
1479
|
+
command: z9.string().min(1).optional(),
|
|
1480
|
+
args: z9.array(z9.string()).optional(),
|
|
1572
1481
|
env: stringMapSchema9.optional(),
|
|
1573
|
-
cwd:
|
|
1574
|
-
disabled:
|
|
1482
|
+
cwd: z9.string().min(1).optional(),
|
|
1483
|
+
disabled: z9.boolean().optional()
|
|
1575
1484
|
}).passthrough();
|
|
1576
1485
|
|
|
1577
1486
|
class QwenAdapter {
|
|
1578
1487
|
id = "qwen";
|
|
1579
1488
|
detectConfigPath() {
|
|
1580
|
-
return
|
|
1489
|
+
return path10.join(homeDir(), ".qwen", "settings.json");
|
|
1581
1490
|
}
|
|
1582
1491
|
supportsHttp() {
|
|
1583
1492
|
return true;
|
|
@@ -1674,23 +1583,23 @@ class QwenAdapter {
|
|
|
1674
1583
|
}
|
|
1675
1584
|
}
|
|
1676
1585
|
syncSkills(skills) {
|
|
1677
|
-
const skillsDir =
|
|
1678
|
-
if (!
|
|
1679
|
-
|
|
1586
|
+
const skillsDir = path10.join(homeDir(), ".qwen", "skills");
|
|
1587
|
+
if (!fs5.existsSync(skillsDir)) {
|
|
1588
|
+
fs5.mkdirSync(skillsDir, { recursive: true });
|
|
1680
1589
|
}
|
|
1681
|
-
const existingFiles =
|
|
1590
|
+
const existingFiles = fs5.readdirSync(skillsDir);
|
|
1682
1591
|
const managedIds = new Set(skills.map((s) => s.id));
|
|
1683
1592
|
for (const file of existingFiles) {
|
|
1684
1593
|
if (file.endsWith(".md")) {
|
|
1685
|
-
const id =
|
|
1594
|
+
const id = path10.basename(file, ".md");
|
|
1686
1595
|
if (!managedIds.has(id)) {
|
|
1687
|
-
|
|
1596
|
+
fs5.unlinkSync(path10.join(skillsDir, file));
|
|
1688
1597
|
}
|
|
1689
1598
|
}
|
|
1690
1599
|
}
|
|
1691
1600
|
for (const skill of skills) {
|
|
1692
|
-
const targetPath =
|
|
1693
|
-
|
|
1601
|
+
const targetPath = path10.join(skillsDir, `${skill.id}.md`);
|
|
1602
|
+
fs5.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1694
1603
|
}
|
|
1695
1604
|
}
|
|
1696
1605
|
}
|
|
@@ -1746,6 +1655,9 @@ function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
|
1746
1655
|
enabled: isServerEnabled(spec)
|
|
1747
1656
|
}));
|
|
1748
1657
|
}
|
|
1658
|
+
function isClientInternalServerCommand(candidate) {
|
|
1659
|
+
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
1660
|
+
}
|
|
1749
1661
|
function syncAllClients(config, secrets, targetClients) {
|
|
1750
1662
|
const adapters = getAdapters();
|
|
1751
1663
|
const managedIndexPath = getManagedIndexPath();
|
|
@@ -1764,6 +1676,16 @@ function syncAllClients(config, secrets, targetClients) {
|
|
|
1764
1676
|
const scan = adapter.scanForImports(config, managedIndex);
|
|
1765
1677
|
imports.skipped.push(...scan.skipped);
|
|
1766
1678
|
for (const candidate of scan.candidates) {
|
|
1679
|
+
if (isClientInternalServerCommand(candidate)) {
|
|
1680
|
+
imports.skipped.push({
|
|
1681
|
+
clientId: candidate.clientId,
|
|
1682
|
+
configPath: candidate.configPath,
|
|
1683
|
+
sourceEntryName: candidate.sourceEntryName,
|
|
1684
|
+
serverName: candidate.serverName,
|
|
1685
|
+
reason: "client-internal server"
|
|
1686
|
+
});
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1767
1689
|
const existing = config.servers[candidate.serverName];
|
|
1768
1690
|
if (!existing) {
|
|
1769
1691
|
config.servers[candidate.serverName] = candidate.spec;
|
|
@@ -1963,13 +1885,299 @@ function maybePrefixBearer(target, value, raw = false) {
|
|
|
1963
1885
|
return `Bearer ${value}`;
|
|
1964
1886
|
}
|
|
1965
1887
|
|
|
1888
|
+
// src/core/spawn-env.ts
|
|
1889
|
+
import os from "node:os";
|
|
1890
|
+
import path11 from "node:path";
|
|
1891
|
+
import fs6 from "node:fs";
|
|
1892
|
+
function existingNvmCurrentBin() {
|
|
1893
|
+
const nvmDir = process.env.NVM_DIR ?? path11.join(os.homedir(), ".nvm");
|
|
1894
|
+
const current = path11.join(nvmDir, "current", "bin");
|
|
1895
|
+
return fs6.existsSync(current) ? current : null;
|
|
1896
|
+
}
|
|
1897
|
+
function buildEnrichedPath(current = process.env.PATH ?? "") {
|
|
1898
|
+
const candidates = [
|
|
1899
|
+
...current.split(":"),
|
|
1900
|
+
"/opt/homebrew/bin",
|
|
1901
|
+
"/usr/local/bin",
|
|
1902
|
+
path11.join(os.homedir(), ".bun", "bin"),
|
|
1903
|
+
path11.join(os.homedir(), ".local", "bin"),
|
|
1904
|
+
existingNvmCurrentBin()
|
|
1905
|
+
].filter((entry) => Boolean(entry));
|
|
1906
|
+
const seen = new Set;
|
|
1907
|
+
const entries = [];
|
|
1908
|
+
for (const candidate of candidates) {
|
|
1909
|
+
if (!candidate || seen.has(candidate)) {
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
seen.add(candidate);
|
|
1913
|
+
entries.push(candidate);
|
|
1914
|
+
}
|
|
1915
|
+
return entries.join(":");
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/core/oauth.ts
|
|
1919
|
+
import http from "node:http";
|
|
1920
|
+
import crypto from "node:crypto";
|
|
1921
|
+
import {
|
|
1922
|
+
auth,
|
|
1923
|
+
discoverOAuthServerInfo,
|
|
1924
|
+
refreshAuthorization
|
|
1925
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
1926
|
+
function oauthSecretName(serverName, suffix) {
|
|
1927
|
+
return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
|
|
1928
|
+
}
|
|
1929
|
+
function readJsonSecret(secrets, name) {
|
|
1930
|
+
const raw = secrets.getSecret(name);
|
|
1931
|
+
if (!raw) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
try {
|
|
1935
|
+
return JSON.parse(raw);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function writeJsonSecret(secrets, name, value) {
|
|
1941
|
+
secrets.setSecret(name, JSON.stringify(value));
|
|
1942
|
+
}
|
|
1943
|
+
function isOAuthReference(value) {
|
|
1944
|
+
return value.startsWith("oauth://");
|
|
1945
|
+
}
|
|
1946
|
+
function oauthReferenceServerName(value) {
|
|
1947
|
+
if (!isOAuthReference(value)) {
|
|
1948
|
+
throw new Error(`Not an OAuth reference: ${value}`);
|
|
1949
|
+
}
|
|
1950
|
+
return decodeURIComponent(value.slice("oauth://".length));
|
|
1951
|
+
}
|
|
1952
|
+
function tokensAreExpiring(tokens) {
|
|
1953
|
+
if (!tokens.tokens.expires_in) {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
|
|
1957
|
+
return Date.now() >= expiresAt - 60000;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
class McpxOAuthProvider {
|
|
1961
|
+
#serverName;
|
|
1962
|
+
#secrets;
|
|
1963
|
+
#redirectUrlValue;
|
|
1964
|
+
#stateValue;
|
|
1965
|
+
constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
|
|
1966
|
+
this.#serverName = serverName;
|
|
1967
|
+
this.#secrets = secrets;
|
|
1968
|
+
this.#redirectUrlValue = redirectUrl;
|
|
1969
|
+
this.#stateValue = state;
|
|
1970
|
+
}
|
|
1971
|
+
get redirectUrl() {
|
|
1972
|
+
return this.#redirectUrlValue;
|
|
1973
|
+
}
|
|
1974
|
+
get clientMetadata() {
|
|
1975
|
+
if (!this.#redirectUrlValue) {
|
|
1976
|
+
return {
|
|
1977
|
+
redirect_uris: [],
|
|
1978
|
+
token_endpoint_auth_method: "none",
|
|
1979
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1980
|
+
response_types: ["code"],
|
|
1981
|
+
client_name: "mcpx"
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
redirect_uris: [this.#redirectUrlValue],
|
|
1986
|
+
token_endpoint_auth_method: "none",
|
|
1987
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1988
|
+
response_types: ["code"],
|
|
1989
|
+
client_name: "mcpx"
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
state() {
|
|
1993
|
+
return this.#stateValue;
|
|
1994
|
+
}
|
|
1995
|
+
clientInformation() {
|
|
1996
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
|
|
1997
|
+
}
|
|
1998
|
+
saveClientInformation(clientInformation) {
|
|
1999
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
|
|
2000
|
+
}
|
|
2001
|
+
tokens() {
|
|
2002
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
|
|
2003
|
+
}
|
|
2004
|
+
saveTokens(tokens) {
|
|
2005
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
2006
|
+
tokens,
|
|
2007
|
+
obtainedAt: Date.now()
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
redirectToAuthorization(_authorizationUrl) {}
|
|
2011
|
+
saveCodeVerifier(codeVerifier) {
|
|
2012
|
+
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
2013
|
+
}
|
|
2014
|
+
codeVerifier() {
|
|
2015
|
+
const verifier = this.#secrets.getSecret(oauthSecretName(this.#serverName, "verifier"));
|
|
2016
|
+
if (!verifier) {
|
|
2017
|
+
throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
|
|
2018
|
+
}
|
|
2019
|
+
return verifier;
|
|
2020
|
+
}
|
|
2021
|
+
saveDiscoveryState(state) {
|
|
2022
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
|
|
2023
|
+
}
|
|
2024
|
+
discoveryState() {
|
|
2025
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
|
|
2026
|
+
}
|
|
2027
|
+
invalidateCredentials(scope) {
|
|
2028
|
+
const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
|
|
2029
|
+
for (const suffix of suffixes) {
|
|
2030
|
+
this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
class InteractiveOAuthProvider extends McpxOAuthProvider {
|
|
2036
|
+
#openUrl;
|
|
2037
|
+
constructor(serverName, secrets, redirectUrl, state, openUrl) {
|
|
2038
|
+
super(serverName, secrets, redirectUrl, state);
|
|
2039
|
+
this.#openUrl = openUrl;
|
|
2040
|
+
}
|
|
2041
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
2042
|
+
await this.#openUrl(authorizationUrl.toString());
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function listen(server) {
|
|
2046
|
+
return new Promise((resolve, reject) => {
|
|
2047
|
+
server.once("error", reject);
|
|
2048
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2049
|
+
const address = server.address();
|
|
2050
|
+
if (!address || typeof address === "string") {
|
|
2051
|
+
reject(new Error("Failed to start OAuth callback server."));
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
resolve(address.port);
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
function closeServer(server) {
|
|
2059
|
+
return new Promise((resolve) => {
|
|
2060
|
+
server.close(() => resolve());
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
function waitForAuthorizationCode(server, expectedState, timeoutMs = 120000) {
|
|
2064
|
+
return new Promise((resolve, reject) => {
|
|
2065
|
+
const timeout = setTimeout(() => {
|
|
2066
|
+
reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
|
|
2067
|
+
}, timeoutMs);
|
|
2068
|
+
server.on("request", (request, response) => {
|
|
2069
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
2070
|
+
const code = url.searchParams.get("code");
|
|
2071
|
+
const state = url.searchParams.get("state");
|
|
2072
|
+
const error = url.searchParams.get("error");
|
|
2073
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
2074
|
+
if (error) {
|
|
2075
|
+
response.statusCode = 400;
|
|
2076
|
+
response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
|
|
2077
|
+
clearTimeout(timeout);
|
|
2078
|
+
reject(new Error(`OAuth authorization failed: ${error}`));
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (!code) {
|
|
2082
|
+
response.statusCode = 400;
|
|
2083
|
+
response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (state !== expectedState) {
|
|
2087
|
+
response.statusCode = 400;
|
|
2088
|
+
response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
|
|
2089
|
+
clearTimeout(timeout);
|
|
2090
|
+
reject(new Error("OAuth state mismatch."));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
|
|
2094
|
+
clearTimeout(timeout);
|
|
2095
|
+
resolve(code);
|
|
2096
|
+
});
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
function bindOAuthReference(config, serverName) {
|
|
2100
|
+
const spec = config.servers[serverName];
|
|
2101
|
+
if (!spec) {
|
|
2102
|
+
throw new Error(`Server "${serverName}" not found.`);
|
|
2103
|
+
}
|
|
2104
|
+
if (spec.transport !== "http") {
|
|
2105
|
+
throw new Error(`OAuth login only supports HTTP servers.`);
|
|
2106
|
+
}
|
|
2107
|
+
spec.headers = {
|
|
2108
|
+
...spec.headers ?? {},
|
|
2109
|
+
Authorization: `oauth://${encodeURIComponent(serverName)}`
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath) {
|
|
2113
|
+
const state = crypto.randomUUID();
|
|
2114
|
+
const callbackServer = http.createServer();
|
|
2115
|
+
const port = await listen(callbackServer);
|
|
2116
|
+
const redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
2117
|
+
const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, openUrl);
|
|
2118
|
+
const codePromise = waitForAuthorizationCode(callbackServer, state);
|
|
2119
|
+
try {
|
|
2120
|
+
const initial = await auth(provider, {
|
|
2121
|
+
serverUrl: spec.url
|
|
2122
|
+
});
|
|
2123
|
+
if (initial !== "REDIRECT") {
|
|
2124
|
+
throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
|
|
2125
|
+
}
|
|
2126
|
+
const authorizationCode = await codePromise;
|
|
2127
|
+
const result = await auth(provider, {
|
|
2128
|
+
serverUrl: spec.url,
|
|
2129
|
+
authorizationCode
|
|
2130
|
+
});
|
|
2131
|
+
if (result !== "AUTHORIZED") {
|
|
2132
|
+
throw new Error(`OAuth login did not authorize "${serverName}".`);
|
|
2133
|
+
}
|
|
2134
|
+
const config = loadConfig(configPath);
|
|
2135
|
+
bindOAuthReference(config, serverName);
|
|
2136
|
+
saveConfig(config, configPath);
|
|
2137
|
+
syncAllClients(config, secrets);
|
|
2138
|
+
return { serverName, authorized: true };
|
|
2139
|
+
} finally {
|
|
2140
|
+
await closeServer(callbackServer);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
2144
|
+
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
2145
|
+
if (!stored) {
|
|
2146
|
+
throw new Error(`No OAuth tokens stored for "${serverName}". Run mcpx auth login ${serverName}.`);
|
|
2147
|
+
}
|
|
2148
|
+
if (!options.forceRefresh && !tokensAreExpiring(stored)) {
|
|
2149
|
+
return stored.tokens.access_token;
|
|
2150
|
+
}
|
|
2151
|
+
if (!stored.tokens.refresh_token) {
|
|
2152
|
+
if (!options.forceRefresh) {
|
|
2153
|
+
return stored.tokens.access_token;
|
|
2154
|
+
}
|
|
2155
|
+
throw new Error(`No OAuth refresh token stored for "${serverName}". Re-authentication required.`);
|
|
2156
|
+
}
|
|
2157
|
+
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
2158
|
+
const clientInformation = provider.clientInformation();
|
|
2159
|
+
if (!clientInformation) {
|
|
2160
|
+
throw new Error(`No OAuth client information stored for "${serverName}". Re-authentication required.`);
|
|
2161
|
+
}
|
|
2162
|
+
const cachedDiscovery = provider.discoveryState();
|
|
2163
|
+
const serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
2164
|
+
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
2165
|
+
metadata: serverInfo.authorizationServerMetadata,
|
|
2166
|
+
clientInformation,
|
|
2167
|
+
refreshToken: stored.tokens.refresh_token,
|
|
2168
|
+
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
2169
|
+
});
|
|
2170
|
+
provider.saveTokens(refreshed);
|
|
2171
|
+
return refreshed.access_token;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1966
2174
|
// src/version.ts
|
|
1967
2175
|
var APP_VERSION = "0.1.3";
|
|
1968
2176
|
|
|
1969
2177
|
// src/core/update.ts
|
|
1970
|
-
import
|
|
1971
|
-
import
|
|
1972
|
-
import { execFileSync as
|
|
2178
|
+
import fs7 from "node:fs";
|
|
2179
|
+
import path12 from "node:path";
|
|
2180
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1973
2181
|
function compareVersions(a, b) {
|
|
1974
2182
|
const partsA = a.split(".").map(Number);
|
|
1975
2183
|
const partsB = b.split(".").map(Number);
|
|
@@ -1985,7 +2193,7 @@ function compareVersions(a, b) {
|
|
|
1985
2193
|
}
|
|
1986
2194
|
async function checkForUpdates() {
|
|
1987
2195
|
try {
|
|
1988
|
-
const output =
|
|
2196
|
+
const output = execFileSync2("npm", ["view", "@kwonye/mcpx", "version"], {
|
|
1989
2197
|
encoding: "utf8",
|
|
1990
2198
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1991
2199
|
}).trim();
|
|
@@ -2007,17 +2215,17 @@ async function checkForUpdates() {
|
|
|
2007
2215
|
}
|
|
2008
2216
|
function getStagedUpdate() {
|
|
2009
2217
|
const stagedPath = getStagedVersionPath();
|
|
2010
|
-
if (!
|
|
2218
|
+
if (!fs7.existsSync(stagedPath)) {
|
|
2011
2219
|
return null;
|
|
2012
2220
|
}
|
|
2013
2221
|
try {
|
|
2014
|
-
const raw =
|
|
2222
|
+
const raw = fs7.readFileSync(stagedPath, "utf8");
|
|
2015
2223
|
const info = JSON.parse(raw);
|
|
2016
2224
|
if (!info.version || !info.cliPath) {
|
|
2017
2225
|
return null;
|
|
2018
2226
|
}
|
|
2019
|
-
const absoluteCliPath =
|
|
2020
|
-
if (!
|
|
2227
|
+
const absoluteCliPath = path12.isAbsolute(info.cliPath) ? info.cliPath : path12.join(getUpdatesDir(), info.cliPath);
|
|
2228
|
+
if (!fs7.existsSync(absoluteCliPath)) {
|
|
2021
2229
|
return null;
|
|
2022
2230
|
}
|
|
2023
2231
|
return {
|
|
@@ -2033,15 +2241,15 @@ function stageUpdate(version, cliPath) {
|
|
|
2033
2241
|
ensureDir(updatesDir);
|
|
2034
2242
|
const stagedInfo = {
|
|
2035
2243
|
version,
|
|
2036
|
-
cliPath:
|
|
2244
|
+
cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
|
|
2037
2245
|
stagedAt: new Date().toISOString()
|
|
2038
2246
|
};
|
|
2039
|
-
|
|
2247
|
+
fs7.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
2040
2248
|
}
|
|
2041
2249
|
function clearStagedUpdate() {
|
|
2042
2250
|
const stagedPath = getStagedVersionPath();
|
|
2043
|
-
if (
|
|
2044
|
-
|
|
2251
|
+
if (fs7.existsSync(stagedPath)) {
|
|
2252
|
+
fs7.unlinkSync(stagedPath);
|
|
2045
2253
|
}
|
|
2046
2254
|
}
|
|
2047
2255
|
function getStagedCliPath() {
|
|
@@ -2050,31 +2258,31 @@ function getStagedCliPath() {
|
|
|
2050
2258
|
}
|
|
2051
2259
|
function removeOldVersions(keepCount = 2) {
|
|
2052
2260
|
const updatesDir = getUpdatesDir();
|
|
2053
|
-
if (!
|
|
2261
|
+
if (!fs7.existsSync(updatesDir)) {
|
|
2054
2262
|
return;
|
|
2055
2263
|
}
|
|
2056
|
-
const entries =
|
|
2264
|
+
const entries = fs7.readdirSync(updatesDir, { withFileTypes: true });
|
|
2057
2265
|
const versionDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith("v")).map((entry) => entry.name).sort((a, b) => compareVersions(b.slice(1), a.slice(1)));
|
|
2058
2266
|
if (versionDirs.length <= keepCount) {
|
|
2059
2267
|
return;
|
|
2060
2268
|
}
|
|
2061
2269
|
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
2062
|
-
const oldPath =
|
|
2270
|
+
const oldPath = path12.join(updatesDir, oldVersion);
|
|
2063
2271
|
try {
|
|
2064
|
-
|
|
2272
|
+
fs7.rmSync(oldPath, { recursive: true, force: true });
|
|
2065
2273
|
} catch {}
|
|
2066
2274
|
}
|
|
2067
2275
|
}
|
|
2068
2276
|
|
|
2069
2277
|
// src/core/update-manager.ts
|
|
2070
|
-
import
|
|
2071
|
-
import
|
|
2278
|
+
import fs8 from "node:fs";
|
|
2279
|
+
import path13 from "node:path";
|
|
2072
2280
|
import { execSync, spawn } from "node:child_process";
|
|
2073
2281
|
function acquireLock() {
|
|
2074
2282
|
const lockPath = getUpdateLockPath();
|
|
2075
2283
|
try {
|
|
2076
|
-
ensureDir(
|
|
2077
|
-
|
|
2284
|
+
ensureDir(path13.dirname(lockPath));
|
|
2285
|
+
fs8.writeFileSync(lockPath, `${process.pid}
|
|
2078
2286
|
`, { flag: "wx" });
|
|
2079
2287
|
return true;
|
|
2080
2288
|
} catch {
|
|
@@ -2084,25 +2292,25 @@ function acquireLock() {
|
|
|
2084
2292
|
function releaseLock() {
|
|
2085
2293
|
const lockPath = getUpdateLockPath();
|
|
2086
2294
|
try {
|
|
2087
|
-
|
|
2295
|
+
fs8.unlinkSync(lockPath);
|
|
2088
2296
|
} catch {}
|
|
2089
2297
|
}
|
|
2090
2298
|
function isUpdateInProgress() {
|
|
2091
2299
|
const lockPath = getUpdateLockPath();
|
|
2092
|
-
if (!
|
|
2300
|
+
if (!fs8.existsSync(lockPath)) {
|
|
2093
2301
|
return false;
|
|
2094
2302
|
}
|
|
2095
2303
|
try {
|
|
2096
|
-
const raw =
|
|
2304
|
+
const raw = fs8.readFileSync(lockPath, "utf8").trim();
|
|
2097
2305
|
const pid = Number(raw);
|
|
2098
2306
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2099
|
-
|
|
2307
|
+
fs8.unlinkSync(lockPath);
|
|
2100
2308
|
return false;
|
|
2101
2309
|
}
|
|
2102
2310
|
process.kill(pid, 0);
|
|
2103
2311
|
return true;
|
|
2104
2312
|
} catch {
|
|
2105
|
-
|
|
2313
|
+
fs8.unlinkSync(lockPath);
|
|
2106
2314
|
return false;
|
|
2107
2315
|
}
|
|
2108
2316
|
}
|
|
@@ -2113,8 +2321,8 @@ async function downloadAndStageUpdate() {
|
|
|
2113
2321
|
}
|
|
2114
2322
|
const targetVersion = updateStatus.latestVersion;
|
|
2115
2323
|
const updatesDir = getUpdatesDir();
|
|
2116
|
-
const versionDir =
|
|
2117
|
-
if (
|
|
2324
|
+
const versionDir = path13.join(updatesDir, `v${targetVersion}`);
|
|
2325
|
+
if (fs8.existsSync(versionDir)) {
|
|
2118
2326
|
const existingStaged = getStagedUpdate();
|
|
2119
2327
|
if (existingStaged?.version === targetVersion) {
|
|
2120
2328
|
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
@@ -2126,8 +2334,8 @@ async function downloadAndStageUpdate() {
|
|
|
2126
2334
|
cwd: versionDir,
|
|
2127
2335
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2128
2336
|
});
|
|
2129
|
-
const tarballPath =
|
|
2130
|
-
if (!
|
|
2337
|
+
const tarballPath = path13.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
2338
|
+
if (!fs8.existsSync(tarballPath)) {
|
|
2131
2339
|
throw new Error("Downloaded tarball not found");
|
|
2132
2340
|
}
|
|
2133
2341
|
execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
|
|
@@ -2138,8 +2346,8 @@ async function downloadAndStageUpdate() {
|
|
|
2138
2346
|
cwd: versionDir,
|
|
2139
2347
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2140
2348
|
});
|
|
2141
|
-
const cliPath =
|
|
2142
|
-
if (!
|
|
2349
|
+
const cliPath = path13.join(versionDir, "dist", "cli.js");
|
|
2350
|
+
if (!fs8.existsSync(cliPath)) {
|
|
2143
2351
|
throw new Error("CLI entry point not found in downloaded package");
|
|
2144
2352
|
}
|
|
2145
2353
|
stageUpdate(targetVersion, cliPath);
|
|
@@ -2147,7 +2355,7 @@ async function downloadAndStageUpdate() {
|
|
|
2147
2355
|
return { success: true, version: targetVersion };
|
|
2148
2356
|
} catch (error) {
|
|
2149
2357
|
try {
|
|
2150
|
-
|
|
2358
|
+
fs8.rmSync(versionDir, { recursive: true, force: true });
|
|
2151
2359
|
} catch {}
|
|
2152
2360
|
return {
|
|
2153
2361
|
success: false,
|
|
@@ -2225,14 +2433,14 @@ function performRollback() {
|
|
|
2225
2433
|
}
|
|
2226
2434
|
|
|
2227
2435
|
// src/core/daemon.ts
|
|
2228
|
-
import
|
|
2436
|
+
import fs9 from "node:fs";
|
|
2229
2437
|
import net from "node:net";
|
|
2230
2438
|
import { spawn as spawn2 } from "node:child_process";
|
|
2231
2439
|
|
|
2232
2440
|
// src/gateway/server.ts
|
|
2233
|
-
import
|
|
2234
|
-
import { URL } from "node:url";
|
|
2235
|
-
import
|
|
2441
|
+
import http2 from "node:http";
|
|
2442
|
+
import { URL as URL2 } from "node:url";
|
|
2443
|
+
import crypto2 from "node:crypto";
|
|
2236
2444
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2237
2445
|
import {
|
|
2238
2446
|
StdioClientTransport,
|
|
@@ -2321,16 +2529,16 @@ function getWellKnownPrefix(pathname) {
|
|
|
2321
2529
|
return null;
|
|
2322
2530
|
}
|
|
2323
2531
|
function getUpstreamPathSuffixForWellKnown(upstream) {
|
|
2324
|
-
const upstreamUrl = new
|
|
2532
|
+
const upstreamUrl = new URL2(upstream.spec.url);
|
|
2325
2533
|
if (upstreamUrl.pathname === "/") {
|
|
2326
2534
|
return "";
|
|
2327
2535
|
}
|
|
2328
2536
|
return upstreamUrl.pathname.endsWith("/") ? upstreamUrl.pathname.slice(0, -1) : upstreamUrl.pathname;
|
|
2329
2537
|
}
|
|
2330
2538
|
function buildWellKnownUpstreamUrl(upstream, prefix) {
|
|
2331
|
-
const upstreamUrl = new
|
|
2539
|
+
const upstreamUrl = new URL2(upstream.spec.url);
|
|
2332
2540
|
const suffix = getUpstreamPathSuffixForWellKnown(upstream);
|
|
2333
|
-
return new
|
|
2541
|
+
return new URL2(`${prefix}${suffix}`, upstreamUrl.origin);
|
|
2334
2542
|
}
|
|
2335
2543
|
function getLocalOriginFromRequest(request) {
|
|
2336
2544
|
return `http://${request.headers.host ?? "127.0.0.1"}`;
|
|
@@ -2346,7 +2554,7 @@ function appendUpstreamQuery(url, upstream) {
|
|
|
2346
2554
|
if (!upstream) {
|
|
2347
2555
|
return url;
|
|
2348
2556
|
}
|
|
2349
|
-
const parsed = new
|
|
2557
|
+
const parsed = new URL2(url);
|
|
2350
2558
|
parsed.searchParams.set("upstream", upstream);
|
|
2351
2559
|
return parsed.toString();
|
|
2352
2560
|
}
|
|
@@ -2411,6 +2619,62 @@ async function callUpstream(upstream, method, params, id, secrets, runtime, pass
|
|
|
2411
2619
|
}
|
|
2412
2620
|
return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
|
|
2413
2621
|
}
|
|
2622
|
+
async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
2623
|
+
if (!runtime.tokenCache) {
|
|
2624
|
+
runtime.tokenCache = new Map;
|
|
2625
|
+
}
|
|
2626
|
+
const results = {};
|
|
2627
|
+
const upstreams = listUpstreams(config);
|
|
2628
|
+
for (const upstream of upstreams) {
|
|
2629
|
+
const fingerprint = specFingerprint(upstream.spec);
|
|
2630
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
2631
|
+
if (cached?.fingerprint === fingerprint) {
|
|
2632
|
+
results[upstream.name] = cached.count;
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
let toolsCount = 0;
|
|
2636
|
+
let resourcesCount = 0;
|
|
2637
|
+
let promptsCount = 0;
|
|
2638
|
+
const errors = [];
|
|
2639
|
+
try {
|
|
2640
|
+
const toolsResult = await callUpstream(upstream, "tools/list", {}, "token-tools", secrets, runtime);
|
|
2641
|
+
if (toolsResult && toolsResult.tools) {
|
|
2642
|
+
toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
|
|
2643
|
+
}
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
errors.push(`tools/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2646
|
+
}
|
|
2647
|
+
try {
|
|
2648
|
+
const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
|
|
2649
|
+
if (resourcesResult && resourcesResult.resources) {
|
|
2650
|
+
resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
|
|
2651
|
+
}
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
errors.push(`resources/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
|
|
2657
|
+
if (promptsResult && promptsResult.prompts) {
|
|
2658
|
+
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
2659
|
+
}
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
errors.push(`prompts/list: ${error instanceof Error ? error.message : String(error)}`);
|
|
2662
|
+
}
|
|
2663
|
+
const total = toolsCount + resourcesCount + promptsCount;
|
|
2664
|
+
const countObj = {
|
|
2665
|
+
tools: toolsCount,
|
|
2666
|
+
resources: resourcesCount,
|
|
2667
|
+
prompts: promptsCount,
|
|
2668
|
+
total,
|
|
2669
|
+
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
2670
|
+
};
|
|
2671
|
+
if (errors.length === 0) {
|
|
2672
|
+
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
2673
|
+
}
|
|
2674
|
+
results[upstream.name] = countObj;
|
|
2675
|
+
}
|
|
2676
|
+
return results;
|
|
2677
|
+
}
|
|
2414
2678
|
function getConfiguredTimeoutMs() {
|
|
2415
2679
|
const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
|
|
2416
2680
|
return Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_UPSTREAM_TIMEOUT_MS;
|
|
@@ -2431,11 +2695,9 @@ function specFingerprint(spec) {
|
|
|
2431
2695
|
return JSON.stringify(spec);
|
|
2432
2696
|
}
|
|
2433
2697
|
function resolveStdioEnv(spec, secrets) {
|
|
2434
|
-
if (!spec.env || Object.keys(spec.env).length === 0) {
|
|
2435
|
-
return;
|
|
2436
|
-
}
|
|
2437
2698
|
const env = getDefaultEnvironment();
|
|
2438
|
-
|
|
2699
|
+
env.PATH = buildEnrichedPath(env.PATH);
|
|
2700
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
2439
2701
|
env[key] = secrets.resolveMaybeSecret(value);
|
|
2440
2702
|
}
|
|
2441
2703
|
return env;
|
|
@@ -2544,12 +2806,21 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
|
|
|
2544
2806
|
}
|
|
2545
2807
|
}
|
|
2546
2808
|
async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
|
|
2809
|
+
return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
|
|
2810
|
+
}
|
|
2811
|
+
async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
|
|
2547
2812
|
const headers = {
|
|
2548
2813
|
"content-type": "application/json",
|
|
2549
2814
|
accept: "application/json, text/event-stream"
|
|
2550
2815
|
};
|
|
2551
2816
|
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
2552
|
-
|
|
2817
|
+
if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
|
|
2818
|
+
const oauthServerName = oauthReferenceServerName(value);
|
|
2819
|
+
const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
|
|
2820
|
+
headers[key] = `Bearer ${accessToken}`;
|
|
2821
|
+
} else {
|
|
2822
|
+
headers[key] = secrets.resolveMaybeSecret(value);
|
|
2823
|
+
}
|
|
2553
2824
|
}
|
|
2554
2825
|
if (passthroughAuthorizationHeader) {
|
|
2555
2826
|
headers.Authorization = passthroughAuthorizationHeader;
|
|
@@ -2581,6 +2852,9 @@ async function callHttpUpstream(upstream, method, params, id, secrets, passthrou
|
|
|
2581
2852
|
}
|
|
2582
2853
|
if (!response.ok) {
|
|
2583
2854
|
const responseText = await response.text();
|
|
2855
|
+
if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
|
|
2856
|
+
return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
|
|
2857
|
+
}
|
|
2584
2858
|
throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
|
|
2585
2859
|
}
|
|
2586
2860
|
let payload = null;
|
|
@@ -2719,6 +2993,15 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
2719
2993
|
for (const upstream of upstreams) {
|
|
2720
2994
|
try {
|
|
2721
2995
|
const result = await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
2996
|
+
if (!runtime.tokenCache) {
|
|
2997
|
+
runtime.tokenCache = new Map;
|
|
2998
|
+
}
|
|
2999
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3000
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
3001
|
+
existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
|
|
3002
|
+
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
3003
|
+
delete existing.error;
|
|
3004
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2722
3005
|
for (const tool of result.tools ?? []) {
|
|
2723
3006
|
const name = typeof tool.name === "string" ? tool.name : "tool";
|
|
2724
3007
|
tools.push({
|
|
@@ -2727,9 +3010,10 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
2727
3010
|
});
|
|
2728
3011
|
}
|
|
2729
3012
|
} catch (error) {
|
|
2730
|
-
if (upstreams.length === 1
|
|
3013
|
+
if (upstreams.length === 1) {
|
|
2731
3014
|
throw error;
|
|
2732
3015
|
}
|
|
3016
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2733
3017
|
}
|
|
2734
3018
|
}
|
|
2735
3019
|
return { tools };
|
|
@@ -2742,6 +3026,15 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
2742
3026
|
for (const upstream of upstreams) {
|
|
2743
3027
|
try {
|
|
2744
3028
|
const result = await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
3029
|
+
if (!runtime.tokenCache) {
|
|
3030
|
+
runtime.tokenCache = new Map;
|
|
3031
|
+
}
|
|
3032
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3033
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
3034
|
+
existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
|
|
3035
|
+
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
3036
|
+
delete existing.error;
|
|
3037
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2745
3038
|
for (const resource of result.resources ?? []) {
|
|
2746
3039
|
const originalUri = typeof resource.uri === "string" ? resource.uri : "";
|
|
2747
3040
|
const originalName = typeof resource.name === "string" ? resource.name : originalUri;
|
|
@@ -2752,9 +3045,10 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
2752
3045
|
});
|
|
2753
3046
|
}
|
|
2754
3047
|
} catch (error) {
|
|
2755
|
-
if (upstreams.length === 1
|
|
3048
|
+
if (upstreams.length === 1) {
|
|
2756
3049
|
throw error;
|
|
2757
3050
|
}
|
|
3051
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2758
3052
|
}
|
|
2759
3053
|
}
|
|
2760
3054
|
return { resources };
|
|
@@ -2767,6 +3061,15 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
2767
3061
|
for (const upstream of upstreams) {
|
|
2768
3062
|
try {
|
|
2769
3063
|
const result = await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader);
|
|
3064
|
+
if (!runtime.tokenCache) {
|
|
3065
|
+
runtime.tokenCache = new Map;
|
|
3066
|
+
}
|
|
3067
|
+
const cached = runtime.tokenCache.get(upstream.name);
|
|
3068
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
3069
|
+
existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
|
|
3070
|
+
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
3071
|
+
delete existing.error;
|
|
3072
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2770
3073
|
for (const prompt of result.prompts ?? []) {
|
|
2771
3074
|
const name = typeof prompt.name === "string" ? prompt.name : "prompt";
|
|
2772
3075
|
prompts.push({
|
|
@@ -2775,9 +3078,10 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
2775
3078
|
});
|
|
2776
3079
|
}
|
|
2777
3080
|
} catch (error) {
|
|
2778
|
-
if (upstreams.length === 1
|
|
3081
|
+
if (upstreams.length === 1) {
|
|
2779
3082
|
throw error;
|
|
2780
3083
|
}
|
|
3084
|
+
runtime.upstreamErrors?.set(upstream.name, error instanceof Error ? error.message : String(error));
|
|
2781
3085
|
}
|
|
2782
3086
|
}
|
|
2783
3087
|
return { prompts };
|
|
@@ -2908,11 +3212,20 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
|
|
|
2908
3212
|
if (request.method === "ping") {
|
|
2909
3213
|
return makeResult(id, { ok: true });
|
|
2910
3214
|
}
|
|
2911
|
-
const config =
|
|
3215
|
+
const config = loadMergedConfig();
|
|
2912
3216
|
if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
|
|
2913
3217
|
return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
|
|
2914
3218
|
}
|
|
2915
3219
|
await reconcileStdioConnections(config, runtime);
|
|
3220
|
+
if (request.method === "custom/tokenCounts") {
|
|
3221
|
+
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
3222
|
+
return makeResult(id, result);
|
|
3223
|
+
}
|
|
3224
|
+
if (request.method === "custom/refreshTokenCounts") {
|
|
3225
|
+
runtime.tokenCache?.clear();
|
|
3226
|
+
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
3227
|
+
return makeResult(id, result);
|
|
3228
|
+
}
|
|
2916
3229
|
if (request.method === "tools/list") {
|
|
2917
3230
|
const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
|
|
2918
3231
|
return makeResult(id, result);
|
|
@@ -2942,7 +3255,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
|
|
|
2942
3255
|
response.end(JSON.stringify({ error: "method_not_allowed" }));
|
|
2943
3256
|
return true;
|
|
2944
3257
|
}
|
|
2945
|
-
const config =
|
|
3258
|
+
const config = loadMergedConfig();
|
|
2946
3259
|
const requestedUpstream = getRequestedUpstream(requestUrl);
|
|
2947
3260
|
const upstream = getScopedHttpUpstream(config, requestedUpstream);
|
|
2948
3261
|
if (!upstream) {
|
|
@@ -2999,12 +3312,13 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
|
|
|
2999
3312
|
function createGatewayServer(options) {
|
|
3000
3313
|
const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
|
|
3001
3314
|
const runtime = {
|
|
3002
|
-
stdioConnections: new Map
|
|
3315
|
+
stdioConnections: new Map,
|
|
3316
|
+
upstreamErrors: new Map
|
|
3003
3317
|
};
|
|
3004
|
-
const server =
|
|
3005
|
-
let requestUrl = new
|
|
3318
|
+
const server = http2.createServer(async (request, response) => {
|
|
3319
|
+
let requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
3006
3320
|
try {
|
|
3007
|
-
requestUrl = new
|
|
3321
|
+
requestUrl = new URL2(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
3008
3322
|
const upstreamFilter = getRequestedUpstream(requestUrl);
|
|
3009
3323
|
if (debug) {
|
|
3010
3324
|
console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
|
|
@@ -3115,7 +3429,7 @@ function createGatewayServer(options) {
|
|
|
3115
3429
|
response.setHeader("content-type", "application/json");
|
|
3116
3430
|
}
|
|
3117
3431
|
if (hasInitialize) {
|
|
3118
|
-
const sessionId =
|
|
3432
|
+
const sessionId = crypto2.randomUUID();
|
|
3119
3433
|
response.setHeader("mcp-session-id", sessionId);
|
|
3120
3434
|
response.setHeader("MCP-Session-Id", sessionId);
|
|
3121
3435
|
}
|
|
@@ -3145,7 +3459,11 @@ function createGatewayServer(options) {
|
|
|
3145
3459
|
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
3146
3460
|
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
3147
3461
|
}
|
|
3148
|
-
response.end(
|
|
3462
|
+
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
3463
|
+
status: error.status,
|
|
3464
|
+
body: error.bodyText,
|
|
3465
|
+
wwwAuthenticate: error.wwwAuthenticate
|
|
3466
|
+
})));
|
|
3149
3467
|
if (debug) {
|
|
3150
3468
|
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
3151
3469
|
}
|
|
@@ -3171,10 +3489,10 @@ function createGatewayServer(options) {
|
|
|
3171
3489
|
|
|
3172
3490
|
// src/core/daemon.ts
|
|
3173
3491
|
function readPidFromFile(pidPath = getPidPath()) {
|
|
3174
|
-
if (!
|
|
3492
|
+
if (!fs9.existsSync(pidPath)) {
|
|
3175
3493
|
return null;
|
|
3176
3494
|
}
|
|
3177
|
-
const raw =
|
|
3495
|
+
const raw = fs9.readFileSync(pidPath, "utf8").trim();
|
|
3178
3496
|
if (!raw) {
|
|
3179
3497
|
return null;
|
|
3180
3498
|
}
|
|
@@ -3221,6 +3539,29 @@ async function resolveGatewayPort(config) {
|
|
|
3221
3539
|
}
|
|
3222
3540
|
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
3223
3541
|
}
|
|
3542
|
+
async function waitForGatewayReady(port, token, timeoutMs = 5000) {
|
|
3543
|
+
const deadline = Date.now() + timeoutMs;
|
|
3544
|
+
let lastError;
|
|
3545
|
+
while (Date.now() < deadline) {
|
|
3546
|
+
try {
|
|
3547
|
+
const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
3548
|
+
method: "GET",
|
|
3549
|
+
headers: {
|
|
3550
|
+
Authorization: `Bearer ${token}`
|
|
3551
|
+
},
|
|
3552
|
+
signal: AbortSignal.timeout(500)
|
|
3553
|
+
});
|
|
3554
|
+
if (response.ok) {
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
lastError = new Error(`Gateway returned HTTP ${response.status}.`);
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
lastError = error;
|
|
3560
|
+
}
|
|
3561
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3562
|
+
}
|
|
3563
|
+
throw new Error(`Gateway did not become ready within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
3564
|
+
}
|
|
3224
3565
|
function getDaemonStatus(config) {
|
|
3225
3566
|
const pidPath = getPidPath();
|
|
3226
3567
|
const pid = readPidFromFile(pidPath);
|
|
@@ -3253,16 +3594,16 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3253
3594
|
};
|
|
3254
3595
|
}
|
|
3255
3596
|
try {
|
|
3256
|
-
|
|
3597
|
+
fs9.unlinkSync(getPidPath());
|
|
3257
3598
|
} catch {}
|
|
3258
3599
|
}
|
|
3259
3600
|
const port = await resolveGatewayPort(config);
|
|
3260
|
-
ensureGatewayToken(config, secrets);
|
|
3601
|
+
const token = ensureGatewayToken(config, secrets);
|
|
3261
3602
|
const pidPath = getPidPath();
|
|
3262
3603
|
const logPath = getLogPath();
|
|
3263
3604
|
ensureParentDir(pidPath);
|
|
3264
3605
|
ensureParentDir(logPath);
|
|
3265
|
-
const logFd =
|
|
3606
|
+
const logFd = fs9.openSync(logPath, "a", 384);
|
|
3266
3607
|
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
3267
3608
|
detached: true,
|
|
3268
3609
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -3272,9 +3613,10 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3272
3613
|
}
|
|
3273
3614
|
});
|
|
3274
3615
|
child.unref();
|
|
3275
|
-
|
|
3616
|
+
fs9.writeFileSync(pidPath, `${child.pid}
|
|
3276
3617
|
`, { mode: 384 });
|
|
3277
3618
|
startBackgroundUpdateCheck();
|
|
3619
|
+
await waitForGatewayReady(port, token);
|
|
3278
3620
|
return {
|
|
3279
3621
|
started: true,
|
|
3280
3622
|
pid: child.pid ?? -1,
|
|
@@ -3295,7 +3637,7 @@ function stopDaemon() {
|
|
|
3295
3637
|
process.kill(pid, "SIGTERM");
|
|
3296
3638
|
} catch {}
|
|
3297
3639
|
try {
|
|
3298
|
-
|
|
3640
|
+
fs9.unlinkSync(pidPath);
|
|
3299
3641
|
} catch {}
|
|
3300
3642
|
return {
|
|
3301
3643
|
stopped: true,
|
|
@@ -3308,10 +3650,10 @@ async function restartDaemon(config, cliPath, secrets) {
|
|
|
3308
3650
|
}
|
|
3309
3651
|
function readDaemonLogs(maxLines = 200) {
|
|
3310
3652
|
const logPath = getLogPath();
|
|
3311
|
-
if (!
|
|
3653
|
+
if (!fs9.existsSync(logPath)) {
|
|
3312
3654
|
return "";
|
|
3313
3655
|
}
|
|
3314
|
-
const lines =
|
|
3656
|
+
const lines = fs9.readFileSync(logPath, "utf8").split(`
|
|
3315
3657
|
`);
|
|
3316
3658
|
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
3317
3659
|
`);
|
|
@@ -3380,26 +3722,75 @@ function buildAuthBindings(spec) {
|
|
|
3380
3722
|
secretName: secretRefName(binding.value) ?? undefined
|
|
3381
3723
|
}));
|
|
3382
3724
|
}
|
|
3383
|
-
function
|
|
3725
|
+
async function fetchTokenCounts(gatewayUrl, token) {
|
|
3726
|
+
try {
|
|
3727
|
+
const res = await fetch(gatewayUrl, {
|
|
3728
|
+
method: "POST",
|
|
3729
|
+
headers: {
|
|
3730
|
+
"content-type": "application/json",
|
|
3731
|
+
authorization: `Bearer ${token}`
|
|
3732
|
+
},
|
|
3733
|
+
body: JSON.stringify({
|
|
3734
|
+
jsonrpc: "2.0",
|
|
3735
|
+
id: "status-tokens",
|
|
3736
|
+
method: "custom/tokenCounts",
|
|
3737
|
+
params: {}
|
|
3738
|
+
}),
|
|
3739
|
+
signal: AbortSignal.timeout(4000)
|
|
3740
|
+
});
|
|
3741
|
+
if (!res.ok) {
|
|
3742
|
+
return {};
|
|
3743
|
+
}
|
|
3744
|
+
const data = await res.json();
|
|
3745
|
+
return data?.result ?? {};
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
return {};
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(config)) {
|
|
3751
|
+
const secrets = new SecretsManager;
|
|
3752
|
+
const token = ensureGatewayToken(config, secrets);
|
|
3753
|
+
const gatewayUrl = getGatewayUrl(config);
|
|
3754
|
+
let tokenCounts = {};
|
|
3755
|
+
if (daemon.running) {
|
|
3756
|
+
tokenCounts = await fetchTokenCounts(gatewayUrl, token);
|
|
3757
|
+
}
|
|
3758
|
+
const projectEntries = Object.values(config.projects ?? {});
|
|
3759
|
+
let totalGlobalTokens = 0;
|
|
3760
|
+
const totalProjectTokens = {};
|
|
3384
3761
|
const serverNames = Object.keys(config.servers).sort((left, right) => left.localeCompare(right));
|
|
3385
3762
|
const servers = serverNames.map((name) => {
|
|
3386
3763
|
const spec = config.servers[name];
|
|
3387
|
-
|
|
3764
|
+
const tokenCount = tokenCounts[name];
|
|
3765
|
+
const serverEntry = {
|
|
3388
3766
|
name,
|
|
3389
3767
|
enabled: isServerEnabled(spec),
|
|
3390
3768
|
transport: spec.transport,
|
|
3391
3769
|
target: describeServerTarget(spec),
|
|
3392
3770
|
authBindings: buildAuthBindings(spec),
|
|
3393
|
-
clients: buildClientMappings(config, managedIndex, name)
|
|
3771
|
+
clients: buildClientMappings(config, managedIndex, name),
|
|
3772
|
+
tokenCount
|
|
3394
3773
|
};
|
|
3774
|
+
if (serverEntry.enabled && tokenCount) {
|
|
3775
|
+
const matchingProject = projectEntries.find((p) => name.startsWith(`${p.name}.`));
|
|
3776
|
+
if (matchingProject) {
|
|
3777
|
+
totalProjectTokens[matchingProject.path] = (totalProjectTokens[matchingProject.path] ?? 0) + tokenCount.total;
|
|
3778
|
+
} else {
|
|
3779
|
+
totalGlobalTokens += tokenCount.total;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
return serverEntry;
|
|
3395
3783
|
});
|
|
3396
3784
|
return {
|
|
3397
|
-
gatewayUrl
|
|
3785
|
+
gatewayUrl,
|
|
3398
3786
|
daemon,
|
|
3399
3787
|
upstreamCount: servers.length,
|
|
3400
3788
|
servers,
|
|
3401
|
-
clients: config.clients
|
|
3789
|
+
clients: config.clients,
|
|
3790
|
+
projects: config.projects,
|
|
3791
|
+
totalGlobalTokens,
|
|
3792
|
+
totalProjectTokens
|
|
3402
3793
|
};
|
|
3403
3794
|
}
|
|
3404
3795
|
|
|
3405
|
-
export {
|
|
3796
|
+
export { listSkills, getSkill, saveSkill, deleteSkill, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|