Skip to content
Open
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
64 changes: 54 additions & 10 deletions docs/concepts/elicitation/elicitation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ uid: elicitation

The **elicitation** feature allows servers to request additional information from users during interactions. This enables more dynamic and interactive AI experiences, making it easier to gather necessary context before executing tasks.

The protocol supports two modes of elicitation:
- **Form (In-Band)**: The server requests structured data (strings, numbers, booleans, enums) which the client collects via a form interface and returns to the server.
- **URL Mode**: The server provides a URL for the user to visit (e.g., for OAuth, payments, or sensitive data entry). The interaction happens outside the MCP client.

### Server Support for Elicitation

Servers request structured data from users with the <xref:ModelContextProtocol.Server.McpServer.ElicitAsync*> extension method on <xref:ModelContextProtocol.Server.McpServer>.
Servers request information from users with the <xref:ModelContextProtocol.Server.McpServer.ElicitAsync*> extension method on <xref:ModelContextProtocol.Server.McpServer>.
The C# SDK registers an instance of <xref:ModelContextProtocol.Server.McpServer> with the dependency injection container,
so tools can simply add a parameter of type <xref:ModelContextProtocol.Server.McpServer> to their method signature to access it.

The MCP Server must specify the schema of each input value it is requesting from the user.
#### Form Mode Elicitation (In-Band)

For form-based elicitation, the MCP Server must specify the schema of each input value it is requesting from the user.
Primitive types (string, number, boolean) and enum types are supported for elicitation requests.
The schema may include a description to help the user understand what is being requested.

Expand All @@ -33,18 +39,56 @@ The following example demonstrates how a server could request a boolean response

[!code-csharp[](samples/server/Tools/InteractiveTools.cs?name=snippet_GuessTheNumber)]

### Client Support for Elicitation
#### URL Mode Elicitation (Out-of-Band)

Elicitation is an optional feature so clients declare their support for it in their capabilities as part of the `initialize` request. In the MCP C# SDK, this is done by configuring an <xref:ModelContextProtocol.Client.McpClientHandlers.ElicitationHandler> in the <xref:ModelContextProtocol.Client.McpClientOptions>:
For URL mode elicitation, the server provides a URL that the user must visit to complete an action. This is useful for scenarios like OAuth flows, payment processing, or collecting sensitive credentials that should not be exposed to the MCP client.

[!code-csharp[](samples/client/Program.cs?name=snippet_McpInitialize)]
To request a URL mode interaction, set the `Mode` to "url" and provide a `Url` and `ElicitationId` in the `ElicitRequestParams`.

The ElicitationHandler is an asynchronous method that will be called when the server requests additional information.
The ElicitationHandler must request input from the user and return the data in a format that matches the requested schema.
This will be highly dependent on the client application and how it interacts with the user.
```csharp
var elicitationId = Guid.NewGuid().ToString();
var result = await server.ElicitAsync(
new ElicitRequestParams
{
Mode = "url",
ElicitationId = elicitationId,
Url = $"https://auth.example.com/oauth/authorize?state={elicitationId}",
Message = "Please authorize access to your account by logging in through your browser."
},
cancellationToken);
```

### Client Support for Elicitation

If the user provides the requested information, the ElicitationHandler should return an <xref:ModelContextProtocol.Protocol.ElicitResult> with the action set to "accept" and the content containing the user's input.
If the user does not provide the requested information, the ElicitationHandler should return an [<xref:ModelContextProtocol.Protocol.ElicitResult> with the action set to "reject" and no content.
Elicitation is an optional feature so clients declare their support for it in their capabilities as part of the `initialize` request. Clients can support `Form` (in-band), `Url` (out-of-band), or both.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional capability? It's a little redundant as almost every capability is optional. Maybe just "Clients declare their support.." ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Elicitation is an optional feature so clients declare their support for it in their capabilities as part of the `initialize` request. Clients can support `Form` (in-band), `Url` (out-of-band), or both.
Clients declare their support for elicitation in their capabilities as part of the `initialize` request. Clients can support `Form` (in-band), `Url` (out-of-band), or both.


In the MCP C# SDK, this is done by configuring the capabilities and an <xref:ModelContextProtocol.Client.McpClientHandlers.ElicitationHandler> in the <xref:ModelContextProtocol.Client.McpClientOptions>:

```csharp
var options = new McpClientOptions
{
Capabilities = new ClientCapabilities
{
Elicitation = new ElicitationCapability
{
Form = new FormElicitationCapability(),
Url = new UrlElicitationCapability()
}
},
Handlers = new McpClientHandlers
{
ElicitationHandler = HandleElicitationAsync
}
};
```

The `ElicitationHandler` is an asynchronous method that will be called when the server requests additional information. The handler should check the `Mode` of the request:

- **Form Mode**: Present the form defined by `RequestedSchema` to the user. Return the user's input in the `Content` of the result.
- **URL Mode**: Present the `Message` and `Url` to the user. Ask for consent to open the URL. If the user consents, open the URL and return `Action="accept"`. If the user declines, return `Action="decline"`.

If the user provides the requested information (or consents to URL mode), the ElicitationHandler should return an <xref:ModelContextProtocol.Protocol.ElicitResult> with the action set to "accept".
If the user does not provide the requested information, the ElicitationHandler should return an <xref:ModelContextProtocol.Protocol.ElicitResult> with the action set to "reject" (or "decline" / "cancel").

Below is an example of how a console application might handle elicitation requests.
Here's an example implementation:
Expand Down
16 changes: 14 additions & 2 deletions src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);

