Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
{
subscriptions.Add(uri);

await ctx.Server.RequestSamplingAsync([
await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
],
Expand Down
2 changes: 1 addition & 1 deletion samples/EverythingServer/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static async Task<string> SampleLLM(
CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await server.RequestSamplingAsync(samplingParams, cancellationToken);
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);

return $"LLM sampling result: {sampleResult.Content.Text}";
}
Expand Down
2 changes: 1 addition & 1 deletion samples/TestServerWithHosting/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static async Task<string> SampleLLM(
CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await thisServer.RequestSamplingAsync(samplingParams, cancellationToken);
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);

return $"LLM sampling result: {sampleResult.Content.Text}";
}
Expand Down
14 changes: 14 additions & 0 deletions src/ModelContextProtocol/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);
}

if (capabilities.Elicitation is { } elicitationCapability)
{
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
{
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
}

RequestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/ModelContextProtocol/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(CompleteResult))]
[JsonSerializable(typeof(CreateMessageRequestParams))]
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(ElicitRequestParams))]
[JsonSerializable(typeof(ElicitResult))]
[JsonSerializable(typeof(EmptyResult))]
[JsonSerializable(typeof(GetPromptRequestParams))]
[JsonSerializable(typeof(GetPromptResult))]
Expand Down
7 changes: 7 additions & 0 deletions src/ModelContextProtocol/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ public class ClientCapabilities
[JsonPropertyName("sampling")]
public SamplingCapability? Sampling { get; set; }

/// <summary>
/// Gets or sets the client's elicitation capability, which indicates whether the client
/// supports elicitation of additional information from the user on behalf of the server.
/// </summary>
[JsonPropertyName("elicitation")]
public ElicitationCapability? Elicitation { get; set; }

/// <summary>Gets or sets notification handlers to register with the client.</summary>
/// <remarks>
/// <para>
Expand Down
230 changes: 230 additions & 0 deletions src/ModelContextProtocol/Protocol/ElicitRequestParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;

/// <summary>
/// Represents a message issued from the server to elicit additional information from the user via the client.
/// </summary>
public class ElicitRequestParams
{
/// <summary>
/// Gets or sets the message to present to the user.
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the requested schema.
/// </summary>
/// <remarks>
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
/// </remarks>
[JsonPropertyName("requestedSchema")]
[field: MaybeNull]
public RequestSchema RequestedSchema
{
get => field ??= new RequestSchema();
set => field = value;
}

/// <summary>Represents a request schema used in an elicitation request.</summary>
public class RequestSchema
{
/// <summary>Gets the type of the schema.</summary>
/// <remarks>This is always "object".</remarks>
[JsonPropertyName("type")]
public string Type => "object";

/// <summary>Gets or sets the properties of the schema.</summary>
[JsonPropertyName("properties")]
[field: MaybeNull]
public IDictionary<string, PrimitiveSchemaDefinition> Properties
{
get => field ??= new Dictionary<string, PrimitiveSchemaDefinition>();
set
{
Throw.IfNull(value);
field = value;
}
}

/// <summary>Gets or sets the required properties of the schema.</summary>
[JsonPropertyName("required")]
public IList<string>? Required { get; set; }
}


/// <summary>
/// Represents restricted subset of JSON Schema:
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
/// </summary>
[JsonDerivedType(typeof(BooleanSchema))]
[JsonDerivedType(typeof(EnumSchema))]
[JsonDerivedType(typeof(NumberSchema))]
[JsonDerivedType(typeof(StringSchema))]
public abstract class PrimitiveSchemaDefinition
{
protected private PrimitiveSchemaDefinition()
{
}
}

/// <summary>Represents a schema for a string type.</summary>
public sealed class StringSchema : PrimitiveSchemaDefinition
{
/// <summary>Gets the type of the schema.</summary>
/// <remarks>This is always "string".</remarks>
[JsonPropertyName("type")]
public string Type => "string";

/// <summary>Gets or sets a title for the string.</summary>
[JsonPropertyName("title")]
public string? Title { get; set; }

/// <summary>Gets or sets a description for the string.</summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>Gets or sets the minimum length for the string.</summary>
[JsonPropertyName("minLength")]
public int? MinLength
{
get => field;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Minimum length cannot be negative.");
}

field = value;
}
}

/// <summary>Gets or sets the maximum length for the string.</summary>
[JsonPropertyName("maxLength")]
public int? MaxLength
{
get => field;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Maximum length cannot be negative.");
}

field = value;
}
}

/// <summary>Gets or sets a specific format for the string ("email", "uri", "date", or "date-time").</summary>
[JsonPropertyName("format")]
public string? Format
{
get => field;
set
{
if (value is not (null or "email" or "uri" or "date" or "date-time"))
{
throw new ArgumentException("Format must be 'email', 'uri', 'date', or 'date-time'.", nameof(value));
}

field = value;
}
}
}

/// <summary>Represents a schema for a number or integer type.</summary>
public sealed class NumberSchema : PrimitiveSchemaDefinition
{
/// <summary>Gets the type of the schema.</summary>
/// <remarks>This should be "number" or "integer".</remarks>
[JsonPropertyName("type")]
[field: MaybeNull]
public string Type
{
get => field ??= "number";
set
{
if (value is not ("number" or "integer"))
{
throw new ArgumentException("Type must be 'number' or 'integer'.", nameof(value));
}

field = value;
}
}

/// <summary>Gets or sets a title for the number input.</summary>
[JsonPropertyName("title")]
public string? Title { get; set; }

/// <summary>Gets or sets a description for the number input.</summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>Gets or sets the minimum allowed value.</summary>
[JsonPropertyName("minimum")]
public double? Minimum { get; set; }

/// <summary>Gets or sets the maximum allowed value.</summary>
[JsonPropertyName("maximum")]
public double? Maximum { get; set; }
}

/// <summary>Represents a schema for a Boolean type.</summary>
public sealed class BooleanSchema : PrimitiveSchemaDefinition
{
/// <summary>Gets the type of the schema.</summary>
/// <remarks>This is always "boolean".</remarks>
[JsonPropertyName("type")]
public string Type => "boolean";

/// <summary>Gets or sets a title for the Boolean.</summary>
[JsonPropertyName("title")]
public string? Title { get; set; }

/// <summary>Gets or sets a description for the Boolean.</summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>Gets or sets the default value for the Boolean.</summary>
[JsonPropertyName("default")]
public bool? Default { get; set; }
}

/// <summary>Represents a schema for an enum type.</summary>
public sealed class EnumSchema : PrimitiveSchemaDefinition
{
/// <summary>Gets the type of the schema.</summary>
/// <remarks>This is always "string".</remarks>
[JsonPropertyName("type")]
public string Type => "string";

/// <summary>Gets or sets a title for the enum.</summary>
[JsonPropertyName("title")]
public string? Title { get; set; }

/// <summary>Gets or sets a description for the enum.</summary>
[JsonPropertyName("description")]
public string? Description { get; set; }

/// <summary>Gets or sets the list of allowed string values for the enum.</summary>
[JsonPropertyName("enum")]
[field: MaybeNull]
public IList<string> Enum
{
get => field ??= [];
set
{
Throw.IfNull(value);
field = value;
}
}

/// <summary>Gets or sets optional display names corresponding to the enum values.</summary>
[JsonPropertyName("enumNames")]
public IList<string>? EnumNames { get; set; }
}
}
41 changes: 41 additions & 0 deletions src/ModelContextProtocol/Protocol/ElicitResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;

/// <summary>
/// Represents the client's response to an elicitation request.
/// </summary>
public class ElicitResult
{
/// <summary>
/// Gets or sets the user action in response to the elicitation.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>
/// <term>"accept"</term>
/// <description>User submitted the form/confirmed the action</description>
/// </item>
/// <item>
/// <term>"decline"</term>
/// <description>User explicitly declined the action</description>
/// </item>
/// <item>
/// <term>"cancel"</term>
/// <description>User dismissed without making an explicit choice</description>
/// </item>
/// </list>
/// </remarks>
[JsonPropertyName("action")]
public string Action { get; set; } = "cancel";

/// <summary>
/// Gets or sets the submitted form data.
/// </summary>
/// <remarks>
/// This is typically omitted if the action is "cancel" or "decline".
/// </remarks>
[JsonPropertyName("content")]
public JsonElement? Content { get; set; }
}
36 changes: 36 additions & 0 deletions src/ModelContextProtocol/Protocol/ElicitationCapability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;

/// <summary>
/// Represents the capability for a client to provide server-requested additional information during interactions.
/// </summary>
/// <remarks>
/// <para>
/// This capability enables the MCP client to respond to elicitation requests from an MCP server.
/// </para>
/// <para>
/// When this capability is enabled, an MCP server can request the client to provide additional information
/// during interactions. The client must set a <see cref="ElicitationHandler"/> to process these requests.
/// </para>
/// </remarks>
public class ElicitationCapability
{
// Currently empty in the spec, but may be extended in the future.

/// <summary>
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
/// </summary>
/// <remarks>
/// <para>
/// This handler function is called when an MCP server requests the client to provide additional
/// information during interactions. The client must set this property for the elicitation capability to work.
/// </para>
/// <para>
/// The handler receives message parameters and a cancellation token.
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
/// </para>
/// </remarks>
[JsonIgnore]
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
}
9 changes: 9 additions & 0 deletions src/ModelContextProtocol/Protocol/RequestMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ public static class RequestMethods
/// </remarks>
public const string SamplingCreateMessage = "sampling/createMessage";

/// <summary>
/// The name of the request method sent from the client to the server to elicit additional information from the user via the client.
/// </summary>
/// <remarks>
/// This request is used when the server needs more information from the client to proceed with a task or interaction.
/// Servers can request structured data from users, with optional JSON schemas to validate responses.
/// </remarks>
public const string ElicitationCreate = "elicitation/create";

/// <summary>
/// The name of the request method sent from the client to the server when it first connects, asking it initialize.
/// </summary>
Expand Down
Loading