Add schemas for form-based elicitation (#1020)

* Add StringSchema, BooleanSchema and NumberSchema
* Add enum schemas for form-based elicitation matching SEP-1330

Closes #691

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-06-11 11:16:47 +02:00 • committed by GitHub
commit fe82ad508a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1531 additions and 31 deletions

View file

@ -5,11 +5,12 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.AudioContent;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@ -43,6 +44,16 @@ import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.modelcontextprotocol.spec.McpSchema.EnumSchemaOption;
import static io.modelcontextprotocol.spec.McpSchema.JSON_SCHEMA_DIALECT_2020_12;
import static io.modelcontextprotocol.spec.McpSchema.LegacyTitledEnumSchema;
import static io.modelcontextprotocol.spec.McpSchema.TitledMultiSelectEnumSchema;
import static io.modelcontextprotocol.spec.McpSchema.TitledMultiSelectItems;
import static io.modelcontextprotocol.spec.McpSchema.TitledSingleSelectEnumSchema;
import static io.modelcontextprotocol.spec.McpSchema.UntitledMultiSelectEnumSchema;
import static io.modelcontextprotocol.spec.McpSchema.UntitledMultiSelectItems;
import static io.modelcontextprotocol.spec.McpSchema.UntitledSingleSelectEnumSchema;
public class ConformanceServlet {
private static final Logger logger = LoggerFactory.getLogger(ConformanceServlet.class);
@ -141,6 +152,7 @@ public class ConformanceServlet {
return tomcat;
}
@SuppressWarnings("deprecation")
private static List<McpServerFeatures.SyncToolSpecification> createToolSpecs() {
return List.of(
// test_simple_text - Returns simple text content
@ -406,8 +418,8 @@ public class ConformanceServlet {
// json_schema_2020_12_tool - SEP-1613 dialect/keyword preservation
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool
.builder("json_schema_2020_12_tool", Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12,
"type", "object", "$defs",
.builder("json_schema_2020_12_tool", Map.of("$schema", JSON_SCHEMA_DIALECT_2020_12, "type",
"object", "$defs",
Map.of("address",
Map.of("type", "object", "properties",
Map.of("street", Map.of("type", "string"), "city",
@ -434,33 +446,44 @@ public class ConformanceServlet {
.callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation_sep1330_enums' called");
// Create schema with all 5 enum variants
Map<String, Object> requestedSchema = Map.of("type", "object", "properties", Map.of(
// 1. Untitled single-select
"untitledSingle",
Map.of("type", "string", "enum", List.of("option1", "option2", "option3")),
// 2. Titled single-select using oneOf with const/title
"titledSingle",
Map.of("type", "string", "oneOf",
List.of(Map.of("const", "value1", "title", "First Option"),
Map.of("const", "value2", "title", "Second Option"),
Map.of("const", "value3", "title", "Third Option"))),
// 3. Legacy titled using enumNames (deprecated)
"legacyEnum",
Map.of("type", "string", "enum", List.of("opt1", "opt2", "opt3"), "enumNames",
List.of("Option One", "Option Two", "Option Three")),
// 4. Untitled multi-select
"untitledMulti",
Map.of("type", "array", "items",
Map.of("type", "string", "enum", List.of("option1", "option2", "option3"))),
// 5. Titled multi-select using items.anyOf with
// const/title
"titledMulti",
Map.of("type", "array", "items",
Map.of("anyOf",
List.of(Map.of("const", "value1", "title", "First Choice"),
Map.of("const", "value2", "title", "Second Choice"),
Map.of("const", "value3", "title", "Third Choice"))))),
TypeRef<Map<String, Object>> mapType = new TypeRef<>() {
};
var mapper = McpJsonDefaults.getMapper();
// 1. Untitled single-select
var untitledSingle = UntitledSingleSelectEnumSchema.builder()
.enumValues("option1", "option2", "option3")
.build();
// 2. Titled single-select using oneOf with const/title
var titledSingle = TitledSingleSelectEnumSchema.builder()
.oneOf(new EnumSchemaOption("value1", "First Option"),
new EnumSchemaOption("value2", "Second Option"),
new EnumSchemaOption("value3", "Third Option"))
.build();
// 3. Legacy titled using enumNames (deprecated)
var legacyEnum = LegacyTitledEnumSchema.builder()
.enumValues("opt1", "opt2", "opt3")
.enumNames("Option One", "Option Two", "Option Three")
.build();
// 4. Untitled multi-select
var untitledMulti = UntitledMultiSelectEnumSchema.builder(
UntitledMultiSelectItems.builder().enumValues("option1", "option2", "option3").build())
.build();
// 5. Titled multi-select using items.anyOf with const/title
var titledMulti = TitledMultiSelectEnumSchema
.builder(TitledMultiSelectItems.builder()
.anyOf(new EnumSchemaOption("value1", "First Choice"),
new EnumSchemaOption("value2", "Second Choice"),
new EnumSchemaOption("value3", "Third Choice"))
.build())
.build();
Map<String, Object> requestedSchema = Map.of("type", "object", "properties",
Map.of("untitledSingle", mapper.convertValue(untitledSingle, mapType), "titledSingle",
mapper.convertValue(titledSingle, mapType), "legacyEnum",
mapper.convertValue(legacyEnum, mapType), "untitledMulti",
mapper.convertValue(untitledMulti, mapType), "titledMulti",
mapper.convertValue(titledMulti, mapType)),
"required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti",
"titledMulti"));

View file

@ -6,6 +6,7 @@ package io.modelcontextprotocol.spec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -3888,6 +3889,817 @@ public final class McpSchema {
}
// Elicitation
/**
* An option in a titled enum schema, with a machine-readable value and a
* human-readable display label.
*
* @param constValue The machine-readable value of the option
* @param title The human-readable display label
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record EnumSchemaOption( // @formatter:off
@JsonProperty("const") String constValue,
@JsonProperty("title") String title) { // @formatter:on
public EnumSchemaOption {
Assert.notNull(constValue, "constValue must not be null");
Assert.notNull(title, "title must not be null");
}
@JsonCreator
static EnumSchemaOption fromJson(@JsonProperty("const") String constValue,
@JsonProperty("title") String title) {
if (constValue == null || title == null) {
List<String> missing = new ArrayList<>();
if (constValue == null) {
missing.add("constValue -> ''");
constValue = "";
}
if (title == null) {
missing.add("title -> ''");
title = "";
}
logger.warn("EnumSchemaOption: missing required fields during deserialization: {}",
String.join(", ", missing));
}
return new EnumSchemaOption(constValue, title);
}
}
/**
* Legacy enum schema with optional display names via the non-standard
* {@code enumNames} property. Use {@link TitledSingleSelectEnumSchema} instead.
*
* @param title Optional title for the enum field
* @param description Optional description for the enum field
* @param enumValues Array of enum values to choose from
* @param enumNames Optional display names for enum values (non-standard per JSON
* Schema 2020-12)
* @param defaultValue Optional default value
* @deprecated Use {@link TitledSingleSelectEnumSchema} instead
*/
@Deprecated
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record LegacyTitledEnumSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("enum") List<String> enumValues,
@JsonProperty("enumNames") List<String> enumNames,
@JsonProperty("default") String defaultValue) { // @formatter:on
public LegacyTitledEnumSchema {
Assert.notNull(enumValues, "enumValues must not be null");
}
@JsonProperty("type")
public String type() {
return "string";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private List<String> enumValues;
private List<String> enumNames;
private String defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder enumValues(List<String> enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = new ArrayList<>(enumValues);
return this;
}
public Builder enumValues(String... enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = Arrays.asList(enumValues);
return this;
}
public Builder enumNames(List<String> enumNames) {
Assert.notNull(enumNames, "enumNames must not be null");
this.enumNames = new ArrayList<>(enumNames);
return this;
}
public Builder enumNames(String... enumNames) {
Assert.notNull(enumNames, "enumNames must not be null");
this.enumNames = Arrays.asList(enumNames);
return this;
}
public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public LegacyTitledEnumSchema build() {
Assert.notEmpty(enumValues, "enumValues must not be empty");
return new LegacyTitledEnumSchema(title, description, enumValues, enumNames, defaultValue);
}
}
}
/**
* Schema for single-selection enumeration without display titles for options.
*
* @param title Optional title for the enum field
* @param description Optional description for the enum field
* @param enumValues Array of enum values to choose from
* @param defaultValue Optional default value
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record UntitledSingleSelectEnumSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("enum") List<String> enumValues,
@JsonProperty("default") String defaultValue) { // @formatter:on
public UntitledSingleSelectEnumSchema {
Assert.notNull(enumValues, "enumValues must not be null");
}
@JsonProperty("type")
public String type() {
return "string";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private List<String> enumValues;
private String defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder enumValues(List<String> enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = new ArrayList<>(enumValues);
return this;
}
public Builder enumValues(String... enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = Arrays.asList(enumValues);
return this;
}
public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public UntitledSingleSelectEnumSchema build() {
Assert.notEmpty(enumValues, "enumValues must not be empty");
return new UntitledSingleSelectEnumSchema(title, description, enumValues, defaultValue);
}
}
}
/**
* Schema for single-selection enumeration with display titles for each option.
*
* @param title Optional title for the enum field
* @param description Optional description for the enum field
* @param oneOf Array of enum options, each with a machine-readable value and a
* human-readable display label
* @param defaultValue Optional default value
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record TitledSingleSelectEnumSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("oneOf") List<EnumSchemaOption> oneOf,
@JsonProperty("default") String defaultValue) { // @formatter:on
public TitledSingleSelectEnumSchema {
Assert.notEmpty(oneOf, "oneOf must not be empty");
}
@JsonProperty("type")
public String type() {
return "string";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private List<EnumSchemaOption> oneOf;
private String defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder oneOf(List<EnumSchemaOption> oneOf) {
Assert.notNull(oneOf, "oneOf must not be null");
this.oneOf = new ArrayList<>(oneOf);
return this;
}
public Builder oneOf(EnumSchemaOption... oneOf) {
Assert.notNull(oneOf, "oneOf must not be null");
this.oneOf = Arrays.asList(oneOf);
return this;
}
public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public TitledSingleSelectEnumSchema build() {
Assert.notEmpty(oneOf, "oneOf must not be empty");
return new TitledSingleSelectEnumSchema(title, description, oneOf, defaultValue);
}
}
}
/**
* The items schema for {@link UntitledMultiSelectEnumSchema}, describing the allowed
* enum values.
*
* @param enumValues Array of enum values to choose from
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record UntitledMultiSelectItems( // @formatter:off
@JsonProperty("enum") List<String> enumValues) { // @formatter:on
public UntitledMultiSelectItems {
Assert.notNull(enumValues, "enumValues must not be null");
}
@JsonProperty("type")
public String type() {
return "string";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private List<String> enumValues;
private Builder() {
}
public Builder enumValues(List<String> enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = new ArrayList<>(enumValues);
return this;
}
public Builder enumValues(String... enumValues) {
Assert.notNull(enumValues, "enumValues must not be null");
this.enumValues = Arrays.asList(enumValues);
return this;
}
public UntitledMultiSelectItems build() {
Assert.notEmpty(enumValues, "enumValues must not be empty");
return new UntitledMultiSelectItems(enumValues);
}
}
}
/**
* Schema for multiple-selection enumeration without display titles for options.
*
* @param title Optional title for the enum field
* @param description Optional description for the enum field
* @param items Schema for the array items, containing the list of enum values
* @param minItems Optional minimum number of items to select
* @param maxItems Optional maximum number of items to select
* @param defaultValue Optional default selected values
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record UntitledMultiSelectEnumSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("items") UntitledMultiSelectItems items,
@JsonProperty("minItems") Integer minItems,
@JsonProperty("maxItems") Integer maxItems,
@JsonProperty("default") List<String> defaultValue) { // @formatter:on
public UntitledMultiSelectEnumSchema {
Assert.notNull(items, "items must not be null");
}
@JsonProperty("type")
public String type() {
return "array";
}
public static Builder builder(UntitledMultiSelectItems items) {
return new Builder(items);
}
public static class Builder {
private String title;
private String description;
private UntitledMultiSelectItems items;
private Integer minItems;
private Integer maxItems;
private List<String> defaultValue;
private Builder(UntitledMultiSelectItems items) {
Assert.notNull(items, "items must not be null");
this.items = items;
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder items(UntitledMultiSelectItems items) {
Assert.notNull(items, "items must not be null");
this.items = items;
return this;
}
public Builder minItems(Integer minItems) {
this.minItems = minItems;
return this;
}
public Builder maxItems(Integer maxItems) {
this.maxItems = maxItems;
return this;
}
public Builder defaults(String... defaultValue) {
Assert.notNull(defaultValue, "defaultValue must not be null");
this.defaultValue = Arrays.asList(defaultValue);
return this;
}
public Builder defaults(List<String> defaultValue) {
Assert.notNull(defaultValue, "defaultValue must not be null");
this.defaultValue = new ArrayList<>(defaultValue);
return this;
}
public UntitledMultiSelectEnumSchema build() {
return new UntitledMultiSelectEnumSchema(title, description, items, minItems, maxItems, defaultValue);
}
}
}
/**
* The items schema for {@link TitledMultiSelectEnumSchema}, describing the allowed
* enum options with display labels.
*
* @param anyOf Array of enum options, each with a machine-readable value and a
* human-readable display label
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record TitledMultiSelectItems( // @formatter:off
@JsonProperty("anyOf") List<EnumSchemaOption> anyOf) { // @formatter:on
public TitledMultiSelectItems {
Assert.notNull(anyOf, "anyOf must not be null");
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private List<EnumSchemaOption> anyOf;
private Builder() {
}
public Builder anyOf(List<EnumSchemaOption> anyOf) {
Assert.notNull(anyOf, "anyOf must not be null");
this.anyOf = new ArrayList<>(anyOf);
return this;
}
public Builder anyOf(EnumSchemaOption... anyOf) {
Assert.notNull(anyOf, "anyOf must not be null");
this.anyOf = Arrays.asList(anyOf);
return this;
}
public TitledMultiSelectItems build() {
Assert.notEmpty(anyOf, "anyOf must not be empty");
return new TitledMultiSelectItems(anyOf);
}
}
}
/**
* Schema for multiple-selection enumeration with display titles for each option.
*
* @param title Optional title for the enum field
* @param description Optional description for the enum field
* @param items Schema for the array items, containing the list of titled enum options
* @param minItems Optional minimum number of items to select
* @param maxItems Optional maximum number of items to select
* @param defaultValue Optional default selected values
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record TitledMultiSelectEnumSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("items") TitledMultiSelectItems items,
@JsonProperty("minItems") Integer minItems,
@JsonProperty("maxItems") Integer maxItems,
@JsonProperty("default") List<String> defaultValue) { // @formatter:on
public TitledMultiSelectEnumSchema {
Assert.notNull(items, "items must not be null");
}
@JsonProperty("type")
public String type() {
return "array";
}
public static Builder builder(TitledMultiSelectItems items) {
return new Builder(items);
}
public static class Builder {
private String title;
private String description;
private TitledMultiSelectItems items;
private Integer minItems;
private Integer maxItems;
private List<String> defaultValue;
private Builder(TitledMultiSelectItems items) {
Assert.notNull(items, "items must not be null");
this.items = items;
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder items(TitledMultiSelectItems items) {
Assert.notNull(items, "items must not be null");
this.items = items;
return this;
}
public Builder minItems(Integer minItems) {
this.minItems = minItems;
return this;
}
public Builder maxItems(Integer maxItems) {
this.maxItems = maxItems;
return this;
}
public Builder defaults(List<String> defaultValue) {
Assert.notNull(defaultValue, "defaultValue must not be null");
this.defaultValue = new ArrayList<>(defaultValue);
return this;
}
public Builder defaults(String... defaultValue) {
Assert.notNull(defaultValue, "defaultValue must not be null");
this.defaultValue = Arrays.asList(defaultValue);
return this;
}
public TitledMultiSelectEnumSchema build() {
return new TitledMultiSelectEnumSchema(title, description, items, minItems, maxItems, defaultValue);
}
}
}
/**
* Schema for a boolean field in a form-based elicitation request.
*
* @param title Optional title for the boolean field
* @param description Optional description for the boolean field
* @param defaultValue Optional default value
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record BooleanSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("default") Boolean defaultValue) { // @formatter:on
@JsonProperty("type")
public String type() {
return "boolean";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private Boolean defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder defaultValue(Boolean defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public BooleanSchema build() {
return new BooleanSchema(title, description, defaultValue);
}
}
}
/**
* Schema for a numeric field in a form-based elicitation request, supporting both
* {@code "number"} (floating-point) and {@code "integer"} types.
*
* @param title Optional title for the numeric field
* @param description Optional description for the numeric field
* @param type The JSON Schema type, either {@code "number"} or {@code "integer"};
* defaults to {@code "number"} in the builder
* @param minimum Optional minimum value (inclusive)
* @param maximum Optional maximum value (inclusive)
* @param defaultValue Optional default value
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record NumberSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("type") String type,
@JsonProperty("minimum") Number minimum,
@JsonProperty("maximum") Number maximum,
@JsonProperty("default") Number defaultValue) { // @formatter:on
public NumberSchema {
Assert.notNull(type, "type must not be null");
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private String type = "number";
private Number minimum;
private Number maximum;
private Number defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder integer() {
this.type = "integer";
return this;
}
public Builder minimum(Number minimum) {
this.minimum = minimum;
return this;
}
public Builder maximum(Number maximum) {
this.maximum = maximum;
return this;
}
public Builder defaultValue(Number defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public NumberSchema build() {
return new NumberSchema(title, description, type, minimum, maximum, defaultValue);
}
}
}
/**
* Schema for a text input field in a form-based elicitation request.
*
* @param title Optional title for the text field
* @param description Optional description for the text field
* @param minLength Optional minimum string length
* @param maxLength Optional maximum string length
* @param format Optional format hint (e.g. {@code "email"}, {@code "uri"})
* @param defaultValue Optional default value
*/
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record StringSchema( // @formatter:off
@JsonProperty("title") String title,
@JsonProperty("description") String description,
@JsonProperty("minLength") Integer minLength,
@JsonProperty("maxLength") Integer maxLength,
@JsonProperty("format") String format,
@JsonProperty("default") String defaultValue) { // @formatter:on
@JsonProperty("type")
public String type() {
return "string";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String title;
private String description;
private Integer minLength;
private Integer maxLength;
private String format;
private String defaultValue;
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder minLength(Integer minLength) {
this.minLength = minLength;
return this;
}
public Builder maxLength(Integer maxLength) {
this.maxLength = maxLength;
return this;
}
public Builder format(String format) {
this.format = format;
return this;
}
public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public StringSchema build() {
Assert.isTrue(
format == null || format.equals("uri") || format.equals("email") || format.equals("date")
|| format.equals("date-time"),
"format must be one of: null, \"uri\", \"email\", \"date\", \"date-time\"");
return new StringSchema(title, description, minLength, maxLength, format, defaultValue);
}
}
}
/**
* A request from the server to elicit additional information from the user, either
* through the client or out-of-band.
@ -3930,13 +4742,48 @@ public final class McpSchema {
/**
* A request from the server to elicit additional information from the user via the
* client, using {@code form} mode.
* <p>
* The requested schema is flexible, but for standard schemas, consider using one the
* following types:
* <ul>
* <li>{@link BooleanSchema}
* <li>{@link NumberSchema}
* <li>{@link StringSchema}
* <li>{@link LegacyTitledEnumSchema}
* <li>{@link TitledSingleSelectEnumSchema}
* <li>{@link TitledMultiSelectEnumSchema}
* <li>{@link UntitledSingleSelectEnumSchema}
* <li>{@link UntitledMultiSelectEnumSchema}
* </ul>
*
* These can be used with a JSON mapper:
*
* <pre>
* var mapper = McpJsonDefaults.getMapper();
* TypeRef&lt;Map&lt;String, Object&gt;&gt; mapType = new TypeRef<>() { };
* var first = UntitledSingleSelectEnumSchema.builder()
* .enumValues("option1", "option2", "option3")
* .build();
* var second = BooleanSchema
* .builder()
* .title("Say yes")
* .description("By selecting this, you say yes to the thing")
* .build();
* Map&lt;String, Object&gt; requestedSchema = Map.of(
* "type", "object",
* "properties", Map.of(
* "first-thing", mapper.convertValue(first, mapType),
* "second-thing", mapper.convertValue(second, mapType)),
* "required", List.of("first-thing", "second-thing"));
* </pre>
*
* @param message The message to present to the user
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
* properties are allowed, without nesting. Per SEP-1613, the dialect defaults to JSON
* Schema 2020-12 ({@link #JSON_SCHEMA_DIALECT_2020_12}) when no explicit
* {@code $schema} entry is present. To declare a different dialect, include a
* {@code "$schema"} key in the map.
* {@code "$schema"} key in the map. For type-safety in the schemas, use one of the
* supported schema types.
* @param meta See specification for notes on _meta usage
* <p>
* Note: {@code message} and {@code requestedSchema} are required by the MCP

View file

@ -16,6 +16,9 @@ import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import net.javacrumbs.jsonunit.core.Option;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
@ -1726,6 +1729,633 @@ public class McpSchemaTests {
assertThat(request.message()).isEqualTo("hello");
}
// Enum Schema Tests
@Test
void testEnumSchemaOptionDeserialization() throws Exception {
var option = JSON_MAPPER.readValue("""
{
"const": "low",
"title": "Low Priority"
}""", McpSchema.EnumSchemaOption.class);
assertThat(option.constValue()).isEqualTo("low");
assertThat(option.title()).isEqualTo("Low Priority");
}
@Test
void testEnumSchemaOptionDeserializationWithUnknownField() throws Exception {
var option = JSON_MAPPER.readValue("""
{
"futureField": 42
}""", McpSchema.EnumSchemaOption.class);
assertThat(option).isNotNull();
}
@Test
void testEnumSchemaOptionDeserializationWithBothFieldsMissing() throws Exception {
var option = JSON_MAPPER.readValue("{}", McpSchema.EnumSchemaOption.class);
assertThat(option.constValue()).isEqualTo("");
assertThat(option.title()).isEqualTo("");
}
@Test
void testEnumSchemaOptionsRequiredField() {
assertThatThrownBy(() -> new McpSchema.EnumSchemaOption("~~~", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("title must not be null");
assertThatThrownBy(() -> new McpSchema.EnumSchemaOption(null, "~~~"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("constValue must not be null");
}
@Test
void testUntitledSingleSelectEnumSchemaSerialization() throws Exception {
var schema = new McpSchema.UntitledSingleSelectEnumSchema(null, "Choose a color",
List.of("red", "green", "blue"), null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{"type":"string","description":"Choose a color","enum":["red","green","blue"]}"""));
}
@Test
void testUntitledSingleSelectEnumSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{"type":"string","description":"Pick one","enum":["a","b","c"],"default":"a"}""",
McpSchema.UntitledSingleSelectEnumSchema.class);
assertThat(schema.type()).isEqualTo("string");
assertThat(schema.description()).isEqualTo("Pick one");
assertThat(schema.enumValues()).containsExactly("a", "b", "c");
assertThat(schema.defaultValue()).isEqualTo("a");
}
@Test
void testTitledSingleSelectEnumSchemaSerialization() throws Exception {
var schema = new McpSchema.TitledSingleSelectEnumSchema("Priority", "Select a priority",
List.of(new McpSchema.EnumSchemaOption("low", "Low"), new McpSchema.EnumSchemaOption("high", "High")),
null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "string",
"title": "Priority",
"description": "Select a priority",
"oneOf": [
{"const": "low", "title": "Low"},
{"const": "high", "title": "High"}
]
}"""));
}
@Test
void testTitledSingleSelectEnumSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "string",
"title": "Color",
"oneOf": [
{"const": "red", "title": "Red"},
{"const": "blue", "title": "Blue"}
],
"default": "red"
}""", McpSchema.TitledSingleSelectEnumSchema.class);
assertThat(schema.type()).isEqualTo("string");
assertThat(schema.title()).isEqualTo("Color");
assertThat(schema.oneOf()).hasSize(2);
assertThat(schema.oneOf().get(0).constValue()).isEqualTo("red");
assertThat(schema.oneOf().get(0).title()).isEqualTo("Red");
assertThat(schema.defaultValue()).isEqualTo("red");
}
@Test
@SuppressWarnings("deprecation")
void testLegacyTitledEnumSchemaSerialization() throws Exception {
var schema = new McpSchema.LegacyTitledEnumSchema(null, null, List.of("a", "b"),
List.of("Option A", "Option B"), null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{"type":"string","enum":["a","b"],"enumNames":["Option A","Option B"]}"""));
}
@Test
@SuppressWarnings("deprecation")
void testLegacyTitledEnumSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{"type":"string","enum":["x","y"],"enumNames":["Ex","Why"]}""", McpSchema.LegacyTitledEnumSchema.class);
assertThat(schema.type()).isEqualTo("string");
assertThat(schema.enumValues()).containsExactly("x", "y");
assertThat(schema.enumNames()).containsExactly("Ex", "Why");
}
@Test
void testUntitledMultiSelectEnumSchemaSerialization() throws Exception {
var items = new McpSchema.UntitledMultiSelectItems(List.of("js", "java", "python"));
var schema = new McpSchema.UntitledMultiSelectEnumSchema("Languages", null, items, 1, 3, null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "array",
"title": "Languages",
"items": {"type": "string", "enum": ["js", "java", "python"]},
"minItems": 1,
"maxItems": 3
}"""));
}
@Test
void testUntitledMultiSelectEnumSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "array",
"items": {"type": "string", "enum": ["a", "b", "c"]},
"default": ["a"]
}""", McpSchema.UntitledMultiSelectEnumSchema.class);
assertThat(schema.type()).isEqualTo("array");
assertThat(schema.items().enumValues()).containsExactly("a", "b", "c");
assertThat(schema.defaultValue()).containsExactly("a");
}
@Test
void testTitledMultiSelectEnumSchemaSerialization() throws Exception {
var options = List.of(new McpSchema.EnumSchemaOption("js", "JavaScript"),
new McpSchema.EnumSchemaOption("java", "Java"));
var items = new McpSchema.TitledMultiSelectItems(options);
var schema = new McpSchema.TitledMultiSelectEnumSchema("Languages", "Pick languages", items, null, null, null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "array",
"title": "Languages",
"description": "Pick languages",
"items": {
"anyOf": [
{"const": "js", "title": "JavaScript"},
{"const": "java", "title": "Java"}
]
}
}"""));
}
@Test
void testTitledMultiSelectEnumSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "array",
"title": "Flavors",
"items": {
"anyOf": [
{"const": "vanilla", "title": "Vanilla"},
{"const": "chocolate", "title": "Chocolate"}
]
},
"default": ["vanilla"]
}""", McpSchema.TitledMultiSelectEnumSchema.class);
assertThat(schema.type()).isEqualTo("array");
assertThat(schema.title()).isEqualTo("Flavors");
assertThat(schema.items().anyOf()).hasSize(2);
assertThat(schema.items().anyOf().get(0).constValue()).isEqualTo("vanilla");
assertThat(schema.items().anyOf().get(0).title()).isEqualTo("Vanilla");
assertThat(schema.defaultValue()).containsExactly("vanilla");
}
@Test
void testUntitledSingleSelectEnumSchemaBuilderRequiresEnumValues() {
assertThatThrownBy(() -> McpSchema.UntitledSingleSelectEnumSchema.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
void testUntitledSingleSelectEnumSchemaBuilderRejectsEmptyEnumValues() {
assertThatThrownBy(() -> McpSchema.UntitledSingleSelectEnumSchema.builder().enumValues(List.of()).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
void testTitledSingleSelectEnumSchemaBuilderRequiresOneOf() {
assertThatThrownBy(() -> McpSchema.TitledSingleSelectEnumSchema.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("oneOf must not be empty");
}
@Test
void testTitledSingleSelectEnumSchemaBuilderRejectsEmptyOneOf() {
assertThatThrownBy(() -> McpSchema.TitledSingleSelectEnumSchema.builder().oneOf(List.of()).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("oneOf must not be empty");
}
@Test
@SuppressWarnings("deprecation")
void testLegacyTitledEnumSchemaBuilderRequiresEnumValues() {
assertThatThrownBy(() -> McpSchema.LegacyTitledEnumSchema.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
@SuppressWarnings("deprecation")
void testLegacyTitledEnumSchemaBuilderRejectsEmptyEnumValues() {
assertThatThrownBy(() -> McpSchema.LegacyTitledEnumSchema.builder().enumValues(List.of()).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
void testUntitledMultiSelectItemsBuilderRequiresEnumValues() {
assertThatThrownBy(() -> McpSchema.UntitledMultiSelectItems.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
void testUntitledMultiSelectItemsBuilderRejectsEmptyEnumValues() {
assertThatThrownBy(() -> McpSchema.UntitledMultiSelectItems.builder().enumValues(List.of()).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("enumValues must not be empty");
}
@Test
void testTitledMultiSelectItemsBuilderRequiresAnyOf() {
assertThatThrownBy(() -> McpSchema.TitledMultiSelectItems.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("anyOf must not be empty");
}
@Test
void testTitledMultiSelectItemsBuilderRejectsEmptyAnyOf() {
assertThatThrownBy(() -> McpSchema.TitledMultiSelectItems.builder().anyOf(List.of()).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("anyOf must not be empty");
}
@Test
void testUntitledSingleSelectEnumSchemaBuilderSingularAdd() {
var schema = McpSchema.UntitledSingleSelectEnumSchema.builder().enumValues("a", "b").build();
assertThat(schema.enumValues()).containsExactly("a", "b");
}
@Test
void testUntitledSingleSelectEnumSchemaBuilderOptionalFields() {
var schema = McpSchema.UntitledSingleSelectEnumSchema.builder()
.title("Color")
.description("Pick a color")
.enumValues("red", "blue")
.defaultValue("red")
.build();
assertThat(schema.title()).isEqualTo("Color");
assertThat(schema.description()).isEqualTo("Pick a color");
assertThat(schema.defaultValue()).isEqualTo("red");
}
@Test
void testTitledSingleSelectEnumSchemaBuilderSingularAdd() {
var opt1 = new McpSchema.EnumSchemaOption("v1", "Option 1");
var schema = McpSchema.TitledSingleSelectEnumSchema.builder().oneOf(opt1).build();
assertThat(schema.oneOf()).hasSize(1)
.first()
.extracting(McpSchema.EnumSchemaOption::constValue)
.isEqualTo("v1");
}
@Test
@SuppressWarnings("deprecation")
void testLegacyTitledEnumSchemaBuilderSingularAdds() {
var schema = McpSchema.LegacyTitledEnumSchema.builder().enumValues("a", "b").enumNames("Alpha", "Beta").build();
assertThat(schema.enumValues()).containsExactly("a", "b");
assertThat(schema.enumNames()).containsExactly("Alpha", "Beta");
}
@Test
void testTitledMultiSelectItemsBuilderSingularAdd() {
var opt1 = new McpSchema.EnumSchemaOption("v1", "First");
var opt2 = new McpSchema.EnumSchemaOption("v2", "Second");
var items = McpSchema.TitledMultiSelectItems.builder().anyOf(opt1, opt2).build();
assertThat(items.anyOf()).hasSize(2);
assertThat(items.anyOf().get(1).constValue()).isEqualTo("v2");
}
@Test
void testUntitledMultiSelectEnumSchemaBuilderOptionalFields() {
var items = McpSchema.UntitledMultiSelectItems.builder().enumValues("a", "b").build();
var schema = McpSchema.UntitledMultiSelectEnumSchema.builder(items)
.title("Tags")
.description("Select tags")
.minItems(1)
.maxItems(2)
.defaults("a", "b")
.build();
assertThat(schema.title()).isEqualTo("Tags");
assertThat(schema.minItems()).isEqualTo(1);
assertThat(schema.maxItems()).isEqualTo(2);
assertThat(schema.defaultValue()).containsExactly("a", "b");
}
// Primitive Elicitation Schema Tests (BooleanSchema, NumberSchema, StringSchema)
@Test
void testBooleanSchemaSerialization() throws Exception {
var schema = new McpSchema.BooleanSchema(null, "Enable feature", true);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "boolean",
"description": "Enable feature",
"default": true
}"""));
}
@Test
void testBooleanSchemaSerializationOmitsNullFields() throws Exception {
var schema = new McpSchema.BooleanSchema(null, null, null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "boolean"
}"""));
}
@Test
void testBooleanSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "boolean",
"title": "Subscribe",
"description": "Opt in",
"default": false
}""", McpSchema.BooleanSchema.class);
assertThat(schema.type()).isEqualTo("boolean");
assertThat(schema.title()).isEqualTo("Subscribe");
assertThat(schema.description()).isEqualTo("Opt in");
assertThat(schema.defaultValue()).isEqualTo(false);
}
@Test
void testBooleanSchemaBuilderAllFields() {
var schema = McpSchema.BooleanSchema.builder()
.title("Send notifications")
.description("Receive email updates")
.defaultValue(true)
.build();
assertThat(schema.title()).isEqualTo("Send notifications");
assertThat(schema.description()).isEqualTo("Receive email updates");
assertThat(schema.defaultValue()).isTrue();
assertThat(schema.type()).isEqualTo("boolean");
}
@Test
void testBooleanSchemaToleratesUnknownFields() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "boolean",
"futureField": 42
}""", McpSchema.BooleanSchema.class);
assertThat(schema.type()).isEqualTo("boolean");
}
@Test
void testNumberSchemaSerialization() throws Exception {
var schema = new McpSchema.NumberSchema(null, "Enter a score", "number", 0.0, 100.0, 50.0);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "number",
"description": "Enter a score",
"minimum": 0.0,
"maximum": 100.0,
"default": 50.0
}"""));
}
@Test
void testNumberSchemaSerializationIntegerType() throws Exception {
var schema = McpSchema.NumberSchema.builder()
.integer()
.description("Enter age")
.minimum(0)
.maximum(150)
.build();
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "integer",
"description": "Enter age",
"minimum": 0,
"maximum": 150
}"""));
}
@Test
void testNumberSchemaSerializationOmitsNullFields() throws Exception {
var schema = McpSchema.NumberSchema.builder().build();
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "number"
}"""));
}
@Test
void testNumberSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "number",
"title": "Score",
"minimum": 0,
"maximum": 10,
"default": 5.5
}""", McpSchema.NumberSchema.class);
assertThat(schema.type()).isEqualTo("number");
assertThat(schema.title()).isEqualTo("Score");
assertThat(schema.minimum()).isEqualTo(0);
assertThat(schema.maximum()).isEqualTo(10);
assertThat(schema.defaultValue()).isEqualTo(5.5);
}
@Test
void testNumberSchemaDeserializationIntegerType() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "integer",
"description": "Age",
"minimum": 18
}""", McpSchema.NumberSchema.class);
assertThat(schema.type()).isEqualTo("integer");
assertThat(schema.description()).isEqualTo("Age");
assertThat(schema.minimum()).isEqualTo(18);
}
@Test
void testNumberSchemaBuilderDefaultsToNumberType() {
var schema = McpSchema.NumberSchema.builder().build();
assertThat(schema.type()).isEqualTo("number");
}
@Test
void testNumberSchemaBuilderIntegerType() {
var schema = McpSchema.NumberSchema.builder().integer().build();
assertThat(schema.type()).isEqualTo("integer");
}
@Test
void testNumberSchemaBuilderAllFields() {
var schema = McpSchema.NumberSchema.builder()
.title("Price")
.description("Item price")
.minimum(0.01)
.maximum(9999.99)
.defaultValue(19.99)
.build();
assertThat(schema.title()).isEqualTo("Price");
assertThat(schema.description()).isEqualTo("Item price");
assertThat(schema.minimum()).isEqualTo(0.01);
assertThat(schema.maximum()).isEqualTo(9999.99);
assertThat(schema.defaultValue()).isEqualTo(19.99);
}
@Test
void testNumberSchemaToleratesUnknownFields() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "number",
"futureField": "ignored"
}""", McpSchema.NumberSchema.class);
assertThat(schema.type()).isEqualTo("number");
}
@Test
void testStringSchemaSerialization() throws Exception {
var schema = new McpSchema.StringSchema("Email", "Your email address", 5, 255, "email", "user@example.com");
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "string",
"title": "Email",
"description": "Your email address",
"minLength": 5,
"maxLength": 255,
"format": "email",
"default": "user@example.com"
}"""));
}
@Test
void testStringSchemaSerializationOmitsNullFields() throws Exception {
var schema = new McpSchema.StringSchema(null, null, null, null, null, null);
String json = JSON_MAPPER.writeValueAsString(schema);
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().isEqualTo(json("""
{
"type": "string"
}"""));
}
@Test
void testStringSchemaDeserialization() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "string",
"title": "Name",
"description": "Your name",
"minLength": 1,
"maxLength": 100,
"default": "Alice"
}""", McpSchema.StringSchema.class);
assertThat(schema.type()).isEqualTo("string");
assertThat(schema.title()).isEqualTo("Name");
assertThat(schema.description()).isEqualTo("Your name");
assertThat(schema.minLength()).isEqualTo(1);
assertThat(schema.maxLength()).isEqualTo(100);
assertThat(schema.defaultValue()).isEqualTo("Alice");
}
@Test
void testStringSchemaBuilderAllFields() {
var schema = McpSchema.StringSchema.builder()
.title("Website")
.description("Your website URL")
.minLength(10)
.maxLength(200)
.format("uri")
.defaultValue("https://example.com")
.build();
assertThat(schema.title()).isEqualTo("Website");
assertThat(schema.description()).isEqualTo("Your website URL");
assertThat(schema.minLength()).isEqualTo(10);
assertThat(schema.maxLength()).isEqualTo(200);
assertThat(schema.format()).isEqualTo("uri");
assertThat(schema.defaultValue()).isEqualTo("https://example.com");
assertThat(schema.type()).isEqualTo("string");
}
@Test
void testStringSchemaToleratesUnknownFields() throws Exception {
var schema = JSON_MAPPER.readValue("""
{
"type": "string",
"futureField": "ignored"
}""", McpSchema.StringSchema.class);
assertThat(schema.type()).isEqualTo("string");
}
@ParameterizedTest
@ValueSource(strings = { "uri", "email", "date", "date-time" })
@NullSource
void testStringSchemaBuilderAcceptsValidFormats(String format) {
var schema = McpSchema.StringSchema.builder().format(format).build();
assertThat(schema.format()).isEqualTo(format);
}
@Test
void testStringSchemaBuilderAcceptsNullFormat() {
var schema = McpSchema.StringSchema.builder().build();
assertThat(schema.format()).isNull();
}
@Test
void testStringSchemaBuilderRejectsInvalidFormat() {
assertThatThrownBy(() -> McpSchema.StringSchema.builder().format("uuid").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("format must be one of");
}
// Pagination Tests
@Test