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 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,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue