You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/08/05 01:27:51 UTC

(juneau) branch master updated: MCP 2026-07-28 v2 config consolidation + Spring Boot integration + SubscriptionPump race fix (TODO-330, TODO-319)

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 8eea1d7b14 MCP 2026-07-28 v2 config consolidation + Spring Boot integration + SubscriptionPump race fix (TODO-330, TODO-319)
8eea1d7b14 is described below

commit 8eea1d7b14729d5c3d07591bc03ed4e6772e57db
Author: James Bognar <ja...@apache.org>
AuthorDate: Tue Aug 4 18:27:33 2026 -0700

    MCP 2026-07-28 v2 config consolidation + Spring Boot integration + SubscriptionPump race fix (TODO-330, TODO-319)
    
    - TODO-330: consolidate scattered v2-adapter config into a single McpOptions
      aggregate; unify stateful defaults to per-binding on both servlet and mixin
      paths (fixes accidental JVM-wide sharing); add identity-keyed
      McpEndpointOptionsCache; split ElicitationAccess into ElicitationRequests /
      ElicitationResponses; delete SharedMrtrConfig / SharedSubscriptionBroker.
    - TODO-319: add McpSpringBootIntegration_Test proving SpringMcpRestServlet
      resolves Spring beans in MCP handlers (with negative no-DI contrast).
    - Fix happens-before race in SubscriptionPump: set open=false before invoking
      the terminal onComplete/onError callback at all three sites so callers that
      synchronize on the callback no longer observe a stale isOpen()==true.
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 .../v20260728/McpElicitationIntegration_Test.java  |  58 ++--
 .../mcp/v20260728/McpMrtrIntegration_Test.java     |   4 +-
 .../McpSubscriptionsListenIntegration_Test.java    |  15 +-
 .../mcp/v20260728/Characterization_Test.java       |  14 +-
 .../v20260728/McpCacheBindingIntegration_Test.java |  16 +-
 .../v20260728/McpSpringBootIntegration_Test.java   | 262 +++++++++++++++++
 .../McpTemplateCompletionIntegration_Test.java     |   2 +-
 ...itationAccess.java => ElicitationRequests.java} |  67 +----
 .../client/mcp/v20260728/ElicitationResponses.java |  89 ++++++
 .../rest/client/mcp/v20260728/McpClient.java       |  23 +-
 .../mcp/v20260728/ElicitationAccess_Test.java      | 176 ------------
 .../mcp/v20260728/ElicitationRequests_Test.java    |  99 +++++++
 .../mcp/v20260728/ElicitationResponses_Test.java   | 108 +++++++
 .../McpClientSubscriptionsListen_Test.java         | 175 ++++++++++++
 .../server/mcp/v20260728/ElicitationRequests.java  |   2 +-
 .../rest/server/mcp/v20260728/McpEndpoint.java     | 159 +++--------
 .../mcp/v20260728/McpEndpointOptionsCache.java     | 129 +++++++++
 .../rest/server/mcp/v20260728/McpOptions.java      | 309 +++++++++++++++++++++
 .../rest/server/mcp/v20260728/McpRestServlet.java  | 280 ++++---------------
 .../rest/server/mcp/v20260728/McpRevision.java     |  31 ++-
 .../mcp/v20260728/McpSubscriptionsConfig.java      |   7 +-
 .../server/mcp/v20260728/SharedMrtrConfig.java     |  52 ----
 .../mcp/v20260728/SharedSubscriptionBroker.java    |  57 ----
 .../server/mcp/v20260728/SpringMcpRestServlet.java |   7 +-
 .../mcp/v20260728/Characterization_Test.java       |  14 +-
 .../server/mcp/v20260728/McpBindings_Test.java     | 127 +++++----
 .../v20260728/McpEndpointOptionsCache_Test.java    |  98 +++++++
 .../rest/server/mcp/v20260728/McpOptions_Test.java | 196 +++++++++++++
 .../McpSubscriptionBrokerBinding_Test.java         |  67 ++---
 .../v20260728/McpSubscriptionBrokerMixin_Test.java |  70 +++--
 .../v20260728/McpSubscriptionsEndToEnd_Test.java   |   3 +
 .../SubscriptionsListenDispatch_Test.java          |   3 +-
 .../juneau/rest/server/mcp/McpEndpointMixin.java   |   3 +-
 .../mcp/BasicMcpSubscriptionBroker_Test.java       |  16 +-
 34 files changed, 1835 insertions(+), 903 deletions(-)

diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
index 94543a5c8d..f7005ee93b 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
@@ -41,10 +41,11 @@ import jakarta.servlet.*;
 
 /**
  * End-to-end coverage for MCP {@code 2026-07-28} SEP-2322 elicitation (C6), driven through the real
- * {@link McpClient} and its {@link ElicitationAccess} typed helper against a live embedded-Jetty server whose
- * tools pause/resume via {@link ElicitationRequests}/{@link ElicitationResponses} &mdash; proving the full typed
- * path works across a real wire boundary, complementing {@link McpMrtrIntegration_Test}'s generic (untyped)
- * PAUSE/RESUME coverage and Phase 7's {@code Characterization_Test} in-process replay fixtures.
+ * {@link McpClient} and its client-side {@link ElicitationRequests}/{@link ElicitationResponses} typed helpers
+ * against a live embedded-Jetty server whose tools pause/resume via the server-side (same-named)
+ * {@code ElicitationRequests}/{@code ElicitationResponses} &mdash; proving the full typed path works across a
+ * real wire boundary, complementing {@link McpMrtrIntegration_Test}'s generic (untyped) PAUSE/RESUME coverage
+ * and Phase 7's {@code Characterization_Test} in-process replay fixtures.
  */
 @org.apache.juneau.testing.JettyMicroserviceTest
 class McpElicitationIntegration_Test extends TestBase {
@@ -55,14 +56,19 @@ class McpElicitationIntegration_Test extends TestBase {
 			.setRequestedSchema(ElicitSchema.create().booleanField("confirm").title("Confirm").build());
 	}
 
+	// The server-side org.apache.juneau.rest.server.mcp.v20260728.ElicitationRequests/ElicitationResponses
+	// (wildcard-imported above) share their simple names with this test's own client-side package
+	// (org.apache.juneau.rest.client.mcp.v20260728.ElicitationRequests/ElicitationResponses), where same-package
+	// visibility wins over a wildcard import - so every server-side call below is qualified with its full
+	// package to disambiguate, while every client-side call in the test bodies below stays unqualified.
 	private static McpToolHandler confirm() {
 		return new McpToolHandler() {
 			@Override public McpToolSpec descriptor() { return new McpToolSpec().setName("confirm").setDescription("Pauses for one confirmation"); }
 			@Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) {
 				var resume = ctx.getBean(McpMrtrResumeContext.class);
 				if (resume.isEmpty())
-					throw ElicitationRequests.of("confirm", confirmQuestion(), "cont-1");
-				var answer = ElicitationResponses.get(resume.get(), "confirm");
+					throw org.apache.juneau.rest.server.mcp.v20260728.ElicitationRequests.of("confirm", confirmQuestion(), "cont-1");
+				var answer = org.apache.juneau.rest.server.mcp.v20260728.ElicitationResponses.get(resume.get(), "confirm");
 				// Echoes the answer's content (not just its action) and the continuation this tool paused
 				// with, so the assertions below actually exercise ElicitResult.content marshalling and prove
 				// the continuation round-trips through McpMrtrResumeContext, instead of only the action enum.
@@ -83,9 +89,9 @@ class McpElicitationIntegration_Test extends TestBase {
 					requests.put("confirm", confirmQuestion());
 					requests.put("reason", new ElicitRequest().setMessage("Why?")
 						.setRequestedSchema(ElicitSchema.create().stringField("reason").build()));
-					throw ElicitationRequests.of(requests, "cont-multi");
+					throw org.apache.juneau.rest.server.mcp.v20260728.ElicitationRequests.of(requests, "cont-multi");
 				}
-				var answers = ElicitationResponses.all(resume.get());
+				var answers = org.apache.juneau.rest.server.mcp.v20260728.ElicitationResponses.all(resume.get());
 				var confirmAnswer = answers.get("confirm");
 				var reasonAnswer = answers.get("reason");
 				var confirmValue = confirmAnswer.getContent() == null ? null : confirmAnswer.getContent().get("confirm");
@@ -135,14 +141,14 @@ class McpElicitationIntegration_Test extends TestBase {
 	}
 
 	// =================================================================================================================
-	// A: single-question pause -> resume -> complete, driven entirely through ElicitationAccess.
+	// A: single-question pause -> resume -> complete, driven entirely through the client-side ElicitationRequests/ElicitationResponses.
 	// =================================================================================================================
 
 	@Test void a01_singleQuestion_pauseResumeAccept_fullLoop() throws Exception {
 		try (var client = clientBuilder(true).build()) {
 			var paused = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm").setArguments(Map.of()));
-			assertTrue(ElicitationAccess.isInputRequired(paused));
-			var requests = ElicitationAccess.requests(paused);
+			assertTrue(ElicitationRequests.isInputRequired(paused));
+			var requests = ElicitationRequests.requests(paused);
 			assertEquals("Proceed with deletion?", requests.get("confirm").getMessage());
 			// The requestedSchema (built via ElicitSchema.booleanField("confirm").title("Confirm")) must survive
 			// the wire intact, not just the message - otherwise a real client could not render the right input
@@ -152,9 +158,9 @@ class McpElicitationIntegration_Test extends TestBase {
 
 			var answer = new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true);
 			var completed = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm")
-				.setRequestState(ElicitationAccess.requestState(paused))
-				.setInputResponses(ElicitationAccess.toInputResponse("confirm", answer)));
-			assertFalse(ElicitationAccess.isInputRequired(completed));
+				.setRequestState(ElicitationRequests.requestState(paused))
+				.setInputResponses(ElicitationResponses.toInputResponse("confirm", answer)));
+			assertFalse(ElicitationRequests.isInputRequired(completed));
 			assertEquals("complete", completed.get("resultType"));
 			assertEquals("action:accept,confirm:true,cont:cont-1", firstText(completed));
 		}
@@ -167,7 +173,7 @@ class McpElicitationIntegration_Test extends TestBase {
 	@Test void b01_multiQuestion_pauseResumeBoth_singleRoundTrip() throws Exception {
 		try (var client = clientBuilder(true).build()) {
 			var paused = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirmTwo").setArguments(Map.of()));
-			var requests = ElicitationAccess.requests(paused);
+			var requests = ElicitationRequests.requests(paused);
 			assertEquals(2, requests.size());
 			assertTrue(requests.containsKey("confirm"));
 			assertTrue(requests.containsKey("reason"));
@@ -176,8 +182,8 @@ class McpElicitationIntegration_Test extends TestBase {
 			answers.put("confirm", new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true));
 			answers.put("reason", new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("reason", "cleanup"));
 			var completed = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirmTwo")
-				.setRequestState(ElicitationAccess.requestState(paused))
-				.setInputResponses(ElicitationAccess.toInputResponses(answers)));
+				.setRequestState(ElicitationRequests.requestState(paused))
+				.setInputResponses(ElicitationResponses.toInputResponses(answers)));
 			assertEquals("complete", completed.get("resultType"));
 			assertEquals("confirm:accept=true,reason:accept=cleanup,cont:cont-multi", firstText(completed));
 		}
@@ -190,12 +196,12 @@ class McpElicitationIntegration_Test extends TestBase {
 	@Test void c01_decline_resumeCarriesDeclineAction() throws Exception {
 		try (var client = clientBuilder(true).build()) {
 			var paused = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm").setArguments(Map.of()));
-			assertTrue(ElicitationAccess.isInputRequired(paused));
-			assertNotNull(ElicitationAccess.requestState(paused));
+			assertTrue(ElicitationRequests.isInputRequired(paused));
+			assertNotNull(ElicitationRequests.requestState(paused));
 			var answer = new ElicitResult().setAction(ElicitAction.DECLINE);
 			var completed = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm")
-				.setRequestState(ElicitationAccess.requestState(paused))
-				.setInputResponses(ElicitationAccess.toInputResponse("confirm", answer)));
+				.setRequestState(ElicitationRequests.requestState(paused))
+				.setInputResponses(ElicitationResponses.toInputResponse("confirm", answer)));
 			assertEquals("complete", completed.get("resultType"));
 			assertEquals("action:decline,confirm:null,cont:cont-1", firstText(completed));
 		}
@@ -204,12 +210,12 @@ class McpElicitationIntegration_Test extends TestBase {
 	@Test void c02_cancel_resumeCarriesCancelAction() throws Exception {
 		try (var client = clientBuilder(true).build()) {
 			var paused = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm").setArguments(Map.of()));
-			assertTrue(ElicitationAccess.isInputRequired(paused));
-			assertNotNull(ElicitationAccess.requestState(paused));
+			assertTrue(ElicitationRequests.isInputRequired(paused));
+			assertNotNull(ElicitationRequests.requestState(paused));
 			var answer = new ElicitResult().setAction(ElicitAction.CANCEL);
 			var completed = client.callRaw(McpMethods.TOOLS_CALL, new CallToolRequest().setName("confirm")
-				.setRequestState(ElicitationAccess.requestState(paused))
-				.setInputResponses(ElicitationAccess.toInputResponse("confirm", answer)));
+				.setRequestState(ElicitationRequests.requestState(paused))
+				.setInputResponses(ElicitationResponses.toInputResponse("confirm", answer)));
 			assertEquals("complete", completed.get("resultType"));
 			assertEquals("action:cancel,confirm:null,cont:cont-1", firstText(completed));
 		}
@@ -221,7 +227,7 @@ class McpElicitationIntegration_Test extends TestBase {
 
 	/**
 	 * Delta vs {@link McpMrtrIntegration_Test#b01_unsupportedCapability_rejectedOverTheWire()}: proves the same
-	 * capability gate fires for a signal built via the typed {@code ElicitationRequests.of(...)} helper (not a
+	 * capability gate fires for a signal built via the typed server-side {@code ElicitationRequests.of(...)} helper (not a
 	 * hand-built {@code McpInputRequiredSignal}) — i.e. the helper yields a gate-recognized signal.
 	 */
 	@Test void d01_unsupportedCapability_typedHelperPauseStillRejectedOverTheWire() throws Exception {
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
index 8192e2d0d9..9436b300d3 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
@@ -106,8 +106,8 @@ class McpMrtrIntegration_Test extends TestBase {
 		}
 
 		@Override
-		protected McpMrtrConfig createMrtrConfig() {
-			return new McpMrtrConfig().setCodec(CODEC);
+		protected McpOptions createMcpOptions() {
+			return new McpOptions().mrtr(m -> m.setCodec(CODEC));
 		}
 	}
 
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpSubscriptionsListenIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpSubscriptionsListenIntegration_Test.java
index c8bcec4371..8addd12cba 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpSubscriptionsListenIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpSubscriptionsListenIntegration_Test.java
@@ -101,18 +101,15 @@ class McpSubscriptionsListenIntegration_Test extends TestBase {
 				.addTool(publishTool()).addTool(activeCountTool());
 		}
 
-		@Override
-		protected ServerCapabilities capabilities() {
-			return new ServerCapabilities()
-				.setResources(new ResourceCapability().setSubscribe(true).setListChanged(true))
-				.setTools(new ToolCapability().setListChanged(true));
-		}
-
 		// Short heartbeat so c01 (below) can observe repeated "ping" frames over a real held-open connection
 		// without waiting through the 15s production default.
 		@Override
-		protected McpSubscriptionsConfig createSubscriptionsConfig() {
-			return new McpSubscriptionsConfig().setHeartbeatIntervalMs(HEARTBEAT_INTERVAL_MS);
+		protected McpOptions createMcpOptions() {
+			return new McpOptions()
+				.setCapabilities(new ServerCapabilities()
+					.setResources(new ResourceCapability().setSubscribe(true).setListChanged(true))
+					.setTools(new ToolCapability().setListChanged(true)))
+				.subscriptions(s -> s.setHeartbeatIntervalMs(HEARTBEAT_INTERVAL_MS));
 		}
 	}
 
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 282e32de83..f7042e32a0 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
@@ -92,15 +92,15 @@ class Characterization_Test {
 	public static class F_Cache extends F_Full {
 		private static final long serialVersionUID = 1L;
 
-		@Override protected McpCacheConfig createCacheConfig() {
-			return new McpCacheConfig()
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().cache(c -> c
 				.setDefaultHint(new McpCacheHint().setTtlMs(30000))
 				.setToolsList(new McpCacheHint().setTtlMs(5000).setCacheScope(McpCacheScope.PRIVATE))
 				.setPromptsList(new McpCacheHint().setTtlMs(0).setCacheScope(McpCacheScope.PUBLIC))
 				.setResourceTemplatesList(new McpCacheHint().setTtlMs(60000).setCacheScope(McpCacheScope.PRIVATE))
 				.setResourcesRead(new McpCacheHint().setTtlMs(2000))
 				.addResourceReadOverride("file:///a",
-					new McpCacheHint().setTtlMs(1000).setCacheScope(McpCacheScope.PRIVATE));
+					new McpCacheHint().setTtlMs(1000).setCacheScope(McpCacheScope.PRIVATE)));
 		}
 	}
 
@@ -307,8 +307,8 @@ class Characterization_Test {
 				}
 			});
 		}
-		@Override protected McpMrtrConfig createMrtrConfig() {
-			return new McpMrtrConfig().setCodec(new FixedKeyGcmCodec());
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().mrtr(m -> m.setCodec(new FixedKeyGcmCodec()));
 		}
 	}
 
@@ -359,8 +359,8 @@ class Characterization_Test {
 				});
 		}
 
