Skip to content

Commit 0a0a575

Browse files
Copilotstephentoub
andauthored
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>
1 parent a2ee0c8 commit 0a0a575

File tree

6 files changed

+25
-21
lines changed

6 files changed

+25
-21
lines changed

src/ModelContextProtocol.Core/Protocol/CallToolResult.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using System.Diagnostics.CodeAnalysis;
2-
using System.Text.Json.Nodes;
2+
using System.Text.Json;
33
using System.Text.Json.Serialization;
44

55
namespace ModelContextProtocol.Protocol;
@@ -41,7 +41,7 @@ public sealed class CallToolResult : Result
4141
/// Gets or sets an optional JSON object representing the structured result of the tool call.
4242
/// </summary>
4343
[JsonPropertyName("structuredContent")]
44-
public JsonNode? StructuredContent { get; set; }
44+
public JsonElement? StructuredContent { get; set; }
4545

4646
/// <summary>
4747
/// Gets or sets a value that indicates whether the tool call was unsuccessful.

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ public override async ValueTask<CallToolResult> InvokeAsync(
266266
object? result;
267267
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
268268

269-
JsonNode? structuredContent = CreateStructuredResponse(result);
269+
JsonElement? structuredContent = CreateStructuredResponse(result);
270270
return result switch
271271
{
272272
AIContent aiContent => new()
@@ -529,33 +529,37 @@ typeProperty.ValueKind is not JsonValueKind.String ||
529529
return outputSchema;
530530
}
531531

532-
private JsonNode? CreateStructuredResponse(object? aiFunctionResult)
532+
private JsonElement? CreateStructuredResponse(object? aiFunctionResult)
533533
{
534534
if (ProtocolTool.OutputSchema is null)
535535
{
536536
// Only provide structured responses if the tool has an output schema defined.
537537
return null;
538538
}
539539

540-
JsonNode? nodeResult = aiFunctionResult switch
540+
JsonElement? elementResult = aiFunctionResult switch
541541
{
542-
JsonNode node => node,
543-
JsonElement jsonElement => JsonSerializer.SerializeToNode(jsonElement, McpJsonUtilities.JsonContext.Default.JsonElement),
544-
_ => JsonSerializer.SerializeToNode(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
542+
JsonElement jsonElement => jsonElement,
543+
JsonNode node => JsonSerializer.SerializeToElement(node, McpJsonUtilities.JsonContext.Default.JsonNode),
544+
null => null,
545+
_ => JsonSerializer.SerializeToElement(aiFunctionResult, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))),
545546
};
546547

547548
if (_structuredOutputRequiresWrapping)
548549
{
549-
return new JsonObject
550+
JsonNode? resultNode = elementResult is { } je
551+
? JsonSerializer.SerializeToNode(je, McpJsonUtilities.JsonContext.Default.JsonElement)
552+
: null;
553+
return JsonSerializer.SerializeToElement(new JsonObject
550554
{
551-
["result"] = nodeResult
552-
};
555+
["result"] = resultNode
556+
}, McpJsonUtilities.JsonContext.Default.JsonObject);
553557
}
554558

555-
return nodeResult;
559+
return elementResult;
556560
}
557561

558-
private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable<AIContent> contentItems, JsonNode? structuredContent)
562+
private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable<AIContent> contentItems, JsonElement? structuredContent)
559563
{
560564
List<ContentBlock> contentList = [];
561565
bool allErrorContent = true;

tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public static CallToolResult StructuredContentTool() =>
122122
new()
123123
{
124124
Content = [new TextContentBlock { Text = "Regular content" }],
125-
StructuredContent = JsonNode.Parse("{\"key\":\"value\"}")
125+
StructuredContent = JsonElement.Parse("{\"key\":\"value\"}")
126126
};
127127

128128
// Tool that returns CallToolResult with Meta

tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public static void CallToolResult_SerializationRoundTrip_PreservesAllProperties(
1212
var original = new CallToolResult
1313
{
1414
Content = [new TextContentBlock { Text = "Result text" }],
15-
StructuredContent = JsonNode.Parse("""{"temperature":72}"""),
15+
StructuredContent = JsonElement.Parse("""{"temperature":72}"""),
1616
IsError = false,
1717
Task = new McpTask
1818
{
@@ -32,7 +32,7 @@ public static void CallToolResult_SerializationRoundTrip_PreservesAllProperties(
3232
var textBlock = Assert.IsType<TextContentBlock>(deserialized.Content[0]);
3333
Assert.Equal("Result text", textBlock.Text);
3434
Assert.NotNull(deserialized.StructuredContent);
35-
Assert.Equal(72, deserialized.StructuredContent["temperature"]!.GetValue<int>());
35+
Assert.Equal(72, deserialized.StructuredContent.Value.GetProperty("temperature").GetInt32());
3636
Assert.False(deserialized.IsError);
3737
Assert.NotNull(deserialized.Task);
3838
Assert.Equal("task-1", deserialized.Task.TaskId);

tests/ModelContextProtocol.Tests/Protocol/UnknownPropertiesTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public void CallToolResult_WithStructuredContentAtCorrectLevel_PreservesProperty
252252
// Assert
253253
Assert.NotNull(deserialized);
254254
Assert.NotNull(deserialized.StructuredContent);
255-
Assert.Equal("correctly placed here", deserialized.StructuredContent["result"]?.ToString());
256-
Assert.Equal(42, (int?)deserialized.StructuredContent["value"]);
255+
Assert.Equal("correctly placed here", deserialized.StructuredContent.Value.GetProperty("result").GetString());
256+
Assert.Equal(42, deserialized.StructuredContent.Value.GetProperty("value").GetInt32());
257257
}
258258
}

tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -654,11 +654,11 @@ public object InstanceMethod()
654654
}
655655
}
656656

657-
private static void AssertMatchesJsonSchema(JsonElement schemaDoc, JsonNode? value)
657+
private static void AssertMatchesJsonSchema(JsonElement schemaDoc, JsonElement? value)
658658
{
659659
JsonSchema schema = JsonSerializer.Deserialize(schemaDoc, JsonContext2.Default.JsonSchema)!;
660660
EvaluationOptions options = new() { OutputFormat = OutputFormat.List };
661-
EvaluationResults results = schema.Evaluate(JsonSerializer.SerializeToElement(value, JsonContext2.Default.JsonNode), options);
661+
EvaluationResults results = schema.Evaluate(value!.Value, options);
662662
if (!results.IsValid)
663663
{
664664
IEnumerable<string> errors = (results.Details ?? [])
@@ -670,7 +670,7 @@ Instance JSON document does not match the specified schema.
670670
Schema:
671671
{JsonSerializer.Serialize(schema)}
672672
Instance:
673-
{value?.ToJsonString() ?? "null"}
673+
{value?.ToString() ?? "null"}
674674
Errors:
675675
{string.Join(Environment.NewLine, errors)}
676676
""");

0 commit comments

Comments
 (0)