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 Implementation? _serverInfo;
private string? _serverInstructions;
private string? _negotiatedProtocolVersion;
private bool _disposed;
@ -112,6 +113,9 @@ internal sealed partial class McpClientImpl : McpClient
/// <inheritdoc/>
public override string? SessionId => _transport.SessionId;
/// <inheritdoc/>
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
/// <inheritdoc/>
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,

View file

@ -38,6 +38,15 @@ public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable
/// </remarks>
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>
/// Sends a JSON-RPC request to the connected session and waits for a response.
/// </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>
/// <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="McpException">An error occured during request processing.</exception>
/// <exception cref="McpException">An error occurred during request processing.</exception>
/// <remarks>
/// 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.

View file

@ -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;

View file

@ -7,8 +7,6 @@ using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Server;
// TODO: Fix merge conflicts in this file.
/// <inheritdoc />
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
/// <inheritdoc/>
public override string? SessionId => _sessionTransport.SessionId;
/// <inheritdoc/>
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
/// <inheritdoc/>
public ServerCapabilities ServerCapabilities { get; } = new();
@ -212,6 +214,8 @@ internal sealed partial class McpServerImpl : McpServer
McpSessionHandler.LatestProtocolVersion;
}
_negotiatedProtocolVersion = protocolVersion;
return new InitializeResult
{
ProtocolVersion = protocolVersion,

View file

@ -52,6 +52,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// Assert
Assert.NotNull(client.ServerCapabilities);
Assert.NotNull(client.ServerInfo);
Assert.NotNull(client.NegotiatedProtocolVersion);
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 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);

View file

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

View file

@ -41,6 +41,7 @@ public class McpServerTests : LoggedTest
// Assert
Assert.NotNull(server);
Assert.Null(server.NegotiatedProtocolVersion);
}
[Fact]
@ -232,7 +233,7 @@ public class McpServerTests : LoggedTest
serverCapabilities: null,
method: RequestMethods.Ping,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
JsonObject jObj = Assert.IsType<JsonObject>(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<InitializeResult>(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<CompleteResult>(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<ListResourceTemplatesResult>(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<ListResourcesResult>(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<ReadResourceResult>(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<ListPromptsResult>(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<GetPromptResult>(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<ListToolsResult>(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<CallToolResult>(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<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();
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();