Skip to content

Commit fa0dc6a

Browse files
committed
Elicitation extension methods to improve code readability and reduce potential string comparison errors
1 parent 5e5b1af commit fa0dc6a

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using ModelContextProtocol.Protocol;
2+
3+
namespace ModelContextProtocol.ExtensionMethods;
4+
5+
/// <summary>
6+
/// Provides extension methods for interacting with an <see cref="ElicitResult"/> instance.
7+
/// </summary>
8+
public static class ElicitResultExtensions
9+
{
10+
/// <summary>
11+
/// Determines whether given <see cref="ElicitResult"/> represents an accepted action.
12+
/// </summary>
13+
/// <param name="result">Elicit result to check.</param>
14+
/// <returns><see langword="true"/> if the action is "accept"; otherwise, <see langword="false"/>.</returns>
15+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="result"/> is <see langword="null"/>.</exception>
16+
public static bool IsAccepted(this ElicitResult result)
17+
{
18+
Throw.IfNull(result);
19+
return string.Equals(result.Action, "accept", StringComparison.OrdinalIgnoreCase);
20+
}
21+
22+
/// <summary>
23+
/// Determines whether given <see cref="ElicitResult"/> represents a declined action.
24+
/// </summary>
25+
/// <param name="result">Elicit result to check.</param>
26+
/// <returns><see langword="true"/> if the action is "decline"; otherwise, <see langword="false"/>.</returns>
27+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="result"/> is <see langword="null"/>.</exception>
28+
public static bool IsDeclined(this ElicitResult result)
29+
{
30+
Throw.IfNull(result);
31+
return string.Equals(result.Action, "decline", StringComparison.OrdinalIgnoreCase);
32+
}
33+
34+
/// <summary>
35+
/// Determines whether given <see cref="ElicitResult"/> represents a cancelled action.
36+
/// </summary>
37+
/// <param name="result">Elicit result to check.</param>
38+
/// <returns><see langword="true"/> if the action is "cancel"; otherwise, <see langword="false"/>.</returns>
39+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="result"/> is <see langword="null"/>.</exception>
40+
public static bool IsCancelled(this ElicitResult result)
41+
{
42+
Throw.IfNull(result);
43+
return string.Equals(result.Action, "cancel", StringComparison.OrdinalIgnoreCase);
44+
}
45+
}

src/ModelContextProtocol.Core/Server/McpServerExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,23 @@ public static ValueTask<ElicitResult> ElicitAsync(
234234
cancellationToken: cancellationToken);
235235
}
236236

237+
/// <summary>
238+
/// Determines whether client supports elicitation capability.
239+
/// </summary>
240+
/// <param name="server">McpServer instance to check.</param>
241+
/// <returns>
242+
/// <see langword="true"/> if client supports elicitation requests; otherwise, <see langword="false"/>.
243+
/// </returns>
244+
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
245+
/// <remarks>
246+
/// When <see langword="true"/>, the server can call <see cref="McpServerExtensions.ElicitAsync"/> to request additional information from the user via the client.
247+
/// </remarks>
248+
public static bool SupportsElicitation(this IMcpServer server)
249+
{
250+
Throw.IfNull(server);
251+
return server.ClientCapabilities?.Elicitation is not null;
252+
}
253+
237254
private static void ThrowIfSamplingUnsupported(IMcpServer server)
238255
{
239256
if (server.ClientCapabilities?.Sampling is null)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using ModelContextProtocol.ExtensionMethods;
2+
using ModelContextProtocol.Protocol;
3+
4+
namespace ModelContextProtocol.Tests.ExtensionMethods;
5+
6+
public class ElicitResultExtensionsTests
7+
{
8+
[Fact]
9+
public void IsAccepted_Throws_ArgumentNullException_If_Result_Is_Null()
10+
{
11+
// Arrange
12+
ElicitResult? result = null;
13+
14+
// Act & Assert
15+
Assert.Throws<ArgumentNullException>(() => result!.IsAccepted());
16+
}
17+
18+
[Fact]
19+
public void IsDeclined_Throws_ArgumentNullException_If_Result_Is_Null()
20+
{
21+
// Arrange
22+
ElicitResult? result = null;
23+
24+
// Act & Assert
25+
Assert.Throws<ArgumentNullException>(() => result!.IsDeclined());
26+
}
27+
28+
[Fact]
29+
public void IsCancelled_Throws_ArgumentNullException_If_Result_Is_Null()
30+
{
31+
// Arrange
32+
ElicitResult? result = null;
33+
34+
// Act & Assert
35+
Assert.Throws<ArgumentNullException>(() => result!.IsCancelled());
36+
}
37+
38+
[Theory]
39+
[InlineData("accept")]
40+
[InlineData("ACCEPT")]
41+
[InlineData("Accept")]
42+
[InlineData("AccEpt")]
43+
public void IsAccepted_Returns_True_For_VariousActions(string action)
44+
{
45+
// Arrange
46+
var result = new ElicitResult { Action = action };
47+
48+
// Act & Assert
49+
Assert.True(result.IsAccepted());
50+
}
51+
52+
[Theory]
53+
[InlineData("decline")]
54+
[InlineData("cancel")]
55+
[InlineData("unknown")]
56+
public void IsAccepted_Returns_False_For_NonAcceptedActions(string action)
57+
{
58+
// Arrange
59+
var result = new ElicitResult { Action = action };
60+
61+
// Act & Assert
62+
Assert.False(result.IsAccepted());
63+
}
64+
65+
[Theory]
66+
[InlineData("accept")]
67+
[InlineData("cancel")]
68+
[InlineData("unknown")]
69+
public void IsDeclined_Returns_False_For_NonDeclinedActions(string action)
70+
{
71+
// Arrange
72+
var result = new ElicitResult { Action = action };
73+
74+
// Act & Assert
75+
Assert.False(result.IsDeclined());
76+
}
77+
78+
[Theory]
79+
[InlineData("accept")]
80+
[InlineData("decline")]
81+
[InlineData("unknown")]
82+
public void IsCancelled_Returns_False_For_NonCancelledActions(string action)
83+
{
84+
// Arrange
85+
var result = new ElicitResult { Action = action };
86+
87+
// Act & Assert
88+
Assert.False(result.IsCancelled());
89+
}
90+
91+
[Theory]
92+
[InlineData("decline")]
93+
[InlineData("DECLINE")]
94+
[InlineData("Decline")]
95+
[InlineData("DecLine")]
96+
public void IsDeclined_Returns_True_For_VariousActions(string action)
97+
{
98+
// Arrange
99+
var result = new ElicitResult { Action = action };
100+
101+
// Act & Assert
102+
Assert.True(result.IsDeclined());
103+
}
104+
105+
[Theory]
106+
[InlineData("cancel")]
107+
[InlineData("CANCEL")]
108+
[InlineData("Cancel")]
109+
[InlineData("CanCel")]
110+
public void IsCancelled_Returns_True_For_VariousActions(string action)
111+
{
112+
// Arrange
113+
var result = new ElicitResult { Action = action };
114+
115+
// Act & Assert
116+
Assert.True(result.IsCancelled());
117+
}
118+
}

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,31 @@ public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_
177177
await Assert.ThrowsAsync<InvalidOperationException>(async () => await server.ElicitAsync(new ElicitRequestParams(), CancellationToken.None));
178178
}
179179

180+
[Fact]
181+
public void SupportsElicitation_Should_Throw_ArgumentNullException_If_Server_Is_Null()
182+
{
183+
// Arrange
184+
IMcpServer? server = null;
185+
186+
// Act & Assert
187+
Assert.Throws<ArgumentNullException>(() => server!.SupportsElicitation());
188+
}
189+
190+
[Fact]
191+
public async Task SupportsElicitation_Should_Return_False_If_ElicitationCapability_Is_Not_Set()
192+
{
193+
// Arrange
194+
await using var transport = new TestServerTransport();
195+
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory);
196+
SetClientCapabilities(server, new ClientCapabilities { Elicitation = null });
197+
198+
// Act
199+
var supportsElicitation = server.SupportsElicitation();
200+
201+
// Assert
202+
Assert.False(supportsElicitation);
203+
}
204+
180205
[Fact]
181206
public async Task ElicitAsync_Should_SendRequest()
182207
{

0 commit comments

Comments
 (0)