Skip to content

Commit ae081ca

Browse files
Add icon support to Prompts and Resources (CreateOptions and Attributes)
Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com>
1 parent a25abde commit ae081ca

File tree

9 files changed

+197
-0
lines changed

9 files changed

+197
-0
lines changed

src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ public sealed class ResourceTemplate : IBaseMetadata
7272
[JsonPropertyName("annotations")]
7373
public Annotations? Annotations { get; init; }
7474

75+
/// <summary>
76+
/// Gets or sets the icons for this resource template.
77+
/// </summary>
78+
/// <remarks>
79+
/// This can be used by clients to display the resource's icon in a user interface.
80+
/// </remarks>
81+
[JsonPropertyName("icons")]
82+
public IList<Icon>? Icons { get; set; }
83+
7584
/// <summary>
7685
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
7786
/// </summary>
@@ -108,6 +117,7 @@ public sealed class ResourceTemplate : IBaseMetadata
108117
Description = Description,
109118
MimeType = MimeType,
110119
Annotations = Annotations,
120+
Icons = Icons,
111121
Meta = Meta,
112122
McpServerResource = McpServerResource,
113123
};

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
135135
Title = options?.Title,
136136
Description = options?.Description ?? function.Description,
137137
Arguments = args,
138+
Icons = options?.Icons,
138139
};
139140

140141
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
@@ -148,6 +149,15 @@ private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, Mcp
148149
{
149150
newOptions.Name ??= promptAttr.Name;
150151
newOptions.Title ??= promptAttr.Title;
152+
153+
// Handle icon from attribute if not already specified in options
154+
if (newOptions.Icons is null && !string.IsNullOrEmpty(promptAttr.IconSource))
155+
{
156+
newOptions.Icons = new List<Icon>
157+
{
158+
new() { Source = promptAttr.IconSource }
159+
};
160+
}
151161
}
152162

153163
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
218218
Title = options?.Title,
219219
Description = options?.Description,
220220
MimeType = options?.MimeType ?? "application/octet-stream",
221+
Icons = options?.Icons,
221222
};
222223

223224
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);
@@ -233,6 +234,15 @@ private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, M
233234
newOptions.Name ??= resourceAttr.Name;
234235
newOptions.Title ??= resourceAttr.Title;
235236
newOptions.MimeType ??= resourceAttr.MimeType;
237+
238+
// Handle icon from attribute if not already specified in options
239+
if (newOptions.Icons is null && !string.IsNullOrEmpty(resourceAttr.IconSource))
240+
{
241+
newOptions.Icons = new List<Icon>
242+
{
243+
new() { Source = resourceAttr.IconSource }
244+
};
245+
}
236246
}
237247

238248
if (member.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)

src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,19 @@ public McpServerPromptAttribute()
120120

121121
/// <summary>Gets or sets the title of the prompt.</summary>
122122
public string? Title { get; set; }
123+
124+
/// <summary>
125+
/// Gets or sets the source URI for the prompt's icon.
126+
/// </summary>
127+
/// <remarks>
128+
/// <para>
129+
/// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
130+
/// When specified, a single icon will be added to the prompt.
131+
/// </para>
132+
/// <para>
133+
/// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
134+
/// use <see cref="McpServerPromptCreateOptions.Icons"/> when creating the prompt programmatically.
135+
/// </para>
136+
/// </remarks>
137+
public string? IconSource { get; set; }
123138
}

src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.AI;
2+
using ModelContextProtocol.Protocol;
23
using System.ComponentModel;
34
using System.Text.Json;
45

@@ -77,6 +78,14 @@ public sealed class McpServerPromptCreateOptions
7778
/// </remarks>
7879
public IReadOnlyList<object>? Metadata { get; set; }
7980

81+
/// <summary>
82+
/// Gets or sets the icons for this prompt.
83+
/// </summary>
84+
/// <remarks>
85+
/// This can be used by clients to display the prompt's icon in a user interface.
86+
/// </remarks>
87+
public IList<Icon>? Icons { get; set; }
88+
8089
/// <summary>
8190
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
8291
/// </summary>
@@ -90,5 +99,6 @@ internal McpServerPromptCreateOptions Clone() =>
9099
SerializerOptions = SerializerOptions,
91100
SchemaCreateOptions = SchemaCreateOptions,
92101
Metadata = Metadata,
102+
Icons = Icons,
93103
};
94104
}

