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 <[email protected]> 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 <[email protected]> --- .../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]); + } }
