## [Copy link to heading](#form-mode-vs-url-mode-in-mcp-elicitation:-how-to-choose-and-build-mcp-servers)Form mode vs URL mode in MCP elicitation: How to choose and build MCP servers

Imagine a tool call getting halfway through, and the server realizes it needs one thing it cannot infer. It has to pick between two accounts, or confirm a write that deletes data, or collect an API key it was never handed. Without a way to ask, the agent stalls or invents an answer. Model Context Protocol (MCP) elicitation lets the server ask, and it ships in two modes that aren't interchangeable.

Form mode sends a structured schema through the MCP client, which renders it as a form and returns typed values. URL mode sends the user out-of-band to a URL, and the data never touches the client.

The [2026-07-28 specification](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation) splits the two on data sensitivity. Non-sensitive input travels in-band through a form, and credentials travel out of band through a browser. Across shipped clients, though, the choice hinges on something else: support.

This guide covers the differences between the two modes as the current spec defines them, the security line drawn between them, and what shipping each one on Vercel looks like today.

Key takeaways:

- Form mode requests structured, non-sensitive input in-band through the MCP client, while URL mode sends the user out of band to a URL for credentials and third-party authorization.

- The spec prohibits form mode for passwords, API keys, access tokens, and payment credentials (MUST NOT), and it requires URL mode for those interactions.

- Under the 2026-07-28 spec, both modes run through Multi Round-Trip Requests. The server returns an `InputRequiredResult`, and the client answers by retrying the original call with `inputResponses` and an echoed `requestState`.

- Clients declare elicitation per request in `_meta.io.modelcontextprotocol/clientCapabilities`, and an empty `elicitation: {}` object means form mode only.

- The `elicitationId` field and the `notifications/elicitation/complete` notification are gone, and error code `32042` is retired, so URL mode written against 2025-11-25 needs a rewrite, not a patch.

- Form mode is close to universal among clients that implement elicitation at all, so ship it first and treat URL mode as an optional path for clients that declare it.

### [Copy link to heading](#what-are-the-differences-between-form-mode-and-url-mode-mcp-elicitation)What are the differences between form mode and URL mode MCP elicitation?

These are not two flavors of the same prompt. They are two different security postures, and the dimension teams underweight most is the data path. In form mode, data transits the MCP client and the model's context. URL mode data does not, and that single difference is why the spec assigns each mode a fixed job.

Form mode is for anything non-sensitive that a form can carry. URL mode is for credentials and third-party authorization, where routing the secret through the client would expose it. [MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) covers the client's access to your server, so URL mode fills the gap left by the authorization flow: your server obtaining access to a third-party API on the user's behalf.

Here are the dimensions where the two modes diverge in production:

| Dimension | Form mode | URL mode |
| --- | --- | --- |
| **Data sensitivity** | Non-sensitive input only, with passwords, API keys, tokens, and payment credentials prohibited (MUST NOT) | Required for credentials and third-party authorization |
| **Request parameters** | `message` plus `requestedSchema`, with `mode` optional and defaulting to form | `mode: "url"`, `message`, and `url`, all required |
| **Schema** | Flat objects, primitives only (string, number, Boolean, enum) | No schema, only a `message` and a `url` |
| **Client rendering** | A form in the client's own interface | Full URL shown, then a browser surface the client cannot inspect |
| **Consent model** | User fills, declines, or cancels | Explicit consent before opening, never pre-fetched |
| **URL handling** | Should not embed clickable URLs in fields | Must not embed credentials or pre-authenticated parameters, should use HTTPS |
| **Data path** | Transits the MCP client and model context | Stays between the user and the target site |
| **What** `**accept**` **means** | The user submitted data, returned in `content` | The user consented to open the URL, with `content` omitted and the outcome still unknown |

Every row is a consequence of that first one. Once you accept that credentials cannot pass through the client, the schema restriction, the browser handoff, and the consent model all follow. The next two sections take each mode on its own terms.

#### [Copy link to heading](#data-sensitivity-is-the-line-the-spec-draws)Data sensitivity is the line the spec draws

