Skip to content

Commit 068f041

Browse files
authored
Fix deserialization of PrimitiveSchemaDefinition (#537)
This is preventing clients from correctly handling elicitation requests.
1 parent db37495 commit 068f041

File tree

3 files changed

+425
-48
lines changed

3 files changed

+425
-48
lines changed

src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs

Lines changed: 276 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
13
using System.Diagnostics.CodeAnalysis;
4+
using System.Text.Json;
25
using System.Text.Json.Serialization;
36

47
namespace ModelContextProtocol.Protocol;
@@ -54,39 +57,273 @@ public IDictionary<string, PrimitiveSchemaDefinition> Properties
5457
public IList<string>? Required { get; set; }
5558
}
5659

57-
5860
/// <summary>
5961
/// Represents restricted subset of JSON Schema:
6062
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
6163
/// </summary>
62-
[JsonDerivedType(typeof(BooleanSchema))]
63-
[JsonDerivedType(typeof(EnumSchema))]
64-
[JsonDerivedType(typeof(NumberSchema))]
65-
[JsonDerivedType(typeof(StringSchema))]
64+
[JsonConverter(typeof(Converter))] // TODO: This converter exists due to the lack of downlevel support for AllowOutOfOrderMetadataProperties.
6665
public abstract class PrimitiveSchemaDefinition
6766
{
6867
/// <summary>Prevent external derivations.</summary>
6968
protected private PrimitiveSchemaDefinition()
7069
{
7170
}
72-
}
7371

