Releases: YawLabs/npmjs-mcp
Releases · YawLabs/npmjs-mcp
Release list
v0.15.1
Fixed
- The launcher no longer dies with a raw stack trace when
spawnfails. Node throws synchronously rather than emittingerrorfor some unexecutable targets — notably a.cmd/.baton Windows — and theerrorlistener is registered after thespawncall, so it could never observe that throw. Both failure modes now route through one handler. - Windows
PATHdiscovery acceptsoam.exeonly, instead of walking everyPATHEXTentry and returning anoam.cmdNode 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.exitare 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 macOSEAGAINon 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
--versionoutput — and every one produced "older than oam 0.9.0 … runoam 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 callsTerminateProcess, an immediate hard kill (verified: a child with aSIGTERMhandler never runs it and dies withcode=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'sprocess.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 thatkill()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 (SIGINTthenSIGTERMmilliseconds apart) from being misread as impatience. - The test suite is bounded by
--test-timeout, so a hang cannot wedge a release.node:testhas no default per-test timeout, so a test awaiting an event that never arrives runs forever -- andnpm testruns unattended insiderelease.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.nodenow declares>=20.11.0, matching what the test tooling actually requires (--test-timeoutlanded in Node 20.11.0). The previous>=20admitted 20.0-20.10, where the flag does not exist. README updated to match.
v0.15.0
v0.14.1
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
Added
- Cross-building standalone binaries via an oam carrier.
NPMJS_MCP_BINARY_RUNTIME=oambuilds through oam instead of Node SEA, andNPMJS_MCP_BINARY_TARGET=<platform>-<arch>cross-builds for another target from any host — SEA cannot, because it embeds the runningnodeas 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 itsSHA256SUMSentry; a mismatch or missing entry aborts. Verified: a linux-x64 cross build from windows-arm64 produces a genuineELF 64-bit LSB pie executable, x86-64whose trailer length matches the bundle byte for byte. - Runtime launcher at
bin/npmjs-mcp.mjs: the publishednpmjs-mcpcommand now prefers the oam runtime and falls back to Node.NPMJS_MCP_RUNTIMEselects (auto/oam/node) andOAM_BINoverrides 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 onlynode:builtins oam implements. dev:oamscript:oam run --check=warn src/index.tsruns the TypeScript source with no build step, type-checking concurrently rather than blocking execution.
Changed
binpoints at the launcher rather thandist/index.jsdirectly. The fallback does not re-exec Node — npm has already started Node to run the launcher, so it is an in-processimport()with no extra spawn or startup for users without oam..gitignorenow excludesbin/*rather thanbin/, 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.js116 ms (0.67x),node dist/index.js172 ms, launcher 243 ms (1.41x). npmbinentries are Node scripts, so reaching oam through one pays Node's startup plus oam's — more than oam saves. Point an MCP host directly atoam run <abs>/dist/index.jsfor the fast path; the launcher is fornpxconvenience. - 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
--permissionsandbox 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-workerand--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
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_setno longer demotes an existing admin or owner whenroleis omitted. The registry membership spec definesroleas "defaults todeveloperif 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 "omitroleto 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/DELETEthat 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 auditPOSTs 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-Afteris clamped to 30s. An unbounded value parked a single tool call for minutes per attempt, an order of magnitude past the request timeout.npm_undeprecateretries once on a 409_revconflict, matchingnpm_deprecate. Both do the same read-modify-write against the same revision, so both race the same way.npm_owner_addmatches existing maintainers case-insensitively (asnpm_owner_removealready did). A packument entry whose case differed from the canonical/-/userrecord read as "not an owner" and appended a duplicate maintainer for the same person.npm_dep_treememoization 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 asok: truewith nodata) produced a raw "Cannot read properties of undefined" at the MCP boundary; it now returns an actionable 502. Same fix applied topackument.time,versionDoc.dist, and the array-shaped org/trust responses. npm_publish_preflightno 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
periodbefore interpolating it into the request path. npm_typesdistinguishes "no@typespackage exists" (a 404) from "the lookup failed", via a newtypesLookupReliablefield. 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 underset -e, so exhausting its retries terminated the script before the warn-only branch -- turning a slow registry into[FAIL] Release failedon an already-successful release. It now runs as anifcondition.src/index.tsno longer relies on a top-levelawait 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
sourceMappingURLcomment pointing at a file that was never shipped. The build now emits the sourcemap asexternal, keepingdist/index.js.mapon 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 everynpxcold start.
Changed
- Tool annotations:
npm_owner_add,npm_team_create,npm_team_member_add, andnpm_hook_addnow reportdestructiveHint: false. Per the MCP spec the hint means "may perform destructive updates", and these four only add. Previously a test invariant requiredreadOnlyHintanddestructiveHintto be strict opposites, which forced every write to claim destructiveness. MCP hosts that gate confirmation prompts ondestructiveHintwill 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_authandnpm_publish_preflighttreat 2FA modeauth-onlyas non-blocking for publishing. That mode challenges login, not writes, so those accounts publish headlessly with any valid token -- they were previously reported ascanPublishHeadless: nullwith an EOTP warning.auth-and-writes(and any unrecognized future mode) still gates.npm_ops_playbookno 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: bothendpointandsecretare required, and the call always rotates the secret because the registry PUT replaces the whole hook config.release.shchecks for a provenance attestation on every publish, not only in CI, and names the cause when one is absent.
Documentation
- README: the
npm_audit_deepexample omitted the requireddependenciesfield 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.mjsno longer suggests verifying with adoctor --jsonsubcommand that does not exist, or citing aBINARY_DISTRIBUTION.mdthat is not in the repo.
v0.12.2
v0.12.1
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_setpre-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_revokevalidates 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_revokevalidate thepackagefield before building the request -- previously the only package-accepting handlers with no validation guard in the call path.npm_downloads_bulkandnpm_comparevalidate every package name up front and return a clean 400 naming the offender, instead of throwing insideencPkgand surfacing a raw error at the MCP boundary.npm_owner_removematches usernames case-insensitively; a caller passingBobagainst a storedbobno 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_revconflict (CouchDB optimistic-concurrency), so a concurrent write no longer hard-fails a retryable conflict. npm_unpublish_versionskips 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_preflightreport a distinctfetch-failed2FA 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_provenancerejects an empty/whitespace version with a 400 before the registry call.npm_package_accesssurfaces both endpoint errors when/accessand/collaboratorsboth fail, instead of reporting only the collaborators error and hiding the real root cause.npm_healthstays well-defined for a partially-written packument (dist-tags.latestpresent but its version doc absent).build.mjsguards thepackage.jsonread with an actionable error instead of an unhandled exception.
Changed
npm_recent_changes: thetotalPackagesresponse field is renamed toregistryPackageCount. It holds the registry-wide doc count (~3M fromreplicate.npmjs.com), not the per-callchanges.length, and the old name read as if it were a per-call count. Consumers readingtotalPackagesmust update toregistryPackageCount.npm_recent_changes: theidempotentHintannotation is corrected tofalse-- 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 usespublic/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_setnow requiresconfirm: true, matchingnpm_org_member_remove.npm_compare/npm_healthcarry the deprecation message string when a package is deprecated, instead of collapsing it to a boolean.npm_trusted_publisherstolerates a string-or-objectworkflow_ref/ci_config_ref_uri, surfacing the raw string instead of silently yieldingundefined.
Added
npm_tokenssurfaces each token'stypeandautomationflag, 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_preflightfire their independent auth reads (whoami / profile / tokens) concurrently rather than serially.
Documentation
- Description fixes:
npm_deprecateempty-message wording, enumerated downloadperiodvalues, consistent org-name@-prefix wording,npm_dep_treedepth semantics (depth counts the root), and named staleness constants innpm_health. - ASCII-normalized the
missing _reverror strings (were a mix of em-dash and--).
Tests
mockFetchSequencethrows 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 annpm_token_revokemalformed-key negative test. 739 passing.- Internal:
Packumentdist-tags/maintainerstyped optional to match the defensive runtime guards.
v0.12.0
v0.11.15
- 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
- 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