From 7cfe0ef6216c7f8e1af70c3bb5e9cac76e9d5c3c Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 19 Sep 2025 13:11:01 +0300 Subject: [PATCH] Add McpSessions.NegotiatedProtocolVersion property. (#794) --- .../Client/McpClientImpl.cs | 6 +++++ src/ModelContextProtocol.Core/McpSession.cs | 11 +++++++- .../Server/DestinationBoundMcpServer.cs | 1 + .../Server/McpServerImpl.cs | 8 ++++-- .../HttpServerIntegrationTests.cs | 1 + .../MapMcpStreamableHttpTests.cs | 10 +++---- .../Client/McpClientTests.cs | 9 +++++++ .../ClientIntegrationTests.cs | 4 +++ .../Server/McpServerTests.cs | 27 ++++++++++--------- 9 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 6397188..729185a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -26,6 +26,7 @@ internal sealed partial class McpClientImpl : McpClient private ServerCapabilities? _serverCapabilities; private Implementation? _serverInfo; private string? _serverInstructions; + private string? _negotiatedProtocolVersion; private bool _disposed; @@ -112,6 +113,9 @@ internal sealed partial class McpClientImpl : McpClient /// public override string? SessionId => _transport.SessionId; + /// + public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion; + /// public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); @@ -177,6 +181,8 @@ internal sealed partial class McpClientImpl : McpClient throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}"); } + _negotiatedProtocolVersion = initializeResponse.ProtocolVersion; + // Send initialized notification await this.SendNotificationAsync( NotificationMethods.InitializedNotification, diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 241c36d..429fdbf 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -38,6 +38,15 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable /// public abstract string? SessionId { get; } + /// + /// Gets the negotiated protocol version for the current MCP session. + /// + /// + /// Returns the protocol version negotiated during session initialization, + /// or if initialization hasn't yet occurred. + /// + public abstract string? NegotiatedProtocolVersion { get; } + /// /// Sends a JSON-RPC request to the connected session and waits for a response. /// @@ -45,7 +54,7 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable /// The to monitor for cancellation requests. The default is . /// A task containing the session's response. /// The transport is not connected, or another error occurs during request processing. - /// An error occured during request processing. + /// An error occurred during request processing. /// /// This method provides low-level access to send raw JSON-RPC requests. For most use cases, /// consider using the strongly-typed methods that provide a more convenient API. diff --git a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs index f74dc29..bbbc45d 100644 --- a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs +++ b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs @@ -6,6 +6,7 @@ namespace ModelContextProtocol.Server; internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer { public override string? SessionId => transport?.SessionId ?? server.SessionId; + public override string? NegotiatedProtocolVersion => server.NegotiatedProtocolVersion; public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities; public override Implementation? ClientInfo => server.ClientInfo; public override McpServerOptions ServerOptions => server.ServerOptions; diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 1ece8af..f4f9e8b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -7,8 +7,6 @@ using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol.Server; -// TODO: Fix merge conflicts in this file. - /// internal sealed partial class McpServerImpl : McpServer { @@ -31,6 +29,7 @@ internal sealed partial class McpServerImpl : McpServer private Implementation? _clientInfo; private readonly string _serverOnlyEndpointName; + private string? _negotiatedProtocolVersion; private string _endpointName; private int _started; @@ -116,6 +115,9 @@ internal sealed partial class McpServerImpl : McpServer /// public override string? SessionId => _sessionTransport.SessionId; + /// + public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion; + /// public ServerCapabilities ServerCapabilities { get; } = new(); @@ -212,6 +214,8 @@ internal sealed partial class McpServerImpl : McpServer McpSessionHandler.LatestProtocolVersion; } + _negotiatedProtocolVersion = protocolVersion; + return new InitializeResult { ProtocolVersion = protocolVersion, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index f9aa5a5..5da3714 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -52,6 +52,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture s)); } + + [Theory] + [InlineData(null)] + [InlineData("2025-03-26")] + public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion) + { + await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion }); + Assert.Equal(protocolVersion ?? "2025-06-18", client.NegotiatedProtocolVersion); + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 2116884..e2f0380 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -52,8 +52,12 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture + assertResult: (_, response) => { JsonObject jObj = Assert.IsType(response); Assert.Empty(jObj); @@ -247,13 +248,14 @@ public class McpServerTests : LoggedTest serverCapabilities: null, method: RequestMethods.Initialize, configureOptions: null, - assertResult: response => + assertResult: (server, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name); Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version); Assert.Equal("2024", result.ProtocolVersion); + Assert.Equal("2024", server.NegotiatedProtocolVersion); }); } @@ -279,7 +281,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.CompletionComplete, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Completion); @@ -316,7 +318,7 @@ public class McpServerTests : LoggedTest }, RequestMethods.ResourcesTemplatesList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.ResourceTemplates); @@ -345,7 +347,7 @@ public class McpServerTests : LoggedTest }, RequestMethods.ResourcesList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Resources); @@ -380,7 +382,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.ResourcesRead, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Contents); @@ -417,7 +419,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.PromptsList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Prompts); @@ -446,7 +448,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.PromptsGet, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -480,7 +482,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.ToolsList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -515,7 +517,7 @@ public class McpServerTests : LoggedTest }, method: RequestMethods.ToolsCall, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -530,7 +532,7 @@ public class McpServerTests : LoggedTest await Succeeds_Even_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured"); } - private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action? configureOptions, Action assertResult) + private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action? configureOptions, Action assertResult) { await using var transport = new TestServerTransport(); var options = CreateOptions(serverCapabilities); @@ -559,7 +561,7 @@ public class McpServerTests : LoggedTest var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.NotNull(response); - assertResult(response.Result); + assertResult(server, response.Result); await transport.DisposeAsync(); await runTask; @@ -682,6 +684,7 @@ public class McpServerTests : LoggedTest public override ValueTask DisposeAsync() => default; public override string? SessionId => throw new NotImplementedException(); + public override string? NegotiatedProtocolVersion => throw new NotImplementedException(); public override Implementation? ClientInfo => throw new NotImplementedException(); public override IServiceProvider? Services => throw new NotImplementedException(); public override LoggingLevel? LoggingLevel => throw new NotImplementedException();