-		@Override protected McpMrtrConfig createMrtrConfig() {
-			return new McpMrtrConfig().setCodec(new FixedKeyGcmCodec());
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().mrtr(m -> m.setCodec(new FixedKeyGcmCodec()));
 		}
 	}
 
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpCacheBindingIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpCacheBindingIntegration_Test.java
index bfa37e62f3..2c838bbd86 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpCacheBindingIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpCacheBindingIntegration_Test.java
@@ -36,8 +36,8 @@ import org.apache.juneau.rest.server.servlet.*;
 import org.junit.jupiter.api.*;
 
 /**
- * Cross-module coverage proving the servlet-subclass and endpoint-mixin cache-config lifecycle hooks
- * are consumable exactly as an external artifact consumer would use them, through the published
+ * Cross-module coverage proving the servlet-subclass and endpoint-mixin {@link McpOptions} cache configuration
+ * seams are consumable exactly as an external artifact consumer would use them, through the published
  * public API of {@code juneau-rest-server-mcp-v20260728}.
  */
 @SuppressWarnings({
@@ -77,9 +77,9 @@ class McpCacheBindingIntegration_Test {
 		@Override protected McpServerConfig createMcpConfig() {
 			return new McpServerConfig().addTool(echo());
 		}
-		@Override protected McpCacheConfig createCacheConfig() {
-			return new McpCacheConfig().setToolsList(
-				new McpCacheHint().setTtlMs(77).setCacheScope(McpCacheScope.PRIVATE));
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().cache(c -> c.setToolsList(
+				new McpCacheHint().setTtlMs(77).setCacheScope(McpCacheScope.PRIVATE)));
 		}
 	}
 
@@ -91,9 +91,9 @@ class McpCacheBindingIntegration_Test {
 		@Override public McpServerConfig getMcpConfig() {
 			return new McpServerConfig().addTool(echo());
 		}
-		@Override public McpCacheConfig cacheConfig() {
-			return new McpCacheConfig().setToolsList(
-				new McpCacheHint().setTtlMs(77).setCacheScope(McpCacheScope.PRIVATE));
+		@Override public McpOptions getMcpOptions() {
+			return new McpOptions().cache(c -> c.setToolsList(
+				new McpCacheHint().setTtlMs(77).setCacheScope(McpCacheScope.PRIVATE)));
 		}
 	}
 
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSpringBootIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSpringBootIntegration_Test.java
new file mode 100644
index 0000000000..59929ff893
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSpringBootIntegration_Test.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+import java.net.http.*;
+import java.net.http.HttpRequest.*;
+import java.net.http.HttpResponse.*;
+import java.time.*;
+import java.util.*;
+
+import org.apache.juneau.bean.jsonrpc.*;
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.marshaller.Json;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.springboot.*;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.*;
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.test.context.*;
+import org.springframework.boot.test.context.SpringBootTest.*;
+import org.springframework.boot.test.web.server.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.annotation.*;
+import org.springframework.test.annotation.*;
+
+/**
+ * Real-Spring-Boot proof for the MCP-on-Spring-Boot recipe: that {@link SpringMcpRestServlet} actually
+ * bridges the Spring {@code ApplicationContext} into an MCP tool handler's per-request {@code BeanStore},
+ * versus the plain servlet-subclass path ({@link McpRestServlet}) which does not.
+ *
+ * <p>
+ * Before this test, nothing anywhere exercised MCP + Spring Boot together: the recipe was documented
+ * (see {@code SpringMcpRestServlet}'s javadoc and the {@code juneau-docs} MCP recipes topic) but never
+ * proven against a real embedded-Tomcat Spring Boot context.
+ *
+ * <p>
+ * Boots a single Spring Boot context with embedded Tomcat on a random port (mirroring
+ * {@code BasicApiDocs_Springboot_Test}'s idiom) registering <b>two</b> independent MCP servlets side by
+ * side, both backed by the very same {@code ApplicationContext} and the very same {@code @Bean
+ * MySpringService} singleton:
+ * <ul>
+ * 	<li><b>Positive case</b> ({@link SpringMcpResource}, {@code /mcp-spring/mcp}): extends
+ * 		{@link SpringMcpRestServlet}. Its {@code whoAmI} tool handler calls
+ * 		{@code BeanStore.getBean(MySpringService.class)} and must resolve the real Spring-managed
+ * 		singleton &mdash; proven by echoing back a per-instance random marker that exactly matches the
+ * 		same instance {@code @Autowired} into this test class.
+ * 	<li><b>Negative/contrast case</b> ({@link PlainMcpResource}, {@code /mcp-plain/mcp}): extends
+ * 		{@link McpRestServlet} (the plain servlet-subclass path, no Spring base class). The identical
+ * 		{@code whoAmI} tool handler's {@code BeanStore.getBean(MySpringService.class)} call must come
+ * 		back empty, documenting the recipe's known boundary: subclassing {@link McpRestServlet} does not
+ * 		carry Spring DI, even when deployed inside the same Spring Boot app as a servlet that does.
+ * </ul>
+ *
+ * @since 10.0.0
+ */
+@org.apache.juneau.testing.SpringbootTest
+@SpringBootTest(classes = McpSpringBootIntegration_Test.TestApp.class, webEnvironment = WebEnvironment.RANDOM_PORT)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+class McpSpringBootIntegration_Test {
+
+	/**
+	 * A Spring-managed singleton service. Generates a random per-instance marker at construction so any
+	 * two distinct instances are trivially distinguishable: a tool handler that echoes back a marker
+	 * matching {@link #mySpringService}'s (the instance this test class itself has {@code @Autowired})
+	 * can only be doing so because it resolved that exact same Spring-container-managed instance, not a
+	 * coincidentally-equal one it constructed itself.
+	 */
+	static class MySpringService {
+		private final String marker = "spring-bean-" + UUID.randomUUID();
+		String getMarker() { return marker; }
+	}
+
+	/** Sentinel returned by {@link #whoAmITool()} when {@code BeanStore.getBean(MySpringService.class)} finds nothing. */
+	private static final String NO_BEAN = "NO_SPRING_BEAN_RESOLVED";
+
+	/**
+	 * Shared tool logic registered identically on both {@link SpringMcpResource} and
+	 * {@link PlainMcpResource} below: resolve {@link MySpringService} through the per-request
+	 * {@link BeanStore} and echo back its marker, or {@link #NO_BEAN} if it could not be resolved. Using
+	 * one shared handler implementation for both fixtures ensures the only variable between the two test
+	 * cases is which servlet base class is doing the resolving, not any difference in the handler itself.
+	 */
+	private static McpToolHandler whoAmITool() {
+		return McpToolHandler.of(
+			new McpToolSpec().setName("whoAmI").setDescription("Resolves MySpringService via BeanStore.getBean(...)"),
+			(arguments, ctx) -> McpToolOutcome.text(ctx.getBean(MySpringService.class).map(MySpringService::getMarker).orElse(NO_BEAN)));
+	}
+
+	// -------- positive case: the MCP-on-Spring-Boot recipe ---------
+
+	/**
+	 * The recipe under test. {@link SpringMcpRestServlet} extends {@link BasicSpringRestServlet}, whose
+	 * {@code SpringRestServlet} ancestor has an {@code @Autowired ApplicationContext} field and a
+	 * {@code @Bean createBeanStore(...)} factory method that wraps it in a {@code SpringBeanStore}; that
+	 * store becomes this resource's {@code RestContext} bean store, and the MCP endpoint mixin's
+	 * {@code handleMcpRequest} wraps a fresh per-request {@code BasicBeanStore} around it before invoking
+	 * the tool handler &mdash; so {@code BeanStore.getBean(MySpringService.class)} chains all the way
+	 * through to {@code ApplicationContext.getBeanProvider(MySpringService.class)}.
+	 */
+	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
+	public static class SpringMcpResource extends SpringMcpRestServlet {
+		private static final long serialVersionUID = 1L;
+		@Override public McpServerConfig getMcpConfig() {
+			return new McpServerConfig().addTool(whoAmITool());
+		}
+	}
+
+	// -------- negative/contrast case: the servlet-subclass path does NOT carry Spring DI ---------
+
+	/**
+	 * Contrast fixture documenting the recipe's known boundary. {@link McpRestServlet} is a sibling
+	 * {@code RestServlet} subclass to {@link BasicSpringRestServlet} (both ultimately extend
+	 * {@code RestServlet}), <b>not</b> a subclass of it &mdash; so it never gets the
+	 * {@code @Autowired ApplicationContext} field {@code SpringRestServlet} injects, and never publishes
+	 * a {@code SpringBeanStore}-backed {@code createBeanStore()} bean. Its {@code RestContext} bean store
+	 * is therefore a plain, Spring-oblivious store even though this servlet is registered in the very
+	 * same Spring Boot context as {@link SpringMcpResource} (same {@code ApplicationContext}, same
+	 * {@code @Bean MySpringService}). Its {@code whoAmI} tool handler's
+	 * {@code BeanStore.getBean(MySpringService.class)} call therefore cannot see that bean.
+	 */
+	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
+	public static class PlainMcpResource extends McpRestServlet {
+		private static final long serialVersionUID = 1L;
+		@Override protected McpServerConfig createMcpConfig() {
+			return new McpServerConfig().addTool(whoAmITool());
+		}
+	}
+
+	/**
+	 * Minimal Spring Boot application registering both fixture servlets side by side at distinct
+	 * container URL mappings, plus the single shared {@code @Bean MySpringService} both endpoints (try
+	 * to) resolve.
+	 */
+	@SpringBootConfiguration
+	@EnableAutoConfiguration
+	public static class TestApp {
+
+		@Bean
+		public MySpringService mySpringService() {
+			return new MySpringService();
+		}
+
+		@Bean
+		public SpringMcpResource springMcpResource() {
+			return new SpringMcpResource();
+		}
+
+		@Bean
+		public ServletRegistrationBean<SpringMcpResource> springMcpRegistration(SpringMcpResource servlet) {
+			return new ServletRegistrationBean<>(servlet, "/mcp-spring/*");
+		}
+
+		@Bean
+		public PlainMcpResource plainMcpResource() {
+			return new PlainMcpResource();
+		}
+
+		@Bean
+		public ServletRegistrationBean<PlainMcpResource> plainMcpRegistration(PlainMcpResource servlet) {
+			return new ServletRegistrationBean<>(servlet, "/mcp-plain/*");
+		}
+	}
+
+	@Autowired
+	MySpringService mySpringService;
+
+	@LocalServerPort
+	int port;
+
+	private static final HttpClient HTTP = HttpClient.newBuilder()
+		.connectTimeout(Duration.ofSeconds(5))
+		.followRedirects(HttpClient.Redirect.NEVER)
+		.build();
+
+	private static Object validMeta() {
+		return JsonMap.of(
+			RequestMeta.KEY_PROTOCOL_VERSION, "2026-07-28",
+			RequestMeta.KEY_CLIENT_INFO, JsonMap.of("name", "fixture-client", "version", "1.0"),
+			RequestMeta.KEY_CLIENT_CAPABILITIES, JsonMap.of());
+	}
+
+	private static String toolCallBody(String toolName) {
+		var params = JsonMap.of("name", toolName, "arguments", JsonMap.of(), "_meta", validMeta());
+		return Json.of(new JsonRpcRequest().setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1).setMethod("tools/call").setParams(params));
+	}
+
+	/**
+	 * Drives a real {@code tools/call} for the {@code whoAmI} tool over real HTTP against {@code path},
+	 * and returns the text content of the (successful) result.
+	 */
+	private String callWhoAmI(String path) throws Exception {
+		var req = HttpRequest.newBuilder()
+			.uri(URI.create("http://localhost:" + port + path))
+			.timeout(Duration.ofSeconds(10))
+			.header("Content-Type", "application/json")
+			.header("Accept", "application/json")
+			.header("Mcp-Method", "tools/call")
+			.header("Mcp-Name", "whoAmI")
+			.POST(BodyPublishers.ofString(toolCallBody("whoAmI")))
+			.build();
+		var resp = HTTP.send(req, BodyHandlers.ofString());
+		assertEquals(200, resp.statusCode(), () -> "Unexpected status; body=" + resp.body());
+		var envelope = Json.to(resp.body(), JsonMap.class);
+		assertNull(envelope.get("error"), () -> "Unexpected JSON-RPC error: " + envelope.get("error"));
+		var result = (Map<?,?>) envelope.get("result");
+		var content = (List<?>) result.get("content");
+		var first = (Map<?,?>) content.get(0);
+		return (String) first.get("text");
+	}
+
+	@Test
+	void a01_springMcpRestServlet_toolHandlerResolvesRealSpringManagedBean() throws Exception {
+		// SpringMcpRestServlet exposes the MCP endpoint via the McpEndpoint mixin's handleMcpRequest,
+		// which is annotated @RestPost(path = "/mcp") -- relative to this resource's own (default,
+		// root) @Rest path -- so the wire URL is basePath + "/mcp".
+		var marker = callWhoAmI("/mcp-spring/mcp");
+
+		assertNotEquals(NO_BEAN, marker, "Positive case: the Spring-DI-aware endpoint must resolve MySpringService");
+		assertEquals(mySpringService.getMarker(), marker,
+			"The MCP tool handler must resolve the SAME Spring-managed singleton this test class has @Autowired -- "
+				+ "proving BeanStore.getBean(...) really flowed through to Spring's ApplicationContext, not just "
+				+ "some independently-constructed lookalike");
+	}
+
+	@Test
+	void b01_plainServletSubclassPath_doesNotCarrySpringDi() throws Exception {
+		// Contrast/documentation case: same Spring Boot app, same ApplicationContext, same @Bean
+		// MySpringService -- but this endpoint extends McpRestServlet (not SpringMcpRestServlet /
+		// BasicSpringRestServlet), so its RestContext BeanStore was never bridged to Spring. This is a
+		// known, documented boundary of the recipe, not a bug: BeanStore.getBean(MySpringService.class)
+		// must come back empty here, in contrast to a01 above.
+		// McpRestServlet's operation is declared @RestPost(path = "/") -- its servlet root, not "/mcp"
+		// -- unlike the mixin path exercised in a01 above, so the wire URL is just basePath itself.
+		var marker = callWhoAmI("/mcp-plain");
+
+		assertEquals(NO_BEAN, marker,
+			"extends McpRestServlet (the plain servlet-subclass path, no Spring bridge) must NOT resolve the "
+				+ "Spring-managed bean -- this is the recipe's documented boundary, not a regression");
+	}
+}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpTemplateCompletionIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpTemplateCompletionIntegration_Test.java
index 80fef0c201..79298a9152 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpTemplateCompletionIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpTemplateCompletionIntegration_Test.java
@@ -216,7 +216,7 @@ class McpTemplateCompletionIntegration_Test {
 	public static class C_OverrideWithCompleterRegistered extends McpRestServlet {
 		private static final long serialVersionUID = 1L;
 		@Override protected McpServerConfig createMcpConfig() { return new McpServerConfig().addPrompt(promptWithCompleter()); }
-		@Override protected ServerCapabilities capabilities() { return explicit(); }
+		@Override protected McpOptions createMcpOptions() { return new McpOptions().setCapabilities(explicit()); }
 	}
 
 	private static String discover(Class<?> servlet) throws Exception {
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests.java
similarity index 61%
rename from juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess.java
rename to juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests.java
index 834f941255..638690c8f0 100644
--- a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess.java
+++ b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests.java
@@ -21,26 +21,33 @@ import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import java.util.*;
 
 import org.apache.juneau.bean.mcp.v20260728.*;
-import org.apache.juneau.marshall.collections.*;
 import org.apache.juneau.marshall.marshaller.*;
 
 /**
- * Static helper for driving MCP {@code 2026-07-28} SEP-2322 elicitation over {@link McpClient#callRaw}'s raw
- * {@code Map<String,Object>} result (MRTR, TODO-318's Multi-Round-Trip Requests loop, client side).
+ * Static helper for reading server&rarr;client MCP {@code 2026-07-28} SEP-2322 elicitation requests out of
+ * {@link McpClient#callRaw}'s raw {@code Map<String,Object>} result (MRTR, TODO-318's Multi-Round-Trip Requests
+ * loop, client side).
+ *
+ * <p>
+ * Paired with {@link ElicitationResponses}, which encodes the client's answers back into the wire shape a resume
+ * call expects &mdash; the same {@code ElicitationRequests}/{@code ElicitationResponses} split the server side
+ * already uses (server {@code ElicitationRequests} builds the pause signal; server {@code ElicitationResponses}
+ * reads the resumed answers).
  *
  * <p>
  * Works at the raw map/JSON level rather than exposing typed overloads per concrete request bean, because
  * {@code CallToolRequest}/{@code GetPromptRequest}/{@code ReadResourceRequest} share no common
  * "has-inputResponses-and-requestState" interface (each independently declares its own pair of fields; their
  * actual common base, {@code RequestParams<T>}, carries only {@code _meta}). A caller resuming, say, a
- * {@code CallToolRequest} calls {@link #toInputResponses(Map)} for the raw payload, then makes its own
- * {@code .setInputResponses(...).setRequestState(...)} call on the concrete bean it already knows it holds.
+ * {@code CallToolRequest} calls {@link ElicitationResponses#toInputResponses(Map)} for the raw payload, then
+ * makes its own {@code .setInputResponses(...).setRequestState(...)} call on the concrete bean it already knows
+ * it holds.
  *
  * @since 10.0.0
  */
-public final class ElicitationAccess {
+public final class ElicitationRequests {
 
-	private ElicitationAccess() {}
+	private ElicitationRequests() {}
 
 	/**
 	 * Whether a {@link McpClient#callRaw} result represents a paused elicitation (or any other MRTR pause).
@@ -106,50 +113,4 @@ public final class ElicitationAccess {
 		assertArgNotNull("raw", raw);
 		return (String) raw.get("requestState");
 	}
-
-	/**
-	 * Encodes a single typed {@link ElicitResult} into the raw {@code inputResponses} payload shape.
-	 *
-	 * @param id The server-assigned id this answer responds to.  Must not be <jk>null</jk>.
-	 * @param result The typed answer.  Must not be <jk>null</jk>.
-	 * @return A single-entry map ready to pass to {@link #toInputResponses(Map)}'s multi-answer form, or to hand
-	 * 	directly to a concrete request bean's {@code setInputResponses(...)} after merging with other answers.
-	 * @throws IllegalArgumentException If {@code id} or {@code result} is <jk>null</jk>.
-	 */
-	public static Map<String,Object> toInputResponse(String id, ElicitResult result) {
-		assertArgNotNull("id", id);
-		assertArgNotNull("result", result);
-		return toInputResponses(Map.of(id, result));
-	}
-
-	/**
-	 * Encodes several typed {@link ElicitResult}s into the raw {@code inputResponses} payload shape a resume
-	 * call's request bean expects (e.g. {@code CallToolRequest.setInputResponses(...)}).
-	 *
-	 * <p>
-	 * Unlike {@code ElicitationRequests.of(Map,Object)} (server side), an empty {@code results} map is accepted (returning
-	 * an empty map), not rejected &mdash; deliberately asymmetric, since a caller may legitimately resume with
-	 * zero elicitation answers if a round only carried non-elicitation MRTR pauses, whereas building a signal
-	 * with zero questions server-side would be a pointless pause.
-	 *
-	 * @param results Server-assigned-id-keyed typed answers.  Must not be <jk>null</jk>, and no value may be
-	 * 	<jk>null</jk>.  Output iteration order mirrors this map's order, so a caller wanting deterministic
-	 * 	ordering should pass a {@link LinkedHashMap}.
-	 * @return A keyed map of raw, wire-shaped answers.  Never <jk>null</jk> (empty if {@code results} was empty).
-	 * @throws IllegalArgumentException If {@code results} is <jk>null</jk>, or any value in it is <jk>null</jk>.
-	 */
-	public static Map<String,Object> toInputResponses(Map<String,ElicitResult> results) {
-		assertArgNotNull("results", results);
-		Map<String,Object> out = new LinkedHashMap<>();
-		results.forEach((id, result) -> {
-			assertArgNotNull("results[" + id + "]", result);
-			// Pre-marshalled to JsonMap here (rather than left as the typed ElicitResult for McpClient.call's
-			// own toWireParams flattening) so that (a) a null-check on the encoded shape is meaningful even
-			// when this helper is used standalone, outside McpClient.call's flow, and (b) the returned map is
-			// immediately wire-ready for a caller who hands it straight to a concrete request bean's
-			// setInputResponses(...) without ever going through McpClient at all.
-			out.put(id, Json.to(Json.of(result), JsonMap.class));
-		});
-		return out;
-	}
 }
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses.java
new file mode 100644
index 0000000000..19e1b96f54
--- /dev/null
+++ b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses.java
@@ -0,0 +1,89 @@
+/*
+ * 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.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.marshaller.*;
+
+/**
+ * Static helper for encoding the client's answers to server&rarr;client MCP {@code 2026-07-28} SEP-2322
+ * elicitation requests back into the raw {@code inputResponses} wire shape a resume call expects (MRTR,
+ * TODO-318's Multi-Round-Trip Requests loop, client side).
+ *
+ * <p>
+ * Paired with {@link ElicitationRequests}, which reads the server's questions out of a paused
+ * {@link McpClient#callRaw} result &mdash; the same {@code ElicitationRequests}/{@code ElicitationResponses}
+ * split the server side already uses (server {@code ElicitationRequests} builds the pause signal; server
+ * {@code ElicitationResponses} reads the resumed answers).
+ *
+ * @since 10.0.0
+ */
+public final class ElicitationResponses {
+
+	private ElicitationResponses() {}
+
+	/**
+	 * Encodes a single typed {@link ElicitResult} into the raw {@code inputResponses} payload shape.
+	 *
+	 * @param id The server-assigned id this answer responds to.  Must not be <jk>null</jk>.
+	 * @param result The typed answer.  Must not be <jk>null</jk>.
+	 * @return A single-entry map ready to pass to {@link #toInputResponses(Map)}'s multi-answer form, or to hand
+	 * 	directly to a concrete request bean's {@code setInputResponses(...)} after merging with other answers.
+	 * @throws IllegalArgumentException If {@code id} or {@code result} is <jk>null</jk>.
+	 */
+	public static Map<String,Object> toInputResponse(String id, ElicitResult result) {
+		assertArgNotNull("id", id);
+		assertArgNotNull("result", result);
+		return toInputResponses(Map.of(id, result));
+	}
+
+	/**
+	 * Encodes several typed {@link ElicitResult}s into the raw {@code inputResponses} payload shape a resume
+	 * call's request bean expects (e.g. {@code CallToolRequest.setInputResponses(...)}).
+	 *
+	 * <p>
+	 * Unlike {@code ElicitationRequests.of(Map,Object)} (server side), an empty {@code results} map is accepted
+	 * (returning an empty map), not rejected &mdash; deliberately asymmetric, since a caller may legitimately
+	 * resume with zero elicitation answers if a round only carried non-elicitation MRTR pauses, whereas building
+	 * a signal with zero questions server-side would be a pointless pause.
+	 *
+	 * @param results Server-assigned-id-keyed typed answers.  Must not be <jk>null</jk>, and no value may be
+	 * 	<jk>null</jk>.  Output iteration order mirrors this map's order, so a caller wanting deterministic
+	 * 	ordering should pass a {@link LinkedHashMap}.
+	 * @return A keyed map of raw, wire-shaped answers.  Never <jk>null</jk> (empty if {@code results} was empty).
+	 * @throws IllegalArgumentException If {@code results} is <jk>null</jk>, or any value in it is <jk>null</jk>.
+	 */
+	public static Map<String,Object> toInputResponses(Map<String,ElicitResult> results) {
+		assertArgNotNull("results", results);
+		Map<String,Object> out = new LinkedHashMap<>();
+		results.forEach((id, result) -> {
+			assertArgNotNull("results[" + id + "]", result);
+			// Pre-marshalled to JsonMap here (rather than left as the typed ElicitResult for McpClient.call's
+			// own toWireParams flattening) so that (a) a null-check on the encoded shape is meaningful even
+			// when this helper is used standalone, outside McpClient.call's flow, and (b) the returned map is
+			// immediately wire-ready for a caller who hands it straight to a concrete request bean's
+			// setInputResponses(...) without ever going through McpClient at all.
+			out.put(id, Json.to(Json.of(result), JsonMap.class));
+		});
+		return out;
+	}
+}
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 1ea2a92249..51ff43da35 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
@@ -459,13 +459,29 @@ public final class McpClient extends AbstractMcpClient {
 				// The stream ended (clean EOF, reader.hasNext() == false) without ever reaching a terminal
 				// frame, and the caller did not initiate this close (open is still true): an abrupt drop the
 				// listener must be told about, not silent - it is otherwise indistinguishable from a hang.
-				if (open && ! reachedTerminal)
+				//
+				// open is flipped to false BEFORE invoking the listener (guarded on the pre-read value so a
+				// concurrent cancel() can't cause a double-fire), not after in the finally block below: the
+				// terminal callback is very often the exact synchronization point (a latch/future countdown,
+				// or a direct isOpen() read from inside the callback itself) a caller relies on to learn the
+				// pump is done, and CountDownLatch/Future's happens-before is one-directional (it orders the
+				// signal before the waiter's wakeup, never the reverse) - so this volatile write must
+				// happen-before that signal, not after it, or a woken caller can transiently observe open
+				// still true.
+				if (open && ! reachedTerminal) {
+					open = false;
 					invokeListener(() -> listener.onError(new EOFException(
 						"Subscription stream closed before a terminal frame was received.")));
+				}
 			} catch (Exception e) {
-				if (open)
+				if (open) {
+					open = false;
 					invokeListener(() -> listener.onError(e));
+				}
 			} finally {
+				// Idempotent safety net: open is already false on every path above that invoked a terminal
+				// callback, and closeReaderQuietly() must run unconditionally regardless of which path (if
+				// any) was taken - including a plain open cancel()/close() with no callback at all.
 				open = false;
 				closeReaderQuietly();
 			}
@@ -483,6 +499,9 @@ public final class McpClient extends AbstractMcpClient {
 				return true;
 			}
 			var res = JsonParser.DEFAULT.read(data, JsonRpcResponse.class);
+			// open is flipped to false BEFORE invoking either terminal callback below, not after in run()'s
+			// finally block - see the longer rationale on the abrupt-EOF branch in run().
+			open = false;
 			if (res.getError() != null) {
 				var mcpException = McpException.fromJsonRpcError(res.getError());
 				invokeListener(() -> listener.onError(mcpException));
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess_Test.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess_Test.java
deleted file mode 100644
index cf0ba044fa..0000000000
--- a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationAccess_Test.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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.bean.mcp.v20260728.*;
-import org.apache.juneau.marshall.collections.*;
-import org.junit.jupiter.api.*;
-
-/**
- * Coverage for {@link ElicitationAccess}.
- */
-class ElicitationAccess_Test {
-
-	@Test void a01_isInputRequired_trueOnInputRequiredResultType() {
-		assertTrue(ElicitationAccess.isInputRequired(Map.of("resultType", "input_required")));
-	}
-
-	@Test void a02_isInputRequired_falseOnCompleteResultType() {
-		assertFalse(ElicitationAccess.isInputRequired(Map.of("resultType", "complete")));
-	}
-
-	@Test void a03_isInputRequired_nullRawThrows() {
-		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationAccess.isInputRequired(null));
-		assertEquals("Argument 'raw' cannot be null.", e.getMessage());
-	}
-
-	@Test void a04_requests_decodesTypedElicitRequestsFromRawMap() {
-		var raw = Map.<String,Object>of(
-			"resultType", "input_required",
-			"inputRequests", JsonMap.of(
-				"q1", JsonMap.of(
-					"message", "Pick",
-					"requestedSchema", JsonMap.of(
-						"type", "object",
-						"properties", JsonMap.of("choice", JsonMap.of("type", "string"))
-					)
-				)
-			)
-		);
-		var requests = ElicitationAccess.requests(raw);
-		var q1 = requests.get("q1");
-		assertEquals("Pick", q1.getMessage());
-		assertNotNull(q1.getRequestedSchema());
-	}
-
-	@Test void a05_requests_missingInputRequestsReturnsEmptyMap() {
-		var requests = ElicitationAccess.requests(Map.of("resultType", "complete"));
-		assertNotNull(requests);
-		assertTrue(requests.isEmpty());
-	}
-
-	@Test void a06_requestState_returnsEchoedToken() {
-		assertEquals("tok-abc", ElicitationAccess.requestState(Map.of("resultType", "input_required", "requestState", "tok-abc")));
-	}
-
-	@Test void a07_requestState_missingReturnsNull() {
-		assertNull(ElicitationAccess.requestState(Map.of("resultType", "complete")));
-	}
-
-	@Test void a08_toInputResponse_singleAnswer_encodesAcceptWithContent() {
-		var result = new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red");
-		var encoded = ElicitationAccess.toInputResponse("q1", result);
-		assertEquals(1, encoded.size());
-		var q1 = (JsonMap) encoded.get("q1");
-		assertEquals("accept", q1.getString("action"));
-		assertEquals("red", q1.getMap("content").getString("choice"));
-	}
-
-	@Test void a09_toInputResponses_multipleAnswers_encodesAll() {
-		var results = new LinkedHashMap<String,ElicitResult>();
-		results.put("q1", new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red"));
-		results.put("q2", new ElicitResult().setAction(ElicitAction.DECLINE));
-		var encoded = ElicitationAccess.toInputResponses(results);
-		assertEquals(2, encoded.size());
-		var q1 = (JsonMap) encoded.get("q1");
-		assertEquals("accept", q1.getString("action"));
-		assertEquals("red", q1.getMap("content").getString("choice"));
-		var q2 = (JsonMap) encoded.get("q2");
-		assertEquals("decline", q2.getString("action"));
-		assertNull(q2.get("content"));
-	}
-
-	@Test void a10_toInputResponse_nullIdThrows() {
-		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationAccess.toInputResponse(null, new ElicitResult()));
-		assertEquals("Argument 'id' cannot be null.", e.getMessage());
-	}
-
-	@Test void a11_toInputResponse_nullResultThrows() {
-		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationAccess.toInputResponse("q1", null));
-		assertEquals("Argument 'result' cannot be null.", e.getMessage());
-	}
-
-	@Test void a12_toInputResponses_nullMapThrows() {
-		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationAccess.toInputResponses(null));
-		assertEquals("Argument 'results' cannot be null.", e.getMessage());
-	}
-
-	/**
-	 * Documents the wire-shape symmetry between this client module's {@link ElicitationAccess} and the server
-	 * module's {@code ElicitationRequests}/{@code ElicitationResponses} (Phase 4) without introducing an illegal
-	 * cross-module test dependency: {@code juneau-rest-client-mcp-v20260728} must not depend on
-	 * {@code juneau-rest-server-mcp-v20260728}, so the server-side raw shape is hand-built here to match it
-	 * exactly (verified by inspection/spec cross-reference against {@code ElicitationRequests.of}/
-	 * {@code ElicitationResponses.get}, not a live cross-module call).
-	 */
-	@Test void a13_roundTrip_requestsToResponses_matchesServerSideHelpers() {
-		// Mirrors the raw shape ElicitationRequests.of("q1", elicitRequest, ...) would place into
-		// McpInputRequiredSignal.getInputRequests(), which callRaw ultimately surfaces as raw's "inputRequests".
-		var rawInputRequests = JsonMap.of(
-			"q1", JsonMap.of("message", "Pick one", "requestedSchema", JsonMap.of("type", "object"))
-		);
-		var raw = Map.<String,Object>of("resultType", "input_required", "requestState", "tok-abc", "inputRequests", rawInputRequests);
-
-		var decodedRequests = ElicitationAccess.requests(raw);
-		assertEquals("Pick one", decodedRequests.get("q1").getMessage());
-		assertEquals("tok-abc", ElicitationAccess.requestState(raw));
-
-		var answer = new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red");
-		var encodedResponses = ElicitationAccess.toInputResponse("q1", answer);
-
-		// This is exactly the shape ElicitationResponses.get(ctx, "q1") decodes via
-		// Json.to(Json.of(ctx.inputResponses().get("q1")), ElicitResult.class).
-		var q1 = (JsonMap) encodedResponses.get("q1");
-		assertEquals("accept", q1.getString("action"));
-		assertEquals("red", q1.getMap("content").getString("choice"));
-	}
-
-	@Test void a14_requests_malformedEntryThrows() {
-		// A JSON-array entry cannot be converted to a bean target: the marshaller throws (which a caller's own
-		// error handling surfaces). Mirrors ElicitationResponses_Test's a09_get_malformedShapeThrows for the
-		// server-side decode direction.
-		var raw = Map.<String,Object>of("resultType", "input_required", "inputRequests", JsonMap.of("q1", JsonList.of(1, 2)));
-		assertThrows(RuntimeException.class, () -> ElicitationAccess.requests(raw));
-	}
-
-	@Test void a15_toInputResponses_nullEntryThrows() {
-		var results = new LinkedHashMap<String,ElicitResult>();
-		results.put("q1", new ElicitResult().setAction(ElicitAction.ACCEPT));
-		results.put("q2", null);
-		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationAccess.toInputResponses(results));
-		assertEquals("Argument 'results[q2]' cannot be null.", e.getMessage());
-	}
-
-	@Test void a16_isInputRequired_falseOnMissingResultType() {
-		assertFalse(ElicitationAccess.isInputRequired(Map.of("foo", "bar")));
-	}
-
-	@Test void a17_requests_nullEntryDecodesToNull() {
-		// Mirrors ElicitationResponses.get()'s explicit null handling: a null raw value decodes to a null
-		// typed entry rather than round-tripping through the marshaller.
-		var rawInputRequests = new LinkedHashMap<String,Object>();
-		rawInputRequests.put("q1", null);
-		var raw = Map.<String,Object>of("resultType", "input_required", "inputRequests", rawInputRequests);
-		var requests = ElicitationAccess.requests(raw);
-		assertTrue(requests.containsKey("q1"));
-		assertNull(requests.get("q1"));
-	}
-}
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests_Test.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests_Test.java
new file mode 100644
index 0000000000..be3b50602e
--- /dev/null
+++ b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationRequests_Test.java
@@ -0,0 +1,99 @@
+/*
+ * 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.marshall.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Coverage for {@link ElicitationRequests}.
+ */
+class ElicitationRequests_Test {
+
+	@Test void a01_isInputRequired_trueOnInputRequiredResultType() {
+		assertTrue(ElicitationRequests.isInputRequired(Map.of("resultType", "input_required")));
+	}
+
+	@Test void a02_isInputRequired_falseOnCompleteResultType() {
+		assertFalse(ElicitationRequests.isInputRequired(Map.of("resultType", "complete")));
+	}
+
+	@Test void a03_isInputRequired_nullRawThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationRequests.isInputRequired(null));
+		assertEquals("Argument 'raw' cannot be null.", e.getMessage());
+	}
+
+	@Test void a04_requests_decodesTypedElicitRequestsFromRawMap() {
+		var raw = Map.<String,Object>of(
+			"resultType", "input_required",
+			"inputRequests", JsonMap.of(
+				"q1", JsonMap.of(
+					"message", "Pick",
+					"requestedSchema", JsonMap.of(
+						"type", "object",
+						"properties", JsonMap.of("choice", JsonMap.of("type", "string"))
+					)
+				)
+			)
+		);
+		var requests = ElicitationRequests.requests(raw);
+		var q1 = requests.get("q1");
+		assertEquals("Pick", q1.getMessage());
+		assertNotNull(q1.getRequestedSchema());
+	}
+
+	@Test void a05_requests_missingInputRequestsReturnsEmptyMap() {
+		var requests = ElicitationRequests.requests(Map.of("resultType", "complete"));
+		assertNotNull(requests);
+		assertTrue(requests.isEmpty());
+	}
+
+	@Test void a06_requestState_returnsEchoedToken() {
+		assertEquals("tok-abc", ElicitationRequests.requestState(Map.of("resultType", "input_required", "requestState", "tok-abc")));
+	}
+
+	@Test void a07_requestState_missingReturnsNull() {
+		assertNull(ElicitationRequests.requestState(Map.of("resultType", "complete")));
+	}
+
+	@Test void a08_requests_malformedEntryThrows() {
+		// A JSON-array entry cannot be converted to a bean target: the marshaller throws (which a caller's own
+		// error handling surfaces). Mirrors ElicitationResponses_Test's a05_toInputResponses_nullEntryThrows-style
+		// server-side decode direction coverage.
+		var raw = Map.<String,Object>of("resultType", "input_required", "inputRequests", JsonMap.of("q1", JsonList.of(1, 2)));
+		assertThrows(RuntimeException.class, () -> ElicitationRequests.requests(raw));
+	}
+
+	@Test void a09_isInputRequired_falseOnMissingResultType() {
+		assertFalse(ElicitationRequests.isInputRequired(Map.of("foo", "bar")));
+	}
+
+	@Test void a10_requests_nullEntryDecodesToNull() {
+		// Mirrors ElicitationResponses.get()'s explicit null handling (server side): a null raw value decodes
+		// to a null typed entry rather than round-tripping through the marshaller.
+		var rawInputRequests = new LinkedHashMap<String,Object>();
+		rawInputRequests.put("q1", null);
+		var raw = Map.<String,Object>of("resultType", "input_required", "inputRequests", rawInputRequests);
+		var requests = ElicitationRequests.requests(raw);
+		assertTrue(requests.containsKey("q1"));
+		assertNull(requests.get("q1"));
+	}
+}
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses_Test.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses_Test.java
new file mode 100644
index 0000000000..f2df2e05d9
--- /dev/null
+++ b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/ElicitationResponses_Test.java
@@ -0,0 +1,108 @@
+/*
+ * 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.bean.mcp.v20260728.*;
+import org.apache.juneau.marshall.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Coverage for {@link ElicitationResponses}.
+ */
+class ElicitationResponses_Test {
+
+	@Test void a01_toInputResponse_singleAnswer_encodesAcceptWithContent() {
+		var result = new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red");
+		var encoded = ElicitationResponses.toInputResponse("q1", result);
+		assertEquals(1, encoded.size());
+		var q1 = (JsonMap) encoded.get("q1");
+		assertEquals("accept", q1.getString("action"));
+		assertEquals("red", q1.getMap("content").getString("choice"));
+	}
+
+	@Test void a02_toInputResponses_multipleAnswers_encodesAll() {
+		var results = new LinkedHashMap<String,ElicitResult>();
+		results.put("q1", new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red"));
+		results.put("q2", new ElicitResult().setAction(ElicitAction.DECLINE));
+		var encoded = ElicitationResponses.toInputResponses(results);
+		assertEquals(2, encoded.size());
+		var q1 = (JsonMap) encoded.get("q1");
+		assertEquals("accept", q1.getString("action"));
+		assertEquals("red", q1.getMap("content").getString("choice"));
+		var q2 = (JsonMap) encoded.get("q2");
+		assertEquals("decline", q2.getString("action"));
+		assertNull(q2.get("content"));
+	}
+
+	@Test void a03_toInputResponse_nullIdThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationResponses.toInputResponse(null, new ElicitResult()));
+		assertEquals("Argument 'id' cannot be null.", e.getMessage());
+	}
+
+	@Test void a04_toInputResponse_nullResultThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationResponses.toInputResponse("q1", null));
+		assertEquals("Argument 'result' cannot be null.", e.getMessage());
+	}
+
+	@Test void a05_toInputResponses_nullMapThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationResponses.toInputResponses(null));
+		assertEquals("Argument 'results' cannot be null.", e.getMessage());
+	}
+
+	@Test void a06_toInputResponses_nullEntryThrows() {
+		var results = new LinkedHashMap<String,ElicitResult>();
+		results.put("q1", new ElicitResult().setAction(ElicitAction.ACCEPT));
+		results.put("q2", null);
+		var e = assertThrows(IllegalArgumentException.class, () -> ElicitationResponses.toInputResponses(results));
+		assertEquals("Argument 'results[q2]' cannot be null.", e.getMessage());
+	}
+
+	/**
+	 * Documents the wire-shape symmetry between this client module's {@link ElicitationRequests}/
+	 * {@link ElicitationResponses} pair and the server module's {@code ElicitationRequests}/
+	 * {@code ElicitationResponses} (Phase 4) without introducing an illegal cross-module test dependency:
+	 * {@code juneau-rest-client-mcp-v20260728} must not depend on {@code juneau-rest-server-mcp-v20260728}, so
+	 * the server-side raw shape is hand-built here to match it exactly (verified by inspection/spec
+	 * cross-reference against {@code ElicitationRequests.of}/{@code ElicitationResponses.get}, not a live
+	 * cross-module call).
+	 */
+	@Test void a07_roundTrip_requestsToResponses_matchesServerSideHelpers() {
+		// Mirrors the raw shape ElicitationRequests.of("q1", elicitRequest, ...) (server side) would place into
+		// McpInputRequiredSignal.getInputRequests(), which callRaw ultimately surfaces as raw's "inputRequests".
+		var rawInputRequests = JsonMap.of(
+			"q1", JsonMap.of("message", "Pick one", "requestedSchema", JsonMap.of("type", "object"))
+		);
+		var raw = Map.<String,Object>of("resultType", "input_required", "requestState", "tok-abc", "inputRequests", rawInputRequests);
+
+		var decodedRequests = ElicitationRequests.requests(raw);
+		assertEquals("Pick one", decodedRequests.get("q1").getMessage());
+		assertEquals("tok-abc", ElicitationRequests.requestState(raw));
+
+		var answer = new ElicitResult().setAction(ElicitAction.ACCEPT).putContent("choice", "red");
+		var encodedResponses = ElicitationResponses.toInputResponse("q1", answer);
+
+		// This is exactly the shape ElicitationResponses.get(ctx, "q1") (server side) decodes via
+		// Json.to(Json.of(ctx.inputResponses().get("q1")), ElicitResult.class).
+		var q1 = (JsonMap) encodedResponses.get("q1");
+		assertEquals("accept", q1.getString("action"));
+		assertEquals("red", q1.getMap("content").getString("choice"));
+	}
+}
diff --git a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClientSubscriptionsListen_Test.java b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClientSubscriptionsListen_Test.java
index 895dec0daf..604a443cfe 100644
--- a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClientSubscriptionsListen_Test.java
+++ b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClientSubscriptionsListen_Test.java
@@ -324,6 +324,181 @@ class McpClientSubscriptionsListen_Test {
 		}
 	}
 
+	/**
+	 * Blocks on {@code latch} swallowing {@link InterruptedException} (re-asserting the interrupt flag), so
+	 * the {@link McpSubscriptionListener} callbacks below - which override interfaces methods that declare no
+	 * checked exception - can wait on it inline.
+	 */
+	private static void awaitUninterruptibly(CountDownLatch latch) {
+		try {
+			latch.await();
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+		}
+	}
+
+	/**
+	 * Regression coverage for the happens-before race root-caused from a CI flake in {@link #a01_listen_singleStream_deliversAckThenNotificationsThenComplete_inOrder()}:
+	 * {@code assertFalse(handle.isOpen())} right after {@code done.await()} intermittently saw {@code true}
+	 * because {@code SubscriptionPump} used to flip its {@code open} flag to <jk>false</jk> in the
+	 * {@code finally} block - i.e. AFTER invoking the terminal listener callback - so a caller thread woken
+	 * from a latch/future the callback itself completes could observe the callback's effects while
+	 * {@code open} was still <jk>true</jk> ({@code CountDownLatch}'s happens-before is one-directional: it
+	 * only orders {@code countDown()} before the matching {@code await()} return, not the reverse).
+	 *
+	 * <p>
+	 * Rather than reproduce that as a timing-dependent flake, {@code b01}-{@code b04} below observe
+	 * {@code isOpen()} from a place where the ordering is no longer a race at all: <i>from inside the
+	 * terminal callback itself</i>, which always runs on the pump thread strictly before the pump thread
+	 * reaches its {@code finally} block. By the time {@code onComplete()}/{@code onError(Throwable)} starts
+	 * running, the pump has therefore already made its "did I flip {@code open} before or after invoking
+	 * this callback" decision - reading {@code isOpen()} from inside the callback can only ever observe
+	 * whichever the pump did first, deterministically:
+	 * <ul>
+	 * 	<li>Buggy ({@code open=false} only in {@code finally}): the callback observes {@code open==true}.
+	 * 	<li>Fixed ({@code open=false} immediately before {@code invokeListener(...)} at each terminal site):
+	 * 		the callback observes {@code open==false}.
+	 * </ul>
+	 *
+	 * <p>
+	 * The one wrinkle is that the callback needs a reference to the handle {@link McpClient#listen} returns,
+	 * and that return does not happen until after the pump thread has already been started - so the pump
+	 * thread could reach the callback before the test thread has published the handle. Each test below closes
+	 * that window with a small handoff gate ({@code handleReady}): the callback blocks on it before reading
+	 * {@code isOpen()}. That gate only delays <i>when</i> {@code isOpen()} is read, never <i>what</i> it
+	 * reads - the buggy-vs-fixed ordering decision the assertion is pinning was already made by the pump
+	 * before it ever called into the callback, so blocking briefly inside the callback cannot change it.
+	 */
+	@Test
+	void b01_isOpenObservedFromWithinOnComplete_isFalse_notTrueEvenThoughFinallyHasNotRunYet() throws Exception {
+		var ack = new SubscriptionsAcknowledgedNotification().setNotifications(new SubscriptionFilter())
+			.setMeta(new RequestMeta().set(RequestMeta.KEY_SUBSCRIPTION_ID, "sub-b1"));
+		var inbound =
+			"data: {\"jsonrpc\":\"2.0\",\"method\":\"" + McpMethods.NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED + "\",\"params\":" + JsonSerializer.DEFAULT.write(ack) + "}\n\n"
+			+ "data: {\"jsonrpc\":\"2.0\",\"id\":\"sub-b1\",\"result\":{\"resultType\":\"complete\"}}\n\n";
+		HttpTransport transport = tReq -> TransportResponse.builder().statusCode(200)
+			.header("Content-Type", "text/event-stream")
+			.body(new ByteArrayInputStream(inbound.getBytes(StandardCharsets.UTF_8))).build();
+
+		var handleRef = new AtomicReference<McpSubscriptionHandle>();
+		var handleReady = new CountDownLatch(1);
+		var capturedOpen = new AtomicBoolean(true);
+		var done = new CountDownLatch(1);
+		var listener = new McpSubscriptionListener() {
+			@Override public void onComplete() {
+				awaitUninterruptibly(handleReady);
+				capturedOpen.set(handleRef.get().isOpen());
+				done.countDown();
+			}
+		};
+
+		try (var client = McpClient.builder().endpoint("http://x/mcp").transport(transport).build()) {
+			var handle = client.listen(new SubscriptionFilter(), listener);
+			handleRef.set(handle);
+			handleReady.countDown();
+			assertTrue(done.await(5, TimeUnit.SECONDS), "onComplete never fired");
+			assertFalse(capturedOpen.get(),
+				"isOpen() read from inside onComplete() itself must already be false - open must be flipped "
+				+ "before the terminal callback is invoked, not after, in the finally block");
+		}
+	}
+
+	@Test
+	void b02_isOpenObservedFromWithinOnError_forServerErrorFrame_isFalse_notTrueEvenThoughFinallyHasNotRunYet() throws Exception {
+		var inbound = "data: {\"jsonrpc\":\"2.0\",\"id\":\"sub-b2\",\"error\":{\"code\":-32000,\"message\":\"boom\"}}\n\n";
+		HttpTransport transport = tReq -> TransportResponse.builder().statusCode(200)
+			.header("Content-Type", "text/event-stream")
+			.body(new ByteArrayInputStream(inbound.getBytes(StandardCharsets.UTF_8))).build();
+
+		var handleRef = new AtomicReference<McpSubscriptionHandle>();
+		var handleReady = new CountDownLatch(1);
+		var capturedOpen = new AtomicBoolean(true);
+		var done = new CountDownLatch(1);
+		var listener = new McpSubscriptionListener() {
+			@Override public void onError(Throwable t) {
+				awaitUninterruptibly(handleReady);
+				capturedOpen.set(handleRef.get().isOpen());
+				done.countDown();
+			}
+		};
+
+		try (var client = McpClient.builder().endpoint("http://x/mcp").transport(transport).build()) {
+			var handle = client.listen(new SubscriptionFilter(), listener);
+			handleRef.set(handle);
+			handleReady.countDown();
+			assertTrue(done.await(5, TimeUnit.SECONDS), "onError never fired for a server JSON-RPC error terminal frame");
+			assertFalse(capturedOpen.get(),
+				"isOpen() read from inside onError() itself must already be false for the dispatch(...) "
+				+ "error-terminal-frame branch");
+		}
+	}
+
+	@Test
+	void b03_isOpenObservedFromWithinOnError_forAbruptEof_isFalse_notTrueEvenThoughFinallyHasNotRunYet() throws Exception {
+		var ack = new SubscriptionsAcknowledgedNotification().setNotifications(new SubscriptionFilter())
+			.setMeta(new RequestMeta().set(RequestMeta.KEY_SUBSCRIPTION_ID, "sub-b3"));
+		// Deliberately no terminal frame, exactly like a04: the stream just ends (clean EOF).
+		var inbound =
+			"data: {\"jsonrpc\":\"2.0\",\"method\":\"" + McpMethods.NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED + "\",\"params\":" + JsonSerializer.DEFAULT.write(ack) + "}\n\n";
+		HttpTransport transport = tReq -> TransportResponse.builder().statusCode(200)
+			.header("Content-Type", "text/event-stream")
+			.body(new ByteArrayInputStream(inbound.getBytes(StandardCharsets.UTF_8))).build();
+
+		var handleRef = new AtomicReference<McpSubscriptionHandle>();
+		var handleReady = new CountDownLatch(1);
+		var capturedOpen = new AtomicBoolean(true);
+		var done = new CountDownLatch(1);
+		var listener = new McpSubscriptionListener() {
+			@Override public void onError(Throwable t) {
+				awaitUninterruptibly(handleReady);
+				capturedOpen.set(handleRef.get().isOpen());
+				done.countDown();
+			}
+		};
+
+		try (var client = McpClient.builder().endpoint("http://x/mcp").transport(transport).build()) {
+			var handle = client.listen(new SubscriptionFilter(), listener);
+			handleRef.set(handle);
+			handleReady.countDown();
+			assertTrue(done.await(5, TimeUnit.SECONDS), "onError never fired for an abrupt EOF with no terminal frame");
+			assertFalse(capturedOpen.get(),
+				"isOpen() read from inside onError() itself must already be false for run()'s abrupt-EOF "
+				+ "(reached-terminal==false) branch");
+		}
+	}
+
+	@Test
+	void b04_isOpenObservedFromWithinOnError_forMalformedFrameDecodeException_isFalse_notTrueEvenThoughFinallyHasNotRunYet() throws Exception {
+		// Same malformed payload as a03, but this exercises run()'s catch (Exception e) branch specifically
+		// (the decode failure is thrown out of dispatch(...) itself, not returned as a JSON-RPC error frame).
+		var inbound = "data: {not valid json\n\n";
+		HttpTransport transport = tReq -> TransportResponse.builder().statusCode(200)
+			.header("Content-Type", "text/event-stream")
+			.body(new ByteArrayInputStream(inbound.getBytes(StandardCharsets.UTF_8))).build();
+
+		var handleRef = new AtomicReference<McpSubscriptionHandle>();
+		var handleReady = new CountDownLatch(1);
+		var capturedOpen = new AtomicBoolean(true);
+		var done = new CountDownLatch(1);
+		var listener = new McpSubscriptionListener() {
+			@Override public void onError(Throwable t) {
+				awaitUninterruptibly(handleReady);
+				capturedOpen.set(handleRef.get().isOpen());
+				done.countDown();
+			}
+		};
+
+		try (var client = McpClient.builder().endpoint("http://x/mcp").transport(transport).build()) {
+			var handle = client.listen(new SubscriptionFilter(), listener);
+			handleRef.set(handle);
+			handleReady.countDown();
+			assertTrue(done.await(5, TimeUnit.SECONDS), "onError never fired for a malformed frame");
+			assertFalse(capturedOpen.get(),
+				"isOpen() read from inside onError() itself must already be false for run()'s catch "
+				+ "(Exception e) decode-failure branch");
+		}
+	}
+
 	/**
 	 * A genuinely blocking {@link InputStream}: {@link #read()} parks on a latch (no busy-polling) until
 	 * {@link #release()} is called from another thread, then throws — modeling a real closed-socket read
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationRequests.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationRequests.java
index 334fb739f2..01a3b73672 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationRequests.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationRequests.java
@@ -63,7 +63,7 @@ public final class ElicitationRequests {
 	 * elicitations paused on together, resumed together).
 	 *
 	 * <p>
-	 * Unlike {@code ElicitationAccess.toInputResponses(Map)} (client side), an empty {@code requests} map is
+	 * Unlike {@code ElicitationResponses.toInputResponses(Map)} (client side), an empty {@code requests} map is
 	 * rejected, not accepted &mdash; deliberately asymmetric, since a signal built with zero questions would be
 	 * a pointless pause, whereas a caller resuming may legitimately have zero elicitation answers on hand if
 	 * the round it is resuming only carried non-elicitation MRTR pauses.
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpoint.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpoint.java
index c26a47a2de..b3b0de127f 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpoint.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpoint.java
@@ -16,9 +16,6 @@
  */
 package org.apache.juneau.rest.server.mcp.v20260728;
 