src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,19 @@ public McpServerResourceAttribute()
135135

136136
/// <summary>Gets or sets the MIME (media) type of the resource.</summary>
137137
public string? MimeType { get; set; }
138+
139+
/// <summary>
140+
/// Gets or sets the source URI for the resource's icon.
141+
/// </summary>
142+
/// <remarks>
143+
/// <para>
144+
/// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
145+
/// When specified, a single icon will be added to the resource.
146+
/// </para>
147+
/// <para>
148+
/// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
149+
/// use <see cref="McpServerResourceCreateOptions.Icons"/> when creating the resource programmatically.
150+
/// </para>
151+
/// </remarks>
152+
public string? IconSource { get; set; }
138153
}

src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.AI;
2+
using ModelContextProtocol.Protocol;
23
using System.ComponentModel;
34
using System.Text.Json;
45

@@ -92,6 +93,14 @@ public sealed class McpServerResourceCreateOptions
9293
/// </remarks>
9394
public IReadOnlyList<object>? Metadata { get; set; }
9495

96+
/// <summary>
97+
/// Gets or sets the icons for this resource.
98+
/// </summary>
99+
/// <remarks>
100+
/// This can be used by clients to display the resource's icon in a user interface.
101+
/// </remarks>
102+
public IList<Icon>? Icons { get; set; }
103+
95104
/// <summary>
96105
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
97106
/// </summary>
@@ -107,5 +116,6 @@ internal McpServerResourceCreateOptions Clone() =>
107116
SerializerOptions = SerializerOptions,
108117
SchemaCreateOptions = SchemaCreateOptions,
109118
Metadata = Metadata,
119+
Icons = Icons,
110120
};
111121
}

tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,62 @@ public ChatMessage InstanceMethod()
492492
return _message;
493493
}
494494
}
495+
496+
[Fact]
497+
public void SupportsIconsInCreateOptions()
498+
{
499+
var icons = new List<Icon>
500+
{
501+
new() { Source = "https://example.com/prompt-icon.png", MimeType = "image/png", Sizes = new List<string> { "48x48" } }
502+
};
503+
504+
McpServerPrompt prompt = McpServerPrompt.Create(() => "test prompt", new McpServerPromptCreateOptions
505+
{
506+
Icons = icons
507+
});
508+
509+
Assert.NotNull(prompt.ProtocolPrompt.Icons);
510+
Assert.Single(prompt.ProtocolPrompt.Icons);
511+
Assert.Equal("https://example.com/prompt-icon.png", prompt.ProtocolPrompt.Icons[0].Source);
512+
Assert.Equal("image/png", prompt.ProtocolPrompt.Icons[0].MimeType);
513+
}
514+
515+
[Fact]
516+
public void SupportsIconSourceInAttribute()
517+
{
518+
McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt(IconSource = "https://example.com/prompt-icon.svg")] () => "test prompt");
519+
520+
Assert.NotNull(prompt.ProtocolPrompt.Icons);
521+
Assert.Single(prompt.ProtocolPrompt.Icons);
522+
Assert.Equal("https://example.com/prompt-icon.svg", prompt.ProtocolPrompt.Icons[0].Source);
523+
Assert.Null(prompt.ProtocolPrompt.Icons[0].MimeType);
524+
Assert.Null(prompt.ProtocolPrompt.Icons[0].Sizes);
525+
}
526+
527+
[Fact]
528+
public void CreateOptionsIconsOverrideAttributeIconSource_Prompt()
529+
{
530+
var optionsIcons = new List<Icon>
531+
{
532+
new() { Source = "https://example.com/override-icon.svg", MimeType = "image/svg+xml" }
533+
};
534+
535+
McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt(IconSource = "https://example.com/prompt-icon.png")] () => "test prompt", new McpServerPromptCreateOptions
536+
{
537+
Icons = optionsIcons
538+
});
539+
540+
Assert.NotNull(prompt.ProtocolPrompt.Icons);
541+
Assert.Single(prompt.ProtocolPrompt.Icons);
542+
Assert.Equal("https://example.com/override-icon.svg", prompt.ProtocolPrompt.Icons[0].Source);
543+
Assert.Equal("image/svg+xml", prompt.ProtocolPrompt.Icons[0].MimeType);
544+
}
545+
546+
[Fact]
547+
public void SupportsPromptWithoutIcons()
548+
{
549+
McpServerPrompt prompt = McpServerPrompt.Create([McpServerPrompt] () => "test prompt");
550+
551+
Assert.Null(prompt.ProtocolPrompt.Icons);
552+
}
495553
}

tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,65 @@ private class DisposableResourceType : IDisposable
677677
public static object StaticMethod() => "42";
678678
}
679679

680+
[Fact]
681+
public void SupportsIconsInResourceCreateOptions()
682+
{
683+
var icons = new List<Icon>
684+
{
685+
new() { Source = "https://example.com/resource-icon.png", MimeType = "image/png", Sizes = new List<string> { "32x32" } }
686+
};
687+
688+
McpServerResource resource = McpServerResource.Create(() => "test content", new McpServerResourceCreateOptions
689+
{
690+
UriTemplate = "test://resource/with-icon",
691+
Icons = icons
692+
});
693+
694+
Assert.NotNull(resource.ProtocolResourceTemplate.Icons);
695+
Assert.Single(resource.ProtocolResourceTemplate.Icons);
696+
Assert.Equal("https://example.com/resource-icon.png", resource.ProtocolResourceTemplate.Icons[0].Source);
697+
Assert.Equal("image/png", resource.ProtocolResourceTemplate.Icons[0].MimeType);
698+
}
699+
700+
[Fact]
701+
public void SupportsIconSourceInResourceAttribute()
702+
{
703+
McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource", IconSource = "https://example.com/resource-icon.svg")] () => "test content");
704+
705+
Assert.NotNull(resource.ProtocolResourceTemplate.Icons);
706+
Assert.Single(resource.ProtocolResourceTemplate.Icons);
707+
Assert.Equal("https://example.com/resource-icon.svg", resource.ProtocolResourceTemplate.Icons[0].Source);
708+
Assert.Null(resource.ProtocolResourceTemplate.Icons[0].MimeType);
709+
Assert.Null(resource.ProtocolResourceTemplate.Icons[0].Sizes);
710+
}
711+
712+
[Fact]
713+
public void CreateOptionsIconsOverrideAttributeIconSource_Resource()
714+
{
715+
var optionsIcons = new List<Icon>
716+
{
717+
new() { Source = "https://example.com/override-icon.svg", MimeType = "image/svg+xml" }
718+
};
719+
720+
McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource", IconSource = "https://example.com/resource-icon.png")] () => "test content", new McpServerResourceCreateOptions
721+
{
722+
Icons = optionsIcons
723+
});
724+
725+
Assert.NotNull(resource.ProtocolResourceTemplate.Icons);
726+
Assert.Single(resource.ProtocolResourceTemplate.Icons);
727+
Assert.Equal("https://example.com/override-icon.svg", resource.ProtocolResourceTemplate.Icons[0].Source);
728+
Assert.Equal("image/svg+xml", resource.ProtocolResourceTemplate.Icons[0].MimeType);
729+
}
730+
731+
[Fact]
732+
public void SupportsResourceWithoutIcons()
733+
{
734+
McpServerResource resource = McpServerResource.Create([McpServerResource(UriTemplate = "test://resource")] () => "test content");
735+
736+
Assert.Null(resource.ProtocolResourceTemplate.Icons);
737+
}
738+
680739
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
681740
[JsonSerializable(typeof(DisposableResourceType))]
682741
[JsonSerializable(typeof(List<AIContent>))]

0 commit comments

Comments
 (0)