Skip to content

Releases: YawLabs/npmjs-mcp

v0.15.1

Choose a tag to compare

@jeffyaw jeffyaw released this 23 Aug 16:52

Fixed

  • The launcher no longer dies with a raw stack trace when spawn fails. Node throws synchronously rather than emitting error for some unexecutable targets — notably a .cmd/.bat on Windows — and the error listener is registered after the spawn call, so it could never observe that throw. Both failure modes now route through one handler.
  • Windows PATH discovery accepts oam.exe only, instead of walking every PATHEXT entry and returning an oam.cmd Node cannot execute. A skipped shim is still named in the diagnostic, so an npm-style install no longer reports as "no oam binary was found".
  • A failing in-process fallback no longer escapes as an unhandled rejection. void runInProcess() discarded the promise, replacing the launcher's own diagnostic with a raw stack trace.
  • Diagnostics that precede process.exit are written synchronously. stderr is async for TTYs and pipes on Windows, so the exit could truncate them. They route through one helper that also handles short writes and macOS EAGAIN on a non-blocking piped stderr.
  • Removed a literal backspace byte (U+0008) from the runtime-discovery comment, which made git treat the file as binary so its diff could not be reviewed.
  • An oam that cannot be run is no longer reported as an outdated one. The version probe returns null for several distinct causes — not executable, wrong architecture, a shim Node refuses, deleted since the stat, unparseable --version output — and every one produced "older than oam 0.9.0 … run oam self-update", pointing at the single cause it definitely was not. The two cases now carry separate wording and remedies, and the outdated message reports the version actually detected.
  • Windows: the launcher no longer hard-kills the server on the first Ctrl-C. There are no POSIX signals on Windows — child.kill(sig) ignores the name and calls TerminateProcess, an immediate hard kill (verified: a child with a SIGTERM handler never runs it and dies with code=null). The launcher forwarded anyway, on the stated assumption that this was a "no-op on Windows", so it aborted the graceful shutdown the console's own Ctrl-C had just started and skipped the server's process.on("exit") cleanup. The console already delivers the event to the whole process group, so on Windows the launcher now forwards nothing.
  • A wedged server no longer leaves the launcher hanging. Forwarding was gated on child.killed, which records only that kill() was called — never that the child is gone — so every signal after the first was swallowed and there was no escape hatch. Escalation is now armed by a timer on the first signal: one press is enough, and a child still alive after a 2s grace window is killed. Using a timer rather than counting signals also stops the ordinary supervisor sequence (SIGINT then SIGTERM milliseconds apart) from being misread as impatience.
  • The test suite is bounded by --test-timeout, so a hang cannot wedge a release. node:test has no default per-test timeout, so a test awaiting an event that never arrives runs forever -- and npm test runs unattended inside release.sh, which turns a wedged release rather than a failed one. 300000ms is deliberately generous (files measure ~7.5s worst case) and converts an infinite hang into a reported failure. Note the flag is per-FILE until Node 24, and requires Node >= 20.11.0.
  • engines.node now declares >=20.11.0, matching what the test tooling actually requires (--test-timeout landed in Node 20.11.0). The previous >=20 admitted 20.0-20.10, where the flag does not exist. README updated to match.

v0.15.0

Choose a tag to compare

