@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.
- package/README.md +44 -4
- package/dist/index.js +835 -342
- 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
|
|
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
|
|
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
|
|
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]}/
|
|
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
|
-
|
|
661
|
-
{
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
{ name: "
|
|
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
|
-
|
|
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
|
|
801
|
+
complete -F _yaw-mcp yaw-mcp
|
|
755
802
|
`;
|
|
756
803
|
}
|
|
757
804
|
function renderZsh() {
|
|
758
|
-
const
|
|
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
|
|
785
|
-
# (e.g., ~/.zsh/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
1461
|
-
var
|
|
1462
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1883
|
+
const writeYawMcpConfig = !opts.skipYawMcpConfig && token5 !== null;
|
|
1751
1884
|
const home = opts.home ?? homedir3();
|
|
1752
|
-
const
|
|
1753
|
-
const
|
|
1754
|
-
if ("backupPath" in
|
|
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 ${
|
|
1889
|
+
`yaw-mcp install: existing ${yawMcpConfigPath} was malformed; original bytes backed up to ${yawMcpConfigComposed.backupPath} before overwriting.`
|
|
1757
1890
|
);
|
|
1758
1891
|
}
|
|
1759
|
-
const
|
|
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 (
|
|
1770
|
-
${
|
|
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 (
|
|
1909
|
+
if (legacyEntry) {
|
|
1777
1910
|
log2(
|
|
1778
|
-
`Note: legacy "${
|
|
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 (
|
|
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 (
|
|
1921
|
+
if (writeYawMcpConfig) {
|
|
1789
1922
|
try {
|
|
1790
|
-
await atomicWriteFile(
|
|
1923
|
+
await atomicWriteFile(yawMcpConfigPath, yawMcpConfigJson);
|
|
1791
1924
|
if (process.platform !== "win32") {
|
|
1792
1925
|
try {
|
|
1793
|
-
await chmod(
|
|
1926
|
+
await chmod(yawMcpConfigPath, 384);
|
|
1794
1927
|
} catch {
|
|
1795
1928
|
}
|
|
1796
1929
|
}
|
|
1797
1930
|
} catch (e) {
|
|
1798
|
-
err(`yaw-mcp install: failed to write ${
|
|
1931
|
+
err(`yaw-mcp install: failed to write ${yawMcpConfigPath}: ${e.message}`);
|
|
1799
1932
|
return { written: [], wouldWrite: [], messages, exitCode: 1 };
|
|
1800
1933
|
}
|
|
1801
|
-
log2(`Wrote ${
|
|
1802
|
-
written.push(
|
|
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 (
|
|
1957
|
+
if (legacyEntry) {
|
|
1825
1958
|
log2(
|
|
1826
|
-
`Note: legacy "${
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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>
|
|
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(
|
|
2352
|
-
const
|
|
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:
|
|
2383
|
-
name:
|
|
2384
|
-
command:
|
|
2385
|
-
args:
|
|
2386
|
-
requiredEnvVars:
|
|
2488
|
+
slug: resolved.slug,
|
|
2489
|
+
name: resolved.name,
|
|
2490
|
+
command: resolved.command,
|
|
2491
|
+
args: resolved.args,
|
|
2492
|
+
requiredEnvVars: resolved.requiredEnvKeys
|
|
2387
2493
|
};
|
|
2388
|
-
if (
|
|
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.
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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.
|
|
3120
|
-
return `OK \u2014 has "${ENTRY_NAME}" entry; legacy "${
|
|
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.
|
|
3226
|
+
if (c.hasMcpEntry) return `OK \u2014 has "${ENTRY_NAME}" entry`;
|
|
3123
3227
|
if (c.hasLegacyEntry) {
|
|
3124
|
-
return `legacy "${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3246
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3563
|
-
import { homedir as
|
|
3564
|
-
import { join as
|
|
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 =
|
|
3589
|
-
return
|
|
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
|
|
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
|
|
4229
|
+
const content = [
|
|
4230
|
+
"Yaw MCP -- support the project",
|
|
3655
4231
|
"",
|
|
3656
|
-
"
|
|
3657
|
-
"
|
|
3658
|
-
"
|
|
3659
|
-
"
|
|
3660
|
-
"
|
|
3661
|
-
"
|
|
3662
|
-
"
|
|
3663
|
-
"
|
|
3664
|
-
"
|
|
3665
|
-
"
|
|
3666
|
-
"
|
|
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
|
|
3703
|
-
import { join as
|
|
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 ??
|
|
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 =
|
|
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
|
|
4342
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3774
4343
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
3775
|
-
import { homedir as
|
|
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
|
|
3780
|
-
import { chmod as chmod3, readFile as
|
|
3781
|
-
import { homedir as
|
|
3782
|
-
import { dirname as dirname2, join as
|
|
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 =
|
|
3841
|
-
return
|
|
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 (!
|
|
4420
|
+
if (!existsSync5(path3)) return null;
|
|
3852
4421
|
let raw;
|
|
3853
4422
|
try {
|
|
3854
|
-
raw = await
|
|
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 (!
|
|
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 ??
|
|
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:
|
|
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 = !
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
9599
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
10210
|
+
import { existsSync as existsSync7 } from "fs";
|
|
9768
10211
|
import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
|
|
9769
|
-
import { homedir as
|
|
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
|
|
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 (!
|
|
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 ??
|
|
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
|
|
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>
|
|
10183
|
-
|
|
10184
|
-
|
|
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
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
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
|
-
|
|
10242
|
-
|
|
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.
|
|
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
|
|
10767
|
+
const config = await loadYawMcpConfig();
|
|
10275
10768
|
for (const w of config.warnings) {
|
|
10276
10769
|
log("warn", "Config warning", { warning: w });
|
|
10277
10770
|
}
|