diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java index f5fa170..05b34cf 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractMcpToolMethodCallback.java @@ -23,8 +23,8 @@ import java.util.Objects; import java.util.stream.Stream; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.*; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult.*; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; import org.springaicommunity.mcp.annotation.McpTool; @@ -144,36 +144,48 @@ protected Object buildTypedArgument(Object value, Type type) { * @return A CallToolResult representing the processed result */ protected CallToolResult convertValueToCallToolResult(Object result) { + Builder callToolResultBuilder = CallToolResult.builder(); + + // According to the MCP protocol For backwards compatibility, a tool that returns + // structured content SHOULD also return the serialized JSON in a TextContent + // block. + if (this.returnMode == ReturnMode.STRUCTURED) { + String jsonOutput = JsonParser.toJson(result); + Object structuredOutput = JsonParser.fromJson(jsonOutput, Object.class); + callToolResultBuilder.structuredContent(structuredOutput); + } + // Return the result if it's already a CallToolResult if (result instanceof CallToolResult) { return (CallToolResult) result; } + else if (result instanceof TextContent textContent) { + // Structured content is only supported in TextContent + return callToolResultBuilder.addContent(textContent).isError(false).meta(null).build(); + } + else if (result instanceof Content content) { + return CallToolResult.builder().addContent(content).isError(false).meta(null).build(); + } Type returnType = this.toolMethod.getGenericReturnType(); if (returnMode == ReturnMode.VOID || returnType == Void.TYPE || returnType == void.class) { - return CallToolResult.builder().addTextContent(JsonParser.toJson("Done")).build(); - } - - if (this.returnMode == ReturnMode.STRUCTURED) { - String jsonOutput = JsonParser.toJson(result); - Object structuredOutput = JsonParser.fromJson(jsonOutput, Object.class); - return CallToolResult.builder().structuredContent(structuredOutput).build(); + return callToolResultBuilder.addTextContent(JsonParser.toJson("Done")).build(); } // Default to text output if (result == null) { - return CallToolResult.builder().addTextContent("null").build(); + return callToolResultBuilder.addTextContent("null").build(); } // For string results in TEXT mode, return the string directly without JSON // serialization if (result instanceof String) { - return CallToolResult.builder().addTextContent((String) result).build(); + return callToolResultBuilder.addTextContent((String) result).build(); } // For other types, serialize to JSON - return CallToolResult.builder().addTextContent(JsonParser.toJson(result)).build(); + return callToolResultBuilder.addTextContent(JsonParser.toJson(result)).build(); } /** diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java index d08e32c..9c837d2 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java @@ -120,9 +120,9 @@ public List getToolSpecifications() { var tool = toolBuilder.build(); - boolean useStructuredOtput = tool.outputSchema() != null; + boolean useStructuredOutput = tool.outputSchema() != null; - ReturnMode returnMode = useStructuredOtput ? ReturnMode.STRUCTURED + ReturnMode returnMode = useStructuredOutput ? ReturnMode.STRUCTURED : (methodReturnType == Void.TYPE || methodReturnType == void.class ? ReturnMode.VOID : ReturnMode.TEXT); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncMcpToolMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncMcpToolMethodCallbackTests.java index 0f5d9cf..6920524 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncMcpToolMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncMcpToolMethodCallbackTests.java @@ -567,7 +567,6 @@ public void testMonoToolReturningComplexObject() throws Exception { StepVerifier.create(callback.apply(exchange, request)).assertNext(result -> { assertThat(result).isNotNull(); assertThat(result.isError()).isFalse(); - assertThat(result.content()).isEmpty(); assertThat(result.structuredContent()).isNotNull(); assertThat((Map) result.structuredContent()).containsEntry("name", "test"); assertThat((Map) result.structuredContent()).containsEntry("value", 42); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncStatelessMcpToolMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncStatelessMcpToolMethodCallbackTests.java index 42f0e60..37b4321 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncStatelessMcpToolMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/tool/AsyncStatelessMcpToolMethodCallbackTests.java @@ -603,7 +603,6 @@ public void testMonoToolReturningComplexObject() throws Exception { StepVerifier.create(callback.apply(context, request)).assertNext(result -> { assertThat(result).isNotNull(); assertThat(result.isError()).isFalse(); - assertThat(result.content()).isEmpty(); assertThat(result.structuredContent()).isNotNull(); assertThat((Map) result.structuredContent()).containsEntry("name", "test"); assertThat((Map) result.structuredContent()).containsEntry("value", 42); 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 1f84686..d8feec1 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 @@ -514,7 +514,6 @@ public void testToolReturningComplexObject() throws Exception { assertThat(result.isError()).isFalse(); // For complex return types (non-primitive, non-wrapper, non-CallToolResult), // the new implementation should return structured content - assertThat(result.content()).isEmpty(); assertThat(result.structuredContent()).isNotNull(); assertThat((Map) result.structuredContent()).containsEntry("name", "test"); assertThat((Map) result.structuredContent()).containsEntry("value", 42); 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 9f7120a..79e34ee 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 @@ -543,7 +543,6 @@ public void testToolReturningComplexObject() throws Exception { assertThat(result.isError()).isFalse(); // For complex return types (non-primitive, non-wrapper, non-CallToolResult), // the new implementation should return structured content - assertThat(result.content()).isEmpty(); assertThat(result.structuredContent()).isNotNull(); assertThat((Map) result.structuredContent()).containsEntry("name", "test"); assertThat((Map) result.structuredContent()).containsEntry("value", 42);