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
-
+
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",