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

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


The following commit(s) were added to refs/heads/master by this push:
     new 1dd9ea59c5 MCP 2026-07-28 MRTR requestState KeyProvider SPI (TODO-324)
1dd9ea59c5 is described below

commit 1dd9ea59c584d2bb4a059811a3b2a2feea2a0c37
Author: James Bognar <[email protected]>
AuthorDate: Wed Aug 5 00:32:23 2026 -0700

    MCP 2026-07-28 MRTR requestState KeyProvider SPI (TODO-324)
    
    Introduces a pluggable KeyProvider SPI beneath AeadRequestStateCodec so an
    operator can supply a stable, shared sealing key (fixing horizontally-scaled
    RESUME behind a load balancer) and rotate keys via a keyId, while the
    zero-config EphemeralKeyProvider default preserves today's per-process
    behavior. Adds StaticKeyProvider (shared key + static rotation), a versioned
    4-segment wire format with keyId folded into the GCM AAD and fail-closed
    unseal, and McpMrtrConfig.setKeyProvider wiring, with dispatch- and
    integration-level cross-instance-resume proofs.
    
    Co-authored-by: Cursor <[email protected]>
---
 .../mcp/v20260728/McpMrtrIntegration_Test.java     | 103 ++++++++++++-
 .../mcp/v20260728/AeadRequestStateCodec.java       | 135 ++++++++++------
 .../server/mcp/v20260728/EphemeralKeyProvider.java |  76 +++++++++
 .../rest/server/mcp/v20260728/KeyProvider.java     |  78 ++++++++++
 .../rest/server/mcp/v20260728/KeyedSecret.java     |  62 ++++++++
 .../rest/server/mcp/v20260728/McpMrtrConfig.java   |  20 +++
 .../server/mcp/v20260728/StaticKeyProvider.java    | 171 +++++++++++++++++++++
 .../mcp/v20260728/AeadRequestStateCodec_Test.java  | 137 +++++++++++++++--
 .../mcp/v20260728/EphemeralKeyProvider_Test.java   |  51 ++++++
 .../server/mcp/v20260728/KeyProvider_Test.java     |  76 +++++++++
 .../server/mcp/v20260728/KeyedSecret_Test.java     |  74 +++++++++
 .../server/mcp/v20260728/McpMrtrConfig_Test.java   |  31 ++++
 .../server/mcp/v20260728/McpMrtrDispatch_Test.java |  33 +++-
 .../rest/server/mcp/v20260728/McpOptions_Test.java |   6 +
 .../mcp/v20260728/StaticKeyProvider_Test.java      | 111 +++++++++++++
 15 files changed, 1099 insertions(+), 65 deletions(-)

diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
index 9436b300d3..e39019b652 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
@@ -18,6 +18,7 @@ package org.apache.juneau.rest.client.mcp.v20260728;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.security.SecureRandom;
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -66,6 +67,18 @@ class McpMrtrIntegration_Test extends TestBase {
 
        static final AeadRequestStateCodec CODEC = new AeadRequestStateCodec();
 
+       // A 32-byte AES-256 key shared by exactly the two fixture bindings 
below (KeyProviderFixtureA/B) -- the
+       // live-wire analog of the in-process McpMrtrDispatch_Test#d04 proof: 
two independently-constructed
+       // AeadRequestStateCodec instances (one per binding, each built fresh 
by its own createMcpOptions() call)
+       // share only this KeyProvider object, never the codec object.
+       static final KeyProvider SHARED_KEY_PROVIDER = 
StaticKeyProvider.of("2026-08-it", 
StaticKeyProvider.aesKey(randomAesKeyBytes()));
+
+       private static byte[] randomAesKeyBytes() {
+               var bytes = new byte[32];
+               new SecureRandom().nextBytes(bytes);
+               return bytes;
+       }
+
        private static String aad(String method) {
                return method + '\u0000' + McpProtocol.VERSION_2026_07_28;
        }
@@ -111,9 +124,56 @@ class McpMrtrIntegration_Test extends TestBase {
                }
        }
 
+       private static McpToolHandler askShared() {
+               return new McpToolHandler() {
+                       @Override public McpToolSpec descriptor() { return new 
McpToolSpec().setName("askShared").setDescription("Pauses once under a shared 
KeyProvider, then completes"); }
+                       @Override public McpToolOutcome call(Map<String,Object> 
arguments, BeanStore ctx) {
+                               var resume = 
ctx.getBean(McpMrtrResumeContext.class);
+                               if (resume.isEmpty())
+                                       throw new 
McpInputRequiredSignal(Map.of("q1", Map.of("type", "elicitation")), 
"cont-shared");
+                               return McpToolOutcome.text("resumed-shared:" + 
resume.get().inputResponses().get("q1"));
+                       }
+               };
+       }
+
+       // Two independently-constructed bindings sharing only 
SHARED_KEY_PROVIDER: each createMcpOptions() call below
+       // constructs its own fresh AeadRequestStateCodec (via setKeyProvider's 
"return setCodec(new
+       // AeadRequestStateCodec(value))"), so /kp-a and /kp-b never share a 
codec instance, only the key material.
+       @Rest(path="/kp-a", serializers = JsonSerializer.class, parsers = 
JsonParser.class, defaultAccept = "application/json")
+       public static class KeyProviderFixtureA extends 
org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               protected McpServerConfig createMcpConfig() {
+                       return new 
McpServerConfig().setName("it-mrtr-kp-a").setVersion("1.0.0").addTool(askShared());
+               }
+
+               @Override
+               protected McpOptions createMcpOptions() {
+                       return new McpOptions().mrtr(m -> 
m.setKeyProvider(SHARED_KEY_PROVIDER));
+               }
+       }
+
+       @Rest(path="/kp-b", serializers = JsonSerializer.class, parsers = 
JsonParser.class, defaultAccept = "application/json")
+       public static class KeyProviderFixtureB extends 
org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               protected McpServerConfig createMcpConfig() {
+                       return new 
McpServerConfig().setName("it-mrtr-kp-b").setVersion("1.0.0").addTool(askShared());
+               }
+
+               @Override
+               protected McpOptions createMcpOptions() {
+                       return new McpOptions().mrtr(m -> 
m.setKeyProvider(SHARED_KEY_PROVIDER));
+               }
+       }
+
        @Configuration
        public static class FixtureConfig {
-               @Bean public Servlet mcpServlet() { return new Fixture(); }
+               @Bean(name="mcpServlet") public Servlet mcpServlet() { return 
new Fixture(); }
+               @Bean(name="mcpKeyProviderServletA") public Servlet 
mcpKeyProviderServletA() { return new KeyProviderFixtureA(); }
+               @Bean(name="mcpKeyProviderServletB") public Servlet 
mcpKeyProviderServletB() { return new KeyProviderFixtureB(); }
        }
 
        @RegisterExtension
@@ -129,6 +189,15 @@ class McpMrtrIntegration_Test extends TestBase {
                        .clientCapabilities(caps);
        }
 
+       private static McpClient.Builder clientBuilderAt(String path, boolean 
withElicitation) {
+               var caps = new ClientCapabilities();
+               if (withElicitation)
+                       caps.setElicitation(new ElicitationCapability());
+               return McpClient.builder()
+                       .endpoint(fixture.getRootUrl() + path)
+                       .clientCapabilities(caps);
+       }
+
        // 
=================================================================================================================
        // A: full PAUSE -> RESUME -> complete loop across the real wire.
        // 
=================================================================================================================
@@ -188,14 +257,40 @@ class McpMrtrIntegration_Test extends TestBase {
                        var token = (String) paused.get("requestState");
                        // Flip one ciphertext byte to corrupt the AEAD tag; a 
final-character flip can land on an unpadded
                        // base64url "don't-care" low bit and decode to 
identical bytes, leaving the tag (and test) flaky.
-                       var parts = token.split("\\.", 2);
-                       var ciphertext = 
Base64.getUrlDecoder().decode(parts[1]);
+                       var parts = token.split("\\.", 4);
+                       var ciphertext = 
Base64.getUrlDecoder().decode(parts[3]);
                        ciphertext[0] ^= 1;
-                       var tampered = parts[0] + "." + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
+                       var tampered = parts[0] + "." + parts[1] + "." + 
parts[2] + "."
+                               + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
                        var e = assertThrows(McpException.class,
                                () -> client.callRaw(McpMethods.TOOLS_CALL,
                                        new 
CallToolRequest().setName("ask").setRequestState(tampered).setInputResponses(Map.of("q1",
 "answer"))));
                        
assertEquals(org.apache.juneau.rest.server.mcp.v20260728.McpRevision.CODE_INVALID_PARAMS,
 e.getCode());
                }
        }
