Hide experimental types from external source generators using internal property pattern#1301
Hide experimental types from external source generators using internal property pattern#1301MackinnonBuck wants to merge 3 commits intomainfrom
Conversation
…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>
| [JsonInclude] | ||
| [JsonPropertyName("task")] | ||
| public McpTaskMetadata? Task { get; set; } | ||
| internal McpTaskMetadata? TaskCore { get; set; } |
There was a problem hiding this comment.
Nit: Since we sometimes use a *Core suffix for protected or internal members, I suggest a suffix with stronger connotation about its purpose, such as Task_Experimental.
There was a problem hiding this comment.
Since we sometimes use a *Core suffix for protected or internal members
Just to clarify, this member is internal, so the name doesn't surface in public API. That said, I'm happy to consider other names if they better convey the purpose of the property.
| /// Validates the internal property pattern used for experimental MCP properties. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Experimental properties on stable protocol types must use an internal serialization |
There was a problem hiding this comment.
How can we make sure this is being adequately advertised to users? The fact that we're silently dropping experimental properties only for externally defined source generators (but not externally defined reflection-based JSOs) seems to violate the principle of least astonishment, particularly in the face of us documenting that STJ has the same behavior regardless of what contract resolver is being used.
Given how STJ is being used typically in AOT apps, users could be seeing regressions trigger in unexpected manner, consider the following example:
JsonSerializerOptions options = new()
{
TypeInfoResolverchain = { MyContext.Default, McpJsonUtilities.DefaultOptions.Resolver }
};
[JsonSerializable(typeof(MyCustomType)]
//[JsonSerializable(typeof(CallToolRequestParams)]
partial class MyContext : JsonSerializerContext;Either uncommenting CallToolRequestParams or otherwise including it in the transitive type graph of MyContext will result in the experimental properties on CallToolRequestParams getting skipped silently. This seems like a very brittle arrangement to me.
There was a problem hiding this comment.
Yeah, that's the unfortunate footgun that this approach has. The brittle configuration is the main tradeoff this PR makes compared to #1260.
I would think that at a minimum, we'd need to document somewhere that anyone serializing MCP protocol types via JsonSerializerContext needs to configure their TypeInfoResolverChain with McpJsonUtilities.DefaultOptions.Resolver being the first entry.
We could also add some kind of McpJsonUtilities.CreateOptions(MyContext.Default) helper that sets up the TypeInfoResolverChain correctly, but customers still need to know to do that.
A more involved option could be to emit a compiler diagnostic if we detect that any protocol types are either directly or transitively included in a JsonSerializerContext. The warning would suggest adding an [McpJsonContext] attribute to MyContext, and another source generator would detect that attribute and add a correctly-configured McpOptions property to MyContext. I haven't explored this though, so the implementation might be prohibitively complex for the benefit it brings.
Problem
Experimental properties on stable protocol types (e.g.
Tool.Execution,ServerCapabilities.Tasks) cause two problems for consumers who don't use those APIs:MCPEXP001at 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 aninternalproperty marked[JsonInclude][JsonPropertyName]:This resolves both issues. Because
internalmembers 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 usesobjectrather than the experimental type, avoiding theMCPEXP001diagnostic entirely. The only code path that serializes these properties is the SDK's ownMcpJsonUtilities.DefaultOptions.Consumer experience
MCPEXP001as usual. Serialization throughMcpJsonUtilities.DefaultOptionshandles everything.JsonSerializerContext+ experimental APIs: ConfigureTypeInfoResolverChainso the SDK's resolver takes precedence for SDK types. Only needed by consumers who both use experimental APIs and define a custom serialization context.[Experimental]/[JsonIgnore], add[JsonPropertyName], convert to auto-property, delete the*Coreproperty. No consumer changes required.Changes
ExperimentalApiRegressionTest) that builds with[JsonSerializable]references to all affected types, verifying noMCPEXP001diagnostics are emittedFixes #1255