Add McpSessions.NegotiatedProtocolVersion property. (#794)
This commit is contained in:
parent
38b4a26998
commit
7cfe0ef621
9 changed files with 57 additions and 20 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue