diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index 39387f50..988ac992 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -44,4 +44,25 @@ public sealed class ElicitResult : Result /// [JsonPropertyName("content")] public IDictionary? Content { get; set; } + + /// + /// Gets a value indicating whether the user accepted the elicitation request. + /// + /// if the action is "accept"; otherwise, . + [JsonIgnore] + public bool IsAccepted => string.Equals(Action, "accept", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets a value indicating whether the user declined the elicitation request. + /// + /// if the action is "decline"; otherwise, . + [JsonIgnore] + public bool IsDeclined => string.Equals(Action, "decline", StringComparison.OrdinalIgnoreCase); + + /// + /// Gets a value indicating whether the user canceled the elicitation request. + /// + /// if the action is "cancel"; otherwise, . + [JsonIgnore] + public bool IsCanceled => string.Equals(Action, "cancel", StringComparison.OrdinalIgnoreCase); } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index 277ed737..2c194d65 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -30,7 +30,7 @@ public static ValueTask SampleAsync( this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); - ThrowIfSamplingUnsupported(server); + ThrowIfClientSamplingUnsupported(server); return server.SendRequestAsync( RequestMethods.SamplingCreateMessage, @@ -164,7 +164,7 @@ public static async Task SampleAsync( public static IChatClient AsSamplingChatClient(this IMcpServer server) { Throw.IfNull(server); - ThrowIfSamplingUnsupported(server); + ThrowIfClientSamplingUnsupported(server); return new SamplingChatClient(server); } @@ -198,7 +198,7 @@ public static ValueTask RequestRootsAsync( this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); - ThrowIfRootsUnsupported(server); + ThrowIfClientRootsUnsupported(server); return server.SendRequestAsync( RequestMethods.RootsList, @@ -224,7 +224,7 @@ public static ValueTask ElicitAsync( this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); - ThrowIfElicitationUnsupported(server); + ThrowIfClientElicitationUnsupported(server); return server.SendRequestAsync( RequestMethods.ElicitationCreate, @@ -234,7 +234,58 @@ public static ValueTask ElicitAsync( cancellationToken: cancellationToken); } - private static void ThrowIfSamplingUnsupported(IMcpServer server) + /// + /// Determines whether client supports elicitation capability. + /// + /// McpServer instance to check. + /// + /// if client supports elicitation requests; otherwise, . + /// + /// is . + /// + /// When , the server can call to request additional information from the user via the client. + /// + public static bool ClientSupportsElicitation(this IMcpServer server) + { + Throw.IfNull(server); + return server.ClientCapabilities?.Elicitation is not null; + } + + /// + /// Determines whether client supports roots capability. + /// + /// McpServer instance to check. + /// + /// if client supports roots requests; otherwise, . + /// + /// is . + /// + /// When , the server can call to request the list of roots exposed by the client. + /// + public static bool ClientSupportsRoots(this IMcpServer server) + { + Throw.IfNull(server); + return server.ClientCapabilities?.Roots is not null; + } + + /// + /// Determines whether client supports sampling capability. + /// + /// McpServer instance to check. + /// + /// if client supports sampling requests; otherwise, . + /// + /// is . + /// + /// When , the server can call sampling methods to request LLM sampling via the client. + /// + public static bool ClientSupportsSampling(this IMcpServer server) + { + Throw.IfNull(server); + return server.ClientCapabilities?.Sampling is not null; + } + + private static void ThrowIfClientSamplingUnsupported(IMcpServer server) { if (server.ClientCapabilities?.Sampling is null) { @@ -247,7 +298,7 @@ private static void ThrowIfSamplingUnsupported(IMcpServer server) } } - private static void ThrowIfRootsUnsupported(IMcpServer server) + private static void ThrowIfClientRootsUnsupported(IMcpServer server) { if (server.ClientCapabilities?.Roots is null) { @@ -260,7 +311,7 @@ private static void ThrowIfRootsUnsupported(IMcpServer server) } } - private static void ThrowIfElicitationUnsupported(IMcpServer server) + private static void ThrowIfClientElicitationUnsupported(IMcpServer server) { if (server.ClientCapabilities?.Elicitation is null) { diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitResultTests.cs new file mode 100644 index 00000000..283e30dd --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitResultTests.cs @@ -0,0 +1,160 @@ +using System.Text.Json; +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Tests.Protocol; + +public class ElicitResultTests +{ + [Theory] + [InlineData("accept")] + [InlineData("Accept")] + [InlineData("ACCEPT")] + [InlineData("AccEpt")] + public void IsAccepted_Returns_True_For_VariousAcceptedActions(string action) + { + // Arrange + var result = new ElicitResult { Action = action }; + + // Act + var isAccepted = result.IsAccepted; + + // Assert + Assert.True(isAccepted); + } + + [Theory] + [InlineData("decline")] + [InlineData("Decline")] + [InlineData("DECLINE")] + [InlineData("DecLine")] + public void IsDeclined_Returns_True_For_VariousDeclinedActions(string action) + { + // Arrange + var result = new ElicitResult { Action = action }; + + // Act + var isDeclined = result.IsDeclined; + + // Assert + Assert.True(isDeclined); + } + + [Theory] + [InlineData("cancel")] + [InlineData("Cancel")] + [InlineData("CANCEL")] + [InlineData("CanCel")] + public void IsCancelled_Returns_True_For_VariousCancelledActions(string action) + { + // Arrange + var result = new ElicitResult { Action = action }; + + // Act + var isCancelled = result.IsCanceled; + + // Assert + Assert.True(isCancelled); + } + + [Fact] + public void IsAccepted_Returns_False_For_DefaultAction() + { + // Arrange + var result = new ElicitResult(); + + // Act & Assert + Assert.False(result.IsAccepted); + } + + [Fact] + public void IsDeclined_Returns_False_For_DefaultAction() + { + // Arrange + var result = new ElicitResult(); + + // Act & Assert + Assert.False(result.IsDeclined); + } + + [Fact] + public void IsCancelled_Returns_True_For_DefaultAction() + { + // Arrange + var result = new ElicitResult(); + + // Act & Assert + Assert.True(result.IsCanceled); + } + + [Fact] + public void IsAccepted_Returns_False_For_Null_Action() + { + // Arrange + var result = new ElicitResult { Action = null! }; + + // Act & Assert + Assert.False(result.IsAccepted); + } + + [Fact] + public void IsDeclined_Returns_False_For_Null_Action() + { + // Arrange + var result = new ElicitResult { Action = null! }; + + // Act & Assert + Assert.False(result.IsDeclined); + } + + [Fact] + public void IsCancelled_Returns_False_For_Null_Action() + { + // Arrange + var result = new ElicitResult { Action = null! }; + + // Act & Assert + Assert.False(result.IsCanceled); + } + + [Theory] + [InlineData("accept")] + [InlineData("decline")] + [InlineData("cancel")] + [InlineData("unknown")] + public void JsonSerialization_ExcludesJsonIgnoredProperties(string action) + { + // Arrange + var result = new ElicitResult { Action = action }; + + // Act + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.DoesNotContain("IsAccepted", json); + Assert.DoesNotContain("IsDeclined", json); + Assert.DoesNotContain("IsCanceled", json); + Assert.Contains($"\"action\":\"{action}\"", json); + } + + [Theory] + [InlineData("accept", true, false, false)] + [InlineData("decline", false, true, false)] + [InlineData("cancel", false, false, true)] + [InlineData("unknown", false, false, false)] + public void JsonRoundTrip_PreservesActionAndComputedProperties(string action, bool isAccepted, bool isDeclined, bool isCancelled) + { + // Arrange + var result = new ElicitResult { Action = action }; + + // Act + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(action, deserialized.Action); + Assert.Equal(isAccepted, deserialized.IsAccepted); + Assert.Equal(isDeclined, deserialized.IsDeclined); + Assert.Equal(isCancelled, deserialized.IsCanceled); + } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 6750b2ca..150101ad 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -177,6 +177,126 @@ public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_ await Assert.ThrowsAsync(async () => await server.ElicitAsync(new ElicitRequestParams(), CancellationToken.None)); } + [Fact] + public void ClientSupportsElicitation_Should_Throw_ArgumentNullException_If_Server_Is_Null() + { + // Arrange + IMcpServer? server = null; + + // Act & Assert + Assert.Throws(() => server!.ClientSupportsElicitation()); + } + + [Fact] + public async Task ClientSupportsElicitation_Should_Return_False_If_ElicitationCapability_Is_Not_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Elicitation = null }); + + // Act + var clientSupportsElicitation = server.ClientSupportsElicitation(); + + // Assert + Assert.False(clientSupportsElicitation); + } + + [Fact] + public async Task ClientSupportsElicitation_Should_Return_True_If_ElicitationCapability_Is_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Elicitation = new ElicitationCapability() }); + + // Act + var clientSupportsElicitation = server.ClientSupportsElicitation(); + + // Assert + Assert.True(clientSupportsElicitation); + } + + [Fact] + public void ClientSupportsRoots_Should_Throw_ArgumentNullException_If_Server_Is_Null() + { + // Arrange + IMcpServer? server = null; + + // Act & Assert + Assert.Throws(() => server!.ClientSupportsRoots()); + } + + [Fact] + public async Task ClientSupportsRoots_Should_Return_False_If_RootsCapability_Is_Not_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Roots = null }); + + // Act + var clientSupportsRoots = server.ClientSupportsRoots(); + + // Assert + Assert.False(clientSupportsRoots); + } + + [Fact] + public async Task ClientSupportsRoots_Should_Return_True_If_RootsCapability_Is_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() }); + + // Act + var clientSupportsRoots = server.ClientSupportsRoots(); + + // Assert + Assert.True(clientSupportsRoots); + } + + [Fact] + public void ClientSupportsSampling_Should_Throw_ArgumentNullException_If_Server_Is_Null() + { + // Arrange + IMcpServer? server = null; + + // Act & Assert + Assert.Throws(() => server!.ClientSupportsSampling()); + } + + [Fact] + public async Task ClientSupportsSampling_Should_Return_False_If_SamplingCapability_Is_Not_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Sampling = null }); + + // Act + var clientSupportsSampling = server.ClientSupportsSampling(); + + // Assert + Assert.False(clientSupportsSampling); + } + + [Fact] + public async Task ClientSupportsSampling_Should_Return_True_If_SamplingCapability_Is_Set() + { + // Arrange + await using var transport = new TestServerTransport(); + await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + SetClientCapabilities(server, new ClientCapabilities { Sampling = new SamplingCapability() }); + + // Act + var clientSupportsSampling = server.ClientSupportsSampling(); + + // Assert + Assert.True(clientSupportsSampling); + } + [Fact] public async Task ElicitAsync_Should_SendRequest() {