@kwonye/mcpx 0.1.85 → 0.1.88

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.
@@ -2,11 +2,11 @@ import {
2
2
  loadConfig,
3
3
  loadMergedConfig,
4
4
  saveConfig
5
- } from "./cli-bq7zsrv6.js";
5
+ } from "./cli-btx8jwra.js";
6
6
  import {
7
7
  SecretsManager,
8
8
  ensureGatewayToken
9
- } from "./cli-dvj5waat.js";
9
+ } from "./cli-qfvawh8e.js";
10
10
  import {
11
11
  ensureDir,
12
12
  ensureParentDir,
@@ -290,6 +290,7 @@ function mapRegistryToSpec(_name, option, resolvedValues) {
290
290
  }
291
291
 
292
292
  // src/adapters/claude.ts
293
+ import fs2 from "node:fs";
293
294
  import path2 from "node:path";
294
295
  import { z } from "zod";
295
296
 
@@ -352,6 +353,9 @@ function errorResult(clientId, configPath, message) {
352
353
  message
353
354
  };
354
355
  }
356
+ function managedGatewayEntryName(serverName) {
357
+ return `${serverName} (mcpx)`;
358
+ }
355
359
  function isManagedGatewayProjection(entryName) {
356
360
  return entryName.endsWith(" (mcpx)");
357
361
  }
@@ -419,6 +423,22 @@ function syncMcpExcludedArray(raw, managedNames, managedEntries) {
419
423
  }
420
424
 
421
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
+ }
422
442
  var stringMapSchema = z.record(z.string(), z.string());
423
443
  var claudeEntrySchema = z.object({
424
444
  type: z.string().optional(),
@@ -531,12 +551,26 @@ class ClaudeAdapter {
531
551
  }
532
552
  raw.mcpServers = topLevelServers;
533
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
+ }
534
558
  const projects = raw.projects ?? {};
535
559
  for (const projectPath of Object.keys(projects)) {
536
560
  const project = projects[projectPath] ?? {};
537
- syncDisabledMcpServersArray(project, managedNames, options.managedEntries);
561
+ const disabledForThis = scopeByNormalizedPath.get(normalizeProjectKey(projectPath)) ?? [];
562
+ syncProjectDisabledArray(project, managedNames, disabledForThis);
538
563
  projects[projectPath] = project;
539
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
+ }
540
574
  raw.projects = projects;
541
575
  writeJsonAtomic(configPath, raw);
542
576
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
@@ -680,7 +714,7 @@ class ClaudeDesktopAdapter {
680
714
 
681
715
  // src/adapters/codex.ts
682
716
  import path4 from "node:path";
683
- import fs2 from "node:fs";
717
+ import fs3 from "node:fs";
684
718
  import { parse, stringify } from "@iarna/toml";
685
719
  import { z as z3 } from "zod";
686
720
  var stringMapSchema3 = z3.preprocess((value) => {
@@ -711,10 +745,10 @@ class CodexAdapter {
711
745
  scanForImports(_config, managedIndex) {
712
746
  const configPath = this.detectConfigPath();
713
747
  const result = emptyImportScan(this.id, configPath ?? undefined);
714
- if (!configPath || !fs2.existsSync(configPath)) {
748
+ if (!configPath || !fs3.existsSync(configPath)) {
715
749
  return result;
716
750
  }
717
- const existing = fs2.readFileSync(configPath, "utf8");
751
+ const existing = fs3.readFileSync(configPath, "utf8");
718
752
  const doc = existing.trim().length > 0 ? parse(existing) : {};
719
753
  const mcpServers = doc.mcp_servers ?? {};
720
754
  for (const [name, rawEntry] of Object.entries(mcpServers)) {
@@ -769,7 +803,7 @@ class CodexAdapter {
769
803
  return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
770
804
  }
771
805
  try {
772
- const existing = fs2.existsSync(configPath) ? fs2.readFileSync(configPath, "utf8") : "";
806
+ const existing = fs3.existsSync(configPath) ? fs3.readFileSync(configPath, "utf8") : "";
773
807
  const doc = existing.trim().length > 0 ? parse(existing) : {};
774
808
  const mcpServers = doc.mcp_servers ?? {};
775
809
  removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
@@ -790,10 +824,10 @@ class CodexAdapter {
790
824
  mcpServers[name] = entry;
791
825
  }
792
826
  doc.mcp_servers = mcpServers;
793
- fs2.mkdirSync(path4.dirname(configPath), { recursive: true });
827
+ fs3.mkdirSync(path4.dirname(configPath), { recursive: true });
794
828
  const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
795
- fs2.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
796
- fs2.renameSync(tmpPath, configPath);
829
+ fs3.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
830
+ fs3.renameSync(tmpPath, configPath);
797
831
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
798
832
  return okResult(this.id, configPath);
799
833
  } catch (error) {
@@ -933,7 +967,7 @@ class CursorAdapter {
933
967
 
934
968
  // src/adapters/cline.ts
935
969
  import path6 from "node:path";
936
- import fs3 from "node:fs";
970
+ import fs4 from "node:fs";
937
971
  import { z as z5 } from "zod";
938
972
  var stringMapSchema5 = z5.record(z5.string(), z5.string());
939
973
  var clineEntrySchema = z5.object({
@@ -970,7 +1004,7 @@ class ClineAdapter {
970
1004
  detectConfigPath() {
971
1005
  const candidates = getCandidatePaths();
972
1006
  for (const candidate of candidates) {
973
- if (fs3.existsSync(candidate)) {
1007
+ if (fs4.existsSync(candidate)) {
974
1008
  return candidate;
975
1009
  }
976
1010
  }
@@ -982,7 +1016,7 @@ class ClineAdapter {
982
1016
  scanForImports(_config, managedIndex) {
983
1017
  const configPath = this.detectConfigPath();
984
1018
  const result = emptyImportScan(this.id, configPath ?? undefined);
985
- if (!configPath || !fs3.existsSync(configPath)) {
1019
+ if (!configPath || !fs4.existsSync(configPath)) {
986
1020
  return result;
987
1021
  }
988
1022
  const raw = readJsonFile(configPath, {});
@@ -1040,7 +1074,7 @@ class ClineAdapter {
1040
1074
  if (!configPath) {
1041
1075
  return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
1042
1076
  }
1043
- if (!fs3.existsSync(configPath)) {
1077
+ if (!fs4.existsSync(configPath)) {
1044
1078
  return okResult(this.id, configPath, "Cline config not found; skipping sync.");
1045
1079
  }
1046
1080
  try {
@@ -1080,7 +1114,7 @@ class ClineAdapter {
1080
1114
  }
1081
1115
 
1082
1116
  // src/adapters/kiro.ts
1083
- import fs4 from "node:fs";
1117
+ import fs5 from "node:fs";
1084
1118
  import path7 from "node:path";
1085
1119
  import { z as z6 } from "zod";
1086
1120
  var stringMapSchema6 = z6.record(z6.string(), z6.string());
@@ -1198,22 +1232,22 @@ class KiroAdapter {
1198
1232
  }
1199
1233
  syncSkills(skills) {
1200
1234
  const skillsDir = path7.join(homeDir(), ".kiro", "skills");
1201
- if (!fs4.existsSync(skillsDir)) {
1202
- fs4.mkdirSync(skillsDir, { recursive: true });
1235
+ if (!fs5.existsSync(skillsDir)) {
1236
+ fs5.mkdirSync(skillsDir, { recursive: true });
1203
1237
  }
1204
- const existingFiles = fs4.readdirSync(skillsDir);
1238
+ const existingFiles = fs5.readdirSync(skillsDir);
1205
1239
  const managedIds = new Set(skills.map((s) => s.id));
1206
1240
  for (const file of existingFiles) {
1207
1241
  if (file.endsWith(".md")) {
1208
1242
  const id = path7.basename(file, ".md");
1209
1243
  if (!managedIds.has(id)) {
1210
- fs4.unlinkSync(path7.join(skillsDir, file));
1244
+ fs5.unlinkSync(path7.join(skillsDir, file));
1211
1245
  }
1212
1246
  }
1213
1247
  }
1214
1248
  for (const skill of skills) {
1215
1249
  const targetPath = path7.join(skillsDir, `${skill.id}.md`);
1216
- fs4.writeFileSync(targetPath, skill.content, "utf-8");
1250
+ fs5.writeFileSync(targetPath, skill.content, "utf-8");
1217
1251
  }
1218
1252
  }
1219
1253
  }
@@ -1468,7 +1502,7 @@ class VsCodeAdapter {
1468
1502
  }
1469
1503
 
1470
1504
  // src/adapters/qwen.ts
1471
- import fs5 from "node:fs";
1505
+ import fs6 from "node:fs";
1472
1506
  import path10 from "node:path";
1473
1507
  import { z as z9 } from "zod";
1474
1508
  var stringMapSchema9 = z9.record(z9.string(), z9.string());
@@ -1583,22 +1617,22 @@ class QwenAdapter {
1583
1617
  }
1584
1618
  syncSkills(skills) {
1585
1619
  const skillsDir = path10.join(homeDir(), ".qwen", "skills");
1586
- if (!fs5.existsSync(skillsDir)) {
1587
- fs5.mkdirSync(skillsDir, { recursive: true });
1620
+ if (!fs6.existsSync(skillsDir)) {
1621
+ fs6.mkdirSync(skillsDir, { recursive: true });
1588
1622
  }
1589
- const existingFiles = fs5.readdirSync(skillsDir);
1623
+ const existingFiles = fs6.readdirSync(skillsDir);
1590
1624
  const managedIds = new Set(skills.map((s) => s.id));
1591
1625
  for (const file of existingFiles) {
1592
1626
  if (file.endsWith(".md")) {
1593
1627
  const id = path10.basename(file, ".md");
1594
1628
  if (!managedIds.has(id)) {
1595
- fs5.unlinkSync(path10.join(skillsDir, file));
1629
+ fs6.unlinkSync(path10.join(skillsDir, file));
1596
1630
  }
1597
1631
  }
1598
1632
  }
1599
1633
  for (const skill of skills) {
1600
1634
  const targetPath = path10.join(skillsDir, `${skill.id}.md`);
1601
- fs5.writeFileSync(targetPath, skill.content, "utf-8");
1635
+ fs6.writeFileSync(targetPath, skill.content, "utf-8");
1602
1636
  }
1603
1637
  }
1604
1638
  }
@@ -1643,6 +1677,13 @@ function saveManagedIndex(index, filePath = getManagedIndexPath()) {
1643
1677
  function getGatewayUrl(config) {
1644
1678
  return `http://127.0.0.1:${config.gateway.port}/mcp`;
1645
1679
  }
1680
+ function buildProjectScopes(config) {
1681
+ const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
1682
+ return Object.values(config.projects ?? {}).map((project) => {
1683
+ const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
1684
+ return { path: project.path, disabledServerNames };
1685
+ });
1686
+ }
1646
1687
  function buildManagedEntries(config, gatewayUrl, localToken) {
1647
1688
  const entries = Object.entries(config.servers);
1648
1689
  return entries.map(([name, spec]) => ({
@@ -1752,7 +1793,8 @@ function syncAllClients(config, secrets, targetClients) {
1752
1793
  managedEntries,
1753
1794
  managedIndex,
1754
1795
  managedIndexPath,
1755
- sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? [])
1796
+ sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
1797
+ projectScopes: buildProjectScopes(config)
1756
1798
  });
1757
1799
  results.push(result);
1758
1800
  }
@@ -1887,11 +1929,11 @@ function maybePrefixBearer(target, value, raw = false) {
1887
1929
  // src/core/spawn-env.ts
1888
1930
  import os from "node:os";
1889
1931
  import path11 from "node:path";
1890
- import fs6 from "node:fs";
1932
+ import fs7 from "node:fs";
1891
1933
  function existingNvmCurrentBin() {
1892
1934
  const nvmDir = process.env.NVM_DIR ?? path11.join(os.homedir(), ".nvm");
1893
1935
  const current = path11.join(nvmDir, "current", "bin");
1894
- return fs6.existsSync(current) ? current : null;
1936
+ return fs7.existsSync(current) ? current : null;
1895
1937
  }
1896
1938
  function buildEnrichedPath(current = process.env.PATH ?? "") {
1897
1939
  const candidates = [
@@ -2211,7 +2253,7 @@ async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
2211
2253
  var APP_VERSION = "0.1.3";
2212
2254
 
2213
2255
  // src/core/update.ts
2214
- import fs7 from "node:fs";
2256
+ import fs8 from "node:fs";
2215
2257
  import path12 from "node:path";
2216
2258
  import { execFileSync as execFileSync2 } from "node:child_process";
2217
2259
  function compareVersions(a, b) {
@@ -2251,17 +2293,17 @@ async function checkForUpdates() {
2251
2293
  }
2252
2294
  function getStagedUpdate() {
2253
2295
  const stagedPath = getStagedVersionPath();
2254
- if (!fs7.existsSync(stagedPath)) {
2296
+ if (!fs8.existsSync(stagedPath)) {
2255
2297
  return null;
2256
2298
  }
2257
2299
  try {
2258
- const raw = fs7.readFileSync(stagedPath, "utf8");
2300
+ const raw = fs8.readFileSync(stagedPath, "utf8");
2259
2301
  const info = JSON.parse(raw);
2260
2302
  if (!info.version || !info.cliPath) {
2261
2303
  return null;
2262
2304
  }
2263
2305
  const absoluteCliPath = path12.isAbsolute(info.cliPath) ? info.cliPath : path12.join(getUpdatesDir(), info.cliPath);
2264
- if (!fs7.existsSync(absoluteCliPath)) {
2306
+ if (!fs8.existsSync(absoluteCliPath)) {
2265
2307
  return null;
2266
2308
  }
2267
2309
  return {
@@ -2280,15 +2322,15 @@ function stageUpdate(version, cliPath) {
2280
2322
  cliPath: path12.isAbsolute(cliPath) ? path12.relative(updatesDir, cliPath) : cliPath,
2281
2323
  stagedAt: new Date().toISOString()
2282
2324
  };
2283
- fs7.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
2325
+ fs8.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
2284
2326
  }
2285
2327
  function shouldUseStagedCli(stagedVersion, currentVersion) {
2286
2328
  return compareVersions(stagedVersion, currentVersion) > 0;
2287
2329
  }
2288
2330
  function clearStagedUpdate() {
2289
2331
  const stagedPath = getStagedVersionPath();
2290
- if (fs7.existsSync(stagedPath)) {
2291
- fs7.unlinkSync(stagedPath);
2332
+ if (fs8.existsSync(stagedPath)) {
2333
+ fs8.unlinkSync(stagedPath);
2292
2334
  }
2293
2335
  }
2294
2336
  function getStagedCliPath() {
@@ -2297,10 +2339,10 @@ function getStagedCliPath() {
2297
2339
  }
2298
2340
  function removeOldVersions(keepCount = 2) {
2299
2341
  const updatesDir = getUpdatesDir();
2300
- if (!fs7.existsSync(updatesDir)) {
2342
+ if (!fs8.existsSync(updatesDir)) {
2301
2343
  return;
2302
2344
  }
2303
- const entries = fs7.readdirSync(updatesDir, { withFileTypes: true });
2345
+ const entries = fs8.readdirSync(updatesDir, { withFileTypes: true });
2304
2346
  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)));
2305
2347
  if (versionDirs.length <= keepCount) {
2306
2348
  return;
@@ -2308,20 +2350,20 @@ function removeOldVersions(keepCount = 2) {
2308
2350
  for (const oldVersion of versionDirs.slice(keepCount)) {
2309
2351
  const oldPath = path12.join(updatesDir, oldVersion);
2310
2352
  try {
2311
- fs7.rmSync(oldPath, { recursive: true, force: true });
2353
+ fs8.rmSync(oldPath, { recursive: true, force: true });
2312
2354
  } catch {}
2313
2355
  }
2314
2356
  }
2315
2357
 
2316
2358
  // src/core/update-manager.ts
2317
- import fs8 from "node:fs";
2359
+ import fs9 from "node:fs";
2318
2360
  import path13 from "node:path";
2319
2361
  import { execSync, spawn } from "node:child_process";
2320
2362
  function acquireLock() {
2321
2363
  const lockPath = getUpdateLockPath();
2322
2364
  try {
2323
2365
  ensureDir(path13.dirname(lockPath));
2324
- fs8.writeFileSync(lockPath, `${process.pid}
2366
+ fs9.writeFileSync(lockPath, `${process.pid}
2325
2367
  `, { flag: "wx" });
2326
2368
  return true;
2327
2369
  } catch {
@@ -2331,25 +2373,25 @@ function acquireLock() {
2331
2373
  function releaseLock() {
2332
2374
  const lockPath = getUpdateLockPath();
2333
2375
  try {
2334
- fs8.unlinkSync(lockPath);
2376
+ fs9.unlinkSync(lockPath);
2335
2377
  } catch {}
2336
2378
  }
2337
2379
  function isUpdateInProgress() {
2338
2380
  const lockPath = getUpdateLockPath();
2339
- if (!fs8.existsSync(lockPath)) {
2381
+ if (!fs9.existsSync(lockPath)) {
2340
2382
  return false;
2341
2383
  }
2342
2384
  try {
2343
- const raw = fs8.readFileSync(lockPath, "utf8").trim();
2385
+ const raw = fs9.readFileSync(lockPath, "utf8").trim();
2344
2386
  const pid = Number(raw);
2345
2387
  if (!Number.isFinite(pid) || pid <= 0) {
2346
- fs8.unlinkSync(lockPath);
2388
+ fs9.unlinkSync(lockPath);
2347
2389
  return false;
2348
2390
  }
2349
2391
  process.kill(pid, 0);
2350
2392
  return true;
2351
2393
  } catch {
2352
- fs8.unlinkSync(lockPath);
2394
+ fs9.unlinkSync(lockPath);
2353
2395
  return false;
2354
2396
  }
2355
2397
  }
@@ -2361,7 +2403,7 @@ async function downloadAndStageUpdate() {
2361
2403
  const targetVersion = updateStatus.latestVersion;
2362
2404
  const updatesDir = getUpdatesDir();
2363
2405
  const versionDir = path13.join(updatesDir, `v${targetVersion}`);
2364
- if (fs8.existsSync(versionDir)) {
2406
+ if (fs9.existsSync(versionDir)) {
2365
2407
  const existingStaged = getStagedUpdate();
2366
2408
  if (existingStaged?.version === targetVersion) {
2367
2409
  return { success: false, error: `Version ${targetVersion} already staged` };
@@ -2374,7 +2416,7 @@ async function downloadAndStageUpdate() {
2374
2416
  stdio: ["pipe", "pipe", "pipe"]
2375
2417
  });
2376
2418
  const tarballPath = path13.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
2377
- if (!fs8.existsSync(tarballPath)) {
2419
+ if (!fs9.existsSync(tarballPath)) {
2378
2420
  throw new Error("Downloaded tarball not found");
2379
2421
  }
2380
2422
  execSync(`tar -xzf ${tarballPath} --strip-components=1`, {
@@ -2386,7 +2428,7 @@ async function downloadAndStageUpdate() {
2386
2428
  stdio: ["pipe", "pipe", "pipe"]
2387
2429
  });
2388
2430
  const cliPath = path13.join(versionDir, "dist", "cli.js");
2389
- if (!fs8.existsSync(cliPath)) {
2431
+ if (!fs9.existsSync(cliPath)) {
2390
2432
  throw new Error("CLI entry point not found in downloaded package");
2391
2433
  }
2392
2434
  stageUpdate(targetVersion, cliPath);
@@ -2394,7 +2436,7 @@ async function downloadAndStageUpdate() {
2394
2436
  return { success: true, version: targetVersion };
2395
2437
  } catch (error) {
2396
2438
  try {
2397
- fs8.rmSync(versionDir, { recursive: true, force: true });
2439
+ fs9.rmSync(versionDir, { recursive: true, force: true });
2398
2440
  } catch {}
2399
2441
  return {
2400
2442
  success: false,
@@ -2472,7 +2514,7 @@ function performRollback() {
2472
2514
  }
2473
2515
 
2474
2516
  // src/core/daemon.ts
2475
- import fs9 from "node:fs";
2517
+ import fs10 from "node:fs";
2476
2518
  import net from "node:net";
2477
2519
  import { spawn as spawn2 } from "node:child_process";
2478
2520
 
@@ -2485,7 +2527,7 @@ import {
2485
2527
  StdioClientTransport,
2486
2528
  getDefaultEnvironment
2487
2529
  } from "@modelcontextprotocol/sdk/client/stdio.js";
2488
- var JSON_RPC_VERSION2 = "2.0";
2530
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2489
2531
  var SERVER_VERSION = APP_VERSION;
2490
2532
  var DEFAULT_UPSTREAM_TIMEOUT_MS = 60000;
2491
2533
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
@@ -2653,10 +2695,7 @@ function parseNamespacedUri(uri) {
2653
2695
  };
2654
2696
  }
2655
2697
  async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
2656
- if (upstream.spec.transport === "stdio") {
2657
- return callStdioUpstream(upstream, method, params, secrets, runtime);
2658
- }
2659
- return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
2698
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
2660
2699
  }
2661
2700
  async function getUpstreamTokenCounts(config, secrets, runtime) {
2662
2701
  if (!runtime.tokenCache) {
@@ -2749,75 +2788,110 @@ function buildStdioServerParameters(spec, secrets) {
2749
2788
  env: resolveStdioEnv(spec, secrets)
2750
2789
  };
2751
2790
  }
2752
- async function closeStdioConnection(entry) {
2791
+ async function buildHttpHeaders(upstream, secrets, options) {
2792
+ const headers = {};
2793
+ for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2794
+ if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2795
+ const oauthServerName = oauthReferenceServerName(value);
2796
+ const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, {
2797
+ forceRefresh: options.forceOAuthRefresh
2798
+ });
2799
+ headers[key] = `Bearer ${accessToken}`;
2800
+ } else {
2801
+ headers[key] = secrets.resolveMaybeSecret(value);
2802
+ }
2803
+ }
2804
+ if (options.passthroughAuthorizationHeader) {
2805
+ headers.Authorization = options.passthroughAuthorizationHeader;
2806
+ }
2807
+ return headers;
2808
+ }
2809
+ async function closeUpstreamConnection(entry) {
2753
2810
  try {
2754
2811
  const connection = await entry.promise;
2755
2812
  await connection.transport.close();
2756
2813
  } catch {}
2757
2814
  }
2758
- async function reconcileStdioConnections(config, runtime) {
2815
+ async function reconcileUpstreamConnections(config, runtime) {
2759
2816
  const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
2760
- const staleNames = [];
2761
- for (const [name, entry] of runtime.stdioConnections.entries()) {
2762
- const expectedFingerprint = activeSpecs.get(name);
2817
+ const staleKeys = [];
2818
+ for (const [key, entry] of runtime.upstreamConnections.entries()) {
2819
+ const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
2820
+ const expectedFingerprint = activeSpecs.get(upstreamName);
2763
2821
  if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
2764
- staleNames.push(name);
2822
+ staleKeys.push(key);
2765
2823
  }
2766
2824
  }
2767
- for (const name of staleNames) {
2768
- const entry = runtime.stdioConnections.get(name);
2825
+ for (const key of staleKeys) {
2826
+ const entry = runtime.upstreamConnections.get(key);
2769
2827
  if (!entry) {
2770
2828
  continue;
2771
2829
  }
2772
- runtime.stdioConnections.delete(name);
2773
- await closeStdioConnection(entry);
2830
+ runtime.upstreamConnections.delete(key);
2831
+ await closeUpstreamConnection(entry);
2774
2832
  }
2775
2833
  }
2776
- function invalidateStdioConnection(name, runtime) {
2777
- const existing = runtime.stdioConnections.get(name);
2834
+ function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
2835
+ const key = passthroughAuthorizationHeader ? `${upstreamName}:passthrough` : upstreamName;
2836
+ const existing = runtime.upstreamConnections.get(key);
2778
2837
  if (!existing) {
2779
2838
  return;
2780
2839
  }
2781
- runtime.stdioConnections.delete(name);
2782
- closeStdioConnection(existing);
2840
+ runtime.upstreamConnections.delete(key);
2841
+ closeUpstreamConnection(existing);
2783
2842
  }
2784
- async function getStdioConnection(upstream, secrets, runtime) {
2843
+ async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
2844
+ const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
2845
+ const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:passthrough` : upstream.name;
2785
2846
  const fingerprint = specFingerprint(upstream.spec);
2786
- const existing = runtime.stdioConnections.get(upstream.name);
2787
- if (existing) {
2788
- if (existing.fingerprint === fingerprint) {
2789
- return existing.promise;
2847
+ if (!forceOAuthRefresh) {
2848
+ const existing = runtime.upstreamConnections.get(cacheKey);
2849
+ if (existing) {
2850
+ if (existing.fingerprint === fingerprint) {
2851
+ return existing.promise;
2852
+ }
2853
+ runtime.upstreamConnections.delete(cacheKey);
2854
+ closeUpstreamConnection(existing);
2790
2855
  }
2791
- runtime.stdioConnections.delete(upstream.name);
2792
- closeStdioConnection(existing);
2793
2856
  }
2794
2857
  const promise = (async () => {
2795
- const transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2796
- const client = new Client({
2797
- name: "mcpx",
2798
- version: SERVER_VERSION
2799
- });
2858
+ let transport;
2859
+ if (upstream.spec.transport === "stdio") {
2860
+ transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
2861
+ } else {
2862
+ const headers = await buildHttpHeaders(upstream, secrets, { forceOAuthRefresh, passthroughAuthorizationHeader });
2863
+ const capturingFetch = async (url, init) => {
2864
+ const response = await fetch(url, init);
2865
+ const wwwAuth = response.headers.get("www-authenticate");
2866
+ if (wwwAuth) {
2867
+ if (!runtime.lastWwwAuthenticate) {
2868
+ runtime.lastWwwAuthenticate = new Map;
2869
+ }
2870
+ runtime.lastWwwAuthenticate.set(upstream.name, wwwAuth);
2871
+ }
2872
+ return response;
2873
+ };
2874
+ transport = new StreamableHTTPClientTransport(new URL2(upstream.spec.url), { requestInit: { headers }, fetch: capturingFetch });
2875
+ }
2876
+ const client = new Client({ name: "mcpx", version: SERVER_VERSION });
2800
2877
  await withTimeout(client.connect(transport), DEFAULT_CONNECT_TIMEOUT_MS, `Upstream ${upstream.name} failed to connect within ${DEFAULT_CONNECT_TIMEOUT_MS}ms.`);
2801
- return {
2802
- fingerprint,
2803
- client,
2804
- transport
2805
- };
2878
+ return { fingerprint, client, transport };
2806
2879
  })();
2807
- runtime.stdioConnections.set(upstream.name, {
2808
- fingerprint,
2809
- promise
2810
- });
2880
+ runtime.upstreamConnections.set(cacheKey, { fingerprint, promise });
2811
2881
  try {
2812
2882
  return await promise;
2813
2883
  } catch (error) {
2814
- runtime.stdioConnections.delete(upstream.name);
2884
+ runtime.upstreamConnections.delete(cacheKey);
2815
2885
  throw error;
2816
2886
  }
2817
2887
  }
2818
- async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2888
+ async function callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, forceOAuthRefresh) {
2889
+ const hasOAuth = upstream.spec.transport === "http" && Object.values(upstream.spec.headers ?? {}).some(isOAuthReference);
2819
2890
  try {
2820
- const connection = await getStdioConnection(upstream, secrets, runtime);
2891
+ const connection = await getUpstreamConnection(upstream, secrets, runtime, {
2892
+ forceOAuthRefresh,
2893
+ passthroughAuthorizationHeader
2894
+ });
2821
2895
  const timeoutMs = getConfiguredTimeoutMs();
2822
2896
  const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
2823
2897
  if (method === "tools/list") {
@@ -2838,161 +2912,17 @@ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
2838
2912
  if (method === "prompts/get") {
2839
2913
  return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
2840
2914
  }
2841
- throw new Error(`Unsupported stdio passthrough method: ${method}`);
2915
+ throw new Error(`Unsupported method: ${method}`);
2842
2916
  } catch (error) {
2843
- invalidateStdioConnection(upstream.name, runtime);
2844
- throw error;
2845
- }
2846
- }
2847
- async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
2848
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, false);
2849
- }
2850
- async function callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, forceOAuthRefresh) {
2851
- const headers = {
2852
- "content-type": "application/json",
2853
- accept: "application/json, text/event-stream"
2854
- };
2855
- for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
2856
- if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
2857
- const oauthServerName = oauthReferenceServerName(value);
2858
- const accessToken = await getOAuthAccessToken(oauthServerName, upstream.spec, secrets, { forceRefresh: forceOAuthRefresh });
2859
- headers[key] = `Bearer ${accessToken}`;
2860
- } else {
2861
- headers[key] = secrets.resolveMaybeSecret(value);
2917
+ invalidateUpstreamConnection(upstream.name, passthroughAuthorizationHeader, runtime);
2918
+ if (!forceOAuthRefresh && hasOAuth && error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
2919
+ return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, true);
2862
2920
  }
2863
- }
2864
- if (passthroughAuthorizationHeader) {
2865
- headers.Authorization = passthroughAuthorizationHeader;
2866
- }
2867
- const timeoutMs = getConfiguredTimeoutMs();
2868
- const timeoutController = new AbortController;
2869
- const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
2870
- let response;
2871
- try {
2872
- response = await fetch(upstream.spec.url, {
2873
- method: "POST",
2874
- headers,
2875
- body: JSON.stringify({
2876
- jsonrpc: JSON_RPC_VERSION2,
2877
- id,
2878
- method,
2879
- params
2880
- }),
2881
- signal: timeoutController.signal
2882
- });
2883
- } catch (error) {
2884
- const name = error.name;
2885
- if (name === "AbortError") {
2886
- throw new Error(`Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`);
2921
+ if (upstream.spec.transport === "http" && error instanceof StreamableHTTPError && error.code) {
2922
+ const wwwAuthenticate = runtime.lastWwwAuthenticate?.get(upstream.name);
2923
+ throw new UpstreamHttpError(upstream.name, error.code, error.message ?? "", wwwAuthenticate);
2887
2924
  }
2888
2925
  throw error;
2889
- } finally {
2890
- clearTimeout(timeoutHandle);
2891
- }
2892
- if (!response.ok) {
2893
- const responseText = await response.text();
2894
- if (!forceOAuthRefresh && (response.status === 401 || response.status === 403) && Object.values(upstream.spec.headers ?? {}).some((value) => isOAuthReference(value))) {
2895
- return callHttpUpstreamOnce(upstream, method, params, id, secrets, passthroughAuthorizationHeader, true);
2896
- }
2897
- throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
2898
- }
2899
- let payload = null;
2900
- const contentType = response.headers.get("content-type") ?? "";
2901
- if (contentType.includes("application/json")) {
2902
- payload = JSON.parse(await response.text());
2903
- } else if (contentType.includes("text/event-stream")) {
2904
- payload = await readSseJsonRpcResponse(response, id);
2905
- } else {
2906
- try {
2907
- payload = JSON.parse(await response.text());
2908
- } catch {
2909
- payload = null;
2910
- }
2911
- }
2912
- if (!payload) {
2913
- throw new Error(`Upstream ${upstream.name} response could not be parsed as JSON-RPC payload.`);
2914
- }
2915
- if (payload.error) {
2916
- throw new Error(`Upstream ${upstream.name} error: ${payload.error.message}`);
2917
- }
2918
- return payload.result;
2919
- }
2920
- async function readSseJsonRpcResponse(response, expectedId) {
2921
- if (!response.body) {
2922
- return null;
2923
- }
2924
- const decoder = new TextDecoder;
2925
- const reader = response.body.getReader();
2926
- let buffer = "";
2927
- let dataLines = [];
2928
- let latestPayload = null;
2929
- const consumeEvent = () => {
2930
- if (dataLines.length === 0) {
2931
- return null;
2932
- }
2933
- const combined = dataLines.join(`
2934
- `).trim();
2935
- dataLines = [];
2936
- if (!combined || combined === "[DONE]") {
2937
- return null;
2938
- }
2939
- try {
2940
- const parsed = JSON.parse(combined);
2941
- latestPayload = parsed;
2942
- const parsedId = parsed.id ?? null;
2943
- if (parsedId === expectedId) {
2944
- return parsed;
2945
- }
2946
- return null;
2947
- } catch {
2948
- return null;
2949
- }
2950
- };
2951
- try {
2952
- while (true) {
2953
- const { value, done } = await reader.read();
2954
- if (done) {
2955
- break;
2956
- }
2957
- buffer += decoder.decode(value, { stream: true });
2958
- while (true) {
2959
- const newlineIndex = buffer.indexOf(`
2960
- `);
2961
- if (newlineIndex < 0) {
2962
- break;
2963
- }
2964
- let line = buffer.slice(0, newlineIndex);
2965
- buffer = buffer.slice(newlineIndex + 1);
2966
- if (line.endsWith("\r")) {
2967
- line = line.slice(0, -1);
2968
- }
2969
- if (line.startsWith("data:")) {
2970
- dataLines.push(line.slice("data:".length).trimStart());
2971
- continue;
2972
- }
2973
- if (line === "") {
2974
- const matched = consumeEvent();
2975
- if (matched) {
2976
- return matched;
2977
- }
2978
- }
2979
- }
2980
- }
2981
- buffer += decoder.decode();
2982
- if (buffer.length > 0) {
2983
- let finalLine = buffer;
2984
- if (finalLine.endsWith("\r")) {
2985
- finalLine = finalLine.slice(0, -1);
2986
- }
2987
- if (finalLine.startsWith("data:")) {
2988
- dataLines.push(finalLine.slice("data:".length).trimStart());
2989
- }
2990
- }
2991
- return consumeEvent() ?? latestPayload;
2992
- } finally {
2993
- try {
2994
- await reader.cancel();
2995
- } catch {}
2996
2926
  }
2997
2927
  }
2998
2928
  function authHeaderIsValid(request, expectedToken) {
@@ -3255,7 +3185,7 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
3255
3185
  if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
3256
3186
  return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
3257
3187
  }
3258
- await reconcileStdioConnections(config, runtime);
3188
+ await reconcileUpstreamConnections(config, runtime);
3259
3189
  if (request.method === "custom/tokenCounts") {
3260
3190
  const result = await getUpstreamTokenCounts(config, secrets, runtime);
3261
3191
  return makeResult(id, result);
@@ -3351,7 +3281,7 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
3351
3281
  function createGatewayServer(options) {
3352
3282
  const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
3353
3283
  const runtime = {
3354
- stdioConnections: new Map,
3284
+ upstreamConnections: new Map,
3355
3285
  upstreamErrors: new Map
3356
3286
  };
3357
3287
  const server = http2.createServer(async (request, response) => {
@@ -3517,10 +3447,10 @@ function createGatewayServer(options) {
3517
3447
  }
3518
3448
  });
3519
3449
  server.on("close", () => {
3520
- for (const entry of runtime.stdioConnections.values()) {
3521
- closeStdioConnection(entry);
3450
+ for (const entry of runtime.upstreamConnections.values()) {
3451
+ closeUpstreamConnection(entry);
3522
3452
  }
3523
- runtime.stdioConnections.clear();
3453
+ runtime.upstreamConnections.clear();
3524
3454
  });
3525
3455
  server.listen(options.port, "127.0.0.1");
3526
3456
  return server;
@@ -3528,10 +3458,10 @@ function createGatewayServer(options) {
3528
3458
 
3529
3459
  // src/core/daemon.ts
3530
3460
  function readPidFromFile(pidPath = getPidPath()) {
3531
- if (!fs9.existsSync(pidPath)) {
3461
+ if (!fs10.existsSync(pidPath)) {
3532
3462
  return null;
3533
3463
  }
3534
- const raw = fs9.readFileSync(pidPath, "utf8").trim();
3464
+ const raw = fs10.readFileSync(pidPath, "utf8").trim();
3535
3465
  if (!raw) {
3536
3466
  return null;
3537
3467
  }
@@ -3633,7 +3563,7 @@ async function startDaemon(config, cliPath, secrets) {
3633
3563
  };
3634
3564
  }
3635
3565
  try {
3636
- fs9.unlinkSync(getPidPath());
3566
+ fs10.unlinkSync(getPidPath());
3637
3567
  } catch {}
3638
3568
  }
3639
3569
  const port = await resolveGatewayPort(config);
@@ -3642,7 +3572,7 @@ async function startDaemon(config, cliPath, secrets) {
3642
3572
  const logPath = getLogPath();
3643
3573
  ensureParentDir(pidPath);
3644
3574
  ensureParentDir(logPath);
3645
- const logFd = fs9.openSync(logPath, "a", 384);
3575
+ const logFd = fs10.openSync(logPath, "a", 384);
3646
3576
  const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
3647
3577
  detached: true,
3648
3578
  stdio: ["ignore", logFd, logFd],
@@ -3652,7 +3582,7 @@ async function startDaemon(config, cliPath, secrets) {
3652
3582
  }
3653
3583
  });
3654
3584
  child.unref();
3655
- fs9.writeFileSync(pidPath, `${child.pid}
3585
+ fs10.writeFileSync(pidPath, `${child.pid}
3656
3586
  `, { mode: 384 });
3657
3587
  startBackgroundUpdateCheck();
3658
3588
  await waitForGatewayReady(port, token);
@@ -3676,7 +3606,7 @@ function stopDaemon() {
3676
3606
  process.kill(pid, "SIGTERM");
3677
3607
  } catch {}
3678
3608
  try {
3679
- fs9.unlinkSync(pidPath);
3609
+ fs10.unlinkSync(pidPath);
3680
3610
  } catch {}
3681
3611
  return {
3682
3612
  stopped: true,
@@ -3689,10 +3619,10 @@ async function restartDaemon(config, cliPath, secrets) {
3689
3619
  }
3690
3620
  function readDaemonLogs(maxLines = 200) {
3691
3621
  const logPath = getLogPath();
3692
- if (!fs9.existsSync(logPath)) {
3622
+ if (!fs10.existsSync(logPath)) {
3693
3623
  return "";
3694
3624
  }
3695
- const lines = fs9.readFileSync(logPath, "utf8").split(`
3625
+ const lines = fs10.readFileSync(logPath, "utf8").split(`
3696
3626
  `);
3697
3627
  return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
3698
3628
  `);
@@ -3809,11 +3739,11 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
3809
3739
  tokenCount
3810
3740
  };
3811
3741
  if (serverEntry.enabled && tokenCount) {
3812
- const matchingProject = projectEntries.find((p) => name.startsWith(`${p.name}.`));
3813
- if (matchingProject) {
3814
- totalProjectTokens[matchingProject.path] = (totalProjectTokens[matchingProject.path] ?? 0) + tokenCount.total;
3815
- } else {
3816
- totalGlobalTokens += tokenCount.total;
3742
+ totalGlobalTokens += tokenCount.total;
3743
+ for (const projectEntry of projectEntries) {
3744
+ if (!(projectEntry.disabledServers ?? []).includes(name)) {
3745
+ totalProjectTokens[projectEntry.path] = (totalProjectTokens[projectEntry.path] ?? 0) + tokenCount.total;
3746
+ }
3817
3747
  }
3818
3748
  }
3819
3749
  return serverEntry;