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 995dc12937 MCP 2026-07-28 MRTR wire fixtures + end-to-end client 
integration (TODO-318 phases 4-6)
995dc12937 is described below

commit 995dc129379d8e82384f3fb0b966c11858922e89
Author: James Bognar <[email protected]>
AuthorDate: Mon Aug 3 09:45:16 2026 -0700

    MCP 2026-07-28 MRTR wire fixtures + end-to-end client integration (TODO-318 
phases 4-6)
    
    Completes the MRTR / input_required work for the 2026-07-28 revision.
    
    - Characterization wire fixtures pinning the input_required response, the
      resume request (inputResponses + echoed requestState), and the tamper /
      expired / max-rounds / unsupported-capability error shapes; mirrored in 
the
      integration-tests fixture tree, with a deterministic fixed-key GCM codec 
and
      requestState redaction so AEAD non-determinism doesn't break 
byte-comparison.
    - End-to-end pause->resume->complete loop exercised through the real 
McpClient
      against a live embedded-Jetty v2 server, plus capability-gate and tamper
      negative paths.
    - McpClient.callRaw(method, params): additive, result-type-agnostic dispatch
      that bypasses the typed response cache, letting a client receive
      input_required and re-issue with requestState/inputResponses.
    - Fixes a residual BasicBeanStore resource leak in McpRevision by deferring
      store construction until requestState validation succeeds, and clears lint
      (unused import; resolved TODO and empty default-case in McpClient).
    
    v2-only; neutral core and the 2025-06-18 revision are unchanged.
    
    Co-authored-by: Cursor <[email protected]>
---
 .../mcp/v20260728/McpMrtrIntegration_Test.java     | 201 +++++++++++++++++++++
 .../mcp/v20260728/Characterization_Test.java       | 169 ++++++++++++++++-
 .../MRTR-expired-request-state.headers.properties  |  17 ++
 .../MRTR-expired-request-state.request.json        |   1 +
 .../MRTR-expired-request-state.response.json       |   1 +
 ...MRTR-input-required-response.headers.properties |  17 ++
 .../MRTR-input-required-response.request.json      |   1 +
 .../MRTR-input-required-response.response.json     |   1 +
 .../MRTR-max-rounds-exceeded.headers.properties    |  17 ++
 .../MRTR-max-rounds-exceeded.request.json          |   1 +
 .../MRTR-max-rounds-exceeded.response.json         |   1 +
 .../MRTR-resume-complete.headers.properties        |  17 ++
 .../MRTR-resume-complete.request.json              |   1 +
 .../MRTR-resume-complete.response.json             |   1 +
 ...-resume-input-required-again.headers.properties |  17 ++
 .../MRTR-resume-input-required-again.request.json  |   1 +
 .../MRTR-resume-input-required-again.response.json |   1 +
 .../MRTR-tampered-request-state.headers.properties |  17 ++
 .../MRTR-tampered-request-state.request.json       |   1 +
 .../MRTR-tampered-request-state.response.json      |   1 +
 .../MRTR-unsupported-capability.headers.properties |  17 ++
 .../MRTR-unsupported-capability.request.json       |   1 +
 .../MRTR-unsupported-capability.response.json      |   1 +
 .../rest/client/mcp/v20260728/McpClient.java       |  59 +++++-
 .../mcp/v20260728/McpClient_CallRaw_Test.java      | 131 ++++++++++++++
 .../rest/server/mcp/v20260728/McpRevision.java     |  16 +-
 .../mcp/v20260728/Characterization_Test.java       | 169 ++++++++++++++++-
 .../MRTR-expired-request-state.headers.properties  |  17 ++
 .../MRTR-expired-request-state.request.json        |   1 +
 .../MRTR-expired-request-state.response.json       |   1 +
 ...MRTR-input-required-response.headers.properties |  17 ++
 .../MRTR-input-required-response.request.json      |   1 +
 .../MRTR-input-required-response.response.json     |   1 +
 .../MRTR-max-rounds-exceeded.headers.properties    |  17 ++
 .../MRTR-max-rounds-exceeded.request.json          |   1 +
 .../MRTR-max-rounds-exceeded.response.json         |   1 +
 .../MRTR-resume-complete.headers.properties        |  17 ++
 .../MRTR-resume-complete.request.json              |   1 +
 .../MRTR-resume-complete.response.json             |   1 +
 ...-resume-input-required-again.headers.properties |  17 ++
 .../MRTR-resume-input-required-again.request.json  |   1 +
 .../MRTR-resume-input-required-again.response.json |   1 +
 .../MRTR-tampered-request-state.headers.properties |  17 ++
 .../MRTR-tampered-request-state.request.json       |   1 +
 .../MRTR-tampered-request-state.response.json      |   1 +
 .../MRTR-unsupported-capability.headers.properties |  17 ++
 .../MRTR-unsupported-capability.request.json       |   1 +
 .../MRTR-unsupported-capability.response.json      |   1 +
 48 files changed, 997 insertions(+), 14 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
new file mode 100644
index 0000000000..8192e2d0d9
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
@@ -0,0 +1,201 @@
+/*
+ * 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.client.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.bean.jsonrpc.*;
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.marshaller.Json;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+
+import jakarta.servlet.*;
+
+/**
+ * End-to-end coverage for the {@code 2026-07-28} Multi-Round-Trip Request 
(SEP-2322 {@code input_required}) loop,
+ * driven through the first-party {@link McpClient} against a live 
embedded-Jetty v2
+ * {@link org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet} fixture 
&mdash; proving PAUSE&rarr;RESUME&rarr;
+ * complete works across a real client/server wire boundary, not just the 
in-process dispatcher assertions the
+ * module-local {@code McpMrtrDispatch_Test} already covers.
+ *
+ * <p>
+ * Placed in the {@code rest.client.mcp.v20260728} package (beside {@link 
McpClientV20260728_Integration_Test},
+ * the canonical real-client/real-server fixture pattern) rather than the 
server package, because Task 15 mandates
+ * driving the loop through the real client stack; the server-package 
integration tests use raw {@code MockRestClient}
+ * bodies instead.
+ *
+ * <p>
+ * <b>Client resume path:</b> both the pausing and the resuming calls go 
through {@link McpClient#callRaw} &mdash;
+ * the typed {@code callTool} overload can neither represent an {@code 
input_required} result nor set
+ * {@code requestState}/{@code inputResponses} on the follow-up request. 
Ergonomic client-side MRTR-resume support
+ * (auto-detect + re-issue) is intentionally out of scope and remains a 
separate follow-up; here the test drives
+ * the loop explicitly, exactly as the plan's Task 15 scopes it.
+ */
[email protected]
+class McpMrtrIntegration_Test extends TestBase {
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Fixture server: a v2 MCP servlet whose tools pause via 
McpInputRequiredSignal and resume via the injected
+       // McpMrtrResumeContext. Holds the exact AeadRequestStateCodec instance 
the dispatcher seals with, so a02 can
+       // unseal the emitted requestState without re-deriving a key (the 
ephemeral AES key lives inside this instance).
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       static final AeadRequestStateCodec CODEC = new AeadRequestStateCodec();
+
+       private static String aad(String method) {
+               return method + '\u0000' + McpProtocol.VERSION_2026_07_28;
+       }
+
+       private static McpToolHandler ask() {
+               return new McpToolHandler() {
+                       @Override public McpToolSpec descriptor() { return new 
McpToolSpec().setName("ask").setDescription("Pauses once, 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-1");
+                               return McpToolOutcome.text("resumed:" + 
resume.get().inputResponses().get("q1"));
+                       }
+               };
+       }
+
+       private static McpToolHandler askTwice() {
+               return new McpToolHandler() {
+                       @Override public McpToolSpec descriptor() { return new 
McpToolSpec().setName("askTwice").setDescription("Pauses twice"); }
+                       @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-1");
+                               if 
("cont-1".equals(resume.get().continuation()))
+                                       throw new 
McpInputRequiredSignal(Map.of("q2", Map.of("type", "elicitation")), "cont-2");
+                               return McpToolOutcome.text("resumed2:" + 
resume.get().inputResponses().get("q2"));
+                       }
+               };
+       }
+
+       @Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, 
defaultAccept = "application/json")
+       public static class Fixture 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-fixture").setVersion("1.0.0").addTool(ask()).addTool(askTwice());
+               }
+
+               @Override
+               protected McpMrtrConfig createMrtrConfig() {
+                       return new McpMrtrConfig().setCodec(CODEC);
+               }
+       }
+
+       @Configuration
+       public static class FixtureConfig {
+               @Bean public Servlet mcpServlet() { return new Fixture(); }
+       }
+
+       @RegisterExtension
+       static MicroserviceTestFixture fixture = 
MicroserviceTestFixture.create()
+               .configurations(FixtureConfig.class);
+
+       private static McpClient.Builder clientBuilder(boolean withElicitation) 
{
+               var caps = new ClientCapabilities();
+               if (withElicitation)
+                       caps.setElicitation(new ElicitationCapability());
+               return McpClient.builder()
+                       .endpoint(fixture.getRootUrl() + "/")
+                       .clientCapabilities(caps);
+       }
+
+       // 
=================================================================================================================
+       // A: full PAUSE -> RESUME -> complete loop across the real wire.
+       // 
=================================================================================================================
+
+       @Test void a01_pauseResumeComplete_fullLoop() throws Exception {
+               try (var client = clientBuilder(true).build()) {
+                       // First call pauses: expect an input_required result 
carrying a requestState and one input request.
+                       var paused = client.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("ask").setArguments(Map.of()));
+                       assertEquals("input_required", 
paused.get("resultType"));
+                       var token = (String) paused.get("requestState");
+                       assertNotNull(token);
+                       assertTrue(((Map<?,?>) 
paused.get("inputRequests")).containsKey("q1"));
+
+                       // Resume with the echoed requestState + the client's 
answers: expect completion echoing the answer.
+                       var completed = client.callRaw(McpMethods.TOOLS_CALL,
+                               new 
CallToolRequest().setName("ask").setRequestState(token).setInputResponses(Map.of("q1",
 "answer")));
+                       assertEquals("complete", completed.get("resultType"));
+                       var result = Json.to(Json.of(completed), 
CallToolResult.class);
+                       assertEquals("resumed:answer", ((TextContent) 
result.getContent().get(0)).getText());
+               }
+       }
+
+       @Test void a02_pauseResumeToPauseAgain_secondRoundIncrementsCount() 
throws Exception {
+               try (var client = clientBuilder(true).build()) {
+                       var paused1 = client.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("askTwice").setArguments(Map.of()));
+                       assertEquals("input_required", 
paused1.get("resultType"));
+                       var token1 = (String) paused1.get("requestState");
+
+                       // Resume once; the handler pauses again -> a second 
input_required with an incremented round counter.
+                       var paused2 = client.callRaw(McpMethods.TOOLS_CALL,
+                               new 
CallToolRequest().setName("askTwice").setRequestState(token1).setInputResponses(Map.of("q1",
 "a1")));
+                       assertEquals("input_required", 
paused2.get("resultType"));
+                       var token2 = (String) paused2.get("requestState");
+
+                       // Unseal via the same codec instance the server used 
(round counter lives inside the sealed token).
+                       var state = CODEC.unseal(token2, 
aad(McpMethods.TOOLS_CALL)).orElseThrow();
+                       assertEquals(2, state.round());
+                       assertEquals("cont-2", state.continuation());
+               }
+       }
+
+       // 
=================================================================================================================
+       // B: capability gate + negative path, enforced at the real HTTP 
boundary.
+       // 
=================================================================================================================
+
+       @Test void b01_unsupportedCapability_rejectedOverTheWire() throws 
Exception {
+               try (var client = clientBuilder(false).build()) {
+                       var e = assertThrows(McpException.class,
+                               () -> client.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("ask").setArguments(Map.of())));
+                       
assertEquals(org.apache.juneau.rest.server.mcp.v20260728.McpRevision.CODE_MISSING_REQUIRED_CLIENT_CAPABILITY,
 e.getCode());
+               }
+       }
+
+       @Test void b02_tamperedRequestState_rejectedOverTheWire() throws 
Exception {
+               try (var client = clientBuilder(true).build()) {
+                       var paused = client.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("ask").setArguments(Map.of()));
+                       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]);
+                       ciphertext[0] ^= 1;
+                       var tampered = parts[0] + "." + 
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());
+               }
+       }
+}
diff --git 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
index e2a3b4b9d2..77926b59de 100644
--- 
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
+++ 
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
@@ -19,19 +19,25 @@ package org.apache.juneau.rest.server.mcp.v20260728;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.*;
+import java.nio.charset.*;
 import java.nio.file.*;
 import java.util.*;
 import java.util.function.*;
 
+import javax.crypto.*;
+import javax.crypto.spec.*;
+
 import org.apache.juneau.bean.mcp.v20260728.*;
 import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.http.tracing.TraceContextCarrier;
 import org.apache.juneau.marshall.collections.*;
 import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.marshaller.Json;
 import org.apache.juneau.rest.mock.classic.*;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.mcp.*;
 import org.apache.juneau.rest.server.tracing.*;
+import org.junit.jupiter.api.*;
 import org.junit.jupiter.params.*;
 import org.junit.jupiter.params.provider.*;
 
@@ -271,6 +277,84 @@ class Characterization_Test {
                }
        }
 
+       /**
+        * Backs every {@code MRTR-*} fixture (SEP-2322 Multi-Round-Trip 
Requests): one {@code ask} tool that pauses
+        * (throws {@link McpInputRequiredSignal}) on a first-round call, and 
on RESUME either completes (echoing the
+        * client's {@code inputResponses}) or pauses a second time &mdash; 
branched on the decoded continuation so a
+        * single handler drives the complete / pause-again / max-rounds 
fixtures.
+        *
+        * <p>
+        * Wires a <b>deterministic, fixed-key</b> {@link FixedKeyGcmCodec} 
instead of the production ephemeral-key
+        * {@link AeadRequestStateCodec}, so a {@code requestState} the harness 
seals for a RESUME fixture (see
+        * {@link #mrtrToken(String)}) is unsealable by this servlet across a 
fresh process &mdash; a property the
+        * random-per-process production key deliberately does <b>not</b> have. 
The opaque token minted <em>into</em> a
+        * PAUSE response still embeds a wall-clock expiry, so responses are 
compared with the token value redacted (see
+        * {@link #normalizeRequestState(String)}); the fixtures pin the wire 
<em>shape</em>, never the opaque bytes.
+        */
+       @Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, 
defaultAccept = "application/json")
+       public static class F_Mrtr extends McpRestServlet {
+               private static final long serialVersionUID = 1L;
+               @Override protected McpServerConfig createMcpConfig() {
+                       return new McpServerConfig().addTool(new 
McpToolHandler() {
+                               @Override public McpToolSpec descriptor() { 
return new McpToolSpec().setName("ask").setDescription("desc:ask"); }
+                               @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-1");
+                                       if 
("pause-again".equals(resume.get().continuation()))
+                                               throw new 
McpInputRequiredSignal(Map.of("q2", Map.of("type", "elicitation")), "cont-2");
+                                       return McpToolOutcome.text("resumed:" + 
resume.get().inputResponses().get("q1"));
+                               }
+                       });
+               }
+               @Override protected McpMrtrConfig createMrtrConfig() {
+                       return new McpMrtrConfig().setCodec(new 
FixedKeyGcmCodec());
+               }
+       }
+
+       /**
+        * Test-only deterministic {@link RequestStateCodec}: AES-256-GCM with 
a <b>hardcoded</b> key and nonce.
+        *
+        * <p>
+        * Unlike the production {@link AeadRequestStateCodec} (random 
per-process key + random per-seal nonce), this
+        * codec produces identical ciphertext for identical {@code (state, 
aad)} inputs and can unseal a token sealed by
+        * any other instance &mdash; exactly the reproducibility a committed 
characterization fixture needs. It is still
+        * genuinely tamper-evident (the GCM auth tag rejects the {@code 
MRTR-tampered-request-state} corruption) and
+        * honors the AAD binding. Fixed-nonce AES-GCM is catastrophically 
insecure for real traffic; it is used here
+        * <em>solely</em> because determinism, not confidentiality, is the 
goal in a fixture.
+        */
+       static final class FixedKeyGcmCodec implements RequestStateCodec {
+               private static final SecretKey KEY = new SecretKeySpec(new 
byte[32], "AES");
+               private static final byte[] NONCE = new byte[12];
+               private static final Base64.Encoder B64 = 
Base64.getUrlEncoder().withoutPadding();
+
+               @Override public String seal(McpRequestState state, String aad) 
{
+                       try {
+                               var cipher = 
Cipher.getInstance("AES/GCM/NoPadding");
+                               cipher.init(Cipher.ENCRYPT_MODE, KEY, new 
GCMParameterSpec(128, NONCE));
+                               
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                               var ciphertext = 
cipher.doFinal(Json.of(state).getBytes(StandardCharsets.UTF_8));
+                               return B64.encodeToString(NONCE) + "." + 
B64.encodeToString(ciphertext);
+                       } catch (Exception e) {
+                               throw new RuntimeException("Fixture codec seal 
failed", e);
+                       }
+               }
+
+               @Override public Optional<McpRequestState> unseal(String token, 
String aad) {
+                       try {
+                               var parts = token.split("\\.", 2);
+                               var nonce = 
Base64.getUrlDecoder().decode(parts[0]);
+                               var ciphertext = 
Base64.getUrlDecoder().decode(parts[1]);
+                               var cipher = 
Cipher.getInstance("AES/GCM/NoPadding");
+                               cipher.init(Cipher.DECRYPT_MODE, KEY, new 
GCMParameterSpec(128, nonce));
+                               
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                               return Optional.of(Json.to(new 
String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8), 
McpRequestState.class));
+                       } catch (@SuppressWarnings("unused") Exception e) {
+                               return Optional.empty();
+                       }
+               }
+       }
+
        // --- fixture handler factories 
---------------------------------------------------------
 
        private static McpToolHandler tool(String name, McpSchema schema, 
Function<Map<String,Object>,McpToolOutcome> fn) {
@@ -329,10 +413,74 @@ class Characterization_Test {
                                "COMPLETE-empty-unknown", "COMPLETE-capped" -> 
F_Complete.class;
                        case "TRACE-meta-parent", "TRACE-http-fallback", 
"TRACE-meta-wins",
                                "TRACE-tracestate-baggage", 
"TRACE-response-echo", "TRACE-jsonrpc-error" -> F_Traced.class;
+                       case "MRTR-input-required-response", 
"MRTR-resume-complete", "MRTR-resume-input-required-again",
+                               "MRTR-tampered-request-state", 
"MRTR-expired-request-state", "MRTR-unsupported-capability",
+                               "MRTR-max-rounds-exceeded" -> F_Mrtr.class;
                        default -> F_Empty.class;
                };
        }
 
