diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index 3036f301e..1090c5377 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -78,5 +78,6 @@
+
diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
index fa6939d7a..8267cd06f 100644
--- a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -33,6 +34,16 @@ public sealed class CallToolRequestParams : RequestParams
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTaskMetadata? Task
+ {
+ get => TaskCore;
+ set => TaskCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("task")]
- public McpTaskMetadata? Task { get; set; }
+ internal McpTaskMetadata? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
index 509436010..e40dc560a 100644
--- a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -65,6 +66,16 @@ public sealed class CallToolResult : Result
/// (, , ) may not be populated.
/// The actual tool result can be retrieved later via tasks/result.
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTask? Task
+ {
+ get => TaskCore;
+ set => TaskCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("task")]
- public McpTask? Task { get; set; }
+ internal McpTask? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
index cb85ef5e3..9841e3da0 100644
--- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
@@ -80,6 +81,16 @@ public sealed class ClientCapabilities
/// See for details on configuring which operations support tasks.
///
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTasksCapability? Tasks
+ {
+ get => TasksCore;
+ set => TasksCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("tasks")]
- public McpTasksCapability? Tasks { get; set; }
+ internal McpTasksCapability? TasksCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
index 59be7fab8..eba472246 100644
--- a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -128,6 +129,16 @@ public sealed class CreateMessageRequestParams : RequestParams
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTaskMetadata? Task
+ {
+ get => TaskCore;
+ set => TaskCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("task")]
- public McpTaskMetadata? Task { get; set; }
+ internal McpTaskMetadata? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
index 91cbb307d..921008574 100644
--- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
@@ -99,8 +99,18 @@ public string Mode
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTaskMetadata? Task
+ {
+ get => TaskCore;
+ set => TaskCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("task")]
- public McpTaskMetadata? Task { get; set; }
+ internal McpTaskMetadata? TaskCore { get; set; }
/// Represents a request schema used in a form mode elicitation request.
public sealed class RequestSchema
diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
index 499819662..d4e55653e 100644
--- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ModelContextProtocol.Server;
@@ -79,6 +80,16 @@ public sealed class ServerCapabilities
/// See for details on configuring which operations support tasks.
///
///
+ [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public McpTasksCapability? Tasks
+ {
+ get => TasksCore;
+ set => TasksCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("tasks")]
- public McpTasksCapability? Tasks { get; set; }
+ internal McpTasksCapability? TasksCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs
index 8e06d7104..473b70f25 100644
--- a/src/ModelContextProtocol.Core/Protocol/Tool.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs
@@ -120,8 +120,17 @@ public JsonElement? OutputSchema
/// regarding task augmentation support. See for details.
///
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
+ [JsonIgnore]
+ public ToolExecution? Execution
+ {
+ get => ExecutionCore;
+ set => ExecutionCore = value;
+ }
+
+ // See ExperimentalInternalPropertyTests.cs before modifying this property.
+ [JsonInclude]
[JsonPropertyName("execution")]
- public ToolExecution? Execution { get; set; }
+ internal ToolExecution? ExecutionCore { get; set; }
///
/// Gets or sets an optional list of icons for this tool.
diff --git a/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ExperimentalPropertyRegressionContext.cs b/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ExperimentalPropertyRegressionContext.cs
new file mode 100644
index 000000000..d30036822
--- /dev/null
+++ b/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ExperimentalPropertyRegressionContext.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using ModelContextProtocol.Protocol;
+
+namespace ModelContextProtocol.ExperimentalApiRegressionTest;
+
+///
+/// This file validates that the System.Text.Json source generator does not produce
+/// MCPEXP001 diagnostics for MCP protocol types with experimental properties.
+///
+[JsonSerializable(typeof(Tool))]
+[JsonSerializable(typeof(ServerCapabilities))]
+[JsonSerializable(typeof(ClientCapabilities))]
+[JsonSerializable(typeof(CallToolResult))]
+[JsonSerializable(typeof(CallToolRequestParams))]
+[JsonSerializable(typeof(CreateMessageRequestParams))]
+[JsonSerializable(typeof(ElicitRequestParams))]
+internal partial class ExperimentalPropertyRegressionContext : JsonSerializerContext;
diff --git a/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ModelContextProtocol.ExperimentalApiRegressionTest.csproj b/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ModelContextProtocol.ExperimentalApiRegressionTest.csproj
new file mode 100644
index 000000000..fb2423548
--- /dev/null
+++ b/tests/ModelContextProtocol.ExperimentalApiRegressionTest/ModelContextProtocol.ExperimentalApiRegressionTest.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0;net9.0;net8.0
+ enable
+ enable
+ false
+
+
+ $(NoWarn.Replace('MCPEXP001',''))
+
+
+ $(NoWarn);SYSLIB1038
+
+
+
+
+
+
+
diff --git a/tests/ModelContextProtocol.Tests/ExperimentalInternalPropertyTests.cs b/tests/ModelContextProtocol.Tests/ExperimentalInternalPropertyTests.cs
new file mode 100644
index 000000000..17e18dc2b
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/ExperimentalInternalPropertyTests.cs
@@ -0,0 +1,69 @@
+using System.Reflection;
+using System.Text.Json.Serialization;
+using ModelContextProtocol.Protocol;
+
+namespace ModelContextProtocol.Tests;
+
+///
+/// Validates the internal property pattern used for experimental MCP properties.
+///
+///
+/// Experimental properties on stable protocol types must use an internal serialization
+/// property to hide the experimental type from external source generators. The public
+/// property is marked [Experimental][JsonIgnore] and delegates to an internal *Core
+/// property marked [JsonInclude][JsonPropertyName].
+///
+public class ExperimentalInternalPropertyTests
+{
+ [Fact]
+ public void ExperimentalProperties_MustBeHiddenFromSourceGenerator()
+ {
+ // [Experimental] properties on stable protocol types must use the internal property
+ // pattern so the STJ source generator does not reference experimental types in
+ // generated code (which would trigger MCPEXP001 for consumers).
+ //
+ // Required pattern:
+ // 1. Mark the public property [Experimental][JsonIgnore]
+ // 2. Add an internal *Core property with [JsonInclude][JsonPropertyName]
+ // that the public property delegates to
+ //
+ // To stabilize:
+ // 1. Remove [Experimental] and [JsonIgnore] from the public property
+ // 2. Add [JsonPropertyName] to the public property
+ // 3. Convert to auto-property
+ // 4. Remove the internal *Core property
+
+ foreach (var (type, prop) in GetExperimentalPropertiesOnStableTypes())
+ {
+ Assert.True(
+ prop.GetCustomAttribute() is not null,
+ $"{type.Name}.{prop.Name} is [Experimental] but missing [JsonIgnore].");
+
+ Assert.True(
+ prop.GetCustomAttribute() is null,
+ $"{type.Name}.{prop.Name} is [Experimental] and must not have [JsonPropertyName].");
+ }
+ }
+
+ private static IEnumerable<(Type Type, PropertyInfo Property)> GetExperimentalPropertiesOnStableTypes()
+ {
+ var protocolTypes = typeof(Tool).Assembly.GetTypes()
+ .Where(t => t.Namespace == "ModelContextProtocol.Protocol" && t.IsClass && !t.IsAbstract);
+
+ foreach (var type in protocolTypes)
+ {
+ if (type.GetCustomAttributes().Any(a => a.GetType().Name == "ExperimentalAttribute"))
+ {
+ continue;
+ }
+
+ foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (prop.GetCustomAttributes().Any(a => a.GetType().Name == "ExperimentalAttribute"))
+ {
+ yield return (type, prop);
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs b/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs
new file mode 100644
index 000000000..d68902ef5
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs
@@ -0,0 +1,118 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using ModelContextProtocol.Protocol;
+
+namespace ModelContextProtocol.Tests;
+
+///
+/// Validates that the internal property pattern used for experimental properties
+/// produces the expected serialization behavior for SDK consumers using source generators.
+///
+///
+///
+/// Experimental properties (e.g. , )
+/// use an internal *Core property for serialization. A consumer's source-generated
+/// cannot see internal members, so experimental data is
+/// silently dropped unless the consumer chains the SDK's resolver into their options.
+///
+///
+/// These tests depend on and
+/// being experimental. When those APIs stabilize, update these tests to reference whatever
+/// experimental properties exist at that time, or remove them entirely if no experimental
+/// APIs remain.
+///
+///
+public class ExperimentalPropertySerializationTests
+{
+ [Fact]
+ public void ExperimentalProperties_Dropped_WithConsumerContextOnly()
+ {
+ var options = new JsonSerializerOptions
+ {
+ TypeInfoResolverChain = { ConsumerJsonContext.Default }
+ };
+
+ var tool = new Tool
+ {
+ Name = "test-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ };
+
+ string json = JsonSerializer.Serialize(tool, options);
+ Assert.DoesNotContain("\"execution\"", json);
+ Assert.Contains("\"name\"", json);
+ }
+
+ [Fact]
+ public void ExperimentalProperties_IgnoredOnDeserialize_WithConsumerContextOnly()
+ {
+ string json = JsonSerializer.Serialize(
+ new Tool
+ {
+ Name = "test-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ },
+ McpJsonUtilities.DefaultOptions);
+ Assert.Contains("\"execution\"", json);
+
+ var options = new JsonSerializerOptions
+ {
+ TypeInfoResolverChain = { ConsumerJsonContext.Default }
+ };
+ var deserialized = JsonSerializer.Deserialize(json, options)!;
+ Assert.Equal("test-tool", deserialized.Name);
+ Assert.Null(deserialized.Execution);
+ }
+
+ [Fact]
+ public void ExperimentalProperties_RoundTrip_WhenSdkResolverIsChained()
+ {
+ var options = new JsonSerializerOptions
+ {
+ TypeInfoResolverChain =
+ {
+ McpJsonUtilities.DefaultOptions.TypeInfoResolver!,
+ ConsumerJsonContext.Default,
+ }
+ };
+
+ var tool = new Tool
+ {
+ Name = "test-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ };
+
+ string json = JsonSerializer.Serialize(tool, options);
+ Assert.Contains("\"execution\"", json);
+ Assert.Contains("\"name\"", json);
+
+ var deserialized = JsonSerializer.Deserialize(json, options)!;
+ Assert.Equal("test-tool", deserialized.Name);
+ Assert.NotNull(deserialized.Execution);
+ Assert.Equal(ToolTaskSupport.Optional, deserialized.Execution.TaskSupport);
+ }
+
+ [Fact]
+ public void ExperimentalProperties_RoundTrip_WithDefaultOptions()
+ {
+ var capabilities = new ServerCapabilities
+ {
+ Tasks = new McpTasksCapability()
+ };
+
+ string json = JsonSerializer.Serialize(capabilities, McpJsonUtilities.DefaultOptions);
+ Assert.Contains("\"tasks\"", json);
+
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
+ Assert.NotNull(deserialized.Tasks);
+ }
+}
+
+[JsonSerializable(typeof(Tool))]
+[JsonSerializable(typeof(ServerCapabilities))]
+[JsonSerializable(typeof(ClientCapabilities))]
+[JsonSerializable(typeof(CallToolResult))]
+[JsonSerializable(typeof(CallToolRequestParams))]
+[JsonSerializable(typeof(CreateMessageRequestParams))]
+[JsonSerializable(typeof(ElicitRequestParams))]
+internal partial class ConsumerJsonContext : JsonSerializerContext;