Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ContentBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ private static JsonSerializerOptions CreateNestedOptions(JsonSerializerOptions c
// this custom converter. Keep caller contracts first, then supply the SDK metadata
// (including nested collection types) using the same settings and converters.
JsonSerializerOptions nestedOptions = new(callerOptions);
nestedOptions.TypeInfoResolverChain.Add(McpJsonUtilities.JsonContext.Default);

// When the caller is the SDK context itself (for example the server writing a
// CallToolResult), it is already the resolver and can't be added to its own chain.
if (!ReferenceEquals(nestedOptions.TypeInfoResolver, McpJsonUtilities.JsonContext.Default))
{
nestedOptions.TypeInfoResolverChain.Add(McpJsonUtilities.JsonContext.Default);
}

nestedOptions.MakeReadOnly();
return nestedOptions;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

namespace ModelContextProtocol.Tests.Server;

public class McpServerToolAnnotatedContentTests : ClientServerTestBase
{
public McpServerToolAnnotatedContentTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithTools<AnnotatedContentTools>();
}

[Fact]
public async Task CallTool_TextBlockWithAnnotations_RoundTrips()
{
await using McpClient client = await CreateMcpClientForServer();

var result = await client.CallToolAsync("annotated_text", cancellationToken: TestContext.Current.CancellationToken);

Assert.NotEqual(true, result.IsError);
var block = Assert.IsType<TextContentBlock>(Assert.Single(result.Content));
Assert.Equal("hello", block.Text);
Assert.NotNull(block.Annotations);
Assert.Equal([Role.User], block.Annotations.Audience);
Assert.Equal((float?)0.5, block.Annotations.Priority);
}

[Fact]
public async Task CallTool_ResourceLinkWithAnnotationsAndIcons_RoundTrips()
{
await using McpClient client = await CreateMcpClientForServer();

var result = await client.CallToolAsync("annotated_link", cancellationToken: TestContext.Current.CancellationToken);

Assert.NotEqual(true, result.IsError);
var block = Assert.IsType<ResourceLinkBlock>(Assert.Single(result.Content));
Assert.Equal("file:///test", block.Uri);
Assert.NotNull(block.Annotations);
Assert.Equal([Role.Assistant], block.Annotations.Audience);
Assert.Equal("https://example.com/icon.png", Assert.Single(Assert.IsAssignableFrom<IList<Icon>>(block.Icons)).Source);
}

[McpServerToolType]
public sealed class AnnotatedContentTools
{
[McpServerTool(Name = "annotated_text")]
public static CallToolResult AnnotatedText() => new()
{
Content = [new TextContentBlock { Text = "hello", Annotations = new() { Audience = [Role.User], Priority = 0.5f } }],
};

[McpServerTool(Name = "annotated_link")]
public static CallToolResult AnnotatedLink() => new()
{
Content =
[
new ResourceLinkBlock
{
Uri = "file:///test",
Name = "test",
Annotations = new() { Audience = [Role.Assistant] },
Icons = [new Icon { Source = "https://example.com/icon.png" }],
},
],
};
}
}