Add McpSessions.NegotiatedProtocolVersion property. (#794)

This commit is contained in:
Eirik Tsarpalis 2025-09-19 13:11:01 +03:00 • committed by GitHub
commit 7cfe0ef621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 57 additions and 20 deletions

View file

@ -26,6 +26,7 @@ internal sealed partial class McpClientImpl : McpClient
private ServerCapabilities? _serverCapabilities; private ServerCapabilities? _serverCapabilities;
private Implementation? _serverInfo; private Implementation? _serverInfo;
private string? _serverInstructions; private string? _serverInstructions;
private string? _negotiatedProtocolVersion;
private bool _disposed; private bool _disposed;
@ -112,6 +113,9 @@ internal sealed partial class McpClientImpl : McpClient
/// <inheritdoc/> /// <inheritdoc/>
public override string? SessionId => _transport.SessionId; public override string? SessionId => _transport.SessionId;
/// <inheritdoc/>
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
/// <inheritdoc/> /// <inheritdoc/>
public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); 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}"); throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
} }
_negotiatedProtocolVersion = initializeResponse.ProtocolVersion;
// Send initialized notification // Send initialized notification
await this.SendNotificationAsync( await this.SendNotificationAsync(
NotificationMethods.InitializedNotification, NotificationMethods.InitializedNotification,

View file

@ -38,6 +38,15 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable
/// </remarks> /// </remarks>
public abstract string? SessionId { get; } public abstract string? SessionId { get; }
/// <summary>
/// Gets the negotiated protocol version for the current MCP session.
/// </summary>
/// <remarks>
/// Returns the protocol version negotiated during session initialization,
/// or <see langword="null"/> if initialization hasn't yet occurred.
/// </remarks>
public abstract string? NegotiatedProtocolVersion { get; }
/// <summary> /// <summary>
/// Sends a JSON-RPC request to the connected session and waits for a response. /// Sends a JSON-RPC request to the connected session and waits for a response.
/// </summary> /// </summary>
@ -45,7 +54,7 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task containing the session's response.</returns> /// <returns>A task containing the session's response.</returns>
/// <exception cref="InvalidOperationException">The transport is not connected, or another error occurs during request processing.</exception> /// <exception cref="InvalidOperationException">The transport is not connected, or another error occurs during request processing.</exception>
/// <exception cref="McpException">An error occured during request processing.</exception> /// <exception cref="McpException">An error occurred during request processing.</exception>
/// <remarks> /// <remarks>
/// This method provides low-level access to send raw JSON-RPC requests. For most use cases, /// 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. /// consider using the strongly-typed methods that provide a more convenient API.

View file

@ -6,6 +6,7 @@ namespace ModelContextProtocol.Server;
internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer
{ {
public override string? SessionId => transport?.SessionId ?? server.SessionId; public override string? SessionId => transport?.SessionId ?? server.SessionId;
public override string? NegotiatedProtocolVersion => server.NegotiatedProtocolVersion;
public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities; public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
public override Implementation? ClientInfo => server.ClientInfo; public override Implementation? ClientInfo => server.ClientInfo;
public override McpServerOptions ServerOptions => server.ServerOptions; public override McpServerOptions ServerOptions => server.ServerOptions;

View file

@ -7,8 +7,6 @@ using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Server; namespace ModelContextProtocol.Server;
// TODO: Fix merge conflicts in this file.
/// <inheritdoc /> /// <inheritdoc />
internal sealed partial class McpServerImpl : McpServer internal sealed partial class McpServerImpl : McpServer
{ {
@ -31,6 +29,7 @@ internal sealed partial class McpServerImpl : McpServer
private Implementation? _clientInfo; private Implementation? _clientInfo;
private readonly string _serverOnlyEndpointName; private readonly string _serverOnlyEndpointName;
private string? _negotiatedProtocolVersion;
private string _endpointName; private string _endpointName;
private int _started; private int _started;
@ -116,6 +115,9 @@ internal sealed partial class McpServerImpl : McpServer
/// <inheritdoc/> /// <inheritdoc/>
public override string? SessionId => _sessionTransport.SessionId; public override string? SessionId => _sessionTransport.SessionId;
/// <inheritdoc/>
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
/// <inheritdoc/> /// <inheritdoc/>
public ServerCapabilities ServerCapabilities { get; } = new(); public ServerCapabilities ServerCapabilities { get; } = new();
@ -212,6 +214,8 @@ internal sealed partial class McpServerImpl : McpServer
McpSessionHandler.LatestProtocolVersion; McpSessionHandler.LatestProtocolVersion;
} }
_negotiatedProtocolVersion = protocolVersion;
return new InitializeResult return new InitializeResult
{ {
ProtocolVersion = protocolVersion, ProtocolVersion = protocolVersion,

View file

@ -52,6 +52,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// Assert // Assert
Assert.NotNull(client.ServerCapabilities); Assert.NotNull(client.ServerCapabilities);
Assert.NotNull(client.ServerInfo); Assert.NotNull(client.ServerInfo);
Assert.NotNull(client.NegotiatedProtocolVersion);
if (ClientTransportOptions.Endpoint.AbsolutePath.EndsWith("/sse")) if (ClientTransportOptions.Endpoint.AbsolutePath.EndsWith("/sse"))
{ {

View file

@ -171,13 +171,13 @@ public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpT
await app.StartAsync(TestContext.Current.CancellationToken); 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", 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. // The header should be included in the GET request, the initialized notification, the tools/list call, and the delete request.
Assert.NotEmpty(protocolVersionHeaderValues); Assert.NotEmpty(protocolVersionHeaderValues);

View file

@ -468,4 +468,13 @@ public class McpClientTests : ClientServerTestBase
], ],
data.OrderBy(s => s)); 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);
}
} }

View file

@ -52,8 +52,12 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
// Assert // Assert
Assert.NotNull(client.ServerCapabilities); Assert.NotNull(client.ServerCapabilities);
Assert.NotNull(client.ServerInfo); 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 if (clientId != "everything") // Note: Comment the below assertion back when the everything server is updated to provide instructions
{
Assert.NotNull(client.ServerInstructions); Assert.NotNull(client.ServerInstructions);
}
Assert.Null(client.SessionId); Assert.Null(client.SessionId);
} }

