@kwonye/mcpx 0.1.75 → 0.1.77

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/cli.js CHANGED
@@ -2,54 +2,71 @@
2
2
  import {
3
3
  APP_VERSION,
4
4
  STATUS_CLIENTS,
5
- SecretsManager,
6
- addServer,
7
5
  applyAuthReference,
8
6
  buildStatusReport,
9
7
  checkForUpdates,
10
8
  defaultAuthSecretName,
11
9
  deleteSkill,
12
- ensureGatewayToken,
10
+ extractRequiredInputs,
11
+ fetchRegistryServerDetail,
13
12
  getAdapters,
14
- getConfigPath,
15
13
  getDaemonStatus,
16
14
  getGatewayUrl,
17
- getManagedIndexPath,
18
15
  getSkill,
19
16
  getStagedCliPath,
20
17
  getStagedUpdate,
21
18
  listAuthBindings,
22
19
  listSkills,
23
- loadConfig,
24
20
  loadManagedIndex,
21
+ mapRegistryToSpec,
25
22
  maybePrefixBearer,
26
23
  performRollback,
27
24
  performUpdate,
28
25
  probeHttpAuthRequirement,
29
26
  readDaemonLogs,
30
- readSecretValueFromStdin,
31
27
  removeAuthReference,
32
- removeServer,
33
28
  resolveAuthTarget,
34
29
  restartDaemon,
35
30
  runBackgroundUpdate,
36
31
  runDaemonForeground,
37
- saveConfig,
38
32
  saveSkill,
39
33
  secretRefName,
40
- setServerEnabled,
34
+ selectBestPackage,
41
35
  startDaemon,
42
36
  stopDaemon,
43
37
  syncAllClients,
44
38
  toSecretRef
45
- } from "./cli-fh6jh7fs.js";
39
+ } from "./cli-f16kphtq.js";
46
40
  import {
41
+ loadConfig,
42
+ loadMergedConfig,
43
+ loadProjectConfig,
44
+ resolveActiveConfig,
45
+ saveConfig,
46
+ saveProjectConfig
47
+ } from "./cli-7bsf6akp.js";
48
+ import {
49
+ SecretsManager,
50
+ addServer,
51
+ ensureGatewayToken,
52
+ readSecretValueFromStdin,
53
+ registerProject,
54
+ removeServer,
55
+ setServerEnabled
56
+ } from "./cli-s4py8ckf.js";
57
+ import {
58
+ getConfigPath,
59
+ getManagedIndexPath
60
+ } from "./cli-fsf33sgp.js";
61
+ import {
62
+ __require,
47
63
  isServerEnabled
48
- } from "./cli-q8de7zb0.js";
64
+ } from "./cli-s4gspqdf.js";
49
65
 
50
66
  // src/cli.ts
51
67
  import { Command } from "commander";
52
68
  import fs from "node:fs";
69
+ import path from "node:path";
53
70
  import { execFileSync } from "node:child_process";
54
71
  import { pathToFileURL } from "node:url";
55
72
  import { emitKeypressEvents } from "node:readline";
@@ -880,6 +897,7 @@ async function runStdioProxy(serverName) {
880
897
  }
881
898
 
882
899
  // src/cli.ts
900
+ var __dirname = "/home/runner/work/mcpx/mcpx/cli/src";
883
901
  var VALID_CLIENTS = STATUS_CLIENTS;
