This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new 5615f15460 docs: MCP OAuth 2.1 client + server resource-server + 
DCR/scope step-up (TODO-312f); v1 resource-not-found error code (TODO-332)
5615f15460 is described below

commit 5615f15460afce1eedb01c052c54077564f0289a
Author: James Bognar <[email protected]>
AuthorDate: Wed Aug 5 11:18:57 2026 -0700

    docs: MCP OAuth 2.1 client + server resource-server + DCR/scope step-up 
(TODO-312f); v1 resource-not-found error code (TODO-332)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/10.0.0.md             |  30 +++-
 pages/topics/11.03.JuneauMcpRecipes.md    | 288 ++++++++++++++++++++++++++++++
 pages/topics/11.04.JuneauRestServerMcp.md |  78 +++++++-
 pages/topics/11.05.JuneauRestClientMcp.md |  84 +++++++++
 4 files changed, 477 insertions(+), 3 deletions(-)

diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index a30a27078d..4afad79718 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -805,7 +805,7 @@ A new adapter module, `juneau-rest-server-mcp-v20250618` 
(package `org.apache.ju
 
 ### New Classes
 
-- **`org.apache.juneau.rest.server.mcp.v20250618.McpRevision`** — the 
`McpRevision` implementation for protocol revision `2025-06-18`. Replaces the 
old `McpDispatcher`/`Mcp` façade: owns the JSON-RPC method table (`initialize`, 
`ping`, `tools/list|call`, `prompts/list|get`, `resources/list|read`) and the 
error-code table. The error-code table intentionally preserves a known-wrong 
mapping carried over unmodified from the pre-re-layering dispatcher — 
unknown-method, tool-not-found, prompt- [...]
+- **`org.apache.juneau.rest.server.mcp.v20250618.McpRevision`** — the 
`McpRevision` implementation for protocol revision `2025-06-18`. Replaces the 
old `McpDispatcher`/`Mcp` façade: owns the JSON-RPC method table (`initialize`, 
`ping`, `tools/list|call`, `prompts/list|get`, `resources/list|read`) and the 
error-code table. The error-code table preserves a known-wrong mapping carried 
over unmodified from the pre-re-layering dispatcher for three of the four "not 
found" kinds — unknown-metho [...]
 - **`org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet`** — concrete 
abstract servlet for this revision; subclass it (instead of the core 
`McpRestServlet`) and implement `createMcpConfig()`. Exposes a `protected 
ServerCapabilities capabilities()` hook (returns `null` by default) for 
explicitly overriding the `initialize` capabilities advertisement instead of 
relying on auto-derivation from the registered tool/prompt/resource lists.
 - **`org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint`** — mixin 
interface for this revision, at parity with 
`org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet`: implement 
`getMcpConfig()`, and optionally override its `default ServerCapabilities 
capabilities()` hook.
 - **`McpTypedPromptHandler<A>`**, **`McpTypedHandlers`** (prompt-only) — the 
typed *prompt* sugar layer stays here, since it's genuinely revision-specific 
by construction: it binds arguments into this revision's wire-bean argument 
types and adapts the result to the neutral `McpPromptHandler` raw interface the 
core registry holds.
@@ -952,10 +952,36 @@ See 
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#configuring-the-en
 - **`AeadRequestStateCodec` now takes a `KeyProvider`.** The no-arg 
constructor is unchanged behaviorally (`this(new EphemeralKeyProvider())`); a 
new `AeadRequestStateCodec(KeyProvider)` constructor is the extension point. 
The sealed-token wire format grows from two to four dot-joined segments — 
`version . b64url(keyId) . b64url(nonce) . b64url(ciphertext+tag)` — with 
`keyId` authenticated (folded into the AEAD's AAD) but not encrypted (it must 
be readable before decryption selects the k [...]
 - **`McpMrtrConfig.setKeyProvider(KeyProvider)` (new convenience).** Sugar for 
`setCodec(new AeadRequestStateCodec(value))`, reachable through the 
`McpOptions` aggregate via `new McpOptions().mrtr(m -> m.setKeyProvider(kp))`. 
Last-wins against `setCodec(...)`; no getter for the provider (`getCodec()` 
remains the sole accessor).
 - **Error mapping unchanged.** The never-throw `unseal` contract and the 
existing `-32602`/`-32022`/`-32023` error codes are untouched — an 
unknown/retired `keyId` flows through the same `Optional.empty()` path as any 
other unseal failure, surfacing as the standard `-32602` "Invalid or tampered 
requestState".
-- **Scope fence, explicitly out of this change** — the OAuth/OIDC 
authorization SEPs, TODO-325's MRTR hardenings (principal-in-AAD, replay cache, 
argument-hash binding), and the pre-existing `-32602`/`-32002` missing-resource 
v1 error-code mismatch are all tracked separately and untouched here.
+- **Scope fence, explicitly out of this change** — the OAuth/OIDC 
authorization SEPs and TODO-325's MRTR hardenings (principal-in-AAD, replay 
cache, argument-hash binding) are tracked separately and untouched here; the 
pre-existing `-32602`/`-32002` missing-resource v1 error-code mismatch is now 
resolved — see the resource-not-found error-code correction (TODO-332) below.
 
 See 
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#key-management-keyprovider)
 and the [MRTR key management 
recipes](/docs/topics/JuneauMcpRecipes#mrtr-key-management-v2-only) for the 
full topics.
 
+### MCP `2026-07-28` — OAuth 2.1 authorization: client acquisition + server 
resource server (TODO-312f)
+
+`2026-07-28`-only, and **off by default** — an endpoint that doesn't opt in 
behaves exactly as before. 10.0.0 lands the OAuth 2.1 / MCP authorization 
baseline in three slices: client-side token acquisition (F1), a server-side 
resource-server (F2), and dynamic client registration + authorization hardening 
(F3). Together they let a Juneau MCP client authenticate to, and a Juneau MCP 
endpoint protect, a `2026-07-28` MCP resource end to end, covering the MCP 
authorization SEPs at a baseline  [...]
+
+- **New client module `juneau-rest-client-mcp-auth`** (package 
`org.apache.juneau.rest.client.mcp.auth`, on the Nimbus OAuth 2.0 SDK) — feeds 
the existing `McpAuthInterceptor` token seam (a `Supplier<String>`):
+  - **`McpTokenProvider`** — a thread-safe headless token manager in three 
modes: pre-provisioned static (`ofStaticToken`), client-credentials 
(`clientCredentials()`, RFC 6749 §4.4), and refresh-token (`refreshToken(...)`, 
RFC 6749 §6 / SEP-2207 with rotation capture via `currentRefreshToken()` and 
terminal-`invalid_grant` latching). The dynamic modes require `resource(URI)` 
(RFC 8707), carried on every token request; `interceptor()` wires it straight 
into a client builder.
+  - **`McpAuthorizationCodeAcquirer`** — interactive authorization-code + PKCE 
(`S256`) for CLI/native use: opens a `LoopbackRedirectReceiver`, launches the 
authorization URL (pluggable `browserLauncher`), validates the callback 
(single-use `state` CSRF, SEP-2468 `iss` — `expectedIssuer(URI)` required 
unless `skipIssuerValidation(true)`), and exchanges the code. 
`offlineAccess(true)` requests a refresh token.
+  - **`McpProtectedResourceMetadataClient`** — RFC 9728 PRM discovery with an 
`expectedResource(URI)` §3.3 identity check and an `https`-by-default scheme 
requirement (loopback exempt), plus `discoverAuthorizationServer(...)`; 
`WwwAuthenticateChallenge` parses the `401`/`403` `Bearer` challenge to recover 
the `resource_metadata` pointer.
+  - **`OidcDiscoveryClient`** — RFC 8414 Authorization Server Metadata with 
OIDC Discovery fallback (both required by the spec), each performing the 
issuer-identity check.
+- **New server RS baseline on `juneau-rest-server-mcp-v20260728`** — 
`McpResourceServerConfig`, folded into `McpOptions` as the 
`resourceServer(Consumer<McpResourceServerConfig>)` nested config. When enabled 
it requires a valid RFC 6750 bearer token on the MCP `POST` endpoint 
(validation mode chosen by the supplied `TokenValidator`: 
`OAuthIntrospectionValidator` for opaque tokens, `JwtTokenValidator` for JWTs), 
serves an RFC 9728 PRM document from `.well-known/oauth-protected-resource`,  
[...]
+- **Origin-root mount constraint.** Because RFC 9728 requires the PRM document 
at an origin-root well-known location — the exact URL advertised in the 
`resource_metadata` challenge — RS auth requires the MCP endpoint to be mounted 
at the origin root (no context path, no non-root servlet path, no 
`@Mixin(path=...)` re-mount prefix). A non-root or re-mounted mount **fails 
fast with a `500`** carrying an `origin-root` diagnostic (on both the `POST` 
gate and the well-known route) rather than [...]
+
+**F3 — dynamic client registration + authorization hardening.** The third 
slice completes the client story with RFC 7591 / OIDC Dynamic Client 
Registration, issuer-keyed credential persistence (SEP-2352), and scoped 
step-up authorization (SEP-2350, both halves), all in 
`juneau-rest-client-mcp-auth` except the server step-up gate:
+
+- **Dynamic Client Registration (SEP-837 / RFC 7591 / OIDC).** 
`McpDynamicClientRegistrar` POSTs client metadata to an AS 
`registration_endpoint` and returns an immutable, secret-redacting 
`McpClientRegistration`. `McpApplicationType` is a Juneau-owned `NATIVE`/`WEB` 
enum (native is the SEP-837 SHOULD for CLI/desktop/`localhost` clients) so the 
public API does not leak the Nimbus `provided` type. Loopback redirects are 
built with `LoopbackRedirectUris`: `portAgnostic(path)` is the RFC 82 [...]
+- **Issuer-keyed credential binding (SEP-2352).** Persisted credentials 
**MUST** be keyed by the AS `issuer` and never reused across authorization 
servers. `McpClientRegistrationStore` is the persistence SPI; 
`InMemoryMcpClientRegistrationStore` is the thread-safe, secret-redacting 
default. `McpClientRegistrationManager` orchestrates mechanism selection, 
issuer-keying, and migration: on-demand (no store), store-hit reuse, or 
re-registration when discovery indicates a different issuer — s [...]
+- **Scoped step-up authorization (SEP-2350, both halves).** Server side, 
`McpResourceServerConfig.addOperationScope(operation, scopes…)` declares 
per-operation required scopes; the RS answers an under-scoped call with `403 
insufficient_scope` + a `WWW-Authenticate` `scope=` hint, and the baseline 
`401` now also carries the `scope` hint when a baseline required-scope set is 
configured. Both the baseline and per-operation gates are **hierarchy-aware** 
(a broader granted scope satisfies a n [...]
+
+See the [OAuth 2.1 
recipe](/docs/topics/JuneauMcpRecipes#securing-an-mcp-client-and-server-with-oauth-21-v2-only)
 (end-to-end client + server), the [DCR + step-up 
recipes](/docs/topics/JuneauMcpRecipes#dynamic-client-registration-and-step-up-v2-only),
 the [juneau-rest-client-mcp auth 
reference](/docs/topics/JuneauRestClientMcp#oauth-21-authorization-juneau-rest-client-mcp-auth),
 and the [juneau-rest-server-mcp resource-server 
reference](/docs/topics/JuneauRestServerMcp#oauth-21-resource- [...]
+
+### MCP `2025-06-18` — resource-not-found error code corrected to `-32002` 
(TODO-332)
+
+`2025-06-18`-only bug fix. A `resources/read` for a URI the server does not 
serve now returns the JSON-RPC error code **`-32002`** ("Resource not found") — 
the `2025-06-18` spec's dedicated missing-resource code — instead of the 
previous generic **`-32601`** ("method not found"). This was one of the four 
"not found" kinds that the re-layered `2025-06-18` `McpRevision` initially 
collapsed onto `-32601` (see the `juneau-rest-server-mcp-v20250618` module 
notes above); resource-not-found is  [...]
+
+See 
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#resource-templates-reads-and-completions)
 for the resolution-order and error-code details.
+
 ### Bug Fixes
 
 - **Fixed RRPC method calls never dispatching over POST.** Every HTTP POST to 
an `@RestOp(method="RRPC")` operation previously returned a 404 instead of 
reaching the target method. `RrpcRestOpSession` derived the RRPC method key by 
splitting the request path on the last `/`, but RRPC keys are of the form 
`methodName/(paramTypes)` and themselves contain a `/`, so the method name was 
stripped off and the lookup always fell through to `NotFound`. The key is now 
derived from the already-comp [...]
diff --git a/pages/topics/11.03.JuneauMcpRecipes.md 
b/pages/topics/11.03.JuneauMcpRecipes.md
index 8ecaa6fee4..4942e7f7c6 100644
--- a/pages/topics/11.03.JuneauMcpRecipes.md
+++ b/pages/topics/11.03.JuneauMcpRecipes.md
@@ -14,6 +14,8 @@ Copy-pasteable snippets for common MCP tasks, targeting 
revision `2026-07-28` un
 - [MRTR key management (v2 only)](#mrtr-key-management-v2-only)
 - [Subscriptions (v2 only)](#subscriptions-v2-only)
 - [Client: call, read, and handle errors](#client-call-read-and-handle-errors)
+- [Securing an MCP client and server with OAuth 2.1 (v2 
only)](#securing-an-mcp-client-and-server-with-oauth-21-v2-only)
+- [Dynamic client registration and step-up (v2 
only)](#dynamic-client-registration-and-step-up-v2-only)
 
 ## Writing a tool
 
@@ -409,6 +411,292 @@ try (var client = 
McpClient.connect("http://localhost:8080/mcp";)) {
 
 `McpClient` is `Closeable`, so try-with-resources closes the underlying 
transport when the block exits, on either the success or error path. Every 
typed call (`callTool`, `getPrompt`, `readResource`, `listTools`, 
`listPrompts`, `listResources`, `listResourceTemplates`, `complete`) throws 
`McpException` for a JSON-RPC error response and `IOException` for a 
transport/(de)serialization failure — there's no single unified exception type, 
so a caller that needs both should catch both, as shown.
 
+## Securing an MCP client and server with OAuth 2.1 (v2 only)
+
+This recipe walks both sides of an OAuth 2.1 / MCP `2026-07-28` deployment as 
one story: turn a v2 MCP endpoint into a protected resource server, then 
acquire a token on the client and call it. Authorization is **v2-only** and 
**off by default** — an endpoint that doesn't opt in behaves exactly as before.
+
+**Dependencies.** On the server, add a token-validator module alongside 
`juneau-rest-server-mcp-v20260728`: `juneau-rest-server-auth-jwt` (JWT/JWKS 
validation) or `juneau-rest-server-auth-oauth` (RFC 7662 introspection). On the 
client, add `juneau-rest-client-mcp-auth` alongside 
`juneau-rest-client-mcp-v20260728`:
+
+```xml
+<!-- client -->
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-client-mcp-auth</artifactId>
+    <version>${juneau.version}</version>
+</dependency>
+```
+
+### Server: turn on the resource server
+
+RS auth is configured on the v2 `McpOptions` via its 
`resourceServer(Consumer<McpResourceServerConfig>)` block. The minimal working 
setup enables it, declares the canonical resource identifier, and supplies a 
`TokenValidator`:
+
+```java
+import java.net.URI;
+
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.auth.jwt.JwtTokenValidator;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.*;
+import org.apache.juneau.rest.server.servlet.*;
+
+@Rest   // root-mounted host: MCP is served at POST /mcp on the origin root 
(see the origin-root callout below)
+public class SecureApi extends BasicRestServlet implements McpEndpoint {
+
+    @Override
+    public McpServerConfig getMcpConfig() {
+        return new 
McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new 
EchoTool());
+    }
+
+    @Override
+    public McpOptions getMcpOptions() {
+        var validator = JwtTokenValidator.create()
+            
.jwksUrl(URI.create("https://login.example.com/realms/api/protocol/openid-connect/certs";))
+            .issuer("https://login.example.com/realms/api";)
+            .audience("https://api.example.com/mcp";)
+            .build();
+        return new McpOptions().resourceServer(rs -> rs
+            .setEnabled(true)
+            .setResource(URI.create("https://api.example.com/mcp";))            
  // RFC 9728 resource identity / RFC 8707 audience default
+            .setTokenValidator(validator)                                      
  // JWT mode; use OAuthIntrospectionValidator for opaque tokens
+            
.addAuthorizationServer(URI.create("https://login.example.com/realms/api";))
+            .addRequiredScope("mcp.read"));
+    }
+}
+```
+
+Once enabled, the endpoint:
+
+- **Serves an RFC 9728 Protected Resource Metadata (PRM) document** — 
unauthenticated — from the origin-root well-known location 
`/.well-known/oauth-protected-resource[/<resource-path>]`, advertising the 
`authorization_servers`, `scopes_supported`, and `bearer_methods_supported` you 
configured. (While RS auth is disabled, that route returns `404`.)
+- **Requires a valid RFC 6750 bearer token** on the MCP `POST` endpoint. A 
missing token is answered with `401` + `WWW-Authenticate: Bearer realm="mcp", 
resource_metadata="…"`; an invalid/expired token or audience mismatch adds 
`error="invalid_token"`; a token missing a required scope is `403` + 
`error="insufficient_scope", scope="…"`. Every challenge carries the 
`resource_metadata` pointer so a client can discover where to get a token.
+- **Enforces RFC 8707 audience matching** against the resource identifier (or 
an explicit `setAudience(...)`). By default (`requireAudienceClaim = true`) a 
validated principal that exposes no audience claims is rejected — set 
`setRequireAudienceClaim(false)` only when the validator itself verifies `aud` 
internally (e.g. a `JwtTokenValidator` with `audience(...)`).
+
+:::caution The MCP endpoint must be mounted at the origin root
+RFC 9728 requires the PRM document to be discoverable at the **origin-root** 
well-known location 
(`https://host/.well-known/oauth-protected-resource[/path]`) — which is exactly 
the URL advertised in the `resource_metadata` challenge. Juneau registers the 
well-known routes servlet-relative, so a **context path, a non-root servlet 
path, or an `@Mixin(path=...)` re-mount prefix** would move the advertised URL 
somewhere it can't be reached. Rather than silently advertising an unreachable 
doc [...]
+:::
+
+### Client: acquire a token (easy path first)
+
+All token acquisition funnels through the `McpAuthInterceptor` token seam — a 
`Supplier<String>` that yields the current bearer token per request. 
`McpTokenProvider` implements that supplier and exposes `interceptor()` to 
attach it to a client builder.
+
+**1. Pre-provisioned / static token** — the simplest path when you already 
hold a token:
+
+```java
+import org.apache.juneau.rest.client.mcp.auth.*;
+import org.apache.juneau.rest.client.mcp.v20260728.McpClient;
+
+var tokens = McpTokenProvider.ofStaticToken("eyJhbGciOi…");
+var client = McpClient.connect(McpClient.builder()
+    .endpoint("https://api.example.com/mcp";)
+    .interceptor(tokens.interceptor()));
+```
+
+**2. Client-credentials** (RFC 6749 §4.4) — headless service-to-service; 
re-acquired automatically on expiry:
+
+```java
+import java.net.URI;
+
+import org.apache.juneau.rest.client.mcp.auth.*;
+
+var tokens = McpTokenProvider.clientCredentials()
+    
.tokenEndpoint(URI.create("https://login.example.com/realms/api/protocol/openid-connect/token";))
+    .clientId("my-client")
+    .clientSecret("s3cr3t")
+    .resource(URI.create("https://api.example.com/mcp";))   // RFC 8707 — 
REQUIRED for the dynamic modes
+    .scope("mcp.read")
+    .build();
+```
+
+**3. Refresh-token** (RFC 6749 §6, SEP-2207) — long-lived, with automatic 
rotation capture:
+
+```java
+var tokens = McpTokenProvider.refreshToken(savedRefreshToken)
+    
.tokenEndpoint(URI.create("https://login.example.com/realms/api/protocol/openid-connect/token";))
+    .clientId("my-client")
+    .resource(URI.create("https://api.example.com/mcp";))
+    .build();
+
+// After each request, persist a rotated refresh token so a restart can resume:
+tokens.currentRefreshToken().ifPresent(this::saveRefreshToken);
+```
+
+A rotated refresh token the IdP returns is captured and used for the next 
refresh with no re-consent. A terminal `invalid_grant` **latches** the 
provider: every later `get()` throws an `McpAuthException` (re-authorization 
required) rather than re-hammering the IdP with a dead token.
+
+**4. Interactive authorization-code + PKCE** (SEP-837 loopback, SEP-2468 `iss` 
check) — for a CLI/native app where a human authorizes in a browser:
+
+```java
+import java.net.URI;
+import java.time.Duration;
+
+import org.apache.juneau.rest.client.mcp.auth.*;
+
+var acquirer = McpAuthorizationCodeAcquirer.create()
+    
.authorizationEndpoint(URI.create("https://login.example.com/realms/api/protocol/openid-connect/auth";))
+    
.tokenEndpoint(URI.create("https://login.example.com/realms/api/protocol/openid-connect/token";))
+    .clientId("my-native-client")
+    .resource(URI.create("https://api.example.com/mcp";))                 // 
RFC 8707 — REQUIRED
+    .expectedIssuer(URI.create("https://login.example.com/realms/api";))  // 
SEP-2468 — REQUIRED (or skipIssuerValidation(true))
+    .offlineAccess(true)                                                 // 
add offline_access → the IdP issues a refresh token
+    .scope("mcp.read")
+    .build();   // default browserLauncher prints the URL; supply 
browserLauncher(...) to open a real browser
+
+var token = acquirer.acquire(Duration.ofMinutes(2));   // opens a loopback 
listener, waits for the redirect, exchanges the code
+```
+
+The acquirer opens a `LoopbackRedirectReceiver` on an ephemeral `127.0.0.1` 
port, hands the authorization URL to the browser launcher, validates the 
callback (single-use `state` for CSRF, `iss` against `expectedIssuer`), and 
exchanges the code with PKCE `S256`. Because `offlineAccess(true)` requested a 
refresh token, hand it to a refresh-mode provider so the client stays 
authenticated without re-prompting:
+
+```java
+var tokens = McpTokenProvider.refreshToken(token.refreshToken().orElseThrow())
+    
.tokenEndpoint(URI.create("https://login.example.com/realms/api/protocol/openid-connect/token";))
+    .clientId("my-native-client")
+    .resource(URI.create("https://api.example.com/mcp";))
+    .build();
+```
+
+### Let PRM discovery drive authorization-server selection
+
+Instead of hardcoding the token/authorization endpoints, let the server's PRM 
document point the way (RFC 9728 → RFC 8414 / OIDC Discovery):
+
+```java
+import java.net.URI;
+
+import org.apache.juneau.rest.client.mcp.auth.*;
+
+var prmClient = McpProtectedResourceMetadataClient.create()
+    .expectedResource(URI.create("https://api.example.com/mcp";))   // RFC 9728 
§3.3 identity check — rejects a PRM minted for a different resource
+    .build();
+
+// Fetch the PRM from the origin-root well-known location...
+var prm = 
prmClient.fetch(URI.create("https://api.example.com/.well-known/oauth-protected-resource/mcp";));
+
+// ...or recover the pointer from a 401 challenge header:
+//   
WwwAuthenticateChallenge.parse(wwwAuthenticateHeader).flatMap(WwwAuthenticateChallenge::resourceMetadata)
+
+// Discover + validate the authorization server the PRM delegates to (tries 
RFC 8414, falls back to OIDC Discovery):
+var as = prmClient.discoverAuthorizationServer(prm);
+
+var tokens = McpTokenProvider.clientCredentials()
+    .tokenEndpoint(as.tokenEndpoint())
+    .clientId("my-client")
+    .clientSecret("s3cr3t")
+    .resource(prm.resource())
+    .scope("mcp.read")
+    .build();
+```
+
+`discoverAuthorizationServer(...)` selects the first advertised 
`authorization_servers` entry, discovers its metadata, and verifies the 
discovered `issuer` exactly matches the requested one. By default the PRM URL 
and any discovered AS URL must use `https` (loopback hosts are always allowed 
for local testing); `allowInsecureHttp(true)` disables that check.
+
+### Putting it together
+
+With a provider wired into the interceptor, an authenticated call is 
indistinguishable from an unauthenticated one — the bearer header is attached 
transparently on every request:
+
+```java
+import java.util.Map;
+
+import org.apache.juneau.rest.client.mcp.v20260728.McpClient;
+
+try (var client = McpClient.connect(McpClient.builder()
+        .endpoint("https://api.example.com/mcp";)
+        .interceptor(tokens.interceptor()))) {
+    var result = client.callTool("echo", Map.of("text", "hello"));
+    System.out.println(result.getContent());
+}
+```
+
+If the token is missing, expired, or under-scoped, the server answers with the 
`401`/`403` challenge above and the call surfaces the failure — recover by 
re-acquiring (or re-authorizing) and retrying. See the [juneau-rest-client-mcp 
auth 
reference](/docs/topics/JuneauRestClientMcp#oauth-21-authorization-juneau-rest-client-mcp-auth)
 and the [juneau-rest-server-mcp resource-server 
reference](/docs/topics/JuneauRestServerMcp#oauth-21-resource-server-mcp-2026-07-28)
 for the full API surface.
+
+## Dynamic client registration and step-up (v2 only)
+
+Two authorization-hardening recipes for the `2026-07-28` client: registering a 
client on the fly (RFC 7591 / OIDC DCR, SEP-837 / SEP-2352) and re-authorizing 
with broader scopes when the server asks (SEP-2350). Both build on the [OAuth 
2.1 recipe](#securing-an-mcp-client-and-server-with-oauth-21-v2-only) above.
+
+### Register a client on the fly (DCR)
+
+When you have no pre-provisioned `client_id`, register one against the AS 
`registration_endpoint`, then bridge the result straight into the interactive 
acquirer. `portAgnostic(...)` is the easy default — the receiver may bind any 
port:
+
+```java
+import java.net.URI;
+import java.time.Duration;
+
+import org.apache.juneau.rest.client.mcp.auth.*;
+
+var registration = McpDynamicClientRegistrar.create()
+    .registrationEndpoint(URI.create("https://as.example.com/register";))
+    .issuer(URI.create("https://as.example.com";))
+    .applicationType(McpApplicationType.NATIVE)              // SEP-837 SHOULD 
for CLI/desktop/loopback clients
+    .addRedirectUris(LoopbackRedirectUris.portAgnostic("/callback"))
+    .scope("mcp.read", "mcp.write")
+    .clientName("my-cli")
+    .register();                                            // POSTs metadata; 
returns a secret-redacting McpClientRegistration
+
+// Bridge the DCR result into the interactive auth-code + PKCE flow 
(credential provenance only — resource/iss unchanged):
+var acquirer = McpClientRegistrations.configure(
+        McpAuthorizationCodeAcquirer.create()
+            
.authorizationEndpoint(URI.create("https://as.example.com/authorize";))
+            .tokenEndpoint(URI.create("https://as.example.com/token";))
+            .resource(URI.create("https://api.example.com/mcp";))
+            .expectedIssuer(URI.create("https://as.example.com";)),
+        registration)
+    .build();
+
+var token = acquirer.acquire(Duration.ofMinutes(2));       // opens the 
loopback receiver, launches the browser, exchanges the code
+```
+
+**Strict exact-match AS?** Register a fixed port with `forPort(...)` instead; 
`configure(...)` carries that port onto the acquirer so the receiver binds 
exactly what the AS was told:
+
+```java
+int port = 49215;   // any free loopback port you reserve
+var registration = McpDynamicClientRegistrar.create()
+    .registrationEndpoint(URI.create("https://as.example.com/register";))
+    .issuer(URI.create("https://as.example.com";))
+    .applicationType(McpApplicationType.NATIVE)
+    .addRedirectUris(LoopbackRedirectUris.forPort(port, "/callback"))
+    .register();
+// McpClientRegistrations.configure(acquirerBuilder, registration) now also 
wires redirectPort(port).
+```
+
+**Persist across runs (SEP-2352).** Let `McpClientRegistrationManager` key 
credentials by issuer and re-register on AS migration, backed by a store:
+
+```java
+var manager = McpClientRegistrationManager.create()
+    .store(new InMemoryMcpClientRegistrationStore())        // or a durable, 
at-rest-protected implementation
+    .redirectUris(LoopbackRedirectUris.portAgnostic("/callback"))
+    .scope("mcp.read")
+    .clientName("my-cli")
+    .build();
+
+// 'as' is the discovered OidcMetadata (its extras carry 
registration_endpoint) from PRM discovery above.
+var registration = manager.resolve(as);                    // store-hit reuse, 
or a fresh DCR keyed under as.issuer()
+```
+
+After an AS migration you must build a **new** `McpTokenProvider` for the new 
issuer — the manager migrates the client-registration half, but never carries a 
token across authorization servers.
+
+### Re-authorize with broader scopes on step-up
+
+When the server answers a privileged operation with `403 insufficient_scope`, 
wrap the call in an `McpStepUpAuthorizer`: it unions the challenged scopes onto 
what you already requested, re-authorizes, and retries under a bounded attempt 
cap.
+
+```java
+import java.net.URI;
+
+import org.apache.juneau.rest.client.mcp.auth.*;
+
+var authorizer = McpStepUpAuthorizer.create()
+    .resource(URI.create("https://api.example.com/mcp";))
+    .issuer(URI.create("https://as.example.com";))
+    .previouslyRequestedScopes("mcp.read")
+    .reauthorizer(scopes -> {
+        // Re-acquire a token for the widened scope union and install it on 
the client.
+        // For a user-delegation client this is a fresh auth-code + PKCE 
round-trip; for M2M, a client_credentials re-acquire.
+    })
+    .build();
+
+var result = authorizer.execute("tools/call:deploy", () -> {
+    // Run the MCP call. On a 403 insufficient_scope, throw 
McpInsufficientScopeException(challenge)
+    // (parse the WWW-Authenticate header via 
WwwAuthenticateChallenge.parse(...)) so the authorizer can step up and retry.
+    return client.callTool("deploy", args);
+});
+```
+
+A successful `execute(...)` clears that operation's attempt counter, so a 
later legitimate step-up starts fresh; call `reset("tools/call:deploy")` (or 
`resetAll()`) to recover an operation that hit the cap after you fix the 
underlying grant. A `client_credentials` client can be configured with 
`attemptStepUp(false)` to abort immediately instead of retrying.
+
 ## Next steps
 
 - **[juneau-rest-server-mcp reference](/docs/topics/JuneauRestServerMcp)** — 
capabilities, pagination, cache hints, tracing, MRTR internals.
diff --git a/pages/topics/11.04.JuneauRestServerMcp.md 
b/pages/topics/11.04.JuneauRestServerMcp.md
index 687d00faa6..d435934971 100644
--- a/pages/topics/11.04.JuneauRestServerMcp.md
+++ b/pages/topics/11.04.JuneauRestServerMcp.md
@@ -458,7 +458,7 @@ For each `resources/read`:
 1. **Exact resources always win** — `McpResourceHandler` string-equality 
lookup runs first, unconditionally, even if a more-literal template would also 
match.
 2. If no exact resource matches, every reverse-matchable template is 
evaluated, and the winner is selected by, in order: more literal URI octets 
outside `{...}` (after percent normalization) → fewer declared variables → 
earlier registration index. Operator type, capture width, and descriptor 
name/title are never tie-breakers.
 3. The winning handler receives the **original concrete URI**, an **immutable, 
insertion-ordered map of decoded variables**, and the same per-request 
`BeanStore` an exact resource handler would get.
-4. No candidate, or the winning handler's `read(...)` returning `null`, is 
resource-not-found — `-32601` on `2025-06-18` (preserving that revision's 
existing known-wrong error-code mapping) and `RESOURCE_NOT_FOUND` (`-32602`) on 
`2026-07-28`. A handler throwing is `-32603` on both.
+4. No candidate, or the winning handler's `read(...)` returning `null`, is 
resource-not-found — `-32002` (the `2025-06-18` spec's dedicated 
missing-resource code) on `2025-06-18` and `RESOURCE_NOT_FOUND` (`-32602`) on 
`2026-07-28`. A handler throwing is `-32603` on both.
 
 On `2026-07-28`, a template-backed read applies the adapter's existing 
cache-hint precedence (`resourceReadOverrides` → `resourcesRead` → 
`defaultHint`) using the **original concrete request URI** — the same 
precedence and lookup key an exact-resource read already uses; C4 adds no new 
cache concept.
 
@@ -541,6 +541,82 @@ This internal `@Bean` publication is **authoritative** and 
takes precedence over
 
 `getSubscriptionBroker()` defaults to `null`, meaning "framework-derived": the 
binding lazily derives a `BasicMcpSubscriptionBroker` sized from 
`subscriptions`' `queueSize` the first time one is needed, and memoizes that 
single derived instance for the binding's lifetime. Call 
`setSubscriptionBroker(...)` to supply a custom broker instead (for example, 
one deliberately shared across multiple bindings).
 
+## OAuth 2.1 resource server (MCP `2026-07-28`)
+
+**v2-only.** `McpResourceServerConfig` 
(`org.apache.juneau.rest.server.mcp.v20260728`) turns a `2026-07-28` MCP 
endpoint into an OAuth 2.1 protected resource server. It's one of the nested 
configs on [`McpOptions`](#configuring-the-endpoint-mcpoptions-mcp-2026-07-28) 
— mutate it in place via the 
`resourceServer(Consumer<McpResourceServerConfig>)` configure-block. For a 
copy-pasteable end-to-end (server + client) walkthrough, see the [OAuth 2.1 
recipe](/docs/topics/JuneauMcpRecipes#securi [...]
+
+**Off by default.** `isEnabled()` is `false` unless `setEnabled(true)`, so an 
endpoint that doesn't opt in behaves exactly as before — no bearer requirement, 
and the well-known route returns `404`. When enabled, `validateEnabled()` 
requires a `resource` identifier (an absolute URI with scheme + authority) and 
a `TokenValidator`, or the binding fails.
+
+### Configuration surface
+
+```java
+@Override
+public McpOptions getMcpOptions() {
+    var validator = JwtTokenValidator.create()
+        
.jwksUrl(URI.create("https://login.example.com/realms/api/protocol/openid-connect/certs";))
+        .issuer("https://login.example.com/realms/api";)
+        .audience("https://api.example.com/mcp";)
+        .build();
+    return new McpOptions().resourceServer(rs -> rs
+        .setEnabled(true)
+        .setResource(URI.create("https://api.example.com/mcp";))
+        .setTokenValidator(validator)
+        
.addAuthorizationServer(URI.create("https://login.example.com/realms/api";))
+        .addScopeSupported("mcp.read")
+        .addRequiredScope("mcp.read")
+        .setRealm("mcp"));
+}
+```
+
+| Setting | Meaning |
+|---|---|
+| `setEnabled(boolean)` | Master switch. Default `false`. |
+| `setResource(URI)` | The canonical RFC 9728 `resource` identifier and 
(unless overridden) the RFC 8707 audience. Required when enabled. |
+| `setTokenValidator(TokenValidator)` | Selects the validation mode. Required 
when enabled. `OAuthIntrospectionValidator` (`juneau-rest-server-auth-oauth`, 
RFC 7662 introspection of opaque tokens) or `JwtTokenValidator` 
(`juneau-rest-server-auth-jwt`, JWKS/issuer JWT verification). |
+| `addAuthorizationServer(URI)` | Advertised `authorization_servers` in the 
PRM document. |
+| `addScopeSupported(String)` / `addRequiredScope(String)` | 
`scopes_supported` advertisement, and the baseline scopes a token must carry (a 
required scope is also advertised as supported). |
+| `addOperationScope(String method, String name, String... scopes)` | SEP-2350 
per-operation step-up scopes (see 
[below](#per-operation-step-up-scopes-sep-2350)). A blank/`null` `name` 
registers a method-wide entry; a specific name wins over it. Each scope is also 
advertised as supported. |
+| `setOperationScopeResolver(McpOperationScopeResolver)` | Overrides the 
static per-operation map with a dynamic resolver (seam for argument-derived 
scopes; not required for 10.0). |
+| `setAudience(String)` | Explicit RFC 8707 audience, overriding the 
resource-identifier default. |
+| `setRequireAudienceClaim(boolean)` | Default `true`: a validated principal 
exposing no audience claims is rejected (`401 invalid_token`). Set `false` only 
when the validator enforces `aud` internally (e.g. a `JwtTokenValidator` with 
`audience(...)`, or an introspection validator that legitimately omits `aud`). |
+| `setRealm(String)` | The `WWW-Authenticate: Bearer realm="…"` challenge 
realm. Default `mcp`. |
+| `getBearerMethodsSupported()` | Advertised `bearer_methods_supported`. 
Defaults to `[header]`. |
+
+### Runtime behavior
+
+- **PRM discovery.** Both bindings register unauthenticated `@RestGet` routes 
at `/.well-known/oauth-protected-resource` (SEP-2351 root fallback) and 
`/.well-known/oauth-protected-resource/*` (RFC 9728 path-inserted, matched 
against this resource's expected path). They serve the RFC 9728 document built 
from the config, or `404` when RS auth is disabled.
+- **Bearer gate + challenges.** Bearer extraction and token validation are 
delegated to the reusable `OAuthFilter` (RFC 6750) from 
`juneau-rest-server-auth-oauth`, memoized once per config. A missing token → 
`401` + `WWW-Authenticate: Bearer realm="…", resource_metadata="…"`; an 
invalid/expired token or audience mismatch → `401` + `error="invalid_token"`; a 
token missing a required scope → `403` + `error="insufficient_scope", 
scope="…"`. On success the authenticated principal is stashed  [...]
+- **Audience enforcement (RFC 8707).** The validated principal's audience 
claims must include the configured audience; see `setRequireAudienceClaim(...)` 
above for the claims-less-principal policy.
+
+:::caution Origin-root mount requirement
+RS auth requires the MCP endpoint to be mounted at the **origin root** — no 
context path, no non-root servlet path, and no `@Mixin(path=...)` re-mount 
prefix. RFC 9728 requires the PRM document at an origin-root well-known 
location, which is exactly the URL advertised in the `resource_metadata` 
challenge; Juneau registers the well-known routes servlet-relative, so any 
non-root mount would advertise a URL that resolves elsewhere and `404`s. Rather 
than advertise an unreachable document, R [...]
+:::
+
+**Effectively immutable once published** — same mutable-setup idiom as 
`McpMrtrConfig`/`McpCacheConfig`: configure it fully via the 
`createMcpOptions()` / `getMcpOptions()` override, then never mutate it 
afterward.
+
+### Per-operation step-up scopes (SEP-2350)
+
+Beyond the endpoint-wide baseline (`addRequiredScope`), you can require 
*additional* scopes for *specific* operations — e.g. a `delete_repo` tool that 
needs `repo.delete` on top of the baseline `mcp.read`:
+
+```java
+return new McpOptions().resourceServer(rs -> rs
+    .setEnabled(true)
+    .setResource(URI.create("https://api.example.com/mcp";))
+    .setTokenValidator(validator)
+    .addRequiredScope("mcp.read")                                  // baseline 
for every call
+    .addOperationScope("tools/call", "delete_repo", "repo.delete") // extra 
for one tool
+);
+```
+
+Because the required MCP method (`tools/call` vs `resources/read`) — and thus 
the operation identity — isn't known until the JSON-RPC body is parsed, this 
enforcement runs at a **POST-parse point in the dispatch layer**, not at the 
pre-parse bearer gate. A caller whose token satisfies the baseline but not the 
operation's scopes gets a scoped step-up challenge:
+
+```http
+HTTP/1.1 403 Forbidden
+WWW-Authenticate: Bearer realm="mcp", error="insufficient_scope", 
scope="repo.delete", 
resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp";
+```
+
+All of the operation's required scopes are emitted in a single `scope=` param 
(the SEP-2350 "single challenge" SHOULD). Sufficiency is **hierarchy-aware** 
per the server MUST: a broader granted scope implies its narrower children 
(granted `repo` satisfies required `repo.delete` / `repo:read`, but granted 
`repo.read` does *not* satisfy required `repo`). Operations with no configured 
per-operation scope are unaffected — the baseline alone governs them. The 
client half of the flow (union th [...]
+
 ## Cache Hints (MCP `2026-07-28`, SEP-2549)
 
 The `2026-07-28` adapter (`org.apache.juneau.rest.server.mcp.v20260728`) adds 
two capabilities on top of the neutral core: SEP-2549 cache hints on five 
list/read results, and a `resources/templates/list` endpoint. Both are 
configured statically, at server-construction time — there is no 
dynamic/per-request cache callback, and neither concept exists on the neutral 
`juneau-rest-server-mcp` core or the `2025-06-18` adapter.
diff --git a/pages/topics/11.05.JuneauRestClientMcp.md 
b/pages/topics/11.05.JuneauRestClientMcp.md
index eb0ac42497..a6ef98a67c 100644
--- a/pages/topics/11.05.JuneauRestClientMcp.md
+++ b/pages/topics/11.05.JuneauRestClientMcp.md
@@ -63,6 +63,90 @@ See 
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#elicitation-mcp-20
 
 `McpAuthInterceptor` is the client-side auth seam. It injects `Authorization: 
Bearer ...` from a supplier callback at request-init time. OAuth/OIDC flows 
plug in through the supplier implementation.
 
+`McpAuthInterceptor` is a `RestCallInterceptor`: on `onInit(...)` it reads the 
current token from its `Supplier<String>` and sets `Authorization: Bearer 
<token>`. A `null` token suppresses the header (the call proceeds 
unauthenticated); a supplier that throws aborts the call before it is sent. 
`McpAuthInterceptor.ofStaticBearer(token)` is the fixed-token shortcut. Attach 
it via the client builder's `interceptor(...)`:
+
+```java
+var client = McpClient.connect(McpClient.builder()
+    .endpoint("https://api.example.com/mcp";)
+    .interceptor(McpAuthInterceptor.ofStaticBearer("eyJ…")));
+```
+
+## OAuth 2.1 authorization (`juneau-rest-client-mcp-auth`)
+
+**v2-only.** The `juneau-rest-client-mcp-auth` module supplies the OAuth 2.1 
token-acquisition machinery that feeds the [auth seam](#auth-seam) above. It is 
an add-on to the client stack (it depends on the [Nimbus OAuth 2.0 
SDK](https://connect2id.com/products/nimbus-oauth-openid-connect-sdk)); nothing 
here is on the classpath unless you add it. For a copy-pasteable end-to-end 
walkthrough see the [OAuth 2.1 
recipe](/docs/topics/JuneauMcpRecipes#securing-an-mcp-client-and-server-with-oaut
 [...]
+
+### `McpTokenProvider`
+
+A thread-safe headless `Supplier<String>` that acquires, caches, and refreshes 
a bearer token. Three modes:
+
+- **Static** — `McpTokenProvider.ofStaticToken(token)` always returns a fixed, 
non-blank token (issues no token request, takes no resource indicator).
+- **Client-credentials** — `McpTokenProvider.clientCredentials()` (RFC 6749 
§4.4); re-acquired on expiry.
+- **Refresh-token** — `McpTokenProvider.refreshToken(initialRefreshToken)` 
(RFC 6749 §6, SEP-2207); a rotated refresh token returned by the IdP is 
captured for the next refresh with no re-consent, and `currentRefreshToken()` 
exposes the most-recently-rotated value so callers can persist it across 
restarts.
+
+Both dynamic modes are builder-configured (`tokenEndpoint`, `clientId`, 
`clientSecret`/`clientSecretSupplier`, `scope`, `expirySkew`, `httpTimeout`, 
`httpRequestConfigurator`) and **require `resource(URI)`** — the RFC 8707 
resource indicator carried on every token request, binding the token's audience 
to the target MCP server (`build()` throws without it). `get()` never returns a 
blank token; a terminal `invalid_grant` in refresh mode latches the provider so 
every later `get()` throws a  [...]
+
+### `McpAuthorizationCodeAcquirer`
+
+Interactive authorization-code + PKCE (`S256`) acquisition for CLI/native use, 
built via `McpAuthorizationCodeAcquirer.create()`. `acquire(Duration)` opens a 
`LoopbackRedirectReceiver`, generates the `state` and PKCE verifier, hands the 
authorization URL to the `browserLauncher` (default: prints the URL), waits for 
the loopback redirect, validates it, and exchanges the code for an 
`OAuthToken`. Builder essentials:
+
+- `authorizationEndpoint`, `tokenEndpoint`, `clientId` — required; 
`clientSecret` optional (omit for a public PKCE-only client).
+- **`resource(URI)`** — required (RFC 8707 audience binding).
+- **`expectedIssuer(URI)`** — required by default (SEP-2468 `iss` / RFC 9207 
check, gating both success and error callbacks); opt out with 
`skipIssuerValidation(true)`, or tighten with 
`requireIssuerResponseParameter(true)` when the AS advertises 
`authorization_response_iss_parameter_supported`.
+- `offlineAccess(true)` — adds the `offline_access` scope so the IdP issues a 
refresh token (feed it to a refresh-mode `McpTokenProvider`).
+- `browserLauncher(Consumer<URI>)`, `redirectPath`, `scope`, `store`, 
`httpTimeout`, `httpRequestConfigurator`.
+
+Static helpers `validateAuthorizationResponse(...)` perform the pure SEP-2468 
/ CSRF validation step for callers driving their own user-agent interaction.
+
+### `LoopbackRedirectReceiver`
+
+A minimal `AutoCloseable` loopback (`http://127.0.0.1:<ephemeral>`) HTTP 
listener that captures the single authorization-code redirect (SEP-837). 
`redirectUri()` is the URI to register with the IdP; `awaitCallback(Duration)` 
blocks for the redirect and returns the full callback URI (or throws 
`McpAuthException` on timeout). `McpAuthorizationCodeAcquirer` uses it 
internally; it's public for callers driving the flow manually.
+
+### `McpProtectedResourceMetadataClient` + `WwwAuthenticateChallenge`
+
+`McpProtectedResourceMetadataClient` fetches and parses an RFC 9728 PRM 
document and resolves the authorization server a protected MCP resource 
delegates to. `fetch(prmUrl)` / `parse(json, source)` return an 
`McpProtectedResourceMetadata` (`resource()`, `authorizationServers()`, 
`firstAuthorizationServer()`, `scopesSupported()`, `extras()`). When 
`expectedResource(URI)` is set (or passed per-call), the document's `resource` 
field is validated against it (RFC 9728 §3.3 exact-match) — defe [...]
+
+`WwwAuthenticateChallenge.parse(headerValue)` parses a `WWW-Authenticate: 
Bearer …` challenge (RFC 6750 §3 / RFC 9728 §5.1); `resourceMetadata()` 
recovers the PRM pointer, `scopes()` the challenged step-up scopes, `error()` 
the error code.
+
+### `OidcDiscoveryClient`
+
+Fetches an authorization server's metadata as an `OidcMetadata` record 
(`issuer`, `tokenEndpoint`, `authorizationEndpoint`, `introspectionEndpoint`, 
`jwksUri`, `supportedScopes`, `extras`, plus 
`authorizationResponseIssParameterSupported()`). `discover()` tries the RFC 
8414 OAuth Authorization Server Metadata endpoint 
(`/.well-known/oauth-authorization-server`) first and falls back to OIDC 
Discovery (`/.well-known/openid-configuration`) — the MCP `2026-07-28` spec 
requires both — and bot [...]
+
+:::note Roadmap
+The full SEP-2351 path-insertion well-known-URI construction (path-bearing 
issuers, ordered fallbacks) is a deferred enhancement; `OidcDiscoveryClient` 
currently implements the two-endpoint (RFC 8414 → OIDC) fallback the baseline 
requires. An OIDC ID token returned alongside an access token is surfaced 
verbatim on `OAuthToken.idToken()` but is **not validated** in this baseline — 
do not trust its claims for auth decisions.
+:::
+
+### Dynamic Client Registration (SEP-837)
+
+When you have no pre-registered `client_id`, `McpDynamicClientRegistrar` 
performs RFC 7591 / OIDC Dynamic Client Registration against an authorization 
server's `registration_endpoint`, built via 
`McpDynamicClientRegistrar.create()`. `register()` POSTs the client metadata 
and returns an immutable `McpClientRegistration` (`clientId`, `clientSecret`, 
`clientSecretExpiresAt`, `registrationAccessToken`, `registrationClientUri`, 
`issuer`, `redirectUris`, `applicationType`, `extras`) whose `toS [...]
+
+- `registrationEndpoint`, `issuer` — required; the endpoint must be `https` 
(loopback exempt).
+- `applicationType(McpApplicationType)` — `NATIVE` (the default for 
CLI/desktop/loopback clients) or `WEB`; a Juneau-owned enum so the public API 
does not leak the Nimbus `provided` type. Per SEP-837 native apps (including 
CLI tools and `localhost`-accessed web apps) **SHOULD** register as `native`.
+- `addRedirectUris(...)` / `addRedirectUri(...)` — build loopback redirects 
with `LoopbackRedirectUris`: `portAgnostic(path)` → 
`[http://127.0.0.1/callback, http://localhost/callback]` (RFC 8252 §7.3 
port-agnostic matching) is the **easy, spec-blessed default** — the receiver 
may bind any ephemeral port and still match. `forPort(port, path)` is the 
**bind-first** strategy for strict exact-match ASes: register a specific fixed 
port, then have the acquirer bind that same port via `redirect [...]
+- `scope(...)`, `addGrantType(...)`, `addResponseType(...)`, `clientName`, 
`confidential(...)`, `initialAccessToken(...)` (bearer for a protected 
registration endpoint, redacted in `toString()`).
+
+`McpClientRegistrations` bridges a registration into the F1 flows: 
`McpClientRegistrations.configure(acquirerBuilder, registration)` and 
`configure(tokenProviderBuilder, registration)` apply the `clientId`, 
`clientSecret`, and registered loopback redirect so a DCR-obtained client 
drives `McpAuthorizationCodeAcquirer` / `McpTokenProvider` unchanged — still 
carrying the RFC 8707 `resource=` indicator and RFC 9207 `iss` validation (DCR 
changes only credential provenance). When the registrat [...]
+
+### Issuer-bound credentials + re-registration (SEP-2352)
+
+Per SEP-2352, credentials that are persisted **MUST** be keyed by the 
authorization server's `issuer`, kept separate per AS, and never reused across 
authorization servers. `McpClientRegistrationStore` is the persistence SPI 
(`find(issuer)` / `put(issuer, registration)` / `remove(issuer)`); 
`InMemoryMcpClientRegistrationStore` is the thread-safe default (redacts 
secrets in `toString()`). A caller may supply a durable implementation — it is 
responsible for at-rest protection of secrets and [...]
+
+`McpClientRegistrationManager` orchestrates mechanism selection, 
issuer-keying, and migration. Given a discovered `OidcMetadata` (whose `extras` 
carry `registration_endpoint`) and an optional store, `resolve(...)` returns a 
usable `McpClientRegistration`:
+
+- **On-demand mode** (no store) registers on every call — fully compliant 
(persistence is SHOULD-level).
+- **Store hit** — reuses the issuer-keyed entry without a second DCR 
round-trip.
+- **Migration** — when discovery now indicates a *different* issuer than a 
stored/pre-registered credential, the manager **MUST NOT** reuse the cross-AS 
credential: it re-registers via DCR (and stores under the new issuer), or, for 
pre-registered credentials, surfaces an `McpAuthException` rather than silently 
using a mismatched credential. A defensive issuer-mismatch check also guards 
against a buggy durable store returning an entry keyed under the wrong issuer.
+- An AS advertising neither a `registration_endpoint` nor pre-registered 
credentials yields an `McpAuthException`.
+
+The manager migrates only the **registration** (client credentials) half. The 
**token** half of SEP-2352 is caller discipline: after an AS migration you must 
build a **new** `McpTokenProvider` (and re-run any interactive acquisition) for 
the new issuer — a token minted for the old AS is never carried across.
+
+### Scope accumulation on step-up (SEP-2350, client half)
+
+When the resource server answers an operation with `403 insufficient_scope`, 
the client re-authorizes with a **broader** grant and retries. 
`McpScopeAccumulator.union(previouslyRequested, challengeScopes[, …opt-in 
contributors])` computes the mandated-minimum union of `{previously-requested} 
∪ {challenge}` (order-preserving, exact-string dedup — scopes are opaque, so no 
hierarchy dedup client-side); optional contributors (granted-token scopes, PRM 
`scopes_supported`, client-metadata scop [...]
+
+`McpStepUpAuthorizer` is the step-up state machine, built via 
`McpStepUpAuthorizer.create()`. `execute(String operation, ScopedCall<T> call)` 
runs an MCP call under the given operation identifier; on an 
`McpInsufficientScopeException` (raised from a parsed 
`WwwAuthenticateChallenge`) it computes the new union, drives a caller-supplied 
`Reauthorizer` (a fresh auth-code+PKCE round-trip for user-delegation clients, 
or an M2M re-acquire), installs the new token, and retries — bounded by a ha 
[...]
+
+**Attempt-counter lifetime.** The per-`(resource, operation)` attempt counter 
is cleared automatically on a **successful** `execute(...)`, so a later 
legitimate step-up for the same operation starts fresh rather than being 
permanently refused by a long-lived authorizer. To recover an operation that 
hit the cap (e.g. after fixing the underlying grant), call `reset(operation)`; 
`resetAll()` clears every operation's counter for the authorizer's resource.
+
 ## Response cache seam
 
 `McpResponseCache` is opt-in and off by default. `InMemoryMcpResponseCache` is 
the built-in default implementation. The v2 adapter applies protocol cache 
hints (`ttlMs`, `cacheScope`) when available.

Reply via email to