Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -112,6 +113,9 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl
/// <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.");

Expand Down Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion src/ModelContextProtocol.Core/McpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,23 @@ 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>
/// <param name="request">The JSON-RPC request to send.</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>
/// <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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

namespace ModelContextProtocol.Server;

// TODO: Fix merge conflicts in this file.

/// <inheritdoc />
internal sealed partial class McpServerImpl : McpServer
{
Expand All @@ -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;

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

/// <inheritdoc/>
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;

/// <inheritdoc/>
public ServerCapabilities ServerCapabilities { get; } = new();

Expand Down Expand Up @@ -212,6 +214,8 @@ private void ConfigureInitialize(McpServerOptions options)
McpSessionHandler.LatestProtocolVersion;
}

_negotiatedProtocolVersion = protocolVersion;

return new InitializeResult
{
ProtocolVersion = protocolVersion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions tests/ModelContextProtocol.Tests/Client/McpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
27 changes: 15 additions & 12 deletions tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public async Task Create_Should_Initialize_With_Valid_Parameters()

// Assert
Assert.NotNull(server);
Assert.Null(server.NegotiatedProtocolVersion);
}

[Fact]
Expand Down Expand Up @@ -232,7 +233,7 @@ await Can_Handle_Requests(
serverCapabilities: null,
method: RequestMethods.Ping,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
JsonObject jObj = Assert.IsType<JsonObject>(response);
Assert.Empty(jObj);
Expand All @@ -247,13 +248,14 @@ await Can_Handle_Requests(
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);
});
}

Expand All @@ -279,7 +281,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.CompletionComplete,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Completion);
Expand Down Expand Up @@ -316,7 +318,7 @@ await Can_Handle_Requests(
},
RequestMethods.ResourcesTemplatesList,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.ResourceTemplates);
Expand Down Expand Up @@ -345,7 +347,7 @@ await Can_Handle_Requests(
},
RequestMethods.ResourcesList,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Resources);
Expand Down Expand Up @@ -380,7 +382,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.ResourcesRead,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Contents);
Expand Down Expand Up @@ -417,7 +419,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.PromptsList,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result?.Prompts);
Expand Down Expand Up @@ -446,7 +448,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.PromptsGet,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Expand Down Expand Up @@ -480,7 +482,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.ToolsList,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Expand Down Expand Up @@ -515,7 +517,7 @@ await Can_Handle_Requests(
},
method: RequestMethods.ToolsCall,
configureOptions: null,
assertResult: response =>
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Expand All @@ -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<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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -682,6 +684,7 @@ public override Task<JsonRpcResponse> 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();
Expand Down
Loading