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:
Copilot 2025-11-19 22:51:02 -05:00 • committed by GitHub
commit 83098e7cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 963 additions and 20 deletions

View file

@ -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.
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.
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.
To help distinguish multiple inputs, each input has a unique name.

View file

@ -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>
}

View file

@ -21,7 +21,10 @@ public sealed class ElicitRequestParams
/// Gets or sets the requested schema.
/// </summary>
/// <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>
[JsonPropertyName("requestedSchema")]
[field: MaybeNull]
@ -59,7 +62,10 @@ public sealed class ElicitRequestParams
/// <summary>
/// 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>
[JsonConverter(typeof(Converter))]
public abstract class PrimitiveSchemaDefinition
@ -113,8 +119,13 @@ public sealed class ElicitRequestParams
bool? defaultBool = null;
double? defaultNumber = null;
string? defaultString = null;
IList<string>? defaultStringArray = null;
IList<string>? enumValues = 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)
{
@ -161,6 +172,14 @@ public sealed class ElicitRequestParams
maximum = reader.GetDouble();
break;
case "minItems":
minItems = reader.GetInt32();
break;
case "maxItems":
maxItems = reader.GetInt32();
break;
case "default":
// We need to handle different types for default values
// Store the value based on the JSON token type
@ -178,6 +197,9 @@ public sealed class ElicitRequestParams
case JsonTokenType.String:
defaultString = reader.GetString();
break;
case JsonTokenType.StartArray:
defaultStringArray = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
break;
}
break;
@ -189,6 +211,14 @@ public sealed class ElicitRequestParams
enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
break;
case "oneOf":
oneOf = DeserializeEnumOptions(ref reader);
break;
case "items":
items = DeserializeEnumItemsSchema(ref reader);
break;
default:
reader.Skip();
break;
@ -204,15 +234,39 @@ public sealed class ElicitRequestParams
switch (type)
{
case "string":
if (enumValues is not null)
if (oneOf is not null)
{
psd = new EnumSchema
// TitledSingleSelectEnumSchema
psd = new TitledSingleSelectEnumSchema
{
Enum = enumValues,
EnumNames = enumNames,
OneOf = oneOf,
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
{
psd = new StringSchema
@ -225,6 +279,31 @@ public sealed class ElicitRequestParams
}
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 "number":
psd = new NumberSchema
@ -253,6 +332,108 @@ public sealed class ElicitRequestParams
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/>
public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options)
{
@ -317,20 +498,82 @@ public sealed class ElicitRequestParams
}
break;
case EnumSchema enumSchema:
if (enumSchema.Enum is not null)
case UntitledSingleSelectEnumSchema untitledSingleSelect:
if (untitledSingleSelect.Enum is not null)
{
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");
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;
@ -340,6 +583,36 @@ public sealed class ElicitRequestParams
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; }
}
/// <summary>Represents a schema for an enum type.</summary>
public sealed class EnumSchema : PrimitiveSchemaDefinition
/// <summary>
/// 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/>
[JsonPropertyName("type")]
@ -505,4 +782,196 @@ public sealed class ElicitRequestParams
[JsonPropertyName("default")]
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
{
}
}

View file

@ -51,7 +51,7 @@ public sealed class ElicitResult : Result
/// </para>
/// <para>
/// 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>
/// </remarks>
[JsonPropertyName("content")]

View file

@ -1,6 +1,8 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
#pragma warning disable CS0618 // Type or member is obsolete
namespace ModelContextProtocol.Tests.Protocol;
public class ElicitationDefaultValuesTests
@ -259,7 +261,8 @@ public class ElicitationDefaultValuesTests
// Assert
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", "published", "archived"], enumSchema.Enum);
}
@ -326,7 +329,8 @@ public class ElicitationDefaultValuesTests
var activeSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(deserialized.RequestedSchema.Properties["active"]);
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);
}
}

View file

@ -3,6 +3,8 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using System.Text.Json;
#pragma warning disable CS0618 // Type or member is obsolete
namespace ModelContextProtocol.Tests.Configuration;
public partial class ElicitationTests : ClientServerTestBase

View file

@ -4,6 +4,8 @@ using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Serialization;
#pragma warning disable CS0618 // Type or member is obsolete
namespace ModelContextProtocol.Tests.Configuration;
public partial class ElicitationTypedTests : ClientServerTestBase
@ -147,7 +149,7 @@ public partial class ElicitationTypedTests : ClientServerTestBase
break;
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([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum);
break;

View 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);
}
}

View file

@ -1,6 +1,8 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
#pragma warning disable CS0618 // Type or member is obsolete
namespace ModelContextProtocol.Tests.Protocol;
public static class PrimitiveSchemaDefinitionTests