> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcp-use.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Elicitation

> Request user input from MCP tools with form and URL flows.

Elicitation lets a tool return an `input_required` result. A capable client
collects input and retries the original tool call; the callback runs again with
the response in its request context.

Use form mode for ordinary structured input and URL mode when values must stay
on an external site.

## Handle the input-required round

Every call has a stable correlation key, a user-facing message, and either a
Standard Schema form schema or a URL:

```typescript theme={null}
import {
  acceptedContent,
  inputRequired,
  inputResponse,
  MCPServer,
} from "mcp-use";
import { z } from "zod";

const server = new MCPServer({
  name: "deployment-server",
  version: "1.0.0",
});

const approvalSchema = z.object({
  approve: z.boolean().describe("Approve this deployment"),
  note: z.string().max(200).optional(),
});

server.tool(
  {
    name: "deploy",
    inputSchema: z.object({
      environment: z.enum(["staging", "production"]),
    }),
    outputSchema: z.object({
      environment: z.string(),
      deployed: z.boolean(),
    }),
  },
  async ({ environment }, ctx) => {
    // A stateless handler starts from the top on both the initial call and
    // every retry. Inspect this round's response before deciding to ask again.
    const response = inputResponse(ctx.inputResponses, "deployment-approval");
    if (response.kind === "elicit" && response.action !== "accept") {
      return {
        isError: true,
        content: [{ type: "text", text: "Deployment was not approved." }],
      };
    }

    const confirmation = acceptedContent(
      ctx.inputResponses,
      "deployment-approval",
      approvalSchema,
    );
    // Missing or invalid accepted content means this invocation still needs
    // input, so return input_required instead of continuing.
    if (confirmation === undefined) {
      return inputRequired({
        inputRequests: {
          "deployment-approval": inputRequired.elicit({
            message: `Deploy to ${environment}?`,
            requestedSchema: approvalSchema,
          }),
        },
      });
    }

    if (!confirmation.approve) {
      return {
        isError: true,
        content: [{ type: "text", text: "Deployment was not approved." }],
      };
    }

    // Side effects belong after accepted, validated input because every
    // input_required round invokes this callback again from the beginning.
    await deployments.start(environment, confirmation.note);

    const data = { environment, deployed: true };
    return {
      content: [{ type: "text", text: JSON.stringify(data) }],
      structuredContent: data,
    };
  },
);

export default server;
```

The stable key correlates the embedded request with the matching value in
`ctx.inputResponses`. `inputResponse()` reads that response for you, and
`acceptedContent()` validates accepted form data against the schema. Missing or
invalid accepted content makes `acceptedContent()` return `undefined`, so the
handler returns another `input_required` round.

For an elicitation key, `inputResponse()` returns one of these. The full union
also carries `kind: "roots"` and `kind: "sampling"` for the other request types:

| Response                              | Meaning                                                                                          |
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `kind: "missing"`                     | No response for this key yet, so return another `input_required` round                           |
| `kind: "elicit"`, `action: "accept"`  | The user submitted; in form mode read it with `acceptedContent()`, URL mode carries no form data |
| `kind: "elicit"`, `action: "decline"` | The user explicitly refused                                                                      |
| `kind: "elicit"`, `action: "cancel"`  | The user dismissed or interrupted the flow                                                       |

## Form mode

Form mode accepts a Standard Schema that can be converted to JSON Schema. Keep
forms flat and use primitive fields, enums, and arrays of enum values. Add
descriptions and refinements before passing the schema to `inputRequired.elicit()`.

Do not collect passwords, API keys, payment details, or OAuth secrets in form
mode. Submitted form values pass through the MCP client.

## URL mode

Pass the absolute URL to `inputRequired.elicitUrl()` instead of a schema. Keep
the external flow's state in a trusted backend and look it up with a stable,
unguessable handle from the original tool input:

```typescript theme={null}
const flow = await githubAuth.getOrCreateFlow(connectionId);

// This is either the initial call (missing) or a fresh retry. Resolve the
// retry response first; only the missing case should request input.
const response = inputResponse(ctx.inputResponses, "github-authorization");
if (response.kind === "elicit" && response.action !== "accept") {
  return {
    isError: true,
    content: [{ type: "text", text: "Authorization was not completed." }],
  };
}

if (response.kind !== "elicit") {
  return inputRequired({
    inputRequests: {
      "github-authorization": inputRequired.elicitUrl({
        message: "Authorize GitHub access",
        url: flow.authorizationUrl,
      }),
    },
  });
}

const connection = await githubAuth.requireCompleted(flow.id);
```

URL mode asks the client to open an external flow; it does not return secrets or
prove that the flow succeeded. `getOrCreateFlow` must return the same
server-stored flow when the callback reruns with `connectionId`. Verify callback
state and completion on your own backend before performing a protected action.

## Keep multi-round workflows safe

Because the callback re-runs for each input-required round:

* Perform reads and validation before elicitation as needed.
* Perform irreversible side effects only after the relevant `accept`.
* Use a different stable key for each distinct question.
* Use verified `requestState` when a multi-step workflow needs trusted state.
* Treat bare `ctx.inputResponses` as untrusted client input if you read it
  directly.

## Run and test the example

```bash theme={null}
cd libraries/typescript/packages/server/examples/elicitation
pnpm dev
```

Connect a client that supports form and URL elicitation to
`http://localhost:3000/mcp`. Test accept, decline, cancel, an invalid form
response that triggers another round, and the URL callback-verification path.
