Files
awesome-copilot/skills/dotnet-mcp-builder/references/elicitation.md
T
Adrien Clerbois 2c275f2ef9 feat(skills): add dotnet-mcp-builder, deprecate csharp-mcp-server-gen… (#1645)
* feat(skills): add dotnet-mcp-builder, deprecate csharp-mcp-server-generator

Adds a comprehensive skill for building MCP (Model Context Protocol)
servers in C#/.NET against the official ModelContextProtocol 1.x NuGet
packages. Covers both transports (STDIO, Streamable HTTP — SSE is
deprecated) and every primitive in the current MCP spec (2025-11-25):
tools, prompts, resources, elicitation (form + URL mode), sampling,
roots, completions, logging, and MCP Apps. Includes a thin .NET MCP
client reference and testing guidance (MCP Inspector + in-memory
transport for unit tests).

Steers the model toward the current stable 1.x packages instead of the
0.x previews it tends to pin by default, and enforces the STDIO
stdout/stderr trap.

Also deprecates the existing csharp-mcp-server-generator skill, which
predates ModelContextProtocol 1.0 and only covered a subset of the
current spec. Its SKILL.md now redirects users to dotnet-mcp-builder so
existing install URLs keep working without surprises.

* fix: address PR review from aaronpowell

- Delete csharp-mcp-server-generator skill (rather than deprecating it)
- Update mcp-apps.md pitfalls section to reference .NET Tool.Meta type
  instead of the serialized _meta JSON property names
- Rebuild docs/README.skills.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove C# MCP development plugin files

* chore: remove csharp-mcp-development plugin entry from marketplace

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 09:35:26 +10:00

6.5 KiB

Elicitation

Elicitation lets a tool ask the user for input mid-execution, via the client. The LLM doesn't see the question; the client surfaces it directly to the user. This turns one-shot tool calls into interactive flows — collecting confirmation, missing parameters, credentials (URL mode), etc.

Spec version: 2025-11-25. URL mode is the newer addition (originally 2025-06-18 had only form mode).

Two modes

Mode What it does When to use
Form (in-band) Server sends a JSON Schema; client renders a form; user submits values back through the same MCP channel. Confirmations, missing parameters, structured choices.
URL (out-of-band) Server sends a URL; client opens it in a browser; user completes the flow there; server checks state separately. OAuth, payments, anything the MCP channel must not see.

Prerequisite: stateful transport

Elicitation requires the server to send a request to the client and wait for a response. That only works on:

  • STDIO (always).
  • Stateful HTTP (options.Stateless = false).

In stateless HTTP, ElicitAsync will throw — there's no transport channel back.

Form mode — full example

using System.ComponentModel;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

[McpServerToolType]
public class BookingTools
{
    [McpServerTool, Description("Books a meeting room. Asks the user for confirmation.")]
    public static async Task<string> BookRoom(
        IMcpServer server,
        [Description("Room name")] string room,
        [Description("Start time (ISO 8601)")] DateTime start,
        CancellationToken ct)
    {
        var elicit = await server.ElicitAsync(new ElicitRequestParams
        {
            Message = $"Confirm booking '{room}' at {start:HH:mm}?",
            RequestedSchema = new ElicitRequestParams.RequestSchema
            {
                Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
                {
                    ["confirm"] = new ElicitRequestParams.BooleanSchema
                    {
                        Description = "Confirm the booking",
                        Default = true
                    },
                    ["notes"] = new ElicitRequestParams.StringSchema
                    {
                        Description = "Optional notes for the booking"
                    }
                }
            }
        }, ct);

        if (elicit.Action != "accept")
            return "Booking cancelled by user.";

        var confirmed = elicit.Content?["confirm"].GetBoolean() ?? false;
        var notes     = elicit.Content?["notes"].GetString() ?? "";

        if (!confirmed)
            return "User declined to confirm.";

        // …perform the booking…
        return $"Booked '{room}' at {start:O}. Notes: {notes}";
    }
}

Schema primitive types

You can build a RequestedSchema from these:

Type C# class Notes
String StringSchema Default, Description. Add JSON-Schema validation at server side if you need it.
Number NumberSchema Use for ints and floats.
Boolean BooleanSchema Renders as a checkbox / toggle.
Single-select enum (untitled) UntitledSingleSelectEnumSchema List of values; client renders as dropdown/radio.
Single-select enum (titled) TitledSingleSelectEnumSchema Each value has a display title.
Multi-select enum UntitledMultiSelectEnumSchema / TitledMultiSelectEnumSchema Multi-select dropdown / checkbox group.

Each accepts Description and Default.

Response shape

ElicitResult:

  • Action"accept", "reject", or "cancel". Always check this first.
  • ContentDictionary<string, JsonElement>? with the user's submitted values. null if the user rejected/cancelled.

Always handle the non-accept paths:

if (elicit.Action == "cancel")
    return "User cancelled. No changes made.";
if (elicit.Action == "reject")
    return "User declined.";
// Action == "accept" → safe to read elicit.Content

URL mode — full example

URL mode is for flows where the user must complete something outside the MCP channel — typically OAuth.

[McpServerTool, Description("Connects the user's GitHub account.")]
public static async Task<string> ConnectGitHub(
    IMcpServer server,
    IOAuthService oauth,
    CancellationToken ct)
{
    var elicitationId = Guid.NewGuid().ToString();
    var authUrl = oauth.BuildAuthorizationUrl(state: elicitationId);

    var result = await server.ElicitAsync(new ElicitRequestParams
    {
        Mode = "url",
        ElicitationId = elicitationId,
        Url = authUrl,
        Message = "Please authorize access to GitHub in the browser window that just opened."
    }, ct);

    if (result.Action != "accept")
        return "Authorization cancelled.";

    // The user has come back. Look up the persisted token by elicitationId.
    var token = await oauth.GetTokenByStateAsync(elicitationId, ct);
    return token is not null ? "Connected." : "Authorization did not complete.";
}

UrlElicitationRequiredException

When a tool is blocked on auth (rather than walking the user through it), throw UrlElicitationRequiredException. The client surfaces the URL to the user and the call fails cleanly. Useful for retry-after-auth patterns:

if (!oauth.HasValidToken)
{
    var id = Guid.NewGuid().ToString();
    throw new UrlElicitationRequiredException(
        "Authorization required",
        new[]
        {
            new ElicitRequestParams
            {
                Mode = "url",
                ElicitationId = id,
                Url = oauth.BuildAuthorizationUrl(state: id),
                Message = "Sign in to continue."
            }
        });
}

When NOT to use elicitation

  • Trivial confirmations the LLM can ask in natural language. If you can phrase "Should I do X?" in your tool's docstring and let the LLM ask, that's lower friction than a modal form.
  • Branching that the LLM should reason about. Don't replace the LLM's judgment with a form — only elicit for things the LLM literally cannot decide (user secrets, real-time consent, picking from a list only the user knows).
  • Stateless deployments. Doesn't work — see prerequisite above.

Client capability check

Don't blindly call ElicitAsync. Check first:

if (server.ClientCapabilities?.Elicitation is null)
    return "This client doesn't support elicitation; please pass the value as an argument.";

var elicit = await server.ElicitAsync(...);

This degrades gracefully on older clients.