Skip to content

Commit 04eec1c

Browse files
Copilotstephentoub
andcommitted
Move SupportedProtocolVersions to McpServer as instance member
Add abstract SupportedProtocolVersions property to McpServer, implemented by McpServerImpl (delegating to McpSessionHandler.SupportedProtocolVersions) and DestinationBoundMcpServer. Protocol version validation in StreamableHttpHandler now occurs after session resolution, querying the server instance directly. Remove private HashSet from handler. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent b907c99 commit 04eec1c

5 files changed

Lines changed: 30 additions & 29 deletions

File tree

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,13 @@ internal sealed class StreamableHttpHandler(
2626
private const string McpProtocolVersionHeaderName = "MCP-Protocol-Version";
2727
private const string LastEventIdHeaderName = "Last-Event-ID";
2828

29-
/// <summary>All protocol versions supported by this implementation.</summary>
30-
private static readonly HashSet<string> s_supportedProtocolVersions =
31-
[
32-
"2024-11-05",
33-
"2025-03-26",
34-
"2025-06-18",
35-
"2025-11-25",
36-
];
37-
3829
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
3930
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
4031

4132
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
4233

4334
public async Task HandlePostRequestAsync(HttpContext context)
4435
{
45-
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
46-
{
47-
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
48-
return;
49-
}
50-
5136
// The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
5237
// ASP.NET Core Minimal APIs mostly try to stay out of the business of response content negotiation,
5338
// so we have to do this manually. The spec doesn't mandate that servers MUST reject these requests,
@@ -67,6 +52,12 @@ await WriteJsonRpcErrorAsync(context,
6752
return;
6853
}
6954

55+
if (!ValidateProtocolVersionHeader(context, session.Server, out var errorMessage))
56+
{
57+
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
58+
return;
59+
}
60+
7061
await using var _ = await session.AcquireReferenceAsync(context.RequestAborted);
7162

7263
var message = await ReadJsonRpcMessageAsync(context);
@@ -90,12 +81,6 @@ await WriteJsonRpcErrorAsync(context,
9081

9182
public async Task HandleGetRequestAsync(HttpContext context)
9283
{
93-
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
94-
{
95-
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
96-
return;
97-
}
98-
9984
if (!context.Request.GetTypedHeaders().Accept.Any(MatchesTextEventStreamMediaType))
10085
{
10186
await WriteJsonRpcErrorAsync(context,
@@ -111,6 +96,12 @@ await WriteJsonRpcErrorAsync(context,
11196
return;
11297
}
11398

99+
if (!ValidateProtocolVersionHeader(context, session.Server, out var errorMessage))
100+
{
101+
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
102+
return;
103+
}
104+
114105
var lastEventId = context.Request.Headers[LastEventIdHeaderName].ToString();
115106
if (!string.IsNullOrEmpty(lastEventId))
116107
{
@@ -193,15 +184,15 @@ private static async Task HandleResumePostResponseStreamAsync(HttpContext contex
193184

194185
public async Task HandleDeleteRequestAsync(HttpContext context)
195186
{
196-
if (!ValidateProtocolVersionHeader(context, out var errorMessage))
197-
{
198-
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
199-
return;
200-
}
201-
202187
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
203188
if (sessionManager.TryRemove(sessionId, out var session))
204189
{
190+
if (!ValidateProtocolVersionHeader(context, session.Server, out var errorMessage))
191+
{
192+
await WriteJsonRpcErrorAsync(context, errorMessage!, StatusCodes.Status400BadRequest);
193+
return;
194+
}
195+
205196
await session.DisposeAsync();
206197
}
207198
}
@@ -422,11 +413,11 @@ internal static Task RunSessionAsync(HttpContext httpContext, McpServer session,
422413
/// Validates the MCP-Protocol-Version header if present. A missing header is allowed for backwards compatibility,
423414
/// but an invalid or unsupported value must be rejected with 400 Bad Request per the MCP spec.
424415
/// </summary>
425-
private static bool ValidateProtocolVersionHeader(HttpContext context, out string? errorMessage)
416+
private static bool ValidateProtocolVersionHeader(HttpContext context, McpServer server, out string? errorMessage)
426417
{
427418
var protocolVersionHeader = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
428419
if (!string.IsNullOrEmpty(protocolVersionHeader) &&
429-
!s_supportedProtocolVersions.Contains(protocolVersionHeader))
420+
!server.SupportedProtocolVersions.Contains(protocolVersionHeader))
430421
{
431422
errorMessage = $"Bad Request: The MCP-Protocol-Version header value '{protocolVersionHeader}' is not supported.";
432423
return false;

src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport
1212
public override McpServerOptions ServerOptions => server.ServerOptions;
1313
public override IServiceProvider? Services => server.Services;
1414
public override LoggingLevel? LoggingLevel => server.LoggingLevel;
15+
public override ICollection<string> SupportedProtocolVersions => server.SupportedProtocolVersions;
1516

1617
public override ValueTask DisposeAsync() => server.DisposeAsync();
1718

src/ModelContextProtocol.Core/Server/McpServer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public abstract partial class McpServer : McpSession
5555
/// <summary>Gets the last logging level set by the client, or <see langword="null"/> if it's never been set.</summary>
5656
public abstract LoggingLevel? LoggingLevel { get; }
5757

58+
/// <summary>
59+
/// Gets the protocol versions supported by this server implementation.
60+
/// </summary>
61+
public abstract ICollection<string> SupportedProtocolVersions { get; }
62+
5863
/// <summary>
5964
/// Runs the server, listening for and handling client requests.
6065
/// </summary>

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection,
155155
/// <inheritdoc />
156156
public override LoggingLevel? LoggingLevel => _loggingLevel?.Value;
157157

158+
/// <inheritdoc />
159+
public override ICollection<string> SupportedProtocolVersions => McpSessionHandler.SupportedProtocolVersions;
160+
158161
/// <inheritdoc />
159162
public override async Task RunAsync(CancellationToken cancellationToken = default)
160163
{

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, C
938938
public override Implementation? ClientInfo => throw new NotImplementedException();
939939
public override IServiceProvider? Services => throw new NotImplementedException();
940940
public override LoggingLevel? LoggingLevel => throw new NotImplementedException();
941+
public override ICollection<string> SupportedProtocolVersions => throw new NotImplementedException();
941942
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) =>
942943
throw new NotImplementedException();
943944
public override Task RunAsync(CancellationToken cancellationToken = default) =>

0 commit comments

Comments
 (0)