Form mode is prohibited for secrets, and the prohibition is normative rather than advisory. A compromised or manipulated server can request an API key via a seemingly ordinary form, and the model cannot reliably refuse it on the user's behalf. Keeping secrets out of the form is what limits exposure through prompt injection and confused-deputy attacks.

The prohibition is narrower than it first reads. It covers secrets and credentials that grant access or authorize transactions, so a name, an email address, or a username is not categorically off limits. Whether to request that kind of data through a form is left to the server, subject to the user's ability to review and decline.

#### [Copy link to heading](#form-mode-renders-in-the-client;-url-mode-opens-the-browser)Form mode renders in the client; URL mode opens the browser

Form mode produces an interface that the client controls, and the user's typed values come back through the same connection. URL mode hands off the interaction to a browser surface that neither the client nor the model can inspect, so the client learns only whether the user consented to open the link.

That handoff is the whole point, because it keeps the sensitive exchange between the user and the site that owns it.

#### [Copy link to heading](#the-consent-models-are-not-the-same)The consent models are not the same

Form mode returns one of three actions: accept, decline, or cancel. URL mode uses the same three actions, and it adds a consent step before anything opens. Clients must show the full URL, must not pre-fetch it or any of its metadata, and must open it somewhere the client and the LLM cannot read the page or the user's input.

The spec names the platform distinction directly, citing `SFSafariViewController` on iOS as acceptable and `WKWebView` as not.

Two softer rules sit on top. Clients should highlight the domain to blunt subdomain spoofing, and they should warn about ambiguous URIs, such as those encoded in Punycode. The heavier ceremony matches the higher stakes of sending someone to serve as an authenticator.

#### [Copy link to heading](#both-modes-run-as-multi-round-trip-requests)Both modes run as multi-round-trip requests

This change catches servers written against an earlier revision. Elicitation is no longer a server-initiated request on a live connection. Under [Multi Round-Trip Requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr), the server answers `tools/call` with an `InputRequiredResult` carrying `resultType: "input_required"`, an `inputRequests` map holding the `elicitation/create` requests, and an optional opaque `requestState`.

The initial request terminates there. The client gathers the answers, then re-issues the original call with an `inputResponses` map keyed to the same identifiers, echoing `requestState` back untouched. Servers may return an `InputRequiredResult` only on `tools/call`, `prompts/get`, and `resources/read`, and must not return one on any other request.

That applies to form mode as much as to URL mode, which most migration notes miss. Form mode is no longer a single round trip against a held-open connection, and any server that assumed one needs reworking, regardless of which mode it uses.

### [Copy link to heading](#a-deep-dive-into-how-form-mode-mcp-elicitation-collects-input-in-band)A deep-dive into how form mode MCP elicitation collects input in-band

Form mode is the in-band path, and it is the first option for anything a form can hold. The server sends an `elicitation/create` request with `mode: "form"`, or omits the mode entirely, along with a `message` and a `requestedSchema`. Clients must treat a request with no `mode` field as form mode, so servers written before the mode parameter existed keep working without changes.

The schema is deliberately narrow. It is limited to flat objects of primitives, meaning strings, numbers, Booleans, and enums, with no nested objects or arrays of objects beyond enum lists. Strings support the `email`, `uri`, `date`, and `date-time` formats; enums can carry display titles through `oneOf` or `anyOf`, and every primitive can carry a `default` that clients should pre-populate.

That constraint keeps the client's rendering job predictable and the returned values straightforward to validate.

#### [Copy link to heading](#what-form-mode-requests-and-returns)What form mode requests and returns

The client renders the schema as a form, the user fills it in, and the response comes back as an accept, decline, or cancel action, with typed content on accept. Form mode was introduced in the [2025-06-18 spec](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation), making it the older and more widely implemented of the two modes. Because the data is structured and non-sensitive by definition, the client can present it, and the model can reason over it, without crossing a security boundary.

The response now travels in the `inputResponses` map on the retried call rather than as a reply to a server-initiated request. The result shape is unchanged, so client-side handlers written for the older flow mostly survive. What changes is where the result gets delivered.

