Skip to content

Commit 5cc7c00

Browse files
authored
Add elicitation capability (#467)
* Add elicitation capability
1 parent 36a70a0 commit 5cc7c00

File tree

16 files changed

+480
-22
lines changed

16 files changed

+480
-22
lines changed

samples/EverythingServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
{
4747
subscriptions.Add(uri);
4848

49-
await ctx.Server.RequestSamplingAsync([
49+
await ctx.Server.SampleAsync([
5050
new ChatMessage(ChatRole.System, "You are a helpful test server"),
5151
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
5252
],

samples/EverythingServer/Tools/SampleLlmTool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static async Task<string> SampleLLM(
1515
CancellationToken cancellationToken)
1616
{
1717
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
18-
var sampleResult = await server.RequestSamplingAsync(samplingParams, cancellationToken);
18+
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);
1919

2020
return $"LLM sampling result: {sampleResult.Content.Text}";
2121
}

samples/TestServerWithHosting/Tools/SampleLlmTool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static async Task<string> SampleLLM(
1818
CancellationToken cancellationToken)
1919
{
2020
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
21-
var sampleResult = await thisServer.RequestSamplingAsync(samplingParams, cancellationToken);
21+
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);
2222

2323
return $"LLM sampling result: {sampleResult.Content.Text}";
2424
}

src/ModelContextProtocol/Client/McpClient.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL
7676
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
7777
McpJsonUtilities.JsonContext.Default.ListRootsResult);
7878
}
79+
80+
if (capabilities.Elicitation is { } elicitationCapability)
81+
{
82+
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
83+
{
84+
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
85+
}
86+
87+
RequestHandlers.Set(
88+
RequestMethods.ElicitationCreate,
89+
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
90+
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
91+
McpJsonUtilities.JsonContext.Default.ElicitResult);
92+
}
7993
}
8094
}
8195

src/ModelContextProtocol/McpJsonUtilities.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
9696
[JsonSerializable(typeof(CompleteResult))]
9797
[JsonSerializable(typeof(CreateMessageRequestParams))]
9898
[JsonSerializable(typeof(CreateMessageResult))]
99+
[JsonSerializable(typeof(ElicitRequestParams))]
100+
[JsonSerializable(typeof(ElicitResult))]
99101
[JsonSerializable(typeof(EmptyResult))]
100102
[JsonSerializable(typeof(GetPromptRequestParams))]
101103
[JsonSerializable(typeof(GetPromptResult))]

src/ModelContextProtocol/Protocol/ClientCapabilities.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ public class ClientCapabilities
5858
[JsonPropertyName("sampling")]
5959
public SamplingCapability? Sampling { get; set; }
6060

