@yawlabs/mcp 0.58.3 → 0.59.0

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 +835 -342
  3. package/package.json +2 -2
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.3" : "dev";
2880
+ var VERSION = true ? "0.59.0" : "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)");
@@ -2852,10 +2958,8 @@ async function runDoctor(opts = {}) {
2852
2958
  let exitCode = 0;
2853
2959
  if (config.token === null) {
2854
2960
  print("DIAGNOSIS");
2855
- print(" Local mode (Free) \u2014 no account token resolved. yaw-mcp runs fine and serves");
2961
+ print(" Local mode (Free) -- fully functional, no account needed. yaw-mcp serves");
2856
2962
  print(" whatever servers are configured locally in ~/.yaw-mcp/bundles.json.");
2857
- print(" Sign in with `yaw-mcp login` (or set YAW_MCP_TOKEN) to add account-synced");
2858
- print(" servers and compliance grades.");
2859
2963
  } else if (config.warnings.length > 0) {
2860
2964
  exitCode = 2;
2861
2965
  print("DIAGNOSIS");
@@ -2876,7 +2980,7 @@ async function runDoctorJson(opts) {
2876
2980
  const os = opts.os ?? CURRENT_OS;
2877
2981
  const env = opts.env ?? process.env;
2878
2982
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2879
- const config = await loadMcphConfig({ cwd, home, env });
2983
+ const config = await loadYawMcpConfig({ cwd, home, env });
2880
2984
  const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
2881
2985
  const clients = probeClients({ home, os, cwd, claudeConfigDir });
2882
2986
  const envVarNames = [
@@ -2930,7 +3034,7 @@ async function runDoctorJson(opts) {
2930
3034
  let exitCode = 0;
2931
3035
  let summary;
2932
3036
  if (config.token === null) {
2933
- summary = "Local mode (Free) \u2014 running without an account token. Sign in with `yaw-mcp login` for account-synced servers.";
3037
+ summary = "Local mode (Free) -- fully functional, no account needed.";
2934
3038
  } else if (config.warnings.length > 0) {
2935
3039
  exitCode = 2;
2936
3040
  summary = "Token present, but warnings need attention.";
@@ -3116,12 +3220,12 @@ function schemaSuffix(f) {
3116
3220
  function renderClientStatus(c, installCmd) {
3117
3221
  if (c.unavailable) return "unavailable on this OS";
3118
3222
  if (c.malformed) return "exists but JSON is malformed \u2014 fix or rerun `yaw-mcp install`";
3119
- if (c.hasMcphEntry && c.hasLegacyEntry) {
3120
- 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`;
3121
3225
  }
3122
- if (c.hasMcphEntry) return `OK \u2014 has "${ENTRY_NAME}" entry`;
3226
+ if (c.hasMcpEntry) return `OK \u2014 has "${ENTRY_NAME}" entry`;
3123
3227
  if (c.hasLegacyEntry) {
3124
- 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`;
3125
3229
  }
3126
3230
  if (c.exists) return `present, no "${ENTRY_NAME}" entry \u2014 run \`${installCmd}\``;
3127
3231
  return `not configured \u2014 run \`${installCmd}\``;
@@ -3136,8 +3240,9 @@ function probeClients(opts) {
3136
3240
  scope: target.scopes[0].scope,
3137
3241
  path: "(n/a)",
3138
3242
  exists: false,
3139
- hasMcphEntry: false,
3243
+ hasMcpEntry: false,
3140
3244
  hasLegacyEntry: false,
3245
+ legacyEntryName: null,
3141
3246
  malformed: false,
3142
3247
  unavailable: true
3143
3248
  });
@@ -3158,8 +3263,9 @@ function probeClients(opts) {
3158
3263
  continue;
3159
3264
  }
3160
3265
  const exists3 = existsSync3(resolved.absolute);
3161
- let hasMcphEntry = false;
3266
+ let hasMcpEntry = false;
3162
3267
  let hasLegacyEntry = false;
3268
+ let legacyEntryName = null;
3163
3269
  let malformed = false;
3164
3270
  if (exists3) {
3165
3271
  try {
@@ -3170,8 +3276,9 @@ function probeClients(opts) {
3170
3276
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3171
3277
  const container = walkContainer(parsed, resolved.containerPath);
3172
3278
  if (container) {
3173
- hasMcphEntry = ENTRY_NAME in container;
3174
- hasLegacyEntry = LEGACY_ENTRY_NAME in container;
3279
+ hasMcpEntry = ENTRY_NAME in container;
3280
+ legacyEntryName = findLegacyEntry(container);
3281
+ hasLegacyEntry = legacyEntryName !== null;
3175
3282
  }
3176
3283
  } else {
3177
3284
  malformed = true;
@@ -3186,8 +3293,9 @@ function probeClients(opts) {
3186
3293
  scope: scope.scope,
3187
3294
  path: resolved.absolute,
3188
3295
  exists: exists3,
3189
- hasMcphEntry,
3296
+ hasMcpEntry,
3190
3297
  hasLegacyEntry,
3298
+ legacyEntryName,
3191
3299
  malformed,
3192
3300
  unavailable: false
3193
3301
  });
@@ -3214,8 +3322,9 @@ async function probeClientsAsync(opts) {
3214
3322
  scope: target.scopes[0].scope,
3215
3323
  path: "(n/a)",
3216
3324
  exists: false,
3217
- hasMcphEntry: false,
3325
+ hasMcpEntry: false,
3218
3326
  hasLegacyEntry: false,
3327
+ legacyEntryName: null,
3219
3328
  malformed: false,
3220
3329
  unavailable: true
3221
3330
  });
@@ -3231,8 +3340,9 @@ async function probeClientsAsync(opts) {
3231
3340
  claudeConfigDir: opts.claudeConfigDir
3232
3341
  });
3233
3342
  const exists3 = existsSync3(resolved.absolute);
3234
- let hasMcphEntry = false;
3343
+ let hasMcpEntry = false;
3235
3344
  let hasLegacyEntry = false;
3345
+ let legacyEntryName = null;
3236
3346
  let malformed = false;
3237
3347
  if (exists3) {
3238
3348
  try {
@@ -3242,8 +3352,9 @@ async function probeClientsAsync(opts) {
3242
3352
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3243
3353
  const container = walkContainer(parsed, resolved.containerPath);
3244
3354
  if (container) {
3245
- hasMcphEntry = ENTRY_NAME in container;
3246
- hasLegacyEntry = LEGACY_ENTRY_NAME in container;
3355
+ hasMcpEntry = ENTRY_NAME in container;
3356
+ legacyEntryName = findLegacyEntry(container);
3357
+ hasLegacyEntry = legacyEntryName !== null;
3247
3358
  }
3248
3359
  } else {
3249
3360
  malformed = true;
@@ -3258,8 +3369,9 @@ async function probeClientsAsync(opts) {
3258
3369
  scope: scope.scope,
3259
3370
  path: resolved.absolute,
3260
3371
  exists: exists3,
3261
- hasMcphEntry,
3372
+ hasMcpEntry,
3262
3373
  hasLegacyEntry,
3374
+ legacyEntryName,
3263
3375
  malformed,
3264
3376
  unavailable: false
3265
3377
  });
@@ -3441,10 +3553,470 @@ function closestNames(query, candidates, limit) {
3441
3553
  return scored.slice(0, limit).map((s) => s.name);
3442
3554
  }
3443
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
+
3444
4016
  // src/login-cmd.ts
3445
4017
  var LOGIN_USAGE = `Usage: yaw-mcp login --key <license-key>
3446
4018
 
3447
- Sign in to your Yaw Team or Yaw MCP Pro account. Your license
4019
+ Sign in to your Yaw Team account. Your license
3448
4020
  key was emailed after purchase.
3449
4021
 
3450
4022
  --key <license-key> Required. The license key from your purchase email.
@@ -3520,7 +4092,7 @@ async function runLogin(opts, io = {
3520
4092
  // src/logout-cmd.ts
3521
4093
  var LOGOUT_USAGE = `Usage: yaw-mcp logout
3522
4094
 
3523
- Sign out of your Yaw Team or Yaw MCP Pro account. Clears the
4095
+ Sign out of your Yaw Team account. Clears the
3524
4096
  local session cookie at ~/.yaw-mcp/team-session.json. Free mode
3525
4097
  resumes on the next yaw-mcp invocation if no YAW_MCP_TOKEN is set.
3526
4098
 
@@ -3559,15 +4131,18 @@ async function runLogout(opts = {}, io = {
3559
4131
  }
3560
4132
 
3561
4133
  // src/nag.ts
3562
- import { readFile as readFile6 } from "fs/promises";
3563
- import { homedir as homedir6 } from "os";
3564
- 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";
3565
4137
  var NAG_STATE_FILENAME = "nag-state.json";
3566
4138
  var MIN_THRESHOLD = 2;
3567
4139
  var MAX_THRESHOLD = 4;
3568
4140
  var FLOOR_MS = 36 * 60 * 60 * 1e3;
3569
4141
  var NAG_ELIGIBLE_SUBCOMMANDS = /* @__PURE__ */ new Set([
3570
4142
  "install",
4143
+ "add",
4144
+ "remove",
4145
+ "list",
3571
4146
  "doctor",
3572
4147
  "servers",
3573
4148
  "bundles",
@@ -3585,15 +4160,15 @@ var NAG_ELIGIBLE_SUBCOMMANDS = /* @__PURE__ */ new Set([
3585
4160
  function emptyNagState() {
3586
4161
  return { touchPoints: 0, nextThreshold: MIN_THRESHOLD, lastShownAt: 0 };
3587
4162
  }
3588
- function nagStatePath(home = homedir6()) {
3589
- return join7(home, CONFIG_DIRNAME, NAG_STATE_FILENAME);
4163
+ function nagStatePath(home = homedir8()) {
4164
+ return join8(home, CONFIG_DIRNAME, NAG_STATE_FILENAME);
3590
4165
  }
3591
4166
  function isFileNotFound2(err) {
3592
4167
  return !!err && typeof err === "object" && "code" in err && err.code === "ENOENT";
3593
4168
  }
3594
4169
  async function loadNagState(filePath = nagStatePath()) {
3595
4170
  try {
3596
- const raw = await readFile6(filePath, "utf8");
4171
+ const raw = await readFile7(filePath, "utf8");
3597
4172
  const parsed = JSON.parse(raw);
3598
4173
  if (!parsed || typeof parsed !== "object") return emptyNagState();
3599
4174
  const p = parsed;
@@ -3651,33 +4226,27 @@ async function showNagInterstitial(opts = {}) {
3651
4226
  const stdin = opts.stdin ?? process.stdin;
3652
4227
  const tty = opts.isTTY ?? (stdout.isTTY === true && stdin.isTTY === true);
3653
4228
  if (!tty) return;
3654
- const lines = [
4229
+ const content = [
4230
+ "Yaw MCP -- support the project",
3655
4231
  "",
3656
- "+----------------------------------------------------------+",
3657
- "| Yaw MCP -- support the project |",
3658
- "+----------------------------------------------------------+",
3659
- "| |",
3660
- "| You're using Yaw MCP free. |",
3661
- "| |",
3662
- "| Pro ($5/mo or $50/yr) adds: |",
3663
- "| * sync bundles + secrets across machines |",
3664
- "| * encrypted secret vault (never logged) |",
3665
- "| * 90-day analytics on AI tool usage |",
3666
- "| * `yaw-mcp stats` command |",
3667
- "| |",
3668
- "| Yaw Team ($15/seat/mo or $150/seat/yr) adds: |",
3669
- "| * everything in Pro, per seat |",
3670
- "| * shared team bundles |",
3671
- "| * shared org secrets |",
3672
- "| * per-seat audit log |",
3673
- "| * SSO |",
3674
- "| |",
3675
- "| Learn more: https://yaw.sh/mcp |",
3676
- "| |",
3677
- "| Press Enter to continue (Ctrl-C to quit). |",
3678
- "+----------------------------------------------------------+",
3679
- ""
4232
+ "You're using Yaw MCP free -- all features included.",
4233
+ "",
4234
+ "Working with a team? Yaw Team ($15/seat/mo or",
4235
+ "$150/seat/yr) adds:",
4236
+ " * shared team bundles across every seat",
4237
+ " * shared org secrets",
4238
+ " * per-seat audit log",
4239
+ " * SSO",
4240
+ "",
4241
+ "Learn more: https://yaw.sh/mcp",
4242
+ "",
4243
+ "Press Enter to continue (Ctrl-C to quit)."
3680
4244
  ];
4245
+ const PAD = 2;
4246
+ const inner = Math.max(...content.map((l) => l.length));
4247
+ const border = `+${"-".repeat(inner + PAD * 2)}+`;
4248
+ const boxed = content.map((l) => `|${" ".repeat(PAD)}${l.padEnd(inner)}${" ".repeat(PAD)}|`);
4249
+ const lines = ["", border, ...boxed, border, ""];
3681
4250
  stdout.write(`${lines.join("\n")}
3682
4251
  `);
3683
4252
  await new Promise((resolve5) => {
@@ -3699,8 +4268,8 @@ async function showNagInterstitial(opts = {}) {
3699
4268
 
3700
4269
  // src/reset-learning-cmd.ts
3701
4270
  import { unlink as unlink2 } from "fs/promises";
3702
- import { homedir as homedir7 } from "os";
3703
- import { join as join8 } from "path";
4271
+ import { homedir as homedir9 } from "os";
4272
+ import { join as join9 } from "path";
3704
4273
  var RESET_LEARNING_USAGE = `Usage: yaw-mcp reset-learning
3705
4274
 
3706
4275
  Delete ~/.yaw-mcp/state.json so cross-session learning starts fresh.
@@ -3722,7 +4291,7 @@ ${RESET_LEARNING_USAGE}`
3722
4291
  return { kind: "ok", options: {} };
3723
4292
  }
3724
4293
  async function runResetLearning(opts = {}) {
3725
- const home = opts.home ?? homedir7();
4294
+ const home = opts.home ?? homedir9();
3726
4295
  const env = opts.env ?? process.env;
3727
4296
  const write = opts.out ?? ((s) => process.stdout.write(s));
3728
4297
  const writeErr = opts.err ?? ((s) => process.stderr.write(s));
@@ -3737,7 +4306,7 @@ async function runResetLearning(opts = {}) {
3737
4306
  writeErr(`${s}
3738
4307
  `);
3739
4308
  };
3740
- const filePath = join8(userConfigDir(home), STATE_FILENAME);
4309
+ const filePath = join9(userConfigDir(home), STATE_FILENAME);
3741
4310
  const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
3742
4311
  const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
3743
4312
  if (disabled) {
@@ -3770,16 +4339,16 @@ function isFileNotFound3(err) {
3770
4339
  }
3771
4340
 
3772
4341
  // src/secrets-cmd.ts
3773
- import { existsSync as existsSync5 } from "fs";
4342
+ import { existsSync as existsSync6 } from "fs";
3774
4343
  import { mkdir as mkdir3 } from "fs/promises";
3775
- import { homedir as homedir9 } from "os";
4344
+ import { homedir as homedir11 } from "os";
3776
4345
  import { dirname as dirname3 } from "path";
3777
4346
 
3778
4347
  // src/secrets-vault.ts
3779
- import { existsSync as existsSync4 } from "fs";
3780
- import { chmod as chmod3, readFile as readFile7 } from "fs/promises";
3781
- import { homedir as homedir8 } from "os";
3782
- 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";
3783
4352
 
3784
4353
  // src/secrets-crypto.ts
3785
4354
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -3837,8 +4406,8 @@ function decryptEntry(entry, key) {
3837
4406
  // src/secrets-vault.ts
3838
4407
  var SECRETS_FILENAME = "secrets.json";
3839
4408
  var SECRETS_SCHEMA_VERSION = 1;
3840
- function vaultPath(home = homedir8()) {
3841
- return join9(home, CONFIG_DIRNAME, SECRETS_FILENAME);
4409
+ function vaultPath(home = homedir10()) {
4410
+ return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
3842
4411
  }
3843
4412
  function emptyVault() {
3844
4413
  return {
@@ -3848,10 +4417,10 @@ function emptyVault() {
3848
4417
  };
3849
4418
  }
3850
4419
  async function loadVault(path3) {
3851
- if (!existsSync4(path3)) return null;
4420
+ if (!existsSync5(path3)) return null;
3852
4421
  let raw;
3853
4422
  try {
3854
- raw = await readFile7(path3, "utf8");
4423
+ raw = await readFile8(path3, "utf8");
3855
4424
  } catch (err) {
3856
4425
  log("warn", "Failed to read vault", { path: path3, error: err instanceof Error ? err.message : String(err) });
3857
4426
  return null;
@@ -4105,13 +4674,13 @@ async function readStdinValue(io) {
4105
4674
  }
4106
4675
  async function ensureVaultDir(path3) {
4107
4676
  const dir = dirname3(path3);
4108
- if (!existsSync5(dir)) await mkdir3(dir, { recursive: true });
4677
+ if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
4109
4678
  }
4110
4679
  async function runSecrets(opts, io = {
4111
4680
  out: (s) => process.stdout.write(s),
4112
4681
  err: (s) => process.stderr.write(s)
4113
4682
  }) {
4114
- const home = opts.home ?? homedir9();
4683
+ const home = opts.home ?? homedir11();
4115
4684
  const path3 = vaultPath(home);
4116
4685
  if (opts.action === "lock") {
4117
4686
  lock();
@@ -4129,7 +4698,7 @@ async function runSecrets(opts, io = {
4129
4698
  if (opts.action === "list") {
4130
4699
  const vault2 = await loadVault(path3);
4131
4700
  const keys = vault2 ? listKeys(vault2) : [];
4132
- 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)}
4133
4702
  `);
4134
4703
  else if (!vault2) io.out(`No vault at ${path3}. Run \`yaw-mcp secrets set <name>\` to create one.
4135
4704
  `);
@@ -4144,7 +4713,7 @@ async function runSecrets(opts, io = {
4144
4713
  return { exitCode: 0 };
4145
4714
  }
4146
4715
  let vault = await loadVault(path3) ?? newVault();
4147
- const isFresh = !existsSync5(path3);
4716
+ const isFresh = !existsSync6(path3);
4148
4717
  const passphrase = await resolvePassphrase(opts);
4149
4718
  if (passphrase === null) {
4150
4719
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -4239,7 +4808,7 @@ async function runSecrets(opts, io = {
4239
4808
  }
4240
4809
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
4241
4810
  async function runSecretsPush(opts, io) {
4242
- const home = opts.home ?? homedir9();
4811
+ const home = opts.home ?? homedir11();
4243
4812
  const path3 = vaultPath(home);
4244
4813
  const session = await getSession({ home, baseUrl: opts.baseUrl });
4245
4814
  if (!session) {
@@ -4302,7 +4871,7 @@ async function runSecretsPush(opts, io) {
4302
4871
  }
4303
4872
  }
4304
4873
  async function runSecretsPull(opts, io) {
4305
- const home = opts.home ?? homedir9();
4874
+ const home = opts.home ?? homedir11();
4306
4875
  const path3 = vaultPath(home);
4307
4876
  const session = await getSession({ home, baseUrl: opts.baseUrl });
4308
4877
  if (!session) {
@@ -4358,7 +4927,7 @@ async function runSecretsPull(opts, io) {
4358
4927
 
4359
4928
  // src/server.ts
4360
4929
  import { readFile as readFile10 } from "fs/promises";
4361
- import { homedir as homedir11 } from "os";
4930
+ import { homedir as homedir12 } from "os";
4362
4931
  import { isAbsolute, relative, resolve as resolve4 } from "path";
4363
4932
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4364
4933
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -4558,7 +5127,7 @@ Or re-run with --run to upgrade in place.`);
4558
5127
  return { exitCode: 3, lines };
4559
5128
  }
4560
5129
  function readCurrentVersion() {
4561
- return true ? "0.58.3" : "dev";
5130
+ return true ? "0.59.0" : "dev";
4562
5131
  }
4563
5132
 
4564
5133
  // src/auto-upgrade.ts
@@ -4606,7 +5175,7 @@ function defaultSpawn2(cmd, args) {
4606
5175
  async function maybeAutoUpgrade(deps = {}) {
4607
5176
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
4608
5177
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
4609
- const current = deps.currentVersion ?? (true ? "0.58.3" : "dev");
5178
+ const current = deps.currentVersion ?? (true ? "0.59.0" : "dev");
4610
5179
  if (current === "dev") return;
4611
5180
  const method = detectInstallMethod(deps.argvPath ?? process.argv[1]);
4612
5181
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -4977,13 +5546,13 @@ function stepBindingKey(step, index) {
4977
5546
  }
4978
5547
 
4979
5548
  // src/guide.ts
4980
- import { readFile as readFile8 } from "fs/promises";
5549
+ import { readFile as readFile9 } from "fs/promises";
4981
5550
  var GUIDE_READ_TIMEOUT_MS = 1e3;
4982
5551
  async function readGuide(path3, scope) {
4983
5552
  let raw;
4984
5553
  try {
4985
5554
  raw = await Promise.race([
4986
- readFile8(path3, "utf8"),
5555
+ readFile9(path3, "utf8"),
4987
5556
  new Promise(
4988
5557
  (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
4989
5558
  )
@@ -5133,7 +5702,7 @@ async function reportHeartbeat(clientName, clientVersion, isRefresh = false) {
5133
5702
  lastLoggedFailureStatus = null;
5134
5703
  lastLoggedErrorMessage = null;
5135
5704
  if (!isRefresh) {
5136
- log("info", "Reported AI client connect to mcp.hosting", {
5705
+ log("info", "Reported AI client connect to Yaw MCP", {
5137
5706
  clientName: clientName ?? null,
5138
5707
  clientVersion: clientVersion ?? null
5139
5708
  });
@@ -5265,137 +5834,11 @@ var LearningStore = class {
5265
5834
  }
5266
5835
  };
5267
5836
 
5268
- // src/local-bundles.ts
5269
- import { createHash as createHash2 } from "crypto";
5270
- import { readFile as readFile9 } from "fs/promises";
5271
- import { homedir as homedir10 } from "os";
5272
- import { join as join10 } from "path";
5273
- var BUNDLES_FILENAME = "bundles.json";
5274
- var CURRENT_BUNDLES_SCHEMA_VERSION = 1;
5275
- function localBundlesPath(configDir) {
5276
- return join10(configDir, BUNDLES_FILENAME);
5277
- }
5278
- var NAMESPACE_RE = /^[a-z][a-z0-9_]{0,29}$/;
5279
- function validateEntry(entry, warnings) {
5280
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5281
- warnings.push("bundles.json: skipping non-object server entry");
5282
- return null;
5283
- }
5284
- const e = entry;
5285
- const namespace = typeof e.namespace === "string" ? e.namespace : "";
5286
- if (!namespace || !NAMESPACE_RE.test(namespace)) {
5287
- warnings.push(`bundles.json: skipping server with invalid namespace ${JSON.stringify(namespace)}`);
5288
- return null;
5289
- }
5290
- const name = typeof e.name === "string" && e.name.length > 0 ? e.name : namespace;
5291
- const type = e.type === "remote" ? "remote" : "local";
5292
- const transport = e.transport === "streamable-http" || e.transport === "sse" || e.transport === "stdio" ? e.transport : void 0;
5293
- const command = typeof e.command === "string" ? e.command : void 0;
5294
- const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
5295
- const env = e.env && typeof e.env === "object" && !Array.isArray(e.env) ? Object.fromEntries(
5296
- Object.entries(e.env).filter(([, v]) => typeof v === "string")
5297
- ) : void 0;
5298
- const url = typeof e.url === "string" ? e.url : void 0;
5299
- const description = typeof e.description === "string" ? e.description : void 0;
5300
- const isActive = e.isActive !== false;
5301
- const id = typeof e.id === "string" && e.id.length > 0 ? e.id : `local-${namespace}`;
5302
- return {
5303
- id,
5304
- name,
5305
- namespace,
5306
- type,
5307
- transport,
5308
- command,
5309
- args,
5310
- env,
5311
- url,
5312
- isActive,
5313
- description
5314
- };
5315
- }
5316
- async function readBundlesAt(path3, warnings) {
5317
- let raw;
5318
- try {
5319
- raw = await readFile9(path3, "utf8");
5320
- } catch {
5321
- return { exists: false, file: null };
5322
- }
5323
- let parsed;
5324
- try {
5325
- parsed = parseJsonc(raw);
5326
- } catch (err) {
5327
- const msg = err instanceof Error ? err.message : String(err);
5328
- warnings.push(`${path3}: invalid JSON (${msg}) -- file ignored`);
5329
- log("warn", "bundles.json is not valid JSON; ignoring", { path: path3, error: msg });
5330
- return { exists: true, file: null };
5331
- }
5332
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5333
- warnings.push(`${path3}: root must be a JSON object -- file ignored`);
5334
- return { exists: true, file: null };
5335
- }
5336
- const obj = parsed;
5337
- const version = typeof obj.version === "number" ? obj.version : void 0;
5338
- if (version !== void 0 && version > CURRENT_BUNDLES_SCHEMA_VERSION) {
5339
- warnings.push(
5340
- `${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.`
5341
- );
5342
- }
5343
- const rawServers = obj.servers;
5344
- if (!Array.isArray(rawServers)) {
5345
- warnings.push(`${path3}: 'servers' must be an array -- file ignored`);
5346
- return { exists: true, file: null };
5347
- }
5348
- return {
5349
- exists: true,
5350
- file: { version, servers: rawServers }
5351
- };
5352
- }
5353
- function hashContent(servers) {
5354
- const h = createHash2("sha256");
5355
- h.update(JSON.stringify(servers));
5356
- return `local-${h.digest("hex").slice(0, 16)}`;
5357
- }
5358
- async function loadLocalBundles(opts = {}) {
5359
- const cwd = opts.cwd ?? process.cwd();
5360
- const home = opts.home ?? homedir10();
5361
- const warnings = [];
5362
- const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
5363
- const projectPath = projectDir ? localBundlesPath(projectDir) : null;
5364
- const globalPath = localBundlesPath(join10(home, CONFIG_DIRNAME));
5365
- const projectResult = projectPath ? await readBundlesAt(projectPath, warnings) : { exists: false, file: null };
5366
- let file;
5367
- let sourcePath;
5368
- if (projectResult.exists) {
5369
- file = projectResult.file;
5370
- sourcePath = projectPath;
5371
- } else {
5372
- const globalResult = await readBundlesAt(globalPath, warnings);
5373
- file = globalResult.file;
5374
- sourcePath = globalResult.exists ? globalPath : null;
5375
- }
5376
- if (!file) {
5377
- return { config: null, path: sourcePath, warnings };
5378
- }
5379
- const servers = [];
5380
- for (const raw of file.servers) {
5381
- const validated = validateEntry(raw, warnings);
5382
- if (validated) servers.push(validated);
5383
- }
5384
- return {
5385
- config: {
5386
- servers,
5387
- configVersion: hashContent(servers)
5388
- },
5389
- path: sourcePath,
5390
- warnings
5391
- };
5392
- }
5393
-
5394
5837
  // src/meta-tools.ts
5395
5838
  var META_TOOLS = {
5396
5839
  discover: {
5397
5840
  name: "mcp_connect_discover",
5398
- description: 'List the MCP servers installed on the user\'s mcp.hosting account and ready to use. Call this when browsing what\'s available or when the task isn\'t specific yet. If the task is already clear ("file a github issue", "query postgres", "post to slack"), prefer `mcp_connect_dispatch` \u2014 it picks the right server and loads its tools in one call. Load only the servers the CURRENT task needs; each one adds tools to your context. Shows names, namespaces, tool counts, a token-cost estimate per server (e.g. "22 tools, ~2.8k tokens") so you can budget context before activating \u2014 tilde values are estimates based on cached tool metadata, unprefixed values reflect live tool schemas. Scored servers carry an inline `[A]`\u2013`[F]` compliance grade from the mcp.hosting test suite \u2014 treat it as a trust signal and prefer higher-graded alternatives when otherwise equivalent (ungraded servers are unmarked, not penalized). Also surfaces whether each server is loaded, any local CLI it shadows (prefer the MCP tools over the CLI when a shadow is listed), and usage hints ("used Nx" or "often loaded with X") when the signals are present (counts persist across yaw-mcp restarts). Recurring packs that have been loaded together \u22652 times get their own block at the top with a ready-to-run `activate` call \u2014 skip the extra `mcp_connect_suggest` round-trip when the signal is already there. If a `yaw-mcp://guide` resource is listed, read it FIRST: it carries project/user-specific routing rules and credential conventions that override generic defaults.',
5841
+ description: 'List the MCP servers installed on the user\'s Yaw MCP account and ready to use. Call this when browsing what\'s available or when the task isn\'t specific yet. If the task is already clear ("file a github issue", "query postgres", "post to slack"), prefer `mcp_connect_dispatch` \u2014 it picks the right server and loads its tools in one call. Load only the servers the CURRENT task needs; each one adds tools to your context. Shows names, namespaces, tool counts, a token-cost estimate per server (e.g. "22 tools, ~2.8k tokens") so you can budget context before activating \u2014 tilde values are estimates based on cached tool metadata, unprefixed values reflect live tool schemas. Scored servers carry an inline `[A]`\u2013`[F]` compliance grade from the Yaw MCP test suite \u2014 treat it as a trust signal and prefer higher-graded alternatives when otherwise equivalent (ungraded servers are unmarked, not penalized). Also surfaces whether each server is loaded, any local CLI it shadows (prefer the MCP tools over the CLI when a shadow is listed), and usage hints ("used Nx" or "often loaded with X") when the signals are present (counts persist across yaw-mcp restarts). Recurring packs that have been loaded together \u22652 times get their own block at the top with a ready-to-run `activate` call \u2014 skip the extra `mcp_connect_suggest` round-trip when the signal is already there. If a `yaw-mcp://guide` resource is listed, read it FIRST: it carries project/user-specific routing rules and credential conventions that override generic defaults.',
5399
5842
  inputSchema: {
5400
5843
  type: "object",
5401
5844
  properties: {
@@ -5531,7 +5974,7 @@ var META_TOOLS = {
5531
5974
  },
5532
5975
  install: {
5533
5976
  name: "mcp_connect_install",
5534
- description: 'Install a new MCP server on the user\'s mcp.hosting account so it shows up in `mcp_connect_discover` and is ready to use. Call this when the user asks to install/add a server they don\'t already have (check `mcp_connect_discover` first \u2014 if the namespace is already listed, the server is already installed; use `mcp_connect_activate` to load its tools into this session). Fill the install spec from your knowledge of the server: for most official Model Context Protocol servers this is `{ type: "local", command: "npx", args: ["-y", "@modelcontextprotocol/server-<name>"] }`; for uvx/python it\'s `{ command: "uvx", args: ["mcp-server-<name>"] }`; for remote HTTP it\'s `{ type: "remote", url: "https://..." }`. Namespace must match /^[a-z][a-z0-9_]{0,29}$/ and must not collide with one the user already has. If the server needs secrets (API tokens, etc.) pass them in `env` \u2014 they are stored encrypted and never logged. On 403 with `code: "plan_limit_exceeded"` the user is on the free tier cap (3 servers); surface the returned error body verbatim so they see the upgrade URL. After install yaw-mcp auto-refreshes its server list \u2014 the new namespace becomes callable without a restart.',
5977
+ description: 'Install a new MCP server on the user\'s Yaw MCP account so it shows up in `mcp_connect_discover` and is ready to use. Call this when the user asks to install/add a server they don\'t already have (check `mcp_connect_discover` first \u2014 if the namespace is already listed, the server is already installed; use `mcp_connect_activate` to load its tools into this session). Fill the install spec from your knowledge of the server: for most official Model Context Protocol servers this is `{ type: "local", command: "npx", args: ["-y", "@modelcontextprotocol/server-<name>"] }`; for uvx/python it\'s `{ command: "uvx", args: ["mcp-server-<name>"] }`; for remote HTTP it\'s `{ type: "remote", url: "https://..." }`. Namespace must match /^[a-z][a-z0-9_]{0,29}$/ and must not collide with one the user already has. If the server needs secrets (API tokens, etc.) pass them in `env` \u2014 they are stored encrypted and never logged. On 403 with `code: "plan_limit_exceeded"` the user is on the free tier cap (3 servers); surface the returned error body verbatim so they see the upgrade URL. After install yaw-mcp auto-refreshes its server list \u2014 the new namespace becomes callable without a restart.',
5535
5978
  inputSchema: {
5536
5979
  type: "object",
5537
5980
  properties: {
@@ -6522,7 +6965,7 @@ async function probe(name, p) {
6522
6965
  // node/npx/uvx arrive as `npx.cmd` in PATH, and native spawn
6523
6966
  // with shell:false only resolves .exe. Without this, probes
6524
6967
  // falsely report `npx: false` on every Windows machine and
6525
- // mcp.hosting's Test button pre-flight short-circuits with
6968
+ // Yaw MCP's Test button pre-flight short-circuits with
6526
6969
  // "npx not detected" even though upstream activation (which
6527
6970
  // goes through cross-spawn in the MCP SDK) would work fine.
6528
6971
  // All probe args are fixed `--version` strings with no shell
@@ -6602,7 +7045,7 @@ async function reportRuntimes() {
6602
7045
  if (res.statusCode >= 400 && res.statusCode !== 404) {
6603
7046
  log("warn", "Runtime report failed", { status: res.statusCode });
6604
7047
  } else {
6605
- log("info", "Reported runtimes to mcp.hosting", { runtimes });
7048
+ log("info", "Reported runtimes to Yaw MCP", { runtimes });
6606
7049
  }
6607
7050
  } catch (err) {
6608
7051
  log("warn", "Runtime report error", { error: err?.message });
@@ -7001,7 +7444,7 @@ function categorizeSpawnError(err) {
7001
7444
  }
7002
7445
  async function connectToUpstream(config, onDisconnect, onListChanged) {
7003
7446
  const client = new Client(
7004
- { name: "yaw-mcp", version: true ? "0.58.3" : "dev" },
7447
+ { name: "yaw-mcp", version: true ? "0.59.0" : "dev" },
7005
7448
  { capabilities: {} }
7006
7449
  );
7007
7450
  let transport;
@@ -7310,7 +7753,7 @@ var ConnectServer = class _ConnectServer {
7310
7753
  this.apiUrl = apiUrl5;
7311
7754
  this.token = token5;
7312
7755
  this.server = new Server(
7313
- { name: "yaw-mcp", version: true ? "0.58.3" : "dev" },
7756
+ { name: "yaw-mcp", version: true ? "0.59.0" : "dev" },
7314
7757
  {
7315
7758
  capabilities: {
7316
7759
  tools: { listChanged: true },
@@ -7375,7 +7818,7 @@ var ConnectServer = class _ConnectServer {
7375
7818
  activationFailures = /* @__PURE__ */ new Map();
7376
7819
  // Session-scoped credential overrides supplied by the user via MCP
7377
7820
  // elicitation when a server's stderr indicated a missing env var.
7378
- // Cleared on shutdown — persistence belongs in the mcp.hosting
7821
+ // Cleared on shutdown — persistence belongs in the Yaw MCP
7379
7822
  // dashboard, these are a "get me running now" shortcut.
7380
7823
  elicitedEnv = /* @__PURE__ */ new Map();
7381
7824
  // In-flight activation promises, keyed by namespace. Dedupes
@@ -8841,7 +9284,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
8841
9284
  }
8842
9285
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
8843
9286
  try {
8844
- 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);
8845
9288
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
8846
9289
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
8847
9290
  return {
@@ -8858,7 +9301,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
8858
9301
  const rel = relative(base, p);
8859
9302
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
8860
9303
  };
8861
- if (!isUnder(homedir11(), resolved) && !isUnder(process.cwd(), resolved)) {
9304
+ if (!isUnder(homedir12(), resolved) && !isUnder(process.cwd(), resolved)) {
8862
9305
  return {
8863
9306
  content: [
8864
9307
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -8948,7 +9391,7 @@ Use mcp_connect_discover to see imported servers.`
8948
9391
  return { content: [{ type: "text", text: `Import error: ${err.message}` }], isError: true };
8949
9392
  }
8950
9393
  }
8951
- // Install a new MCP server on the user's mcp.hosting account. Validates
9394
+ // Install a new MCP server on the user's Yaw MCP account. Validates
8952
9395
  // via the shared buildInstallPayload helper so local/remote + namespace
8953
9396
  // shape errors fail here with a clear message instead of burning a
8954
9397
  // round-trip to the backend. On 403 plan-limit we forward the structured
@@ -9512,7 +9955,7 @@ async function runServersCommand(opts = {}) {
9512
9955
  writeErr(`${s}
9513
9956
  `);
9514
9957
  };
9515
- const config = await loadMcphConfig({
9958
+ const config = await loadYawMcpConfig({
9516
9959
  cwd: opts.cwd,
9517
9960
  home: opts.home,
9518
9961
  env: opts.env
@@ -9592,18 +10035,18 @@ function truncateVersion(v) {
9592
10035
  }
9593
10036
 
9594
10037
  // src/stats-cmd.ts
9595
- import { homedir as homedir12 } from "os";
10038
+ import { homedir as homedir13 } from "os";
9596
10039
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
9597
10040
 
9598
- Print a digest of recent AI tool calls recorded against your Yaw MCP
9599
- Pro or Yaw Team account.
10041
+ Print a digest of recent AI tool calls recorded against your Yaw
10042
+ Team account.
9600
10043
 
9601
10044
  --limit N Show the most recent N events (default 50, max 1000).
9602
10045
  --days N Restrict to events from the last N days (default 7).
9603
10046
  --json Emit machine-readable JSON (the full event list + summary).
9604
10047
 
9605
10048
  Requires sign-in: \`yaw-mcp login --key <license-key>\`. Free users
9606
- get a pointer to Pro instead -- analytics requires an account.`;
10049
+ get a pointer to Yaw Team instead -- analytics requires an account.`;
9607
10050
  function parseStatsArgs(argv) {
9608
10051
  const opts = {};
9609
10052
  for (let i = 0; i < argv.length; i++) {
@@ -9707,10 +10150,10 @@ async function runStats(opts, io = {
9707
10150
  out: (s) => process.stdout.write(s),
9708
10151
  err: (s) => process.stderr.write(s)
9709
10152
  }) {
9710
- const home = opts.home ?? homedir12();
10153
+ const home = opts.home ?? homedir13();
9711
10154
  const session = await getSession({ home, baseUrl: opts.baseUrl });
9712
10155
  if (!session) {
9713
- const msg = "Not signed in. Yaw MCP analytics requires a Pro or Yaw Team account.\n - Pro: $5/mo or $50/yr -- https://yaw.sh/mcp#pricing\n - Yaw Team: $15/seat/mo (includes Yaw Terminal team features)\nSign in with: yaw-mcp login --key <license-key>";
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>";
9714
10157
  if (opts.json) io.err(`${JSON.stringify({ ok: false, error: "Not signed in.", upsell: msg })}
9715
10158
  `);
9716
10159
  else io.err(`yaw-mcp stats: ${msg}
@@ -9764,14 +10207,14 @@ async function runStats(opts, io = {
9764
10207
  }
9765
10208
 
9766
10209
  // src/sync-cmd.ts
9767
- import { existsSync as existsSync6 } from "fs";
10210
+ import { existsSync as existsSync7 } from "fs";
9768
10211
  import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
9769
- import { homedir as homedir13 } from "os";
10212
+ import { homedir as homedir14 } from "os";
9770
10213
  import { dirname as dirname4, join as join11 } from "path";
9771
10214
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
9772
10215
 
9773
10216
  Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
9774
- Team or Yaw MCP Pro account.
10217
+ Team account.
9775
10218
 
9776
10219
  push Strip env values from the local bundles and upload the
9777
10220
  schema to mcp_bundles. Env values stay machine-local.
@@ -9814,7 +10257,7 @@ function bundlesPath(home) {
9814
10257
  }
9815
10258
  async function readLocalBundles(home) {
9816
10259
  const path3 = bundlesPath(home);
9817
- if (!existsSync6(path3)) return { version: 1, servers: [] };
10260
+ if (!existsSync7(path3)) return { version: 1, servers: [] };
9818
10261
  const raw = await readFile11(path3, "utf8");
9819
10262
  const parsed = JSON.parse(raw);
9820
10263
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.servers)) {
@@ -9852,7 +10295,7 @@ async function runSync(opts, io = {
9852
10295
  out: (s) => process.stdout.write(s),
9853
10296
  err: (s) => process.stderr.write(s)
9854
10297
  }) {
9855
- const home = opts.home ?? homedir13();
10298
+ const home = opts.home ?? homedir14();
9856
10299
  const session = await getSession({ home, baseUrl: opts.baseUrl });
9857
10300
  if (!session) {
9858
10301
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -9995,6 +10438,9 @@ function handleSyncError(err, opts, io) {
9995
10438
  var KNOWN_SUBCOMMANDS = [
9996
10439
  "compliance",
9997
10440
  "install",
10441
+ "add",
10442
+ "remove",
10443
+ "list",
9998
10444
  "doctor",
9999
10445
  "reset-learning",
10000
10446
  "servers",
@@ -10020,7 +10466,7 @@ if (subcommand && NAG_ELIGIBLE_SUBCOMMANDS.has(subcommand) && process.env.YAW_MC
10020
10466
  let inAccountMode = envHasToken;
10021
10467
  if (!inAccountMode) {
10022
10468
  try {
10023
- const cfg = await loadMcphConfig();
10469
+ const cfg = await loadYawMcpConfig();
10024
10470
  inAccountMode = Boolean(cfg.token);
10025
10471
  } catch {
10026
10472
  inAccountMode = false;
@@ -10129,6 +10575,30 @@ if (subcommand === "compliance") {
10129
10575
  process.exit(2);
10130
10576
  }
10131
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));
10132
10602
  } else if (subcommand === "login") {
10133
10603
  const parsed = parseLoginArgs(process.argv.slice(3));
10134
10604
  if (!parsed.ok) {
@@ -10178,16 +10648,26 @@ if (subcommand === "compliance") {
10178
10648
  2. Install yaw-mcp yaw-mcp install claude-code --token mcp_pat_...
10179
10649
  3. Verify setup yaw-mcp doctor
10180
10650
 
10181
- Setup:
10182
- install <client> Configure one MCP client to launch yaw-mcp.
10183
- <client> is one of: claude-code, claude-desktop,
10184
- 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.
10185
10656
  install --list List which MCP clients are installed on this
10186
10657
  machine (read-only; no writes).
10187
10658
  install --all Configure every installed MCP client in one go.
10188
- try <slug> Wire a one-off trial of an upstream MCP server
10189
- into your AI client. No account needed; expires
10190
- 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.
10191
10671
  try-cleanup <slug> Remove a wired trial early.
10192
10672
 
10193
10673
  Inspection:
@@ -10208,6 +10688,17 @@ if (subcommand === "compliance") {
10208
10688
  fish, or powershell. Redirect to your
10209
10689
  completions directory to install.
10210
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
+
10211
10702
  Other:
10212
10703
  compliance <target> Run the 88-test compliance suite against an MCP
10213
10704
  server. --publish posts the report to
@@ -10238,8 +10729,10 @@ if (subcommand === "compliance") {
10238
10729
  upgraded in the background).
10239
10730
  YAW_MCP_PRUNE_RESPONSES Set to \`0\` to disable response pruning.
10240
10731
  YAW_MCP_DISABLE_PERSISTENCE Disable cross-session learning state.
10241
- YAW_MCP_BASE_URL Override the host \`yaw-mcp try\` queries for
10242
- /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).
10243
10736
 
10244
10737
  Config resolution (highest precedence first):
10245
10738
  1. YAW_MCP_TOKEN / YAW_MCP_URL env vars
@@ -10257,7 +10750,7 @@ if (subcommand === "compliance") {
10257
10750
  `);
10258
10751
  process.exit(0);
10259
10752
  } else if (subcommand === "--version" || subcommand === "-V") {
10260
- process.stdout.write(`yaw-mcp ${true ? "0.58.3" : "dev"}
10753
+ process.stdout.write(`yaw-mcp ${true ? "0.59.0" : "dev"}
10261
10754
  `);
10262
10755
  process.exit(0);
10263
10756
  } else if (subcommand && !subcommand.startsWith("-")) {
@@ -10271,7 +10764,7 @@ if (subcommand === "compliance") {
10271
10764
  runServer();
10272
10765
  }
10273
10766
  async function runServer() {
10274
- const config = await loadMcphConfig();
10767
+ const config = await loadYawMcpConfig();
10275
10768
  for (const w of config.warnings) {
10276
10769
  log("warn", "Config warning", { warning: w });
10277
10770
  }