#### [Copy link to heading](#where-form-mode-fits)Where form mode fits

Form mode fills in the missing non-sensitive input that a server discovers mid-execution. A tool has most of what it needs, hits one gap, and pauses to gather only that gap rather than failing the call.

The situations where form mode is the right tool:

- Missing parameter mid-task: The server needs a value it could not infer from the initial call, such as a target environment or a display name. It pauses, asks, and continues.

- A choice between known options: The user has to pick one account, region, or resource from an enumerated set before the tool can proceed.

- A non-destructive confirmation: The tool requires a Boolean acknowledgment before it acts, and the acknowledgment itself contains no sensitive information.

The tradeoff is the boundary itself. Form mode buys broad client support and a well-understood rendering contract, and it costs you any ability to collect a secret. The moment the input is a credential, form mode is off the table, and URL mode starts.

### [Copy link to heading](#exploring-how-url-mode-mcp-elicitation-moves-credentials-out-of-band)Exploring how URL mode MCP elicitation moves credentials out of band

URL mode is the out-of-band path built for exactly the cases form mode forbids. The server sends `mode: "url"` with a `message` and a `url`, and it encodes whatever correlation it needs in the `requestState` on the enclosing `InputRequiredResult`.

The client shows the full URL, asks for explicit consent, and opens it in a browser surface it cannot inspect, never pre-fetched. The interaction stays entirely between the user and the target site.

An `accept` response in URL mode means the user consented to open the link. It does not mean the interaction finished. The interaction happens out of band, and the client is never told the outcome directly.

When the client retries the original request, the server decides from the echoed `requestState`, or from its own stored state, whether the out-of-band work completed, and it either returns the final result or answers with another `InputRequiredResult`. Clients should provide users with manual controls to retry or cancel the original request, since nothing else advances the flow.

