Async Human Input for Coding Agents
MCP elicitation asks whoever is running the agent, in the same session. An answer from a client who isn't there needs an asynchronous channel instead.
The gap: elicitation is synchronous and in-session
MCP's elicitation/create request (part of the stable 2025-06-18 specification) lets a server ask the connected client for structured information mid-call, nested inside whatever tool is running. The client must declare the elicitation capability at initialization, and the response comes back as one of three actions — accept with the submitted data, decline, or cancel. The message flow in the spec is a single round trip: server asks, client shows UI to the user in front of it, that user answers, the client replies to the server, and the original call continues.
That shape is exactly right for "confirm this before I proceed" or "which of these three options do you want" — questions the person driving the agent can answer in the next few seconds without leaving the terminal. It is the wrong shape for a question aimed at someone who is not part of the session at all. A client-intake workflow — "get the client's logo, hero copy, and CMS login before continuing the build" — needs an answer from a business owner who has never opened Claude Code and won't see any UI the MCP client renders. Elicitation has no path to that person; it only reaches whoever is on the other end of the current connection.
The spec also puts a second, sharper boundary on it: "Servers MUST NOT use elicitation to request sensitive information." That rules out asking for a password or an API key through elicitation even when the right person is in the session — one more reason credential collection needs its own mechanism rather than a bigger elicitation prompt.
Tasks: durable and pollable, but still the same client
The Tasks extension is worth naming precisely: it's an experimental, opt-in extension to core MCP (documented in the ext-tasks repository), not a ratified part of the base spec, and it requires both the client and the server to negotiate support for it explicitly — actual support varies by client. Where it applies, it solves a real piece of the puzzle: instead of blocking a connection for the length of a long-running operation, the server returns a durable taskId immediately, the caller polls tasks/get (or subscribes to notifications/tasks), and if the task needs input mid-flight it moves to an input_required state that's resolved with tasks/update.
That gets you two things elicitation alone doesn't: a handle that survives a disconnect, and a task that can sit in input_required for as long as it needs to instead of holding a connection open. What it does not get you is a way to reach someone outside the MCP session. The input_required state is still answered by whoever is operating the same connected client — a person approving a deployment inside the tool they're already using. A client who has never installed an MCP client and never will still isn't reachable through tasks/update, for the same reason elicitation can't reach them: both mechanisms resolve within the client-server connection, not outside it.
What reaching a third party actually requires
Getting an answer from someone who isn't in the session takes three things neither elicitation nor Tasks provide on their own:
- A channel that works without an MCP client — email, a link, a web form. The person needs to be reachable the way they're already reachable, not the way your agent is.
- State that outlives the request — something the agent can check back on minutes, hours, or days later, because the answer isn't coming in the next few seconds.
- Chasing — a way to prompt the person again if they don't respond, because a single email sent once is easy to miss or forget.
This is a product-level concern, not a protocol one — MCP defines how a server talks to a client, not how a client's operator reaches a third party who has no client at all.
The pattern with BriefGate
BriefGate's MCP tools are built around exactly this gap. The flow:
define_intakecreates the request — a list of items (text, files, ortype: "secret"for credentials) — and emails the client a portal link. The client fills it in from their phone or laptop; no account, no MCP client, no software to install.get_intake_statusis a cheap, synchronous check: how many items are in, which are still pending, when the client last opened the portal. Calling it is a normal step in an agent's loop, not an error path.manage_webhookregisters an HTTPS endpoint so the agent's process is notified the moment something changes (item.submitted,intake.completed,intake.stalled, and others), instead of polling on a guess.get_intake_resultsretrieves the typed, submitted data once it's in — including a one-time reveal for anysecretitem, since credentials never travel through the earlier steps in plaintext.
define_intake's response includes a follow_up block that names which of the two check-back strategies fits: "recommended": "webhook" when an active endpoint already covers the relevant events, or "schedule" with a concrete every_hours and an until timestamp when it doesn't. An agent doesn't have to guess how often to check back — the API states it.
Chasing is handled the same way a person would do it, minus the part where a person forgets: chase_schedule on define_intake (gentle, default, aggressive, or a custom interval) sends reminder emails automatically until the client finishes or the intake stalls, at which point intake.stalled fires with the specific missing_items still outstanding.
How the agent should behave while waiting
The behavior worth internalizing: a pending status is a normal answer, not a failure. An agent that calls get_intake_status and gets {"status": "in_progress", "progress": {"submitted": 2, "total": 5}} has learned something useful — three items are still outstanding — and should report that plainly and move on to whatever else it can do, rather than retrying in a tight loop or treating the incomplete state as an error.
Concretely:
- If a webhook is registered, don't poll at all — build (or continue other work) until the notification arrives, then react to it.
- If no webhook is registered, follow the
follow_up.scheduleinterval rather than inventing a polling cadence. - Never substitute an elicitation prompt for a question meant for the client. If the person who needs to answer isn't the one running the agent, that question belongs in
define_intake, notelicitation/create. - Never ask for a credential through elicitation or a chat message — the secrets vault exists precisely because the spec forbids the former and email leaves a permanent copy for the latter.
- On
intake.stalled, don't keep silently re-polling — surface it, since the chase engine has already exhausted its own attempts and a human now needs to intervene by some other channel.
The short version: elicitation answers "what does the person in front of me want right now," and — where a client and server both support it — Tasks answers "how do I not block a connection while that same person thinks about it." Neither answers "what does the client, who isn't here, want," and that's a separate, async channel by design.