From 5dbad1158ff8a264e368643d3c559afec3d579a9 Mon Sep 17 00:00:00 2001 From: TheEterna <125226601+TheEterna@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:27:02 +0800 Subject: [PATCH 1/3] refactor: fix the return format of CallToolResult - corrected the return format for structured content results, following the MCP protocol. - fix variable name --- .../tool/AbstractMcpToolMethodCallback.java | 36 ++++++++++++------- .../provider/tool/SyncMcpToolProvider.java | 4 +-- 2 files changed, 26 insertions(+), 14 deletions(-) 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); From 702a47fab6654f462c924cb9e79bdf59ad3d9704 Mon Sep 17 00:00:00 2001 From: TheEterna <125226601+TheEterna@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:54:18 +0800 Subject: [PATCH 2/3] test: "Modify the test cases according to the MCP protocol --- .../mcp/method/tool/AsyncMcpToolMethodCallbackTests.java | 1 - .../method/tool/AsyncStatelessMcpToolMethodCallbackTests.java | 1 - .../mcp/method/tool/SyncMcpToolMethodCallbackTests.java | 1 - .../mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java | 1 - 4 files changed, 4 deletions(-) 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); From fb4022eda5e4139316e20cc9230d3bf1f2dbeed0 Mon Sep 17 00:00:00 2001 From: TheEterna <125226601+TheEterna@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:54:18 +0800 Subject: [PATCH 3/3] test: "Modify the test cases according to the MCP protocol fix #67 --- .../mcp/method/tool/AsyncMcpToolMethodCallbackTests.java | 1 - .../method/tool/AsyncStatelessMcpToolMethodCallbackTests.java | 1 - .../mcp/method/tool/SyncMcpToolMethodCallbackTests.java | 1 - .../mcp/method/tool/SyncStatelessMcpToolMethodCallbackTests.java | 1 - 4 files changed, 4 deletions(-) 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);