Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<Project Path="tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj" />
<Project Path="tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj" />
<Project Path="tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj" />
<Project Path="tests/ModelContextProtocol.SuppressorRegressionTest/ModelContextProtocol.SuppressorRegressionTest.csproj" />
<Project Path="tests/ModelContextProtocol.TestOAuthServer/ModelContextProtocol.TestOAuthServer.csproj" />
<Project Path="tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj" />
<Project Path="tests/ModelContextProtocol.TestServer/ModelContextProtocol.TestServer.csproj" />
Expand Down
51 changes: 51 additions & 0 deletions src/ModelContextProtocol.Analyzers/MCPEXP001Suppressor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace ModelContextProtocol.Analyzers;

/// <summary>
/// Suppresses MCPEXP001 diagnostics in source-generated code.
/// </summary>
/// <remarks>
/// <para>
/// The MCP SDK uses <c>object?</c> backing fields with <c>[JsonConverter(typeof(ExperimentalJsonConverter&lt;T&gt;))]</c>
/// to handle serialization of experimental types. When consumers define their own <c>JsonSerializerContext</c>,
/// the System.Text.Json source generator emits code referencing these converters with experimental type arguments,
/// which triggers MCPEXP001 diagnostics in the generated code.
/// </para>
/// <para>
/// This suppressor suppresses MCPEXP001 only in source-generated files (identified by <c>.g.cs</c> file extension),
/// so that hand-written user code that directly references experimental types still produces the diagnostic.
/// </para>
/// </remarks>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MCPEXP001Suppressor : DiagnosticSuppressor
{
private static readonly SuppressionDescriptor SuppressInGeneratedCode = new(
id: "MCP_MCPEXP001_GENERATED",
suppressedDiagnosticId: "MCPEXP001",
justification: "MCPEXP001 is suppressed in source-generated code because the experimental type reference originates from the MCP SDK's backing field infrastructure, not from user code.");

/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions =>
ImmutableArray.Create(SuppressInGeneratedCode);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
{
if (diagnostic.Id == "MCPEXP001" && IsInGeneratedCode(diagnostic))
{
context.ReportSuppression(Suppression.Create(SuppressInGeneratedCode, diagnostic));
}
}
}

private static bool IsInGeneratedCode(Diagnostic diagnostic)
{
string? filePath = diagnostic.Location.SourceTree?.FilePath;
return filePath is not null && filePath.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase);
}
}
59 changes: 59 additions & 0 deletions src/ModelContextProtocol.Core/ExperimentalJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace ModelContextProtocol;

/// <summary>
/// A JSON converter that handles serialization of experimental MCP types through <c>object?</c> backing fields.
/// </summary>
/// <typeparam name="T">The experimental type to serialize/deserialize.</typeparam>
/// <remarks>
/// <para>
/// This converter is used on <c>object?</c> backing fields that shadow public experimental properties
/// marked with <see cref="ExperimentalAttribute"/>. By declaring the backing field
/// as <c>object?</c>, the System.Text.Json source generator does not walk the experimental type graph.
/// </para>
/// <para>
/// Serialization delegates to <see cref="McpJsonUtilities.DefaultOptions"/>, which already contains source-generated
/// contracts for all experimental types.
/// </para>
/// <para>
/// This type is not intended to be used directly. It supports the MCP infrastructure and is subject to change.
/// </para>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public class ExperimentalJsonConverter<T> : JsonConverter<object?> where T : class
{
private static JsonTypeInfo<T> TypeInfo => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));

/// <inheritdoc/>
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

return JsonSerializer.Deserialize(ref reader, TypeInfo);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

if (value is not T typed)
{
throw new JsonException($"Expected value of type '{typeof(T).Name}' but got '{value.GetType().Name}'.");
}

JsonSerializer.Serialize(writer, typed, TypeInfo);
}
}
16 changes: 15 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -33,6 +35,18 @@ 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.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTaskMetadata? Task
{
get => _task as McpTaskMetadata;
set => _task = value;
}

/// <summary>Backing field for <see cref="Task"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTaskMetadata>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _task;
Comment on lines +46 to +51
Copy link
Collaborator

Choose a reason for hiding this comment

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

I recommend a much stronger statement about not being intended for consumption. We can take inspiration from efcore. Example: https://github.com/dotnet/efcore/blob/a471ebfd9564bc14fb188407a84a031b69d29f77/src/EFCore/Query/QueryContext.cs#L109-L116

We should also give the backing field a more egregious name to emphasize the internal aspect too.

}
16 changes: 15 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -65,6 +67,18 @@ public sealed class CallToolResult : Result
/// (<see cref="Content"/>, <see cref="StructuredContent"/>, <see cref="IsError"/>) may not be populated.
/// The actual tool result can be retrieved later via <c>tasks/result</c>.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTask? Task
{
get => _task as McpTask;
set => _task = value;
}

/// <summary>Backing field for <see cref="Task"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("task")]
public McpTask? Task { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTask>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _task;
}
15 changes: 14 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
Expand Down Expand Up @@ -80,6 +81,18 @@ public sealed class ClientCapabilities
/// See <see cref="McpTasksCapability"/> for details on configuring which operations support tasks.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTasksCapability? Tasks
{
get => _tasks as McpTasksCapability;
set => _tasks = value;
}

/// <summary>Backing field for <see cref="Tasks"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("tasks")]
public McpTasksCapability? Tasks { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTasksCapability>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _tasks;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -128,6 +130,18 @@ 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.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTaskMetadata? Task
{
get => _task as McpTaskMetadata;
set => _task = value;
}

/// <summary>Backing field for <see cref="Task"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTaskMetadata>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _task;
}
14 changes: 13 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,20 @@ 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.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTaskMetadata? Task
{
get => _task as McpTaskMetadata;
set => _task = value;
}

/// <summary>Backing field for <see cref="Task"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTaskMetadata>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _task;

/// <summary>Represents a request schema used in a form mode elicitation request.</summary>
public sealed class RequestSchema
Expand Down
15 changes: 14 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ModelContextProtocol.Server;

Expand Down Expand Up @@ -79,6 +80,18 @@ public sealed class ServerCapabilities
/// See <see cref="McpTasksCapability"/> for details on configuring which operations support tasks.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public McpTasksCapability? Tasks
{
get => _tasks as McpTasksCapability;
set => _tasks = value;
}

/// <summary>Backing field for <see cref="Tasks"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("tasks")]
public McpTasksCapability? Tasks { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<McpTasksCapability>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _tasks;
}
14 changes: 13 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/Tool.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
Expand Down Expand Up @@ -120,8 +121,19 @@ public JsonElement? OutputSchema
/// regarding task augmentation support. See <see cref="ToolExecution"/> for details.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
[JsonIgnore]
public ToolExecution? Execution
{
get => _execution as ToolExecution;
set => _execution = value;
}

/// <summary>Backing field for <see cref="Execution"/>. This field is not intended to be used directly.</summary>
[JsonInclude]
[JsonPropertyName("execution")]
public ToolExecution? Execution { get; set; }
[JsonConverter(typeof(ExperimentalJsonConverter<ToolExecution>))]
[EditorBrowsable(EditorBrowsableState.Never)]
public object? _execution;

/// <summary>
/// Gets or sets an optional list of icons for this tool.
Expand Down
Loading