Skip to content

Commit 8b1b684

Browse files
Implement SEP-973: Add Icon class and icon support to Implementation, Resource, Tool, and Prompt
Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com>
1 parent 31ad1e4 commit 8b1b684

10 files changed

Lines changed: 514 additions & 1 deletion

File tree

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.119",
3+
"version": "9.0.204",
44
"rollForward": "minor"
55
}
66
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol;
4+
5+
/// <summary>
6+
/// Represents an icon that can be used to visually identify an implementation, resource, tool, or prompt.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// Icons enhance user interfaces by providing visual context and improving the discoverability of available functionality.
11+
/// Each icon includes a source URI pointing to the icon resource, and optional MIME type and size information.
12+
/// </para>
13+
/// <para>
14+
/// Clients that support rendering icons MUST support at least the following MIME types:
15+
/// </para>
16+
/// <list type="bullet">
17+
/// <item><description>image/png - PNG images (safe, universal compatibility)</description></item>
18+
/// <item><description>image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)</description></item>
19+
/// </list>
20+
/// <para>
21+
/// Clients that support rendering icons SHOULD also support:
22+
/// </para>
23+
/// <list type="bullet">
24+
/// <item><description>image/svg+xml - SVG images (scalable but requires security precautions)</description></item>
25+
/// <item><description>image/webp - WebP images (modern, efficient format)</description></item>
26+
/// </list>
27+
/// <para>
28+
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
29+
/// </para>
30+
/// </remarks>
31+
public sealed class Icon
32+
{
33+
/// <summary>
34+
/// Gets or sets the URI pointing to the icon resource.
35+
/// </summary>
36+
/// <remarks>
37+
/// <para>
38+
/// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
39+
/// </para>
40+
/// <para>
41+
/// Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the client/server
42+
/// or a trusted domain.
43+
/// </para>
44+
/// <para>
45+
/// Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.
46+
/// </para>
47+
/// </remarks>
48+
[JsonPropertyName("src")]
49+
public required string Src { get; init; }
50+
51+
/// <summary>
52+
/// Gets or sets the optional MIME type of the icon.
53+
/// </summary>
54+
/// <remarks>
55+
/// This can be used to override the server's MIME type if it's missing or generic.
56+
/// Common values include "image/png", "image/jpeg", "image/svg+xml", and "image/webp".
57+
/// </remarks>
58+
[JsonPropertyName("mimeType")]
59+
public string? MimeType { get; init; }
60+
61+
/// <summary>
62+
/// Gets or sets the optional size specification for the icon.
63+
/// </summary>
64+
/// <remarks>
65+
/// <para>
66+
/// This can specify one or more sizes at which the icon file can be used.
67+
/// Examples include "48x48", "any" for scalable formats like SVG, or "48x48 96x96" for multiple sizes.
68+
/// </para>
69+
/// <para>
70+
/// If not provided, clients should assume that the icon can be used at any size.
71+
/// </para>
72+
/// </remarks>
73+
[JsonPropertyName("sizes")]
74+
public string? Sizes { get; init; }
75+
}

src/ModelContextProtocol.Core/Protocol/Implementation.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,36 @@ public sealed class Implementation : IBaseMetadata
3636
/// </remarks>
3737
[JsonPropertyName("version")]
3838
public required string Version { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets an optional list of icons for this implementation.
42+
/// </summary>
43+
/// <remarks>
44+
/// <para>
45+
/// This can be used by clients to display the implementation in a user interface.
46+
/// Multiple icons can be provided to support different display contexts and resolutions.
47+
/// Clients should select the most appropriate icon based on their UI requirements.
48+
/// </para>
49+
/// <para>
50+
/// Each icon should specify a source URI that points to the icon file or data representation,
51+
/// and may also include MIME type and size information to help clients choose the best icon.
52+
/// </para>
53+
/// </remarks>
54+
[JsonPropertyName("icons")]
55+
public IList<Icon>? Icons { get; set; }
56+
57+
/// <summary>
58+
/// Gets or sets an optional URL of the website for this implementation.
59+
/// </summary>
60+
/// <remarks>
61+
/// <para>
62+
/// This URL can be used by clients to link to documentation or more information about the implementation.
63+
/// </para>
64+
/// <para>
65+
/// Consumers SHOULD take steps to ensure URLs are from the same domain as the client/server
66+
/// or a trusted domain to prevent security issues.
67+
/// </para>
68+
/// </remarks>
69+
[JsonPropertyName("websiteUrl")]
70+
public string? WebsiteUrl { get; set; }
3971
}

src/ModelContextProtocol.Core/Protocol/Prompt.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ public sealed class Prompt : IBaseMetadata
5252
[JsonPropertyName("arguments")]
5353
public IList<PromptArgument>? Arguments { get; set; }
5454

55+
/// <summary>
56+
/// Gets or sets an optional list of icons for this prompt.
57+
/// </summary>
58+
/// <remarks>
59+
/// <para>
60+
/// This can be used by clients to display the prompt's icon in a user interface.
61+
/// Multiple icons can be provided to support different display contexts and resolutions.
62+
/// Clients should select the most appropriate icon based on their UI requirements.
63+
/// </para>
64+
/// <para>
65+
/// Each icon should specify a source URI that points to the icon file or data representation,
66+
/// and may also include MIME type and size information to help clients choose the best icon.
67+
/// </para>
68+
/// </remarks>
69+
[JsonPropertyName("icons")]
70+
public IList<Icon>? Icons { get; set; }
71+
5572
/// <summary>
5673
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
5774
/// </summary>

src/ModelContextProtocol.Core/Protocol/Resource.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ public sealed class Resource : IBaseMetadata
8080
[JsonPropertyName("size")]
8181
public long? Size { get; init; }
8282

83+
/// <summary>
84+
/// Gets or sets an optional list of icons for this resource.
85+
/// </summary>
86+
/// <remarks>
87+
/// <para>
88+
/// This can be used by clients to display the resource's icon in a user interface.
89+
/// Multiple icons can be provided to support different display contexts and resolutions.
90+
/// Clients should select the most appropriate icon based on their UI requirements.
91+
/// </para>
92+
/// <para>
93+
/// Each icon should specify a source URI that points to the icon file or data representation,
94+
/// and may also include MIME type and size information to help clients choose the best icon.
95+
/// </para>
96+
/// </remarks>
97+
[JsonPropertyName("icons")]
98+
public IList<Icon>? Icons { get; init; }
99+
83100
/// <summary>
84101
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
85102
/// </summary>

src/ModelContextProtocol.Core/Protocol/Tool.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ public JsonElement? OutputSchema
107107
[JsonPropertyName("annotations")]
108108
public ToolAnnotations? Annotations { get; set; }
109109

