diff --git a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs
index 677606eb..2a34a17a 100644
--- a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs
+++ b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs
@@ -35,6 +35,20 @@ public class HttpServerTransportOptions
///
public bool Stateless { get; set; }
+ ///
+ /// Gets or sets whether the server should use a single execution context for the entire session.
+ /// If , handlers like tools get called with the
+ /// belonging to the corresponding HTTP request which can change throughout the MCP session.
+ /// If , handlers will get called with the same
+ /// used to call and .
+ ///
+ ///
+ /// Enabling a per-session can be useful for setting variables
+ /// that persist for the entire session, but it prevents you from using IHttpContextAccessor in handlers.
+ /// Defaults to .
+ ///
+ public bool PerSessionExecutionContext { get; set; }
+
///
/// Gets or sets the duration of time the server will wait between any active requests before timing out an MCP session.
///
diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
index aeac38bf..6dac1c3e 100644
--- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
+++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
@@ -188,6 +188,7 @@ private async ValueTask> StartNewS
transport = new()
{
SessionId = sessionId,
+ FlowExecutionContextFromRequests = !HttpServerTransportOptions.PerSessionExecutionContext,
};
context.Response.Headers[McpSessionIdHeaderName] = sessionId;
}
diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs
index 47c4d212..06b2894b 100644
--- a/src/ModelContextProtocol.Core/McpSession.cs
+++ b/src/ModelContextProtocol.Core/McpSession.cs
@@ -115,7 +115,16 @@ public async Task ProcessMessagesAsync(CancellationToken cancellationToken)
LogMessageRead(EndpointName, message.GetType().Name);
// Fire and forget the message handling to avoid blocking the transport.
- _ = ProcessMessageAsync();
+ if (message.ExecutionContext is null)
+ {
+ _ = ProcessMessageAsync();
+ }
+ else
+ {
+ // Flow the execution context from the HTTP request corresponding to this message if provided.
+ ExecutionContext.Run(message.ExecutionContext, _ => _ = ProcessMessageAsync(), null);
+ }
+
async Task ProcessMessageAsync()
{
JsonRpcMessageWithId? messageWithId = message as JsonRpcMessageWithId;
@@ -609,9 +618,9 @@ private static void AddExceptionTags(ref TagList tags, Activity? activity, Excep
e = ae.InnerException;
}
- int? intErrorCode =
+ int? intErrorCode =
(int?)((e as McpException)?.ErrorCode) is int errorCode ? errorCode :
- e is JsonException ? (int)McpErrorCode.ParseError :
+ e is JsonException ? (int)McpErrorCode.ParseError :
null;
string? errorType = intErrorCode?.ToString() ?? e.GetType().FullName;
diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
index 77866add..b3176937 100644
--- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
+++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs
@@ -1,3 +1,4 @@
+using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -38,6 +39,19 @@ private protected JsonRpcMessage()
[JsonIgnore]
public ITransport? RelatedTransport { get; set; }
+ ///
+ /// Gets or sets the that should be used to run any handlers
+ ///
+ ///
+ /// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
+ /// the outlives the initial HTTP request context it was created on, and new
+ /// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
+ /// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
+ /// in tool calls.
+ ///
+ [JsonIgnore]
+ public ExecutionContext? ExecutionContext { get; set; }
+
///
/// Provides a for messages,
/// handling polymorphic deserialization of different message types.
diff --git a/src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs b/src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs
index 343b5748..9d225caa 100644
--- a/src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs
+++ b/src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs
@@ -91,6 +91,11 @@ private async ValueTask OnMessageReceivedAsync(JsonRpcMessage? message, Cancella
message.RelatedTransport = this;
+ if (parentTransport.FlowExecutionContextFromRequests)
+ {
+ message.ExecutionContext = ExecutionContext.Capture();
+ }
+
await parentTransport.MessageWriter.WriteAsync(message, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs
index 1f5775e6..b63c8a65 100644
--- a/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs
+++ b/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs
@@ -10,7 +10,7 @@ namespace ModelContextProtocol.Server;
///
///
/// This transport provides one-way communication from server to client using the SSE protocol over HTTP,
-/// while receiving client messages through a separate mechanism. It writes messages as
+/// while receiving client messages through a separate mechanism. It writes messages as
/// SSE events to a response stream, typically associated with an HTTP response.
///
///
@@ -36,6 +36,9 @@ public sealed class StreamableHttpServerTransport : ITransport
private int _getRequestStarted;
+ ///
+ public string? SessionId { get; set; }
+
///
/// Configures whether the transport should be in stateless mode that does not require all requests for a given session
/// to arrive to the same ASP.NET Core application process. Unsolicited server-to-client messages are not supported in this mode,
@@ -45,6 +48,15 @@ public sealed class StreamableHttpServerTransport : ITransport
///
public bool Stateless { get; init; }
+ ///
+ /// Gets a value indicating whether the execution context should flow from the calls to
+ /// to the corresponding emitted by the .
+ ///
+ ///
+ /// Defaults to .
+ ///
+ public bool FlowExecutionContextFromRequests { get; init; }
+
///
/// Gets or sets a callback to be invoked before handling the initialize request.
///
@@ -55,9 +67,6 @@ public sealed class StreamableHttpServerTransport : ITransport
internal ChannelWriter MessageWriter => _incomingChannel.Writer;
- ///
- public string? SessionId { get; set; }
-
///
/// Handles an optional SSE GET request a client using the Streamable HTTP transport might make by
/// writing any unsolicited JSON-RPC messages sent via
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs
index cf54e777..4d0d7356 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs
@@ -52,12 +52,6 @@ public async Task MapMcp_ThrowsInvalidOperationException_IfWithHttpTransportIsNo
[Fact]
public async Task Can_UseIHttpContextAccessor_InTool()
{
- Assert.SkipWhen(UseStreamableHttp && !Stateless,
- """
- IHttpContextAccessor is not currently supported with non-stateless Streamable HTTP.
- TODO: Support it in stateless mode by manually capturing and flowing execution context.
- """);
-
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools();
Builder.Services.AddHttpContextAccessor();
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
index 8c7f736d..0b3ae4c2 100644
--- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs
@@ -387,7 +387,7 @@ public async Task Progress_IsReported_InSameSseResponseAsRpcResponse()
}
[Fact]
- public async Task AsyncLocalSetInRunSessionHandlerCallback_Flows_ToAllToolCalls()
+ public async Task AsyncLocalSetInRunSessionHandlerCallback_Flows_ToAllToolCalls_IfPerSessionExecutionContextEnabled()
{
var asyncLocal = new AsyncLocal();
var totalSessionCount = 0;
@@ -395,6 +395,7 @@ public async Task AsyncLocalSetInRunSessionHandlerCallback_Flows_ToAllToolCalls(
Builder.Services.AddMcpServer()
.WithHttpTransport(options =>
{
+ options.PerSessionExecutionContext = true;
options.RunSessionHandler = async (httpContext, mcpServer, cancellationToken) =>
{
asyncLocal.Value = $"RunSessionHandler ({totalSessionCount++})";