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 03153783dd76b92149cbd671eb7a105b17d6265b Author: James Bognar <[email protected]> AuthorDate: Tue Jul 28 18:08:37 2026 -0400 refactor: make juneau-rest-server-mcp revision-neutral Retypes the handler interfaces and McpServerConfig onto the neutral model, adds the bidirectional neutral/wire boundary in the 2025-06-18 adapter, moves the explicit-capabilities override from a core setter to a typed adapter hook, and drops core's dependency on the revision bean module. Characterization fixtures replay byte-identical, including the explicit-capabilities initialize response. Note: McpServerConfig.setProtocolVersion had no equivalent hook and is removed outright, so a v1 endpoint can no longer report a non-2025-06-18 version. Co-authored-by: Cursor <[email protected]> --- .../juneau/rest/server/mcp/McpConfigCore_Test.java | 126 +++++++++++ .../mcp/v20250618/Mcp20250618Revision_Test.java | 65 +++--- .../mcp/v20250618/McpHandlerDefaults_Test.java | 7 +- .../mcp/v20250618/McpRestServlet20250618_Test.java | 18 +- .../server/mcp/v20250618/McpServerConfig_Test.java | 137 ------------ .../mcp/v20250618/McpTypedHandlers_Test.java | 6 +- .../server/mcp/v20250618/Mcp20250618Revision.java | 27 ++- .../rest/server/mcp/v20250618/Mcp20250618Wire.java | 244 +++++++++++++++++++++ .../server/mcp/v20250618/McpTypedHandlers.java | 16 +- .../mcp/v20250618/Characterization_Test.java | 61 +++--- .../mcp/v20250618/Mcp20250618Revision_Test.java | 65 +++--- .../mcp/v20250618/McpHandlerDefaults_Test.java | 7 +- .../mcp/v20250618/McpRestServlet20250618_Test.java | 18 +- .../server/mcp/v20250618/McpServerConfig_Test.java | 137 ------------ .../mcp/v20250618/McpTypedHandlers_Test.java | 6 +- juneau-rest/juneau-rest-server-mcp/pom.xml | 5 - .../juneau/rest/server/mcp/McpPromptHandler.java | 12 +- .../juneau/rest/server/mcp/McpResourceHandler.java | 12 +- .../juneau/rest/server/mcp/McpRestServlet.java | 1 - .../juneau/rest/server/mcp/McpServerConfig.java | 75 +++---- .../juneau/rest/server/mcp/McpToolHandler.java | 17 +- .../juneau/rest/server/mcp/package-info.java | 15 +- .../rest/server/mcp/FakeMcpRevision_Test.java | 17 +- .../juneau/rest/server/mcp/McpConfigCore_Test.java | 126 +++++++++++ 24 files changed, 720 insertions(+), 500 deletions(-) diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java new file mode 100644 index 0000000000..3452f72a78 --- /dev/null +++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java @@ -0,0 +1,126 @@ +/* + * 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.test.bct.BctAssertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.*; + +import org.apache.juneau.commons.inject.*; +import org.junit.jupiter.api.*; + +/** + * Coverage for the revision-neutral {@link McpServerConfig}. + */ +class McpConfigCore_Test { + + private static McpToolHandler dummyTool(String name) { + return new McpToolHandler() { + @Override public McpToolSpec descriptor() { return new McpToolSpec().setName(name); } + @Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { return new McpToolOutcome(); } + }; + } + + private static McpPromptHandler dummyPrompt(String name) { + return new McpPromptHandler() { + @Override public McpPromptSpec descriptor() { return new McpPromptSpec().setName(name); } + @Override public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { return new McpPromptOutcome(); } + }; + } + + private static McpResourceHandler dummyResource(String uri) { + return new McpResourceHandler() { + @Override public McpResourceSpec descriptor() { return new McpResourceSpec().setUri(uri); } + @Override public McpResourceOutcome read(String u, BeanStore ctx) { return new McpResourceOutcome(); } + }; + } + + @Test + void defaults() { + var a = new McpServerConfig(); + assertNull(a.getName()); + assertNull(a.getVersion()); + assertNull(a.getInstructions()); + assertNotNull(a.getTools()); + assertNotNull(a.getPrompts()); + assertNotNull(a.getResources()); + assertSame(McpCursor.SINGLE_PAGE, a.getCursor()); + } + + @Test + void setters_and_addCalls() { + var a = new McpServerConfig() + .setName("x") + .setVersion("1") + .setInstructions("hello") + .setCursor(McpCursor.fixedSize(5)) + .addTool(dummyTool("t1"), dummyTool("t2")) + .addPrompt(dummyPrompt("p1")) + .addResource(dummyResource("r://a")); + + assertString("x", a.getName()); + assertString("1", a.getVersion()); + assertString("hello", a.getInstructions()); + assertNotSame(McpCursor.SINGLE_PAGE, a.getCursor()); + assertSize(2, a.getTools()); + assertSize(1, a.getPrompts()); + assertSize(1, a.getResources()); + } + + @Test + void setLists_replacingAndNullClears() { + var a = new McpServerConfig().addTool(dummyTool("t")).addPrompt(dummyPrompt("p")).addResource(dummyResource("r")); + a.setTools(null); + a.setPrompts(null); + a.setResources(null); + assertEmpty(a.getTools()); + assertEmpty(a.getPrompts()); + assertEmpty(a.getResources()); + + a.setTools(List.of(dummyTool("a"))); + a.setPrompts(List.of(dummyPrompt("a"))); + a.setResources(List.of(dummyResource("a"))); + assertSize(1, a.getTools()); + assertSize(1, a.getPrompts()); + assertSize(1, a.getResources()); + } + + @Test + void setCursor_nullResets() { + var a = new McpServerConfig().setCursor(McpCursor.fixedSize(2)); + a.setCursor(null); + assertSame(McpCursor.SINGLE_PAGE, a.getCursor()); + } + + @Test + void noRevisionSpecificSurfaceRemains() { + var m = Arrays.stream(McpServerConfig.class.getMethods()).map(java.lang.reflect.Method::getName).toList(); + assertFalse(m.contains("getCapabilities"), "capabilities are revision-owned; core must hold none"); + assertFalse(m.contains("setCapabilities"), "capabilities are revision-owned; core must hold none"); + assertFalse(m.contains("getProtocolVersion"), "the protocol version is owned by McpRevision.protocolVersion()"); + assertFalse(m.contains("setProtocolVersion"), "the protocol version is owned by McpRevision.protocolVersion()"); + assertFalse(m.contains("getServerInfo"), "server identity is plain name/version on the neutral config"); + } + + @Test + void nullListElementsAreDistinctFromEmpty() { + assertNull(new McpToolOutcome().getContent()); + assertNull(new McpPromptOutcome().getMessages()); + assertNull(new McpResourceOutcome().getContents()); + } +} diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java index 5deec891fa..1db6ef9aa4 100644 --- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java +++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_Test.java @@ -35,43 +35,43 @@ class Mcp20250618Revision_Test { private final BeanStore ctx = new BasicBeanStore(); - private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,CallToolResult> fn) { + private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,McpToolOutcome> fn) { return new McpToolHandler() { @Override - public Tool descriptor() { - return new Tool().setName(name).setDescription("desc:" + name); + public McpToolSpec descriptor() { + return new McpToolSpec().setName(name).setDescription("desc:" + name); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpPromptHandler prompt(String name, java.util.function.Function<Map<String,Object>,GetPromptResult> fn) { + private static McpPromptHandler prompt(String name, java.util.function.Function<Map<String,Object>,McpPromptOutcome> fn) { return new McpPromptHandler() { @Override - public Prompt descriptor() { - return new Prompt().setName(name); + public McpPromptSpec descriptor() { + return new McpPromptSpec().setName(name); } @Override - public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { + public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpResourceHandler resource(String uri, java.util.function.Function<String,ReadResourceResult> fn) { + private static McpResourceHandler resource(String uri, java.util.function.Function<String,McpResourceOutcome> fn) { return new McpResourceHandler() { @Override - public Resource descriptor() { - return new Resource().setUri(uri).setName("res"); + public McpResourceSpec descriptor() { + return new McpResourceSpec().setUri(uri).setName("res"); } @Override - public ReadResourceResult read(String u, BeanStore ctx) { + public McpResourceOutcome read(String u, BeanStore ctx) { return fn.apply(u); } }; @@ -88,9 +88,9 @@ class Mcp20250618Revision_Test { @Test void initialize_default_capabilities() { var config = new McpServerConfig() - .addTool(tool("a", a -> new CallToolResult())) - .addPrompt(prompt("p", a -> new GetPromptResult())) - .addResource(resource("r://x", u -> new ReadResourceResult())); + .addTool(tool("a", a -> new McpToolOutcome())) + .addPrompt(prompt("p", a -> new McpPromptOutcome())) + .addResource(resource("r://x", u -> new McpResourceOutcome())); var resp = send(config, req(1, McpMethods.INITIALIZE, null)); assertNotNull(resp); @@ -103,14 +103,11 @@ class Mcp20250618Revision_Test { } @Test - void initialize_explicit_capabilitiesAndServerInfo() { - var caps = new ServerCapabilities().setLogging(new LoggingCapability()); - var info = new Implementation().setName("custom").setVersion("9.9"); - var config = new McpServerConfig().setCapabilities(caps).setServerInfo(info).setInstructions("hi"); + void initialize_explicitServerInfoAndInstructions() { + var config = new McpServerConfig().setName("custom").setVersion("9.9").setInstructions("hi"); var resp = send(config, req(1, McpMethods.INITIALIZE, null)); var result = (InitializeResult) resp.getResult(); - assertSame(caps, result.getCapabilities()); - assertSame(info, result.getServerInfo()); + assertBean(result.getServerInfo(), "name,version", "custom,9.9"); assertString("hi", result.getInstructions()); } @@ -166,7 +163,7 @@ class Mcp20250618Revision_Test { @Test void notification_returnsNullResponse() { - var config = new McpServerConfig().addTool(tool("a", args -> new CallToolResult())); + var config = new McpServerConfig().addTool(tool("a", args -> new McpToolOutcome())); var notif = req(null, McpMethods.TOOLS_CALL, JsonMap.of("name", "a")); assertNull(send(config, notif)); } @@ -192,8 +189,8 @@ class Mcp20250618Revision_Test { @Test void tools_list_singlePage() { var config = new McpServerConfig() - .addTool(tool("a", args -> new CallToolResult())) - .addTool(tool("b", args -> new CallToolResult())); + .addTool(tool("a", args -> new McpToolOutcome())) + .addTool(tool("b", args -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_LIST, null)); var result = (ListToolsResult) resp.getResult(); assertSize(2, result.getTools()); @@ -203,8 +200,8 @@ class Mcp20250618Revision_Test { @Test void tools_list_paged() { var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)) - .addTool(tool("a", args -> new CallToolResult())) - .addTool(tool("b", args -> new CallToolResult())); + .addTool(tool("a", args -> new McpToolOutcome())) + .addTool(tool("b", args -> new McpToolOutcome())); var first = (ListToolsResult) send(config, req(1, McpMethods.TOOLS_LIST, null)).getResult(); assertSize(1, first.getTools()); assertString("1", first.getNextCursor()); @@ -218,9 +215,9 @@ class Mcp20250618Revision_Test { @Test void tools_call_routes_byName() { var config = new McpServerConfig().addTool(tool("echo", args -> { - var ctr = new CallToolResult(); - ctr.setContent(List.of(new TextContent().setText(String.valueOf(args.get("text"))))); - return ctr; + var outcome = new McpToolOutcome(); + outcome.setContent(List.of(McpContentBlock.text(String.valueOf(args.get("text"))))); + return outcome; })); var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "echo", "arguments", JsonMap.of("text", "hi")))); var ctr = (CallToolResult) resp.getResult(); @@ -241,14 +238,14 @@ class Mcp20250618Revision_Test { @Test void tools_call_argumentsNotObject_throwsInvalidParams() { - var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult())); + var config = new McpServerConfig().addTool(tool("e", a -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e", "arguments", "string-not-map"))); 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 config = new McpServerConfig().addTool(tool("e", a -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_CALL, "not-a-map")); assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode()); } @@ -288,7 +285,7 @@ class Mcp20250618Revision_Test { @Test void prompts_list_and_get() { - var config = new McpServerConfig().addPrompt(prompt("p", args -> new GetPromptResult().setDescription("ok"))); + var config = new McpServerConfig().addPrompt(prompt("p", args -> new McpPromptOutcome().setDescription("ok"))); var list = (ListPromptsResult) send(config, req(1, McpMethods.PROMPTS_LIST, null)).getResult(); assertSize(1, list.getPrompts()); var get = (GetPromptResult) send(config, req(1, McpMethods.PROMPTS_GET, JsonMap.of("name", "p"))).getResult(); @@ -311,7 +308,7 @@ class Mcp20250618Revision_Test { @Test void resources_list_and_read() { - var config = new McpServerConfig().addResource(resource("file://a", uri -> new ReadResourceResult().setContents(List.of(new TextResourceContents().setUri(uri).setText("ok"))))); + var config = new McpServerConfig().addResource(resource("file://a", uri -> new McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri, null, "ok"))))); var list = (ListResourcesResult) send(config, req(1, McpMethods.RESOURCES_LIST, null)).getResult(); assertSize(1, list.getResources()); var read = (ReadResourceResult) send(config, req(1, McpMethods.RESOURCES_READ, JsonMap.of("uri", "file://a"))).getResult(); @@ -334,7 +331,7 @@ class Mcp20250618Revision_Test { @Test void cursor_paramsAreOptional() { - var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)).addPrompt(prompt("a", args -> new GetPromptResult())).addPrompt(prompt("b", args -> new GetPromptResult())); + var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)).addPrompt(prompt("a", args -> new McpPromptOutcome())).addPrompt(prompt("b", args -> new McpPromptOutcome())); // params null var resp = (ListPromptsResult) send(config, req(1, McpMethods.PROMPTS_LIST, null)).getResult(); assertSize(1, resp.getPrompts()); diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java index 55b78d910e..d7400d8d86 100644 --- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java +++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpHandlerDefaults_Test.java @@ -19,7 +19,6 @@ 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.*; @@ -32,19 +31,19 @@ class McpHandlerDefaults_Test { @Test void toolHandler_defaultDescriptor_throws() { - McpToolHandler h = (args, ctx) -> new CallToolResult(); + McpToolHandler h = (args, ctx) -> new McpToolOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } @Test void promptHandler_defaultDescriptor_throws() { - McpPromptHandler h = (args, ctx) -> new GetPromptResult(); + McpPromptHandler h = (args, ctx) -> new McpPromptOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } @Test void resourceHandler_defaultDescriptor_throws() { - McpResourceHandler h = (uri, ctx) -> new ReadResourceResult(); + McpResourceHandler h = (uri, ctx) -> new McpResourceOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java index ca289b1eaf..12c54c1b34 100644 --- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java +++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_Test.java @@ -51,18 +51,16 @@ class McpRestServlet20250618_Test extends TestBase { @Override protected McpServerConfig createMcpConfig() { return new McpServerConfig() - .setServerInfo(new Implementation().setName("test").setVersion("1.0.0")) + .setName("test").setVersion("1.0.0") .addTool(new McpToolHandler() { @Override - public Tool descriptor() { - return new Tool().setName("echo").setDescription("Echoes back"); + public McpToolSpec descriptor() { + return new McpToolSpec().setName("echo").setDescription("Echoes back"); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - var ctr = new CallToolResult(); - ctr.setContent(List.of(new TextContent().setText(String.valueOf(arguments.get("text"))))); - return ctr; + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { + return McpToolOutcome.text(String.valueOf(arguments.get("text"))); } }); } @@ -123,11 +121,11 @@ class McpRestServlet20250618_Test extends TestBase { public McpServerConfig getMcpConfig() { return new McpServerConfig().addTool(new McpToolHandler() { @Override - public Tool descriptor() { return new Tool().setName("ping"); } + public McpToolSpec descriptor() { return new McpToolSpec().setName("ping"); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - return new CallToolResult().setContent(List.of(new TextContent().setText("pong"))); + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { + return McpToolOutcome.text("pong"); } }); } diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java deleted file mode 100644 index 155c7798a7..0000000000 --- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java +++ /dev/null @@ -1,137 +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.v20250618; - -import static org.apache.juneau.test.bct.BctAssertions.*; -import static org.junit.jupiter.api.Assertions.*; - -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.*; - -/** - * Coverage for {@link McpServerConfig}. - */ -class McpServerConfig_Test { - - private static McpToolHandler dummyTool(String name) { - return new McpToolHandler() { - @Override - public Tool descriptor() { - return new Tool().setName(name); - } - - @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - return new CallToolResult(); - } - }; - } - - private static McpPromptHandler dummyPrompt(String name) { - return new McpPromptHandler() { - @Override - public Prompt descriptor() { - return new Prompt().setName(name); - } - - @Override - public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { - return new GetPromptResult(); - } - }; - } - - private static McpResourceHandler dummyResource(String uri) { - return new McpResourceHandler() { - @Override - public Resource descriptor() { - return new Resource().setUri(uri); - } - - @Override - public ReadResourceResult read(String u, BeanStore ctx) { - return new ReadResourceResult(); - } - }; - } - - @Test - void defaults() { - var c = new McpServerConfig(); - assertNull(c.getServerInfo()); - assertString(McpProtocol.VERSION_2025_06_18, c.getProtocolVersion()); - assertNull(c.getInstructions()); - assertNotNull(c.getTools()); - assertNotNull(c.getPrompts()); - assertNotNull(c.getResources()); - assertNull(c.getCapabilities()); - assertSame(McpCursor.SINGLE_PAGE, c.getCursor()); - } - - @Test - void setters_and_addCalls() { - var info = new Implementation().setName("x").setVersion("1"); - var caps = new ServerCapabilities().setLogging(new LoggingCapability()); - var c = new McpServerConfig() - .setServerInfo(info) - .setProtocolVersion("2024-11-05") - .setInstructions("hello") - .setCapabilities(caps) - .setCursor(McpCursor.fixedSize(5)) - .addTool(dummyTool("t1"), dummyTool("t2")) - .addPrompt(dummyPrompt("p1")) - .addResource(dummyResource("r://a")); - - assertSame(info, c.getServerInfo()); - assertString("2024-11-05", c.getProtocolVersion()); - assertString("hello", c.getInstructions()); - assertSame(caps, c.getCapabilities()); - assertNotSame(McpCursor.SINGLE_PAGE, c.getCursor()); - assertSize(2, c.getTools()); - assertSize(1, c.getPrompts()); - assertSize(1, c.getResources()); - } - - @Test - void setLists_replacingAndNullClears() { - var c = new McpServerConfig().addTool(dummyTool("t")).addPrompt(dummyPrompt("p")).addResource(dummyResource("r")); - c.setTools(null); - c.setPrompts(null); - c.setResources(null); - assertEmpty(c.getTools()); - assertEmpty(c.getPrompts()); - assertEmpty(c.getResources()); - - c.setTools(List.of(dummyTool("a"))); - c.setPrompts(List.of(dummyPrompt("a"))); - c.setResources(List.of(dummyResource("a"))); - assertSize(1, c.getTools()); - assertSize(1, c.getPrompts()); - assertSize(1, c.getResources()); - } - - @Test - void setCursor_nullResets() { - var c = new McpServerConfig().setCursor(McpCursor.fixedSize(2)); - c.setCursor(null); - assertSame(McpCursor.SINGLE_PAGE, c.getCursor()); - } -} diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java index 72fcf1da66..aec3ccc06d 100644 --- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java +++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers_Test.java @@ -115,7 +115,9 @@ class McpTypedHandlers_Test { .setMethod(McpMethods.TOOLS_CALL) .setParams(JsonMap.of("name", "d")); var resp = dispatch(req, config); - assertSame(ctr, resp.getResult()); + // The neutral/wire boundary always remaps the result, so only value equality survives here. + var result = (CallToolResult) resp.getResult(); + assertString("direct", ((TextContent) result.getContent().get(0)).getText()); } @Test @@ -234,7 +236,7 @@ class McpTypedHandlers_Test { }; var raw = McpTypedHandlers.adaptTool(typed); var ctr = raw.call(null, ctx); - assertString("null", ((TextContent) ctr.getContent().get(0)).getText()); + assertString("null", ctr.getContent().get(0).text()); } @Test 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 index 09da0a545c..128e7a0f6e 100644 --- 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 @@ -152,7 +152,7 @@ public final class Mcp20250618Revision implements McpRevision { } private InitializeResult initialize(McpServerConfig config) { - var caps = config.getCapabilities(); + var caps = this.capabilities; if (caps == null) { caps = new ServerCapabilities(); if (! config.getTools().isEmpty()) @@ -162,20 +162,19 @@ public final class Mcp20250618Revision implements McpRevision { 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()) + .setProtocolVersion(protocolVersion()) .setCapabilities(caps) - .setServerInfo(info) + .setServerInfo(Mcp20250618Wire.serverInfo(config)) .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()); + return new ListToolsResult() + .setTools(page.items().stream().map(Mcp20250618Wire::toWire).toList()) + .setNextCursor(page.nextCursor()); } private CallToolResult callTool(McpServerConfig config, Object params, BeanStore ctx) { @@ -188,13 +187,15 @@ public final class Mcp20250618Revision implements McpRevision { .findFirst() .orElseThrow(() -> new McpException(errorCode(McpErrorKind.TOOL_NOT_FOUND), "Tool not found: " + name)); var args = McpParamUtils.mapParam(p, "arguments"); - return handler.call(args, ctx); + return Mcp20250618Wire.toWire(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()); + return new ListPromptsResult() + .setPrompts(page.items().stream().map(Mcp20250618Wire::toWire).toList()) + .setNextCursor(page.nextCursor()); } private GetPromptResult getPrompt(McpServerConfig config, Object params, BeanStore ctx) { @@ -207,13 +208,15 @@ public final class Mcp20250618Revision implements McpRevision { .findFirst() .orElseThrow(() -> new McpException(errorCode(McpErrorKind.PROMPT_NOT_FOUND), "Prompt not found: " + name)); var args = McpParamUtils.mapParam(p, "arguments"); - return handler.get(args, ctx); + return Mcp20250618Wire.toWire(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()); + return new ListResourcesResult() + .setResources(page.items().stream().map(Mcp20250618Wire::toWire).toList()) + .setNextCursor(page.nextCursor()); } private ReadResourceResult readResource(McpServerConfig config, Object params, BeanStore ctx) { @@ -225,6 +228,6 @@ public final class Mcp20250618Revision implements McpRevision { .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); + return Mcp20250618Wire.toWire(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/Mcp20250618Wire.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Wire.java new file mode 100644 index 0000000000..9b7b4aac4e --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Wire.java @@ -0,0 +1,244 @@ +/* + * 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 java.util.*; + +import org.apache.juneau.bean.jsonrpc.*; +import org.apache.juneau.bean.mcp.v20250618.*; +import org.apache.juneau.marshall.collections.*; +import org.apache.juneau.marshall.marshaller.*; +import org.apache.juneau.rest.server.mcp.*; + +/** + * Mapping between the revision-neutral model and the {@code 2025-06-18} wire beans. + * + * <p> + * The mapping runs in both directions. Neutral-to-wire is the request path: what a handler returns + * becomes what a client receives. Wire-to-neutral is needed because {@link McpTypedHandlers} accepts + * user handlers written against this revision's wire beans and must adapt them to the neutral + * handler interfaces the core registry holds. + * + * <p> + * <b>{@code null} is preserved as {@code null} throughout.</b> Juneau omits null bean properties, so + * normalizing a null collection to an empty one would add a property to the wire output. An empty + * {@code CallToolResult} must keep serializing as <c>{}</c>. + */ +final class Mcp20250618Wire { + + private Mcp20250618Wire() {} + + // --- neutral -> wire ------------------------------------------------------------------- + + static Tool toWire(McpToolSpec x) { + if (x == null) + return null; + return new Tool() + .setName(x.getName()) + .setDescription(x.getDescription()) + .setInputSchema(toWire(x.getInputSchema())); + } + + static JsonSchema toWire(McpSchema x) { + return x == null ? null : Json.to(Json.of(x.toJsonMap()), JsonSchema.class); + } + + static CallToolResult toWire(McpToolOutcome x) { + if (x == null) + return null; + var r = new CallToolResult().setIsError(x.getError()); + if (x.getContent() != null) + r.setContent(x.getContent().stream().map(Mcp20250618Wire::toWire).toList()); + return r; + } + + static Content toWire(McpContentBlock x) { + if (x == null) + return null; + return switch (x.kind()) { + case TEXT -> new TextContent().setText(x.text()); + case IMAGE -> new ImageContent().setData(x.data()).setMimeType(x.mimeType()); + case RESOURCE -> new EmbeddedResourceContent().setResource(toWire(x.resource())); + }; + } + + static ResourceContents toWire(McpResourceContents x) { + if (x == null) + return null; + return switch (x.kind()) { + case TEXT -> new TextResourceContents().setUri(x.uri()).setMimeType(x.mimeType()).setText(x.text()); + case BLOB -> new BlobResourceContents().setUri(x.uri()).setMimeType(x.mimeType()).setBlob(x.blob()); + }; + } + + static Prompt toWire(McpPromptSpec x) { + if (x == null) + return null; + var r = new Prompt().setName(x.getName()).setDescription(x.getDescription()); + if (x.getArguments() != null) + r.setArguments(x.getArguments().stream().map(Mcp20250618Wire::toWire).toList()); + return r; + } + + static PromptArgument toWire(McpPromptArgument x) { + if (x == null) + return null; + return new PromptArgument().setName(x.getName()).setDescription(x.getDescription()).setRequired(x.getRequired()); + } + + static GetPromptResult toWire(McpPromptOutcome x) { + if (x == null) + return null; + var r = new GetPromptResult().setDescription(x.getDescription()); + if (x.getMessages() != null) + r.setMessages(x.getMessages().stream().map(Mcp20250618Wire::toWire).toList()); + return r; + } + + static PromptMessage toWire(McpPromptMessage x) { + if (x == null) + return null; + return new PromptMessage().setRole(toWire(x.getRole())).setContent(toWire(x.getContent())); + } + + static Role toWire(McpRole x) { + if (x == null) + return null; + return switch (x) { + case USER -> Role.USER; + case ASSISTANT -> Role.ASSISTANT; + case SYSTEM -> Role.SYSTEM; + case TOOL -> Role.TOOL; + }; + } + + static Resource toWire(McpResourceSpec x) { + if (x == null) + return null; + return new Resource() + .setUri(x.getUri()) + .setName(x.getName()) + .setTitle(x.getTitle()) + .setDescription(x.getDescription()) + .setMimeType(x.getMimeType()) + .setSize(x.getSize()); + } + + static ReadResourceResult toWire(McpResourceOutcome x) { + if (x == null) + return null; + var r = new ReadResourceResult(); + if (x.getContents() != null) + r.setContents(x.getContents().stream().map(Mcp20250618Wire::toWire).toList()); + return r; + } + + static Implementation serverInfo(McpServerConfig config) { + if (config.getName() == null && config.getVersion() == null) + return new Implementation() + .setName(Mcp20250618Revision.DEFAULT_SERVER_NAME) + .setVersion("unknown"); + return new Implementation().setName(config.getName()).setVersion(config.getVersion()); + } + + // --- wire -> neutral ------------------------------------------------------------------- + + static McpToolSpec toNeutral(Tool x) { + if (x == null) + return null; + return new McpToolSpec() + .setName(x.getName()) + .setDescription(x.getDescription()) + .setInputSchema(toNeutral(x.getInputSchema())); + } + + static McpSchema toNeutral(JsonSchema x) { + return x == null ? null : McpSchema.of(Json.to(Json.of(x), JsonMap.class)); + } + + static McpToolOutcome toNeutral(CallToolResult x) { + if (x == null) + return null; + var r = new McpToolOutcome().setError(x.getIsError()); + if (x.getContent() != null) + r.setContent(x.getContent().stream().map(Mcp20250618Wire::toNeutral).toList()); + return r; + } + + static McpContentBlock toNeutral(Content x) { + if (x == null) + return null; + if (x instanceof TextContent x2) + return McpContentBlock.text(x2.getText()); + if (x instanceof ImageContent x2) + return McpContentBlock.image(x2.getData(), x2.getMimeType()); + if (x instanceof EmbeddedResourceContent x2) + return McpContentBlock.resource(toNeutral(x2.getResource())); + throw new McpException(Mcp20250618Revision.CODE_INTERNAL_ERROR, "Unsupported content type: " + x.getClass().getName()); + } + + static McpResourceContents toNeutral(ResourceContents x) { + if (x == null) + return null; + if (x instanceof TextResourceContents x2) + return McpResourceContents.text(x2.getUri(), x2.getMimeType(), x2.getText()); + if (x instanceof BlobResourceContents x2) + return McpResourceContents.blob(x2.getUri(), x2.getMimeType(), x2.getBlob()); + throw new McpException(Mcp20250618Revision.CODE_INTERNAL_ERROR, "Unsupported resource contents type: " + x.getClass().getName()); + } + + static McpPromptSpec toNeutral(Prompt x) { + if (x == null) + return null; + var r = new McpPromptSpec().setName(x.getName()).setDescription(x.getDescription()); + if (x.getArguments() != null) + r.setArguments(x.getArguments().stream().map(Mcp20250618Wire::toNeutral).toList()); + return r; + } + + static McpPromptArgument toNeutral(PromptArgument x) { + if (x == null) + return null; + return new McpPromptArgument().setName(x.getName()).setDescription(x.getDescription()).setRequired(x.getRequired()); + } + + static McpPromptOutcome toNeutral(GetPromptResult x) { + if (x == null) + return null; + var r = new McpPromptOutcome().setDescription(x.getDescription()); + if (x.getMessages() != null) + r.setMessages(x.getMessages().stream().map(Mcp20250618Wire::toNeutral).toList()); + return r; + } + + static McpPromptMessage toNeutral(PromptMessage x) { + if (x == null) + return null; + return new McpPromptMessage().setRole(toNeutral(x.getRole())).setContent(toNeutral(x.getContent())); + } + + static McpRole toNeutral(Role x) { + if (x == null) + return null; + return switch (x) { + case USER -> McpRole.USER; + case ASSISTANT -> McpRole.ASSISTANT; + case SYSTEM -> McpRole.SYSTEM; + case TOOL -> McpRole.TOOL; + }; + } +} diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java index 1d24b73a35..e043dd5eff 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java +++ b/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/main/java/org/apache/juneau/rest/server/mcp/v20250618/McpTypedHandlers.java @@ -56,15 +56,15 @@ public final class McpTypedHandlers { assertArgNotNull("typed", typed); return new McpToolHandler() { @Override - public Tool descriptor() { - return typed.descriptor(); + public McpToolSpec descriptor() { + return Mcp20250618Wire.toNeutral(typed.descriptor()); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { A bound = bindArguments(arguments, typed.argumentType()); R result = typed.call(bound, ctx); - return wrapToolResult(result); + return Mcp20250618Wire.toNeutral(wrapToolResult(result)); } }; } @@ -84,14 +84,14 @@ public final class McpTypedHandlers { assertArgNotNull("typed", typed); return new McpPromptHandler() { @Override - public Prompt descriptor() { - return typed.descriptor(); + public McpPromptSpec descriptor() { + return Mcp20250618Wire.toNeutral(typed.descriptor()); } @Override - public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { + public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { A bound = bindArguments(arguments, typed.argumentType()); - return typed.get(bound, ctx); + return Mcp20250618Wire.toNeutral(typed.get(bound, ctx)); } }; } diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 index 207f4ed696..af924b5aa0 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 @@ -68,21 +68,18 @@ class Characterization_Test { private static final long serialVersionUID = 1L; @Override protected McpServerConfig createMcpConfig() { return new McpServerConfig() - .setServerInfo(new Implementation().setName("characterization").setVersion("1.0.0")) + .setName("characterization").setVersion("1.0.0") .setInstructions("Be concise.") - .addTool(tool("echo", new JsonSchema().setType("object").setRequired("text"), a -> new CallToolResult() - .setContent(List.of(new TextContent().setText(String.valueOf(a.get("text"))))))) - .addTool(tool("mixed", null, a -> new CallToolResult().setContent(List.of( - new TextContent().setText("t"), - new ImageContent().setData("AAA=").setMimeType("image/png"), - new EmbeddedResourceContent().setResource( - new TextResourceContents().setUri("file:///e").setMimeType("text/plain").setText("emb")))))) - .addTool(tool("failing", null, a -> new CallToolResult().setIsError(true) - .setContent(List.of(new TextContent().setText("nope"))))) - .addPrompt(prompt("greet", a -> new GetPromptResult().setDescription("d").setMessages(List.of( - new PromptMessage().setRole(Role.USER).setContent(new TextContent().setText("hi " + a.get("who"))))))) - .addResource(resource("file:///a", u -> new ReadResourceResult().setContents(List.of( - new TextResourceContents().setUri(u).setMimeType("text/plain").setText("body"))))); + .addTool(tool("echo", McpSchema.of(JsonMap.of("type", "object", "required", List.of("text"))), a -> McpToolOutcome.text(String.valueOf(a.get("text"))))) + .addTool(tool("mixed", null, a -> McpToolOutcome.of( + McpContentBlock.text("t"), + McpContentBlock.image("AAA=", "image/png"), + McpContentBlock.resource(McpResourceContents.text("file:///e", "text/plain", "emb"))))) + .addTool(tool("failing", null, a -> McpToolOutcome.text("nope").setError(true))) + .addPrompt(prompt("greet", a -> new McpPromptOutcome().setDescription("d").setMessages(List.of( + new McpPromptMessage().setRole(McpRole.USER).setContent(McpContentBlock.text("hi " + a.get("who"))))))) + .addResource(resource("file:///a", u -> new McpResourceOutcome().setContents(List.of( + McpResourceContents.text(u, "text/plain", "body"))))); } } @@ -90,11 +87,15 @@ class Characterization_Test { public static class F_Caps extends McpRestServlet20250618 { private static final long serialVersionUID = 1L; @Override protected McpServerConfig createMcpConfig() { - return new McpServerConfig() - .setCapabilities(new ServerCapabilities() - .setLogging(new LoggingCapability().setLevel("info")) - .setResources(new ResourceCapability().setSubscribe(true).setListChanged(true)) - .setExperimental(JsonMap.of("flag", 1))); + return new McpServerConfig(); + } + + @Override + protected ServerCapabilities capabilities() { + return new ServerCapabilities() + .setLogging(new LoggingCapability().setLevel("info")) + .setResources(new ResourceCapability().setSubscribe(true).setListChanged(true)) + .setExperimental(JsonMap.of("flag", 1)); } } @@ -104,8 +105,8 @@ class Characterization_Test { @Override protected McpServerConfig createMcpConfig() { return new McpServerConfig() .setCursor(McpCursor.fixedSize(1)) - .addTool(tool("t1", null, a -> new CallToolResult())) - .addTool(tool("t2", null, a -> new CallToolResult())); + .addTool(tool("t1", null, a -> new McpToolOutcome())) + .addTool(tool("t2", null, a -> new McpToolOutcome())); } } @@ -122,24 +123,24 @@ class Characterization_Test { // --- fixture handler factories --------------------------------------------------------- - private static McpToolHandler tool(String name, JsonSchema schema, Function<Map<String,Object>,CallToolResult> fn) { + private static McpToolHandler tool(String name, McpSchema schema, Function<Map<String,Object>,McpToolOutcome> fn) { return new McpToolHandler() { - @Override public Tool descriptor() { return new Tool().setName(name).setDescription("desc:" + name).setInputSchema(schema); } - @Override public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } + @Override public McpToolSpec descriptor() { return new McpToolSpec().setName(name).setDescription("desc:" + name).setInputSchema(schema); } + @Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpPromptHandler prompt(String name, Function<Map<String,Object>,GetPromptResult> fn) { + private static McpPromptHandler prompt(String name, Function<Map<String,Object>,McpPromptOutcome> fn) { return new McpPromptHandler() { - @Override public Prompt descriptor() { return new Prompt().setName(name).setDescription("pd"); } - @Override public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } + @Override public McpPromptSpec descriptor() { return new McpPromptSpec().setName(name).setDescription("pd"); } + @Override public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpResourceHandler resource(String uri, Function<String,ReadResourceResult> fn) { + private static McpResourceHandler resource(String uri, Function<String,McpResourceOutcome> fn) { return new McpResourceHandler() { - @Override public Resource descriptor() { return new Resource().setUri(uri).setName("a").setMimeType("text/plain"); } - @Override public ReadResourceResult read(String u, BeanStore ctx) { return fn.apply(u); } + @Override public McpResourceSpec descriptor() { return new McpResourceSpec().setUri(uri).setName("a").setMimeType("text/plain"); } + @Override public McpResourceOutcome read(String u, BeanStore ctx) { return fn.apply(u); } }; } diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_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 index fda7de737f..465a15afd8 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/Mcp20250618Revision_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 @@ -35,43 +35,43 @@ class Mcp20250618Revision_Test { private final BeanStore ctx = new BasicBeanStore(); - private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,CallToolResult> fn) { + private static McpToolHandler tool(String name, java.util.function.Function<Map<String,Object>,McpToolOutcome> fn) { return new McpToolHandler() { @Override - public Tool descriptor() { - return new Tool().setName(name).setDescription("desc:" + name); + public McpToolSpec descriptor() { + return new McpToolSpec().setName(name).setDescription("desc:" + name); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpPromptHandler prompt(String name, java.util.function.Function<Map<String,Object>,GetPromptResult> fn) { + private static McpPromptHandler prompt(String name, java.util.function.Function<Map<String,Object>,McpPromptOutcome> fn) { return new McpPromptHandler() { @Override - public Prompt descriptor() { - return new Prompt().setName(name); + public McpPromptSpec descriptor() { + return new McpPromptSpec().setName(name); } @Override - public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { + public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { return fn.apply(arguments); } }; } - private static McpResourceHandler resource(String uri, java.util.function.Function<String,ReadResourceResult> fn) { + private static McpResourceHandler resource(String uri, java.util.function.Function<String,McpResourceOutcome> fn) { return new McpResourceHandler() { @Override - public Resource descriptor() { - return new Resource().setUri(uri).setName("res"); + public McpResourceSpec descriptor() { + return new McpResourceSpec().setUri(uri).setName("res"); } @Override - public ReadResourceResult read(String u, BeanStore ctx) { + public McpResourceOutcome read(String u, BeanStore ctx) { return fn.apply(u); } }; @@ -88,9 +88,9 @@ class Mcp20250618Revision_Test { @Test void a01_initialize_default_capabilities() { var config = new McpServerConfig() - .addTool(tool("a", a -> new CallToolResult())) - .addPrompt(prompt("p", a -> new GetPromptResult())) - .addResource(resource("r://x", u -> new ReadResourceResult())); + .addTool(tool("a", a -> new McpToolOutcome())) + .addPrompt(prompt("p", a -> new McpPromptOutcome())) + .addResource(resource("r://x", u -> new McpResourceOutcome())); var resp = send(config, req(1, McpMethods.INITIALIZE, null)); assertNotNull(resp); @@ -103,14 +103,11 @@ class Mcp20250618Revision_Test { } @Test - void a02_initialize_explicit_capabilitiesAndServerInfo() { - var caps = new ServerCapabilities().setLogging(new LoggingCapability()); - var info = new Implementation().setName("custom").setVersion("9.9"); - var config = new McpServerConfig().setCapabilities(caps).setServerInfo(info).setInstructions("hi"); + void a02_initialize_explicitServerInfoAndInstructions() { + var config = new McpServerConfig().setName("custom").setVersion("9.9").setInstructions("hi"); var resp = send(config, req(1, McpMethods.INITIALIZE, null)); var result = (InitializeResult) resp.getResult(); - assertSame(caps, result.getCapabilities()); - assertSame(info, result.getServerInfo()); + assertBean(result.getServerInfo(), "name,version", "custom,9.9"); assertString("hi", result.getInstructions()); } @@ -166,7 +163,7 @@ class Mcp20250618Revision_Test { @Test void b03_notification_returnsNullResponse() { - var config = new McpServerConfig().addTool(tool("a", args -> new CallToolResult())); + var config = new McpServerConfig().addTool(tool("a", args -> new McpToolOutcome())); var notif = req(null, McpMethods.TOOLS_CALL, JsonMap.of("name", "a")); assertNull(send(config, notif)); } @@ -192,8 +189,8 @@ class Mcp20250618Revision_Test { @Test void c01_tools_list_singlePage() { var config = new McpServerConfig() - .addTool(tool("a", args -> new CallToolResult())) - .addTool(tool("b", args -> new CallToolResult())); + .addTool(tool("a", args -> new McpToolOutcome())) + .addTool(tool("b", args -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_LIST, null)); var result = (ListToolsResult) resp.getResult(); assertSize(2, result.getTools()); @@ -203,8 +200,8 @@ class Mcp20250618Revision_Test { @Test void c02_tools_list_paged() { var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)) - .addTool(tool("a", args -> new CallToolResult())) - .addTool(tool("b", args -> new CallToolResult())); + .addTool(tool("a", args -> new McpToolOutcome())) + .addTool(tool("b", args -> new McpToolOutcome())); var first = (ListToolsResult) send(config, req(1, McpMethods.TOOLS_LIST, null)).getResult(); assertSize(1, first.getTools()); assertString("1", first.getNextCursor()); @@ -218,9 +215,9 @@ class Mcp20250618Revision_Test { @Test void d01_tools_call_routes_byName() { var config = new McpServerConfig().addTool(tool("echo", args -> { - var ctr = new CallToolResult(); - ctr.setContent(List.of(new TextContent().setText(String.valueOf(args.get("text"))))); - return ctr; + var outcome = new McpToolOutcome(); + outcome.setContent(List.of(McpContentBlock.text(String.valueOf(args.get("text"))))); + return outcome; })); var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "echo", "arguments", JsonMap.of("text", "hi")))); var ctr = (CallToolResult) resp.getResult(); @@ -241,14 +238,14 @@ class Mcp20250618Revision_Test { @Test void d04_tools_call_argumentsNotObject_throwsInvalidParams() { - var config = new McpServerConfig().addTool(tool("e", a -> new CallToolResult())); + var config = new McpServerConfig().addTool(tool("e", a -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_CALL, JsonMap.of("name", "e", "arguments", "string-not-map"))); 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 config = new McpServerConfig().addTool(tool("e", a -> new McpToolOutcome())); var resp = send(config, req(1, McpMethods.TOOLS_CALL, "not-a-map")); assertEquals(Mcp20250618Revision.CODE_INVALID_PARAMS, resp.getError().getCode()); } @@ -288,7 +285,7 @@ class Mcp20250618Revision_Test { @Test void e01_prompts_list_and_get() { - var config = new McpServerConfig().addPrompt(prompt("p", args -> new GetPromptResult().setDescription("ok"))); + var config = new McpServerConfig().addPrompt(prompt("p", args -> new McpPromptOutcome().setDescription("ok"))); var list = (ListPromptsResult) send(config, req(1, McpMethods.PROMPTS_LIST, null)).getResult(); assertSize(1, list.getPrompts()); var get = (GetPromptResult) send(config, req(1, McpMethods.PROMPTS_GET, JsonMap.of("name", "p"))).getResult(); @@ -311,7 +308,7 @@ class Mcp20250618Revision_Test { @Test void f01_resources_list_and_read() { - var config = new McpServerConfig().addResource(resource("file://a", uri -> new ReadResourceResult().setContents(List.of(new TextResourceContents().setUri(uri).setText("ok"))))); + var config = new McpServerConfig().addResource(resource("file://a", uri -> new McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri, null, "ok"))))); var list = (ListResourcesResult) send(config, req(1, McpMethods.RESOURCES_LIST, null)).getResult(); assertSize(1, list.getResources()); var read = (ReadResourceResult) send(config, req(1, McpMethods.RESOURCES_READ, JsonMap.of("uri", "file://a"))).getResult(); @@ -334,7 +331,7 @@ class Mcp20250618Revision_Test { @Test void g01_cursor_paramsAreOptional() { - var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)).addPrompt(prompt("a", args -> new GetPromptResult())).addPrompt(prompt("b", args -> new GetPromptResult())); + var config = new McpServerConfig().setCursor(McpCursor.fixedSize(1)).addPrompt(prompt("a", args -> new McpPromptOutcome())).addPrompt(prompt("b", args -> new McpPromptOutcome())); // params null var resp = (ListPromptsResult) send(config, req(1, McpMethods.PROMPTS_LIST, null)).getResult(); assertSize(1, resp.getPrompts()); diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 index dbbb8e883e..9540afec35 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 @@ -19,7 +19,6 @@ 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.*; @@ -32,19 +31,19 @@ class McpHandlerDefaults_Test { @Test void a01_toolHandler_defaultDescriptor_throws() { - McpToolHandler h = (args, ctx) -> new CallToolResult(); + McpToolHandler h = (args, ctx) -> new McpToolOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } @Test void a02_promptHandler_defaultDescriptor_throws() { - McpPromptHandler h = (args, ctx) -> new GetPromptResult(); + McpPromptHandler h = (args, ctx) -> new McpPromptOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } @Test void a03_resourceHandler_defaultDescriptor_throws() { - McpResourceHandler h = (uri, ctx) -> new ReadResourceResult(); + McpResourceHandler h = (uri, ctx) -> new McpResourceOutcome(); assertThrows(UnsupportedOperationException.class, h::descriptor); } diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_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 index 53dd178d03..d452ecdb16 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpRestServlet20250618_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 @@ -51,18 +51,16 @@ class McpRestServlet20250618_Test extends TestBase { @Override protected McpServerConfig createMcpConfig() { return new McpServerConfig() - .setServerInfo(new Implementation().setName("test").setVersion("1.0.0")) + .setName("test").setVersion("1.0.0") .addTool(new McpToolHandler() { @Override - public Tool descriptor() { - return new Tool().setName("echo").setDescription("Echoes back"); + public McpToolSpec descriptor() { + return new McpToolSpec().setName("echo").setDescription("Echoes back"); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - var ctr = new CallToolResult(); - ctr.setContent(List.of(new TextContent().setText(String.valueOf(arguments.get("text"))))); - return ctr; + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { + return McpToolOutcome.text(String.valueOf(arguments.get("text"))); } }); } @@ -123,11 +121,11 @@ class McpRestServlet20250618_Test extends TestBase { public McpServerConfig getMcpConfig() { return new McpServerConfig().addTool(new McpToolHandler() { @Override - public Tool descriptor() { return new Tool().setName("ping"); } + public McpToolSpec descriptor() { return new McpToolSpec().setName("ping"); } @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - return new CallToolResult().setContent(List.of(new TextContent().setText("pong"))); + public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { + return McpToolOutcome.text("pong"); } }); } diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 deleted file mode 100644 index 6ec4f9fbd6..0000000000 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/McpServerConfig_Test.java +++ /dev/null @@ -1,137 +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.v20250618; - -import static org.apache.juneau.test.bct.BctAssertions.*; -import static org.junit.jupiter.api.Assertions.*; - -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.*; - -/** - * Coverage for {@link McpServerConfig}. - */ -class McpServerConfig_Test { - - private static McpToolHandler dummyTool(String name) { - return new McpToolHandler() { - @Override - public Tool descriptor() { - return new Tool().setName(name); - } - - @Override - public CallToolResult call(Map<String,Object> arguments, BeanStore ctx) { - return new CallToolResult(); - } - }; - } - - private static McpPromptHandler dummyPrompt(String name) { - return new McpPromptHandler() { - @Override - public Prompt descriptor() { - return new Prompt().setName(name); - } - - @Override - public GetPromptResult get(Map<String,Object> arguments, BeanStore ctx) { - return new GetPromptResult(); - } - }; - } - - private static McpResourceHandler dummyResource(String uri) { - return new McpResourceHandler() { - @Override - public Resource descriptor() { - return new Resource().setUri(uri); - } - - @Override - public ReadResourceResult read(String u, BeanStore ctx) { - return new ReadResourceResult(); - } - }; - } - - @Test - void a01_defaults() { - var c = new McpServerConfig(); - assertNull(c.getServerInfo()); - assertString(McpProtocol.VERSION_2025_06_18, c.getProtocolVersion()); - assertNull(c.getInstructions()); - assertNotNull(c.getTools()); - assertNotNull(c.getPrompts()); - assertNotNull(c.getResources()); - assertNull(c.getCapabilities()); - assertSame(McpCursor.SINGLE_PAGE, c.getCursor()); - } - - @Test - void a02_setters_and_addCalls() { - var info = new Implementation().setName("x").setVersion("1"); - var caps = new ServerCapabilities().setLogging(new LoggingCapability()); - var c = new McpServerConfig() - .setServerInfo(info) - .setProtocolVersion("2024-11-05") - .setInstructions("hello") - .setCapabilities(caps) - .setCursor(McpCursor.fixedSize(5)) - .addTool(dummyTool("t1"), dummyTool("t2")) - .addPrompt(dummyPrompt("p1")) - .addResource(dummyResource("r://a")); - - assertSame(info, c.getServerInfo()); - assertString("2024-11-05", c.getProtocolVersion()); - assertString("hello", c.getInstructions()); - assertSame(caps, c.getCapabilities()); - assertNotSame(McpCursor.SINGLE_PAGE, c.getCursor()); - assertSize(2, c.getTools()); - assertSize(1, c.getPrompts()); - assertSize(1, c.getResources()); - } - - @Test - void a03_setLists_replacingAndNullClears() { - var c = new McpServerConfig().addTool(dummyTool("t")).addPrompt(dummyPrompt("p")).addResource(dummyResource("r")); - c.setTools(null); - c.setPrompts(null); - c.setResources(null); - assertEmpty(c.getTools()); - assertEmpty(c.getPrompts()); - assertEmpty(c.getResources()); - - c.setTools(List.of(dummyTool("a"))); - c.setPrompts(List.of(dummyPrompt("a"))); - c.setResources(List.of(dummyResource("a"))); - assertSize(1, c.getTools()); - assertSize(1, c.getPrompts()); - assertSize(1, c.getResources()); - } - - @Test - void a04_setCursor_nullResets() { - var c = new McpServerConfig().setCursor(McpCursor.fixedSize(2)); - c.setCursor(null); - assertSame(McpCursor.SINGLE_PAGE, c.getCursor()); - } -} diff --git a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 index 7c5f2c2157..a01ba476c6 100644 --- a/juneau-rest/juneau-rest-server-mcp-2025-06-18/src/test/java/org/apache/juneau/rest/server/mcp/v20250618/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 @@ -115,7 +115,9 @@ class McpTypedHandlers_Test { .setMethod(McpMethods.TOOLS_CALL) .setParams(JsonMap.of("name", "d")); var resp = dispatch(req, config); - assertSame(ctr, resp.getResult()); + // The neutral/wire boundary always remaps the result, so only value equality survives here. + var result = (CallToolResult) resp.getResult(); + assertString("direct", ((TextContent) result.getContent().get(0)).getText()); } @Test @@ -234,7 +236,7 @@ class McpTypedHandlers_Test { }; var raw = McpTypedHandlers.adaptTool(typed); var ctr = raw.call(null, ctx); - assertString("null", ((TextContent) ctr.getContent().get(0)).getText()); + assertString("null", ctr.getContent().get(0).text()); } @Test diff --git a/juneau-rest/juneau-rest-server-mcp/pom.xml b/juneau-rest/juneau-rest-server-mcp/pom.xml index f3ec6d2046..dff22f2baa 100644 --- a/juneau-rest/juneau-rest-server-mcp/pom.xml +++ b/juneau-rest/juneau-rest-server-mcp/pom.xml @@ -40,11 +40,6 @@ <artifactId>juneau-rest-server</artifactId> <version>${project.version}</version> </dependency> - <dependency> - <groupId>org.apache.juneau</groupId> - <artifactId>juneau-bean-mcp-2025-06-18</artifactId> - <version>${project.version}</version> - </dependency> <dependency> <groupId>org.apache.juneau</groupId> <artifactId>juneau-bean-jsonrpc</artifactId> 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 e8612f8d8a..3e571f554b 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 @@ -18,15 +18,15 @@ package org.apache.juneau.rest.server.mcp; import java.util.*; -import org.apache.juneau.bean.mcp.v20250618.*; import org.apache.juneau.commons.inject.*; /** * Handler for a single MCP prompt. * * <p> - * Implementations declare a {@link #descriptor() descriptor} (the {@link Prompt} returned by {@code prompts/list}) - * and a {@link #get(Map, BeanStore) get} body invoked when the matching {@code prompts/get} method runs. + * Implementations declare a {@link #descriptor() descriptor} (the {@link McpPromptSpec} returned by + * {@code prompts/list}) and a {@link #get(Map, BeanStore) get} body invoked when the matching + * {@code prompts/get} method runs. */ @FunctionalInterface public interface McpPromptHandler { @@ -35,13 +35,13 @@ public interface McpPromptHandler { * Returns the static descriptor for this prompt. * * <p> - * The {@link Prompt#getName() name} value is used by the bound {@link McpRevision} to route + * The {@link McpPromptSpec#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}. */ - default Prompt descriptor() { + default McpPromptSpec descriptor() { throw new UnsupportedOperationException("descriptor() must be implemented by McpPromptHandler subclasses."); } @@ -52,5 +52,5 @@ public interface McpPromptHandler { * @param ctx Per-request bean store. Never {@code null}. * @return The rendered prompt. Never {@code null}. */ - GetPromptResult get(Map<String,Object> arguments, BeanStore ctx); + McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx); } 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 db02fe3e77..f79a91b87f 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 @@ -16,15 +16,15 @@ */ package org.apache.juneau.rest.server.mcp; -import org.apache.juneau.bean.mcp.v20250618.*; import org.apache.juneau.commons.inject.*; /** * Handler for a single MCP resource. * * <p> - * Implementations declare a {@link #descriptor() descriptor} (the {@link Resource} returned by {@code resources/list}) - * and a {@link #read(String, BeanStore) read} body invoked when the matching {@code resources/read} method runs. + * Implementations declare a {@link #descriptor() descriptor} (the {@link McpResourceSpec} returned by + * {@code resources/list}) and a {@link #read(String, BeanStore) read} body invoked when the matching + * {@code resources/read} method runs. */ @FunctionalInterface public interface McpResourceHandler { @@ -33,13 +33,13 @@ public interface McpResourceHandler { * Returns the static descriptor for this resource. * * <p> - * The {@link Resource#getUri() uri} value is used by the bound {@link McpRevision} to route + * The {@link McpResourceSpec#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}. */ - default Resource descriptor() { + default McpResourceSpec descriptor() { throw new UnsupportedOperationException("descriptor() must be implemented by McpResourceHandler subclasses."); } @@ -50,5 +50,5 @@ public interface McpResourceHandler { * @param ctx Per-request bean store. Never {@code null}. * @return The resource contents. Never {@code null}. */ - ReadResourceResult read(String uri, BeanStore ctx); + McpResourceOutcome read(String uri, BeanStore ctx); } 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 3475544b92..b51302eee3 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 @@ -19,7 +19,6 @@ package org.apache.juneau.rest.server.mcp; import java.util.concurrent.atomic.*; 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.marshall.serializer.*; 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 da8fd2ac99..ae301961d7 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 @@ -20,8 +20,6 @@ import static org.apache.juneau.commons.utils.Shorts.*; import java.util.*; -import org.apache.juneau.bean.mcp.v20250618.*; - /** * Aggregate configuration consumed by the bound {@link McpRevision}. * @@ -29,58 +27,64 @@ import org.apache.juneau.bean.mcp.v20250618.*; * Applications register a single {@link McpServerConfig} (typically as a bean in their {@code RestContext} * bean store) listing the tools, prompts, and resources to expose, plus optional server metadata and a * pagination strategy. + * + * <p> + * This type is revision-neutral: it holds no field typed with any protocol revision's wire beans, and + * in particular holds no capabilities field in any form. The protocol version is owned by + * {@link McpRevision#protocolVersion()}, and an explicit capabilities advertisement is owned by a + * revision-specific hook on that revision's servlet or endpoint mixin. */ public class McpServerConfig { - private Implementation serverInfo; - private String protocolVersion = McpProtocol.VERSION_2025_06_18; + private String name; + private String version; private String instructions; private List<McpToolHandler> tools = l(); private List<McpPromptHandler> prompts = l(); private List<McpResourceHandler> resources = l(); - private ServerCapabilities capabilities; private McpCursor cursor = McpCursor.SINGLE_PAGE; /** - * Server identity reported in {@code initialize}. + * Server name reported in {@code initialize}. * * <p> - * If {@code null}, the bound {@link McpRevision} fills in a default identity. + * When both this and {@link #getVersion()} are {@code null}, the bound revision substitutes its + * own default server identity. * - * @return The server info, or {@code null} if not set. + * @return The name, or {@code null} if not set. */ - public Implementation getServerInfo() { - return serverInfo; + public String getName() { + return name; } /** - * Sets the server identity. + * Sets the server name. * - * @param serverInfo The new value. - * @return This object (for method chaining). + * @param value The new value. Can be <jk>null</jk> to unset the property. + * @return This object. */ - public McpServerConfig setServerInfo(Implementation serverInfo) { - this.serverInfo = serverInfo; + public McpServerConfig setName(String value) { + name = value; return this; } /** - * MCP protocol revision returned by {@code initialize}. + * Server version reported in {@code initialize}. * - * @return The protocol version, or <jk>null</jk> if not set. + * @return The version, or {@code null} if not set. */ - public String getProtocolVersion() { - return protocolVersion; + public String getVersion() { + return version; } /** - * Sets the protocol revision. + * Sets the server version. * - * @param protocolVersion The new value. Can be <jk>null</jk> to unset it. - * @return This object (for method chaining). + * @param value The new value. Can be <jk>null</jk> to unset the property. + * @return This object. */ - public McpServerConfig setProtocolVersion(String protocolVersion) { - this.protocolVersion = protocolVersion; + public McpServerConfig setVersion(String value) { + version = value; return this; } @@ -197,29 +201,6 @@ public class McpServerConfig { return this; } - /** - * Optional explicit capabilities advertisement. - * - * <p> - * When {@code null}, the bound {@link McpRevision} synthesizes one from the registered handler lists. - * - * @return The override, or {@code null} if auto-derived. - */ - public ServerCapabilities getCapabilities() { - return capabilities; - } - - /** - * Sets the explicit capabilities advertisement. - * - * @param capabilities The new value. - * @return This object (for method chaining). - */ - public McpServerConfig setCapabilities(ServerCapabilities capabilities) { - this.capabilities = capabilities; - return this; - } - /** * Pagination strategy for {@code list} dispatchers (tools / prompts / resources). * 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 6d0169d020..0c88a0bd20 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 @@ -18,19 +18,20 @@ package org.apache.juneau.rest.server.mcp; import java.util.*; -import org.apache.juneau.bean.mcp.v20250618.*; import org.apache.juneau.commons.inject.*; /** * Handler for a single MCP tool. * * <p> - * Implementations declare a {@link #descriptor() descriptor} (the {@link Tool} returned by {@code tools/list}) - * and a {@link #call(Map, BeanStore) call} body invoked when the matching {@code tools/call} method runs. + * Implementations declare a {@link #descriptor() descriptor} (returned from {@code tools/list}) and + * a {@link #call(Map, BeanStore) call} body invoked when a matching {@code tools/call} arrives. Both + * use the revision-neutral model, so a handler compiles against exactly one protocol revision's + * worth of assumptions: none. * * <p> - * The {@link BeanStore} argument is the per-request bean store, allowing handlers to look up additional - * services (or the underlying {@code RestRequest}) without making this interface depend on REST runtime types. + * The {@link BeanStore} argument is the per-request bean store, letting handlers look up additional + * services (or the underlying {@code RestRequest}) without this interface depending on REST runtime types. */ @FunctionalInterface public interface McpToolHandler { @@ -39,13 +40,13 @@ public interface McpToolHandler { * Returns the static descriptor for this tool. * * <p> - * The {@link Tool#getName() name} value is used by the bound {@link McpRevision} to route + * The {@link McpToolSpec#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}. */ - default Tool descriptor() { + default McpToolSpec descriptor() { throw new UnsupportedOperationException("descriptor() must be implemented by McpToolHandler subclasses."); } @@ -56,5 +57,5 @@ public interface McpToolHandler { * @param ctx Per-request bean store. Never {@code null}. * @return The call result. Never {@code null}. */ - CallToolResult call(Map<String,Object> arguments, BeanStore ctx); + McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx); } diff --git a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/package-info.java b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/package-info.java index 4799b0818a..ce41ce58fc 100644 --- a/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/package-info.java +++ b/juneau-rest/juneau-rest-server-mcp/src/main/java/org/apache/juneau/rest/server/mcp/package-info.java @@ -16,7 +16,18 @@ */ /** - * Stateless Model Context Protocol (MCP) endpoint built on {@code juneau-rest-server} using the - * {@code juneau-bean-mcp} wire types. + * Revision-neutral core for MCP endpoints built on {@code juneau-rest-server}. + * + * <p> + * This package knows the shape of an MCP server — tools, prompts, resources, pagination, the + * JSON-RPC dispatch contract, and the two HTTP entry points — without knowing any MCP protocol + * revision. It has no compile dependency on any revision's wire beans, and that is enforced at + * build time. A protocol revision is supplied by an implementation of {@link McpRevision} living in + * its own module (for example {@code juneau-rest-server-mcp-2025-06-18}), and a consumer binds one + * at compile time by extending that revision's abstract servlet or composing its endpoint mixin. + * + * <h5 class='section'>See Also:</h5><ul> + * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestServerMcp">juneau-rest-server-mcp</a> + * </ul> */ package org.apache.juneau.rest.server.mcp; diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/FakeMcpRevision_Test.java b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/FakeMcpRevision_Test.java index 80ec9c80de..5b7658aa02 100644 --- a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/FakeMcpRevision_Test.java +++ b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/FakeMcpRevision_Test.java @@ -1,3 +1,19 @@ +/* + * 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.junit.jupiter.api.Assertions.*; @@ -103,7 +119,6 @@ class FakeMcpRevision_Test extends TestBase { } @Test - @org.junit.jupiter.api.Disabled("Enabled by the task that drops core's dependency on juneau-bean-mcp-2025-06-18.") void a04_coreTestTreeSeesNoRevisionModule() { assertThrows(ClassNotFoundException.class, () -> Class.forName("org.apache.juneau.rest.server.mcp.v20250618.Mcp20250618Revision"), diff --git a/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java new file mode 100644 index 0000000000..83d5c67dde --- /dev/null +++ b/juneau-rest/juneau-rest-server-mcp/src/test/java/org/apache/juneau/rest/server/mcp/McpConfigCore_Test.java @@ -0,0 +1,126 @@ +/* + * 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.test.bct.BctAssertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.*; + +import org.apache.juneau.commons.inject.*; +import org.junit.jupiter.api.*; + +/** + * Coverage for the revision-neutral {@link McpServerConfig}. + */ +class McpConfigCore_Test { + + private static McpToolHandler dummyTool(String name) { + return new McpToolHandler() { + @Override public McpToolSpec descriptor() { return new McpToolSpec().setName(name); } + @Override public McpToolOutcome call(Map<String,Object> arguments, BeanStore ctx) { return new McpToolOutcome(); } + }; + } + + private static McpPromptHandler dummyPrompt(String name) { + return new McpPromptHandler() { + @Override public McpPromptSpec descriptor() { return new McpPromptSpec().setName(name); } + @Override public McpPromptOutcome get(Map<String,Object> arguments, BeanStore ctx) { return new McpPromptOutcome(); } + }; + } + + private static McpResourceHandler dummyResource(String uri) { + return new McpResourceHandler() { + @Override public McpResourceSpec descriptor() { return new McpResourceSpec().setUri(uri); } + @Override public McpResourceOutcome read(String u, BeanStore ctx) { return new McpResourceOutcome(); } + }; + } + + @Test + void a01_defaults() { + var a = new McpServerConfig(); + assertNull(a.getName()); + assertNull(a.getVersion()); + assertNull(a.getInstructions()); + assertNotNull(a.getTools()); + assertNotNull(a.getPrompts()); + assertNotNull(a.getResources()); + assertSame(McpCursor.SINGLE_PAGE, a.getCursor()); + } + + @Test + void a02_setters_and_addCalls() { + var a = new McpServerConfig() + .setName("x") + .setVersion("1") + .setInstructions("hello") + .setCursor(McpCursor.fixedSize(5)) + .addTool(dummyTool("t1"), dummyTool("t2")) + .addPrompt(dummyPrompt("p1")) + .addResource(dummyResource("r://a")); + + assertString("x", a.getName()); + assertString("1", a.getVersion()); + assertString("hello", a.getInstructions()); + assertNotSame(McpCursor.SINGLE_PAGE, a.getCursor()); + assertSize(2, a.getTools()); + assertSize(1, a.getPrompts()); + assertSize(1, a.getResources()); + } + + @Test + void a03_setLists_replacingAndNullClears() { + var a = new McpServerConfig().addTool(dummyTool("t")).addPrompt(dummyPrompt("p")).addResource(dummyResource("r")); + a.setTools(null); + a.setPrompts(null); + a.setResources(null); + assertEmpty(a.getTools()); + assertEmpty(a.getPrompts()); + assertEmpty(a.getResources()); + + a.setTools(List.of(dummyTool("a"))); + a.setPrompts(List.of(dummyPrompt("a"))); + a.setResources(List.of(dummyResource("a"))); + assertSize(1, a.getTools()); + assertSize(1, a.getPrompts()); + assertSize(1, a.getResources()); + } + + @Test + void a04_setCursor_nullResets() { + var a = new McpServerConfig().setCursor(McpCursor.fixedSize(2)); + a.setCursor(null); + assertSame(McpCursor.SINGLE_PAGE, a.getCursor()); + } + + @Test + void a05_noRevisionSpecificSurfaceRemains() { + var m = Arrays.stream(McpServerConfig.class.getMethods()).map(java.lang.reflect.Method::getName).toList(); + assertFalse(m.contains("getCapabilities"), "capabilities are revision-owned; core must hold none"); + assertFalse(m.contains("setCapabilities"), "capabilities are revision-owned; core must hold none"); + assertFalse(m.contains("getProtocolVersion"), "the protocol version is owned by McpRevision.protocolVersion()"); + assertFalse(m.contains("setProtocolVersion"), "the protocol version is owned by McpRevision.protocolVersion()"); + assertFalse(m.contains("getServerInfo"), "server identity is plain name/version on the neutral config"); + } + + @Test + void a06_nullListElementsAreDistinctFromEmpty() { + assertNull(new McpToolOutcome().getContent()); + assertNull(new McpPromptOutcome().getMessages()); + assertNull(new McpResourceOutcome().getContents()); + } +}