-import java.util.*;
-
-import org.apache.juneau.bean.mcp.v20260728.*;
 import org.apache.juneau.commons.inject.Bean;
 import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
 import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
@@ -28,8 +25,9 @@ import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
  * resource.
  *
  * <p>
- * The mixin path and the servlet-subclass path ({@link McpRestServlet}) are at parity: both bind the
- * revision at compile time and both expose the same {@link #capabilities()} hook.
+ * The mixin path and the servlet-subclass path ({@link McpRestServlet}) are at parity: both bind the revision
+ * at compile time and both read the same {@link McpOptions} shape for behavior configuration
+ * (capabilities/instructions/cache/mrtr/subscriptions/broker) via a single override.
  *
  * <h5 class='section'>Example:</h5>
  * <pre>
@@ -39,6 +37,10 @@ import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
  *     public McpServerConfig getMcpConfig() {
  *         return new McpServerConfig().addTool(new MyEchoTool());
  *     }
+ *     @Override                                        // optional
+ *     public McpOptions getMcpOptions() {
+ *         return new McpOptions().mrtr(m -&gt; m.setTtlMs(600_000));
+ *     }
  * }
  * </pre>
  */
@@ -46,8 +48,8 @@ public interface McpEndpoint extends org.apache.juneau.rest.server.mcp.McpEndpoi
 
 	@Override /* McpEndpointMixin */
 	default org.apache.juneau.rest.server.mcp.McpRevision revision() {
-		return new McpRevision(capabilities(), Objects.requireNonNull(cacheConfig(), "cacheConfig"), instructions(),
-			Objects.requireNonNull(mrtrConfig(), "mrtrConfig"));
+		var o = McpEndpointOptionsCache.resolve(this);
+		return new McpRevision(o.getCapabilities(), o.getCache(), o.getInstructions(), o.getMrtr());
 	}
 
 	/**
@@ -67,137 +69,60 @@ public interface McpEndpoint extends org.apache.juneau.rest.server.mcp.McpEndpoi
 	}
 
 	/**
-	 * Optional explicit capabilities advertisement for {@code server/discover}.
-	 *
-	 * <p>
-	 * Returning <jk>null</jk> (the default) leaves capabilities auto-derived from the registered
-	 * tool / prompt / resource lists; a non-<jk>null</jk> value bypasses auto-derivation.
-	 *
-	 * @return The explicit capabilities, or <jk>null</jk> to auto-derive.
-	 */
-	default ServerCapabilities capabilities() {
-		return null;
-	}
-
-	/**
-	 * Optional free-form {@code server/discover} usage instructions advertised to clients.
-	 *
-	 * <p>
-	 * Returning <jk>null</jk> (the default) omits the {@code instructions} member from the discovery
-	 * result.
-	 *
-	 * @return The instructions, or <jk>null</jk> to omit them.
-	 */
-	default String instructions() {
-		return null;
-	}
-
-	/**
-	 * Cache configuration for this endpoint's bound revision.
-	 *
-	 * <p>
-	 * The default returns an empty {@link McpCacheConfig} (no cache hints emitted on any result).
-	 * Override to supply TTL/scope hints. An overriding implementation must return a stable instance
-	 * that is not mutated after it is returned — this mixin does not lazily cache the result the way
-	 * the servlet-subclass path does.
-	 *
-	 * @return The cache configuration. Must not be <jk>null</jk>.
-	 */
-	default McpCacheConfig cacheConfig() {
-		return new McpCacheConfig();
-	}
-
-	/**
-	 * MRTR (Multi-Round-Trip Request) configuration for this endpoint's bound revision.
-	 *
-	 * <p>
-	 * The default returns a <b>per-process shared</b> {@link McpMrtrConfig} (AES-GCM ephemeral codec, 5-minute
-	 * {@code requestState} TTL, 10-round cap) &mdash; a single JVM-wide instance holding one ephemeral AES key,
-	 * lazily created on first use (see {@link SharedMrtrConfig}). Because {@link #revision()} constructs a fresh
-	 * {@link McpRevision} on every request, returning that same shared instance from every call is what lets a
-	 * {@code requestState} sealed on a PAUSE request be unsealed on the follow-up RESUME request; a fresh
-	 * {@link McpMrtrConfig} per call would mint a fresh AES key per request and make every RESUME fail. So the
-	 * common mixin case is resumable out of the box with no override.
-	 *
-	 * <p>
-	 * Override to supply a custom {@link RequestStateCodec}, TTL, or max-rounds cap. <b>An overriding
-	 * implementation must return a stable instance</b> (e.g. stored in a field) that is not mutated after it is
-	 * returned — this mixin does not lazily cache an override the way the servlet-subclass path
-	 * ({@link McpRestServlet#getMrtrConfig()}) does, and returning a fresh custom instance per call would
-	 * reintroduce the per-request-key RESUME failure the default avoids.
-	 *
-	 * @return The MRTR configuration. Must not be <jk>null</jk>.
-	 */
-	default McpMrtrConfig mrtrConfig() {
-		return SharedMrtrConfig.get();
-	}
-
-	/**
-	 * Subscriptions configuration for this endpoint's bound revision.
+	 * This endpoint's behavior configuration &mdash; capabilities, instructions, cache hints, MRTR, subscriptions,
+	 * and the subscription broker, all in one place.
 	 *
 	 * <p>
-	 * The default returns a fresh {@link McpSubscriptionsConfig} (all defaults) on every call — knobs-only, so
-	 * unlike {@link #subscriptionBroker()} there is no live state a fresh instance could split, matching
-	 * {@link #cacheConfig()}'s precedent exactly. Override to supply custom
-	 * concurrency/queue/heartbeat/idle-timeout knobs.
+	 * The default returns a per-binding-fresh {@link McpOptions} (all defaults; no live-state sharing across
+	 * endpoint instances). Override to customize any of the six concepts.
 	 *
 	 * <p>
-	 * <b>Warning:</b> overriding {@link McpSubscriptionsConfig#getQueueSize() queueSize} here has <b>no
-	 * effect</b> on the JVM-wide default shared broker {@link #subscriptionBroker()} returns — that broker is
-	 * already sized from {@link McpSubscriptionsConfig#DEFAULT_QUEUE_SIZE} the first time it is lazily created
-	 * (see {@link SharedSubscriptionBroker}), before this method's returned queue size could ever be read. A
-	 * non-default queue size only takes effect if {@link #subscriptionBroker()} is also overridden to size a
-	 * broker from it.
-	 *
-	 * @return The subscriptions configuration. Must not be <jk>null</jk>.
+	 * <b>This method is invoked exactly once per endpoint instance</b> and the result is memoized for that
+	 * instance's lifetime by the framework internals that read it ({@link #revision()},
+	 * {@link #subscriptionBroker()}, the {@code @Bean}-published accessor) &mdash; so an override may freely
+	 * construct and configure a fresh instance on every call, exactly as shown above; it never needs to cache the
+	 * result itself, and returning a fresh instance per call cannot reintroduce the "must return a stable
+	 * instance" footgun the pre-consolidation per-concept hooks warned about.
+	 *
+	 * @return This endpoint's options. Must not be <jk>null</jk>.
 	 */
-	default McpSubscriptionsConfig subscriptionsConfig() {
-		return new McpSubscriptionsConfig();
+	default McpOptions getMcpOptions() {
+		return new McpOptions();
 	}
 
 	/**
-	 * Publishes {@link #subscriptionsConfig()} into this resource's {@code RestContext} bean store, the
-	 * same {@code @Bean}-method mechanism already used by {@link #mcpTraceContextExtractor()}.
+	 * Publishes this endpoint's memoized {@link McpOptions} into this resource's {@code RestContext} bean store,
+	 * the same {@code @Bean}-method mechanism already used by {@link #mcpTraceContextExtractor()}.
 	 *
 	 * <p>
-	 * This is what lets {@code McpRevision.dispatch(...)}'s {@code subscriptions/listen} branch resolve
-	 * the real, possibly-overridden config via {@code ctx.getBean(McpSubscriptionsConfig.class)} instead
-	 * of always falling through to that call's defensive {@code new McpSubscriptionsConfig()} default —
-	 * the neutral core cannot add this bean itself ({@code McpSubscriptionsConfig} is a v2 type, and the
-	 * neutral module must not import it), so this v2-side {@code @Bean} method is the wiring's only seam.
+	 * This is what lets {@code McpRevision.dispatch(...)}'s {@code subscriptions/listen} branch resolve the
+	 * real, possibly-overridden subscriptions configuration via {@code ctx.getBean(McpOptions.class)} instead of
+	 * always falling through to that call's defensive {@code new McpSubscriptionsConfig()} default &mdash; the
+	 * neutral core cannot add this bean itself ({@code McpOptions} is a v2 type, and the neutral module must not
+	 * import it), so this v2-side {@code @Bean} method is the wiring's only seam.
 	 *
-	 * @return This endpoint's subscriptions configuration. Never <jk>null</jk>.
+	 * @return This endpoint's memoized options. Never <jk>null</jk>.
 	 */
 	@Bean
-	default McpSubscriptionsConfig subscriptionsConfigBean() {
-		return subscriptionsConfig();
+	default McpOptions mcpOptionsBean() {
+		return McpEndpointOptionsCache.resolve(this);
 	}
 
 	/**
 	 * Subscription broker for this endpoint's bound revision.
 	 *
 	 * <p>
-	 * The default returns a <b>per-process shared</b> {@link McpSubscriptionBroker} (see
-	 * {@link SharedSubscriptionBroker}) — a single JVM-wide registry, lazily created on first use — since a
-	 * fresh broker per call (unlike the knobs-only {@link #subscriptionsConfig()}) would silently split live
-	 * subscribers and publishers across unrelated registries. Override to supply a custom broker; an
-	 * overriding implementation must return a stable instance (this mixin does not lazily cache an override
-	 * the way {@link McpRestServlet#getSubscriptionBroker()} does).
-	 *
-	 * <p>
-	 * Because this registry is shared JVM-wide, every live subscription still shares one namespace across
-	 * however many mixin resources bind this same broker — but {@code subscriptions/listen}'s dispatch
-	 * branch registers each stream under a fresh server-minted key (a random {@code UUID}, not the
-	 * client-supplied JSON-RPC request id), so distinct listens can never collide and evict each other's
-	 * stream merely because two unrelated clients (or the same client, twice) happened to reuse the same
-	 * id. The only thing still shared process-wide is capacity: {@code maxConcurrentSubscriptions} is
-	 * enforced against this one registry's total active count, mirroring the same per-process-sharing
-	 * trade-off {@link #mrtrConfig()} documents for its shared AES key.
-	 *
-	 * @return The subscription broker. Must not be <jk>null</jk>.
+	 * Reads {@link #getMcpOptions()}'s memoized {@link McpOptions#getSubscriptionBroker()}: an explicitly
+	 * configured broker is returned as-is; otherwise one is derived and memoized per-binding, sized from
+	 * {@link McpOptions#getSubscriptions()}'s queue bound (see {@link McpOptions#resolveSubscriptionBroker()}).
+	 * Because {@link McpOptions} itself is memoized once per endpoint instance ({@link McpEndpointOptionsCache}),
+	 * two distinct endpoint instances resolve to two distinct broker registries by default &mdash; there is no
+	 * accidental JVM-wide sharing the way the pre-consolidation mixin default had.
+	 *
+	 * @return The subscription broker. Never <jk>null</jk>.
 	 */
 	@Override
 	default McpSubscriptionBroker subscriptionBroker() {
-		return SharedSubscriptionBroker.get();
+		return McpEndpointOptionsCache.resolve(this).resolveSubscriptionBroker();
 	}
 }
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache.java
new file mode 100644
index 0000000000..d1dec9ba5b
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Per-endpoint-instance memoized {@link McpOptions} resolution for the {@link McpEndpoint} mixin path.
+ *
+ * <p>
+ * The mixin path has no per-instance field to memoize onto (it is an interface, not a class), and
+ * {@link McpEndpoint#getMcpOptions()} is deliberately the raw, unmemoized user override point &mdash; its
+ * default returns a fresh {@link McpOptions} on every call, and an overriding implementation is free to do the
+ * same (see that method's Javadoc example). Internal plumbing that needs a <b>stable</b>, per-binding
+ * {@link McpOptions} ({@link McpEndpoint#revision()}, {@link McpEndpoint#subscriptionBroker()}, and the
+ * {@code @Bean}-published accessor) resolves it through this cache instead of calling
+ * {@link McpEndpoint#getMcpOptions()} directly: the first resolution for a given endpoint instance invokes
+ * {@link McpEndpoint#getMcpOptions()} exactly once and publishes the result; every later resolution for that
+ * <i>same</i> instance returns the identical object, while a <i>different</i> endpoint instance resolves to a
+ * distinct {@link McpOptions} &mdash; giving per-binding memoization "for free" regardless of whether
+ * {@link McpEndpoint#getMcpOptions()} itself was overridden, and regardless of whether a {@code RestContext} has
+ * been initialized (unlike the {@code RestContext} bean store, this cache works against a bare-constructed
+ * mixin instance too, which is what lets direct-construction unit tests exercise {@link McpEndpoint#revision()}
+ * without standing up a full REST context).
+ *
+ * <p>
+ * Keyed by <b>reference identity</b>, not {@code equals()}/{@code hashCode()}: a resource class that overrides
+ * those (a record, a Lombok {@code @Data} class, or any other value-ish endpoint type) must still get its own
+ * cache entry per distinct instance, or two unrelated bindings would collapse onto one shared {@link McpOptions}
+ * &mdash; silently reintroducing the JVM-wide MRTR-key/broker sharing this whole cache exists to eliminate. The
+ * key type is a small {@link WeakReference} subclass whose {@code hashCode()} is the referent's
+ * {@link System#identityHashCode(Object) identity hash} (captured at construction, so it survives the referent
+ * being cleared) and whose {@code equals()} is reference equality (<code>==</code>) on the referent, never
+ * delegating to the endpoint's own {@code equals()}/{@code hashCode()}.
+ *
+ * <p>
+ * Backed by a lock-free {@link ConcurrentHashMap} (no shared monitor serializing mixin traffic across the JVM,
+ * unlike a {@code synchronized}-guarded {@code WeakHashMap}): the read/populate path is a single
+ * {@link ConcurrentHashMap#computeIfAbsent} call. An endpoint instance that becomes otherwise unreachable does
+ * not leak its cache entry: stale keys (whose referent has been garbage-collected) are enqueued on a
+ * {@link ReferenceQueue} and drained &mdash; removing themselves from the map &mdash; at the start of every
+ * {@link #resolve} call. (As a side effect of using {@code computeIfAbsent}, a {@link McpEndpoint#getMcpOptions()}
+ * override that re-enters {@link #resolve} for the same endpoint instance fails fast with an
+ * {@code IllegalStateException} ("Recursive update"), rather than the unbounded recursion / {@code StackOverflowError}
+ * a {@code WeakHashMap}-backed implementation would suffer.)
+ */
+final class McpEndpointOptionsCache {
+
+	private McpEndpointOptionsCache() {}
+
+	private static final ReferenceQueue<McpEndpoint> QUEUE = new ReferenceQueue<>();
+	private static final Map<IdentityKey,McpOptions> CACHE = new ConcurrentHashMap<>();
+
+	/**
+	 * Resolves {@code endpoint}'s memoized {@link McpOptions}, invoking {@link McpEndpoint#getMcpOptions()}
+	 * exactly once per endpoint instance.
+	 *
+	 * @param endpoint The endpoint instance to resolve. Must not be <jk>null</jk>.
+	 * @return The memoized options. Never <jk>null</jk>.
+	 */
+	static McpOptions resolve(McpEndpoint endpoint) {
+		expungeStaleEntries();
+		return CACHE.computeIfAbsent(new IdentityKey(endpoint), IdentityKey::resolveOptions);
+	}
+
+	/**
+	 * Drains {@link #QUEUE} of keys whose referent has been garbage-collected, removing the corresponding
+	 * (now-orphaned) cache entry. Called on every {@link #resolve} so the cache never grows unbounded with
+	 * entries for endpoint instances nothing references anymore.
+	 */
+	private static void expungeStaleEntries() {
+		Reference<? extends McpEndpoint> ref;
+		while ((ref = QUEUE.poll()) != null)
+			CACHE.remove(ref);
+	}
+
+	/**
+	 * A weak, identity-keyed {@link McpEndpoint} reference usable as a {@link Map} key: {@code hashCode()} is
+	 * the referent's identity hash (captured up front, so it remains stable even after the referent is
+	 * cleared), and {@code equals()} is reference equality on the referent &mdash; deliberately never
+	 * delegating to the endpoint's own {@code equals()}/{@code hashCode()}, since those may be value-based.
+	 */
+	private static final class IdentityKey extends WeakReference<McpEndpoint> {
+		private final int hash;
+
+		IdentityKey(McpEndpoint referent) {
+			super(referent, QUEUE);
+			hash = System.identityHashCode(referent);
+		}
+
+		McpOptions resolveOptions() {
+			var endpoint = get();
+			return endpoint == null ? new McpOptions() : endpoint.getMcpOptions();
+		}
+
+		@Override
+		public int hashCode() {
+			return hash;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o)
+				return true;
+			if (!(o instanceof IdentityKey other))
+				return false;
+			var mine = get();
+			return mine != null && mine == other.get();
+		}
+	}
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions.java
new file mode 100644
index 0000000000..f642003243
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions.java
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.util.function.Consumer;
+
+import org.apache.juneau.bean.mcp.v20260728.ServerCapabilities;
+import org.apache.juneau.rest.server.mcp.BasicMcpSubscriptionBroker;
+import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
+
+/**
+ * Binding-owned behavior configuration ("how" a {@code 2026-07-28} MCP endpoint behaves) &mdash; the single
+ * v2-only aggregate consolidating every per-binding tunable {@link McpEndpoint}/{@link McpRestServlet} previously
+ * exposed as six separate hooks: {@code capabilities}, {@code instructions}, {@code cache}, {@code mrtr},
+ * {@code subscriptions}, and {@code subscriptionBroker}.
+ *
+ * <p>
+ * <b>Mutable during setup, effectively immutable once published.</b> Same builder-less mutable-setup idiom as
+ * {@link McpCacheConfig}/{@link McpMrtrConfig}/{@link McpSubscriptionsConfig} and the neutral
+ * {@code org.apache.juneau.rest.server.mcp.McpServerConfig} it sits next to: configure fully via the single
+ * {@code createMcpOptions()} (servlet-subclass path) / {@code getMcpOptions()} (mixin path) override, then never
+ * mutate it afterward. The framework memoizes exactly one instance per binding (see {@link McpRestServlet#getMcpOptions()}
+ * and {@link McpEndpoint#getMcpOptions()}), so a request-time reader sees a stable, request-independent snapshot.
+ *
+ * <p>
+ * The three nested configs ({@link #getCache() cache}, {@link #getMrtr() mrtr}, {@link #getSubscriptions()
+ * subscriptions}) are framework-owned instances created once, here, at construction time. Use the
+ * {@link #cache(Consumer)}/{@link #mrtr(Consumer)}/{@link #subscriptions(Consumer)} configure-blocks to mutate
+ * them in place &mdash; callers never {@code new} a replacement nested config, so the "must return a stable
+ * instance" footgun the pre-consolidation hooks warned about cannot recur.
+ *
+ * <h5 class='section'>The subscription broker default:</h5>
+ * <p>
+ * {@link #getSubscriptionBroker()} defaults to <jk>null</jk>, meaning "framework-derived": the binding lazily
+ * derives a {@link BasicMcpSubscriptionBroker} sized from {@link #getSubscriptions()}'s
+ * {@link McpSubscriptionsConfig#getQueueSize() queueSize} the first time a broker is needed, and memoizes that
+ * one derived instance for the lifetime of the binding (a fresh broker per call would silently split live
+ * subscribers and publishers across unrelated registries). Call {@link #setSubscriptionBroker(McpSubscriptionBroker)}
+ * to supply a custom broker instead (for example one shared across multiple bindings on purpose).
+ *
+ * <h5 class='section'>The idleTimeoutMs / heartbeatIntervalMs caveat:</h5>
+ * <p>
+ * When configuring {@link #subscriptions(Consumer)}, keep
+ * {@link McpSubscriptionsConfig#getHeartbeatIntervalMs() heartbeatIntervalMs} below
+ * {@link McpSubscriptionsConfig#getIdleTimeoutMs() idleTimeoutMs} (see that method's Javadoc) &mdash; otherwise
+ * the idle-timeout watchdog can reap a perfectly healthy connection between two heartbeats.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <pre>
+ * McpOptions <jv>options</jv> = <jk>new</jk> McpOptions()
+ *     .setInstructions(<js>"Use tool 'echo' to test."</js>)
+ *     .cache(<jv>c</jv> -&gt; <jv>c</jv>.setToolsList(<jk>new</jk> McpCacheHint().setTtlMs(60_000)))
+ *     .subscriptions(<jv>s</jv> -&gt; <jv>s</jv>.setQueueSize(2048));
+ * </pre>
+ */
+public class McpOptions {
+
+	private ServerCapabilities capabilities;
+	private String instructions;
+	private McpCacheConfig cache = new McpCacheConfig();
+	private McpMrtrConfig mrtr = new McpMrtrConfig();
+	private McpSubscriptionsConfig subscriptions = new McpSubscriptionsConfig();
+	private McpSubscriptionBroker subscriptionBroker;
+
+	@SuppressWarnings({
+		"java:S3077" // Volatile is required for correct double-checked locking in resolveSubscriptionBroker(): unlike a plain lazy read, two BasicMcpSubscriptionBroker instances are NOT equivalent (each holds distinct live per-connection subscription state), so exactly one derived broker must ever be created and published or a subscription registered against one instance would never be visible to a publish call against the other.
+	})
+	private volatile McpSubscriptionBroker derivedBroker;
+
+	private final Object lock = new Object();
+
+	/**
+	 * Optional explicit capabilities advertisement for {@code server/discover}.
+	 *
+	 * @return The explicit capabilities, or <jk>null</jk> to auto-derive from the registered tool / prompt /
+	 * 	resource lists (the default).
+	 */
+	public ServerCapabilities getCapabilities() {
+		return capabilities;
+	}
+
+	/**
+	 * Sets the explicit {@code server/discover} capabilities advertisement.
+	 *
+	 * <p>
+	 * A non-<jk>null</jk> value bypasses auto-derivation entirely and advertises exactly what is supplied.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to auto-derive.
+	 * @return This object (for method chaining).
+	 */
+	public McpOptions setCapabilities(ServerCapabilities value) {
+		capabilities = value;
+		return this;
+	}
+
+	/**
+	 * Optional free-form {@code server/discover} usage instructions advertised to clients.
+	 *
+	 * @return The instructions, or <jk>null</jk> to omit them from the discovery result (the default).
+	 */
+	public String getInstructions() {
+		return instructions;
+	}
+
+	/**
+	 * Sets the {@code server/discover} usage instructions.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to omit them.
+	 * @return This object (for method chaining).
+	 */
+	public McpOptions setInstructions(String value) {
+		instructions = value;
+		return this;
+	}
+
+	/**
+	 * This binding's cache-hint configuration.
+	 *
+	 * @return The framework-owned cache configuration. Never <jk>null</jk>.
+	 */
+	public McpCacheConfig getCache() {
+		return cache;
+	}
+
+	/**
+	 * Replaces this binding's cache configuration outright.
+	 *
+	 * @param value The new value. Must not be <jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+	 */
+	public McpOptions setCache(McpCacheConfig value) {
+		if (value == null)
+			throw iaex("cache must not be null");
+		cache = value;
+		return this;
+	}
+
+	/**
+	 * Mutates this binding's cache configuration in place.
+	 *
+	 * <p>
+	 * Preferred over {@link #setCache(McpCacheConfig)} for incremental configuration since it operates on the
+	 * framework-owned instance rather than requiring the caller to construct a replacement.
+	 *
+	 * @param consumer Callback invoked with the framework-owned {@link McpCacheConfig}. Must not be <jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
+	 */
+	public McpOptions cache(Consumer<McpCacheConfig> consumer) {
+		if (consumer == null)
+			throw iaex("consumer must not be null");
+		consumer.accept(cache);
+		return this;
+	}
+
+	/**
+	 * This binding's MRTR (Multi-Round-Trip Request) configuration.
+	 *
+	 * @return The framework-owned MRTR configuration. Never <jk>null</jk>.
+	 */
+	public McpMrtrConfig getMrtr() {
+		return mrtr;
+	}
+
+	/**
+	 * Replaces this binding's MRTR configuration outright.
+	 *
+	 * @param value The new value. Must not be <jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+	 */
+	public McpOptions setMrtr(McpMrtrConfig value) {
+		if (value == null)
+			throw iaex("mrtr must not be null");
+		mrtr = value;
+		return this;
+	}
+
+	/**
+	 * Mutates this binding's MRTR configuration in place.
+	 *
+	 * @param consumer Callback invoked with the framework-owned {@link McpMrtrConfig}. Must not be <jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
+	 */
+	public McpOptions mrtr(Consumer<McpMrtrConfig> consumer) {
+		if (consumer == null)
+			throw iaex("consumer must not be null");
+		consumer.accept(mrtr);
+		return this;
+	}
+
+	/**
+	 * This binding's {@code subscriptions/listen} configuration.
+	 *
+	 * @return The framework-owned subscriptions configuration. Never <jk>null</jk>.
+	 */
+	public McpSubscriptionsConfig getSubscriptions() {
+		return subscriptions;
+	}
+
+	/**
+	 * Replaces this binding's subscriptions configuration outright.
+	 *
+	 * @param value The new value. Must not be <jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code value} is <jk>null</jk>.
+	 */
+	public McpOptions setSubscriptions(McpSubscriptionsConfig value) {
+		if (value == null)
+			throw iaex("subscriptions must not be null");
+		subscriptions = value;
+		return this;
+	}
+
+	/**
+	 * Mutates this binding's subscriptions configuration in place.
+	 *
+	 * @param consumer Callback invoked with the framework-owned {@link McpSubscriptionsConfig}. Must not be
+	 * 	<jk>null</jk>.
+	 * @return This object (for method chaining).
+	 * @throws IllegalArgumentException If {@code consumer} is <jk>null</jk>.
+	 */
+	public McpOptions subscriptions(Consumer<McpSubscriptionsConfig> consumer) {
+		if (consumer == null)
+			throw iaex("consumer must not be null");
+		consumer.accept(subscriptions);
+		return this;
+	}
+
+	/**
+	 * The explicit subscription broker for this binding, if one was supplied.
+	 *
+	 * @return The explicit broker, or <jk>null</jk> if none was supplied (the default) &mdash; in which case the
+	 * 	binding lazily derives and memoizes one (see {@link #resolveSubscriptionBroker()}).
+	 */
+	public McpSubscriptionBroker getSubscriptionBroker() {
+		return subscriptionBroker;
+	}
+
+	/**
+	 * Sets an explicit subscription broker, bypassing the framework-derived default.
+	 *
+	 * <p>
+	 * Setting this after {@link #resolveSubscriptionBroker()} has already derived and published a default
+	 * broker for this instance does not retroactively clear or replace that already-published derived
+	 * broker &mdash; {@link #resolveSubscriptionBroker()} always prefers an explicit value set here over a
+	 * previously-derived one, but under the never-mutate-after-setup contract this method should only be
+	 * called during configuration, before the binding is handed to the framework.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to revert to the framework-derived default.
+	 * @return This object (for method chaining).
+	 */
+	public McpOptions setSubscriptionBroker(McpSubscriptionBroker value) {
+		subscriptionBroker = value;
+		return this;
+	}
+
+	/**
+	 * Returns {@link #getSubscriptionBroker()} if explicitly set, otherwise lazily derives and memoizes a
+	 * {@link BasicMcpSubscriptionBroker} sized from {@link #getSubscriptions()}'s queue bound.
+	 *
+	 * <p>
+	 * This is the <b>effective</b> broker accessor: unlike {@link #getSubscriptionBroker()} (which returns
+	 * <jk>null</jk> by default), this method never returns <jk>null</jk>, deriving a default the first time
+	 * one is needed. Internal plumbing for {@link McpEndpoint#subscriptionBroker()} /
+	 * {@link McpRestServlet#getSubscriptionBroker()}. Since this {@link McpOptions} instance is itself
+	 * memoized once per binding, the derived broker this method publishes is likewise stable for the
+	 * binding's lifetime &mdash; publication uses double-checked locking (not a plain lazy read) because two
+	 * {@link BasicMcpSubscriptionBroker} instances are <b>not</b> equivalent: each holds distinct live
+	 * per-connection subscription state, so a benign publication race would silently split subscribers and
+	 * publishers across two unrelated registries.
+	 *
+	 * @return The subscription broker to use. Never <jk>null</jk>.
+	 */
+	public McpSubscriptionBroker resolveSubscriptionBroker() {
+		var explicit = subscriptionBroker;
+		if (explicit != null)
+			return explicit;
+		var result = derivedBroker;
+		if (result == null) {
+			synchronized (lock) {
+				result = derivedBroker;
+				if (result == null) {
+					result = new BasicMcpSubscriptionBroker(subscriptions.getQueueSize());
+					derivedBroker = result;
+				}
+			}
+		}
+		return result;
+	}
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRestServlet.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRestServlet.java
index c07d947ff1..24d5def4ac 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRestServlet.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpRestServlet.java
@@ -16,10 +16,12 @@
  */
 package org.apache.juneau.rest.server.mcp.v20260728;
 
-import org.apache.juneau.bean.mcp.v20260728.*;
+import static org.apache.juneau.commons.utils.Shorts.*;
+
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.apache.juneau.commons.inject.Bean;
 import org.apache.juneau.rest.server.*;
-import org.apache.juneau.rest.server.mcp.BasicMcpSubscriptionBroker;
 import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
 import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
 
@@ -49,6 +51,13 @@ import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
  *     protected McpServerConfig createMcpConfig() {
  *         return new McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new MyEchoTool());
  *     }
+ *     @Override                                            // optional
+ *     protected McpOptions createMcpOptions() {
+ *         return new McpOptions()
+ *             .setInstructions("Use tool 'echo' to test.")
+ *             .cache(c -&gt; c.setToolsList(new McpCacheHint().setTtlMs(60_000)))
+ *             .subscriptions(s -&gt; s.setQueueSize(2048));
+ *     }
  * }
  * </pre>
  *
@@ -61,32 +70,12 @@ import org.apache.juneau.rest.server.tracing.TraceContextExtractor;
 public abstract class McpRestServlet extends org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet {
 	private static final long serialVersionUID = 1L;
 
-	@SuppressWarnings({
-		"java:S2226", // Lazily-published, per-servlet-instance cache config; cannot be static (per-instance) or final (assigned after construction).
-		"java:S3077" // Volatile + idempotent createCacheConfig() makes concurrent first-access races safe: any racing writer publishes an equivalent value.
-	})
-	private transient volatile McpCacheConfig cacheConfig;
-
-	@SuppressWarnings({
-		"java:S2226", // Lazily-published, per-servlet-instance MRTR config; cannot be static (per-instance) or final (assigned after construction).
-		"java:S3077" // Volatile is required for the correct double-checked locking in getMrtrConfig(): unlike the cache config, two McpMrtrConfig instances are NOT equivalent (each holds a different random AES key), so exactly one config/key must ever be created and published or a requestState sealed against one key could never be unsealed against another.
-	})
-	private transient volatile McpMrtrConfig mrtrConfig;
-
-	@SuppressWarnings({
-		"java:S2226", // Lazily-published, per-servlet-instance subscriptions config; cannot be static (per-instance) or final (assigned after construction).
-	})
-	private transient volatile McpSubscriptionsConfig subscriptionsConfig;
-
-	@SuppressWarnings({
-		"java:S2226", // Lazily-published, per-servlet-instance subscription broker; cannot be static (per-instance) or final (assigned after construction).
-		"java:S3077" // Volatile is required for correct double-checked locking in getSubscriptionBroker(): unlike the plain-read cache config, two BasicMcpSubscriptionBroker instances are NOT equivalent (each holds distinct live per-connection subscription state), so exactly one broker must ever be created and published or a subscription registered against one instance would never be visible to a publish call against the other.
-	})
-	private transient volatile McpSubscriptionBroker subscriptionBroker;
+	private final transient AtomicReference<McpOptions> mcpOptions = new AtomicReference<>();
 
 	@Override /* AbstractMcpRestServlet */
-	protected org.apache.juneau.rest.server.mcp.McpRevision revision() {
-		return new McpRevision(capabilities(), getCacheConfig(), instructions(), getMrtrConfig());
+	protected final org.apache.juneau.rest.server.mcp.McpRevision revision() {
+		var o = getMcpOptions();
+		return new McpRevision(o.getCapabilities(), o.getCache(), o.getInstructions(), o.getMrtr());
 	}
 
 	/**
@@ -101,234 +90,71 @@ public abstract class McpRestServlet extends org.apache.juneau.rest.server.mcp.A
 	 * @return This revision's shared {@link TraceContextExtractor}. Never <jk>null</jk>.
 	 */
 	@Bean
-	public TraceContextExtractor mcpTraceContextExtractor() {
+	public final TraceContextExtractor mcpTraceContextExtractor() {
 		return McpRevision.TRACE_CONTEXT_EXTRACTOR;
 	}
 
 	/**
-	 * Optional explicit capabilities advertisement for {@code server/discover}.
+	 * Factory method for the behavior configuration published by {@link #getMcpOptions()}.
 	 *
 	 * <p>
-	 * Returning <jk>null</jk> (the default) leaves capabilities auto-derived from the registered
-	 * tool / prompt / resource lists. Returning a non-<jk>null</jk> value bypasses auto-derivation
-	 * entirely and advertises exactly what is returned — the way to advertise capabilities that are
-	 * not derivable from a handler registry.
+	 * Override to customize any of capabilities/instructions/cache/mrtr/subscriptions/broker in one place. The
+	 * default returns a plain {@link McpOptions} (all defaults).
 	 *
 	 * <p>
-	 * This hook is typed against this revision's wire beans on purpose: capabilities diverge between
-	 * MCP revisions, so a neutral carrier in the core would accept data that is nonsense for the
-	 * bound revision and fail only at serialization time.
+	 * <b>Must be side-effect-free and idempotent</b>, exactly like
+	 * {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet#createMcpConfig()}: {@link #getMcpOptions()}
+	 * calls this hook at most once per servlet instance and publishes the result via a lock-free
+	 * {@link java.util.concurrent.atomic.AtomicReference#compareAndSet}, so under a first-access race it may be
+	 * invoked by more than one thread, but only one resulting instance is ever published and returned.
 	 *
-	 * @return The explicit capabilities, or <jk>null</jk> to auto-derive.
+	 * @return A non-<jk>null</jk> options instance.
 	 */
-	protected ServerCapabilities capabilities() {
-		return null;
+	protected McpOptions createMcpOptions() {
+		return new McpOptions();
 	}
 
 	/**
-	 * Optional free-form {@code server/discover} usage instructions advertised to clients.
+	 * Returns this servlet's lazily-published, binding-owned {@link McpOptions}.
 	 *
 	 * <p>
-	 * Returning <jk>null</jk> (the default) omits the {@code instructions} member from the discovery
-	 * result.
+	 * The first successful call publishes the result of {@link #createMcpOptions()}; every later call returns
+	 * the same instance, mirroring exactly how
+	 * {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet#getMcpConfig() getMcpConfig()} memoizes.
+	 * Also published as a {@code @Bean} so downstream plumbing ({@link #getSubscriptionBroker()},
+	 * {@code subscriptions/listen} dispatch) can resolve it uniformly through the {@code BeanStore}. This
+	 * {@code @Bean} publication is authoritative and takes precedence over any Spring {@code @Bean McpOptions}
+	 * in the app context: a Spring user who wants their configuration honored must inject it INTO
+	 * {@link #createMcpOptions()}'s override (for example an {@code @Autowired} field read from there), not
+	 * publish a bare {@code @Bean McpOptions} in their Spring context and expect it to be picked up instead.
 	 *
-	 * @return The instructions, or <jk>null</jk> to omit them.
-	 */
-	protected String instructions() {
-		return null;
-	}
-
-	/**
-	 * Creates the cache configuration published by {@link #getCacheConfig()}.
-	 *
-	 * <p>
-	 * Override to supply TTL/scope hints. The default returns an empty {@link McpCacheConfig} (no
-	 * cache hints emitted on any result).
-	 *
-	 * <p>
-	 * <b>Must be side-effect-free and idempotent.</b> Concurrent first access under {@link #getCacheConfig()}
-	 * may invoke this hook more than once before the published value settles; every invocation must return
-	 * an equivalent, independently valid configuration.
-	 *
-	 * @return The cache configuration. Must not be <jk>null</jk>.
-	 */
-	protected McpCacheConfig createCacheConfig() {
-		return new McpCacheConfig();
-	}
-
-	/**
-	 * Returns this servlet's lazily-published, binding-owned cache configuration.
-	 *
-	 * <p>
-	 * The first successful call publishes the result of {@link #createCacheConfig()}; every later call
-	 * returns the same instance. The published instance must not be mutated by callers.
-	 *
-	 * @return The cache configuration. Never <jk>null</jk>.
-	 * @throws IllegalStateException If {@link #createCacheConfig()} returns <jk>null</jk>.
-	 */
-	public McpCacheConfig getCacheConfig() {
-		var result = cacheConfig;
-		if (result == null) {
-			result = createCacheConfig();
-			if (result == null)
-				throw new IllegalStateException("createCacheConfig() returned null");
-			cacheConfig = result;
-		}
-		return result;
-	}
-
-	/**
-	 * Creates the MRTR (Multi-Round-Trip Request) configuration published by {@link #getMrtrConfig()}.
-	 *
-	 * <p>
-	 * Override to supply a custom {@link RequestStateCodec}, TTL, or max-rounds cap. The default returns a
-	 * new {@link McpMrtrConfig} (AES-GCM ephemeral codec, 5-minute {@code requestState} TTL, 10-round cap).
-	 *
-	 * <p>
-	 * <b>Must be side-effect-free.</b> {@link #getMrtrConfig()} calls this hook <b>exactly once</b> per servlet
-	 * instance under a lock, so &mdash; unlike {@link #createCacheConfig()} &mdash; the returned instance (and its
-	 * single random AES key) is the one and only config ever published; it must not depend on being called more
-	 * than once.
-	 *
-	 * @return The MRTR configuration. Must not be <jk>null</jk>.
-	 */
-	protected McpMrtrConfig createMrtrConfig() {
-		return new McpMrtrConfig();
-	}
-
-	/**
-	 * Returns this servlet's lazily-published, binding-owned MRTR configuration.
-	 *
-	 * <p>
-	 * The first call publishes the result of {@link #createMrtrConfig()} under a lock; every later call returns
-	 * that same instance. Publication is done with double-checked locking (not the plain lazy read
-	 * {@link #getCacheConfig()} uses) on purpose: two {@link McpMrtrConfig} instances are <b>not</b> equivalent
-	 * &mdash; each {@link AeadRequestStateCodec} generates a distinct random AES key at construction time &mdash;
-	 * so a benign publication race that let two threads each create and publish a config would leave a
-	 * {@code requestState} sealed against one key impossible to unseal against the other, failing RESUME. The lock
-	 * guarantees exactly one config/key is ever created and published. The published instance must not be mutated
-	 * by callers.
-	 *
-	 * @return The MRTR configuration. Never <jk>null</jk>.
-	 * @throws IllegalStateException If {@link #createMrtrConfig()} returns <jk>null</jk>.
-	 */
-	public McpMrtrConfig getMrtrConfig() {
-		var result = mrtrConfig;
-		if (result == null) {
-			synchronized (this) {
-				result = mrtrConfig;
-				if (result == null) {
-					result = createMrtrConfig();
-					if (result == null)
-						throw new IllegalStateException("createMrtrConfig() returned null");
-					mrtrConfig = result;
-				}
-			}
-		}
-		return result;
-	}
-
-	/**
-	 * Creates the subscriptions configuration published by {@link #getSubscriptionsConfig()}.
-	 *
-	 * <p>
-	 * Override to supply custom concurrency/queue/heartbeat/idle-timeout knobs. The default returns a plain
-	 * {@link McpSubscriptionsConfig} (all defaults).
-	 *
-	 * @return The subscriptions configuration. Must not be <jk>null</jk>.
-	 */
-	protected McpSubscriptionsConfig createSubscriptionsConfig() {
-		return new McpSubscriptionsConfig();
-	}
-
-	/**
-	 * Returns this servlet's lazily-published, binding-owned subscriptions configuration.
-	 *
-	 * <p>
-	 * Knobs-only (no live/broker state), so — unlike {@link #getSubscriptionBroker()} — this uses the same
-	 * plain lazy-read strategy as {@link #getCacheConfig()}: a benign publication race is harmless because any
-	 * two {@link McpSubscriptionsConfig} instances built by an idempotent {@link #createSubscriptionsConfig()}
-	 * are equivalent.
-	 *
-	 * @return The subscriptions configuration. Never <jk>null</jk>.
-	 * @throws IllegalStateException If {@link #createSubscriptionsConfig()} returns <jk>null</jk>.
-	 */
-	public McpSubscriptionsConfig getSubscriptionsConfig() {
-		var result = subscriptionsConfig;
-		if (result == null) {
-			result = createSubscriptionsConfig();
-			if (result == null)
-				throw new IllegalStateException("createSubscriptionsConfig() returned null");
-			subscriptionsConfig = result;
-		}
-		return result;
-	}
-
-	/**
-	 * Publishes {@link #getSubscriptionsConfig()} into this servlet's {@code RestContext} bean store
-	 * (the parent store {@link org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet#handleMcp} wraps its
-	 * per-request {@code BeanStore} with), the same {@code @Bean}-method mechanism already used by
-	 * {@link #mcpTraceContextExtractor()}.
-	 *
-	 * <p>
-	 * This is what lets {@code McpRevision.dispatch(...)}'s {@code subscriptions/listen} branch resolve
-	 * the real, possibly-overridden config via {@code ctx.getBean(McpSubscriptionsConfig.class)} instead
-	 * of always falling through to that call's defensive {@code new McpSubscriptionsConfig()} default —
-	 * the neutral core cannot add this bean itself ({@code McpSubscriptionsConfig} is a v2 type, and the
-	 * neutral module must not import it), so this v2-side {@code @Bean} method is the wiring's only seam.
-	 *
-	 * @return This servlet's subscriptions configuration. Never <jk>null</jk>.
+	 * @return The options. Never <jk>null</jk>.
+	 * @throws IllegalStateException If {@link #createMcpOptions()} returns <jk>null</jk>.
 	 */
 	@Bean
-	public McpSubscriptionsConfig subscriptionsConfigBean() {
-		return getSubscriptionsConfig();
-	}
-
-	/**
-	 * Creates the subscription broker published by {@link #getSubscriptionBroker()}.
-	 *
-	 * <p>
-	 * Override to supply a custom {@link McpSubscriptionBroker} implementation. The default returns a new
-	 * {@link BasicMcpSubscriptionBroker} sized from {@link #getSubscriptionsConfig()}'s queue bound.
-	 *
-	 * <p>
-	 * <b>Must be side-effect-free.</b> {@link #getSubscriptionBroker()} calls this hook <b>exactly once</b>
-	 * per servlet instance under a lock: the returned instance is the one and only broker (and its live
-	 * subscription registry) ever published.
-	 *
-	 * @return The subscription broker. Must not be <jk>null</jk>.
-	 */
-	protected McpSubscriptionBroker createSubscriptionBroker() {
-		return new BasicMcpSubscriptionBroker(getSubscriptionsConfig().getQueueSize());
+	public final McpOptions getMcpOptions() {
+		var o = mcpOptions.get();
+		if (o == null) {
+			var no = createMcpOptions();
+			if (no == null)
+				throw isex("createMcpOptions() returned null");
+			o = mcpOptions.compareAndSet(null, no) ? no : mcpOptions.get(); // HTT: the CAS-loses branch requires a genuine concurrent first-access race; untestable deterministically.
+		}
+		return o;
 	}
 
 	/**
 	 * Returns this servlet's lazily-published, binding-owned subscription broker.
 	 *
 	 * <p>
-	 * The first call publishes the result of {@link #createSubscriptionBroker()} under a lock; every later
-	 * call returns that same instance. Publication is double-checked locking (MRTR-grade, not the plain lazy
-	 * read {@link #getSubscriptionsConfig()} uses) on purpose: two {@link BasicMcpSubscriptionBroker}
-	 * instances are <b>not</b> equivalent — each holds distinct, live per-connection subscription state — so a
-	 * benign publication race would silently split subscribers and publishers across two unrelated registries.
+	 * Reads {@link #getMcpOptions()}'s memoized {@link McpOptions#getSubscriptionBroker()}: an explicitly
+	 * configured broker is returned as-is; otherwise one is derived and memoized per-binding, sized from
+	 * {@link McpOptions#getSubscriptions()}'s queue bound (see {@link McpOptions#resolveSubscriptionBroker()}).
 	 *
 	 * @return The subscription broker. Never <jk>null</jk>.
-	 * @throws IllegalStateException If {@link #createSubscriptionBroker()} returns <jk>null</jk>.
 	 */
 	@Override
-	public McpSubscriptionBroker getSubscriptionBroker() {
-		var result = subscriptionBroker;
-		if (result == null) {
-			synchronized (this) {
-				result = subscriptionBroker;
-				if (result == null) {
-					result = createSubscriptionBroker();
-					if (result == null)
-						throw new IllegalStateException("createSubscriptionBroker() returned null");
-					subscriptionBroker = result;
-				}
-			}
-		}
-		return result;
+	public final McpSubscriptionBroker getSubscriptionBroker() {
+		return getMcpOptions().resolveSubscriptionBroker();
 	}
 }
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 56a8b7a783..e424a5fc96 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
@@ -57,7 +57,7 @@ import org.apache.juneau.rest.server.mcp.McpToolOutcome;
  *
  * <p>
  * <b>Constructed per binding, not shared as a singleton.</b> The bound servlet or mixin builds a
- * fresh instance on every {@code revision()} call, passing its own {@code capabilities()} hook
+ * fresh instance on every {@code revision()} call, passing its {@link McpOptions#getCapabilities()}
  * result and its published {@link McpCacheConfig} into the constructor — see {@link McpRestServlet}
  * and {@link McpEndpoint}. A {@code null} capabilities override auto-derives {@code server/discover}
  * capabilities from the registered tool/prompt/resource/resource-template lists; a non-{@code null}
@@ -183,7 +183,7 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
 	 *
 	 * <p>
 	 * Package-visible so tests can confirm the binding-level memoization contract documented on
-	 * {@link McpRestServlet#getMrtrConfig()} / {@link McpEndpoint#mrtrConfig()}: two {@link McpRevision}
+	 * {@link McpRestServlet#getMcpOptions()} / {@link McpEndpoint#getMcpOptions()}: two {@link McpRevision}
 	 * instances built from the same binding must share the same {@link McpMrtrConfig} (and therefore the
 	 * same {@link RequestStateCodec}), even though each dispatched request constructs its own
 	 * {@link McpRevision}.
@@ -265,12 +265,12 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
 	 * @param params The opaque request params. Must decode to a {@code SubscriptionsListenRequest}.
 	 * @param config The neutral handler registry, used to auto-derive capabilities when none were set explicitly.
 	 * @param ctx The per-request bean store, expected to carry a bound {@link McpSubscriptionBroker}
-	 * 	(Phase 1-3 wiring) and a bound {@link McpSubscriptionsConfig}. In production the config bean is
-	 * 	normally present — {@code McpRestServlet#subscriptionsConfigBean()} / {@code McpEndpoint#subscriptionsConfigBean()}
-	 * 	(Task 3.3/3.4) publish it into the {@code RestContext} bean store that this request-scoped {@code ctx}
-	 * 	wraps as its parent, since the neutral core cannot add it directly ({@code McpSubscriptionsConfig} is a
-	 * 	v2 type). The {@code orElseGet(McpSubscriptionsConfig::new)} fallback below is defense-in-depth only
-	 * 	(e.g. a hand-built {@code ctx} in a direct-dispatch test that omits it).
+	 * 	(Phase 1-3 wiring) and a bound {@link McpOptions}. In production the options bean is normally present —
+	 * 	{@code McpRestServlet#getMcpOptions()} / {@code McpEndpoint#mcpOptionsBean()} (TODO-330) publish it into
+	 * 	the {@code RestContext} bean store that this request-scoped {@code ctx} wraps as its parent, since the
+	 * 	neutral core cannot add it directly ({@code McpOptions} is a v2 type). The
+	 * 	{@code orElseGet(McpSubscriptionsConfig::new)} fallback below is defense-in-depth only (e.g. a
+	 * 	hand-built {@code ctx} in a direct-dispatch test that omits it).
 	 * <p>
 	 * <b>SSE negotiation gate (checked first, before any broker registration):</b> every MCP servlet binding
 	 * defaults an unqualified request to {@code Accept: application/json}. A {@code subscriptions/listen}
@@ -288,10 +288,10 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
 	 * <p>
 	 * {@code maxConcurrentSubscriptions} is enforced as a hard cap, not an advisory one: the admission check
 	 * and the registration are one atomic step via {@link McpSubscriptionBroker#registerIfUnder}, so
-	 * concurrent {@code subscriptions/listen} calls (in particular against the JVM-wide {@code
-	 * SharedSubscriptionBroker} multiple mixin resources can share) cannot all observe room under the cap
-	 * and all register, which a separate {@code activeCount()} check followed by a separate {@code
-	 * register(...)} call would allow.
+	 * concurrent {@code subscriptions/listen} calls against the same broker (e.g. a custom broker explicitly
+	 * shared across multiple bindings via {@link McpOptions#setSubscriptionBroker}) cannot all observe room
+	 * under the cap and all register, which a separate {@code activeCount()} check followed by a separate
+	 * {@code register(...)} call would allow.
 	 *
 	 * <p>
 	 * The broker registers under a fresh {@link UUID}, never under the client-supplied JSON-RPC request
@@ -315,11 +315,12 @@ public final class McpRevision implements org.apache.juneau.rest.server.mcp.McpR
 
 		var broker = ctx.getBean(McpSubscriptionBroker.class)
 			.orElseThrow(() -> new McpException(CODE_INTERNAL_ERROR, "McpSubscriptionBroker not bound in BeanStore"));
-		var subscriptionsConfig = ctx.getBean(McpSubscriptionsConfig.class).orElseGet(McpSubscriptionsConfig::new);
+		var subscriptionsConfig = ctx.getBean(McpOptions.class).map(McpOptions::getSubscriptions).orElseGet(McpSubscriptionsConfig::new);
 		// registerIfUnder admission-checks and registers as one atomic step (hard cap, not advisory) — a
 		// separate activeCount() check followed by a separate register() call would leave a TOCTOU window
-		// open for concurrent listen requests racing the same (possibly JVM-wide shared) broker. The
-		// registry key is a server-minted UUID, deliberately NOT String.valueOf(id): see the javadoc above.
+		// open for concurrent listen requests racing the same per-binding broker (see
+		// McpOptions#resolveSubscriptionBroker()). The registry key is a server-minted UUID, deliberately
+		// NOT String.valueOf(id): see the javadoc above.
 		var subscription = broker.registerIfUnder(subscriptionsConfig.getMaxConcurrentSubscriptions(), UUID.randomUUID().toString(), honoredFilter)
 			.orElseThrow(() -> new McpException(CODE_TOO_MANY_SUBSCRIPTIONS, "Maximum concurrent subscriptions exceeded"));
 		var honoredWireFilter = SubscriptionCapabilityGate.toWireFilter(honoredFilter);
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsConfig.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsConfig.java
index 8e8de697c2..9836ebc3d1 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsConfig.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsConfig.java
@@ -32,9 +32,10 @@ package org.apache.juneau.rest.server.mcp.v20260728;
  * <p>
  * Unlike the {@code org.apache.juneau.rest.server.mcp.McpSubscriptionBroker} singleton (which holds live
  * per-connection state), this type is knobs-only — every field is a tunable numeric limit with no embedded
- * broker reference — so a servlet binding may memoize it with a plain lazy read (see
- * {@link McpRestServlet#getSubscriptionsConfig()}) rather than the MRTR-grade double-checked lock the broker
- * itself requires (see {@link McpRestServlet#getSubscriptionBroker()}).
+ * broker reference. It is one of {@link McpOptions}'s nested configs (see {@link McpOptions#getSubscriptions()}),
+ * memoized once per binding along with the rest of {@link McpOptions}; the broker itself is derived and
+ * memoized separately, sized from this config's {@link #getQueueSize() queueSize} (see
+ * {@link McpOptions#resolveSubscriptionBroker()}).
  */
 public class McpSubscriptionsConfig {
 
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedMrtrConfig.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedMrtrConfig.java
deleted file mode 100644
index 2398541f65..0000000000
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedMrtrConfig.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.juneau.rest.server.mcp.v20260728;
-
-/**
- * Holds the lazily-initialized, per-process shared default {@link McpMrtrConfig} returned by the
- * {@link McpEndpoint#mrtrConfig()} mixin default.
- *
- * <p>
- * The mixin's {@code revision()} constructs a fresh {@link McpRevision} on every request, so a default that
- * returned a fresh {@link McpMrtrConfig} each call would mint a fresh {@link AeadRequestStateCodec} AES key
- * per request &mdash; a {@code requestState} sealed on the PAUSE request could never be unsealed on the
- * follow-up RESUME request. Returning this single shared instance instead means the whole JVM shares one
- * ephemeral AES key across every mixin deployment, which is exactly the "per-process ephemeral key" design
- * intent and makes RESUME work out of the box without an override.
- *
- * <p>
- * Uses the initialization-on-demand holder idiom: the instance (and its single per-JVM key) is created the
- * first time {@link #get()} is called &mdash; i.e. the first mixin {@code mrtrConfig()} call &mdash; and the
- * JVM guarantees that class initialization is thread-safe, so no explicit synchronization is needed.
- */
-final class SharedMrtrConfig {
-
-	private SharedMrtrConfig() {}
-
-	private static final class Holder {
-		static final McpMrtrConfig INSTANCE = new McpMrtrConfig();
-	}
-
-	/**
-	 * Returns the per-process shared default MRTR configuration, creating it on first access.
-	 *
-	 * @return The shared configuration. Never <jk>null</jk>.
-	 */
-	static McpMrtrConfig get() {
-		return Holder.INSTANCE;
-	}
-}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedSubscriptionBroker.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedSubscriptionBroker.java
deleted file mode 100644
index 279ff12263..0000000000
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SharedSubscriptionBroker.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.juneau.rest.server.mcp.v20260728;
-
-import org.apache.juneau.rest.server.mcp.BasicMcpSubscriptionBroker;
-import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
-
-/**
- * Holds the lazily-initialized, per-process shared default {@link McpSubscriptionBroker} returned by the
- * {@link McpEndpoint#subscriptionBroker()} mixin default.
- *
- * <p>
- * The mixin path has no per-endpoint-instance field to memoize a broker onto (it's an interface), and unlike
- * the knobs-only {@link McpSubscriptionsConfig}, a {@link McpSubscriptionBroker} holds live per-connection
- * subscription state that must be shared across every {@code subscriptions/listen} stream a given mixin
- * deployment serves — a fresh broker per call would silently split subscribers and publishers across
- * unrelated registries. Returning this single shared instance instead means the whole JVM shares one
- * subscription registry across every mixin deployment, mirroring {@link SharedMrtrConfig}'s reasoning for why
- * MRTR's per-process default exists.
- *
- * <p>
- * Uses the initialization-on-demand holder idiom: the instance is created the first time {@link #get()} is
- * called &mdash; i.e. the first mixin {@code subscriptionBroker()} call &mdash; and the JVM guarantees that
- * class initialization is thread-safe, so no explicit synchronization is needed.
- */
-final class SharedSubscriptionBroker {
-
-	private SharedSubscriptionBroker() {}
-
-	private static final class Holder {
-		static final McpSubscriptionBroker INSTANCE =
-			new BasicMcpSubscriptionBroker(McpSubscriptionsConfig.DEFAULT_QUEUE_SIZE);
-	}
-
-	/**
-	 * Returns the per-process shared default subscription broker, creating it on first access.
-	 *
-	 * @return The shared broker. Never <jk>null</jk>.
-	 */
-	static McpSubscriptionBroker get() {
-		return Holder.INSTANCE;
-	}
-}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SpringMcpRestServlet.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SpringMcpRestServlet.java
index 465029975d..a0567db5f6 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SpringMcpRestServlet.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/SpringMcpRestServlet.java
@@ -34,7 +34,8 @@ import org.apache.juneau.rest.server.springboot.*;
  *
  * <p>
  * Subclasses supply their {@link org.apache.juneau.rest.server.mcp.McpServerConfig} by implementing
- * {@link #getMcpConfig()}.
+ * {@link #getMcpConfig()}, and may optionally override {@link McpEndpoint#getMcpOptions()} to customize
+ * capabilities/instructions/cache/mrtr/subscriptions/broker behavior.
  *
  * <h5 class='section'>Example:</h5>
  * <pre>
@@ -44,6 +45,10 @@ import org.apache.juneau.rest.server.springboot.*;
  * 	public McpServerConfig getMcpConfig() {
  * 		return new McpServerConfig().addTool(new MySpringAwareTool());
  * 	}
+ * 	@Override                                        // optional
+ * 	public McpOptions getMcpOptions() {
+ * 		return new McpOptions().mrtr(m -&gt; m.setTtlMs(600_000));
+ * 	}
  * }
  * </pre>
  *
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 8452c4ffe5..ab785c41ea 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
@@ -92,15 +92,15 @@ class Characterization_Test {
 	public static class F_Cache extends F_Full {
 		private static final long serialVersionUID = 1L;
 
-		@Override protected McpCacheConfig createCacheConfig() {
-			return new McpCacheConfig()
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().cache(c -> c
 				.setDefaultHint(new McpCacheHint().setTtlMs(30000))
 				.setToolsList(new McpCacheHint().setTtlMs(5000).setCacheScope(McpCacheScope.PRIVATE))
 				.setPromptsList(new McpCacheHint().setTtlMs(0).setCacheScope(McpCacheScope.PUBLIC))
 				.setResourceTemplatesList(new McpCacheHint().setTtlMs(60000).setCacheScope(McpCacheScope.PRIVATE))
 				.setResourcesRead(new McpCacheHint().setTtlMs(2000))
 				.addResourceReadOverride("file:///a",
-					new McpCacheHint().setTtlMs(1000).setCacheScope(McpCacheScope.PRIVATE));
+					new McpCacheHint().setTtlMs(1000).setCacheScope(McpCacheScope.PRIVATE)));
 		}
 	}
 
@@ -309,8 +309,8 @@ class Characterization_Test {
 				}
 			});
 		}
-		@Override protected McpMrtrConfig createMrtrConfig() {
-			return new McpMrtrConfig().setCodec(new FixedKeyGcmCodec());
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().mrtr(m -> m.setCodec(new FixedKeyGcmCodec()));
 		}
 	}
 
@@ -361,8 +361,8 @@ class Characterization_Test {
 				});
 		}
 
-		@Override protected McpMrtrConfig createMrtrConfig() {
-			return new McpMrtrConfig().setCodec(new FixedKeyGcmCodec());
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().mrtr(m -> m.setCodec(new FixedKeyGcmCodec()));
 		}
 	}
 
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpBindings_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpBindings_Test.java
index 3749e7584e..128a56d976 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpBindings_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpBindings_Test.java
@@ -103,7 +103,7 @@ class McpBindings_Test extends TestBase {
 		assertContains("hello", resp);
 	}
 
-	// -------- servlet with explicit capability override ---------
+	// -------- servlet with explicit capability override (via McpOptions) ---------
 
 	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
 	public static class D extends McpRestServlet {
@@ -113,8 +113,8 @@ class McpBindings_Test extends TestBase {
 			return new McpServerConfig().setName("override").setVersion("1.0.0").addTool(echo());
 		}
 		@Override
-		protected ServerCapabilities capabilities() {
-			return new ServerCapabilities().setPrompts(new PromptCapability());
+		protected McpOptions createMcpOptions() {
+			return new McpOptions().setCapabilities(new ServerCapabilities().setPrompts(new PromptCapability()));
 		}
 	}
 
@@ -160,17 +160,17 @@ class McpBindings_Test extends TestBase {
 		assertContains("pong", resp);
 	}
 
-	// -------- default null capability hooks ---------
+	// -------- default null capability hooks (now read through McpOptions) ---------
 
 	@Test void c01_servletCapabilityHook_defaultsToNull() {
-		assertNull(new A().capabilities());
+		assertNull(new A().createMcpOptions().getCapabilities());
 	}
 
 	@Test void c02_endpointCapabilityHook_defaultsToNull() {
-		assertNull(new B().capabilities());
+		assertNull(new B().getMcpOptions().getCapabilities());
 	}
 
-	// -------- cache-config lifecycle hooks ---------
+	// -------- cache-config lifecycle, now folded into McpOptions ---------
 
 	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
 	public static class E extends McpRestServlet {
@@ -181,9 +181,9 @@ class McpBindings_Test extends TestBase {
 			return new McpServerConfig().addTool(echo());
 		}
 		@Override
-		protected McpCacheConfig createCacheConfig() {
+		protected McpOptions createMcpOptions() {
 			calls.incrementAndGet();
-			return new McpCacheConfig().setToolsList(new McpCacheHint().setTtlMs(21));
+			return new McpOptions().cache(c -> c.setToolsList(new McpCacheHint().setTtlMs(21)));
 		}
 	}
 
@@ -195,7 +195,7 @@ class McpBindings_Test extends TestBase {
 			return new McpServerConfig();
 		}
 		@Override
-		protected McpCacheConfig createCacheConfig() {
+		protected McpOptions createMcpOptions() {
 			return null;
 		}
 	}
@@ -208,8 +208,8 @@ class McpBindings_Test extends TestBase {
 			return new McpServerConfig().addTool(echo());
 		}
 		@Override
-		public McpCacheConfig cacheConfig() {
-			return new McpCacheConfig().setToolsList(new McpCacheHint().setCacheScope(McpCacheScope.PRIVATE));
+		public McpOptions getMcpOptions() {
+			return new McpOptions().cache(c -> c.setToolsList(new McpCacheHint().setCacheScope(McpCacheScope.PRIVATE)));
 		}
 	}
 
@@ -221,50 +221,32 @@ class McpBindings_Test extends TestBase {
 		return MockRestClient.create(G.class).json().contentType("application/json").accept("application/json").build();
 	}
 
-	@Test void d01_servletCacheConfig_isLazilyCachedAndInjected() throws Exception {
+	@Test void d01_servletOptions_isLazilyCachedAndInjected() throws Exception {
 		var servlet = new E();
-		assertSame(servlet.getCacheConfig(), servlet.getCacheConfig());
+		assertSame(servlet.getMcpOptions(), servlet.getMcpOptions());
 		assertEquals(1, E.calls.get());
 		var body = client(E.class).post("/").contentString(body(1, "tools/list", null))
 			.header("Mcp-Method", "tools/list").header("Mcp-Name", "").run().getContent().asString();
 		assertContains("\"ttlMs\":21", body);
 	}
 
-	@Test void d02_servletNullFactoryFailsFast() {
-		var e = assertThrows(IllegalStateException.class, () -> new F().getCacheConfig());
-		assertEquals("createCacheConfig() returned null", e.getMessage());
+	@Test void d02_servletNullOptionsFactoryFailsFast() {
+		var e = assertThrows(IllegalStateException.class, () -> new F().getMcpOptions());
+		assertEquals("createMcpOptions() returned null", e.getMessage());
 	}
 
 	@Test void d03_endpointDefaultIsEmptyAndOverrideIsInjected() throws Exception {
-		assertNotNull(new B().cacheConfig());
+		assertNotNull(new B().getMcpOptions().getCache());
 		var body = clientBWithCache().post("/mcp").contentString(body(1, "tools/list", null))
 			.header("Mcp-Method", "tools/list").header("Mcp-Name", "").run().getContent().asString();
 		assertContains("\"cacheScope\":\"private\"", body);
 	}
 
-	// -------- mrtr-config lifecycle hooks ---------
+	// -------- MRTR lifecycle, now folded into McpOptions ---------
 
-	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
-	public static class H extends McpRestServlet {
-		private static final long serialVersionUID = 1L;
-		@Override
-		protected McpServerConfig createMcpConfig() {
-			return new McpServerConfig();
-		}
-		@Override
-		protected McpMrtrConfig createMrtrConfig() {
-			return null;
-		}
-	}
-
-	@Test void e01_servletMrtrConfig_isLazilyCachedAcrossCalls() {
+	@Test void e01_servletOptions_isLazilyCachedAcrossCalls() {
 		var servlet = new A();
-		assertSame(servlet.getMrtrConfig(), servlet.getMrtrConfig());
-	}
-
-	@Test void e02_servletNullMrtrFactoryFailsFast() {
-		var e = assertThrows(IllegalStateException.class, () -> new H().getMrtrConfig());
-		assertEquals("createMrtrConfig() returned null", e.getMessage());
+		assertSame(servlet.getMcpOptions(), servlet.getMcpOptions());
 	}
 
 	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
@@ -276,40 +258,42 @@ class McpBindings_Test extends TestBase {
 			return new McpServerConfig();
 		}
 		@Override
-		protected McpMrtrConfig createMrtrConfig() {
+		protected McpOptions createMcpOptions() {
 			createCalls.incrementAndGet();
-			return new McpMrtrConfig();
+			return new McpOptions();
 		}
 	}
 
 	/**
-	 * H2 regression: {@link McpRestServlet#getMrtrConfig()} must publish exactly one {@link McpMrtrConfig} (and
-	 * thus one AES key) even under a concurrent cold-start race. A plain lazy read would let two racing threads
-	 * each create and publish a distinct config, so a {@code requestState} sealed against one key could never be
-	 * unsealed against the other. Every concurrent first-access caller must observe the same instance and
-	 * {@link #createMrtrConfig()} must run exactly once.
+	 * H2 regression: {@link McpRestServlet#getMcpOptions()} must publish exactly one {@link McpOptions} (and
+	 * thus one MRTR AES key) even under a concurrent cold-start race, even though (mirroring
+	 * {@code AbstractMcpRestServlet#getMcpConfig()}'s lock-free {@link java.util.concurrent.atomic.AtomicReference}
+	 * pattern) {@link McpRestServlet#createMcpOptions()} itself MAY run more than once under that race: a
+	 * losing thread's locally-computed instance (and its distinct, never-otherwise-used MRTR key) is simply
+	 * discarded, never published, and never returned to any caller. Every concurrent first-access caller must
+	 * observe the SAME published options instance regardless of how many times the factory itself ran.
 	 */
-	@Test void e06_servletMrtrConfig_concurrentFirstAccessPublishesExactlyOneInstance() throws Exception {
+	@Test void e02_servletOptions_concurrentFirstAccessPublishesExactlyOneInstance() throws Exception {
 		var servlet = new I();
 		var threads = 16;
 		var pool = Executors.newFixedThreadPool(threads);
 		try {
 			var start = new CountDownLatch(1);
-			var results = new ArrayList<Future<McpMrtrConfig>>();
+			var results = new ArrayList<Future<McpOptions>>();
 			for (var i = 0; i < threads; i++)
-				results.add(pool.submit(() -> { start.await(); return servlet.getMrtrConfig(); }));
+				results.add(pool.submit(() -> { start.await(); return servlet.getMcpOptions(); }));
 			start.countDown();
 			var first = results.get(0).get();
 			for (var f : results)
-				assertSame(first, f.get(), "every concurrent first-access caller must observe the same published config");
-			assertEquals(1, servlet.createCalls.get(), "createMrtrConfig() must be invoked exactly once under the lock");
+				assertSame(first, f.get(), "every concurrent first-access caller must observe the same published options");
+			assertTrue(servlet.createCalls.get() >= 1, "createMcpOptions() must run at least once");
 		} finally {
 			pool.shutdownNow();
 		}
 	}
 
-	@Test void e03_endpointMrtrConfigHook_defaultsToNonNull() {
-		assertNotNull(new B().mrtrConfig());
+	@Test void e03_endpointOptionsHook_defaultsToNonNull() {
+		assertNotNull(new B().getMcpOptions().getMrtr());
 	}
 
 	/**
@@ -340,22 +324,21 @@ class McpBindings_Test extends TestBase {
 
 	/**
 	 * Mixin-path analogue of {@link #e04_mrtrConfigIsStableAcrossSeparateRevisionInstancesFromSameBinding}: two
-	 * {@link McpRevision}s built through the {@link McpEndpoint} mixin default share the per-process
-	 * {@link McpMrtrConfig}/{@link RequestStateCodec}, so a {@code requestState} sealed on a PAUSE request unseals
-	 * on a later RESUME request. The mixin default returns a single JVM-wide shared config (see
-	 * {@link SharedMrtrConfig}), so the sharing holds even across two distinct endpoint instances &mdash; not just
-	 * repeated {@code revision()} calls on one instance &mdash; which is what makes the common mixin case resumable
-	 * with no override.
+	 * {@link McpRevision}s built through the SAME {@link McpEndpoint} instance's mixin default share the
+	 * per-binding {@link McpMrtrConfig}/{@link RequestStateCodec} (memoized by {@link McpEndpointOptionsCache}),
+	 * so a {@code requestState} sealed on a PAUSE request unseals on a later RESUME request &mdash; without ever
+	 * standing up a full {@code RestContext} (bare construction).
 	 */
-	@Test void e05_mixinMrtrConfigIsStableAndSharedAcrossEndpointInstances() {
-		var rev1 = (McpRevision)new B().revision();
-		var rev2 = (McpRevision)new B().revision();
+	@Test void e05_mixinMrtrConfigIsStableAcrossSeparateRevisionInstancesFromSameEndpoint() {
+		var b = new B();
+		var rev1 = (McpRevision)b.revision();
+		var rev2 = (McpRevision)b.revision();
 		assertNotSame(rev1, rev2, "revision() must construct a fresh McpRevision per call (per-request)");
-		assertSame(rev1.mrtrConfig(), rev2.mrtrConfig(), "the mixin default MRTR config must be a per-process shared instance");
+		assertSame(rev1.mrtrConfig(), rev2.mrtrConfig(), "the mixin MRTR config must be memoized at the binding level");
 
 		var codec1 = rev1.mrtrConfig().getCodec();
 		var codec2 = rev2.mrtrConfig().getCodec();
-		assertSame(codec1, codec2, "the codec (and its AES key) must be the per-process shared instance");
+		assertSame(codec1, codec2, "the codec (and its AES key) must be memoized at the binding level");
 
 		var state = new McpRequestState("resume-here", "tools/call", 1, System.currentTimeMillis() + 60_000L);
 		var token = codec1.seal(state, "tools/call" + '\u0000' + "2026-07-28");
@@ -363,4 +346,20 @@ class McpBindings_Test extends TestBase {
 		assertTrue(unsealed.isPresent(), "a requestState sealed on one request must unseal on the next");
 		assertEquals(state, unsealed.get());
 	}
+
+	/**
+	 * TODO-330 regression: the pre-consolidation mixin default accidentally shared its MRTR key/broker
+	 * JVM-wide ({@code SharedMrtrConfig}/{@code SharedSubscriptionBroker}) across every distinct endpoint
+	 * instance. Post-consolidation, {@link McpEndpoint#getMcpOptions()}'s default is per-binding: two distinct
+	 * endpoint instances must resolve to two distinct {@link McpOptions} (and therefore distinct MRTR keys),
+	 * proving the accidental JVM-wide sharing is gone.
+	 */
+	@Test void e06_mixinMrtrConfig_distinctAcrossDistinctEndpointInstances() {
+		var rev1 = (McpRevision)new B().revision();
+		var rev2 = (McpRevision)new B().revision();
+		assertNotSame(rev1.mrtrConfig(), rev2.mrtrConfig(),
+			"two distinct endpoint instances must NOT share the same MRTR config (no JVM-wide sharing)");
+		assertNotSame(rev1.mrtrConfig().getCodec(), rev2.mrtrConfig().getCodec(),
+			"two distinct endpoint instances must NOT share the same AES key (no JVM-wide sharing)");
+	}
 }
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache_Test.java
new file mode 100644
index 0000000000..5ad3895b95
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpEndpointOptionsCache_Test.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.rest.server.mcp.McpServerConfig;
+import org.junit.jupiter.api.Test;
+
+/**
+ * TODO-330 code-review regression coverage: {@link McpEndpointOptionsCache} must key by reference
+ * <b>identity</b>, never by an endpoint's own {@code equals()}/{@code hashCode()}.
+ */
+class McpEndpointOptionsCache_Test {
+
+	/**
+	 * A resource "fixture" that overrides {@code equals}/{@code hashCode} the way a record, a Lombok
+	 * {@code @Data} class, or any other value-ish endpoint type might: every instance of this class
+	 * compares equal to every other instance of this class, and every instance hashes identically.
+	 *
+	 * <p>
+	 * A {@code WeakHashMap}-backed cache (keyed by {@code equals()}/{@code hashCode()}) collapses distinct
+	 * instances of this class onto ONE cache entry, so a second instance would silently observe the FIRST
+	 * instance's memoized {@link McpOptions} (and thus its MRTR AES key and subscription broker) &mdash;
+	 * exactly the JVM-wide sharing regression TODO-330 set out to eliminate.
+	 */
+	public static class EqualsOverridingEndpoint implements McpEndpoint {
+		@Override public McpServerConfig getMcpConfig() { return new McpServerConfig(); }
+		@Override public boolean equals(Object o) { return o instanceof EqualsOverridingEndpoint; }
+		@Override public int hashCode() { return 42; }
+	}
+
+	@Test void a01_fixtureReallyDoesCollapseUnderEqualsHashCode() {
+		var e1 = new EqualsOverridingEndpoint();
+		var e2 = new EqualsOverridingEndpoint();
+		assertEquals(e1, e2, "fixture sanity check: two distinct instances must compare equal");
+		assertEquals(e1.hashCode(), e2.hashCode(), "fixture sanity check: two distinct instances must hash identically");
+		assertNotSame(e1, e2, "fixture sanity check: they must still be distinct object instances");
+	}
+
+	/**
+	 * The core regression: two distinct {@link EqualsOverridingEndpoint} instances &mdash; which compare
+	 * {@code equals()} and hash identically &mdash; must still resolve to two distinct memoized
+	 * {@link McpOptions}. This is the assertion that FAILS (two instances collapse onto one shared
+	 * {@link McpOptions}) against a {@code WeakHashMap}-backed cache keyed by {@code equals()}/
+	 * {@code hashCode()}, and PASSES against the identity-keyed {@link McpEndpointOptionsCache}.
+	 */
+	@Test void a02_distinctInstances_resolveToDistinctMcpOptions_despiteEqualsCollapsing() {
+		var e1 = new EqualsOverridingEndpoint();
+		var e2 = new EqualsOverridingEndpoint();
+		var options1 = McpEndpointOptionsCache.resolve(e1);
+		var options2 = McpEndpointOptionsCache.resolve(e2);
+		assertNotSame(options1, options2,
+			"two distinct endpoint instances must resolve to distinct McpOptions even though equals()/hashCode() "
+			+ "collapse them, or the identity-key contract has regressed back to equals()-keying");
+	}
+
+	/** Same regression, surfaced through the mixin's own {@code subscriptionBroker()} seam. */
+	@Test void a03_distinctInstances_resolveToDistinctBrokers_despiteEqualsCollapsing() {
+		var e1 = new EqualsOverridingEndpoint();
+		var e2 = new EqualsOverridingEndpoint();
+		assertNotSame(e1.subscriptionBroker(), e2.subscriptionBroker(),
+			"two distinct endpoint instances must NOT share a subscription broker even though equals()/hashCode() "
+			+ "collapse them");
+	}
+
+	/** Same regression, surfaced through the mixin's own {@code revision()} seam (MRTR codec/AES key). */
+	@Test void a04_distinctInstances_resolveToDistinctMrtrCodecs_despiteEqualsCollapsing() {
+		var e1 = new EqualsOverridingEndpoint();
+		var e2 = new EqualsOverridingEndpoint();
+		var rev1 = (McpRevision)e1.revision();
+		var rev2 = (McpRevision)e2.revision();
+		assertNotSame(rev1.mrtrConfig(), rev2.mrtrConfig(),
+			"two distinct endpoint instances must NOT share an MRTR config even though equals()/hashCode() collapse them");
+		assertNotSame(rev1.mrtrConfig().getCodec(), rev2.mrtrConfig().getCodec(),
+			"two distinct endpoint instances must NOT share an MRTR AES key even though equals()/hashCode() collapse them");
+	}
+
+	@Test void a05_resolve_isStillMemoizedPerInstance() {
+		var e1 = new EqualsOverridingEndpoint();
+		assertSame(McpEndpointOptionsCache.resolve(e1), McpEndpointOptionsCache.resolve(e1),
+			"repeated resolution of the SAME instance must still return the identical memoized McpOptions");
+	}
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
new file mode 100644
index 0000000000..5f55bb31e3
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpOptions_Test.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.server.mcp.v20260728;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.bean.mcp.v20260728.PromptCapability;
+import org.apache.juneau.bean.mcp.v20260728.ServerCapabilities;
+import org.apache.juneau.rest.server.mcp.BasicMcpSubscriptionBroker;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Coverage for {@link McpOptions}, the v2 aggregate consolidating capabilities/instructions/cache/mrtr/
+ * subscriptions/subscriptionBroker.
+ */
+class McpOptions_Test {
+
+	// -------- capabilities / instructions: plain getter/setter round-trips ---------
+
+	@Test void a01_capabilities_defaultsToNull() {
+		assertNull(new McpOptions().getCapabilities());
+	}
+
+	@Test void a02_capabilities_setterRoundTrips() {
+		var caps = new ServerCapabilities().setPrompts(new PromptCapability());
+		var o = new McpOptions().setCapabilities(caps);
+		assertSame(caps, o.getCapabilities());
+	}
+
+	@Test void a03_capabilities_setterAcceptsNull() {
+		var o = new McpOptions().setCapabilities(new ServerCapabilities()).setCapabilities(null);
+		assertNull(o.getCapabilities());
+	}
+
+	@Test void a04_instructions_defaultsToNull() {
+		assertNull(new McpOptions().getInstructions());
+	}
+
+	@Test void a05_instructions_setterRoundTrips() {
+		var o = new McpOptions().setInstructions("Use tool 'echo' to test.");
+		assertEquals("Use tool 'echo' to test.", o.getInstructions());
+	}
+
+	@Test void a06_instructions_setterAcceptsNull() {
+		var o = new McpOptions().setInstructions("x").setInstructions(null);
+		assertNull(o.getInstructions());
+	}
+
+	// -------- cache: nested config, replace-outright, and Consumer configure-block ---------
+
+	@Test void b01_cache_defaultsToNonNullFrameworkOwnedInstance() {
+		assertNotNull(new McpOptions().getCache());
+	}
+
+	@Test void b02_setCache_replacesOutrightAndRoundTrips() {
+		var cache = new McpCacheConfig();
+		var o = new McpOptions().setCache(cache);
+		assertSame(cache, o.getCache());
+	}
+
+	@Test void b03_setCache_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().setCache(null));
+		assertEquals("cache must not be null", e.getMessage());
+	}
+
+	@Test void b04_cacheConsumer_mutatesFrameworkOwnedInstanceInPlace() {
+		var o = new McpOptions();
+		var before = o.getCache();
+		var same = o.cache(c -> c.setToolsList(new McpCacheHint().setTtlMs(60_000)));
+		assertSame(o, same, "cache(Consumer) must return this for chaining");
+		assertSame(before, o.getCache(), "the Consumer block must mutate the existing nested instance in place, never replace it");
+		assertEquals(Integer.valueOf(60_000), o.getCache().getToolsList().getTtlMs());
+	}
+
+	@Test void b05_cacheConsumer_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().cache(null));
+		assertEquals("consumer must not be null", e.getMessage());
+	}
+
+	// -------- mrtr: nested config, replace-outright, and Consumer configure-block ---------
+
+	@Test void c01_mrtr_defaultsToNonNullFrameworkOwnedInstance() {
+		assertNotNull(new McpOptions().getMrtr());
+	}
+
+	@Test void c02_setMrtr_replacesOutrightAndRoundTrips() {
+		var mrtr = new McpMrtrConfig();
+		var o = new McpOptions().setMrtr(mrtr);
+		assertSame(mrtr, o.getMrtr());
+	}
+
+	@Test void c03_setMrtr_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().setMrtr(null));
+		assertEquals("mrtr must not be null", e.getMessage());
+	}
+
+	@Test void c04_mrtrConsumer_mutatesFrameworkOwnedInstanceInPlace() {
+		var o = new McpOptions();
+		var before = o.getMrtr();
+		var same = o.mrtr(m -> m.setTtlMs(600_000L));
+		assertSame(o, same, "mrtr(Consumer) must return this for chaining");
+		assertSame(before, o.getMrtr(), "the Consumer block must mutate the existing nested instance in place, never replace it");
+		assertEquals(600_000L, o.getMrtr().getTtlMs());
+	}
+
+	@Test void c05_mrtrConsumer_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().mrtr(null));
+		assertEquals("consumer must not be null", e.getMessage());
+	}
+
+	// -------- subscriptions: nested config, replace-outright, and Consumer configure-block ---------
+
+	@Test void d01_subscriptions_defaultsToNonNullFrameworkOwnedInstance() {
+		assertNotNull(new McpOptions().getSubscriptions());
+	}
+
+	@Test void d02_setSubscriptions_replacesOutrightAndRoundTrips() {
+		var subs = new McpSubscriptionsConfig();
+		var o = new McpOptions().setSubscriptions(subs);
+		assertSame(subs, o.getSubscriptions());
+	}
+
+	@Test void d03_setSubscriptions_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().setSubscriptions(null));
+		assertEquals("subscriptions must not be null", e.getMessage());
+	}
+
+	@Test void d04_subscriptionsConsumer_mutatesFrameworkOwnedInstanceInPlace() {
+		var o = new McpOptions();
+		var before = o.getSubscriptions();
+		var same = o.subscriptions(s -> s.setQueueSize(2048));
+		assertSame(o, same, "subscriptions(Consumer) must return this for chaining");
+		assertSame(before, o.getSubscriptions(), "the Consumer block must mutate the existing nested instance in place, never replace it");
+		assertEquals(2048, o.getSubscriptions().getQueueSize());
+	}
+
+	@Test void d05_subscriptionsConsumer_nullThrows() {
+		var e = assertThrows(IllegalArgumentException.class, () -> new McpOptions().subscriptions(null));
+		assertEquals("consumer must not be null", e.getMessage());
+	}
+
+	// -------- subscriptionBroker: explicit-slot getter/setter vs. resolveSubscriptionBroker() derivation ---------
+
+	@Test void e01_getSubscriptionBroker_defaultsToNull() {
+		assertNull(new McpOptions().getSubscriptionBroker(),
+			"the explicit-slot getter must default to null (meaning framework-derived), unlike resolveSubscriptionBroker()");
+	}
+
+	@Test void e02_resolveSubscriptionBroker_neverNull_derivesADefault() {
+		assertNotNull(new McpOptions().resolveSubscriptionBroker(),
+			"resolveSubscriptionBroker() is the effective accessor and must never return null");
+	}
+
+	@Test void e03_resolveSubscriptionBroker_memoizesTheDerivedInstance() {
+		var o = new McpOptions();
+		assertSame(o.resolveSubscriptionBroker(), o.resolveSubscriptionBroker(),
+			"repeated calls must return the SAME derived broker, or subscribers/publishers would split across registries");
+	}
+
+	@Test void e04_setSubscriptionBroker_explicitValueIsReturnedAsIsByResolve() {
+		var broker = new BasicMcpSubscriptionBroker(16);
+		var o = new McpOptions().setSubscriptionBroker(broker);
+		assertSame(broker, o.getSubscriptionBroker());
+		assertSame(broker, o.resolveSubscriptionBroker(), "an explicitly-set broker must be returned as-is, never overridden by derivation");
+	}
+
+	@Test void e05_setSubscriptionBrokerNull_revertsToDerivedDefault() {
+		var explicit = new BasicMcpSubscriptionBroker(16);
+		var o = new McpOptions().setSubscriptionBroker(explicit);
+		assertSame(explicit, o.resolveSubscriptionBroker());
+
+		o.setSubscriptionBroker(null);
+		assertNull(o.getSubscriptionBroker());
+		assertNotNull(o.resolveSubscriptionBroker(), "after reverting to null, resolveSubscriptionBroker() must derive a default rather than staying null");
+		assertNotSame(explicit, o.resolveSubscriptionBroker(), "the derived default must not be the previously-set explicit broker");
+	}
+
+	@Test void e06_resolveSubscriptionBroker_sizedFromSubscriptionsQueueSize() {
+		var o = new McpOptions().subscriptions(s -> s.setQueueSize(7));
+		assertInstanceOf(BasicMcpSubscriptionBroker.class, o.resolveSubscriptionBroker());
+	}
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerBinding_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerBinding_Test.java
index af3498ca7e..d4b103c857 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerBinding_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerBinding_Test.java
@@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.*;
 import java.util.concurrent.*;
-import java.util.concurrent.atomic.*;
 
 import org.apache.juneau.marshall.json.*;
 import org.apache.juneau.rest.server.*;
@@ -50,79 +49,53 @@ class McpSubscriptionBrokerBinding_Test {
 
 	@Test void a03_servletSubscriptionsConfig_isLazilyCachedAcrossCalls() {
 		var servlet = new A();
-		assertSame(servlet.getSubscriptionsConfig(), servlet.getSubscriptionsConfig());
+		assertSame(servlet.getMcpOptions().getSubscriptions(), servlet.getMcpOptions().getSubscriptions());
 	}
 
-	@Test void a03b_subscriptionsConfigBean_isAnnotatedAndDelegatesToGetSubscriptionsConfig() throws Exception {
-		var servlet = new A();
-		var m = McpRestServlet.class.getMethod("subscriptionsConfigBean");
+	@Test void a03b_getMcpOptions_isAnnotatedAsBean() throws Exception {
+		var m = McpRestServlet.class.getMethod("getMcpOptions");
 		assertTrue(m.isAnnotationPresent(org.apache.juneau.commons.inject.Bean.class),
-			"subscriptionsConfigBean() must be @Bean-annotated so the RestContext bean store discovers it, "
+			"getMcpOptions() must be @Bean-annotated so the RestContext bean store discovers it, "
 			+ "the same mechanism already used by mcpTraceContextExtractor()");
-		assertSame(servlet.getSubscriptionsConfig(), servlet.subscriptionsConfigBean());
 	}
 
 	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
-	public static class B extends McpRestServlet {
+	public static class D extends McpRestServlet {
 		private static final long serialVersionUID = 1L;
 		@Override protected org.apache.juneau.rest.server.mcp.McpServerConfig createMcpConfig() {
 			return new org.apache.juneau.rest.server.mcp.McpServerConfig();
 		}
-		@Override protected McpSubscriptionsConfig createSubscriptionsConfig() { return null; }
-	}
-
-	@Test void a04_servletNullSubscriptionsConfigFactoryFailsFast() {
-		var e = assertThrows(IllegalStateException.class, () -> new B().getSubscriptionsConfig());
-		assertEquals("createSubscriptionsConfig() returned null", e.getMessage());
-	}
-
-	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
-	public static class C extends McpRestServlet {
-		private static final long serialVersionUID = 1L;
-		@Override protected org.apache.juneau.rest.server.mcp.McpServerConfig createMcpConfig() {
-			return new org.apache.juneau.rest.server.mcp.McpServerConfig();
+		@Override protected McpOptions createMcpOptions() {
+			return new McpOptions().setSubscriptionBroker(new BasicMcpSubscriptionBroker(42));
 		}
-		@Override protected McpSubscriptionBroker createSubscriptionBroker() { return null; }
 	}
 
-	@Test void a05_servletNullBrokerFactoryFailsFast() {
-		var e = assertThrows(IllegalStateException.class, () -> new C().getSubscriptionBroker());
-		assertEquals("createSubscriptionBroker() returned null", e.getMessage());
-	}
-
-	@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
-	public static class D extends McpRestServlet {
-		private static final long serialVersionUID = 1L;
-		final AtomicInteger createCalls = new AtomicInteger();
-		@Override protected org.apache.juneau.rest.server.mcp.McpServerConfig createMcpConfig() {
-			return new org.apache.juneau.rest.server.mcp.McpServerConfig();
-		}
-		@Override protected McpSubscriptionBroker createSubscriptionBroker() {
-			createCalls.incrementAndGet();
-			return new BasicMcpSubscriptionBroker(getSubscriptionsConfig().getQueueSize());
-		}
+	@Test void a04_servletExplicitBroker_isReturnedAsIs() {
+		var servlet = new D();
+		var broker = servlet.getSubscriptionBroker();
+		assertSame(servlet.getMcpOptions().getSubscriptionBroker(), broker);
 	}
 
 	/**
-	 * Mirrors {@code McpBindings_Test#e06_servletMrtrConfig_concurrentFirstAccessPublishesExactlyOneInstance}:
-	 * a plain lazy read would let two racing threads each publish a distinct broker, silently splitting live
-	 * subscription state across two registries. Every concurrent first-access caller must observe the same
-	 * instance and {@code createSubscriptionBroker()} must run exactly once.
+	 * Mirrors {@code McpBindings_Test#e02_servletOptions_concurrentFirstAccessPublishesExactlyOneInstance}, but
+	 * exercises {@link McpOptions#resolveSubscriptionBroker()}'s own inner double-checked-locking directly: a
+	 * plain lazy read would let two racing threads each publish a distinct derived broker, silently splitting
+	 * live subscription state across two registries. Every concurrent first-access caller must observe the same
+	 * derived instance.
 	 */
-	@Test void a06_servletBroker_concurrentFirstAccessPublishesExactlyOneInstance() throws Exception {
-		var servlet = new D();
+	@Test void a05_derivedBroker_concurrentFirstAccessPublishesExactlyOneInstance() throws Exception {
+		var options = new McpOptions();
 		var threads = 16;
 		var pool = Executors.newFixedThreadPool(threads);
 		try {
 			var start = new CountDownLatch(1);
 			var results = new ArrayList<Future<McpSubscriptionBroker>>();
 			for (var i = 0; i < threads; i++)
-				results.add(pool.submit(() -> { start.await(); return servlet.getSubscriptionBroker(); }));
+				results.add(pool.submit(() -> { start.await(); return options.resolveSubscriptionBroker(); }));
 			start.countDown();
 			var first = results.get(0).get();
 			for (var f : results)
-				assertSame(first, f.get(), "every concurrent first-access caller must observe the same published broker");
-			assertEquals(1, servlet.createCalls.get(), "createSubscriptionBroker() must be invoked exactly once under the lock");
+				assertSame(first, f.get(), "every concurrent first-access caller must observe the same derived broker");
 		} finally {
 			pool.shutdownNow();
 		}
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerMixin_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerMixin_Test.java
index d39a59b40f..95f7980fe6 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerMixin_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionBrokerMixin_Test.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.util.*;
 
 import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.rest.mock.classic.MockRestClient;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.mcp.McpServerConfig;
 import org.apache.juneau.rest.server.mcp.McpSubscriptionBroker;
@@ -28,6 +29,9 @@ import org.apache.juneau.rest.server.mcp.McpSubscriptionFilter;
 import org.apache.juneau.rest.server.servlet.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+	"resource" // MockRestClient is a Closeable test helper; lifetime is bounded by the test method.
+})
 class McpSubscriptionBrokerMixin_Test {
 
 	@Rest(path = "/api", serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
@@ -40,37 +44,71 @@ class McpSubscriptionBrokerMixin_Test {
 		assertNotNull(new A().subscriptionBroker());
 	}
 
-	@Test void a02_mixinDefault_returnsSharedInstanceAcrossSeparateEndpointInstances() {
+	@Test void a02_mixinDefault_isStableAcrossCallsOnSameInstance() {
+		var a = new A();
+		assertSame(a.subscriptionBroker(), a.subscriptionBroker(), "the mixin default broker must be memoized per-binding");
+	}
+
+	/**
+	 * TODO-330 regression: the pre-consolidation mixin default accidentally shared its broker JVM-wide
+	 * ({@code SharedSubscriptionBroker}) across every distinct endpoint instance. Post-consolidation, the
+	 * broker is derived per-binding (memoized via {@link McpEndpointOptionsCache}): two distinct endpoint
+	 * instances must resolve to two distinct brokers, proving the accidental JVM-wide sharing is gone.
+	 */
+	@Test void a03_mixinDefault_distinctAcrossDistinctEndpointInstances() {
 		var broker1 = new A().subscriptionBroker();
 		var broker2 = new A().subscriptionBroker();
-		assertSame(broker1, broker2, "the mixin default broker must be a per-process shared instance");
+		assertNotSame(broker1, broker2, "two distinct endpoint instances must NOT share the same broker (no JVM-wide sharing)");
 	}
 
-	@Test void a03_mixinDefault_subscriptionRegisteredOnOneInstanceVisibleOnAnother() {
+	@Test void a04_mixinDefault_subscriptionRegisteredOnOneInstanceNotVisibleOnAnother() {
 		McpSubscriptionBroker broker1 = new A().subscriptionBroker();
 		McpSubscriptionBroker broker2 = new A().subscriptionBroker();
 		var before = broker2.activeCount();
-		var sub = broker1.register("mixin-shared-test-s1", new McpSubscriptionFilter(true, true, true, Set.of()));
-		assertEquals(before + 1, broker2.activeCount(), "a subscription registered via one mixin instance's "
-			+ "broker must be visible via another instance's broker, since both share the JVM-wide default");
+		var sub = broker1.register("mixin-isolated-test-s1", new McpSubscriptionFilter(true, true, true, Set.of()));
+		assertEquals(before, broker2.activeCount(), "a subscription registered via one mixin instance's "
+			+ "broker must NOT be visible via another instance's broker, since each binding now derives its own");
 		sub.close();
 	}
 
-	@Test void a04_mixinDefault_subscriptionsConfigIsFreshPerCallAndEquivalent() {
+	@Test void a05_mixinDefault_subscriptionsConfigIsStableAcrossCallsOnSameInstance() {
 		var a = new A();
-		var config1 = a.subscriptionsConfig();
-		var config2 = a.subscriptionsConfig();
-		assertNotSame(config1, config2, "unlike the broker, the knobs-only config has no live state to "
-			+ "share, so the mixin default (matching cacheConfig()'s precedent) returns a fresh instance");
-		assertEquals(config1.getQueueSize(), config2.getQueueSize());
+		var config1 = a.mcpOptionsBean().getSubscriptions();
+		var config2 = a.mcpOptionsBean().getSubscriptions();
+		assertSame(config1, config2, "McpOptions (and its nested subscriptions config) is memoized per-binding via "
+			+ "the mcpOptionsBean()/McpEndpointOptionsCache seam, so repeated access on the same endpoint instance "
+			+ "returns the identical instance, even though the raw getMcpOptions() override point may return a "
+			+ "fresh instance per call");
 	}
 
-	@Test void a05_subscriptionsConfigBean_isAnnotatedAndDelegatesToSubscriptionsConfig() throws Exception {
+	@Test void a06_mcpOptionsBean_isAnnotatedAndMemoizedPerInstance() throws Exception {
 		var a = new A();
-		var m = McpEndpoint.class.getMethod("subscriptionsConfigBean");
+		var m = McpEndpoint.class.getMethod("mcpOptionsBean");
 		assertTrue(m.isAnnotationPresent(org.apache.juneau.commons.inject.Bean.class),
-			"subscriptionsConfigBean() must be @Bean-annotated so the RestContext bean store discovers it, "
+			"mcpOptionsBean() must be @Bean-annotated so the RestContext bean store discovers it, "
 			+ "the same mechanism already used by mcpTraceContextExtractor()");
-		assertNotNull(a.subscriptionsConfigBean());
+		assertSame(a.mcpOptionsBean(), a.mcpOptionsBean(),
+			"mcpOptionsBean() must return the SAME memoized McpOptions on every call for a given endpoint instance");
+		assertNotSame(a.mcpOptionsBean(), new A().mcpOptionsBean(),
+			"a distinct endpoint instance must resolve to a distinct memoized McpOptions");
+	}
+
+	/**
+	 * IMPORTANT-5 code-review regression: {@code a06} above only checks the {@code @Bean} annotation
+	 * reflectively, which would NOT catch a future break in interface-default {@code @Bean} discovery (for
+	 * example, if the {@code RestContext} bean-collection walk stopped considering default methods on mixin
+	 * interfaces). This test proves the wiring end-to-end through a real {@code RestContext}: the resource's
+	 * bean store must resolve {@link McpOptions} to the exact SAME instance {@link McpEndpoint#mcpOptionsBean()}
+	 * returns, so {@code subscriptions/listen} dispatch (which reads {@code McpOptions} from the bean store,
+	 * not by calling {@code mcpOptionsBean()} directly) never silently falls back to defaults.
+	 */
+	@Test void a07_mcpOptionsBean_isPublishedInRestContextBeanStore_sameInstanceAsMemoized() {
+		var a = new A();
+		MockRestClient.create(a).json().contentType("application/json").accept("application/json").build();
+		var fromBeanStore = a.getContext().getBeanStore().getBean(McpOptions.class)
+			.orElseThrow(() -> new AssertionError("McpOptions bean not found in RestContext's bean store"));
+		assertSame(a.mcpOptionsBean(), fromBeanStore,
+			"the RestContext bean store's published McpOptions must be the SAME instance as the mixin's memoized "
+			+ "mcpOptionsBean(), proving the @Bean interface-default discovery wires the memoized instance end-to-end");
 	}
 }
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsEndToEnd_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsEndToEnd_Test.java
index 2104496bf1..914029374d 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsEndToEnd_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpSubscriptionsEndToEnd_Test.java
@@ -64,6 +64,9 @@ class McpSubscriptionsEndToEnd_Test {
 		return Map.of("Mcp-Method", "tools/call", "Mcp-Name", toolName);
 	}
 
+	@SuppressWarnings({
+		"resource" // Each .addBean(...) link returns 'this' (a Closeable) already owned by the enclosing try-with-resources; Eclipse JDT @Owning warning on the intermediate chain links is by design.
+	})
 	@Test void a01_toolHandler_publishReachesSubscriptionRegisteredThroughTheSameRequestScopedBroker() throws Exception {
 		var broker = new BasicMcpSubscriptionBroker(4);
 		var capturedSub = new AtomicReference<McpSubscription>();
diff --git a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/SubscriptionsListenDispatch_Test.java b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/SubscriptionsListenDispatch_Test.java
index 04d0864caf..ed785379a3 100644
--- a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/SubscriptionsListenDispatch_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/SubscriptionsListenDispatch_Test.java
@@ -68,7 +68,8 @@ class SubscriptionsListenDispatch_Test {
 		"resource" // Returned BeanStore is owned by the caller (a fresh per-test fixture, GC'd with the test); Eclipse JDT @Owning warning is by design.
 	})
 	private static BasicBeanStore ctxWith(McpSubscriptionBroker broker, McpSubscriptionsConfig config) {
-		return new BasicBeanStore().addBean(McpSubscriptionBroker.class, broker).addBean(McpSubscriptionsConfig.class, config);
+		return new BasicBeanStore().addBean(McpSubscriptionBroker.class, broker)
+			.addBean(McpOptions.class, new McpOptions().setSubscriptions(config));
 	}
 
 	// C1: a subscriptions/listen request that never negotiated SSE (Accept missing, or some other media type)
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpointMixin.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpointMixin.java
index 0cae40e298..5cefd5c2b1 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpointMixin.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpointMixin.java
@@ -75,7 +75,8 @@ public interface McpEndpointMixin {
 	 * <p>
 	 * A revision binding that implements {@code subscriptions/listen} (for example
 	 * {@code org.apache.juneau.rest.server.mcp.v20260728.McpEndpoint}) overrides this to return a
-	 * per-process shared broker instance.
+	 * binding-owned broker instance (its own per-binding vs. shared-instance semantics are that
+	 * revision's own choice — see its Javadoc).
 	 *
 	 * @return The subscription broker, or <jk>null</jk> if this endpoint does not support subscriptions.
 	 */
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/BasicMcpSubscriptionBroker_Test.java b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/BasicMcpSubscriptionBroker_Test.java
index 32d5f42ae7..e303acea2e 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/BasicMcpSubscriptionBroker_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/BasicMcpSubscriptionBroker_Test.java
@@ -198,12 +198,10 @@ class BasicMcpSubscriptionBroker_Test {
 	@Test void a11_registerIfUnder_underCap_registersAndReturnsPresent() {
 		var broker = new BasicMcpSubscriptionBroker(4);
 		var result = broker.registerIfUnder(2, "s1", new McpSubscriptionFilter(true, true, true, Set.of()));
-		try {
-			assertTrue(result.isPresent());
+		assertTrue(result.isPresent());
+		try (var sub = result.get()) {
 			assertEquals(1, broker.activeCount());
-			assertFalse(result.get().isClosed());
-		} finally {
-			result.ifPresent(McpSubscription::close);
+			assertFalse(sub.isClosed());
 		}
 	}
 
@@ -223,13 +221,11 @@ class BasicMcpSubscriptionBroker_Test {
 		try {
 			// At cap (1 active, max 1): re-registering the SAME id must still succeed since it replaces, not grows.
 			var second = broker.registerIfUnder(1, "s1", new McpSubscriptionFilter(false, false, false, Set.of()));
-			try {
-				assertTrue(second.isPresent());
+			assertTrue(second.isPresent());
+			try (var sub = second.get()) {
 				assertTrue(first.isClosed());
-				assertFalse(second.get().isClosed());
+				assertFalse(sub.isClosed());
 				assertEquals(1, broker.activeCount());
-			} finally {
-				second.ifPresent(McpSubscription::close);
 			}
 		} finally {
 			first.close();