Skip to content

feat: notify when a newer npm package version is available - #96

Draft
Gregory Joseph (gnjoseph) wants to merge 6 commits into
microsoft:mainfrom
gnjoseph:feature/npm-update-awareness
Draft

feat: notify when a newer npm package version is available#96
Gregory Joseph (gnjoseph) wants to merge 6 commits into
microsoft:mainfrom
gnjoseph:feature/npm-update-awareness

Conversation

@gnjoseph

@gnjoseph Gregory Joseph (gnjoseph) commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a lightweight npm update check so installed @microsoft/spe-mcp users can see when a newer release is available without changing normal MCP behavior.

  • Starts the check asynchronously after the MCP transport connects.
  • Emits a one-time informational notice on the first successful tool response when an update is available.
  • Exposes cached update status through status_get.
  • Checks at most once every 24 hours and fails silently.
  • Never downloads, installs, or executes an update.
  • Supports five zero-network opt-outs, including --no-update-check and the existing telemetry opt-out.
  • Skips the check in CI and source checkouts.

Proof

Targeted regression suite

184 targeted tests pass, including three spawned JSON-RPC restart scenarios.

Targeted update-awareness regression suite passing

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.

Exact update message returned to an outdated SPE MCP user

One-time delivery behavior

The first successful tool result includes the update notice; later calls in the same process do not repeat it.

One-time update notice and cached status behavior

Status and opt-out behavior

status_get shows 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.

Zero-network update-check opt-out behavior

Validation

  • npm run ci
    • Typecheck passed
    • Build passed
    • 1,007 tests passed, 7 expected Windows skips
    • Lint passed with no errors
  • Package dry-run passed: 271 files; update-check runtime files and disclosure documents included; tests and source excluded
  • Spawned JSON-RPC restart/delivery regression passed
  • git diff --check origin/main...HEAD passed

Tracks AB#3219463.

grjoseph and others added 6 commits August 20, 2026 19:28
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_get and 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.ts should classify custom registries neutrally rather than labeling all as third-party.
  • Moderate (3 votes): src/update-check.ts disclosure 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

  • newerTagVersion accepts prerelease SemVer values for the latest dist-tag. Since npm dist-tags are mutable and latest can point to a prerelease, an alpha install can classify (for example) 2.0.0-beta.1 as stableVersion and present it as a stable release. Filter the latest candidate 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.isFinite does not guarantee that a value is representable by Date; a corrupted cache containing a finite value such as 1e308 passes this check, then getUpdateStatus() throws RangeError when line 993 calls toISOString(). Validate a safe integer within JavaScript Date's supported millisecond range before accepting checkedAt, so the documented corruption-tolerant cache path cannot break status_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-mcp data directory, two server processes started while the cache is stale or absent can both observe fresh === null and each call fetchDistTags, 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 latest target. For a prerelease build, a newer channel version can be announced first; when the stable release is published later, latest can 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 channelVersion or latest verbatim even though readCache() only type-checks them as strings. A corrupted cache can therefore inject arbitrary text or newlines into the status_get Markdown, 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.

Comment thread src/tools/status.ts
}
rows += `| **Last update check** | ${status.lastCheckedAt ?? "never"} |\n`;
if (status.registry) {
rows += `| **Update registry** | \`${status.registry}\` (third party, outside the M365/Azure boundary) |\n`;
Comment thread src/update-check.ts
const body = await readCappedText(response, MAX_RESPONSE_BYTES);
if (body === null) return null;

return extractDistTags(body);
Comment thread src/update-check.ts
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.",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants