Skip to main content
MCP form elicitation asks the client for a flat object of primitives. FormElicitation builds that schema. server.ElicitAsync(form) sends it. When the tool is running as an MCP task, the SDK interceptor parks the form on the task for you.

Field by field

Create plus named starters. Best when the shape comes from metadata.

From a CLR type

From<T>() seeds public properties. Customize, then Exclude.

Inside a task

await server.ElicitAsync(form) is enough. Do not call the store yourself.

Pick an authoring style

Start empty and add fields by wire name. Keep the builder when you add several fields in a loop.
Create, From<T>, and every field facade implement IFormElicitationSource. The current node on the chain is a valid argument to ElicitAsync and SetInputRequestsAsync.

Start a form

From<T>().Build() with no chain is a valid form. Zero fields is a confirm dialog: the client shows the message and accept / decline. Create(null) and From<T>(null) throw ArgumentNullException. Whitespace messages are allowed at create and throw from Build().

Field kinds

Starters camelCase the name (FirstName → firstName) and Titleize the label (TimeToLive → "Time To Live"). .Title(...) overrides the label.
String after Email on the same name clears the format. Email / Uri / Date / DateTime on an existing string field set the format in place.
Between throws immediately when min > max. Setting Min then Max inverted throws from Build().
Untitled options emit enum. Any titled option emits oneOf / anyOf with const + title.
Duplicate option values throw immediately. An empty option list throws from Build(). A default that is not in the list throws from Build().

Required vs optional

Non-nullable properties and the required keyword seed as required. string? and int? seed as optional. .Required() / .Optional() override after seed.

From a type

From<T>() walks public instance readable properties. Unmappable members are skipped. Two properties that camelCase to the same wire name throw.
Nested objects, Dictionary<,>, object, TimeSpan, byte[], string[], int[], indexers, write-only properties, static properties. Exclude of a skipped member is a successful no-op.
Calling a starter for an existing name updates that field in place. A different kind resets kind-specific constraints and keeps name, title, description, and required.

Send the form

Content is IDictionary<string, JsonElement> keyed by camelCase wire names.
Wire names are camelCase. Enum members match Enum.GetNames (BlueGreen, not blueGreen). Native AOT callers pass JsonSerializerOptions from a JsonSerializerContext:

Tasks

A tool that awaits server.ElicitAsync(form) inside a task does not call the store. The SDK interceptor writes the form as an InputRequest and resumes when the client responds.
1

Tool awaits ElicitAsync

Same call as a non-task tool. Use the McpServer from the request context.
2

Interceptor parks the form

The task moves to InputRequired. The client polls tasks/get and fulfills the elicitation.
3

Client responds

InputResponseReceived completes the await. Bind or read result.Content and continue the tool.
Park a form yourself from a background worker or when you need a stable request id:
ToInputRequest() is the same payload ElicitAsync sends. Omit the request id to get a 32-character hex key.

Validation that throws

These fail at the fluent call or at Build() / ElicitAsync / SetInputRequestsAsync, not after the client answers.
An empty property list is legal. Nested property access in Exclude / expression starters (x => x.Address.City) throws ArgumentException.

API

FormElicitation

Create, From<T>, and Bind<T>.

FormElicitationBuilder

Field starters, Build, ToInputRequest.

McpServer.ElicitAsync

Send a form. Typed overloads infer T or bind a dynamic form.

IMcpTaskStore

SetInputRequestsAsync(taskId, form) when you park the form yourself.