---
title: "Request input from users with elicitation"
description: "Ask the user for structured input or send them to a URL with useMcpElicitation()."
canonical_url: "https://mcp-toolkit.nuxt.dev/advanced/elicitation"
---
# Request input from users with elicitation

> Ask the user for structured input or send them to a URL with useMcpElicitation().

## What is Elicitation?

Elicitation lets a server ask the connected client for additional information **mid-request**. The MCP spec defines two modes:

- **Form mode** — present a structured form to the user and validate the response against a schema you define.
- **URL mode** *(spec 2025-11-25)* — redirect the user to an external page (sign-in, payment, account verification, …) and resume once they come back.

<prompt :actions="["copy","cursor","windsurf"]" description="Add MCP elicitation (form & URL)" icon="i-lucide-message-square-quote">

Add elicitation to my Nuxt MCP server (@nuxtjs/mcp-toolkit).

- Use useMcpElicitation() inside a tool handler (auto-imported)
- Form mode: pass a Zod raw shape via `schema` and a human-readable `message`
- The shape must be flat — primitives, single-/multi-select enums only (spec restriction)
- Always check the action: 'accept' | 'decline' | 'cancel' before reading content
- URL mode: pass a `url` and `message`; client opens the URL and reports back
- Use `confirm(message)` for a quick yes/no prompt
- Use `supports('form' | 'url')` before calling to gate cleanly when the client doesn't support it
- Wrap in try/catch and check for McpElicitationError (codes: 'unsupported', 'invalid-schema', 'invalid-response')

Docs: [https://mcp-toolkit.nuxt.dev/advanced/elicitation](https://mcp-toolkit.nuxt.dev/advanced/elicitation)

</prompt>

## When to Use Elicitation

<table>
<thead>
  <tr>
    <th>
      Use Case
    </th>
    
    <th>
      Mode
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Disambiguate input ("which project did you mean?")
    </td>
    
    <td>
      <code>
        form
      </code>
      
       with an enum
    </td>
  </tr>
  
  <tr>
    <td>
      Confirm a destructive action before running it
    </td>
    
    <td>
      <code>
        confirm
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Collect missing required parameters interactively
    </td>
    
    <td>
      <code>
        form
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Gather optional metadata (priority, tags, …) before submitting
    </td>
    
    <td>
      <code>
        form
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Prompt for sign-in or payment via an external page
    </td>
    
    <td>
      <code>
        url
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Trigger an OAuth consent flow
    </td>
    
    <td>
      <code>
        url
      </code>
    </td>
  </tr>
</tbody>
</table>

<callout color="info" icon="i-lucide-info">

Elicitation is **client-driven**. Even when you call it server-side, the client is the one rendering the form or opening the URL. Always handle the case where the user declines or the client doesn't support the requested mode.

</callout>

## `useMcpElicitation()`

Auto-imported. Must be called inside a tool, resource, or prompt handler.

```typescript [server/mcp/tools/release.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'create_release',
  description: 'Create a new release after asking for the channel',
  inputSchema: {
    name: z.string(),
  },
  handler: async ({ name }) => {
    const elicit = useMcpElicitation()

    const result = await elicit.form({
      message: `Pick a release channel for "${name}"`,
      schema: {
        channel: z.enum(['stable', 'beta', 'canary']).describe('Release channel'),
        notify: z.boolean().default(true).describe('Notify subscribers'),
      },
    })

    if (result.action !== 'accept') {
      return `Release cancelled (${result.action}).`
    }

    return `Created "${name}" on ${result.content.channel} (notify=${result.content.notify}).`
  },
})
```

The Zod shape is converted to the spec-restricted JSON Schema, the response is validated against the same shape, and `result.content` is fully typed.

### API

<table>
<thead>
  <tr>
    <th>
      Method
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        form({ message, schema })
      </code>
    </td>
    
    <td>
      Ask for structured input. <code>
        schema
      </code>
      
       is a Zod raw shape (same format as <code>
        inputSchema
      </code>
      
      ). Returns <code>
        { action, content? }
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        url({ message, url })
      </code>
    </td>
    
    <td>
      Open an external URL. Returns <code>
        { action }
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        confirm(message)
      </code>
    </td>
    
    <td>
      Convenience yes/no prompt. Returns <code>
        boolean
      </code>
      
       (<code>
        true
      </code>
      
       only when the user accepts and confirms).
    </td>
  </tr>
  
  <tr>
    <td>
      `supports('form'
    </td>
    
    <td>
      'url')`
    </td>
  </tr>
</tbody>
</table>

The `action` is one of `'accept' | 'decline' | 'cancel'`. The `content` field is only present when `action === 'accept'`.

## Schema Restrictions (Form Mode)

The MCP spec restricts elicitation requests to **flat objects with primitive properties** so any client can render them as a form. The toolkit enforces this at request time and throws `McpElicitationError('invalid-schema')` when you violate it.

Allowed:

- `z.string()`, `z.number()`, `z.boolean()`, `z.string().email()`, `z.number().int()`
- `z.enum([...])` — single-select dropdown
- `z.array(z.enum([...]))` — multi-select
- `.describe(...)`, `.default(...)`, `.optional()`

Not allowed:

- Nested `z.object({ ... })`
- `z.array(z.number())` or any array of non-string-enums
- `z.record(...)`, `z.tuple(...)`, `z.union(...)`, `z.discriminatedUnion(...)`

Need richer input? Split into multiple elicitation calls or take the data via the regular `inputSchema`.

## Confirm Helper

```typescript [server/mcp/tools/delete-project.ts]
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'delete_project',
  description: 'Delete a project after confirming with the user',
  inputSchema: { id: z.string() },
  handler: async ({ id }) => {
    const elicit = useMcpElicitation()

    if (!await elicit.confirm(`Permanently delete project ${id}?`)) {
      return 'Aborted.'
    }

    await deleteProject(id)
    return `Deleted ${id}.`
  },
})
```

`confirm()` builds on `form()` with a single `confirm: z.boolean()` field, so it inherits the same capability checks and decline handling.

## URL Mode

URL mode is opt-in per the spec — clients must declare `elicitation.url` in their capabilities. Use `supports('url')` to branch cleanly:

```typescript [server/mcp/tools/connect-github.ts]
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'connect_github',
  description: 'Connect the user GitHub account',
  inputSchema: {},
  handler: async () => {
    const elicit = useMcpElicitation()

    if (!elicit.supports('url')) {
      return 'Open https://app.example.com/settings/github to connect your account, then try again.'
    }

    const result = await elicit.url({
      message: 'Authorize the integration',
      url: 'https://app.example.com/oauth/github/start',
    })

    return result.action === 'accept'
      ? 'GitHub connected.'
      : `User did not complete the flow (${result.action}).`
  },
})
```

<callout color="warning" icon="i-lucide-shield">

URL-mode elicitation triggers a redirect on the user's machine — only use it for trusted endpoints. The MCP spec recommends pairing it with [origin/scheme validation](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) on your callback.

</callout>

## Capability Checks

Always handle the case where the client doesn't declare the elicitation capability — many clients (and self-hosted relays) advertise tools without it.

```typescript
const elicit = useMcpElicitation()

if (!elicit.supports('form')) {
  return 'This tool needs an interactive client (Cursor, Claude Desktop, …).'
}
```

When you call `form()` or `url()` against an unsupported client, the composable throws `McpElicitationError` with `code: 'unsupported'` so you can recover gracefully:

```typescript
import { McpElicitationError } from '@nuxtjs/mcp-toolkit/server'

try {
  await elicit.form({ message: '…', schema: { … } })
}
catch (err) {
  if (err instanceof McpElicitationError && err.code === 'unsupported') {
    return 'Client does not support elicitation — falling back to defaults.'
  }
  throw err
}
```

## Requirements

<callout color="warning" icon="i-lucide-triangle-alert">

`useMcpElicitation()` requires:

- `nitro.experimental.asyncContext` set to `true` (default since Nuxt 3.8+)
- A client that declared the `elicitation` capability during initialization

</callout>

## Next Steps

- [Sessions](https://mcp-toolkit.nuxt.dev/advanced/sessions) — store the user's elicitation answers across subsequent tool calls.
- [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) — gate which tools require elicitation based on context.
- [Dynamic definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) — hide tools behind `enabled` guards instead of asking interactively.
- [Logging](https://mcp-toolkit.nuxt.dev/advanced/logging) — record elicitation outcomes (`accept` / `decline` / `cancel`) on the wide event.


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