74-
/// <summary>Represents a schema for a string type.</summary>
75-
public sealed class StringSchema : PrimitiveSchemaDefinition
76-
{
7772
/// <summary>Gets the type of the schema.</summary>
78-
/// <remarks>This is always "string".</remarks>
7973
[JsonPropertyName("type")]
80-
public string Type => "string";
74+
public abstract string Type { get; set; }
8175

82-
/// <summary>Gets or sets a title for the string.</summary>
76+
/// <summary>Gets or sets a title for the schema.</summary>
8377
[JsonPropertyName("title")]
8478
public string? Title { get; set; }
8579

86-
/// <summary>Gets or sets a description for the string.</summary>
80+
/// <summary>Gets or sets a description for the schema.</summary>
8781
[JsonPropertyName("description")]
8882
public string? Description { get; set; }
8983

84+
/// <summary>
85+
/// Provides a <see cref="JsonConverter"/> for <see cref="ResourceContents"/>.
86+
/// </summary>
87+
[EditorBrowsable(EditorBrowsableState.Never)]
88+
public class Converter : JsonConverter<PrimitiveSchemaDefinition>
89+
{
90+
/// <inheritdoc/>
91+
public override PrimitiveSchemaDefinition? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
92+
{
93+
if (reader.TokenType == JsonTokenType.Null)
94+
{
95+
return null;
96+
}
97+
98+
if (reader.TokenType != JsonTokenType.StartObject)
99+
{
100+
throw new JsonException();
101+
}
102+
103+
string? type = null;
104+
string? title = null;
105+
string? description = null;
106+
int? minLength = null;
107+
int? maxLength = null;
108+
string? format = null;
109+
double? minimum = null;
110+
double? maximum = null;
111+
bool? defaultBool = null;
112+
IList<string>? enumValues = null;
113+
IList<string>? enumNames = null;
114+
115+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
116+
{
117+
if (reader.TokenType != JsonTokenType.PropertyName)
118+
{
119+
continue;
120+
}
121+
122+
string? propertyName = reader.GetString();
123+
bool success = reader.Read();
124+
Debug.Assert(success, "STJ must have buffered the entire object for us.");
125+
126+
switch (propertyName)
127+
{
128+
case "type":
129+
type = reader.GetString();
130+
break;
131+
132+
case "title":
133+
title = reader.GetString();
134+
break;
135+
136+
case "description":
137+
description = reader.GetString();
138+
break;
139+
140+
case "minLength":
141+
minLength = reader.GetInt32();
142+
break;
143+
144+
case "maxLength":
145+
maxLength = reader.GetInt32();
146+
break;
147+
148+
case "format":
149+
format = reader.GetString();
150+
break;
151+
152+
case "minimum":
153+
minimum = reader.GetDouble();
154+
break;
155+
156+
case "maximum":
157+
maximum = reader.GetDouble();
158+
break;
159+
160+
case "default":
161+
defaultBool = reader.GetBoolean();
162+
break;
163+
164+
case "enum":
165+
enumValues = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
166+
break;
167+
168+
case "enumNames":
169+
enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString);
170+
break;
171+
172+
default:
173+
break;
174+
}
175+
}
176+
177+
if (type is null)
178+
{
179+
throw new JsonException("The 'type' property is required.");
180+
}
181+
182+
PrimitiveSchemaDefinition? psd = null;
183+
switch (type)
184+
{
185+
case "string":
186+
if (enumValues is not null)
187+
{
188+
psd = new EnumSchema
189+
{
190+
Enum = enumValues,
191+
EnumNames = enumNames
192+
};
193+
}
194+
else
195+
{
196+
psd = new StringSchema
197+
{
198+
MinLength = minLength,
199+
MaxLength = maxLength,
200+
Format = format,
201+
};
202+
}
203+
break;
204+
205+
case "integer":
206+
case "number":
207+
psd = new NumberSchema
208+
{
209+
Minimum = minimum,
210+
Maximum = maximum,
211+
};
212+
break;
213+
214+
case "boolean":
215+
psd = new BooleanSchema
216+
{
217+
Default = defaultBool,
218+
};
219+
break;
220+
}
221+
222+
if (psd is not null)
223+
{
224+
psd.Type = type;
225+
psd.Title = title;
226+
psd.Description = description;
227+
}
228+
229+
return psd;
230+
}
231+
232+
/// <inheritdoc/>
233+
public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options)
234+
{
235+
if (value is null)
236+
{
237+
writer.WriteNullValue();
238+
return;
239+
}
240+
241+
writer.WriteStartObject();
242+
243+
writer.WriteString("type", value.Type);
244+
if (value.Title is not null)
245+
{
246+
writer.WriteString("title", value.Title);
247+
}
248+
if (value.Description is not null)
249+
{
250+
writer.WriteString("description", value.Description);
251+
}
252+
253+
switch (value)
254+
{
255+
case StringSchema stringSchema:
256+
if (stringSchema.MinLength.HasValue)
257+
{
258+
writer.WriteNumber("minLength", stringSchema.MinLength.Value);
259+
}
260+
if (stringSchema.MaxLength.HasValue)
261+
{
262+
writer.WriteNumber("maxLength", stringSchema.MaxLength.Value);
263+
}
264+
if (stringSchema.Format is not null)
265+
{
266+
writer.WriteString("format", stringSchema.Format);
267+
}
268+
break;
269+
270+
case NumberSchema numberSchema:
271+
if (numberSchema.Minimum.HasValue)
272+
{
273+
writer.WriteNumber("minimum", numberSchema.Minimum.Value);
274+
}
275+
if (numberSchema.Maximum.HasValue)
276+
{
277+
writer.WriteNumber("maximum", numberSchema.Maximum.Value);
278+
}
279+
break;
280+
281+
case BooleanSchema booleanSchema:
282+
if (booleanSchema.Default.HasValue)
283+
{
284+
writer.WriteBoolean("default", booleanSchema.Default.Value);
285+
}
286+
break;
287+
288+
case EnumSchema enumSchema:
289+
if (enumSchema.Enum is not null)
290+
{
291+
writer.WritePropertyName("enum");
292+
JsonSerializer.Serialize(writer, enumSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString);
293+
}
294+
if (enumSchema.EnumNames is not null)
295+
{
296+
writer.WritePropertyName("enumNames");
297+
JsonSerializer.Serialize(writer, enumSchema.EnumNames, McpJsonUtilities.JsonContext.Default.IListString);
298+
}
299+
break;
300+
301+
default:
302+
throw new JsonException($"Unexpected schema type: {value.GetType().Name}");
303+
}
304+
305+
writer.WriteEndObject();
306+
}
307+
}
308+
}
309+
310+
/// <summary>Represents a schema for a string type.</summary>
311+
public sealed class StringSchema : PrimitiveSchemaDefinition
312+
{
313+
/// <inheritdoc/>
314+
[JsonPropertyName("type")]
315+
public override string Type
316+
{
317+
get => "string";
318+
set
319+
{
320+
if (value is not "string")
321+
{
322+
throw new ArgumentException("Type must be 'string'.", nameof(value));
323+
}
324+
}
325+
}
326+
90327
/// <summary>Gets or sets the minimum length for the string.</summary>
91328
[JsonPropertyName("minLength")]
92329
public int? MinLength
@@ -139,11 +376,9 @@ public string? Format
139376
/// <summary>Represents a schema for a number or integer type.</summary>
140377
public sealed class NumberSchema : PrimitiveSchemaDefinition
141378
{
142-
/// <summary>Gets the type of the schema.</summary>
143-
/// <remarks>This should be "number" or "integer".</remarks>
144-
[JsonPropertyName("type")]
379+
/// <inheritdoc/>
145380
[field: MaybeNull]
146-
public string Type
381+
public override string Type
147382
{
148383
get => field ??= "number";
149384
set
@@ -157,14 +392,6 @@ public string Type
157392
}
158393
}
159394

160-
/// <summary>Gets or sets a title for the number input.</summary>
161-
[JsonPropertyName("title")]
162-
public string? Title { get; set; }
163-
164-
/// <summary>Gets or sets a description for the number input.</summary>
165-
[JsonPropertyName("description")]
166-
public string? Description { get; set; }
167-
168395
/// <summary>Gets or sets the minimum allowed value.</summary>
169396
[JsonPropertyName("minimum")]
170397
public double? Minimum { get; set; }
@@ -177,18 +404,19 @@ public string Type
177404
/// <summary>Represents a schema for a Boolean type.</summary>
178405
public sealed class BooleanSchema : PrimitiveSchemaDefinition
179406
{
180-
/// <summary>Gets the type of the schema.</summary>
181-
/// <remarks>This is always "boolean".</remarks>
407+
/// <inheritdoc/>
182408
[JsonPropertyName("type")]
183-
public string Type => "boolean";
184-
185-
/// <summary>Gets or sets a title for the Boolean.</summary>
186-
[JsonPropertyName("title")]
187-
public string? Title { get; set; }
188-
189-
/// <summary>Gets or sets a description for the Boolean.</summary>
190-
[JsonPropertyName("description")]
191-
public string? Description { get; set; }
409+
public override string Type
410+
{
411+
get => "boolean";
412+
set
413+
{
414+
if (value is not "boolean")
415+
{
416+
throw new ArgumentException("Type must be 'boolean'.", nameof(value));
417+
}
418+
}
419+
}
192420

193421
/// <summary>Gets or sets the default value for the Boolean.</summary>
194422
[JsonPropertyName("default")]
@@ -198,18 +426,19 @@ public sealed class BooleanSchema : PrimitiveSchemaDefinition
198426
/// <summary>Represents a schema for an enum type.</summary>
199427
public sealed class EnumSchema : PrimitiveSchemaDefinition
200428
{
201-
/// <summary>Gets the type of the schema.</summary>
202-
/// <remarks>This is always "string".</remarks>
429+
/// <inheritdoc/>
203430
[JsonPropertyName("type")]
204-
public string Type => "string";
205-
206-
/// <summary>Gets or sets a title for the enum.</summary>
207-
[JsonPropertyName("title")]
208-
public string? Title { get; set; }
209-
210-
/// <summary>Gets or sets a description for the enum.</summary>
211-
[JsonPropertyName("description")]
212-
public string? Description { get; set; }
431+
public override string Type
432+
{
433+
get => "string";
434+
set
435+
{
436+
if (value is not "string")
437+
{
438+
throw new ArgumentException("Type must be 'string'.", nameof(value));
439+
}
440+
}
441+
}
213442

214443
/// <summary>Gets or sets the list of allowed string values for the enum.</summary>
215444
[JsonPropertyName("enum")]

0 commit comments

Comments
 (0)