diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
index 22c861326..e9aca10ac 100644
--- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
+++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
@@ -23,8 +23,21 @@ internal sealed class StreamableHttpHandler(
ILoggerFactory loggerFactory)
{
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
+ private const string McpProtocolVersionHeaderName = "MCP-Protocol-Version";
private const string LastEventIdHeaderName = "Last-Event-ID";
+ ///
+ /// All protocol versions supported by this implementation.
+ /// Keep in sync with McpSessionHandler.SupportedProtocolVersions in ModelContextProtocol.Core.
+ ///
+ private static readonly HashSet s_supportedProtocolVersions =
+ [
+ "2024-11-05",
+ "2025-03-26",
+ "2025-06-18",
+ "2025-11-25",
+ ];
+
private static readonly JsonTypeInfo s_messageTypeInfo = GetRequiredJsonTypeInfo();
private static readonly JsonTypeInfo s_errorTypeInfo = GetRequiredJsonTypeInfo();
@@ -32,6 +45,12 @@ internal sealed class StreamableHttpHandler(
public async Task HandlePostRequestAsync(HttpContext context)
{
+ if (!ValidateProtocolVersionHeader(context, out var errorMessage))
+ {
+ await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
+ return;
+ }
+
// The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
// ASP.NET Core Minimal APIs mostly try to stay out of the business of response content negotiation,
// so we have to do this manually. The spec doesn't mandate that servers MUST reject these requests,
@@ -74,6 +93,12 @@ await WriteJsonRpcErrorAsync(context,
public async Task HandleGetRequestAsync(HttpContext context)
{
+ if (!ValidateProtocolVersionHeader(context, out var errorMessage))
+ {
+ await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
+ return;
+ }
+
if (!context.Request.GetTypedHeaders().Accept.Any(MatchesTextEventStreamMediaType))
{
await WriteJsonRpcErrorAsync(context,
@@ -171,6 +196,12 @@ private static async Task HandleResumePostResponseStreamAsync(HttpContext contex
public async Task HandleDeleteRequestAsync(HttpContext context)
{
+ if (!ValidateProtocolVersionHeader(context, out var errorMessage))
+ {
+ await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
+ return;
+ }
+
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
if (sessionManager.TryRemove(sessionId, out var session))
{
@@ -390,6 +421,24 @@ internal static Task RunSessionAsync(HttpContext httpContext, McpServer session,
internal static JsonTypeInfo GetRequiredJsonTypeInfo() => (JsonTypeInfo)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
+ ///
+ /// Validates the MCP-Protocol-Version header if present. A missing header is allowed for backwards compatibility,
+ /// but an invalid or unsupported value must be rejected with 400 Bad Request per the MCP spec.
+ ///
+ private static bool ValidateProtocolVersionHeader(HttpContext context, out string? errorMessage)
+ {
+ var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
+ if (!string.IsNullOrEmpty(protocolVersionHeader) &&
+ !s_supportedProtocolVersions.Contains(protocolVersionHeader))
+ {
+ errorMessage = $"Bad Request: The MCP-Protocol-Version header value '{protocolVersionHeader}' is not supported.";
+ return false;
+ }
+
+ errorMessage = null;
+ return true;
+ }
+
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
=> acceptHeaderValue.MatchesMediaType("application/json");
diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs
index 9137218eb..1aa444692 100644
--- a/src/ModelContextProtocol.Core/McpSessionHandler.cs
+++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs
@@ -31,7 +31,10 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
/// The latest version of the protocol supported by this implementation.
internal const string LatestProtocolVersion = "2025-11-25";
- /// All protocol versions supported by this implementation.
+ ///
+ /// All protocol versions supported by this implementation.
+ /// Keep in sync with s_supportedProtocolVersions in StreamableHttpHandler.
+ ///
internal static readonly string[] SupportedProtocolVersions =
[
"2024-11-05",
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
index 0c42b3e82..a667302df 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
@@ -158,6 +158,54 @@ public async Task GetRequest_IsAcceptable_WithWildcardOrAddedQualityInAcceptHead
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
+ [Theory]
+ [InlineData("invalid-version")]
+ [InlineData("9999-01-01")]
+ [InlineData("not-a-date")]
+ public async Task PostRequest_IsBadRequest_WithInvalidProtocolVersionHeader(string invalidVersion)
+ {
+ await StartAsync();
+
+ HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", invalidVersion);
+
+ using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostRequest_Succeeds_WithoutProtocolVersionHeader()
+ {
+ await StartAsync();
+
+ // No MCP-Protocol-Version header is set - this should be accepted for backwards compatibility
+ using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostRequest_Succeeds_WithValidProtocolVersionHeader()
+ {
+ await StartAsync();
+
+ HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", "2025-03-26");
+
+ using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetRequest_IsBadRequest_WithInvalidProtocolVersionHeader()
+ {
+ await StartAsync();
+
+ await CallInitializeAndValidateAsync();
+
+ HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", "invalid-version");
+
+ using var response = await HttpClient.GetAsync("", HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
[Fact]
public async Task PostRequest_IsNotFound_WithUnrecognizedSessionId()
{