Skip to content

Commit 1ee552a

Browse files
committed
Merge branch 'main' into halter73/773
2 parents fe76c79 + ae17ba0 commit 1ee552a

File tree

58 files changed

+2672
-399
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2672
-399
lines changed

docs/concepts/elicitation/elicitation.md

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

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

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

43+
#### URL Mode Elicitation (Out-of-Band)
44+
45+
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.
46+
47+
To request a URL mode interaction, set the `Mode` to "url" and provide a `Url` and `ElicitationId` in the `ElicitRequestParams`.
48+
49+
```csharp
50+
var elicitationId = Guid.NewGuid().ToString();
51+
var result = await server.ElicitAsync(
52+
new ElicitRequestParams
53+
{
54+
Mode = "url",
55+
ElicitationId = elicitationId,
56+
Url = $"https://auth.example.com/oauth/authorize?state={elicitationId}",
57+
Message = "Please authorize access to your account by logging in through your browser."
58+
},
59+
cancellationToken);
60+
```
61+
3762
### Client Support for Elicitation
3863

39-
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>:
64+
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.
65+
66+
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>:
4067

41-
[!code-csharp[](samples/client/Program.cs?name=snippet_McpInitialize)]
68+
```csharp
69+
var options = new McpClientOptions
70+
{
71+
Capabilities = new ClientCapabilities
72+
{
73+
Elicitation = new ElicitationCapability
74+
{
75+
Form = new FormElicitationCapability(),
76+
Url = new UrlElicitationCapability()
77+
}
78+
},
79+
Handlers = new McpClientHandlers
80+
{
81+
ElicitationHandler = HandleElicitationAsync
82+
}
83+
};
84+
```
4285

43-
The ElicitationHandler is an asynchronous method that will be called when the server requests additional information.
44-
The ElicitationHandler must request input from the user and return the data in a format that matches the requested schema.
45-
This will be highly dependent on the client application and how it interacts with the user.
86+
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:
4687

47-
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.
48-
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.
88+
- **Form Mode**: Present the form defined by `RequestedSchema` to the user. Return the user's input in the `Content` of the result.
89+
- **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"`.
90+
91+
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".
92+
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").
4993

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

5296
[!code-csharp[](samples/client/Program.cs?name=snippet_ElicitationHandler)]
97+
98+
### URL Elicitation Required Error
99+
100+
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.
101+
102+
#### Throwing UrlElicitationRequiredException on the Server
103+
104+
A server tool can throw `UrlElicitationRequiredException` when it detects that authorization or other out-of-band interaction is required:
105+
106+
```csharp
107+
[McpServerTool, Description("A tool that requires third-party authorization")]
108+
public async Task<string> AccessThirdPartyResource(McpServer server, CancellationToken token)
109+
{
110+
// Check if we already have valid credentials for this user
111+
// (In a real app, you'd check stored tokens based on user identity)
112+
bool hasValidCredentials = false;
113+
114+
if (!hasValidCredentials)
115+
{
116+
// Generate a unique elicitation ID for tracking
117+
var elicitationId = Guid.NewGuid().ToString();
118+
119+
// Throw the exception to signal the client needs to complete URL elicitation
120+
throw new UrlElicitationRequiredException(
121+
"Authorization is required to access the third-party service.",
122+
[
123+
new ElicitRequestParams
124+
{
125+
Mode = "url",
126+
ElicitationId = elicitationId,
127+
Url = $"https://auth.example.com/connect?elicitationId={elicitationId}",
128+
Message = "Please authorize access to your Example Co account."
129+
}
130+
]);
131+
}
132+
133+
// Proceed with the authorized operation
134+
return "Successfully accessed the resource!";
135+
}
136+
```
137+
138+
The exception can include multiple elicitations if the operation requires authorization from multiple services.
139+
140+
#### Catching UrlElicitationRequiredException on the Client
141+
142+
When the client calls a tool and receives a `UrlElicitationRequiredException`, it should:
143+
144+
1. Present each URL elicitation to the user (showing the URL and message)
145+
2. Get user consent before opening each URL
146+
3. Optionally wait for completion notifications from the server
147+
4. Retry the original request after the user completes the out-of-band interactions
148+
149+
```csharp
150+
try
151+
{
152+
var result = await client.CallToolAsync("AccessThirdPartyResource");
153+
Console.WriteLine($"Tool succeeded: {result.Content[0]}");
154+
}
155+
catch (UrlElicitationRequiredException ex)
156+
{
157+
Console.WriteLine($"Authorization required: {ex.Message}");
158+
159+
// Process each required elicitation
160+
foreach (var elicitation in ex.Elicitations)
161+
{
162+
Console.WriteLine($"\nServer requests URL interaction:");
163+
Console.WriteLine($" Message: {elicitation.Message}");
164+
Console.WriteLine($" URL: {elicitation.Url}");
165+
Console.WriteLine($" Elicitation ID: {elicitation.ElicitationId}");
166+
167+
// Show security warning and get user consent
168+
Console.Write("\nDo you want to open this URL? (y/n): ");
169+
var consent = Console.ReadLine();
170+
171+
if (consent?.ToLower() == "y")
172+
{
173+
// Open the URL in the system browser
174+
Process.Start(new ProcessStartInfo(elicitation.Url!) { UseShellExecute = true });
175+
176+
Console.WriteLine("Waiting for you to complete the interaction in your browser...");
177+
// Optionally listen for notifications/elicitation/complete notification
178+
}
179+
}
180+
181+
// After user completes the out-of-band interaction, retry the tool call
182+
Console.Write("\nPress Enter to retry the tool call...");
183+
Console.ReadLine();
184+
185+
var retryResult = await client.CallToolAsync("AccessThirdPartyResource");
186+
Console.WriteLine($"Tool succeeded on retry: {retryResult.Content[0]}");
187+
}
188+
```
189+
190+
#### Listening for Elicitation Completion Notifications
191+
192+
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:
193+
194+
```csharp
195+
await using var completionHandler = client.RegisterNotificationHandler(
196+
NotificationMethods.ElicitationCompleteNotification,
197+
async (notification, cancellationToken) =>
198+
{
199+
var payload = notification.Params?.Deserialize<ElicitationCompleteNotificationParams>(
200+
McpJsonUtilities.DefaultOptions);
201+
202+
if (payload is not null)
203+
{
204+
Console.WriteLine($"Elicitation {payload.ElicitationId} completed!");
205+
// Signal that the client can now retry the original request
206+
}
207+
});
208+
```
209+
210+
This pattern is particularly useful for:
211+
- **Third-party OAuth flows**: When the MCP server needs to obtain tokens from external services on behalf of the user
212+
- **Payment processing**: When user confirmation is required through a secure payment interface
213+
- **Sensitive credential collection**: When API keys or other secrets must be entered directly on a trusted server page rather than through the MCP client

samples/EverythingServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ await ctx.Server.SampleAsync([
7575
new ChatMessage(ChatRole.System, "You are a helpful test server"),
7676
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
7777
],
78-
options: new ChatOptions
78+
chatOptions: new ChatOptions
7979
{
8080
MaxOutputTokens = 100,
8181
Temperature = 0.7f,

samples/EverythingServer/Tools/SampleLlmTool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static async Task<string> SampleLLM(
1515
CancellationToken cancellationToken)
1616
{
1717
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
18-
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);
18+
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken: cancellationToken);
1919

2020
return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
2121
}

samples/TestServerWithHosting/Tools/SampleLlmTool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static async Task<string> SampleLLM(
1818
CancellationToken cancellationToken)
1919
{
2020
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
21-
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);
21+
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken: cancellationToken);
2222

2323
return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
2424
}

src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,20 +321,25 @@ private static void AppendMethodDeclaration(
321321
writer.WriteLine($"[return: Description(\"{EscapeString(xmlDocs.Returns)}\")]");
322322
}
323323

324-
// Copy modifiers from original method syntax.
324+
// Copy modifiers from original method syntax, excluding 'async' which is invalid on partial declarations (CS1994).
325325
// Add return type (without nullable annotations).
326326
// Add method name.
327-
writer.Write(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text)));
327+
var modifiers = methodDeclaration.Modifiers
328+
.Where(m => !m.IsKind(SyntaxKind.AsyncKeyword))
329+
.Select(m => m.Text);
330+
writer.Write(string.Join(" ", modifiers));
328331
writer.Write(' ');
329332
writer.Write(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
330333
writer.Write(' ');
331334
writer.Write(methodSymbol.Name);
332335

333336
// Add parameters with their Description attributes.
334337
writer.Write("(");
338+
var parameterSyntaxList = methodDeclaration.ParameterList.Parameters;
335339
for (int i = 0; i < methodSymbol.Parameters.Length; i++)
336340
{
337341
IParameterSymbol param = methodSymbol.Parameters[i];
342+
ParameterSyntax? paramSyntax = i < parameterSyntaxList.Count ? parameterSyntaxList[i] : null;
338343

339344
if (i > 0)
340345
{
@@ -352,6 +357,13 @@ private static void AppendMethodDeclaration(
352357
writer.Write(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
353358
writer.Write(' ');
354359
writer.Write(param.Name);
360+
361+
// Preserve default parameter values from the original syntax.
362+
if (paramSyntax?.Default is { } defaultValue)
363+
{
364+
writer.Write(' ');
365+
writer.Write(defaultValue.ToFullString().Trim());
366+
}
355367
}
356368
writer.WriteLine(");");
357369
}

src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class HttpServerTransportOptions
3939
/// the <see cref="RunSessionHandler"/> will be called once for for each request, and the "/sse" endpoint will be disabled.
4040
/// Unsolicited server-to-client messages and all server-to-client requests are also unsupported, because any responses
4141
/// might arrive at another ASP.NET Core application process.
42-
/// Client sampling and roots capabilities are also disabled in stateless mode, because the server cannot make requests.
42+
/// Client sampling, elicitation, and roots capabilities are also disabled in stateless mode, because the server cannot make requests.
4343
/// </remarks>
4444
public bool Stateless { get; set; }
4545

0 commit comments

Comments
 (0)