_options.Capabilities ??= new();
_options.Capabilities.Sampling ??= new();
}
Expand All @@ -106,7 +106,19 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
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();
}
Copy link
Contributor

@stephentoub stephentoub Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this could be simplified slightly to:

_options.Capabilities.Elicitation ??= new();
if (_options.Capabilities.Elicitation.Form is null &&
    _options.Capabilities.Elicitation.Url is null)
{
    // If both modes are null, default to form mode for backward compatibility.
    _options.Capabilities.Elicitation.Form = new();
}

}
}

Expand Down
16 changes: 16 additions & 0 deletions src/ModelContextProtocol.Core/McpErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 4 additions & 2 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static partial class McpJsonUtilities
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for all common exchange types contained in the ModelContextProtocol library.
/// </para>
/// <para>
Expand Down Expand Up @@ -88,7 +88,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]

// JSON-RPC
[JsonSerializable(typeof(JsonRpcMessage))]
[JsonSerializable(typeof(JsonRpcMessage[]))]
Expand All @@ -101,6 +101,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(CancelledNotificationParams))]
[JsonSerializable(typeof(InitializedNotificationParams))]
[JsonSerializable(typeof(LoggingMessageNotificationParams))]
[JsonSerializable(typeof(ElicitationCompleteNotificationParams))]
[JsonSerializable(typeof(ProgressNotificationParams))]
[JsonSerializable(typeof(PromptListChangedNotificationParams))]
[JsonSerializable(typeof(ResourceListChangedNotificationParams))]
Expand All @@ -117,6 +118,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(ElicitRequestParams))]
[JsonSerializable(typeof(ElicitResult))]
[JsonSerializable(typeof(UrlElicitationRequiredErrorData))]
[JsonSerializable(typeof(EmptyResult))]
[JsonSerializable(typeof(GetPromptRequestParams))]
[JsonSerializable(typeof(GetPromptResult))]
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/McpProtocolException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ namespace ModelContextProtocol;
/// <see cref="McpProtocolException"/>.
/// </para>
/// <para>
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpProtocolException"/> may be
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpProtocolException"/> may be
/// propagated to the remote endpoint; sensitive information should not be included. If sensitive details need
/// 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.
Expand Down
39 changes: 30 additions & 9 deletions src/ModelContextProtocol.Core/McpSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,23 +181,30 @@ ex is OperationCanceledException &&
{
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,
} : 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
{
Expand Down Expand Up @@ -452,7 +459,7 @@ public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, Canc
if (response is JsonRpcError error)
{
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
throw new McpProtocolException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
throw CreateRemoteProtocolException(error);
}

if (response is JsonRpcResponse success)
Expand Down Expand Up @@ -769,6 +776,20 @@ private static TimeSpan GetElapsed(long startingTimestamp) =>
return null;
}

private static McpProtocolException CreateRemoteProtocolException(JsonRpcError error)
{
string formattedMessage = $"Request failed (remote): {error.Error.Message}";
var errorCode = (McpErrorCode)error.Error.Code;

if (errorCode == McpErrorCode.UrlElicitationRequired &&
UrlElicitationRequiredException.TryCreateFromError(formattedMessage, error.Error, out var urlException))
{
return urlException;
}

return new McpProtocolException(formattedMessage, errorCode);
}

[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} message processing canceled.")]
private partial void LogEndpointMessageProcessingCanceled(string endpointName);

Expand Down
75 changes: 65 additions & 10 deletions src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,87 @@ 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".
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,
/// or <see cref="LegacyTitledEnumSchema"/> (deprecated).
/// </remarks>
[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>
Expand All @@ -61,7 +116,7 @@ public IDictionary<string, PrimitiveSchemaDefinition> Properties
}

/// <summary>
/// Represents restricted subset of JSON Schema:
/// Represents restricted subset of JSON Schema:
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,
Expand Down
Loading
Loading