diff --git a/src/ModelContextProtocol.Core/Protocol/Icon.cs b/src/ModelContextProtocol.Core/Protocol/Icon.cs
new file mode 100644
index 000000000..2ccd13734
--- /dev/null
+++ b/src/ModelContextProtocol.Core/Protocol/Icon.cs
@@ -0,0 +1,85 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol;
+
+///
+/// Represents an icon that can be used to visually identify an implementation, resource, tool, or prompt.
+///
+///
+///
+/// Icons enhance user interfaces by providing visual context and improving the discoverability of available functionality.
+/// Each icon includes a source URI pointing to the icon resource, and optional MIME type and size information.
+///
+///
+/// Clients that support rendering icons MUST support at least the following MIME types:
+///
+///
+/// image/png - PNG images (safe, universal compatibility)
+/// image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)
+///
+///
+/// Clients that support rendering icons SHOULD also support:
+///
+///
+/// image/svg+xml - SVG images (scalable but requires security precautions)
+/// image/webp - WebP images (modern, efficient format)
+///
+///
+/// See the schema for details.
+///
+///
+public sealed class Icon
+{
+ ///
+ /// Gets or sets the URI pointing to the icon resource.
+ ///
+ ///
+ ///
+ /// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
+ ///
+ ///
+ /// Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the client/server
+ /// or a trusted domain.
+ ///
+ ///
+ /// Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.
+ ///
+ ///
+ [JsonPropertyName("src")]
+ public required string Source { get; init; }
+
+ ///
+ /// Gets or sets the optional MIME type of the icon.
+ ///
+ ///
+ /// This can be used to override the server's MIME type if it's missing or generic.
+ /// Common values include "image/png", "image/jpeg", "image/svg+xml", and "image/webp".
+ ///
+ [JsonPropertyName("mimeType")]
+ public string? MimeType { get; init; }
+
+ ///
+ /// Gets or sets the optional size specifications for the icon.
+ ///
+ ///
+ ///
+ /// This can specify one or more sizes at which the icon file can be used.
+ /// Examples include "48x48", "any" for scalable formats like SVG.
+ ///
+ ///
+ /// If not provided, clients should assume that the icon can be used at any size.
+ ///
+ ///
+ [JsonPropertyName("sizes")]
+ public IList? Sizes { get; init; }
+
+ ///
+ /// Gets or sets the optional theme for this icon.
+ ///
+ ///
+ /// Can be "light", "dark", or a custom theme identifier.
+ /// Used to specify which UI theme the icon is designed for.
+ ///
+ [JsonPropertyName("theme")]
+ public string? Theme { get; init; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.Core/Protocol/Implementation.cs b/src/ModelContextProtocol.Core/Protocol/Implementation.cs
index af177000c..4bf9d5a15 100644
--- a/src/ModelContextProtocol.Core/Protocol/Implementation.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Implementation.cs
@@ -36,4 +36,28 @@ public sealed class Implementation : IBaseMetadata
///
[JsonPropertyName("version")]
public required string Version { get; set; }
+
+ ///
+ /// Gets or sets an optional list of icons for this implementation.
+ ///
+ ///
+ /// This can be used by clients to display the implementation's icon in a user interface.
+ ///
+ [JsonPropertyName("icons")]
+ public IList? Icons { get; set; }
+
+ ///
+ /// Gets or sets an optional URL of the website for this implementation.
+ ///
+ ///
+ ///
+ /// This URL can be used by clients to link to documentation or more information about the implementation.
+ ///
+ ///
+ /// Consumers SHOULD take steps to ensure URLs are from the same domain as the client/server
+ /// or a trusted domain to prevent security issues.
+ ///
+ ///
+ [JsonPropertyName("websiteUrl")]
+ public string? WebsiteUrl { get; set; }
}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs
index fcd3053f5..35c4c470e 100644
--- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs
@@ -52,6 +52,15 @@ public sealed class Prompt : IBaseMetadata
[JsonPropertyName("arguments")]
public IList? Arguments { get; set; }
+ ///
+ /// Gets or sets an optional list of icons for this prompt.
+ ///
+ ///
+ /// This can be used by clients to display the prompt's icon in a user interface.
+ ///
+ [JsonPropertyName("icons")]
+ public IList? Icons { get; set; }
+
///
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
///
diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs
index 1b8a0e9cd..d8441488e 100644
--- a/src/ModelContextProtocol.Core/Protocol/Resource.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs
@@ -80,6 +80,15 @@ public sealed class Resource : IBaseMetadata
[JsonPropertyName("size")]
public long? Size { get; init; }
+ ///
+ /// Gets or sets an optional list of icons for this resource.
+ ///
+ ///
+ /// This can be used by clients to display the resource's icon in a user interface.
+ ///
+ [JsonPropertyName("icons")]
+ public IList? Icons { get; set; }
+
///
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
///
diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs
index f0f294985..fe753510f 100644
--- a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs
@@ -72,6 +72,15 @@ public sealed class ResourceTemplate : IBaseMetadata
[JsonPropertyName("annotations")]
public Annotations? Annotations { get; init; }
+ ///
+ /// Gets or sets an optional list of icons for this resource template.
+ ///
+ ///
+ /// This can be used by clients to display the resource template's icon in a user interface.
+ ///
+ [JsonPropertyName("icons")]
+ public IList? Icons { get; set; }
+
///
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
///
@@ -108,6 +117,7 @@ public sealed class ResourceTemplate : IBaseMetadata
Description = Description,
MimeType = MimeType,
Annotations = Annotations,
+ Icons = Icons,
Meta = Meta,
McpServerResource = McpServerResource,
};
diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs
index 1c4716691..9365a85a9 100644
--- a/src/ModelContextProtocol.Core/Protocol/Tool.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs
@@ -107,6 +107,15 @@ public JsonElement? OutputSchema
[JsonPropertyName("annotations")]
public ToolAnnotations? Annotations { get; set; }
+ ///
+ /// Gets or sets an optional list of icons for this tool.
+ ///
+ ///
+ /// This can be used by clients to display the tool's icon in a user interface.
+ ///
+ [JsonPropertyName("icons")]
+ public IList? Icons { get; set; }
+
///
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
///
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs
index ef068c551..dadd876bb 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs
@@ -135,6 +135,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Title = options?.Title,
Description = options?.Description ?? function.Description,
Arguments = args,
+ Icons = options?.Icons,
};
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
@@ -148,6 +149,12 @@ private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, Mcp
{
newOptions.Name ??= promptAttr.Name;
newOptions.Title ??= promptAttr.Title;
+
+ // Handle icon from attribute if not already specified in options
+ if (newOptions.Icons is null && promptAttr.IconSource is { Length: > 0 } iconSource)
+ {
+ newOptions.Icons = [new() { Source = iconSource }];
+ }
}
if (method.GetCustomAttribute() is { } descAttr)
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs
index 69b8deb8d..350f0d9b2 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs
@@ -218,6 +218,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Title = options?.Title,
Description = options?.Description,
MimeType = options?.MimeType ?? "application/octet-stream",
+ Icons = options?.Icons,
};
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);
@@ -233,6 +234,12 @@ private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, M
newOptions.Name ??= resourceAttr.Name;
newOptions.Title ??= resourceAttr.Title;
newOptions.MimeType ??= resourceAttr.MimeType;
+
+ // Handle icon from attribute if not already specified in options
+ if (newOptions.Icons is null && resourceAttr.IconSource is { Length: > 0 } iconSource)
+ {
+ newOptions.Icons = [new() { Source = iconSource }];
+ }
}
if (member.GetCustomAttribute() is { } descAttr)
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
index cb4758486..91fbb3d6a 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
@@ -121,6 +121,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
Description = options?.Description ?? function.Description,
InputSchema = function.JsonSchema,
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
+ Icons = options?.Icons,
};
if (options is not null)
@@ -176,6 +177,11 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
newOptions.ReadOnly ??= readOnly;
}
+ if (newOptions.Icons is null && toolAttr.IconSource is { Length: > 0 } iconSource)
+ {
+ newOptions.Icons = [new() { Source = iconSource }];
+ }
+
newOptions.UseStructuredContent = toolAttr.UseStructuredContent;
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs
index ac9e247f6..7d9f877a9 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs
@@ -120,4 +120,19 @@ public McpServerPromptAttribute()
/// Gets or sets the title of the prompt.
public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the source URI for the prompt's icon.
+ ///
+ ///
+ ///
+ /// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
+ /// When specified, a single icon will be added to the prompt.
+ ///
+ ///
+ /// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
+ /// use when creating the prompt programmatically.
+ ///
+ ///
+ public string? IconSource { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs
index 1853b0f1a..146c0e063 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.AI;
+using ModelContextProtocol.Protocol;
using System.ComponentModel;
using System.Text.Json;
@@ -77,6 +78,14 @@ public sealed class McpServerPromptCreateOptions
///
public IReadOnlyList? Metadata { get; set; }
+ ///
+ /// Gets or sets the icons for this prompt.
+ ///
+ ///
+ /// This can be used by clients to display the prompt's icon in a user interface.
+ ///
+ public IList? Icons { get; set; }
+
///
/// Creates a shallow clone of the current instance.
///
@@ -90,5 +99,6 @@ internal McpServerPromptCreateOptions Clone() =>
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
+ Icons = Icons,
};
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs
index 66c593e47..8639a0bda 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs
@@ -135,4 +135,19 @@ public McpServerResourceAttribute()
/// Gets or sets the MIME (media) type of the resource.
public string? MimeType { get; set; }
+
+ ///
+ /// Gets or sets the source URI for the resource's icon.
+ ///
+ ///
+ ///
+ /// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
+ /// When specified, a single icon will be added to the resource.
+ ///
+ ///
+ /// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
+ /// use when creating the resource programmatically.
+ ///
+ ///
+ public string? IconSource { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs
index 2d6b66b32..c2ec444cd 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.AI;
+using ModelContextProtocol.Protocol;
using System.ComponentModel;
using System.Text.Json;
@@ -92,6 +93,14 @@ public sealed class McpServerResourceCreateOptions
///
public IReadOnlyList? Metadata { get; set; }
+ ///
+ /// Gets or sets the icons for this resource.
+ ///
+ ///
+ /// This can be used by clients to display the resource's icon in a user interface.
+ ///
+ public IList? Icons { get; set; }
+
///
/// Creates a shallow clone of the current instance.
///
@@ -107,5 +116,6 @@ internal McpServerResourceCreateOptions Clone() =>
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
+ Icons = Icons,
};
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
index 7d5bf488b..9e71e0eab 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
@@ -254,4 +254,19 @@ public bool ReadOnly
///
///
public bool UseStructuredContent { get; set; }
+
+ ///
+ /// Gets or sets the source URI for the tool's icon.
+ ///
+ ///
+ ///
+ /// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
+ /// When specified, a single icon will be added to the tool.
+ ///
+ ///
+ /// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
+ /// use when creating the tool programmatically.
+ ///
+ ///
+ public string? IconSource { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
index d18af8c02..cb4205be1 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
@@ -164,6 +164,14 @@ public sealed class McpServerToolCreateOptions
///
public IReadOnlyList? Metadata { get; set; }
+ ///
+ /// Gets or sets the icons for this tool.
+ ///
+ ///
+ /// This can be used by clients to display the tool's icon in a user interface.
+ ///
+ public IList? Icons { get; set; }
+
///
/// Creates a shallow clone of the current instance.
///
@@ -182,5 +190,6 @@ internal McpServerToolCreateOptions Clone() =>
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
+ Icons = Icons,
};
}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs
index 3f5b80ae7..3e29fa8a2 100644
--- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs
@@ -27,6 +27,46 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
}
mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" })]);
mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })]);
+
+ services.Configure(o =>
+ {
+ o.ServerInfo = new Implementation
+ {
+ Name = "test-server",
+ Version = "1.0.0",
+ WebsiteUrl = "https://example.com",
+ Icons =
+ [
+ new Icon { Source = "https://example.com/icon-48.png", MimeType = "image/png", Sizes = ["48x48"], Theme = "light" },
+ new Icon { Source = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = ["any"], Theme = "dark" }
+ ]
+ };
+ });
+ }
+
+ [Fact]
+ public async Task CanReadServerInfo()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var serverInfo = client.ServerInfo;
+ Assert.Equal("test-server", serverInfo.Name);
+ Assert.Equal("1.0.0", serverInfo.Version);
+ Assert.Equal("https://example.com", serverInfo.WebsiteUrl);
+ Assert.NotNull(serverInfo.Icons);
+ Assert.Equal(2, serverInfo.Icons.Count);
+
+ var icon0 = serverInfo.Icons[0];
+ Assert.Equal("https://example.com/icon-48.png", icon0.Source);
+ Assert.Equal("image/png", icon0.MimeType);
+ Assert.Single(icon0.Sizes!, "48x48");
+ Assert.Equal("light", icon0.Theme);
+
+ var icon1 = serverInfo.Icons[1];
+ Assert.Equal("https://example.com/icon.svg", icon1.Source);
+ Assert.Equal("image/svg+xml", icon1.MimeType);
+ Assert.Single(icon1.Sizes!, "any");
+ Assert.Equal("dark", icon1.Theme);
}
[Theory]
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
index 18db1f14b..2df57dbf3 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
@@ -166,7 +166,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes()
}
[Fact]
- public async Task TitleAttributeProperty_PropagatedToTitle()
+ public async Task AttributeProperties_Propagated()
{
await using McpClient client = await CreateMcpClientForServer();
@@ -177,6 +177,12 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
McpClientPrompt prompt = prompts.First(t => t.Name == "returns_string");
Assert.Equal("This is a title", prompt.Title);
+
+ Assert.NotNull(prompt.ProtocolPrompt.Icons);
+ Assert.NotEmpty(prompt.ProtocolPrompt.Icons);
+ var icon = Assert.Single(prompt.ProtocolPrompt.Icons);
+ Assert.Equal("https://example.com/prompt-icon.svg", icon.Source);
+ Assert.Null(icon.Theme);
}
[Fact]
@@ -325,12 +331,11 @@ public static ChatMessage[] ReturnsChatMessages([Description("The first paramete
new(ChatRole.User, "Summarize."),
];
-
[McpServerPrompt, Description("Returns chat messages")]
public static ChatMessage[] ThrowsException([Description("The first parameter")] string message) =>
throw new FormatException("uh oh");
- [McpServerPrompt(Title = "This is a title"), Description("Returns chat messages")]
+ [McpServerPrompt(Title = "This is a title", IconSource = "https://example.com/prompt-icon.svg"), Description("Returns chat messages")]
public string ReturnsString([Description("The first parameter")] string message) =>
$"The prompt is: {message}. The id is {id}.";
}
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
index 939904cb7..3a8a63e8f 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
@@ -201,7 +201,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes()
}
[Fact]
- public async Task TitleAttributeProperty_PropagatedToTitle()
+ public async Task AttributeProperties_Propagated()
{
await using McpClient client = await CreateMcpClientForServer();
@@ -211,11 +211,23 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
McpClientResource resource = resources.First(t => t.Name == "some_neat_direct_resource");
Assert.Equal("This is a title", resource.Title);
+ Assert.NotNull(resource.ProtocolResource.Icons);
+ Assert.NotEmpty(resource.ProtocolResource.Icons);
+ var resourceIcon = Assert.Single(resource.ProtocolResource.Icons);
+ Assert.Equal("https://example.com/direct-resource-icon.svg", resourceIcon.Source);
+ Assert.Null(resourceIcon.Theme);
+
var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(resourceTemplates);
Assert.NotEmpty(resourceTemplates);
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == "some_neat_templated_resource");
Assert.Equal("This is another title", resourceTemplate.Title);
+
+ Assert.NotNull(resourceTemplate.ProtocolResourceTemplate.Icons);
+ Assert.NotEmpty(resourceTemplate.ProtocolResourceTemplate.Icons);
+ var templateIcon = Assert.Single(resourceTemplate.ProtocolResourceTemplate.Icons);
+ Assert.Equal("https://example.com/templated-resource-icon.svg", templateIcon.Source);
+ Assert.Null(templateIcon.Theme);
}
[Fact]
@@ -341,10 +353,10 @@ public void Register_Resources_From_Multiple_Sources()
[McpServerResourceType]
public sealed class SimpleResources
{
- [McpServerResource(Title = "This is a title"), Description("Some neat direct resource")]
+ [McpServerResource(Title = "This is a title", IconSource = "https://example.com/direct-resource-icon.svg"), Description("Some neat direct resource")]
public static string SomeNeatDirectResource() => "This is a neat resource";
- [McpServerResource(Title = "This is another title"), Description("Some neat resource with parameters")]
+ [McpServerResource(Title = "This is another title", IconSource = "https://example.com/templated-resource-icon.svg"), Description("Some neat resource with parameters")]
public static string SomeNeatTemplatedResource(string name) => $"This is a neat resource with parameters: {name}";
[McpServerResource]
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
index cf2dfd0f7..97fd3e330 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
@@ -630,7 +630,7 @@ public void Create_ExtractsToolAnnotations_SomeSet()
}
[Fact]
- public async Task TitleAttributeProperty_PropagatedToTitle()
+ public async Task AttributeProperties_Propagated()
{
await using McpClient client = await CreateMcpClientForServer();
@@ -643,6 +643,12 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
Assert.Equal("This is a title", tool.Title);
Assert.Equal("This is a title", tool.ProtocolTool.Title);
Assert.Equal("This is a title", tool.ProtocolTool.Annotations?.Title);
+
+ Assert.NotNull(tool.ProtocolTool.Icons);
+ Assert.NotEmpty(tool.ProtocolTool.Icons);
+ var icon = Assert.Single(tool.ProtocolTool.Icons);
+ Assert.Equal("https://example.com/tool-icon.svg", icon.Source);
+ Assert.Null(icon.Theme);
}
[Fact]
@@ -786,7 +792,7 @@ public static int ReturnCancellationToken(CancellationToken cancellationToken)
return cancellationToken.GetHashCode();
}
- [McpServerTool(Title = "This is a title")]
+ [McpServerTool(Title = "This is a title", IconSource = "https://example.com/tool-icon.svg")]
public static string EchoComplex(ComplexObject complex)
{
return complex.Name!;
diff --git a/tests/ModelContextProtocol.Tests/Protocol/IconTests.cs b/tests/ModelContextProtocol.Tests/Protocol/IconTests.cs
new file mode 100644
index 000000000..ff248ec17
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/IconTests.cs
@@ -0,0 +1,95 @@
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol;
+
+public static class IconTests
+{
+ [Fact]
+ public static void Icon_SerializationRoundTrip_PreservesAllProperties()
+ {
+ // Arrange
+ var original = new Icon
+ {
+ Source = "https://example.com/icon.png",
+ MimeType = "image/png",
+ Sizes = new List { "48x48" },
+ Theme = "light"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Source, deserialized.Source);
+ Assert.Equal(original.MimeType, deserialized.MimeType);
+ Assert.Equal(original.Sizes, deserialized.Sizes);
+ Assert.Equal(original.Theme, deserialized.Theme);
+ }
+
+ [Fact]
+ public static void Icon_SerializationRoundTrip_WithOnlyRequiredProperties()
+ {
+ // Arrange
+ var original = new Icon
+ {
+ Source = "data:image/svg+xml;base64,PHN2Zy4uLjwvc3ZnPg=="
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Source, deserialized.Source);
+ Assert.Equal(original.MimeType, deserialized.MimeType);
+ Assert.Equal(original.Sizes, deserialized.Sizes);
+ Assert.Null(deserialized.Theme);
+ }
+
+ [Fact]
+ public static void Icon_HasCorrectJsonPropertyNames()
+ {
+ var icon = new Icon
+ {
+ Source = "https://example.com/icon.svg",
+ MimeType = "image/svg+xml",
+ Sizes = new List { "any" },
+ Theme = "dark"
+ };
+
+ string json = JsonSerializer.Serialize(icon, McpJsonUtilities.DefaultOptions);
+
+ Assert.Contains("\"src\":", json);
+ Assert.Contains("\"mimeType\":", json);
+ Assert.Contains("\"sizes\":", json);
+ Assert.Contains("\"theme\":", json);
+ }
+
+ [Theory]
+ [InlineData("""{}""")]
+ [InlineData("""{"mimeType":"image/png"}""")]
+ [InlineData("""{"sizes":"48x48"}""")]
+ [InlineData("""{"mimeType":"image/png","sizes":"48x48"}""")]
+ public static void Icon_DeserializationWithMissingSrc_ThrowsJsonException(string invalidJson)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(invalidJson, McpJsonUtilities.DefaultOptions));
+ }
+
+ [Theory]
+ [InlineData("false")]
+ [InlineData("true")]
+ [InlineData("42")]
+ [InlineData("[]")]
+ public static void Icon_DeserializationWithInvalidJson_ThrowsJsonException(string invalidJson)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(invalidJson, McpJsonUtilities.DefaultOptions));
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ImplementationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ImplementationTests.cs
new file mode 100644
index 000000000..ff938eff9
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/ImplementationTests.cs
@@ -0,0 +1,105 @@
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol;
+
+public static class ImplementationTests
+{
+ [Fact]
+ public static void Implementation_SerializationRoundTrip_PreservesAllProperties()
+ {
+ // Arrange
+ var original = new Implementation
+ {
+ Name = "test-server",
+ Title = "Test MCP Server",
+ Version = "1.0.0",
+ Icons =
+ [
+ new() { Source = "https://example.com/icon.png", MimeType = "image/png", Sizes = new List { "48x48" } },
+ new() { Source = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = new List { "any" } }
+ ],
+ WebsiteUrl = "https://example.com"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Version, deserialized.Version);
+ Assert.Equal(original.WebsiteUrl, deserialized.WebsiteUrl);
+ Assert.NotNull(deserialized.Icons);
+ Assert.Equal(original.Icons.Count, deserialized.Icons.Count);
+
+ for (int i = 0; i < original.Icons.Count; i++)
+ {
+ Assert.Equal(original.Icons[i].Source, deserialized.Icons[i].Source);
+ Assert.Equal(original.Icons[i].MimeType, deserialized.Icons[i].MimeType);
+ Assert.Equal(original.Icons[i].Sizes, deserialized.Icons[i].Sizes);
+ }
+ }
+
+ [Fact]
+ public static void Implementation_SerializationRoundTrip_WithoutOptionalProperties()
+ {
+ // Arrange
+ var original = new Implementation
+ {
+ Name = "simple-server",
+ Version = "1.0.0"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Version, deserialized.Version);
+ Assert.Equal(original.Icons, deserialized.Icons);
+ Assert.Equal(original.WebsiteUrl, deserialized.WebsiteUrl);
+ }
+
+ [Fact]
+ public static void Implementation_HasCorrectJsonPropertyNames()
+ {
+ var implementation = new Implementation
+ {
+ Name = "test-server",
+ Title = "Test Server",
+ Version = "1.0.0",
+ Icons = [new() { Source = "https://example.com/icon.png" }],
+ WebsiteUrl = "https://example.com"
+ };
+
+ string json = JsonSerializer.Serialize(implementation, McpJsonUtilities.DefaultOptions);
+
+ Assert.Contains("\"name\":", json);
+ Assert.Contains("\"title\":", json);
+ Assert.Contains("\"version\":", json);
+ Assert.Contains("\"icons\":", json);
+ Assert.Contains("\"websiteUrl\":", json);
+ }
+
+ [Theory]
+ [InlineData("""{}""")]
+ [InlineData("""{"title":"Test Server"}""")]
+ [InlineData("""{"name":"test-server"}""")]
+ [InlineData("""{"version":"1.0.0"}""")]
+ [InlineData("""{"title":"Test Server","version":"1.0.0"}""")]
+ [InlineData("""{"name":"test-server","title":"Test Server"}""")]
+ public static void Implementation_DeserializationWithMissingRequiredProperties_ThrowsJsonException(string invalidJson)
+ {
+ Assert.Throws(() => JsonSerializer.Deserialize(invalidJson, McpJsonUtilities.DefaultOptions));
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/PromptTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PromptTests.cs
new file mode 100644
index 000000000..e73ee4cf5
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/PromptTests.cs
@@ -0,0 +1,97 @@
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol;
+
+public static class PromptTests
+{
+ [Fact]
+ public static void Prompt_SerializationRoundTrip_PreservesAllProperties()
+ {
+ // Arrange
+ var original = new Prompt
+ {
+ Name = "code_review",
+ Title = "Code Review Prompt",
+ Description = "Review the provided code",
+ Icons =
+ [
+ new() { Source = "https://example.com/review-icon.svg", MimeType = "image/svg+xml", Sizes = new List { "any" } }
+ ],
+ Arguments =
+ [
+ new() { Name = "code", Description = "The code to review", Required = true }
+ ]
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.NotNull(deserialized.Icons);
+ Assert.Equal(original.Icons.Count, deserialized.Icons.Count);
+ Assert.Equal(original.Icons[0].Source, deserialized.Icons[0].Source);
+ Assert.Equal(original.Icons[0].MimeType, deserialized.Icons[0].MimeType);
+ Assert.Equal(original.Icons[0].Sizes, deserialized.Icons[0].Sizes);
+ Assert.NotNull(deserialized.Arguments);
+ Assert.Equal(original.Arguments.Count, deserialized.Arguments.Count);
+ Assert.Equal(original.Arguments[0].Name, deserialized.Arguments[0].Name);
+ Assert.Equal(original.Arguments[0].Description, deserialized.Arguments[0].Description);
+ Assert.Equal(original.Arguments[0].Required, deserialized.Arguments[0].Required);
+ }
+
+ [Fact]
+ public static void Prompt_SerializationRoundTrip_WithMinimalProperties()
+ {
+ // Arrange
+ var original = new Prompt
+ {
+ Name = "simple_prompt"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.Equal(original.Icons, deserialized.Icons);
+ Assert.Equal(original.Arguments, deserialized.Arguments);
+ }
+
+ [Fact]
+ public static void Prompt_HasCorrectJsonPropertyNames()
+ {
+ var prompt = new Prompt
+ {
+ Name = "test_prompt",
+ Title = "Test Prompt",
+ Description = "A test prompt",
+ Icons = [new() { Source = "https://example.com/icon.webp" }],
+ Arguments =
+ [
+ new() { Name = "input", Description = "Input parameter" }
+ ]
+ };
+
+ string json = JsonSerializer.Serialize(prompt, McpJsonUtilities.DefaultOptions);
+
+ Assert.Contains("\"name\":", json);
+ Assert.Contains("\"title\":", json);
+ Assert.Contains("\"description\":", json);
+ Assert.Contains("\"icons\":", json);
+ Assert.Contains("\"arguments\":", json);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ResourceTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ResourceTests.cs
new file mode 100644
index 000000000..e0e71a036
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/ResourceTests.cs
@@ -0,0 +1,104 @@
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol;
+
+public static class ResourceTests
+{
+ [Fact]
+ public static void Resource_SerializationRoundTrip_PreservesAllProperties()
+ {
+ // Arrange
+ var original = new Resource
+ {
+ Name = "document.pdf",
+ Title = "Important Document",
+ Uri = "file:///path/to/document.pdf",
+ Description = "An important document",
+ MimeType = "application/pdf",
+ Size = 1024,
+ Icons =
+ [
+ new() { Source = "https://example.com/pdf-icon.png", MimeType = "image/png", Sizes = new List { "32x32" } }
+ ],
+ Annotations = new Annotations { Audience = [Role.User] }
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Uri, deserialized.Uri);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.Equal(original.MimeType, deserialized.MimeType);
+ Assert.Equal(original.Size, deserialized.Size);
+ Assert.NotNull(deserialized.Icons);
+ Assert.Equal(original.Icons.Count, deserialized.Icons.Count);
+ Assert.Equal(original.Icons[0].Source, deserialized.Icons[0].Source);
+ Assert.Equal(original.Icons[0].MimeType, deserialized.Icons[0].MimeType);
+ Assert.Equal(original.Icons[0].Sizes, deserialized.Icons[0].Sizes);
+ Assert.NotNull(deserialized.Annotations);
+ Assert.Equal(original.Annotations.Audience, deserialized.Annotations.Audience);
+ }
+
+ [Fact]
+ public static void Resource_SerializationRoundTrip_WithMinimalProperties()
+ {
+ // Arrange
+ var original = new Resource
+ {
+ Name = "data.json",
+ Uri = "file:///path/to/data.json"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Uri, deserialized.Uri);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.Equal(original.MimeType, deserialized.MimeType);
+ Assert.Equal(original.Size, deserialized.Size);
+ Assert.Equal(original.Icons, deserialized.Icons);
+ Assert.Equal(original.Annotations, deserialized.Annotations);
+ }
+
+ [Fact]
+ public static void Resource_HasCorrectJsonPropertyNames()
+ {
+ var resource = new Resource
+ {
+ Name = "test_resource",
+ Title = "Test Resource",
+ Uri = "file:///test",
+ Description = "A test resource",
+ MimeType = "text/plain",
+ Size = 512,
+ Icons = new List { new() { Source = "https://example.com/icon.svg" } },
+ Annotations = new Annotations { Audience = [Role.User] }
+ };
+
+ string json = JsonSerializer.Serialize(resource, McpJsonUtilities.DefaultOptions);
+
+ Assert.Contains("\"name\":", json);
+ Assert.Contains("\"title\":", json);
+ Assert.Contains("\"uri\":", json);
+ Assert.Contains("\"description\":", json);
+ Assert.Contains("\"mimeType\":", json);
+ Assert.Contains("\"size\":", json);
+ Assert.Contains("\"icons\":", json);
+ Assert.Contains("\"annotations\":", json);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ToolTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ToolTests.cs
new file mode 100644
index 000000000..630ced49c
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Protocol/ToolTests.cs
@@ -0,0 +1,94 @@
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
+
+namespace ModelContextProtocol.Tests.Protocol;
+
+public static class ToolTests
+{
+ [Fact]
+ public static void Tool_SerializationRoundTrip_PreservesAllProperties()
+ {
+ // Arrange
+ var original = new Tool
+ {
+ Name = "get_weather",
+ Title = "Get Weather",
+ Description = "Get current weather information",
+ Icons =
+ [
+ new() { Source = "https://example.com/weather.png", MimeType = "image/png", Sizes = new List { "48x48" } }
+ ],
+ Annotations = new ToolAnnotations
+ {
+ Title = "Weather Tool",
+ ReadOnlyHint = true
+ }
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.NotNull(deserialized.Icons);
+ Assert.Equal(original.Icons.Count, deserialized.Icons.Count);
+ Assert.Equal(original.Icons[0].Source, deserialized.Icons[0].Source);
+ Assert.Equal(original.Icons[0].MimeType, deserialized.Icons[0].MimeType);
+ Assert.Equal(original.Icons[0].Sizes, deserialized.Icons[0].Sizes);
+ Assert.NotNull(deserialized.Annotations);
+ Assert.Equal(original.Annotations.Title, deserialized.Annotations.Title);
+ Assert.Equal(original.Annotations.ReadOnlyHint, deserialized.Annotations.ReadOnlyHint);
+ }
+
+ [Fact]
+ public static void Tool_SerializationRoundTrip_WithMinimalProperties()
+ {
+ // Arrange
+ var original = new Tool
+ {
+ Name = "calculate"
+ };
+
+ // Act - Serialize to JSON
+ string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
+
+ // Act - Deserialize back from JSON
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Title, deserialized.Title);
+ Assert.Equal(original.Description, deserialized.Description);
+ Assert.Equal(original.Icons, deserialized.Icons);
+ Assert.Equal(original.Annotations, deserialized.Annotations);
+ }
+
+ [Fact]
+ public static void Tool_HasCorrectJsonPropertyNames()
+ {
+ var tool = new Tool
+ {
+ Name = "test_tool",
+ Title = "Test Tool",
+ Description = "A test tool",
+ Icons = [new() { Source = "https://example.com/icon.png" }],
+ Annotations = new ToolAnnotations { Title = "Annotation Title" }
+ };
+
+ string json = JsonSerializer.Serialize(tool, McpJsonUtilities.DefaultOptions);
+
+ Assert.Contains("\"name\":", json);
+ Assert.Contains("\"title\":", json);
+ Assert.Contains("\"description\":", json);
+ Assert.Contains("\"icons\":", json);
+ Assert.Contains("\"annotations\":", json);
+ Assert.Contains("\"inputSchema\":", json);
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs
index 41c26f405..1cb7548db 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs
@@ -492,4 +492,59 @@ public ChatMessage InstanceMethod()
return _message;
}
}
+
+ [Fact]
+ public void SupportsIconsInCreateOptions()
+ {
+ var icons = new List
+ {
+ new() { Source = "https://example.com/prompt-icon.png", MimeType = "image/png", Sizes = new List { "48x48" } }
+ };
+
+ McpServerPrompt prompt = McpServerPrompt.Create(() => "test prompt", new McpServerPromptCreateOptions
+ {
+ Icons = icons
+ });
+
+ var icon = Assert.Single(prompt.ProtocolPrompt.Icons!);
+ Assert.Equal("https://example.com/prompt-icon.png", icon.Source);
+ Assert.Equal("image/png", icon.MimeType);
+ }
+
+ [Fact]
+ public void SupportsIconSourceInAttribute()
+ {
+ McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt(IconSource = "https://example.com/prompt-icon.svg")] () => "test prompt");
+
+ var icon = Assert.Single(prompt.ProtocolPrompt.Icons!);
+ Assert.Equal("https://example.com/prompt-icon.svg", icon.Source);
+ Assert.Null(icon.MimeType);
+ Assert.Null(icon.Sizes);
+ }
+
+ [Fact]
+ public void CreateOptionsIconsOverrideAttributeIconSource_Prompt()
+ {
+ var optionsIcons = new List
+ {
+ new() { Source = "https://example.com/override-icon.svg", MimeType = "image/svg+xml" }
+ };
+
+ McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt(IconSource = "https://example.com/prompt-icon.png")] () => "test prompt", new McpServerPromptCreateOptions
+ {
+ Icons = optionsIcons
+ });
+
+ var icon = Assert.Single(prompt.ProtocolPrompt.Icons!);
+ Assert.Equal("https://example.com/override-icon.svg", icon.Source);
+ Assert.Equal("image/svg+xml", icon.MimeType);
+ }
+
+ [Fact]
+ public void SupportsPromptWithoutIcons()
+ {
+ McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt] () => "test prompt");
+
+ Assert.Null(prompt.ProtocolPrompt.Icons);
+ }
}
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
index f7f2a7742..c52778df1 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
@@ -677,6 +677,62 @@ private class DisposableResourceType : IDisposable
public static object StaticMethod() => "42";
}
+ [Fact]
+ public void SupportsIconsInResourceCreateOptions()
+ {
+ var icons = new List
+ {
+ new() { Source = "https://example.com/resource-icon.png", MimeType = "image/png", Sizes = new List { "32x32" } }
+ };
+
+ McpServerResource resource = McpServerResource.Create(() => "test content", new McpServerResourceCreateOptions
+ {
+ UriTemplate = "test://resource/with-icon",
+ Icons = icons
+ });
+
+ var icon = Assert.Single(resource.ProtocolResourceTemplate.Icons!);
+ Assert.Equal("https://example.com/resource-icon.png", icon.Source);
+ Assert.Equal("image/png", icon.MimeType);
+ }
+
+ [Fact]
+ public void SupportsIconSourceInResourceAttribute()
+ {
+ McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource", IconSource = "https://example.com/resource-icon.svg")] () => "test content");
+
+ var icon = Assert.Single(resource.ProtocolResourceTemplate.Icons!);
+ Assert.Equal("https://example.com/resource-icon.svg", icon.Source);
+ Assert.Null(icon.MimeType);
+ Assert.Null(icon.Sizes);
+ }
+
+ [Fact]
+ public void CreateOptionsIconsOverrideAttributeIconSource_Resource()
+ {
+ var optionsIcons = new List
+ {
+ new() { Source = "https://example.com/override-icon.svg", MimeType = "image/svg+xml" }
+ };
+
+ McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource", IconSource = "https://example.com/resource-icon.png")] () => "test content", new McpServerResourceCreateOptions
+ {
+ Icons = optionsIcons
+ });
+
+ var icon = Assert.Single(resource.ProtocolResourceTemplate.Icons!);
+ Assert.Equal("https://example.com/override-icon.svg", icon.Source);
+ Assert.Equal("image/svg+xml", icon.MimeType);
+ }
+
+ [Fact]
+ public void SupportsResourceWithoutIcons()
+ {
+ McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource")] () => "test content");
+
+ Assert.Null(resource.ProtocolResourceTemplate.Icons);
+ }
+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(DisposableResourceType))]
[JsonSerializable(typeof(List))]
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs
index b9463e18f..111d13430 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs
@@ -678,6 +678,71 @@ Instance JSON document does not match the specified schema.
record Person(string Name, int Age);
+ [Fact]
+ public void SupportsIconsInCreateOptions()
+ {
+ var icons = new List
+ {
+ new() { Source = "https://example.com/icon.png", MimeType = "image/png", Sizes = new List { "48x48" } },
+ new() { Source = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = new List { "any" } }
+ };
+
+ McpServerTool tool = McpServerTool.Create(() => "test", new McpServerToolCreateOptions
+ {
+ Icons = icons
+ });
+
+ Assert.NotNull(tool.ProtocolTool.Icons);
+ Assert.Equal(2, tool.ProtocolTool.Icons.Count);
+ Assert.Equal("https://example.com/icon.png", tool.ProtocolTool.Icons[0].Source);
+ Assert.Equal("image/png", tool.ProtocolTool.Icons[0].MimeType);
+ Assert.NotNull(tool.ProtocolTool.Icons[0].Sizes);
+ Assert.Single(tool.ProtocolTool.Icons[0].Sizes!);
+ Assert.Equal("48x48", tool.ProtocolTool.Icons[0].Sizes![0]);
+ Assert.Equal("https://example.com/icon.svg", tool.ProtocolTool.Icons[1].Source);
+ Assert.Equal("image/svg+xml", tool.ProtocolTool.Icons[1].MimeType);
+ Assert.NotNull(tool.ProtocolTool.Icons[1].Sizes);
+ Assert.Single(tool.ProtocolTool.Icons[1].Sizes!);
+ Assert.Equal("any", tool.ProtocolTool.Icons[1].Sizes![0]);
+ }
+
+ [Fact]
+ public void SupportsIconSourceInAttribute()
+ {
+ McpServerTool tool = McpServerTool.Create([McpServerTool(IconSource = "https://example.com/tool-icon.png")] () => "result");
+
+ var icon = Assert.Single(tool.ProtocolTool.Icons!);
+ Assert.Equal("https://example.com/tool-icon.png", icon.Source);
+ Assert.Null(icon.MimeType);
+ Assert.Null(icon.Sizes);
+ }
+
+ [Fact]
+ public void CreateOptionsIconsOverrideAttributeIconSource()
+ {
+ var optionsIcons = new List
+ {
+ new() { Source = "https://example.com/override-icon.svg", MimeType = "image/svg+xml" }
+ };
+
+ McpServerTool tool = McpServerTool.Create([McpServerTool(IconSource = "https://example.com/tool-icon.png")] () => "result", new McpServerToolCreateOptions
+ {
+ Icons = optionsIcons
+ });
+
+ var icon = Assert.Single(tool.ProtocolTool.Icons!);
+ Assert.Equal("https://example.com/override-icon.svg", icon.Source);
+ Assert.Equal("image/svg+xml", icon.MimeType);
+ }
+
+ [Fact]
+ public void SupportsToolWithoutIcons()
+ {
+ McpServerTool tool = McpServerTool.Create([McpServerTool] () => "result");
+
+ Assert.Null(tool.ProtocolTool.Icons);
+ }
+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(DisposableToolType))]
[JsonSerializable(typeof(AsyncDisposableToolType))]