Skip to content

Commit 84a7253

Browse files
authored
Merge branch 'main' into localden/experimental
2 parents f2c779d + 1e8fb04 commit 84a7253

File tree

23 files changed

+227
-23
lines changed

23 files changed

+227
-23
lines changed

src/ModelContextProtocol.AspNetCore/SseHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async Task HandleSseRequestAsync(HttpContext context)
3131

3232
var requestPath = (context.Request.PathBase + context.Request.Path).ToString();
3333
var endpointPattern = requestPath[..(requestPath.LastIndexOf('/') + 1)];
34-
await using var transport = new SseResponseStreamTransport(context.Response.Body, $"{endpointPattern}message?sessionId={sessionId}");
34+
await using var transport = new SseResponseStreamTransport(context.Response.Body, $"{endpointPattern}message?sessionId={sessionId}", sessionId);
3535

3636
var userIdClaim = StreamableHttpHandler.GetUserIdClaim(context.User);
3737
await using var httpMcpSession = new HttpMcpSession<SseResponseStreamTransport>(sessionId, transport, userIdClaim, httpMcpServerOptions.Value.TimeProvider);

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.AspNetCore.WebUtilities;
55
using Microsoft.Extensions.Logging;
66
using Microsoft.Extensions.Options;
7-
using Microsoft.Extensions.Primitives;
87
using Microsoft.Net.Http.Headers;
98
using ModelContextProtocol.AspNetCore.Stateless;
109
using ModelContextProtocol.Protocol;
@@ -136,6 +135,7 @@ public async Task HandleDeleteRequestAsync(HttpContext context)
136135
var transport = new StreamableHttpServerTransport
137136
{
138137
Stateless = true,
138+
SessionId = sessionId,
139139
};
140140
session = await CreateSessionAsync(context, transport, sessionId, statelessSessionId);
141141
}
@@ -184,7 +184,10 @@ private async ValueTask<HttpMcpSession<StreamableHttpServerTransport>> StartNewS
184184
if (!HttpServerTransportOptions.Stateless)
185185
{
186186
sessionId = MakeNewSessionId();
187-
transport = new();
187+
transport = new()
188+
{
189+
SessionId = sessionId,
190+
};
188191
context.Response.Headers["mcp-session-id"] = sessionId;
189192
}
190193
else
@@ -286,21 +289,19 @@ internal static string MakeNewSessionId()
286289

287290
private void ScheduleStatelessSessionIdWrite(HttpContext context, StreamableHttpServerTransport transport)
288291
{
289-
context.Response.OnStarting(() =>
292+
transport.OnInitRequestReceived = initRequestParams =>
290293
{
291294
var statelessId = new StatelessSessionId
292295
{
293-
ClientInfo = transport?.InitializeRequest?.ClientInfo,
296+
ClientInfo = initRequestParams?.ClientInfo,
294297
UserIdClaim = GetUserIdClaim(context.User),
295298
};
296299

297300
var sessionJson = JsonSerializer.Serialize(statelessId, StatelessSessionIdJsonContext.Default.StatelessSessionId);
298-
var sessionId = Protector.Protect(sessionJson);
299-
300-
context.Response.Headers["mcp-session-id"] = sessionId;
301-
302-
return Task.CompletedTask;
303-
});
301+
transport.SessionId = Protector.Protect(sessionJson);
302+
context.Response.Headers["mcp-session-id"] = transport.SessionId;
303+
return ValueTask.CompletedTask;
304+
};
304305
}
305306

306307
internal static Task RunSessionAsync(HttpContext httpContext, IMcpServer session, CancellationToken requestAborted)

src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public AutoDetectingClientSessionTransport(SseClientTransportOptions transportOp
4545

4646
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel.Reader;
4747

48+
string? ITransport.SessionId => ActiveTransport?.SessionId;
49+
4850
/// <inheritdoc/>
4951
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
5052
{

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.Logging;
22
using ModelContextProtocol.Protocol;
3+
using System.Diagnostics;
34
using System.Text.Json;
45

56
namespace ModelContextProtocol.Client;
@@ -93,6 +94,20 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL
9394
}
9495
}
9596

97+
/// <inheritdoc/>
98+
public string? SessionId
99+
{
100+
get
101+
{
102+
if (_sessionTransport is null)
103+
{
104+
throw new InvalidOperationException("Must have already initialized a session when invoking this property.");
105+
}
106+
107+
return _sessionTransport.SessionId;
108+
}
109+
}
110+
96111
/// <inheritdoc/>
97112
public ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected.");
98113

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
2727
private readonly CancellationTokenSource _connectionCts;
2828
private readonly ILogger _logger;
2929

30-
private string? _mcpSessionId;
3130
private Task? _getReceiveTask;
3231
private int _disposed;
3332

@@ -86,7 +85,7 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
8685
},
8786
};
8887

89-
CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, _mcpSessionId);
88+
CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, SessionId);
9089

9190
var response = await _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
9291

@@ -125,7 +124,7 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
125124
// We've successfully initialized! Copy session-id and start GET request if any.
126125
if (response.Headers.TryGetValues("mcp-session-id", out var sessionIdValues))
127126
{
128-
_mcpSessionId = sessionIdValues.FirstOrDefault();
127+
SessionId = sessionIdValues.FirstOrDefault();
129128
}
130129

131130
_getReceiveTask = ReceiveUnsolicitedMessagesAsync();
@@ -176,7 +175,7 @@ private async Task ReceiveUnsolicitedMessagesAsync()
176175
// Send a GET request to handle any unsolicited messages not sent over a POST response.
177176
using var request = new HttpRequestMessage(HttpMethod.Get, _options.Endpoint);
178177
request.Headers.Accept.Add(s_textEventStreamMediaType);
179-
CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, _mcpSessionId);
178+
CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, SessionId);
180179

181180
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _connectionCts.Token).ConfigureAwait(false);
182181

src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
using System.Diagnostics.CodeAnalysis;
66
#if !NET9_0_OR_GREATER
77
using System.Reflection;
8-
using System.Text.Json;
98
#endif
9+
using System.Text.Json;
1010
using System.Text.Json.Serialization;
1111
#if !NET9_0_OR_GREATER
1212
using ModelContextProtocol;
@@ -66,6 +66,31 @@ public override string ConvertName(string name) =>
6666
}
6767
#endif
6868
}
69+
70+
/// <summary>
71+
/// A JSON converter for enums that allows customizing the serialized string value of enum members
72+
/// using the <see cref="JsonStringEnumMemberNameAttribute"/>.
73+
/// </summary>
74+
/// <remarks>
75+
/// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter&lt;T&gt;
76+
/// 9.x support for custom enum member naming. It will be replaced by the built-in functionality
77+
/// once .NET 9 is fully adopted.
78+
/// </remarks>
79+
[EditorBrowsable(EditorBrowsableState.Never)]
80+
[RequiresUnreferencedCode("Requires unreferenced code to instantiate the generic enum converter.")]
81+
[RequiresDynamicCode("Requires dynamic code to instantiate the generic enum converter.")]
82+
public sealed class CustomizableJsonStringEnumConverter : JsonConverterFactory
83+
{
84+
/// <inheritdoc/>
85+
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
86+
/// <inheritdoc/>
87+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
88+
{
89+
Type converterType = typeof(CustomizableJsonStringEnumConverter<>).MakeGenericType(typeToConvert)!;
90+
var factory = (JsonConverterFactory)Activator.CreateInstance(converterType)!;
91+
return factory.CreateConverter(typeToConvert, options);
92+
}
93+
}
6994
}
7095

7196
#if !NET9_0_OR_GREATER

src/ModelContextProtocol.Core/IMcpEndpoint.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ namespace ModelContextProtocol;
2828
/// </remarks>
2929
public interface IMcpEndpoint : IAsyncDisposable
3030
{
31+
/// <summary>Gets an identifier associated with the current MCP session.</summary>
32+
/// <remarks>
33+
/// Typically populated in transports supporting multiple sessions such as Streamable HTTP or SSE.
34+
/// Can return <see langword="null"/> if the session hasn't initialized or if the transport doesn't
35+
/// support multiple sessions (as is the case with STDIO).
36+
/// </remarks>
37+
string? SessionId { get; }
38+
3139
/// <summary>
3240
/// Sends a JSON-RPC request to the connected endpoint and waits for a response.
3341
/// </summary>

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModelContextProtocol.Protocol;
44
using ModelContextProtocol.Server;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
67
using System.Text.Json;
78
using System.Text.Json.Serialization;
89
using System.Text.Json.Serialization.Metadata;
@@ -35,14 +36,22 @@ public static partial class McpJsonUtilities
3536
/// Creates default options to use for MCP-related serialization.
3637
/// </summary>
3738
/// <returns>The configured options.</returns>
39+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
40+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
3841
private static JsonSerializerOptions CreateDefaultOptions()
3942
{
4043
// Copy the configuration from the source generated context.
4144
JsonSerializerOptions options = new(JsonContext.Default.Options);
4245

43-
// Chain with all supported types from MEAI
46+
// Chain with all supported types from MEAI.
4447
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
4548

49+
// Add a converter for user-defined enums, if reflection is enabled by default.
50+
if (JsonSerializer.IsReflectionEnabledByDefault)
51+
{
52+
options.Converters.Add(new CustomizableJsonStringEnumConverter());
53+
}
54+
4655
options.MakeReadOnly();
4756
return options;
4857
}

src/ModelContextProtocol.Core/Protocol/ITransport.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ namespace ModelContextProtocol.Protocol;
2525
/// </remarks>
2626
public interface ITransport : IAsyncDisposable
2727
{
28+
/// <summary>Gets an identifier associated with the current MCP session.</summary>
29+
/// <remarks>
30+
/// Typically populated in transports supporting multiple sessions such as Streamable HTTP or SSE.
31+
/// Can return <see langword="null"/> if the session hasn't initialized or if the transport doesn't
32+
/// support multiple sessions (as is the case with STDIO).
33+
/// </remarks>
34+
string? SessionId { get; }
35+
2836
/// <summary>
2937
/// Gets a channel reader for receiving messages from the transport.
3038
/// </summary>

src/ModelContextProtocol.Core/Protocol/TransportBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ internal TransportBase(string name, Channel<JsonRpcMessage>? messageChannel, ILo
5959
/// <summary>Gets the logger used by this transport.</summary>
6060
private protected ILogger Logger => _logger;
6161

62+
/// <inheritdoc/>
63+
public virtual string? SessionId { get; protected set; }
64+
6265
/// <summary>
6366
/// Gets the name that identifies this transport endpoint in logs.
6467
/// </summary>

0 commit comments

Comments
 (0)