Skip to content

Preserve nulls required by structured output schemas - #1929

Open
mikemikimike wants to merge 41 commits into
modelcontextprotocol:mainfrom
mikemikimike:fix/structured-content-null-schema
Open

mikemikimike wants to merge 41 commits into
modelcontextprotocol:mainfrom
mikemikimike:fix/structured-content-null-schema

Conversation

@mikemikimike

Copy link
Copy Markdown

Fixes #1915

Issue: #1915

When structured tool output has an output schema and serializer options ignore null values, required nullable properties are omitted from structuredContent. Preserve explicit nulls for this serialization path so the result matches its schema while leaving ordinary text serialization unchanged.

Add regression coverage for record and class outputs with null and non-null values across the current protocol and 2025-11-25.

Validation passed: dotnet test in Debug and Release across the repository test suites; dotnet format whitespace --verify-no-changes; Windows x64 NativeAOT publish and execution; Release NuGet pack; and DocFX with warnings treated as errors. The Core suites passed 2,398 tests per net8.0/net9.0/net10.0 target (4 skipped) and 2,081 tests on net472 (294 skipped); ASP.NET Core passed 635 tests per modern target (30 skipped); Analyzer passed 57 tests.

@PederHP

PederHP commented Oct 14, 2026

Copy link
Copy Markdown
Member

Adding a data point on why this matters beyond the TypeScript 1.x client in #1915: it breaks interop with the other official SDKs' current clients, too. I hit it in a companion sample for my book. A C# server returns a positional record with nullable members (OperationStatus(string OperationId, string Kind, string Status, string? Result, string? Guidance), source), and both other SDKs' clients reject every call where one of those members is null. The repro calls the tool over stdio with an unknown ID, so Result is null:

Client Server on ModelContextProtocol 2.2.0 Same server built against this PR (519e96c)
Python mcp 2.0.0 (negotiates 2026-07-28) Invalid structured content returned by tool check_operation_status: 'result' is a required property OK, "result": null
TypeScript @modelcontextprotocol/client 2.2.0 (versionNegotiation: { mode: 'auto' }) Structured content does not match the tool's output schema: data must have required property 'result' OK, "result": null

Both clients validate structuredContent against outputSchema by default (the spec's SHOULD), while the C# client doesn't, so a C# server passes its own end-to-end tests and only fails when a Python or TypeScript client calls it. The equivalent Python (Pydantic str | None) and TypeScript (zod .nullable()) servers already put an explicit null on the wire, so with this change the three SDKs agree.

Until this ships, the per-member workaround that worked for me is [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] on the nullable record parameters.

@tarekgh tarekgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a real interoperability issue: required nullable properties must be present in structured output, and Python and TypeScript clients reject results when those properties are omitted.

I found one narrow compatibility regression with explicit output schemas, noted inline. Two related ignore-policy gaps also remain, but those predate this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Globally including nulls can invalidate previously valid explicit-schema responses whose optional properties prohibit null.

1 open finding
What changed in this PR

Addresses #1915 by preserving nulls in structured tool results to match required nullable output properties.

Changes:

  • Adds separate serializer options for schema-backed structured content, leaving text serialization unchanged.
  • Adds record and class regression tests across two protocol versions.
File Description
tests/​ModelContextProtocol.Tests/​Server/​McpServerToolTests.cs Tests nullable output properties against their schemas.
src/​ModelContextProtocol.Core/​Server/​AIFunctionMcpServerTool.cs Preserves nulls during schema-backed structured serialization.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The previously reported regression remains unresolved for explicit schemas whose optional properties prohibit null values.

1 open finding

🧠 Review effort: Balanced

Signed-off-by: mikemikimike <13286568797@163.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Schema traversal remains incomplete, and restoration can replace actual values with null or invalidate previously valid output.

5 open issues
1 resolved since last review

Review effort: Balanced

@tarekgh

tarekgh commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

@mikemikimike thanks for last updates. Could you please look at the copilot comments and address them?

Signed-off-by: mikemikimike <13286568797@163.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarekgh tarekgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two additional gaps reproduced against f1a39f8: source recovery can fabricate values, and dependentSchemas requirements are not collected.

@tarekgh tarekgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three remaining issues reproduced against 0170ae6: caller-filter bypass for default-ignored values, incompatible intermediate union repairs, and missing dependentRequired handling.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unbounded diagnostic expansion and regex matching can exhaust server resources, while delegator and anyOf handling can leave required null fields unrepaired.

4 open issues

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved issues can produce schema-invalid responses by misclassifying get-only properties, discarding effective schemas for pass-through wrappers, and failing to restore required nulls in non-indexable collections.

2 open issues
4 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Recovery can discard the advertised schema for pass-through wrappers and let callback-dependent getter exceptions escape, turning valid responses into schema violations or tool errors.

1 open issue
2 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Pass-through invocation overrides can discard the effective outer schema, causing recovery to inject null into an optional string field and violate the advertised tool schema.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Recovery adds avoidable schema compilation and document scanning for JsonElement/JsonNode results, whose opaque serialization contracts cannot recover omitted CLR members.

1 open issue

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The omitted schema-repair implementation requires maintainer review to verify correctness and compatibility across delegating overrides, mutable request state, and Native AOT serialization contracts.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Deferred structured-content recovery can omit required fields in stacked wrappers and emit mutated values instead of the original result values.

2 open issues

Review effort: Balanced

Comment on lines +1659 to +1660
CallToolResult result = await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
request.RecoverStructuredContent(result);
Comment on lines +1659 to +1660
CallToolResult result = await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
request.RecoverStructuredContent(result);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

4 participants