URL mode was introduced in the 2025-11-25 revision through [SEP-1036](https://modelcontextprotocol.io/seps/1036-url-mode-elicitation-for-secure-out-of-band-intera), a Specification Enhancement Proposal written for sensitive credential collection, third-party OAuth flows, and payments. That SEP is now Final and preserved as a historical record, so treat the [current spec](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation) as the authority on what URL mode requires and the SEP as the record of why it exists.

#### [Copy link to heading](#how-url-mode-protects-credentials)How URL mode protects credentials

The security value is structural. Because the secret is entered on a page that the target site controls, it never passes through the MCP client or the model's context, so it cannot leak through either. The spec backs that boundary with hard server-side rules.

A server must not include credentials or personally identifiable information in the URL, must not hand over a URL that is pre-authenticated to a protected resource, and should use HTTPS outside development.

URL mode is not risk-free. Look-alike domains remain a phishing surface, and beyond the requirement to show the full URL and the guidance to highlight the domain and warn on suspicious URIs, the spec leaves trust signals to each client.

The out-of-band design still earns its place, because the credential stays outside the client and the model even when a look-alike slips through.

The spec also calls out a second phishing shape by name. Because a URL mode elicitation returns a link an attacker can forward, a server must verify that the user who opens the URL is the user the elicitation was generated for.

The recommended pattern is a connect route on the server that compares the browser session's subject against the `sub` claim the MCP authorization server issued, and only then forwards the user to the third-party authorization endpoint. Skip that check and a malicious user can have a victim complete an authorization that binds to the attacker's identity.

#### [Copy link to heading](#url-mode-elicitation-is-not-mcp-oauth-authorization)URL mode elicitation is not MCP OAuth authorization

These two get conflated, and the distinction matters for design. URL mode obtains authorization for your server to reach a third-party API on the user's behalf. [MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) handles a different relationship: the MCP client's authorization to your MCP server. One is your server reaching outward to another service. The other is a client reaching inward to you.

The spec is explicit that servers must not use URL mode to authorize users for themselves, and that a server must not pass the client's bearer token through to a third party. Doing so is [token passthrough](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#token-passthrough), which the security best practices document forbids outright.

In the correct arrangement, your server is an OAuth resource server to the MCP client and an OAuth client to the third party, and it stores the third-party tokens.

### [Copy link to heading](#choosing-between-mcp-elicitation-modes)Choosing between MCP elicitation modes

The spec makes the sensitivity call for you. Non-sensitive input is form mode, credentials are URL mode, and there is no discretion in that part. Your decision is narrower and more practical. Given the clients you are targeting, which mode can you rely on at all?

#### [Copy link to heading](#client-support-is-the-constraint-that-drives-the-choice)Client support is the constraint that drives the choice

Guidance written against earlier revisions tends to assume URL mode exists wherever elicitation does. Support has broadened since then, but it is still uneven, and for anything built on Vercel's AI SDK that unevenness sets the boundary. You cannot make URL mode a required path while the SDK declares form mode only, and some clients implement no elicitation in any mode.

The matrix below reflects what each client's own documentation, changelog, or source declares as of August 2026.

Read the two support columns first, then the notes:

| Client | Form mode | URL mode | Notes |
| --- | --- | --- | --- |
| VS Code | Yes | Yes | Form in [v1.102](https://code.visualstudio.com/updates/v1_102), URL in [v1.107](https://code.visualstudio.com/updates/v1_107), reworked form interface in [v1.112](https://code.visualstudio.com/updates/v1_112) |
| OpenAI Codex | Yes | Yes | Both modes advertised by default, with granular approval policies deciding whether prompts surface or auto-reject |
| Cloudflare Agents SDK | Yes | Yes | URL mode added in [agents 0.17.4](https://github.com/cloudflare/agents/pull/1903); a connection with no handler now advertises no elicitation at all |
| Claude Code | Yes | Documented | [Both modes in the docs](https://code.claude.com/docs/en/mcp), with `Elicitation` and `ElicitationResult` hooks; URL mode reported failing in an [open issue](https://github.com/anthropics/claude-code/issues/48164) |
| Cursor | Yes | Not documented | [Docs](https://cursor.com/docs/mcp) list elicitation without a mode breakdown; a [Windows hang](https://forum.cursor.com/t/mcp-elicitation-create-hangs-agent-on-windows-in-cursor-3-10-20-but-works-on-macos/165391) reported in 3.10.20 is unresolved |
| GitHub Copilot CLI | Yes | Not documented | Form input through elicitation since 0.0.421 per the [CLI changelog](https://github.com/github/copilot-cli/blob/main/changelog.md), with no URL mode entry |
| Goose (Block) | Yes | No | Form mode is on by [default](https://goose-docs.ai/docs/guides/mcp-elicitation) and times out after five minutes |
| Vercel AI SDK | Yes | No | `onElicitationRequest` handles form mode; the request schema carries no `url` field |
| Gemini CLI | No | No | The client [advertises roots only](https://github.com/google-gemini/gemini-cli/issues/28074), so requests come back as `Method not found` |
| Claude.ai | No | No | An elicitation [request](https://github.com/anthropics/claude-ai-mcp/issues/153) has been open since April 2026 |

Two patterns matter more than any single row. Form mode is close to universal among clients that implement elicitation at all, and URL mode has spread well beyond where it started, so writing URL mode off is no longer correct.

What has not arrived is uniformity. Several clients document elicitation without saying which modes they accept, two carry unresolved reliability bugs, and the declared capability is the only thing your server can actually read at runtime.

Treat the table as a snapshot, not a contract. Client support moves faster than any published matrix, and the versions above will be stale before long, so confirm the modes against the release notes of the clients you target and design the server to negotiate rather than assume.

#### [Copy link to heading](#how-capability-negotiation-decides-the-mode)How capability negotiation decides the mode

The negotiation makes the gap concrete, and the current spec moved where it happens. There is no `initialize` handshake anymore. Clients declare capabilities on every request under `_meta.io.modelcontextprotocol/clientCapabilities`, and a client that supports elicitation must declare it there:

```
{
  "_meta": {
    "io.modelcontextprotocol/clientCapabilities": {
      "elicitation": {
        "form": {},
        "url": {}
      }
    }
  }
}
```

An empty `elicitation: {}` object is read as form mode only, which is the backward-compatibility rule that lets older clients keep working. A client declaring the capability must support at least one mode, and servers must not send a request in a mode the client has not declared. What arrives in that `_meta` block, not your preference, sets the ceiling on every request.

The error codes tell you where you stand, and we renumbered them in this revision. When processing a request needs a client capability that was not declared, the server returns `MissingRequiredClientCapabilityError` with code `-32021` and a `data.requiredCapabilities` object naming what was missing.

On HTTP, that comes back as a `400 Bad Request`. A malformed request, including one missing required `_meta` fields, returns `-32602` for invalid params instead.

One code is worth knowing about precisely because it is gone. `-32042`, `URLElicitationRequiredError`, existed only in 2025-11-25 and is now [reserved and unusable](https://modelcontextprotocol.io/specification/2026-07-28/basic/index#error-codes). Implementations of the current protocol must not emit it, so a server still returning it is signaling to clients that no longer have a rule for reading it.

Skipping the negotiation step is what turns a mode mismatch into an opaque failure, where the tool call dies somewhere in the client and the real cause, an undeclared capability, never reaches the user.

#### [Copy link to heading](#a-decision-table-for-picking-a-mode)A decision table for picking a mode

With the sensitivity rule and the support reality in hand, the choice collapses to a short table. Match your scenario to the row:

| Scenario | Use | Why |
| --- | --- | --- |
| Non-sensitive structured data mid-task | Form mode | In-band and broadly supported |
| Passwords, API keys, payment credentials | URL mode | Form mode is prohibited (MUST NOT) |
| Authorize a client's access to your server | [MCP authorization](https://vercel.com/i/mcp-server-oauth-authorization) | Client access is an authorization concern |
| Authorize your server's access to a third-party API | URL mode | Out-of-band user authorization for your server |
| All inputs known before invocation | Tool parameters | No elicitation needed |
| Missing non-sensitive input mid-execution | [Form mode](https://github.blog/ai-and-ml/github-copilot/building-smarter-interactions-with-mcp-elicitation-from-clunky-tool-calls-to-seamless-user-experiences) | Pause and gather only what's missing |
| Clients that may lack URL mode | Form primary, URL optional | Negotiated capability sets the ceiling |
| Any mode on the 2026-07-28 protocol | [MRTR round trip](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr) | Server-initiated requests are no longer supported |

One caveat cuts across every row. A server that must serve both 2025-era clients and 2026-07-28 clients has two different delivery paths for the same elicitation, because one expects a server-initiated request and the other expects an `InputRequiredResult`. Keep those paths explicit and branch on the protocol version each request carries, rather than assuming a single shape holds everywhere.

### [Copy link to heading](#how-vercel's-stack-shapes-mcp-elicitation-choices)How Vercel's stack shapes MCP elicitation choices

Shipping an MCP server on Vercel touches two layers. mcp-handler handles transport on the server, and the AI SDK handles the client side if you are building the agent as well. Each layer shows where elicitation stands in practice and where theory and running code part ways.

#### [Copy link to heading](#the-ai-sdk-handles-form-mode,-not-url-mode)The AI SDK handles form mode, not URL mode

Teams read the spec, see that URL mode exists, and plan a credential flow around it, only to find the client cannot receive one. On Vercel's AI SDK, that plan stalls before it starts.

Elicitation shipped as part of the stable MCP client in [AI SDK 6](/blog/ai-sdk-6) and lives in the `@ai-sdk/mcp` package.

The client declares the capability at creation and registers one handler:

```
import { createMCPClient, ElicitationRequestSchema } from '@ai-sdk/mcp';

const mcpClient = await createMCPClient({
  transport,
  capabilities: {
    elicitation: {},
  },
});

mcpClient.onElicitationRequest(ElicitationRequestSchema, async request => {
  // request.params.message, request.params.requestedSchema
  return { action: 'accept', content: gatheredValues };
});
```

Two limits sit inside that snippet. The declared capability is the empty `elicitation: {}` object, which the spec reads as form mode only, and the request schema the SDK validates against carries `message` and `requestedSchema` with no `mode` or `url` field, so a URL mode request has nothing wired to receive it. The [package documentation](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools) covers the form mode path and the three response actions, and nothing else.

Check the version boundary against your server. The MCP client in `@ai-sdk/mcp` negotiates `2025-11-25` as its newest protocol version, which means it speaks the pre-MRTR shape and receives `elicitation/create` as a server-initiated request.

If your server has moved to 2026-07-28, the elicitation reaches this client through mcp-handler's compatibility path rather than through `InputRequiredResult`. For non-sensitive input that is workable. For credentials, it means routing users to an HTTPS page you control rather than through the SDK.

#### [Copy link to heading](#mcp-handler-moves-transport-while-the-sdk-orchestrates-elicitation)mcp-handler moves transport while the SDK orchestrates elicitation

Knowing which layer owns elicitation is where teams get stuck. It feels like a transport concern, so they look for it in the handler, find nothing, and assume it is unsupported.

[mcp-handler 2.x](https://vercel.com/changelog/latest-mcp-spec-now-supported-in-mcp-handler) serves the stateless 2026-07-28 protocol natively over Streamable HTTP, with a stateless compatibility layer for 2025-era Streamable HTTP clients on the same `/mcp` endpoint. It needs no Redis dependency and no session storage, and the deprecated HTTP+SSE transport is gone, with `/sse` and `/message` now returning `410 Gone`.

Its surface is transport, not elicitation. Search the [package](https://github.com/vercel/mcp-handler) for an elicitation API, config option, or example, and you will find none.

That absence is a layering decision, not a missing feature. mcp-handler takes the official `@modelcontextprotocol/server` package as a peer dependency, and version 2 of that SDK is where the `InputRequiredResult`, `inputRequests`, and `requestState` primitives live.

You return an `InputRequiredResult` from your tool handler using the server SDK's types, and mcp-handler carries it over the wire. Teams that go looking for `elicitation` in the handler and conclude the platform does not support it are reading the wrong layer.

The separation is deliberate, and it pays off at scale. One MCP server on Vercel [cut CPU usage in half](/blog/building-efficient-mcp-servers) after moving to Streamable HTTP, even with continued user growth, which is the kind of gain that only lands once traffic grows past the point where a persistent-connection transport stops keeping up.

#### [Copy link to heading](#migrating-a-url-mode-server-to-the-current-spec)Migrating a URL mode server to the current spec

A working URL mode flow can break on a version bump, and this one breaks in more than one place. A server built against 2025-11-25 minted an `elicitationId`, waited on a `notifications/elicitation/complete` notification, and could return `-32042` to say a URL elicitation was required. All three are gone.

The current path replaces them with state the server owns:

1.  Return an `InputRequiredResult` with `resultType: "input_required"`, an `inputRequests` entry holding the `elicitation/create` request, and a `requestState` that encodes your correlation identifier and the user it belongs to.

2.  Accept the client's retry of the original call, read `inputResponses` for the consent action, and read the echoed `requestState` to recover your context.

3.  Decide whether the out-of-band interaction finished. Return the final result if it did, or another `InputRequiredResult` if it has not.

Because the server encodes its own correlation identifier instead of waiting on a notification, any request can land on any stateless instance, which is exactly what you want behind a function-based deployment. The [MCP C# SDK](https://csharp.sdk.modelcontextprotocol.io/v2/concepts/elicitation/elicitation.html) makes the migration concrete. Its `ElicitAsync` throws `InvalidOperationException` with the message "Elicitation is not supported in stateless mode" on any Streamable HTTP request served under 2026-07-28, and the documented path is to throw `InputRequiredException` and let the SDK emit the `InputRequiredResult`.

That path works across both protocol eras, provided MRTR is available, so the SDK's own samples still guard with a support check before using it.

The Python side has landed the same pattern. [FastMCP](https://gofastmcp.com/servers/elicitation) reads `ctx.input_responses` and `ctx.request_state` inside the tool, returns an `InputRequiredResult` when input is missing, and caps the client-driven retry loop at ten rounds by default through `input_required_max_rounds`.

Its docs make the consequence explicit: the tool holds no state between rounds, and everything it needs travels on the request. If you are porting a server between ecosystems, that round-trip shape is now the portable part.

### [Copy link to heading](#ship-mcp-elicitation-the-way-the-spec-intends-with-vercel)Ship MCP elicitation the way the spec intends with Vercel

The failure that starts this whole problem is small and common. A tool call needs one more thing but has nowhere to ask for it, so the agent stalls or fabricates it. The spec's answer holds up through a breaking revision.

Non-sensitive input travels in-band through a form, credentials travel out-of-band through a URL, and the security boundary is worth keeping even when a client has not caught up. What changed in 2026-07-28 is the plumbing, not the boundary, and the servers that survived the change are the ones that never depended on a held-open connection in the first place.

Here is how Vercel supports each mode on the same primitives:

- mcp-handler 2.x transport: The handler serves the stateless 2026-07-28 protocol and a 2025-era compatibility layer from one `/mcp` endpoint, so old and new clients connect without a Redis session store.

- Fluid compute for MCP workloads: Optimized concurrency, dynamic scaling, and instance sharing absorb the long idle waits and bursty traffic typical of MCP servers, so you pay for compute you use rather than for connections you hold.

- AI SDK form mode handler: The `onElicitationRequest` path receives a server's schema and returns typed input, giving you the broadly supported mode with a handler that already exists.

- `@ai-sdk/mcp` orchestration: Elicitation is stable in the package, so the client side of the round trip is wired rather than something you assemble by hand.

- Vercel MCP as a reference: Vercel's own remote MCP server runs OAuth with per-client consent and explicit protection against confused-deputy attacks, a working example of the authorization boundary this guide describes.

[Start a Vercel project](https://vercel.com/new) and ship your MCP server on your first `git push`, or browse [vercel.com/templates](https://vercel.com/templates) to begin from a foundation you can grow into.

### [Copy link to heading](#frequently-asked-questions-about-mcp-elicitation)Frequently asked questions about MCP elicitation

#### [Copy link to heading](#does-vercel's-ai-sdk-support-url-mode-elicitation)Does Vercel's AI SDK support URL mode elicitation?

No. The `@ai-sdk/mcp` client supports form mode only. It declares `capabilities: { elicitation: {} }`, which the spec reads as form mode, and the request schema it validates carries `message` and `requestedSchema` with no `url` field. For credentials, route users to an HTTPS page you control instead.

#### [Copy link to heading](#what-changed-for-mcp-elicitation-in-the-2026-07-28-spec)What changed for MCP elicitation in the 2026-07-28 spec?

Server-initiated `elicitation/create` requests are gone. Both modes now run through Multi Round-Trip Requests, where the server returns an `InputRequiredResult` and the client retries the original call with `inputResponses` and an echoed `requestState`. The `elicitationId` field, the completion notification, and error code `-32042` were all removed.

#### [Copy link to heading](#what-happens-when-a-client-doesn't-support-elicitation-at-all)What happens when a client doesn't support elicitation at all?

The client declares no elicitation capability, and the server must not send elicitation requests that depend on it. A server that needs the capability returns `MissingRequiredClientCapabilityError` with code `-32021`, naming what was missing. Design tools so missing inputs can arrive as ordinary tool parameters instead.

#### [Copy link to heading](#does-elicitation-work-in-mcp-handler's-stateless-mode)Does elicitation work in mcp-handler's stateless mode?

Yes. Stateless elicitation uses the MRTR round trip, with the server encoding its context in `requestState` rather than holding a connection open. mcp-handler itself exposes no elicitation-specific API, because that orchestration lives in the SDK layer, not in the transport handler.

#### [Copy link to heading](#can-i-use-form-mode-to-collect-a-password-if-i-handle-it-securely-server-side)Can I use form mode to collect a password if I handle it securely server-side?

No. The prohibition is normative (MUST NOT), because the data would transit the MCP client and the model context regardless of how the server handles it afterward. Use URL mode and route the user to an HTTPS page you control, then bind the stored credential to the user's verified identity.