@@ -152,15 +152,17 @@ void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection,
152152
153153 /// <summary>
154154 /// Wraps <paramref name="inner"/> so that, for every JSON-RPC request, a built-in filter first
155- /// synchronizes server-side state (<see cref="_negotiatedProtocolVersion"/>,
156- /// <see cref="_clientCapabilities"/>, <see cref="_clientInfo"/>) from the per-request <c>_meta</c>
157- /// values projected onto <see cref="JsonRpcMessageContext"/> and validates the per-request protocol
158- /// version, before delegating to the user-supplied incoming filters.
155+ /// synchronizes server-side state (<see cref="_negotiatedProtocolVersion"/>, <see cref="_clientInfo"/>)
156+ /// from the per-request <c>_meta</c> values projected onto <see cref="JsonRpcMessageContext"/> and
157+ /// validates the per-request protocol version, before delegating to the user-supplied incoming filters.
159158 /// </summary>
160159 /// <remarks>
161160 /// Under the 2026-07-28 protocol revision (SEP-2575) there is no <c>initialize</c> handshake, so these values
162- /// MUST be populated per-request. For legacy clients the per-request values are absent and the built-in
163- /// filter is a no-op (the values were captured during the initialize handler).
161+ /// MUST be populated per-request. Per-request client capabilities and client info are consumed request-scoped
162+ /// by <see cref="DestinationBoundMcpServer"/> and are not read from server-wide state by request handlers. The
163+ /// shared <see cref="_clientInfo"/> write below is best-effort and used only to derive the session endpoint
164+ /// name for logging/telemetry. For legacy clients the per-request values are absent and the built-in filter is
165+ /// a no-op (the values were captured during the initialize handler).
164166 /// </remarks>
165167 private JsonRpcMessageFilter PrependMetaReadingFilter ( JsonRpcMessageFilter inner )
166168 {
@@ -185,23 +187,16 @@ private JsonRpcMessageFilter PrependMetaReadingFilter(JsonRpcMessageFilter inner
185187 SetNegotiatedProtocolVersion ( protocolVersion ) ;
186188 }
187189
188- if ( context . ClientCapabilities is { } clientCapabilities && IsJuly2026OrLaterProtocol ( ) && HasStatefulTransport ( ) )
189- {
190- // Under the 2026-07-28 revision the per-request _meta envelope carries the client's FULL
191- // capabilities (SEP-2575), so a plain overwrite is correct. The IsJuly2026OrLaterProtocol() gate
192- // makes any legacy per-request envelope a no-op (legacy capabilities stay as the
193- // initialize handshake established them); the HasStatefulTransport() gate keeps
194- // _clientCapabilities null under StreamableHttpServerTransport { Stateless = true }
195- // (where the same server instance handles every request, so persisting per-request
196- // capability state would both leak across requests and break the StatelessServerTests
197- // invariant that surfaces the "X is not supported in stateless mode" errors).
198- _clientCapabilities = clientCapabilities ;
199- }
200-
201190 if ( context . ClientInfo is { } clientInfo &&
202191 ( _clientInfo is null || ! string . Equals ( _clientInfo . Name , clientInfo . Name , StringComparison . Ordinal ) ||
203192 ! string . Equals ( _clientInfo . Version , clientInfo . Version , StringComparison . Ordinal ) ) )
204193 {
194+ // This shared write is best-effort and used only to derive the session endpoint name for
195+ // logging/telemetry. It is intentionally NOT read by request handlers on 2026-07-28+ sessions:
196+ // DestinationBoundMcpServer resolves ClientInfo (and ClientCapabilities) request-scoped from
197+ // the per-request _meta so concurrent requests never observe each other's values. Under a
198+ // draft stateful session with differing per-request client info, the last writer wins here,
199+ // which only affects the logged endpoint name and never the request-scoped values handlers see.
205200 _clientInfo = clientInfo ;
206201 endpointNameNeedsRefresh = true ;
207202 }
@@ -1627,7 +1622,7 @@ async ValueTask<TResult> InvokeScopedAsync(
16271622
16281623 private DestinationBoundMcpServer CreateDestinationBoundServer ( JsonRpcRequest jsonRpcRequest )
16291624 {
1630- var server = new DestinationBoundMcpServer ( this , jsonRpcRequest . Context ? . RelatedTransport ) ;
1625+ var server = new DestinationBoundMcpServer ( this , jsonRpcRequest . Context ? . RelatedTransport , jsonRpcRequest . Context ) ;
16311626
16321627 if ( _mrtrContextsByRequestId . TryRemove ( jsonRpcRequest . Id , out var mrtrContext ) )
16331628 {
@@ -1740,7 +1735,7 @@ private JsonRpcMessageFilter BuildMessageFilterPipeline(IList<McpMessageFilter>
17401735 {
17411736 // Ensure message has a Context so Items can be shared through the pipeline
17421737 message . Context ??= new ( ) ;
1743- var context = new MessageContext ( new DestinationBoundMcpServer ( this , message . Context . RelatedTransport ) , message ) ;
1738+ var context = new MessageContext ( new DestinationBoundMcpServer ( this , message . Context . RelatedTransport , message . Context ) , message ) ;
17441739 await current ( context , cancellationToken ) . ConfigureAwait ( false ) ;
17451740 } ;
17461741 } ;
@@ -1795,8 +1790,12 @@ internal bool HasStatefulTransport() =>
17951790 /// Used to gate the SEP-2663 Tasks extension, which only interoperates on the 2026-07-28 revision.
17961791 /// </summary>
17971792 private bool IsJuly2026OrLaterProtocolRequest ( JsonRpcRequest ? request ) =>
1793+ IsJuly2026OrLaterProtocolRequest ( request ? . Context ) ;
1794+
1795+ /// <inheritdoc cref="IsJuly2026OrLaterProtocolRequest(JsonRpcRequest?)"/>
1796+ internal bool IsJuly2026OrLaterProtocolRequest ( JsonRpcMessageContext ? requestContext ) =>
17981797 McpHttpHeaders . IsJuly2026OrLaterProtocolVersion (
1799- request ? . Context ? . ProtocolVersion ?? NegotiatedProtocolVersion ) ;
1798+ requestContext ? . ProtocolVersion ?? NegotiatedProtocolVersion ) ;
18001799
18011800 /// <inheritdoc />
18021801 public override bool IsMrtrSupported => ClientSupportsMrtr ( ) || HasStatefulTransport ( ) ;
0 commit comments