@yawlabs/mcp 0.60.6 → 0.63.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/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `@yawlabs/mcp` (formerly `@yawlabs/mcph`) are documented here. This project uses [semantic versioning](https://semver.org) and a script-gated release flow: `./release.sh <version>` runs lint + tests + build, bumps, tags, publishes to npm, and creates the GitHub release.
4
4
 
5
+ ## 0.63.0 -- CLI hardening: flag parsing, exit-code consistency, secret-file perms, dispatch error handling
6
+
7
+ A full-pass sweep of the `yaw-mcp` subcommand surface. Every change is a fix, a hardening, or additive; there are no breaking changes to the MCP server or the public CLI contract.
8
+
9
+ - **Value-taking flags no longer swallow a following flag.** `login --key`, `secrets set --value`, `try --base`, and `add --catalog` reject a dash-prefixed token instead of storing it as the value -- e.g. `login --key --json` no longer POSTs `"--json"` as the license key, and `try slug --base --dry-run` no longer silently drops `--dry-run` and wires the trial for real. (`--value` points at `--stdin` for a genuinely dash-leading secret.)
10
+ - **`--help` / exit-code consistency.** `yaw-mcp audit --help` and `compliance --help` now print usage to stdout and exit 0 like every other subcommand (compliance previously forwarded `--help` into an `npx` download); `compliance` with no target exits 2 (arg error) instead of 1.
11
+ - **Uncaught command rejections are handled.** Every subcommand funnels through a shared dispatcher `.catch` that prints `yaw-mcp <cmd>: <message>` and exits 1, instead of dumping a raw Node stack and bypassing the logger (reachable e.g. via `secrets` against a corrupt vault).
12
+ - **Secret-bearing files are born 0600.** `atomicWriteFile` gained a creation-mode option; the token config, team session cookie, encrypted vault, and the install config backup are now written owner-only rather than sitting at the default umask in the window before the post-hoc chmod. `install --dry-run` redacts the token in the config dump, and `--token` carries a process-table exposure note.
13
+ - **`doctor`:** `--json` now runs the same expired-trial GC as the text path, and the snapshot carries the `trials` + `backgroundPosters` sections (it was not the "1:1 mirror" its comment claimed). The header documents the config read-modify-write side effect and the now-unreachable exit code 1.
14
+ - **Completion drift guard made real.** Shell completion now offers `foundry`, and the completion test asserts coverage against the real dispatch table (extracted to `src/subcommands.ts`) instead of a hand-maintained list that had silently diverged.
15
+ - **`secrets`:** `get` documents that it prints cleartext (with an interactive-TTY stderr warning); Ctrl-D at the passphrase prompt cancels instead of submitting a partial passphrase; `pull` empty-remote `--json` carries the same hint as the prose path.
16
+ - **`compliance --publish`** projects the report to an explicit allowlist before upload (no echoed env/argv/stack leaks), and the suite child's stdout is capped (16 MB) behind a wall-clock watchdog.
17
+ - **`upgrade`:** the `_npx` marker now requires the full npm-cache hex context, so a user project path that merely contains a `_npx` segment is no longer misclassified as an npx run; the 1->2 exit sequence for non-runnable methods (binary/dev-checkout) is documented.
18
+ - **`add`** trims whitespace-only `--env` values so a blank-ish required secret is never persisted to bundles.json. Did-you-mean now includes `help`, gates its substring tier for very short queries, and a leading-dash near-miss (`--versionn`) suggests the flag instead of silently booting the MCP server.
19
+ - **deps:** `esbuild` override pinned to `^0.28.1`.
20
+
21
+ ## 0.62.0 -- verifiable-signal routing: graded reward, miss tracking, and an eval foundry
22
+
23
+ Lands #25, #26, and #27. The dispatch router's learning signal moves from a binary "any non-error reply counts as success" to a sound, quality-graded reward, plus the surrounding machinery to manufacture and verify that signal. All of the new behavior is additive and the new knobs are off by default, so existing setups are unchanged.
24
+
25
+ - **Dispatch reward is now graded, not binary.** An empty body or an error-shaped 200 (e.g. `{isError:false, text:"not found"}`) no longer banks full credit toward a server's `mcp_connect_dispatch` boost -- the learning signal is a quality-weighted reward in [0,1], so a server that "replies" but does not actually help stops accruing a nudge. When every reply is a clean success or a hard error this collapses to the old behavior.
26
+ - **Re-dispatch miss signal.** When an intent routes to server A, A replies cleanly but is abandoned, and a token-similar intent then routes to a different server B within ~2 minutes, A is penalized as the wrong route. Designed multi-server flows (curated bundles, detected packs) are excluded so an intentional A-then-B chain is not mistaken for a miss.
27
+ - **Step-level (process) reward in `mcp_connect_exec`.** Each pipeline step is graded on its own; a step that fails on bad `$ref` input it consumed from an upstream step splits the blame with the producer instead of being fully blamed, so a flaky producer does not hide behind a healthy consumer (or vice versa).
28
+ - **Routing effort dial -- `YAW_MCP_ROUTE_EFFORT=off|auto|aggressive`** (also a per-call `routeEffort` arg on `mcp_connect_dispatch`). Controls how much LLM sampling dispatch spends to disambiguate close rankings. `auto` (default) preserves the prior fixed top-2 tiebreak exactly -- no latency change on the default path; `aggressive` samples best-of-3 on milder ambiguity; `off` never samples.
29
+ - **Opt-in LLM reward grader -- `YAW_MCP_REWARD_GRADER=1`** (off by default). On the uncertain reward bands only, it asks the client's own LLM (via MCP sampling) whether a call accomplished the goal and revises the credit in the background. Non-blocking (the tool result never waits on the grade), capability-gated, and never-throwing -- a missing capability / timeout / unparseable reply just leaves the heuristic reward standing.
30
+ - **Opt-in routing-eval harvest + CI gate -- `YAW_MCP_FOUNDRY=1`** (off by default). Writes a privacy-safe `(intent -> chosen server)` corpus to `~/.yaw-mcp/foundry.jsonl`: the intent is reduced to a redacted, secret-stripped, order-shuffled token bag, never the raw text. New `yaw-mcp foundry export` folds the harvest into a checked-in regression corpus (snapshotting the local server catalog), and a BM25-floor gate scores the ranker against real dispatches -- it skips cleanly until a corpus is committed.
31
+
32
+ ## 0.61.0 -- audit-fix wave: exec binding payloads (BREAKING), live-bug fixes, 50+ hardening items
33
+
34
+ - **BREAKING: `mcp_connect_exec` step bindings now hold the step's semantic payload, not the raw MCP wire wrapper.** A single-text-item JSON response binds as the parsed value, a non-JSON text response binds as the string, and anything else binds as the content array. `$ref` paths written against the old wire shape -- e.g. `"stepId.content[0].text"` -- now throw a RefError; migrate to `"stepId"` (whole value) or `"stepId.field"` (a specific field). This matches what the tool description always promised.
35
+ - `mcp_connect_activate` / `mcp_connect_dispatch` set `isError` whenever a real activation failure occurs (partial successes no longer mask failures); concurrent-server-cap refusals are flagged internally and stay informational in both. Deactivating an already-unloaded namespace is now an idempotent success.
36
+ - ~30 further fixes from a full-implementation audit: bundles.json write serialization + ENOENT-only absence handling, `list` surfaces parse warnings, analytics 401 no longer clears the team session, `secrets pull` refuses cross-passphrase overwrites without `--force`, uv probe/retry fixes on Windows, stable array positions in pruned tool output, `--help` exits 0 on stdout across subcommands, ASCII-safe terminal output, and assorted copy corrections.
37
+
38
+ ## 0.60.3 -- npm-prefix refinement + pnpm/bun self-upgrade
39
+
40
+ - `refineInstallMethod` now probes `npm prefix -g` (3s timeout) and normalises the result through `realpathSync` so junctioned prefixes (scoop's `current` symlink, Volta shims) resolve to the real path before comparison. When the running entrypoint lives under the npm global prefix, ambiguous `local-node-modules` / `unknown` detections are promoted to `global-npm` -- fixes exotic prefix setups the path-marker list doesn't know.
41
+ - The probe is wired into both `yaw-mcp upgrade` and `yaw-mcp doctor` via the shared `refineInstallMethod` call in `runUpgrade` / `runDoctor`.
42
+ - `maybeAutoUpgrade` (the fire-and-forget startup check) now acts on stale pnpm and bun global installs with `pnpm add -g` / `bun add -g @yawlabs/mcp@latest` in addition to the existing `npm install -g` path. The background spawn log messages interpolate the actual tool name instead of hardcoding `npm`.
43
+
5
44
  ## 0.60.2 -- pnpm/bun global stores upgrade with their owning tool
6
45
 
7
46
  - `yaw-mcp upgrade` now detects pnpm global stores (`<pnpm-home>/global/<n>/node_modules/...`) and bun global installs (`~/.bun/install/global/...`) as their own install methods. `--run` spawns `pnpm add -g` / `bun add -g @yawlabs/mcp@latest` instead of misclassifying them as local node_modules trees -- which would have npm-installed a foreign package-lock + node_modules into the tool manager's internal store.
package/README.md CHANGED
@@ -367,7 +367,7 @@ Rotate a credential in one place (the dashboard), every machine picks up the new
367
367
  | `LOG_LEVEL` | No | Log verbosity: `debug`, `info`, `warn`, `error` (default: `info`) |
368
368
  | `YAW_MCP_POLL_INTERVAL` | No | Config-poll interval in seconds. `0` disables polling (config fetched once at startup). Default: `60` |
369
369
  | `YAW_MCP_AUTO_ACTIVATE` | No | When `discover` is called with a context string and one server clearly wins, auto-load it. Set to `0` to disable. Default: enabled |
370
- | `YAW_MCP_AUTO_UPGRADE` | No | When yaw-mcp starts as a server, runs a non-blocking background check for a newer global-npm install and upgrades quietly. Set to `0` to disable. Default: enabled. |
370
+ | `YAW_MCP_AUTO_UPGRADE` | No | When yaw-mcp starts as a server, runs a non-blocking background check for a newer npm/pnpm/bun global install and upgrades quietly with the owning tool. Set to `0` to disable. Default: enabled. |
371
371
  | `YAW_MCP_SERVER_CAP` | No | Hard cap on concurrently activated servers. Default: `6`. Set to `0` to disable. |
372
372
  | `YAW_MCP_PRUNE_RESPONSES` | No | Conservative response pruning (redact large file blobs etc. before returning to the client). Set to `0` or `false` to disable. Default: enabled. |
373
373
  | `YAW_MCP_DISABLE_PERSISTENCE` | No | Set to `1` or `true` to keep learning + pack-history scoped to the current process -- nothing loaded at start, nothing written on shutdown. Intended for ephemeral / shared environments (CI, containers). Default: cross-session persistence enabled at `~/.yaw-mcp/state.json`. |
@@ -410,7 +410,7 @@ If you find a security issue in yaw-mcp itself, report it via [GitHub's private
410
410
  ## Requirements
411
411
 
412
412
  - Node.js 18+
413
- - A [yaw.sh/mcp](https://yaw.sh/mcp) account
413
+ - No account required for core features. A Yaw Team license key is needed only for sync and shared bundles (yaw.sh/mcp).
414
414
 
415
415
  ## Links
416
416
 
@@ -9,12 +9,12 @@ import { join } from "path";
9
9
  // src/atomic-write.ts
10
10
  import { mkdir, rename, unlink, writeFile } from "fs/promises";
11
11
  import path from "path";
12
- async function atomicWriteFile(filePath, contents, encoding = "utf8") {
12
+ async function atomicWriteFile(filePath, contents, encoding = "utf8", mode) {
13
13
  const dir = path.dirname(filePath);
14
14
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
15
15
  await mkdir(dir, { recursive: true });
16
16
  try {
17
- await writeFile(tmp, contents, encoding);
17
+ await writeFile(tmp, contents, mode === void 0 ? { encoding } : { encoding, mode });
18
18
  await rename(tmp, filePath);
19
19
  } catch (err) {
20
20
  await unlink(tmp).catch(() => void 0);
@@ -27,7 +27,7 @@ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
27
27
  var minLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
28
28
  function log(level, msg, data) {
29
29
  if (LOG_LEVELS[level] < minLevel) return;
30
- const entry = JSON.stringify({ level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), ...data });
30
+ const entry = JSON.stringify({ ...data, level, msg, ts: (/* @__PURE__ */ new Date()).toISOString() });
31
31
  process.stderr.write(`${entry}
32
32
  `);
33
33
  }
@@ -115,10 +115,10 @@ function invalidateState() {
115
115
  cachedState = null;
116
116
  }
117
117
  async function loadStoredState(filePath) {
118
- if (cachedState) {
118
+ if (cachedState && cachedState.filePath === filePath) {
119
119
  const s = cachedState.state;
120
120
  if (s && expMs(s.session) < Date.now()) {
121
- cachedState = { state: null };
121
+ cachedState = { filePath, state: null };
122
122
  return null;
123
123
  }
124
124
  return s;
@@ -135,16 +135,16 @@ async function loadStoredState(filePath) {
135
135
  parsed = null;
136
136
  }
137
137
  if (parsed && expMs(parsed.session) < Date.now()) {
138
- cachedState = { state: null };
138
+ cachedState = { filePath, state: null };
139
139
  return null;
140
140
  }
141
- cachedState = { state: parsed };
141
+ cachedState = { filePath, state: parsed };
142
142
  return parsed;
143
143
  }
144
144
  async function saveStoredState(filePath, state) {
145
- cachedState = { state };
145
+ cachedState = { filePath, state };
146
146
  try {
147
- await atomicWriteFile(filePath, JSON.stringify(state, null, 2));
147
+ await atomicWriteFile(filePath, JSON.stringify(state, null, 2), "utf8", 384);
148
148
  if (process.platform !== "win32") {
149
149
  try {
150
150
  await chmod(filePath, 384);
@@ -328,7 +328,6 @@ async function postAnalyticsEvent(event, opts = {}) {
328
328
  baseUrl: opts.baseUrl
329
329
  });
330
330
  if (res.status === 401) {
331
- await clearStoredState(filePath);
332
331
  return { ok: false };
333
332
  }
334
333
  return { ok: res.status === 200 };
@@ -353,6 +352,11 @@ async function listAnalyticsEvents(opts = {}) {
353
352
  }
354
353
  return { events: res.body.events ?? [], cap: res.body.cap ?? 0, order_id: res.body.order_id ?? "" };
355
354
  }
355
+ function getCachedCookie(home = homedir2()) {
356
+ void home;
357
+ const s = cachedState?.state ?? null;
358
+ return s ? s.cookie : null;
359
+ }
356
360
  function _resetForTests() {
357
361
  invalidateState();
358
362
  }
@@ -377,5 +381,6 @@ export {
377
381
  putResource,
378
382
  postAnalyticsEvent,
379
383
  listAnalyticsEvents,
384
+ getCachedCookie,
380
385
  _resetForTests
381
386
  };