+       // --- MRTR requestState sealing/redaction 
------------------------------------------------
+
+       /** Placeholder in an {@code MRTR-*} resume fixture's committed request 
body, replaced at replay time. */
+       private static final String TOKEN_PLACEHOLDER = "__REQUEST_STATE__";
+
+       // Fixed expiry constants embedded in a sealed requestState so a RESUME 
fixture's token is reproducible
+       // (never "now + ttl"): far-future for a still-valid token, a fixed 
past instant for the expired fixture.
+       private static final long FAR_FUTURE_MS = 32503680000000L; // ~ year 
3000
+       private static final long PAST_MS = 1000L;
+
+       private static String aad(String method) {
+               return method + '\u0000' + McpProtocol.VERSION_2026_07_28;
+       }
+
+       /**
+        * The {@code requestState} the harness seals (with {@link 
FixedKeyGcmCodec}, matching {@link F_Mrtr}'s codec)
+        * for each RESUME-family fixture. Substituted into the committed 
request in place of {@link #TOKEN_PLACEHOLDER}
+        * so the token stays framework-owned/opaque while the request pins the 
surrounding wire shape.
+        *
+        * <p>
+        * {@code MRTR-resume-complete} and {@code 
MRTR-resume-input-required-again} have byte-identical committed
+        * request files by design: the behavioral difference between the two 
fixtures lives entirely inside the sealed
+        * opaque token's continuation value ({@code "complete-me"} vs {@code 
"pause-again"}), not in anything visible
+        * on the wire request body.
+        *
+        * <p>
+        * The plan's original "commit the corrupted requestState as a frozen 
literal" constraint for
+        * {@code MRTR-tampered-request-state} was consciously superseded: 
because {@link FixedKeyGcmCodec} makes
+        * {@code seal(...)} fully deterministic (fixed key, fixed nonce), 
regenerating the tampered token on every run
+        * via {@code tamper(codec.seal(...))} is equally reproducible and 
avoids hand-maintaining opaque ciphertext.
+        */
+       private static String mrtrToken(String fixture) {
+               var codec = new FixedKeyGcmCodec();
+               return switch (fixture) {
+                       case "MRTR-resume-complete" -> codec.seal(new 
McpRequestState("complete-me", "tools/call", 1, FAR_FUTURE_MS), 
aad("tools/call"));
+                       case "MRTR-resume-input-required-again" -> 
codec.seal(new McpRequestState("pause-again", "tools/call", 1, FAR_FUTURE_MS), 
aad("tools/call"));
+                       case "MRTR-expired-request-state" -> codec.seal(new 
McpRequestState("cont-1", "tools/call", 1, PAST_MS), aad("tools/call"));
+                       case "MRTR-max-rounds-exceeded" -> codec.seal(new 
McpRequestState("cont-1", "tools/call", McpMrtrConfig.DEFAULT_MAX_ROUNDS, 
FAR_FUTURE_MS), aad("tools/call"));
+                       case "MRTR-tampered-request-state" -> 
tamper(codec.seal(new McpRequestState("cont-1", "tools/call", 1, 
FAR_FUTURE_MS), aad("tools/call")));
+                       default -> throw new IllegalArgumentException("No MRTR 
token mapping for fixture: " + fixture);
+               };
+       }
+
+       /** Flips one ciphertext byte of a valid token so its GCM tag fails 
verification (the tamper fixture). */
+       private static String tamper(String token) {
+               var parts = token.split("\\.", 2);
+               var ciphertext = Base64.getUrlDecoder().decode(parts[1]);
+               ciphertext[0] ^= 1;
+               return parts[0] + "." + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
+       }
+
+       /**
+        * Redacts the opaque {@code requestState} token value in a response 
body to a fixed placeholder before
+        * comparison. The token is per-process-nondeterministic ciphertext 
(production uses a random key and embeds a
+        * wall-clock expiry), so pinning its exact bytes is neither possible 
nor meaningful; the fixture pins its
+        * presence and the surrounding wire shape instead.
+        */
+       private static String normalizeRequestState(String responseBody) {
+               return responseBody.replaceAll("\"requestState\":\"[^\"]+\"", 
"\"requestState\":\"<sealed>\"");
+       }
+
        // --- replay 
----------------------------------------------------------------------------
 
        static List<String> fixtures() throws Exception {
@@ -348,7 +496,7 @@ class Characterization_Test {
        @ParameterizedTest
        @MethodSource("fixtures")
        void a01_wireIsUnchanged(String fixture) throws Exception {
-               var actual = replayHttp(fixture);
+               var actual = normalizeRequestState(replayHttp(fixture));
                var expected = DIR.resolve(fixture + ".response.json");
                if (WRITE) {
                        Files.writeString(expected, actual);
@@ -358,8 +506,27 @@ class Characterization_Test {
                        () -> fixture + ": WIRE FORMAT CHANGED. Do not update 
the fixture — fix the code.");
        }
 
+       /**
+        * The opaque {@code requestState} minted into {@code 
MRTR-resume-input-required-again}'s second PAUSE is
+        * redacted for {@code a01}'s byte comparison, so its embedded round 
counter is verified here instead: unseal
+        * the live token with the same fixed-key codec {@link F_Mrtr} uses and 
assert the round advanced to 2 (plan
+        * Phase 4 Task 12 decode-and-check).
+        */
+       @Test
+       void a02_resumeInputRequiredAgain_sealedRoundIsTwo() throws Exception {
+               var raw = replayHttp("MRTR-resume-input-required-again");
+               var envelope = Json.to(raw, JsonMap.class);
+               var result = (Map<?,?>) envelope.get("result");
+               var token = (String) result.get("requestState");
+               var state = new FixedKeyGcmCodec().unseal(token, 
aad("tools/call")).orElseThrow();
+               assertEquals(2, state.round());
+               assertEquals("cont-2", state.continuation());
+       }
+
        private String replayHttp(String fixture) throws Exception {
                var requestBody = Files.readString(DIR.resolve(fixture + 
".request.json")).strip();
+               if (requestBody.contains(TOKEN_PLACEHOLDER))
+                       requestBody = requestBody.replace(TOKEN_PLACEHOLDER, 
mrtrToken(fixture));
                var headers = loadHeaders(fixture);
                var client = MockRestClient.create(servletFor(fixture)).json()
                        
.contentType("application/json").accept("application/json").ignoreErrors().build();
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.response.json
new file mode 100644
index 0000000000..a1727204b3
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-expired-request-state.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32022,"message":"requestState has 
expired"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.request.json
new file mode 100644
index 0000000000..0e2aa4847b
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.response.json
new file mode 100644
index 0000000000..6f7cc8908b
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-input-required-response.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"inputRequests":{"q1":{"type":"elicitation"}},"requestState":"<sealed>","resultType":"input_required"}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.response.json
new file mode 100644
index 0000000000..b3eef6825e
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-max-rounds-exceeded.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32023,"message":"Max MRTR rounds 
exceeded"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.request.json
new file mode 100644
index 0000000000..2e2ecabcba
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","inputResponses":{"q1":"answer"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.response.json
new file mode 100644
index 0000000000..763cc3f7cc
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-complete.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"content":[{"type":"text","text":"resumed:answer"}],"resultType":"complete"}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.request.json
new file mode 100644
index 0000000000..2e2ecabcba
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","inputResponses":{"q1":"answer"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.response.json
new file mode 100644
index 0000000000..0980582389
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-resume-input-required-again.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"inputRequests":{"q2":{"type":"elicitation"}},"requestState":"<sealed>","resultType":"input_required"}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.response.json
new file mode 100644
index 0000000000..1157a8dfb7
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-tampered-request-state.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32602,"message":"Invalid or tampered 
requestState"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.headers.properties
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.request.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.request.json
new file mode 100644
index 0000000000..1c5c5da477
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}
\ No newline at end of file
diff --git 
a/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.response.json
 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.response.json
new file mode 100644
index 0000000000..56242140a5
--- /dev/null
+++ 
b/juneau-integration-tests/src/test/resources/mcp/v20260728/characterization/MRTR-unsupported-capability.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32021,"message":"Client does not advertise the elicitation 
capability required for input_required"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
index bd750f9547..5c79a3619c 100644
--- 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
+++ 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
@@ -222,6 +222,34 @@ public final class McpClient extends AbstractMcpClient {
                return call(McpMethods.COMPLETION_COMPLETE, params, 
CompleteResult.class);
        }
 
+       /**
+        * Performs a JSON-RPC call using a caller-supplied, fully-populated 
request bean, returning the raw result
+        * payload without deserializing into a specific {@link Result} subtype.
+        *
+        * <p>
+        * Every convenience method on this class ({@link #callTool}, {@link 
#getPrompt}, {@link #readResource}, etc.)
+        * both builds its own request bean internally and deserializes 
strictly into one known result type, so none of
+        * them can represent a paused {@code input_required} response (MCP 
{@code 2026-07-28} SEP-2322 Multi-Round-Trip
+        * Requests) or carry the {@code requestState}/{@code inputResponses} 
fields a resume call needs to set. This
+        * method exists specifically for MRTR-aware callers that must both 
populate those fields on the request (e.g.
+        * on a {@link CallToolRequest}) and branch on the response's
+        * {@code resultType} themselves, decoding into a typed result only 
once {@code resultType} reads
+        * {@code "complete"}.
+        *
+        * @param method The JSON-RPC method (e.g. {@link 
McpMethods#TOOLS_CALL}).
+        * @param params The fully-populated request bean.
+        * @return The raw result as a generic {@link Map}, exactly as received 
over the wire, before any
+        *      subclass-specific deserialization. Never <jk>null</jk> on a 
successful JSON-RPC response.
+        * @throws IOException On a transport failure.
+        * @throws McpException If the server returns a JSON-RPC error.
+        */
+       @SuppressWarnings({
+               "unchecked" // Map.class.cast(...) below always yields a raw 
Map from JSON deserialization.
+       })
+       public Map<String,Object> callRaw(String method, RequestParams<?> 
params) throws IOException {
+               return call(method, params, Map.class, false);
+       }
+
        /**
         * Returns this instance's cache-partition prefix used to namespace 
{@link McpResponseCache#SCOPE_PRIVATE}
         * entries so two client instances sharing the same {@link 
McpResponseCache} never see each other's
@@ -275,19 +303,36 @@ public final class McpClient extends AbstractMcpClient {
                }
        }
 
-       // TODO C5/C6: finalize duplex return-channel header contract. Mcp-Name 
is always empty here because
-       // DUPLEX_RETURN_METHOD has no routing-name mapping in McpRoutingNames; 
it is unclear whether the real
-       // contract instead wants it to echo the correlated inbound request's 
tool/prompt/resource name.
+       // C5/C6 duplex return-channel header contract remains unsettled: 
Mcp-Name is always empty here because
+       // DUPLEX_RETURN_METHOD has no routing-name mapping in McpRoutingNames. 
It is unclear whether the real
+       // contract instead wants it to echo the correlated inbound request's 
tool/prompt/resource name; that
+       // requires spec clarification and is deliberately left open rather 
than guessed at here.
        private void postClientResult(Object id, JsonRpcResponse payload) 
throws IOException {
                var headers = Map.of("Mcp-Method", DUPLEX_RETURN_METHOD, 
"Mcp-Name", McpRoutingNames.routingName(DUPLEX_RETURN_METHOD, payload));
                send(new 
JsonRpcRequest().setJsonrpc("2.0").setId(id).setMethod(DUPLEX_RETURN_METHOD).setParams(payload),
 headers);
        }
 
        private <T> T call(String method, RequestParams<?> params, Class<T> 
resultType) throws IOException {
+               return call(method, params, resultType, true);
+       }
+
+       /**
+        * Shared wire implementation for every typed method and {@link 
#callRaw}.
+        *
+        * <p>
+        * {@code useCache=false} (used only by {@link #callRaw}) skips the 
cache READ so a {@code callRaw} call never
+        * retrieves a differently-typed cached result (e.g. a {@link 
CallToolResult} primed by {@link #callTool}) under
+        * the same cache key, which would otherwise throw a {@link 
ClassCastException} on the subsequent {@code cast}.
+        * The cache WRITE at the end is unaffected by this flag: it is already 
a no-op for {@code callRaw} because a
+        * plain {@link Map} result is never a {@link CacheableResult}.
+        */
+       private <T> T call(String method, RequestParams<?> params, Class<T> 
resultType, boolean useCache) throws IOException {
                var cacheKey = cacheKey(method, params);
-               var cached = readCache(cacheKey);
-               if (cached != null)
-                       return resultType.cast(cached);
+               if (useCache) {
+                       var cached = readCache(cacheKey);
+                       if (cached != null)
+                               return resultType.cast(cached);
+               }
                stampMeta(params.getMeta() == null ? params.setMeta(new 
RequestMeta()).getMeta() : params.getMeta());
                var wireParams = toWireParams(params);
                var headers = Map.of("Mcp-Method", method, "Mcp-Name", 
McpRoutingNames.routingName(method, wireParams));
@@ -377,7 +422,7 @@ public final class McpClient extends AbstractMcpClient {
                                case RequestMeta.KEY_TRACEPARENT -> 
meta.setTraceparent(value);
                                case RequestMeta.KEY_TRACESTATE -> 
meta.setTracestate(value);
                                case RequestMeta.KEY_BAGGAGE -> 
meta.setBaggage(value);
-                               default -> { }
+                               default -> { /* Unknown trace-context key: no 
corresponding RequestMeta field to set. */ }
                        }
                }
        }
diff --git 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_CallRaw_Test.java
 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_CallRaw_Test.java
new file mode 100644
index 0000000000..c08487af2f
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_CallRaw_Test.java
@@ -0,0 +1,131 @@
+/*
+ * 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.client.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.bean.jsonrpc.*;
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.rest.client.*;
+import org.apache.juneau.rest.client.mcp.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Coverage for {@link McpClient#callRaw(String, RequestParams)}: the 
result-type-agnostic dispatch a
+ * Multi-Round-Trip-Request (SEP-2322) caller needs to receive an {@code 
input_required} response and echo its
+ * {@code requestState}/{@code inputResponses} on a resume call (plan Phase 5, 
Task 14).
+ */
+class McpClient_CallRaw_Test {
+
+       private static HttpTransport ok(String wireJson) {
+               return tReq -> TransportResponse.builder()
+                       .statusCode(200)
+                       .header("Content-Type", "application/json")
+                       .body(new 
ByteArrayInputStream(wireJson.getBytes(StandardCharsets.UTF_8)))
+                       .build();
+       }
+
+       private static McpClient client(HttpTransport transport) {
+               return 
McpClient.builder().endpoint("http://x/mcp";).transport(transport).build();
+       }
+
+       @Test
+       void a01_callRaw_completeResult_returnsRawMapMatchingTypedCall() throws 
Exception {
+               var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"resultType\":\"complete\"}}";
+               try (var c = client(ok(wire))) {
+                       var raw = c.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("echo").setArguments(Map.of("text", "hello")));
+                       assertEquals("complete", raw.get("resultType"));
+                       assertNotNull(raw.get("content"));
+               }
+               // The same wire, consumed through the typed method, yields a 
CallToolResult with matching content --
+               // proving callRaw returns the identical underlying JSON, only 
undiscriminated.
+               try (var c = client(ok(wire))) {
+                       var typed = c.callTool("echo", Map.of("text", "hello"));
+                       assertEquals("hello", ((TextContent) 
typed.getContent().get(0)).getText());
+               }
+       }
+
+       @Test
+       void 
a02_callRaw_inputRequiredResult_exposesRequestStateAndInputRequests() throws 
Exception {
+               var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"inputRequests\":{\"q1\":{\"type\":\"elicitation\"}},\"requestState\":\"tok-abc\",\"resultType\":\"input_required\"}}";
+               try (var c = client(ok(wire))) {
+                       var raw = c.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("ask"));
+                       assertEquals("input_required", raw.get("resultType"));
+                       assertEquals("tok-abc", raw.get("requestState"));
+                       assertInstanceOf(Map.class, raw.get("inputRequests"));
+                       assertTrue(((Map<?,?>) 
raw.get("inputRequests")).containsKey("q1"));
+               }
+       }
+
+       @Test
+       void a03_callRaw_jsonRpcError_throwsMcpExceptionLikeEveryTypedMethod() 
throws Exception {
+               var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"error\":{\"code\":-32602,\"message\":\"bad 
params\"}}";
+               try (var c = client(ok(wire))) {
+                       var e = assertThrows(McpException.class,
+                               () -> c.callRaw(McpMethods.TOOLS_CALL, new 
CallToolRequest().setName("ask").setRequestState("stale")));
+                       assertEquals(-32602, e.getCode());
+               }
+       }
+
+       /**
+        * Reproduces the mixed-API cache-key collision: a typed call caches a 
non-{@link Map} result bean under a key
+        * that {@code callRaw} would compute identically for the same 
method+params. {@code CallToolResult} (the
+        * {@code tools/call} result type) is never actually written to the 
cache because it doesn't implement
+        * {@link CacheableResult}, so {@code resources/read} (whose
+        * {@link ReadResourceResult} does implement it) is the type that is 
genuinely reachable end-to-end: prime the
+        * cache via the typed {@link McpClient#readResource} call, then issue 
{@link McpClient#callRaw} for the same
+        * {@code resources/read} URI. Before the fix, {@code callRaw}'s cache 
READ would return the cached
+        * {@code ReadResourceResult} and {@code Map.class.cast(...)} it, 
throwing {@link ClassCastException}.
+        */
+       @Test
+       void a04_callRaw_doesNotReadTypedResultFromSharedCacheKey() throws 
Exception {
+               var calls = new AtomicInteger();
+               HttpTransport transport = tReq -> {
+                       var n = calls.incrementAndGet();
+                       var wire = n == 1
+                               ? 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"contents\":[{\"type\":\"resourceText\",\"uri\":\"file:///a\",\"text\":\"cached-body\"}],\"ttlMs\":60000}}"
+                               : 
"{\"jsonrpc\":\"2.0\",\"id\":\"2\",\"result\":{\"contents\":[{\"type\":\"resourceText\",\"uri\":\"file:///a\",\"text\":\"wire-body\"}]}}";
+                       return TransportResponse.builder()
+                               .statusCode(200)
+                               .header("Content-Type", "application/json")
+                               .body(new 
ByteArrayInputStream(wire.getBytes(StandardCharsets.UTF_8)))
+                               .build();
+               };
+               try (var c = McpClient.builder()
+                       .endpoint("http://x/mcp";)
+                       .transport(transport)
+                       .responseCache(new InMemoryMcpResponseCache())
+                       .build()) {
+                       // Primes the cache with a typed ReadResourceResult 
under the resources/read + uri cache key.
+                       c.readResource("file:///a");
+                       assertEquals(1, calls.get());
+
+                       // callRaw on the same method+params must not retrieve 
the cached ReadResourceResult and must not throw
+                       // ClassCastException; it goes to the wire instead, 
proven by the second (distinct) wire body winning.
+                       var raw = assertDoesNotThrow(() -> 
c.callRaw(McpMethods.RESOURCES_READ, new 
ReadResourceRequest().setUri("file:///a")));
+                       assertEquals(2, calls.get());
+                       var contents = (List<?>) raw.get("contents");
+                       var content = (Map<?,?>) contents.get(0);
+                       assertEquals("wire-body", content.get("text"));
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
index f0ac386572..ce1604b2ef 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRevision.java
@@ -640,11 +640,15 @@ public final class McpRevision implements 
org.apache.juneau.rest.server.mcp.McpR
         * @return The resolved MRTR context. Never <jk>null</jk>.
         */
        private MrtrContext resolveMrtrContext(String method, 
Map<String,Object> params, BeanStore ctx) {
-               var wrapped = new BasicBeanStore(ctx)
-                       .addBean(McpMrtrCapabilityContext.class, new 
McpMrtrCapabilityContext(clientElicitationSupported(params)));
                var requestState = McpParamUtils.strParam(params, 
"requestState");
-               if (requestState == null)
+               if (requestState == null) {
+                       var wrapped = new BasicBeanStore(ctx)
+                               .addBean(McpMrtrCapabilityContext.class, new 
McpMrtrCapabilityContext(clientElicitationSupported(params)));
                        return new MrtrContext(wrapped, 0);
+               }
+               // Validate the echoed requestState before constructing the 
BasicBeanStore below: every failure here
+               // throws, and a BasicBeanStore built earlier would never reach 
a caller's try-with-resources to be
+               // closed (java:S2095). Deferring construction until validation 
succeeds means no path leaks it.
                var sealed = mrtrConfig.getCodec().unseal(requestState, 
aad(method))
                        .orElseThrow(() -> new 
McpException(CODE_INVALID_PARAMS, "Invalid or tampered requestState"));
                if (! method.equals(sealed.method()))
@@ -655,8 +659,10 @@ public final class McpRevision implements 
org.apache.juneau.rest.server.mcp.McpR
                        // Client-facing message deliberately omits the 
configured cap value so server config is not leaked.
                        throw new McpException(CODE_MAX_ROUNDS_EXCEEDED, "Max 
MRTR rounds exceeded");
                var inputResponses = McpParamUtils.mapParam(params, 
"inputResponses");
-               var store = wrapped.addBean(McpMrtrResumeContext.class, new 
McpMrtrResumeContext(sealed.continuation(), inputResponses));
-               return new MrtrContext(store, sealed.round());
+               var wrapped = new BasicBeanStore(ctx)
+                       .addBean(McpMrtrCapabilityContext.class, new 
McpMrtrCapabilityContext(clientElicitationSupported(params)))
+                       .addBean(McpMrtrResumeContext.class, new 
McpMrtrResumeContext(sealed.continuation(), inputResponses));
+               return new MrtrContext(wrapped, sealed.round());
        }
 
        /**
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
index 6e65a87702..da3d05cdb6 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/Characterization_Test.java
@@ -19,19 +19,25 @@ package org.apache.juneau.rest.server.mcp.v20260728;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.*;
+import java.nio.charset.*;
 import java.nio.file.*;
 import java.util.*;
 import java.util.function.*;
 
+import javax.crypto.*;
+import javax.crypto.spec.*;
+
 import org.apache.juneau.bean.mcp.v20260728.*;
 import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.http.tracing.TraceContextCarrier;
 import org.apache.juneau.marshall.collections.*;
 import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.marshaller.Json;
 import org.apache.juneau.rest.mock.classic.*;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.mcp.*;
 import org.apache.juneau.rest.server.tracing.*;
+import org.junit.jupiter.api.*;
 import org.junit.jupiter.params.*;
 import org.junit.jupiter.params.provider.*;
 
@@ -273,6 +279,84 @@ class Characterization_Test {
                }
        }
 
+       /**
+        * Backs every {@code MRTR-*} fixture (SEP-2322 Multi-Round-Trip 
Requests): one {@code ask} tool that pauses
+        * (throws {@link McpInputRequiredSignal}) on a first-round call, and 
on RESUME either completes (echoing the
+        * client's {@code inputResponses}) or pauses a second time &mdash; 
branched on the decoded continuation so a
+        * single handler drives the complete / pause-again / max-rounds 
fixtures.
+        *
+        * <p>
+        * Wires a <b>deterministic, fixed-key</b> {@link FixedKeyGcmCodec} 
instead of the production ephemeral-key
+        * {@link AeadRequestStateCodec}, so a {@code requestState} the harness 
seals for a RESUME fixture (see
+        * {@link #mrtrToken(String)}) is unsealable by this servlet across a 
fresh process &mdash; a property the
+        * random-per-process production key deliberately does <b>not</b> have. 
The opaque token minted <em>into</em> a
+        * PAUSE response still embeds a wall-clock expiry, so responses are 
compared with the token value redacted (see
+        * {@link #normalizeRequestState(String)}); the fixtures pin the wire 
<em>shape</em>, never the opaque bytes.
+        */
+       @Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, 
defaultAccept = "application/json")
+       public static class F_Mrtr extends McpRestServlet {
+               private static final long serialVersionUID = 1L;
+               @Override protected McpServerConfig createMcpConfig() {
+                       return new McpServerConfig().addTool(new 
McpToolHandler() {
+                               @Override public McpToolSpec descriptor() { 
return new McpToolSpec().setName("ask").setDescription("desc:ask"); }
+                               @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-1");
+                                       if 
("pause-again".equals(resume.get().continuation()))
+                                               throw new 
McpInputRequiredSignal(Map.of("q2", Map.of("type", "elicitation")), "cont-2");
+                                       return McpToolOutcome.text("resumed:" + 
resume.get().inputResponses().get("q1"));
+                               }
+                       });
+               }
+               @Override protected McpMrtrConfig createMrtrConfig() {
+                       return new McpMrtrConfig().setCodec(new 
FixedKeyGcmCodec());
+               }
+       }
+
+       /**
+        * Test-only deterministic {@link RequestStateCodec}: AES-256-GCM with 
a <b>hardcoded</b> key and nonce.
+        *
+        * <p>
+        * Unlike the production {@link AeadRequestStateCodec} (random 
per-process key + random per-seal nonce), this
+        * codec produces identical ciphertext for identical {@code (state, 
aad)} inputs and can unseal a token sealed by
+        * any other instance &mdash; exactly the reproducibility a committed 
characterization fixture needs. It is still
+        * genuinely tamper-evident (the GCM auth tag rejects the {@code 
MRTR-tampered-request-state} corruption) and
+        * honors the AAD binding. Fixed-nonce AES-GCM is catastrophically 
insecure for real traffic; it is used here
+        * <em>solely</em> because determinism, not confidentiality, is the 
goal in a fixture.
+        */
+       static final class FixedKeyGcmCodec implements RequestStateCodec {
+               private static final SecretKey KEY = new SecretKeySpec(new 
byte[32], "AES");
+               private static final byte[] NONCE = new byte[12];
+               private static final Base64.Encoder B64 = 
Base64.getUrlEncoder().withoutPadding();
+
+               @Override public String seal(McpRequestState state, String aad) 
{
+                       try {
+                               var cipher = 
Cipher.getInstance("AES/GCM/NoPadding");
+                               cipher.init(Cipher.ENCRYPT_MODE, KEY, new 
GCMParameterSpec(128, NONCE));
+                               
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                               var ciphertext = 
cipher.doFinal(Json.of(state).getBytes(StandardCharsets.UTF_8));
+                               return B64.encodeToString(NONCE) + "." + 
B64.encodeToString(ciphertext);
+                       } catch (Exception e) {
+                               throw new RuntimeException("Fixture codec seal 
failed", e);
+                       }
+               }
+
+               @Override public Optional<McpRequestState> unseal(String token, 
String aad) {
+                       try {
+                               var parts = token.split("\\.", 2);
+                               var nonce = 
Base64.getUrlDecoder().decode(parts[0]);
+                               var ciphertext = 
Base64.getUrlDecoder().decode(parts[1]);
+                               var cipher = 
Cipher.getInstance("AES/GCM/NoPadding");
+                               cipher.init(Cipher.DECRYPT_MODE, KEY, new 
GCMParameterSpec(128, nonce));
+                               
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8));
+                               return Optional.of(Json.to(new 
String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8), 
McpRequestState.class));
+                       } catch (@SuppressWarnings("unused") Exception e) {
+                               return Optional.empty();
+                       }
+               }
+       }
+
        // --- fixture handler factories 
---------------------------------------------------------
 
        private static McpToolHandler tool(String name, McpSchema schema, 
Function<Map<String,Object>,McpToolOutcome> fn) {
@@ -331,10 +415,74 @@ class Characterization_Test {
                                "COMPLETE-empty-unknown", "COMPLETE-capped" -> 
F_Complete.class;
                        case "TRACE-meta-parent", "TRACE-http-fallback", 
"TRACE-meta-wins",
                                "TRACE-tracestate-baggage", 
"TRACE-response-echo", "TRACE-jsonrpc-error" -> F_Traced.class;
+                       case "MRTR-input-required-response", 
"MRTR-resume-complete", "MRTR-resume-input-required-again",
+                               "MRTR-tampered-request-state", 
"MRTR-expired-request-state", "MRTR-unsupported-capability",
+                               "MRTR-max-rounds-exceeded" -> F_Mrtr.class;
                        default -> F_Empty.class;
                };
        }
 
+       // --- MRTR requestState sealing/redaction 
------------------------------------------------
+
+       /** Placeholder in an {@code MRTR-*} resume fixture's committed request 
body, replaced at replay time. */
+       private static final String TOKEN_PLACEHOLDER = "__REQUEST_STATE__";
+
+       // Fixed expiry constants embedded in a sealed requestState so a RESUME 
fixture's token is reproducible
+       // (never "now + ttl"): far-future for a still-valid token, a fixed 
past instant for the expired fixture.
+       private static final long FAR_FUTURE_MS = 32503680000000L; // ~ year 
3000
+       private static final long PAST_MS = 1000L;
+
+       private static String aad(String method) {
+               return method + '\u0000' + McpProtocol.VERSION_2026_07_28;
+       }
+
+       /**
+        * The {@code requestState} the harness seals (with {@link 
FixedKeyGcmCodec}, matching {@link F_Mrtr}'s codec)
+        * for each RESUME-family fixture. Substituted into the committed 
request in place of {@link #TOKEN_PLACEHOLDER}
+        * so the token stays framework-owned/opaque while the request pins the 
surrounding wire shape.
+        *
+        * <p>
+        * {@code MRTR-resume-complete} and {@code 
MRTR-resume-input-required-again} have byte-identical committed
+        * request files by design: the behavioral difference between the two 
fixtures lives entirely inside the sealed
+        * opaque token's continuation value ({@code "complete-me"} vs {@code 
"pause-again"}), not in anything visible
+        * on the wire request body.
+        *
+        * <p>
+        * The plan's original "commit the corrupted requestState as a frozen 
literal" constraint for
+        * {@code MRTR-tampered-request-state} was consciously superseded: 
because {@link FixedKeyGcmCodec} makes
+        * {@code seal(...)} fully deterministic (fixed key, fixed nonce), 
regenerating the tampered token on every run
+        * via {@code tamper(codec.seal(...))} is equally reproducible and 
avoids hand-maintaining opaque ciphertext.
+        */
+       private static String mrtrToken(String fixture) {
+               var codec = new FixedKeyGcmCodec();
+               return switch (fixture) {
+                       case "MRTR-resume-complete" -> codec.seal(new 
McpRequestState("complete-me", "tools/call", 1, FAR_FUTURE_MS), 
aad("tools/call"));
+                       case "MRTR-resume-input-required-again" -> 
codec.seal(new McpRequestState("pause-again", "tools/call", 1, FAR_FUTURE_MS), 
aad("tools/call"));
+                       case "MRTR-expired-request-state" -> codec.seal(new 
McpRequestState("cont-1", "tools/call", 1, PAST_MS), aad("tools/call"));
+                       case "MRTR-max-rounds-exceeded" -> codec.seal(new 
McpRequestState("cont-1", "tools/call", McpMrtrConfig.DEFAULT_MAX_ROUNDS, 
FAR_FUTURE_MS), aad("tools/call"));
+                       case "MRTR-tampered-request-state" -> 
tamper(codec.seal(new McpRequestState("cont-1", "tools/call", 1, 
FAR_FUTURE_MS), aad("tools/call")));
+                       default -> throw new IllegalArgumentException("No MRTR 
token mapping for fixture: " + fixture);
+               };
+       }
+
+       /** Flips one ciphertext byte of a valid token so its GCM tag fails 
verification (the tamper fixture). */
+       private static String tamper(String token) {
+               var parts = token.split("\\.", 2);
+               var ciphertext = Base64.getUrlDecoder().decode(parts[1]);
+               ciphertext[0] ^= 1;
+               return parts[0] + "." + 
Base64.getUrlEncoder().withoutPadding().encodeToString(ciphertext);
+       }
+
+       /**
+        * Redacts the opaque {@code requestState} token value in a response 
body to a fixed placeholder before
+        * comparison. The token is per-process-nondeterministic ciphertext 
(production uses a random key and embeds a
+        * wall-clock expiry), so pinning its exact bytes is neither possible 
nor meaningful; the fixture pins its
+        * presence and the surrounding wire shape instead.
+        */
+       private static String normalizeRequestState(String responseBody) {
+               return responseBody.replaceAll("\"requestState\":\"[^\"]+\"", 
"\"requestState\":\"<sealed>\"");
+       }
+
        // --- replay 
----------------------------------------------------------------------------
 
        static List<String> fixtures() throws Exception {
@@ -350,7 +498,7 @@ class Characterization_Test {
        @ParameterizedTest
        @MethodSource("fixtures")
        void a01_wireIsUnchanged(String fixture) throws Exception {
-               var actual = replayHttp(fixture);
+               var actual = normalizeRequestState(replayHttp(fixture));
                var expected = DIR.resolve(fixture + ".response.json");
                if (WRITE) {
                        Files.writeString(expected, actual);
@@ -360,8 +508,27 @@ class Characterization_Test {
                        () -> fixture + ": WIRE FORMAT CHANGED. Do not update 
the fixture — fix the code.");
        }
 
+       /**
+        * The opaque {@code requestState} minted into {@code 
MRTR-resume-input-required-again}'s second PAUSE is
+        * redacted for {@code a01}'s byte comparison, so its embedded round 
counter is verified here instead: unseal
+        * the live token with the same fixed-key codec {@link F_Mrtr} uses and 
assert the round advanced to 2 (plan
+        * Phase 4 Task 12 decode-and-check).
+        */
+       @Test
+       void a02_resumeInputRequiredAgain_sealedRoundIsTwo() throws Exception {
+               var raw = replayHttp("MRTR-resume-input-required-again");
+               var envelope = Json.to(raw, JsonMap.class);
+               var result = (Map<?,?>) envelope.get("result");
+               var token = (String) result.get("requestState");
+               var state = new FixedKeyGcmCodec().unseal(token, 
aad("tools/call")).orElseThrow();
+               assertEquals(2, state.round());
+               assertEquals("cont-2", state.continuation());
+       }
+
        private String replayHttp(String fixture) throws Exception {
                var requestBody = Files.readString(DIR.resolve(fixture + 
".request.json")).strip();
+               if (requestBody.contains(TOKEN_PLACEHOLDER))
+                       requestBody = requestBody.replace(TOKEN_PLACEHOLDER, 
mrtrToken(fixture));
                var headers = loadHeaders(fixture);
                var client = MockRestClient.create(servletFor(fixture)).json()
                        
.contentType("application/json").accept("application/json").ignoreErrors().build();
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.response.json
new file mode 100644
index 0000000000..a1727204b3
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-expired-request-state.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32022,"message":"requestState has 
expired"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.request.json
new file mode 100644
index 0000000000..0e2aa4847b
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.response.json
new file mode 100644
index 0000000000..6f7cc8908b
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-input-required-response.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"inputRequests":{"q1":{"type":"elicitation"}},"requestState":"<sealed>","resultType":"input_required"}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.response.json
new file mode 100644
index 0000000000..b3eef6825e
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-max-rounds-exceeded.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32023,"message":"Max MRTR rounds 
exceeded"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.request.json
new file mode 100644
index 0000000000..2e2ecabcba
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","inputResponses":{"q1":"answer"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.response.json
new file mode 100644
index 0000000000..763cc3f7cc
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-complete.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"content":[{"type":"text","text":"resumed:answer"}],"resultType":"complete"}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.request.json
new file mode 100644
index 0000000000..2e2ecabcba
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","inputResponses":{"q1":"answer"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.response.json
new file mode 100644
index 0000000000..0980582389
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-resume-input-required-again.response.json
@@ -0,0 +1 @@
+{"id":1,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"juneau-rest-server-mcp","version":"unknown"}},"inputRequests":{"q2":{"type":"elicitation"}},"requestState":"<sealed>","resultType":"input_required"}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.request.json
new file mode 100644
index 0000000000..97fc1a6f99
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","requestState":"__REQUEST_STATE__","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.response.json
new file mode 100644
index 0000000000..1157a8dfb7
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-tampered-request-state.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32602,"message":"Invalid or tampered 
requestState"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.headers.properties
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.headers.properties
new file mode 100644
index 0000000000..4c862e26b1
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.headers.properties
@@ -0,0 +1,17 @@
+# 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.
+
+Mcp-Method=tools/call
+Mcp-Name=ask
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.request.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.request.json
new file mode 100644
index 0000000000..1c5c5da477
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.request.json
@@ -0,0 +1 @@
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ask","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"fixture-client","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.response.json
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.response.json
new file mode 100644
index 0000000000..56242140a5
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/resources/characterization/MRTR-unsupported-capability.response.json
@@ -0,0 +1 @@
+{"error":{"code":-32021,"message":"Client does not advertise the elicitation 
capability required for input_required"},"id":1,"jsonrpc":"2.0"}
\ No newline at end of file

Reply via email to