Skip to content
Merged
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
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