Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
65fd2b9
feat(auth): revoke OAuth tokens at the authorization server on clear …
cliffhall Aug 28, 2026
15f381c
test(auth): cover the no-window guard in getWebProxiedFetch (#2144)
cliffhall Aug 28, 2026
7f27d7e
fix(auth): address Copilot review round 1 on the RFC 7009 revocation …
cliffhall Aug 28, 2026
4a24c48
fix(auth): address Copilot review round 2 on the RFC 7009 revocation …
cliffhall Aug 28, 2026
781aa69
fix(auth): revoke every issuer-bound grant, not just the active one (…
cliffhall Aug 28, 2026
6c499f3
fix(auth): read enumerated issuers exactly, and stop the TUI tone goi…
cliffhall Aug 28, 2026
e9cd678
fix(auth): key grant dedup by issuer, and isolate per-slot read failu…
cliffhall Aug 28, 2026
56ac830
fix(auth): RFC-encode the Basic credential; make the TUI clear await …
cliffhall Aug 28, 2026
43c817c
fix(auth): bound the whole teardown, and scope the TUI clear to its s…
cliffhall Aug 28, 2026
5170cc4
fix(auth): close the fail-open issuer check and the remaining clear r…
cliffhall Aug 28, 2026
1661f09
fix(web): surface a failed OAuth clear instead of floating its reject…
cliffhall Aug 28, 2026
e2cbf0d
fix(web): lock the OAuth clear so a double click cannot run it twice …
cliffhall Aug 28, 2026
c0bcdc7
fix(tui): forward the clear promise from App, so AuthTab can actually…
cliffhall Aug 28, 2026
69fcdd3
fix(auth): clear before waiting on the network, not after (#2144)
cliffhall Aug 28, 2026
4429c0c
fix(auth): keep the failure path total, and align the docs with the n…
cliffhall Aug 28, 2026
a4ee263
fix(auth): refuse a grant whose authorization server cannot be establ…
cliffhall Aug 28, 2026
b9012da
fix: close three stale-completion holes from the round-16 suppressed …
cliffhall Aug 28, 2026
0ded02b
fix(auth): revoke with the registration bound to the grant, not the c…
cliffhall Aug 28, 2026
463227c
fix(auth): make snapshot-and-clear one atomic storage step (#2144)
cliffhall Aug 28, 2026
579a3ee
fix(auth): salvage a malformed fallback registration, and correct the…
cliffhall Aug 28, 2026
1f2e3e1
fix: address the four suppressed findings from review round 20 (#2144)
cliffhall Aug 28, 2026
5ba2fad
Merge branch 'v2/main' into v2/feat/2144-oauth-revocation
cliffhall Aug 28, 2026
eede9c1
fix: use Node storage in the revocation e2e, and drop the now-dead st…
cliffhall Aug 28, 2026
c7b18ad
Merge remote-tracking branch 'origin/v2/feat/2144-oauth-revocation' i…
cliffhall Aug 28, 2026
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
37 changes: 37 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,43 @@ v2/main/
│ │ # needs to know WHICH candidate
│ │ # answered, since that is the base its
│ │ # token request is made against — #2110;
│ │ # revocation.ts RFC 7009 token revocation —
│ │ # the request the three clear paths send.
│ │ # TWO halves — clearAndPlanRevocation then
│ │ # executeOAuthRevocation — and the split is
│ │ # the contract. The first TAKES the stored
│ │ # state and CLEARS it in one atomic storage
│ │ # step (OAuthStorage.takeRevocationSnapshot);
│ │ # the second sends from what was taken.
│ │ # There is no separate `storage.clear()` in
│ │ # any caller, deliberately — the clear cannot
│ │ # be forgotten or reordered. Both halves of
│ │ # that matter: the requests need state the
│ │ # clear destroys, and the clear must not wait
│ │ # on the network, or a fresh authorization
│ │ # completing during a 5s request is deleted
│ │ # by a clear reasoning about the grant it
│ │ # replaced. NOT cross-process atomic — no
│ │ # OAuth-store mutation takes a lock, and
│ │ # `clear` alone always had that property.
│ │ # Names the
│ │ # REFRESH token when there is one (§2.1 asks
│ │ # the AS to invalidate the access tokens
│ │ # under the same grant, so one request
│ │ # covers both). BEST-EFFORT by construction:
│ │ # every path returns a
│ │ # TokenRevocationOutcome rather than
│ │ # throwing — no advertised endpoint means
│ │ # nothing is sent and the AS behaves exactly
│ │ # as before, and a network error / non-2xx /
│ │ # timeout is reported so the local clear
│ │ # always finishes. Opt out per server with
│ │ # `oauth.revokeOnClear`, read at CLEAR time
│ │ # rather than connect time. `lost_authorization_state`
│ │ # recovery passes `revoke: false` — it clears a
│ │ # half-finished flow to retry it, and an
│ │ # authorization that never completed has no
│ │ # grant to revoke — #2144;
│ │ # scopes.ts SEP-2350 scope union, oauthUx.ts
│ │ # shared copy, mcpAuth.ts force-reauthorization,
│ │ # issuerBinding.ts SEP-2352 callback-leg failure
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ inspector/
│ └── launcher/ # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/ # Shared code consumed via the `@inspector/core` alias (no package.json)
│ ├── auth/ # OAuth: providers, discovery, storage, endpoint overrides, mid-session recovery (browser/node/remote backends);
│ │ # RFC 7009 token revocation on clear (revocation.ts);
│ │ # plus per-server secret storage — the keychain/file/memory SecretStore
│ │ # implementations, the selection policy, and the descriptor the banner and UI report
│ ├── client/ # Install-level client config (`client.json`): browser-safe parse/validate + Node load/save, remote backend, secrets
Expand Down Expand Up @@ -159,6 +160,7 @@ Each config below is a ready-made server for exercising one feature by hand. Loa
| `rfc6570-templates-http.json` | Resources tab: RFC 6570 resource-template expansion | [#1919](https://github.com/modelcontextprotocol/inspector/issues/1919) |
| `advertised-extensions-http.json` | Tool registration gated on advertised extensions | [#1739](https://github.com/modelcontextprotocol/inspector/issues/1739) |
| `oauth-custom-resource-metadata-http.json` **(legacy era)** | OAuth discovery driven by the challenge's `resource_metadata` | [#2071](https://github.com/modelcontextprotocol/inspector/issues/2071) |
| `oauth-revocation-http.json` / `oauth-no-revocation-http.json` **(legacy era)** | RFC 7009 token revocation on clear, with and without a `revocation_endpoint` | [#2144](https://github.com/modelcontextprotocol/inspector/issues/2144) |
| `logging-{legacy,modern}-http.json` | Logging, both eras | [#1629](https://github.com/modelcontextprotocol/inspector/issues/1629) |
| `subscriptions-{legacy,modern}-http.json` | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
| `subscriptions-never-acknowledged-http.json` | A `subscriptions/listen` answered with a bare result | [#2097](https://github.com/modelcontextprotocol/inspector/issues/2097) |
Expand Down Expand Up @@ -483,6 +485,21 @@ The same server is worth running against `--cli` / `--tui`, which reach it by a

The value now rides the normalized `AuthChallenge` as a string — it has to be serializable, because the web client's challenge crosses the remote-backend boundary as JSON — and is converted to a `URL` at the OAuth boundary, where it is handed to `auth()` as `resourceMetadataUrl` and to the CIMD pre-registration probe, which runs *before* `auth()` and would otherwise do its own default-location discovery. A malformed value is ignored rather than surfaced, matching the SDK's own `WWW-Authenticate` parser: discovery falls back to the default locations instead of failing the whole authorization on a bad header. The callback leg needs nothing extra — SDK `auth()` persists the URL in its discovery state, so it survives both the web full-page redirect and the CLI/TUI loopback callback.

#### Revoking tokens on clear (RFC 7009)

`oauth-revocation-http.json` and `oauth-no-revocation-http.json` are the same OAuth-protected server (combined AS + resource, DCR, refresh tokens) differing in one thing: the first advertises a `revocation_endpoint`, the second advertises none. Plain streamable-HTTP — connect with the **default (legacy)** protocol era.

Add either server, connect and complete authorization, then use **Clear OAuth state and disconnect** (Server Settings → Authorization) and watch the Network tab.

- On `oauth-revocation-http.json` a `POST /oauth/revoke` goes out naming the **refresh token**. RFC 7009 §2.1 asks the authorization server to invalidate the access tokens issued under the same grant, so one request ends both halves — the fixture implements that linkage, so re-sending the old bearer token to `/mcp` afterwards gets a 401. The request is built from the stored state *before* the local clear and sent *after* it, so the clear never waits on the network; in the Network tab the POST therefore follows the local teardown rather than preceding it.
- On `oauth-no-revocation-http.json` nothing is sent at all, and the clear behaves exactly as it did before the feature existed. That no-op path is what makes this safe against every authorization server with no RFC 7009 support ([#2144](https://github.com/modelcontextprotocol/inspector/issues/2144)).

On the broken build both servers behaved like the second: the Inspector deleted its local copy and the grant stayed valid at the authorization server until it expired on its own — which for a refresh token is a long time, by design.

Uncheck **Revoke tokens on clear** in the same panel (persisted as `oauth.revokeOnClear: false`) and the first server behaves like the second. That is not only an escape hatch: a client that disconnects still holding live tokens is a case worth reproducing when the server is the thing under test.

The same behavior is reachable from the other clients — the TUI's **Clear OAuth State**, and the CLI's `--relogin` (with `--no-revoke` as the per-run opt-out).

#### Logging, both eras

`logging-legacy-http.json` and `logging-modern-http.json` both serve `logging: true` plus a `send_notification` tool that emits a `notifications/message` at a chosen level. The legacy one is a plain streamable-HTTP server; the modern one sets `transport.modern: true`.
Expand Down
18 changes: 18 additions & 0 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,26 @@ Options that specify the MCP server (catalog/config file, ad-hoc command/URL, en
| `--strict` | With `--method tools/list`: report tool-schema portability problems in full (path, issue, suggested fix) on stderr, and exit `6` if any is error-severity. Without it, a one-line count is printed instead. See [Schema portability](#schema-portability---strict). |
| `--format <text\|json>` | Output format. `text` (default) pretty-prints the result. `json` emits a single JSON object on stdout (`{ "result": … }`, plus `{ "appInfo": … }` as a sibling key for App tools) with no banners, so the whole output pipes cleanly into `jq`. |
| `--relogin` | Delete stored OAuth for this server URL from the shared store before connect; interactive login still only runs if the server requires auth. Requires an HTTP/SSE URL (rejected for stdio). Conflicts with `--stored-auth-only` / `--use-stored-auth` / `--wait-for-auth` / catalog short-circuits. |
| `--no-revoke` | With `--relogin`, skip the [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) revocation request that would otherwise end the grant at the authorization server when the local state is deleted. The per-server `oauth.revokeOnClear` setting is the persistent form of the same opt-out; either one is enough to skip it. See [Revoking on `--relogin`](#revoking-on---relogin). |
| `--stored-auth-only` | **CI / non-interactive safe:** never start interactive OAuth / step-up (and never auto-open a browser); use the shared store if present, otherwise fail immediately with `auth_required`. Prefer this over a bare pipe/CI run that would otherwise attempt interactive login. |

#### Revoking on `--relogin`

`--relogin` deletes this server's stored OAuth state so the next connect cannot silently reuse it. By default it now also **revokes the grant at the authorization server**, per [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) ([#2144](https://github.com/modelcontextprotocol/inspector/issues/2144)) — otherwise the delete is invisible to the AS, and the access token, plus the refresh token when one was issued, stay valid there until they expire on their own.

The request is built from the stored state before the delete and sent after it, so the local delete never waits on the network. That matters because the OAuth store is shared: holding this process's view of it across a five-second request would let another CLI or TUI write a fresh grant that this one then erased.

The request names the refresh token when there is one: RFC 7009 §2.1 asks the authorization server to invalidate the access tokens issued under the same grant, so a single request covers both.

It is best-effort and never changes the exit code. An authorization server that advertises no `revocation_endpoint` gets no request at all; a network error, a non-2xx, or the short timeout prints a one-line warning on stderr and the local delete proceeds either way.

Turn it off per run with `--no-revoke`, or per server with `oauth.revokeOnClear: false` in the catalog — either is enough, and neither can turn it on for the other. Disconnecting while still holding live tokens is a case worth reproducing when the server under test is the thing being debugged.

```bash
mcp-inspector --cli --server-url https://example.com/mcp --relogin --method tools/list
mcp-inspector --cli --server-url https://example.com/mcp --relogin --no-revoke --method tools/list
```

`servers/show` redacts secret-bearing fields (`env` values, sensitive headers, sensitive `settings.metadata` keys whose whole value is replaced whether or not it is structured, `requestInit` / `eventSourceInit` headers, `oauthClientSecret`). It does **not** scrub credentials embedded in a server `url` (userinfo or query tokens) or in stdio `args` — treat `detail` / raw URL fields as potentially sensitive before pasting into issues.

#### App probing (`--app-info`) and machine-readable output (`--format json`)
Expand Down
Loading