Add support for URL mode elicitation (#1021)

This commit is contained in:
Stephen Halter 2025-12-05 00:03:16 -08:00 • committed by GitHub
commit ae17ba0b4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1241 additions and 92 deletions

View file

@ -9,17 +9,79 @@ namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents a message issued from the server to elicit additional information from the user via the client.
/// </summary>
public sealed class ElicitRequestParams
public sealed class ElicitRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the elicitation mode: "form" for in-band data collection or "url" for out-of-band URL navigation.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><description><b>form</b>: Client collects structured data via a form interface. Data is exposed to the client.</description></item>
/// <item><description><b>url</b>: Client navigates user to a URL for out-of-band interaction. Sensitive data is not exposed to the client.</description></item>
/// </list>
/// </remarks>
[JsonPropertyName("mode")]
[field: MaybeNull]
public string Mode
{
get => field ??= "form";
set
{
if (value is not ("form" or "url"))
{
throw new ArgumentException("Mode must be 'form' or 'url'.", nameof(value));
}
field = value;
}
}
/// <summary>
/// Gets or sets a unique identifier for this elicitation request.
/// </summary>
/// <remarks>
/// <para>
/// Used to track and correlate the elicitation across multiple messages, especially for out-of-band flows
/// that may complete asynchronously.
/// </para>
/// <para>
/// Required for url mode elicitation to enable progress tracking and completion detection.
/// </para>
/// </remarks>
[JsonPropertyName("elicitationId")]
public string? ElicitationId { get; set; }
/// <summary>
/// Gets or sets the URL to navigate to for out-of-band elicitation.
/// </summary>
/// <remarks>
/// <para>
/// Required when <see cref="Mode"/> is "url". The client should prompt the user for consent
/// and then navigate to this URL in a user-agent (browser) where the user completes
/// the required interaction.
/// </para>
/// <para>
/// URLs must not appear in any other field of the elicitation request for security reasons.
/// </para>
/// </remarks>
[JsonPropertyName("url")]
public string? Url { get; set; }
/// <summary>
/// Gets or sets the message to present to the user.
/// </summary>
/// <remarks>
/// For form mode, this describes what information is being requested.
/// For url mode, this explains why the user needs to navigate to the URL.
/// </remarks>
[JsonPropertyName("message")]
public required string Message { get; set; }
/// <summary>
/// Gets or sets the requested schema.
/// Gets or sets the requested schema for form mode elicitation.
/// </summary>
/// <remarks>
/// Only applicable when <see cref="Mode"/> is "form".
/// </remarks>
/// <value>
/// Possible values are <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
@ -27,14 +89,9 @@ public sealed class ElicitRequestParams
/// and <see cref="LegacyTitledEnumSchema"/> (deprecated).
/// </value>
[JsonPropertyName("requestedSchema")]
[field: MaybeNull]
public RequestSchema RequestedSchema
{
get => field ??= new RequestSchema();
set => field = value;
}
public RequestSchema? RequestedSchema { get; set; }
/// <summary>Represents a request schema used in an elicitation request.</summary>
/// <summary>Represents a request schema used in a form mode elicitation request.</summary>
public class RequestSchema
{
/// <summary>Gets the type of the schema.</summary>

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using ModelContextProtocol.Client;
@ -10,17 +11,117 @@ namespace ModelContextProtocol.Protocol;
/// <remarks>
/// <para>
/// This capability enables the MCP client to respond to elicitation requests from an MCP server.
/// Clients must support at least one elicitation mode: form (in-band) or url (out-of-band via URL).
/// </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="McpClientHandlers.ElicitationHandler"/> to process these requests.
/// </para>
/// <para>
/// This class is intentionally empty as the Model Context Protocol specification does not
/// currently define additional properties for sampling capabilities. Future versions of the
/// specification may extend this capability with additional configuration options.
/// Two modes of elicitation are supported:
/// <list type="bullet">
/// <item><description><b>form</b>: In-band elicitation where data is collected via a form and exposed to the client</description></item>
/// <item><description><b>url</b>: URL mode (out-of-band) elicitation via navigation where sensitive data is not exposed to the client</description></item>
/// </list>
/// </para>
/// </remarks>
[JsonConverter(typeof(Converter))]
public sealed class ElicitationCapability
{
}
/// <summary>
/// Gets or sets the form mode elicitation (in-band) capability, indicating support for in-band elicitation.
/// </summary>
/// <remarks>
/// When present, indicates the client supports form mode elicitation where structured data
/// is collected through a form interface and returned to the server.
/// </remarks>
[JsonPropertyName("form")]
public FormElicitationCapability? Form { get; set; }
/// <summary>
/// Gets or sets the URL mode (out-of-band) elicitation capability.
/// </summary>
/// <remarks>
/// When present, indicates the client supports URL mode elicitation for secure out-of-band
/// interactions such as OAuth flows, payments, or collecting sensitive credentials.
/// </remarks>
[JsonPropertyName("url")]
public UrlElicitationCapability? Url { get; set; }
/// <summary>
/// Provides a converter that normalizes blank capability objects to imply form support.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter<ElicitationCapability>
{
/// <inheritdoc />
public override ElicitationCapability? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
using var document = JsonDocument.ParseValue(ref reader);
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
throw new JsonException("elicitation capability must be an object.");
}
var capability = new ElicitationCapability();
bool hasForm = false;
bool hasUrl = false;
foreach (var property in document.RootElement.EnumerateObject())
{
if (property.NameEquals("form"))
{
capability.Form = property.Value.ValueKind == JsonValueKind.Null
? null
: capability.Form ?? new FormElicitationCapability();
hasForm = true;
}
else if (property.NameEquals("url"))
{
capability.Url = property.Value.ValueKind == JsonValueKind.Null
? null
: capability.Url ?? new UrlElicitationCapability();
hasUrl = capability.Url is not null;
}
}
if (!hasForm && !hasUrl)
{
capability.Form = new FormElicitationCapability();
}
return capability;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ElicitationCapability value, JsonSerializerOptions options)
{
Throw.IfNull(writer);
writer.WriteStartObject();
bool writeForm = value.Form is not null || value.Url is null;
if (writeForm)
{
writer.WritePropertyName("form");
writer.WriteStartObject();
writer.WriteEndObject();
}
if (value.Url is not null)
{
writer.WritePropertyName("url");
writer.WriteStartObject();
writer.WriteEndObject();
}
writer.WriteEndObject();
}
}
}

View file

@ -0,0 +1,26 @@
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the parameters used with a <see cref="NotificationMethods.ElicitationCompleteNotification"/>
/// notification emitted after a URL-mode elicitation finishes out-of-band.
/// </summary>
/// <remarks>
/// <para>
/// The payload references the original elicitation by ID so that clients can resume deferred
/// requests or update pending UI once the external flow completes.
/// </para>
/// </remarks>
public sealed class ElicitationCompleteNotificationParams : NotificationParams
{
/// <summary>
/// Gets or sets the unique identifier of the elicitation that completed.
/// </summary>
/// <remarks>
/// This matches <see cref="ElicitRequestParams.ElicitationId"/> from the originating request and allows
/// clients to correlate the completion notification with previously issued prompts.
/// </remarks>
[JsonPropertyName("elicitationId")]
public required string ElicitationId { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the capability for form-based (in-band) elicitation.
/// </summary>
/// <remarks>
/// This capability enables the client to collect structured data from users through form interfaces
/// where the data is visible to and processed by the client before being sent to the server.
/// </remarks>
public sealed class FormElicitationCapability;

View file

@ -81,6 +81,15 @@ public static class NotificationMethods
/// </remarks>
public const string LoggingMessageNotification = "notifications/message";
/// <summary>
/// The name of the notification sent by the server when a URL-mode elicitation flow completes.
/// </summary>
/// <remarks>
/// This notification references the original elicitation by ID, allowing clients to retry blocked requests
/// or update their UI state once the out-of-band interaction finishes.
/// </remarks>
public const string ElicitationCompleteNotification = "notifications/elicitation/complete";
/// <summary>
/// The name of the notification sent from the client to the server after initialization has finished.
/// </summary>

View file

@ -41,13 +41,13 @@ public static class RequestMethods
public const string ResourcesTemplatesList = "resources/templates/list";
/// <summary>
/// The name of the request method sent from the client to request <see cref="NotificationMethods.ResourceUpdatedNotification"/>
/// The name of the request method sent from the client to request <see cref="NotificationMethods.ResourceUpdatedNotification"/>
/// notifications from the server whenever a particular resource changes.
/// </summary>
public const string ResourcesSubscribe = "resources/subscribe";
/// <summary>
/// The name of the request method sent from the client to request unsubscribing from <see cref="NotificationMethods.ResourceUpdatedNotification"/>
/// The name of the request method sent from the client to request unsubscribing from <see cref="NotificationMethods.ResourceUpdatedNotification"/>
/// notifications from the server.
/// </summary>
public const string ResourcesUnsubscribe = "resources/unsubscribe";
@ -78,7 +78,7 @@ public static class RequestMethods
/// </summary>
/// <remarks>
/// This is used to provide autocompletion-like functionality for arguments in a resource reference or a prompt template.
/// The client provides a reference (resource or prompt), argument name, and partial value, and the server
/// The client provides a reference (resource or prompt), argument name, and partial value, and the server
/// responds with matching completion options.
/// </remarks>
public const string CompletionComplete = "completion/complete";
@ -94,11 +94,21 @@ public static class RequestMethods
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.
/// The name of the request method sent from the server to elicit additional information from the user via the client.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// Servers can request structured data from users, with optional JSON schemas to validate responses (form mode),
/// or request URL mode (out-of-band) user interaction via navigation for sensitive operations.
/// </para>
/// <para>
/// Two modes are supported:
/// <list type="bullet">
/// <item><description><b>form</b>: In-band elicitation where structured data is collected and returned to the server</description></item>
/// <item><description><b>url</b>: URL mode (out-of-band) elicitation for sensitive operations like OAuth or payments</description></item>
/// </list>
/// </para>
/// </remarks>
public const string ElicitationCreate = "elicitation/create";

View file

@ -0,0 +1,17 @@
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the capability for URL mode (out-of-band) elicitation.
/// </summary>
/// <remarks>
/// <para>
/// This capability enables secure out-of-band interactions where the user is directed to a URL
/// (typically opened in a browser) to complete sensitive operations like OAuth authorization,
/// payments, or credential entry.
/// </para>
/// <para>
/// Unlike form mode, sensitive data in URL mode is never exposed to the MCP client, providing
/// better security for sensitive interactions.
/// </para>
/// </remarks>
public sealed class UrlElicitationCapability;

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the payload for the <c>URL_ELICITATION_REQUIRED</c> JSON-RPC error.
/// </summary>
public sealed class UrlElicitationRequiredErrorData
{
/// <summary>
/// Gets or sets the elicitations that must be completed before retrying the original request.
/// </summary>
[JsonPropertyName("elicitations")]
public required IReadOnlyList<ElicitRequestParams> Elicitations { get; init; }
}