Normalize CallToolResult.StructuredContent to JsonElement? (#1357)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Copilot 2026-02-23 18:04:09 -05:00 • committed by GitHub
commit 0a0a575a3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 25 additions and 21 deletions

View file

@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol; namespace ModelContextProtocol.Protocol;
@ -41,7 +41,7 @@ public sealed class CallToolResult : Result
/// Gets or sets an optional JSON object representing the structured result of the tool call. /// Gets or sets an optional JSON object representing the structured result of the tool call.
/// </summary> /// </summary>
[JsonPropertyName("structuredContent")] [JsonPropertyName("structuredContent")]
public JsonNode? StructuredContent { get; set; } public JsonElement? StructuredContent { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether the tool call was unsuccessful. /// Gets or sets a value that indicates whether the tool call was unsuccessful.

View file

@ -266,7 +266,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
object? result; object? result;
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
JsonNode? structuredContent = CreateStructuredResponse(result); JsonElement? structuredContent = CreateStructuredResponse(result);
return result switch return result switch
{ {
AIContent aiContent => new() AIContent aiContent => new()
@ -529,7 +529,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
return outputSchema; return outputSchema;
} }
private JsonNode? CreateStructuredResponse(object? aiFunctionResult) private JsonElement? CreateStructuredResponse(object? aiFunctionResult)
{ {
if (ProtocolTool.OutputSchema is null) if (ProtocolTool.OutputSchema is null)
{ {
@ -537,25 +537,29 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
return null; return null;
} }
JsonNode? nodeResult = aiFunctionResult switch JsonElement? elementResult = aiFunctionResult switch
{ {
JsonNode node => node, JsonElement jsonElement => jsonElement,
JsonElement jsonElement => JsonSerializer.SerializeToNode(jsonElement, McpJsonUtilities.JsonContext.Default.JsonElement), JsonNode node => JsonSerializer.SerializeToElement(node, McpJsonUtilities.JsonContext.Default.JsonNode),
_ => JsonSerializer.SerializeToNode(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))), null => null,
_ => JsonSerializer.SerializeToElement(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
}; };
if (_structuredOutputRequiresWrapping) if (_structuredOutputRequiresWrapping)
{ {
return new JsonObject JsonNode? resultNode = elementResult is { } je
? JsonSerializer.SerializeToNode(je, McpJsonUtilities.JsonContext.Default.JsonElement)
: null;
return JsonSerializer.SerializeToElement(new JsonObject
{ {
["result"] = nodeResult ["result"] = resultNode
}; }, McpJsonUtilities.JsonContext.Default.JsonObject);
} }
return nodeResult; return elementResult;
} }
private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable<AIContent> contentItems, JsonNode? structuredContent) private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable<AIContent> contentItems, JsonElement? structuredContent)
{ {
List<ContentBlock> contentList = []; List<ContentBlock> contentList = [];
bool allErrorContent = true; bool allErrorContent = true;

View file

@ -122,7 +122,7 @@ public class McpClientToolTests : ClientServerTestBase
new() new()
{ {
Content = [new TextContentBlock { Text = "Regular content" }], Content = [new TextContentBlock { Text = "Regular content" }],
StructuredContent = JsonNode.Parse("{\"key\":\"value\"}") StructuredContent = JsonElement.Parse("{\"key\":\"value\"}")
}; };
// Tool that returns CallToolResult with Meta // Tool that returns CallToolResult with Meta

View file

@ -12,7 +12,7 @@ public static class CallToolResultTests
var original = new CallToolResult var original = new CallToolResult
{ {
Content = [new TextContentBlock { Text = "Result text" }], Content = [new TextContentBlock { Text = "Result text" }],
StructuredContent = JsonNode.Parse("""{"temperature":72}"""), StructuredContent = JsonElement.Parse("""{"temperature":72}"""),
IsError = false, IsError = false,
Task = new McpTask Task = new McpTask
{ {
@ -32,7 +32,7 @@ public static class CallToolResultTests
var textBlock = Assert.IsType<TextContentBlock>(deserialized.Content[0]); var textBlock = Assert.IsType<TextContentBlock>(deserialized.Content[0]);
Assert.Equal("Result text", textBlock.Text); Assert.Equal("Result text", textBlock.Text);
Assert.NotNull(deserialized.StructuredContent); Assert.NotNull(deserialized.StructuredContent);
Assert.Equal(72, deserialized.StructuredContent["temperature"]!.GetValue<int>()); Assert.Equal(72, deserialized.StructuredContent.Value.GetProperty("temperature").GetInt32());
Assert.False(deserialized.IsError); Assert.False(deserialized.IsError);
Assert.NotNull(deserialized.Task); Assert.NotNull(deserialized.Task);
Assert.Equal("task-1", deserialized.Task.TaskId); Assert.Equal("task-1", deserialized.Task.TaskId);

View file

@ -252,7 +252,7 @@ public class UnknownPropertiesTests
// Assert // Assert
Assert.NotNull(deserialized); Assert.NotNull(deserialized);
Assert.NotNull(deserialized.StructuredContent); Assert.NotNull(deserialized.StructuredContent);
Assert.Equal("correctly placed here", deserialized.StructuredContent["result"]?.ToString()); Assert.Equal("correctly placed here", deserialized.StructuredContent.Value.GetProperty("result").GetString());
Assert.Equal(42, (int?)deserialized.StructuredContent["value"]); Assert.Equal(42, deserialized.StructuredContent.Value.GetProperty("value").GetInt32());
} }
} }

View file

@ -654,11 +654,11 @@ public partial class McpServerToolTests
} }
} }
private static void AssertMatchesJsonSchema(JsonElement schemaDoc, JsonNode? value) private static void AssertMatchesJsonSchema(JsonElement schemaDoc, JsonElement? value)
{ {
JsonSchema schema = JsonSerializer.Deserialize(schemaDoc, JsonContext2.Default.JsonSchema)!; JsonSchema schema = JsonSerializer.Deserialize(schemaDoc, JsonContext2.Default.JsonSchema)!;
EvaluationOptions options = new() { OutputFormat = OutputFormat.List }; EvaluationOptions options = new() { OutputFormat = OutputFormat.List };
EvaluationResults results = schema.Evaluate(JsonSerializer.SerializeToElement(value, JsonContext2.Default.JsonNode), options); EvaluationResults results = schema.Evaluate(value!.Value, options);
if (!results.IsValid) if (!results.IsValid)
{ {
IEnumerable<string> errors = (results.Details ?? []) IEnumerable<string> errors = (results.Details ?? [])
@ -670,7 +670,7 @@ public partial class McpServerToolTests
Schema: Schema:
{JsonSerializer.Serialize(schema)} {JsonSerializer.Serialize(schema)}
Instance: Instance:
{value?.ToJsonString() ?? "null"} {value?.ToString() ?? "null"}
Errors: Errors:
{string.Join(Environment.NewLine, errors)} {string.Join(Environment.NewLine, errors)}
"""); """);