Skip to content

Commit b2ac6b6

Browse files
committed
Send MCP-Protocol-Version header with transport messages
1 parent fa017c0 commit b2ac6b6

12 files changed

Lines changed: 76 additions & 12 deletions

File tree

src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal sealed partial class AutoDetectingClientSessionTransport : ITransport
1818
private readonly ILogger _logger;
1919
private readonly string _name;
2020
private readonly Channel<JsonRpcMessage> _messageChannel;
21+
private string? _protocolVersion;
2122

2223
public AutoDetectingClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName)
2324
{
@@ -43,6 +44,20 @@ public AutoDetectingClientSessionTransport(SseClientTransportOptions transportOp
4344
/// </summary>
4445
internal ITransport? ActiveTransport { get; private set; }
4546

47+
/// <inheritdoc />
48+
public string? ProtocolVersion
49+
{
50+
get => ActiveTransport?.ProtocolVersion ?? _protocolVersion;
51+
set
52+
{
53+
_protocolVersion = value;
54+
if (ActiveTransport is { } transport)
55+
{
56+
transport.ProtocolVersion = value;
57+
}
58+
}
59+
}
60+
4661
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel.Reader;
4762

4863
/// <inheritdoc/>
@@ -70,6 +85,10 @@ private async Task InitializeAsync(JsonRpcMessage message, CancellationToken can
7085
{
7186
LogUsingStreamableHttp(_name);
7287
ActiveTransport = streamableHttpTransport;
88+
if (_protocolVersion is { } protocolVersion)
89+
{
90+
ActiveTransport.ProtocolVersion = protocolVersion;
91+
}
7392
}
7493
else
7594
{

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
118118
// Connect transport
119119
_sessionTransport = await _clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
120120
InitializeSession(_sessionTransport);
121+
121122
// We don't want the ConnectAsync token to cancel the session after we've successfully connected.
122123
// The base class handles cleaning up the session in DisposeAsync without our help.
123124
StartSession(_sessionTransport, fullSessionCancellationToken: CancellationToken.None);
@@ -164,6 +165,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
164165
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
165166
}
166167

168+
_sessionTransport.ProtocolVersion = initializeResponse.ProtocolVersion;
169+
167170
// Send initialized notification
168171
await SendMessageAsync(
169172
new JsonRpcNotification { Method = NotificationMethods.InitializedNotification },

src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public override async Task SendMessageAsync(
9191
{
9292
Content = content,
9393
};
94-
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders);
94+
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, protocolVersion: ProtocolVersion);
9595
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
9696

9797
if (!response.IsSuccessStatusCode)
@@ -152,7 +152,7 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
152152
{
153153
using var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
154154
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
155-
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders);
155+
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, protocolVersion: ProtocolVersion);
156156

157157
using var response = await _httpClient.SendAsync(
158158
request,

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
8585
},
8686
};
8787

88-
CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, _mcpSessionId);
88+
CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, _mcpSessionId, ProtocolVersion);
8989

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

@@ -170,7 +170,7 @@ private async Task ReceiveUnsolicitedMessagesAsync()
170170
// Send a GET request to handle any unsolicited messages not sent over a POST response.
171171
using var request = new HttpRequestMessage(HttpMethod.Get, _options.Endpoint);
172172
request.Headers.Accept.Add(s_textEventStreamMediaType);
173-
CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, _mcpSessionId);
173+
CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, _mcpSessionId, ProtocolVersion);
174174

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

@@ -245,23 +245,30 @@ private void LogJsonException(JsonException ex, string data)
245245
}
246246
}
247247

248-
internal static void CopyAdditionalHeaders(HttpRequestHeaders headers, Dictionary<string, string>? additionalHeaders, string? sessionId = null)
248+
internal static void CopyAdditionalHeaders(
249+
HttpRequestHeaders headers,
250+
Dictionary<string, string>? additionalHeaders,
251+
string? sessionId = null,
252+
string? protocolVersion = null)
249253
{
250254
if (sessionId is not null)
251255
{
252256
headers.Add("mcp-session-id", sessionId);
253257
}
254258

255-
if (additionalHeaders is null)
259+
if (protocolVersion is not null)
256260
{
257-
return;
261+
headers.Add("MCP-Protocol-Version", protocolVersion);
258262
}
259263

260-
foreach (var header in additionalHeaders)
264+
if (additionalHeaders is not null)
261265
{
262-
if (!headers.TryAddWithoutValidation(header.Key, header.Value))
266+
foreach (var header in additionalHeaders)
263267
{
264-
throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(SseClientTransportOptions.AdditionalHeaders)}.");
268+
if (!headers.TryAddWithoutValidation(header.Key, header.Value))
269+
{
270+
throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(SseClientTransportOptions.AdditionalHeaders)}.");
271+
}
265272
}
266273
}
267274
}

src/ModelContextProtocol.Core/Protocol/ITransport.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,12 @@ public interface ITransport : IAsyncDisposable
6060
/// </para>
6161
/// </remarks>
6262
Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default);
63+
64+
/// <summary>Gets or sets the protocol version that's in use.</summary>
65+
/// <remarks>
66+
/// Setting the protocol version does not change the protocol version actively employed by the transport.
67+
/// It provides that information to the transport for situations where the transport needs to be able to
68+
/// propagate the version information, for example as part of HTTP headers or for logging and diagnostic purposes.
69+
/// </remarks>
70+
string? ProtocolVersion { get; set; }
6371
}

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 string? ProtocolVersion { get; set; }
64+
6265
/// <summary>
6366
/// Gets the name that identifies this transport endpoint in logs.
6467
/// </summary>

src/ModelContextProtocol.Core/Server/SseResponseStreamTransport.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
3434

3535
private bool _isConnected;
3636

37+
/// <inheritdoc />
38+
public string? ProtocolVersion { get; set; }
39+
3740
/// <summary>
3841
/// Starts the transport and writes the JSON-RPC messages sent via <see cref="SendMessageAsync"/>
3942
/// to the SSE response stream until cancellation is requested or the transport is disposed.

src/ModelContextProtocol.Core/Server/StreamableHttpPostTransport.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ internal sealed class StreamableHttpPostTransport(StreamableHttpServerTransport
1818

1919
public ChannelReader<JsonRpcMessage> MessageReader => throw new NotSupportedException("JsonRpcMessage.RelatedTransport should only be used for sending messages.");
2020

21+
/// <inheritdoc />
22+
public string? ProtocolVersion { get; set; }
23+
2124
/// <returns>
2225
/// True, if data was written to the respond body.
2326
/// False, if nothing was written because the request body did not contain any <see cref="JsonRpcRequest"/> messages to respond to.

src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public sealed class StreamableHttpServerTransport : ITransport
3535
private readonly CancellationTokenSource _disposeCts = new();
3636

3737
private int _getRequestStarted;
38+
39+
/// <inheritdoc />
40+
public string? ProtocolVersion { get; set; }
3841

3942
/// <summary>
4043
/// Configures whether the transport should be in stateless mode that does not require all requests for a given session

tests/Common/Utils/TestServerTransport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public class TestServerTransport : ITransport
1010

1111
public bool IsConnected { get; set; }
1212

13+
public string? ProtocolVersion { get; set; }
14+
1315
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel;
1416

1517
public List<JsonRpcMessage> SentMessages { get; } = [];

0 commit comments

Comments
 (0)