Skip to content

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

@MackinnonBuck

Description

@MackinnonBuck

Summary

Users of the MCP C# SDK encounter several related problems when defining their own JsonSerializerContext:

  1. Forced diagnostic suppression - Users must suppress experimental API diagnostics for their project, even when they aren't using experimental features, because the System.Text.Json source generator generates code that references experimental types.

  2. Binary breaking change risk - Changes to experimental types could introduce binary breaking changes that affect source-generated serialization code.

  3. Version mismatch deserialization risk (hypothetical) - If an MCP server and client use different versions of the SDK with incompatible JSON representations for experimental types, deserialization could fail during the initialization handshake. This is mostly theoretical since it would require a spec change to drive the difference, but it's worth noting since types like ServerCapabilities are exchanged during every MCP connection.

Background

The MCP C# SDK marks certain features as experimental using [Experimental] attributes (e.g., MCPEXP001 for Tasks). These experimental types are exposed as nullable properties on stable types:

// Stable type with experimental property
public class Tool
{
    // ... stable properties ...
    
    [Experimental("MCPEXP001")]
    public ToolExecution? Execution { get; init; }
}

The Problem

Affected Users

Two types of users are affected:

  1. MCP app developers building servers or clients with custom JSON serialization
  2. SDK developers building libraries on top of the MCP C# SDK that need AOT compatibility

Root Cause

When a user defines their own JsonSerializerContext and registers any MCP type (e.g., Tool), the source generator follows all property types transitively and generates serialization code that references experimental types, causing diagnostics to be emitted.

Minimal Reproduction

using System.Text.Json.Serialization;
using ModelContextProtocol.Protocol;

// User wants AOT-compatible serialization for just the Tool type
[JsonSerializable(typeof(Tool))]
public partial class MyJsonContext : JsonSerializerContext { }

Result: Many experimental API diagnostics from source-generated code referencing types like ToolExecution, ToolTaskSupport, etc.

Possible Solution

The following approach is admittedly involved and not ideal, but it may address both problems. It uses object backing fields with reflection-based serialization for experimental properties.

1. Restructure Experimental Properties

public class Tool
{
    public string Name { get; set; }
    
    // Backing field with custom converter
    [JsonInclude]
    [JsonPropertyName("execution")]
    [JsonConverter(typeof(ExperimentalJsonConverter<ToolExecution>))]
    internal object? _execution;
    
    // Public API property
    [JsonIgnore]
    [Experimental("MCPEXP001")]
    public ToolExecution? Execution
    {
        get => _execution as ToolExecution;
        set => _execution = value;
    }
}

2. Custom Converter for Experimental Types

An ExperimentalJsonConverter<T> would:

  • Check if experimental features are enabled via feature flag; if not, serialize as null and skip deserialization
  • Check if reflection is available (JsonSerializer.IsReflectionEnabledByDefault); if not, throw a clear error
  • Use reflection-based serialization for the actual experimental type

3. DiagnosticSuppressor for Remaining Warnings

The source generator will still emit property metadata that references experimental types (though without member access). A DiagnosticSuppressor would suppress experimental diagnostics in source-generated files.

4. MSBuild Property for Opt-In

<PropertyGroup>
  <McpEnableExperimentalFeatures>true</McpEnableExperimentalFeatures>
</PropertyGroup>

This would:

  • Enable experimental serialization at runtime via feature flag
  • Emit a warning if combined with <PublishAot>true</PublishAot>

How This Could Solve the Problems

The object? backing field prevents the source generator from emitting serialization code for the experimental type. Since the field's static type is object, the generator cannot know what concrete type will be stored there at runtime, so it cannot generate type-specific serialization logic. The custom converter then handles the actual serialization using reflection.

Problem Solution
Experimental diagnostics in source-generated code [JsonIgnore] prevents transitive generation; DiagnosticSuppressor handles remaining type refs
Binary breaking changes affect generated serialization code Generated code only has type references, not member accesses; actual serialization uses reflection
Experimental data leaks into payloads Serializes as null when experimental is disabled
AOT compatibility Non-experimental users get full AOT support; experimental users require reflection

Limitations

If a library author includes an experimental MCP type in their own JSON-serializable type, the source generator will follow that property transitively and reintroduce the same problem for consuming applications. Library authors would need to employ a similar object? backing field pattern, which is awkward.

Alternative considered

A more complete solution might require a first-class STJ feature: a new attribute on JsonSerializerContext that instructs the source generator to skip code generation for specified types entirely, deferring to custom converters at runtime. This would allow the MCP C# SDK to mark experimental types as "converter-only" without requiring library authors to use workarounds. This may also eliminate the need to introduce a diagnostic suppressor.

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions