-
Notifications
You must be signed in to change notification settings - Fork 617
Description
Summary
Users of the MCP C# SDK encounter several related problems when defining their own JsonSerializerContext:
-
Forced diagnostic suppression - Users must suppress experimental API diagnostics for their project, even when they aren't using experimental features, because the
System.Text.Jsonsource generator generates code that references experimental types. -
Binary breaking change risk - Changes to experimental types could introduce binary breaking changes that affect source-generated serialization code.
-
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
ServerCapabilitiesare 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:
- MCP app developers building servers or clients with custom JSON serialization
- 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
nulland 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.