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
16 changed files with 1241 additions and 92 deletions

View file

@ -104,7 +104,19 @@ internal sealed partial class McpClientImpl : McpClient
McpJsonUtilities.JsonContext.Default.ElicitResult);
_options.Capabilities ??= new();
_options.Capabilities.Elicitation ??= new();
if (_options.Capabilities.Elicitation is null)
{
// Default to supporting only form mode if not explicitly configured
_options.Capabilities.Elicitation = new()
{
Form = new(),
};
}
else if (_options.Capabilities.Elicitation.Form is null && _options.Capabilities.Elicitation.Url is null)
{
// If ElicitationCapability is set but both modes are null, default to form mode for backward compatibility
_options.Capabilities.Elicitation.Form = new();
}
}
}

View file

@ -166,6 +166,10 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
LogTransportShutdownFailed(Name, ex);
}
finally
{
_connectionCts.Dispose();
@ -268,15 +272,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
using var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, _options.Endpoint);
CopyAdditionalHeaders(deleteRequest.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion);
try
{
// Do not validate we get a successful status code, because server support for the DELETE request is optional
(await _httpClient.SendAsync(deleteRequest, message: null, CancellationToken.None).ConfigureAwait(false)).Dispose();
}
catch (Exception ex)
{
LogTransportShutdownFailed(Name, ex);
}
// Do not validate we get a successful status code, because server support for the DELETE request is optional
(await _httpClient.SendAsync(deleteRequest, message: null, CancellationToken.None).ConfigureAwait(false)).Dispose();
}
private void LogJsonException(JsonException ex, string data)

View file

@ -56,4 +56,20 @@ public enum McpErrorCode
/// This error is used when the endpoint encounters an unexpected condition that prevents it from fulfilling the request.
/// </remarks>
InternalError = -32603,
/// <summary>
/// Indicates that URL-mode elicitation is required to complete the requested operation.
/// </summary>
/// <remarks>
/// <para>
/// This error is returned when a server operation requires additional user input through URL-mode elicitation
/// before it can proceed. The error data must include the `data.elicitations` payload describing the pending
/// elicitation(s) for the client to present to the user.
/// </para>
/// <para>
/// Common scenarios include OAuth authorization and other out-of-band flows that cannot be completed inside
/// the MCP client.
/// </para>
/// </remarks>
UrlElicitationRequired = -32042,
}

View file

@ -101,6 +101,7 @@ public static partial class McpJsonUtilities
[JsonSerializable(typeof(CancelledNotificationParams))]
[JsonSerializable(typeof(InitializedNotificationParams))]
[JsonSerializable(typeof(LoggingMessageNotificationParams))]
[JsonSerializable(typeof(ElicitationCompleteNotificationParams))]
[JsonSerializable(typeof(ProgressNotificationParams))]
[JsonSerializable(typeof(PromptListChangedNotificationParams))]
[JsonSerializable(typeof(ResourceListChangedNotificationParams))]
@ -117,6 +118,7 @@ public static partial class McpJsonUtilities
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(ElicitRequestParams))]
[JsonSerializable(typeof(ElicitResult))]
[JsonSerializable(typeof(UrlElicitationRequiredErrorData))]
[JsonSerializable(typeof(EmptyResult))]
[JsonSerializable(typeof(GetPromptRequestParams))]
[JsonSerializable(typeof(GetPromptResult))]

View file

@ -21,7 +21,7 @@ namespace ModelContextProtocol;
/// to be included, a different exception type should be used.
/// </para>
/// </remarks>
public sealed class McpProtocolException : McpException
public class McpProtocolException : McpException
{
/// <summary>
/// Initializes a new instance of the <see cref="McpProtocolException"/> class.

View file

@ -181,23 +181,31 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
{
LogRequestHandlerException(EndpointName, request.Method, ex);
JsonRpcErrorDetail detail = ex is McpProtocolException mcpProtocolException ?
new()
JsonRpcErrorDetail detail = ex switch
{
UrlElicitationRequiredException urlException => new()
{
Code = (int)urlException.ErrorCode,
Message = urlException.Message,
Data = urlException.CreateErrorDataNode(),
},
McpProtocolException mcpProtocolException => new()
{
Code = (int)mcpProtocolException.ErrorCode,
Message = mcpProtocolException.Message,
Data = ConvertExceptionData(mcpProtocolException.Data),
} : ex is McpException mcpException ?
new()
},
McpException mcpException => new()
{
Code = (int)McpErrorCode.InternalError,
Message = mcpException.Message,
} :
new()
},
_ => new()
{
Code = (int)McpErrorCode.InternalError,
Message = "An error occurred.",
};
},
};
var errorMessage = new JsonRpcError
{
@ -453,40 +461,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
if (response is JsonRpcError error)
{
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
var exception = new McpProtocolException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
// Populate exception.Data with the error data if present.
// When deserializing JSON, Data will be a JsonElement.
// We extract primitive values (strings, numbers, bools) for broader compatibility,
// as JsonElement is not [Serializable] and cannot be stored in Exception.Data on .NET Framework.
if (error.Error.Data is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object)
{
foreach (var property in jsonElement.EnumerateObject())
{
object? value = property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString(),
JsonValueKind.Number => property.Value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
#if NET
// Objects and arrays are stored as JsonElement on .NET Core only
_ => property.Value,
#else
// Skip objects/arrays on .NET Framework as JsonElement is not serializable
_ => (object?)null,
#endif
};
if (value is not null || property.Value.ValueKind == JsonValueKind.Null)
{
exception.Data[property.Name] = value;
}
}
}
throw exception;
throw CreateRemoteProtocolException(error);
}
if (response is JsonRpcResponse success)
@ -803,6 +778,56 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
return null;
}
private static McpProtocolException CreateRemoteProtocolException(JsonRpcError error)
{
string formattedMessage = $"Request failed (remote): {error.Error.Message}";
var errorCode = (McpErrorCode)error.Error.Code;
McpProtocolException exception;
if (errorCode == McpErrorCode.UrlElicitationRequired &&
UrlElicitationRequiredException.TryCreateFromError(formattedMessage, error.Error, out var urlException))
{
exception = urlException;
}
else
{
exception = new McpProtocolException(formattedMessage, errorCode);
}
// Populate exception.Data with the error data if present.
// When deserializing JSON, Data will be a JsonElement.
// We extract primitive values (strings, numbers, bools) for broader compatibility,
// as JsonElement is not [Serializable] and cannot be stored in Exception.Data on .NET Framework.
if (error.Error.Data is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object)
{
foreach (var property in jsonElement.EnumerateObject())
{
object? value = property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString(),
JsonValueKind.Number => property.Value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
#if NET
// Objects and arrays are stored as JsonElement on .NET Core only
_ => property.Value,
#else
// Skip objects/arrays on .NET Framework as JsonElement is not serializable
_ => (object?)null,
#endif
};
if (value is not null || property.Value.ValueKind == JsonValueKind.Null)
{
exception.Data[property.Name] = value;
}
}
}
return exception;
}
/// <summary>
/// Converts the <see cref="Exception.Data"/> dictionary to a serializable <see cref="Dictionary{TKey, TValue}"/>.
/// Returns null if the data dictionary is empty or contains no string keys with serializable values.

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; }
}

View file

@ -240,7 +240,8 @@ public abstract partial class McpServer : McpSession
public ValueTask<ElicitResult> ElicitAsync(
ElicitRequestParams request, CancellationToken cancellationToken = default)
{
ThrowIfElicitationUnsupported();
Throw.IfNull(request);
ThrowIfElicitationUnsupported(request);
return SendRequestAsync(
RequestMethods.ElicitationCreate,
@ -268,8 +269,6 @@ public abstract partial class McpServer : McpSession
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
ThrowIfElicitationUnsupported();
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
@ -287,6 +286,8 @@ public abstract partial class McpServer : McpSession
RequestedSchema = schema,
};
ThrowIfElicitationUnsupported(request);
var raw = await ElicitAsync(request, cancellationToken).ConfigureAwait(false);
if (!raw.IsAccepted || raw.Content is null)
@ -453,16 +454,47 @@ public abstract partial class McpServer : McpSession
}
}
private void ThrowIfElicitationUnsupported()
private void ThrowIfElicitationUnsupported(ElicitRequestParams request)
{
if (ClientCapabilities?.Elicitation is null)
if (ClientCapabilities is null)
{
if (ClientCapabilities is null)
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
}
var elicitationCapability = ClientCapabilities.Elicitation;
if (elicitationCapability is null)
{
throw new InvalidOperationException("Client does not support elicitation requests.");
}
if (string.Equals(request.Mode, "form", StringComparison.Ordinal) && elicitationCapability.Form is null)
{
if (request.RequestedSchema is null)
{
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
throw new ArgumentException("Form mode elicitation requests require a requested schema.");
}
throw new InvalidOperationException("Client does not support elicitation requests.");
if (elicitationCapability.Form is null)
{
throw new InvalidOperationException("Client does not support form mode elicitation requests.");
}
}
else if (string.Equals(request.Mode, "url", StringComparison.Ordinal))
{
if (request.Url is null)
{
throw new ArgumentException("URL mode elicitation requests require a URL.");
}
if (request.ElicitationId is null)
{
throw new ArgumentException("URL mode elicitation requests require an elicitation ID.");
}
if (elicitationCapability.Url is null)
{
throw new InvalidOperationException("Client does not support URL mode elicitation requests.");
}
}
}

View file

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol;
/// <summary>
/// Represents an exception used to indicate that URL-mode elicitation must be completed before the request can proceed.
/// </summary>
public sealed class UrlElicitationRequiredException : McpProtocolException
{
private readonly IReadOnlyList<ElicitRequestParams> _elicitations;
/// <summary>
/// Initializes a new instance of the <see cref="UrlElicitationRequiredException"/> class with the specified message and pending elicitations.
/// </summary>
/// <param name="message">A description of why the elicitation is required.</param>
/// <param name="elicitations">One or more URL-mode elicitation requests that must complete before retrying the original request.</param>
public UrlElicitationRequiredException(string message, IEnumerable<ElicitRequestParams> elicitations)
: base(message, McpErrorCode.UrlElicitationRequired)
{
Throw.IfNull(elicitations);
_elicitations = Validate(elicitations);
}
/// <summary>
/// Gets the collection of pending URL-mode elicitation requests that must be completed.
/// </summary>
public IReadOnlyList<ElicitRequestParams> Elicitations => _elicitations;
internal JsonNode CreateErrorDataNode()
{
var payload = new UrlElicitationRequiredErrorData
{
Elicitations = _elicitations,
};
return JsonSerializer.SerializeToNode(
payload,
McpJsonUtilities.JsonContext.Default.UrlElicitationRequiredErrorData)!;
}
internal static bool TryCreateFromError(
string formattedMessage,
JsonRpcErrorDetail detail,
[NotNullWhen(true)] out UrlElicitationRequiredException? exception)
{
exception = null;
if (detail.Data is not JsonElement dataElement)
{
return false;
}
if (!TryParseElicitations(dataElement, out var elicitations))
{
return false;
}
exception = new UrlElicitationRequiredException(formattedMessage, elicitations);
return true;
}
private static bool TryParseElicitations(JsonElement dataElement, out IReadOnlyList<ElicitRequestParams> elicitations)
{
elicitations = Array.Empty<ElicitRequestParams>();
if (dataElement.ValueKind is not JsonValueKind.Object)
{
return false;
}
var payload = dataElement.Deserialize(McpJsonUtilities.JsonContext.Default.UrlElicitationRequiredErrorData);
if (payload?.Elicitations is not { Count: > 0 } elicitationsFromPayload)
{
return false;
}
foreach (var elicitation in elicitationsFromPayload)
{
if (!IsValidUrlElicitation(elicitation))
{
return false;
}
}
elicitations = elicitationsFromPayload;
return true;
}
private static IReadOnlyList<ElicitRequestParams> Validate(IEnumerable<ElicitRequestParams> elicitations)
{
var list = new List<ElicitRequestParams>();
foreach (var elicitation in elicitations)
{
Throw.IfNull(elicitation);
if (!IsValidUrlElicitation(elicitation))
{
throw new ArgumentException(
"Elicitations must be URL-mode requests that include an elicitationId, message, and url.",
nameof(elicitations));
}
list.Add(elicitation);
}
if (list.Count == 0)
{
throw new ArgumentException("At least one elicitation must be provided.", nameof(elicitations));
}
return list;
}
private static bool IsValidUrlElicitation(ElicitRequestParams elicitation)
{
return string.Equals(elicitation.Mode, "url", StringComparison.Ordinal) &&
elicitation.Url is not null &&
elicitation.ElicitationId is not null;
}
}