Skip to content

Hide experimental types from external source generators using internal property pattern#1301

Merged
MackinnonBuck merged 3 commits intomainfrom
mbuck/experimental-json-fix-2
Feb 19, 2026
Merged

Hide experimental types from external source generators using internal property pattern#1301
MackinnonBuck merged 3 commits intomainfrom
mbuck/experimental-json-fix-2

Conversation

@MackinnonBuck
Copy link
Collaborator

@MackinnonBuck MackinnonBuck commented Feb 17, 2026

Problem

Experimental properties on stable protocol types (e.g. Tool.Execution, ServerCapabilities.Tasks) cause two problems for consumers who don't use those APIs:

  1. Binary compatibility. The STJ source generator emits code that directly references experimental types. If an experimental type is renamed, restructured, or removed in a future SDK version, previously compiled source-generated code breaks, even if the consumer never used the experimental API.
  2. Unwanted diagnostics. The source generator's references to experimental types trigger MCPEXP001 at compile time, forcing consumers to suppress a diagnostic for an API they aren't using.

Approach

Alternative to #1260. Each experimental property is marked [JsonIgnore] and delegates to an internal property marked [JsonInclude][JsonPropertyName]:

[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public ToolExecution? Execution
{
    get => ExecutionCore;
    set => ExecutionCore = value;
}

[JsonInclude]
[JsonPropertyName("execution")]
internal ToolExecution? ExecutionCore { get; set; }

This resolves both issues. Because internal members are invisible across assembly boundaries, external source generators never emit code referencing experimental types, eliminating the binary compatibility concern. And because the public property is [JsonIgnore]d, the source generator uses object rather than the experimental type, avoiding the MCPEXP001 diagnostic entirely. The only code path that serializes these properties is the SDK's own McpJsonUtilities.DefaultOptions.

Consumer experience

  • Not using experimental APIs: No diagnostics, no binary coupling to experimental types. Experimental properties are still serialized on the wire through the SDK's own resolver, so protocol compatibility is maintained.
  • Using experimental APIs: Suppress MCPEXP001 as usual. Serialization through McpJsonUtilities.DefaultOptions handles everything.
  • Custom JsonSerializerContext + experimental APIs: Configure TypeInfoResolverChain so the SDK's resolver takes precedence for SDK types. Only needed by consumers who both use experimental APIs and define a custom serialization context.
  • Stabilization: Remove [Experimental]/[JsonIgnore], add [JsonPropertyName], convert to auto-property, delete the *Core property. No consumer changes required.

Changes

  • Applied the internal property pattern to all 7 experimental properties across protocol types
  • Added a compile-time regression test project (ExperimentalApiRegressionTest) that builds with [JsonSerializable] references to all affected types, verifying no MCPEXP001 diagnostics are emitted
  • Added a scan-based test enforcing the internal property pattern across all experimental properties
  • Added serialization round-trip tests verifying behavior with consumer-defined source-generated contexts

Fixes #1255

…l source generators

Experimental properties on stable protocol types cause external STJ source
generators to emit code referencing experimental types, creating binary
compatibility risks and unwanted MCPEXP001 diagnostics for consumers who
don't use those APIs.

This commit addresses both concerns:

- Internal property pattern: each experimental property delegates to an
  internal *Core property with [JsonInclude][JsonPropertyName]. External
  source generators cannot see internal members, so only the SDK's own
  McpJsonUtilities.DefaultOptions serializes these properties. This
  eliminates binary coupling to experimental types across assemblies.

- MCPEXP001Suppressor: on net8.0/net9.0, the SG still references
  experimental types in [JsonIgnore] property metadata (fixed in net10.0
  by dotnet/runtime#120181). The suppressor silences MCPEXP001 in .g.cs
  files so it only surfaces in hand-written code.

Applied to all 7 experimental properties: Tool.Execution,
ServerCapabilities.Tasks, ClientCapabilities.Tasks, CallToolResult.Task,
CallToolRequestParams.Task, CreateMessageRequestParams.Task,
ElicitRequestParams.Task.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MackinnonBuck MackinnonBuck marked this pull request as draft February 17, 2026 22:19
@jeffhandley jeffhandley added the breaking-change This issue or PR introduces a breaking change label Feb 19, 2026
Copy link
Member

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

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

Thanks

@MackinnonBuck MackinnonBuck merged commit cc6ef5b into main Feb 19, 2026
10 checks passed
@MackinnonBuck MackinnonBuck deleted the mbuck/experimental-json-fix-2 branch February 19, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change This issue or PR introduces a breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Experimental APIs force users to suppress diagnostics even when they are not used

5 participants

Comments