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
- Field by field
- From a type
- Metadata-driven
Start empty and add fields by wire name. Keep the builder when you add several fields in a loop.
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, email, URI, date
String, email, URI, date
String after Email on the same name clears the format. Email / Uri / Date / DateTime on an existing string field set the format in place.Integer and number
Integer and number
Between throws immediately when min > max. Setting Min then Max inverted throws from Build().Boolean
Boolean
Single-select and multi-select
Single-select and multi-select
Untitled options emit Duplicate option values throw immediately. An empty option list throws from
enum. Any titled option emits oneOf / anyOf with const + title.Build(). A default that is not in the list throws from Build().Required vs optional
- C# nullability
- Fluent override
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.
What maps
What maps
What is skipped
What is skipped
Nested objects,
Dictionary<,>, object, TimeSpan, byte[], string[], int[], indexers, write-only properties, static properties. Exclude of a skipped member is a successful no-op.Reconfigure and extra fields
Reconfigure and extra fields
Send the form
- Untyped
- Typed From<T>
- Bind a dynamic form
Content is
IDictionary<string, JsonElement> keyed by camelCase wire names.Enum.GetNames (BlueGreen, not blueGreen). Native AOT callers pass JsonSerializerOptions from a JsonSerializerContext:
Tasks
A tool thatawaits 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.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 atBuild() / 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.