Skip to content

Commit 0debdd5

Browse files
committed
Add support for URL mode elicitation
1 parent 707c083 commit 0debdd5

20 files changed

+1105
-72
lines changed

docs/concepts/elicitation/elicitation.md

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ uid: elicitation
99

1010
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.
1111

12+
The protocol supports two modes of elicitation:
13+
- **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.
14+
- **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.
15+
1216
### Server Support for Elicitation
1317

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

18-
The MCP Server must specify the schema of each input value it is requesting from the user.
22+
#### Form Mode Elicitation (In-Band)
23+
24+
For form-based elicitation, the MCP Server must specify the schema of each input value it is requesting from the user.
1925
Primitive types (string, number, boolean) and enum types are supported for elicitation requests.
2026
The schema may include a description to help the user understand what is being requested.
2127

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

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

36-
### Client Support for Elicitation
42+
#### URL Mode Elicitation (Out-of-Band)
3743

38-
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>:
44+
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.
3945

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

42-
The ElicitationHandler is an asynchronous method that will be called when the server requests additional information.
43-
The ElicitationHandler must request input from the user and return the data in a format that matches the requested schema.
44-
This will be highly dependent on the client application and how it interacts with the user.
48+
```csharp
49+
var elicitationId = Guid.NewGuid().ToString();
50+
var result = await server.ElicitAsync(
51+
new ElicitRequestParams
52+
{
53+
Mode = "url",
54+
ElicitationId = elicitationId,
55+
Url = $"https://auth.example.com/oauth/authorize?state={elicitationId}",
56+
Message = "Please authorize access to your account by logging in through your browser."
57+
},
58+
cancellationToken);
59+
```
60+
61+
### Client Support for Elicitation
4562

46-
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.
47-
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.
63+
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.
64+
65+
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>:
66+
67+
```csharp
68+
var options = new McpClientOptions
69+
{
70+
Capabilities = new ClientCapabilities
71+
{
72+
Elicitation = new ElicitationCapability
73+
{
74+
Form = new FormElicitationCapability(),
75+
Url = new UrlElicitationCapability()
76+
}
77+
},
78+
Handlers = new McpClientHandlers
79+
{
80+
ElicitationHandler = HandleElicitationAsync
81+
}
82+
};
83+
```
84+
85+
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:
86+
87+
- **Form Mode**: Present the form defined by `RequestedSchema` to the user. Return the user's input in the `Content` of the result.
88+
- **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"`.
89+
90+
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".
91+
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").
4892

4993
Below is an example of how a console application might handle elicitation requests.
5094
Here's an example implementation:

src/ModelContextProtocol.Core/Client/McpClientImpl.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
8080
cancellationToken),
8181
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
8282
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
83-
83+
8484
_options.Capabilities ??= new();
8585
_options.Capabilities.Sampling ??= new();
8686
}
@@ -106,7 +106,19 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
106106
McpJsonUtilities.JsonContext.Default.ElicitResult);
107107

108108
_options.Capabilities ??= new();
109-
_options.Capabilities.Elicitation ??= new();
109+
if (_options.Capabilities.Elicitation is null)
110+
{
111+
// Default to supporting only form mode if not explicitly configured
112+
_options.Capabilities.Elicitation = new()
113+
{
114+
Form = new(),
115+
};
116+
}
117+
else if (_options.Capabilities.Elicitation.Form is null && _options.Capabilities.Elicitation.Url is null)
118+
{
119+
// If ElicitationCapability is set but both modes are null, default to form mode for backward compatibility
120+
_options.Capabilities.Elicitation.Form = new();
121+
}
110122
}
111123
}
112124

src/ModelContextProtocol.Core/McpErrorCode.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,20 @@ public enum McpErrorCode
5656
/// This error is used when the endpoint encounters an unexpected condition that prevents it from fulfilling the request.
5757
/// </remarks>
5858
InternalError = -32603,
59+
60+
/// <summary>
61+
/// Indicates that URL-mode elicitation is required to complete the requested operation.
62+
/// </summary>
63+
/// <remarks>
64+
/// <para>
65+
/// This error is returned when a server operation requires additional user input through URL-mode elicitation
66+
/// before it can proceed. The error data must include the `data.elicitations` payload describing the pending
67+
/// elicitation(s) for the client to present to the user.
68+
/// </para>
69+
/// <para>
70+
/// Common scenarios include OAuth authorization and other out-of-band flows that cannot be completed inside
71+
/// the MCP client.
72+
/// </para>
73+
/// </remarks>
74+
UrlElicitationRequired = -32042,
5975
}

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static partial class McpJsonUtilities
1616
/// </summary>
1717
/// <remarks>
1818
/// <para>
19-
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
19+
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
2020
/// includes source generated contracts for all common exchange types contained in the ModelContextProtocol library.
2121
/// </para>
2222
/// <para>
@@ -88,7 +88,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
8888
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
8989
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
9090
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
91-
91+
9292
// JSON-RPC
9393
[JsonSerializable(typeof(JsonRpcMessage))]
9494
[JsonSerializable(typeof(JsonRpcMessage[]))]
@@ -101,6 +101,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
101101
[JsonSerializable(typeof(CancelledNotificationParams))]
102102
[JsonSerializable(typeof(InitializedNotificationParams))]
103103
[JsonSerializable(typeof(LoggingMessageNotificationParams))]
104+
[JsonSerializable(typeof(ElicitationCompleteNotificationParams))]
104105
[JsonSerializable(typeof(ProgressNotificationParams))]
105106
[JsonSerializable(typeof(PromptListChangedNotificationParams))]
106107
[JsonSerializable(typeof(ResourceListChangedNotificationParams))]
@@ -117,6 +118,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
117118
[JsonSerializable(typeof(CreateMessageResult))]
118119
[JsonSerializable(typeof(ElicitRequestParams))]
119120
[JsonSerializable(typeof(ElicitResult))]
121+
[JsonSerializable(typeof(UrlElicitationRequiredErrorData))]
120122
[JsonSerializable(typeof(EmptyResult))]
121123
[JsonSerializable(typeof(GetPromptRequestParams))]
122124
[JsonSerializable(typeof(GetPromptResult))]

src/ModelContextProtocol.Core/McpProtocolException.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ namespace ModelContextProtocol;
1616
/// <see cref="McpProtocolException"/>.
1717
/// </para>
1818
/// <para>
19-
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpProtocolException"/> may be
19+
/// <see cref="Exception.Message"/> or <see cref="ErrorCode"/> from a <see cref="McpProtocolException"/> may be
2020
/// propagated to the remote endpoint; sensitive information should not be included. If sensitive details need
2121
/// to be included, a different exception type should be used.
2222
/// </para>
2323
/// </remarks>
24-
public sealed class McpProtocolException : McpException
24+
public class McpProtocolException : McpException
2525
{
2626
/// <summary>
2727
/// Initializes a new instance of the <see cref="McpProtocolException"/> class.

src/ModelContextProtocol.Core/McpSessionHandler.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,23 +181,30 @@ ex is OperationCanceledException &&
181181
{
182182
LogRequestHandlerException(EndpointName, request.Method, ex);
183183

184-
JsonRpcErrorDetail detail = ex is McpProtocolException mcpProtocolException ?
185-
new()
184+
JsonRpcErrorDetail detail = ex switch
185+
{
186+
UrlElicitationRequiredException urlException => new()
187+
{
188+
Code = (int)urlException.ErrorCode,
189+
Message = urlException.Message,
190+
Data = urlException.CreateErrorDataNode(),
191+
},
192+
McpProtocolException mcpProtocolException => new()
186193
{
187194
Code = (int)mcpProtocolException.ErrorCode,
188195
Message = mcpProtocolException.Message,
189-
} : ex is McpException mcpException ?
190-
new()
196+
},
197+
McpException mcpException => new()
191198
{
192-
193199
Code = (int)McpErrorCode.InternalError,
194200
Message = mcpException.Message,
195-
} :
196-
new()
201+
},
202+
_ => new()
197203
{
198204
Code = (int)McpErrorCode.InternalError,
199205
Message = "An error occurred.",
200-
};
206+
},
207+
};
201208

202209
var errorMessage = new JsonRpcError
203210
{
@@ -452,7 +459,7 @@ public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, Canc
452459
if (response is JsonRpcError error)
453460
{
454461
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
455-
throw new McpProtocolException($"Request failed (remote): {error.Error.Message}", (McpErrorCode)error.Error.Code);
462+
throw CreateRemoteProtocolException(error);
456463
}
457464

458465
if (response is JsonRpcResponse success)
@@ -769,6 +776,20 @@ private static TimeSpan GetElapsed(long startingTimestamp) =>
769776
return null;
770777
}
771778

779+
private static McpProtocolException CreateRemoteProtocolException(JsonRpcError error)
780+
{
781+
string formattedMessage = $"Request failed (remote): {error.Error.Message}";
782+
var errorCode = (McpErrorCode)error.Error.Code;
783+
784+
if (errorCode == McpErrorCode.UrlElicitationRequired &&
785+
UrlElicitationRequiredException.TryCreateFromError(formattedMessage, error.Error, out var urlException))
786+
{
787+
return urlException;
788+
}
789+
790+
return new McpProtocolException(formattedMessage, errorCode);
791+
}
792+
772793
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} message processing canceled.")]
773794
private partial void LogEndpointMessageProcessingCanceled(string endpointName);
774795

src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,87 @@ namespace ModelContextProtocol.Protocol;
99
/// <summary>
1010
/// Represents a message issued from the server to elicit additional information from the user via the client.
1111
/// </summary>
12-
public sealed class ElicitRequestParams
12+
public sealed class ElicitRequestParams : RequestParams
1313
{
14+
/// <summary>
15+
/// Gets or sets the elicitation mode: "form" for in-band data collection or "url" for out-of-band URL navigation.
16+
/// </summary>
17+
/// <remarks>
18+
/// <list type="bullet">
19+
/// <item><description><b>form</b>: Client collects structured data via a form interface. Data is exposed to the client.</description></item>
20+
/// <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>
21+
/// </list>
22+
/// </remarks>
23+
[JsonPropertyName("mode")]
24+
[field: MaybeNull]
25+
public string Mode
26+
{
27+
get => field ??= "form";
28+
set
29+
{
30+
if (value is not ("form" or "url"))
31+
{
32+
throw new ArgumentException("Mode must be 'form' or 'url'.", nameof(value));
33+
}
34+
field = value;
35+
}
36+
}
37+
38+
/// <summary>
39+
/// Gets or sets a unique identifier for this elicitation request.
40+
/// </summary>
41+
/// <remarks>
42+
/// <para>
43+
/// Used to track and correlate the elicitation across multiple messages, especially for out-of-band flows
44+
/// that may complete asynchronously.
45+
/// </para>
46+
/// <para>
47+
/// Required for url mode elicitation to enable progress tracking and completion detection.
48+
/// </para>
49+
/// </remarks>
50+
[JsonPropertyName("elicitationId")]
51+
public string? ElicitationId { get; set; }
52+
53+
/// <summary>
54+
/// Gets or sets the URL to navigate to for out-of-band elicitation.
55+
/// </summary>
56+
/// <remarks>
57+
/// <para>
58+
/// Required when <see cref="Mode"/> is "url". The client should prompt the user for consent
59+
/// and then navigate to this URL in a user-agent (browser) where the user completes
60+
/// the required interaction.
61+
/// </para>
62+
/// <para>
63+
/// URLs must not appear in any other field of the elicitation request for security reasons.
64+
/// </para>
65+
/// </remarks>
66+
[JsonPropertyName("url")]
67+
public string? Url { get; set; }
68+
1469
/// <summary>
1570
/// Gets or sets the message to present to the user.
1671
/// </summary>
72+
/// <remarks>
73+
/// For form mode, this describes what information is being requested.
74+
/// For url mode, this explains why the user needs to navigate to the URL.
75+
/// </remarks>
1776
[JsonPropertyName("message")]
1877
public required string Message { get; set; }
1978

2079
/// <summary>
21-
/// Gets or sets the requested schema.
80+
/// Gets or sets the requested schema for form mode elicitation.
2281
/// </summary>
2382
/// <remarks>
83+
/// Only applicable when <see cref="Mode"/> is "form".
2484
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
2585
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
2686
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,
2787
/// or <see cref="LegacyTitledEnumSchema"/> (deprecated).
2888
/// </remarks>
2989
[JsonPropertyName("requestedSchema")]
30-
[field: MaybeNull]
31-
public RequestSchema RequestedSchema
32-
{
33-
get => field ??= new RequestSchema();
34-
set => field = value;
35-
}
90+
public RequestSchema? RequestedSchema { get; set; }
3691

37-
/// <summary>Represents a request schema used in an elicitation request.</summary>
92+
/// <summary>Represents a request schema used in a form mode elicitation request.</summary>
3893
public class RequestSchema
3994
{
4095
/// <summary>Gets the type of the schema.</summary>
@@ -61,7 +116,7 @@ public IDictionary<string, PrimitiveSchemaDefinition> Properties
61116
}
62117

63118
/// <summary>
64-
/// Represents restricted subset of JSON Schema:
119+
/// Represents restricted subset of JSON Schema:
65120
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>,
66121
/// <see cref="UntitledSingleSelectEnumSchema"/>, <see cref="TitledSingleSelectEnumSchema"/>,
67122
/// <see cref="UntitledMultiSelectEnumSchema"/>, <see cref="TitledMultiSelectEnumSchema"/>,

0 commit comments

Comments
 (0)