+
+       // 
=================================================================================================================
+       // C: shared KeyProvider (TODO-324) -- resume succeeds across two 
independently-constructed server bindings.
+       // 
=================================================================================================================
+
+       @Test void 
c01_sharedKeyProviderResume_succeedsAcrossIndependentBindings() throws 
Exception {
+               String token;
+               try (var clientA = clientBuilderAt("/kp-a/", true).build()) {
+                       var paused = clientA.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("askShared").setArguments(Map.of()));
+                       assertEquals("input_required", 
paused.get("resultType"));
+                       token = (String) paused.get("requestState");
+                       assertNotNull(token);
+               }
+
+               // Resume against a DIFFERENT binding (/kp-b), backed by an 
independently-constructed
+               // AeadRequestStateCodec that shares only SHARED_KEY_PROVIDER 
with /kp-a's codec, never the codec object
+               // itself -- the live-wire analog of McpMrtrDispatch_Test#d04.
+               try (var clientB = clientBuilderAt("/kp-b/", true).build()) {
+                       var completed = clientB.callRaw(McpMethods.TOOLS_CALL,
+                               new 
CallToolRequest().setName("askShared").setRequestState(token).setInputResponses(Map.of("q1",
 "answer")));
+                       assertEquals("complete", completed.get("resultType"));
+                       var result = Json.to(Json.of(completed), 
CallToolResult.class);
+                       assertEquals("resumed-shared:answer", ((TextContent) 
result.getContent().get(0)).getText());
+               }
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec.java
index be7c84fe3c..552df8603d 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec.java
@@ -24,85 +24,118 @@ import java.util.Base64;
 import java.util.Optional;
 
 import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
 
 import org.apache.juneau.marshall.marshaller.Json;
 
 /**
- * Built-in default {@link RequestStateCodec}: AES-256-GCM with a per-process 
ephemeral key.
+ * Built-in default {@link RequestStateCodec}: AES-256-GCM keyed by a 
pluggable {@link KeyProvider}.
  *
  * <p>
- * <b>Not restart-durable and not shareable across process instances by 
design.</b> Each instance generates
- * its own random key at construction time; a token sealed by one instance can 
never be unsealed by another
+ * The no-arg constructor uses a fresh, per-process {@link 
EphemeralKeyProvider} &mdash; <b>not restart-durable
+ * and not shareable across process instances by design.</b> Each instance 
generates its own random key and
+ * {@code keyId} at construction time; a token sealed by one instance can 
never be unsealed by another
  * (including the same process after a restart). Operators who need 
cross-restart or multi-instance resumption
- * must supply a shared/rotating-key {@link RequestStateCodec} implementation 
via {@link McpMrtrConfig} instead
- * of relying on this default. This is a documented, tested property (see 
{@code AeadRequestStateCodec_Test}),
- * not an accidental limitation.
+ * must supply a shared/rotating {@link KeyProvider} (e.g. {@link 
StaticKeyProvider}) via
+ * {@link #AeadRequestStateCodec(KeyProvider)} instead of relying on this 
default. This is a documented, tested
+ * property (see {@code AeadRequestStateCodec_Test}), not an accidental 
limitation.
  *
  * <p>
- * Sealed-token format (all opaque to callers): {@code base64(nonce) '.' 
base64(ciphertext+tag)}. The
- * plaintext is the JSON serialization of the {@link McpRequestState} record. 
The AAD passed to
- * {@link #seal}/{@link #unseal} is authenticated but never encrypted 
(standard AES-GCM AAD semantics) &mdash;
- * the dispatcher passes the canonical {@code method + '\u0000' + 
protocolVersion} (NUL-separated) form as the
- * AAD (see {@code McpRevision#aad}).
+ * Sealed-token format (all opaque to callers, versioned so it can evolve):
+ * {@code version '.' base64url(keyId) '.' base64url(nonce) '.' 
base64url(ciphertext+tag)}. The current
+ * {@code version} literal is {@code "1"}; an unrecognized version fails 
{@link #unseal} closed. The
+ * {@code keyId} segment is cleartext base64url of the UTF-8 {@code keyId} 
bytes &mdash; it has to be read
+ * before the key it names can be resolved for decryption &mdash; but it is 
folded into the AEAD's authenticated
+ * data, so a swapped {@code keyId} fails the GCM tag check exactly like any 
other tamper. The plaintext is the
+ * JSON serialization of the {@link McpRequestState} record.
  *
  * <p>
- * {@link #unseal} is wired to untrusted, client-supplied {@code requestState} 
strings, so it applies a
- * defensive length guard (a plausibly-sized token only) before any base64 
decode; an out-of-bounds input
- * returns {@link Optional#empty()} rather than allocating from an 
attacker-controlled length.
+ * The AAD passed to {@link #seal}/{@link #unseal} is authenticated but never 
encrypted (standard AES-GCM AAD
+ * semantics). The dispatcher passes the canonical {@code method + '\u0000' + 
protocolVersion} (NUL-separated)
+ * form as the caller-supplied AAD (see {@code McpRevision#aad}); this codec 
appends {@code '\u0000' + keyId}
+ * to that value before passing it to the cipher, per {@link KeyProvider}'s 
implicit {@code keyId}-authentication
+ * contract.
+ *
+ * <p>
+ * Random per-seal nonces are safe up to roughly 2^32 seals under a single key 
(the AES-GCM birthday bound). The
+ * ephemeral default's key is per-process and never approaches that. A 
long-lived custom {@link KeyProvider} key
+ * (e.g. a {@link StaticKeyProvider} entry left in place for months) is the 
case where this bound actually
+ * matters &mdash; rotate to a fresh {@code keyId} well before it, not after.
+ *
+ * <p>
+ * {@link #unseal} is wired to untrusted, client-supplied {@code requestState} 
strings, so it applies defensive
+ * length guards (a plausibly-sized token, and a bounded {@code keyId} 
segment) before any base64 decode; an
+ * out-of-bounds input returns {@link Optional#empty()} rather than allocating 
from an attacker-controlled
+ * length.
  */
 public class AeadRequestStateCodec implements RequestStateCodec {
 
        private static final String ALGORITHM = "AES/GCM/NoPadding";
-       private static final int KEY_BITS = 256;
        private static final int GCM_TAG_BITS = 128;
        private static final int NONCE_BYTES = 12;
 
-       // Exact unpadded base64url length of the 12-byte nonce prefix 
(ceil(12*4/3)=16). The nonce part of a
+       // Wire-format version literal (see class Javadoc). Bumping this is how 
the format could evolve; unseal
+       // rejects any other value.
+       private static final String VERSION = "1";
+
+       // Exact unpadded base64url length of the 12-byte nonce segment 
(ceil(12*4/3)=16). The nonce segment of a
        // well-formed token is always exactly this many chars, checked after 
the split in unseal.
        private static final int NONCE_B64_CHARS = 16;
 
-       // Defensive bounds on the untrusted, client-supplied token string (see 
unseal). The real floor of a
-       // well-formed token is NONCE_B64_CHARS + '.' + base64url of at least 
the 16-byte GCM tag (22 chars) = 39, so
-       // anything shorter cannot be a valid token; 64KB is orders of 
magnitude above any legitimate sealed
-       // McpRequestState.
-       private static final int MIN_TOKEN_CHARS = NONCE_B64_CHARS + 1 + 22;
+       // Defensive ceiling on the base64url'd keyId segment of an untrusted, 
client-supplied token (see unseal).
+       // Derived from KeyedSecret's MAX_KEY_ID_CHARS=128 producer limit 
(measured in UTF-16 code units, no
+       // charset restriction) at the worst case of 3 UTF-8 bytes/char: 
128*3=384 bytes, whose unpadded base64url
+       // encoding is ceil(384/3)*4=512 chars. Set to that worst case so it 
dominates any legitimately-issued
+       // keyId while still capping an attacker-inflated segment before base64 
decode.
+       private static final int MAX_KEY_ID_B64_CHARS = 512;
+
+       // Defensive floor on the untrusted, client-supplied token string as a 
whole (see unseal). The real floor of
+       // a well-formed token is VERSION(1) + '.' + smallest possible keyId 
segment (2, for a 1-byte keyId) + '.' +
+       // NONCE_B64_CHARS + '.' + base64url of at least the 16-byte GCM tag 
(22 chars). 64KB is orders of magnitude
+       // above any legitimate sealed McpRequestState.
+       private static final int MIN_TOKEN_CHARS = VERSION.length() + 1 + 2 + 1 
+ NONCE_B64_CHARS + 1 + 22;
        private static final int MAX_TOKEN_CHARS = 64 * 1024;
 
        private static final Base64.Encoder B64URL = 
Base64.getUrlEncoder().withoutPadding();
 
-       private final SecretKey key;
+       private final KeyProvider keyProvider;
        private final SecureRandom random = new SecureRandom();
 
        /**
-        * Constructor. Generates a fresh, per-instance AES-256 key.
+        * Constructor. Uses a fresh, per-process {@link EphemeralKeyProvider} 
&mdash; the zero-config, non-shareable,
+        * non-durable default described in the class Javadoc.
         */
        public AeadRequestStateCodec() {
-               try {
-                       var gen = KeyGenerator.getInstance("AES");
-                       gen.init(KEY_BITS);
-                       key = gen.generateKey();
-               } catch (Exception e) { // HTT every JDK guarantees AES-256 key 
generation via the standard JCE provider
-                       throw rex(e, "Failed to generate AES-GCM key");
-               }
+               this(new EphemeralKeyProvider());
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param keyProvider The source of sealing/resolving keys. Must not be 
<jk>null</jk>.
+        */
+       public AeadRequestStateCodec(KeyProvider keyProvider) {
+               if (keyProvider == null)
+                       throw iaex("keyProvider must not be null");
+               this.keyProvider = keyProvider;
        }
 
        @Override /* RequestStateCodec */
        public String seal(McpRequestState state, String aad) {
                try {
+                       var ks = keyProvider.currentKey();
                        // Random 96-bit nonce per seal. AES-GCM's birthday 
bound makes random nonces safe up to roughly 2^32
-                       // seals under a single key; the built-in default's key 
is per-process and ephemeral, so it never
-                       // approaches that, but a long-lived custom-codec key 
should be rotated well before 2^32 seals.
+                       // seals under a single key; see the class Javadoc for 
why this matters more for a long-lived custom
+                       // KeyProvider key than for the per-process ephemeral 
default.
                        var nonce = new byte[NONCE_BYTES];
                        random.nextBytes(nonce);
                        var cipher = Cipher.getInstance(ALGORITHM);
-                       cipher.init(Cipher.ENCRYPT_MODE, key, new 
GCMParameterSpec(GCM_TAG_BITS, nonce));
-                       cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                       cipher.init(Cipher.ENCRYPT_MODE, ks.key(), new 
GCMParameterSpec(GCM_TAG_BITS, nonce));
+                       cipher.updateAAD((aad + '\u0000' + 
ks.keyId()).getBytes(StandardCharsets.UTF_8));
                        var plaintext = 
Json.of(state).getBytes(StandardCharsets.UTF_8);
                        var ciphertext = cipher.doFinal(plaintext);
-                       return B64URL.encodeToString(nonce) + "." + 
B64URL.encodeToString(ciphertext);
+                       var keyIdB64 = 
B64URL.encodeToString(ks.keyId().getBytes(StandardCharsets.UTF_8));
+                       return VERSION + "." + keyIdB64 + "." + 
B64URL.encodeToString(nonce) + "." + B64URL.encodeToString(ciphertext);
                } catch (Exception e) { // HTT encryption with a 
freshly-generated 12-byte nonce cannot fail under the standard JCE provider
                        throw rex(e, "Failed to seal requestState");
                }
@@ -113,21 +146,29 @@ public class AeadRequestStateCodec implements 
RequestStateCodec {
                try {
                        if (token.length() < MIN_TOKEN_CHARS || token.length() 
> MAX_TOKEN_CHARS)
                                return Optional.empty();
-                       var parts = token.split("\\.", 2);
-                       // A well-formed token is exactly base64url(12-byte 
nonce) '.' base64url(ciphertext+tag); reject any
-                       // token missing the separator or whose nonce part is 
not the expected length before any base64 decode.
-                       if (parts.length != 2 || parts[0].length() != 
NONCE_B64_CHARS)
+                       var parts = token.split("\\.", 4);
+                       // A well-formed token is exactly version '.' 
base64url(keyId) '.' base64url(12-byte nonce) '.'
+                       // base64url(ciphertext+tag); reject any token with the 
wrong segment count, an unrecognized version,
+                       // an empty/oversized keyId segment, or a nonce segment 
that isn't the expected length before any
+                       // base64 decode.
+                       if (parts.length != 4 || !VERSION.equals(parts[0]) || 
parts[1].isEmpty() || parts[1].length() > MAX_KEY_ID_B64_CHARS
+                                       || parts[2].length() != NONCE_B64_CHARS)
+                               return Optional.empty();
+                       var keyId = new 
String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
+                       var nonce = Base64.getUrlDecoder().decode(parts[2]);
+                       var ciphertext = 
Base64.getUrlDecoder().decode(parts[3]);
+                       var resolved = keyProvider.resolveKey(keyId);
+                       if (resolved.isEmpty())
                                return Optional.empty();
-                       var nonce = Base64.getUrlDecoder().decode(parts[0]);
-                       var ciphertext = 
Base64.getUrlDecoder().decode(parts[1]);
                        var cipher = Cipher.getInstance(ALGORITHM);
-                       cipher.init(Cipher.DECRYPT_MODE, key, new 
GCMParameterSpec(GCM_TAG_BITS, nonce));
-                       cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                       cipher.init(Cipher.DECRYPT_MODE, resolved.get(), new 
GCMParameterSpec(GCM_TAG_BITS, nonce));
+                       cipher.updateAAD((aad + '\u0000' + 
keyId).getBytes(StandardCharsets.UTF_8));
                        var plaintext = cipher.doFinal(ciphertext);
                        return Optional.of(Json.to(new String(plaintext, 
StandardCharsets.UTF_8), McpRequestState.class));
                } catch (@SuppressWarnings("unused") Exception e) {
-                       // Any failure (bad base64, AEAD tag mismatch from 
tamper/AAD mismatch, malformed JSON) is a
-                       // verification failure per the RequestStateCodec 
contract, not an exceptional condition to propagate.
+                       // Any failure (bad base64, unknown/retired keyId, AEAD 
tag mismatch from tamper/AAD/keyId mismatch,
+                       // malformed JSON) is a verification failure per the 
RequestStateCodec contract, not an exceptional
+                       // condition to propagate.
                        return Optional.empty();
                }
        }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider.java
new file mode 100644
index 0000000000..59424a9d44
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Optional;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/**
+ * Built-in default {@link KeyProvider}: a random AES-256 key with a random 
{@code keyId}, both fixed for the
+ * lifetime of the instance.
+ *
+ * <p>
+ * <b>Not restart-durable and not shareable across process instances by 
design</b> &mdash; mirrors
+ * {@code AeadRequestStateCodec}'s pre-{@code KeyProvider} ephemeral-key 
behavior exactly, including the
+ * cross-instance-unseal-fails guarantee (see {@code 
AeadRequestStateCodec_Test}'s {@code a06}), just relocated
+ * one layer down. Operators who need cross-restart or multi-instance RESUME 
must supply {@link StaticKeyProvider}
+ * (or a custom {@link KeyProvider}) instead of relying on this default.
+ *
+ * @since 10.0.0
+ */
+public class EphemeralKeyProvider implements KeyProvider {
+
+       private static final int KEY_BITS = 256;
+       // 8 random bytes -> 11 base64url chars (unpadded): short but 
collision-safe for a per-process identifier.
+       private static final int KEY_ID_BYTES = 8;
+
+       private static final Base64.Encoder B64URL = 
Base64.getUrlEncoder().withoutPadding();
+
+       private final KeyedSecret current;
+
+       /**
+        * Constructor. Generates a fresh, per-instance AES-256 key and a 
fresh, per-instance random {@code keyId}.
+        */
+       public EphemeralKeyProvider() {
+               try {
+                       var gen = KeyGenerator.getInstance("AES");
+                       gen.init(KEY_BITS);
+                       var key = gen.generateKey();
+                       var idBytes = new byte[KEY_ID_BYTES];
+                       new SecureRandom().nextBytes(idBytes);
+                       current = new 
KeyedSecret(B64URL.encodeToString(idBytes), key);
+               } catch (Exception e) { // HTT every JDK guarantees AES-256 key 
generation via the standard JCE provider
+                       throw rex(e, "Failed to generate AES key");
+               }
+       }
+
+       @Override /* KeyProvider */
+       public KeyedSecret currentKey() {
+               return current;
+       }
+
+       @Override /* KeyProvider */
+       public Optional<SecretKey> resolveKey(String keyId) {
+               return keyId.equals(current.keyId()) ? 
Optional.of(current.key()) : Optional.empty();
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider.java
new file mode 100644
index 0000000000..3bb0fe2d09
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+
+/**
+ * SPI for supplying the key(s) used to seal/unseal MCP MRTR {@code 
requestState} continuation tokens (see
+ * {@code AeadRequestStateCodec}).
+ *
+ * <p>
+ * A dedicated abstraction beneath the AEAD codec so an operator can supply a 
stable, shared sealing key (fixing
+ * horizontally-scaled RESUME behind a load balancer) and rotate keys via a 
{@code keyId}, while the codec's
+ * zero-config default ({@link EphemeralKeyProvider}) keeps today's 
per-process ephemeral-key behavior unchanged.
+ *
+ * <p>
+ * <b>Thread-safety.</b> A single provider instance is shared across all 
requests against a binding (the codec
+ * is per-binding), so implementations must be safe for concurrent calls to 
both {@link #currentKey()} and
+ * {@link #resolveKey(String)}.
+ *
+ * <p>
+ * <b>Never-throw contract.</b> {@link #resolveKey(String)} is called on the 
unseal path with an untrusted,
+ * client-supplied {@code keyId}. Implementations must treat any unknown or 
retired {@code keyId} as
+ * {@link Optional#empty()} and must never throw &mdash; the caller ({@code 
AeadRequestStateCodec#unseal}) has no
+ * exception-handling path for this call.
+ *
+ * <p>
+ * Not annotated {@code @FunctionalInterface}: unlike single-method SPIs such 
as
+ * {@link org.apache.juneau.rest.server.auth.ApiKeyStore}, this SPI declares 
two abstract methods.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link KeyedSecret}
+ *     <li class='jc'>{@link EphemeralKeyProvider}
+ *     <li class='jc'>{@link StaticKeyProvider}
+ * </ul>
+ *
+ * @since 10.0.0
+ */
+public interface KeyProvider {
+
+       /**
+        * The key to seal new tokens with, together with its identifier.
+        *
+        * <p>
+        * May return a different value over time &mdash; a changing return 
value is the sanctioned mechanism for
+        * live runtime key rotation in a custom provider. Built-in providers 
({@link EphemeralKeyProvider},
+        * {@link StaticKeyProvider}) return a fixed value for the lifetime of 
the instance.
+        *
+        * @return The current sealing key. Never <jk>null</jk>.
+        */
+       KeyedSecret currentKey();
+
+       /**
+        * Resolves a {@code keyId} seen on an incoming token to the key that 
sealed it.
+        *
+        * @param keyId The key identifier read from the untrusted, 
client-supplied token, before decryption. Never
+        *      <jk>null</jk>.
+        * @return The resolved key, or {@link Optional#empty()} if {@code 
keyId} is unknown or retired. Never
+        *      throws.
+        */
+       Optional<SecretKey> resolveKey(String keyId);
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret.java
new file mode 100644
index 0000000000..a45731a169
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import javax.crypto.SecretKey;
+
+/**
+ * A key paired with the identifier an operator chose for it, as returned by 
{@link KeyProvider#currentKey()}
+ * and resolved by {@link KeyProvider#resolveKey(String)}.
+ *
+ * <p>
+ * <b>Never log {@code key}.</b> Mirrors {@link RequestStateCodec}'s "never 
log" contract for the sealed token:
+ * this record holds the actual sealing key, so {@link #toString()} 
deliberately redacts {@code key} rather than
+ * relying on the record's auto-generated form, which would print whatever a 
custom/HSM {@link SecretKey} impl's
+ * own {@code toString()} legally chooses to include.
+ *
+ * @param keyId An opaque, operator-chosen short identifier for {@code key} 
(e.g. {@code "2026-08-a"}). Must not
+ *     be <jk>null</jk> or blank, and is length-bounded so a sealed token's 
cleartext {@code keyId} wire segment
+ *     (see {@code AeadRequestStateCodec}'s versioned wire format) stays 
bounded.
+ * @param key The key material. Must not be <jk>null</jk>. The AEAD codec is 
AES-GCM-specific by design, so
+ *     non-AES key material fails at cipher-init time in {@code 
AeadRequestStateCodec}, not here.
+ * @since 10.0.0
+ */
+public record KeyedSecret(String keyId, SecretKey key) {
+
+       // Bounds the base64url'd keyId wire segment so an oversized keyId 
cannot inflate a sealed token.
+       private static final int MAX_KEY_ID_CHARS = 128;
+
+       /**
+        * Compact constructor &mdash; validates {@code keyId} is 
non-null/non-blank/length-bounded and {@code key}
+        * is non-null.
+        */
+       public KeyedSecret {
+               assertArgNotNullOrBlank("keyId", keyId);
+               assertArg(keyId.length() <= MAX_KEY_ID_CHARS, "Argument 'keyId' 
length (%s) exceeds max of %s chars.", keyId.length(), MAX_KEY_ID_CHARS);
+               assertArgNotNull("key", key);
+       }
+
+       /**
+        * Redacts {@code key} so key material never reaches logs via this 
record's {@code toString()}.
+        */
+       @Override
+       public String toString() {
+               return "KeyedSecret[keyId=" + keyId + ", key=<redacted>]";
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig.java
index 949b38bda9..4d291d51cb 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig.java
@@ -83,6 +83,26 @@ public class McpMrtrConfig {
                return this;
        }
 
+       /**
+        * Sets the key provider, wrapping it in a fresh {@link 
AeadRequestStateCodec}.
+        *
+        * <p>
+        * Sugar for the common "keep AES-GCM, just supply my keys" case 
&mdash; equivalent to
+        * {@code setCodec(new AeadRequestStateCodec(value))}. {@link 
#setKeyProvider} and {@link #setCodec} are
+        * last-wins: both assign the same {@code codec} field, so whichever is 
called more recently determines the
+        * effective codec. There is no getter for the provider itself &mdash; 
{@link #getCodec()} remains the sole
+        * accessor, since the codec (not the provider) is the source of truth 
once either setter has been called.
+        *
+        * @param value The new value. Must not be {@code null}.
+        * @return This object.
+        * @throws IllegalArgumentException If {@code value} is {@code null}.
+        */
+       public McpMrtrConfig setKeyProvider(KeyProvider value) {
+               if (value == null)
+                       throw new IllegalArgumentException("keyProvider must 
not be null");
+               return setCodec(new AeadRequestStateCodec(value));
+       }
+
        /**
         * The {@code requestState} time-to-live in milliseconds.
         *
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider.java
new file mode 100644
index 0000000000..031c3cd0bd
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Built-in, immutable, static-at-startup {@link KeyProvider}: an 
operator-supplied keyring of one or more AES
+ * keys, one of which is designated the current sealing key. Unlike {@link 
EphemeralKeyProvider}, the keyring is
+ * fixed at construction time (via {@link Builder} or {@link #of(String, 
SecretKey)}) rather than generated
+ * randomly, so it can be shared across process instances &mdash; the fix for 
horizontally-scaled MRTR RESUME
+ * (see {@code AeadRequestStateCodec}).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     KeyProvider <jv>kp</jv> = StaticKeyProvider.<jsm>create</jsm>()
+ *             .addKey(<js>"2026-08-a"</js>, <jv>k1</jv>)
+ *             .current(<js>"2026-08-a"</js>)
+ *             .addKey(<js>"2026-07-z"</js>, <jv>kOld</jv>)
+ *             .build();
+ *
+ *     <jc>// Or, for the common single-shared-key case:</jc>
+ *     KeyProvider <jv>kp2</jv> = 
StaticKeyProvider.<jsm>of</jsm>(<js>"2026-08-a"</js>, <jv>k1</jv>);
+ * </p>
+ *
+ * @since 10.0.0
+ */
+public final class StaticKeyProvider implements KeyProvider {
+
+       private final KeyedSecret current;
+       private final Map<String,SecretKey> keysById;
+
+       private StaticKeyProvider(KeyedSecret current, Map<String,SecretKey> 
keysById) {
+               this.current = current;
+               // Defensive copy: keysById may be the Builder's own live map, 
and the Builder is not consumed by
+               // build() (an operator can keep calling addKey(...)/build() to 
mint successive keyrings), so this
+               // instance must not observe later mutations of that map.
+               this.keysById = Collections.unmodifiableMap(new 
HashMap<>(keysById));
+       }
+
+       /**
+        * Creates a new {@link Builder}.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       /**
+        * One-liner for the common single-shared-key case: a provider with 
exactly one key, designated current.
+        *
+        * @param keyId The key's identifier. Must not be <jk>null</jk> or 
blank.
+        * @param key The key material. Must not be <jk>null</jk>.
+        * @return A new, immutable {@link StaticKeyProvider} holding just this 
one key. Never <jk>null</jk>.
+        * @throws IllegalArgumentException If {@code keyId} is 
<jk>null</jk>/blank or {@code key} is <jk>null</jk>.
+        */
+       public static StaticKeyProvider of(String keyId, SecretKey key) {
+               return create().addKey(keyId, key).current(keyId).build();
+       }
+
+       /**
+        * Builds an AES {@link SecretKey} from raw key bytes.
+        *
+        * @param value The raw AES key bytes (16/24/32 bytes for 
AES-128/192/256). Must not be <jk>null</jk>.
+        * @return A new AES {@link SecretKey} wrapping {@code value}. Never 
<jk>null</jk>.
+        */
+       public static SecretKey aesKey(byte[] value) {
+               assertArgNotNull("value", value);
+               return new SecretKeySpec(value, "AES");
+       }
+
+       /**
+        * Builds an AES {@link SecretKey} from base64-encoded key bytes.
+        *
+        * @param base64 The base64-encoded AES key bytes. Must not be 
<jk>null</jk> or blank.
+        * @return A new AES {@link SecretKey} wrapping the decoded bytes. 
Never <jk>null</jk>.
+        */
+       public static SecretKey aesKey(String base64) {
+               assertArgNotNullOrBlank("base64", base64);
+               return aesKey(Base64.getDecoder().decode(base64));
+       }
+
+       @Override /* KeyProvider */
+       public KeyedSecret currentKey() {
+               return current;
+       }
+
+       @Override /* KeyProvider */
+       public Optional<SecretKey> resolveKey(String keyId) {
+               return Optional.ofNullable(keysById.get(keyId));
+       }
+
+       /**
+        * Builder for {@link StaticKeyProvider}.
+        */
+       public static final class Builder {
+
+               private final Map<String,SecretKey> keys = new HashMap<>();
+               private String currentKeyId;
+
+               private Builder() {}
+
+               /**
+                * Adds a resolvable key.
+                *
+                * @param keyId The key's identifier. Must not be <jk>null</jk> 
or blank.
+                * @param key The key material. Must not be <jk>null</jk>.
+                * @return This object.
+                * @throws IllegalArgumentException If {@code keyId} is 
<jk>null</jk>/blank or {@code key} is
+                *      <jk>null</jk>.
+                */
+               public Builder addKey(String keyId, SecretKey key) {
+                       assertArgNotNullOrBlank("keyId", keyId);
+                       assertArgNotNull("key", key);
+                       keys.put(keyId, key);
+                       return this;
+               }
+
+               /**
+                * Designates the current sealing key by identifier. Must name 
a key previously added via
+                * {@link #addKey(String, SecretKey)}.
+                *
+                * @param keyId The identifier of the key to designate as 
current. Must not be <jk>null</jk> or blank.
+                * @return This object.
+                * @throws IllegalArgumentException If {@code keyId} is 
<jk>null</jk> or blank.
+                */
+               public Builder current(String keyId) {
+                       assertArgNotNullOrBlank("keyId", keyId);
+                       currentKeyId = keyId;
+                       return this;
+               }
+
+               /**
+                * Builds an immutable {@link StaticKeyProvider} snapshot of 
this builder's current state.
+                *
+                * @return A new, immutable {@link StaticKeyProvider}. Never 
<jk>null</jk>.
+                * @throws IllegalArgumentException If no current key was 
designated via {@link #current(String)}, or
+                *      the designated current key was never added via {@link 
#addKey(String, SecretKey)}.
+                */
+               public StaticKeyProvider build() {
+                       assertArg(currentKeyId != null, "No current key 
designated; call current(keyId) before build().");
+                       var key = keys.get(currentKeyId);
+                       assertArg(key != null, "current(''%s'') does not name a 
key added via addKey(...).", currentKeyId);
+                       return new StaticKeyProvider(new 
KeyedSecret(currentKeyId, key), keys);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec_Test.java
index 3ec98fde28..e582a87776 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/AeadRequestStateCodec_Test.java
@@ -18,9 +18,12 @@ package org.apache.juneau.rest.server.mcp.v20260728;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.Map;
 
+import javax.crypto.KeyGenerator;
+
 import org.apache.juneau.marshall.collections.JsonMap;
 import org.junit.jupiter.api.Test;
 
@@ -36,6 +39,8 @@ class AeadRequestStateCodec_Test {
                var a = new AeadRequestStateCodec();
                var b = new McpRequestState("continuation-value", "tools/call", 
1, 123456789L);
                var token = a.seal(b, AAD);
+               assertEquals(4, token.split("\\.", 4).length, "wire format must 
be version.keyId.nonce.ciphertext");
+               assertTrue(token.startsWith("1."), "version segment must be the 
literal \"1\"");
                var c = a.unseal(token, AAD);
                assertTrue(c.isPresent());
                assertEquals(b, c.get());
@@ -45,10 +50,11 @@ class AeadRequestStateCodec_Test {
                var a = new AeadRequestStateCodec();
                var b = new McpRequestState("continuation-value", "tools/call", 
1, 123456789L);
                var token = a.seal(b, AAD);
-               var parts = token.split("\\.", 2);
-               var ciphertext = Base64.getUrlDecoder().decode(parts[1]);
+               var parts = token.split("\\.", 4);
+               var ciphertext = Base64.getUrlDecoder().decode(parts[3]);
                ciphertext[0] ^= 1;
-               var tampered = parts[0] + "." + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
+               var tampered = parts[0] + "." + parts[1] + "." + parts[2] + "."
+                       + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
                var c = a.unseal(tampered, AAD);
                assertTrue(c.isEmpty());
        }
@@ -62,24 +68,27 @@ class AeadRequestStateCodec_Test {
        }
 
        @Test void a04_malformedTokenMissingSeparatorFailsUnseal() {
-               // Long enough (>= MIN_TOKEN_CHARS) to clear the length guard 
and actually reach the separator check.
+               // Long enough (>= MIN_TOKEN_CHARS) to clear the length guard 
and actually reach the segment-count check.
                var a = new AeadRequestStateCodec();
-               var c = a.unseal("a".repeat(40), AAD);
+               var c = a.unseal("a".repeat(50), AAD);
                assertTrue(c.isEmpty());
        }
 
        @Test void a05_malformedTokenInvalidBase64FailsUnseal() {
-               // A correctly-shaped token (16-char nonce part '.' body, >= 
MIN_TOKEN_CHARS) whose nonce part is not valid
-               // base64url: clears the length and separator/nonce-length 
guards and fails inside the base64 decode.
+               // A correctly-shaped token (version '.' keyId '.' 16-char 
nonce '.' body, >= MIN_TOKEN_CHARS) whose nonce
+               // segment is not valid base64url: clears the length, 
segment-count, and nonce-length guards and fails
+               // inside the base64 decode.
                var a = new AeadRequestStateCodec();
-               var c = a.unseal("!".repeat(16) + "." + "a".repeat(22), AAD);
+               var keyId = 
Base64.getUrlEncoder().withoutPadding().encodeToString("k".getBytes(StandardCharsets.UTF_8));
+               var c = a.unseal("1." + keyId + "." + "!".repeat(16) + "." + 
"a".repeat(22), AAD);
                assertTrue(c.isEmpty());
        }
 
        @Test void a05b_malformedTokenWrongNonceLengthFailsUnseal() {
-               // Long enough to clear the length guard and reach the 
nonce-length check, with a nonce part != 16 chars.
+               // Long enough to clear the length guard and reach the 
nonce-length check, with a nonce segment != 16 chars.
                var a = new AeadRequestStateCodec();
-               var c = a.unseal("aaaa" + "." + "a".repeat(40), AAD);
+               var keyId = 
Base64.getUrlEncoder().withoutPadding().encodeToString("k".getBytes(StandardCharsets.UTF_8));
+               var c = a.unseal("1." + keyId + "." + "aaaa" + "." + 
"a".repeat(40), AAD);
                assertTrue(c.isEmpty());
        }
 
@@ -87,7 +96,7 @@ class AeadRequestStateCodec_Test {
                // Below MIN_TOKEN_CHARS: rejected by the defensive length 
guard before any split/decode allocation.
                var a = new AeadRequestStateCodec();
                assertTrue(a.unseal("short", AAD).isEmpty());
-               assertTrue(a.unseal("!!!.!!!", AAD).isEmpty());
+               assertTrue(a.unseal("1.a.bbbb.cccc", AAD).isEmpty());
        }
 
        @Test void a06_perProcessEphemeralKeyPreventsCrossInstanceUnseal() {
@@ -99,6 +108,93 @@ class AeadRequestStateCodec_Test {
                assertTrue(c.isEmpty());
        }
 
+       @Test void a09_sharedKeyProviderAllowsCrossInstanceUnseal() throws 
Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               var sharedKey = gen.generateKey();
+               var keyProvider = StaticKeyProvider.of("2026-08-a", sharedKey);
+               var a = new AeadRequestStateCodec(keyProvider);
+               var b = new AeadRequestStateCodec(keyProvider);
+               var state = new McpRequestState("continuation-value", 
"tools/call", 1, 123456789L);
+               var token = a.seal(state, AAD);
+               var c = b.unseal(token, AAD);
+               assertTrue(c.isPresent(), "two codecs sharing one 
StaticKeyProvider must unseal each other's tokens");
+               assertEquals(state, c.get());
+       }
+
+       @Test void 
a10_rotatingKeyProviderUnsealsRetiredKeyAndSealsUnderNewCurrent() throws 
Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               var keyA = gen.generateKey();
+               var keyB = gen.generateKey();
+               var providerBeforeRotation = StaticKeyProvider.of("2026-07-z", 
keyA);
+               var codecBeforeRotation = new 
AeadRequestStateCodec(providerBeforeRotation);
+               var oldState = new McpRequestState("continuation-value", 
"tools/call", 1, 123456789L);
+               var oldToken = codecBeforeRotation.seal(oldState, AAD);
+
+               var providerAfterRotation = StaticKeyProvider.create()
+                       .addKey("2026-07-z", keyA)
+                       .addKey("2026-08-a", keyB)
+                       .current("2026-08-a")
+                       .build();
+               var codecAfterRotation = new 
AeadRequestStateCodec(providerAfterRotation);
+
+               var recoveredOld = codecAfterRotation.unseal(oldToken, AAD);
+               assertTrue(recoveredOld.isPresent(), "a token sealed under a 
still-resolvable retired key must still unseal after rotation");
+               assertEquals(oldState, recoveredOld.get());
+
+               var newState = new McpRequestState("continuation-value-2", 
"tools/call", 1, 123456789L);
+               var newToken = codecAfterRotation.seal(newState, AAD);
+               var newKeyId = new 
String(Base64.getUrlDecoder().decode(newToken.split("\\.", 4)[1]), 
StandardCharsets.UTF_8);
+               assertEquals("2026-08-a", newKeyId, "new tokens must seal under 
the new current key's keyId");
+       }
+
+       @Test void a11_swappedKeyIdSegmentFailsUnseal() throws Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               var keyA = gen.generateKey();
+               var keyB = gen.generateKey();
+               var provider = StaticKeyProvider.create()
+                       .addKey("2026-08-a", keyA)
+                       .addKey("2026-08-b", keyB)
+                       .current("2026-08-a")
+                       .build();
+               var a = new AeadRequestStateCodec(provider);
+               var state = new McpRequestState("continuation-value", 
"tools/call", 1, 123456789L);
+               var token = a.seal(state, AAD);
+               var parts = token.split("\\.", 4);
+               var swappedKeyId = 
Base64.getUrlEncoder().withoutPadding().encodeToString("2026-08-b".getBytes(StandardCharsets.UTF_8));
+               var tampered = parts[0] + "." + swappedKeyId + "." + parts[2] + 
"." + parts[3];
+               var c = a.unseal(tampered, AAD);
+               assertTrue(c.isEmpty(), "swapping the keyId wire segment 
selects the wrong key and AAD, so the GCM tag check must fail");
+       }
+
+       @Test void a12_unknownVersionSegmentFailsUnseal() {
+               var a = new AeadRequestStateCodec();
+               var state = new McpRequestState("continuation-value", 
"tools/call", 1, 123456789L);
+               var token = a.seal(state, AAD);
+               var parts = token.split("\\.", 4);
+               var tampered = "2." + parts[1] + "." + parts[2] + "." + 
parts[3];
+               var c = a.unseal(tampered, AAD);
+               assertTrue(c.isEmpty(), "an unrecognized version literal must 
fail closed");
+       }
+
+       @Test void a13_unknownKeyIdFailsUnseal() throws Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               var keyA = gen.generateKey();
+               var sealingProvider = StaticKeyProvider.of("2026-08-a", keyA);
+               var sealingCodec = new AeadRequestStateCodec(sealingProvider);
+               var state = new McpRequestState("continuation-value", 
"tools/call", 1, 123456789L);
+               var token = sealingCodec.seal(state, AAD);
+
+               var keyB = gen.generateKey();
+               var unsealingProvider = StaticKeyProvider.of("2026-08-b", 
keyB); // does not know "2026-08-a"
+               var unsealingCodec = new 
AeadRequestStateCodec(unsealingProvider);
+               var c = unsealingCodec.unseal(token, AAD);
+               assertTrue(c.isEmpty(), "a keyId the provider cannot resolve 
must fail closed");
+       }
+
        /**
         * Pins the continuation type-fidelity contract documented on {@link 
McpRequestState}: a {@code Map}-valued
         * continuation round-trips as generic JSON ({@link JsonMap}), never 
the original {@code Map} implementation.
@@ -162,4 +258,23 @@ class AeadRequestStateCodec_Test {
                assertEquals(2, recoveredMap.get("step"));
                assertEquals("resume", recoveredMap.get("note"));
        }
+
+       /**
+        * Pins the fix for the latent {@code MAX_KEY_ID_B64_CHARS} bug: a 
legitimately-issued keyId at
+        * {@code KeyedSecret}'s max length (128 UTF-16 code units), made 
entirely of non-ASCII, 3-byte-UTF-8
+        * characters, must still round-trip through {@code seal}/{@code 
unseal} rather than getting rejected by
+        * the keyId-segment length guard before {@code resolveKey} is ever 
consulted.
+        */
+       @Test void a14_maxLengthNonAsciiKeyIdRoundTrips() throws Exception {
+               var keyId = "\u4e2d".repeat(128);
+               var keyGen = KeyGenerator.getInstance("AES");
+               keyGen.init(256);
+               var provider = StaticKeyProvider.of(keyId, 
keyGen.generateKey());
+               var a = new AeadRequestStateCodec(provider);
+               var b = new McpRequestState("continuation-value", "tools/call", 
1, 123456789L);
+               var token = a.seal(b, AAD);
+               var c = a.unseal(token, AAD);
+               assertTrue(c.isPresent());
+               assertEquals(b, c.get());
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider_Test.java
new file mode 100644
index 0000000000..289bc68057
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/EphemeralKeyProvider_Test.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Coverage for {@link EphemeralKeyProvider}: self-resolve, foreign-keyId 
miss, and cross-instance distinctness
+ * (the provider-level counterpart of {@code AeadRequestStateCodec_Test}'s 
{@code a06}).
+ */
+class EphemeralKeyProvider_Test {
+
+       @Test void a01_resolveOwnKeyIdSucceeds() {
+               var a = new EphemeralKeyProvider();
+               var b = a.resolveKey(a.currentKey().keyId());
+               assertTrue(b.isPresent());
+               assertEquals(a.currentKey().key(), b.get());
+       }
+
+       @Test void a02_resolveForeignKeyIdReturnsEmpty() {
+               var a = new EphemeralKeyProvider();
+               assertTrue(a.resolveKey("not-my-key-id").isEmpty());
+       }
+
+       @Test void a03_twoInstancesHaveDistinctKeyIdsAndKeys() {
+               var a = new EphemeralKeyProvider();
+               var b = new EphemeralKeyProvider();
+               assertNotEquals(a.currentKey().keyId(), b.currentKey().keyId());
+               // Compare encoded key material directly rather than relying on 
SecretKey#equals(), which not every
+               // JCE provider's key implementation overrides meaningfully.
+               assertFalse(Arrays.equals(a.currentKey().key().getEncoded(), 
b.currentKey().key().getEncoded()));
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider_Test.java
new file mode 100644
index 0000000000..4e2b5ec6f4
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyProvider_Test.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Optional;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Interface-contract coverage for {@link KeyProvider}, proving the SPI shape 
itself is sound via a trivial fake
+ * implementation, before {@link EphemeralKeyProvider} / {@link 
StaticKeyProvider}'s real behavior exists.
+ */
+class KeyProvider_Test {
+
+       private static SecretKey aesKey() throws Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               return gen.generateKey();
+       }
+
+       private static final class FakeKeyProvider implements KeyProvider {
+
+               private final KeyedSecret current;
+
+               FakeKeyProvider(KeyedSecret current) {
+                       this.current = current;
+               }
+
+               @Override /* KeyProvider */
+               public KeyedSecret currentKey() {
+                       return current;
+               }
+
+               @Override /* KeyProvider */
+               public Optional<SecretKey> resolveKey(String keyId) {
+                       return keyId.equals(current.keyId()) ? 
Optional.of(current.key()) : Optional.empty();
+               }
+       }
+
+       @Test void a01_currentKeyIsNeverNull() throws Exception {
+               var a = new FakeKeyProvider(new KeyedSecret("k1", aesKey()));
+               assertNotNull(a.currentKey());
+       }
+
+       @Test void a02_resolveKeyMissReturnsEmptyAndNeverThrows() throws 
Exception {
+               var a = new FakeKeyProvider(new KeyedSecret("k1", aesKey()));
+               assertDoesNotThrow(() -> 
assertTrue(a.resolveKey("unknown-key-id").isEmpty()));
+       }
+
+       @Test void a03_resolveKeyHitReturnsKey() throws Exception {
+               var key = aesKey();
+               var a = new FakeKeyProvider(new KeyedSecret("k1", key));
+               var b = a.resolveKey("k1");
+               assertTrue(b.isPresent());
+               assertArrayEquals(key.getEncoded(), b.get().getEncoded());
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret_Test.java
new file mode 100644
index 0000000000..3a45838d32
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/KeyedSecret_Test.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Base64;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Coverage for {@link KeyedSecret}'s compact-constructor guards.
+ */
+class KeyedSecret_Test {
+
+       private static SecretKey aesKey() throws Exception {
+               var gen = KeyGenerator.getInstance("AES");
+               gen.init(256);
+               return gen.generateKey();
+       }
+
+       @Test void a01_validConstructionExposesKeyIdAndKey() throws Exception {
+               var a = aesKey();
+               var b = new KeyedSecret("2026-08-a", a);
+               assertEquals("2026-08-a", b.keyId());
+               assertEquals(a, b.key());
+       }
+
+       @Test void a02_nullKeyIdThrows() throws Exception {
+               var a = aesKey();
+               assertThrows(IllegalArgumentException.class, () -> new 
KeyedSecret(null, a));
+       }
+
+       @Test void a03_blankKeyIdThrows() throws Exception {
+               var a = aesKey();
+               assertThrows(IllegalArgumentException.class, () -> new 
KeyedSecret("   ", a));
+       }
+
+       @Test void a04_overLongKeyIdThrows() throws Exception {
+               var a = aesKey();
+               assertThrows(IllegalArgumentException.class, () -> new 
KeyedSecret("x".repeat(129), a));
+       }
+
+       @Test void a05_nullKeyThrows() {
+               assertThrows(IllegalArgumentException.class, () -> new 
KeyedSecret("2026-08-a", null));
+       }
+
+       @Test void a06_toStringRedactsKeyMaterial() {
+               var key = new SecretKeySpec(new byte[32], "AES");
+               var a = new KeyedSecret("my-key-id-sentinel", key);
+               var s = a.toString();
+               assertTrue(s.contains("my-key-id-sentinel"));
+               assertTrue(s.contains("redacted"));
+               
assertFalse(s.contains(Base64.getEncoder().encodeToString(key.getEncoded())));
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig_Test.java
index 7798a74578..b0f22e1c57 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrConfig_Test.java
@@ -72,6 +72,37 @@ class McpMrtrConfig_Test {
                assertEquals(3, a.getMaxRounds());
        }
 
+       @Test void a07_setKeyProviderWiresAeadCodecSharingTheProvider() {
+               // "behaves": prove the KeyProvider is actually threaded 
through, not just type-checked -- two
+               // independently-constructed McpMrtrConfig instances sharing 
one StaticKeyProvider unseal each other's
+               // tokens, the config-level analog of the dispatch-level d04 
test added in Task 11.
+               var provider = StaticKeyProvider.of("k1", 
StaticKeyProvider.aesKey(new byte[32]));
+               var a = new McpMrtrConfig().setKeyProvider(provider);
+               assertInstanceOf(AeadRequestStateCodec.class, a.getCodec());
+               var b = new McpMrtrConfig().setKeyProvider(provider);
+               var state = new McpRequestState("cont-1", "tools/call", 1, 
System.currentTimeMillis() + 60_000L);
+               var token = a.getCodec().seal(state, "aad");
+               var unsealed = b.getCodec().unseal(token, "aad").orElseThrow();
+               assertEquals("cont-1", unsealed.continuation());
+       }
+
+       @Test void a08_setKeyProviderAndSetCodecAreLastWins() {
+               var provider = StaticKeyProvider.of("k1", 
StaticKeyProvider.aesKey(new byte[32]));
+               var explicitCodec = new AeadRequestStateCodec();
+               // setCodec(...) called after setKeyProvider(...): the explicit 
codec wins.
+               var a = new 
McpMrtrConfig().setKeyProvider(provider).setCodec(explicitCodec);
+               assertSame(explicitCodec, a.getCodec());
+               // setKeyProvider(...) called after setCodec(...): the 
provider-wrapping codec wins.
+               var b = new 
McpMrtrConfig().setCodec(explicitCodec).setKeyProvider(provider);
+               assertNotSame(explicitCodec, b.getCodec());
+               assertInstanceOf(AeadRequestStateCodec.class, b.getCodec());
+       }
+
+       @Test void a09_setKeyProviderNullThrows() {
+               var e = assertThrows(IllegalArgumentException.class, () -> new 
McpMrtrConfig().setKeyProvider(null));
+               assertEquals("keyProvider must not be null", e.getMessage());
+       }
+
        // -------- McpRevision four-arg constructor wiring ---------
 
        private static Object validMeta() {
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrDispatch_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrDispatch_Test.java
index 41b72db577..ef2dc56236 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrDispatch_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrDispatch_Test.java
@@ -280,10 +280,11 @@ class McpMrtrDispatch_Test {
                var calls = new AtomicInteger();
                var config = new McpServerConfig().addTool(tool("ask", (args, 
c) -> { calls.incrementAndGet(); return text("done"); }));
                var token = codec.seal(new McpRequestState("cont-1", 
"tools/call", 1, System.currentTimeMillis() + 60_000L), aad("tools/call"));
-               var parts = token.split("\\.", 2);
-               var ciphertext = Base64.getUrlDecoder().decode(parts[1]);
+               var parts = token.split("\\.", 4);
+               var ciphertext = Base64.getUrlDecoder().decode(parts[3]);
                ciphertext[0] ^= 1;
-               var tampered = parts[0] + "." + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
+               var tampered = parts[0] + "." + parts[1] + "." + parts[2] + "."
+                       + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
                var params = JsonMap.of("name", "ask", "requestState", 
tampered);
                var resp = send(rev, config, req(1, "tools/call", params, 
true), hdrs("tools/call", "ask"));
                assertEquals(-32602, resp.getError().getCode());
@@ -491,4 +492,30 @@ class McpMrtrDispatch_Test {
                assertEquals(-32602, resp.getError().getCode());
                assertEquals(0, calls.get());
        }
+
+       @Test void 
d04_sharedStaticKeyProviderResumeSucceedsAcrossIndependentRevisions() {
+               // Direct contrast to d03: two independently-constructed 
McpRevision instances (via two independently-
+               // constructed McpMrtrConfig -> AeadRequestStateCodec 
instances) wired to the SAME StaticKeyProvider DO
+               // resume each other's tokens -- the horizontal-scaling proof 
design.md §2/§7 exists to enable.
+               var sharedKeyProvider = StaticKeyProvider.of("2026-08-d04", 
StaticKeyProvider.aesKey(new byte[32]));
+               var revA = revision(new 
McpMrtrConfig().setKeyProvider(sharedKeyProvider));
+               var revB = revision(new 
McpMrtrConfig().setKeyProvider(sharedKeyProvider));
+
+               // Pause dispatched through revision A: mints a requestState 
sealed by A's own, independently-constructed
+               // codec instance.
+               var pauseConfig = new McpServerConfig().addTool(tool("ask",
+                       (args, c) -> { throw new 
McpInputRequiredSignal(Map.of("q1", reqEntry("elicitation")), "cont-1"); }));
+               var paused = (InputRequiredResult) send(revA, pauseConfig, 
req(1, "tools/call", JsonMap.of("name", "ask"), true), hdrs("tools/call", 
"ask")).getResult();
+               var token = paused.getRequestState();
+               assertNotNull(token);
+
+               // Resume dispatched through revision B (a DIFFERENT 
McpRevision, DIFFERENT McpMrtrConfig, DIFFERENT
+               // AeadRequestStateCodec instance -- sharing only 
sharedKeyProvider) succeeds.
+               var resumeConfig = new McpServerConfig().addTool(tool("ask", 
(args, c) -> text("done")));
+               var params = JsonMap.of("name", "ask", "requestState", token, 
"inputResponses", JsonMap.of("q1", "answer"));
+               var resp = send(revB, resumeConfig, req(1, "tools/call", 
params, true), hdrs("tools/call", "ask"));
+               assertNull(resp.getError());
+               var result = (CallToolResult) resp.getResult();
+               assertEquals("done", ((TextContent) 
result.getContent().get(0)).getText());
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
index 5f55bb31e3..00d9ef492e 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
@@ -122,6 +122,12 @@ class McpOptions_Test {
                assertEquals("consumer must not be null", e.getMessage());
        }
 
+       @Test void 
c06_mrtrConsumer_setKeyProviderReachableThroughConfigureBlock() {
+               var provider = StaticKeyProvider.of("k1", 
StaticKeyProvider.aesKey(new byte[32]));
+               var o = new McpOptions().mrtr(m -> m.setKeyProvider(provider));
+               assertInstanceOf(AeadRequestStateCodec.class, 
o.getMrtr().getCodec());
+       }
+
        // -------- subscriptions: nested config, replace-outright, and 
Consumer configure-block ---------
 
        @Test void d01_subscriptions_defaultsToNonNullFrameworkOwnedInstance() {
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider_Test.java
new file mode 100644
index 0000000000..a0056db44d
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/StaticKeyProvider_Test.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+
+import javax.crypto.SecretKey;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Coverage for {@link StaticKeyProvider}: the {@code aesKey(...)} helpers, 
builder current-designation and
+ * multi-key resolve, unknown-keyId miss, {@code build()} guards, the {@code 
of(...)} one-liner, and
+ * post-{@code build()} immutability.
+ */
+class StaticKeyProvider_Test {
+
+       private static SecretKey randomAesKey() {
+               var b = new byte[32];
+               new SecureRandom().nextBytes(b);
+               return StaticKeyProvider.aesKey(b);
+       }
+
+       @Test void a01_aesKeyFromBytesProducesAesSecretKey() {
+               var a = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 
14, 15, 16};
+               var b = StaticKeyProvider.aesKey(a);
+               assertEquals("AES", b.getAlgorithm());
+               assertArrayEquals(a, b.getEncoded());
+       }
+
+       @Test void a02_aesKeyFromBase64ProducesAesSecretKey() {
+               var a = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 
14, 15, 16};
+               var b = 
StaticKeyProvider.aesKey(Base64.getEncoder().encodeToString(a));
+               assertEquals("AES", b.getAlgorithm());
+               assertArrayEquals(a, b.getEncoded());
+       }
+
+       @Test void b01_currentDesignationAndMultiKeyResolve() {
+               var a = randomAesKey();
+               var b = randomAesKey();
+               var c = StaticKeyProvider.create().addKey("2026-08-a", 
a).current("2026-08-a").addKey("2026-07-z", b).build();
+               assertEquals("2026-08-a", c.currentKey().keyId());
+               assertEquals(a, c.currentKey().key());
+               assertEquals(a, c.resolveKey("2026-08-a").orElseThrow());
+               assertEquals(b, c.resolveKey("2026-07-z").orElseThrow());
+       }
+
+       @Test void b02_unknownKeyIdResolvesToEmpty() {
+               var a = StaticKeyProvider.create().addKey("2026-08-a", 
randomAesKey()).current("2026-08-a").build();
+               assertTrue(a.resolveKey("unknown").isEmpty());
+       }
+
+       @Test void c01_buildRejectsMissingCurrent() {
+               assertThrows(IllegalArgumentException.class, () -> 
StaticKeyProvider.create().addKey("2026-08-a", randomAesKey()).build());
+       }
+
+       @Test void c02_buildRejectsDanglingCurrent() {
+               assertThrows(IllegalArgumentException.class, () -> 
StaticKeyProvider.create().addKey("2026-08-a", 
randomAesKey()).current("does-not-exist").build());
+       }
+
+       @Test void c03_addKeyRejectsNullOrBlankKeyId() {
+               var a = StaticKeyProvider.create();
+               var b = randomAesKey();
+               assertThrows(IllegalArgumentException.class, () -> 
a.addKey(null, b));
+               assertThrows(IllegalArgumentException.class, () -> a.addKey("  
", b));
+       }
+
+       @Test void c04_addKeyRejectsNullKey() {
+               var a = StaticKeyProvider.create();
+               assertThrows(IllegalArgumentException.class, () -> 
a.addKey("2026-08-a", null));
+       }
+
+       @Test void c05_currentRejectsNullOrBlankKeyId() {
+               var a = StaticKeyProvider.create();
+               assertThrows(IllegalArgumentException.class, () -> 
a.current(null));
+               assertThrows(IllegalArgumentException.class, () -> a.current("  
"));
+       }
+
+       @Test void d01_ofOneLinerBuildsSingleKeyProvider() {
+               var a = randomAesKey();
+               var b = StaticKeyProvider.of("2026-08-a", a);
+               assertEquals("2026-08-a", b.currentKey().keyId());
+               assertEquals(a, b.resolveKey("2026-08-a").orElseThrow());
+       }
+
+       @Test void e01_builderReuseAfterBuildDoesNotMutatePriorInstance() {
+               var a = randomAesKey();
+               var b = randomAesKey();
+               var builder = StaticKeyProvider.create().addKey("2026-08-a", 
a).current("2026-08-a");
+               var c = builder.build();
+               builder.addKey("2026-07-z", b);
+               assertTrue(c.resolveKey("2026-07-z").isEmpty(), "mutating the 
builder after build() must not affect the already-built instance");
+       }
+}

Reply via email to