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
12 changed files with 982 additions and 8 deletions
|
|
@ -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 = "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]
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
||||
public async Task AttributeProperties_Propagated()
|
||||
{
|
||||
await using McpClient client = await CreateMcpClientForServer();
|
||||
|
||||
|
|
@ -177,6 +177,12 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
|||
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 partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
|||
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}.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
||||
public async Task AttributeProperties_Propagated()
|
||||
{
|
||||
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");
|
||||
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 partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
|||
[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]
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TitleAttributeProperty_PropagatedToTitle()
|
||||
public async Task AttributeProperties_Propagated()
|
||||
{
|
||||
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.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 partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
|||
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!;
|
||||
|
|
|
|||
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));
|
||||
}
|
||||
}
|
||||
105
tests/ModelContextProtocol.Tests/Protocol/ImplementationTests.cs
Normal file
105
tests/ModelContextProtocol.Tests/Protocol/ImplementationTests.cs
Normal file
|
|
@ -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<string> { "48x48" } },
|
||||
new() { Source = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = new List<string> { "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<Implementation>(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<Implementation>(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<JsonException>(() => JsonSerializer.Deserialize<Implementation>(invalidJson, McpJsonUtilities.DefaultOptions));
|
||||
}
|
||||
}
|
||||
97
tests/ModelContextProtocol.Tests/Protocol/PromptTests.cs
Normal file
97
tests/ModelContextProtocol.Tests/Protocol/PromptTests.cs
Normal file
|
|
@ -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<string> { "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<Prompt>(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<Prompt>(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);
|
||||
}
|
||||
}
|
||||
104
tests/ModelContextProtocol.Tests/Protocol/ResourceTests.cs
Normal file
104
tests/ModelContextProtocol.Tests/Protocol/ResourceTests.cs
Normal file
|
|
@ -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<string> { "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<Resource>(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<Resource>(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<Icon> { 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);
|
||||
}
|
||||
}
|
||||
94
tests/ModelContextProtocol.Tests/Protocol/ToolTests.cs
Normal file
94
tests/ModelContextProtocol.Tests/Protocol/ToolTests.cs
Normal file
|
|
@ -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<string> { "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<Tool>(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<Tool>(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);
|
||||
}
|
||||
}
|
||||
|
|
@ -492,4 +492,59 @@ public class McpServerPromptTests
|
|||
return _message;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsIconsInCreateOptions()
|
||||
{
|
||||
var icons = new List<Icon>
|
||||
{
|
||||
new() { Source = "https://example.com/prompt-icon.png", MimeType = "image/png", Sizes = new List<string> { "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<Icon>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -677,6 +677,62 @@ public partial class McpServerResourceTests
|
|||
public static object StaticMethod() => "42";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsIconsInResourceCreateOptions()
|
||||
{
|
||||
var icons = new List<Icon>
|
||||
{
|
||||
new() { Source = "https://example.com/resource-icon.png", MimeType = "image/png", Sizes = new List<string> { "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<Icon>
|
||||
{
|
||||
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<AIContent>))]
|
||||
|
|
|
|||
|
|
@ -678,6 +678,71 @@ public partial class McpServerToolTests
|
|||
|
||||
record Person(string Name, int Age);
|
||||
|
||||
[Fact]
|
||||
public void SupportsIconsInCreateOptions()
|
||||
{
|
||||
var icons = new List<Icon>
|
||||
{
|
||||
new() { Source = "https://example.com/icon.png", MimeType = "image/png", Sizes = new List<string> { "48x48" } },
|
||||
new() { Source = "https://example.com/icon.svg", MimeType = "image/svg+xml", Sizes = new List<string> { "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<Icon>
|
||||
{
|
||||
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))]
|
||||
|
|
|
|||
Loading…
Reference in a new issue