Skip to content

Introduce acceptance helpers to ElicitResult and client capability checks on IMcpServer #666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 21 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/ElicitResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,25 @@ public sealed class ElicitResult : Result
/// </remarks>
[JsonPropertyName("content")]
public IDictionary<string, JsonElement>? Content { get; set; }

/// <summary>
/// Gets a value indicating whether the user accepted the elicitation request.
/// </summary>
/// <returns><see langword="true"/> if the action is "accept"; otherwise, <see langword="false"/>.</returns>
[JsonIgnore]
public bool IsAccepted => string.Equals(Action, "accept", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Gets a value indicating whether the user declined the elicitation request.
/// </summary>
/// <returns><see langword="true"/> if the action is "decline"; otherwise, <see langword="false"/>.</returns>
[JsonIgnore]
public bool IsDeclined => string.Equals(Action, "decline", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Gets a value indicating whether the user canceled the elicitation request.
/// </summary>
/// <returns><see langword="true"/> if the action is "cancel"; otherwise, <see langword="false"/>.</returns>
[JsonIgnore]
public bool IsCanceled => string.Equals(Action, "cancel", StringComparison.OrdinalIgnoreCase);
}
65 changes: 58 additions & 7 deletions src/ModelContextProtocol.Core/Server/McpServerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static ValueTask<CreateMessageResult> SampleAsync(
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default)
{
Throw.IfNull(server);
ThrowIfSamplingUnsupported(server);
ThrowIfClientSamplingUnsupported(server);

return server.SendRequestAsync(
RequestMethods.SamplingCreateMessage,
Expand Down Expand Up @@ -164,7 +164,7 @@ public static async Task<ChatResponse> SampleAsync(
public static IChatClient AsSamplingChatClient(this IMcpServer server)
{
Throw.IfNull(server);
ThrowIfSamplingUnsupported(server);
ThrowIfClientSamplingUnsupported(server);

return new SamplingChatClient(server);
}
Expand Down Expand Up @@ -198,7 +198,7 @@ public static ValueTask<ListRootsResult> RequestRootsAsync(
this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default)
{
Throw.IfNull(server);
ThrowIfRootsUnsupported(server);
ThrowIfClientRootsUnsupported(server);

return server.SendRequestAsync(
RequestMethods.RootsList,
Expand All @@ -224,7 +224,7 @@ public static ValueTask<ElicitResult> ElicitAsync(
this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default)
{
Throw.IfNull(server);
ThrowIfElicitationUnsupported(server);
ThrowIfClientElicitationUnsupported(server);

return server.SendRequestAsync(
RequestMethods.ElicitationCreate,
Expand All @@ -234,7 +234,58 @@ public static ValueTask<ElicitResult> ElicitAsync(
cancellationToken: cancellationToken);
}

private static void ThrowIfSamplingUnsupported(IMcpServer server)
/// <summary>
/// Determines whether client supports elicitation capability.
/// </summary>
/// <param name="server">McpServer instance to check.</param>
/// <returns>
/// <see langword="true"/> if client supports elicitation requests; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <remarks>
/// When <see langword="true"/>, the server can call <see cref="McpServerExtensions.ElicitAsync"/> to request additional information from the user via the client.
/// </remarks>
public static bool ClientSupportsElicitation(this IMcpServer server)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These would be better as properties rather than as methods. Should we start thinking about using C# 14?

Copy link
Author

@dogukandemir dogukandemir Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you're referring to extension members, right? if that's the case, I suggest we merge this PR as-is and then I can work on a follow up PR to convert all extension methods in whole solution to extension members for consistency.

{
Throw.IfNull(server);
return server.ClientCapabilities?.Elicitation is not null;
}

/// <summary>
/// Determines whether client supports roots capability.
/// </summary>
/// <param name="server">McpServer instance to check.</param>
/// <returns>
/// <see langword="true"/> if client supports roots requests; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <remarks>
/// When <see langword="true"/>, the server can call <see cref="McpServerExtensions.RequestRootsAsync"/> to request the list of roots exposed by the client.
/// </remarks>
public static bool ClientSupportsRoots(this IMcpServer server)
{
Throw.IfNull(server);
return server.ClientCapabilities?.Roots is not null;
}

/// <summary>
/// Determines whether client supports sampling capability.
/// </summary>
/// <param name="server">McpServer instance to check.</param>
/// <returns>
/// <see langword="true"/> if client supports sampling requests; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <remarks>
/// When <see langword="true"/>, the server can call sampling methods to request LLM sampling via the client.
/// </remarks>
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)
{
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand Down
160 changes: 160 additions & 0 deletions tests/ModelContextProtocol.Tests/Protocol/ElicitResultTests.cs
Original file line number Diff line number Diff line change
@@ -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<ElicitResult>(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);
}
}
Loading