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
179 changes: 170 additions & 9 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's 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 might include a description to help the user understand what's being requested.

Expand All @@ -34,19 +40,174 @@ The following example demonstrates how a server could request a Boolean response

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

#### URL Mode Elicitation (Out-of-Band)

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.

To request a URL mode interaction, set the `Mode` to "url" and provide a `Url` and `ElicitationId` in the `ElicitRequestParams`.

```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

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>:
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>:

[!code-csharp[](samples/client/Program.cs?name=snippet_McpInitialize)]
```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 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.
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:

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.
- **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").

Here's an example implementation of how a console application might handle elicitation requests:

[!code-csharp[](samples/client/Program.cs?name=snippet_ElicitationHandler)]

### URL Elicitation Required Error

When a tool cannot proceed without first completing a URL-mode elicitation (for example, when third-party OAuth authorization is needed), and calling `ElicitAsync` is not practical (for example in <xref: ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> is enabled disabling server-to-client requets), the server may throw a <xref:ModelContextProtocol.UrlElicitationRequiredException>. This is a specialized error (JSON-RPC error code `-32042`) that signals to the client that one or more URL-mode elicitations must be completed before the original request can be retried.

#### Throwing UrlElicitationRequiredException on the Server

A server tool can throw `UrlElicitationRequiredException` when it detects that authorization or other out-of-band interaction is required:

```csharp
[McpServerTool, Description("A tool that requires third-party authorization")]
public async Task<string> AccessThirdPartyResource(McpServer server, CancellationToken token)
{
// Check if we already have valid credentials for this user
// (In a real app, you'd check stored tokens based on user identity)
bool hasValidCredentials = false;

if (!hasValidCredentials)
{
// Generate a unique elicitation ID for tracking
var elicitationId = Guid.NewGuid().ToString();

// Throw the exception to signal the client needs to complete URL elicitation
throw new UrlElicitationRequiredException(
"Authorization is required to access the third-party service.",
[
new ElicitRequestParams
{
Mode = "url",
ElicitationId = elicitationId,
Url = $"https://auth.example.com/connect?elicitationId={elicitationId}",
Message = "Please authorize access to your Example Co account."
}
]);
}

// Proceed with the authorized operation
return "Successfully accessed the resource!";
}
```

The exception can include multiple elicitations if the operation requires authorization from multiple services.

#### Catching UrlElicitationRequiredException on the Client

When the client calls a tool and receives a `UrlElicitationRequiredException`, it should:

1. Present each URL elicitation to the user (showing the URL and message)
2. Get user consent before opening each URL
3. Optionally wait for completion notifications from the server
4. Retry the original request after the user completes the out-of-band interactions

```csharp
try
{
var result = await client.CallToolAsync("AccessThirdPartyResource");
Console.WriteLine($"Tool succeeded: {result.Content[0]}");
}
catch (UrlElicitationRequiredException ex)
{
Console.WriteLine($"Authorization required: {ex.Message}");

// Process each required elicitation
foreach (var elicitation in ex.Elicitations)
{
Console.WriteLine($"\nServer requests URL interaction:");
Console.WriteLine($" Message: {elicitation.Message}");
Console.WriteLine($" URL: {elicitation.Url}");
Console.WriteLine($" Elicitation ID: {elicitation.ElicitationId}");

// Show security warning and get user consent
Console.Write("\nDo you want to open this URL? (y/n): ");
var consent = Console.ReadLine();

if (consent?.ToLower() == "y")
{
// Open the URL in the system browser
Process.Start(new ProcessStartInfo(elicitation.Url!) { UseShellExecute = true });

Console.WriteLine("Waiting for you to complete the interaction in your browser...");
// Optionally listen for notifications/elicitation/complete notification
}
}

// After user completes the out-of-band interaction, retry the tool call
Console.Write("\nPress Enter to retry the tool call...");
Console.ReadLine();

var retryResult = await client.CallToolAsync("AccessThirdPartyResource");
Console.WriteLine($"Tool succeeded on retry: {retryResult.Content[0]}");
}
```

#### Listening for Elicitation Completion Notifications

Servers can optionally send a `notifications/elicitation/complete` notification when the out-of-band interaction is complete. Clients can register a handler to receive these notifications:

```csharp
await using var completionHandler = client.RegisterNotificationHandler(
NotificationMethods.ElicitationCompleteNotification,
async (notification, cancellationToken) =>
{
var payload = notification.Params?.Deserialize<ElicitationCompleteNotificationParams>(
McpJsonUtilities.DefaultOptions);

if (payload is not null)
{
Console.WriteLine($"Elicitation {payload.ElicitationId} completed!");
// Signal that the client can now retry the original request
}
});
```

This pattern is particularly useful for:
- **Third-party OAuth flows**: When the MCP server needs to obtain tokens from external services on behalf of the user
- **Payment processing**: When user confirmation is required through a secure payment interface
- **Sensitive credential collection**: When API keys or other secrets must be entered directly on a trusted server page rather than through the MCP client
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class HttpServerTransportOptions
/// the <see cref="RunSessionHandler"/> will be called once for for each request, and the "/sse" endpoint will be disabled.
/// Unsolicited server-to-client messages and all server-to-client requests are also unsupported, because any responses
/// might arrive at another ASP.NET Core application process.
/// Client sampling and roots capabilities are also disabled in stateless mode, because the server cannot make requests.
/// Client sampling, elicitation, and roots capabilities are also disabled in stateless mode, because the server cannot make requests.
/// </remarks>
public bool Stateless { get; set; }

Expand Down
14 changes: 13 additions & 1 deletion src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ public override async ValueTask DisposeAsync()
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
LogTransportShutdownFailed(Name, ex);
}
finally
{
_connectionCts.Dispose();
Expand Down Expand Up @@ -268,15 +272,8 @@ private async Task SendDeleteRequest()
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)
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,
}
2 changes: 2 additions & 0 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/ModelContextProtocol.Core/McpProtocolException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading