Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
519e96c
Fix nullable structured tool results
mikemikimike Oct 13, 2026
de107a5
Fix structured output for required nullable properties
mikemikimike Oct 19, 2026
55b7972
Preserve schema-required nullable output members
mikemikimike Oct 21, 2026
f1a39f8
Fix structured output required member restoration
mikemikimike Oct 22, 2026
6c530f6
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 22, 2026
c4f9d71
Repair structured output against explicit schemas
mikemikimike Oct 23, 2026
0170ae6
Add regression for structured output source lookups
mikemikimike Oct 23, 2026
d633262
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 23, 2026
dfed8d3
Fix structured content schema recovery
mikemikimike Oct 26, 2026
f756988
Test structured output schema edge cases
mikemikimike Oct 26, 2026
e294aeb
Cover branch-isolated structured output recovery
mikemikimike Oct 26, 2026
a3384df
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 26, 2026
5dd99de
Fix structured output repair edge cases
mikemikimike Oct 27, 2026
4dfae0e
Fix structured output recovery edge cases
Oct 27, 2026
db3d9e7
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 27, 2026
4ca13cf
Fix composed structured output recovery and embedded references
Oct 27, 2026
351451e
Fix structured output recovery review issues
mikemikimike Oct 28, 2026
d3d0080
Merge upstream review fixes and preserve expanded regression coverage
Oct 28, 2026
66dfae7
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 28, 2026
9b2f5c8
Fix structured output recovery contracts and bound schema traversal
Oct 28, 2026
5b24706
Fix structured output recovery boundaries and branch selection
Oct 28, 2026
8731f21
Cache structured output recovery contracts without mutating metadata
Oct 28, 2026
a1fb212
Merge upstream main into structured output recovery fixes
Oct 28, 2026
d4c38b2
Preserve ancestor cycle scope during structured output recovery
Oct 28, 2026
d0fe5e4
Respect effective schemas and serialization boundaries during output …
Oct 28, 2026
f249b4a
Fix structured output recovery edge cases
mikemikimike Oct 29, 2026
875cc0f
Avoid recovery from reordered enumerables
mikemikimike Oct 29, 2026
4bac0eb
Align NativeAOT iterator assertions with conservative recovery
Oct 29, 2026
1bee8f7
Recover structured output from generic lists
mikemikimike Oct 29, 2026
49b1bf9
Bound structured output recovery allocation and schema work
Oct 29, 2026
c23927f
Fix structured output recovery safety and annotation boundaries
Oct 29, 2026
4225c52
Preserve serializer readonly-member recovery capabilities
Oct 29, 2026
bd84672
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 29, 2026
21d1b3e
Retain original structured output after recovery failures
Oct 29, 2026
67b6f7a
Preserve effective schemas through forwarding tool invocations
Oct 30, 2026
ee847e7
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 30, 2026
493eb4d
Skip structured output recovery for serialized JSON results
Oct 30, 2026
cad6719
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 30, 2026
64f57d1
Preserve structured output across mutable forwarding boundaries
Oct 31, 2026
13a1357
Merge branch 'main' into fix/structured-content-null-schema
tarekgh Oct 31, 2026
3f0015d
Address structured output recovery review feedback
mikemikimike Oct 31, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="JsonSchema.Net" />
</ItemGroup>

<!-- Reference analyzers -->
Expand Down
3,248 changes: 3,236 additions & 12 deletions src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ namespace ModelContextProtocol.Server;
/// <remarks>
/// This is recommended as a base type when building tools that can be chained around an underlying <see cref="McpServerTool"/>.
/// The default implementation simply passes each call to the inner tool instance.
/// When an invocation override retains the original <see cref="CallToolResult.StructuredContent"/> element,
/// server dispatch can apply required-member recovery against the registered tool's effective output schema.
/// Overrides that replace or reserialize structured content are responsible for matching their advertised schema.
/// </remarks>
public abstract class DelegatingMcpServerTool : McpServerTool
{
Expand Down Expand Up @@ -35,7 +38,7 @@ protected DelegatingMcpServerTool(McpServerTool innerTool)

/// <inheritdoc />
public override ValueTask<CallToolResult> InvokeAsync(
RequestContext<CallToolRequestParams> request,
RequestContext<CallToolRequestParams> request,
CancellationToken cancellationToken = default) =>
_innerTool.InvokeAsync(request, cancellationToken);

Expand Down
41 changes: 39 additions & 2 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,7 +1603,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
{
if (request.MatchedPrimitive is McpServerTool tool)
{
return tool.InvokeAsync(request, cancellationToken);
return InvokeToolAsync(tool, request, cancellationToken);
}

return originalHandler(request, cancellationToken);
Expand Down Expand Up @@ -1636,7 +1636,44 @@ private static async ValueTask<ResultOrAlternate<CallToolResult>> InvokeToolWith
RequestContext<CallToolRequestParams> request,
CancellationToken cancellationToken)
{
return await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
return await InvokeToolAsync(tool, request, cancellationToken).ConfigureAwait(false);
}

private static ValueTask<CallToolResult> InvokeToolAsync(
McpServerTool tool,
RequestContext<CallToolRequestParams> request,
CancellationToken cancellationToken) =>
tool is DelegatingMcpServerTool
? InvokeDelegatingToolAsync(tool, request, cancellationToken)
: tool.InvokeAsync(request, cancellationToken);

private static async ValueTask<CallToolResult> InvokeDelegatingToolAsync(
McpServerTool tool,
RequestContext<CallToolRequestParams> request,
CancellationToken cancellationToken)
{
// Custom delegators may transform the result or mutate the source object after
// invoking their inner tool. Recover against the immediate inner contract before
// those transformations so omitted values come from the original invocation.
bool hasCustomInvocation = tool is DelegatingMcpServerTool delegatingTool && delegatingTool.HasCustomInvocation;
McpServerTool recoveryTool = hasCustomInvocation ? delegatingTool!.InnerTool : tool;

request.ClearStructuredContentRecovery();
request.StructuredContentRecoveryTarget = new(recoveryTool, recoveryTool.ProtocolTool.OutputSchema);
try
{
CallToolResult result = await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
if (!hasCustomInvocation)
{
request.RecoverStructuredContent(result);
}
return result;
}
finally
{
request.StructuredContentRecoveryTarget = null;
request.ClearStructuredContentRecovery();
}
}

private McpRequestHandler<CallToolRequestParams, ResultOrAlternate<CallToolResult>> BuildComposedCallToolHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,16 @@ public sealed class McpServerToolCreateOptions
/// The default is <see langword="false"/>.
/// </value>
/// <remarks>
/// <para>
/// When enabled, the tool will attempt to populate the <see cref="Tool.OutputSchema"/>
/// and provide structured content in the <see cref="CallToolResult.StructuredContent"/> property.
/// </para>
/// <para>
/// Recovery of required members omitted by null or default-value serialization policies is best effort.
/// Recovery does not re-enumerate non-indexable collections, such as sets, queues, or linked lists.
/// Materialize these collections into arrays or lists when their elements need this recovery.
/// If safe recovery cannot produce a schema-valid result, the original serialized content is retained.
/// </para>
/// </remarks>
public bool UseStructuredContent { get; set; }

Expand Down
11 changes: 11 additions & 0 deletions src/ModelContextProtocol.Core/Server/RequestContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;

namespace ModelContextProtocol.Server;

Expand Down Expand Up @@ -47,6 +48,16 @@ public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest)
/// </summary>
public IMcpServerPrimitive? MatchedPrimitive { get; set; }

internal StructuredContentRecoveryState? StructuredContentRecoveryTarget { get; set; }
internal bool SupportsDeferredStructuredContentRecovery => StructuredContentRecoveryTarget is not null;
internal event Action<CallToolResult>? StructuredContentRecovery;

internal void RecoverStructuredContent(CallToolResult result) => StructuredContentRecovery?.Invoke(result);

internal void ClearStructuredContentRecovery() => StructuredContentRecovery = null;

internal sealed record StructuredContentRecoveryState(McpServerTool Tool, JsonElement? Schema);

/// <summary>
/// Gets the JSON-RPC request associated with this context.
/// </summary>
Expand Down
Loading