884
902
  function parseKeyValueFlag(value, label) {
885
903
  const split = value.indexOf("=");
@@ -1049,7 +1067,8 @@ async function ensureDaemonIfEnabled(cliPath, secrets) {
1049
1067
  async function autoSyncManagedEntries(config, cliPath) {
1050
1068
  const secrets = new SecretsManager;
1051
1069
  await ensureDaemonIfEnabled(cliPath, secrets);
1052
- const summary = syncAllClients(config, secrets);
1070
+ const mergedConfig = loadMergedConfig();
1071
+ const summary = syncAllClients(mergedConfig, secrets);
1053
1072
  printSyncSummary(summary, false);
1054
1073
  ensureExitCodeForSyncFailures(summary.hasErrors);
1055
1074
  }
@@ -1058,9 +1077,13 @@ function isNegativeChoice(value) {
1058
1077
  return normalized === "n" || normalized === "no";
1059
1078
  }
1060
1079
  async function maybeAutoConfigureAuthForAddedServer(serverName, spec, secrets) {
1061
- if (spec.transport !== "http") {
1062
- return;
1080
+ if (spec.transport === "http") {
1081
+ await maybeConfigureHttpAuth(serverName, spec, secrets);
1082
+ } else {
1083
+ await maybeConfigureStdioAuth(serverName, spec, secrets);
1063
1084
  }
1085
+ }
1086
+ async function maybeConfigureHttpAuth(serverName, spec, secrets) {
1064
1087
  const probe = await probeHttpAuthRequirement(spec, secrets);
1065
1088
  if (!probe.authRequired) {
1066
1089
  return;
@@ -1133,9 +1156,48 @@ async function maybeAutoConfigureAuthForAddedServer(serverName, spec, secrets) {
1133
1156
  `);
1134
1157
  }
1135
1158
  }
1136
- function loadStatusReport() {
1137
- const config = loadConfig();
1138
- return buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
1159
+ async function maybeConfigureStdioAuth(serverName, spec, secrets) {
1160
+ const binds = listAuthBindings(spec);
1161
+ const plainEnvVars = binds.filter((b) => b.kind === "env" && !secretRefName(b.value));
1162
+ if (plainEnvVars.length === 0) {
1163
+ return;
1164
+ }
1165
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1166
+ process.stdout.write(`Server "${serverName}" has plain-text env vars. Run \`mcpx auth set ${serverName} --env KEY=VALUE --value "<token>"\` to store them securely.
1167
+ `);
1168
+ return;
1169
+ }
1170
+ process.stdout.write(`Server "${serverName}" has ${plainEnvVars.length} env var(s) stored in plain text.
1171
+ `);
1172
+ const rl = createInterface({
1173
+ input: process.stdin,
1174
+ output: process.stdout
1175
+ });
1176
+ try {
1177
+ const shouldMigrate = await promptLine(rl, "Store them in the system keychain instead? (Y/n): ");
1178
+ if (isNegativeChoice(shouldMigrate)) {
1179
+ process.stdout.write(`Skipping keychain migration.
1180
+ `);
1181
+ return;
1182
+ }
1183
+ for (const binding of plainEnvVars) {
1184
+ const target = { kind: "env", key: binding.key };
1185
+ const secretName = defaultAuthSecretName(serverName, target);
1186
+ secrets.setSecret(secretName, binding.value);
1187
+ applyAuthReference(spec, target, toSecretRef(secretName));
1188
+ process.stdout.write(`Stored ${binding.key} as secret://${secretName}
1189
+ `);
1190
+ }
1191
+ } catch (error) {
1192
+ process.stdout.write(`Keychain migration skipped: ${error.message}
1193
+ `);
1194
+ } finally {
1195
+ rl.close();
1196
+ }
1197
+ }
1198
+ async function loadStatusReport() {
1199
+ const config = loadMergedConfig();
1200
+ return await buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
1139
1201
  }
1140
1202
  function daemonEmoji(running) {
1141
1203
  return running ? "✅" : "⚠️";
@@ -1192,6 +1254,16 @@ function printSyncedConfigBullets(server, indent) {
1192
1254
  `);
1193
1255
  }
1194
1256
  }
1257
+ function formatTokenApprox(tokens) {
1258
+ if (tokens === 0)
1259
+ return "0";
1260
+ if (tokens > 0 && tokens < 500)
1261
+ return "<1k";
1262
+ const thousands = Math.round(tokens / 1000);
1263
+ if (thousands === 0)
1264
+ return "<1k";
1265
+ return `~${thousands}k`;
1266
+ }
1195
1267
  function printStatusReportText(report) {
1196
1268
  process.stdout.write(`Gateway URL: ${report.gatewayUrl}
1197
1269
  `);
@@ -1199,12 +1271,17 @@ function printStatusReportText(report) {
1199
1271
  `);
1200
1272
  process.stdout.write(`Upstream servers: ${report.upstreamCount}
1201
1273
  `);
1274
+ if (report.daemon.running) {
1275
+ process.stdout.write(`Global active tokens: ${formatTokenApprox(report.totalGlobalTokens ?? 0)}
1276
+ `);
1277
+ }
1202
1278
  if (report.servers.length === 0) {
1203
1279
  process.stdout.write(`⚠️ No upstream servers configured.
1204
1280
  `);
1205
1281
  } else {
1206
1282
  for (const server of report.servers) {
1207
- process.stdout.write(`- ${serverHealthEmoji(server)} ${server.name} (${server.transport})
1283
+ const tokenSuffix = server.enabled && server.tokenCount ? ` [tokens: ${formatTokenApprox(server.tokenCount.total)} (tools: ${server.tokenCount.tools}, resources: ${server.tokenCount.resources}, prompts: ${server.tokenCount.prompts})]` : "";
1284
+ process.stdout.write(`- ${serverHealthEmoji(server)} ${server.name} (${server.transport})${tokenSuffix}
1208
1285
  `);
1209
1286
  process.stdout.write(` state: ${server.enabled ? "enabled" : "disabled"}
1210
1287
  `);
@@ -1219,6 +1296,15 @@ function printStatusReportText(report) {
1219
1296
  printSyncedConfigBullets(server, " ");
1220
1297
  }
1221
1298
  }
1299
+ if (report.projects && Object.keys(report.projects).length > 0 && report.daemon.running) {
1300
+ process.stdout.write(`Project active tokens:
1301
+ `);
1302
+ for (const project of Object.values(report.projects)) {
1303
+ const projectTokens = report.totalProjectTokens?.[project.path] ?? 0;
1304
+ process.stdout.write(`- ${project.name}: ${formatTokenApprox(projectTokens)} tokens (${project.path})
1305
+ `);
1306
+ }
1307
+ }
1222
1308
  process.stdout.write(`Client sync states:
1223
1309
  `);
1224
1310
  for (const client of STATUS_CLIENTS) {
@@ -1486,6 +1572,31 @@ async function setServerEnabledInteractively(rl, serverName, enabled, cliPath) {
1486
1572
  `);
1487
1573
  return false;
1488
1574
  }
1575
+ if (serverName.includes(".")) {
1576
+ const parts = serverName.split(".");
1577
+ const projectName = parts[0];
1578
+ const baseServerName = parts.slice(1).join(".");
1579
+ const globalConfigForProjects = loadConfig();
1580
+ if (globalConfigForProjects.projects) {
1581
+ const projectEntry = Object.values(globalConfigForProjects.projects).find((p) => p.name === projectName);
1582
+ if (projectEntry) {
1583
+ const localPath = path.join(projectEntry.path, ".mcpx.json");
1584
+ if (fs.existsSync(localPath)) {
1585
+ const projectConfig = loadProjectConfig(localPath);
1586
+ if (projectConfig.servers[baseServerName]) {
1587
+ setServerEnabled(projectConfig, baseServerName, enabled);
1588
+ saveProjectConfig(projectConfig, localPath);
1589
+ process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${serverName}
1590
+ `);
1591
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1592
+ `);
1593
+ await autoSyncManagedEntries(loadMergedConfig(), cliPath);
1594
+ return true;
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1489
1600
  const config = loadConfig();
1490
1601
  setServerEnabled(config, serverName, enabled);
1491
1602
  saveConfig(config);
@@ -1493,7 +1604,7 @@ async function setServerEnabledInteractively(rl, serverName, enabled, cliPath) {
1493
1604
  `);
1494
1605
  process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1495
1606
  `);
1496
- await autoSyncManagedEntries(config, cliPath);
1607
+ await autoSyncManagedEntries(loadMergedConfig(), cliPath);
1497
1608
  return true;
1498
1609
  }
1499
1610
  function buildServerActionTitle(server) {
@@ -1530,7 +1641,7 @@ function buildServerMenuDetail(server) {
1530
1641
  }
1531
1642
  async function runServerActionsMenu(rl, serverName, cliPath) {
1532
1643
  while (true) {
1533
- const report = loadStatusReport();
1644
+ const report = await loadStatusReport();
1534
1645
  const server = report.servers.find((entry) => entry.name === serverName);
1535
1646
  if (!server) {
1536
1647
  process.stdout.write(`Server "${serverName}" no longer exists.
@@ -1577,7 +1688,7 @@ async function runInteractiveStatusMenu(cliPath) {
1577
1688
  });
1578
1689
  try {
1579
1690
  while (true) {
1580
- const report = loadStatusReport();
1691
+ const report = await loadStatusReport();
1581
1692
  if (report.servers.length === 0) {
1582
1693
  clearTerminalScreen();
1583
1694
  process.stdout.write(`mcpx status
@@ -1614,47 +1725,170 @@ async function runInteractiveStatusMenu(cliPath) {
1614
1725
  }
1615
1726
  }
1616
1727
  function registerEnabledCommand(parent, enabled, cliPath) {
1617
- parent.command(enabled ? "enable <name>" : "disable <name>").description(`${enabled ? "Enable" : "Disable"} an upstream MCP server`).action(async (name) => {
1618
- const config = loadConfig();
1619
- setServerEnabled(config, name, enabled);
1620
- saveConfig(config);
1621
- process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${name}
1728
+ parent.command(enabled ? "enable <name>" : "disable <name>").option("-g, --global", "Force target the global configuration").option("-l, --local", "Force target (or initialize) a local `.mcpx.json` configuration").description(`${enabled ? "Enable" : "Disable"} an upstream MCP server`).action(async (name, options) => {
1729
+ const context = resolveActiveConfig({ global: options.global, local: options.local });
1730
+ setServerEnabled(context.config, name, enabled);
1731
+ context.save();
1732
+ if (context.type === "project" && context.projectPath) {
1733
+ const globalConfig = loadConfig();
1734
+ registerProject(globalConfig, context.projectPath);
1735
+ saveConfig(globalConfig);
1736
+ }
1737
+ process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${name} (${context.type} config)
1622
1738
  `);
1623
1739
  process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1624
1740
  `);
1625
- await autoSyncManagedEntries(config, cliPath);
1741
+ const mergedConfig = loadMergedConfig();
1742
+ await autoSyncManagedEntries(mergedConfig, cliPath);
1626
1743
  });
1627
1744
  }
1628
1745
  function registerAddCommand(parent, cliPath) {
1629
- parent.command("add [values...]").allowUnknownOption(true).option("--transport <transport>", "MCP transport (auto|http|stdio)", "auto").option("--header <header>", "Header entry in KEY=VALUE form", (value, previous = []) => [...previous, value], []).option("--env <env>", "Env entry in KEY=VALUE form for stdio upstreams", (value, previous = []) => [...previous, value], []).option("--cwd <cwd>", "Working directory for stdio upstream").option("--force", "Overwrite existing server with same name").description("Add an upstream MCP server").action(async (values, options) => {
1630
- const config = loadConfig();
1631
- const parsed = parseAddServerSpec(values ?? [], options);
1632
- addServer(config, parsed.name, parsed.spec, options.force ?? false);
1633
- await maybeAutoConfigureAuthForAddedServer(parsed.name, parsed.spec, new SecretsManager);
1634
- saveConfig(config);
1635
- process.stdout.write(`Added server: ${parsed.name} (${parsed.spec.transport})
1746
+ parent.command("add [values...]").allowUnknownOption(true).option("--transport <transport>", "MCP transport (auto|http|stdio)", "auto").option("--header <header>", "Header entry in KEY=VALUE form", (value, previous = []) => [...previous, value], []).option("--env <env>", "Env entry in KEY=VALUE form for stdio upstreams", (value, previous = []) => [...previous, value], []).option("--cwd <cwd>", "Working directory for stdio upstream").option("--force", "Overwrite existing server with same name").option("-g, --global", "Force saving to global configuration").option("-l, --local", "Force saving to local project configuration").description("Add an upstream MCP server").action(async (values, options) => {
1747
+ const context = resolveActiveConfig({ global: options.global, local: options.local });
1748
+ const safeValues = values ?? [];
1749
+ const secrets = new SecretsManager;
1750
+ if (safeValues.length === 1 && safeValues[0]?.includes("/")) {
1751
+ const registryName = safeValues[0];
1752
+ const shortName = registryName.split("/").pop() ?? registryName;
1753
+ process.stdout.write(`Fetching "${registryName}" from registry...
1754
+ `);
1755
+ const detail = await fetchRegistryServerDetail(registryName);
1756
+ const option = selectBestPackage(detail.packages, detail.remotes);
1757
+ const requiredInputs = extractRequiredInputs(option);
1758
+ const resolvedValues = {};
1759
+ if (requiredInputs.length > 0) {
1760
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1761
+ throw new Error(`Server "${registryName}" requires inputs: ${requiredInputs.map((i) => `${i.name} (${i.kind})`).join(", ")}. Run in an interactive terminal.`);
1762
+ }
1763
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1764
+ try {
1765
+ for (const input of requiredInputs) {
1766
+ const promptText = input.description ? `${input.name} (${input.kind}): ${input.description}
1767
+ Value: ` : `${input.name} (${input.kind}): `;
1768
+ const value = await promptLine(rl, promptText);
1769
+ if (!value) {
1770
+ process.stdout.write(`Skipping ${input.name} (empty value).
1771
+ `);
1772
+ continue;
1773
+ }
1774
+ resolvedValues[input.name] = value;
1775
+ }
1776
+ } finally {
1777
+ rl.close();
1778
+ }
1779
+ } else {
1780
+ process.stdout.write(`No required inputs.
1781
+ `);
1782
+ }
1783
+ let spec = mapRegistryToSpec(shortName, option, resolvedValues);
1784
+ for (const input of requiredInputs) {
1785
+ if (!input.isSecret)
1786
+ continue;
1787
+ const rawValue = resolvedValues[input.name];
1788
+ if (!rawValue)
1789
+ continue;
1790
+ const secretName = `auth_${shortName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${input.kind}_${input.name.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}`;
1791
+ secrets.setSecret(secretName, rawValue);
1792
+ if (input.kind === "header" && spec.transport === "http") {
1793
+ const headers = { ...spec.headers ?? {} };
1794
+ headers[input.name] = `secret://${secretName}`;
1795
+ spec.headers = Object.keys(headers).length > 0 ? headers : undefined;
1796
+ } else if (input.kind === "env" && spec.transport === "stdio") {
1797
+ const env = { ...spec.env ?? {} };
1798
+ env[input.name] = `secret://${secretName}`;
1799
+ spec.env = Object.keys(env).length > 0 ? env : undefined;
1800
+ }
1801
+ }
1802
+ let resolvedServerName = shortName;
1803
+ if (context.type === "project") {
1804
+ const globalConfig = loadConfig();
1805
+ registerProject(globalConfig, context.projectPath, context.config.name);
1806
+ saveConfig(globalConfig);
1807
+ const projectName = context.config.name || path.basename(context.projectPath);
1808
+ resolvedServerName = `${projectName}.${shortName}`;
1809
+ }
1810
+ addServer(context.config, shortName, spec, options.force ?? false);
1811
+ context.save();
1812
+ process.stdout.write(`Added server: ${shortName} (${spec.transport}) from registry "${registryName}" ${context.type === "project" ? `to project: ${context.projectPath}` : "globally"}
1813
+ `);
1814
+ } else {
1815
+ const parsed = parseAddServerSpec(safeValues, options);
1816
+ let resolvedServerName = parsed.name;
1817
+ if (context.type === "project") {
1818
+ const globalConfig = loadConfig();
1819
+ registerProject(globalConfig, context.projectPath, context.config.name);
1820
+ saveConfig(globalConfig);
1821
+ const projectName = context.config.name || path.basename(context.projectPath);
1822
+ resolvedServerName = `${projectName}.${parsed.name}`;
1823
+ }
1824
+ addServer(context.config, parsed.name, parsed.spec, options.force ?? false);
1825
+ await maybeAutoConfigureAuthForAddedServer(resolvedServerName, parsed.spec, secrets);
1826
+ context.save();
1827
+ process.stdout.write(`Added server: ${parsed.name} (${parsed.spec.transport}) ${context.type === "project" ? `to project: ${context.projectPath}` : "globally"}
1636
1828
  `);
1829
+ }
1637
1830
  process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1638
1831
  `);
1639
- await autoSyncManagedEntries(config, cliPath);
1832
+ const mergedConfig = loadMergedConfig();
1833
+ await autoSyncManagedEntries(mergedConfig, cliPath);
1640
1834
  });
1641
1835
  }
1642
1836
  function registerRemoveCommand(parent, cliPath) {
1643
- parent.command("remove <name>").option("--force", "Do not error if server does not exist").description("Remove an upstream MCP server").action(async (name, options) => {
1644
- const config = loadConfig();
1645
- removeServer(config, name, options.force ?? false);
1646
- saveConfig(config);
1647
- process.stdout.write(`Removed server: ${name}
1837
+ parent.command("remove <name>").option("--force", "Do not error if server does not exist").option("-g, --global", "Force target global configuration").option("-l, --local", "Force target local project configuration").description("Remove an upstream MCP server").action(async (name, options) => {
1838
+ const context = resolveActiveConfig({ global: options.global, local: options.local });
1839
+ removeServer(context.config, name, options.force ?? false);
1840
+ context.save();
1841
+ process.stdout.write(`Removed server: ${name} ${context.type === "project" ? `from project: ${context.projectPath}` : "globally"}
1648
1842
  `);
1649
1843
  process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1650
1844
  `);
1651
- await autoSyncManagedEntries(config, cliPath);
1845
+ const mergedConfig = loadMergedConfig();
1846
+ await autoSyncManagedEntries(mergedConfig, cliPath);
1652
1847
  });
1653
1848
  }
1654
1849
  function registerListCommand(parent) {
1655
- parent.command("list").option("--json", "Output JSON").description("List configured upstream MCP servers").action((options) => {
1656
- const config = loadConfig();
1657
- const servers = Object.entries(config.servers).map(([name, spec]) => ({ name, ...spec }));
1850
+ parent.command("list").option("--json", "Output JSON").option("-g, --global", "Force target global configuration").option("-l, --local", "Force target local project configuration").description("List configured upstream MCP servers").action((options) => {
1851
+ let servers = [];
1852
+ if (options.global || options.local) {
1853
+ const context = resolveActiveConfig({ global: options.global, local: options.local });
1854
+ servers = Object.entries(context.config.servers).map(([name, spec]) => ({
1855
+ name,
1856
+ ...spec,
1857
+ projectName: context.type === "project" ? context.config.name || path.basename(context.projectPath) : undefined
1858
+ }));
1859
+ } else {
1860
+ const mergedConfig = loadMergedConfig();
1861
+ const globalConfig = loadConfig();
1862
+ const projectNamesMap = new Map;
1863
+ if (globalConfig.projects) {
1864
+ for (const [projectPath, entry] of Object.entries(globalConfig.projects)) {
1865
+ const localPath = path.join(projectPath, ".mcpx.json");
1866
+ if (fs.existsSync(localPath)) {
1867
+ try {
1868
+ const localConfig = loadProjectConfig(localPath);
1869
+ const pName = localConfig.name || entry.name || path.basename(projectPath);
1870
+ projectNamesMap.set(pName, projectPath);
1871
+ } catch {}
1872
+ }
1873
+ }
1874
+ }
1875
+ servers = Object.entries(mergedConfig.servers).map(([name, spec]) => {
1876
+ let projectName;
1877
+ let baseName = name;
1878
+ for (const pName of projectNamesMap.keys()) {
1879
+ if (name.startsWith(`${pName}.`)) {
1880
+ projectName = pName;
1881
+ baseName = name.slice(pName.length + 1);
1882
+ break;
1883
+ }
1884
+ }
1885
+ return {
1886
+ name: baseName,
1887
+ ...spec,
1888
+ projectName
1889
+ };
1890
+ });
1891
+ }
1658
1892
  if (options.json) {
1659
1893
  process.stdout.write(`${JSON.stringify({ servers }, null, 2)}
1660
1894
  `);
@@ -1667,12 +1901,13 @@ function registerListCommand(parent) {
1667
1901
  }
1668
1902
  for (const server of servers) {
1669
1903
  const state = isServerEnabled(server) ? "enabled" : "disabled";
1904
+ const scope = server.projectName ? `project: ${server.projectName}` : "global";
1670
1905
  if (server.transport === "http") {
1671
- process.stdout.write(`- ${server.name} (${state}, http) ${server.url}
1906
+ process.stdout.write(`- ${server.name} (${state}, http, ${scope}) ${server.url}
1672
1907
  `);
1673
1908
  } else {
1674
1909
  const args = (server.args ?? []).join(" ");
1675
- process.stdout.write(`- ${server.name} (${state}, stdio) ${server.command}${args ? ` ${args}` : ""}
1910
+ process.stdout.write(`- ${server.name} (${state}, stdio, ${scope}) ${server.command}${args ? ` ${args}` : ""}
1676
1911
  `);
1677
1912
  }
1678
1913
  }
@@ -1680,7 +1915,7 @@ function registerListCommand(parent) {
1680
1915
  }
1681
1916
  function registerSyncCommand(program, cliPath) {
1682
1917
  program.command("sync [clients...]").option("--client <id>", "Limit sync to specific client(s), comma-separated or repeated", (value, prev = []) => [...prev, value], []).option("--json", "Output JSON").description("Sync gateway configuration to supported clients (e.g. `mcpx sync claude`)").action(async (clients, options) => {
1683
- const config = loadConfig();
1918
+ const config = loadMergedConfig();
1684
1919
  const targetClients = parseClientList([...clients ?? [], ...options.client ?? []]);
1685
1920
  const secrets = new SecretsManager;
1686
1921
  await ensureDaemonIfEnabled(cliPath, secrets);
@@ -1691,7 +1926,7 @@ function registerSyncCommand(program, cliPath) {
1691
1926
  }
1692
1927
  function registerStatusCommand(program, cliPath) {
1693
1928
  program.command("status").option("--no-interactive", "Disable interactive status menu").option("--json", "Output JSON").description("Show gateway, daemon, MCP inventory, and client sync status").action(async (options) => {
1694
- const report = loadStatusReport();
1929
+ const report = await loadStatusReport();
1695
1930
  if (options.json) {
1696
1931
  process.stdout.write(`${JSON.stringify(report, null, 2)}
1697
1932
  `);
@@ -1720,15 +1955,22 @@ function registerDoctorCommand(program) {
1720
1955
  status: fs.existsSync(getManagedIndexPath()) ? "ok" : "warn",
1721
1956
  details: getManagedIndexPath()
1722
1957
  });
1723
- if (process.platform === "darwin") {
1958
+ try {
1959
+ const keytar = __require(path.join(__dirname, "..", "node_modules", "keytar", "lib", "keytar.js"));
1724
1960
  try {
1725
- execFileSync("security", ["help"], { stdio: "ignore" });
1726
- checks.push({ check: "macos_keychain", status: "ok", details: "security CLI available" });
1727
- } catch {
1728
- checks.push({ check: "macos_keychain", status: "fail", details: "security CLI unavailable" });
1961
+ keytar.getPassword("mcpx-doctor-check", "__probe__");
1962
+ } catch {}
1963
+ checks.push({ check: "keyring", status: "ok", details: "keytar native addon loaded" });
1964
+ } catch {
1965
+ if (process.platform === "linux") {
1966
+ checks.push({ check: "keyring", status: "warn", details: "keytar native addon unavailable; install libsecret-1-dev and reinstall dependencies" });
1967
+ } else if (process.platform === "win32") {
1968
+ checks.push({ check: "keyring", status: "warn", details: "keytar native addon unavailable; rebuild keytar with 'npx node-gyp rebuild' in app/node_modules/keytar" });
1969
+ } else if (process.platform === "darwin") {
1970
+ checks.push({ check: "keyring", status: "warn", details: "keytar native addon unavailable; reinstall dependencies or check Xcode CLI tools" });
1971
+ } else {
1972
+ checks.push({ check: "keyring", status: "warn", details: "keytar native addon unavailable" });
1729
1973
  }
1730
- } else {
1731
- checks.push({ check: "macos_keychain", status: "warn", details: "Non-macOS platform; keychain integration unavailable" });
1732
1974
  }
1733
1975
  const daemon = getDaemonStatus(config);
1734
1976
  checks.push({
@@ -2043,6 +2285,66 @@ function registerUpdateCommand(program) {
2043
2285
  }
2044
2286
  });
2045
2287
  }
2288
+ function registerProjectCommands(program, cliPath) {
2289
+ const projectCmd = program.command("project").description("Manage project-based MCP configurations");
2290
+ projectCmd.command("init [name]").description("Initialize a local .mcpx.json configuration in the current directory").action(async (name) => {
2291
+ const targetPath = path.join(process.cwd(), ".mcpx.json");
2292
+ if (fs.existsSync(targetPath)) {
2293
+ process.stderr.write(`Local project configuration already exists at: ${targetPath}
2294
+ `);
2295
+ process.exit(1);
2296
+ }
2297
+ const projectName = name?.trim() || path.basename(process.cwd());
2298
+ const localConfig = {
2299
+ name: projectName,
2300
+ servers: {}
2301
+ };
2302
+ const { saveProjectConfig: saveProjectConfig2 } = await import("./config-kayej21q.js");
2303
+ saveProjectConfig2(localConfig, targetPath);
2304
+ process.stdout.write(`Initialized project configuration at: ${targetPath}
2305
+ `);
2306
+ const { registerProject: registerProject2 } = await import("./registry-1s14wd58.js");
2307
+ const globalConfig = loadConfig();
2308
+ registerProject2(globalConfig, process.cwd(), projectName);
2309
+ saveConfig(globalConfig);
2310
+ process.stdout.write(`Registered project "${projectName}" globally.
2311
+ `);
2312
+ });
2313
+ projectCmd.command("list").description("List all globally registered project paths").action(() => {
2314
+ const globalConfig = loadConfig();
2315
+ const projects = Object.entries(globalConfig.projects ?? {});
2316
+ if (projects.length === 0) {
2317
+ process.stdout.write(`No projects currently registered.
2318
+ `);
2319
+ return;
2320
+ }
2321
+ process.stdout.write(`Registered Projects:
2322
+ `);
2323
+ for (const [projectPath, entry] of projects) {
2324
+ process.stdout.write(`- ${entry.name}: ${projectPath}
2325
+ `);
2326
+ }
2327
+ });
2328
+ projectCmd.command("remove <path>").description("Unregister a project path from the global configuration").action(async (projectPath) => {
2329
+ const resolvedPath = path.resolve(projectPath);
2330
+ const globalConfig = loadConfig();
2331
+ if (!globalConfig.projects || !globalConfig.projects[resolvedPath]) {
2332
+ process.stderr.write(`No registered project found at path: ${resolvedPath}
2333
+ `);
2334
+ process.exit(1);
2335
+ }
2336
+ const entry = globalConfig.projects[resolvedPath];
2337
+ const { unregisterProject: unregisterProject2 } = await import("./registry-1s14wd58.js");
2338
+ unregisterProject2(globalConfig, resolvedPath);
2339
+ saveConfig(globalConfig);
2340
+ process.stdout.write(`Unregistered project "${entry.name}" (${resolvedPath}) from the global configuration.
2341
+ `);
2342
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
2343
+ `);
2344
+ const mergedConfig = loadMergedConfig();
2345
+ await autoSyncManagedEntries(mergedConfig, cliPath);
2346
+ });
2347
+ }
2046
2348
  function registerProxyCommand(program) {
2047
2349
  program.command("proxy <name>").description("Run a stdio MCP proxy bridging to the local mcpx HTTP gateway for the given upstream server").action(async (name) => {
2048
2350
  await runStdioProxy(name);
@@ -2107,6 +2409,7 @@ async function runCli(argv = process.argv) {
2107
2409
  registerProxyCommand(program);
2108
2410
  registerMcpCompat(program, cliPath);
2109
2411
  registerUpdateCommand(program);
2412
+ registerProjectCommands(program, cliPath);
2110
2413
  await program.parseAsync(argv);
2111
2414
  }
2112
2415
  if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) {
@@ -0,0 +1,20 @@
1
+ import {
2
+ defaultConfig,
3
+ loadConfig,
4
+ loadMergedConfig,
5
+ loadProjectConfig,
6
+ resolveActiveConfig,
7
+ saveConfig,
8
+ saveProjectConfig
9
+ } from "./cli-7bsf6akp.js";
10
+ import"./cli-fsf33sgp.js";
11
+ import"./cli-s4gspqdf.js";
12
+ export {
13
+ saveProjectConfig,
14
+ saveConfig,
15
+ resolveActiveConfig,
16
+ loadProjectConfig,
17
+ loadMergedConfig,
18
+ loadConfig,
19
+ defaultConfig
20
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"auth-probe.d.ts","sourceRoot":"","sources":["../../src/core/auth-probe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAK9C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAeD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,cAAc,EACvB,SAAS,SAAgC,GACxC,OAAO,CAAC,mBAAmB,CAAC,CAyC9B"}
1
+ {"version":3,"file":"auth-probe.d.ts","sourceRoot":"","sources":["../../src/core/auth-probe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAwB9C,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoBD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,cAAc,EACvB,SAAS,SAAgC,GACxC,OAAO,CAAC,mBAAmB,CAAC,CAmE9B"}
@@ -1,5 +1,23 @@
1
- import { type McpxConfig } from "../types.js";
1
+ import { type McpxConfig, type UpstreamServerSpec } from "../types.js";
2
+ export interface ProjectLocalConfig {
3
+ name?: string;
4
+ servers: Record<string, UpstreamServerSpec>;
5
+ }
2
6
  export declare function defaultConfig(): McpxConfig;
3
7
  export declare function loadConfig(configPath?: string): McpxConfig;
4
8
  export declare function saveConfig(config: McpxConfig, configPath?: string): void;
9
+ export declare function loadProjectConfig(projectConfigPath: string): ProjectLocalConfig;
10
+ export declare function saveProjectConfig(config: ProjectLocalConfig, projectConfigPath: string): void;
11
+ export declare function loadMergedConfig(configPath?: string): McpxConfig;
12
+ export interface ActiveConfigContext {
13
+ type: "global" | "project";
14
+ configPath: string;
15
+ projectPath?: string;
16
+ config: McpxConfig;
17
+ save: () => void;
18
+ }
19
+ export declare function resolveActiveConfig(options?: {
20
+ global?: boolean;
21
+ local?: boolean;
22
+ }): ActiveConfigContext;
5
23
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AACA,OAAO,EAA6C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CzF,wBAAgB,aAAa,IAAI,UAAU,CAW1C;AAED,wBAAgB,UAAU,CAAC,UAAU,SAAkB,GAAG,UAAU,CAwBnE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,SAAkB,GAAG,IAAI,CAKjF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6C,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAkDlH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C;AAOD,wBAAgB,aAAa,IAAI,UAAU,CAY1C;AAED,wBAAgB,UAAU,CAAC,UAAU,SAAkB,GAAG,UAAU,CAyBnE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,SAAkB,GAAG,IAAI,CA6BjF;AAED,wBAAgB,iBAAiB,CAAC,iBAAiB,EAAE,MAAM,GAAG,kBAAkB,CAO/E;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAK7F;AAED,wBAAgB,gBAAgB,CAAC,UAAU,SAAkB,GAAG,UAAU,CAiCzE;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,mBAAmB,CAoD5G"}