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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another thing we could do in addition to or instead of throwing a UrlElicitationRequiredException is to always attach the JsonRpcError to a nullable property on McpProtocolExceptions if it was created from one.

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.

That seems better / more general-purpose than a custom exception type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you not like the ability to specifically catch a UrlElicitationRequiredException? The alternative would be to catch all McpProtocolException and then parse the JsonNode Data property.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I've misunderstood. Why does someone need to catch it? Are we using it for control flow?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. This is tested by UrlElicitationRequired_Exception_Propagates_To_Client. I don't have a proper client sample, but if I did, it would look a lot like the TypeScript one in https://github.com/modelcontextprotocol/typescript-sdk/blob/e6c71bbab1dff7bf0c84eee96e74ef87f82a1dbe/src/examples/client/elicitationUrlExample.ts#L711-L719.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does that mean the mcp spec dictates that url elicitations be modeled as json rpc errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's either an error or a request. Section 4.2 and 4.3 of the new elicitation spec go over the two flows for URL elicitation.

URL Mode Flow

sequenceDiagram
    participant UserAgent as User Agent (Browser)
    participant User
    participant Client
    participant Server

    Note over Server: Server initiates elicitation
    Server->>Client: elicitation/create (mode: url)

    Client->>User: Present consent to open URL
    User-->>Client: Provide consent

    Client->>UserAgent: Open URL
    Client->>Server: Accept response

    Note over User,UserAgent: User interaction
    UserAgent-->>Server: Interaction complete
    Server-->>Client: notifications/elicitation/complete (optional)

    Note over Server: Continue processing with new information
Loading

URL Mode With Elicitation Required Error Flow

sequenceDiagram
    participant UserAgent as User Agent (Browser)
    participant User
    participant Client
    participant Server

    Client->>Server: tools/call

    Note over Server: Server needs authorization
    Server->>Client: URLElicitationRequiredError
    Note over Client: Client notes the original request can be retried after elicitation

    Client->>User: Present consent to open URL
    User-->>Client: Provide consent

    Client->>UserAgent: Open URL

    Note over User,UserAgent: User interaction

    UserAgent-->>Server: Interaction complete
    Server-->>Client: notifications/elicitation/complete (optional)

    Client->>Server: Retry tools/call (optional)
Loading

https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#message-flow

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, in that case it makes sense to keep the exception. I think it's unfortunate that elicitation requests are being modeled as errors.

}

[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