tools/call. The first POST returns resultType: "input_required" with an elicitation/create form inside inputRequests. The client shows that form, then POSTs the same tools/call again with the answers in inputResponses. That retry is a new HTTP request. Everything the tool needs is in that body.
Protocol revision
2026-07-28. Transport: Streamable HTTP with WithHttpTransport(o => o.Stateless = true). Pattern: Multi Round-Trip Requests (SEP-2322).POST 1 asks
The tool throws
InputRequiredException. The SDK writes InputRequiredResult. That HTTP request is finished.The client collects
The host renders
message and requestedSchema. The user accepts, declines, or cancels.POST 2 answers
Same tool name and arguments, new JSON-RPC
id, answers under the same key the server used.Three names
These three strings travel in different places. Mixing them up is the usual first-try failure.Build the server
Packages:ModelContextProtocol.AspNetCore 2.x and CloudNimble.McpEssentials.Core.
Register the form
AddFormElicitation("ship", …) stores a keyed singleton IFormElicitationSource. ToInputRequest() seals the builder and wraps Build() as InputRequest.ForElicitation: method is elicitation/create, params.mode is "form", plus message and a flat requestedSchema.Write the tool
Return an immediate result (
string). Stamp the method with [McpElicitForm("ship")]. Call context.RequireForm("ship").First visit throws InputRequiredException. The SDK writes that as resultType: "input_required". That POST is finished. The retry returns the ElicitResult.IsAccepted is Action == "accept" (ordinal ignore-case). The other actions are "decline" and "cancel". Unset Action defaults to "cancel". If a required answer is still missing, RequireForm throws again. The client retries.What ToInputRequest() puts on the wire
What ToInputRequest() puts on the wire
For the
"ship" form above:method:elicitation/create(RequestMethods.ElicitationCreate)params.mode:"form"params.message:"Ship this order"params.requestedSchema: a JSON Schema object whoseproperties.trackingNumberis{ "type": "string", "title": "Tracking Number" }and whoserequiredlist contains"trackingNumber"
Schema profile
Schema profile
Form mode is a flat object of primitives:
Nested objects, arrays of objects, and other JSON Schema features are out of profile.
If the host already registers Tasks
If the host already registers Tasks
[McpElicitForm("ship")] is enough. WithTasksPlus selects McpTaskExecutionMode.Synchronous for that tool. [McpElicitUrl] is Optional so a task can outlive the POST. Other tools stay Optional.Call the tool
Every request carries protocol metadata in the body. Streamable HTTP also mirrors selected fields into headers; those headers must match the body.- POST 1 — ask
- The form answer
- POST 2 — answer
elicitation/create object is nested inside the tools/call result. It is part of InputRequiredResult.inputRequests.requestState. The retry omits it as well. If the server does send requestState, the client copies that string onto the retry unchanged.Required headers and _meta
Required headers and _meta
"elicitation": { "form": {} } and "elicitation": {} both advertise form mode.clientInfo is optional. Origin is optional; if it is sent and invalid, the spec requires HTTP 403.C# SDK client
CallToolAsync retries input_required through McpClientImpl.SendRequestAsync. The handler is required; without it the client throws InvalidOperationException("Server sent an elicitation input request, but no ElicitationHandler is registered.").
That client injects _meta (protocol version and capabilities) on every POST. Raw HTTP clients send it themselves.
Form-mode rules
What a form may collect
What a form may collect
Fields are a flat object of primitives. Clients that support defaults pre-fill
default. The server validates returned content against the schema it sent.Secrets and credentials
Secrets and credentials
Passwords, API keys, access tokens, and payment credentials use URL mode. Form mode is for ordinary structured values the host may see.
Accept, decline, cancel
Accept, decline, cancel
The client UI names the server that asked, and it offers accept, decline, and cancel. Handle all three on the retry.
requestState
requestState
When used,
requestState is an opaque string. If it affects authorization or business logic, integrity-protect it (HMAC/AEAD), bind it to the user, give it a TTL, and reject a mismatch. Treat it as attacker-controlled on the retry.First-try checklist
Complete this list before the first
tools/call.1
Host
WithHttpTransport(o => o.Stateless = true), WithTools<ShippingTools>(), MapMcp().2
Form and tool
AddFormElicitation("ship", …) and a [McpServerTool(Name = "ShipOrder")] method that returns string.3
Tasks, if already registered
[McpElicitForm("ship")] makes WithTasksPlus keep ShipOrder on tools/call.4
First visit and retry
RequireForm("ship") throws on the first visit. The retry returns the ElicitResult.5
Client
2026-07-28 in the header and in _meta, form elicitation advertised, and an ElicitationHandler (C# SDK) or an equivalent form UI (raw HTTP).6
Headers
MCP-Protocol-Version, Mcp-Method, and Mcp-Name match the body.7
Retry payload
New JSON-RPC
id, the same tool name and arguments, and inputResponses.ship.Related
Form elicitation
Create, From<T>, field kinds, XML, and analyzers.Analyzers
Compiler diagnostics for
From<T> and expression starters.