110+
/// <summary>
111+
/// Gets or sets an optional list of icons for this tool.
112+
/// </summary>
113+
/// <remarks>
114+
/// <para>
115+
/// This can be used by clients to display the tool's icon in a user interface.
116+
/// Multiple icons can be provided to support different display contexts and resolutions.
117+
/// Clients should select the most appropriate icon based on their UI requirements.
118+
/// </para>
119+
/// <para>
120+
/// Each icon should specify a source URI that points to the icon file or data representation,
121+
/// and may also include MIME type and size information to help clients choose the best icon.
122+
/// </para>
123+
/// </remarks>
124+
[JsonPropertyName("icons")]
125+
public IList<Icon>? Icons { get; set; }
126+
110127
/// <summary>
111128
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
112129
/// </summary>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Text.Json;
3+
4+
namespace ModelContextProtocol.Tests.Protocol;
5+
6+
public static class IconTests
7+
{
8+
[Fact]
9+
public static void Icon_SerializesToJson_WithAllProperties()
10+
{
11+
var icon = new Icon
12+
{
13+
Src = "https://example.com/icon.png",
14+
MimeType = "image/png",
15+
Sizes = "48x48"
16+
};
17+
18+
string json = JsonSerializer.Serialize(icon);
19+
var result = JsonSerializer.Deserialize<Icon>(json);
20+
21+
Assert.Equal("https://example.com/icon.png", result!.Src);
22+
Assert.Equal("image/png", result.MimeType);
23+
Assert.Equal("48x48", result.Sizes);
24+
}
25+
26+
[Fact]
27+
public static void Icon_SerializesToJson_WithOnlyRequiredProperties()
28+
{
29+
var icon = new Icon
30+
{
31+
Src = "data:image/svg+xml;base64,PHN2Zy4uLjwvc3ZnPg=="
32+
};
33+
34+
string json = JsonSerializer.Serialize(icon);
35+
var result = JsonSerializer.Deserialize<Icon>(json);
36+
37+
Assert.Equal("data:image/svg+xml;base64,PHN2Zy4uLjwvc3ZnPg==", result!.Src);
38+
Assert.Null(result.MimeType);
39+
Assert.Null(result.Sizes);
40+
}
41+
42+
[Fact]
43+
public static void Icon_HasCorrectJsonPropertyNames()
44+
{
45+
var icon = new Icon
46+
{
47+
Src = "https://example.com/icon.svg",
48+
MimeType = "image/svg+xml",
49+
Sizes = "any"
50+
};
51+
52+
string json = JsonSerializer.Serialize(icon);
53+
54+
Assert.Contains("\"src\":", json);
55+
Assert.Contains("\"mimeType\":", json);
56+
Assert.Contains("\"sizes\":", json);
57+
}
58+
59+
[Theory]
60+
[InlineData("")]
61+
[InlineData(" ")]
62+
public static void Icon_DoesNotValidateEmptyOrWhitespaceSrc(string src)
63+
{
64+
// The Icon class doesn't enforce validation in the constructor
65+
// It's up to consumers to validate the URI format
66+
var icon = new Icon { Src = src };
67+
Assert.Equal(src, icon.Src);
68+
}
69+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Text.Json;
3+
4+
namespace ModelContextProtocol.Tests.Protocol;
5+
6+
public static class ImplementationTests
7+
{
8+
[Fact]
9+
public static void Implementation_SerializesToJson_WithAllProperties()
10+
{
11+
var implementation = new Implementation
12+
{
13+
Name = "test-server",
14+
Title = "Test MCP Server",
15+
Version = "1.0.0",
16+
Icons = new List<Icon>
17+
{
18+
new() { Src = "https://example.com/icon.png", MimeType = "image/png", Sizes = "48x48" },
19+
new() { Src = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = "any" }
20+
},
21+
WebsiteUrl = "https://example.com"
22+
};
23+
24+
string json = JsonSerializer.Serialize(implementation);
25+
var result = JsonSerializer.Deserialize<Implementation>(json);
26+
27+
Assert.Equal("test-server", result!.Name);
28+
Assert.Equal("Test MCP Server", result.Title);
29+
Assert.Equal("1.0.0", result.Version);
30+
Assert.Equal("https://example.com", result.WebsiteUrl);
31+
Assert.NotNull(result.Icons);
32+
Assert.Equal(2, result.Icons.Count);
33+
Assert.Equal("https://example.com/icon.png", result.Icons[0].Src);
34+
Assert.Equal("https://example.com/icon.svg", result.Icons[1].Src);
35+
}
36+
37+
[Fact]
38+
public static void Implementation_SerializesToJson_WithoutOptionalProperties()
39+
{
40+
var implementation = new Implementation
41+
{
42+
Name = "simple-server",
43+
Version = "1.0.0"
44+
};
45+
46+
string json = JsonSerializer.Serialize(implementation);
47+
var result = JsonSerializer.Deserialize<Implementation>(json);
48+
49+
Assert.Equal("simple-server", result!.Name);
50+
Assert.Null(result.Title);
51+
Assert.Equal("1.0.0", result.Version);
52+
Assert.Null(result.Icons);
53+
Assert.Null(result.WebsiteUrl);
54+
}
55+
56+
[Fact]
57+
public static void Implementation_HasCorrectJsonPropertyNames()
58+
{
59+
var implementation = new Implementation
60+
{
61+
Name = "test-server",
62+
Title = "Test Server",
63+
Version = "1.0.0",
64+
Icons = new List<Icon> { new() { Src = "https://example.com/icon.png" } },
65+
WebsiteUrl = "https://example.com"
66+
};
67+
68+
string json = JsonSerializer.Serialize(implementation);
69+
70+
Assert.Contains("\"name\":", json);
71+
Assert.Contains("\"title\":", json);
72+
Assert.Contains("\"version\":", json);
73+
Assert.Contains("\"icons\":", json);
74+
Assert.Contains("\"websiteUrl\":", json);
75+
}
76+
77+
[Fact]
78+
public static void Implementation_EmptyIconsList_SerializesAsEmptyArray()
79+
{
80+
var implementation = new Implementation
81+
{
82+
Name = "test-server",
83+
Version = "1.0.0",
84+
Icons = new List<Icon>()
85+
};
86+
87+
string json = JsonSerializer.Serialize(implementation);
88+
var result = JsonSerializer.Deserialize<Implementation>(json);
89+
90+
Assert.NotNull(result!.Icons);
91+
Assert.Empty(result.Icons);
92+
}
93+
}

0 commit comments

Comments
 (0)