-
Notifications
You must be signed in to change notification settings - Fork 472
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
c595228
4f99673
68fe047
9c6314f
e0ee1be
b46ee63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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); | ||
} | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
{ | ||
|
@@ -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) | ||
{ | ||
|
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.IsCancelled; | ||
|
||
// 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.IsCancelled); | ||
} | ||
|
||
[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.IsCancelled); | ||
} | ||
|
||
[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("IsCancelled", 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.IsCancelled); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.