Skip to content

Commit 7cfe0ef

Browse files
Add McpSessions.NegotiatedProtocolVersion property. (#794)
1 parent 38b4a26 commit 7cfe0ef

File tree

9 files changed

+57
-20
lines changed

9 files changed

+57
-20
lines changed

src/ModelContextProtocol.Core/Client/McpClientImpl.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal sealed partial class McpClientImpl : McpClient
2626
private ServerCapabilities? _serverCapabilities;
2727
private Implementation? _serverInfo;
2828
private string? _serverInstructions;
29+
private string? _negotiatedProtocolVersion;
2930

3031
private bool _disposed;
3132

@@ -112,6 +113,9 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl
112113
/// <inheritdoc/>
113114
public override string? SessionId => _transport.SessionId;
114115

116+
/// <inheritdoc/>
117+
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
118+
115119
/// <inheritdoc/>
116120
public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected.");
117121

@@ -177,6 +181,8 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
177181
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
178182
}
179183

184+
_negotiatedProtocolVersion = initializeResponse.ProtocolVersion;
185+
180186
// Send initialized notification
181187
await this.SendNotificationAsync(
182188
NotificationMethods.InitializedNotification,

src/ModelContextProtocol.Core/McpSession.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,23 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable
3838
/// </remarks>
3939
public abstract string? SessionId { get; }
4040

41+
/// <summary>
42+
/// Gets the negotiated protocol version for the current MCP session.
43+
/// </summary>
44+
/// <remarks>
45+
/// Returns the protocol version negotiated during session initialization,
46+
/// or <see langword="null"/> if initialization hasn't yet occurred.
47+
/// </remarks>
48+
public abstract string? NegotiatedProtocolVersion { get; }
49+
4150
/// <summary>
4251
/// Sends a JSON-RPC request to the connected session and waits for a response.
4352
/// </summary>
4453
/// <param name="request">The JSON-RPC request to send.</param>
4554
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
4655
/// <returns>A task containing the session's response.</returns>
4756
/// <exception cref="InvalidOperationException">The transport is not connected, or another error occurs during request processing.</exception>
48-
/// <exception cref="McpException">An error occured during request processing.</exception>
57+
/// <exception cref="McpException">An error occurred during request processing.</exception>
4958
/// <remarks>
5059
/// This method provides low-level access to send raw JSON-RPC requests. For most use cases,
5160
/// consider using the strongly-typed methods that provide a more convenient API.

src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace ModelContextProtocol.Server;
66
internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer
77
{
88
public override string? SessionId => transport?.SessionId ?? server.SessionId;
9+
public override string? NegotiatedProtocolVersion => server.NegotiatedProtocolVersion;
910
public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
1011
public override Implementation? ClientInfo => server.ClientInfo;
1112
public override McpServerOptions ServerOptions => server.ServerOptions;

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
namespace ModelContextProtocol.Server;
99

10-
// TODO: Fix merge conflicts in this file.
11-
1210
/// <inheritdoc />
1311
internal sealed partial class McpServerImpl : McpServer
1412
{
@@ -31,6 +29,7 @@ internal sealed partial class McpServerImpl : McpServer
3129
private Implementation? _clientInfo;
3230

3331
private readonly string _serverOnlyEndpointName;
32+
private string? _negotiatedProtocolVersion;
3433
private string _endpointName;
3534
private int _started;
3635

@@ -116,6 +115,9 @@ void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection,
116115
/// <inheritdoc/>
117116
public override string? SessionId => _sessionTransport.SessionId;
118117

118+
/// <inheritdoc/>
119+
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
120+
119121
/// <inheritdoc/>
120122
public ServerCapabilities ServerCapabilities { get; } = new();
121123

@@ -212,6 +214,8 @@ private void ConfigureInitialize(McpServerOptions options)
212214
McpSessionHandler.LatestProtocolVersion;
213215
}
214216

217+
_negotiatedProtocolVersion = protocolVersion;
218+
215219
return new InitializeResult
216220
{
217221
ProtocolVersion = protocolVersion,

tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public async Task Connect_TestServer_ShouldProvideServerFields()
5252
// Assert
5353
Assert.NotNull(client.ServerCapabilities);
5454
Assert.NotNull(client.ServerInfo);
55+
Assert.NotNull(client.NegotiatedProtocolVersion);
5556

5657
if (ClientTransportOptions.Endpoint.AbsolutePath.EndsWith("/sse"))
5758
{

tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ public async Task StreamableHttpClient_SendsMcpProtocolVersionHeader_AfterInitia
171171

172172
await app.StartAsync(TestContext.Current.CancellationToken);
173173

174-
await using (var mcpClient = await ConnectAsync(clientOptions: new()
174+
await using var mcpClient = await ConnectAsync(clientOptions: new()
175175
{
176176
ProtocolVersion = "2025-03-26",
177-
}))
178-
{
179-
await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
180-
}
177+
});
178+
179+
Assert.Equal("2025-03-26", mcpClient.NegotiatedProtocolVersion);
180+
await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
181181

182182
// The header should be included in the GET request, the initialized notification, the tools/list call, and the delete request.
183183
Assert.NotEmpty(protocolVersionHeaderValues);

tests/ModelContextProtocol.Tests/Client/McpClientTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,13 @@ public async Task AsClientLoggerProvider_MessagesSentToClient()
468468
],
469469
data.OrderBy(s => s));
470470
}
471+
472+
[Theory]
473+
[InlineData(null)]
474+
[InlineData("2025-03-26")]
475+
public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion)
476+
{
477+
await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion });
478+
Assert.Equal(protocolVersion ?? "2025-06-18", client.NegotiatedProtocolVersion);
479+
}
471480
}

tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ public async Task Connect_ShouldProvideServerFields(string clientId)
5252
// Assert
5353
Assert.NotNull(client.ServerCapabilities);
5454
Assert.NotNull(client.ServerInfo);
55+
Assert.NotNull(client.NegotiatedProtocolVersion);
56+
5557
if (clientId != "everything") // Note: Comment the below assertion back when the everything server is updated to provide instructions
58+
{
5659
Assert.NotNull(client.ServerInstructions);
60+
}
5761

5862
Assert.Null(client.SessionId);
5963
}

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public async Task Create_Should_Initialize_With_Valid_Parameters()
4141

4242
// Assert
4343
Assert.NotNull(server);
44+
Assert.Null(server.NegotiatedProtocolVersion);
4445
}
4546

4647
[Fact]
@@ -232,7 +233,7 @@ await Can_Handle_Requests(
232233
serverCapabilities: null,
233234
method: RequestMethods.Ping,
234235
configureOptions: null,
235-
assertResult: response =>
236+
assertResult: (_, response) =>
236237
{
237238
JsonObject jObj = Assert.IsType<JsonObject>(response);
238239
Assert.Empty(jObj);
@@ -247,13 +248,14 @@ await Can_Handle_Requests(
247248
serverCapabilities: null,
248249
method: RequestMethods.Initialize,
249250
configureOptions: null,
250-
assertResult: response =>
251+
assertResult: (server, response) =>
251252
{
252253
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
253254
Assert.NotNull(result);
254255
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
255256
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
256257
Assert.Equal("2024", result.ProtocolVersion);
258+
Assert.Equal("2024", server.NegotiatedProtocolVersion);
257259
});
258260
}
259261

@@ -279,7 +281,7 @@ await Can_Handle_Requests(
279281
},
280282
method: RequestMethods.CompletionComplete,
281283
configureOptions: null,
282-
assertResult: response =>
284+
assertResult: (_, response) =>
283285
{
284286
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
285287
Assert.NotNull(result?.Completion);
@@ -316,7 +318,7 @@ await Can_Handle_Requests(
316318
},
317319
RequestMethods.ResourcesTemplatesList,
318320
configureOptions: null,
319-
assertResult: response =>
321+
assertResult: (_, response) =>
320322
{
321323
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
322324
Assert.NotNull(result?.ResourceTemplates);
@@ -345,7 +347,7 @@ await Can_Handle_Requests(
345347
},
346348
RequestMethods.ResourcesList,
347349
configureOptions: null,
348-
assertResult: response =>
350+
assertResult: (_, response) =>
349351
{
350352
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
351353
Assert.NotNull(result?.Resources);
@@ -380,7 +382,7 @@ await Can_Handle_Requests(
380382
},
381383
method: RequestMethods.ResourcesRead,
382384
configureOptions: null,
383-
assertResult: response =>
385+
assertResult: (_, response) =>
384386
{
385387
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
386388
Assert.NotNull(result?.Contents);
@@ -417,7 +419,7 @@ await Can_Handle_Requests(
417419
},
418420
method: RequestMethods.PromptsList,
419421
configureOptions: null,
420-
assertResult: response =>
422+
assertResult: (_, response) =>
421423
{
422424
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
423425
Assert.NotNull(result?.Prompts);
@@ -446,7 +448,7 @@ await Can_Handle_Requests(
446448
},
447449
method: RequestMethods.PromptsGet,
448450
configureOptions: null,
449-
assertResult: response =>
451+
assertResult: (_, response) =>
450452
{
451453
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
452454
Assert.NotNull(result);
@@ -480,7 +482,7 @@ await Can_Handle_Requests(
480482
},
481483
method: RequestMethods.ToolsList,
482484
configureOptions: null,
483-
assertResult: response =>
485+
assertResult: (_, response) =>
484486
{
485487
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
486488
Assert.NotNull(result);
@@ -515,7 +517,7 @@ await Can_Handle_Requests(
515517
},
516518
method: RequestMethods.ToolsCall,
517519
configureOptions: null,
518-
assertResult: response =>
520+
assertResult: (_, response) =>
519521
{
520522
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
521523
Assert.NotNull(result);
@@ -530,7 +532,7 @@ public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_A
530532
await Succeeds_Even_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured");
531533
}
532534

533-
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<JsonNode?> assertResult)
535+
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<McpServer, JsonNode?> assertResult)
534536
{
535537
await using var transport = new TestServerTransport();
536538
var options = CreateOptions(serverCapabilities);
@@ -559,7 +561,7 @@ await transport.SendMessageAsync(
559561
var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5));
560562
Assert.NotNull(response);
561563

562-
assertResult(response.Result);
564+
assertResult(server, response.Result);
563565

564566
await transport.DisposeAsync();
565567
await runTask;
@@ -682,6 +684,7 @@ public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, C
682684
public override ValueTask DisposeAsync() => default;
683685

684686
public override string? SessionId => throw new NotImplementedException();
687+
public override string? NegotiatedProtocolVersion => throw new NotImplementedException();
685688
public override Implementation? ClientInfo => throw new NotImplementedException();
686689
public override IServiceProvider? Services => throw new NotImplementedException();
687690
public override LoggingLevel? LoggingLevel => throw new NotImplementedException();

0 commit comments

Comments
 (0)