@jeffyaw jeffyaw released this 08 Aug 23:58
  • v0.15.0
  • feat(runtime): require oam 0.9.0 and add an opt-in --permission sandbox
  • test: assert server.json and package.json stay in sync (#36)

v0.14.1

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Aug 18:08

Fixed

  • Corrected the benchmarking caveat published in 0.14.0. It attributed the earlier bad measurement to an on-access virus scanner rescanning build outputs on every exec, citing a 5.0x penalty (306 ms → 61 ms). That does not reproduce — the same interleaved comparison on a settled tree measures 1.03x. The original was taken while a concurrent session was rebuilding oam, so every exec hit different bytes. The guidance survives with the right reason: measure an installed oam, never one out of target/, because a build directory's contents change underneath a running benchmark. The performance figures in 0.14.0 were re-verified against an installed binary and are unchanged — node 167 ms vs oam 112 ms, 0.67x.

v0.14.0

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Aug 16:09

Added

  • Cross-building standalone binaries via an oam carrier. NPMJS_MCP_BINARY_RUNTIME=oam builds through oam instead of Node SEA, and NPMJS_MCP_BINARY_TARGET=<platform>-<arch> cross-builds for another target from any host — SEA cannot, because it embeds the running node as its carrier. oam's embed format is a tail trailer ([JS][u64 LE len][OAMEXEC\0]), and appending to an executable's tail is tolerated identically by PE, ELF and Mach-O, so only the carrier is platform-specific. The carrier is fetched from the published oam release and verified against its SHA256SUMS entry; a mismatch or missing entry aborts. Verified: a linux-x64 cross build from windows-arm64 produces a genuine ELF 64-bit LSB pie executable, x86-64 whose trailer length matches the bundle byte for byte.
  • Runtime launcher at bin/npmjs-mcp.mjs: the published npmjs-mcp command now prefers the oam runtime and falls back to Node. NPMJS_MCP_RUNTIME selects (auto / oam / node) and OAM_BIN overrides discovery. Both paths were verified against the full MCP surface — handshake, 64 tools, live registry call, and the destructive-op confirm gate — and behave identically, because the server is a pre-bundled ESM file using only node: builtins oam implements.
  • dev:oam script: oam run --check=warn src/index.ts runs the TypeScript source with no build step, type-checking concurrently rather than blocking execution.

Changed

  • bin points at the launcher rather than dist/index.js directly. The fallback does not re-exec Node — npm has already started Node to run the launcher, so it is an in-process import() with no extra spawn or startup for users without oam.
  • .gitignore now excludes bin/* rather than bin/, so the launcher can be re-included with a negation. A directory-level exclusion cannot be undone by a negation for a file inside it, which would have left the launcher untracked and absent from any fresh clone while still appearing to work locally.

Notes

  • oam is faster than Node for this server; the launcher is not. Windows-arm64, n=12 medians, spawn to first MCP initialize: oam run dist/index.js 116 ms (0.67x), node dist/index.js 172 ms, launcher 243 ms (1.41x). npm bin entries are Node scripts, so reaching oam through one pays Node's startup plus oam's — more than oam saves. Point an MCP host directly at oam run <abs>/dist/index.js for the fast path; the launcher is for npx convenience.
  • Benchmarking caveat worth recording: an earlier revision of these notes claimed oam was a regression (~382 ms Node vs ~510 ms oam). That was measuring a virus scanner, not a runtime — the oam binary was being run out of a cargo target/ directory, which the active AV rescanned on every exec. The identical bytes copied elsewhere and run once to absorb the scan started 5.0x faster (306 ms → 61 ms, interleaved and reproducible). Never benchmark out of a build-output directory on Windows.
  • oam's --permission sandbox is deliberately unused. Its own divergence notes record that it denies filesystem, environment and network access, and the only grants implemented are --allow-fs-read/write, --allow-child-process, --allow-worker and --allow-addons. With no network grant, enabling it produces a server that completes the MCP handshake and then fails every tool call — confirmed empirically before ruling it out.

v0.13.0

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Aug 14:53

A second full-pass review over src/ plus the build and release tooling. The headline items are a silent org-role demotion, a retry policy that could re-apply writes, and a release script whose "verify" step could fail an already-successful release.

Fixed

  • npm_org_member_set no longer demotes an existing admin or owner when role is omitted. The registry membership spec defines role as "defaults to developer if not given" -- it does not preserve the current value, and the npm CLI never exposes that because it fills the same default itself (lib/commands/org.js: role = role || 'developer'). A bare { user } body therefore silently demoted the member. The handler now reads the roster and re-sends the existing role explicitly, so "omit role to keep the current role" is finally true. If the roster read fails it refuses the write rather than risking the demotion.
  • Write requests (PUT/POST/DELETE that mutate) are no longer retried after a network error or timeout. The registry may have applied the mutation and lost the response; retrying re-sent it with the same _rev, which came back 409 and read like a failure on an operation that had actually succeeded. Those requests now surface the ambiguity explicitly. Writes still retry on 429 and 503 -- the statuses that mean "I did not process this" -- but no longer on 502/504, which a gateway can return after the origin applied the change. Read-only audit POSTs keep the full retry policy.
  • A malformed JSON body on a 2xx is reported against its real status instead of being caught as a transport failure and retried -- that path re-sent a request the registry had already answered.
  • Retry-After is clamped to 30s. An unbounded value parked a single tool call for minutes per attempt, an order of magnitude past the request timeout.
  • npm_undeprecate retries once on a 409 _rev conflict, matching npm_deprecate. Both do the same read-modify-write against the same revision, so both race the same way.
  • npm_owner_add matches existing maintainers case-insensitively (as npm_owner_remove already did). A packument entry whose case differed from the canonical /-/user record read as "not an owner" and appended a duplicate maintainer for the same person.
  • npm_dep_tree memoization is depth-aware. A node first reached at the depth limit was recorded without expanding its children, and a later arrival at a shallower depth short-circuited on the "already seen" check -- leaving that subtree missing. Because it turned on fetch-latency ordering, the same query could return different trees on different runs.
  • Handlers no longer assert res.data! on responses that can legitimately arrive empty. A 2xx with no body (which the client returns as ok: true with no data) produced a raw "Cannot read properties of undefined" at the MCP boundary; it now returns an actionable 502. Same fix applied to packument.time, versionDoc.dist, and the array-shaped org/trust responses.
  • npm_publish_preflight no longer reports "2FA is enabled and token type cannot be verified" when the profile fetch failed -- a state it never established. That case gets its own summary naming the real uncertainty.
  • Download and registry-stats handlers validate the period before interpolating it into the request path.
  • npm_types distinguishes "no @types package exists" (a 404) from "the lookup failed", via a new typesLookupReliable field. Any non-404 failure previously reported a definitive "No TypeScript types available".
  • release.sh: the post-publish npx smoke test ran as a bare subshell under set -e, so exhausting its retries terminated the script before the warn-only branch -- turning a slow registry into [FAIL] Release failed on an already-successful release. It now runs as an if condition.
  • src/index.ts no longer relies on a top-level await import(). TLA cannot be emitted in the CJS format the single-binary build uses, and it only ever compiled because the __VERSION__ define let esbuild eliminate that branch first -- making a build-breaking construct load-bearing on constant folding.
  • The published bundle no longer carries a sourceMappingURL comment pointing at a file that was never shipped. The build now emits the sourcemap as external, keeping dist/index.js.map on disk for local debugging while leaving it out of the tarball -- shipping it instead would have taken the packed download from 225 kB to 583 kB on every npx cold start.

Changed

  • Tool annotations: npm_owner_add, npm_team_create, npm_team_member_add, and npm_hook_add now report destructiveHint: false. Per the MCP spec the hint means "may perform destructive updates", and these four only add. Previously a test invariant required readOnlyHint and destructiveHint to be strict opposites, which forced every write to claim destructiveness. MCP hosts that gate confirmation prompts on destructiveHint will stop prompting for these four tools. The invariant is now one-directional (read-only implies non-destructive), with explicit pins for both the additive set and the ten irreversible operations.
  • npm_check_auth and npm_publish_preflight treat 2FA mode auth-only as non-blocking for publishing. That mode challenges login, not writes, so those accounts publish headlessly with any valid token -- they were previously reported as canPublishHeadless: null with an EOTP warning. auth-and-writes (and any unrecognized future mode) still gates.
  • npm_ops_playbook no longer claims that period-and-capital deprecation messages trigger a 422. That heuristic was traced to a wildcard version range matching no published versions and removed from validation in v0.10; the playbook had kept propagating it to the agents it exists to advise. It now points at the 1024-character limit and the semver range instead.
  • npm_hook_update's description matches its schema: both endpoint and secret are required, and the call always rotates the secret because the registry PUT replaces the whole hook config.
  • release.sh checks for a provenance attestation on every publish, not only in CI, and names the cause when one is absent.

Documentation

  • README: the npm_audit_deep example omitted the required dependencies field and passed the dependency as the project name, so it could not run as written. The publish row of the decision matrix pointed at a CI tag-push workflow that was removed in b2c256c.
  • scripts/build-binary.mjs no longer suggests verifying with a doctor --json subcommand that does not exist, or citing a BINARY_DISTRIBUTION.md that is not in the repo.

v0.12.2

Choose a tag to compare

@github-actions github-actions released this 11 Jun 17:36
v0.12.2

v0.12.1

Choose a tag to compare

@jeffyaw jeffyaw released this 07 Jun 18:21

Hardening pass over the tool surface from a full-pass review: input validation on the write/bulk handlers, output-contract fixes, and test-suite robustness.

Fixed

  • npm_dist_tag_set pre-flights the packument and returns a 404 (with the published-version list) when the target version does not exist, instead of PUTting a tag at a nonexistent version. It was the only write handler that skipped the packument pre-flight.
  • npm_token_revoke validates the token key (rejects empty/malformed) before the DELETE, so passing the token value instead of its UUID key no longer silently targets the wrong resource.
  • npm_team_grant / npm_team_revoke validate the package field before building the request -- previously the only package-accepting handlers with no validation guard in the call path.
  • npm_downloads_bulk and npm_compare validate every package name up front and return a clean 400 naming the offender, instead of throwing inside encPkg and surfacing a raw error at the MCP boundary.
  • npm_owner_remove matches usernames case-insensitively; a caller passing Bob against a stored bob no longer gets a misleading 404.
  • The packument GET-mutate-PUT flows (npm_deprecate, npm_undeprecate, npm_owner_add, npm_owner_remove) retry once on a 409 _rev conflict (CouchDB optimistic-concurrency), so a concurrent write no longer hard-fails a retryable conflict.
  • npm_unpublish_version skips the tarball DELETE when the tarball origin does not match the configured registry origin, instead of misrouting the DELETE to the wrong host under proxy registries (Verdaccio/Nexus in proxy mode).
  • npm_check_auth / npm_publish_preflight report a distinct fetch-failed 2FA state when the profile fetch fails, instead of rendering "2FA is enabled (unknown)" -- which implied confirmed 2FA when the real cause was a token lacking read on /-/npm/v1/user.
  • npm_provenance rejects an empty/whitespace version with a 400 before the registry call.
  • npm_package_access surfaces both endpoint errors when /access and /collaborators both fail, instead of reporting only the collaborators error and hiding the real root cause.
  • npm_health stays well-defined for a partially-written packument (dist-tags.latest present but its version doc absent).
  • build.mjs guards the package.json read with an actionable error instead of an unhandled exception.

Changed

  • npm_recent_changes: the totalPackages response field is renamed to registryPackageCount. It holds the registry-wide doc count (~3M from replicate.npmjs.com), not the per-call changes.length, and the old name read as if it were a per-call count. Consumers reading totalPackages must update to registryPackageCount.
  • npm_recent_changes: the idempotentHint annotation is corrected to false -- it serves a live changes feed, so MCP clients must not cache or de-duplicate repeated calls.
  • npm_access_set: access: "private" now maps to the registry wire value "restricted" rather than being passed through verbatim (the registry uses public/restricted).
  • npm_provenance: predicate-type detection tightened from substring to prefix match, and the description reworded to make clear the tool retrieves attestations -- it does not cryptographically verify signatures, certificate chains, or Rekor entries.
  • npm_org_member_set now requires confirm: true, matching npm_org_member_remove.
  • npm_compare / npm_health carry the deprecation message string when a package is deprecated, instead of collapsing it to a boolean.
  • npm_trusted_publishers tolerates a string-or-object workflow_ref / ci_config_ref_uri, surfacing the raw string instead of silently yielding undefined.

Added

  • npm_tokens surfaces each token's type and automation flag, so callers can distinguish automation tokens (which bypass 2FA) from granular/legacy ones -- the distinction the tool description already promised.
  • npm_check_auth / npm_publish_preflight fire their independent auth reads (whoami / profile / tokens) concurrently rather than serially.

Documentation

  • Description fixes: npm_deprecate empty-message wording, enumerated download period values, consistent org-name @-prefix wording, npm_dep_tree depth semantics (depth counts the root), and named staleness constants in npm_health.
  • ASCII-normalized the missing _rev error strings (were a mix of em-dash and --).

Tests

  • mockFetchSequence throws on over-run instead of silently replaying the last canned response, which had masked spurious extra-request regressions across the write suite. Tightened write/semver assertions, derived the total tool-count check from the per-module sums, and added an npm_token_revoke malformed-key negative test. 739 passing.
  • Internal: Packument dist-tags / maintainers typed optional to match the defensive runtime guards.

v0.12.0

Choose a tag to compare

@jeffyaw jeffyaw released this 04 Jun 13:16
  • v0.12.0
  • docs+chore: address full-pass review findings (round 2)
  • fix: defensive guards for empty/short-circuit API responses

v0.11.15

Choose a tag to compare

@jeffyaw jeffyaw released this 03 Jun 02:56
  • v0.11.15
  • test: cover write-op auth/_rev/PUT-error/owner-resolve high-sev gaps
  • fix(release): compare tag-object shas in drift guard so resume runs don't false-abort
  • fix(readme): point "Add to Yaw MCP" badge at the https forwarder
  • docs(release): fix tag-drift comment to describe the real failure mode
  • fix(release): refuse to push if origin's tag drifted from local

v0.11.14

Choose a tag to compare

@jeffyaw jeffyaw released this 28 May 21:48
  • v0.11.14
  • fix(release): add SKIP_LINT=1 escape hatch for broken lint runners
  • chore: sync server.json version to match package.json
  • fix(release): tty-gate the confirmation prompt
  • fix(release): sync server.json unconditionally, not only inside the bump else
  • fix(release): fall back to gh auth token for MCP Registry
  • chore: fold MCP Registry publish into release.sh, drop release.yml
  • fix(release): drop needs: ref to removed reusable workflow
  • chore(ci): drop non-release workflows
  • chore(readme): swap mcp.hosting badge -> Add to Yaw MCP yaw:// deep link
  • docs(readme): pin npx spawn to @latest for auto-update