fix: support valid JSON value in tool structured content output (#551)

- Change CallToolResult.structuredContent type from Map<String,Object> to Object
  to support both objects and arrays
- Update JsonSchemaValidator to validate any Object type, not just Maps
- Add test cases for array-type structured output validation
- Fix type casting in existing tests to handle the more generic Object type
- Deprecate CallToolResult constructors in favor of builder pattern
- Update server implementations to use CallToolResult builder

This allows tools to return arrays as structured content, not just objects,
which is required by the MCP specification for tools with array-type output schemas.

It is realated to MPC spec fix: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/834

Fixes #550

BREAKING CHANGE!

Migration Guide:
----------------
1. If you're accessing CallToolResult.structuredContent():
   - Add explicit casting when you know it's a Map:
     Before: result.structuredContent().get("key")
     After:  ((Map<String,Object>) result.structuredContent()).get("key")

   - Or check the type first:
     if (result.structuredContent() instanceof Map) {
         Map<String,Object> map = (Map<String,Object>) result.structuredContent();
         // use map
     } else if (result.structuredContent() instanceof List) {
         List<?> list = (List<?>) result.structuredContent();
         // use list
     }

2. If you're creating CallToolResult instances:
   - Switch to using the builder pattern:
     Before: new CallToolResult(content, isError, structuredContent)
     After:  CallToolResult.builder()
               .content(content)
               .isError(isError)
               .structuredContent(structuredContent)
               .build()

3. If you're implementing JsonSchemaValidator:
   - Update your validate() method signature:
     Before: validate(Map<String,Object> schema, Map<String,Object> structuredContent)
     After:  validate(Map<String,Object> schema, Object structuredContent)

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-09-15 14:22:06 +02:00 • committed by GitHub
commit d8959ef854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 283 additions and 39 deletions

View file

@ -15,15 +15,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
import io.modelcontextprotocol.spec.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
@ -34,14 +28,17 @@ import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -420,8 +417,11 @@ public class McpAsyncServer {
// TextContent block.)
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return new CallToolResult(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())),
result.isError(), result.structuredContent());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
.isError(result.isError())
.structuredContent(result.structuredContent())
.build();
}
return result;

View file

@ -293,8 +293,11 @@ public class McpStatelessAsyncServer {
// TextContent block.)
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
return new CallToolResult(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())),
result.isError(), result.structuredContent());
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
.isError(result.isError())
.structuredContent(result.structuredContent())
.build();
}
return result;

View file

@ -51,7 +51,7 @@ public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
}
@Override
public ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent) {
public ValidationResponse validate(Map<String, Object> schema, Object structuredContent) {
Assert.notNull(schema, "Schema must not be null");
Assert.notNull(structuredContent, "Structured content must not be null");

View file

@ -40,6 +40,6 @@ public interface JsonSchemaValidator {
* @return A ValidationResponse indicating whether the validation was successful or
* not.
*/
ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent);
ValidationResponse validate(Map<String, Object> schema, Object structuredContent);
}

View file

@ -11,9 +11,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
@ -23,8 +20,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
@ -1508,15 +1506,21 @@ public final class McpSchema {
public record CallToolResult( // @formatter:off
@JsonProperty("content") List<Content> content,
@JsonProperty("isError") Boolean isError,
@JsonProperty("structuredContent") Map<String, Object> structuredContent,
@JsonProperty("structuredContent") Object structuredContent,
@JsonProperty("_meta") Map<String, Object> meta) implements Result { // @formatter:on
// backwards compatibility constructor
/**
* @deprecated use the builder instead.
*/
@Deprecated
public CallToolResult(List<Content> content, Boolean isError) {
this(content, isError, null, null);
this(content, isError, (Object) null, null);
}
// backwards compatibility constructor
/**
* @deprecated use the builder instead.
*/
@Deprecated
public CallToolResult(List<Content> content, Boolean isError, Map<String, Object> structuredContent) {
this(content, isError, structuredContent, null);
}
@ -1551,7 +1555,7 @@ public final class McpSchema {
private Boolean isError = false;
private Map<String, Object> structuredContent;
private Object structuredContent;
private Map<String, Object> meta;
@ -1566,7 +1570,7 @@ public final class McpSchema {
return this;
}
public Builder structuredContent(Map<String, Object> structuredContent) {
public Builder structuredContent(Object structuredContent) {
Assert.notNull(structuredContent, "structuredContent must not be null");
this.structuredContent = structuredContent;
return this;
@ -1644,7 +1648,7 @@ public final class McpSchema {
* @return a new CallToolResult instance
*/
public CallToolResult build() {
return new CallToolResult(content, isError, structuredContent, meta);
return new CallToolResult(content, isError, (Object) structuredContent, meta);
}
}