> ## Documentation Index
> Fetch the complete documentation index at: https://ai.easyaf.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Form elicitation

> Build MCP form schemas fluently, send them with ElicitAsync, and park them on a task.

MCP form elicitation asks the client for a **flat object of primitives**. [`FormElicitation`](https://ai.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/Elicitations/FormElicitation) builds that schema. [`server.ElicitAsync(form)`](https://ai.easyaf.dev/mcpessentials/api-reference/ModelContextProtocol/Server/McpServer) 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**

Start empty and add fields by wire name. Keep the builder when you add several fields in a loop.

```csharp theme={"dark"}
var result = await server.ElicitAsync(
    FormElicitation.Create("Tell us who you are")
        .String("firstName").Required().MinLength(1)
        .Email("email").Required()
        .Integer("age").Between(18, 120),
    cancellationToken);

if (!result.IsAccepted)
{
    return "cancelled";
}

var name = result.Content!["firstName"].GetString();
```

**From a type**

Seed from a DTO, then override titles, bounds, and required. `T` is inferred on `ElicitAsync`.

```csharp theme={"dark"}
public class ProductQuery
{
    public string? Filter { get; set; }
    public int Top { get; set; }
    public int Skip { get; set; }
}

var result = await server.ElicitAsync(
    FormElicitation.From<ProductQuery>("Query products")
        .Exclude(x => x.Skip)
        .Integer(x => x.Top).Min(1).Max(1000).Default(20),
    cancellationToken);

if (result.IsAccepted)
{
    Query(result.Content!);
}
```

**Metadata-driven**

Keep the builder. Add a field per runtime name (EDM property, query option, action parameter). Bind accepted content onto a DTO if you have one.

```csharp theme={"dark"}
var form = FormElicitation.Create($"How should we query {entitySet}?");
form.String("filter").Title("$filter").Description("OData filter expression");
form.Integer("top").Title("$top").Min(1).Max(1000).Default(20);

var result = await server.ElicitAsync<ProductQuery>(form, cancellationToken);
if (result.IsAccepted)
{
    Query(result.Content!);
}
```

> [!TIP]
>
> `Create`, `From<T>`, and every field facade implement [`IFormElicitationSource`](https://ai.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/Elicitations/IFormElicitationSource). The current node on the chain is a valid argument to `ElicitAsync` and `SetInputRequestsAsync`.

## Start a form

```csharp Create theme={"dark"}
var form = FormElicitation.Create("Need a shipping address");
```

```csharp From a type theme={"dark"}
var form = FormElicitation.From<CheckoutForm>("Confirm checkout");
```

```csharp Confirm dialog theme={"dark"}
var result = await server.ElicitAsync(
    FormElicitation.From<EmptyForm>("Delete this order?"),
    cancellationToken);
```

`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**
>
> ```csharp theme={"dark"}
> form.String("notes").MinLength(1).MaxLength(400).Default("n/a");
> form.Email("email").Required();
> form.Uri("homepage");
> form.Date("birthday");       // format date
> form.DateTime("startsAt");   // format date-time
> ```
>
> `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**
>
> ```csharp theme={"dark"}
> form.Integer("age").Min(0).Max(130).Default(42);
> form.Number("price").Between(0.5, 99.9);
> ```
>
> `Between` throws immediately when min > max. Setting `Min` then `Max` inverted throws from `Build()`.

> **Boolean**
>
> ```csharp theme={"dark"}
> form.Boolean("subscribe").Default(true).Optional();
> ```

> **Single-select and multi-select**
>
> Untitled options emit `enum`. Any titled option emits `oneOf` / `anyOf` with `const` + `title`.
>
> ```csharp Untitled theme={"dark"}
> form.Choice("color")
>     .Option("red")
>     .Option("blue")
>     .Default("red");
> ```
>
> ```csharp Titled theme={"dark"}
> form.Choice("color")
>     .Option("red", "Crimson")
>     .Option("blue", "Blue")
>     .Default("red");
> ```
>
> ```csharp Multi-select theme={"dark"}
> form.MultiChoice("roles")
>     .Option("admin", "Administrator")
>     .Option("user", "User")
>     .MinItems(1)
>     .MaxItems(2)
>     .Default("user");
> ```
>
> 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

**C# nullability**

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

```csharp theme={"dark"}
public class SignupForm
{
    public required string Code { get; set; }
    public required string? Note { get; set; } // still required
    public string? Hint { get; set; }          // optional
    public int Age { get; set; }               // required
    public int? Score { get; set; }            // optional
}
```

**Fluent override**

```csharp theme={"dark"}
FormElicitation.From<SignupForm>("Sign up")
    .String(x => x.Code).Optional()
    .String(x => x.Hint).Required();
```

## 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**
>
> | CLR | Field |
> | - | - |
> | `string`, `Guid`, `char` | string |
> | `Uri` | string, format `uri` |
> | `DateOnly` | string, format `date` |
> | `DateTime`, `DateTimeOffset` | string, format `date-time` |
> | `bool` | boolean |
> | integer types (`byte` … `ulong`) | integer |
> | `float`, `double`, `decimal` | number |
> | enum | single-select (titled from the member names) |
> | `[Flags]` enum, `TEnum[]`, `IEnumerable<TEnum>` | multi-select |

> **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**
>
> ```csharp theme={"dark"}
> FormElicitation.From<KitchenSinkForm>("Fill this out")
>     .Exclude(x => x.Nickname)
>     .String(x => x.Name).Title("Full name").MinLength(1)
>     .Email(x => x.Homepage)          // reconfigure a seeded URI as email
>     .Integer("extraCount").Min(0);   // extra string-named field
> ```
>
> 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

**Untyped**

Content is `IDictionary<string, JsonElement>` keyed by camelCase wire names.

```csharp theme={"dark"}
var result = await server.ElicitAsync(
    FormElicitation.Create("Need a name").String("name").Required(),
    cancellationToken);

if (result.IsAccepted)
{
    var name = result.Content!["name"].GetString();
}
```

**Typed From\<T>**

`T` is inferred. Cancel and decline leave `Content` default.

```csharp theme={"dark"}
var result = await server.ElicitAsync(
    FormElicitation.From<ProductQuery>("Query products"),
    cancellationToken);

if (result.IsAccepted)
{
    Use(result.Content!);
}
```

**Bind a dynamic form**

Specify `T` when the form was built from strings.

```csharp theme={"dark"}
var result = await server.ElicitAsync<ProductQuery>(form, cancellationToken);
```

Or bind later:

```csharp theme={"dark"}
var typed = FormElicitation.Bind<ProductQuery>(raw);
```

Wire names are camelCase. Enum members match `Enum.GetNames` (`BlueGreen`, not `blueGreen`). Native AOT callers pass `JsonSerializerOptions` from a `JsonSerializerContext`:

```csharp theme={"dark"}
var result = await server.ElicitAsync<ProductQuery>(
    form,
    ProductQueryJsonContext.Default.Options,
    cancellationToken);
```

## Tasks

A tool that `await`s `server.ElicitAsync(form)` inside a task **does not call the store**. The SDK interceptor writes the form as an [`InputRequest`](https://ai.easyaf.dev/mcpessentials/api-reference/ModelContextProtocol/Extensions/Tasks/IMcpTaskStore) 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:

```csharp Generated id theme={"dark"}
await store.SetInputRequestsAsync(taskId, form);
```

```csharp Explicit id theme={"dark"}
await store.SetInputRequestsAsync(taskId, "create-product", form);
```

```csharp InputRequest theme={"dark"}
await store.SetInputRequestsAsync(taskId, form.ToInputRequest());
```

`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.

| When | Exception |
| - | - |
| Empty or whitespace field name | `ArgumentException` |
| Negative `MinLength` / `MaxLength` / `MinItems` / `MaxItems` | `ArgumentOutOfRangeException` |
| `Between` with min > max | `ArgumentException` |
| Duplicate option value | `InvalidOperationException` |
| Empty option value or titled option with blank title | `ArgumentException` |
| Whitespace message at `Build` | `InvalidOperationException` |
| Choice with no options | `InvalidOperationException` |
| Default not in the option list | `InvalidOperationException` |
| `minLength` > `maxLength` (or min/max, minItems/maxItems) at `Build` | `InvalidOperationException` |
| Two `From<T>` properties that camelCase to the same name | `InvalidOperationException` |

> [!NOTE]
>
> An empty property list is legal. Nested property access in `Exclude` / expression starters (`x => x.Address.City`) throws `ArgumentException`.

## API

> **[FormElicitation](https://ai.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/Elicitations/FormElicitation.md)**
>
> `Create`, `From<T>`, and `Bind<T>`.

> **[FormElicitationBuilder](https://ai.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/Elicitations/FormElicitationBuilder.md)**
>
> Field starters, `Build`, `ToInputRequest`.

> **[McpServer.ElicitAsync](https://ai.easyaf.dev/mcpessentials/api-reference/ModelContextProtocol/Server/McpServer.md)**
>
> Send a form. Typed overloads infer `T` or bind a dynamic form.

> **[IMcpTaskStore](https://ai.easyaf.dev/mcpessentials/api-reference/ModelContextProtocol/Extensions/Tasks/IMcpTaskStore.md)**
>
> `SetInputRequestsAsync(taskId, form)` when you park the form yourself.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.