From bc7d4ee79342db27dcf10318f38f6794365b7df2 Mon Sep 17 00:00:00 2001 From: Ilayaperumal Gopinathan Date: Thu, 11 Sep 2025 18:42:31 +0100 Subject: [PATCH] Fix MCP toolcall returning list values - When the MCP toolcall method returns list of values, the returned content needs to be converted as list of text content values (after converting the value to String type). - Add test to verify this behaviour Signed-off-by: Ilayaperumal Gopinathan --- .../AbstractSyncMcpToolMethodCallback.java | 15 +++++++++--- .../tool/SyncMcpToolMethodCallbackTests.java | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) 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 a6f9c07..235b0b3 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 @@ -19,7 +19,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springaicommunity.mcp.annotation.McpMeta; @@ -41,6 +43,7 @@ * @param The type of the context parameter (e.g., McpTransportContext or * McpSyncServerExchange) * @author Christian Tzolov + * @author Ilayaperumal Gopinathan */ public abstract class AbstractSyncMcpToolMethodCallback { @@ -156,9 +159,15 @@ protected CallToolResult processResult(Object result) { return CallToolResult.builder().addTextContent(JsonParser.toJson("Done")).build(); } else if (this.returnMode == ReturnMode.STRUCTURED) { - String jsonOutput = JsonParser.toJson(result); - Map structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE); - return CallToolResult.builder().structuredContent(structuredOutput).build(); + if (result instanceof List) { + List texts = ((List) result).stream().map(String::valueOf).collect(Collectors.toList()); + return CallToolResult.builder().textContent(texts).build(); + } + else { + String jsonOutput = JsonParser.toJson(result); + Map structuredOutput = JsonParser.fromJson(jsonOutput, MAP_TYPE_REFERENCE); + return CallToolResult.builder().structuredContent(structuredOutput).build(); + } } // Default to text output 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 9c00379..27718d7 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 @@ -106,6 +106,11 @@ public TestObject returnObjectTool(String name, int value) { return new TestObject(name, value); } + @McpTool(name = "return-list-tool", description = "Tool that returns a list") + public List returnListTool() { + return List.of("this", "is", "a", "test"); + } + } public static class TestObject { @@ -507,4 +512,23 @@ public void testToolReturningComplexObject() throws Exception { assertThat(result.structuredContent()).containsEntry("value", 42); } + @Test + public void testToolReturningList() throws Exception { + TestToolProvider provider = new TestToolProvider(); + Method method = TestToolProvider.class.getMethod("returnListTool", null); + SyncMcpToolMethodCallback callback = new SyncMcpToolMethodCallback(ReturnMode.STRUCTURED, method, provider); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + CallToolRequest request = new CallToolRequest("return-list-tool", Map.of()); + + CallToolResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.isError()).isFalse(); + assertThat(result.content()).isNotEmpty(); + result.content().forEach(textContent -> { + assertThat(((TextContent) textContent).text()).containsAnyOf("this", "is", "a", "test"); + }); + } + }