If you run a remote MCP server — one your users hit over HTTP, behind a load balancer, not a local stdio process — you could not do elicitation until the 2026-07-28 spec. Now you can, and it takes about twenty lines. This is the how-to: what changed, and the exact round-trip to implement.

Elicitation is a server pausing a tool call to ask the user for structured input mid-execution — a missing date, which of three accounts to bill, a typed "yes, deploy." (Its mirror image is sampling, which asks the client's model instead of the human; we drew the full sampling vs elicitation distinction separately.) The feature isn't new. Being able to use it from a remote server is.

Why remote servers couldn't elicit before#

The old mechanism was a push. To ask a mid-call question, the server held a Server-Sent Events stream open and pushed the elicitation/create request down it while the tool sat blocked waiting for the answer.

That is fine for a local server bound to one client over one pipe. It falls apart the moment the server is remote and scaled. Put two replicas behind a round-robin load balancer and there is no guaranteed persistent socket to push down — the request that started the tool call and the packet carrying the user's answer can land on different instances. No held stream, nowhere to push, no shared memory to resume from. Every team that tried to run a real production MCP server hit this wall, which is the same wall that motivated the stateless core rewrite.

The fix: return the question, don't push it#

The 2026-07-28 spec makes the protocol stateless and introduces Multi Round-Trip Requests (SEP-2322). The inversion is the whole trick: instead of pushing a prompt down a stream, the server returns one as its result.

A tools/call that needs input comes back not with a final result but with an InputRequiredResult — a list of inputRequests (each an elicitation/create carrying a requestedSchema) plus an opaque requestState blob. The client renders the form, collects the user's answer, and re-issues the original tools/call with an inputResponses field and the echoed requestState.

Elicitation stopped being a live conversation over a socket and became a value the server returns and the client hands back. State rides in the payload, so any instance can resume.

That last property is why it works statelessly. Per-request context — capabilities, client info, and the round-trip's requestState — travels in _meta on the wire, not in a server-side session. The instance that answers the resumed call needs nothing it didn't receive in the request. This is continuation-passing style wearing a wire protocol.

The server-side handler#

Most SDKs wrap the return-and-reissue as an awaitable helper so you write it linearly. Under the hood the await is exactly the mechanism above: the SDK yields the InputRequiredResult, the runtime returns to the client, and the resumed call rehydrates your function at the same point. Here is a minimal booking tool that elicits a missing date, branching on all three actions:

server.registerTool("book_flight", { /* input schema */ }, async (args, ctx) => {
  // A required field is missing — ask the user instead of failing.
  const result = await ctx.elicitInput({
    message: "What date should I book this flight for?",
    // requestedSchema: flat object, primitive fields only.
    requestedSchema: {
      type: "object",
      properties: {
        date: { type: "string", format: "date", title: "Departure date" },
        window: {
          type: "string",
          enum: ["morning", "afternoon", "evening"],
          title: "Preferred time",
        },
      },
      required: ["date"],
    },
  });

  switch (result.action) {
    case "accept":
      // The user filled the form. Re-validate on YOUR side before trusting it.
      return bookFlight(args.origin, args.destination, result.content.date);
    case "decline":
      // Refused this field: take a documented fallback, don't re-ask.
      return { content: [{ type: "text", text: "No date given — nothing booked." }] };
    case "cancel":
      // Abandoned the whole interaction: stop cleanly.
      return { content: [{ type: "text", text: "Booking cancelled." }] };
  }
});

If your SDK doesn't expose an await helper yet, you implement the same shape by hand: return the InputRequiredResult and, on the reissued call, read inputResponses and continue. The three-way switch is identical either way.

Why you must handle accept, decline, and cancel#

They are three different user intents, and the spec keeps them distinct on purpose:

Collapse decline and cancel into one "no data" branch and you get tools that hang, retry forever, or silently do the wrong thing — the same failure mode as orphaning a long-running task. Handle all three explicitly.

The rules that keep it safe#

Four constraints, none optional:

  1. Never elicit secrets. Servers MUST NOT use elicitation to request passwords, API keys, or tokens. It's for the missing field, not the credential — route auth through proper OAuth, never a form.
  2. Keep requestedSchema flat and primitive. A single-level object of strings, numbers, booleans, and enums. No nested objects, no arrays of objects. Clients render these as forms; deep schemas don't render.
  3. Declare the capability. Elicitation is a client capability. Check that the client advertised it before you rely on it, and design a fallback (a tool argument) for clients that didn't.
  4. Only elicit while processing a request. Server-initiated requests may only be issued while the server is actively handling a client call — a prompt must trace to a user action, never appear out of nowhere.

Ship it#

The mental shift is small once you see it: don't reach out to the user, return a request for them and let the client bring the answer back. That single inversion is what lets an elicitation survive a load balancer — and it's why the feature that used to be a local-only nicety is now something your remote server can lean on. Wire up the three-way branch, keep the schema boring, and you've got interactive tools that scale horizontally like any other stateless endpoint.