61+
/// <summary>
62+
/// Gets or sets the client's elicitation capability, which indicates whether the client
63+
/// supports elicitation of additional information from the user on behalf of the server.
64+
/// </summary>
65+
[JsonPropertyName("elicitation")]
66+
public ElicitationCapability? Elicitation { get; set; }
67+
6168
/// <summary>Gets or sets notification handlers to register with the client.</summary>
6269
/// <remarks>
6370
/// <para>
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.Json.Serialization;
3+
4+
namespace ModelContextProtocol.Protocol;
5+
6+
/// <summary>
7+
/// Represents a message issued from the server to elicit additional information from the user via the client.
8+
/// </summary>
9+
public class ElicitRequestParams
10+
{
11+
/// <summary>
12+
/// Gets or sets the message to present to the user.
13+
/// </summary>
14+
[JsonPropertyName("message")]
15+
public string Message { get; set; } = string.Empty;
16+
17+
/// <summary>
18+
/// Gets or sets the requested schema.
19+
/// </summary>
20+
/// <remarks>
21+
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
22+
/// </remarks>
23+
[JsonPropertyName("requestedSchema")]
24+
[field: MaybeNull]
25+
public RequestSchema RequestedSchema
26+
{
27+
get => field ??= new RequestSchema();
28+
set => field = value;
29+
}
30+
31+
/// <summary>Represents a request schema used in an elicitation request.</summary>
32+
public class RequestSchema
33+
{
34+
/// <summary>Gets the type of the schema.</summary>
35+
/// <remarks>This is always "object".</remarks>
36+
[JsonPropertyName("type")]
37+
public string Type => "object";
38+
39+
/// <summary>Gets or sets the properties of the schema.</summary>
40+
[JsonPropertyName("properties")]
41+
[field: MaybeNull]
42+
public IDictionary<string, PrimitiveSchemaDefinition> Properties
43+
{
44+
get => field ??= new Dictionary<string, PrimitiveSchemaDefinition>();
45+
set
46+
{
47+
Throw.IfNull(value);
48+
field = value;
49+
}
50+
}
51+
52+
/// <summary>Gets or sets the required properties of the schema.</summary>
53+
[JsonPropertyName("required")]
54+
public IList<string>? Required { get; set; }
55+
}
56+
57+
58+
/// <summary>
59+
/// Represents restricted subset of JSON Schema:
60+
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
61+
/// </summary>
62+
[JsonDerivedType(typeof(BooleanSchema))]
63+
[JsonDerivedType(typeof(EnumSchema))]
64+
[JsonDerivedType(typeof(NumberSchema))]
65+
[JsonDerivedType(typeof(StringSchema))]
66+
public abstract class PrimitiveSchemaDefinition
67+
{
68+
protected private PrimitiveSchemaDefinition()
69+
{
70+
}
71+
}
72+
73+
/// <summary>Represents a schema for a string type.</summary>
74+
public sealed class StringSchema : PrimitiveSchemaDefinition
75+
{
76+
/// <summary>Gets the type of the schema.</summary>
77+
/// <remarks>This is always "string".</remarks>
78+
[JsonPropertyName("type")]
79+
public string Type => "string";
80+
81+
/// <summary>Gets or sets a title for the string.</summary>
82+
[JsonPropertyName("title")]
83+
public string? Title { get; set; }
84+
85+
/// <summary>Gets or sets a description for the string.</summary>
86+
[JsonPropertyName("description")]
87+
public string? Description { get; set; }
88+
89+
/// <summary>Gets or sets the minimum length for the string.</summary>
90+
[JsonPropertyName("minLength")]
91+
public int? MinLength
92+
{
93+
get => field;
94+
set
95+
{
96+
if (value < 0)
97+
{
98+
throw new ArgumentOutOfRangeException(nameof(value), "Minimum length cannot be negative.");
99+
}
100+
101+
field = value;
102+
}
103+
}
104+
105+
/// <summary>Gets or sets the maximum length for the string.</summary>
106+
[JsonPropertyName("maxLength")]
107+
public int? MaxLength
108+
{
109+
get => field;
110+
set
111+
{
112+
if (value < 0)
113+
{
114+
throw new ArgumentOutOfRangeException(nameof(value), "Maximum length cannot be negative.");
115+
}
116+
117+
field = value;
118+
}
119+
}
120+
121+
/// <summary>Gets or sets a specific format for the string ("email", "uri", "date", or "date-time").</summary>
122+
[JsonPropertyName("format")]
123+
public string? Format
124+
{
125+
get => field;
126+
set
127+
{
128+
if (value is not (null or "email" or "uri" or "date" or "date-time"))
129+
{
130+
throw new ArgumentException("Format must be 'email', 'uri', 'date', or 'date-time'.", nameof(value));
131+
}
132+
133+
field = value;
134+
}
135+
}
136+
}
137+
138+
/// <summary>Represents a schema for a number or integer type.</summary>
139+
public sealed class NumberSchema : PrimitiveSchemaDefinition
140+
{
141+
/// <summary>Gets the type of the schema.</summary>
142+
/// <remarks>This should be "number" or "integer".</remarks>
143+
[JsonPropertyName("type")]
144+
[field: MaybeNull]
145+
public string Type
146+
{
147+
get => field ??= "number";
148+
set
149+
{
150+
if (value is not ("number" or "integer"))
151+
{
152+
throw new ArgumentException("Type must be 'number' or 'integer'.", nameof(value));
153+
}
154+
155+
field = value;
156+
}
157+
}
158+
159+
/// <summary>Gets or sets a title for the number input.</summary>
160+
[JsonPropertyName("title")]
161+
public string? Title { get; set; }
162+
163+
/// <summary>Gets or sets a description for the number input.</summary>
164+
[JsonPropertyName("description")]
165+
public string? Description { get; set; }
166+
167+
/// <summary>Gets or sets the minimum allowed value.</summary>
168+
[JsonPropertyName("minimum")]
169+
public double? Minimum { get; set; }
170+
171+
/// <summary>Gets or sets the maximum allowed value.</summary>
172+
[JsonPropertyName("maximum")]
173+
public double? Maximum { get; set; }
174+
}
175+
176+
/// <summary>Represents a schema for a Boolean type.</summary>
177+
public sealed class BooleanSchema : PrimitiveSchemaDefinition
178+
{
179+
/// <summary>Gets the type of the schema.</summary>
180+
/// <remarks>This is always "boolean".</remarks>
181+
[JsonPropertyName("type")]
182+
public string Type => "boolean";
183+
184+
/// <summary>Gets or sets a title for the Boolean.</summary>
185+
[JsonPropertyName("title")]
186+
public string? Title { get; set; }
187+
188+
/// <summary>Gets or sets a description for the Boolean.</summary>
189+
[JsonPropertyName("description")]
190+
public string? Description { get; set; }
191+
192+
/// <summary>Gets or sets the default value for the Boolean.</summary>
193+
[JsonPropertyName("default")]
194+
public bool? Default { get; set; }
195+
}
196+
197+
/// <summary>Represents a schema for an enum type.</summary>
198+
public sealed class EnumSchema : PrimitiveSchemaDefinition
199+
{
200+
/// <summary>Gets the type of the schema.</summary>
201+
/// <remarks>This is always "string".</remarks>
202+
[JsonPropertyName("type")]
203+
public string Type => "string";
204+
205+
/// <summary>Gets or sets a title for the enum.</summary>
206+
[JsonPropertyName("title")]
207+
public string? Title { get; set; }
208+
209+
/// <summary>Gets or sets a description for the enum.</summary>
210+
[JsonPropertyName("description")]
211+
public string? Description { get; set; }
212+
213+
/// <summary>Gets or sets the list of allowed string values for the enum.</summary>
214+
[JsonPropertyName("enum")]
215+
[field: MaybeNull]
216+
public IList<string> Enum
217+
{
218+
get => field ??= [];
219+
set
220+
{
221+
Throw.IfNull(value);
222+
field = value;
223+
}
224+
}
225+
226+
/// <summary>Gets or sets optional display names corresponding to the enum values.</summary>
227+
[JsonPropertyName("enumNames")]
228+
public IList<string>? EnumNames { get; set; }
229+
}
230+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace ModelContextProtocol.Protocol;
5+
6+
/// <summary>
7+
/// Represents the client's response to an elicitation request.
8+
/// </summary>
9+
public class ElicitResult
10+
{
11+
/// <summary>
12+
/// Gets or sets the user action in response to the elicitation.
13+
/// </summary>
14+
/// <remarks>
15+
/// <list type="bullet">
16+
/// <item>
17+
/// <term>"accept"</term>
18+
/// <description>User submitted the form/confirmed the action</description>
19+
/// </item>
20+
/// <item>
21+
/// <term>"decline"</term>
22+
/// <description>User explicitly declined the action</description>
23+
/// </item>
24+
/// <item>
25+
/// <term>"cancel"</term>
26+
/// <description>User dismissed without making an explicit choice</description>
27+
/// </item>
28+
/// </list>
29+
/// </remarks>
30+
[JsonPropertyName("action")]
31+
public string Action { get; set; } = "cancel";
32+
33+
/// <summary>
34+
/// Gets or sets the submitted form data.
35+
/// </summary>
36+
/// <remarks>
37+
/// This is typically omitted if the action is "cancel" or "decline".
38+
/// </remarks>
39+
[JsonPropertyName("content")]
40+
public JsonElement? Content { get; set; }
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol;
4+
5+
/// <summary>
6+
/// Represents the capability for a client to provide server-requested additional information during interactions.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// This capability enables the MCP client to respond to elicitation requests from an MCP server.
11+
/// </para>
12+
/// <para>
13+
/// When this capability is enabled, an MCP server can request the client to provide additional information
14+
/// during interactions. The client must set a <see cref="ElicitationHandler"/> to process these requests.
15+
/// </para>
16+
/// </remarks>
17+
public class ElicitationCapability
18+
{
19+
// Currently empty in the spec, but may be extended in the future.
20+
21+
/// <summary>
22+
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
23+
/// </summary>
24+
/// <remarks>
25+
/// <para>
26+
/// This handler function is called when an MCP server requests the client to provide additional
27+
/// information during interactions. The client must set this property for the elicitation capability to work.
28+
/// </para>
29+
/// <para>
30+
/// The handler receives message parameters and a cancellation token.
31+
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
32+
/// </para>
33+
/// </remarks>
34+
[JsonIgnore]
35+
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
36+
}

src/ModelContextProtocol/Protocol/RequestMethods.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ public static class RequestMethods
9393
/// </remarks>
9494
public const string SamplingCreateMessage = "sampling/createMessage";
9595

96+
/// <summary>
97+
/// The name of the request method sent from the client to the server to elicit additional information from the user via the client.
98+
/// </summary>
99+
/// <remarks>
100+
/// This request is used when the server needs more information from the client to proceed with a task or interaction.
101+
/// Servers can request structured data from users, with optional JSON schemas to validate responses.
102+
/// </remarks>
103+
public const string ElicitationCreate = "elicitation/create";
104+
96105
/// <summary>
97106
/// The name of the request method sent from the client to the server when it first connects, asking it initialize.
98107
/// </summary>

0 commit comments

Comments
 (0)