* Update dotnet-mcp-builder skill to ModelContextProtocol 2.x Align the skill with the C# SDK 2.0.0 release and the MCP 2026-07-28 spec: stable line is now 2.x, HttpServerTransportOptions.Stateless defaults to true, roots/sampling/MCP-channel logging are [Obsolete] (MCP9005) with the multi-round-trip input_required pattern as the replacement, discovery-first negotiation (server/discover) supersedes the initialize handshake, Mcp-Method/Mcp-Name routable headers, raw structuredContent for non-object results, required Tool.inputSchema, and the new ModelContextProtocol.Extensions.Tasks and ModelContextProtocol.Extensions.Apps packages (typed MCP Apps support replacing the hand-rolled _meta/ui:// pattern on 1.x). * Address Copilot review: Apps extension accuracy, header scope, capability ownership - packages.md: the Apps package replaces the manual _meta wiring, not the ui:// resource; note the experimental MCPEXP003 diagnostic; label the 1.x -> 2.0 list as highlights and add the OAuth/SSE runtime changes with a pointer to the full release notes. - transport-http.md: Mcp-Method is on every POST, Mcp-Name only on named invocations (tools/call, prompts/get, resources/read) - do not require it globally at gateways. - mcp-apps.md: current MIME type is text/html;profile=mcp-app (skybridge is a legacy draft value); document [McpAppUi] + WithMcpApps(). - server-features.md: roots/sampling are client capabilities, only logging sits on ServerCapabilities. * Correct stateful HTTP guidance: 2026-07-28 has no HTTP sessions Per the official SDK v2 elicitation docs, a server with Stateless=false refuses the 2026-07-28 revision so dual-path clients fall back to an initialize-capable revision; ElicitAsync cannot be used on 2026-07-28 Streamable HTTP at all. Reframe stateful HTTP as down-level compatibility mode and document the multi-round-trip pattern (InputRequiredException / InputRequest.ForElicitation, retry with InputResponses -> ElicitResult) as the current-protocol way to ask mid-tool, across SKILL.md, transport-http.md, and elicitation.md.
7.8 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: current through 2026-07-28 (elicitation is not among the v2 deprecations — sampling/roots/logging are). URL mode is the newer addition (originally 2025-06-18 had only form mode).
2026-07-28 changes the rules on HTTP. The current revision removes HTTP sessions and the server→client
elicitation/createrequest, soElicitAsynccannot be used on 2026-07-28 Streamable HTTP at all — a server withStateless = falserefuses that revision and serves clients via the legacyinitializefallback. The current-protocol way to ask mid-tool is the multi-round-trip pattern: throwInputRequiredExceptionfrom the tool (building requests withInputRequest.ForElicitation(...)), and on the client's retried call readcontext.Params.InputResponsesback as anElicitResult. It works under both protocol revisions and both session modes, including fully stateless HTTP. TheElicitAsyncguidance below applies to STDIO and down-level stateful HTTP — see the SDK elicitation docs for the full MRTR pattern.
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 for ElicitAsync: stateful transport (down-level revisions)
ElicitAsync 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) — which, on 2.x, means down-level clients on an initialize-capable revision (see banner above).
In stateless HTTP — including all 2026-07-28 Streamable HTTP — ElicitAsync throws (InvalidOperationException): there's no transport channel back. Use the multi-round-trip InputRequiredException pattern instead.
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.Content—Dictionary<string, JsonElement>?with the user's submitted values.nullif 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.