feat: add structured output support for MCP tools (#357)

- Add JsonSchemaValidator interface and DefaultJsonSchemaValidator implementation
 - put the JsonSchemaValidator classes into a dedicated spec package
 - add schema caching to DefaultJsonSchemaValidator for better performance
- Extend Tool schema to support outputSchema field for defining expected output structure
- Add structuredContent field to CallToolResult for validated structured responses
- Implement automatic validation of tool outputs against their defined schemas
- Add comprehensive test coverage for structured output validation scenarios
- Add json-schema-validator and json-unit-assertj dependencies for validation and testing
- Update McpServer builders to accept custom JsonSchemaValidator instances
- Ensure backward compatibility with existing tools without output schemas

This implements the MCP specification requirement that tools with output schemas
must provide structured results conforming to those schemas, with automatic
validation and error handling for non-conforming outputs.
https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-06-30 14:08:50 +02:00
commit 8a2f97f1b1
5 changed files with 2291 additions and 140 deletions

View file

@ -15,8 +15,13 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
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.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
@ -28,11 +33,10 @@ import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
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;
@ -86,6 +90,8 @@ public class McpAsyncServer {
private final ObjectMapper objectMapper;
private final JsonSchemaValidator jsonSchemaValidator;
private final McpSchema.ServerCapabilities serverCapabilities;
private final McpSchema.Implementation serverInfo;
@ -119,18 +125,19 @@ public class McpAsyncServer {
*/
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory) {
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
this.mcpTransportProvider = mcpTransportProvider;
this.objectMapper = objectMapper;
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities();
this.instructions = features.instructions();
this.tools.addAll(features.tools());
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
@ -286,15 +293,17 @@ public class McpAsyncServer {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}
var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);
return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {
return Mono
.error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(wrappedToolSpecification.tool().name()))) {
return Mono.error(
new McpError("Tool with name '" + wrappedToolSpecification.tool().name() + "' already exists"));
}
this.tools.add(toolSpecification);
logger.debug("Added tool handler: {}", toolSpecification.tool().name());
this.tools.add(wrappedToolSpecification);
logger.debug("Added tool handler: {}", wrappedToolSpecification.tool().name());
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
@ -303,6 +312,107 @@ public class McpAsyncServer {
});
}
private static class StructuredOutputCallToolHandler
implements BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<McpSchema.CallToolResult>> {
private final BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<McpSchema.CallToolResult>> delegateCallToolResult;
private final JsonSchemaValidator jsonSchemaValidator;
private final Map<String, Object> outputSchema;
public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator,
Map<String, Object> outputSchema,
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<McpSchema.CallToolResult>> delegateHandler) {
Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
Assert.notNull(delegateHandler, "Delegate call tool result handler must not be null");
this.delegateCallToolResult = delegateHandler;
this.outputSchema = outputSchema;
this.jsonSchemaValidator = jsonSchemaValidator;
}
@Override
public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, Map<String, Object> arguments) {
return this.delegateCallToolResult.apply(exchange, arguments).map(result -> {
if (outputSchema == null) {
if (result.structuredContent() != null) {
logger.warn(
"Tool call with no outputSchema is not expected to have a result with structured content, but got: {}",
result.structuredContent());
}
// Pass through. No validation is required if no output schema is
// provided.
return result;
}
// If an output schema is provided, servers MUST provide structured
// results that conform to this schema.
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
if (result.structuredContent() == null) {
logger.warn(
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
return new CallToolResult(
"Response missing structured content which is expected when calling tool with non-empty outputSchema",
true);
}
// Validate the result against the output schema
var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
if (!validation.valid()) {
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
return new CallToolResult(validation.errorMessage(), true);
}
if (Utils.isEmpty(result.content())) {
// For backwards compatibility, a tool that returns structured
// content SHOULD also return functionally equivalent unstructured
// content. (For example, serialized JSON can be returned in a
// 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 result;
});
}
}
private static List<McpServerFeatures.AsyncToolSpecification> withStructuredOutputHandling(
JsonSchemaValidator jsonSchemaValidator, List<McpServerFeatures.AsyncToolSpecification> tools) {
if (Utils.isEmpty(tools)) {
return tools;
}
return tools.stream().map(tool -> withStructuredOutputHandling(jsonSchemaValidator, tool)).toList();
}
private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHandling(
JsonSchemaValidator jsonSchemaValidator, McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification.call() instanceof StructuredOutputCallToolHandler) {
// If the tool is already wrapped, return it as is
return toolSpecification;
}
if (toolSpecification.tool().outputSchema() == null) {
// If the tool does not have an output schema, return it as is
return toolSpecification;
}
return new McpServerFeatures.AsyncToolSpecification(toolSpecification.tool(),
new StructuredOutputCallToolHandler(jsonSchemaValidator, toolSpecification.tool().outputSchema(),
toolSpecification.call()));
}
/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove

View file

@ -14,6 +14,9 @@ import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.DefaultJsonSchemaValidator;
import io.modelcontextprotocol.spec.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
@ -170,6 +173,8 @@ public interface McpServer {
private McpSchema.ServerCapabilities serverCapabilities;
private JsonSchemaValidator jsonSchemaValidator;
private String instructions;
/**
@ -670,6 +675,20 @@ public interface McpServer {
return this;
}
/**
* Sets the JSON schema validator to use for validating tool and resource schemas.
* This ensures that the server's tools and resources conform to the expected
* schema definitions.
* @param jsonSchemaValidator The validator to use. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if jsonSchemaValidator is null
*/
public AsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonSchemaValidator) {
Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
this.jsonSchemaValidator = jsonSchemaValidator;
return this;
}
/**
* Builds an asynchronous MCP server that provides non-blocking operations.
* @return A new instance of {@link McpAsyncServer} configured with this builder's
@ -680,8 +699,10 @@ public interface McpServer {
this.resources, this.resourceTemplates, this.prompts, this.completions, this.rootsChangeHandlers,
this.instructions);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: new DefaultJsonSchemaValidator(mapper);
return new McpAsyncServer(this.transportProvider, mapper, features, this.requestTimeout,
this.uriTemplateManagerFactory);
this.uriTemplateManagerFactory, jsonSchemaValidator);
}
}
@ -726,6 +747,8 @@ public interface McpServer {
private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
private JsonSchemaValidator jsonSchemaValidator;
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose prompt templates to clients. Prompts allow servers to provide structured
@ -1206,6 +1229,12 @@ public interface McpServer {
return this;
}
public SyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonSchemaValidator) {
Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
this.jsonSchemaValidator = jsonSchemaValidator;
return this;
}
/**
* Enable on "immediate execution" of the operations on the underlying
* {@link McpAsyncServer}. Defaults to false, which does blocking code offloading
@ -1234,8 +1263,11 @@ public interface McpServer {
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
this.immediateExecution);
var mapper = this.objectMapper != null ? this.objectMapper : new ObjectMapper();
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: new DefaultJsonSchemaValidator(mapper);
var asyncServer = new McpAsyncServer(this.transportProvider, mapper, asyncFeatures, this.requestTimeout,
this.uriTemplateManagerFactory);
this.uriTemplateManagerFactory, jsonSchemaValidator);
return new McpSyncServer(asyncServer, this.immediateExecution);
}

View file

@ -0,0 +1,169 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.spec;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import io.modelcontextprotocol.util.Assert;
/**
* Default implementation of the {@link JsonSchemaValidator} interface. This class
* provides methods to validate structured content against a JSON schema. It uses the
* NetworkNT JSON Schema Validator library for validation.
*
* @author Christian Tzolov
*/
public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
private static final Logger logger = LoggerFactory.getLogger(DefaultJsonSchemaValidator.class);
private final ObjectMapper objectMapper;
private final JsonSchemaFactory schemaFactory;
// TODO: Implement a strategy to purge the cache (TTL, size limit, etc.)
private final ConcurrentHashMap<String, JsonSchema> schemaCache;
public DefaultJsonSchemaValidator() {
this(new ObjectMapper());
}
public DefaultJsonSchemaValidator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
this.schemaCache = new ConcurrentHashMap<>();
}
@Override
public ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent) {
Assert.notNull(schema, "Schema must not be null");
Assert.notNull(structuredContent, "Structured content must not be null");
try {
JsonNode jsonStructuredOutput = this.objectMapper.valueToTree(structuredContent);
Set<ValidationMessage> validationResult = this.getOrCreateJsonSchema(schema).validate(jsonStructuredOutput);
// Check if validation passed
if (!validationResult.isEmpty()) {
return ValidationResponse
.asInvalid("Validation failed: structuredContent does not match tool outputSchema. "
+ "Validation errors: " + validationResult);
}
return ValidationResponse.asValid(jsonStructuredOutput.toString());
}
catch (JsonProcessingException e) {
logger.error("Failed to validate CallToolResult: Error parsing schema: {}", e);
return ValidationResponse.asInvalid("Error parsing tool JSON Schema: " + e.getMessage());
}
catch (Exception e) {
logger.error("Failed to validate CallToolResult: Unexpected error: {}", e);
return ValidationResponse.asInvalid("Unexpected validation error: " + e.getMessage());
}
}
/**
* Gets a cached JsonSchema or creates and caches a new one.
* @param schema the schema map to convert
* @return the compiled JsonSchema
* @throws JsonProcessingException if schema processing fails
*/
private JsonSchema getOrCreateJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
// Generate cache key based on schema content
String cacheKey = this.generateCacheKey(schema);
// Try to get from cache first
JsonSchema cachedSchema = this.schemaCache.get(cacheKey);
if (cachedSchema != null) {
return cachedSchema;
}
// Create new schema if not in cache
JsonSchema newSchema = this.createJsonSchema(schema);
// Cache the schema
JsonSchema existingSchema = this.schemaCache.putIfAbsent(cacheKey, newSchema);
return existingSchema != null ? existingSchema : newSchema;
}
/**
* Creates a new JsonSchema from the given schema map.
* @param schema the schema map
* @return the compiled JsonSchema
* @throws JsonProcessingException if schema processing fails
*/
private JsonSchema createJsonSchema(Map<String, Object> schema) throws JsonProcessingException {
// Convert schema map directly to JsonNode (more efficient than string
// serialization)
JsonNode schemaNode = this.objectMapper.valueToTree(schema);
// Handle case where ObjectMapper might return null (e.g., in mocked scenarios)
if (schemaNode == null) {
throw new JsonProcessingException("Failed to convert schema to JsonNode") {
};
}
// Handle additionalProperties setting
if (schemaNode.isObject()) {
ObjectNode objectSchemaNode = (ObjectNode) schemaNode;
if (!objectSchemaNode.has("additionalProperties")) {
// Clone the node before modification to avoid mutating the original
objectSchemaNode = objectSchemaNode.deepCopy();
objectSchemaNode.put("additionalProperties", false);
schemaNode = objectSchemaNode;
}
}
return this.schemaFactory.getSchema(schemaNode);
}
/**
* Generates a cache key for the given schema map.
* @param schema the schema map
* @return a cache key string
*/
protected String generateCacheKey(Map<String, Object> schema) {
if (schema.containsKey("$id")) {
// Use the (optional) "$id" field as the cache key if present
return "" + schema.get("$id");
}
// Fall back to schema's hash code as a simple cache key
// For more sophisticated caching, could use content-based hashing
return String.valueOf(schema.hashCode());
}
/**
* Clears the schema cache. Useful for testing or memory management.
*/
public void clearCache() {
this.schemaCache.clear();
}
/**
* Returns the current size of the schema cache.
* @return the number of cached schemas
*/
public int getCacheSize() {
return this.schemaCache.size();
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.spec;
import java.util.Map;
/**
* Interface for validating structured content against a JSON schema. This interface
* defines a method to validate structured content based on the provided output schema.
*
* @author Christian Tzolov
*/
public interface JsonSchemaValidator {
/**
* Represents the result of a validation operation.
*
* @param valid Indicates whether the validation was successful.
* @param errorMessage An error message if the validation failed, otherwise null.
* @param jsonStructuredOutput The text structured content in JSON format if the
* validation was successful, otherwise null.
*/
public record ValidationResponse(boolean valid, String errorMessage, String jsonStructuredOutput) {
public static ValidationResponse asValid(String jsonStructuredOutput) {
return new ValidationResponse(true, null, jsonStructuredOutput);
}
public static ValidationResponse asInvalid(String message) {
return new ValidationResponse(false, message, null);
}
}
/**
* Validates the structured content against the provided JSON schema.
* @param schema The JSON schema to validate against.
* @param structuredContent The structured content to validate.
* @return A ValidationResponse indicating whether the validation was successful or
* not.
*/
ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent);
}

