Update content according to latest spec (#513)

* Update content according to latest spec

* Add Title to a bunch of types

* Add _meta to lots of types

* Address feedback, more cleanup, fix NAOT test failure
This commit is contained in:
Stephen Toub 2025-06-16 19:39:41 -04:00 • committed by GitHub
commit 2bcff315f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,5 +1,8 @@
using ModelContextProtocol.Client;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
@ -9,13 +12,6 @@ namespace ModelContextProtocol.Protocol;
/// </summary>
/// <remarks>
/// <para>
/// A Reference object identifies either a resource or a prompt:
/// </para>
/// <list type="bullet">
/// <item><description>For resource references, set <see cref="Type"/> to "ref/resource" and provide the <see cref="Uri"/> property.</description></item>
/// <item><description>For prompt references, set <see cref="Type"/> to "ref/prompt" and provide the <see cref="Name"/> property.</description></item>
/// </list>
/// <para>
/// References are commonly used with <see cref="McpClientExtensions.CompleteAsync"/> to request completion suggestions for arguments,
/// and with other methods that need to reference resources or prompts.
/// </para>
@ -23,8 +19,14 @@ namespace ModelContextProtocol.Protocol;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public class Reference
[JsonConverter(typeof(Converter))]
public abstract class Reference
{
/// <summary>Prevent external derivations.</summary>
private protected Reference()
{
}
/// <summary>
/// Gets or sets the type of content.
/// </summary>
@ -34,56 +36,157 @@ public class Reference
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// Provides a <see cref="JsonConverter"/> for <see cref="Reference"/>.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter<Reference>
{
/// <inheritdoc/>
public override Reference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
string? type = null;
string? name = null;
string? title = null;
string? uri = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
continue;
}
string? propertyName = reader.GetString();
bool success = reader.Read();
Debug.Assert(success, "STJ must have buffered the entire object for us.");
switch (propertyName)
{
case "type":
type = reader.GetString();
break;
case "name":
name = reader.GetString();
break;
case "uri":
uri = reader.GetString();
break;
default:
break;
}
}
// TODO: This converter exists due to the lack of downlevel support for AllowOutOfOrderMetadataProperties.
switch (type)
{
case "ref/prompt":
if (name is null)
{
throw new JsonException("Prompt references must have a 'name' property.");
}
return new PromptReference { Name = name, Title = title };
case "ref/resource":
if (uri is null)
{
throw new JsonException("Resource references must have a 'uri' property.");
}
return new ResourceTemplateReference { Uri = uri };
default:
throw new JsonException($"Unknown content type: '{type}'");
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, Reference value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
writer.WriteStartObject();
writer.WriteString("type", value.Type);
switch (value)
{
case PromptReference pr:
writer.WriteString("name", pr.Name);
if (pr.Title is not null)
{
writer.WriteString("title", pr.Title);
}
break;
case ResourceTemplateReference rtr:
writer.WriteString("uri", rtr.Uri);
break;
}
writer.WriteEndObject();
}
}
}
/// <summary>
/// Represents a reference to a prompt, identified by its name.
/// </summary>
public sealed class PromptReference : Reference, IBaseMetadata
{
/// <summary>
/// Initializes a new instance of the <see cref="PromptReference"/> class.
/// </summary>
public PromptReference() => Type = "ref/prompt";
/// <inheritdoc />
[JsonPropertyName("name")]
public required string Name { get; set; }
/// <inheritdoc />
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <inheritdoc />
public override string ToString() => $"\"{Type}\": \"{Name}\"";
}
/// <summary>
/// Represents a reference to a resource or resource template definition.
/// </summary>
public sealed class ResourceTemplateReference : Reference
{
/// <summary>
/// Initializes a new instance of the <see cref="ResourceTemplateReference"/> class.
/// </summary>
public ResourceTemplateReference() => Type = "ref/resource";
/// <summary>
/// Gets or sets the URI or URI template of the resource.
/// </summary>
[JsonPropertyName("uri")]
public string? Uri { get; set; }
/// <summary>
/// Gets or sets the name of the prompt or prompt template.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
[StringSyntax(StringSyntaxAttribute.Uri)]
public required string? Uri { get; set; }
/// <inheritdoc />
public override string ToString() => $"\"{Type}\": \"{Uri ?? Name}\"";
/// <summary>
/// Validates the reference object to ensure it contains the required properties for its type.
/// </summary>
/// <param name="validationMessage">When this method returns false, contains a message explaining why validation failed; otherwise, null.</param>
/// <returns>True if the reference is valid; otherwise, false.</returns>
/// <remarks>
/// For "ref/resource" type, the <see cref="Uri"/> property must not be null or empty.
/// For "ref/prompt" type, the <see cref="Name"/> property must not be null or empty.
/// </remarks>
public bool Validate([NotNullWhen(false)] out string? validationMessage)
{
switch (Type)
{
case "ref/resource":
if (string.IsNullOrEmpty(Uri))
{
validationMessage = "Uri is required for ref/resource";
return false;
}
break;
case "ref/prompt":
if (string.IsNullOrEmpty(Name))
{
validationMessage = "Name is required for ref/prompt";
return false;
}
break;
default:
validationMessage = $"Unknown reference type: {Type}";
return false;
}
validationMessage = null;
return true;
}
}
public override string ToString() => $"\"{Type}\": \"{Uri}\"";
}