diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 639718885..729185a3e 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 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl /// 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 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) 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 241c36d4c..429fdbfd4 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 f74dc29b0..bbbc45dcc 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 1ece8af23..f4f9e8b32 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -7,8 +7,6 @@ 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 @@ void Register(McpServerPrimitiveCollection? collection, /// public override string? SessionId => _sessionTransport.SessionId; + /// + public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion; + /// public ServerCapabilities ServerCapabilities { get; } = new(); @@ -212,6 +214,8 @@ private void ConfigureInitialize(McpServerOptions options) 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 f9aa5a5e9..5da37146a 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -52,6 +52,7 @@ public async Task Connect_TestServer_ShouldProvideServerFields() // Assert Assert.NotNull(client.ServerCapabilities); Assert.NotNull(client.ServerInfo); + Assert.NotNull(client.NegotiatedProtocolVersion); if (ClientTransportOptions.Endpoint.AbsolutePath.EndsWith("/sse")) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs index cb1f86db9..f8b61aa21 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs @@ -171,13 +171,13 @@ public async Task StreamableHttpClient_SendsMcpProtocolVersionHeader_AfterInitia await app.StartAsync(TestContext.Current.CancellationToken); - await using (var mcpClient = await ConnectAsync(clientOptions: new() + await using var mcpClient = await ConnectAsync(clientOptions: new() { ProtocolVersion = "2025-03-26", - })) - { - await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); - } + }); + + Assert.Equal("2025-03-26", mcpClient.NegotiatedProtocolVersion); + await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); // The header should be included in the GET request, the initialized notification, the tools/list call, and the delete request. Assert.NotEmpty(protocolVersionHeaderValues); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 779e31e62..d7034660b 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -468,4 +468,13 @@ public async Task AsClientLoggerProvider_MessagesSentToClient() ], data.OrderBy(s => 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 211688419..e2f03805f 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -52,8 +52,12 @@ public async Task Connect_ShouldProvideServerFields(string clientId) // Assert Assert.NotNull(client.ServerCapabilities); Assert.NotNull(client.ServerInfo); + Assert.NotNull(client.NegotiatedProtocolVersion); + if (clientId != "everything") // Note: Comment the below assertion back when the everything server is updated to provide instructions + { Assert.NotNull(client.ServerInstructions); + } Assert.Null(client.SessionId); } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 61cda7015..736d63ec4 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -41,6 +41,7 @@ public async Task Create_Should_Initialize_With_Valid_Parameters() // Assert Assert.NotNull(server); + Assert.Null(server.NegotiatedProtocolVersion); } [Fact] @@ -232,7 +233,7 @@ await Can_Handle_Requests( serverCapabilities: null, method: RequestMethods.Ping, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { JsonObject jObj = Assert.IsType(response); Assert.Empty(jObj); @@ -247,13 +248,14 @@ await Can_Handle_Requests( 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 @@ await Can_Handle_Requests( }, method: RequestMethods.CompletionComplete, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Completion); @@ -316,7 +318,7 @@ await Can_Handle_Requests( }, RequestMethods.ResourcesTemplatesList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.ResourceTemplates); @@ -345,7 +347,7 @@ await Can_Handle_Requests( }, RequestMethods.ResourcesList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Resources); @@ -380,7 +382,7 @@ await Can_Handle_Requests( }, method: RequestMethods.ResourcesRead, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Contents); @@ -417,7 +419,7 @@ await Can_Handle_Requests( }, method: RequestMethods.PromptsList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result?.Prompts); @@ -446,7 +448,7 @@ await Can_Handle_Requests( }, method: RequestMethods.PromptsGet, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -480,7 +482,7 @@ await Can_Handle_Requests( }, method: RequestMethods.ToolsList, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -515,7 +517,7 @@ await Can_Handle_Requests( }, method: RequestMethods.ToolsCall, configureOptions: null, - assertResult: response => + assertResult: (_, response) => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); @@ -530,7 +532,7 @@ public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_A 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 @@ await transport.SendMessageAsync( 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 override Task SendRequestAsync(JsonRpcRequest request, C 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();