Advanced Topics

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

When to Use Elicitation

Use CaseMode
Disambiguate input ("which project did you mean?")form with an enum
Confirm a destructive action before running itconfirm
Collect missing required parameters interactivelyform
Gather optional metadata (priority, tags, …) before submittingform
Prompt for sign-in or payment via an external pageurl
Trigger an OAuth consent flowurl
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.

useMcpElicitation()

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

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

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

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

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:

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}).`
  },
})
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 on your callback.

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.

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:

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

useMcpElicitation() requires:
  • nitro.experimental.asyncContext set to true (default since Nuxt 3.8+)
  • A client that declared the elicitation capability during initialization
Copyright © 2026