Implement SEP-973: Icons and metadata support for Implementations, Resources, Tools, and Prompts (#802)
* Initial plan * Initial plan for SEP-973 implementation Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * 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> * Complete SEP-973 implementation with documentation and fix property consistency Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Address code review feedback: improve tests, remove docs, split test files Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Fix tests to use S.T.J. source generator * Styling fixes * Simplify Icons property documentation as suggested in code review Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Rename Icon.Src property to Icon.Source for more .NET-y naming Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Add icon support to McpServerTool infrastructure and attributes Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Cleanup * Refactor icon tests to use Delegate overload instead of MethodInfo pattern Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Change Icon.Sizes property from string to IList<string> per spec update Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> * Add icon support to Prompts and Resources (CreateOptions and Attributes) Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Add client-server integration tests for Icons and WebsiteUrl Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> * Fix nullability warnings * Revert "Add client-server integration tests for Icons and WebsiteUrl" This reverts commit f37de15569e54e86a0709121c9bdf80c5045511c. * More nullability fixes * Add more tests * Address code review feedback: add Theme property, fix docs, improve test patterns Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> * Update Icon tests to exercise Theme property Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> * Add Theme property assertions to client-server integration tests Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> * Fix nullability --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> Co-authored-by: Mackinnon Buck <mackinnon.buck@gmail.com> Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> Co-authored-by: David Cantú <dacantu@microsoft.com>
This commit is contained in:
parent
a0049c7a3c
commit
f28639119b
27 changed files with 982 additions and 8 deletions
85
src/ModelContextProtocol.Core/Protocol/Icon.cs
Normal file
85
src/ModelContextProtocol.Core/Protocol/Icon.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an icon that can be used to visually identify an implementation, resource, tool, or prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Clients that support rendering icons MUST support at least the following MIME types:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>image/png - PNG images (safe, universal compatibility)</description></item>
|
||||||
|
/// <item><description>image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility)</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// Clients that support rendering icons SHOULD also support:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>image/svg+xml - SVG images (scalable but requires security precautions)</description></item>
|
||||||
|
/// <item><description>image/webp - WebP images (modern, efficient format)</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class Icon
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URI pointing to the icon resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This can be an HTTP/HTTPS URL pointing to an image file or a data URI with base64-encoded image data.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the client/server
|
||||||
|
/// or a trusted domain.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("src")]
|
||||||
|
public required string Source { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional MIME type of the icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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".
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("mimeType")]
|
||||||
|
public string? MimeType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional size specifications for the icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This can specify one or more sizes at which the icon file can be used.
|
||||||
|
/// Examples include "48x48", "any" for scalable formats like SVG.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// If not provided, clients should assume that the icon can be used at any size.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("sizes")]
|
||||||
|
public IList<string>? Sizes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional theme for this icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Can be "light", "dark", or a custom theme identifier.
|
||||||
|
/// Used to specify which UI theme the icon is designed for.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("theme")]
|
||||||
|
public string? Theme { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -36,4 +36,28 @@ public sealed class Implementation : IBaseMetadata
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("version")]
|
[JsonPropertyName("version")]
|
||||||
public required string Version { get; set; }
|
public required string Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional list of icons for this implementation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the implementation's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("icons")]
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional URL of the website for this implementation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This URL can be used by clients to link to documentation or more information about the implementation.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Consumers SHOULD take steps to ensure URLs are from the same domain as the client/server
|
||||||
|
/// or a trusted domain to prevent security issues.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("websiteUrl")]
|
||||||
|
public string? WebsiteUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +52,15 @@ public sealed class Prompt : IBaseMetadata
|
||||||
[JsonPropertyName("arguments")]
|
[JsonPropertyName("arguments")]
|
||||||
public IList<PromptArgument>? Arguments { get; set; }
|
public IList<PromptArgument>? Arguments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional list of icons for this prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the prompt's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("icons")]
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,15 @@ public sealed class Resource : IBaseMetadata
|
||||||
[JsonPropertyName("size")]
|
[JsonPropertyName("size")]
|
||||||
public long? Size { get; init; }
|
public long? Size { get; init; }
|
||||||
|
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,15 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
[JsonPropertyName("annotations")]
|
[JsonPropertyName("annotations")]
|
||||||
public Annotations? Annotations { get; init; }
|
public Annotations? Annotations { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional list of icons for this resource template.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the resource template's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("icons")]
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -108,6 +117,7 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
Description = Description,
|
Description = Description,
|
||||||
MimeType = MimeType,
|
MimeType = MimeType,
|
||||||
Annotations = Annotations,
|
Annotations = Annotations,
|
||||||
|
Icons = Icons,
|
||||||
Meta = Meta,
|
Meta = Meta,
|
||||||
McpServerResource = McpServerResource,
|
McpServerResource = McpServerResource,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,15 @@ public sealed class Tool : IBaseMetadata
|
||||||
[JsonPropertyName("annotations")]
|
[JsonPropertyName("annotations")]
|
||||||
public ToolAnnotations? Annotations { get; set; }
|
public ToolAnnotations? Annotations { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an optional list of icons for this tool.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the tool's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("icons")]
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
/// Gets or sets metadata reserved by MCP for protocol-level metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
Title = options?.Title,
|
Title = options?.Title,
|
||||||
Description = options?.Description ?? function.Description,
|
Description = options?.Description ?? function.Description,
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
|
Icons = options?.Icons,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
|
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
|
||||||
|
|
@ -148,6 +149,12 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
{
|
{
|
||||||
newOptions.Name ??= promptAttr.Name;
|
newOptions.Name ??= promptAttr.Name;
|
||||||
newOptions.Title ??= promptAttr.Title;
|
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<DescriptionAttribute>() is { } descAttr)
|
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||||
Title = options?.Title,
|
Title = options?.Title,
|
||||||
Description = options?.Description,
|
Description = options?.Description,
|
||||||
MimeType = options?.MimeType ?? "application/octet-stream",
|
MimeType = options?.MimeType ?? "application/octet-stream",
|
||||||
|
Icons = options?.Icons,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);
|
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);
|
||||||
|
|
@ -233,6 +234,12 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||||
newOptions.Name ??= resourceAttr.Name;
|
newOptions.Name ??= resourceAttr.Name;
|
||||||
newOptions.Title ??= resourceAttr.Title;
|
newOptions.Title ??= resourceAttr.Title;
|
||||||
newOptions.MimeType ??= resourceAttr.MimeType;
|
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<DescriptionAttribute>() is { } descAttr)
|
if (member.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||||
Description = options?.Description ?? function.Description,
|
Description = options?.Description ?? function.Description,
|
||||||
InputSchema = function.JsonSchema,
|
InputSchema = function.JsonSchema,
|
||||||
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
|
OutputSchema = CreateOutputSchema(function, options, out bool structuredOutputRequiresWrapping),
|
||||||
|
Icons = options?.Icons,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options is not null)
|
if (options is not null)
|
||||||
|
|
@ -176,6 +177,11 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||||
newOptions.ReadOnly ??= readOnly;
|
newOptions.ReadOnly ??= readOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newOptions.Icons is null && toolAttr.IconSource is { Length: > 0 } iconSource)
|
||||||
|
{
|
||||||
|
newOptions.Icons = [new() { Source = iconSource }];
|
||||||
|
}
|
||||||
|
|
||||||
newOptions.UseStructuredContent = toolAttr.UseStructuredContent;
|
newOptions.UseStructuredContent = toolAttr.UseStructuredContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,4 +120,19 @@ public sealed class McpServerPromptAttribute : Attribute
|
||||||
|
|
||||||
/// <summary>Gets or sets the title of the prompt.</summary>
|
/// <summary>Gets or sets the title of the prompt.</summary>
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the source URI for the prompt's icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
|
||||||
|
/// use <see cref="McpServerPromptCreateOptions.Icons"/> when creating the prompt programmatically.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public string? IconSource { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using ModelContextProtocol.Protocol;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
@ -77,6 +78,14 @@ public sealed class McpServerPromptCreateOptions
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IReadOnlyList<object>? Metadata { get; set; }
|
public IReadOnlyList<object>? Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icons for this prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the prompt's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
|
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -90,5 +99,6 @@ public sealed class McpServerPromptCreateOptions
|
||||||
SerializerOptions = SerializerOptions,
|
SerializerOptions = SerializerOptions,
|
||||||
SchemaCreateOptions = SchemaCreateOptions,
|
SchemaCreateOptions = SchemaCreateOptions,
|
||||||
Metadata = Metadata,
|
Metadata = Metadata,
|
||||||
|
Icons = Icons,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,4 +135,19 @@ public sealed class McpServerResourceAttribute : Attribute
|
||||||
|
|
||||||
/// <summary>Gets or sets the MIME (media) type of the resource.</summary>
|
/// <summary>Gets or sets the MIME (media) type of the resource.</summary>
|
||||||
public string? MimeType { get; set; }
|
public string? MimeType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the source URI for the resource's icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
|
||||||
|
/// use <see cref="McpServerResourceCreateOptions.Icons"/> when creating the resource programmatically.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public string? IconSource { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
|
using ModelContextProtocol.Protocol;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
@ -92,6 +93,14 @@ public sealed class McpServerResourceCreateOptions
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IReadOnlyList<object>? Metadata { get; set; }
|
public IReadOnlyList<object>? Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icons for this resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the resource's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
|
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -107,5 +116,6 @@ public sealed class McpServerResourceCreateOptions
|
||||||
SerializerOptions = SerializerOptions,
|
SerializerOptions = SerializerOptions,
|
||||||
SchemaCreateOptions = SchemaCreateOptions,
|
SchemaCreateOptions = SchemaCreateOptions,
|
||||||
Metadata = Metadata,
|
Metadata = Metadata,
|
||||||
|
Icons = Icons,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,4 +254,19 @@ public sealed class McpServerToolAttribute : Attribute
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool UseStructuredContent { get; set; }
|
public bool UseStructuredContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the source URI for the tool's icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// For more advanced icon configuration (multiple icons, MIME type specification, size characteristics),
|
||||||
|
/// use <see cref="McpServerToolCreateOptions.Icons"/> when creating the tool programmatically.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public string? IconSource { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,14 @@ public sealed class McpServerToolCreateOptions
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IReadOnlyList<object>? Metadata { get; set; }
|
public IReadOnlyList<object>? Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icons for this tool.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be used by clients to display the tool's icon in a user interface.
|
||||||
|
/// </remarks>
|
||||||
|
public IList<Icon>? Icons { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
|
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -182,5 +190,6 @@ public sealed class McpServerToolCreateOptions
|
||||||
SerializerOptions = SerializerOptions,
|
SerializerOptions = SerializerOptions,
|
||||||
SchemaCreateOptions = SchemaCreateOptions,
|
SchemaCreateOptions = SchemaCreateOptions,
|
||||||
Metadata = Metadata,
|
Metadata = Metadata,
|
||||||
|
Icons = Icons,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,46 @@ public class McpClientTests : ClientServerTestBase
|
||||||
}
|
}
|
||||||
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 = "ValuesSetViaAttr" })]);
|
||||||
mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })]);
|
mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })]);
|
||||||
|
|
||||||
|
services.Configure<McpServerOptions>(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]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
public async Task AttributeProperties_Propagated()
|
||||||
{
|
{
|
||||||
await using McpClient client = await CreateMcpClientForServer();
|
await using McpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
|
@ -177,6 +177,12 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
McpClientPrompt prompt = prompts.First(t => t.Name == "returns_string");
|
McpClientPrompt prompt = prompts.First(t => t.Name == "returns_string");
|
||||||
|
|
||||||
Assert.Equal("This is a title", prompt.Title);
|
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]
|
[Fact]
|
||||||
|
|
@ -325,12 +331,11 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
new(ChatRole.User, "Summarize."),
|
new(ChatRole.User, "Summarize."),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
[McpServerPrompt, Description("Returns chat messages")]
|
[McpServerPrompt, Description("Returns chat messages")]
|
||||||
public static ChatMessage[] ThrowsException([Description("The first parameter")] string message) =>
|
public static ChatMessage[] ThrowsException([Description("The first parameter")] string message) =>
|
||||||
throw new FormatException("uh oh");
|
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) =>
|
public string ReturnsString([Description("The first parameter")] string message) =>
|
||||||
$"The prompt is: {message}. The id is {id}.";
|
$"The prompt is: {message}. The id is {id}.";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
public async Task AttributeProperties_Propagated()
|
||||||
{
|
{
|
||||||
await using McpClient client = await CreateMcpClientForServer();
|
await using McpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
|
@ -211,11 +211,23 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
||||||
McpClientResource resource = resources.First(t => t.Name == "some_neat_direct_resource");
|
McpClientResource resource = resources.First(t => t.Name == "some_neat_direct_resource");
|
||||||
Assert.Equal("This is a title", resource.Title);
|
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);
|
var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(resourceTemplates);
|
Assert.NotNull(resourceTemplates);
|
||||||
Assert.NotEmpty(resourceTemplates);
|
Assert.NotEmpty(resourceTemplates);
|
||||||
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == "some_neat_templated_resource");
|
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == "some_neat_templated_resource");
|
||||||
Assert.Equal("This is another title", resourceTemplate.Title);
|
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]
|
[Fact]
|
||||||
|
|
@ -341,10 +353,10 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
||||||
[McpServerResourceType]
|
[McpServerResourceType]
|
||||||
public sealed class SimpleResources
|
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";
|
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}";
|
public static string SomeNeatTemplatedResource(string name) => $"This is a neat resource with parameters: {name}";
|
||||||
|
|
||||||
[McpServerResource]
|
[McpServerResource]
|
||||||
|
|
|
||||||
|
|
@ -630,7 +630,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
public async Task AttributeProperties_Propagated()
|
||||||
{
|
{
|
||||||
await using McpClient client = await CreateMcpClientForServer();
|
await using McpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
|
@ -643,6 +643,12 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
Assert.Equal("This is a title", tool.Title);
|
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.Title);
|
||||||
Assert.Equal("This is a title", tool.ProtocolTool.Annotations?.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]
|
[Fact]
|
||||||
|
|
@ -786,7 +792,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
return cancellationToken.GetHashCode();
|
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)
|
public static string EchoComplex(ComplexObject complex)
|
||||||
{
|
{
|
||||||
return complex.Name!;
|
return complex.Name!;
|
||||||
|
|
|
||||||
95
tests/ModelContextProtocol.Tests/Protocol/IconTests.cs
Normal file
95
tests/ModelContextProtocol.Tests/Protocol/IconTests.cs
Normal file
|
|
@ -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<string> { "48x48" },
|
||||||
|
Theme = "light"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act - Serialize to JSON
|
||||||
|
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
// Act - Deserialize back from JSON
|
||||||
|
var deserialized = JsonSerializer.Deserialize<Icon>(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<Icon>(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<string> { "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<JsonException>(() => JsonSerializer.Deserialize<Icon>(invalidJson, McpJsonUtilities.DefaultOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("false")]
|
||||||
|
[InlineData("true")]
|
||||||
|
[InlineData("42")]
|
||||||
|
[InlineData("[]")]
|
||||||
|
public static void Icon_DeserializationWithInvalidJson_ThrowsJsonException(string invalidJson)
|
||||||
|
{
|
||||||
|
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Icon>(invalidJson, McpJsonUtilities.DefaultOptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue