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/07/29 17:11:28 UTC
(juneau) 10/19: refactor: replace McpDispatcher/Mcp with the McpRevision SPI
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
commit 3420e01ccb7894080c9d26e92d48029e2ad2bc72
Author: James Bognar <ja...@apache.org>
AuthorDate: Tue Jul 28 17:21:46 2026 -0400
refactor: replace McpDispatcher/Mcp with the McpRevision SPI
Moves the JSON-RPC method table verbatim into Mcp20250618Revision in the new
2025-06-18 adapter, rewires McpRestServlet/McpEndpoint to dispatch through
revision() with an McpExchange, and deletes McpDispatcher and the Mcp static
facade. Handler interfaces and McpServerConfig still speak wire types; the
neutral model lands next. Characterization fixtures replay byte-identical.
Co-authored-by: Cursor <cu...@cursor.com>
---
.../juneau/rest/server/mcp/McpCursorCtx_Test.java | 47 ++--
.../Mcp20250618Revision_Test.java} | 59 +++--
.../{ => v20250618}/McpHandlerDefaults_Test.java | 28 +--
.../McpRestServlet20250618_Test.java} | 13 +-
.../mcp/{ => v20250618}/McpServerConfig_Test.java | 3 +-
.../mcp/{ => v20250618}/McpTypedHandlers_Test.java | 45 ++--
.../server/mcp/v20250618/Mcp20250618Revision.java | 230 +++++++++++++++++
.../server/mcp/v20250618/McpEndpoint20250618.java | 66 +++++
.../mcp/v20250618/McpRestServlet20250618.java | 75 ++++++
.../server/mcp/v20250618}/McpTypedHandlers.java | 9 +-
.../mcp/v20250618}/McpTypedPromptHandler.java | 3 +-
.../server/mcp/v20250618}/McpTypedToolHandler.java | 3 +-
.../mcp/v20250618}/Characterization_Test.java | 15 +-
.../mcp/v20250618/Mcp20250618Revision_Test.java} | 59 +++--
.../mcp/v20250618}/McpHandlerDefaults_Test.java | 28 +--
.../v20250618/McpRestServlet20250618_Test.java} | 13 +-
.../mcp/v20250618}/McpServerConfig_Test.java | 3 +-
.../mcp/v20250618}/McpTypedHandlers_Test.java | 45 ++--
.../characterization/CAPS-initialize.request.json | 0
.../characterization/CAPS-initialize.response.json | 0
.../EMPTY-empty-method.request.json | 0
.../EMPTY-empty-method.response.json | 0
.../characterization/EMPTY-initialize.request.json | 0
.../EMPTY-initialize.response.json | 0
.../EMPTY-missing-method.request.json | 0
.../EMPTY-missing-method.response.json | 0
.../EMPTY-notification-ping.request.json | 0
.../EMPTY-notification-ping.response.json | 0
.../EMPTY-notification-unknown.request.json | 0
.../EMPTY-notification-unknown.response.json | 0
.../characterization/EMPTY-ping.request.json | 0
.../characterization/EMPTY-ping.response.json | 0
.../EMPTY-prompts-get-missing-name.request.json | 0
.../EMPTY-prompts-get-missing-name.response.json | 0
.../EMPTY-prompts-get-unknown.request.json | 0
.../EMPTY-prompts-get-unknown.response.json | 0
.../EMPTY-prompts-list.request.json | 0
.../EMPTY-prompts-list.response.json | 0
.../EMPTY-resources-list.request.json | 0
.../EMPTY-resources-list.response.json | 0
.../EMPTY-resources-read-missing-uri.request.json | 0
.../EMPTY-resources-read-missing-uri.response.json | 0
.../EMPTY-resources-read-unknown.request.json | 0
.../EMPTY-resources-read-unknown.response.json | 0
.../EMPTY-tools-call-missing-name.request.json | 0
.../EMPTY-tools-call-missing-name.response.json | 0
.../EMPTY-tools-call-unknown-tool.request.json | 0
.../EMPTY-tools-call-unknown-tool.response.json | 0
.../characterization/EMPTY-tools-list.request.json | 0
.../EMPTY-tools-list.response.json | 0
.../EMPTY-unknown-method.request.json | 0
.../EMPTY-unknown-method.response.json | 0
.../characterization/FULL-initialize.request.json | 0
.../characterization/FULL-initialize.response.json | 0
.../characterization/FULL-prompts-get.request.json | 0
.../FULL-prompts-get.response.json | 0
.../FULL-prompts-list.request.json | 0
.../FULL-prompts-list.response.json | 0
.../FULL-resources-list.request.json | 0
.../FULL-resources-list.response.json | 0
.../FULL-resources-read.request.json | 0
.../FULL-resources-read.response.json | 0
.../characterization/FULL-string-id.request.json | 0
.../characterization/FULL-string-id.response.json | 0
.../FULL-tools-call-arguments-not-map.request.json | 0
...FULL-tools-call-arguments-not-map.response.json | 0
.../FULL-tools-call-error-flag.request.json | 0
.../FULL-tools-call-error-flag.response.json | 0
.../FULL-tools-call-mixed-content.request.json | 0
.../FULL-tools-call-mixed-content.response.json | 0
.../FULL-tools-call-no-arguments.request.json | 0
.../FULL-tools-call-no-arguments.response.json | 0
.../FULL-tools-call-params-not-map.request.json | 0
.../FULL-tools-call-params-not-map.response.json | 0
.../FULL-tools-call-text.request.json | 0
.../FULL-tools-call-text.response.json | 0
.../characterization/FULL-tools-list.request.json | 0
.../characterization/FULL-tools-list.response.json | 0
.../PAGED-tools-list-first.request.json | 0
.../PAGED-tools-list-first.response.json | 0
.../PAGED-tools-list-second.request.json | 0
.../PAGED-tools-list-second.response.json | 0
.../THROW-mcp-exception.request.json | 0
.../THROW-mcp-exception.response.json | 0
.../characterization/THROW-no-message.request.json | 0
.../THROW-no-message.response.json | 0
.../THROW-runtime-exception.request.json | 0
.../THROW-runtime-exception.response.json | 0
.../org/apache/juneau/rest/server/mcp/Mcp.java | 59 -----
.../juneau/rest/server/mcp/McpDispatcher.java | 271 ---------------------
.../apache/juneau/rest/server/mcp/McpEndpoint.java | 23 +-
.../juneau/rest/server/mcp/McpPromptHandler.java | 5 +-
.../juneau/rest/server/mcp/McpResourceHandler.java | 5 +-
.../juneau/rest/server/mcp/McpRestServlet.java | 21 +-
.../juneau/rest/server/mcp/McpServerConfig.java | 6 +-
.../juneau/rest/server/mcp/McpToolHandler.java | 5 +-
.../juneau/rest/server/mcp/McpCursorCtx_Test.java} | 47 ++--
97 files changed, 610 insertions(+), 576 deletions(-)
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
similarity index 53%
copy from juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
copy to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
index 524737d38a..4ee720fd73 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
@@ -16,36 +16,29 @@
*/
package org.apache.juneau.rest.server.mcp;
-import org.apache.juneau.bean.mcp.v20250618.*;
+import static org.junit.jupiter.api.Assertions.*;
+
import org.apache.juneau.commons.inject.*;
+import org.junit.jupiter.api.*;
/**
- * Typed variant of {@link McpPromptHandler} where MCP {@code prompts/get} arguments bind into a Juneau bean.
- *
- * @param <A> Argument bean type.
+ * Coverage for {@link McpCursor} bean-store passthrough.
*/
-public interface McpTypedPromptHandler<A> {
-
- /**
- * Returns the static descriptor for this prompt.
- *
- * @return The prompt descriptor. Never {@code null}.
- */
- Prompt descriptor();
-
- /**
- * Returns the runtime argument class for binding.
- *
- * @return The argument class. Never {@code null}.
- */
- Class<A> argumentType();
+class McpCursorCtx_Test {
- /**
- * Renders the prompt.
- *
- * @param arguments Bound argument bean (may be {@code null} when no arguments are supplied).
- * @param ctx Per-request bean store.
- * @return The rendered prompt.
- */
- GetPromptResult get(A arguments, BeanStore ctx);
+ @Test
+ void cursor_passes_ctx_to_strategy() {
+ // Verify ctx parameter reaches the cursor (covers the BeanStore parameter passthrough).
+ var bs = new BasicBeanStore();
+ var got = new Object[1];
+ McpCursor c = new McpCursor() {
+ @Override
+ public <T> McpPage<T> page(java.util.List<T> all, String cursor, BeanStore ctx) {
+ got[0] = ctx;
+ return new McpPage<>(all, null);
+ }
+ };
+ c.page(java.util.List.of(), null, bs);
+ assertSame(bs, got[0]);
+ }
}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
similarity index 82%
rename from juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java
rename to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
index f2773d7d6d..5deec891fa 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -25,14 +25,14 @@ import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
- * Coverage for {@link McpDispatcher}.
+ * Coverage for {@link Mcp20250618Revision}.
*/
-class McpDispatcher_Test {
+class Mcp20250618Revision_Test {
- private final McpDispatcher dispatcher = new McpDispatcher();
private final BeanStore ctx = new BasicBeanStore();
private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,CallToolResult> fn) {
@@ -82,7 +82,7 @@ class McpDispatcher_Test {
}
private JsonRpcResponse send(McpServerConfig config, JsonRpcRequest r) {
- return dispatcher.dispatch(r, config, ctx);
+ return new Mcp20250618Revision(null).dispatch(new McpExchange(r, n -> null), config, ctx);
}
@Test
@@ -99,7 +99,7 @@ class McpDispatcher_Test {
assertNotNull(result.getCapabilities().getTools());
assertNotNull(result.getCapabilities().getPrompts());
assertNotNull(result.getCapabilities().getResources());
- assertString(McpDispatcher.DEFAULT_SERVER_NAME, result.getServerInfo().getName());
+ assertString(Mcp20250618Revision.DEFAULT_SERVER_NAME, result.getServerInfo().getName());
}
@Test
@@ -123,19 +123,19 @@ class McpDispatcher_Test {
@Test
void method_notFound() {
var resp = send(new McpServerConfig(), req(1, "no/such/method", null));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
@Test
void missing_method() {
var resp = send(new McpServerConfig(), req(1, null, null));
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
@Test
void empty_method_string() {
var resp = send(new McpServerConfig(), req(1, "", null));
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
@Test
@@ -183,8 +183,8 @@ class McpDispatcher_Test {
@Test
void nullEnvelope_returnsInvalidRequest() {
- var resp = dispatcher.dispatch(null, new McpServerConfig(), ctx);
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ var resp = new Mcp20250618Revision(null).dispatch(new McpExchange(null, n -> null), new McpServerConfig(), ctx);
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
// -------- tools/list ---------
@@ -230,27 +230,27 @@ class McpDispatcher_Test {
@Test
void tools_call_missingName_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.TOOLS_CALL, JsonMap.of("arguments", JsonMap.of())));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void tools_call_unknownTool_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "missing")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
@Test
void tools_call_argumentsNotObject_throwsInvalidParams() {
var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult()));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e", "arguments", "string-not-map")));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void tools_call_paramsNotMap_invalidParams() {
var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult()));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, "not-a-map"));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
@@ -270,7 +270,7 @@ class McpDispatcher_Test {
throw new RuntimeException("boom");
}));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e")));
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
assertString("boom", resp.getError().getMessage());
}
@@ -280,7 +280,7 @@ class McpDispatcher_Test {
throw new IllegalStateException();
}));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e")));
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
assertString("IllegalStateException", resp.getError().getMessage());
}
@@ -298,13 +298,13 @@ class McpDispatcher_Test {
@Test
void prompts_get_missingName_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.PROMPTS_GET, JsonMap.of()));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void prompts_get_unknown_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.PROMPTS_GET, JsonMap.of("name", "missing")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
// -------- resources ---------
@@ -321,13 +321,13 @@ class McpDispatcher_Test {
@Test
void resources_read_missingUri_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.RESOURCES_READ, JsonMap.of()));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void resources_read_unknown_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.RESOURCES_READ, JsonMap.of("uri", "ghost://")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
// -------- pagination cursor passthrough ---------
@@ -341,12 +341,19 @@ class McpDispatcher_Test {
assertString("1", resp.getNextCursor());
}
- // -------- Mcp facade ---------
+ // -------- error code table ---------
@Test
- void facade_dispatches() {
- var config = new McpServerConfig();
- var resp = Mcp.handle(req(1, McpMethods.PING, null), config, ctx);
- assertNotNull(resp);
+ void errorCode_tableIsComplete() {
+ var a = new Mcp20250618Revision(null);
+ assertEquals(-32600, a.errorCode(McpErrorKind.INVALID_REQUEST));
+ assertEquals(-32601, a.errorCode(McpErrorKind.UNKNOWN_METHOD));
+ assertEquals(-32601, a.errorCode(McpErrorKind.TOOL_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32601, a.errorCode(McpErrorKind.PROMPT_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32601, a.errorCode(McpErrorKind.RESOURCE_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32602, a.errorCode(McpErrorKind.INVALID_PARAMS));
+ assertEquals(-32603, a.errorCode(McpErrorKind.INTERNAL_ERROR));
+ assertEquals(-32700, a.errorCode(McpErrorKind.PARSE_ERROR));
+ assertEquals("2025-06-18", a.protocolVersion());
}
}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
similarity index 75%
rename from juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java
rename to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
index 94f4a309cd..55b78d910e 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
@@ -14,13 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
@@ -57,29 +58,4 @@ class McpHandlerDefaults_Test {
assertNotNull(ctor.newInstance());
});
}
-
- @Test
- void mcp_facade_constructor_isPrivate() {
- assertDoesNotThrow(() -> {
- var ctor = Mcp.class.getDeclaredConstructor();
- ctor.setAccessible(true);
- assertNotNull(ctor.newInstance());
- });
- }
-
- @Test
- void cursor_passes_ctx_to_strategy() {
- // Verify ctx parameter reaches the cursor (covers the BeanStore parameter passthrough).
- var bs = new BasicBeanStore();
- var got = new Object[1];
- McpCursor c = new McpCursor() {
- @Override
- public <T> McpPage<T> page(java.util.List<T> all, String cursor, BeanStore ctx) {
- got[0] = ctx;
- return new McpPage<>(all, null);
- }
- };
- c.page(java.util.List.of(), null, bs);
- assertSame(bs, got[0]);
- }
}
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
similarity index 93%
rename from juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java
rename to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
index 53518668c1..ca289b1eaf 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -28,15 +28,16 @@ import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.rest.mock.classic.*;
import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
- * End-to-end coverage for {@link McpRestServlet} via {@link MockRestClient}.
+ * End-to-end coverage for {@link McpRestServlet20250618} via {@link MockRestClient}.
*/
@SuppressWarnings({
"resource" // Test helpers return Closeables; Eclipse JDT @Owning warning is by design.
})
-class McpRestServlet_Test extends TestBase {
+class McpRestServlet20250618_Test extends TestBase {
private static final JsonParser PAR = JsonParser.create()
.typePropertyName(Content.class, "type")
@@ -44,7 +45,7 @@ class McpRestServlet_Test extends TestBase {
.build();
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class A extends McpRestServlet {
+ public static class A extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override
@@ -115,7 +116,7 @@ class McpRestServlet_Test extends TestBase {
@Rest(path = "/api", serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
@org.apache.juneau.marshall.serializer.SerializerConfig(addBeanTypes = "true")
- public static class B extends org.apache.juneau.rest.server.servlet.BasicRestServlet implements McpEndpoint {
+ public static class B extends org.apache.juneau.rest.server.servlet.BasicRestServlet implements McpEndpoint20250618 {
private static final long serialVersionUID = 1L;
@Override
@@ -146,7 +147,7 @@ class McpRestServlet_Test extends TestBase {
// -------- failure modes --------
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class C extends McpRestServlet {
+ public static class C extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override
protected McpServerConfig createMcpConfig() {
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
similarity index 97%
rename from juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java
rename to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
index b0000ea739..155c7798a7 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -23,6 +23,7 @@ import java.util.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
similarity index 90%
rename from juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java
rename to juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
index 7c2ac864ac..72fcf1da66 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -25,6 +25,7 @@ import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
@@ -50,7 +51,11 @@ class McpTypedHandlers_Test {
}
private final BeanStore ctx = new BasicBeanStore();
- private final McpDispatcher dispatcher = new McpDispatcher();
+ private final Mcp20250618Revision revision = new Mcp20250618Revision(null);
+
+ private JsonRpcResponse dispatch(JsonRpcRequest req, McpServerConfig config) {
+ return revision.dispatch(new McpExchange(req, n -> null), config, ctx);
+ }
@Test
void typedTool_argsBound_andResultWrappedAsText() {
@@ -78,7 +83,7 @@ class McpTypedHandlers_Test {
.setId(1)
.setMethod(McpMethods.TOOLS_CALL)
.setParams(JsonMap.of("name", "echo", "arguments", JsonMap.of("message", "hi", "repeat", 3)));
- var resp = dispatcher.dispatch(req, config, ctx);
+ var resp = dispatch(req, config);
var ctr = (CallToolResult) resp.getResult();
var text = ((TextContent) ctr.getContent().get(0)).getText();
assertContains("\"text\":\"hi:3\"", text);
@@ -109,7 +114,7 @@ class McpTypedHandlers_Test {
.setId(1)
.setMethod(McpMethods.TOOLS_CALL)
.setParams(JsonMap.of("name", "d"));
- var resp = dispatcher.dispatch(req, config, ctx);
+ var resp = dispatch(req, config);
assertSame(ctr, resp.getResult());
}
@@ -124,9 +129,9 @@ class McpTypedHandlers_Test {
public String call(EchoArgs args, BeanStore ctx) { return "hello"; }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "s")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "s")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("hello", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -142,9 +147,9 @@ class McpTypedHandlers_Test {
public EchoResult call(EchoArgs args, BeanStore ctx) { return null; }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "n")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "n")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -162,9 +167,9 @@ class McpTypedHandlers_Test {
}
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "z")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "z")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("null", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -181,11 +186,11 @@ class McpTypedHandlers_Test {
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
// Bad: 'repeat' should be int, supply a non-numeric value to trigger parser failure.
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
.setMethod(McpMethods.TOOLS_CALL)
- .setParams(JsonMap.of("name", "x", "arguments", JsonMap.of("repeat", "not-an-int"))), config, ctx);
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ .setParams(JsonMap.of("name", "x", "arguments", JsonMap.of("repeat", "not-an-int"))), config);
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
@@ -202,9 +207,9 @@ class McpTypedHandlers_Test {
};
var raw = McpTypedHandlers.adaptPrompt(typed);
var config = new McpServerConfig().addPrompt(raw);
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.PROMPTS_GET).setParams(JsonMap.of("name", "p")), config, ctx);
+ .setMethod(McpMethods.PROMPTS_GET).setParams(JsonMap.of("name", "p")), config);
var pr = (GetPromptResult) resp.getResult();
assertString("null", pr.getDescription());
}
@@ -260,10 +265,10 @@ class McpTypedHandlers_Test {
public Unserializable call(EchoArgs args, BeanStore ctx) { return new Unserializable(); }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "u")), config, ctx);
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "u")), config);
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
}
@Test
@@ -280,10 +285,10 @@ class McpTypedHandlers_Test {
};
var raw = McpTypedHandlers.adaptPrompt(typed);
var config = new McpServerConfig().addPrompt(raw);
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
.setMethod(McpMethods.PROMPTS_GET)
- .setParams(JsonMap.of("name", "p", "arguments", JsonMap.of("message", "hello"))), config, ctx);
+ .setParams(JsonMap.of("name", "p", "arguments", JsonMap.of("message", "hello"))), config);
var pr = (GetPromptResult) resp.getResult();
assertString("hello", pr.getDescription());
}
diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision.java
new file mode 100644
index 0000000000..09da0a545c
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision.java
@@ -0,0 +1,230 @@
+/*
+ * 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.v20250618;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Shorts.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.bean.jsonrpc.*;
+import org.apache.juneau.bean.mcp.v20250618.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.rest.server.mcp.*;
+
+/**
+ * {@link McpRevision} implementation for MCP revision {@code 2025-06-18}.
+ *
+ * <p>
+ * Owns this revision's JSON-RPC method table and error-code table. Note the error-code table
+ * deliberately reproduces a known-wrong mapping: {@link McpErrorKind#UNKNOWN_METHOD},
+ * {@link McpErrorKind#TOOL_NOT_FOUND}, {@link McpErrorKind#PROMPT_NOT_FOUND} and
+ * {@link McpErrorKind#RESOURCE_NOT_FOUND} all report {@code -32601}, which is only actually correct
+ * for the first. Preserving that is intentional — the corrective fix is tracked separately and is
+ * not part of this re-layering.
+ *
+ * <p>
+ * <b>Constructed per binding, not shared as a singleton (Correction C8).</b> The bound servlet or
+ * mixin builds a fresh instance on every {@code revision()} call, passing its own
+ * {@code capabilities()} hook result into the constructor — see {@link McpRestServlet20250618} and
+ * {@link McpEndpoint20250618}. That is how this revision learns which explicit capabilities (if any)
+ * to advertise, with no channel back into core: this class is otherwise entirely stateless, so
+ * constructing a new one per call costs nothing.
+ */
+public final class Mcp20250618Revision implements McpRevision {
+
+ private final ServerCapabilities capabilities;
+
+ /**
+ * Constructor.
+ *
+ * @param capabilities Explicit capabilities to advertise on {@code initialize}, or <jk>null</jk>
+ * to auto-derive from the registered tool/prompt/resource lists (today's default behavior).
+ */
+ public Mcp20250618Revision(ServerCapabilities capabilities) {
+ this.capabilities = capabilities;
+ }
+
+ /** JSON-RPC error code: parse error. Never reported by this revision (see {@link McpErrorKind#PARSE_ERROR}). */
+ public static final int CODE_PARSE_ERROR = -32700;
+
+ /** JSON-RPC error code: invalid request. */
+ public static final int CODE_INVALID_REQUEST = -32600;
+
+ /** JSON-RPC error code: method not found. */
+ public static final int CODE_METHOD_NOT_FOUND = -32601;
+
+ /** JSON-RPC error code: invalid params. */
+ public static final int CODE_INVALID_PARAMS = -32602;
+
+ /** JSON-RPC error code: internal error. */
+ public static final int CODE_INTERNAL_ERROR = -32603;
+
+ /** Default server name reported by {@code initialize} when the config supplies no server identity. */
+ public static final String DEFAULT_SERVER_NAME = "juneau-rest-server-mcp";
+
+ @Override /* McpRevision */
+ public String protocolVersion() {
+ return McpProtocol.VERSION_2025_06_18;
+ }
+
+ @Override /* McpRevision */
+ public int errorCode(McpErrorKind kind) {
+ return switch (kind) {
+ case INVALID_REQUEST -> CODE_INVALID_REQUEST;
+ case UNKNOWN_METHOD, TOOL_NOT_FOUND, PROMPT_NOT_FOUND, RESOURCE_NOT_FOUND -> CODE_METHOD_NOT_FOUND;
+ case INVALID_PARAMS -> CODE_INVALID_PARAMS;
+ case INTERNAL_ERROR -> CODE_INTERNAL_ERROR;
+ case PARSE_ERROR -> CODE_PARSE_ERROR;
+ };
+ }
+
+ @Override /* McpRevision */
+ public JsonRpcResponse dispatch(McpExchange exchange, McpServerConfig config, BeanStore ctx) {
+ assertArgNotNull("exchange", exchange);
+ assertArgNotNull("config", config);
+ assertArgNotNull("ctx", ctx);
+
+ var req = exchange.request();
+ if (req == null)
+ return JsonRpcResponse.errorResponse(null, errorCode(McpErrorKind.INVALID_REQUEST), "Request envelope is null");
+
+ var id = req.getId();
+ var method = req.getMethod();
+
+ if (isEmpty(method))
+ return JsonRpcResponse.notification(id) ? null
+ : JsonRpcResponse.errorResponse(id, errorCode(McpErrorKind.INVALID_REQUEST), "Missing method");
+
+ try {
+ var result = invoke(method, req.getParams(), config, ctx);
+ return JsonRpcResponse.notification(id) ? null : JsonRpcResponse.ok(id, result);
+ } catch (McpException e) {
+ return JsonRpcResponse.notification(id) ? null : new JsonRpcResponse()
+ .setJsonrpc(McpProtocol.JSON_RPC_2_0)
+ .setId(id)
+ .setError(e.toJsonRpcError());
+ } catch (Exception e) {
+ if (JsonRpcResponse.notification(id))
+ return null;
+ var message = e.getMessage() == null ? cns(e) : e.getMessage();
+ return JsonRpcResponse.errorResponse(id, errorCode(McpErrorKind.INTERNAL_ERROR), message, JsonMap.of("type", cn(e)));
+ }
+ }
+
+ private Object invoke(String method, Object params, McpServerConfig config, BeanStore ctx) {
+ switch (method) {
+ case McpMethods.INITIALIZE:
+ return initialize(config);
+ case McpMethods.PING:
+ return new JsonMap();
+ case McpMethods.TOOLS_LIST:
+ return listTools(config, params, ctx);
+ case McpMethods.TOOLS_CALL:
+ return callTool(config, params, ctx);
+ case McpMethods.PROMPTS_LIST:
+ return listPrompts(config, params, ctx);
+ case McpMethods.PROMPTS_GET:
+ return getPrompt(config, params, ctx);
+ case McpMethods.RESOURCES_LIST:
+ return listResources(config, params, ctx);
+ case McpMethods.RESOURCES_READ:
+ return readResource(config, params, ctx);
+ default:
+ throw new McpException(errorCode(McpErrorKind.UNKNOWN_METHOD), "Method not found: " + method);
+ }
+ }
+
+ private InitializeResult initialize(McpServerConfig config) {
+ var caps = config.getCapabilities();
+ if (caps == null) {
+ caps = new ServerCapabilities();
+ if (! config.getTools().isEmpty())
+ caps.setTools(new ToolCapability());
+ if (! config.getPrompts().isEmpty())
+ caps.setPrompts(new PromptCapability());
+ if (! config.getResources().isEmpty())
+ caps.setResources(new ResourceCapability());
+ }
+ var info = config.getServerInfo();
+ if (info == null)
+ info = new Implementation().setName(DEFAULT_SERVER_NAME).setVersion("unknown");
+ return new InitializeResult()
+ .setProtocolVersion(config.getProtocolVersion())
+ .setCapabilities(caps)
+ .setServerInfo(info)
+ .setInstructions(config.getInstructions());
+ }
+
+ private static ListToolsResult listTools(McpServerConfig config, Object params, BeanStore ctx) {
+ var descriptors = config.getTools().stream().map(McpToolHandler::descriptor).toList();
+ var page = config.getCursor().page(descriptors, McpCursor.cursorOf(params), ctx);
+ return new ListToolsResult().setTools(page.items()).setNextCursor(page.nextCursor());
+ }
+
+ private CallToolResult callTool(McpServerConfig config, Object params, BeanStore ctx) {
+ var p = McpParamUtils.asMap(params);
+ var name = McpParamUtils.strParam(p, "name");
+ if (name == null)
+ throw new McpException(errorCode(McpErrorKind.INVALID_PARAMS), "Missing tool name");
+ var handler = config.getTools().stream()
+ .filter(h -> name.equals(h.descriptor().getName()))
+ .findFirst()
+ .orElseThrow(() -> new McpException(errorCode(McpErrorKind.TOOL_NOT_FOUND), "Tool not found: " + name));
+ var args = McpParamUtils.mapParam(p, "arguments");
+ return handler.call(args, ctx);
+ }
+
+ private static ListPromptsResult listPrompts(McpServerConfig config, Object params, BeanStore ctx) {
+ var descriptors = config.getPrompts().stream().map(McpPromptHandler::descriptor).toList();
+ var page = config.getCursor().page(descriptors, McpCursor.cursorOf(params), ctx);
+ return new ListPromptsResult().setPrompts(page.items()).setNextCursor(page.nextCursor());
+ }
+
+ private GetPromptResult getPrompt(McpServerConfig config, Object params, BeanStore ctx) {
+ var p = McpParamUtils.asMap(params);
+ var name = McpParamUtils.strParam(p, "name");
+ if (name == null)
+ throw new McpException(errorCode(McpErrorKind.INVALID_PARAMS), "Missing prompt name");
+ var handler = config.getPrompts().stream()
+ .filter(h -> name.equals(h.descriptor().getName()))
+ .findFirst()
+ .orElseThrow(() -> new McpException(errorCode(McpErrorKind.PROMPT_NOT_FOUND), "Prompt not found: " + name));
+ var args = McpParamUtils.mapParam(p, "arguments");
+ return handler.get(args, ctx);
+ }
+
+ private static ListResourcesResult listResources(McpServerConfig config, Object params, BeanStore ctx) {
+ var descriptors = config.getResources().stream().map(McpResourceHandler::descriptor).toList();
+ var page = config.getCursor().page(descriptors, McpCursor.cursorOf(params), ctx);
+ return new ListResourcesResult().setResources(page.items()).setNextCursor(page.nextCursor());
+ }
+
+ private ReadResourceResult readResource(McpServerConfig config, Object params, BeanStore ctx) {
+ var p = McpParamUtils.asMap(params);
+ var uri = McpParamUtils.strParam(p, "uri");
+ if (uri == null)
+ throw new McpException(errorCode(McpErrorKind.INVALID_PARAMS), "Missing resource uri");
+ var handler = config.getResources().stream()
+ .filter(h -> uri.equals(h.descriptor().getUri()))
+ .findFirst()
+ .orElseThrow(() -> new McpException(errorCode(McpErrorKind.RESOURCE_NOT_FOUND), "Resource not found: " + uri));
+ return handler.read(uri, ctx);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpEndpoint20250618.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpEndpoint20250618.java
new file mode 100644
index 0000000000..bcc0134840
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpEndpoint20250618.java
@@ -0,0 +1,66 @@
+/*
+ * 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.v20250618;
+
+import org.apache.juneau.bean.mcp.v20250618.*;
+import org.apache.juneau.rest.server.mcp.*;
+
+/**
+ * Mixin interface that exposes a {@code 2025-06-18} MCP endpoint at {@code POST /mcp} on any Juneau
+ * REST resource.
+ *
+ * <p>
+ * The mixin path and the servlet-subclass path
+ * ({@link McpRestServlet20250618}) are at parity: both bind the revision at compile time and both
+ * expose the same {@link #capabilities()} hook.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <pre>
+ * @Rest(path="/api")
+ * public class MyResource extends BasicRestServlet implements McpEndpoint20250618 {
+ * @Override
+ * public McpServerConfig getMcpConfig() {
+ * return new McpServerConfig().addTool(new MyEchoTool());
+ * }
+ * }
+ * </pre>
+ */
+public interface McpEndpoint20250618 extends McpEndpoint {
+
+ @Override /* McpEndpoint */
+ default McpRevision revision() {
+ return new Mcp20250618Revision(capabilities());
+ }
+
+ /**
+ * Optional explicit capabilities advertisement for {@code initialize}.
+ *
+ * <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.
+ *
+ * <p>
+ * Stays a {@code default} interface method (Correction C8), for the same reason
+ * {@link McpRestServlet20250618#capabilities()} stays {@code protected}: only {@link #revision()}
+ * above, declared on this same interface, ever calls it.
+ *
+ * @return The explicit capabilities, or <jk>null</jk> to auto-derive.
+ */
+ default ServerCapabilities capabilities() {
+ return null;
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618.java
new file mode 100644
index 0000000000..ab83cfbfee
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618.java
@@ -0,0 +1,75 @@
+/*
+ * 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.v20250618;
+
+import org.apache.juneau.bean.mcp.v20250618.*;
+import org.apache.juneau.rest.server.mcp.*;
+
+/**
+ * Abstract MCP servlet bound to protocol revision {@code 2025-06-18}.
+ *
+ * <p>
+ * Subclass this (rather than {@link McpRestServlet}) to expose a {@code 2025-06-18} endpoint; the
+ * revision binding is a compile-time choice made by which class you extend.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <pre>
+ * @Rest(path="/mcp")
+ * public class MyMcpServlet extends McpRestServlet20250618 {
+ * @Override
+ * protected McpServerConfig createMcpConfig() {
+ * return new McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new MyEchoTool());
+ * }
+ * }
+ * </pre>
+ *
+ * @serial exclude
+ */
+public abstract class McpRestServlet20250618 extends McpRestServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Override /* McpRestServlet */
+ protected McpRevision revision() {
+ return new Mcp20250618Revision(capabilities());
+ }
+
+ /**
+ * Optional explicit capabilities advertisement for {@code initialize}.
+ *
+ * <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
+ * {@code resources.subscribe}, {@code logging}, {@code listChanged}, or {@code experimental},
+ * none of which are derivable from a handler registry.
+ *
+ * <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.
+ *
+ * <p>
+ * Stays {@code protected} (Correction C8): {@link #revision()} above is the only caller, and it
+ * lives in this same class, so there is never a need to reach this hook from outside this
+ * class's own hierarchy — in particular, {@code Mcp20250618Revision} never calls back into it.
+ *
+ * @return The explicit capabilities, or <jk>null</jk> to auto-derive.
+ */
+ protected ServerCapabilities capabilities() {
+ return null;
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java
similarity index 91%
rename from juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java
index 82146efe5d..1d24b73a35 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.commons.utils.AssertionUtils.*;
@@ -24,10 +24,11 @@ import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.marshaller.*;
+import org.apache.juneau.rest.server.mcp.*;
/**
* Adapters that convert {@link McpTypedToolHandler} / {@link McpTypedPromptHandler} into the raw
- * {@link McpToolHandler} / {@link McpPromptHandler} interfaces consumed by {@link McpDispatcher}.
+ * {@link McpToolHandler} / {@link McpPromptHandler} interfaces consumed by {@link Mcp20250618Revision}.
*/
public final class McpTypedHandlers {
@@ -102,7 +103,7 @@ public final class McpTypedHandlers {
var json = Json.of(arguments);
return Json.to(json, type);
} catch (Exception e) {
- throw new McpException(McpDispatcher.CODE_INVALID_PARAMS, "Failed to bind arguments to " + type.getName() + ": " + e.getMessage());
+ throw new McpException(Mcp20250618Revision.CODE_INVALID_PARAMS, "Failed to bind arguments to " + type.getName() + ": " + e.getMessage());
}
}
@@ -118,7 +119,7 @@ public final class McpTypedHandlers {
try {
text = Json.of(result);
} catch (Exception e) {
- throw new McpException(McpDispatcher.CODE_INTERNAL_ERROR, "Failed to serialize tool result: " + e.getMessage());
+ throw new McpException(Mcp20250618Revision.CODE_INTERNAL_ERROR, "Failed to serialize tool result: " + e.getMessage());
}
}
return new CallToolResult().setContent(List.of(new TextContent().setText(text)));
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedPromptHandler.java
similarity index 94%
copy from juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
copy to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedPromptHandler.java
index 524737d38a..ff1686f7c7 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedPromptHandler.java
@@ -14,10 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
/**
* Typed variant of {@link McpPromptHandler} where MCP {@code prompts/get} arguments bind into a Juneau bean.
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedToolHandler.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedToolHandler.java
similarity index 95%
rename from juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedToolHandler.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedToolHandler.java
index 6db25a864d..23e4f69d77 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedToolHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedToolHandler.java
@@ -14,10 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
/**
* Typed variant of {@link McpToolHandler} where MCP {@code tools/call} arguments bind into a Juneau bean.
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/Characterization_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Characterization_Test.java
similarity index 94%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/Characterization_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Characterization_Test.java
index 7b5a597526..207f4ed696 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/Characterization_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Characterization_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.junit.jupiter.api.Assertions.*;
@@ -29,6 +29,7 @@ import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.rest.mock.classic.*;
import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -44,7 +45,7 @@ import org.junit.jupiter.params.provider.*;
* <p>
* Regenerate the {@code *.response.json} files (only ever against known-good code) with:
* <p>
- * {@code mvn test -Drat.skip=true -pl juneau-rest/juneau-rest-server-mcp -Dtest=Characterization_Test -Djuneau.mcp.characterization.write=true}
+ * {@code mvn test -Drat.skip=true -pl juneau-rest/juneau-rest-server-mcp-2025-06-18 -Dtest=Characterization_Test -Djuneau.mcp.characterization.write=true}
*/
@SuppressWarnings({
"resource" // MockRestClient is a Closeable test helper; lifetime is bounded by the test method.
@@ -57,13 +58,13 @@ class Characterization_Test {
// --- fixture servlets -------------------------------------------------------------------
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class F_Empty extends McpRestServlet {
+ public static class F_Empty extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override protected McpServerConfig createMcpConfig() { return new McpServerConfig(); }
}
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class F_Full extends McpRestServlet {
+ public static class F_Full extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override protected McpServerConfig createMcpConfig() {
return new McpServerConfig()
@@ -86,7 +87,7 @@ class Characterization_Test {
}
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class F_Caps extends McpRestServlet {
+ public static class F_Caps extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override protected McpServerConfig createMcpConfig() {
return new McpServerConfig()
@@ -98,7 +99,7 @@ class Characterization_Test {
}
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class F_Paged extends McpRestServlet {
+ public static class F_Paged extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override protected McpServerConfig createMcpConfig() {
return new McpServerConfig()
@@ -109,7 +110,7 @@ class Characterization_Test {
}
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class F_Throw extends McpRestServlet {
+ public static class F_Throw extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override protected McpServerConfig createMcpConfig() {
return new McpServerConfig()
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
similarity index 82%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
index 0120bc06ab..fda7de737f 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpDispatcher_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -25,14 +25,14 @@ import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
- * Coverage for {@link McpDispatcher}.
+ * Coverage for {@link Mcp20250618Revision}.
*/
-class McpDispatcher_Test {
+class Mcp20250618Revision_Test {
- private final McpDispatcher dispatcher = new McpDispatcher();
private final BeanStore ctx = new BasicBeanStore();
private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,CallToolResult> fn) {
@@ -82,7 +82,7 @@ class McpDispatcher_Test {
}
private JsonRpcResponse send(McpServerConfig config, JsonRpcRequest r) {
- return dispatcher.dispatch(r, config, ctx);
+ return new Mcp20250618Revision(null).dispatch(new McpExchange(r, n -> null), config, ctx);
}
@Test
@@ -99,7 +99,7 @@ class McpDispatcher_Test {
assertNotNull(result.getCapabilities().getTools());
assertNotNull(result.getCapabilities().getPrompts());
assertNotNull(result.getCapabilities().getResources());
- assertString(McpDispatcher.DEFAULT_SERVER_NAME, result.getServerInfo().getName());
+ assertString(Mcp20250618Revision.DEFAULT_SERVER_NAME, result.getServerInfo().getName());
}
@Test
@@ -123,19 +123,19 @@ class McpDispatcher_Test {
@Test
void a04_method_notFound() {
var resp = send(new McpServerConfig(), req(1, "no/such/method", null));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
@Test
void a05_missing_method() {
var resp = send(new McpServerConfig(), req(1, null, null));
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
@Test
void a06_empty_method_string() {
var resp = send(new McpServerConfig(), req(1, "", null));
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
@Test
@@ -183,8 +183,8 @@ class McpDispatcher_Test {
@Test
void a08_nullEnvelope_returnsInvalidRequest() {
- var resp = dispatcher.dispatch(null, new McpServerConfig(), ctx);
- assertEquals(McpDispatcher.CODE_INVALID_REQUEST, resp.getError().getCode());
+ var resp = new Mcp20250618Revision(null).dispatch(new McpExchange(null, n -> null), new McpServerConfig(), ctx);
+ assertEquals(Mcp20250618Revision.CODE_INVALID_REQUEST, resp.getError().getCode());
}
// -------- tools/list ---------
@@ -230,27 +230,27 @@ class McpDispatcher_Test {
@Test
void d02_tools_call_missingName_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.TOOLS_CALL, JsonMap.of("arguments", JsonMap.of())));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void d03_tools_call_unknownTool_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "missing")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
@Test
void d04_tools_call_argumentsNotObject_throwsInvalidParams() {
var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult()));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e", "arguments", "string-not-map")));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void d05_tools_call_paramsNotMap_invalidParams() {
var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult()));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, "not-a-map"));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
@@ -270,7 +270,7 @@ class McpDispatcher_Test {
throw new RuntimeException("boom");
}));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e")));
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
assertString("boom", resp.getError().getMessage());
}
@@ -280,7 +280,7 @@ class McpDispatcher_Test {
throw new IllegalStateException();
}));
var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e")));
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
assertString("IllegalStateException", resp.getError().getMessage());
}
@@ -298,13 +298,13 @@ class McpDispatcher_Test {
@Test
void e02_prompts_get_missingName_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.PROMPTS_GET, JsonMap.of()));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void e03_prompts_get_unknown_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.PROMPTS_GET, JsonMap.of("name", "missing")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
// -------- resources ---------
@@ -321,13 +321,13 @@ class McpDispatcher_Test {
@Test
void f02_resources_read_missingUri_invalidParams() {
var resp = send(new McpServerConfig(), req(1, McpMethods.RESOURCES_READ, JsonMap.of()));
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
void f03_resources_read_unknown_methodNotFound() {
var resp = send(new McpServerConfig(), req(1, McpMethods.RESOURCES_READ, JsonMap.of("uri", "ghost://")));
- assertEquals(McpDispatcher.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
+ assertEquals(Mcp20250618Revision.CODE_METHOD_NOT_FOUND, resp.getError().getCode());
}
// -------- pagination cursor passthrough ---------
@@ -341,12 +341,19 @@ class McpDispatcher_Test {
assertString("1", resp.getNextCursor());
}
- // -------- Mcp facade ---------
+ // -------- error code table ---------
@Test
- void h01_facade_dispatches() {
- var config = new McpServerConfig();
- var resp = Mcp.handle(req(1, McpMethods.PING, null), config, ctx);
- assertNotNull(resp);
+ void h01_errorCode_tableIsComplete() {
+ var a = new Mcp20250618Revision(null);
+ assertEquals(-32600, a.errorCode(McpErrorKind.INVALID_REQUEST));
+ assertEquals(-32601, a.errorCode(McpErrorKind.UNKNOWN_METHOD));
+ assertEquals(-32601, a.errorCode(McpErrorKind.TOOL_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32601, a.errorCode(McpErrorKind.PROMPT_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32601, a.errorCode(McpErrorKind.RESOURCE_NOT_FOUND), "known-wrong mapping, preserved deliberately");
+ assertEquals(-32602, a.errorCode(McpErrorKind.INVALID_PARAMS));
+ assertEquals(-32603, a.errorCode(McpErrorKind.INTERNAL_ERROR));
+ assertEquals(-32700, a.errorCode(McpErrorKind.PARSE_ERROR));
+ assertEquals("2025-06-18", a.protocolVersion());
}
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
similarity index 75%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
index 5a623eda45..dbbb8e883e 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpHandlerDefaults_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java
@@ -14,13 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
@@ -57,29 +58,4 @@ class McpHandlerDefaults_Test {
assertNotNull(ctor.newInstance());
});
}
-
- @Test
- void c02_mcp_facade_constructor_isPrivate() {
- assertDoesNotThrow(() -> {
- var ctor = Mcp.class.getDeclaredConstructor();
- ctor.setAccessible(true);
- assertNotNull(ctor.newInstance());
- });
- }
-
- @Test
- void d01_cursor_passes_ctx_to_strategy() {
- // Verify ctx parameter reaches the cursor (covers the BeanStore parameter passthrough).
- var bs = new BasicBeanStore();
- var got = new Object[1];
- McpCursor c = new McpCursor() {
- @Override
- public <T> McpPage<T> page(java.util.List<T> all, String cursor, BeanStore ctx) {
- got[0] = ctx;
- return new McpPage<>(all, null);
- }
- };
- c.page(java.util.List.of(), null, bs);
- assertSame(bs, got[0]);
- }
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
similarity index 93%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
index a129f1979e..53dd178d03 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpRestServlet_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -28,15 +28,16 @@ import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.rest.mock.classic.*;
import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
- * End-to-end coverage for {@link McpRestServlet} via {@link MockRestClient}.
+ * End-to-end coverage for {@link McpRestServlet20250618} via {@link MockRestClient}.
*/
@SuppressWarnings({
"resource" // Test helpers return Closeables; Eclipse JDT @Owning warning is by design.
})
-class McpRestServlet_Test extends TestBase {
+class McpRestServlet20250618_Test extends TestBase {
private static final JsonParser PAR = JsonParser.create()
.typePropertyName(Content.class, "type")
@@ -44,7 +45,7 @@ class McpRestServlet_Test extends TestBase {
.build();
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class A extends McpRestServlet {
+ public static class A extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override
@@ -115,7 +116,7 @@ class McpRestServlet_Test extends TestBase {
@Rest(path = "/api", serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
@org.apache.juneau.marshall.serializer.SerializerConfig(addBeanTypes = "true")
- public static class B extends org.apache.juneau.rest.server.servlet.BasicRestServlet implements McpEndpoint {
+ public static class B extends org.apache.juneau.rest.server.servlet.BasicRestServlet implements McpEndpoint20250618 {
private static final long serialVersionUID = 1L;
@Override
@@ -146,7 +147,7 @@ class McpRestServlet_Test extends TestBase {
// -------- failure modes --------
@Rest(serializers = JsonSerializer.class, parsers = JsonParser.class, defaultAccept = "application/json")
- public static class C extends McpRestServlet {
+ public static class C extends McpRestServlet20250618 {
private static final long serialVersionUID = 1L;
@Override
protected McpServerConfig createMcpConfig() {
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
similarity index 97%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
index 57437172fc..6ec4f9fbd6 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpServerConfig_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -23,6 +23,7 @@ import java.util.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
similarity index 90%
rename from juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
index 50f488eb59..7c5f2c2157 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpTypedHandlers_Test.java
+++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.juneau.rest.server.mcp;
+package org.apache.juneau.rest.server.mcp.v20250618;
import static org.apache.juneau.test.bct.BctAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -25,6 +25,7 @@ import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.rest.server.mcp.*;
import org.junit.jupiter.api.*;
/**
@@ -50,7 +51,11 @@ class McpTypedHandlers_Test {
}
private final BeanStore ctx = new BasicBeanStore();
- private final McpDispatcher dispatcher = new McpDispatcher();
+ private final Mcp20250618Revision revision = new Mcp20250618Revision(null);
+
+ private JsonRpcResponse dispatch(JsonRpcRequest req, McpServerConfig config) {
+ return revision.dispatch(new McpExchange(req, n -> null), config, ctx);
+ }
@Test
void a01_typedTool_argsBound_andResultWrappedAsText() {
@@ -78,7 +83,7 @@ class McpTypedHandlers_Test {
.setId(1)
.setMethod(McpMethods.TOOLS_CALL)
.setParams(JsonMap.of("name", "echo", "arguments", JsonMap.of("message", "hi", "repeat", 3)));
- var resp = dispatcher.dispatch(req, config, ctx);
+ var resp = dispatch(req, config);
var ctr = (CallToolResult) resp.getResult();
var text = ((TextContent) ctr.getContent().get(0)).getText();
assertContains("\"text\":\"hi:3\"", text);
@@ -109,7 +114,7 @@ class McpTypedHandlers_Test {
.setId(1)
.setMethod(McpMethods.TOOLS_CALL)
.setParams(JsonMap.of("name", "d"));
- var resp = dispatcher.dispatch(req, config, ctx);
+ var resp = dispatch(req, config);
assertSame(ctr, resp.getResult());
}
@@ -124,9 +129,9 @@ class McpTypedHandlers_Test {
public String call(EchoArgs args, BeanStore ctx) { return "hello"; }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "s")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "s")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("hello", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -142,9 +147,9 @@ class McpTypedHandlers_Test {
public EchoResult call(EchoArgs args, BeanStore ctx) { return null; }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "n")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "n")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -162,9 +167,9 @@ class McpTypedHandlers_Test {
}
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "z")), config, ctx);
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "z")), config);
var ctr = (CallToolResult) resp.getResult();
assertString("null", ((TextContent) ctr.getContent().get(0)).getText());
}
@@ -181,11 +186,11 @@ class McpTypedHandlers_Test {
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
// Bad: 'repeat' should be int, supply a non-numeric value to trigger parser failure.
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
.setMethod(McpMethods.TOOLS_CALL)
- .setParams(JsonMap.of("name", "x", "arguments", JsonMap.of("repeat", "not-an-int"))), config, ctx);
- assertEquals(McpDispatcher.CODE_INVALID_PARAMS, resp.getError().getCode());
+ .setParams(JsonMap.of("name", "x", "arguments", JsonMap.of("repeat", "not-an-int"))), config);
+ assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode());
}
@Test
@@ -202,9 +207,9 @@ class McpTypedHandlers_Test {
};
var raw = McpTypedHandlers.adaptPrompt(typed);
var config = new McpServerConfig().addPrompt(raw);
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.PROMPTS_GET).setParams(JsonMap.of("name", "p")), config, ctx);
+ .setMethod(McpMethods.PROMPTS_GET).setParams(JsonMap.of("name", "p")), config);
var pr = (GetPromptResult) resp.getResult();
assertString("null", pr.getDescription());
}
@@ -260,10 +265,10 @@ class McpTypedHandlers_Test {
public Unserializable call(EchoArgs args, BeanStore ctx) { return new Unserializable(); }
};
var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed));
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
- .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "u")), config, ctx);
- assertEquals(McpDispatcher.CODE_INTERNAL_ERROR, resp.getError().getCode());
+ .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "u")), config);
+ assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode());
}
@Test
@@ -280,10 +285,10 @@ class McpTypedHandlers_Test {
};
var raw = McpTypedHandlers.adaptPrompt(typed);
var config = new McpServerConfig().addPrompt(raw);
- var resp = dispatcher.dispatch(new JsonRpcRequest()
+ var resp = dispatch(new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1)
.setMethod(McpMethods.PROMPTS_GET)
- .setParams(JsonMap.of("name", "p", "arguments", JsonMap.of("message", "hello"))), config, ctx);
+ .setParams(JsonMap.of("name", "p", "arguments", JsonMap.of("message", "hello"))), config);
var pr = (GetPromptResult) resp.getResult();
assertString("hello", pr.getDescription());
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/CAPS-initialize.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/CAPS-initialize.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/CAPS-initialize.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/CAPS-initialize.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/CAPS-initialize.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/CAPS-initialize.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/CAPS-initialize.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/CAPS-initialize.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-empty-method.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-empty-method.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-empty-method.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-empty-method.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-empty-method.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-empty-method.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-empty-method.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-empty-method.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-initialize.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-initialize.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-initialize.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-initialize.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-initialize.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-initialize.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-initialize.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-initialize.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-missing-method.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-missing-method.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-missing-method.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-missing-method.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-missing-method.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-missing-method.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-missing-method.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-missing-method.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-ping.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-ping.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-ping.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-ping.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-ping.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-ping.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-ping.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-ping.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-unknown.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-unknown.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-unknown.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-unknown.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-unknown.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-unknown.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-notification-unknown.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-notification-unknown.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-ping.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-ping.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-ping.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-ping.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-ping.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-ping.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-ping.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-ping.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-missing-name.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-missing-name.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-missing-name.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-missing-name.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-missing-name.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-missing-name.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-missing-name.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-missing-name.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-unknown.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-unknown.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-unknown.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-unknown.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-unknown.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-unknown.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-get-unknown.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-get-unknown.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-prompts-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-prompts-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-missing-uri.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-missing-uri.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-missing-uri.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-missing-uri.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-missing-uri.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-missing-uri.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-missing-uri.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-missing-uri.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-unknown.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-unknown.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-unknown.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-unknown.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-unknown.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-unknown.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-resources-read-unknown.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-resources-read-unknown.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-missing-name.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-missing-name.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-missing-name.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-missing-name.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-missing-name.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-missing-name.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-missing-name.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-missing-name.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-call-unknown-tool.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-tools-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-tools-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-unknown-method.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-unknown-method.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-unknown-method.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-unknown-method.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-unknown-method.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-unknown-method.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/EMPTY-unknown-method.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/EMPTY-unknown-method.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-initialize.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-initialize.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-initialize.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-initialize.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-initialize.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-initialize.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-initialize.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-initialize.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-get.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-get.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-get.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-get.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-get.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-get.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-get.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-get.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-prompts-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-prompts-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-read.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-read.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-read.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-read.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-read.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-read.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-resources-read.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-resources-read.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-string-id.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-string-id.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-string-id.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-string-id.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-string-id.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-string-id.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-string-id.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-string-id.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-arguments-not-map.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-arguments-not-map.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-arguments-not-map.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-arguments-not-map.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-arguments-not-map.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-arguments-not-map.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-arguments-not-map.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-arguments-not-map.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-error-flag.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-error-flag.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-error-flag.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-error-flag.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-error-flag.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-error-flag.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-error-flag.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-error-flag.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-mixed-content.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-mixed-content.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-mixed-content.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-mixed-content.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-mixed-content.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-mixed-content.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-mixed-content.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-mixed-content.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-no-arguments.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-no-arguments.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-no-arguments.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-no-arguments.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-no-arguments.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-no-arguments.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-no-arguments.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-no-arguments.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-params-not-map.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-params-not-map.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-params-not-map.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-params-not-map.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-params-not-map.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-params-not-map.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-params-not-map.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-params-not-map.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-text.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-text.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-text.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-text.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-text.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-text.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-call-text.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-call-text.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-list.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-list.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-list.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-list.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-list.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-list.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/FULL-tools-list.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/FULL-tools-list.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-first.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-first.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-first.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-first.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-first.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-first.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-first.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-first.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-second.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-second.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-second.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-second.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-second.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-second.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/PAGED-tools-list-second.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/PAGED-tools-list-second.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-mcp-exception.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-mcp-exception.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-mcp-exception.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-mcp-exception.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-mcp-exception.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-mcp-exception.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-mcp-exception.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-mcp-exception.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-no-message.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-no-message.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-no-message.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-no-message.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-no-message.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-no-message.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-no-message.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-no-message.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-runtime-exception.request.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-runtime-exception.request.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-runtime-exception.request.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-runtime-exception.request.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-runtime-exception.response.json b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-runtime-exception.response.json
similarity index 100%
rename from juneau-rest/juneau-rest-server-mcp/src/test/resources/characterization/THROW-runtime-exception.response.json
rename to juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/resources/characterization/THROW-runtime-exception.response.json
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/Mcp.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/Mcp.java
deleted file mode 100644
index 9c388cd109..0000000000
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/Mcp.java
+++ /dev/null
@@ -1,59 +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;
-
-import org.apache.juneau.bean.jsonrpc.*;
-import org.apache.juneau.bean.mcp.v20250618.*;
-import org.apache.juneau.commons.inject.*;
-
-/**
- * Static façade over {@link McpDispatcher} for embedders that want a single-line dispatch call.
- *
- * <p>
- * Use this from inside a custom Juneau REST resource that wants to expose an MCP endpoint without
- * subclassing {@link McpRestServlet}:
- *
- * <pre>
- * @Rest(path="/api")
- * public class MyResource extends BasicRestServlet {
- * @RestPost(path="/mcp")
- * public JsonRpcResponse mcp(JsonRpcRequest req, RestRequest rr) {
- * return Mcp.handle(req, getMcpConfig(), rr.getContext().getBeanStore());
- * }
- *
- * private McpServerConfig getMcpConfig() { ... }
- * }
- * </pre>
- */
-public final class Mcp {
-
- private static final McpDispatcher DISPATCHER = new McpDispatcher();
-
- private Mcp() {}
-
- /**
- * Dispatches a JSON-RPC request through the shared {@link McpDispatcher} instance.
- *
- * @param req JSON-RPC request envelope. May be {@code null} (treated as an invalid request).
- * @param config Server config. Never {@code null}.
- * @param ctx Per-request bean store. Never {@code null}.
- * @return The response, or {@code null} for notification requests.
- */
- public static JsonRpcResponse handle(JsonRpcRequest req, McpServerConfig config, BeanStore ctx) {
- return DISPATCHER.dispatch(req, config, ctx);
- }
-}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpDispatcher.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpDispatcher.java
deleted file mode 100644
index 20c1a0c363..0000000000
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpDispatcher.java
+++ /dev/null
@@ -1,271 +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;
-
-import static org.apache.juneau.commons.utils.AssertionUtils.*;
-import static org.apache.juneau.commons.utils.Shorts.*;
-import static org.apache.juneau.commons.utils.StringUtils.*;
-
-import java.util.*;
-
-import org.apache.juneau.bean.jsonrpc.*;
-import org.apache.juneau.bean.mcp.v20250618.*;
-import org.apache.juneau.commons.inject.*;
-import org.apache.juneau.marshall.collections.*;
-
-/**
- * Transport-agnostic JSON-RPC dispatcher for the MCP wire protocol.
- *
- * <p>
- * The dispatcher contains <em>zero</em> HTTP plumbing; it accepts a parsed {@link JsonRpcRequest},
- * routes by {@code method}, and returns a {@link JsonRpcResponse}. {@link McpRestServlet} is a thin
- * adapter that wraps this dispatcher in a Juneau REST {@code @RestPost} method, but embedders are
- * free to call {@link #dispatch(JsonRpcRequest, McpServerConfig, BeanStore)} directly from
- * tests or from a non-Juneau servlet.
- *
- * <p>
- * Notification requests (those with a {@code null} {@link JsonRpcRequest#getId() id}) return
- * {@code null}; HTTP adapters should map this to {@code 204 No Content}.
- */
-public class McpDispatcher {
-
- /** JSON-RPC error code: parse error. */
- public static final int CODE_PARSE_ERROR = -32700;
-
- /** JSON-RPC error code: invalid request. */
- public static final int CODE_INVALID_REQUEST = -32600;
-
- /** JSON-RPC error code: method not found. */
- public static final int CODE_METHOD_NOT_FOUND = -32601;
-
- /** JSON-RPC error code: invalid params. */
- public static final int CODE_INVALID_PARAMS = -32602;
-
- /** JSON-RPC error code: internal error. */
- public static final int CODE_INTERNAL_ERROR = -32603;
-
- /**
- * Default server name used when {@link McpServerConfig#getServerInfo()} is {@code null}.
- */
- public static final String DEFAULT_SERVER_NAME = "juneau-rest-server-mcp";
-
- /**
- * Dispatch the supplied JSON-RPC request against the supplied configuration.
- *
- * @param req JSON-RPC request envelope. Never {@code null}.
- * @param config Server config (handler registry, capabilities, cursor strategy). Never {@code null}.
- * @param ctx Per-request bean store (passed through to handlers). Never {@code null}.
- * @return The response, or {@code null} for notification requests.
- */
- public JsonRpcResponse dispatch(JsonRpcRequest req, McpServerConfig config, BeanStore ctx) {
- assertArgNotNull("config", config);
- assertArgNotNull("ctx", ctx);
- if (req == null)
- return errorResponse(null, CODE_INVALID_REQUEST, "Request envelope is null", null);
-
- var id = req.getId();
- var method = req.getMethod();
-
- if (isEmpty(method))
- return notification(id) ? null : errorResponse(id, CODE_INVALID_REQUEST, "Missing method", null);
-
- try {
- var result = invoke(method, req.getParams(), config, ctx);
- return notification(id) ? null : ok(id, result);
- } catch (McpException e) {
- return notification(id) ? null : new JsonRpcResponse()
- .setJsonrpc(McpProtocol.JSON_RPC_2_0)
- .setId(id)
- .setError(e.toJsonRpcError());
- } catch (Exception e) {
- if (notification(id))
- return null;
- var message = e.getMessage() == null ? cns(e) : e.getMessage();
- return errorResponse(id, CODE_INTERNAL_ERROR, message, JsonMap.of("type", cn(e)));
- }
- }
-
- private static boolean notification(Object id) {
- return id == null;
- }
-
- private Object invoke(String method, Object params, McpServerConfig config, BeanStore ctx) {
- switch (method) {
- case McpMethods.INITIALIZE:
- return initialize(config);
- case McpMethods.PING:
- return new JsonMap();
- case McpMethods.TOOLS_LIST:
- return listTools(config, params, ctx);
- case McpMethods.TOOLS_CALL:
- return callTool(config, params, ctx);
- case McpMethods.PROMPTS_LIST:
- return listPrompts(config, params, ctx);
- case McpMethods.PROMPTS_GET:
- return getPrompt(config, params, ctx);
- case McpMethods.RESOURCES_LIST:
- return listResources(config, params, ctx);
- case McpMethods.RESOURCES_READ:
- return readResource(config, params, ctx);
- default:
- throw new McpException(CODE_METHOD_NOT_FOUND, "Method not found: " + method);
- }
- }
-
- // -------------------------------------------------------------------------------------------
- // initialize / capabilities
- // -------------------------------------------------------------------------------------------
-
- private static InitializeResult initialize(McpServerConfig config) {
- var caps = config.getCapabilities();
- if (caps == null) {
- caps = new ServerCapabilities();
- if (! config.getTools().isEmpty())
- caps.setTools(new ToolCapability());
- if (! config.getPrompts().isEmpty())
- caps.setPrompts(new PromptCapability());
- if (! config.getResources().isEmpty())
- caps.setResources(new ResourceCapability());
- }
- var info = config.getServerInfo();
- if (info == null)
- info = new Implementation().setName(DEFAULT_SERVER_NAME).setVersion("unknown");
- return new InitializeResult()
- .setProtocolVersion(config.getProtocolVersion())
- .setCapabilities(caps)
- .setServerInfo(info)
- .setInstructions(config.getInstructions());
- }
-
- // -------------------------------------------------------------------------------------------
- // tools
- // -------------------------------------------------------------------------------------------
-
- private static ListToolsResult listTools(McpServerConfig config, Object params, BeanStore ctx) {
- var descriptors = config.getTools().stream().map(McpToolHandler::descriptor).toList();
- var page = config.getCursor().page(descriptors, cursorOf(params), ctx);
- return new ListToolsResult().setTools(page.items()).setNextCursor(page.nextCursor());
- }
-
- private static CallToolResult callTool(McpServerConfig config, Object params, BeanStore ctx) {
- var p = asMap(params);
- var name = strParam(p, "name");
- if (name == null)
- throw new McpException(CODE_INVALID_PARAMS, "Missing tool name");
- var handler = config.getTools().stream()
- .filter(h -> name.equals(h.descriptor().getName()))
- .findFirst()
- .orElseThrow(() -> new McpException(CODE_METHOD_NOT_FOUND, "Tool not found: " + name));
- var args = mapParam(p, "arguments");
- return handler.call(args, ctx);
- }
-
- // -------------------------------------------------------------------------------------------
- // prompts
- // -------------------------------------------------------------------------------------------
-
- private static ListPromptsResult listPrompts(McpServerConfig config, Object params, BeanStore ctx) {
- var descriptors = config.getPrompts().stream().map(McpPromptHandler::descriptor).toList();
- var page = config.getCursor().page(descriptors, cursorOf(params), ctx);
- return new ListPromptsResult().setPrompts(page.items()).setNextCursor(page.nextCursor());
- }
-
- private static GetPromptResult getPrompt(McpServerConfig config, Object params, BeanStore ctx) {
- var p = asMap(params);
- var name = strParam(p, "name");
- if (name == null)
- throw new McpException(CODE_INVALID_PARAMS, "Missing prompt name");
- var handler = config.getPrompts().stream()
- .filter(h -> name.equals(h.descriptor().getName()))
- .findFirst()
- .orElseThrow(() -> new McpException(CODE_METHOD_NOT_FOUND, "Prompt not found: " + name));
- var args = mapParam(p, "arguments");
- return handler.get(args, ctx);
- }
-
- // -------------------------------------------------------------------------------------------
- // resources
- // -------------------------------------------------------------------------------------------
-
- private static ListResourcesResult listResources(McpServerConfig config, Object params, BeanStore ctx) {
- var descriptors = config.getResources().stream().map(McpResourceHandler::descriptor).toList();
- var page = config.getCursor().page(descriptors, cursorOf(params), ctx);
- return new ListResourcesResult().setResources(page.items()).setNextCursor(page.nextCursor());
- }
-
- private static ReadResourceResult readResource(McpServerConfig config, Object params, BeanStore ctx) {
- var p = asMap(params);
- var uri = strParam(p, "uri");
- if (uri == null)
- throw new McpException(CODE_INVALID_PARAMS, "Missing resource uri");
- var handler = config.getResources().stream()
- .filter(h -> uri.equals(h.descriptor().getUri()))
- .findFirst()
- .orElseThrow(() -> new McpException(CODE_METHOD_NOT_FOUND, "Resource not found: " + uri));
- return handler.read(uri, ctx);
- }
-
- // -------------------------------------------------------------------------------------------
- // helpers
- // -------------------------------------------------------------------------------------------
-
- private static String cursorOf(Object params) {
- return strParam(asMap(params), "cursor");
- }
-
- @SuppressWarnings({
- "unchecked" // Cast is safe: type parameter verified by MCP protocol contract.
- })
- private static Map<String,Object> asMap(Object params) {
- if (params == null)
- return Map.of();
- if (params instanceof Map)
- return (Map<String,Object>) params;
- throw new McpException(CODE_INVALID_PARAMS, "Params must be an object");
- }
-
- private static String strParam(Map<String,Object> p, String key) {
- var v = p.get(key);
- return v == null ? null : v.toString();
- }
-
- @SuppressWarnings({
- "unchecked" // Cast is safe: type parameter verified by MCP protocol contract.
- })
- private static Map<String,Object> mapParam(Map<String,Object> p, String key) {
- var v = p.get(key);
- if (v == null)
- return Map.of();
- if (v instanceof Map)
- return (Map<String,Object>) v;
- throw new McpException(CODE_INVALID_PARAMS, "Param '" + key + "' must be an object");
- }
-
- private static JsonRpcResponse ok(Object id, Object result) {
- return new JsonRpcResponse()
- .setJsonrpc(McpProtocol.JSON_RPC_2_0)
- .setId(id)
- .setResult(result);
- }
-
- private static JsonRpcResponse errorResponse(Object id, int code, String message, Object data) {
- return new JsonRpcResponse()
- .setJsonrpc(McpProtocol.JSON_RPC_2_0)
- .setId(id)
- .setError(new JsonRpcError().setCode(code).setMessage(message).setData(data));
- }
-}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpoint.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpoint.java
index 2302bf5fd3..1df1bd99a6 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpoint.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpEndpoint.java
@@ -17,7 +17,6 @@
package org.apache.juneau.rest.server.mcp;
import org.apache.juneau.bean.jsonrpc.*;
-import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.commons.inject.*;
import org.apache.juneau.http.Content;
import org.apache.juneau.rest.server.*;
@@ -26,14 +25,14 @@ import org.apache.juneau.rest.server.*;
* Mixin interface that exposes an MCP JSON-RPC endpoint at {@code POST /mcp} on any Juneau REST resource.
*
* <p>
- * Implementing classes provide their {@link McpServerConfig} by implementing {@link #getMcpConfig()}; the
- * default {@link #handleMcpRequest(JsonRpcRequest, RestRequest)} method dispatches incoming requests through
- * {@link Mcp#handle(JsonRpcRequest, McpServerConfig, BeanStore)}.
+ * Implementing classes supply their {@link McpServerConfig} via {@link #getMcpConfig()} and their
+ * protocol revision via {@link #revision()}. In practice a consumer implements a revision-specific
+ * sub-interface (for example {@code McpEndpoint20250618}), which supplies {@link #revision()} for them.
*
* <h5 class='section'>Example:</h5>
* <pre>
* @Rest(path="/api")
- * public class MyResource extends BasicRestServlet implements McpEndpoint {
+ * public class MyResource extends BasicRestServlet implements McpEndpoint20250618 {
* @Override
* public McpServerConfig getMcpConfig() {
* return new McpServerConfig().addTool(new MyEchoTool());
@@ -50,12 +49,19 @@ public interface McpEndpoint {
*/
McpServerConfig getMcpConfig();
+ /**
+ * The MCP protocol revision this endpoint speaks.
+ *
+ * @return The bound revision. Never {@code null}.
+ */
+ McpRevision revision();
+
/**
* Default MCP JSON-RPC endpoint handler.
*
* <p>
- * Implementations may override this method to customize routing (path / annotations) but must still
- * call {@link Mcp#handle(JsonRpcRequest, McpServerConfig, BeanStore)} to dispatch.
+ * Implementations may override this method to customize routing (path / annotations) but must
+ * still dispatch through {@link #revision()}.
*
* @param req JSON-RPC request envelope.
* @param restReq The current REST request.
@@ -68,6 +74,7 @@ public interface McpEndpoint {
default JsonRpcResponse handleMcpRequest(@Content JsonRpcRequest req, RestRequest restReq) {
var bs = new BasicBeanStore(restReq.getContext().getBeanStore())
.addBean(RestRequest.class, restReq);
- return Mcp.handle(req, getMcpConfig(), bs);
+ var exchange = new McpExchange(req, n -> restReq.getHeaderParam(n).asString().orElse(null));
+ return revision().dispatch(exchange, getMcpConfig(), bs);
}
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpPromptHandler.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpPromptHandler.java
index 9b91d6a049..e8612f8d8a 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpPromptHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpPromptHandler.java
@@ -35,8 +35,9 @@ public interface McpPromptHandler {
* Returns the static descriptor for this prompt.
*
* <p>
- * The {@link Prompt#getName() name} value is used by {@link McpDispatcher} to route incoming
- * {@code prompts/get} requests, so each handler in an {@link McpServerConfig} must use a unique name.
+ * The {@link Prompt#getName() name} value is used by the bound {@link McpRevision} to route
+ * incoming {@code prompts/get} requests, so each handler in an {@link McpServerConfig} must use a
+ * unique name.
*
* @return The prompt descriptor. Never {@code null}.
*/
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceHandler.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceHandler.java
index 08eed9aeaa..db02fe3e77 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpResourceHandler.java
@@ -33,8 +33,9 @@ public interface McpResourceHandler {
* Returns the static descriptor for this resource.
*
* <p>
- * The {@link Resource#getUri() uri} value is used by {@link McpDispatcher} to route incoming
- * {@code resources/read} requests, so each handler in an {@link McpServerConfig} must use a unique URI.
+ * The {@link Resource#getUri() uri} value is used by the bound {@link McpRevision} to route
+ * incoming {@code resources/read} requests, so each handler in an {@link McpServerConfig} must use
+ * a unique URI.
*
* @return The resource descriptor. Never {@code null}.
*/
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRestServlet.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRestServlet.java
index 8414192cd4..3475544b92 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRestServlet.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpRestServlet.java
@@ -37,7 +37,7 @@ import org.apache.juneau.rest.server.servlet.*;
* <h5 class='section'>Example:</h5>
* <pre>
* @Rest(path="/mcp")
- * public class MyMcpServlet extends McpRestServlet {
+ * public class MyMcpServlet extends McpRestServlet20250618 {
* @Override
* protected McpServerConfig createMcpConfig() {
* return new McpServerConfig()
@@ -48,8 +48,9 @@ import org.apache.juneau.rest.server.servlet.*;
* </pre>
*
* <p>
- * The servlet enables {@code addBeanTypes} on its serializer so {@link Content} and {@link ResourceContents}
- * polymorphic types are tagged with their {@code type} discriminator on the wire.
+ * The servlet enables {@code addBeanTypes} on its serializer so that polymorphic wire types a
+ * revision emits (content blocks, resource payloads) are tagged with their {@code type}
+ * discriminator.
*
* @serial exclude
*/
@@ -98,6 +99,17 @@ public abstract class McpRestServlet extends BasicRestServlet {
*/
protected abstract McpServerConfig createMcpConfig();
+ /**
+ * The MCP protocol revision this servlet speaks.
+ *
+ * <p>
+ * Supplied by the revision-specific subclass a consumer extends (for example
+ * {@code McpRestServlet20250618}); consumers do not normally implement this themselves.
+ *
+ * @return The bound revision. Never {@code null}.
+ */
+ protected abstract McpRevision revision();
+
/**
* MCP JSON-RPC endpoint.
*
@@ -113,6 +125,7 @@ public abstract class McpRestServlet extends BasicRestServlet {
public JsonRpcResponse handleMcp(@Content JsonRpcRequest req, RestRequest restReq) {
var bs = new BasicBeanStore(restReq.getContext().getBeanStore())
.addBean(RestRequest.class, restReq);
- return Mcp.handle(req, getMcpConfig(), bs);
+ var exchange = new McpExchange(req, n -> restReq.getHeaderParam(n).asString().orElse(null));
+ return revision().dispatch(exchange, getMcpConfig(), bs);
}
}
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
index 8ddae0d0ad..da8fd2ac99 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpServerConfig.java
@@ -23,7 +23,7 @@ import java.util.*;
import org.apache.juneau.bean.mcp.v20250618.*;
/**
- * Aggregate configuration consumed by {@link McpDispatcher}.
+ * Aggregate configuration consumed by the bound {@link McpRevision}.
*
* <p>
* Applications register a single {@link McpServerConfig} (typically as a bean in their {@code RestContext}
@@ -45,7 +45,7 @@ public class McpServerConfig {
* Server identity reported in {@code initialize}.
*
* <p>
- * If {@code null}, {@link McpDispatcher} fills in {@code "juneau-rest-server-mcp" / "unknown"}.
+ * If {@code null}, the bound {@link McpRevision} fills in a default identity.
*
* @return The server info, or {@code null} if not set.
*/
@@ -201,7 +201,7 @@ public class McpServerConfig {
* Optional explicit capabilities advertisement.
*
* <p>
- * When {@code null}, {@link McpDispatcher} synthesizes one from the registered handler lists.
+ * When {@code null}, the bound {@link McpRevision} synthesizes one from the registered handler lists.
*
* @return The override, or {@code null} if auto-derived.
*/
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpToolHandler.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpToolHandler.java
index 3d547976ff..6d0169d020 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpToolHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpToolHandler.java
@@ -39,8 +39,9 @@ public interface McpToolHandler {
* Returns the static descriptor for this tool.
*
* <p>
- * The {@link Tool#getName() name} value is used by {@link McpDispatcher} to route incoming
- * {@code tools/call} requests, so each handler in an {@link McpServerConfig} must use a unique name.
+ * The {@link Tool#getName() name} value is used by the bound {@link McpRevision} to route
+ * incoming {@code tools/call} requests, so each handler in an {@link McpServerConfig} must use a
+ * unique name.
*
* @return The tool descriptor. Never {@code null}.
*/
diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
similarity index 53%
rename from juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
rename to juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
index 524737d38a..8df4f8ca2d 100644
--- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/McpTypedPromptHandler.java
+++ b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpCursorCtx_Test.java
@@ -16,36 +16,29 @@
*/
package org.apache.juneau.rest.server.mcp;
-import org.apache.juneau.bean.mcp.v20250618.*;
+import static org.junit.jupiter.api.Assertions.*;
+
import org.apache.juneau.commons.inject.*;
+import org.junit.jupiter.api.*;
/**
- * Typed variant of {@link McpPromptHandler} where MCP {@code prompts/get} arguments bind into a Juneau bean.
- *
- * @param <A> Argument bean type.
+ * Coverage for {@link McpCursor} bean-store passthrough.
*/
-public interface McpTypedPromptHandler<A> {
-
- /**
- * Returns the static descriptor for this prompt.
- *
- * @return The prompt descriptor. Never {@code null}.
- */
- Prompt descriptor();
-
- /**
- * Returns the runtime argument class for binding.
- *
- * @return The argument class. Never {@code null}.
- */
- Class<A> argumentType();
+class McpCursorCtx_Test {
- /**
- * Renders the prompt.
- *
- * @param arguments Bound argument bean (may be {@code null} when no arguments are supplied).
- * @param ctx Per-request bean store.
- * @return The rendered prompt.
- */
- GetPromptResult get(A arguments, BeanStore ctx);
+ @Test
+ void d01_cursor_passes_ctx_to_strategy() {
+ // Verify ctx parameter reaches the cursor (covers the BeanStore parameter passthrough).
+ var bs = new BasicBeanStore();
+ var got = new Object[1];
+ McpCursor c = new McpCursor() {
+ @Override
+ public <T> McpPage<T> page(java.util.List<T> all, String cursor, BeanStore ctx) {
+ got[0] = ctx;
+ return new McpPage<>(all, null);
+ }
+ };
+ c.page(java.util.List.of(), null, bs);
+ assertSame(bs, got[0]);
+ }
}