View file

@ -41,6 +41,7 @@ public class McpServerTests : LoggedTest
// Assert // Assert
Assert.NotNull(server); Assert.NotNull(server);
Assert.Null(server.NegotiatedProtocolVersion);
} }
[Fact] [Fact]
@ -232,7 +233,7 @@ public class McpServerTests : LoggedTest
serverCapabilities: null, serverCapabilities: null,
method: RequestMethods.Ping, method: RequestMethods.Ping,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
JsonObject jObj = Assert.IsType<JsonObject>(response); JsonObject jObj = Assert.IsType<JsonObject>(response);
Assert.Empty(jObj); Assert.Empty(jObj);
@ -247,13 +248,14 @@ public class McpServerTests : LoggedTest
serverCapabilities: null, serverCapabilities: null,
method: RequestMethods.Initialize, method: RequestMethods.Initialize,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (server, response) =>
{ {
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name); Assert.Equal(expectedAssemblyName.Name, result.ServerInfo.Name);
Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version); Assert.Equal(expectedAssemblyName.Version?.ToString() ?? "1.0.0", result.ServerInfo.Version);
Assert.Equal("2024", result.ProtocolVersion); Assert.Equal("2024", result.ProtocolVersion);
Assert.Equal("2024", server.NegotiatedProtocolVersion);
}); });
} }
@ -279,7 +281,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.CompletionComplete, method: RequestMethods.CompletionComplete,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion); Assert.NotNull(result?.Completion);
@ -316,7 +318,7 @@ public class McpServerTests : LoggedTest
}, },
RequestMethods.ResourcesTemplatesList, RequestMethods.ResourcesTemplatesList,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.ResourceTemplates); Assert.NotNull(result?.ResourceTemplates);
@ -345,7 +347,7 @@ public class McpServerTests : LoggedTest
}, },
RequestMethods.ResourcesList, RequestMethods.ResourcesList,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Resources); Assert.NotNull(result?.Resources);
@ -380,7 +382,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.ResourcesRead, method: RequestMethods.ResourcesRead,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Contents); Assert.NotNull(result?.Contents);
@ -417,7 +419,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.PromptsList, method: RequestMethods.PromptsList,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Prompts); Assert.NotNull(result?.Prompts);
@ -446,7 +448,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.PromptsGet, method: RequestMethods.PromptsGet,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
@ -480,7 +482,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.ToolsList, method: RequestMethods.ToolsList,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); Assert.NotNull(result);
@ -515,7 +517,7 @@ public class McpServerTests : LoggedTest
}, },
method: RequestMethods.ToolsCall, method: RequestMethods.ToolsCall,
configureOptions: null, configureOptions: null,
assertResult: response => assertResult: (_, response) =>
{ {
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions); var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result); 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"); 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<McpServerOptions>? configureOptions, Action<JsonNode?> assertResult) private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<McpServer, JsonNode?> assertResult)
{ {
await using var transport = new TestServerTransport(); await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities); var options = CreateOptions(serverCapabilities);
@ -559,7 +561,7 @@ public class McpServerTests : LoggedTest
var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5)); var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.NotNull(response); Assert.NotNull(response);
assertResult(response.Result); assertResult(server, response.Result);
await transport.DisposeAsync(); await transport.DisposeAsync();
await runTask; await runTask;
@ -682,6 +684,7 @@ public class McpServerTests : LoggedTest
public override ValueTask DisposeAsync() => default; public override ValueTask DisposeAsync() => default;
public override string? SessionId => throw new NotImplementedException(); public override string? SessionId => throw new NotImplementedException();
public override string? NegotiatedProtocolVersion => throw new NotImplementedException();
public override Implementation? ClientInfo => throw new NotImplementedException(); public override Implementation? ClientInfo => throw new NotImplementedException();
public override IServiceProvider? Services => throw new NotImplementedException(); public override IServiceProvider? Services => throw new NotImplementedException();
public override LoggingLevel? LoggingLevel => throw new NotImplementedException(); public override LoggingLevel? LoggingLevel => throw new NotImplementedException();