feat: notify when a newer npm package version is available - #96
Draft
Gregory Joseph (gnjoseph) wants to merge 6 commits into
Draft
feat: notify when a newer npm package version is available#96Gregory Joseph (gnjoseph) wants to merge 6 commits into
Gregory Joseph (gnjoseph) wants to merge 6 commits into
Conversation
Adds an opt-out, notify-only update check that reads the published dist-tags for @microsoft/spe-mcp from the public npm registry and, when a newer release exists, appends one concise notice to a single tool result. The check is fire-and-forget: it never blocks a tool call, never writes to stdout, and never downloads, installs, or executes anything. Auto-update is out of scope. Behaviour - Channel-aware: a prerelease install is compared against its own dist-tag (e.g. alpha) and a newer stable `latest` is detected separately. - Strict, dependency-free SemVer 2.0.0 parsing and precedence comparison (src/semver.ts); zero new runtime dependencies (budget stays at 6, guarded by a packaging test). - Skipped automatically for source checkouts and in CI. - Owner-only cache under the data directory with a 24h TTL, a failure backoff, and per-target notification suppression so a given version is announced once. - `status_get` reports the running version, update state, cached latest, last check time, registry, cache path, and the opt-out controls — read from disk, never from the network. Privacy and security (SEC-008) - Requests exactly one fixed package path over HTTPS with no query string; redirects and cross-host responses are rejected. - Unauthenticated: `credentials: "omit"`, no Authorization header, no cookies, no .npmrc, no npm subprocess. - Sends no install GUID, machine, user, tenant, subscription, correlation, or session identifier. The static product User-Agent is omitted when telemetry is disabled. - Bounded by a 2s timeout and a 64KB response cap; hostile registry payloads are rejected by strict parsing and prototype-pollution-safe key filtering. - `SPE_NPM_REGISTRY` must be an https: URL with no credentials, query, or fragment; anything else disables the check for that run. - Before the first request in a process, a one-time collection notice naming the endpoint, the boundary, and the opt-out is printed to stderr. - Five zero-network opt-outs: SPE_MCP_UPDATE_CHECK=false (preferred), --no-update-check, SPE_NO_UPDATE_CHECK=1 (backward-compatible alias), NO_UPDATE_NOTIFIER=1, and SPE_MCP_COLLECT_TELEMETRY=false. When suppressed there is no request, no stderr notice, and no cache write. - The cache contains no identifier, is retained until deleted, and is removed by `spe-mcp logout` and `spe-mcp auth --reset`. Documentation - README, PRIVACY, docs/DATA-FLOW, docs/SECURITY-CONTROLS, docs/TROUBLESHOOTING, and CHANGELOG disclose that registry.npmjs.org (npm, Inc./GitHub) is outside the Microsoft 365 / Azure compliance boundary and outside EU Data Boundary commitments, that the connection discloses IP address, User-Agent, and request time, that the local cache is retained until deleted, and that there is no auto-update. - Known limitation, documented as an open tradeoff and not a sign-off: Node's built-in fetch ignores HTTP(S)_PROXY/NO_PROXY, so the check cannot be routed through an egress proxy. It fails closed. Adding proxy support would require a new runtime dependency, which is outside the dependency budget. Tests - src/semver.test.ts and src/update-check.test.ts cover SemVer edge cases and prerelease precedence, cache TTL/backoff/suppression, hostile registry data, timeout/offline/non-200/oversize responses, every opt-out, one-time notice behaviour, exact request URL and headers, redirect and cross-host rejection, telemetry opt-out, cache deletion on logout, first-run notice ordering, and offline status reporting. - status, packaging, and protocol e2e suites extended; no e2e test invokes the environment-dependent status_get path. AB#3219463 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d status
Follow-up to the npm update-awareness change, addressing review feedback.
Behaviour fixes:
- Record a version as "already notified" only when the notice is actually
delivered by takePendingUpdateNotice(), not when the probe resolves. The
cache is re-read and merged at delivery time, so a process that exits or
restarts before the notice is shown replays it instead of losing it
permanently. Delivery never resurrects a cache file that logout removed.
- Set the failure backoff to the same 24h TTL as a success, so the documented
"at most one registry request per day" claim holds for failures too.
- Guard startUpdateCheck() with an in-flight check so a second call cannot
start a duplicate probe in the same process.
- Create a { updateAvailable } structured twin when a tool result has no
structuredContent, instead of dropping the structured signal.
Defence in depth:
- Build tag maps on a null-prototype object and validate cached channel tag
names with the same rules applied to registry data, so hostile cache
content stays inert.
- getUpdateStatus() reads no registry, no cached version and no disk at all
when the check is disabled, and skips the disk read entirely once this
process already holds a result. Removed the unreachable registry branch.
Docs:
- Correct the default data directory in PRIVACY.md to ~/.spe-mcp (
%USERPROFILE%\.spe-mcp on Windows) and map the cache file permissions to
SEC-003.
- Update SEC-008 and the changelog for the 24h failure backoff and for the
notice being persisted at delivery.
- Document the remaining last-writer-wins risk on concurrent cache writes as
a follow-up; shared secure-fs atomicity is deliberately unchanged here.
Privacy wording, the first-run collection notice and every opt-out control
are unchanged. Still notify-only: no auto-update, no new runtime dependency
(6), and nothing is ever written to stdout.
Tests: added coverage for delivery-time persistence across simulated process
exits and restarts, the 24h failure backoff, the in-flight guard, hostile
cache content, the cheap/quiet status path and the structured-twin creation.
AB#3219463
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ure wording Adds the affirmative end-to-end regression that the earlier notice-delivery fix was missing, and corrects documentation and CLI wording that described the update check inaccurately. Regression coverage (AB#3219517): - New spawned JSON-RPC suite drives a real server over stdio from a package layout that looks registry-installed, proving the sequence a unit test cannot: connect and exit before any tool call leaves the notice undelivered, the next process still emits it on the first successful tool result, and a third process stays silent. The notice is therefore never lost and never repeated. - Deleting the cache mid-flight no longer lets a pending delivery recreate it, and both CLI credential-clearing paths are asserted to clear the cache. Wording corrections: - Opting out of telemetry suppresses the registry request entirely; it is a skip reason, not a request with a header removed. Documentation no longer implies a request still happens. - The public npm registry and GitHub are described as not being Microsoft M365 or Azure Online Services and therefore outside the Product Terms, DPA, and EUDB commitments, rather than merely "not Microsoft". - Disclosure tables now list the standard TLS and HTTP connection metadata that any HTTPS request reveals, alongside IP address and User-Agent. - CLI help, option types, and docs name SPE_MCP_UPDATE_CHECK=false as the preferred control; SPE_NO_UPDATE_CHECK is labelled a legacy alias throughout. No behaviour change to the check itself, no new runtime dependencies, and auto-update remains out of scope. AB#3219463 AB#3219517 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ecution-neutral Addresses OSS package/release review feedback on the update-awareness change. - Update remediation is now execution-mode neutral. `UpdateAvailable` carries a `packageSpec` (and optional `stablePackageSpec`) instead of a bare global install command, and the rendered notice tells the operator to update or pin the package spec in their MCP client config, or reinstall the copy they actually launch. An unpinned `npx` launch may keep starting a cached build, so recommending only `npm install -g` was inaccurate for the documented default launch mode. `status_get` uses the same wording. Covered by a rendered-guidance unit test and refreshed README guidance. - Regenerated THIRD-PARTY-NOTICES from the locked production tree with `npm run notices`. The checked-in file was stale relative to package-lock.json; the regenerated output was verified byte-identical across two consecutive runs, so the generator is deterministic. A new offline consistency test compares each direct production dependency's version in the notices file against its resolved version in package-lock.json so a stale file fails CI. - Published disclosure documents. `package.json` `files` now ships CHANGELOG.md, NOTICE.md, PRIVACY.md, SECURITY.md, SUPPORT.md, docs/DATA-FLOW.md, docs/SECURITY-CONTROLS.md and docs/TROUBLESHOOTING.md alongside README.md, so the README's relative links resolve in an installed package and the privacy and security disclosures exist on disk. Packaging tests assert the allow-list, the files' presence, and that no .npmignore can override the allow-list; `npm pack --dry-run` confirms all of them are in the tarball. - Wording accuracy. "Anonymous" is replaced with "unauthenticated, without a user identifier" across the collection notice, README, PRIVACY.md and docs/DATA-FLOW.md, since the connection still discloses an IP address. The first-run stderr collection notice now names the registry actually contacted when SPE_NPM_REGISTRY is overridden; the value is still validated (HTTPS only, no credentials, no query or fragment, length capped) before it is echoed. No new runtime dependencies (still 6). Auto-update remains out of scope; stdout is never written to. Known, deliberately unaddressed here: server.json declares 0.1.0-alpha.1 while package.json declares 0.2.0-alpha.1. That mismatch predates this branch and is left for the rebase that follows the in-flight packaging work, to avoid an arbitrary version bump on a feature branch. AB#3219463 AB#3219517 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s informational Documentation and wording follow-ups for the npm update-awareness check. - README: the "Data collection" notice now states plainly that, separately from anything sent to Microsoft, the default-on update check contacts the public npm registry (registry.npmjs.org, operated by npm, Inc./GitHub), which is not a Microsoft 365 or Azure Online Service and sits outside the Microsoft 365 / Azure compliance boundary, the Product Terms, the DPA, and the EU Data Boundary. The verbatim sample notice was regenerated from the implementation so it cannot drift. - NOTICE.md: added a canonical "Third-party services contacted" section covering purpose, what is sent (unauthenticated, no user identifier, no credentials, tenant, machine, session, or customer data), what the endpoint can observe (source IP, package path, User-Agent, TLS/HTTP connection metadata), the compliance boundary, local cache retention and deletion, the opt-out controls, and the fact that nothing is ever downloaded, installed, or self-updated. NOTICE.md already ships in the published package; a packaging test now asserts both that it is packed and that the disclosure content is present. - PRIVACY.md: cross-references the new NOTICE section and no longer describes applying an update as a manual npm install the reader runs. - Update notice and status_get: the remediation text is now explicitly informational. It states that nothing is installed or changed automatically and that updating requires a person to change the MCP client configuration or reinstall the copy the client actually launches. It is no longer phrased as a command to execute. Auto-update remains out of scope; the check only notifies. No new runtime dependencies (6 unchanged). External privacy, CELA, and OSPO confirmations are tracked outside this repository and are not asserted here. AB#3219463 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…port metadata The npm update-check disclosure in docs/DATA-FLOW.md claimed the registry lookup "carries no data of yours" and "transmits nothing about you". Both conflict with the IP address and standard TLS/HTTP connection metadata that the same document already discloses in the outbound endpoint table. Replace both statements with precise wording: the request sends no customer content and no application-level user, tenant, subscription, or install identifier, while the HTTPS connection itself necessarily exposes the source IP address and standard transport metadata (TLS handshake and SNI, Host / Accept / User-Agent headers, request timing) to the registry operator. The vocabulary matches the endpoint table and the NOTICE.md third-party services section so the disclosure reads consistently across the package. Also publish CONTRIBUTING.md so the relative link from the shipped README resolves in an installed copy, and add a packaging test that every root document the README links to relatively is present in the files allow-list. AB#3219463 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds asynchronous npm update awareness with cached status, one-time tool-result notices, opt-outs, and documentation.
Changes:
- Adds SemVer-aware registry checks with 24-hour caching.
- Integrates update status into
status_getand successful tool responses. - Adds CLI controls, tests, packaging updates, and privacy/security disclosures.
Review findings:
- Moderate (4 votes): Invalid registry responses can be cached as successful up-to-date checks in
src/update-check.ts. - Moderate (2 votes):
src/tools/status.tsshould classify custom registries neutrally rather than labeling all as third-party. - Moderate (3 votes):
src/update-check.tsdisclosure text should distinguish the default npm registry from organization-controlled mirrors.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Reviewed change |
|---|---|
THIRD-PARTY-NOTICES |
Regenerated dependency notices |
src/version.ts |
Sources runtime package metadata |
src/update-notice-e2e.test.ts |
Tests spawned-process notices |
src/update-check.ts |
Implements registry checks, caching, notices, and disclosures |
src/update-check.test.ts |
Tests update-check behavior |
src/types.ts |
Adds update-check configuration types |
src/tools/status.ts |
Reports update status |
src/tools/status.test.ts |
Tests status output |
src/semver.ts |
Implements strict SemVer handling |
src/semver.test.ts |
Tests SemVer behavior |
src/protocol-e2e.test.ts |
Tests protocol opt-out behavior |
src/paths.ts |
Defines the update-cache path |
src/packaging.test.ts |
Verifies package contents and disclosures |
src/index.ts |
Integrates startup checks and result notices |
src/index-update-notice.test.ts |
Tests result decoration |
src/cli.ts |
Adds CLI opt-outs and cache cleanup |
README.md |
Documents update notifications and privacy |
PRIVACY.md |
Documents registry disclosure and opt-outs |
package.json |
Updates package metadata and published documents |
NOTICE.md |
Adds third-party service disclosure |
docs/TROUBLESHOOTING.md |
Adds troubleshooting guidance |
docs/SECURITY-CONTROLS.md |
Documents security controls |
docs/DATA-FLOW.md |
Documents data flow |
CHANGELOG.md |
Records the feature |
Suppressed comments (5)
src/update-check.ts:827
newerTagVersionaccepts prerelease SemVer values for thelatestdist-tag. Since npm dist-tags are mutable andlatestcan point to a prerelease, an alpha install can classify (for example)2.0.0-beta.1asstableVersionand present it as a stable release. Filter thelatestcandidate to versions with no prerelease identifiers, and apply the same rule when reconstructing status from the cache.
const stableVersion = newerTagVersion(tags ?? {}, "latest", current);
src/update-check.ts:577
Number.isFinitedoes not guarantee that a value is representable byDate; a corrupted cache containing a finite value such as1e308passes this check, thengetUpdateStatus()throwsRangeErrorwhen line 993 callstoISOString(). Validate a safe integer within JavaScript Date's supported millisecond range before acceptingcheckedAt, so the documented corruption-tolerant cache path cannot breakstatus_get.
if (typeof candidate.checkedAt !== "number" || !Number.isFinite(candidate.checkedAt)) return null;
src/update-check.ts:779
- The cache read and the network request are not synchronized. With the default shared
~/.spe-mcpdata directory, two server processes started while the cache is stale or absent can both observefresh === nulland each callfetchDistTags, violating the documented “at most once every 24 hours” request guarantee (and emitting multiple disclosures). Use an atomic per-cache lock and re-read after acquiring it, or explicitly narrow the guarantee to a single process/cache owner.
const cached = readCache();
const fresh = cached && isCacheFresh(cached, registry, now) ? cached : null;
src/update-check.ts:888
- The suppression check only records the primary
latesttarget. For a prerelease build, a newer channel version can be announced first; when the stable release is published later,latestcan remain that already-notified channel version, so the separately constructed stable target never produces another notice. Track and persist both target versions (or use a composite target key) before returning on suppression.
if (notifiedFor.includes(latest)) return;
pendingNotice = { text: renderNotice(update), updateAvailable: update };
src/update-check.ts:679
- The fallback returns
channelVersionorlatestverbatim even thoughreadCache()only type-checks them as strings. A corrupted cache can therefore inject arbitrary text or newlines into thestatus_getMarkdown, contrary to the module's promise that versions are validated before being shown. Return only SemVer-validated values (or omit the cached version).
return cache.channelVersion ?? cache.latest;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } | ||
| rows += `| **Last update check** | ${status.lastCheckedAt ?? "never"} |\n`; | ||
| if (status.registry) { | ||
| rows += `| **Update registry** | \`${status.registry}\` (third party, outside the M365/Azure boundary) |\n`; |
| const body = await readCappedText(response, MAX_RESPONSE_BYTES); | ||
| if (body === null) return null; | ||
|
|
||
| return extractDistTags(body); |
Comment on lines
+169
to
+174
| "The npm registry is a third-party service OUTSIDE the Microsoft 365 / Azure", | ||
| "compliance boundary. The request is unauthenticated and carries no user", | ||
| "identifier and no account, tenant, machine, session, or content data — but", | ||
| "the connection itself discloses your IP address, the package name, and the", | ||
| "product User-Agent to that third party. The result is cached locally until", | ||
| "you delete it. Nothing is downloaded, installed, or updated automatically.", |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a lightweight npm update check so installed
@microsoft/spe-mcpusers can see when a newer release is available without changing normal MCP behavior.status_get.--no-update-checkand the existing telemetry opt-out.Proof
Targeted regression suite
184 targeted tests pass, including three spawned JSON-RPC restart scenarios.
Exact message shown to an outdated user
MCP clients can style tool results differently, but this is the exact text the server appends to the first successful tool response when it detects a newer release.
One-time delivery behavior
The first successful tool result includes the update notice; later calls in the same process do not repeat it.
Status and opt-out behavior
status_getshows the cached result and registry boundary without triggering a network request. Opt-out modes make no request, print no notice, and create no cache file.Validation
npm run cigit diff --check origin/main...HEADpassedTracks AB#3219463.