Implement SEP-1330: Standards-compliant enum schemas for elicitation (#969)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
3f39ba7936
commit
83098e7cc0
9 changed files with 963 additions and 20 deletions
|
|
@ -16,9 +16,16 @@ The C# SDK registers an instance of <xref:ModelContextProtocol.Server.McpServer>
|
||||||
so tools can simply add a parameter of type <xref:ModelContextProtocol.Server.McpServer> to their method signature to access it.
|
so tools can simply add a parameter of type <xref:ModelContextProtocol.Server.McpServer> to their method signature to access it.
|
||||||
|
|
||||||
The MCP Server must specify the schema of each input value it is requesting from the user.
|
The MCP Server must specify the schema of each input value it is requesting from the user.
|
||||||
Only primitive types (string, number, boolean) are supported for elicitation requests.
|
Primitive types (string, number, boolean) and enum types are supported for elicitation requests.
|
||||||
The schema may include a description to help the user understand what is being requested.
|
The schema may include a description to help the user understand what is being requested.
|
||||||
|
|
||||||
|
For enum types, the SDK supports several schema formats:
|
||||||
|
- **UntitledSingleSelectEnumSchema**: A single-select enum where the enum values serve as both the value and display text
|
||||||
|
- **TitledSingleSelectEnumSchema**: A single-select enum with separate display titles for each option (using JSON Schema `oneOf` with `const` and `title`)
|
||||||
|
- **UntitledMultiSelectEnumSchema**: A multi-select enum allowing multiple values to be selected
|
||||||
|
- **TitledMultiSelectEnumSchema**: A multi-select enum with display titles for each option
|
||||||
|
- **LegacyTitledEnumSchema** (deprecated): The legacy enum schema using `enumNames` for backward compatibility
|
||||||
|
|
||||||
The server can request a single input or multiple inputs at once.
|
The server can request a single input or multiple inputs at once.
|
||||||
To help distinguish multiple inputs, each input has a unique name.
|
To help distinguish multiple inputs, each input has a unique name.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,4 +123,152 @@ public sealed class InteractiveTools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <snippet_EnumExamples>
|
||||||
|
[McpServerTool, Description("Example tool demonstrating various enum schema types")]
|
||||||
|
public async Task<string> EnumExamples(
|
||||||
|
McpServer server,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Example 1: UntitledSingleSelectEnumSchema - Simple enum without display titles
|
||||||
|
var prioritySchema = new RequestSchema
|
||||||
|
{
|
||||||
|
Properties =
|
||||||
|
{
|
||||||
|
["Priority"] = new UntitledSingleSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Priority Level",
|
||||||
|
Description = "Select the priority level",
|
||||||
|
Enum = ["low", "medium", "high", "critical"],
|
||||||
|
Default = "medium"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var priorityResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||||
|
{
|
||||||
|
Message = "Select a priority level:",
|
||||||
|
RequestedSchema = prioritySchema
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
if (priorityResponse.Action != "accept")
|
||||||
|
{
|
||||||
|
return "Operation cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
string? priority = priorityResponse.Content?["Priority"].GetString();
|
||||||
|
|
||||||
|
// Example 2: TitledSingleSelectEnumSchema - Enum with custom display titles
|
||||||
|
var severitySchema = new RequestSchema
|
||||||
|
{
|
||||||
|
Properties =
|
||||||
|
{
|
||||||
|
["Severity"] = new TitledSingleSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Issue Severity",
|
||||||
|
Description = "Select the issue severity level",
|
||||||
|
OneOf =
|
||||||
|
[
|
||||||
|
new EnumSchemaOption { Const = "p0", Title = "P0 - Critical (Immediate attention required)" },
|
||||||
|
new EnumSchemaOption { Const = "p1", Title = "P1 - High (Urgent, within 24 hours)" },
|
||||||
|
new EnumSchemaOption { Const = "p2", Title = "P2 - Medium (Within a week)" },
|
||||||
|
new EnumSchemaOption { Const = "p3", Title = "P3 - Low (As time permits)" }
|
||||||
|
],
|
||||||
|
Default = "p2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var severityResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||||
|
{
|
||||||
|
Message = "Select the issue severity:",
|
||||||
|
RequestedSchema = severitySchema
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
if (severityResponse.Action != "accept")
|
||||||
|
{
|
||||||
|
return "Operation cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
string? severity = severityResponse.Content?["Severity"].GetString();
|
||||||
|
|
||||||
|
// Example 3: UntitledMultiSelectEnumSchema - Select multiple values
|
||||||
|
var tagsSchema = new RequestSchema
|
||||||
|
{
|
||||||
|
Properties =
|
||||||
|
{
|
||||||
|
["Tags"] = new UntitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Tags",
|
||||||
|
Description = "Select one or more tags",
|
||||||
|
MinItems = 1,
|
||||||
|
MaxItems = 3,
|
||||||
|
Items = new UntitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Enum = ["bug", "feature", "documentation", "enhancement", "question"]
|
||||||
|
},
|
||||||
|
Default = ["bug"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var tagsResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||||
|
{
|
||||||
|
Message = "Select up to 3 tags:",
|
||||||
|
RequestedSchema = tagsSchema
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
if (tagsResponse.Action != "accept")
|
||||||
|
{
|
||||||
|
return "Operation cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multi-select, the value is an array
|
||||||
|
var tags = tagsResponse.Content?["Tags"].EnumerateArray()
|
||||||
|
.Select(e => e.GetString())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Example 4: TitledMultiSelectEnumSchema - Multi-select with custom titles
|
||||||
|
var featuresSchema = new RequestSchema
|
||||||
|
{
|
||||||
|
Properties =
|
||||||
|
{
|
||||||
|
["Features"] = new TitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Features",
|
||||||
|
Description = "Select desired features",
|
||||||
|
Items = new TitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
AnyOf =
|
||||||
|
[
|
||||||
|
new EnumSchemaOption { Const = "auth", Title = "Authentication & Authorization" },
|
||||||
|
new EnumSchemaOption { Const = "api", Title = "RESTful API" },
|
||||||
|
new EnumSchemaOption { Const = "ui", Title = "Modern UI Components" },
|
||||||
|
new EnumSchemaOption { Const = "db", Title = "Database Integration" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var featuresResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||||
|
{
|
||||||
|
Message = "Select desired features:",
|
||||||
|
RequestedSchema = featuresSchema
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
if (featuresResponse.Action != "accept")
|
||||||
|
{
|
||||||
|
return "Operation cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = featuresResponse.Content?["Features"].EnumerateArray()
|
||||||
|
.Select(e => e.GetString())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return $"Selected: Priority={priority}, Severity={severity}, Tags=[{string.Join(", ", tags ?? [])}], Features=[{string.Join(", ", features ?? [])}]";
|
||||||
|
}
|
||||||
|
// </snippet_EnumExamples>
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,10 @@ public sealed class ElicitRequestParams
|
||||||
/// Gets or sets the requested schema.
|
/// Gets or sets the requested schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
|
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
|
||||||
|
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
|
||||||
|
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,
|
||||||
|
/// or <see cref="LegacyTitledEnumSchema"/> (deprecated).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("requestedSchema")]
|
[JsonPropertyName("requestedSchema")]
|
||||||
[field: MaybeNull]
|
[field: MaybeNull]
|
||||||
|
|
@ -59,7 +62,10 @@ public sealed class ElicitRequestParams
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents restricted subset of JSON Schema:
|
/// Represents restricted subset of JSON Schema:
|
||||||
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
|
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
|
||||||
|
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
|
||||||
|
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,
|
||||||
|
/// or <see cref="LegacyTitledEnumSchema"/> (deprecated).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(Converter))]
|
[JsonConverter(typeof(Converter))]
|
||||||
public abstract class PrimitiveSchemaDefinition
|
public abstract class PrimitiveSchemaDefinition
|
||||||
|
|
@ -113,8 +119,13 @@ public sealed class ElicitRequestParams
|
||||||
bool? defaultBool = null;
|
bool? defaultBool = null;
|
||||||
double? defaultNumber = null;
|
double? defaultNumber = null;
|
||||||
string? defaultString = null;
|
string? defaultString = null;
|
||||||
|
IList<string>? defaultStringArray = null;
|
||||||
IList<string>? enumValues = null;
|
IList<string>? enumValues = null;
|
||||||
IList<string>? enumNames = null;
|
IList<string>? enumNames = null;
|
||||||
|
IList<EnumSchemaOption>? oneOf = null;
|
||||||
|
int? minItems = null;
|
||||||
|
int? maxItems = null;
|
||||||
|
object? items = null; // Can be UntitledEnumItemsSchema or TitledEnumItemsSchema
|
||||||
|
|
||||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||||
{
|
{
|
||||||
|
|
@ -161,6 +172,14 @@ public sealed class ElicitRequestParams
|
||||||
maximum = reader.GetDouble();
|
maximum = reader.GetDouble();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "minItems":
|
||||||
|
minItems = reader.GetInt32();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "maxItems":
|
||||||
|
maxItems = reader.GetInt32();
|
||||||
|
break;
|
||||||
|
|
||||||
case "default":
|
case "default":
|
||||||
// We need to handle different types for default values
|
// We need to handle different types for default values
|
||||||
// Store the value based on the JSON token type
|
// Store the value based on the JSON token type
|
||||||
|
|
@ -178,6 +197,9 @@ public sealed class ElicitRequestParams
|
||||||
case JsonTokenType.String:
|
case JsonTokenType.String:
|
||||||
defaultString = reader.GetString();
|
defaultString = reader.GetString();
|
||||||
break;
|
break;
|
||||||
|
case JsonTokenType.StartArray:
|
||||||
|
defaultStringArray = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -189,6 +211,14 @@ public sealed class ElicitRequestParams
|
||||||
enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
|
enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "oneOf":
|
||||||
|
oneOf = DeserializeEnumOptions(ref reader);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "items":
|
||||||
|
items = DeserializeEnumItemsSchema(ref reader);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
|
|
@ -204,15 +234,39 @@ public sealed class ElicitRequestParams
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "string":
|
case "string":
|
||||||
if (enumValues is not null)
|
if (oneOf is not null)
|
||||||
{
|
{
|
||||||
psd = new EnumSchema
|
// TitledSingleSelectEnumSchema
|
||||||
|
psd = new TitledSingleSelectEnumSchema
|
||||||
{
|
{
|
||||||
Enum = enumValues,
|
OneOf = oneOf,
|
||||||
EnumNames = enumNames,
|
|
||||||
Default = defaultString,
|
Default = defaultString,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if (enumValues is not null)
|
||||||
|
{
|
||||||
|
if (enumNames is not null)
|
||||||
|
{
|
||||||
|
// EnumSchema for backward compatibility
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
psd = new EnumSchema
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
{
|
||||||
|
Enum = enumValues,
|
||||||
|
EnumNames = enumNames,
|
||||||
|
Default = defaultString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// UntitledSingleSelectEnumSchema
|
||||||
|
psd = new UntitledSingleSelectEnumSchema
|
||||||
|
{
|
||||||
|
Enum = enumValues,
|
||||||
|
Default = defaultString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
psd = new StringSchema
|
psd = new StringSchema
|
||||||
|
|
@ -225,6 +279,31 @@ public sealed class ElicitRequestParams
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "array":
|
||||||
|
if (items is TitledEnumItemsSchema titledItems)
|
||||||
|
{
|
||||||
|
// TitledMultiSelectEnumSchema
|
||||||
|
psd = new TitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
MinItems = minItems,
|
||||||
|
MaxItems = maxItems,
|
||||||
|
Items = titledItems,
|
||||||
|
Default = defaultStringArray,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (items is UntitledEnumItemsSchema untitledItems)
|
||||||
|
{
|
||||||
|
// UntitledMultiSelectEnumSchema
|
||||||
|
psd = new UntitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
MinItems = minItems,
|
||||||
|
MaxItems = maxItems,
|
||||||
|
Items = untitledItems,
|
||||||
|
Default = defaultStringArray,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "integer":
|
case "integer":
|
||||||
case "number":
|
case "number":
|
||||||
psd = new NumberSchema
|
psd = new NumberSchema
|
||||||
|
|
@ -253,6 +332,108 @@ public sealed class ElicitRequestParams
|
||||||
return psd;
|
return psd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IList<EnumSchemaOption> DeserializeEnumOptions(ref Utf8JsonReader reader)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
{
|
||||||
|
throw new JsonException("Expected array for oneOf property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new List<EnumSchemaOption>();
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
throw new JsonException("Expected object in oneOf array.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? constValue = null;
|
||||||
|
string? titleValue = null;
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
string? propertyName = reader.GetString();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
switch (propertyName)
|
||||||
|
{
|
||||||
|
case "const":
|
||||||
|
constValue = reader.GetString();
|
||||||
|
break;
|
||||||
|
case "title":
|
||||||
|
titleValue = reader.GetString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.Skip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constValue is null || titleValue is null)
|
||||||
|
{
|
||||||
|
throw new JsonException("Each option in oneOf must have both 'const' and 'title' properties.");
|
||||||
|
}
|
||||||
|
|
||||||
|
options.Add(new EnumSchemaOption { Const = constValue, Title = titleValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object DeserializeEnumItemsSchema(ref Utf8JsonReader reader)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
throw new JsonException("Expected object for items property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? type = null;
|
||||||
|
IList<string>? enumValues = null;
|
||||||
|
IList<EnumSchemaOption>? anyOf = null;
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
string? propertyName = reader.GetString();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
switch (propertyName)
|
||||||
|
{
|
||||||
|
case "type":
|
||||||
|
type = reader.GetString();
|
||||||
|
break;
|
||||||
|
case "enum":
|
||||||
|
enumValues = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
break;
|
||||||
|
case "anyOf":
|
||||||
|
anyOf = DeserializeEnumOptions(ref reader);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.Skip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which type to create based on the properties
|
||||||
|
if (anyOf is not null)
|
||||||
|
{
|
||||||
|
return new TitledEnumItemsSchema { AnyOf = anyOf };
|
||||||
|
}
|
||||||
|
else if (enumValues is not null)
|
||||||
|
{
|
||||||
|
return new UntitledEnumItemsSchema { Type = type ?? "string", Enum = enumValues };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new JsonException("Items schema must have either 'enum' or 'anyOf' property.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
|
@ -317,20 +498,82 @@ public sealed class ElicitRequestParams
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnumSchema enumSchema:
|
case UntitledSingleSelectEnumSchema untitledSingleSelect:
|
||||||
if (enumSchema.Enum is not null)
|
if (untitledSingleSelect.Enum is not null)
|
||||||
{
|
{
|
||||||
writer.WritePropertyName("enum");
|
writer.WritePropertyName("enum");
|
||||||
JsonSerializer.Serialize(writer, enumSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString);
|
JsonSerializer.Serialize(writer, untitledSingleSelect.Enum, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
}
|
}
|
||||||
if (enumSchema.EnumNames is not null)
|
if (untitledSingleSelect.Default is not null)
|
||||||
|
{
|
||||||
|
writer.WriteString("default", untitledSingleSelect.Default);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TitledSingleSelectEnumSchema titledSingleSelect:
|
||||||
|
if (titledSingleSelect.OneOf is not null && titledSingleSelect.OneOf.Count > 0)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName("oneOf");
|
||||||
|
SerializeEnumOptions(writer, titledSingleSelect.OneOf);
|
||||||
|
}
|
||||||
|
if (titledSingleSelect.Default is not null)
|
||||||
|
{
|
||||||
|
writer.WriteString("default", titledSingleSelect.Default);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UntitledMultiSelectEnumSchema untitledMultiSelect:
|
||||||
|
if (untitledMultiSelect.MinItems.HasValue)
|
||||||
|
{
|
||||||
|
writer.WriteNumber("minItems", untitledMultiSelect.MinItems.Value);
|
||||||
|
}
|
||||||
|
if (untitledMultiSelect.MaxItems.HasValue)
|
||||||
|
{
|
||||||
|
writer.WriteNumber("maxItems", untitledMultiSelect.MaxItems.Value);
|
||||||
|
}
|
||||||
|
writer.WritePropertyName("items");
|
||||||
|
SerializeUntitledEnumItemsSchema(writer, untitledMultiSelect.Items);
|
||||||
|
if (untitledMultiSelect.Default is not null)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName("default");
|
||||||
|
JsonSerializer.Serialize(writer, untitledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TitledMultiSelectEnumSchema titledMultiSelect:
|
||||||
|
if (titledMultiSelect.MinItems.HasValue)
|
||||||
|
{
|
||||||
|
writer.WriteNumber("minItems", titledMultiSelect.MinItems.Value);
|
||||||
|
}
|
||||||
|
if (titledMultiSelect.MaxItems.HasValue)
|
||||||
|
{
|
||||||
|
writer.WriteNumber("maxItems", titledMultiSelect.MaxItems.Value);
|
||||||
|
}
|
||||||
|
writer.WritePropertyName("items");
|
||||||
|
SerializeTitledEnumItemsSchema(writer, titledMultiSelect.Items);
|
||||||
|
if (titledMultiSelect.Default is not null)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName("default");
|
||||||
|
JsonSerializer.Serialize(writer, titledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
case LegacyTitledEnumSchema legacyEnum:
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
if (legacyEnum.Enum is not null)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName("enum");
|
||||||
|
JsonSerializer.Serialize(writer, legacyEnum.Enum, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
}
|
||||||
|
if (legacyEnum.EnumNames is not null)
|
||||||
{
|
{
|
||||||
writer.WritePropertyName("enumNames");
|
writer.WritePropertyName("enumNames");
|
||||||
JsonSerializer.Serialize(writer, enumSchema.EnumNames, McpJsonUtilities.JsonContext.Default.IListString);
|
JsonSerializer.Serialize(writer, legacyEnum.EnumNames, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
}
|
}
|
||||||
if (enumSchema.Default is not null)
|
if (legacyEnum.Default is not null)
|
||||||
{
|
{
|
||||||
writer.WriteString("default", enumSchema.Default);
|
writer.WriteString("default", legacyEnum.Default);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -340,6 +583,36 @@ public sealed class ElicitRequestParams
|
||||||
|
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SerializeEnumOptions(Utf8JsonWriter writer, IList<EnumSchemaOption> options)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
foreach (var option in options)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteString("const", option.Const);
|
||||||
|
writer.WriteString("title", option.Title);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SerializeUntitledEnumItemsSchema(Utf8JsonWriter writer, UntitledEnumItemsSchema itemsSchema)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteString("type", itemsSchema.Type);
|
||||||
|
writer.WritePropertyName("enum");
|
||||||
|
JsonSerializer.Serialize(writer, itemsSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SerializeTitledEnumItemsSchema(Utf8JsonWriter writer, TitledEnumItemsSchema itemsSchema)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WritePropertyName("anyOf");
|
||||||
|
SerializeEnumOptions(writer, itemsSchema.AnyOf);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,8 +740,12 @@ public sealed class ElicitRequestParams
|
||||||
public bool? Default { get; set; }
|
public bool? Default { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Represents a schema for an enum type.</summary>
|
/// <summary>
|
||||||
public sealed class EnumSchema : PrimitiveSchemaDefinition
|
/// Represents a legacy schema for an enum type with enumNames.
|
||||||
|
/// This schema is deprecated in favor of <see cref="TitledSingleSelectEnumSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")]
|
||||||
|
public class LegacyTitledEnumSchema : PrimitiveSchemaDefinition
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[JsonPropertyName("type")]
|
[JsonPropertyName("type")]
|
||||||
|
|
@ -505,4 +782,196 @@ public sealed class ElicitRequestParams
|
||||||
[JsonPropertyName("default")]
|
[JsonPropertyName("default")]
|
||||||
public string? Default { get; set; }
|
public string? Default { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a schema for single-selection enumeration without display titles for options.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UntitledSingleSelectEnumSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public override string Type
|
||||||
|
{
|
||||||
|
get => "string";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not "string")
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type must be 'string'.", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the list of allowed string values for the enum.</summary>
|
||||||
|
[JsonPropertyName("enum")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public IList<string> Enum
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the default value for the enum.</summary>
|
||||||
|
[JsonPropertyName("default")]
|
||||||
|
public string? Default { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single option in a titled enum schema with a constant value and display title.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EnumSchemaOption
|
||||||
|
{
|
||||||
|
/// <summary>Gets or sets the constant value for this option.</summary>
|
||||||
|
[JsonPropertyName("const")]
|
||||||
|
public required string Const { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the display title for this option.</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public required string Title { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a schema for single-selection enumeration with display titles for each option.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TitledSingleSelectEnumSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public override string Type
|
||||||
|
{
|
||||||
|
get => "string";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not "string")
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type must be 'string'.", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the list of enum options with their constant values and display titles.</summary>
|
||||||
|
[JsonPropertyName("oneOf")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public IList<EnumSchemaOption> OneOf
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the default value for the enum.</summary>
|
||||||
|
[JsonPropertyName("default")]
|
||||||
|
public string? Default { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the items schema for untitled multi-select enum arrays.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UntitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
/// <summary>Gets or sets the type of the items.</summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = "string";
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the list of allowed string values.</summary>
|
||||||
|
[JsonPropertyName("enum")]
|
||||||
|
public required IList<string> Enum { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the items schema for titled multi-select enum arrays.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
/// <summary>Gets or sets the list of enum options with constant values and display titles.</summary>
|
||||||
|
[JsonPropertyName("anyOf")]
|
||||||
|
public required IList<EnumSchemaOption> AnyOf { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a schema for multiple-selection enumeration without display titles for options.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UntitledMultiSelectEnumSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public override string Type
|
||||||
|
{
|
||||||
|
get => "array";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not "array")
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type must be 'array'.", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the minimum number of items that can be selected.</summary>
|
||||||
|
[JsonPropertyName("minItems")]
|
||||||
|
public int? MinItems { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the maximum number of items that can be selected.</summary>
|
||||||
|
[JsonPropertyName("maxItems")]
|
||||||
|
public int? MaxItems { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the schema for items in the array.</summary>
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
public required UntitledEnumItemsSchema Items { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the default values for the enum.</summary>
|
||||||
|
[JsonPropertyName("default")]
|
||||||
|
public IList<string>? Default { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a schema for multiple-selection enumeration with display titles for each option.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TitledMultiSelectEnumSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public override string Type
|
||||||
|
{
|
||||||
|
get => "array";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not "array")
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type must be 'array'.", nameof(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the minimum number of items that can be selected.</summary>
|
||||||
|
[JsonPropertyName("minItems")]
|
||||||
|
public int? MinItems { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the maximum number of items that can be selected.</summary>
|
||||||
|
[JsonPropertyName("maxItems")]
|
||||||
|
public int? MaxItems { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the schema for items in the array.</summary>
|
||||||
|
[JsonPropertyName("items")]
|
||||||
|
public required TitledEnumItemsSchema Items { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the default values for the enum.</summary>
|
||||||
|
[JsonPropertyName("default")]
|
||||||
|
public IList<string>? Default { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a schema for an enum type. This is a compatibility alias for <see cref="LegacyTitledEnumSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use UntitledSingleSelectEnumSchema or TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")]
|
||||||
|
public sealed class EnumSchema : LegacyTitledEnumSchema
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ public sealed class ElicitResult : Result
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Values in the dictionary should be of types <see cref="JsonValueKind.String"/>, <see cref="JsonValueKind.Number"/>,
|
/// Values in the dictionary should be of types <see cref="JsonValueKind.String"/>, <see cref="JsonValueKind.Number"/>,
|
||||||
/// <see cref="JsonValueKind.True"/>, or <see cref="JsonValueKind.False"/>.
|
/// <see cref="JsonValueKind.True"/>, <see cref="JsonValueKind.False"/>, or <see cref="JsonValueKind.Array"/> (for multi-select enums).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
namespace ModelContextProtocol.Tests.Protocol;
|
namespace ModelContextProtocol.Tests.Protocol;
|
||||||
|
|
||||||
public class ElicitationDefaultValuesTests
|
public class ElicitationDefaultValuesTests
|
||||||
|
|
@ -259,7 +261,8 @@ public class ElicitationDefaultValuesTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(deserialized);
|
Assert.NotNull(deserialized);
|
||||||
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(deserialized);
|
// EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema
|
||||||
|
var enumSchema = Assert.IsType<ElicitRequestParams.UntitledSingleSelectEnumSchema>(deserialized);
|
||||||
Assert.Equal("draft", enumSchema.Default);
|
Assert.Equal("draft", enumSchema.Default);
|
||||||
Assert.Equal(["draft", "published", "archived"], enumSchema.Enum);
|
Assert.Equal(["draft", "published", "archived"], enumSchema.Enum);
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +329,8 @@ public class ElicitationDefaultValuesTests
|
||||||
var activeSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(deserialized.RequestedSchema.Properties["active"]);
|
var activeSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(deserialized.RequestedSchema.Properties["active"]);
|
||||||
Assert.True(activeSchema.Default);
|
Assert.True(activeSchema.Default);
|
||||||
|
|
||||||
var statusSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(deserialized.RequestedSchema.Properties["status"]);
|
// EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema
|
||||||
|
var statusSchema = Assert.IsType<ElicitRequestParams.UntitledSingleSelectEnumSchema>(deserialized.RequestedSchema.Properties["status"]);
|
||||||
Assert.Equal("active", statusSchema.Default);
|
Assert.Equal("active", statusSchema.Default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ using ModelContextProtocol.Client;
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
namespace ModelContextProtocol.Tests.Configuration;
|
namespace ModelContextProtocol.Tests.Configuration;
|
||||||
|
|
||||||
public partial class ElicitationTests : ClientServerTestBase
|
public partial class ElicitationTests : ClientServerTestBase
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ using ModelContextProtocol.Protocol;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
namespace ModelContextProtocol.Tests.Configuration;
|
namespace ModelContextProtocol.Tests.Configuration;
|
||||||
|
|
||||||
public partial class ElicitationTypedTests : ClientServerTestBase
|
public partial class ElicitationTypedTests : ClientServerTestBase
|
||||||
|
|
@ -147,7 +149,7 @@ public partial class ElicitationTypedTests : ClientServerTestBase
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nameof(SampleForm.Role):
|
case nameof(SampleForm.Role):
|
||||||
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(value);
|
var enumSchema = Assert.IsType<ElicitRequestParams.UntitledSingleSelectEnumSchema>(value);
|
||||||
Assert.Equal("string", enumSchema.Type);
|
Assert.Equal("string", enumSchema.Type);
|
||||||
Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum);
|
Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
309
tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs
Normal file
309
tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
using ModelContextProtocol.Protocol;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Tests.Protocol;
|
||||||
|
|
||||||
|
public class EnumSchemaTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UntitledSingleSelectEnumSchema_Serializes_Correctly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var schema = new ElicitRequestParams.UntitledSingleSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Priority",
|
||||||
|
Description = "Task priority level",
|
||||||
|
Enum = ["low", "medium", "high"],
|
||||||
|
Default = "medium"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string json = JsonSerializer.Serialize<ElicitRequestParams.PrimitiveSchemaDefinition>(schema, McpJsonUtilities.DefaultOptions);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.UntitledSingleSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("string", result.Type);
|
||||||
|
Assert.Equal("Priority", result.Title);
|
||||||
|
Assert.Equal("Task priority level", result.Description);
|
||||||
|
Assert.Equal(["low", "medium", "high"], result.Enum);
|
||||||
|
Assert.Equal("medium", result.Default);
|
||||||
|
Assert.Contains("\"type\":\"string\"", json);
|
||||||
|
Assert.Contains("\"enum\":[\"low\",\"medium\",\"high\"]", json);
|
||||||
|
Assert.DoesNotContain("enumNames", json);
|
||||||
|
Assert.DoesNotContain("oneOf", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TitledSingleSelectEnumSchema_Serializes_Correctly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var schema = new ElicitRequestParams.TitledSingleSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Severity",
|
||||||
|
Description = "Issue severity",
|
||||||
|
OneOf =
|
||||||
|
[
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "critical", Title = "Critical" },
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "high", Title = "High Priority" },
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "low", Title = "Low Priority" }
|
||||||
|
],
|
||||||
|
Default = "high"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string json = JsonSerializer.Serialize<ElicitRequestParams.PrimitiveSchemaDefinition>(schema, McpJsonUtilities.DefaultOptions);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.TitledSingleSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("string", result.Type);
|
||||||
|
Assert.Equal("Severity", result.Title);
|
||||||
|
Assert.Equal("Issue severity", result.Description);
|
||||||
|
Assert.Equal(3, result.OneOf.Count);
|
||||||
|
Assert.Equal("critical", result.OneOf[0].Const);
|
||||||
|
Assert.Equal("Critical", result.OneOf[0].Title);
|
||||||
|
Assert.Equal("high", result.Default);
|
||||||
|
Assert.Contains("\"oneOf\":", json);
|
||||||
|
Assert.Contains("\"const\":\"critical\"", json);
|
||||||
|
Assert.Contains("\"title\":\"Critical\"", json);
|
||||||
|
Assert.DoesNotContain("enum\":", json);
|
||||||
|
Assert.DoesNotContain("enumNames", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UntitledMultiSelectEnumSchema_Serializes_Correctly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var schema = new ElicitRequestParams.UntitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Tags",
|
||||||
|
Description = "Select multiple tags",
|
||||||
|
MinItems = 1,
|
||||||
|
MaxItems = 3,
|
||||||
|
Items = new ElicitRequestParams.UntitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Enum = ["bug", "feature", "documentation", "enhancement"]
|
||||||
|
},
|
||||||
|
Default = ["bug", "feature"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string json = JsonSerializer.Serialize<ElicitRequestParams.PrimitiveSchemaDefinition>(schema, McpJsonUtilities.DefaultOptions);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.UntitledMultiSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("array", result.Type);
|
||||||
|
Assert.Equal("Tags", result.Title);
|
||||||
|
Assert.Equal("Select multiple tags", result.Description);
|
||||||
|
Assert.Equal(1, result.MinItems);
|
||||||
|
Assert.Equal(3, result.MaxItems);
|
||||||
|
Assert.NotNull(result.Items);
|
||||||
|
Assert.Equal("string", result.Items.Type);
|
||||||
|
Assert.Equal(["bug", "feature", "documentation", "enhancement"], result.Items.Enum);
|
||||||
|
Assert.Equal(["bug", "feature"], result.Default);
|
||||||
|
Assert.Contains("\"type\":\"array\"", json);
|
||||||
|
Assert.Contains("\"minItems\":1", json);
|
||||||
|
Assert.Contains("\"maxItems\":3", json);
|
||||||
|
Assert.Contains("\"items\":", json);
|
||||||
|
Assert.DoesNotContain("anyOf", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TitledMultiSelectEnumSchema_Serializes_Correctly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var schema = new ElicitRequestParams.TitledMultiSelectEnumSchema
|
||||||
|
{
|
||||||
|
Title = "Features",
|
||||||
|
Description = "Select desired features",
|
||||||
|
MinItems = 2,
|
||||||
|
Items = new ElicitRequestParams.TitledEnumItemsSchema
|
||||||
|
{
|
||||||
|
AnyOf =
|
||||||
|
[
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "auth", Title = "Authentication" },
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "api", Title = "REST API" },
|
||||||
|
new ElicitRequestParams.EnumSchemaOption { Const = "ui", Title = "User Interface" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
Default = ["auth", "api"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string json = JsonSerializer.Serialize<ElicitRequestParams.PrimitiveSchemaDefinition>(schema, McpJsonUtilities.DefaultOptions);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.TitledMultiSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("array", result.Type);
|
||||||
|
Assert.Equal("Features", result.Title);
|
||||||
|
Assert.Equal("Select desired features", result.Description);
|
||||||
|
Assert.Equal(2, result.MinItems);
|
||||||
|
Assert.NotNull(result.Items);
|
||||||
|
Assert.NotNull(result.Items.AnyOf);
|
||||||
|
Assert.Equal(3, result.Items.AnyOf.Count);
|
||||||
|
Assert.Equal("auth", result.Items.AnyOf[0].Const);
|
||||||
|
Assert.Equal("Authentication", result.Items.AnyOf[0].Title);
|
||||||
|
Assert.Equal(["auth", "api"], result.Default);
|
||||||
|
Assert.Contains("\"type\":\"array\"", json);
|
||||||
|
Assert.Contains("\"anyOf\":", json);
|
||||||
|
Assert.Contains("\"const\":\"auth\"", json);
|
||||||
|
Assert.Contains("\"title\":\"Authentication\"", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LegacyTitledEnumSchema_WithEnumNames_Deserializes_As_EnumSchema()
|
||||||
|
{
|
||||||
|
// Arrange - JSON with enumNames should deserialize as EnumSchema for backward compatibility
|
||||||
|
string json = """
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"title": "Status",
|
||||||
|
"enum": ["active", "inactive", "pending"],
|
||||||
|
"enumNames": ["Active", "Inactive", "Pending"],
|
||||||
|
"default": "active"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.EnumSchema>(deserialized);
|
||||||
|
Assert.Equal("string", result.Type);
|
||||||
|
Assert.Equal("Status", result.Title);
|
||||||
|
Assert.Equal(["active", "inactive", "pending"], result.Enum);
|
||||||
|
Assert.Equal(["Active", "Inactive", "Pending"], result.EnumNames);
|
||||||
|
Assert.Equal("active", result.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnumSchema_WithoutEnumNames_Deserializes_As_UntitledSingleSelect()
|
||||||
|
{
|
||||||
|
// Arrange - JSON without enumNames should deserialize as UntitledSingleSelectEnumSchema
|
||||||
|
string json = """
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"title": "Status",
|
||||||
|
"enum": ["draft", "published", "archived"],
|
||||||
|
"default": "draft"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.UntitledSingleSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("string", result.Type);
|
||||||
|
Assert.Equal("Status", result.Title);
|
||||||
|
Assert.Equal(["draft", "published", "archived"], result.Enum);
|
||||||
|
Assert.Equal("draft", result.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnumSchema_WithOneOf_Deserializes_As_TitledSingleSelect()
|
||||||
|
{
|
||||||
|
// Arrange - JSON with oneOf should deserialize as TitledSingleSelectEnumSchema
|
||||||
|
string json = """
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"title": "Priority",
|
||||||
|
"oneOf": [
|
||||||
|
{ "const": "p0", "title": "P0 - Critical" },
|
||||||
|
{ "const": "p1", "title": "P1 - High" },
|
||||||
|
{ "const": "p2", "title": "P2 - Medium" }
|
||||||
|
],
|
||||||
|
"default": "p1"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.TitledSingleSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("string", result.Type);
|
||||||
|
Assert.Equal("Priority", result.Title);
|
||||||
|
Assert.Equal(3, result.OneOf.Count);
|
||||||
|
Assert.Equal("p0", result.OneOf[0].Const);
|
||||||
|
Assert.Equal("P0 - Critical", result.OneOf[0].Title);
|
||||||
|
Assert.Equal("p1", result.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiSelectEnum_WithEnum_Deserializes_As_UntitledMultiSelect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string json = """
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"title": "Categories",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["tech", "business", "lifestyle"]
|
||||||
|
},
|
||||||
|
"default": ["tech"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.UntitledMultiSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("array", result.Type);
|
||||||
|
Assert.Equal("Categories", result.Title);
|
||||||
|
Assert.NotNull(result.Items);
|
||||||
|
Assert.Equal(["tech", "business", "lifestyle"], result.Items.Enum);
|
||||||
|
Assert.Equal(["tech"], result.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiSelectEnum_WithAnyOf_Deserializes_As_TitledMultiSelect()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string json = """
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"title": "Roles",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{ "const": "admin", "title": "Administrator" },
|
||||||
|
{ "const": "user", "title": "User" },
|
||||||
|
{ "const": "guest", "title": "Guest" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": ["user"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ElicitRequestParams.PrimitiveSchemaDefinition>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var result = Assert.IsType<ElicitRequestParams.TitledMultiSelectEnumSchema>(deserialized);
|
||||||
|
Assert.Equal("array", result.Type);
|
||||||
|
Assert.Equal("Roles", result.Title);
|
||||||
|
Assert.NotNull(result.Items);
|
||||||
|
Assert.NotNull(result.Items.AnyOf);
|
||||||
|
Assert.Equal(3, result.Items.AnyOf.Count);
|
||||||
|
Assert.Equal("admin", result.Items.AnyOf[0].Const);
|
||||||
|
Assert.Equal("Administrator", result.Items.AnyOf[0].Title);
|
||||||
|
Assert.Equal(["user"], result.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
namespace ModelContextProtocol.Tests.Protocol;
|
namespace ModelContextProtocol.Tests.Protocol;
|
||||||
|
|
||||||
public static class PrimitiveSchemaDefinitionTests
|
public static class PrimitiveSchemaDefinitionTests
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue