Add missing Title and Icons properties to ResourceLinkBlock (#1320)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
parent
d9ae6bc266
commit
54283a8590
2 changed files with 102 additions and 1 deletions
|
|
@ -90,11 +90,13 @@ public abstract class ContentBlock
|
||||||
string? type = null;
|
string? type = null;
|
||||||
string? text = null;
|
string? text = null;
|
||||||
string? name = null;
|
string? name = null;
|
||||||
|
string? title = null;
|
||||||
ReadOnlyMemory<byte>? data = null;
|
ReadOnlyMemory<byte>? data = null;
|
||||||
string? mimeType = null;
|
string? mimeType = null;
|
||||||
string? uri = null;
|
string? uri = null;
|
||||||
string? description = null;
|
string? description = null;
|
||||||
long? size = null;
|
long? size = null;
|
||||||
|
IList<Icon>? icons = null;
|
||||||
ResourceContents? resource = null;
|
ResourceContents? resource = null;
|
||||||
Annotations? annotations = null;
|
Annotations? annotations = null;
|
||||||
JsonObject? meta = null;
|
JsonObject? meta = null;
|
||||||
|
|
@ -130,6 +132,10 @@ public abstract class ContentBlock
|
||||||
name = reader.GetString();
|
name = reader.GetString();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "title":
|
||||||
|
title = reader.GetString();
|
||||||
|
break;
|
||||||
|
|
||||||
case "data":
|
case "data":
|
||||||
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
|
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
|
||||||
break;
|
break;
|
||||||
|
|
@ -150,6 +156,18 @@ public abstract class ContentBlock
|
||||||
size = reader.GetInt64();
|
size = reader.GetInt64();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "icons":
|
||||||
|
if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
{
|
||||||
|
icons = [];
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
icons.Add(JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Icon) ??
|
||||||
|
throw new JsonException("Unexpected null item in icons array."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "resource":
|
case "resource":
|
||||||
resource = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.ResourceContents);
|
resource = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.ResourceContents);
|
||||||
break;
|
break;
|
||||||
|
|
@ -233,9 +251,11 @@ public abstract class ContentBlock
|
||||||
{
|
{
|
||||||
Uri = uri ?? throw new JsonException("URI must be provided for 'resource_link' type."),
|
Uri = uri ?? throw new JsonException("URI must be provided for 'resource_link' type."),
|
||||||
Name = name ?? throw new JsonException("Name must be provided for 'resource_link' type."),
|
Name = name ?? throw new JsonException("Name must be provided for 'resource_link' type."),
|
||||||
|
Title = title,
|
||||||
Description = description,
|
Description = description,
|
||||||
MimeType = mimeType,
|
MimeType = mimeType,
|
||||||
Size = size,
|
Size = size,
|
||||||
|
Icons = icons,
|
||||||
},
|
},
|
||||||
|
|
||||||
"tool_use" => new ToolUseContentBlock
|
"tool_use" => new ToolUseContentBlock
|
||||||
|
|
@ -299,6 +319,10 @@ public abstract class ContentBlock
|
||||||
case ResourceLinkBlock resourceLink:
|
case ResourceLinkBlock resourceLink:
|
||||||
writer.WriteString("uri", resourceLink.Uri);
|
writer.WriteString("uri", resourceLink.Uri);
|
||||||
writer.WriteString("name", resourceLink.Name);
|
writer.WriteString("name", resourceLink.Name);
|
||||||
|
if (resourceLink.Title is not null)
|
||||||
|
{
|
||||||
|
writer.WriteString("title", resourceLink.Title);
|
||||||
|
}
|
||||||
if (resourceLink.Description is not null)
|
if (resourceLink.Description is not null)
|
||||||
{
|
{
|
||||||
writer.WriteString("description", resourceLink.Description);
|
writer.WriteString("description", resourceLink.Description);
|
||||||
|
|
@ -311,6 +335,16 @@ public abstract class ContentBlock
|
||||||
{
|
{
|
||||||
writer.WriteNumber("size", resourceLink.Size.Value);
|
writer.WriteNumber("size", resourceLink.Size.Value);
|
||||||
}
|
}
|
||||||
|
if (resourceLink.Icons is { Count: > 0 })
|
||||||
|
{
|
||||||
|
writer.WritePropertyName("icons");
|
||||||
|
writer.WriteStartArray();
|
||||||
|
foreach (var icon in resourceLink.Icons)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, icon, McpJsonUtilities.JsonContext.Default.Icon);
|
||||||
|
}
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ToolUseContentBlock toolUse:
|
case ToolUseContentBlock toolUse:
|
||||||
|
|
@ -595,6 +629,17 @@ public sealed class ResourceLinkBlock : ContentBlock
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a title for this resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is intended for UI and end-user contexts. It is optimized to be human-readable and easily understood,
|
||||||
|
/// even by those unfamiliar with domain-specific terminology.
|
||||||
|
/// If not provided, <see cref="Name"/> can be used for display.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a description of what this resource represents.
|
/// Gets or sets a description of what this resource represents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -638,6 +683,15 @@ public sealed class ResourceLinkBlock : ContentBlock
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("size")]
|
[JsonPropertyName("size")]
|
||||||
public long? Size { get; set; }
|
public long? Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional list of icons for this resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the resource's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("icons")]
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Represents a request from the assistant to call a tool.</summary>
|
/// <summary>Represents a request from the assistant to call a tool.</summary>
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ public class ContentBlockTests
|
||||||
{
|
{
|
||||||
Uri = "https://example.com/resource",
|
Uri = "https://example.com/resource",
|
||||||
Name = "Test Resource",
|
Name = "Test Resource",
|
||||||
|
Title = "Test Resource Title",
|
||||||
Description = "A test resource for validation",
|
Description = "A test resource for validation",
|
||||||
MimeType = "text/plain",
|
MimeType = "text/plain",
|
||||||
Size = 1024
|
Size = 1024,
|
||||||
|
Icons = [new Icon { Source = "https://example.com/icon.png", MimeType = "image/png" }]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act - Serialize to JSON
|
// Act - Serialize to JSON
|
||||||
|
|
@ -30,10 +32,15 @@ public class ContentBlockTests
|
||||||
|
|
||||||
Assert.Equal(original.Uri, resourceLink.Uri);
|
Assert.Equal(original.Uri, resourceLink.Uri);
|
||||||
Assert.Equal(original.Name, resourceLink.Name);
|
Assert.Equal(original.Name, resourceLink.Name);
|
||||||
|
Assert.Equal(original.Title, resourceLink.Title);
|
||||||
Assert.Equal(original.Description, resourceLink.Description);
|
Assert.Equal(original.Description, resourceLink.Description);
|
||||||
Assert.Equal(original.MimeType, resourceLink.MimeType);
|
Assert.Equal(original.MimeType, resourceLink.MimeType);
|
||||||
Assert.Equal(original.Size, resourceLink.Size);
|
Assert.Equal(original.Size, resourceLink.Size);
|
||||||
Assert.Equal("resource_link", resourceLink.Type);
|
Assert.Equal("resource_link", resourceLink.Type);
|
||||||
|
Assert.NotNull(resourceLink.Icons);
|
||||||
|
Assert.Single(resourceLink.Icons);
|
||||||
|
Assert.Equal("https://example.com/icon.png", resourceLink.Icons[0].Source);
|
||||||
|
Assert.Equal("image/png", resourceLink.Icons[0].MimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -57,9 +64,11 @@ public class ContentBlockTests
|
||||||
|
|
||||||
Assert.Equal("https://example.com/minimal", resourceLink.Uri);
|
Assert.Equal("https://example.com/minimal", resourceLink.Uri);
|
||||||
Assert.Equal("Minimal Resource", resourceLink.Name);
|
Assert.Equal("Minimal Resource", resourceLink.Name);
|
||||||
|
Assert.Null(resourceLink.Title);
|
||||||
Assert.Null(resourceLink.Description);
|
Assert.Null(resourceLink.Description);
|
||||||
Assert.Null(resourceLink.MimeType);
|
Assert.Null(resourceLink.MimeType);
|
||||||
Assert.Null(resourceLink.Size);
|
Assert.Null(resourceLink.Size);
|
||||||
|
Assert.Null(resourceLink.Icons);
|
||||||
Assert.Equal("resource_link", resourceLink.Type);
|
Assert.Equal("resource_link", resourceLink.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +90,44 @@ public class ContentBlockTests
|
||||||
Assert.Contains("Name must be provided for 'resource_link' type", exception.Message);
|
Assert.Contains("Name must be provided for 'resource_link' type", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResourceLinkBlock_DeserializationWithTitleAndIcons_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange - JSON with title and icons properties per spec
|
||||||
|
const string Json = """
|
||||||
|
{
|
||||||
|
"type": "resource_link",
|
||||||
|
"uri": "https://example.com/resource",
|
||||||
|
"name": "my-resource",
|
||||||
|
"title": "My Resource",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "https://example.com/icon1.png", "mimeType": "image/png", "sizes": ["48x48"], "theme": "light" },
|
||||||
|
{ "src": "https://example.com/icon2.svg", "mimeType": "image/svg+xml" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ContentBlock>(Json, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var resourceLink = Assert.IsType<ResourceLinkBlock>(deserialized);
|
||||||
|
|
||||||
|
Assert.Equal("https://example.com/resource", resourceLink.Uri);
|
||||||
|
Assert.Equal("my-resource", resourceLink.Name);
|
||||||
|
Assert.Equal("My Resource", resourceLink.Title);
|
||||||
|
Assert.NotNull(resourceLink.Icons);
|
||||||
|
Assert.Equal(2, resourceLink.Icons.Count);
|
||||||
|
Assert.Equal("https://example.com/icon1.png", resourceLink.Icons[0].Source);
|
||||||
|
Assert.Equal("image/png", resourceLink.Icons[0].MimeType);
|
||||||
|
Assert.NotNull(resourceLink.Icons[0].Sizes);
|
||||||
|
Assert.Equal("48x48", resourceLink.Icons[0].Sizes![0]);
|
||||||
|
Assert.Equal("light", resourceLink.Icons[0].Theme);
|
||||||
|
Assert.Equal("https://example.com/icon2.svg", resourceLink.Icons[1].Source);
|
||||||
|
Assert.Equal("image/svg+xml", resourceLink.Icons[1].MimeType);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Deserialize_IgnoresUnknownArrayProperty()
|
public void Deserialize_IgnoresUnknownArrayProperty()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue