@kwonye/mcpx 0.1.91 → 0.1.94
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 +2 -1
- package/dist/adapters/claude-desktop.d.ts.map +1 -1
- package/dist/adapters/claude.d.ts +2 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/cline.d.ts.map +1 -1
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/cursor.d.ts +2 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/kiro.d.ts +2 -1
- package/dist/adapters/kiro.d.ts.map +1 -1
- package/dist/adapters/opencode.d.ts.map +1 -1
- package/dist/adapters/qwen.d.ts +2 -1
- package/dist/adapters/qwen.d.ts.map +1 -1
- package/dist/adapters/utils/index.d.ts +11 -3
- package/dist/adapters/utils/index.d.ts.map +1 -1
- package/dist/adapters/vscode.d.ts +2 -1
- package/dist/adapters/vscode.d.ts.map +1 -1
- package/dist/{cli-b83jnbz7.js → cli-154shqmt.js} +180 -1717
- package/dist/cli-9fmn9y72.js +923 -0
- package/dist/{cli-btx8jwra.js → cli-csfsg1yf.js} +136 -8
- package/dist/{cli-grafmdtc.js → cli-mxmgxdm2.js} +181 -1718
- package/dist/cli-n8vt375e.js +2082 -0
- package/dist/{cli-qfvawh8e.js → cli-tk8bggg5.js} +82 -9
- package/dist/{cli-zbbeadz8.js → cli-zjepj3sj.js} +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +172 -20
- package/dist/{config-nkgc4c4n.js → config-dtc3c79c.js} +7 -3
- package/dist/core/config-repair.d.ts.map +1 -1
- package/dist/core/config-store.d.ts +3 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/daemon.d.ts +1 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/errors.d.ts +18 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/index.d.ts +14 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +133 -12
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/paths.d.ts +4 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/plugin-cache.d.ts +21 -0
- package/dist/core/plugin-cache.d.ts.map +1 -0
- package/dist/core/plugin-data.d.ts +29 -0
- package/dist/core/plugin-data.d.ts.map +1 -0
- package/dist/core/plugin-lifecycle.d.ts +8 -0
- package/dist/core/plugin-lifecycle.d.ts.map +1 -0
- package/dist/core/plugin-manager.d.ts +52 -0
- package/dist/core/plugin-manager.d.ts.map +1 -0
- package/dist/core/plugin-parse.d.ts +6 -0
- package/dist/core/plugin-parse.d.ts.map +1 -0
- package/dist/core/plugin-projections.d.ts +9 -0
- package/dist/core/plugin-projections.d.ts.map +1 -0
- package/dist/core/plugin-source.d.ts +5 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/registry.d.ts +4 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/secrets.d.ts.map +1 -1
- package/dist/core/server-auth.d.ts.map +1 -1
- package/dist/core/status.d.ts +1 -0
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/sync.d.ts +19 -2
- package/dist/core/sync.d.ts.map +1 -1
- package/dist/gateway/server.d.ts +4 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/plugin-manager-1sjwfnah.js +31 -0
- package/dist/{registry-vykv33et.js → registry-n12thgf2.js} +2 -2
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -2
|
@@ -1,73 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getGatewayUrl,
|
|
3
|
+
persistSyncState,
|
|
4
|
+
syncAllClients
|
|
5
|
+
} from "./cli-n8vt375e.js";
|
|
1
6
|
import {
|
|
2
7
|
loadConfig,
|
|
3
8
|
loadMergedConfig,
|
|
4
9
|
saveConfig
|
|
5
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-csfsg1yf.js";
|
|
6
11
|
import {
|
|
7
12
|
SecretsManager,
|
|
13
|
+
UpstreamError,
|
|
14
|
+
classifyUpstreamError,
|
|
8
15
|
ensureGatewayToken
|
|
9
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-tk8bggg5.js";
|
|
10
17
|
import {
|
|
11
18
|
ensureDir,
|
|
12
19
|
ensureParentDir,
|
|
13
20
|
getLogPath,
|
|
14
|
-
getManagedIndexPath,
|
|
15
21
|
getPidPath,
|
|
16
|
-
getSkillsDir,
|
|
17
22
|
getStagedVersionPath,
|
|
18
23
|
getUpdateLockPath,
|
|
19
|
-
getUpdatesDir
|
|
20
|
-
|
|
21
|
-
readJsonFile,
|
|
22
|
-
sha256,
|
|
23
|
-
writeJsonAtomic
|
|
24
|
-
} from "./cli-zbbeadz8.js";
|
|
24
|
+
getUpdatesDir
|
|
25
|
+
} from "./cli-zjepj3sj.js";
|
|
25
26
|
import {
|
|
26
27
|
isServerEnabled
|
|
27
28
|
} from "./cli-s4gspqdf.js";
|
|
28
29
|
|
|
29
|
-
// src/core/skills.ts
|
|
30
|
-
import fs from "node:fs";
|
|
31
|
-
import path from "node:path";
|
|
32
|
-
function listSkills() {
|
|
33
|
-
const skillsDir = getSkillsDir();
|
|
34
|
-
if (!fs.existsSync(skillsDir)) {
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
const files = fs.readdirSync(skillsDir);
|
|
38
|
-
const skills = [];
|
|
39
|
-
for (const file of files) {
|
|
40
|
-
if (file.endsWith(".md")) {
|
|
41
|
-
const id = path.basename(file, ".md");
|
|
42
|
-
const content = fs.readFileSync(path.join(skillsDir, file), "utf-8");
|
|
43
|
-
skills.push({ id, content });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return skills;
|
|
47
|
-
}
|
|
48
|
-
function getSkill(id) {
|
|
49
|
-
const skillsDir = getSkillsDir();
|
|
50
|
-
const filePath = path.join(skillsDir, `${id}.md`);
|
|
51
|
-
if (!fs.existsSync(filePath)) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
55
|
-
return { id, content };
|
|
56
|
-
}
|
|
57
|
-
function saveSkill(id, content) {
|
|
58
|
-
const skillsDir = getSkillsDir();
|
|
59
|
-
ensureDir(skillsDir);
|
|
60
|
-
const filePath = path.join(skillsDir, `${id}.md`);
|
|
61
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
62
|
-
}
|
|
63
|
-
function deleteSkill(id) {
|
|
64
|
-
const skillsDir = getSkillsDir();
|
|
65
|
-
const filePath = path.join(skillsDir, `${id}.md`);
|
|
66
|
-
if (fs.existsSync(filePath)) {
|
|
67
|
-
fs.unlinkSync(filePath);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
30
|
// src/core/auth-probe.ts
|
|
72
31
|
var JSON_RPC_VERSION = "2.0";
|
|
73
32
|
var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
|
|
@@ -289,1557 +248,6 @@ function mapRegistryToSpec(_name, option, resolvedValues) {
|
|
|
289
248
|
};
|
|
290
249
|
}
|
|
291
250
|
|
|
292
|
-
// src/adapters/claude.ts
|
|
293
|
-
import fs2 from "node:fs";
|
|
294
|
-
import path2 from "node:path";
|
|
295
|
-
import { z } from "zod";
|
|
296
|
-
|
|
297
|
-
// src/adapters/utils/index.ts
|
|
298
|
-
function getManagedEntryNames(managedIndex, clientId) {
|
|
299
|
-
return Object.keys(managedIndex.managed[clientId]?.entries ?? {});
|
|
300
|
-
}
|
|
301
|
-
function isManagedEntry(managedIndex, clientId, entryName) {
|
|
302
|
-
return Boolean(managedIndex.managed[clientId]?.entries?.[entryName]);
|
|
303
|
-
}
|
|
304
|
-
function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryNames) {
|
|
305
|
-
const keep = new Set(keepEntryNames);
|
|
306
|
-
const managedNames = getManagedEntryNames(managedIndex, clientId);
|
|
307
|
-
for (const name of managedNames) {
|
|
308
|
-
if (!keep.has(name)) {
|
|
309
|
-
delete servers[name];
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
for (const name of Object.keys(servers)) {
|
|
313
|
-
if (!keep.has(name) && name.endsWith(" (mcpx)")) {
|
|
314
|
-
delete servers[name];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
|
|
319
|
-
if (existingValue === undefined || existingValue === null) {
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
if (isManagedEntry(managedIndex, clientId, entryName)) {
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
|
|
326
|
-
}
|
|
327
|
-
function setManagedEntries(managedIndex, clientId, configPath, entries) {
|
|
328
|
-
if (!managedIndex.managed[clientId]) {
|
|
329
|
-
managedIndex.managed[clientId] = {
|
|
330
|
-
configPath,
|
|
331
|
-
entries: {}
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
managedIndex.managed[clientId].configPath = configPath;
|
|
335
|
-
managedIndex.managed[clientId].entries = Object.fromEntries(Object.entries(entries).map(([entryName, serializedEntry]) => [entryName, {
|
|
336
|
-
fingerprint: sha256(serializedEntry),
|
|
337
|
-
lastSyncedAt: new Date().toISOString()
|
|
338
|
-
}]));
|
|
339
|
-
}
|
|
340
|
-
function okResult(clientId, configPath, message) {
|
|
341
|
-
return {
|
|
342
|
-
clientId,
|
|
343
|
-
status: "SYNCED",
|
|
344
|
-
configPath,
|
|
345
|
-
message
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
function errorResult(clientId, configPath, message) {
|
|
349
|
-
return {
|
|
350
|
-
clientId,
|
|
351
|
-
status: "ERROR",
|
|
352
|
-
configPath,
|
|
353
|
-
message
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
function managedGatewayEntryName(serverName) {
|
|
357
|
-
return `${serverName} (mcpx)`;
|
|
358
|
-
}
|
|
359
|
-
function isManagedGatewayProjection(entryName) {
|
|
360
|
-
return entryName.endsWith(" (mcpx)");
|
|
361
|
-
}
|
|
362
|
-
function sortRecord(record) {
|
|
363
|
-
if (!record || Object.keys(record).length === 0) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
367
|
-
}
|
|
368
|
-
function normalizeServerSpec(spec) {
|
|
369
|
-
if (spec.transport === "http") {
|
|
370
|
-
return {
|
|
371
|
-
transport: "http",
|
|
372
|
-
url: spec.url,
|
|
373
|
-
headers: sortRecord(spec.headers)
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
return {
|
|
377
|
-
transport: "stdio",
|
|
378
|
-
command: spec.command,
|
|
379
|
-
args: spec.args && spec.args.length > 0 ? [...spec.args] : undefined,
|
|
380
|
-
env: sortRecord(spec.env),
|
|
381
|
-
cwd: spec.cwd
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
function serializeServerSpec(spec) {
|
|
385
|
-
return JSON.stringify(normalizeServerSpec(spec));
|
|
386
|
-
}
|
|
387
|
-
function serverSpecsEqual(left, right) {
|
|
388
|
-
return serializeServerSpec(left) === serializeServerSpec(right);
|
|
389
|
-
}
|
|
390
|
-
function emptyImportScan(clientId, configPath) {
|
|
391
|
-
return {
|
|
392
|
-
clientId,
|
|
393
|
-
configPath,
|
|
394
|
-
candidates: [],
|
|
395
|
-
skipped: []
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function buildImportSkip(clientId, sourceEntryName, reason, configPath, serverName = sourceEntryName) {
|
|
399
|
-
return {
|
|
400
|
-
clientId,
|
|
401
|
-
configPath,
|
|
402
|
-
sourceEntryName,
|
|
403
|
-
serverName,
|
|
404
|
-
reason
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function removeSourceEntries(entries, names) {
|
|
408
|
-
for (const name of names ?? []) {
|
|
409
|
-
delete entries[name];
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
function syncDisabledMcpServersArray(raw, managedNames, managedEntries) {
|
|
413
|
-
const existingDisabled = (raw.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
|
|
414
|
-
const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
|
|
415
|
-
raw.disabledMcpServers = [...existingDisabled, ...disabledManaged];
|
|
416
|
-
}
|
|
417
|
-
function syncMcpExcludedArray(raw, managedNames, managedEntries) {
|
|
418
|
-
const mcp = raw.mcp ?? {};
|
|
419
|
-
const existingExcluded = (mcp.excluded ?? []).filter((name) => !managedNames.includes(name));
|
|
420
|
-
const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
|
|
421
|
-
mcp.excluded = [...existingExcluded, ...disabledManaged];
|
|
422
|
-
raw.mcp = mcp;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// src/adapters/claude.ts
|
|
426
|
-
function normalizeProjectKey(p) {
|
|
427
|
-
try {
|
|
428
|
-
return fs2.realpathSync(path2.resolve(p));
|
|
429
|
-
} catch {
|
|
430
|
-
return path2.resolve(p);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
|
|
434
|
-
const existingNonManaged = (project.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
|
|
435
|
-
const combined = [...existingNonManaged, ...disabledForThisProject];
|
|
436
|
-
if (combined.length > 0) {
|
|
437
|
-
project.disabledMcpServers = combined;
|
|
438
|
-
} else {
|
|
439
|
-
delete project.disabledMcpServers;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
var stringMapSchema = z.record(z.string(), z.string());
|
|
443
|
-
var claudeEntrySchema = z.object({
|
|
444
|
-
type: z.string().optional(),
|
|
445
|
-
url: z.string().min(1).optional(),
|
|
446
|
-
headers: stringMapSchema.optional(),
|
|
447
|
-
command: z.string().min(1).optional(),
|
|
448
|
-
args: z.array(z.string()).optional(),
|
|
449
|
-
env: stringMapSchema.optional(),
|
|
450
|
-
cwd: z.string().min(1).optional(),
|
|
451
|
-
disabled: z.boolean().optional()
|
|
452
|
-
}).passthrough();
|
|
453
|
-
|
|
454
|
-
class ClaudeAdapter {
|
|
455
|
-
id = "claude";
|
|
456
|
-
detectConfigPath() {
|
|
457
|
-
return path2.join(homeDir(), ".claude.json");
|
|
458
|
-
}
|
|
459
|
-
supportsHttp() {
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
scanForImports(_config, managedIndex) {
|
|
463
|
-
const configPath = this.detectConfigPath();
|
|
464
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
465
|
-
if (!configPath) {
|
|
466
|
-
return result;
|
|
467
|
-
}
|
|
468
|
-
const raw = readJsonFile(configPath, {});
|
|
469
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
470
|
-
const disabledMcpServers = new Set(raw.disabledMcpServers ?? []);
|
|
471
|
-
const projects = raw.projects ?? {};
|
|
472
|
-
for (const project of Object.values(projects)) {
|
|
473
|
-
const projectDisabled = project?.disabledMcpServers ?? [];
|
|
474
|
-
for (const name of projectDisabled) {
|
|
475
|
-
disabledMcpServers.add(name);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
479
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
const parsed = claudeEntrySchema.safeParse(rawEntry);
|
|
483
|
-
if (!parsed.success) {
|
|
484
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
const entry = parsed.data;
|
|
488
|
-
const isDisabled = entry.disabled === true || disabledMcpServers.has(name);
|
|
489
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
490
|
-
result.candidates.push({
|
|
491
|
-
clientId: this.id,
|
|
492
|
-
configPath,
|
|
493
|
-
sourceEntryName: name,
|
|
494
|
-
serverName: name,
|
|
495
|
-
spec: {
|
|
496
|
-
transport: "http",
|
|
497
|
-
url: entry.url,
|
|
498
|
-
headers: entry.headers,
|
|
499
|
-
enabled: !isDisabled
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
505
|
-
result.candidates.push({
|
|
506
|
-
clientId: this.id,
|
|
507
|
-
configPath,
|
|
508
|
-
sourceEntryName: name,
|
|
509
|
-
serverName: name,
|
|
510
|
-
spec: {
|
|
511
|
-
transport: "stdio",
|
|
512
|
-
command: entry.command,
|
|
513
|
-
args: entry.args,
|
|
514
|
-
env: entry.env,
|
|
515
|
-
cwd: entry.cwd,
|
|
516
|
-
enabled: !isDisabled
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
522
|
-
}
|
|
523
|
-
return result;
|
|
524
|
-
}
|
|
525
|
-
syncGateway(_config, options) {
|
|
526
|
-
const configPath = this.detectConfigPath();
|
|
527
|
-
if (!configPath) {
|
|
528
|
-
return errorResult(this.id, undefined, "Unable to resolve Claude config path.");
|
|
529
|
-
}
|
|
530
|
-
try {
|
|
531
|
-
const raw = readJsonFile(configPath, {});
|
|
532
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
533
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
534
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
535
|
-
type: "http",
|
|
536
|
-
url: entry.url,
|
|
537
|
-
headers: entry.headers
|
|
538
|
-
}]));
|
|
539
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
540
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
541
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
542
|
-
for (const name of enabledManagedNames) {
|
|
543
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
544
|
-
if (topLevelConflict) {
|
|
545
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
549
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
550
|
-
topLevelServers[name] = entry;
|
|
551
|
-
}
|
|
552
|
-
raw.mcpServers = topLevelServers;
|
|
553
|
-
syncDisabledMcpServersArray(raw, managedNames, options.managedEntries);
|
|
554
|
-
const scopeByNormalizedPath = new Map;
|
|
555
|
-
for (const scope of options.projectScopes ?? []) {
|
|
556
|
-
scopeByNormalizedPath.set(normalizeProjectKey(scope.path), scope.disabledServerNames);
|
|
557
|
-
}
|
|
558
|
-
const projects = raw.projects ?? {};
|
|
559
|
-
for (const projectPath of Object.keys(projects)) {
|
|
560
|
-
const project = projects[projectPath] ?? {};
|
|
561
|
-
const disabledForThis = scopeByNormalizedPath.get(normalizeProjectKey(projectPath)) ?? [];
|
|
562
|
-
syncProjectDisabledArray(project, managedNames, disabledForThis);
|
|
563
|
-
projects[projectPath] = project;
|
|
564
|
-
}
|
|
565
|
-
for (const scope of options.projectScopes ?? []) {
|
|
566
|
-
if (scope.disabledServerNames.length === 0)
|
|
567
|
-
continue;
|
|
568
|
-
const normalizedKey = normalizeProjectKey(scope.path);
|
|
569
|
-
const alreadyExists = Object.keys(projects).some((p) => normalizeProjectKey(p) === normalizedKey);
|
|
570
|
-
if (!alreadyExists) {
|
|
571
|
-
projects[scope.path] = { disabledMcpServers: [...scope.disabledServerNames] };
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
raw.projects = projects;
|
|
575
|
-
writeJsonAtomic(configPath, raw);
|
|
576
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
577
|
-
return okResult(this.id, configPath);
|
|
578
|
-
} catch (error) {
|
|
579
|
-
return errorResult(this.id, configPath, error.message);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// src/adapters/claude-desktop.ts
|
|
585
|
-
import { execFileSync } from "node:child_process";
|
|
586
|
-
import path3 from "node:path";
|
|
587
|
-
import { z as z2 } from "zod";
|
|
588
|
-
var stringMapSchema2 = z2.record(z2.string(), z2.string());
|
|
589
|
-
var claudeDesktopEntrySchema = z2.object({
|
|
590
|
-
type: z2.string().optional(),
|
|
591
|
-
url: z2.string().min(1).optional(),
|
|
592
|
-
headers: stringMapSchema2.optional(),
|
|
593
|
-
command: z2.string().min(1).optional(),
|
|
594
|
-
args: z2.array(z2.string()).optional(),
|
|
595
|
-
env: stringMapSchema2.optional(),
|
|
596
|
-
cwd: z2.string().min(1).optional(),
|
|
597
|
-
disabled: z2.boolean().optional()
|
|
598
|
-
}).passthrough();
|
|
599
|
-
function buildProxyEntry(entry) {
|
|
600
|
-
const upstreamName = entry.name.replace(/ \(mcpx\)$/, "");
|
|
601
|
-
try {
|
|
602
|
-
const mcpxPath = execFileSync("which", ["mcpx"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
603
|
-
if (mcpxPath)
|
|
604
|
-
return { command: mcpxPath, args: ["proxy", upstreamName] };
|
|
605
|
-
} catch {}
|
|
606
|
-
return { command: "npx", args: ["-y", "@kwonye/mcpx@latest", "proxy", upstreamName] };
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
class ClaudeDesktopAdapter {
|
|
610
|
-
id = "claude-desktop";
|
|
611
|
-
detectConfigPath() {
|
|
612
|
-
if (process.platform === "linux") {
|
|
613
|
-
return path3.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
|
|
614
|
-
}
|
|
615
|
-
if (process.platform === "win32") {
|
|
616
|
-
return path3.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
|
|
617
|
-
}
|
|
618
|
-
return path3.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
619
|
-
}
|
|
620
|
-
supportsHttp() {
|
|
621
|
-
return true;
|
|
622
|
-
}
|
|
623
|
-
scanForImports(_config, managedIndex) {
|
|
624
|
-
const configPath = this.detectConfigPath();
|
|
625
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
626
|
-
if (!configPath) {
|
|
627
|
-
return result;
|
|
628
|
-
}
|
|
629
|
-
const raw = readJsonFile(configPath, {});
|
|
630
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
631
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
632
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
const parsed = claudeDesktopEntrySchema.safeParse(rawEntry);
|
|
636
|
-
if (!parsed.success) {
|
|
637
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const entry = parsed.data;
|
|
641
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
642
|
-
result.candidates.push({
|
|
643
|
-
clientId: this.id,
|
|
644
|
-
configPath,
|
|
645
|
-
sourceEntryName: name,
|
|
646
|
-
serverName: name,
|
|
647
|
-
spec: {
|
|
648
|
-
transport: "http",
|
|
649
|
-
url: entry.url,
|
|
650
|
-
headers: entry.headers,
|
|
651
|
-
enabled: entry.disabled !== true
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
657
|
-
result.candidates.push({
|
|
658
|
-
clientId: this.id,
|
|
659
|
-
configPath,
|
|
660
|
-
sourceEntryName: name,
|
|
661
|
-
serverName: name,
|
|
662
|
-
spec: {
|
|
663
|
-
transport: "stdio",
|
|
664
|
-
command: entry.command,
|
|
665
|
-
args: entry.args,
|
|
666
|
-
env: entry.env,
|
|
667
|
-
cwd: entry.cwd,
|
|
668
|
-
enabled: entry.disabled !== true
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
674
|
-
}
|
|
675
|
-
return result;
|
|
676
|
-
}
|
|
677
|
-
syncGateway(_config, options) {
|
|
678
|
-
const configPath = this.detectConfigPath();
|
|
679
|
-
if (!configPath) {
|
|
680
|
-
return errorResult(this.id, undefined, "Unable to resolve Claude Desktop config path.");
|
|
681
|
-
}
|
|
682
|
-
try {
|
|
683
|
-
const raw = readJsonFile(configPath, {});
|
|
684
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
685
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => {
|
|
686
|
-
const { command, args } = buildProxyEntry(entry);
|
|
687
|
-
return [entry.name, { command, args }];
|
|
688
|
-
}));
|
|
689
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
690
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
691
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
692
|
-
for (const name of enabledManagedNames) {
|
|
693
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
694
|
-
if (topLevelConflict) {
|
|
695
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
|
|
699
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
700
|
-
topLevelServers[name] = entry;
|
|
701
|
-
}
|
|
702
|
-
raw.mcpServers = topLevelServers;
|
|
703
|
-
writeJsonAtomic(configPath, raw);
|
|
704
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => {
|
|
705
|
-
const { command, args } = buildProxyEntry(entry);
|
|
706
|
-
return [entry.name, JSON.stringify({ command, args })];
|
|
707
|
-
})));
|
|
708
|
-
return okResult(this.id, configPath);
|
|
709
|
-
} catch (error) {
|
|
710
|
-
return errorResult(this.id, configPath, error.message);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// src/adapters/codex.ts
|
|
716
|
-
import path4 from "node:path";
|
|
717
|
-
import fs3 from "node:fs";
|
|
718
|
-
import { parse, stringify } from "@iarna/toml";
|
|
719
|
-
import { z as z3 } from "zod";
|
|
720
|
-
var stringMapSchema3 = z3.preprocess((value) => {
|
|
721
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
722
|
-
return value;
|
|
723
|
-
}
|
|
724
|
-
return Object.fromEntries(Object.entries(value));
|
|
725
|
-
}, z3.record(z3.string(), z3.string()));
|
|
726
|
-
var codexEntrySchema = z3.object({
|
|
727
|
-
enabled: z3.boolean().optional(),
|
|
728
|
-
url: z3.string().min(1).optional(),
|
|
729
|
-
headers: stringMapSchema3.optional(),
|
|
730
|
-
http_headers: stringMapSchema3.optional(),
|
|
731
|
-
command: z3.string().min(1).optional(),
|
|
732
|
-
args: z3.array(z3.string()).optional(),
|
|
733
|
-
env: stringMapSchema3.optional(),
|
|
734
|
-
cwd: z3.string().min(1).optional()
|
|
735
|
-
}).passthrough();
|
|
736
|
-
|
|
737
|
-
class CodexAdapter {
|
|
738
|
-
id = "codex";
|
|
739
|
-
detectConfigPath() {
|
|
740
|
-
return path4.join(homeDir(), ".codex", "config.toml");
|
|
741
|
-
}
|
|
742
|
-
supportsHttp() {
|
|
743
|
-
return true;
|
|
744
|
-
}
|
|
745
|
-
scanForImports(_config, managedIndex) {
|
|
746
|
-
const configPath = this.detectConfigPath();
|
|
747
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
748
|
-
if (!configPath || !fs3.existsSync(configPath)) {
|
|
749
|
-
return result;
|
|
750
|
-
}
|
|
751
|
-
const existing = fs3.readFileSync(configPath, "utf8");
|
|
752
|
-
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
753
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
754
|
-
for (const [name, rawEntry] of Object.entries(mcpServers)) {
|
|
755
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
const parsed = codexEntrySchema.safeParse(rawEntry);
|
|
759
|
-
if (!parsed.success) {
|
|
760
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
const entry = parsed.data;
|
|
764
|
-
if (entry.url && !entry.command) {
|
|
765
|
-
result.candidates.push({
|
|
766
|
-
clientId: this.id,
|
|
767
|
-
configPath,
|
|
768
|
-
sourceEntryName: name,
|
|
769
|
-
serverName: name,
|
|
770
|
-
spec: {
|
|
771
|
-
transport: "http",
|
|
772
|
-
url: entry.url,
|
|
773
|
-
headers: entry.http_headers ?? entry.headers,
|
|
774
|
-
enabled: entry.enabled !== false
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
if (entry.command && !entry.url) {
|
|
780
|
-
result.candidates.push({
|
|
781
|
-
clientId: this.id,
|
|
782
|
-
configPath,
|
|
783
|
-
sourceEntryName: name,
|
|
784
|
-
serverName: name,
|
|
785
|
-
spec: {
|
|
786
|
-
transport: "stdio",
|
|
787
|
-
command: entry.command,
|
|
788
|
-
args: entry.args,
|
|
789
|
-
env: entry.env,
|
|
790
|
-
cwd: entry.cwd,
|
|
791
|
-
enabled: entry.enabled !== false
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
797
|
-
}
|
|
798
|
-
return result;
|
|
799
|
-
}
|
|
800
|
-
syncGateway(_config, options) {
|
|
801
|
-
const configPath = this.detectConfigPath();
|
|
802
|
-
if (!configPath) {
|
|
803
|
-
return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
|
|
804
|
-
}
|
|
805
|
-
try {
|
|
806
|
-
const existing = fs3.existsSync(configPath) ? fs3.readFileSync(configPath, "utf8") : "";
|
|
807
|
-
const doc = existing.trim().length > 0 ? parse(existing) : {};
|
|
808
|
-
const mcpServers = doc.mcp_servers ?? {};
|
|
809
|
-
removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
|
|
810
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
811
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
812
|
-
enabled: entry.enabled,
|
|
813
|
-
url: entry.url,
|
|
814
|
-
http_headers: entry.headers
|
|
815
|
-
}]));
|
|
816
|
-
for (const name of managedNames) {
|
|
817
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
|
|
818
|
-
if (conflict) {
|
|
819
|
-
return errorResult(this.id, configPath, conflict);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
|
|
823
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
824
|
-
mcpServers[name] = entry;
|
|
825
|
-
}
|
|
826
|
-
doc.mcp_servers = mcpServers;
|
|
827
|
-
fs3.mkdirSync(path4.dirname(configPath), { recursive: true });
|
|
828
|
-
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
829
|
-
fs3.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
|
|
830
|
-
fs3.renameSync(tmpPath, configPath);
|
|
831
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
832
|
-
return okResult(this.id, configPath);
|
|
833
|
-
} catch (error) {
|
|
834
|
-
return errorResult(this.id, configPath, error.message);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// src/adapters/cursor.ts
|
|
840
|
-
import path5 from "node:path";
|
|
841
|
-
import { z as z4 } from "zod";
|
|
842
|
-
var stringMapSchema4 = z4.record(z4.string(), z4.string());
|
|
843
|
-
var cursorEntrySchema = z4.object({
|
|
844
|
-
type: z4.string().optional(),
|
|
845
|
-
url: z4.string().min(1).optional(),
|
|
846
|
-
headers: stringMapSchema4.optional(),
|
|
847
|
-
command: z4.string().min(1).optional(),
|
|
848
|
-
args: z4.array(z4.string()).optional(),
|
|
849
|
-
env: stringMapSchema4.optional(),
|
|
850
|
-
cwd: z4.string().min(1).optional(),
|
|
851
|
-
disabled: z4.boolean().optional()
|
|
852
|
-
}).passthrough();
|
|
853
|
-
|
|
854
|
-
class CursorAdapter {
|
|
855
|
-
id = "cursor";
|
|
856
|
-
detectConfigPath() {
|
|
857
|
-
return path5.join(homeDir(), ".cursor", "mcp.json");
|
|
858
|
-
}
|
|
859
|
-
supportsHttp() {
|
|
860
|
-
return true;
|
|
861
|
-
}
|
|
862
|
-
scanForImports(_config, managedIndex) {
|
|
863
|
-
const configPath = this.detectConfigPath();
|
|
864
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
865
|
-
if (!configPath) {
|
|
866
|
-
return result;
|
|
867
|
-
}
|
|
868
|
-
const raw = readJsonFile(configPath, {});
|
|
869
|
-
const servers = {
|
|
870
|
-
...raw.mcpServers ?? {}
|
|
871
|
-
};
|
|
872
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
873
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
const parsed = cursorEntrySchema.safeParse(rawEntry);
|
|
877
|
-
if (!parsed.success) {
|
|
878
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
const entry = parsed.data;
|
|
882
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
883
|
-
result.candidates.push({
|
|
884
|
-
clientId: this.id,
|
|
885
|
-
configPath,
|
|
886
|
-
sourceEntryName: name,
|
|
887
|
-
serverName: name,
|
|
888
|
-
spec: {
|
|
889
|
-
transport: "http",
|
|
890
|
-
url: entry.url,
|
|
891
|
-
headers: entry.headers,
|
|
892
|
-
enabled: entry.disabled !== true
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
898
|
-
result.candidates.push({
|
|
899
|
-
clientId: this.id,
|
|
900
|
-
configPath,
|
|
901
|
-
sourceEntryName: name,
|
|
902
|
-
serverName: name,
|
|
903
|
-
spec: {
|
|
904
|
-
transport: "stdio",
|
|
905
|
-
command: entry.command,
|
|
906
|
-
args: entry.args,
|
|
907
|
-
env: entry.env,
|
|
908
|
-
cwd: entry.cwd,
|
|
909
|
-
enabled: entry.disabled !== true
|
|
910
|
-
}
|
|
911
|
-
});
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
915
|
-
}
|
|
916
|
-
return result;
|
|
917
|
-
}
|
|
918
|
-
syncGateway(_config, options) {
|
|
919
|
-
const configPath = this.detectConfigPath();
|
|
920
|
-
if (!configPath) {
|
|
921
|
-
return errorResult(this.id, undefined, "Unable to resolve Cursor MCP config path.");
|
|
922
|
-
}
|
|
923
|
-
try {
|
|
924
|
-
const raw = readJsonFile(configPath, {});
|
|
925
|
-
const servers = {
|
|
926
|
-
...raw.mcpServers ?? {}
|
|
927
|
-
};
|
|
928
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
929
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
930
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
931
|
-
type: "http",
|
|
932
|
-
url: entry.url,
|
|
933
|
-
headers: entry.headers
|
|
934
|
-
}]));
|
|
935
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
936
|
-
for (const name of enabledManagedNames) {
|
|
937
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
938
|
-
if (conflict) {
|
|
939
|
-
return errorResult(this.id, configPath, conflict);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
943
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
944
|
-
servers[name] = entry;
|
|
945
|
-
}
|
|
946
|
-
const next = {
|
|
947
|
-
...raw,
|
|
948
|
-
mcpServers: servers
|
|
949
|
-
};
|
|
950
|
-
writeJsonAtomic(configPath, next);
|
|
951
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
952
|
-
entry.name,
|
|
953
|
-
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
954
|
-
])));
|
|
955
|
-
return okResult(this.id, configPath);
|
|
956
|
-
} catch (error) {
|
|
957
|
-
return errorResult(this.id, configPath, error.message);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// src/adapters/cline.ts
|
|
963
|
-
import path6 from "node:path";
|
|
964
|
-
import fs4 from "node:fs";
|
|
965
|
-
import { z as z5 } from "zod";
|
|
966
|
-
var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
|
|
967
|
-
var stringMapSchema5 = z5.record(z5.string(), z5.string());
|
|
968
|
-
var clineEntrySchema = z5.object({
|
|
969
|
-
type: z5.string().optional(),
|
|
970
|
-
transportType: z5.string().optional(),
|
|
971
|
-
url: z5.string().min(1).optional(),
|
|
972
|
-
headers: stringMapSchema5.optional(),
|
|
973
|
-
command: z5.string().min(1).optional(),
|
|
974
|
-
args: z5.array(z5.string()).optional(),
|
|
975
|
-
env: stringMapSchema5.optional(),
|
|
976
|
-
cwd: z5.string().min(1).optional(),
|
|
977
|
-
disabled: z5.boolean().optional()
|
|
978
|
-
}).passthrough();
|
|
979
|
-
function getCandidatePaths() {
|
|
980
|
-
if (process.platform === "linux") {
|
|
981
|
-
return [
|
|
982
|
-
path6.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
983
|
-
path6.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
984
|
-
path6.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
985
|
-
];
|
|
986
|
-
}
|
|
987
|
-
if (process.platform === "win32") {
|
|
988
|
-
return [
|
|
989
|
-
path6.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
990
|
-
path6.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
991
|
-
path6.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
992
|
-
];
|
|
993
|
-
}
|
|
994
|
-
return [
|
|
995
|
-
path6.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
996
|
-
path6.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
|
|
997
|
-
path6.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
|
|
998
|
-
];
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
class ClineAdapter {
|
|
1002
|
-
id = "cline";
|
|
1003
|
-
detectConfigPath() {
|
|
1004
|
-
const candidates = getCandidatePaths();
|
|
1005
|
-
for (const candidate of candidates) {
|
|
1006
|
-
if (fs4.existsSync(candidate)) {
|
|
1007
|
-
return candidate;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return candidates[0] ?? null;
|
|
1011
|
-
}
|
|
1012
|
-
supportsHttp() {
|
|
1013
|
-
return true;
|
|
1014
|
-
}
|
|
1015
|
-
scanForImports(_config, managedIndex) {
|
|
1016
|
-
const configPath = this.detectConfigPath();
|
|
1017
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1018
|
-
if (!configPath || !fs4.existsSync(configPath)) {
|
|
1019
|
-
return result;
|
|
1020
|
-
}
|
|
1021
|
-
const raw = readJsonFile(configPath, {});
|
|
1022
|
-
const servers = {
|
|
1023
|
-
...raw.mcpServers ?? {}
|
|
1024
|
-
};
|
|
1025
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1026
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
const parsed = clineEntrySchema.safeParse(rawEntry);
|
|
1030
|
-
if (!parsed.success) {
|
|
1031
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
const entry = parsed.data;
|
|
1035
|
-
const transport = entry.type ?? entry.transportType;
|
|
1036
|
-
if ((transport === undefined || HTTP_TRANSPORT_TYPES.has(transport)) && entry.url && !entry.command) {
|
|
1037
|
-
result.candidates.push({
|
|
1038
|
-
clientId: this.id,
|
|
1039
|
-
configPath,
|
|
1040
|
-
sourceEntryName: name,
|
|
1041
|
-
serverName: name,
|
|
1042
|
-
spec: {
|
|
1043
|
-
transport: "http",
|
|
1044
|
-
url: entry.url,
|
|
1045
|
-
headers: entry.headers,
|
|
1046
|
-
enabled: entry.disabled !== true
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
if ((transport === undefined || transport === "stdio") && entry.command && !entry.url) {
|
|
1052
|
-
result.candidates.push({
|
|
1053
|
-
clientId: this.id,
|
|
1054
|
-
configPath,
|
|
1055
|
-
sourceEntryName: name,
|
|
1056
|
-
serverName: name,
|
|
1057
|
-
spec: {
|
|
1058
|
-
transport: "stdio",
|
|
1059
|
-
command: entry.command,
|
|
1060
|
-
args: entry.args,
|
|
1061
|
-
env: entry.env,
|
|
1062
|
-
cwd: entry.cwd,
|
|
1063
|
-
enabled: entry.disabled !== true
|
|
1064
|
-
}
|
|
1065
|
-
});
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1069
|
-
}
|
|
1070
|
-
return result;
|
|
1071
|
-
}
|
|
1072
|
-
syncGateway(_config, options) {
|
|
1073
|
-
const configPath = this.detectConfigPath();
|
|
1074
|
-
if (!configPath) {
|
|
1075
|
-
return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
|
|
1076
|
-
}
|
|
1077
|
-
if (!fs4.existsSync(configPath)) {
|
|
1078
|
-
return okResult(this.id, configPath, "Cline config not found; skipping sync.");
|
|
1079
|
-
}
|
|
1080
|
-
try {
|
|
1081
|
-
const raw = readJsonFile(configPath, {});
|
|
1082
|
-
const servers = {
|
|
1083
|
-
...raw.mcpServers ?? {}
|
|
1084
|
-
};
|
|
1085
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1086
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1087
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1088
|
-
type: "streamableHttp",
|
|
1089
|
-
url: entry.url,
|
|
1090
|
-
headers: entry.headers,
|
|
1091
|
-
disabled: !entry.enabled
|
|
1092
|
-
}]));
|
|
1093
|
-
for (const name of managedNames) {
|
|
1094
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1095
|
-
if (conflict) {
|
|
1096
|
-
return errorResult(this.id, configPath, conflict);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
|
|
1100
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1101
|
-
servers[name] = entry;
|
|
1102
|
-
}
|
|
1103
|
-
const next = {
|
|
1104
|
-
...raw,
|
|
1105
|
-
mcpServers: servers
|
|
1106
|
-
};
|
|
1107
|
-
writeJsonAtomic(configPath, next);
|
|
1108
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1109
|
-
return okResult(this.id, configPath);
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
return errorResult(this.id, configPath, error.message);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// src/adapters/kiro.ts
|
|
1117
|
-
import fs5 from "node:fs";
|
|
1118
|
-
import path7 from "node:path";
|
|
1119
|
-
import { z as z6 } from "zod";
|
|
1120
|
-
var stringMapSchema6 = z6.record(z6.string(), z6.string());
|
|
1121
|
-
var kiroEntrySchema = z6.object({
|
|
1122
|
-
type: z6.string().optional(),
|
|
1123
|
-
url: z6.string().min(1).optional(),
|
|
1124
|
-
headers: stringMapSchema6.optional(),
|
|
1125
|
-
command: z6.string().min(1).optional(),
|
|
1126
|
-
args: z6.array(z6.string()).optional(),
|
|
1127
|
-
env: stringMapSchema6.optional(),
|
|
1128
|
-
cwd: z6.string().min(1).optional(),
|
|
1129
|
-
disabled: z6.boolean().optional()
|
|
1130
|
-
}).passthrough();
|
|
1131
|
-
|
|
1132
|
-
class KiroAdapter {
|
|
1133
|
-
id = "kiro";
|
|
1134
|
-
detectConfigPath() {
|
|
1135
|
-
return path7.join(homeDir(), ".kiro", "settings", "mcp.json");
|
|
1136
|
-
}
|
|
1137
|
-
supportsHttp() {
|
|
1138
|
-
return true;
|
|
1139
|
-
}
|
|
1140
|
-
scanForImports(_config, managedIndex) {
|
|
1141
|
-
const configPath = this.detectConfigPath();
|
|
1142
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1143
|
-
if (!configPath) {
|
|
1144
|
-
return result;
|
|
1145
|
-
}
|
|
1146
|
-
const raw = readJsonFile(configPath, {});
|
|
1147
|
-
const servers = {
|
|
1148
|
-
...raw.mcpServers ?? {}
|
|
1149
|
-
};
|
|
1150
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1151
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
const parsed = kiroEntrySchema.safeParse(rawEntry);
|
|
1155
|
-
if (!parsed.success) {
|
|
1156
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
const entry = parsed.data;
|
|
1160
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http" || entry.type === "sse") && entry.url && !entry.command) {
|
|
1161
|
-
result.candidates.push({
|
|
1162
|
-
clientId: this.id,
|
|
1163
|
-
configPath,
|
|
1164
|
-
sourceEntryName: name,
|
|
1165
|
-
serverName: name,
|
|
1166
|
-
spec: {
|
|
1167
|
-
transport: "http",
|
|
1168
|
-
url: entry.url,
|
|
1169
|
-
headers: entry.headers,
|
|
1170
|
-
enabled: entry.disabled !== true
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1176
|
-
result.candidates.push({
|
|
1177
|
-
clientId: this.id,
|
|
1178
|
-
configPath,
|
|
1179
|
-
sourceEntryName: name,
|
|
1180
|
-
serverName: name,
|
|
1181
|
-
spec: {
|
|
1182
|
-
transport: "stdio",
|
|
1183
|
-
command: entry.command,
|
|
1184
|
-
args: entry.args,
|
|
1185
|
-
env: entry.env,
|
|
1186
|
-
cwd: entry.cwd,
|
|
1187
|
-
enabled: entry.disabled !== true
|
|
1188
|
-
}
|
|
1189
|
-
});
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1193
|
-
}
|
|
1194
|
-
return result;
|
|
1195
|
-
}
|
|
1196
|
-
syncGateway(_config, options) {
|
|
1197
|
-
const configPath = this.detectConfigPath();
|
|
1198
|
-
if (!configPath) {
|
|
1199
|
-
return errorResult(this.id, undefined, "Unable to resolve Kiro MCP config path.");
|
|
1200
|
-
}
|
|
1201
|
-
try {
|
|
1202
|
-
const raw = readJsonFile(configPath, {});
|
|
1203
|
-
const servers = {
|
|
1204
|
-
...raw.mcpServers ?? {}
|
|
1205
|
-
};
|
|
1206
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1207
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1208
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1209
|
-
url: entry.url,
|
|
1210
|
-
headers: entry.headers,
|
|
1211
|
-
disabled: !entry.enabled
|
|
1212
|
-
}]));
|
|
1213
|
-
for (const name of managedNames) {
|
|
1214
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1215
|
-
if (conflict) {
|
|
1216
|
-
return errorResult(this.id, configPath, conflict);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
|
|
1220
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1221
|
-
servers[name] = entry;
|
|
1222
|
-
}
|
|
1223
|
-
const next = {
|
|
1224
|
-
...raw,
|
|
1225
|
-
mcpServers: servers
|
|
1226
|
-
};
|
|
1227
|
-
writeJsonAtomic(configPath, next);
|
|
1228
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1229
|
-
return okResult(this.id, configPath);
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
return errorResult(this.id, configPath, error.message);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
syncSkills(skills) {
|
|
1235
|
-
const skillsDir = path7.join(homeDir(), ".kiro", "skills");
|
|
1236
|
-
if (!fs5.existsSync(skillsDir)) {
|
|
1237
|
-
fs5.mkdirSync(skillsDir, { recursive: true });
|
|
1238
|
-
}
|
|
1239
|
-
const existingFiles = fs5.readdirSync(skillsDir);
|
|
1240
|
-
const managedIds = new Set(skills.map((s) => s.id));
|
|
1241
|
-
for (const file of existingFiles) {
|
|
1242
|
-
if (file.endsWith(".md")) {
|
|
1243
|
-
const id = path7.basename(file, ".md");
|
|
1244
|
-
if (!managedIds.has(id)) {
|
|
1245
|
-
fs5.unlinkSync(path7.join(skillsDir, file));
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
for (const skill of skills) {
|
|
1250
|
-
const targetPath = path7.join(skillsDir, `${skill.id}.md`);
|
|
1251
|
-
fs5.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// src/adapters/opencode.ts
|
|
1257
|
-
import path8 from "node:path";
|
|
1258
|
-
import { z as z7 } from "zod";
|
|
1259
|
-
var stringMapSchema7 = z7.record(z7.string(), z7.string());
|
|
1260
|
-
var openCodeEntrySchema = z7.object({
|
|
1261
|
-
type: z7.string().optional(),
|
|
1262
|
-
url: z7.string().min(1).optional(),
|
|
1263
|
-
headers: stringMapSchema7.optional(),
|
|
1264
|
-
command: z7.union([z7.string().min(1), z7.array(z7.string())]).optional(),
|
|
1265
|
-
args: z7.array(z7.string()).optional(),
|
|
1266
|
-
env: stringMapSchema7.optional(),
|
|
1267
|
-
environment: stringMapSchema7.optional(),
|
|
1268
|
-
cwd: z7.string().min(1).optional(),
|
|
1269
|
-
enabled: z7.boolean().optional()
|
|
1270
|
-
}).passthrough();
|
|
1271
|
-
|
|
1272
|
-
class OpenCodeAdapter {
|
|
1273
|
-
id = "opencode";
|
|
1274
|
-
detectConfigPath() {
|
|
1275
|
-
return path8.join(homeDir(), ".config", "opencode", "opencode.json");
|
|
1276
|
-
}
|
|
1277
|
-
supportsHttp() {
|
|
1278
|
-
return true;
|
|
1279
|
-
}
|
|
1280
|
-
scanForImports(_config, managedIndex) {
|
|
1281
|
-
const configPath = this.detectConfigPath();
|
|
1282
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1283
|
-
if (!configPath) {
|
|
1284
|
-
return result;
|
|
1285
|
-
}
|
|
1286
|
-
const raw = readJsonFile(configPath, {});
|
|
1287
|
-
const mcp = {
|
|
1288
|
-
...raw.mcp ?? {}
|
|
1289
|
-
};
|
|
1290
|
-
for (const [name, rawEntry] of Object.entries(mcp)) {
|
|
1291
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
const parsed = openCodeEntrySchema.safeParse(rawEntry);
|
|
1295
|
-
if (!parsed.success) {
|
|
1296
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const entry = parsed.data;
|
|
1300
|
-
if ((entry.type === undefined || entry.type === "remote" || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1301
|
-
result.candidates.push({
|
|
1302
|
-
clientId: this.id,
|
|
1303
|
-
configPath,
|
|
1304
|
-
sourceEntryName: name,
|
|
1305
|
-
serverName: name,
|
|
1306
|
-
spec: {
|
|
1307
|
-
transport: "http",
|
|
1308
|
-
url: entry.url,
|
|
1309
|
-
headers: entry.headers,
|
|
1310
|
-
enabled: entry.enabled !== false
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
if ((entry.type === undefined || entry.type === "local" || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1316
|
-
const commandParts = Array.isArray(entry.command) ? entry.command : [entry.command, ...entry.args ?? []];
|
|
1317
|
-
if (commandParts.length === 0) {
|
|
1318
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
result.candidates.push({
|
|
1322
|
-
clientId: this.id,
|
|
1323
|
-
configPath,
|
|
1324
|
-
sourceEntryName: name,
|
|
1325
|
-
serverName: name,
|
|
1326
|
-
spec: {
|
|
1327
|
-
transport: "stdio",
|
|
1328
|
-
command: commandParts[0],
|
|
1329
|
-
args: commandParts.slice(1),
|
|
1330
|
-
env: entry.environment ?? entry.env,
|
|
1331
|
-
cwd: entry.cwd,
|
|
1332
|
-
enabled: entry.enabled !== false
|
|
1333
|
-
}
|
|
1334
|
-
});
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1338
|
-
}
|
|
1339
|
-
return result;
|
|
1340
|
-
}
|
|
1341
|
-
syncGateway(_config, options) {
|
|
1342
|
-
const configPath = this.detectConfigPath();
|
|
1343
|
-
if (!configPath) {
|
|
1344
|
-
return errorResult(this.id, undefined, "Unable to resolve OpenCode config path.");
|
|
1345
|
-
}
|
|
1346
|
-
try {
|
|
1347
|
-
const raw = readJsonFile(configPath, {});
|
|
1348
|
-
const mcp = {
|
|
1349
|
-
...raw.mcp ?? {}
|
|
1350
|
-
};
|
|
1351
|
-
removeSourceEntries(mcp, options.sourceEntriesToRemove);
|
|
1352
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1353
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1354
|
-
type: "remote",
|
|
1355
|
-
url: entry.url,
|
|
1356
|
-
headers: entry.headers,
|
|
1357
|
-
enabled: entry.enabled
|
|
1358
|
-
}]));
|
|
1359
|
-
for (const name of managedNames) {
|
|
1360
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
|
|
1361
|
-
if (conflict) {
|
|
1362
|
-
return errorResult(this.id, configPath, conflict);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, mcp, managedNames);
|
|
1366
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1367
|
-
mcp[name] = entry;
|
|
1368
|
-
}
|
|
1369
|
-
const next = {
|
|
1370
|
-
...raw,
|
|
1371
|
-
mcp
|
|
1372
|
-
};
|
|
1373
|
-
writeJsonAtomic(configPath, next);
|
|
1374
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
|
|
1375
|
-
return okResult(this.id, configPath);
|
|
1376
|
-
} catch (error) {
|
|
1377
|
-
return errorResult(this.id, configPath, error.message);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// src/adapters/vscode.ts
|
|
1383
|
-
import path9 from "node:path";
|
|
1384
|
-
import { z as z8 } from "zod";
|
|
1385
|
-
var stringMapSchema8 = z8.record(z8.string(), z8.string());
|
|
1386
|
-
var vscodeEntrySchema = z8.object({
|
|
1387
|
-
type: z8.string().optional(),
|
|
1388
|
-
url: z8.string().min(1).optional(),
|
|
1389
|
-
headers: stringMapSchema8.optional(),
|
|
1390
|
-
command: z8.string().min(1).optional(),
|
|
1391
|
-
args: z8.array(z8.string()).optional(),
|
|
1392
|
-
env: stringMapSchema8.optional(),
|
|
1393
|
-
cwd: z8.string().min(1).optional(),
|
|
1394
|
-
disabled: z8.boolean().optional()
|
|
1395
|
-
}).passthrough();
|
|
1396
|
-
|
|
1397
|
-
class VsCodeAdapter {
|
|
1398
|
-
id = "vscode";
|
|
1399
|
-
detectConfigPath() {
|
|
1400
|
-
if (process.platform === "linux") {
|
|
1401
|
-
return path9.join(homeDir(), ".config", "Code", "User", "mcp.json");
|
|
1402
|
-
}
|
|
1403
|
-
if (process.platform === "win32") {
|
|
1404
|
-
return path9.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
|
|
1405
|
-
}
|
|
1406
|
-
return path9.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
|
|
1407
|
-
}
|
|
1408
|
-
supportsHttp() {
|
|
1409
|
-
return true;
|
|
1410
|
-
}
|
|
1411
|
-
scanForImports(_config, managedIndex) {
|
|
1412
|
-
const configPath = this.detectConfigPath();
|
|
1413
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1414
|
-
if (!configPath) {
|
|
1415
|
-
return result;
|
|
1416
|
-
}
|
|
1417
|
-
const raw = readJsonFile(configPath, {});
|
|
1418
|
-
const servers = {
|
|
1419
|
-
...raw.servers ?? {}
|
|
1420
|
-
};
|
|
1421
|
-
for (const [name, rawEntry] of Object.entries(servers)) {
|
|
1422
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1423
|
-
continue;
|
|
1424
|
-
}
|
|
1425
|
-
const parsed = vscodeEntrySchema.safeParse(rawEntry);
|
|
1426
|
-
if (!parsed.success) {
|
|
1427
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1428
|
-
continue;
|
|
1429
|
-
}
|
|
1430
|
-
const entry = parsed.data;
|
|
1431
|
-
if ((entry.type === undefined || entry.type === "http" || entry.type === "streamable-http") && entry.url && !entry.command) {
|
|
1432
|
-
result.candidates.push({
|
|
1433
|
-
clientId: this.id,
|
|
1434
|
-
configPath,
|
|
1435
|
-
sourceEntryName: name,
|
|
1436
|
-
serverName: name,
|
|
1437
|
-
spec: {
|
|
1438
|
-
transport: "http",
|
|
1439
|
-
url: entry.url,
|
|
1440
|
-
headers: entry.headers,
|
|
1441
|
-
enabled: entry.disabled !== true
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
if ((entry.type === undefined || entry.type === "stdio") && entry.command && !entry.url) {
|
|
1447
|
-
result.candidates.push({
|
|
1448
|
-
clientId: this.id,
|
|
1449
|
-
configPath,
|
|
1450
|
-
sourceEntryName: name,
|
|
1451
|
-
serverName: name,
|
|
1452
|
-
spec: {
|
|
1453
|
-
transport: "stdio",
|
|
1454
|
-
command: entry.command,
|
|
1455
|
-
args: entry.args,
|
|
1456
|
-
env: entry.env,
|
|
1457
|
-
cwd: entry.cwd,
|
|
1458
|
-
enabled: entry.disabled !== true
|
|
1459
|
-
}
|
|
1460
|
-
});
|
|
1461
|
-
continue;
|
|
1462
|
-
}
|
|
1463
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1464
|
-
}
|
|
1465
|
-
return result;
|
|
1466
|
-
}
|
|
1467
|
-
syncGateway(_config, options) {
|
|
1468
|
-
const configPath = this.detectConfigPath();
|
|
1469
|
-
if (!configPath) {
|
|
1470
|
-
return errorResult(this.id, undefined, "Unable to resolve VS Code MCP config path.");
|
|
1471
|
-
}
|
|
1472
|
-
try {
|
|
1473
|
-
const raw = readJsonFile(configPath, {});
|
|
1474
|
-
const servers = {
|
|
1475
|
-
...raw.servers ?? {}
|
|
1476
|
-
};
|
|
1477
|
-
removeSourceEntries(servers, options.sourceEntriesToRemove);
|
|
1478
|
-
const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
|
|
1479
|
-
const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
|
|
1480
|
-
type: "http",
|
|
1481
|
-
url: entry.url,
|
|
1482
|
-
headers: entry.headers
|
|
1483
|
-
}]));
|
|
1484
|
-
const enabledManagedNames = enabledEntries.map((entry) => entry.name);
|
|
1485
|
-
for (const name of enabledManagedNames) {
|
|
1486
|
-
const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
|
|
1487
|
-
if (conflict) {
|
|
1488
|
-
return errorResult(this.id, configPath, conflict);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
|
|
1492
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1493
|
-
servers[name] = entry;
|
|
1494
|
-
}
|
|
1495
|
-
const next = {
|
|
1496
|
-
...raw,
|
|
1497
|
-
servers
|
|
1498
|
-
};
|
|
1499
|
-
writeJsonAtomic(configPath, next);
|
|
1500
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1501
|
-
entry.name,
|
|
1502
|
-
JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1503
|
-
])));
|
|
1504
|
-
return okResult(this.id, configPath);
|
|
1505
|
-
} catch (error) {
|
|
1506
|
-
return errorResult(this.id, configPath, error.message);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// src/adapters/qwen.ts
|
|
1512
|
-
import fs6 from "node:fs";
|
|
1513
|
-
import path10 from "node:path";
|
|
1514
|
-
import { z as z9 } from "zod";
|
|
1515
|
-
var stringMapSchema9 = z9.record(z9.string(), z9.string());
|
|
1516
|
-
var qwenEntrySchema = z9.object({
|
|
1517
|
-
httpUrl: z9.string().min(1).optional(),
|
|
1518
|
-
url: z9.string().min(1).optional(),
|
|
1519
|
-
headers: stringMapSchema9.optional(),
|
|
1520
|
-
command: z9.string().min(1).optional(),
|
|
1521
|
-
args: z9.array(z9.string()).optional(),
|
|
1522
|
-
env: stringMapSchema9.optional(),
|
|
1523
|
-
cwd: z9.string().min(1).optional(),
|
|
1524
|
-
disabled: z9.boolean().optional()
|
|
1525
|
-
}).passthrough();
|
|
1526
|
-
|
|
1527
|
-
class QwenAdapter {
|
|
1528
|
-
id = "qwen";
|
|
1529
|
-
detectConfigPath() {
|
|
1530
|
-
return path10.join(homeDir(), ".qwen", "settings.json");
|
|
1531
|
-
}
|
|
1532
|
-
supportsHttp() {
|
|
1533
|
-
return true;
|
|
1534
|
-
}
|
|
1535
|
-
scanForImports(_config, managedIndex) {
|
|
1536
|
-
const configPath = this.detectConfigPath();
|
|
1537
|
-
const result = emptyImportScan(this.id, configPath ?? undefined);
|
|
1538
|
-
if (!configPath) {
|
|
1539
|
-
return result;
|
|
1540
|
-
}
|
|
1541
|
-
const raw = readJsonFile(configPath, {});
|
|
1542
|
-
const excludedSet = new Set(raw.mcp?.excluded ?? []);
|
|
1543
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1544
|
-
for (const [name, rawEntry] of Object.entries(topLevelServers)) {
|
|
1545
|
-
if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
|
|
1546
|
-
continue;
|
|
1547
|
-
}
|
|
1548
|
-
const parsed = qwenEntrySchema.safeParse(rawEntry);
|
|
1549
|
-
if (!parsed.success) {
|
|
1550
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
|
|
1551
|
-
continue;
|
|
1552
|
-
}
|
|
1553
|
-
const entry = parsed.data;
|
|
1554
|
-
if (entry.httpUrl && !entry.command) {
|
|
1555
|
-
result.candidates.push({
|
|
1556
|
-
clientId: this.id,
|
|
1557
|
-
configPath,
|
|
1558
|
-
sourceEntryName: name,
|
|
1559
|
-
serverName: name,
|
|
1560
|
-
spec: {
|
|
1561
|
-
transport: "http",
|
|
1562
|
-
url: entry.httpUrl,
|
|
1563
|
-
headers: entry.headers,
|
|
1564
|
-
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1565
|
-
}
|
|
1566
|
-
});
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
if (entry.url && !entry.httpUrl && !entry.command) {
|
|
1570
|
-
result.candidates.push({
|
|
1571
|
-
clientId: this.id,
|
|
1572
|
-
configPath,
|
|
1573
|
-
sourceEntryName: name,
|
|
1574
|
-
serverName: name,
|
|
1575
|
-
spec: {
|
|
1576
|
-
transport: "http",
|
|
1577
|
-
url: entry.url,
|
|
1578
|
-
headers: entry.headers,
|
|
1579
|
-
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1580
|
-
}
|
|
1581
|
-
});
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
if (entry.command && !entry.httpUrl) {
|
|
1585
|
-
result.candidates.push({
|
|
1586
|
-
clientId: this.id,
|
|
1587
|
-
configPath,
|
|
1588
|
-
sourceEntryName: name,
|
|
1589
|
-
serverName: name,
|
|
1590
|
-
spec: {
|
|
1591
|
-
transport: "stdio",
|
|
1592
|
-
command: entry.command,
|
|
1593
|
-
args: entry.args,
|
|
1594
|
-
env: entry.env,
|
|
1595
|
-
cwd: entry.cwd,
|
|
1596
|
-
enabled: !(entry.disabled === true || excludedSet.has(name))
|
|
1597
|
-
}
|
|
1598
|
-
});
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
|
|
1602
|
-
}
|
|
1603
|
-
return result;
|
|
1604
|
-
}
|
|
1605
|
-
syncGateway(_config, options) {
|
|
1606
|
-
const configPath = this.detectConfigPath();
|
|
1607
|
-
if (!configPath) {
|
|
1608
|
-
return errorResult(this.id, undefined, "Unable to resolve Qwen config path.");
|
|
1609
|
-
}
|
|
1610
|
-
try {
|
|
1611
|
-
const raw = readJsonFile(configPath, {});
|
|
1612
|
-
const managedNames = options.managedEntries.map((entry) => entry.name);
|
|
1613
|
-
const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
|
|
1614
|
-
httpUrl: entry.url,
|
|
1615
|
-
headers: entry.headers
|
|
1616
|
-
}]));
|
|
1617
|
-
const topLevelServers = raw.mcpServers ?? {};
|
|
1618
|
-
removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
|
|
1619
|
-
for (const name of managedNames) {
|
|
1620
|
-
const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
|
|
1621
|
-
if (topLevelConflict) {
|
|
1622
|
-
return errorResult(this.id, configPath, topLevelConflict);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, managedNames);
|
|
1626
|
-
for (const [name, entry] of Object.entries(serverEntries)) {
|
|
1627
|
-
topLevelServers[name] = entry;
|
|
1628
|
-
}
|
|
1629
|
-
raw.mcpServers = topLevelServers;
|
|
1630
|
-
syncMcpExcludedArray(raw, managedNames, options.managedEntries);
|
|
1631
|
-
writeJsonAtomic(configPath, raw);
|
|
1632
|
-
setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
|
|
1633
|
-
entry.name,
|
|
1634
|
-
JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
|
|
1635
|
-
])));
|
|
1636
|
-
return okResult(this.id, configPath);
|
|
1637
|
-
} catch (error) {
|
|
1638
|
-
return errorResult(this.id, configPath, error.message);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
syncSkills(skills) {
|
|
1642
|
-
const skillsDir = path10.join(homeDir(), ".qwen", "skills");
|
|
1643
|
-
if (!fs6.existsSync(skillsDir)) {
|
|
1644
|
-
fs6.mkdirSync(skillsDir, { recursive: true });
|
|
1645
|
-
}
|
|
1646
|
-
const existingFiles = fs6.readdirSync(skillsDir);
|
|
1647
|
-
const managedIds = new Set(skills.map((s) => s.id));
|
|
1648
|
-
for (const file of existingFiles) {
|
|
1649
|
-
if (file.endsWith(".md")) {
|
|
1650
|
-
const id = path10.basename(file, ".md");
|
|
1651
|
-
if (!managedIds.has(id)) {
|
|
1652
|
-
fs6.unlinkSync(path10.join(skillsDir, file));
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
for (const skill of skills) {
|
|
1657
|
-
const targetPath = path10.join(skillsDir, `${skill.id}.md`);
|
|
1658
|
-
fs6.writeFileSync(targetPath, skill.content, "utf-8");
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// src/adapters/index.ts
|
|
1664
|
-
function getAdapters() {
|
|
1665
|
-
return [
|
|
1666
|
-
new ClaudeAdapter,
|
|
1667
|
-
new ClaudeDesktopAdapter,
|
|
1668
|
-
new CodexAdapter,
|
|
1669
|
-
new CursorAdapter,
|
|
1670
|
-
new ClineAdapter,
|
|
1671
|
-
new OpenCodeAdapter,
|
|
1672
|
-
new KiroAdapter,
|
|
1673
|
-
new VsCodeAdapter,
|
|
1674
|
-
new QwenAdapter
|
|
1675
|
-
];
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
// src/core/managed-index.ts
|
|
1679
|
-
function defaultManagedIndex() {
|
|
1680
|
-
return {
|
|
1681
|
-
schemaVersion: 1,
|
|
1682
|
-
managed: {}
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
function loadManagedIndex(filePath = getManagedIndexPath()) {
|
|
1686
|
-
const raw = readJsonFile(filePath, defaultManagedIndex());
|
|
1687
|
-
if (!raw || typeof raw !== "object") {
|
|
1688
|
-
return defaultManagedIndex();
|
|
1689
|
-
}
|
|
1690
|
-
if (raw.schemaVersion !== 1) {
|
|
1691
|
-
return defaultManagedIndex();
|
|
1692
|
-
}
|
|
1693
|
-
return raw;
|
|
1694
|
-
}
|
|
1695
|
-
function saveManagedIndex(index, filePath = getManagedIndexPath()) {
|
|
1696
|
-
writeJsonAtomic(filePath, index);
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
// src/core/sync.ts
|
|
1700
|
-
function getGatewayUrl(config) {
|
|
1701
|
-
return `http://127.0.0.1:${config.gateway.port}/mcp`;
|
|
1702
|
-
}
|
|
1703
|
-
function buildProjectScopes(config) {
|
|
1704
|
-
const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
|
|
1705
|
-
return Object.values(config.projects ?? {}).map((project) => {
|
|
1706
|
-
const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
|
|
1707
|
-
return { path: project.path, disabledServerNames };
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
function buildManagedEntries(config, gatewayUrl, localToken) {
|
|
1711
|
-
const entries = Object.entries(config.servers);
|
|
1712
|
-
return entries.map(([name, spec]) => ({
|
|
1713
|
-
name: `${name} (mcpx)`,
|
|
1714
|
-
url: `${gatewayUrl}?upstream=${encodeURIComponent(name)}`,
|
|
1715
|
-
headers: {
|
|
1716
|
-
Authorization: `Bearer ${localToken}`
|
|
1717
|
-
},
|
|
1718
|
-
enabled: isServerEnabled(spec)
|
|
1719
|
-
}));
|
|
1720
|
-
}
|
|
1721
|
-
function isClientInternalServerCommand(candidate) {
|
|
1722
|
-
return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
|
|
1723
|
-
}
|
|
1724
|
-
function syncAllClients(config, secrets, targetClients) {
|
|
1725
|
-
const adapters = getAdapters();
|
|
1726
|
-
const managedIndexPath = getManagedIndexPath();
|
|
1727
|
-
const managedIndex = loadManagedIndex(managedIndexPath);
|
|
1728
|
-
const filteredAdapters = targetClients && targetClients.length > 0 ? adapters.filter((adapter) => targetClients.includes(adapter.id)) : adapters;
|
|
1729
|
-
const imports = {
|
|
1730
|
-
imported: [],
|
|
1731
|
-
duplicates: [],
|
|
1732
|
-
skipped: [],
|
|
1733
|
-
conflicts: [],
|
|
1734
|
-
errors: []
|
|
1735
|
-
};
|
|
1736
|
-
const sourceEntriesToRemove = new Map;
|
|
1737
|
-
for (const adapter of filteredAdapters) {
|
|
1738
|
-
try {
|
|
1739
|
-
const scan = adapter.scanForImports(config, managedIndex);
|
|
1740
|
-
imports.skipped.push(...scan.skipped);
|
|
1741
|
-
for (const candidate of scan.candidates) {
|
|
1742
|
-
if (isClientInternalServerCommand(candidate)) {
|
|
1743
|
-
imports.skipped.push({
|
|
1744
|
-
clientId: candidate.clientId,
|
|
1745
|
-
configPath: candidate.configPath,
|
|
1746
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
1747
|
-
serverName: candidate.serverName,
|
|
1748
|
-
reason: "client-internal server"
|
|
1749
|
-
});
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
const existing = config.servers[candidate.serverName];
|
|
1753
|
-
if (!existing) {
|
|
1754
|
-
config.servers[candidate.serverName] = candidate.spec;
|
|
1755
|
-
imports.imported.push({
|
|
1756
|
-
clientId: candidate.clientId,
|
|
1757
|
-
configPath: candidate.configPath,
|
|
1758
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
1759
|
-
serverName: candidate.serverName
|
|
1760
|
-
});
|
|
1761
|
-
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1762
|
-
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1763
|
-
}
|
|
1764
|
-
sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
|
|
1765
|
-
continue;
|
|
1766
|
-
}
|
|
1767
|
-
if (serverSpecsEqual(existing, candidate.spec)) {
|
|
1768
|
-
imports.duplicates.push({
|
|
1769
|
-
clientId: candidate.clientId,
|
|
1770
|
-
configPath: candidate.configPath,
|
|
1771
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
1772
|
-
serverName: candidate.serverName
|
|
1773
|
-
});
|
|
1774
|
-
if (!sourceEntriesToRemove.has(candidate.clientId)) {
|
|
1775
|
-
sourceEntriesToRemove.set(candidate.clientId, new Set);
|
|
1776
|
-
}
|
|
1777
|
-
sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
imports.conflicts.push({
|
|
1781
|
-
clientId: candidate.clientId,
|
|
1782
|
-
configPath: candidate.configPath,
|
|
1783
|
-
sourceEntryName: candidate.sourceEntryName,
|
|
1784
|
-
serverName: candidate.serverName,
|
|
1785
|
-
message: `Server "${candidate.serverName}" already exists in mcpx with a different configuration.`
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
} catch (error) {
|
|
1789
|
-
imports.errors.push({
|
|
1790
|
-
clientId: adapter.id,
|
|
1791
|
-
configPath: adapter.detectConfigPath() ?? undefined,
|
|
1792
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1793
|
-
});
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
const localToken = ensureGatewayToken(config, secrets);
|
|
1797
|
-
const gatewayUrl = getGatewayUrl(config);
|
|
1798
|
-
const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
|
|
1799
|
-
const skills = listSkills();
|
|
1800
|
-
const results = [];
|
|
1801
|
-
for (const adapter of filteredAdapters) {
|
|
1802
|
-
if (adapter.syncSkills) {
|
|
1803
|
-
try {
|
|
1804
|
-
adapter.syncSkills(skills);
|
|
1805
|
-
} catch (error) {}
|
|
1806
|
-
}
|
|
1807
|
-
if (!adapter.supportsHttp()) {
|
|
1808
|
-
results.push({
|
|
1809
|
-
clientId: adapter.id,
|
|
1810
|
-
status: "UNSUPPORTED_HTTP",
|
|
1811
|
-
message: "Client adapter does not support MCP over HTTP."
|
|
1812
|
-
});
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
const result = adapter.syncGateway(config, {
|
|
1816
|
-
managedEntries,
|
|
1817
|
-
managedIndex,
|
|
1818
|
-
managedIndexPath,
|
|
1819
|
-
sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
|
|
1820
|
-
projectScopes: buildProjectScopes(config)
|
|
1821
|
-
});
|
|
1822
|
-
results.push(result);
|
|
1823
|
-
}
|
|
1824
|
-
for (const result of results) {
|
|
1825
|
-
config.clients[result.clientId] = {
|
|
1826
|
-
status: result.status,
|
|
1827
|
-
configPath: result.configPath,
|
|
1828
|
-
message: result.message,
|
|
1829
|
-
lastSyncAt: new Date().toISOString()
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
saveManagedIndex(managedIndex, managedIndexPath);
|
|
1833
|
-
saveConfig(config);
|
|
1834
|
-
const hasErrors = results.some((result) => result.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
|
|
1835
|
-
return {
|
|
1836
|
-
gatewayUrl,
|
|
1837
|
-
imports,
|
|
1838
|
-
results,
|
|
1839
|
-
hasErrors
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
251
|
// src/core/server-auth.ts
|
|
1844
252
|
function normalizeNamePart(value) {
|
|
1845
253
|
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
@@ -1939,6 +347,7 @@ function listAuthBindings(spec) {
|
|
|
1939
347
|
value
|
|
1940
348
|
}));
|
|
1941
349
|
}
|
|
350
|
+
var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
1942
351
|
function maybePrefixBearer(target, value, raw = false) {
|
|
1943
352
|
if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
|
|
1944
353
|
return value;
|
|
@@ -1946,25 +355,28 @@ function maybePrefixBearer(target, value, raw = false) {
|
|
|
1946
355
|
if (/^\S+\s+\S+/.test(value)) {
|
|
1947
356
|
return value;
|
|
1948
357
|
}
|
|
1949
|
-
|
|
358
|
+
if (TOKEN68.test(value)) {
|
|
359
|
+
return `Bearer ${value}`;
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
1950
362
|
}
|
|
1951
363
|
|
|
1952
364
|
// src/core/spawn-env.ts
|
|
1953
365
|
import os from "node:os";
|
|
1954
|
-
import
|
|
1955
|
-
import
|
|
366
|
+
import path from "node:path";
|
|
367
|
+
import fs from "node:fs";
|
|
1956
368
|
function existingNvmCurrentBin() {
|
|
1957
|
-
const nvmDir = process.env.NVM_DIR ??
|
|
1958
|
-
const current =
|
|
1959
|
-
return
|
|
369
|
+
const nvmDir = process.env.NVM_DIR ?? path.join(os.homedir(), ".nvm");
|
|
370
|
+
const current = path.join(nvmDir, "current", "bin");
|
|
371
|
+
return fs.existsSync(current) ? current : null;
|
|
1960
372
|
}
|
|
1961
373
|
function buildEnrichedPath(current = process.env.PATH ?? "") {
|
|
1962
374
|
const candidates = [
|
|
1963
375
|
...current.split(":"),
|
|
1964
376
|
"/opt/homebrew/bin",
|
|
1965
377
|
"/usr/local/bin",
|
|
1966
|
-
|
|
1967
|
-
|
|
378
|
+
path.join(os.homedir(), ".bun", "bin"),
|
|
379
|
+
path.join(os.homedir(), ".local", "bin"),
|
|
1968
380
|
existingNvmCurrentBin()
|
|
1969
381
|
].filter((entry) => Boolean(entry));
|
|
1970
382
|
const seen = new Set;
|
|
@@ -2200,7 +612,12 @@ function bindOAuthReference(config, serverName) {
|
|
|
2200
612
|
};
|
|
2201
613
|
}
|
|
2202
614
|
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver) {
|
|
2203
|
-
|
|
615
|
+
const oauthName = oauthSecretName(serverName, "tokens");
|
|
616
|
+
const clientName = oauthSecretName(serverName, "client");
|
|
617
|
+
const backupTokens = secrets.getSecret(oauthName);
|
|
618
|
+
const backupClient = secrets.getSecret(clientName);
|
|
619
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
620
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
|
|
2204
621
|
const state = crypto.randomUUID();
|
|
2205
622
|
let callbackServer;
|
|
2206
623
|
let redirectUrl;
|
|
@@ -2233,8 +650,16 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
|
|
|
2233
650
|
const config = loadConfig(configPath);
|
|
2234
651
|
bindOAuthReference(config, serverName);
|
|
2235
652
|
saveConfig(config, configPath);
|
|
2236
|
-
syncAllClients(config, secrets);
|
|
653
|
+
const summary = syncAllClients(config, secrets);
|
|
654
|
+
persistSyncState(summary, config);
|
|
655
|
+
saveConfig(config, configPath);
|
|
2237
656
|
return { serverName, authorized: true };
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (backupTokens)
|
|
659
|
+
secrets.setSecret(oauthName, backupTokens);
|
|
660
|
+
if (backupClient)
|
|
661
|
+
secrets.setSecret(clientName, backupClient);
|
|
662
|
+
throw error;
|
|
2238
663
|
} finally {
|
|
2239
664
|
if (callbackServer) {
|
|
2240
665
|
await closeServer(callbackServer);
|
|
@@ -2244,41 +669,61 @@ async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, cod
|
|
|
2244
669
|
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
2245
670
|
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
2246
671
|
if (!stored) {
|
|
2247
|
-
throw new
|
|
2248
|
-
}
|
|
2249
|
-
if (!options.forceRefresh && !tokensAreExpiring(stored)) {
|
|
2250
|
-
return stored.tokens.access_token;
|
|
672
|
+
throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
|
|
2251
673
|
}
|
|
2252
|
-
if (
|
|
2253
|
-
if (!
|
|
2254
|
-
|
|
674
|
+
if (options.forceRefresh || tokensAreExpiring(stored)) {
|
|
675
|
+
if (!stored.tokens.refresh_token) {
|
|
676
|
+
if (!options.forceRefresh) {
|
|
677
|
+
return stored.tokens.access_token;
|
|
678
|
+
}
|
|
679
|
+
throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
|
|
680
|
+
}
|
|
681
|
+
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
682
|
+
const clientInformation = provider.clientInformation();
|
|
683
|
+
if (!clientInformation) {
|
|
684
|
+
throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
|
|
685
|
+
}
|
|
686
|
+
let serverInfo;
|
|
687
|
+
try {
|
|
688
|
+
const cachedDiscovery = provider.discoveryState();
|
|
689
|
+
serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
692
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
693
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
|
|
694
|
+
}
|
|
695
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
699
|
+
metadata: serverInfo.authorizationServerMetadata,
|
|
700
|
+
clientInformation,
|
|
701
|
+
refreshToken: stored.tokens.refresh_token,
|
|
702
|
+
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
703
|
+
});
|
|
704
|
+
provider.saveTokens(refreshed);
|
|
705
|
+
return refreshed.access_token;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
if (/invalid_grant|invalid_client|4\d{2}/i.test(message)) {
|
|
709
|
+
throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
|
|
710
|
+
}
|
|
711
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
712
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
|
|
713
|
+
}
|
|
714
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
|
|
2255
715
|
}
|
|
2256
|
-
throw new Error(`No OAuth refresh token stored for "${serverName}". Re-authentication required.`);
|
|
2257
|
-
}
|
|
2258
|
-
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
2259
|
-
const clientInformation = provider.clientInformation();
|
|
2260
|
-
if (!clientInformation) {
|
|
2261
|
-
throw new Error(`No OAuth client information stored for "${serverName}". Re-authentication required.`);
|
|
2262
716
|
}
|
|
2263
|
-
|
|
2264
|
-
const serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
2265
|
-
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
2266
|
-
metadata: serverInfo.authorizationServerMetadata,
|
|
2267
|
-
clientInformation,
|
|
2268
|
-
refreshToken: stored.tokens.refresh_token,
|
|
2269
|
-
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
2270
|
-
});
|
|
2271
|
-
provider.saveTokens(refreshed);
|
|
2272
|
-
return refreshed.access_token;
|
|
717
|
+
return stored.tokens.access_token;
|
|
2273
718
|
}
|
|
2274
719
|
|
|
2275
720
|
// src/version.ts
|
|
2276
721
|
var APP_VERSION = "0.1.3";
|
|
2277
722
|
|
|
2278
723
|
// src/core/update.ts
|
|
2279
|
-
import
|
|
2280
|
-
import
|
|
2281
|
-
import { execFileSync
|
|
724
|
+
import fs2 from "node:fs";
|
|
725
|
+
import path2 from "node:path";
|
|
726
|
+
import { execFileSync } from "node:child_process";
|
|
2282
727
|
function compareVersions(a, b) {
|
|
2283
728
|
const partsA = a.split(".").map(Number);
|
|
2284
729
|
const partsB = b.split(".").map(Number);
|
|
@@ -2294,7 +739,7 @@ function compareVersions(a, b) {
|
|
|
2294
739
|
}
|
|
2295
740
|
async function checkForUpdates() {
|
|
2296
741
|
try {
|
|
2297
|
-
const output =
|
|
742
|
+
const output = execFileSync("npm", ["view", "@kwonye/mcpx", "version"], {
|
|
2298
743
|
encoding: "utf8",
|
|
2299
744
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2300
745
|
}).trim();
|
|
@@ -2316,17 +761,17 @@ async function checkForUpdates() {
|
|
|
2316
761
|
}
|
|
2317
762
|
function getStagedUpdate() {
|
|
2318
763
|
const stagedPath = getStagedVersionPath();
|
|
2319
|
-
if (!
|
|
764
|
+
if (!fs2.existsSync(stagedPath)) {
|
|
2320
765
|
return null;
|
|
2321
766
|
}
|
|
2322
767
|
try {
|
|
2323
|
-
const raw =
|
|
768
|
+
const raw = fs2.readFileSync(stagedPath, "utf8");
|
|
2324
769
|
const info = JSON.parse(raw);
|
|
2325
770
|
if (!info.version || !info.cliPath) {
|
|
2326
771
|
return null;
|
|
2327
772
|
}
|
|
2328
|
-
const absoluteCliPath =
|
|
2329
|
-
if (!
|
|
773
|
+
const absoluteCliPath = path2.isAbsolute(info.cliPath) ? info.cliPath : path2.join(getUpdatesDir(), info.cliPath);
|
|
774
|
+
if (!fs2.existsSync(absoluteCliPath)) {
|
|
2330
775
|
return null;
|
|
2331
776
|
}
|
|
2332
777
|
return {
|
|
@@ -2342,18 +787,18 @@ function stageUpdate(version, cliPath) {
|
|
|
2342
787
|
ensureDir(updatesDir);
|
|
2343
788
|
const stagedInfo = {
|
|
2344
789
|
version,
|
|
2345
|
-
cliPath:
|
|
790
|
+
cliPath: path2.isAbsolute(cliPath) ? path2.relative(updatesDir, cliPath) : cliPath,
|
|
2346
791
|
stagedAt: new Date().toISOString()
|
|
2347
792
|
};
|
|
2348
|
-
|
|
793
|
+
fs2.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
2349
794
|
}
|
|
2350
795
|
function shouldUseStagedCli(stagedVersion, currentVersion) {
|
|
2351
796
|
return compareVersions(stagedVersion, currentVersion) > 0;
|
|
2352
797
|
}
|
|
2353
798
|
function clearStagedUpdate() {
|
|
2354
799
|
const stagedPath = getStagedVersionPath();
|
|
2355
|
-
if (
|
|
2356
|
-
|
|
800
|
+
if (fs2.existsSync(stagedPath)) {
|
|
801
|
+
fs2.unlinkSync(stagedPath);
|
|
2357
802
|
}
|
|
2358
803
|
}
|
|
2359
804
|
function getStagedCliPath() {
|
|
@@ -2362,31 +807,31 @@ function getStagedCliPath() {
|
|
|
2362
807
|
}
|
|
2363
808
|
function removeOldVersions(keepCount = 2) {
|
|
2364
809
|
const updatesDir = getUpdatesDir();
|
|
2365
|
-
if (!
|
|
810
|
+
if (!fs2.existsSync(updatesDir)) {
|
|
2366
811
|
return;
|
|
2367
812
|
}
|
|
2368
|
-
const entries =
|
|
813
|
+
const entries = fs2.readdirSync(updatesDir, { withFileTypes: true });
|
|
2369
814
|
const versionDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith("v")).map((entry) => entry.name).sort((a, b) => compareVersions(b.slice(1), a.slice(1)));
|
|
2370
815
|
if (versionDirs.length <= keepCount) {
|
|
2371
816
|
return;
|
|
2372
817
|
}
|
|
2373
818
|
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
2374
|
-
const oldPath =
|
|
819
|
+
const oldPath = path2.join(updatesDir, oldVersion);
|
|
2375
820
|
try {
|
|
2376
|
-
|
|
821
|
+
fs2.rmSync(oldPath, { recursive: true, force: true });
|
|
2377
822
|
} catch {}
|
|
2378
823
|
}
|
|
2379
824
|
}
|
|
2380
825
|
|
|
2381
826
|
// src/core/update-manager.ts
|
|
2382
|
-
import
|
|
2383
|
-
import
|
|
827
|
+
import fs3 from "node:fs";
|
|
828
|
+
import path3 from "node:path";
|
|
2384
829
|
import { execSync, spawn } from "node:child_process";
|
|
2385
830
|
function acquireLock() {
|
|
2386
831
|
const lockPath = getUpdateLockPath();
|
|
2387
832
|
try {
|
|
2388
|
-
ensureDir(
|
|
2389
|
-
|
|
833
|
+
ensureDir(path3.dirname(lockPath));
|
|
834
|
+
fs3.writeFileSync(lockPath, `${process.pid}
|
|
2390
835
|
`, { flag: "wx" });
|
|
2391
836
|
return true;
|
|
2392
837
|
} catch {
|
|
@@ -2396,25 +841,25 @@ function acquireLock() {
|
|
|
2396
841
|
function releaseLock() {
|
|
2397
842
|
const lockPath = getUpdateLockPath();
|
|
2398
843
|
try {
|
|
2399
|
-
|
|
844
|
+
fs3.unlinkSync(lockPath);
|
|
2400
845
|
} catch {}
|
|
2401
846
|
}
|
|
2402
847
|
function isUpdateInProgress() {
|
|
2403
848
|
const lockPath = getUpdateLockPath();
|
|
2404
|
-
if (!
|
|
849
|
+
if (!fs3.existsSync(lockPath)) {
|
|
2405
850
|
return false;
|
|
2406
851
|
}
|
|
2407
852
|
try {
|
|
2408
|
-
const raw =
|
|
853
|
+
const raw = fs3.readFileSync(lockPath, "utf8").trim();
|
|
2409
854
|
const pid = Number(raw);
|
|
2410
855
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2411
|
-
|
|
856
|
+
fs3.unlinkSync(lockPath);
|
|
2412
857
|
return false;
|
|
2413
858
|
}
|
|
2414
859
|
process.kill(pid, 0);
|
|
2415
860
|
return true;
|
|
2416
861
|
} catch {
|
|
2417
|
-
|
|
862
|
+
fs3.unlinkSync(lockPath);
|
|
2418
863
|
return false;
|
|
2419
864
|
}
|
|
2420
865
|
}
|
|
@@ -2425,8 +870,8 @@ async function downloadAndStageUpdate() {
|
|
|
2425
870
|
}
|
|
2426
871
|
const targetVersion = updateStatus.latestVersion;
|
|
2427
872
|
const updatesDir = getUpdatesDir();
|
|
2428
|
-
const versionDir =
|
|
2429
|
-
if (
|
|
873
|
+
const versionDir = path3.join(updatesDir, `v${targetVersion}`);
|
|
874
|
+
if (fs3.existsSync(versionDir)) {
|
|
2430
875
|
const existingStaged = getStagedUpdate();
|
|
2431
876
|
if (existingStaged?.version === targetVersion) {
|
|
2432
877
|
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
@@ -2438,8 +883,8 @@ async function downloadAndStageUpdate() {
|
|
|
2438
883
|
cwd: versionDir,
|
|
2439
884
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2440
885
|
});
|
|
2441
|
-
const tarballPath =
|
|
2442
|
-
if (!
|
|
886
|
+
const tarballPath = path3.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
887
|
+
if (!fs3.existsSync(tarballPath)) {
|
|
2443
888
|
throw new Error("Downloaded tarball not found");
|
|
2444
889
|
}
|
|
2445
890
|
execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
|
|
@@ -2450,8 +895,8 @@ async function downloadAndStageUpdate() {
|
|
|
2450
895
|
cwd: versionDir,
|
|
2451
896
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2452
897
|
});
|
|
2453
|
-
const cliPath =
|
|
2454
|
-
if (!
|
|
898
|
+
const cliPath = path3.join(versionDir, "dist", "cli.js");
|
|
899
|
+
if (!fs3.existsSync(cliPath)) {
|
|
2455
900
|
throw new Error("CLI entry point not found in downloaded package");
|
|
2456
901
|
}
|
|
2457
902
|
stageUpdate(targetVersion, cliPath);
|
|
@@ -2459,7 +904,7 @@ async function downloadAndStageUpdate() {
|
|
|
2459
904
|
return { success: true, version: targetVersion };
|
|
2460
905
|
} catch (error) {
|
|
2461
906
|
try {
|
|
2462
|
-
|
|
907
|
+
fs3.rmSync(versionDir, { recursive: true, force: true });
|
|
2463
908
|
} catch {}
|
|
2464
909
|
return {
|
|
2465
910
|
success: false,
|
|
@@ -2537,9 +982,9 @@ function performRollback() {
|
|
|
2537
982
|
}
|
|
2538
983
|
|
|
2539
984
|
// src/core/daemon.ts
|
|
2540
|
-
import
|
|
985
|
+
import fs4 from "node:fs";
|
|
2541
986
|
import net from "node:net";
|
|
2542
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
987
|
+
import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
|
|
2543
988
|
|
|
2544
989
|
// src/gateway/server.ts
|
|
2545
990
|
import http2 from "node:http";
|
|
@@ -2559,19 +1004,6 @@ var OAUTH_WELL_KNOWN_PREFIXES = [
|
|
|
2559
1004
|
"/.well-known/oauth-authorization-server",
|
|
2560
1005
|
"/.well-known/openid-configuration"
|
|
2561
1006
|
];
|
|
2562
|
-
|
|
2563
|
-
class UpstreamHttpError extends Error {
|
|
2564
|
-
status;
|
|
2565
|
-
wwwAuthenticate;
|
|
2566
|
-
bodyText;
|
|
2567
|
-
constructor(upstreamName, status, bodyText, wwwAuthenticate) {
|
|
2568
|
-
super(`Upstream ${upstreamName} returned HTTP ${status}: ${bodyText.slice(0, 400)}`);
|
|
2569
|
-
this.name = "UpstreamHttpError";
|
|
2570
|
-
this.status = status;
|
|
2571
|
-
this.bodyText = bodyText;
|
|
2572
|
-
this.wwwAuthenticate = wwwAuthenticate;
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
1007
|
function makeError(id, code, message, data) {
|
|
2576
1008
|
return {
|
|
2577
1009
|
jsonrpc: "2.0",
|
|
@@ -2730,8 +1162,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
2730
1162
|
const fingerprint = specFingerprint(upstream.spec);
|
|
2731
1163
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
2732
1164
|
if (cached?.fingerprint === fingerprint) {
|
|
2733
|
-
const
|
|
2734
|
-
results[upstream.name] =
|
|
1165
|
+
const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
|
|
1166
|
+
results[upstream.name] = runtimeErr2 ? { ...cached.count, runtimeError: runtimeErr2.message, runtimeErrorCode: runtimeErr2.code } : cached.count;
|
|
2735
1167
|
continue;
|
|
2736
1168
|
}
|
|
2737
1169
|
let toolsCount = 0;
|
|
@@ -2773,8 +1205,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
2773
1205
|
if (errors.length === 0) {
|
|
2774
1206
|
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
2775
1207
|
}
|
|
2776
|
-
const
|
|
2777
|
-
results[upstream.name] =
|
|
1208
|
+
const runtimeErr = runtime.upstreamErrors?.get(upstream.name);
|
|
1209
|
+
results[upstream.name] = runtimeErr ? { ...countObj, runtimeError: runtimeErr.message, runtimeErrorCode: runtimeErr.code } : countObj;
|
|
2778
1210
|
}
|
|
2779
1211
|
return results;
|
|
2780
1212
|
}
|
|
@@ -2943,11 +1375,8 @@ async function callUpstreamOnce(upstream, method, params, secrets, runtime, pass
|
|
|
2943
1375
|
if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
2944
1376
|
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
|
|
2945
1377
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
|
|
2949
|
-
}
|
|
2950
|
-
throw error;
|
|
1378
|
+
const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
|
|
1379
|
+
throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
|
|
2951
1380
|
}
|
|
2952
1381
|
}
|
|
2953
1382
|
function authHeaderIsValid(request, expectedToken) {
|
|
@@ -2966,7 +1395,7 @@ function authHeaderIsValid(request, expectedToken) {
|
|
|
2966
1395
|
return scheme === "Bearer" && token === expectedToken;
|
|
2967
1396
|
}
|
|
2968
1397
|
function isAuthChallenge(error) {
|
|
2969
|
-
return error instanceof
|
|
1398
|
+
return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
|
|
2970
1399
|
}
|
|
2971
1400
|
function getClientAuthorizationForUpstream(request, expectedToken) {
|
|
2972
1401
|
const authHeader = request.headers.authorization;
|
|
@@ -3007,7 +1436,8 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
3007
1436
|
if (upstreams.length === 1) {
|
|
3008
1437
|
throw error;
|
|
3009
1438
|
}
|
|
3010
|
-
|
|
1439
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1440
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
3011
1441
|
}
|
|
3012
1442
|
}
|
|
3013
1443
|
return { tools };
|
|
@@ -3042,7 +1472,8 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
3042
1472
|
if (upstreams.length === 1) {
|
|
3043
1473
|
throw error;
|
|
3044
1474
|
}
|
|
3045
|
-
|
|
1475
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1476
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
3046
1477
|
}
|
|
3047
1478
|
}
|
|
3048
1479
|
return { resources };
|
|
@@ -3075,7 +1506,8 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
3075
1506
|
if (upstreams.length === 1) {
|
|
3076
1507
|
throw error;
|
|
3077
1508
|
}
|
|
3078
|
-
|
|
1509
|
+
const code = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1510
|
+
runtime.upstreamErrors?.set(upstream.name, { code, message: error instanceof Error ? error.message : String(error) });
|
|
3079
1511
|
}
|
|
3080
1512
|
}
|
|
3081
1513
|
return { prompts };
|
|
@@ -3116,8 +1548,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
3116
1548
|
if (isAuthChallenge(error)) {
|
|
3117
1549
|
throw error;
|
|
3118
1550
|
}
|
|
3119
|
-
|
|
3120
|
-
|
|
1551
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1552
|
+
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1553
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
3121
1554
|
}
|
|
3122
1555
|
}
|
|
3123
1556
|
if (method === "prompts/get") {
|
|
@@ -3149,8 +1582,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
3149
1582
|
if (isAuthChallenge(error)) {
|
|
3150
1583
|
throw error;
|
|
3151
1584
|
}
|
|
3152
|
-
|
|
3153
|
-
|
|
1585
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1586
|
+
runtime.upstreamErrors?.set(upstream2.name, { code: errCode, message: error.message });
|
|
1587
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream2.name });
|
|
3154
1588
|
}
|
|
3155
1589
|
}
|
|
3156
1590
|
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
@@ -3181,8 +1615,9 @@ async function routeNamespacedCall(config, method, params, id, secrets, runtime,
|
|
|
3181
1615
|
if (isAuthChallenge(error)) {
|
|
3182
1616
|
throw error;
|
|
3183
1617
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
1618
|
+
const errCode = error instanceof UpstreamError ? error.code : "upstream_error";
|
|
1619
|
+
runtime.upstreamErrors?.set(upstream.name, { code: errCode, message: error.message });
|
|
1620
|
+
return makeError(id, -32000, error.message, { mcpxCode: errCode, upstream: upstream.name });
|
|
3186
1621
|
}
|
|
3187
1622
|
}
|
|
3188
1623
|
async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
|
|
@@ -3453,16 +1888,16 @@ function createGatewayServer(options) {
|
|
|
3453
1888
|
}
|
|
3454
1889
|
} catch (error) {
|
|
3455
1890
|
if (isAuthChallenge(error)) {
|
|
3456
|
-
response.statusCode = error.status;
|
|
1891
|
+
response.statusCode = error.status ?? 401;
|
|
3457
1892
|
response.setHeader("content-type", "application/json");
|
|
3458
1893
|
if (error.wwwAuthenticate) {
|
|
3459
1894
|
const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
|
|
3460
1895
|
response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
|
|
3461
1896
|
}
|
|
3462
1897
|
response.end(JSON.stringify(makeError(null, -32001, "Upstream authentication required.", {
|
|
1898
|
+
mcpxCode: error.code,
|
|
3463
1899
|
status: error.status,
|
|
3464
|
-
|
|
3465
|
-
wwwAuthenticate: error.wwwAuthenticate
|
|
1900
|
+
upstream: error.upstream
|
|
3466
1901
|
})));
|
|
3467
1902
|
if (debug) {
|
|
3468
1903
|
console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
|
|
@@ -3489,10 +1924,10 @@ function createGatewayServer(options) {
|
|
|
3489
1924
|
|
|
3490
1925
|
// src/core/daemon.ts
|
|
3491
1926
|
function readPidFromFile(pidPath = getPidPath()) {
|
|
3492
|
-
if (!
|
|
1927
|
+
if (!fs4.existsSync(pidPath)) {
|
|
3493
1928
|
return null;
|
|
3494
1929
|
}
|
|
3495
|
-
const raw =
|
|
1930
|
+
const raw = fs4.readFileSync(pidPath, "utf8").trim();
|
|
3496
1931
|
if (!raw) {
|
|
3497
1932
|
return null;
|
|
3498
1933
|
}
|
|
@@ -3522,7 +1957,7 @@ async function isPortAvailable(port) {
|
|
|
3522
1957
|
tester.listen(port, "127.0.0.1");
|
|
3523
1958
|
});
|
|
3524
1959
|
}
|
|
3525
|
-
async function resolveGatewayPort(config) {
|
|
1960
|
+
async function resolveGatewayPort(config, secrets) {
|
|
3526
1961
|
if (await isPortAvailable(config.gateway.port)) {
|
|
3527
1962
|
return config.gateway.port;
|
|
3528
1963
|
}
|
|
@@ -3532,8 +1967,16 @@ async function resolveGatewayPort(config) {
|
|
|
3532
1967
|
break;
|
|
3533
1968
|
}
|
|
3534
1969
|
if (await isPortAvailable(candidate)) {
|
|
1970
|
+
const oldPort = config.gateway.port;
|
|
3535
1971
|
config.gateway.port = candidate;
|
|
3536
1972
|
saveConfig(config);
|
|
1973
|
+
if (secrets) {
|
|
1974
|
+
try {
|
|
1975
|
+
const summary = syncAllClients(config, secrets);
|
|
1976
|
+
persistSyncState(summary, config);
|
|
1977
|
+
saveConfig(config);
|
|
1978
|
+
} catch {}
|
|
1979
|
+
}
|
|
3537
1980
|
return candidate;
|
|
3538
1981
|
}
|
|
3539
1982
|
}
|
|
@@ -3594,16 +2037,16 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3594
2037
|
};
|
|
3595
2038
|
}
|
|
3596
2039
|
try {
|
|
3597
|
-
|
|
2040
|
+
fs4.unlinkSync(getPidPath());
|
|
3598
2041
|
} catch {}
|
|
3599
2042
|
}
|
|
3600
|
-
const port = await resolveGatewayPort(config);
|
|
2043
|
+
const port = await resolveGatewayPort(config, secrets);
|
|
3601
2044
|
const token = ensureGatewayToken(config, secrets);
|
|
3602
2045
|
const pidPath = getPidPath();
|
|
3603
2046
|
const logPath = getLogPath();
|
|
3604
2047
|
ensureParentDir(pidPath);
|
|
3605
2048
|
ensureParentDir(logPath);
|
|
3606
|
-
const logFd =
|
|
2049
|
+
const logFd = fs4.openSync(logPath, "a", 384);
|
|
3607
2050
|
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
3608
2051
|
detached: true,
|
|
3609
2052
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -3613,7 +2056,7 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
3613
2056
|
}
|
|
3614
2057
|
});
|
|
3615
2058
|
child.unref();
|
|
3616
|
-
|
|
2059
|
+
fs4.writeFileSync(pidPath, `${child.pid}
|
|
3617
2060
|
`, { mode: 384 });
|
|
3618
2061
|
startBackgroundUpdateCheck();
|
|
3619
2062
|
await waitForGatewayReady(port, token);
|
|
@@ -3633,11 +2076,29 @@ function stopDaemon() {
|
|
|
3633
2076
|
message: "mcpx daemon is not running."
|
|
3634
2077
|
};
|
|
3635
2078
|
}
|
|
2079
|
+
try {
|
|
2080
|
+
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2081
|
+
if (!cmd.includes("mcpx") && !cmd.includes("daemon") && !cmd.includes("cli.js")) {
|
|
2082
|
+
fs4.unlinkSync(pidPath);
|
|
2083
|
+
return {
|
|
2084
|
+
stopped: false,
|
|
2085
|
+
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
} catch {
|
|
2089
|
+
try {
|
|
2090
|
+
fs4.unlinkSync(pidPath);
|
|
2091
|
+
} catch {}
|
|
2092
|
+
return {
|
|
2093
|
+
stopped: false,
|
|
2094
|
+
message: `PID ${pid} not found. Pidfile cleaned up.`
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
3636
2097
|
try {
|
|
3637
2098
|
process.kill(pid, "SIGTERM");
|
|
3638
2099
|
} catch {}
|
|
3639
2100
|
try {
|
|
3640
|
-
|
|
2101
|
+
fs4.unlinkSync(pidPath);
|
|
3641
2102
|
} catch {}
|
|
3642
2103
|
return {
|
|
3643
2104
|
stopped: true,
|
|
@@ -3650,10 +2111,10 @@ async function restartDaemon(config, cliPath, secrets) {
|
|
|
3650
2111
|
}
|
|
3651
2112
|
function readDaemonLogs(maxLines = 200) {
|
|
3652
2113
|
const logPath = getLogPath();
|
|
3653
|
-
if (!
|
|
2114
|
+
if (!fs4.existsSync(logPath)) {
|
|
3654
2115
|
return "";
|
|
3655
2116
|
}
|
|
3656
|
-
const lines =
|
|
2117
|
+
const lines = fs4.readFileSync(logPath, "utf8").split(`
|
|
3657
2118
|
`);
|
|
3658
2119
|
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
3659
2120
|
`);
|
|
@@ -3760,6 +2221,7 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
3760
2221
|
if (tokenCount?.error && buildAuthBindings(spec).length > 0 && tokenCount.total > 0) {
|
|
3761
2222
|
tokenCount = { ...tokenCount, error: undefined };
|
|
3762
2223
|
}
|
|
2224
|
+
const health = tokenCount ? tokenCount.error || tokenCount.runtimeError ? "error" : "ok" : "unknown";
|
|
3763
2225
|
const serverEntry = {
|
|
3764
2226
|
name,
|
|
3765
2227
|
enabled: isServerEnabled(spec),
|
|
@@ -3767,7 +2229,8 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
3767
2229
|
target: describeServerTarget(spec),
|
|
3768
2230
|
authBindings: buildAuthBindings(spec),
|
|
3769
2231
|
clients: buildClientMappings(config, managedIndex, name),
|
|
3770
|
-
tokenCount
|
|
2232
|
+
tokenCount,
|
|
2233
|
+
health
|
|
3771
2234
|
};
|
|
3772
2235
|
if (serverEntry.enabled && tokenCount) {
|
|
3773
2236
|
totalGlobalTokens += tokenCount.total;
|
|
@@ -3791,4 +2254,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
3791
2254
|
};
|
|
3792
2255
|
}
|
|
3793
2256
|
|
|
3794
|
-
export {
|
|
2257
|
+
export { probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, isOAuthReference, oauthReferenceServerName, runOAuthLogin, getOAuthAccessToken, APP_VERSION, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|