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:
parent
99faac65f7
commit
fe82ad508a
3 changed files with 1531 additions and 31 deletions
|
|
@ -5,11 +5,12 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.McpJsonDefaults;
|
||||||
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
import io.modelcontextprotocol.server.McpServer;
|
import io.modelcontextprotocol.server.McpServer;
|
||||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||||
import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
|
import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
|
||||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.AudioContent;
|
import io.modelcontextprotocol.spec.McpSchema.AudioContent;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
|
|
@ -43,6 +44,16 @@ import org.apache.catalina.startup.Tomcat;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 {
|
public class ConformanceServlet {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConformanceServlet.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConformanceServlet.class);
|
||||||
|
|
@ -141,6 +152,7 @@ public class ConformanceServlet {
|
||||||
return tomcat;
|
return tomcat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private static List<McpServerFeatures.SyncToolSpecification> createToolSpecs() {
|
private static List<McpServerFeatures.SyncToolSpecification> createToolSpecs() {
|
||||||
return List.of(
|
return List.of(
|
||||||
// test_simple_text - Returns simple text content
|
// test_simple_text - Returns simple text content
|
||||||
|
|
@ -406,8 +418,8 @@ public class ConformanceServlet {
|
||||||
// json_schema_2020_12_tool - SEP-1613 dialect/keyword preservation
|
// json_schema_2020_12_tool - SEP-1613 dialect/keyword preservation
|
||||||
McpServerFeatures.SyncToolSpecification.builder()
|
McpServerFeatures.SyncToolSpecification.builder()
|
||||||
.tool(Tool
|
.tool(Tool
|
||||||
.builder("json_schema_2020_12_tool", Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12,
|
.builder("json_schema_2020_12_tool", Map.of("$schema", JSON_SCHEMA_DIALECT_2020_12, "type",
|
||||||
"type", "object", "$defs",
|
"object", "$defs",
|
||||||
Map.of("address",
|
Map.of("address",
|
||||||
Map.of("type", "object", "properties",
|
Map.of("type", "object", "properties",
|
||||||
Map.of("street", Map.of("type", "string"), "city",
|
Map.of("street", Map.of("type", "string"), "city",
|
||||||
|
|
@ -434,33 +446,44 @@ public class ConformanceServlet {
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> {
|
||||||
logger.info("Tool 'test_elicitation_sep1330_enums' called");
|
logger.info("Tool 'test_elicitation_sep1330_enums' called");
|
||||||
|
|
||||||
// Create schema with all 5 enum variants
|
TypeRef<Map<String, Object>> mapType = new TypeRef<>() {
|
||||||
Map<String, Object> requestedSchema = Map.of("type", "object", "properties", Map.of(
|
};
|
||||||
// 1. Untitled single-select
|
var mapper = McpJsonDefaults.getMapper();
|
||||||
"untitledSingle",
|
|
||||||
Map.of("type", "string", "enum", List.of("option1", "option2", "option3")),
|
// 1. Untitled single-select
|
||||||
// 2. Titled single-select using oneOf with const/title
|
var untitledSingle = UntitledSingleSelectEnumSchema.builder()
|
||||||
"titledSingle",
|
.enumValues("option1", "option2", "option3")
|
||||||
Map.of("type", "string", "oneOf",
|
.build();
|
||||||
List.of(Map.of("const", "value1", "title", "First Option"),
|
// 2. Titled single-select using oneOf with const/title
|
||||||
Map.of("const", "value2", "title", "Second Option"),
|
var titledSingle = TitledSingleSelectEnumSchema.builder()
|
||||||
Map.of("const", "value3", "title", "Third Option"))),
|
.oneOf(new EnumSchemaOption("value1", "First Option"),
|
||||||
// 3. Legacy titled using enumNames (deprecated)
|
new EnumSchemaOption("value2", "Second Option"),
|
||||||
"legacyEnum",
|
new EnumSchemaOption("value3", "Third Option"))
|
||||||
Map.of("type", "string", "enum", List.of("opt1", "opt2", "opt3"), "enumNames",
|
.build();
|
||||||
List.of("Option One", "Option Two", "Option Three")),
|
// 3. Legacy titled using enumNames (deprecated)
|
||||||
// 4. Untitled multi-select
|
var legacyEnum = LegacyTitledEnumSchema.builder()
|
||||||
"untitledMulti",
|
.enumValues("opt1", "opt2", "opt3")
|
||||||
Map.of("type", "array", "items",
|
.enumNames("Option One", "Option Two", "Option Three")
|
||||||
Map.of("type", "string", "enum", List.of("option1", "option2", "option3"))),
|
.build();
|
||||||
// 5. Titled multi-select using items.anyOf with
|
// 4. Untitled multi-select
|
||||||
// const/title
|
var untitledMulti = UntitledMultiSelectEnumSchema.builder(
|
||||||
"titledMulti",
|
UntitledMultiSelectItems.builder().enumValues("option1", "option2", "option3").build())
|
||||||
Map.of("type", "array", "items",
|
.build();
|
||||||
Map.of("anyOf",
|
// 5. Titled multi-select using items.anyOf with const/title
|
||||||
List.of(Map.of("const", "value1", "title", "First Choice"),
|
var titledMulti = TitledMultiSelectEnumSchema
|
||||||
Map.of("const", "value2", "title", "Second Choice"),
|
.builder(TitledMultiSelectItems.builder()
|
||||||
Map.of("const", "value3", "title", "Third Choice"))))),
|
.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",
|
"required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti",
|
||||||
"titledMulti"));
|
"titledMulti"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package io.modelcontextprotocol.spec;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -3888,6 +3889,817 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Elicitation
|
// 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
|
* A request from the server to elicit additional information from the user, either
|
||||||
* through the client or out-of-band.
|
* 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
|
* A request from the server to elicit additional information from the user via the
|
||||||
* client, using {@code form} mode.
|
* 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<Map<String, Object>> 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<String, Object> 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 message The message to present to the user
|
||||||
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
|
* @param requestedSchema A restricted subset of JSON Schema. Only top-level
|
||||||
* properties are allowed, without nesting. Per SEP-1613, the dialect defaults to JSON
|
* 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
|
* 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} 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
|
* @param meta See specification for notes on _meta usage
|
||||||
* <p>
|
* <p>
|
||||||
* Note: {@code message} and {@code requestedSchema} are required by the MCP
|
* Note: {@code message} and {@code requestedSchema} are required by the MCP
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
|
||||||
import net.javacrumbs.jsonunit.core.Option;
|
import net.javacrumbs.jsonunit.core.Option;
|
||||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
import org.junit.jupiter.api.Test;
|
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 io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER;
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||||
|
|
@ -1726,6 +1729,633 @@ public class McpSchemaTests {
|
||||||
assertThat(request.message()).isEqualTo("hello");
|
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
|
// Pagination Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue