Send MCP-Protocol-Version header in Streamable HTTP client transport (#500)

This commit is contained in:
Stephen Halter 2025-06-17 14:27:26 -07:00 • committed by GitHub
commit befa31d5c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,6 +26,7 @@ internal sealed class StreamableHttpHandler(
ILoggerFactory loggerFactory,
IServiceProvider applicationServices)
{
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
public ConcurrentDictionary<string, HttpMcpSession<StreamableHttpServerTransport>> Sessions { get; } = new(StringComparer.Ordinal);
@ -70,8 +71,8 @@ internal sealed class StreamableHttpHandler(
}
finally
{
// Stateless sessions are 1:1 with HTTP requests and are outlived by the MCP session tracked by the mcp-session-id.
// Non-stateless sessions are 1:1 with the mcp-session-id and outlive the POST request.
// Stateless sessions are 1:1 with HTTP requests and are outlived by the MCP session tracked by the Mcp-Session-Id.
// Non-stateless sessions are 1:1 with the Mcp-Session-Id and outlive the POST request.
// Non-stateless sessions get disposed by a DELETE request or the IdleTrackingBackgroundService.
if (HttpServerTransportOptions.Stateless)
{
@ -90,7 +91,7 @@ internal sealed class StreamableHttpHandler(
return;
}
var sessionId = context.Request.Headers["mcp-session-id"].ToString();
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
var session = await GetSessionAsync(context, sessionId);
if (session is null)
{
@ -117,7 +118,7 @@ internal sealed class StreamableHttpHandler(
public async Task HandleDeleteRequestAsync(HttpContext context)
{
var sessionId = context.Request.Headers["mcp-session-id"].ToString();
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
if (Sessions.TryRemove(sessionId, out var session))
{
await session.DisposeAsync();
@ -157,14 +158,14 @@ internal sealed class StreamableHttpHandler(
return null;
}
context.Response.Headers["mcp-session-id"] = session.Id;
context.Response.Headers[McpSessionIdHeaderName] = session.Id;
context.Features.Set(session.Server);
return session;
}
private async ValueTask<HttpMcpSession<StreamableHttpServerTransport>?> GetOrCreateSessionAsync(HttpContext context)
{
var sessionId = context.Request.Headers["mcp-session-id"].ToString();
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
if (string.IsNullOrEmpty(sessionId))
{
@ -188,11 +189,11 @@ internal sealed class StreamableHttpHandler(
{
SessionId = sessionId,
};
context.Response.Headers["mcp-session-id"] = sessionId;
context.Response.Headers[McpSessionIdHeaderName] = sessionId;
}
else
{
// "(uninitialized stateless id)" is not written anywhere. We delay writing the mcp-session-id
// "(uninitialized stateless id)" is not written anywhere. We delay writing the MCP-Session-Id
// until after we receive the initialize request with the client info we need to serialize.
sessionId = "(uninitialized stateless id)";
transport = new()
@ -204,7 +205,7 @@ internal sealed class StreamableHttpHandler(
var session = await CreateSessionAsync(context, transport, sessionId);
// The HttpMcpSession is not stored between requests in stateless mode. Instead, the session is recreated from the mcp-session-id.
// The HttpMcpSession is not stored between requests in stateless mode. Instead, the session is recreated from the MCP-Session-Id.
if (!HttpServerTransportOptions.Stateless)
{
if (!Sessions.TryAdd(sessionId, session))
@ -299,7 +300,7 @@ internal sealed class StreamableHttpHandler(
var sessionJson = JsonSerializer.Serialize(statelessId, StatelessSessionIdJsonContext.Default.StatelessSessionId);
transport.SessionId = Protector.Protect(sessionJson);
context.Response.Headers["mcp-session-id"] = transport.SessionId;
context.Response.Headers[McpSessionIdHeaderName] = transport.SessionId;
return ValueTask.CompletedTask;
};
}