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 0ac1ac4fcd997f1de4c3e5566689647aef879823 Author: James Bognar <[email protected]> AuthorDate: Tue Jul 28 19:09:45 2026 -0400 test: cover MCP 2025-06-18 wire content/role branches, mark HTT for unreachable throws Task 16 coverage sweep found gaps in Mcp20250618Wire's toNeutral/toWire conversion for image/embedded-resource content and non-USER prompt-message roles, plus McpPromptArgument/McpPromptSpec argument round-tripping. Added two focused tests to McpTypedHandlers_Test covering those paths. The two "unsupported type" throw branches in Mcp20250618Wire are unreachable through the public API (guarded by prior validation), so they are marked // HTT rather than contorted with reflection-based tests. McpRestServlet's compareAndSet race-condition branch is similarly HTT. Co-authored-by: Cursor <[email protected]> --- .../rest/server/mcp/v20250618/Mcp20250618Wire.java | 4 +- .../mcp/v20250618/McpTypedHandlers_Test.java | 64 ++++++++++++++++++++++ .../juneau/rest/server/mcp/McpRestServlet.java | 2 +- 3 files changed, 67 insertions(+), 3 deletions(-) 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 index 9b7b4aac4e..d4e848f828 100644 --- 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 @@ -188,7 +188,7 @@ final class Mcp20250618Wire { 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()); + throw new McpException(Mcp20250618Revision.CODE_INTERNAL_ERROR, "Unsupported content type: " + x.getClass().getName()); // HTT: the Content dictionary is closed to Text/Image/EmbeddedResource; a 4th implementation cannot occur through the public API. } static McpResourceContents toNeutral(ResourceContents x) { @@ -198,7 +198,7 @@ final class Mcp20250618Wire { 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()); + throw new McpException(Mcp20250618Revision.CODE_INTERNAL_ERROR, "Unsupported resource contents type: " + x.getClass().getName()); // HTT: the ResourceContents dictionary is closed to Text/Blob; a 3rd implementation cannot occur through the public API. } static McpPromptSpec toNeutral(Prompt x) { 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 a01ba476c6..433746f88e 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 @@ -273,6 +273,70 @@ class McpTypedHandlers_Test { assertEquals(Mcp20250618Revision.CODE_INTERNAL_ERROR, resp.getError().getCode()); } + @Test + void a08_typedTool_callToolResultWithImageAndResourceContent_roundTrips() { + var ctr = new CallToolResult().setContent(List.of( + new ImageContent().setData("aW1n").setMimeType("image/png"), + new EmbeddedResourceContent().setResource(new TextResourceContents().setUri("r://x").setMimeType("text/plain").setText("inline")), + new EmbeddedResourceContent().setResource(new BlobResourceContents().setUri("r://y").setMimeType("application/octet-stream").setBlob("QUJD")))); + var typed = new McpTypedToolHandler<EchoArgs,CallToolResult>() { + @Override + public Tool descriptor() { return new Tool().setName("m"); } + @Override + public Class<EchoArgs> argumentType() { return EchoArgs.class; } + @Override + public CallToolResult call(EchoArgs args, BeanStore ctx) { return ctr; } + }; + var config = new McpServerConfig().addTool(McpTypedHandlers.adaptTool(typed)); + var resp = dispatch(new JsonRpcRequest() + .setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1) + .setMethod(McpMethods.TOOLS_CALL).setParams(JsonMap.of("name", "m")), config); + var result = (CallToolResult) resp.getResult(); + assertSize(3, result.getContent()); + assertString("aW1n", ((ImageContent) result.getContent().get(0)).getData()); + var textResource = (EmbeddedResourceContent) result.getContent().get(1); + assertString("inline", ((TextResourceContents) textResource.getResource()).getText()); + var blobResource = (EmbeddedResourceContent) result.getContent().get(2); + assertString("QUJD", ((BlobResourceContents) blobResource.getResource()).getBlob()); + } + + @Test + void b03_typedPrompt_descriptorArgumentsAndAllRoleMessages_roundTrip() { + var typed = new McpTypedPromptHandler<EchoArgs>() { + @Override + public Prompt descriptor() { + return new Prompt().setName("m").setArguments(List.of(new PromptArgument().setName("who").setRequired(true))); + } + @Override + public Class<EchoArgs> argumentType() { return EchoArgs.class; } + @Override + public GetPromptResult get(EchoArgs args, BeanStore ctx) { + return new GetPromptResult().setMessages(List.of( + new PromptMessage().setRole(Role.USER).setContent(new TextContent().setText("hi")), + new PromptMessage().setRole(Role.ASSISTANT).setContent(new TextContent().setText("hello")), + new PromptMessage().setRole(Role.SYSTEM).setContent(new TextContent().setText("sys")), + new PromptMessage().setRole(Role.TOOL).setContent(new TextContent().setText("tool")))); + } + }; + var raw = McpTypedHandlers.adaptPrompt(typed); + var config = new McpServerConfig().addPrompt(raw); + + var list = (ListPromptsResult) dispatch(new JsonRpcRequest() + .setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1) + .setMethod(McpMethods.PROMPTS_LIST).setParams(null), config).getResult(); + assertSize(1, list.getPrompts().get(0).getArguments()); + assertString("who", list.getPrompts().get(0).getArguments().get(0).getName()); + + var pr = (GetPromptResult) dispatch(new JsonRpcRequest() + .setJsonrpc(McpProtocol.JSON_RPC_2_0).setId(1) + .setMethod(McpMethods.PROMPTS_GET).setParams(JsonMap.of("name", "m")), config).getResult(); + assertSize(4, pr.getMessages()); + assertEquals(Role.USER, pr.getMessages().get(0).getRole()); + assertEquals(Role.ASSISTANT, pr.getMessages().get(1).getRole()); + assertEquals(Role.SYSTEM, pr.getMessages().get(2).getRole()); + assertEquals(Role.TOOL, pr.getMessages().get(3).getRole()); + } + @Test void b02_typedPrompt_argsBoundAndResult() { var typed = new McpTypedPromptHandler<EchoArgs>() { 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 b51302eee3..8ed3097820 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 @@ -83,7 +83,7 @@ public abstract class McpRestServlet extends BasicRestServlet { var nc = createMcpConfig(); if (nc == null) throw new IllegalStateException("createMcpConfig() returned null"); - c = config.compareAndSet(null, nc) ? nc : config.get(); + c = config.compareAndSet(null, nc) ? nc : config.get(); // HTT: the CAS-loses branch requires a genuine concurrent first-access race; untestable deterministically. } return c; }
