From ab6d3e1ac51c129d311d19afe2925e434372bbd8 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Sep 2025 17:36:12 +0300 Subject: [PATCH 1/2] Update MEAI dependencies. (#801) --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 27431ddf1..7ea273002 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,7 @@ true 9.0.5 10.0.0-preview.4.25258.110 - 9.8.0 + 9.9.0 @@ -53,7 +53,7 @@ all - + From be6cd290d2ff693559ecbfa2a40b6d727252b534 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 27 Aug 2025 19:27:05 +0300 Subject: [PATCH 2/2] Support multiple contents in sampling results. --- .../Client/McpClient.Methods.cs | 2 +- .../Client/McpClientExtensions.cs | 2 +- .../McpJsonUtilities.McpJsonConverter.cs | 91 +++++++++++++++++++ .../Protocol/CreateMessageResult.cs | 55 ++++++++++- tests/Common/Utils/TestServerTransport.cs | 2 +- .../HttpServerIntegrationTests.cs | 2 +- .../MapMcpTests.cs | 2 +- .../Client/McpClientCreationTests.cs | 8 +- .../ClientIntegrationTests.cs | 2 +- .../DockerEverythingServerTests.cs | 2 +- .../Server/McpServerExtensionsTests.cs | 4 +- .../Server/McpServerTests.cs | 2 +- 12 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 src/ModelContextProtocol.Core/McpJsonUtilities.McpJsonConverter.cs diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 560ce31dc..9a8ea8387 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -623,7 +623,7 @@ internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatRespo return new() { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Contents = [content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }], Model = chatResponse.ModelId ?? "unknown", Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index e987f30f6..ad8828058 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -703,7 +703,7 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat return new() { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Contents = [content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }], Model = chatResponse.ModelId ?? "unknown", Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.McpJsonConverter.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.McpJsonConverter.cs new file mode 100644 index 000000000..553b2a37c --- /dev/null +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.McpJsonConverter.cs @@ -0,0 +1,91 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace ModelContextProtocol; + +public static partial class McpJsonUtilities +{ + [ThreadStatic] + private static McpSession? t_currentMcpSession; + + /// + /// Serializes the given value to a using the provided , + /// + internal static JsonNode? SerializeContextual(T? value, JsonTypeInfo typeInfo, McpSession session) + { + if (session is null) + { + Throw.IfNull(session); + } + + if (t_currentMcpSession is not null) + { + throw new InvalidOperationException("Reentrant call to McpJsonUtilities.SerializeContextual detected."); + } + + t_currentMcpSession = session; + try + { + return JsonSerializer.SerializeToNode(value!, typeInfo); + } + finally + { + t_currentMcpSession = null; + } + } + + /// + /// Deserializes the given value to a using the provided , + /// + internal static T? DeserializeContextual(JsonNode? node, JsonTypeInfo typeInfo, McpSession session) + { + if (session is null) + { + Throw.IfNull(session); + } + + if (t_currentMcpSession is not null) + { + throw new InvalidOperationException("Reentrant call to McpJsonUtilities.DeserializeContextual detected."); + } + + t_currentMcpSession = session; + try + { + return JsonSerializer.Deserialize(node, typeInfo); + } + finally + { + t_currentMcpSession = null; + } + } + + /// + /// Defines an abstract JSON converter that has access to the current context during serialization and deserialization. + /// + /// The type being converted. + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class McpContextualJsonConverter : JsonConverter + { + /// + /// Reads the JSON representation of the value. + /// + public abstract T? Read(ref Utf8JsonReader reader, McpSession? session, JsonSerializerOptions options); + + /// + /// Writes the JSON representation of the value. + /// + public abstract void Write(Utf8JsonWriter writer, T value, McpSession? session, JsonSerializerOptions options); + + /// + public sealed override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + Read(ref reader, t_currentMcpSession, options); + + /// + public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + Write(writer, value, t_currentMcpSession, options); + } +} diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index ba599d6c5..07697ff71 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; +using System.Text.Json; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -14,7 +16,26 @@ public sealed class CreateMessageResult : Result /// Gets or sets the content of the message. /// [JsonPropertyName("content")] - public required ContentBlock Content { get; init; } + [JsonConverter(typeof(SingleOrArrayContentConverter))] + public required List Contents + { + get; + init + { + if (value is null or []) + { + throw new ArgumentException(nameof(Contents)); + } + + field = value; + } + } + + /// + /// Gets or sets the content of the message. + /// + [JsonIgnore] + public ContentBlock Content { get => Contents.First(); init => Contents = [value]; } /// /// Gets or sets the name of the model that generated the message. @@ -50,4 +71,36 @@ public sealed class CreateMessageResult : Result /// [JsonPropertyName("role")] public required Role Role { get; init; } + + /// + /// Defines a converter that handles deserialization of a single or an array of into a . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class SingleOrArrayContentConverter : McpJsonUtilities.McpContextualJsonConverter> + { + /// + public override List? Read(ref Utf8JsonReader reader, McpSession? session, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + var single = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo()); + return [single]; + } + + return JsonSerializer.Deserialize(ref reader, options.GetTypeInfo>()); + } + + /// + public override void Write(Utf8JsonWriter writer, List value, McpSession? session, JsonSerializerOptions options) + { + if (session?.NegotiatedProtocolVersion is string version && + DateTime.Parse(version) < new DateTime(2025, 09, 18)) // A hypothetical future version + { + // The negotiated protocol version is before 2025-09-18, so we need to serialize as a single object. + JsonSerializer.Serialize(value.Single(), options.GetTypeInfo()); + } + + JsonSerializer.Serialize(value, options.GetTypeInfo>()); + } + } } diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs index f875fe504..c0c2ce08c 100644 --- a/tests/Common/Utils/TestServerTransport.cs +++ b/tests/Common/Utils/TestServerTransport.cs @@ -74,7 +74,7 @@ private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cance await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Contents = [new TextContentBlock { Text = "" }], Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), }, cancellationToken); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 5da37146a..c7dac5d21 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -260,7 +260,7 @@ public async Task Sampling_Sse_TestServer() { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Contents = [new TextContentBlock { Text = "Test response" }], }; }; await using var client = await GetClientAsync(options); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index bb9746ed7..248a69cb1 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -179,7 +179,7 @@ public async Task Sampling_DoesNotCloseStream_Prematurely() { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Sampling response from client" }, + Contents = [new TextContentBlock { Text = "Sampling response from client" }], }; }, }, diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs index 15127502e..b62206d8c 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs @@ -68,10 +68,10 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) SamplingHandler = async (c, p, t) => new CreateMessageResult { - Content = new TextContentBlock { Text = "result" }, - Model = "test-model", - Role = Role.User, - StopReason = "endTurn" + Contents = [new TextContentBlock { Text = "result" }], + Model = "test-model", + Role = Role.User, + StopReason = "endTurn" }, }, Roots = new RootsCapability diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index e2f03805f..da7e78aef 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -385,7 +385,7 @@ public async Task Sampling_Stdio(string clientId) { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Contents = [new TextContentBlock { Text = "Test response" }], }; }, }, diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index 842371f88..a7b727565 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -83,7 +83,7 @@ public async Task Sampling_Sse_EverythingServer() { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Contents = [new TextContentBlock { Text = "Test response" }], }; }, }, diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs index 5569f993c..2db9e5cfb 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs @@ -76,7 +76,7 @@ public async Task SampleAsync_Request_Forwards_To_McpServer_SendRequestAsync() var resultPayload = new CreateMessageResult { - Content = new TextContentBlock { Text = "resp" }, + Contents = [new TextContentBlock { Text = "resp" }], Model = "test-model", Role = Role.Assistant, StopReason = "endTurn", @@ -113,7 +113,7 @@ public async Task SampleAsync_Messages_Forwards_To_McpServer_SendRequestAsync() var resultPayload = new CreateMessageResult { - Content = new TextContentBlock { Text = "resp" }, + Contents = [new TextContentBlock { Text = "resp" }], Model = "test-model", Role = Role.Assistant, StopReason = "endTurn", diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 736d63ec4..8847f88ad 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -668,7 +668,7 @@ public override Task SendRequestAsync(JsonRpcRequest request, C CreateMessageResult result = new() { - Content = new TextContentBlock { Text = "The Eiffel Tower." }, + Contents = [new TextContentBlock { Text = "The Eiffel Tower." }], Model = "amazingmodel", Role = Role.Assistant, StopReason = "endTurn",