Skip to content
Draft
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
2 changes: 2 additions & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Tasks/ModelContextProtocol.Extensions.Tasks.csproj" />
<Project Path="src/ModelContextProtocol.Legacy.Tasks-2025-11-30/ModelContextProtocol.Legacy.Tasks-2025-11-30.csproj" />
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
</Folder>
<Folder Name="/tests/">
Expand All @@ -78,6 +79,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.Legacy.Tasks-2025-11-30.Tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests.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
1 change: 1 addition & 0 deletions docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the
| `MCP9004` | In place | <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.EnableLegacySse> opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. |
| `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. |
| `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions> — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. |
| `MCP9007` | In place | The source-compatible `2025-11-30` Tasks APIs in `ModelContextProtocol.Legacy.Tasks-2025-11-30` are retained only to assist migrations from SDK 1.x. Use `ModelContextProtocol.Extensions.Tasks` for new code. |
10 changes: 10 additions & 0 deletions src/Common/McpProtocolVersions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ internal static class McpProtocolVersions
/// </summary>
public const string November2025ProtocolVersion = "2025-11-25";

/// <summary>
/// The 2025-11-30 MCP protocol revision that introduced the experimental Tasks protocol.
/// </summary>
/// <remarks>
/// This revision is retained only so compatibility packages can negotiate and implement
/// historical experimental protocol features. Core does not implement Tasks for this revision.
/// </remarks>
public const string November2025TasksProtocolVersion = "2025-11-30";

/// <summary>The 2025-06-18 MCP protocol revision.</summary>
public const string June2025ProtocolVersion = "2025-06-18";

Expand All @@ -42,6 +51,7 @@ internal static class McpProtocolVersions
March2025ProtocolVersion,
June2025ProtocolVersion,
November2025ProtocolVersion,
November2025TasksProtocolVersion,
];

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ModelContextProtocol.Core/Client/McpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public sealed class McpClientOptions
/// <remarks>
/// <para>
/// Supported values are <c>2024-11-05</c>, <c>2025-03-26</c>, <c>2025-06-18</c>, <c>2025-11-25</c>,
/// and <c>2026-07-28</c>.
/// <c>2025-11-30</c>, and <c>2026-07-28</c>.
/// </para>
/// <para>
/// When <see langword="null"/> (the default), the client prefers the latest revision (<c>2026-07-28</c>),
Expand Down
12 changes: 12 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
Expand Down Expand Up @@ -84,4 +85,15 @@ public sealed class ClientCapabilities
/// </remarks>
[JsonPropertyName("extensions")]
public IDictionary<string, object>? Extensions { get; set; }

/// <summary>
/// Gets or sets unrecognized capability properties for protocol extensions that define
/// top-level capability names.
/// </summary>
/// <remarks>
/// Extension packages should prefer <see cref="Extensions"/> when the negotiated protocol
/// supports it. This property preserves capability names defined by historical protocol revisions.
/// </remarks>
[JsonExtensionData]
public IDictionary<string, JsonElement>? AdditionalProperties { get; set; }
}
12 changes: 12 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ModelContextProtocol.Server;

Expand Down Expand Up @@ -82,4 +83,15 @@ public sealed class ServerCapabilities
/// </remarks>
[JsonPropertyName("extensions")]
public IDictionary<string, object>? Extensions { get; set; }

/// <summary>
/// Gets or sets unrecognized capability properties for protocol extensions that define
/// top-level capability names.
/// </summary>
/// <remarks>
/// Extension packages should prefer <see cref="Extensions"/> when the negotiated protocol
/// supports it. This property preserves capability names defined by historical protocol revisions.
/// </remarks>
[JsonExtensionData]
public IDictionary<string, JsonElement>? AdditionalProperties { get; set; }
}
6 changes: 6 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/Tool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ public JsonElement? OutputSchema
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }

/// <summary>
/// Gets or sets unrecognized top-level tool properties defined by protocol extensions.
/// </summary>
[JsonExtensionData]
public IDictionary<string, JsonElement>? AdditionalProperties { get; set; }

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ internal Tool BuildLegacyWireProtocolTool()
Annotations = ProtocolTool.Annotations,
Icons = ProtocolTool.Icons,
Meta = ProtocolTool.Meta,
AdditionalProperties = ProtocolTool.AdditionalProperties,
};
}

Expand Down
50 changes: 44 additions & 6 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ private JsonRpcMessageFilter PrependMetaReadingFilter(JsonRpcMessageFilter inner
bool hasProtocolVersionMeta = HasMetaKey(request, MetaKeys.ProtocolVersion);
bool hasReservedPerRequestMeta = TryGetPerRequestMetadataKey(request, out var reservedPerRequestMetaKey);

// Initialize-handshake protocols establish their version once per session. Surface that
// established version on later raw requests so extension handlers can gate their wire behavior.
if (context?.ProtocolVersion is null && _negotiatedProtocolVersion is { } negotiatedProtocolVersion)
{
context ??= request.Context = new JsonRpcMessageContext();
context.ProtocolVersion = negotiatedProtocolVersion;
}

if (context?.ProtocolVersion is { } protocolVersion)
{
bool protocolVersionAlreadyEstablished = _negotiatedProtocolVersion is not null;
Expand Down Expand Up @@ -994,6 +1002,7 @@ private void ConfigureExperimentalAndExtensions(McpServerOptions options)
{
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
ServerCapabilities.Extensions = options.Capabilities?.Extensions;
ServerCapabilities.AdditionalProperties = options.Capabilities?.AdditionalProperties;
}

private void ConfigureCustomRequestHandlers(McpServerOptions options)
Expand All @@ -1011,19 +1020,48 @@ private void ConfigureCustomRequestHandlers(McpServerOptions options)
throw new InvalidOperationException(
$"A custom request handler registered through {nameof(McpServerOptions)}.{nameof(McpServerOptions.RequestHandlers)} has a null or empty {nameof(McpServerRequestHandler.Method)}.");
}
}

foreach (var groupedHandlers in customHandlers.GroupBy(static entry => entry.Method, StringComparer.Ordinal))
{
McpServerRequestHandler[] handlers = [.. groupedHandlers];
string method = handlers[0].Method;

// Custom handlers are registered after all built-in handlers, so a method already present
// belongs to a built-in method (e.g. initialize, tools/call) or an earlier custom handler.
// Silently overwriting it would bypass the built-in handler's filters and protocol gating,
// so reject the collision instead.
if (_requestHandlers.ContainsKey(entry.Method))
// belongs to a built-in method (e.g. initialize or tools/call). Silently overwriting it
// would bypass the built-in handler's filters and protocol gating, so reject the collision.
if (_requestHandlers.ContainsKey(method))
{
throw new InvalidOperationException(
$"A custom request handler registered through {nameof(McpServerOptions)}.{nameof(McpServerOptions.RequestHandlers)} " +
$"uses the method '{entry.Method}', which is already handled by the server. Custom handlers cannot replace built-in methods or other custom handlers.");
$"uses the method '{method}', which is already handled by the server. Custom handlers cannot replace built-in methods or other custom handlers.");
}

SetRawHandler(entry.Method, entry.Handler);
if (handlers.Length == 1)
{
SetRawHandler(method, handlers[0].Handler);
continue;
}

if (handlers.Any(static handler => handler.IsApplicable is null))
{
throw new InvalidOperationException(
$"Multiple custom request handlers are registered for method '{method}'. " +
$"Each handler must specify {nameof(McpServerRequestHandler.IsApplicable)} to share a method.");
}

SetRawHandler(method, async (request, cancellationToken) =>
{
foreach (var handler in handlers)
{
if (handler.IsApplicable!(request))
{
return await handler.Handler(request, cancellationToken).ConfigureAwait(false);
}
}

throw new McpProtocolException($"Method '{method}' is not available.", McpErrorCode.MethodNotFound);
});
}
#pragma warning restore MCPEXP002
}
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Server/McpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public sealed class McpServerOptions
/// <remarks>
/// <para>
/// The protocol version defines which features and message formats this server supports. Supported
/// values are <c>2024-11-05</c>, <c>2025-03-26</c>, <c>2025-06-18</c>, <c>2025-11-25</c>, and
/// <c>2026-07-28</c>.
/// values are <c>2024-11-05</c>, <c>2025-03-26</c>, <c>2025-06-18</c>, <c>2025-11-25</c>,
/// <c>2025-11-30</c>, and <c>2026-07-28</c>.
/// </para>
/// <para>
/// If <see langword="null"/>, the server supports all of the versions listed above. For clients using
Expand Down
10 changes: 10 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ public sealed class McpServerRequestHandler
/// </summary>
public required string Method { get; init; }

/// <summary>
/// Gets an optional predicate that determines whether this handler applies to an incoming request.
/// </summary>
/// <remarks>
/// When multiple custom handlers register the same method, each handler must specify this
/// predicate. The first applicable handler is invoked, allowing extensions to share a method
/// name while dispatching by negotiated protocol version or another request characteristic.
/// </remarks>
public Func<JsonRpcRequest, bool>? IsApplicable { get; init; }

/// <summary>
/// Gets the handler function that processes incoming requests for the specified method.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ public void PostConfigure(string? name, McpServerOptions options)
}

options.RequestHandlers ??= new List<McpServerRequestHandler>();
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksGet, Handler = HandleGetTask });
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksUpdate, Handler = HandleUpdateTask });
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksCancel, Handler = HandleCancelTask });
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksGet, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleGetTask });
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksUpdate, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleUpdateTask });
options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksCancel, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleCancelTask });

// Use a filter rather than a handler so it wraps around Core's tool dispatch.
// This ensures it intercepts tool calls BEFORE the tool is invoked, allowing
Expand Down
Loading