Elements

Elements · Tool use

Elicitation form

A server pausing mid-tool-call to ask you for the fields it still needs.

github-mcpneeds input

Confirm where the release notes should be published before the tool runs.

Repository *assistant-ui/assistant-ui
Visibility
PublicPrivate
Notify watchersOn
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-elicitation-form"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

An elicitation form is what a connected server shows when it needs structured input before it can finish: a message, one control per requested field, and a Decline/Send pair that settles into a confirmation once answered. With a runtime this comes from an MCP server's live elicitation request; standalone you supply the fields and state directly.

Getting started

MCP form elicitation is unstyled primitives, not a toolkit renderer: @assistant-ui/react-mcp exposes McpElicitationPrimitive, and you build the same look this element has directly from its parts.

Compose the form from McpElicitationPrimitive

components/assistant-ui/elements/mcp-elicitation-form.tsx
"use client";

import { PlugIcon } from "lucide-react";
import { McpElicitationPrimitive } from "@assistant-ui/react-mcp";
import { cn } from "@/lib/utils";
import { field, inkButton, mono, paper } from "@/components/assistant-ui/elements/surfaces";

function fieldKind(schema: unknown): "text" | "toggle" {
  return (schema as { type?: string } | undefined)?.type === "boolean"
    ? "toggle"
    : "text";
}

export function McpElicitationForm({ serverName }: { serverName: string }) {
  return (
    <McpElicitationPrimitive.Items>
      {() => (
        <McpElicitationPrimitive.Root
          className={cn(paper, "flex w-full max-w-sm flex-col gap-3.5 rounded-[20px] p-4")}
        >
          <div className="flex items-center gap-2.5">
            <span className="bg-foreground/[0.05] text-foreground/45 flex size-7 items-center justify-center rounded-lg">
              <PlugIcon className="size-3.5" />
            </span>
            <span className="min-w-0 flex-1 truncate text-[13.5px] font-medium">
              {serverName}
            </span>
          </div>

          <McpElicitationPrimitive.Message className="text-foreground/55 text-xs leading-relaxed" />
          <McpElicitationPrimitive.Error className="text-xs text-red-600 dark:text-red-400" />

          <div className="flex flex-col gap-2.5">
            <McpElicitationPrimitive.Fields>
              {({ name, schema, value, setValue }) => (
                <div className="flex flex-col gap-1">
                  <span className={cn(mono, "text-foreground/35")}>{name}</span>
                  {fieldKind(schema) === "toggle" ? (
                    <input
                      type="checkbox"
                      checked={value === true}
                      onChange={(e) => setValue(e.target.checked)}
                    />
                  ) : (
                    <input
                      className={cn(field, "rounded-lg px-2.5 py-1.5 text-xs")}
                      value={typeof value === "string" ? value : ""}
                      onChange={(e) => setValue(e.target.value)}
                    />
                  )}
                </div>
              )}
            </McpElicitationPrimitive.Fields>
          </div>

          <div className="flex h-8 items-center justify-end gap-2">
            <McpElicitationPrimitive.Decline className="text-foreground/55 h-8 rounded-full px-3.5 text-xs font-medium">
              Decline
            </McpElicitationPrimitive.Decline>
            <McpElicitationPrimitive.Accept
              className={cn(
                inkButton,
                "flex h-8 items-center rounded-full px-3.5 text-xs font-medium",
              )}
            >
              Send
            </McpElicitationPrimitive.Accept>
          </div>
        </McpElicitationPrimitive.Root>
      )}
    </McpElicitationPrimitive.Items>
  );
}

Mount it inside a connected server

MCPElicitation carries no server id of its own; the request is only ever reached through the mcpServer scope you're already inside when you list a connected server. Render the form in that same scope, for example beside each row of a server list:

{servers.map((server) => (
  <McpElicitationForm key={server.id} serverName={server.name} />
))}

See User-managed MCP servers for connecting servers and enabling their elicitation capability.

Anatomy

<div data-slot="elicitation-form">
  <div>{/* plug icon, server name, "needs input" tag */}</div>
  <p>{/* message */}</p>
  <div>{/* one row per field: label (+ "*" when required), then a pill group, a switch, or a static value chip depending on kind */}</div>
  <div>{/* Decline / Send while state is "request"; a settled "Sent to {server}" or "Declined" row otherwise */}</div>
</div>

state fully replaces the footer rather than disabling it: once it leaves "request", the Decline and Send controls are gone, not just inactive. A "choice" field renders every option in options as a pill, highlighting whichever one equals value, so the element trusts you to keep value one of the listed options. A "toggle" field reads truthiness from the literal string "true", not from a boolean.

Examples

Restyle the form

Both lanes take className on the root. The panel surface, the field labels, and the Send button read from the shared paper, mono, and inkButton tokens in surfaces.tsx.

<ElicitationForm className="max-w-none" /* ... */ />

Marking required fields

requestedSchema.required is a plain JSON Schema array; check membership per field to add the same marker the standalone element uses:

const required = new Set(
  (elicitation.requestedSchema as { required?: string[] })?.required ?? [],
);
// required.has(name) inside the Fields render prop

Declining vs canceling

The protocol has three terminal actions, not two: McpElicitationPrimitive.Cancel submits { action: "cancel" } alongside Accept's "accept" and Decline's "decline". Add it as a third control when your host distinguishes "explicitly declined" from "dismissed without answering":

<McpElicitationPrimitive.Cancel className="text-foreground/45 h-8 rounded-full px-3.5 text-xs">
  Dismiss
</McpElicitationPrimitive.Cancel>

API reference

McpElicitationPrimitive

PartRendersNotes
ItemsfragmentReads state.mcpServer.pendingElicitations; renders nothing while the list is empty. One Root subtree per pending request.
RootdivCarries data-elicitation-id.
MessagespanRenders elicitation.message unless you pass children.
ErrordivRenders the current validation or server error message; renders nothing when there is none.
Fieldsrender propCalls its children once per property in requestedSchema.properties, each already wrapped in that field's own { name, schema, value, setValue }.
AcceptbuttonDisabled while a required field is missing or an entered value is invalid. Submits { action: "accept", content }.
DeclinebuttonSubmits { action: "decline" }.
CancelbuttonSubmits { action: "cancel" }. Has no equivalent in the standalone element's two-state model.

MCP server state

SelectorTypeDescription
s.mcpServer.pendingElicitationsreadonly MCPElicitation[]Every request currently waiting on this server; read by Items.
aui.mcpServer.answerElicitation(id, response)(id: string, response: MCPElicitationResponse) => readonly { property: string; message: string }[] | undefinedWhat Accept, Decline, and Cancel call; returns validation errors instead of submitting when response.action === "accept" and a value is invalid.

Requires @assistant-ui/react-mcp and a connected server whose elicitation capability has not been set to false. See User-managed MCP servers.