diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java index 0d7a840..5ac28ce 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java @@ -251,7 +251,7 @@ protected CallToolResult mapValueToCallToolResult(Object value) { if (this.returnMode == ReturnMode.STRUCTURED) { String jsonOutput = JsonParser.toJson(value); - Map structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE); + Object structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE); return CallToolResult.builder().structuredContent(structuredOutput).build(); } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractSyncMcpToolMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractSyncMcpToolMethodCallback.java index 45be3b9..b775033 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractSyncMcpToolMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractSyncMcpToolMethodCallback.java @@ -162,7 +162,7 @@ protected CallToolResult processResult(Object result) { if (this.returnMode == ReturnMode.STRUCTURED) { String jsonOutput = JsonParser.toJson(result); - Map structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE); + Object structuredOutput = JsonParser.fromJson(jsonOutput, Object.class); return CallToolResult.builder().structuredContent(structuredOutput).build(); } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncMcpToolMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncMcpToolMethodCallbackTests.java index 724a254..6b14ed9 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncMcpToolMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncMcpToolMethodCallbackTests.java @@ -483,7 +483,7 @@ public void testConstructorParameters() { } @Test - public void testToolWithStructuredOutput() throws Exception { + public void testToolWithTextOutput() throws Exception { TestToolProvider provider = new TestToolProvider(); Method method = TestToolProvider.class.getMethod("processObject", TestObject.class); SyncMcpToolMethodCallback callback = new SyncMcpToolMethodCallback(ReturnMode.TEXT, method, provider); @@ -546,6 +546,28 @@ public void testToolReturningComplexListObject() throws Exception { [{"name":"test","value":42}]""")); } + @Test + public void testToolReturningStructuredComplexListObject() throws Exception { + TestToolProvider provider = new TestToolProvider(); + Method method = TestToolProvider.class.getMethod("returnListObjectTool", String.class, int.class); + SyncMcpToolMethodCallback callback = new SyncMcpToolMethodCallback(ReturnMode.STRUCTURED, method, provider); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + CallToolRequest request = new CallToolRequest("return-list-object-tool", Map.of("name", "test", "value", 42)); + + CallToolResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.isError()).isFalse(); + + assertThat(result.structuredContent()).isNotNull(); + assertThat(result.structuredContent()).isInstanceOf(List.class); + assertThat((List) result.structuredContent()).hasSize(1); + Map firstEntry = ((List>) result.structuredContent()).get(0); + assertThat(firstEntry).containsEntry("name", "test"); + assertThat(firstEntry).containsEntry("value", 42); + } + @Test public void testToolReturningStringList() throws Exception { TestToolProvider provider = new TestToolProvider(); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java index e1e066e..b1ea1f8 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java @@ -10,6 +10,7 @@ import java.util.Map; import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.TextContent; @@ -128,6 +129,11 @@ public String toolWithContextAndRequest(McpTransportContext context, CallToolReq return "Context present, Tool: " + request.name(); } + @McpTool(name = "return-list-object-tool", description = "Tool that returns a list of complex objects") + public List returnListObjectTool(String name, int value) { + return List.of(new TestObject(name, value)); + } + /** * Tool with McpMeta parameter */ @@ -543,6 +549,28 @@ public void testToolReturningComplexObject() throws Exception { assertThat((Map) result.structuredContent()).containsEntry("value", 42); } + @Test + public void testToolReturningStructuredComplexListObject() throws Exception { + TestToolProvider provider = new TestToolProvider(); + Method method = TestToolProvider.class.getMethod("returnListObjectTool", String.class, int.class); + SyncMcpToolMethodCallback callback = new SyncMcpToolMethodCallback(ReturnMode.STRUCTURED, method, provider); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + CallToolRequest request = new CallToolRequest("return-list-object-tool", Map.of("name", "test", "value", 42)); + + CallToolResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.isError()).isFalse(); + + assertThat(result.structuredContent()).isNotNull(); + assertThat(result.structuredContent()).isInstanceOf(List.class); + assertThat((List) result.structuredContent()).hasSize(1); + Map firstEntry = ((List>) result.structuredContent()).get(0); + assertThat(firstEntry).containsEntry("name", "test"); + assertThat(firstEntry).containsEntry("value", 42); + } + @Test public void testVoidReturnMode() throws Exception { TestToolProvider provider = new TestToolProvider(); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProviderTests.java index aa94635..812daa7 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProviderTests.java @@ -693,6 +693,47 @@ public List listResponseTool(String input) { [{"message":"Processed: test"}]""")); } + @Test + void testToolWithStructuredListReturnType() { + + record CustomResult(String message) { + } + + class ListResponseTool { + + @McpTool(name = "list-response", description = "Tool List response", generateOutputSchema = true) + public List listResponseTool(String input) { + return List.of(new CustomResult("Processed: " + input)); + } + + } + + ListResponseTool toolObject = new ListResponseTool(); + SyncMcpToolProvider provider = new SyncMcpToolProvider(List.of(toolObject)); + + List toolSpecs = provider.getToolSpecifications(); + + assertThat(toolSpecs).hasSize(1); + SyncToolSpecification toolSpec = toolSpecs.get(0); + + assertThat(toolSpec.tool().name()).isEqualTo("list-response"); + assertThat(toolSpec.tool().outputSchema()).isNotNull(); + + BiFunction callHandler = toolSpec + .callHandler(); + + McpSchema.CallToolResult result = callHandler.apply(mock(McpSyncServerExchange.class), + new CallToolRequest("list-response", Map.of("input", "test"))); + + assertThat(result).isNotNull(); + assertThat(result.isError()).isFalse(); + + assertThat(result.structuredContent()).isInstanceOf(List.class); + assertThat((List) result.structuredContent()).hasSize(1); + Map firstEntry = ((List>) result.structuredContent()).get(0); + assertThat(firstEntry).containsEntry("message", "Processed: test"); + } + @Test void testToolWithPrimitiveReturnTypeNoOutputSchema() { class PrimitiveTool { diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProviderTests.java index 58bfe82..7af35f8 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProviderTests.java @@ -692,6 +692,46 @@ public List listResponseTool(String input) { [{"message":"Processed: test"}]""")); } + @Test + void testToolWithStructuredListReturnType() { + + record CustomResult(String message) { + } + + class ListResponseTool { + + @McpTool(name = "list-response", description = "Tool List response", generateOutputSchema = true) + public List listResponseTool(String input) { + return List.of(new CustomResult("Processed: " + input)); + } + + } + + ListResponseTool toolObject = new ListResponseTool(); + SyncStatelessMcpToolProvider provider = new SyncStatelessMcpToolProvider(List.of(toolObject)); + + List toolSpecs = provider.getToolSpecifications(); + + assertThat(toolSpecs).hasSize(1); + SyncToolSpecification toolSpec = toolSpecs.get(0); + + assertThat(toolSpec.tool().name()).isEqualTo("list-response"); + assertThat(toolSpec.tool().outputSchema()).isNotNull(); + + BiFunction callHandler = toolSpec.callHandler(); + + McpSchema.CallToolResult result = callHandler.apply(mock(McpTransportContext.class), + new CallToolRequest("list-response", Map.of("input", "test"))); + + assertThat(result).isNotNull(); + assertThat(result.isError()).isFalse(); + + assertThat(result.structuredContent()).isInstanceOf(List.class); + assertThat((List) result.structuredContent()).hasSize(1); + Map firstEntry = ((List>) result.structuredContent()).get(0); + assertThat(firstEntry).containsEntry("message", "Processed: test"); + } + @Test void testToolWithPrimitiveReturnTypeNoOutputSchema() { class PrimitiveTool {