Skip to content

Commit 54283a8

Browse files
Copilotstephentoub
andauthored
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>
1 parent d9ae6bc commit 54283a8

2 files changed

Lines changed: 102 additions & 1 deletion

File tree

src/ModelContextProtocol.Core/Protocol/ContentBlock.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ public sealed class Converter : JsonConverter<ContentBlock>
9090
string? type = null;
9191
string? text = null;
9292
string? name = null;
93+
string? title = null;
9394
ReadOnlyMemory<byte>? data = null;
9495
string? mimeType = null;
9596
string? uri = null;
9697
string? description = null;
9798
long? size = null;
99+
IList<Icon>? icons = null;
98100
ResourceContents? resource = null;
99101
Annotations? annotations = null;
100102
JsonObject? meta = null;
@@ -130,6 +132,10 @@ public sealed class Converter : JsonConverter<ContentBlock>
130132
name = reader.GetString();
131133
break;
132134

135+
case "title":
136+
title = reader.GetString();
137+
break;
138+
133139
case "data":
134140
data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
135141
break;
@@ -150,6 +156,18 @@ public sealed class Converter : JsonConverter<ContentBlock>
150156
size = reader.GetInt64();
151157
break;
152158

159+
case "icons":
160+
if (reader.TokenType == JsonTokenType.StartArray)
161+
{
162+
icons = [];
163+
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
164+
{
165+
icons.Add(JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Icon) ??
166+
throw new JsonException("Unexpected null item in icons array."));
167+
}
168+
}
169+
break;
170+
153171
case "resource":
154172
resource = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.ResourceContents);
155173
break;
@@ -233,9 +251,11 @@ public sealed class Converter : JsonConverter<ContentBlock>
233251
{
234252
Uri = uri ?? throw new JsonException("URI must be provided for 'resource_link' type."),
235253
Name = name ?? throw new JsonException("Name must be provided for 'resource_link' type."),
254+
Title = title,
236255
Description = description,
237256
MimeType = mimeType,
238257
Size = size,
258+
Icons = icons,
239259
},
240260

241261
"tool_use" => new ToolUseContentBlock
@@ -299,6 +319,10 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial
299319
case ResourceLinkBlock resourceLink:
300320
writer.WriteString("uri", resourceLink.Uri);
301321
writer.WriteString("name", resourceLink.Name);
322+
if (resourceLink.Title is not null)
323+
{
324+
writer.WriteString("title", resourceLink.Title);
325+
}
302326
if (resourceLink.Description is not null)
303327
{
304328
writer.WriteString("description", resourceLink.Description);
@@ -311,6 +335,16 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial
311335
{
312336
writer.WriteNumber("size", resourceLink.Size.Value);
313337
}
338+
if (resourceLink.Icons is { Count: > 0 })
339+
{
340+
writer.WritePropertyName("icons");
341+
writer.WriteStartArray();
342+
foreach (var icon in resourceLink.Icons)
343+
{
344+
JsonSerializer.Serialize(writer, icon, McpJsonUtilities.JsonContext.Default.Icon);
345+
}
346+
writer.WriteEndArray();
347+
}
314348
break;
315349

316350
case ToolUseContentBlock toolUse:
@@ -595,6 +629,17 @@ public sealed class ResourceLinkBlock : ContentBlock
595629
[JsonPropertyName("name")]
596630
public required string Name { get; set; }
597631

632+
/// <summary>
633+
/// Gets or sets a title for this resource.
634+
/// </summary>
635+
/// <remarks>
636+
/// This is intended for UI and end-user contexts. It is optimized to be human-readable and easily understood,
637+
/// even by those unfamiliar with domain-specific terminology.
638+
/// If not provided, <see cref="Name"/> can be used for display.
639+
/// </remarks>
640+
[JsonPropertyName("title")]
641+
public string? Title { get; set; }
642+
598643
/// <summary>
599644
/// Gets or sets a description of what this resource represents.
600645
/// </summary>
@@ -638,6 +683,15 @@ public sealed class ResourceLinkBlock : ContentBlock
638683
/// </remarks>
639684
[JsonPropertyName("size")]
640685
public long? Size { get; set; }
686+
687+
/// <summary>
688+
/// Gets or sets an optional list of icons for this resource.
689+
/// </summary>
690+
/// <remarks>
691+
/// This can be used by clients to display the resource's icon in a user interface.
692+
/// </remarks>
693+
[JsonPropertyName("icons")]
694+
public IList<Icon>? Icons { get; set; }
641695
}
642696

643697
/// <summary>Represents a request from the assistant to call a tool.</summary>

tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ public void ResourceLinkBlock_SerializationRoundTrip_PreservesAllProperties()
1313
{
1414
Uri = "https://example.com/resource",
1515
Name = "Test Resource",
16+
Title = "Test Resource Title",
1617
Description = "A test resource for validation",
1718
MimeType = "text/plain",
18-
Size = 1024
19+
Size = 1024,
20+
Icons = [new Icon { Source = "https://example.com/icon.png", MimeType = "image/png" }]
1921
};
2022

2123
// Act - Serialize to JSON
@@ -30,10 +32,15 @@ public void ResourceLinkBlock_SerializationRoundTrip_PreservesAllProperties()
3032

3133
Assert.Equal(original.Uri, resourceLink.Uri);
3234
Assert.Equal(original.Name, resourceLink.Name);
35+
Assert.Equal(original.Title, resourceLink.Title);
3336
Assert.Equal(original.Description, resourceLink.Description);
3437
Assert.Equal(original.MimeType, resourceLink.MimeType);
3538
Assert.Equal(original.Size, resourceLink.Size);
3639
Assert.Equal("resource_link", resourceLink.Type);
40+
Assert.NotNull(resourceLink.Icons);
41+
Assert.Single(resourceLink.Icons);
42+
Assert.Equal("https://example.com/icon.png", resourceLink.Icons[0].Source);
43+
Assert.Equal("image/png", resourceLink.Icons[0].MimeType);
3744
}
3845

3946
[Fact]
@@ -57,9 +64,11 @@ public void ResourceLinkBlock_DeserializationWithMinimalProperties_Succeeds()
5764

5865
Assert.Equal("https://example.com/minimal", resourceLink.Uri);
5966
Assert.Equal("Minimal Resource", resourceLink.Name);
67+
Assert.Null(resourceLink.Title);
6068
Assert.Null(resourceLink.Description);
6169
Assert.Null(resourceLink.MimeType);
6270
Assert.Null(resourceLink.Size);
71+
Assert.Null(resourceLink.Icons);
6372
Assert.Equal("resource_link", resourceLink.Type);
6473
}
6574

@@ -81,6 +90,44 @@ public void ResourceLinkBlock_DeserializationWithoutName_ThrowsJsonException()
8190
Assert.Contains("Name must be provided for 'resource_link' type", exception.Message);
8291
}
8392

93+
[Fact]
94+
public void ResourceLinkBlock_DeserializationWithTitleAndIcons_Succeeds()
95+
{
96+
// Arrange - JSON with title and icons properties per spec
97+
const string Json = """
98+
{
99+
"type": "resource_link",
100+
"uri": "https://example.com/resource",
101+
"name": "my-resource",
102+
"title": "My Resource",
103+
"icons": [
104+
{ "src": "https://example.com/icon1.png", "mimeType": "image/png", "sizes": ["48x48"], "theme": "light" },
105+
{ "src": "https://example.com/icon2.svg", "mimeType": "image/svg+xml" }
106+
]
107+
}
108+
""";
109+
110+
// Act
111+
var deserialized = JsonSerializer.Deserialize<ContentBlock>(Json, McpJsonUtilities.DefaultOptions);
112+
113+
// Assert
114+
Assert.NotNull(deserialized);
115+
var resourceLink = Assert.IsType<ResourceLinkBlock>(deserialized);
116+
117+
Assert.Equal("https://example.com/resource", resourceLink.Uri);
118+
Assert.Equal("my-resource", resourceLink.Name);
119+
Assert.Equal("My Resource", resourceLink.Title);
120+
Assert.NotNull(resourceLink.Icons);
121+
Assert.Equal(2, resourceLink.Icons.Count);
122+
Assert.Equal("https://example.com/icon1.png", resourceLink.Icons[0].Source);
123+
Assert.Equal("image/png", resourceLink.Icons[0].MimeType);
124+
Assert.NotNull(resourceLink.Icons[0].Sizes);
125+
Assert.Equal("48x48", resourceLink.Icons[0].Sizes![0]);
126+
Assert.Equal("light", resourceLink.Icons[0].Theme);
127+
Assert.Equal("https://example.com/icon2.svg", resourceLink.Icons[1].Source);
128+
Assert.Equal("image/svg+xml", resourceLink.Icons[1].MimeType);
129+
}
130+
84131
[Fact]
85132
public void Deserialize_IgnoresUnknownArrayProperty()
86133
{

0 commit comments

Comments
 (0)