Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,54 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **Update awareness (notify only).** The server now checks the public npm registry at
most once every 24 hours and, when a newer release of `@microsoft/spe-mcp` exists,
appends a single concise `Update available: …` notice to one tool result (plus an
optional `structuredContent.updateAvailable` payload). 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 explicitly out of scope. It is channel-aware (an
`alpha` install is compared against the `alpha` dist-tag, and a newer `latest` is
reported separately), adds **zero new runtime dependencies**, and is skipped
automatically in CI and when running from a source checkout. Disable it with
`SPE_MCP_UPDATE_CHECK=false` (preferred), `--no-update-check`, `SPE_NO_UPDATE_CHECK=1`
(legacy alias), `NO_UPDATE_NOTIFIER=1`, or `SPE_MCP_COLLECT_TELEMETRY=false` (the telemetry
opt-out suppresses the registry request entirely);
when disabled, **no network request, stderr notice, or cache write occurs**. Point it at a
mirror with `SPE_NPM_REGISTRY` (HTTPS-only).
- **Transparency for the update check.** Before the first registry request in a process, the
server prints a one-time **stderr** collection notice naming the endpoint, the boundary, and
the opt-out. `status_get` now reports the running server version, the update-check state,
the locally cached latest version, the time of the last check, the registry in use, the
cache-file path, and the opt-out controls — all read from disk, with **no network access**.
- **Update-check cache lifecycle.** The cached result at `<data dir>/update-check.json`
contains **no identifier** and is retained until deleted; `spe-mcp logout` and
`spe-mcp auth --reset` now remove it alongside the cached tokens. A version is recorded
as "already notified" only when the notice is actually delivered on a tool result, so a
process that exits before any tool call replays the notice on the next run instead of
losing it.
- **Boundary disclosure.** `NOTICE.md` (new **Third-party services contacted** section),
`README.md`, `PRIVACY.md`, `docs/DATA-FLOW.md`,
`docs/SECURITY-CONTROLS.md`, and `docs/TROUBLESHOOTING.md` document that
`registry.npmjs.org` (npm, Inc./GitHub) is **not a Microsoft 365 or Azure Online Service** and
is therefore the only endpoint **outside the Microsoft 365 / Azure compliance boundary** and
not covered by the Microsoft Product Terms, the DPA, or EU Data Boundary commitments; that the
connection discloses IP address, the static `User-Agent`, standard TLS/HTTP connection
metadata, and the request time; that no auto-update exists; and
that Node's built-in `fetch` cannot route through `HTTP(S)_PROXY` — an open, unresolved
tradeoff accepted to preserve the zero-runtime-dependency budget. `docs/DATA-FLOW.md`
states precisely that the registry lookup sends no customer content and no
application-level user, tenant, subscription, or install identifier, while the HTTPS
connection itself still exposes the source IP address and standard transport metadata —
it makes no absolute-anonymity claim.
- **Informational-only update guidance.** The update notice and `status_get` state that the
message is informational, 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 never phrased as a command to run. The guidance is
execution-mode neutral (`npx`, global install, or project-local install) and reports the
package spec to target rather than a single install command. The published package now also
ships `NOTICE.md`, `PRIVACY.md`, `CHANGELOG.md`, `SUPPORT.md`, `SECURITY.md`,
`CONTRIBUTING.md`, `docs/DATA-FLOW.md`, `docs/SECURITY-CONTROLS.md`, and
`docs/TROUBLESHOOTING.md`, so the disclosure links in the installed `README.md` resolve.
- **Per-instance data directory.** New `--data-dir <path>` flag and `SPE_DATA_DIR`
environment variable select where the provisioning `state.json` and MSAL token
cache are stored (precedence: flag > env > default `~/.spe-mcp`). Point each
Expand All @@ -18,6 +66,22 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Security

- **Update-check hardening (SEC-008).** The npm version check is HTTPS-only and requests
exactly one fixed package path with no query string; redirects and cross-host responses are
rejected. It is unauthenticated (no `Authorization`, cookies, `credentials: "omit"`, no
`.npmrc`, no `npm` subprocess), sends **no install GUID, machine, user, tenant, subscription,
correlation, or session identifier**, and discloses only what any HTTPS connection reveals
(IP address, the static product `User-Agent`, and standard TLS/HTTP connection metadata).
Setting `SPE_MCP_COLLECT_TELEMETRY=false` suppresses the registry request entirely.
It is bounded by a 2-second timeout and a 64 KB response
cap, parsed with strict SemVer and prototype-pollution-safe key filtering, and cached
owner-only (SEC-003) with a 24-hour TTL — a failed check backs off for the same 24 hours,
so at most one request per day is made either way — deleted on `logout` /
`auth --reset`. `SPE_NPM_REGISTRY` values carrying credentials, a query string, or a
fragment are rejected. **Known limitation:** Node's built-in `fetch` ignores
`HTTP(S)_PROXY`/`NO_PROXY`, so the request cannot be routed through an egress proxy; it
fails closed.

- **Fail-closed credential/state file handling.** The data directory and token
cache files are now validated fail-closed: a symlinked, foreign-owned, or
group/other-accessible directory is refused (POSIX `0o700`); an off-`%USERPROFILE%`
Expand Down
39 changes: 38 additions & 1 deletion NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ consent to these practices.
> (`spe-mcp-server/<version>`) attached to the Microsoft Graph and Azure Resource Manager
> requests you already make on your own behalf; it carries no personal, tenant, or usage
> data and is used only for aggregate traffic attribution. It is **on by default** and can be
> suppressed with `SPE_MCP_COLLECT_TELEMETRY=false` (see below). See [PRIVACY.md](PRIVACY.md) and
> suppressed with `SPE_MCP_COLLECT_TELEMETRY=false` (see below). Separately from anything sent
> to Microsoft, a default-on update check contacts the public npm registry — see
> [Third-party services contacted](#third-party-services-contacted) below. See
> [PRIVACY.md](PRIVACY.md) and
> [docs/DATA-FLOW.md](docs/DATA-FLOW.md) for the full data-flow description.

## Telemetry configuration
Expand All @@ -35,6 +38,40 @@ attribution — there is no usage-analytics channel and no personal, tenant, or
opt out, set `SPE_MCP_COLLECT_TELEMETRY=false` in your environment; the product token is then
omitted from all outbound requests.

## Third-party services contacted

Beyond the Microsoft services you explicitly configure (Microsoft Graph, Azure Resource Manager,
and Microsoft Entra ID), this build contacts **one non-Microsoft service by default**.

**Public npm registry — `https://registry.npmjs.org` (npm, Inc., a GitHub company).**

- **Purpose.** After the server connects, it makes a single fire-and-forget request to read the
published version list (`dist-tags`) for `@microsoft/spe-mcp`, so it can tell you in a tool
result when a newer release exists.
- **What is sent.** The request is **unauthenticated and carries no user identifier**. No
credentials, tokens, cookies, account, tenant, machine, session, install, or customer data are
sent. The only application-supplied values are the package name in the request path and a
static product `User-Agent` (`spe-mcp-server/<version>`), which is omitted entirely when
telemetry is disabled — and in that case no request is made at all.
- **What the endpoint can observe.** As with any HTTPS request, the operator can see your
**source IP address**, the requested **package path**, the static **`User-Agent`**, and
standard **TLS/HTTP connection metadata** (TLS handshake details, timestamps, request size).
- **Compliance boundary.** npm and GitHub are **not Microsoft 365 or Azure Online Services**.
This endpoint sits **outside the Microsoft 365 / Azure compliance boundary** and is **not**
covered by the Microsoft Product Terms, the Microsoft Products and Services Data Protection
Addendum (DPA), or the EU Data Boundary. Data handling is governed by the applicable
**GitHub/npm privacy statements**, not by your Microsoft agreements.
- **Nothing is downloaded or installed.** The check reads version metadata only. This build
never downloads, installs, executes, or self-updates anything. Acting on a notice is a human
decision.
- **Local retention.** The result is cached in a local file under the server data directory
until you delete it (`spe-mcp logout` and `spe-mcp auth --reset` remove it).
- **How to turn it off (no request is made).** `--no-update-check`,
`SPE_MCP_UPDATE_CHECK=false` (preferred), `SPE_NO_UPDATE_CHECK` (legacy alias),
`NO_UPDATE_NOTIFIER`, `SPE_MCP_COLLECT_TELEMETRY=false`, or any recognized CI environment.

See [PRIVACY.md](PRIVACY.md) and [docs/DATA-FLOW.md](docs/DATA-FLOW.md) for the full disclosure.

## Compliance responsibility

This MCP server may interact with clients and services outside Microsoft compliance
Expand Down
117 changes: 114 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ organization's agreements with Microsoft.
**The tool opens no dedicated usage-analytics channel and sends no personal, tenant, or
per-user data to Microsoft.** The only Microsoft-bound attribution signal is a static product
`User-Agent` token, which is on by default and can be turned off (see
[Turning it off](#turning-it-off)). Specifically:
[Turning it off](#turning-it-off)). The only destination that is **not a Microsoft 365 or Azure
Online Service** is an unauthenticated public-package lookup — sent without a user identifier —
on the npm registry used to notify you of
newer releases, which can also be turned off. Specifically:

- **No telemetry channel.** The tool does not implement application telemetry and does not
"phone home." Diagnostic logs are written to the local process's **stderr only**, with
Expand All @@ -32,6 +35,83 @@ per-user data to Microsoft.** The only Microsoft-bound attribution signal is a s
aggregate traffic driven by this tool. It is a request header on calls you already make —
not a separate data feed — and it is **on by default**; set `SPE_MCP_COLLECT_TELEMETRY=false`
to omit it (see [Turning it off](#turning-it-off)).
- **Update check (public npm registry — the only destination that is not a Microsoft 365 or
Azure Online Service, and the only destination outside the compliance boundary).** At most
once every 24 hours the tool reads
the published version list for `@microsoft/spe-mcp` from the public npm registry
(`https://registry.npmjs.org`, override with `SPE_NPM_REGISTRY`) so it can tell you when a
newer release exists (`src/update-check.ts`).

> **Boundary disclosure.** `registry.npmjs.org` is operated by **npm, Inc. (GitHub)**. It is
> **not a Microsoft 365 or Azure Online Service**, so it is **outside the Microsoft 365 /
> Azure compliance boundary** and outside any **EU Data Boundary** commitment that applies to
> your tenant. Data sent there is **not covered by the Microsoft Product Terms or by the
> Microsoft Products and Services Data Protection Addendum (DPA)**; it is governed by the
> [npm privacy policy](https://docs.npmjs.com/policies/privacy).

**Exactly one request is made,** to the exact package path with no query string and no
fragment:

```text
GET https://registry.npmjs.org/@microsoft%2fspe-mcp
```

**What the third party can see.** The request is an **unauthenticated HTTP GET of
public package metadata, sent without a user identifier** — the same lookup `npm view` performs.
The request body and headers
carry no identifiers, but the connection itself necessarily discloses to npm:

| Disclosed to npm | Why |
|------------------|-----|
| Your **IP address** (or your egress/NAT address) | Inherent to making an HTTPS connection |
| The **package name** `@microsoft/spe-mcp` | It is the resource being requested |
| The static product **`User-Agent`** `spe-mcp-server/<version>` | Standard client identification |
| Standard **TLS/HTTP connection metadata** — TLS handshake parameters and the SNI host name, the `Host` and `Accept` request headers, and connection/request timing | Inherent to any HTTPS request; not set or enriched by this tool |
| Approximate **time of the request** | Inherent to any server-side request log |

Setting `SPE_MCP_COLLECT_TELEMETRY=false` **suppresses the registry request entirely** — it is
a skip reason, so no connection is opened and none of the rows above occur. (The shared
user-agent helper also omits the product `User-Agent` when telemetry is off; for this endpoint
that is defense in depth only, because no request is made at all.)

**What is never sent:** no credentials, tokens, cookies, or `Authorization` header; no
`.npmrc` and no npm subprocess; **no install GUID, machine identifier, hostname, user name,
tenant ID, subscription ID, correlation ID, or session ID**; no usage, prompt, or content
data; no data about which tools you invoked. The tool generates and stores **no identifier of
any kind** for this feature. Redirects are rejected outright, so the request cannot be
bounced to a different host.

**No auto-update.** Nothing is downloaded, installed, executed, or modified. The tool only
*notifies* you; the notice is informational, and acting on it is a human decision — updating
means pointing your MCP client configuration (or reinstalling the copy it actually launches)
at a newer package spec.

**Local retention.** The result is cached on your machine at
`<data dir>/update-check.json`, written with the same owner-only permissions as the token
cache (0700 directory / 0600 file, control **SEC-003**; the check itself is control
**SEC-008**). `<data dir>` defaults to `%USERPROFILE%\.spe-mcp` on Windows or `~/.spe-mcp`
elsewhere, can be overridden with `SPE_DATA_DIR`, and the exact path in use is reported by
`status_get`. The cache contains only the checked version strings, the
registry URL, a timestamp, and which versions you have already been told about — **no
identifier**. It is **retained locally until you delete it**: there is no automatic expiry of
the file itself, only of its freshness. Run `spe-mcp logout` or `spe-mcp auth --reset` to
delete it, or remove the file by hand.

**First-run notice.** Before the **first** network request in a process, the tool prints a
one-time notice to **stderr** naming the endpoint, the boundary, and how to turn the check
off. No notice is printed when the check is disabled or served from cache.

**Turning it off.** The check is **skipped automatically** in CI and when running from a
source checkout, and can be disabled outright (see [Turning it off](#turning-it-off)); when
disabled, **no request is made, no notice is printed, and no cache file is written**.

**Known limitation (proxy).** The check uses the Node.js built-in `fetch`, which does **not**
honour `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`. On a network that requires an egress proxy
the request simply fails and is silently ignored (fail-closed — no data leaves by another
route), but it also means the check **cannot be routed through your proxy for inspection or
policy enforcement**. Adding proxy support would require a new runtime dependency, which this
project deliberately avoids. This is recorded as an **open, unresolved tradeoff**; if your
environment requires all egress to be proxied, disable the check.

See [docs/DATA-FLOW.md](docs/DATA-FLOW.md) for the full list of network endpoints and what
travels to each.
Expand All @@ -42,7 +122,12 @@ travels to each.
> completeness; **this build opens no usage-analytics channel** — the only Microsoft-bound
> signal is the product `User-Agent` attribution token described above, which is on by default
> and can be turned off (see [Turning it off](#turning-it-off) and the
> [Telemetry configuration](NOTICE.md#telemetry-configuration) note).
> [Telemetry configuration](NOTICE.md#telemetry-configuration) note). Separately from anything
> sent to Microsoft, the default-on update check contacts the public npm registry, which is
> **not a Microsoft 365 or Azure Online Service** and is outside the M365/Azure compliance
> boundary, the Product Terms/DPA, and the EU Data Boundary — see
> [Update check (public npm registry)](#what-the-tool-collects-and-sends) above and
> [NOTICE.md — Third-party services contacted](NOTICE.md#third-party-services-contacted).

## Service-side data handling

Expand All @@ -68,7 +153,33 @@ Microsoft-bound telemetry signal, and it is **on by default**. To opt out, set
outbound Graph and Azure Resource Manager requests. Those requests still go out — they simply
carry the underlying tool's default `User-Agent` instead (e.g. the Azure CLI's own token for
`az`/`azd`, or the Node runtime default for direct Graph calls), whose logging is governed by
those services' own terms. To further limit
those services' own terms.

The **update check** — the only outbound call to a service that is **not a Microsoft 365 or Azure
Online Service**, and therefore the only call that leaves the Microsoft 365 / Azure compliance
boundary (and the Product Terms / DPA / EUDB commitments) — is on by default in published
installs. Any one of the following disables it completely:

| Opt-out | Effect |
|---------|--------|
| `SPE_MCP_UPDATE_CHECK=false` | **Preferred public control.** Disables the check for every instance in that environment (`0`, `off`, `no` also accepted) |
| `spe-mcp start --no-update-check` | Disables the check for that server instance |
| `SPE_NO_UPDATE_CHECK=1` | **Legacy alias** for `SPE_MCP_UPDATE_CHECK=false`, honoured identically |
| `NO_UPDATE_NOTIFIER=1` | Community-standard opt-out, honoured identically |
| `SPE_MCP_COLLECT_TELEMETRY=false` | Opting out of telemetry suppresses the registry request entirely |

When disabled, the tool makes **no registry request, prints no collection notice, and writes no
update-check cache file** — the code path exits before any network or disk access. `status_get`
still reports the state, reading only what is already on disk.

The check is also skipped automatically in CI (`CI`, `GITHUB_ACTIONS`, `TF_BUILD`, …) and when
the server is run from a source checkout rather than an installed package.

To delete data already cached by the check, run `spe-mcp logout` or `spe-mcp auth --reset` —
both remove `<data dir>/update-check.json` along with the cached authentication tokens. You can
also delete the file by hand; `status_get` prints its full path.

To further limit
outbound calls you can run with `--read-only` (no mutating operations) or `--tools` (restrict
the exposed tool set, including the optional Microsoft Learn documentation lookup). See
[docs/DATA-FLOW.md](docs/DATA-FLOW.md), [docs/SECURITY-CONTROLS.md](docs/SECURITY-CONTROLS.md),
Expand Down
Loading
Loading