Copy link to headingForm 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 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 withinputResponsesand an echoedrequestState.Clients declare elicitation per request in
_meta.io.modelcontextprotocol/clientCapabilities, and an emptyelicitation: {}object means form mode only.The
elicitationIdfield and thenotifications/elicitation/completenotification are gone, and error code32042is 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 headingWhat 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 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:
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 headingData 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 headingForm 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 headingThe 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 headingBoth 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, 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 headingA 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 headingWhat 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, 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 headingWhere 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 headingExploring 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, 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 as the authority on what URL mode requires and the SEP as the record of why it exists.
Copy link to headingHow 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 headingURL 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 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, 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 headingChoosing 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 headingClient 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:
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 headingHow 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. 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 headingA 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:
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 headingHow 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 headingThe 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 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 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 headingmcp-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 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 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 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 headingMigrating 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:
Return an
InputRequiredResultwithresultType: "input_required", aninputRequestsentry holding theelicitation/createrequest, and arequestStatethat encodes your correlation identifier and the user it belongs to.Accept the client's retry of the original call, read
inputResponsesfor the consent action, and read the echoedrequestStateto recover your context.Decide whether the out-of-band interaction finished. Return the final result if it did, or another
InputRequiredResultif 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 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 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 headingShip 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
/mcpendpoint, 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
onElicitationRequestpath receives a server's schema and returns typed input, giving you the broadly supported mode with a handler that already exists.@ai-sdk/mcporchestration: 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 and ship your MCP server on your first git push, or browse vercel.com/templates to begin from a foundation you can grow into.
Copy link to headingFrequently asked questions about MCP elicitation
Copy link to headingDoes 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 headingWhat 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 headingWhat 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 headingDoes 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 headingCan 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.