View file

@ -20,6 +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.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -934,16 +937,26 @@ public final class McpSchema {
@JsonProperty("definitions") Map<String, Object> definitions) {
} // @formatter:on
/**
* Additional properties describing a Tool to clients.
*
* NOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to
* provide a faithful description of tool behavior (including descriptive properties
* like `title`).
*
* Clients should never make tool use decisions based on ToolAnnotations received from
* untrusted servers.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ToolAnnotations( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("readOnlyHint") Boolean readOnlyHint,
@JsonProperty("destructiveHint") Boolean destructiveHint,
@JsonProperty("idempotentHint") Boolean idempotentHint,
@JsonProperty("openWorldHint") Boolean openWorldHint,
@JsonProperty("returnDirect") Boolean returnDirect) {
} // @formatter:on
@JsonProperty("title") String title,
@JsonProperty("readOnlyHint") Boolean readOnlyHint,
@JsonProperty("destructiveHint") Boolean destructiveHint,
@JsonProperty("idempotentHint") Boolean idempotentHint,
@JsonProperty("openWorldHint") Boolean openWorldHint,
@JsonProperty("returnDirect") Boolean returnDirect) {
} // @formatter:on
/**
* Represents a tool that the server provides. Tools enable servers to expose
@ -957,17 +970,19 @@ public final class McpSchema {
* used by clients to improve the LLM's understanding of available tools.
* @param inputSchema A JSON Schema object that describes the expected structure of
* the arguments when calling this tool. This allows clients to validate tool
* @param annotations Additional properties describing a Tool to clients. arguments
* before sending them to the server.
* @param outputSchema An optional JSON Schema object defining the structure of the
* tool's output returned in the structuredContent field of a CallToolResult.
* @param annotations Optional additional tool information.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record Tool( // @formatter:off
@JsonProperty("name") String name,
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("inputSchema") JsonSchema inputSchema,
@JsonProperty("annotations") ToolAnnotations annotations) implements BaseMetadata { // @formatter:on
@JsonProperty("name") String name,
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("inputSchema") JsonSchema inputSchema,
@JsonProperty("outputSchema") Map<String, Object> outputSchema,
@JsonProperty("annotations") ToolAnnotations annotations) {// @formatter:on
/**
* @deprecated Only exists for backwards-compatibility purposes. Use
@ -975,15 +990,35 @@ public final class McpSchema {
*/
@Deprecated
public Tool(String name, String description, JsonSchema inputSchema, ToolAnnotations annotations) {
this(name, null, description, inputSchema, annotations);
this(name, null, description, inputSchema, null, annotations);
}
public Tool(String name, String description, String schema) {
this(name, null, description, parseSchema(schema), null);
/**
* @deprecated Only exists for backwards-compatibility purposes. Use
* {@link Tool#builder()} instead.
*/
@Deprecated
public Tool(String name, String description, String inputSchema) {
this(name, null, description, parseSchema(inputSchema), null, null);
}
/**
* @deprecated Only exists for backwards-compatibility purposes. Use
* {@link Tool#builder()} instead.
*/
@Deprecated
public Tool(String name, String description, String schema, ToolAnnotations annotations) {
this(name, null, description, parseSchema(schema), annotations);
this(name, null, description, parseSchema(schema), null, annotations);
}
/**
* @deprecated Only exists for backwards-compatibility purposes. Use
* {@link Tool#builder()} instead.
*/
@Deprecated
public Tool(String name, String description, String inputSchema, String outputSchema,
ToolAnnotations annotations) {
this(name, null, description, parseSchema(inputSchema), schemaToMap(outputSchema), annotations);
}
public static Builder builder() {
@ -1000,6 +1035,8 @@ public final class McpSchema {
private JsonSchema inputSchema;
private Map<String, Object> outputSchema;
private ToolAnnotations annotations;
public Builder name(String name) {
@ -1022,6 +1059,21 @@ public final class McpSchema {
return this;
}
public Builder inputSchema(String inputSchema) {
this.inputSchema = parseSchema(inputSchema);
return this;
}
public Builder outputSchema(Map<String, Object> outputSchema) {
this.outputSchema = outputSchema;
return this;
}
public Builder outputSchema(String outputSchema) {
this.outputSchema = schemaToMap(outputSchema);
return this;
}
public Builder annotations(ToolAnnotations annotations) {
this.annotations = annotations;
return this;
@ -1029,12 +1081,19 @@ public final class McpSchema {
public Tool build() {
Assert.hasText(name, "name must not be empty");
return new Tool(name, title, description, inputSchema, annotations);
return new Tool(name, title, description, inputSchema, outputSchema, annotations);
}
}
}
private static Map<String, Object> schemaToMap(String schema) {
try {
return OBJECT_MAPPER.readValue(schema, MAP_TYPE_REF);
}
catch (IOException e) {
throw new IllegalArgumentException("Invalid schema: " + schema, e);
}
}
private static JsonSchema parseSchema(String schema) {
@ -1059,7 +1118,7 @@ public final class McpSchema {
public record CallToolRequest(// @formatter:off
@JsonProperty("name") String name,
@JsonProperty("arguments") Map<String, Object> arguments,
@JsonProperty("_meta") Map<String, Object> meta) implements Request {
@JsonProperty("_meta") Map<String, Object> meta) implements Request {// @formatter:off
public CallToolRequest(String name, String jsonArguments) {
this(name, parseJsonArguments(jsonArguments), null);
@ -1119,7 +1178,7 @@ public final class McpSchema {
return new CallToolRequest(name, arguments, meta);
}
}
}// @formatter:off
}
/**
* The server's response to a tools/call request from the client.
@ -1128,110 +1187,137 @@ public final class McpSchema {
* or an embedded resource.
* @param isError If true, indicates that the tool execution failed and the content contains error information.
* If false or absent, indicates successful execution.
* @param structuredContent An optional JSON object that represents the structured result of the tool call.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record CallToolResult( // @formatter:off
@JsonProperty("content") List<Content> content,
@JsonProperty("isError") Boolean isError) {
@JsonProperty("isError") Boolean isError,
@JsonProperty("structuredContent") Map<String, Object> structuredContent) {// @formatter:on
/**
* Creates a new instance of {@link CallToolResult} with a string containing the
* tool result.
*
* @param content The content of the tool result. This will be mapped to a one-sized list
* with a {@link TextContent} element.
* @param isError If true, indicates that the tool execution failed and the content contains error information.
* If false or absent, indicates successful execution.
*/
public CallToolResult(String content, Boolean isError) {
this(List.of(new TextContent(content)), isError);
// backwards compatibility constructor
public CallToolResult(List<Content> content, Boolean isError) {
this(content, isError, null);
}
/**
* Creates a new instance of {@link CallToolResult} with a string containing the
* tool result.
* @param content The content of the tool result. This will be mapped to a
* one-sized list with a {@link TextContent} element.
* @param isError If true, indicates that the tool execution failed and the
* content contains error information. If false or absent, indicates successful
* execution.
*/
public CallToolResult(String content, Boolean isError) {
this(List.of(new TextContent(content)), isError);
}
/**
* Creates a builder for {@link CallToolResult}.
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for {@link CallToolResult}.
*/
public static class Builder {
private List<Content> content = new ArrayList<>();
private Boolean isError = false;
private Map<String, Object> structuredContent;
/**
* Sets the content list for the tool result.
* @param content the content list
* @return this builder
*/
public Builder content(List<Content> content) {
Assert.notNull(content, "content must not be null");
this.content = content;
return this;
}
public Builder structuredContent(Map<String, Object> structuredContent) {
Assert.notNull(structuredContent, "structuredContent must not be null");
this.structuredContent = structuredContent;
return this;
}
public Builder structuredContent(String structuredContent) {
Assert.hasText(structuredContent, "structuredContent must not be empty");
try {
this.structuredContent = OBJECT_MAPPER.readValue(structuredContent, MAP_TYPE_REF);
}
/**
* Creates a builder for {@link CallToolResult}.
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
catch (IOException e) {
throw new IllegalArgumentException("Invalid structured content: " + structuredContent, e);
}
return this;
}
/**
* Builder for {@link CallToolResult}.
*/
public static class Builder {
private List<Content> content = new ArrayList<>();
private Boolean isError;
/**
* Sets the text content for the tool result.
* @param textContent the text content
* @return this builder
*/
public Builder textContent(List<String> textContent) {
Assert.notNull(textContent, "textContent must not be null");
textContent.stream().map(TextContent::new).forEach(this.content::add);
return this;
}
/**
* Sets the content list for the tool result.
* @param content the content list
* @return this builder
*/
public Builder content(List<Content> content) {
Assert.notNull(content, "content must not be null");
this.content = content;
return this;
}
/**
* Sets the text content for the tool result.
* @param textContent the text content
* @return this builder
*/
public Builder textContent(List<String> textContent) {
Assert.notNull(textContent, "textContent must not be null");
textContent.stream()
.map(TextContent::new)
.forEach(this.content::add);
return this;
}
/**
* Adds a content item to the tool result.
* @param contentItem the content item to add
* @return this builder
*/
public Builder addContent(Content contentItem) {
Assert.notNull(contentItem, "contentItem must not be null");
if (this.content == null) {
this.content = new ArrayList<>();
}
this.content.add(contentItem);
return this;
}
/**
* Adds a text content item to the tool result.
* @param text the text content
* @return this builder
*/
public Builder addTextContent(String text) {
Assert.notNull(text, "text must not be null");
return addContent(new TextContent(text));
}
/**
* Sets whether the tool execution resulted in an error.
* @param isError true if the tool execution failed, false otherwise
* @return this builder
*/
public Builder isError(Boolean isError) {
Assert.notNull(isError, "isError must not be null");
this.isError = isError;
return this;
}
/**
* Builds a new {@link CallToolResult} instance.
* @return a new CallToolResult instance
*/
public CallToolResult build() {
return new CallToolResult(content, isError);
}
/**
* Adds a content item to the tool result.
* @param contentItem the content item to add
* @return this builder
*/
public Builder addContent(Content contentItem) {
Assert.notNull(contentItem, "contentItem must not be null");
if (this.content == null) {
this.content = new ArrayList<>();
}
this.content.add(contentItem);
return this;
}
} // @formatter:on
/**
* Adds a text content item to the tool result.
* @param text the text content
* @return this builder
*/
public Builder addTextContent(String text) {
Assert.notNull(text, "text must not be null");
return addContent(new TextContent(text));
}
/**
* Sets whether the tool execution resulted in an error.
* @param isError true if the tool execution failed, false otherwise
* @return this builder
*/
public Builder isError(Boolean isError) {
Assert.notNull(isError, "isError must not be null");
this.isError = isError;
return this;
}
/**
* Builds a new {@link CallToolResult} instance.
* @return a new CallToolResult instance
*/
public CallToolResult build() {
return new CallToolResult(content, isError, structuredContent);
}
}
}
// ---------------------------
// Sampling Interfaces
@ -1286,7 +1372,7 @@ public final class McpSchema {
return new ModelPreferences(hints, costPriority, speedPriority, intelligencePriority);
}
}
} // @formatter:on
} // @formatter:on
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
@ -1482,7 +1568,7 @@ public final class McpSchema {
/**
* Used by the server to send an elicitation to the client.
*
* @param message The body of the elicitation message.
* @param errorMessage The body of the elicitation message.
* @param requestedSchema The elicitation response schema that must be satisfied.
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)