Recursive AI

Ask the user to confirm a tool call in C#

OneModelContext is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

Model Context Protocol® (MCP) elicitation lets a tool ask the user for input. Here it asks for confirmation before continuing.

Terminal
dotnet add package RecursiveAI.OneModelContext

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using RecursiveAI.OneModelContext;
await ModelContext.ServeAsync(new DraftTools());
 
public partial class DraftTools
{
    [Tool]
    public async Task<string> Publish(
        RequestContext ctx)
        => await ctx.ConfirmAsync("Publish the draft?")
            ? "Published." : "Canceled.";
}

Unzip the example, then dotnet run.

The client passes onConfirm to ModelContext.ConnectAsync to answer the snippet’s yes/no question. The snippet at the top waits for a client; it prints no application text. The client must support elicitation. The core package generates registration code for the partial tool class.

The download runs a server and client inside the same program, passing messages directly in memory. Its client prints the question and automatically approves it; it does not display a real consent dialog or publish a file. In your application, the onConfirm callback should show the question and return the user’s answer. If the client did not declare elicitation support, the request fails with the missing capability rather than silently approving.

The full program

C#
using System.IO.Pipelines;
using RecursiveAI.OneModelContext;
 
var toServer = new Pipe();
var toClient = new Pipe();
var server = new ContextServer("example", "1.0.0");
new ExampleTools().AddTo(server);
using var stop = new CancellationTokenSource();
var serving = server.RunAsync(ContextTransport.Stream(
    toServer.Reader.AsStream(), toClient.Writer.AsStream()), stop.Token);
await using var client = await ModelContext.ConnectAsync(
    ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream()), onConfirm: (question, _) =>
    { Console.WriteLine(question); return ValueTask.FromResult(!args.Contains("--decline")); });
Console.WriteLine((await client.CallToolAsync("publish")).Text);
await stop.CancelAsync();
try { await serving; }
catch (OperationCanceledException) { }
 
public partial class ExampleTools
{
    [Tool]
    public async Task<string> Publish(RequestContext context)
        => await context.ConfirmAsync("Publish the draft?")
            ? "Published." : "Canceled.";
}

dotnet run prints:

Text
Publish the draft?
Published.

Variations

Decline instead of approving. Run the same download with:

Terminal
dotnet run -- --decline
Text
Publish the draft?
Canceled.

The callback returns false; the tool’s conditional expression selects Canceled.. No exception is needed for an ordinary refusal.


The short answer

For MCP elicitation in C# (Model Context Protocol®), put a RequestContext parameter on the tool and await ConfirmAsync. On the client, supply onConfirm when connecting. The callback receives the question and returns a Boolean. The library handles the protocol exchange; your application handles the user interface.

How do I use elicitation in a C# tool?

The tool method in the snippet at the top is the complete server-side decision. RequestContext is injected into the call and does not become an input property that a model must supply. ConfirmAsync returns whether the client approved the question. The tool then returns either success text or cancellation text.

Keep the continuation explicit. Asking a question is not the same as performing an action. This example returns a string; to publish a real draft, put that operation after an affirmative answer, not before the question. A refusal is a normal branch of application logic, not a malformed protocol request.

The client-side callback in the full program is deliberately automatic so the download finishes unattended. Replace that callback with your host’s dialog or command-line interaction when a person is using the application. Setting the callback also advertises the client’s ability to handle confirmation. It does not make another client support the feature.

How does a C# client answer an elicitation request?

Pass the callback to ModelContext.ConnectAsync, as the full program does. The callback gets the message and cancellation token. It can asynchronously wait for your UI and then return true or false. Keep that wait connected to the cancellation token so a disconnected caller does not leave a dialog waiting indefinitely.

The download demonstrates both outcomes through the same tool. The default run approves; --decline refuses. Those messages are application text chosen by the tool, not special protocol status strings. A client that lacks the capability cannot complete this flow: the multi-round-trip guide documents the capability error.

Does elicitation run a tool more than once?

On protocol revision 2026-07-28, yes. When an answer is missing, the server returns an input request and protected request state. The client collects the answer and retries; the handler runs again from the beginning. Code before the ask must therefore be safe to repeat. Reads are suitable there; appending a file, sending a message or charging a card is not.

On 2025-11-25 and earlier revisions, the ask is a request over the held connection and the handler awaits its answer without replay. Code that respects the replay rule works in both cases. Put values that must survive the retry in ctx.State, as described in the same guide; do not treat client-held request state as an authorization decision of your own.

A yes/no question is the narrow case. OneModelContext also documents ElicitAsync<T> for forms and ElicitUrlAsync for URL interactions. Use confirmation when the only answer you need is approval, rather than inventing a form just to carry a Boolean.

All onemodelcontext recipes