Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid wildcards

import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
import org.springaicommunity.mcp.annotation.McpTool;
Expand Down Expand Up @@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structured content is available only to JSON so you can do that :

return CallToolResult.builder().structuredContent(structuredOutput).addTextContent(jsonOutput).build();

}

// 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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ public List<SyncToolSpecification> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) result.structuredContent()).containsEntry("name", "test");
assertThat((Map<String, Object>) result.structuredContent()).containsEntry("value", 42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) result.structuredContent()).containsEntry("name", "test");
assertThat((Map<String, Object>) result.structuredContent()).containsEntry("value", 42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) result.structuredContent()).containsEntry("name", "test");
assertThat((Map<String, Object>) result.structuredContent()).containsEntry("value", 42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>) result.structuredContent()).containsEntry("name", "test");
assertThat((Map<String, Object>) result.structuredContent()).containsEntry("value", 42);
Expand Down