@yawlabs/mcp 0.58.4 → 0.59.1

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.
Files changed (3) hide show
  1. package/README.md +44 -4
  2. package/dist/index.js +800 -299
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -302,7 +302,7 @@ function unionBlocked(files) {
302
302
  }
303
303
  return touched ? [...set] : void 0;
304
304
  }
305
- async function loadMcphConfig(opts = {}) {
305
+ async function loadYawMcpConfig(opts = {}) {
306
306
  const cwd = resolve(opts.cwd ?? process.cwd());
307
307
  const home = resolve(opts.home ?? homedir());
308
308
  const env = opts.env ?? process.env;
@@ -390,7 +390,7 @@ function toProfile(config) {
390
390
  return result;
391
391
  }
392
392
  async function loadEffectiveProfile(cwd, home) {
393
- const config = await loadMcphConfig({ cwd, home });
393
+ const config = await loadYawMcpConfig({ cwd, home });
394
394
  return toProfile(config);
395
395
  }
396
396
  function isAllowed(rules, namespace) {
@@ -542,7 +542,7 @@ async function runBundlesCommand(opts = {}) {
542
542
  }
543
543
  return { exitCode: 0, lines };
544
544
  }
545
- const config = await loadMcphConfig({
545
+ const config = await loadYawMcpConfig({
546
546
  cwd: opts.cwd,
547
547
  home: opts.home,
548
548
  env: opts.env
@@ -636,13 +636,15 @@ var COMPLETION_USAGE = `Usage: yaw-mcp completion <bash|zsh|fish|powershell>
636
636
  location for your shell:
637
637
 
638
638
  bash yaw-mcp completion bash > ~/.local/share/bash-completion/completions/yaw-mcp
639
- zsh yaw-mcp completion zsh > "\${fpath[1]}/_mcph" (must be on $fpath)
639
+ zsh yaw-mcp completion zsh > "\${fpath[1]}/_yaw-mcp" (must be on $fpath)
640
640
  fish yaw-mcp completion fish > ~/.config/fish/completions/yaw-mcp.fish
641
641
  powershell yaw-mcp completion powershell >> $PROFILE`;
642
642
  var INSTALL_CLIENTS = ["claude-code", "claude-desktop", "cursor", "vscode"];
643
643
  var SUBCOMMAND_SPEC = [
644
+ // Setup -- connect a client to yaw-mcp.
644
645
  {
645
646
  name: "install",
647
+ description: "Connect an MCP client to yaw-mcp",
646
648
  positional: [...INSTALL_CLIENTS],
647
649
  flags: [
648
650
  "--scope",
@@ -657,14 +659,59 @@ var SUBCOMMAND_SPEC = [
657
659
  "--all"
658
660
  ]
659
661
  },
660
- { name: "doctor", flags: ["--json", "--help"] },
661
- { name: "servers", flags: ["--json", "--help"] },
662
- { name: "bundles", positional: ["list", "match"], flags: ["--json", "--help"] },
663
- { name: "compliance", flags: ["--publish", "--help"] },
664
- { name: "reset-learning", flags: ["--help"] },
665
- { name: "completion", positional: ["bash", "zsh", "fish", "powershell"], flags: ["--help"] },
666
- { name: "upgrade", flags: ["--run", "--json", "--help"] },
667
- { name: "help", flags: [] }
662
+ // Local servers -- manage ~/.yaw-mcp/bundles.json (no account).
663
+ {
664
+ name: "add",
665
+ description: "Add a catalog server to bundles.json",
666
+ positional: ["<slug>"],
667
+ flags: ["--env", "--dry-run", "--json", "--catalog", "--help"]
668
+ },
669
+ { name: "remove", description: "Remove a local server", positional: ["<slug-or-namespace>"], flags: ["--help"] },
670
+ { name: "list", description: "List the servers yaw-mcp loads locally", flags: ["--json", "--help"] },
671
+ {
672
+ name: "try",
673
+ description: "Wire a one-off trial of a catalog server",
674
+ positional: ["<slug>"],
675
+ flags: ["--client", "--ttl", "--env", "--dry-run", "--base", "--help"]
676
+ },
677
+ { name: "try-cleanup", description: "Remove a wired trial", positional: ["<slug>"], flags: ["--base", "--help"] },
678
+ // Inspection.
679
+ { name: "doctor", description: "Print diagnostic of yaw-mcp setup", flags: ["--json", "--help"] },
680
+ { name: "servers", description: "List servers in your yaw.sh/mcp dashboard", flags: ["--json", "--help"] },
681
+ {
682
+ name: "bundles",
683
+ description: "Browse curated multi-server bundles",
684
+ positional: ["list", "match"],
685
+ flags: ["--json", "--help"]
686
+ },
687
+ // Maintenance.
688
+ { name: "upgrade", description: "Upgrade @yawlabs/mcp to the latest version", flags: ["--run", "--json", "--help"] },
689
+ { name: "reset-learning", description: "Clear cross-session learning history", flags: ["--help"] },
690
+ {
691
+ name: "completion",
692
+ description: "Print a shell completion script",
693
+ positional: ["bash", "zsh", "fish", "powershell"],
694
+ flags: ["--help"]
695
+ },
696
+ // Account / sync (Pro + Team).
697
+ { name: "login", description: "Authenticate with a Yaw MCP account", flags: ["--key", "--json", "--help"] },
698
+ { name: "logout", description: "Sign out of your account", flags: ["--json", "--help"] },
699
+ {
700
+ name: "sync",
701
+ description: "Sync bundles across machines",
702
+ positional: ["push", "pull", "status"],
703
+ flags: ["--key", "--json", "--help"]
704
+ },
705
+ { name: "stats", description: "Show usage statistics", flags: ["--key", "--limit", "--days", "--json", "--help"] },
706
+ {
707
+ name: "secrets",
708
+ description: "Manage stored secrets",
709
+ positional: ["set", "get", "list", "remove", "lock", "push", "pull"],
710
+ flags: ["--key", "--value", "--stdin", "--json", "--help"]
711
+ },
712
+ // Other.
713
+ { name: "compliance", description: "Run the compliance suite against a server", flags: ["--publish", "--help"] },
714
+ { name: "help", description: "Show usage", flags: [] }
668
715
  ];
669
716
  function parseCompletionArgs(argv) {
670
717
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -737,7 +784,7 @@ ${posClause}
737
784
  return `# bash completion for yaw-mcp \u2014 generated by \`yaw-mcp completion bash\`
738
785
  # Install: save this to ~/.local/share/bash-completion/completions/yaw-mcp
739
786
  # or source it from your .bashrc.
740
- _mcph() {
787
+ _yaw-mcp() {
741
788
  local cur prev words cword
742
789
  cur="\${COMP_WORDS[COMP_CWORD]}"
743
790
  cword=$COMP_CWORD
@@ -751,24 +798,11 @@ _mcph() {
751
798
  ${cases}
752
799
  esac
753
800
  }
754
- complete -F _mcph yaw-mcp
801
+ complete -F _yaw-mcp yaw-mcp
755
802
  `;
756
803
  }
757
804
  function renderZsh() {
758
- const subcommandDescriptions = {
759
- install: "Auto-edit an MCP client's config",
760
- doctor: "Print diagnostic of yaw-mcp setup",
761
- servers: "List servers in your yaw.sh/mcp dashboard",
762
- bundles: "Browse curated multi-server bundles",
763
- compliance: "Run the compliance suite against a server",
764
- "reset-learning": "Clear cross-session learning history",
765
- completion: "Print a shell completion script",
766
- upgrade: "Upgrade @yawlabs/mcp to the latest version",
767
- help: "Show usage"
768
- };
769
- const subcommandList = SUBCOMMAND_SPEC.map((s) => ` '${s.name}:${subcommandDescriptions[s.name] ?? ""}'`).join(
770
- "\n"
771
- );
805
+ const subcommandList = SUBCOMMAND_SPEC.map((s) => ` '${s.name}:${s.description}'`).join("\n");
772
806
  const argsCases = SUBCOMMAND_SPEC.map((spec) => {
773
807
  const lines = [` ${spec.name})`];
774
808
  if (spec.positional) {
@@ -781,10 +815,10 @@ function renderZsh() {
781
815
  }).join("\n");
782
816
  return `#compdef yaw-mcp
783
817
  # zsh completion for yaw-mcp \u2014 generated by \`yaw-mcp completion zsh\`
784
- # Install: save this to a file on your $fpath named _mcph
785
- # (e.g., ~/.zsh/completions/_mcph), then rebuild completions:
818
+ # Install: save this to a file on your $fpath named _yaw-mcp
819
+ # (e.g., ~/.zsh/completions/_yaw-mcp), then rebuild completions:
786
820
  # autoload -U compinit && compinit
787
- _mcph() {
821
+ _yaw-mcp() {
788
822
  local context state line
789
823
  _arguments -C \\
790
824
  '1: :->cmd' \\
@@ -802,7 +836,7 @@ ${argsCases}
802
836
  ;;
803
837
  esac
804
838
  }
805
- _mcph "$@"
839
+ _yaw-mcp "$@"
806
840
  `;
807
841
  }
808
842
  function renderFish() {
@@ -821,7 +855,8 @@ complete -c yaw-mcp -f`;
821
855
  }
822
856
  }
823
857
  for (const f of spec.flags) {
824
- const long = f.replace(/^--/, "");
858
+ if (!f.startsWith("--")) continue;
859
+ const long = f.slice(2);
825
860
  flagLines.push(`complete -c yaw-mcp -n "__fish_seen_subcommand_from ${spec.name}" -l ${long}`);
826
861
  }
827
862
  }
@@ -1457,9 +1492,12 @@ function buildLaunchEntry(opts) {
1457
1492
  if (opts.token) entry.env = { YAW_MCP_TOKEN: opts.token };
1458
1493
  return entry;
1459
1494
  }
1460
- var ENTRY_NAME = "yaw-mcp";
1461
- var LEGACY_ENTRY_NAME = "mcp.hosting";
1462
- var CLAUDE_CODE_ALLOW_PATTERN = "mcp__yaw_mcp__*";
1495
+ var ENTRY_NAME = "mcp";
1496
+ var LEGACY_ENTRY_NAMES = ["mcp.hosting", "mcph", "yaw-mcp"];
1497
+ function findLegacyEntry(container) {
1498
+ return LEGACY_ENTRY_NAMES.find((n) => n in container) ?? null;
1499
+ }
1500
+ var CLAUDE_CODE_ALLOW_PATTERN = "mcp__mcp__*";
1463
1501
  function resolveClaudeCodeSettingsPath(scope, opts) {
1464
1502
  const { home, projectDir, claudeConfigDir } = opts;
1465
1503
  const cfgDir = claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : null;
@@ -1599,6 +1637,101 @@ import { homedir as homedir4, hostname, userInfo } from "os";
1599
1637
  import { join as join5, resolve as resolve3 } from "path";
1600
1638
  import { request as request5 } from "undici";
1601
1639
 
1640
+ // src/catalog.ts
1641
+ var DEFAULT_CATALOG_URL = "https://yaw.sh/data/mcp-catalog.json";
1642
+ var FETCH_TIMEOUT_MS = 1e4;
1643
+ var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1644
+ function tokenizeCommand(cmd) {
1645
+ const out = [];
1646
+ let cur = "";
1647
+ let has = false;
1648
+ let quote = null;
1649
+ for (const ch of cmd) {
1650
+ if (quote) {
1651
+ if (ch === quote) quote = null;
1652
+ else cur += ch;
1653
+ has = true;
1654
+ } else if (ch === '"' || ch === "'") {
1655
+ quote = ch;
1656
+ has = true;
1657
+ } else if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
1658
+ if (has) {
1659
+ out.push(cur);
1660
+ cur = "";
1661
+ has = false;
1662
+ }
1663
+ } else {
1664
+ cur += ch;
1665
+ has = true;
1666
+ }
1667
+ }
1668
+ if (has) out.push(cur);
1669
+ return out;
1670
+ }
1671
+ async function defaultFetchCatalog(url = DEFAULT_CATALOG_URL) {
1672
+ const ac = new AbortController();
1673
+ const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
1674
+ let body;
1675
+ try {
1676
+ const res = await fetch(url, { signal: ac.signal, headers: { accept: "application/json" } });
1677
+ if (!res.ok) {
1678
+ throw new Error(`the Yaw MCP catalog at ${url} returned HTTP ${res.status}.`);
1679
+ }
1680
+ body = await res.json();
1681
+ } catch (err) {
1682
+ if (err instanceof Error && err.name === "AbortError") {
1683
+ throw new Error(`timed out fetching the Yaw MCP catalog at ${url}.`);
1684
+ }
1685
+ throw err instanceof Error ? err : new Error(String(err));
1686
+ } finally {
1687
+ clearTimeout(timer);
1688
+ }
1689
+ const servers = body?.servers;
1690
+ if (!Array.isArray(servers)) {
1691
+ throw new Error(`the Yaw MCP catalog at ${url} was not in the expected shape.`);
1692
+ }
1693
+ return servers.filter(
1694
+ (s) => typeof s === "object" && s !== null && typeof s.slug === "string"
1695
+ );
1696
+ }
1697
+ async function resolveCatalogSlug(slug, opts = {}) {
1698
+ const url = opts.catalogUrl ?? DEFAULT_CATALOG_URL;
1699
+ const fetchCatalog = opts.fetchCatalog ?? defaultFetchCatalog;
1700
+ const servers = await fetchCatalog(url);
1701
+ const entry = servers.find((s) => s.slug === slug);
1702
+ if (!entry) {
1703
+ throw new Error(
1704
+ `no server with slug "${slug}" in the Yaw MCP catalog. Browse https://yaw.sh/mcp/catalog/ for the list.`
1705
+ );
1706
+ }
1707
+ const install = entry.install ?? {};
1708
+ const runtime = typeof install.runtime === "string" ? install.runtime.toLowerCase() : "";
1709
+ if (install.url || install.type === "remote" || /^(remote|https?|sse|url)$/.test(runtime)) {
1710
+ throw new Error(`"${slug}" is a remote server -- add it from the Yaw MCP dashboard, not the local CLI.`);
1711
+ }
1712
+ const cmdStr = typeof install.command === "string" ? install.command.trim() : "";
1713
+ if (!cmdStr) {
1714
+ throw new Error(`catalog entry "${slug}" has no install command.`);
1715
+ }
1716
+ const tokens = tokenizeCommand(cmdStr);
1717
+ if (tokens.length === 0) {
1718
+ throw new Error(`catalog entry "${slug}" install command was empty.`);
1719
+ }
1720
+ const [command, ...args] = tokens;
1721
+ const requiredEnvKeys = Array.isArray(entry.requiredEnv) ? entry.requiredEnv.map((e) => e && typeof e === "object" ? e.key : void 0).filter((k) => typeof k === "string" && ENV_KEY_RE.test(k)) : [];
1722
+ const source = typeof entry.repo === "string" ? entry.repo : typeof entry.homepage === "string" ? entry.homepage : void 0;
1723
+ return {
1724
+ slug,
1725
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : slug,
1726
+ command,
1727
+ args,
1728
+ requiredEnvKeys,
1729
+ description: typeof entry.description === "string" ? entry.description : void 0,
1730
+ source,
1731
+ docUrl: source
1732
+ };
1733
+ }
1734
+
1602
1735
  // src/install-cmd.ts
1603
1736
  import { existsSync } from "fs";
1604
1737
  import { chmod, readFile as readFile3 } from "fs/promises";
@@ -1675,7 +1808,7 @@ ${USAGE}`);
1675
1808
  log2(`File: ${resolved.absolute}`);
1676
1809
  let token5 = opts.token ?? null;
1677
1810
  if (!token5) {
1678
- const cfg = await loadMcphConfig({ home: opts.home, cwd: process.cwd(), env: {} });
1811
+ const cfg = await loadYawMcpConfig({ home: opts.home, cwd: process.cwd(), env: {} });
1679
1812
  token5 = cfg.token;
1680
1813
  }
1681
1814
  if (!token5) {
@@ -1687,7 +1820,7 @@ ${USAGE}`);
1687
1820
  const containerPath = resolved.containerPath;
1688
1821
  let existing = {};
1689
1822
  let existingHasEntry = false;
1690
- let legacyPresent = false;
1823
+ let legacyEntry = null;
1691
1824
  if (existsSync(resolved.absolute)) {
1692
1825
  let raw;
1693
1826
  try {
@@ -1717,7 +1850,7 @@ ${USAGE}`);
1717
1850
  if (typeof container === "object" && container !== null && !Array.isArray(container)) {
1718
1851
  const c = container;
1719
1852
  existingHasEntry = ENTRY_NAME in c;
1720
- legacyPresent = LEGACY_ENTRY_NAME in c;
1853
+ legacyEntry = findLegacyEntry(c);
1721
1854
  }
1722
1855
  }
1723
1856
  if (existingHasEntry) {
@@ -1747,16 +1880,16 @@ ${USAGE}`);
1747
1880
  const merged = mergeClientConfig(existing, containerPath, newEntry);
1748
1881
  const clientJson = `${JSON.stringify(merged, null, 2)}
1749
1882
  `;
1750
- const writeMcphConfig = !opts.skipMcphConfig && token5 !== null;
1883
+ const writeYawMcpConfig = !opts.skipYawMcpConfig && token5 !== null;
1751
1884
  const home = opts.home ?? homedir3();
1752
- const mcphConfigPath = join4(home, CONFIG_DIRNAME, CONFIG_FILENAME);
1753
- const mcphConfigComposed = writeMcphConfig ? await composeMcphConfig(mcphConfigPath, token5) : { json: "" };
1754
- if ("backupPath" in mcphConfigComposed && mcphConfigComposed.backupPath) {
1885
+ const yawMcpConfigPath = join4(home, CONFIG_DIRNAME, CONFIG_FILENAME);
1886
+ const yawMcpConfigComposed = writeYawMcpConfig ? await composeYawMcpConfig(yawMcpConfigPath, token5) : { json: "" };
1887
+ if ("backupPath" in yawMcpConfigComposed && yawMcpConfigComposed.backupPath) {
1755
1888
  log2(
1756
- `yaw-mcp install: existing ${mcphConfigPath} was malformed; original bytes backed up to ${mcphConfigComposed.backupPath} before overwriting.`
1889
+ `yaw-mcp install: existing ${yawMcpConfigPath} was malformed; original bytes backed up to ${yawMcpConfigComposed.backupPath} before overwriting.`
1757
1890
  );
1758
1891
  }
1759
- const mcphConfigJson = mcphConfigComposed.json;
1892
+ const yawMcpConfigJson = yawMcpConfigComposed.json;
1760
1893
  const settingsPatch = opts.clientId === "claude-code" ? await prepareClaudeCodeSettingsPatch({
1761
1894
  scope,
1762
1895
  home,
@@ -1766,40 +1899,40 @@ ${USAGE}`);
1766
1899
  }) : null;
1767
1900
  if (opts.dryRun) {
1768
1901
  log2("\n--- dry run: would write the following ---");
1769
- if (writeMcphConfig) log2(`# ${mcphConfigPath}
1770
- ${mcphConfigJson}`);
1902
+ if (writeYawMcpConfig) log2(`# ${yawMcpConfigPath}
1903
+ ${yawMcpConfigJson}`);
1771
1904
  log2(`
1772
1905
  # ${resolved.absolute}
1773
1906
  ${clientJson}`);
1774
1907
  if (settingsPatch?.changed) log2(`# ${settingsPatch.path}
1775
1908
  ${settingsPatch.nextJson}`);
1776
- if (legacyPresent) {
1909
+ if (legacyEntry) {
1777
1910
  log2(
1778
- `Note: legacy "${LEGACY_ENTRY_NAME}" entry at ${resolved.absolute} would remain \u2014 remove it to avoid running yaw-mcp twice.`
1911
+ `Note: legacy "${legacyEntry}" entry at ${resolved.absolute} would remain \u2014 remove it to avoid running yaw-mcp twice.`
1779
1912
  );
1780
1913
  }
1781
1914
  const wouldWrite = [];
1782
- if (writeMcphConfig) wouldWrite.push(mcphConfigPath);
1915
+ if (writeYawMcpConfig) wouldWrite.push(yawMcpConfigPath);
1783
1916
  wouldWrite.push(resolved.absolute);
1784
1917
  if (settingsPatch?.changed) wouldWrite.push(settingsPatch.path);
1785
1918
  return { written: [], wouldWrite, messages, exitCode: 0 };
1786
1919
  }
1787
1920
  const written = [];
1788
- if (writeMcphConfig) {
1921
+ if (writeYawMcpConfig) {
1789
1922
  try {
1790
- await atomicWriteFile(mcphConfigPath, mcphConfigJson);
1923
+ await atomicWriteFile(yawMcpConfigPath, yawMcpConfigJson);
1791
1924
  if (process.platform !== "win32") {
1792
1925
  try {
1793
- await chmod(mcphConfigPath, 384);
1926
+ await chmod(yawMcpConfigPath, 384);
1794
1927
  } catch {
1795
1928
  }
1796
1929
  }
1797
1930
  } catch (e) {
1798
- err(`yaw-mcp install: failed to write ${mcphConfigPath}: ${e.message}`);
1931
+ err(`yaw-mcp install: failed to write ${yawMcpConfigPath}: ${e.message}`);
1799
1932
  return { written: [], wouldWrite: [], messages, exitCode: 1 };
1800
1933
  }
1801
- log2(`Wrote ${mcphConfigPath}`);
1802
- written.push(mcphConfigPath);
1934
+ log2(`Wrote ${yawMcpConfigPath}`);
1935
+ written.push(yawMcpConfigPath);
1803
1936
  }
1804
1937
  try {
1805
1938
  await atomicWriteFile(resolved.absolute, clientJson);
@@ -1821,9 +1954,9 @@ ${settingsPatch.nextJson}`);
1821
1954
  }
1822
1955
  }
1823
1956
  if (target.notes) log2(`Note: ${target.notes}`);
1824
- if (legacyPresent) {
1957
+ if (legacyEntry) {
1825
1958
  log2(
1826
- `Note: legacy "${LEGACY_ENTRY_NAME}" entry remains at ${resolved.absolute}. Remove it to avoid running yaw-mcp twice.`
1959
+ `Note: legacy "${legacyEntry}" entry remains at ${resolved.absolute}. Remove it to avoid running yaw-mcp twice.`
1827
1960
  );
1828
1961
  }
1829
1962
  log2(`
@@ -1861,14 +1994,14 @@ async function prepareClaudeCodeSettingsPatch(opts) {
1861
1994
  return { path: path3, nextJson: `${JSON.stringify(merged, null, 2)}
1862
1995
  `, changed: true };
1863
1996
  }
1864
- var LEGACY_CLAUDE_CODE_ALLOW_PATTERN = "mcp__mcp_hosting__*";
1997
+ var LEGACY_CLAUDE_CODE_ALLOW_PATTERNS = ["mcp__mcp_hosting__*", "mcp__yaw_mcp__*"];
1865
1998
  function mergePermissionsAllow(existing, patterns) {
1866
1999
  const out = { ...existing };
1867
2000
  const prev = out.permissions;
1868
2001
  const perms = typeof prev === "object" && prev !== null && !Array.isArray(prev) ? { ...prev } : {};
1869
2002
  const prevAllow = perms.allow;
1870
2003
  const allow = Array.isArray(prevAllow) ? prevAllow.filter(
1871
- (x) => typeof x === "string" && x !== LEGACY_CLAUDE_CODE_ALLOW_PATTERN
2004
+ (x) => typeof x === "string" && !LEGACY_CLAUDE_CODE_ALLOW_PATTERNS.includes(x)
1872
2005
  ) : [];
1873
2006
  for (const p of patterns) {
1874
2007
  if (!allow.includes(p)) allow.push(p);
@@ -1943,7 +2076,7 @@ function removeFromClientConfig(existing, containerPath, entryName) {
1943
2076
  parent[leafKey] = container;
1944
2077
  return out;
1945
2078
  }
1946
- async function composeMcphConfig(path3, token5) {
2079
+ async function composeYawMcpConfig(path3, token5) {
1947
2080
  let existing = {};
1948
2081
  let backupPath;
1949
2082
  if (existsSync(path3)) {
@@ -2019,7 +2152,7 @@ function parseInstallArgs(argv) {
2019
2152
  opts.dryRun = true;
2020
2153
  break;
2021
2154
  case "--no-yaw-mcp-config":
2022
- opts.skipMcphConfig = true;
2155
+ opts.skipYawMcpConfig = true;
2023
2156
  break;
2024
2157
  case "--list":
2025
2158
  opts.listOnly = true;
@@ -2070,7 +2203,7 @@ async function runInstallList(opts, log2) {
2070
2203
  path: displayPath(p.path, home),
2071
2204
  status: statusFor(p)
2072
2205
  }));
2073
- const installed = probes.filter((p) => p.hasMcphEntry).length;
2206
+ const installed = probes.filter((p) => p.hasMcpEntry).length;
2074
2207
  const available = probes.filter((p) => !p.unavailable).length;
2075
2208
  log2(`${installed}/${available} client scopes have yaw-mcp configured on ${os}.`);
2076
2209
  log2("");
@@ -2095,7 +2228,7 @@ async function runInstallList(opts, log2) {
2095
2228
  function statusFor(p) {
2096
2229
  if (p.unavailable) return "unavailable";
2097
2230
  if (p.malformed) return "malformed";
2098
- if (p.hasMcphEntry) return "installed";
2231
+ if (p.hasMcpEntry) return "installed";
2099
2232
  if (p.exists) return "other-entries";
2100
2233
  return "not installed";
2101
2234
  }
@@ -2197,8 +2330,9 @@ var TRY_USAGE = `Usage: yaw-mcp try <slug> [flags]
2197
2330
  Required env vars not supplied here AND not in your
2198
2331
  shell's env block the trial with an explainer.
2199
2332
  --dry-run Print what would happen without writing anything.
2200
- --base <url> Override the explore endpoint base (default:
2201
- $YAW_MCP_BASE_URL or https://yaw.sh/mcp).`;
2333
+ --base <url> Base URL for the signup/telemetry links (default:
2334
+ $YAW_MCP_BASE_URL or https://yaw.sh/mcp). The catalog
2335
+ itself is set via $YAW_MCP_CATALOG_URL.`;
2202
2336
  var TRY_CLEANUP_USAGE = `Usage: yaw-mcp try-cleanup <slug>
2203
2337
 
2204
2338
  Remove a previously-wired trial: peels the yaw-mcp-try-<slug> entry out of
@@ -2348,44 +2482,16 @@ async function loadOrCreateAnonId(home = homedir4()) {
2348
2482
  }
2349
2483
  return id;
2350
2484
  }
2351
- async function defaultFetchExplore(baseUrl, slug) {
2352
- const url = `${baseUrl.replace(/\/$/, "")}/api/explore/${encodeURIComponent(slug)}`;
2353
- const ac = new AbortController();
2354
- const timer = setTimeout(() => ac.abort(), 1e4);
2355
- try {
2356
- const res = await fetch(url, { signal: ac.signal, headers: { accept: "application/json" } });
2357
- if (res.status === 404) {
2358
- throw new Error(`yaw-mcp try: no server with slug "${slug}" \u2014 check ${baseUrl}/explore for the catalog.`);
2359
- }
2360
- if (!res.ok) {
2361
- throw new Error(`yaw-mcp try: ${url} returned HTTP ${res.status}`);
2362
- }
2363
- const body = await res.json();
2364
- return validateExploreResponse(body, slug);
2365
- } finally {
2366
- clearTimeout(timer);
2367
- }
2368
- }
2369
- function validateExploreResponse(body, slug) {
2370
- if (!body || typeof body !== "object") {
2371
- throw new Error(`yaw-mcp try: /api/explore/${slug} returned a non-object response.`);
2372
- }
2373
- const b = body;
2374
- if (typeof b.slug !== "string" || typeof b.name !== "string" || typeof b.command !== "string") {
2375
- throw new Error(`yaw-mcp try: /api/explore/${slug} missing required string fields (slug/name/command).`);
2376
- }
2377
- if (!Array.isArray(b.args) || !b.args.every((x) => typeof x === "string")) {
2378
- throw new Error(`yaw-mcp try: /api/explore/${slug} has invalid args (expected string[]).`);
2379
- }
2380
- const req = Array.isArray(b.requiredEnvVars) ? b.requiredEnvVars.filter((x) => typeof x === "string") : [];
2485
+ async function defaultFetchExplore(_baseUrl, slug) {
2486
+ const resolved = await resolveCatalogSlug(slug, { catalogUrl: process.env.YAW_MCP_CATALOG_URL });
2381
2487
  const out = {
2382
- slug: b.slug,
2383
- name: b.name,
2384
- command: b.command,
2385
- args: b.args,
2386
- requiredEnvVars: req
2488
+ slug: resolved.slug,
2489
+ name: resolved.name,
2490
+ command: resolved.command,
2491
+ args: resolved.args,
2492
+ requiredEnvVars: resolved.requiredEnvKeys
2387
2493
  };
2388
- if (typeof b.docUrl === "string") out.docUrl = b.docUrl;
2494
+ if (resolved.docUrl) out.docUrl = resolved.docUrl;
2389
2495
  return out;
2390
2496
  }
2391
2497
  async function defaultPostEvent(baseUrl, body) {
@@ -2771,7 +2877,7 @@ function selectFlakyNamespaces(entries, limit) {
2771
2877
  }
2772
2878
 
2773
2879
  // src/doctor-cmd.ts
2774
- var VERSION = true ? "0.58.4" : "dev";
2880
+ var VERSION = true ? "0.59.1" : "dev";
2775
2881
  async function runDoctor(opts = {}) {
2776
2882
  if (opts.json) return runDoctorJson(opts);
2777
2883
  const lines = [];
@@ -2789,7 +2895,7 @@ async function runDoctor(opts = {}) {
2789
2895
  print(`yaw-mcp version: ${VERSION}`);
2790
2896
  print(`platform: ${os}`);
2791
2897
  print("");
2792
- const config = await loadMcphConfig({ cwd, home, env });
2898
+ const config = await loadYawMcpConfig({ cwd, home, env });
2793
2899
  print("CONFIG FILES");
2794
2900
  if (config.loadedFiles.length === 0) {
2795
2901
  print(" (none \u2014 using defaults + env)");
@@ -2874,7 +2980,7 @@ async function runDoctorJson(opts) {
2874
2980
  const os = opts.os ?? CURRENT_OS;
2875
2981
  const env = opts.env ?? process.env;
2876
2982
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2877
- const config = await loadMcphConfig({ cwd, home, env });
2983
+ const config = await loadYawMcpConfig({ cwd, home, env });
2878
2984
  const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
2879
2985
  const clients = probeClients({ home, os, cwd, claudeConfigDir });
2880
2986
  const envVarNames = [
@@ -3114,12 +3220,12 @@ function schemaSuffix(f) {
3114
3220
  function renderClientStatus(c, installCmd) {
3115
3221
  if (c.unavailable) return "unavailable on this OS";
3116
3222
  if (c.malformed) return "exists but JSON is malformed \u2014 fix or rerun `yaw-mcp install`";
3117
- if (c.hasMcphEntry && c.hasLegacyEntry) {
3118
- return `OK \u2014 has "${ENTRY_NAME}" entry; legacy "${LEGACY_ENTRY_NAME}" entry also present \u2014 remove it to avoid running yaw-mcp twice`;
3223
+ if (c.hasMcpEntry && c.hasLegacyEntry) {
3224
+ return `OK \u2014 has "${ENTRY_NAME}" entry; legacy "${c.legacyEntryName}" entry also present \u2014 remove it to avoid running yaw-mcp twice`;
3119
3225
  }
3120
- if (c.hasMcphEntry) return `OK \u2014 has "${ENTRY_NAME}" entry`;
3226
+ if (c.hasMcpEntry) return `OK \u2014 has "${ENTRY_NAME}" entry`;
3121
3227
  if (c.hasLegacyEntry) {
3122
- return `legacy "${LEGACY_ENTRY_NAME}" entry present \u2014 run \`${installCmd}\` to migrate, then remove the legacy entry by hand`;
3228
+ return `legacy "${c.legacyEntryName}" entry present \u2014 run \`${installCmd}\` to migrate, then remove the legacy entry by hand`;
3123
3229
  }
3124
3230
  if (c.exists) return `present, no "${ENTRY_NAME}" entry \u2014 run \`${installCmd}\``;
3125
3231
  return `not configured \u2014 run \`${installCmd}\``;
@@ -3134,8 +3240,9 @@ function probeClients(opts) {
3134
3240
  scope: target.scopes[0].scope,
3135
3241
  path: "(n/a)",
3136
3242
  exists: false,
3137
- hasMcphEntry: false,
3243
+ hasMcpEntry: false,
3138
3244
  hasLegacyEntry: false,
3245
+ legacyEntryName: null,
3139
3246
  malformed: false,
3140
3247
  unavailable: true
3141
3248
  });
@@ -3156,8 +3263,9 @@ function probeClients(opts) {
3156
3263
  continue;
3157
3264
  }
3158
3265
  const exists3 = existsSync3(resolved.absolute);
3159
- let hasMcphEntry = false;
3266
+ let hasMcpEntry = false;
3160
3267
  let hasLegacyEntry = false;
3268
+ let legacyEntryName = null;
3161
3269
  let malformed = false;
3162
3270
  if (exists3) {
3163
3271
  try {
@@ -3168,8 +3276,9 @@ function probeClients(opts) {
3168
3276
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3169
3277
  const container = walkContainer(parsed, resolved.containerPath);
3170
3278
  if (container) {
3171
- hasMcphEntry = ENTRY_NAME in container;
3172
- hasLegacyEntry = LEGACY_ENTRY_NAME in container;
3279
+ hasMcpEntry = ENTRY_NAME in container;
3280
+ legacyEntryName = findLegacyEntry(container);
3281
+ hasLegacyEntry = legacyEntryName !== null;
3173
3282
  }
3174
3283
  } else {
3175
3284
  malformed = true;
@@ -3184,8 +3293,9 @@ function probeClients(opts) {
3184
3293
  scope: scope.scope,
3185
3294
  path: resolved.absolute,
3186
3295
  exists: exists3,
3187
- hasMcphEntry,
3296
+ hasMcpEntry,
3188
3297
  hasLegacyEntry,
3298
+ legacyEntryName,
3189
3299
  malformed,
3190
3300
  unavailable: false
3191
3301
  });
@@ -3212,8 +3322,9 @@ async function probeClientsAsync(opts) {
3212
3322
  scope: target.scopes[0].scope,
3213
3323
  path: "(n/a)",
3214
3324
  exists: false,
3215
- hasMcphEntry: false,
3325
+ hasMcpEntry: false,
3216
3326
  hasLegacyEntry: false,
3327
+ legacyEntryName: null,
3217
3328
  malformed: false,
3218
3329
  unavailable: true
3219
3330
  });
@@ -3229,8 +3340,9 @@ async function probeClientsAsync(opts) {
3229
3340
  claudeConfigDir: opts.claudeConfigDir
3230
3341
  });
3231
3342
  const exists3 = existsSync3(resolved.absolute);
3232
- let hasMcphEntry = false;
3343
+ let hasMcpEntry = false;
3233
3344
  let hasLegacyEntry = false;
3345
+ let legacyEntryName = null;
3234
3346
  let malformed = false;
3235
3347
  if (exists3) {
3236
3348
  try {
@@ -3240,8 +3352,9 @@ async function probeClientsAsync(opts) {
3240
3352
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3241
3353
  const container = walkContainer(parsed, resolved.containerPath);
3242
3354
  if (container) {
3243
- hasMcphEntry = ENTRY_NAME in container;
3244
- hasLegacyEntry = LEGACY_ENTRY_NAME in container;
3355
+ hasMcpEntry = ENTRY_NAME in container;
3356
+ legacyEntryName = findLegacyEntry(container);
3357
+ hasLegacyEntry = legacyEntryName !== null;
3245
3358
  }
3246
3359
  } else {
3247
3360
  malformed = true;
@@ -3256,8 +3369,9 @@ async function probeClientsAsync(opts) {
3256
3369
  scope: scope.scope,
3257
3370
  path: resolved.absolute,
3258
3371
  exists: exists3,
3259
- hasMcphEntry,
3372
+ hasMcpEntry,
3260
3373
  hasLegacyEntry,
3374
+ legacyEntryName,
3261
3375
  malformed,
3262
3376
  unavailable: false
3263
3377
  });
@@ -3439,6 +3553,466 @@ function closestNames(query, candidates, limit) {
3439
3553
  return scored.slice(0, limit).map((s) => s.name);
3440
3554
  }
3441
3555
 
3556
+ // src/local-add-cmd.ts
3557
+ import { homedir as homedir7 } from "os";
3558
+
3559
+ // src/local-bundles.ts
3560
+ import { createHash as createHash2 } from "crypto";
3561
+ import { existsSync as existsSync4 } from "fs";
3562
+ import { readFile as readFile6 } from "fs/promises";
3563
+ import { homedir as homedir6 } from "os";
3564
+ import { join as join7 } from "path";
3565
+ var BUNDLES_FILENAME = "bundles.json";
3566
+ var CURRENT_BUNDLES_SCHEMA_VERSION = 1;
3567
+ function localBundlesPath(configDir) {
3568
+ return join7(configDir, BUNDLES_FILENAME);
3569
+ }
3570
+ var NAMESPACE_RE = /^[a-z][a-z0-9_]{0,29}$/;
3571
+ function validateEntry(entry, warnings) {
3572
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
3573
+ warnings.push("bundles.json: skipping non-object server entry");
3574
+ return null;
3575
+ }
3576
+ const e = entry;
3577
+ const namespace = typeof e.namespace === "string" ? e.namespace : "";
3578
+ if (!namespace || !NAMESPACE_RE.test(namespace)) {
3579
+ warnings.push(`bundles.json: skipping server with invalid namespace ${JSON.stringify(namespace)}`);
3580
+ return null;
3581
+ }
3582
+ const name = typeof e.name === "string" && e.name.length > 0 ? e.name : namespace;
3583
+ const type = e.type === "remote" ? "remote" : "local";
3584
+ const transport = e.transport === "streamable-http" || e.transport === "sse" || e.transport === "stdio" ? e.transport : void 0;
3585
+ const command = typeof e.command === "string" ? e.command : void 0;
3586
+ const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
3587
+ const env = e.env && typeof e.env === "object" && !Array.isArray(e.env) ? Object.fromEntries(
3588
+ Object.entries(e.env).filter(([, v]) => typeof v === "string")
3589
+ ) : void 0;
3590
+ const url = typeof e.url === "string" ? e.url : void 0;
3591
+ const description = typeof e.description === "string" ? e.description : void 0;
3592
+ const isActive = e.isActive !== false;
3593
+ const id = typeof e.id === "string" && e.id.length > 0 ? e.id : `local-${namespace}`;
3594
+ return {
3595
+ id,
3596
+ name,
3597
+ namespace,
3598
+ type,
3599
+ transport,
3600
+ command,
3601
+ args,
3602
+ env,
3603
+ url,
3604
+ isActive,
3605
+ description
3606
+ };
3607
+ }
3608
+ async function readBundlesAt(path3, warnings) {
3609
+ let raw;
3610
+ try {
3611
+ raw = await readFile6(path3, "utf8");
3612
+ } catch {
3613
+ return { exists: false, file: null };
3614
+ }
3615
+ let parsed;
3616
+ try {
3617
+ parsed = parseJsonc(raw);
3618
+ } catch (err) {
3619
+ const msg = err instanceof Error ? err.message : String(err);
3620
+ warnings.push(`${path3}: invalid JSON (${msg}) -- file ignored`);
3621
+ log("warn", "bundles.json is not valid JSON; ignoring", { path: path3, error: msg });
3622
+ return { exists: true, file: null };
3623
+ }
3624
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3625
+ warnings.push(`${path3}: root must be a JSON object -- file ignored`);
3626
+ return { exists: true, file: null };
3627
+ }
3628
+ const obj = parsed;
3629
+ const version = typeof obj.version === "number" ? obj.version : void 0;
3630
+ if (version !== void 0 && version > CURRENT_BUNDLES_SCHEMA_VERSION) {
3631
+ warnings.push(
3632
+ `${path3}: schema version ${version} is newer than this yaw-mcp (${CURRENT_BUNDLES_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
3633
+ );
3634
+ }
3635
+ const rawServers = obj.servers;
3636
+ if (!Array.isArray(rawServers)) {
3637
+ warnings.push(`${path3}: 'servers' must be an array -- file ignored`);
3638
+ return { exists: true, file: null };
3639
+ }
3640
+ return {
3641
+ exists: true,
3642
+ file: { version, servers: rawServers }
3643
+ };
3644
+ }
3645
+ function hashContent(servers) {
3646
+ const h = createHash2("sha256");
3647
+ h.update(JSON.stringify(servers));
3648
+ return `local-${h.digest("hex").slice(0, 16)}`;
3649
+ }
3650
+ async function loadLocalBundles(opts = {}) {
3651
+ const cwd = opts.cwd ?? process.cwd();
3652
+ const home = opts.home ?? homedir6();
3653
+ const warnings = [];
3654
+ const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
3655
+ const projectPath = projectDir ? localBundlesPath(projectDir) : null;
3656
+ const globalPath = localBundlesPath(join7(home, CONFIG_DIRNAME));
3657
+ const projectResult = projectPath ? await readBundlesAt(projectPath, warnings) : { exists: false, file: null };
3658
+ let file;
3659
+ let sourcePath;
3660
+ if (projectResult.exists) {
3661
+ file = projectResult.file;
3662
+ sourcePath = projectPath;
3663
+ } else {
3664
+ const globalResult = await readBundlesAt(globalPath, warnings);
3665
+ file = globalResult.file;
3666
+ sourcePath = globalResult.exists ? globalPath : null;
3667
+ }
3668
+ if (!file) {
3669
+ return { config: null, path: sourcePath, warnings };
3670
+ }
3671
+ const servers = [];
3672
+ for (const raw of file.servers) {
3673
+ const validated = validateEntry(raw, warnings);
3674
+ if (validated) servers.push(validated);
3675
+ }
3676
+ return {
3677
+ config: {
3678
+ servers,
3679
+ configVersion: hashContent(servers)
3680
+ },
3681
+ path: sourcePath,
3682
+ warnings
3683
+ };
3684
+ }
3685
+ function deriveNamespace(name) {
3686
+ let ns = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
3687
+ if (ns.length === 0) return "server";
3688
+ if (!/^[a-z]/.test(ns)) ns = `s${ns}`;
3689
+ if (ns.length > 30) ns = ns.slice(0, 30);
3690
+ return ns;
3691
+ }
3692
+ async function readRawUserBundles(home) {
3693
+ const path3 = localBundlesPath(userConfigDir(home));
3694
+ if (!existsSync4(path3)) {
3695
+ return { version: CURRENT_BUNDLES_SCHEMA_VERSION, servers: [] };
3696
+ }
3697
+ const warnings = [];
3698
+ const r = await readBundlesAt(path3, warnings);
3699
+ if (!r.file) {
3700
+ const detail = warnings.length > 0 ? ` (${warnings.join("; ")})` : "";
3701
+ throw new Error(`${path3} is malformed${detail}; fix it by hand before adding servers.`);
3702
+ }
3703
+ return { version: r.file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION, servers: r.file.servers };
3704
+ }
3705
+ async function upsertUserBundle(entry, opts = {}) {
3706
+ const home = opts.home ?? homedir6();
3707
+ const path3 = localBundlesPath(userConfigDir(home));
3708
+ const file = await readRawUserBundles(home);
3709
+ const idx = file.servers.findIndex(
3710
+ (s) => s?.namespace === entry.namespace || entry.name != null && s?.name === entry.name
3711
+ );
3712
+ const replaced = idx >= 0;
3713
+ if (replaced) file.servers[idx] = entry;
3714
+ else file.servers.push(entry);
3715
+ file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
3716
+ await atomicWriteFile(path3, `${JSON.stringify(file, null, 2)}
3717
+ `);
3718
+ return { path: path3, replaced };
3719
+ }
3720
+ async function removeUserBundle(namespace, opts = {}) {
3721
+ const home = opts.home ?? homedir6();
3722
+ const path3 = localBundlesPath(userConfigDir(home));
3723
+ if (!existsSync4(path3)) return { path: path3, removed: false };
3724
+ const file = await readRawUserBundles(home);
3725
+ const before = file.servers.length;
3726
+ file.servers = file.servers.filter((s) => s?.namespace !== namespace);
3727
+ if (file.servers.length === before) return { path: path3, removed: false };
3728
+ file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
3729
+ await atomicWriteFile(path3, `${JSON.stringify(file, null, 2)}
3730
+ `);
3731
+ return { path: path3, removed: true };
3732
+ }
3733
+ async function findShadowingProjectBundles(cwd, home = homedir6()) {
3734
+ const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
3735
+ if (!projectDir) return null;
3736
+ const projectPath = localBundlesPath(projectDir);
3737
+ return existsSync4(projectPath) ? projectPath : null;
3738
+ }
3739
+
3740
+ // src/local-add-cmd.ts
3741
+ var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
3742
+ var ADD_USAGE = `Usage: yaw-mcp add <slug> [flags]
3743
+
3744
+ Resolve <slug> from the yaw.sh/mcp catalog and add it to your local
3745
+ ~/.yaw-mcp/bundles.json so yaw-mcp loads it (no account needed).
3746
+
3747
+ This is NOT the same as \`yaw-mcp install\` -- install wires the yaw-mcp
3748
+ aggregator into an AI client; add adds an MCP server to yaw-mcp itself.
3749
+
3750
+ --env KEY=value Provide a required env var's value. Repeatable. Required
3751
+ vars not given here AND not in your shell block the add.
3752
+ --dry-run Print what would be written without writing.
3753
+ --json Emit the written entry as JSON (implies success on stdout).
3754
+ --catalog <url> Override the catalog URL (default the public catalog).`;
3755
+ function parseEnvFlag(v, bag) {
3756
+ if (!v || !v.includes("=")) return "--env requires KEY=value";
3757
+ const eq = v.indexOf("=");
3758
+ const key = v.slice(0, eq);
3759
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return `--env: invalid KEY "${key}"`;
3760
+ bag[key] = v.slice(eq + 1);
3761
+ return null;
3762
+ }
3763
+ function parseAddArgs(argv) {
3764
+ if (argv.length === 0) return { ok: false, error: ADD_USAGE };
3765
+ const positional = [];
3766
+ const opts = {};
3767
+ const env = {};
3768
+ for (let i = 0; i < argv.length; i++) {
3769
+ const a = argv[i];
3770
+ const next = () => argv[++i];
3771
+ switch (a) {
3772
+ case "--env": {
3773
+ const e = parseEnvFlag(next(), env);
3774
+ if (e) return { ok: false, error: e };
3775
+ break;
3776
+ }
3777
+ case "--dry-run":
3778
+ opts.dryRun = true;
3779
+ break;
3780
+ case "--json":
3781
+ opts.json = true;
3782
+ break;
3783
+ case "--catalog": {
3784
+ const v = next();
3785
+ if (!v) return { ok: false, error: "--catalog requires a URL" };
3786
+ opts.catalogUrl = v;
3787
+ break;
3788
+ }
3789
+ case "-h":
3790
+ case "--help":
3791
+ return { ok: false, error: ADD_USAGE };
3792
+ default:
3793
+ if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
3794
+ ${ADD_USAGE}` };
3795
+ positional.push(a);
3796
+ }
3797
+ }
3798
+ if (positional.length !== 1) {
3799
+ return { ok: false, error: `Expected exactly one server slug, got ${positional.length}.
3800
+ ${ADD_USAGE}` };
3801
+ }
3802
+ opts.slug = positional[0];
3803
+ if (Object.keys(env).length > 0) opts.envOverrides = env;
3804
+ return { ok: true, options: opts };
3805
+ }
3806
+ async function runAdd(opts) {
3807
+ const out = opts.out ?? ((s) => process.stdout.write(s));
3808
+ const err = opts.err ?? ((s) => process.stderr.write(s));
3809
+ const print = (s = "") => out(`${s}
3810
+ `);
3811
+ const printErr = (s) => err(`${s}
3812
+ `);
3813
+ if (!opts.slug) {
3814
+ printErr(ADD_USAGE);
3815
+ return { exitCode: 2, written: [] };
3816
+ }
3817
+ const slug = opts.slug;
3818
+ if (!SLUG_RE.test(slug)) {
3819
+ printErr(`yaw-mcp add: invalid slug "${slug}" (lowercase letters, digits, and dashes only).`);
3820
+ return { exitCode: 2, written: [] };
3821
+ }
3822
+ const env = opts.env ?? process.env;
3823
+ const home = opts.home ?? homedir7();
3824
+ const cwd = opts.cwd ?? process.cwd();
3825
+ let server;
3826
+ try {
3827
+ server = await resolveCatalogSlug(slug, {
3828
+ catalogUrl: opts.catalogUrl ?? env.YAW_MCP_CATALOG_URL,
3829
+ fetchCatalog: opts.fetchCatalog
3830
+ });
3831
+ } catch (e) {
3832
+ printErr(`yaw-mcp add: ${e.message}`);
3833
+ return { exitCode: 1, written: [] };
3834
+ }
3835
+ const namespace = deriveNamespace(server.name);
3836
+ const supplied = { ...env, ...opts.envOverrides ?? {} };
3837
+ const missing = server.requiredEnvKeys.filter((k) => !supplied[k] || supplied[k] === "");
3838
+ if (missing.length > 0) {
3839
+ printErr(`yaw-mcp add: ${server.name} needs the following env var(s) before it can run:`);
3840
+ for (const k of missing) printErr(` - ${k}`);
3841
+ printErr("");
3842
+ printErr("Provide them with --env KEY=value (repeatable) or your shell, then re-run:");
3843
+ printErr(` yaw-mcp add ${slug} ${missing.map((k) => `--env ${k}=...`).join(" ")}`);
3844
+ if (server.docUrl) printErr(`Docs: ${server.docUrl}`);
3845
+ return { exitCode: 1, written: [] };
3846
+ }
3847
+ const entryEnv = {};
3848
+ for (const k of server.requiredEnvKeys) entryEnv[k] = "";
3849
+ for (const [k, v] of Object.entries(opts.envOverrides ?? {})) entryEnv[k] = v;
3850
+ const entry = {
3851
+ id: `local-${namespace}`,
3852
+ name: server.name,
3853
+ namespace,
3854
+ type: "local",
3855
+ transport: "stdio",
3856
+ command: server.command,
3857
+ args: server.args,
3858
+ env: Object.keys(entryEnv).length > 0 ? entryEnv : void 0,
3859
+ isActive: true,
3860
+ description: server.description
3861
+ };
3862
+ if (opts.dryRun) {
3863
+ if (opts.json) {
3864
+ print(JSON.stringify({ ok: true, dryRun: true, namespace, entry }, null, 2));
3865
+ } else {
3866
+ print(`yaw-mcp add (dry-run): would write ${server.name} as namespace "${namespace}"`);
3867
+ print(` command: ${entry.command} ${(entry.args ?? []).join(" ")}`);
3868
+ if (entry.env) print(` env keys: ${Object.keys(entry.env).join(", ")}`);
3869
+ }
3870
+ return { exitCode: 0, written: [] };
3871
+ }
3872
+ let res;
3873
+ try {
3874
+ res = await upsertUserBundle(entry, { home });
3875
+ } catch (e) {
3876
+ printErr(`yaw-mcp add: ${e.message}`);
3877
+ return { exitCode: 1, written: [] };
3878
+ }
3879
+ if (opts.json) {
3880
+ print(JSON.stringify({ ok: true, namespace, path: res.path, replaced: res.replaced, entry }, null, 2));
3881
+ } else {
3882
+ print(`${res.replaced ? "Updated" : "Added"} ${server.name} (namespace "${namespace}") in ${res.path}`);
3883
+ print("Restart your MCP client (or yaw-mcp) to pick it up.");
3884
+ }
3885
+ const shadow = await findShadowingProjectBundles(cwd, home).catch(() => null);
3886
+ if (shadow) {
3887
+ printErr(
3888
+ `Note: ${shadow} overrides your user-global bundles.json, so this entry won't load until you add it there or remove that file.`
3889
+ );
3890
+ }
3891
+ return { exitCode: 0, written: [res.path] };
3892
+ }
3893
+ var REMOVE_USAGE = `Usage: yaw-mcp remove <slug-or-namespace>
3894
+
3895
+ Remove a server from your local ~/.yaw-mcp/bundles.json. Accepts either the
3896
+ catalog slug it was added with (e.g. "brave-search") or its namespace as
3897
+ shown by \`yaw-mcp list\` (e.g. "bravesearch"). No-op if it isn't present.`;
3898
+ var REMOVE_TARGET_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
3899
+ function parseRemoveArgs(argv) {
3900
+ if (argv.length === 0) return { ok: false, error: REMOVE_USAGE };
3901
+ const positional = [];
3902
+ for (const a of argv) {
3903
+ if (a === "-h" || a === "--help") return { ok: false, error: REMOVE_USAGE };
3904
+ if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
3905
+ ${REMOVE_USAGE}` };
3906
+ positional.push(a);
3907
+ }
3908
+ if (positional.length !== 1) {
3909
+ return { ok: false, error: `Expected exactly one slug or namespace.
3910
+ ${REMOVE_USAGE}` };
3911
+ }
3912
+ return { ok: true, options: { target: positional[0] } };
3913
+ }
3914
+ async function runRemove(opts) {
3915
+ const out = opts.out ?? ((s) => process.stdout.write(s));
3916
+ const err = opts.err ?? ((s) => process.stderr.write(s));
3917
+ const print = (s = "") => out(`${s}
3918
+ `);
3919
+ const printErr = (s) => err(`${s}
3920
+ `);
3921
+ if (!opts.target) {
3922
+ printErr(REMOVE_USAGE);
3923
+ return { exitCode: 2, written: [] };
3924
+ }
3925
+ if (!REMOVE_TARGET_RE.test(opts.target)) {
3926
+ printErr(`yaw-mcp remove: "${opts.target}" isn't a valid slug or namespace.`);
3927
+ return { exitCode: 2, written: [] };
3928
+ }
3929
+ const home = opts.home ?? homedir7();
3930
+ const cwd = opts.cwd ?? process.cwd();
3931
+ const derived = deriveNamespace(opts.target);
3932
+ const candidates = derived === opts.target ? [opts.target] : [opts.target, derived];
3933
+ let res = null;
3934
+ let matched = "";
3935
+ try {
3936
+ for (const ns of candidates) {
3937
+ res = await removeUserBundle(ns, { home });
3938
+ if (res.removed) {
3939
+ matched = ns;
3940
+ break;
3941
+ }
3942
+ }
3943
+ } catch (e) {
3944
+ printErr(`yaw-mcp remove: ${e.message}`);
3945
+ return { exitCode: 1, written: [] };
3946
+ }
3947
+ if (!res || !res.removed) {
3948
+ print(`yaw-mcp remove: no server matching "${opts.target}" in ${res?.path ?? "bundles.json"} (nothing to do).`);
3949
+ const shadow2 = await findShadowingProjectBundles(cwd, home).catch(() => null);
3950
+ if (shadow2) {
3951
+ printErr(
3952
+ `Note: a project-local ${shadow2} is in effect; \`remove\` only manages your user-global bundles.json, so a server defined there must be removed from that file directly.`
3953
+ );
3954
+ }
3955
+ return { exitCode: 0, written: [] };
3956
+ }
3957
+ print(`Removed "${matched}" from ${res.path}. Restart your MCP client to apply.`);
3958
+ const shadow = await findShadowingProjectBundles(cwd, home).catch(() => null);
3959
+ if (shadow) {
3960
+ printErr(
3961
+ `Note: ${shadow} shadows your user-global bundles.json; a server defined there is unaffected by this removal.`
3962
+ );
3963
+ }
3964
+ return { exitCode: 0, written: [res.path] };
3965
+ }
3966
+ var LIST_USAGE = `Usage: yaw-mcp list [--json]
3967
+
3968
+ List the MCP servers yaw-mcp loads locally from bundles.json (the
3969
+ project-local file wins over user-global). --json for machine output.`;
3970
+ function parseListArgs(argv) {
3971
+ const opts = {};
3972
+ for (const a of argv) {
3973
+ if (a === "-h" || a === "--help") return { ok: false, error: LIST_USAGE };
3974
+ if (a === "--json") {
3975
+ opts.json = true;
3976
+ continue;
3977
+ }
3978
+ return { ok: false, error: `Unknown argument: ${a}
3979
+ ${LIST_USAGE}` };
3980
+ }
3981
+ return { ok: true, options: opts };
3982
+ }
3983
+ async function runList(opts) {
3984
+ const out = opts.out ?? ((s) => process.stdout.write(s));
3985
+ const print = (s = "") => out(`${s}
3986
+ `);
3987
+ const home = opts.home ?? homedir7();
3988
+ const cwd = opts.cwd ?? process.cwd();
3989
+ const loaded = await loadLocalBundles({ home, cwd });
3990
+ const servers = loaded.config?.servers ?? [];
3991
+ if (opts.json) {
3992
+ print(JSON.stringify({ path: loaded.path, servers }, null, 2));
3993
+ return { exitCode: 0, written: [] };
3994
+ }
3995
+ if (servers.length === 0) {
3996
+ print("No local servers configured. Add one with `yaw-mcp add <slug>`");
3997
+ print("(browse the catalog at https://yaw.sh/mcp/catalog/).");
3998
+ return { exitCode: 0, written: [] };
3999
+ }
4000
+ const rows = [...servers].sort((a, b) => a.namespace.localeCompare(b.namespace));
4001
+ const cols = [
4002
+ ["NAMESPACE", (s) => s.namespace],
4003
+ ["NAME", (s) => s.name],
4004
+ ["STATUS", (s) => s.isActive ? "active" : "disabled"],
4005
+ ["LAUNCH", (s) => [s.command, ...s.args ?? []].filter(Boolean).join(" ") || s.url || ""]
4006
+ ];
4007
+ const widths = cols.map(([h, get]) => Math.max(h.length, ...rows.map((r) => get(r).length)));
4008
+ const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
4009
+ print(fmt(cols.map(([h]) => h)));
4010
+ for (const r of rows) print(fmt(cols.map(([, get]) => get(r))));
4011
+ if (loaded.path) print(`
4012
+ ${servers.length} server${servers.length === 1 ? "" : "s"} in ${loaded.path}`);
4013
+ return { exitCode: 0, written: [] };
4014
+ }
4015
+
3442
4016
  // src/login-cmd.ts
3443
4017
  var LOGIN_USAGE = `Usage: yaw-mcp login --key <license-key>
3444
4018
 
@@ -3557,15 +4131,18 @@ async function runLogout(opts = {}, io = {
3557
4131
  }
3558
4132
 
3559
4133
  // src/nag.ts
3560
- import { readFile as readFile6 } from "fs/promises";
3561
- import { homedir as homedir6 } from "os";
3562
- import { join as join7 } from "path";
4134
+ import { readFile as readFile7 } from "fs/promises";
4135
+ import { homedir as homedir8 } from "os";
4136
+ import { join as join8 } from "path";
3563
4137
  var NAG_STATE_FILENAME = "nag-state.json";
3564
4138
  var MIN_THRESHOLD = 2;
3565
4139
  var MAX_THRESHOLD = 4;
3566
4140
  var FLOOR_MS = 36 * 60 * 60 * 1e3;
3567
4141
  var NAG_ELIGIBLE_SUBCOMMANDS = /* @__PURE__ */ new Set([
3568
4142
  "install",
4143
+ "add",
4144
+ "remove",
4145
+ "list",
3569
4146
  "doctor",
3570
4147
  "servers",
3571
4148
  "bundles",
@@ -3583,15 +4160,15 @@ var NAG_ELIGIBLE_SUBCOMMANDS = /* @__PURE__ */ new Set([
3583
4160
  function emptyNagState() {
3584
4161
  return { touchPoints: 0, nextThreshold: MIN_THRESHOLD, lastShownAt: 0 };
3585
4162
  }
3586
- function nagStatePath(home = homedir6()) {
3587
- return join7(home, CONFIG_DIRNAME, NAG_STATE_FILENAME);
4163
+ function nagStatePath(home = homedir8()) {
4164
+ return join8(home, CONFIG_DIRNAME, NAG_STATE_FILENAME);
3588
4165
  }
3589
4166
  function isFileNotFound2(err) {
3590
4167
  return !!err && typeof err === "object" && "code" in err && err.code === "ENOENT";
3591
4168
  }
3592
4169
  async function loadNagState(filePath = nagStatePath()) {
3593
4170
  try {
3594
- const raw = await readFile6(filePath, "utf8");
4171
+ const raw = await readFile7(filePath, "utf8");
3595
4172
  const parsed = JSON.parse(raw);
3596
4173
  if (!parsed || typeof parsed !== "object") return emptyNagState();
3597
4174
  const p = parsed;
@@ -3691,8 +4268,8 @@ async function showNagInterstitial(opts = {}) {
3691
4268
 
3692
4269
  // src/reset-learning-cmd.ts
3693
4270
  import { unlink as unlink2 } from "fs/promises";
3694
- import { homedir as homedir7 } from "os";
3695
- import { join as join8 } from "path";
4271
+ import { homedir as homedir9 } from "os";
4272
+ import { join as join9 } from "path";
3696
4273
  var RESET_LEARNING_USAGE = `Usage: yaw-mcp reset-learning
3697
4274
 
3698
4275
  Delete ~/.yaw-mcp/state.json so cross-session learning starts fresh.
@@ -3714,7 +4291,7 @@ ${RESET_LEARNING_USAGE}`
3714
4291
  return { kind: "ok", options: {} };
3715
4292
  }
3716
4293
  async function runResetLearning(opts = {}) {
3717
- const home = opts.home ?? homedir7();
4294
+ const home = opts.home ?? homedir9();
3718
4295
  const env = opts.env ?? process.env;
3719
4296
  const write = opts.out ?? ((s) => process.stdout.write(s));
3720
4297
  const writeErr = opts.err ?? ((s) => process.stderr.write(s));
@@ -3729,7 +4306,7 @@ async function runResetLearning(opts = {}) {
3729
4306
  writeErr(`${s}
3730
4307
  `);
3731
4308
  };
3732
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
4309
+ const filePath = join9(userConfigDir(home), STATE_FILENAME);
3733
4310
  const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3734
4311
  const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3735
4312
  if (disabled) {
@@ -3762,16 +4339,16 @@ function isFileNotFound3(err) {
3762
4339
  }
3763
4340
 
3764
4341
  // src/secrets-cmd.ts
3765
- import { existsSync as existsSync5 } from "fs";
4342
+ import { existsSync as existsSync6 } from "fs";
3766
4343
  import { mkdir as mkdir3 } from "fs/promises";
3767
- import { homedir as homedir9 } from "os";
4344
+ import { homedir as homedir11 } from "os";
3768
4345
  import { dirname as dirname3 } from "path";
3769
4346
 
3770
4347
  // src/secrets-vault.ts
3771
- import { existsSync as existsSync4 } from "fs";
3772
- import { chmod as chmod3, readFile as readFile7 } from "fs/promises";
3773
- import { homedir as homedir8 } from "os";
3774
- import { dirname as dirname2, join as join9 } from "path";
4348
+ import { existsSync as existsSync5 } from "fs";
4349
+ import { chmod as chmod3, readFile as readFile8 } from "fs/promises";
4350
+ import { homedir as homedir10 } from "os";
4351
+ import { dirname as dirname2, join as join10 } from "path";
3775
4352
 
3776
4353
  // src/secrets-crypto.ts
3777
4354
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -3829,8 +4406,8 @@ function decryptEntry(entry, key) {
3829
4406
  // src/secrets-vault.ts
3830
4407
  var SECRETS_FILENAME = "secrets.json";
3831
4408
  var SECRETS_SCHEMA_VERSION = 1;
3832
- function vaultPath(home = homedir8()) {
3833
- return join9(home, CONFIG_DIRNAME, SECRETS_FILENAME);
4409
+ function vaultPath(home = homedir10()) {
4410
+ return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
3834
4411
  }
3835
4412
  function emptyVault() {
3836
4413
  return {
@@ -3840,10 +4417,10 @@ function emptyVault() {
3840
4417
  };
3841
4418
  }
3842
4419
  async function loadVault(path3) {
3843
- if (!existsSync4(path3)) return null;
4420
+ if (!existsSync5(path3)) return null;
3844
4421
  let raw;
3845
4422
  try {
3846
- raw = await readFile7(path3, "utf8");
4423
+ raw = await readFile8(path3, "utf8");
3847
4424
  } catch (err) {
3848
4425
  log("warn", "Failed to read vault", { path: path3, error: err instanceof Error ? err.message : String(err) });
3849
4426
  return null;
@@ -4097,13 +4674,13 @@ async function readStdinValue(io) {
4097
4674
  }
4098
4675
  async function ensureVaultDir(path3) {
4099
4676
  const dir = dirname3(path3);
4100
- if (!existsSync5(dir)) await mkdir3(dir, { recursive: true });
4677
+ if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
4101
4678
  }
4102
4679
  async function runSecrets(opts, io = {
4103
4680
  out: (s) => process.stdout.write(s),
4104
4681
  err: (s) => process.stderr.write(s)
4105
4682
  }) {
4106
- const home = opts.home ?? homedir9();
4683
+ const home = opts.home ?? homedir11();
4107
4684
  const path3 = vaultPath(home);
4108
4685
  if (opts.action === "lock") {
4109
4686
  lock();
@@ -4121,7 +4698,7 @@ async function runSecrets(opts, io = {
4121
4698
  if (opts.action === "list") {
4122
4699
  const vault2 = await loadVault(path3);
4123
4700
  const keys = vault2 ? listKeys(vault2) : [];
4124
- if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync5(path3), keys }, null, 2)}
4701
+ if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path3), keys }, null, 2)}
4125
4702
  `);
4126
4703
  else if (!vault2) io.out(`No vault at ${path3}. Run \`yaw-mcp secrets set <name>\` to create one.
4127
4704
  `);
@@ -4136,7 +4713,7 @@ async function runSecrets(opts, io = {
4136
4713
  return { exitCode: 0 };
4137
4714
  }
4138
4715
  let vault = await loadVault(path3) ?? newVault();
4139
- const isFresh = !existsSync5(path3);
4716
+ const isFresh = !existsSync6(path3);
4140
4717
  const passphrase = await resolvePassphrase(opts);
4141
4718
  if (passphrase === null) {
4142
4719
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -4231,7 +4808,7 @@ async function runSecrets(opts, io = {
4231
4808
  }
4232
4809
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
4233
4810
  async function runSecretsPush(opts, io) {
4234
- const home = opts.home ?? homedir9();
4811
+ const home = opts.home ?? homedir11();
4235
4812
  const path3 = vaultPath(home);
4236
4813
  const session = await getSession({ home, baseUrl: opts.baseUrl });
4237
4814
  if (!session) {
@@ -4294,7 +4871,7 @@ async function runSecretsPush(opts, io) {
4294
4871
  }
4295
4872
  }
4296
4873
  async function runSecretsPull(opts, io) {
4297
- const home = opts.home ?? homedir9();
4874
+ const home = opts.home ?? homedir11();
4298
4875
  const path3 = vaultPath(home);
4299
4876
  const session = await getSession({ home, baseUrl: opts.baseUrl });
4300
4877
  if (!session) {
@@ -4350,7 +4927,7 @@ async function runSecretsPull(opts, io) {
4350
4927
 
4351
4928
  // src/server.ts
4352
4929
  import { readFile as readFile10 } from "fs/promises";
4353
- import { homedir as homedir11 } from "os";
4930
+ import { homedir as homedir12 } from "os";
4354
4931
  import { isAbsolute, relative, resolve as resolve4 } from "path";
4355
4932
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4356
4933
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4550,7 +5127,7 @@ Or re-run with --run to upgrade in place.`);
4550
5127
  return { exitCode: 3, lines };
4551
5128
  }
4552
5129
  function readCurrentVersion() {
4553
- return true ? "0.58.4" : "dev";
5130
+ return true ? "0.59.1" : "dev";
4554
5131
  }
4555
5132
 
4556
5133
  // src/auto-upgrade.ts
@@ -4598,7 +5175,7 @@ function defaultSpawn2(cmd, args) {
4598
5175
  async function maybeAutoUpgrade(deps = {}) {
4599
5176
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
4600
5177
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
4601
- const current = deps.currentVersion ?? (true ? "0.58.4" : "dev");
5178
+ const current = deps.currentVersion ?? (true ? "0.59.1" : "dev");
4602
5179
  if (current === "dev") return;
4603
5180
  const method = detectInstallMethod(deps.argvPath ?? process.argv[1]);
4604
5181
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -4969,13 +5546,13 @@ function stepBindingKey(step, index) {
4969
5546
  }
4970
5547
 
4971
5548
  // src/guide.ts
4972
- import { readFile as readFile8 } from "fs/promises";
5549
+ import { readFile as readFile9 } from "fs/promises";
4973
5550
  var GUIDE_READ_TIMEOUT_MS = 1e3;
4974
5551
  async function readGuide(path3, scope) {
4975
5552
  let raw;
4976
5553
  try {
4977
5554
  raw = await Promise.race([
4978
- readFile8(path3, "utf8"),
5555
+ readFile9(path3, "utf8"),
4979
5556
  new Promise(
4980
5557
  (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
4981
5558
  )
@@ -5257,132 +5834,6 @@ var LearningStore = class {
5257
5834
  }
5258
5835
  };
5259
5836
 
5260
- // src/local-bundles.ts
5261
- import { createHash as createHash2 } from "crypto";
5262
- import { readFile as readFile9 } from "fs/promises";
5263
- import { homedir as homedir10 } from "os";
5264
- import { join as join10 } from "path";
5265
- var BUNDLES_FILENAME = "bundles.json";
5266
- var CURRENT_BUNDLES_SCHEMA_VERSION = 1;
5267
- function localBundlesPath(configDir) {
5268
- return join10(configDir, BUNDLES_FILENAME);
5269
- }
5270
- var NAMESPACE_RE = /^[a-z][a-z0-9_]{0,29}$/;
5271
- function validateEntry(entry, warnings) {
5272
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5273
- warnings.push("bundles.json: skipping non-object server entry");
5274
- return null;
5275
- }
5276
- const e = entry;
5277
- const namespace = typeof e.namespace === "string" ? e.namespace : "";
5278
- if (!namespace || !NAMESPACE_RE.test(namespace)) {
5279
- warnings.push(`bundles.json: skipping server with invalid namespace ${JSON.stringify(namespace)}`);
5280
- return null;
5281
- }
5282
- const name = typeof e.name === "string" && e.name.length > 0 ? e.name : namespace;
5283
- const type = e.type === "remote" ? "remote" : "local";
5284
- const transport = e.transport === "streamable-http" || e.transport === "sse" || e.transport === "stdio" ? e.transport : void 0;
5285
- const command = typeof e.command === "string" ? e.command : void 0;
5286
- const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
5287
- const env = e.env && typeof e.env === "object" && !Array.isArray(e.env) ? Object.fromEntries(
5288
- Object.entries(e.env).filter(([, v]) => typeof v === "string")
5289
- ) : void 0;
5290
- const url = typeof e.url === "string" ? e.url : void 0;
5291
- const description = typeof e.description === "string" ? e.description : void 0;
5292
- const isActive = e.isActive !== false;
5293
- const id = typeof e.id === "string" && e.id.length > 0 ? e.id : `local-${namespace}`;
5294
- return {
5295
- id,
5296
- name,
5297
- namespace,
5298
- type,
5299
- transport,
5300
- command,
5301
- args,
5302
- env,
5303
- url,
5304
- isActive,
5305
- description
5306
- };
5307
- }
5308
- async function readBundlesAt(path3, warnings) {
5309
- let raw;
5310
- try {
5311
- raw = await readFile9(path3, "utf8");
5312
- } catch {
5313
- return { exists: false, file: null };
5314
- }
5315
- let parsed;
5316
- try {
5317
- parsed = parseJsonc(raw);
5318
- } catch (err) {
5319
- const msg = err instanceof Error ? err.message : String(err);
5320
- warnings.push(`${path3}: invalid JSON (${msg}) -- file ignored`);
5321
- log("warn", "bundles.json is not valid JSON; ignoring", { path: path3, error: msg });
5322
- return { exists: true, file: null };
5323
- }
5324
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5325
- warnings.push(`${path3}: root must be a JSON object -- file ignored`);
5326
- return { exists: true, file: null };
5327
- }
5328
- const obj = parsed;
5329
- const version = typeof obj.version === "number" ? obj.version : void 0;
5330
- if (version !== void 0 && version > CURRENT_BUNDLES_SCHEMA_VERSION) {
5331
- warnings.push(
5332
- `${path3}: schema version ${version} is newer than this yaw-mcp (${CURRENT_BUNDLES_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
5333
- );
5334
- }
5335
- const rawServers = obj.servers;
5336
- if (!Array.isArray(rawServers)) {
5337
- warnings.push(`${path3}: 'servers' must be an array -- file ignored`);
5338
- return { exists: true, file: null };
5339
- }
5340
- return {
5341
- exists: true,
5342
- file: { version, servers: rawServers }
5343
- };
5344
- }
5345
- function hashContent(servers) {
5346
- const h = createHash2("sha256");
5347
- h.update(JSON.stringify(servers));
5348
- return `local-${h.digest("hex").slice(0, 16)}`;
5349
- }
5350
- async function loadLocalBundles(opts = {}) {
5351
- const cwd = opts.cwd ?? process.cwd();
5352
- const home = opts.home ?? homedir10();
5353
- const warnings = [];
5354
- const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
5355
- const projectPath = projectDir ? localBundlesPath(projectDir) : null;
5356
- const globalPath = localBundlesPath(join10(home, CONFIG_DIRNAME));
5357
- const projectResult = projectPath ? await readBundlesAt(projectPath, warnings) : { exists: false, file: null };
5358
- let file;
5359
- let sourcePath;
5360
- if (projectResult.exists) {
5361
- file = projectResult.file;
5362
- sourcePath = projectPath;
5363
- } else {
5364
- const globalResult = await readBundlesAt(globalPath, warnings);
5365
- file = globalResult.file;
5366
- sourcePath = globalResult.exists ? globalPath : null;
5367
- }
5368
- if (!file) {
5369
- return { config: null, path: sourcePath, warnings };
5370
- }
5371
- const servers = [];
5372
- for (const raw of file.servers) {
5373
- const validated = validateEntry(raw, warnings);
5374
- if (validated) servers.push(validated);
5375
- }
5376
- return {
5377
- config: {
5378
- servers,
5379
- configVersion: hashContent(servers)
5380
- },
5381
- path: sourcePath,
5382
- warnings
5383
- };
5384
- }
5385
-
5386
5837
  // src/meta-tools.ts
5387
5838
  var META_TOOLS = {
5388
5839
  discover: {
@@ -6993,7 +7444,7 @@ function categorizeSpawnError(err) {
6993
7444
  }
6994
7445
  async function connectToUpstream(config, onDisconnect, onListChanged) {
6995
7446
  const client = new Client(
6996
- { name: "yaw-mcp", version: true ? "0.58.4" : "dev" },
7447
+ { name: "yaw-mcp", version: true ? "0.59.1" : "dev" },
6997
7448
  { capabilities: {} }
6998
7449
  );
6999
7450
  let transport;
@@ -7302,7 +7753,7 @@ var ConnectServer = class _ConnectServer {
7302
7753
  this.apiUrl = apiUrl5;
7303
7754
  this.token = token5;
7304
7755
  this.server = new Server(
7305
- { name: "yaw-mcp", version: true ? "0.58.4" : "dev" },
7756
+ { name: "yaw-mcp", version: true ? "0.59.1" : "dev" },
7306
7757
  {
7307
7758
  capabilities: {
7308
7759
  tools: { listChanged: true },
@@ -8833,7 +9284,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
8833
9284
  }
8834
9285
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
8835
9286
  try {
8836
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve4(homedir11(), filepath.slice(2)) : resolve4(filepath);
9287
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve4(homedir12(), filepath.slice(2)) : resolve4(filepath);
8837
9288
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
8838
9289
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
8839
9290
  return {
@@ -8850,7 +9301,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
8850
9301
  const rel = relative(base, p);
8851
9302
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
8852
9303
  };
8853
- if (!isUnder(homedir11(), resolved) && !isUnder(process.cwd(), resolved)) {
9304
+ if (!isUnder(homedir12(), resolved) && !isUnder(process.cwd(), resolved)) {
8854
9305
  return {
8855
9306
  content: [
8856
9307
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -9504,7 +9955,7 @@ async function runServersCommand(opts = {}) {
9504
9955
  writeErr(`${s}
9505
9956
  `);
9506
9957
  };
9507
- const config = await loadMcphConfig({
9958
+ const config = await loadYawMcpConfig({
9508
9959
  cwd: opts.cwd,
9509
9960
  home: opts.home,
9510
9961
  env: opts.env
@@ -9584,7 +10035,7 @@ function truncateVersion(v) {
9584
10035
  }
9585
10036
 
9586
10037
  // src/stats-cmd.ts
9587
- import { homedir as homedir12 } from "os";
10038
+ import { homedir as homedir13 } from "os";
9588
10039
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
9589
10040
 
9590
10041
  Print a digest of recent AI tool calls recorded against your Yaw
@@ -9699,7 +10150,7 @@ async function runStats(opts, io = {
9699
10150
  out: (s) => process.stdout.write(s),
9700
10151
  err: (s) => process.stderr.write(s)
9701
10152
  }) {
9702
- const home = opts.home ?? homedir12();
10153
+ const home = opts.home ?? homedir13();
9703
10154
  const session = await getSession({ home, baseUrl: opts.baseUrl });
9704
10155
  if (!session) {
9705
10156
  const msg = "Not signed in. Yaw MCP analytics requires a Yaw Team account.\n - Yaw Team: $15/seat/mo or $150/seat/yr -- https://yaw.sh/mcp\nSign in with: yaw-mcp login --key <license-key>";
@@ -9756,9 +10207,9 @@ async function runStats(opts, io = {
9756
10207
  }
9757
10208
 
9758
10209
  // src/sync-cmd.ts
9759
- import { existsSync as existsSync6 } from "fs";
10210
+ import { existsSync as existsSync7 } from "fs";
9760
10211
  import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
9761
- import { homedir as homedir13 } from "os";
10212
+ import { homedir as homedir14 } from "os";
9762
10213
  import { dirname as dirname4, join as join11 } from "path";
9763
10214
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
9764
10215
 
@@ -9806,7 +10257,7 @@ function bundlesPath(home) {
9806
10257
  }
9807
10258
  async function readLocalBundles(home) {
9808
10259
  const path3 = bundlesPath(home);
9809
- if (!existsSync6(path3)) return { version: 1, servers: [] };
10260
+ if (!existsSync7(path3)) return { version: 1, servers: [] };
9810
10261
  const raw = await readFile11(path3, "utf8");
9811
10262
  const parsed = JSON.parse(raw);
9812
10263
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.servers)) {
@@ -9844,7 +10295,7 @@ async function runSync(opts, io = {
9844
10295
  out: (s) => process.stdout.write(s),
9845
10296
  err: (s) => process.stderr.write(s)
9846
10297
  }) {
9847
- const home = opts.home ?? homedir13();
10298
+ const home = opts.home ?? homedir14();
9848
10299
  const session = await getSession({ home, baseUrl: opts.baseUrl });
9849
10300
  if (!session) {
9850
10301
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -9987,6 +10438,9 @@ function handleSyncError(err, opts, io) {
9987
10438
  var KNOWN_SUBCOMMANDS = [
9988
10439
  "compliance",
9989
10440
  "install",
10441
+ "add",
10442
+ "remove",
10443
+ "list",
9990
10444
  "doctor",
9991
10445
  "reset-learning",
9992
10446
  "servers",
@@ -10012,7 +10466,7 @@ if (subcommand && NAG_ELIGIBLE_SUBCOMMANDS.has(subcommand) && process.env.YAW_MC
10012
10466
  let inAccountMode = envHasToken;
10013
10467
  if (!inAccountMode) {
10014
10468
  try {
10015
- const cfg = await loadMcphConfig();
10469
+ const cfg = await loadYawMcpConfig();
10016
10470
  inAccountMode = Boolean(cfg.token);
10017
10471
  } catch {
10018
10472
  inAccountMode = false;
@@ -10121,6 +10575,30 @@ if (subcommand === "compliance") {
10121
10575
  process.exit(2);
10122
10576
  }
10123
10577
  runTryCleanup(parsed.options).then((r) => process.exit(r.exitCode));
10578
+ } else if (subcommand === "add") {
10579
+ const parsed = parseAddArgs(process.argv.slice(3));
10580
+ if (!parsed.ok) {
10581
+ process.stderr.write(`${parsed.error}
10582
+ `);
10583
+ process.exit(2);
10584
+ }
10585
+ runAdd(parsed.options).then((r) => process.exit(r.exitCode));
10586
+ } else if (subcommand === "remove") {
10587
+ const parsed = parseRemoveArgs(process.argv.slice(3));
10588
+ if (!parsed.ok) {
10589
+ process.stderr.write(`${parsed.error}
10590
+ `);
10591
+ process.exit(2);
10592
+ }
10593
+ runRemove(parsed.options).then((r) => process.exit(r.exitCode));
10594
+ } else if (subcommand === "list") {
10595
+ const parsed = parseListArgs(process.argv.slice(3));
10596
+ if (!parsed.ok) {
10597
+ process.stderr.write(`${parsed.error}
10598
+ `);
10599
+ process.exit(2);
10600
+ }
10601
+ runList(parsed.options).then((r) => process.exit(r.exitCode));
10124
10602
  } else if (subcommand === "login") {
10125
10603
  const parsed = parseLoginArgs(process.argv.slice(3));
10126
10604
  if (!parsed.ok) {
@@ -10170,16 +10648,26 @@ if (subcommand === "compliance") {
10170
10648
  2. Install yaw-mcp yaw-mcp install claude-code --token mcp_pat_...
10171
10649
  3. Verify setup yaw-mcp doctor
10172
10650
 
10173
- Setup:
10174
- install <client> Configure one MCP client to launch yaw-mcp.
10175
- <client> is one of: claude-code, claude-desktop,
10176
- cursor, vscode.
10651
+ Setup (connect a client to yaw-mcp):
10652
+ install <client> Connect one MCP client to yaw-mcp. This wires the
10653
+ aggregator into the client; it does NOT add a
10654
+ server (for that, see \`add\` below). <client> is
10655
+ one of: claude-code, claude-desktop, cursor, vscode.
10177
10656
  install --list List which MCP clients are installed on this
10178
10657
  machine (read-only; no writes).
10179
10658
  install --all Configure every installed MCP client in one go.
10180
- try <slug> Wire a one-off trial of an upstream MCP server
10181
- into your AI client. No account needed; expires
10182
- after --ttl (default 1h). Doctor GCs it after.
10659
+
10660
+ Local servers (no account):
10661
+ add <slug> Add an MCP server from the yaw.sh/mcp catalog to
10662
+ your local ~/.yaw-mcp/bundles.json so yaw-mcp loads
10663
+ it. Pass required env with --env KEY=value.
10664
+ remove <slug> Remove a server (by slug or namespace) from
10665
+ bundles.json.
10666
+ list List the servers yaw-mcp loads locally.
10667
+ try <slug> Wire a one-off trial of a catalog MCP server
10668
+ directly into your AI client (bypassing yaw-mcp).
10669
+ No account needed; expires after --ttl (default
10670
+ 1h). Doctor GCs it after.
10183
10671
  try-cleanup <slug> Remove a wired trial early.
10184
10672
 
10185
10673
  Inspection:
@@ -10200,6 +10688,17 @@ if (subcommand === "compliance") {
10200
10688
  fish, or powershell. Redirect to your
10201
10689
  completions directory to install.
10202
10690
 
10691
+ Account / sync (Pro + Team):
10692
+ login Authenticate this machine with a Yaw MCP account
10693
+ (Pro/Team). --key <license> to pass the key inline.
10694
+ logout Sign this machine out of the account.
10695
+ sync <push|pull|status> Replicate your local bundles.json to/from the
10696
+ account store (env values stripped on push).
10697
+ secrets <action> Manage synced secret VALUES: set, get, list,
10698
+ remove, lock, push, pull.
10699
+ stats Show your account usage statistics
10700
+ (--limit, --days, --json).
10701
+
10203
10702
  Other:
10204
10703
  compliance <target> Run the 88-test compliance suite against an MCP
10205
10704
  server. --publish posts the report to
@@ -10230,8 +10729,10 @@ if (subcommand === "compliance") {
10230
10729
  upgraded in the background).
10231
10730
  YAW_MCP_PRUNE_RESPONSES Set to \`0\` to disable response pruning.
10232
10731
  YAW_MCP_DISABLE_PERSISTENCE Disable cross-session learning state.
10233
- YAW_MCP_BASE_URL Override the host \`yaw-mcp try\` queries for
10234
- /api/explore/:slug (default https://yaw.sh/mcp).
10732
+ YAW_MCP_CATALOG_URL Override the catalog \`add\`/\`try\` resolve slugs
10733
+ against (default https://yaw.sh/data/mcp-catalog.json).
10734
+ YAW_MCP_BASE_URL Base URL for \`yaw-mcp try\` signup/telemetry
10735
+ links (default https://yaw.sh/mcp).
10235
10736
 
10236
10737
  Config resolution (highest precedence first):
10237
10738
  1. YAW_MCP_TOKEN / YAW_MCP_URL env vars
@@ -10249,7 +10750,7 @@ if (subcommand === "compliance") {
10249
10750
  `);
10250
10751
  process.exit(0);
10251
10752
  } else if (subcommand === "--version" || subcommand === "-V") {
10252
- process.stdout.write(`yaw-mcp ${true ? "0.58.4" : "dev"}
10753
+ process.stdout.write(`yaw-mcp ${true ? "0.59.1" : "dev"}
10253
10754
  `);
10254
10755
  process.exit(0);
10255
10756
  } else if (subcommand && !subcommand.startsWith("-")) {
@@ -10263,7 +10764,7 @@ if (subcommand === "compliance") {
10263
10764
  runServer();
10264
10765
  }
10265
10766
  async function runServer() {
10266
- const config = await loadMcphConfig();
10767
+ const config = await loadYawMcpConfig();
10267
10768
  for (const w of config.warnings) {
10268
10769
  log("warn", "Config warning", { warning: w });
10269
10770
  }