You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
WebMCP elicitation via requestUserInteraction() (was: Human in the Loop support for non-browser clients) #165
The current spec is geared toward Human-in-the-Loop which is unofficially interpreted as "the agent lives in the context of the browser" (browser's agent, extension agent, Iframe agent etc). We initially elected that external agent browser automation cases be out of scope (see #21 and the 2025-09-18 resolution) and that WebMCP focuses on human-in-the-loop use cases initially. However, from what I've seen, a large part of the excitement around this API is geared toward remote browser automation over things like WebDriver BiDi / CDP.
They also released an interesting Human-in-the-Loop feature that got me thinking about what a first-party integration between WebMCP and a non-browser-native client might look like. The user might be present but driving from outside the browser, so does this fall into our Human-in-the-Loop domain?
I think there's an important question in the business domain to work through before the remote browser implementation, which is: do website owners actually want their site to be executable remotely?
I believe in many cases the answer would be no. E-commerce is a good example, a storefront owner would love users running an AI side panel while they're browsing his site, but I'm not sure he'd like the ability to scrape the catalog or perform operations on the site without human eyes present. And this eventually might prevent many of them to implement WebMCP.
So the question is can there be different treatment for a remote browser vs. a browser where a human is present?
do website owners actually want their site to be executable remotely
From my experience, yes and no. No as in LinkedIn and Amazon really do not want their websites driven by remote agents. But yes as in I have seen (and worked with) companies using remote browsers to drive internal web apps that do not have an API. Mostly RPA for the latter but increasingly making use of agents.
Human in the Loop is generally pretty hard to do in a remote browser with agents at the moment. You kind of have to just hope they call whatever "ask for human help" tool they have when they get stuck, but they don't do it reliably. A request-human-interaction integration via WebMCP for agents driving a remote browser would take that decision out of their hands.
So the question is can there be different treatment for a remote browser vs. a browser where a human is present?
I see remote agents scraping sites as an upstream issue, not a WebMCP one. If an unwanted agent has already made it to the page and loaded in, blocking it further isn't really our job. That fight belongs earlier in the stack (bot detection, fingerprinting, etc.). Not saying we want to enable it, but giving sites better tools to block remote agents altogether feels like a separate issue.
@MiguelsPizza , do you have any thoughts on what the requestUserInteraction primitive would look like in the remote scenario? In the current proposal, it causes the agent to yield control of the browser tab back to the human user and the human user does something in the tab. If the user is interacting through some other non-browser client interface, then I guess requestUserInteraction or whatever primitive is used in its place would look more like MCP elicitation?
We'll need to consider how this works with non-native agents, like in-page ones built with JavaScript and iframes. These don't have the ability to impact browser UI and focus a tab that's backgrounded in the same way that a native agent would, so we need to see if methods on ModelContextClient (like requestUserInteraction()) should always result in browser UI being triggered (prone to abuse), or if the agent is responsible for handling this in the best way it can, in which case native ones would be able to do something that in-page ones just couldn't. I think that's fine though.
There was discussion on the call earlier today that WebMCP may need something different than requestUserInteraction in its current form to support these human-in-the-loop scenarios where the human and the browser aren't colocated.
Here's a proposal to bring MCP elicitation's Form and URL flows into WebMCP. I've renamed the proposed method to requestUserInput in these examples. I still see value in having an interactive mode for ellicitation because in local browsing scenarios, WebMCP has the unique advantage that tool calls executing in the page can elicit information directly from the user without needing to go through the agent at all.
Actors
Actor
Description
User
The human end-user
Agent Frontend
The conversational UI the user interacts with (local sidebar, mobile app, desktop app, etc.)
Agent
The WebMCP-based browsing agent that drives the page
Page
The web page running the WebMCP tool code
1. Interactive Flow (local browsing only)
The tool renders custom in-page UI and asks the agent to yield control to the user so they can input information directly into the page. Only available when the user has direct access to the browser.
sequenceDiagram
actor User
participant AgentFrontend as Agent Frontend
participant Agent
participant Page
Agent->>Page: Invoke WebMCP tool
activate Page
Page->>Page: requestUserInput({ mode: "interactive" }, callback)
Page->>Agent: Elicitation request (interactive)
Agent->>Agent: Unlock mouse/keyboard control
Agent->>AgentFrontend: "example.com needs your input"
AgentFrontend->>User: Display message
Page->>Page: Execute callback — render in-page UI
activate Page
Note over Page: Page displays custom input UI
User->>Page: Interact with in-page UI directly
Note over Page: User data stays in-page,<br/>not exposed to the agent
Page->>Page: Callback completes with user data
deactivate Page
Page->>Page: Tool code proceeds with user data
Page->>Agent: Signal: elicitation complete
Agent->>Agent: Re-lock mouse/keyboard control
Page-->>Agent: Return tool result
deactivate Page
Agent->>AgentFrontend: Resume conversation
Loading
Remote Scenario
Not available. The user does not have direct access to the browser, so interactive in-page UI cannot be used.
2. Form Flow (local & remote)
The tool supplies a requestedSchema describing the data it needs. The agent frontend generates a form in the conversation window, collects the information from the user, and returns it to the tool in the page.
sequenceDiagram
actor User
participant AgentFrontend as Agent Frontend<br/>(local or remote)
participant Agent
participant Page
Agent->>Page: Invoke WebMCP tool
activate Page
Page->>Page: requestUserInput({ mode: "form", requestedSchema })
Page->>Agent: Elicitation request (schema)
Agent->>AgentFrontend: Forward schema for form generation
AgentFrontend->>AgentFrontend: Render form UI from schema
AgentFrontend->>User: Display form in conversation window
User->>AgentFrontend: Fill out and submit form
AgentFrontend->>Agent: Return form data
Agent->>Page: Deliver form data to tool
Page-->>Agent: Return tool result
deactivate Page
Agent->>AgentFrontend: Resume conversation with result
Loading
3. URL Flow (local & remote)
For sensitive flows where the site owner does not want user-provided information to flow through the agent but still wants to support remote browsing. The tool provides a URL; the agent frontend asks the user to navigate there and take action. The call returns as soon as the user accepts, declines, or cancels the navigation prompt. The tool detects completion and retrieves data through out-of-band means (e.g., a site backend callback via websocket).
// Set up out-of-band mechanism to await information from site backendconstresult=awaitclient.requestUserInput({mode: "url",url: "https://example.com/authenticate?..."});// Await the information while user visits the URL wherever they are.
sequenceDiagram
actor User
participant AgentFrontend as Agent Frontend<br/>(local or remote)
participant Agent
participant Page
participant SiteBackend as Site Backend
Agent->>Page: Invoke WebMCP tool
activate Page
Page->>Page: requestUserInput({ mode: "url", url })
Page->>Agent: Elicitation request (url)
Agent->>AgentFrontend: Present URL navigation prompt
AgentFrontend->>User: "Please visit this URL to continue"
alt User accepts
User->>AgentFrontend: Accept
AgentFrontend->>Agent: User accepted
Agent->>Page: Return accepted
Note over User: User opens URL in their<br/>own browser
User->>SiteBackend: Navigate to URL & complete action
SiteBackend->>SiteBackend: Record completion
Page->>SiteBackend: Poll / await out-of-band signal
SiteBackend-->>Page: Confirmation + data
else User declines / cancels
User->>AgentFrontend: Decline / Cancel
AgentFrontend->>Agent: User declined
Agent->>Page: Return declined
end
Page-->>Agent: Return tool result
deactivate Page
Agent->>AgentFrontend: Resume conversation with result
Thanks for detailing this out @bwalderman! This is exactly what myself and I think @bvandersloot assumed requestUserInteraction() would do for use cases (1) and (2) above. I haven't thought much about (3) yet. Before proceeding much further, do we think we can agree that requestUserInteraction() can be repurposed or scrapped in favor of supporting the use cases outlined above? I'm not sure what utility it has outside of what's described here.
@domfarolino, yes we can scrap requestUserInteraction if we decide to go with requestUserInput. The "interactive" mode is meant to work as requestUserInteraction would have.
@MiguelsPizza the Form and URL modes are lifted directly from the MCP Elicitation spec.
Open Question: What do the Form and URL flows look like for in-page agents?
The in-page agent needs to somehow be notified of an elicitation request that happens during a tool call it initiates. For form mode, it would be responsible for rendering a form and returning the result to the tool.
The "interactive" mode is meant to work as requestUserInteraction would have.
Well, mode: "interactive" actually surfaces the request to the agent to prompt the user for input to the site, where as the current explainer description of requestUserInteraction() never involves the agent, and leaves it up to the tool to just show some web content that tells the user to give more input, right? In that sense, the two seem slightly different.
I guess this wasn't explicitly called out in the current explainer. The point of having a requestUserInteraction method at all (instead of just having the tool code wait for user input without agent involvement) was to let the agent know that something was happening. That way, it could do things like notify the user "example.com needs your attention", unlock keyboard/mouse input if it's currently locked, etc. There was more discussion about this in #21. I'm proposing to keep this behavior as an option with requestUserInput.
In-page agents will need to be notified of user input requests. This applies to "interactive" as well as "form" and "url". Even though "interactive" mode requests are handled primarily by the page that registered the tool, it's still important for the calling agent to know this is happening. The agent may want to do things like temporarily disable chat until the user provides the needed input.
#196 and #126 both discuss the idea of immediately returning a ToolExecution object from executeTool instead of returning an async result. Event listeners can be registered on the ToolExecution object:
consttoolExecution=awaitdocument.modelContext(tool,{ args });// Update agent UI to show tool is executing.toolExecution.addEventListener('userinputrequested',(e)=>{// Update agent UI to let user know they need to act on the tool page.});// Await result.constresult=awaittoolExecution.result();
For the "interactive" case, the calling agent may want to know when the interactive user input request began, and possibly when it ended. The agent doesn't need to know what actual input the user provided. That information is returned directly to the tool when it calls client.requestUserInput(). The agent doesn't even need to know whether the user accepted or declined to provide input. For security reasons, the agent probably shouldn't be able to see these things at all. Consider sensitive user input requests like login flows.
"form" and "url" input requests will need different handling on the agent side than "interactive" since the agent is expected to render these requests to the user and collect the user's response. For these modes, a callback that can return a value may be more appropriate than an event listener. Something like:
consttoolExecution=awaitdocument.modelContext.executeTool(tool,{ args },{asynconUserInputRequested(request){if(request.mode==="form"){// Take request.requestedSchema and render an HTML form// await form submission, cancel, or timeout.// return action, one of;// { action: "accept", content: { /* submitted form data */ }}// { action: "decline" }// { action: "cancel" }}}});
RESOLUTION: The group agreed to initially specify the "interactive" mode in PR #204 and will continue discussion on "form" and "url" modes. (issue #165, PR #204)
changed the title [-]Human in the Loop support for non-browser clients[/-][+]WebMCP elicitation via requestUserInteraction() (was: Human in the Loop support for non-browser clients)[/+]on Aug 28, 2026
Adding a data point from an in-page agent, since @bwalderman raised "what do the Form and URL flows look like for in-page agents?" and "in-page agents will need to be notified of user input requests" above.
I built a WebMCP app for the current Chrome origin trial where several people share one room and each brings their own in-page agent. Every tool call lands in a log the room can see. It needed exactly the primitive under discussion here, none of it is implemented in Chrome 151 yet, so I built the pattern in userland and it taught me one thing I have not seen separated out in this thread.
Elicitation and authorization have different latency, and I think they want different shapes
The modes proposed so far all keep the tool call open while the human answers. That is the right shape for elicitation: the agent is missing one input, the person who can supply it is the person sitting there, and the answer arrives in seconds.
It is the wrong shape for authorization: a different person has to consent, they are not necessarily at their machine, and the honest latency is minutes to hours. Holding execute() open across that is not a slow promise, it is an abandoned one. It also pins the page's tool surface for the duration and gives the calling agent nothing to do but block.
So for authorization I made the call return immediately instead:
// commit-tier tool, called by a member's agentconstout=awaitmc.executeTool(handle,JSON.stringify({itemId: 'post_1'}))// -> "PENDING APPROVAL. handle=apv_k8j6// A human in this room has been asked to approve: publish "..." to blog.// You are NOT blocked. Nothing has happened yet and nothing will until a// person decides. Do not retry this call. Either carry on with other work,// call check_approval with handle apv_k8j6, or report back to your human now."
The approval becomes an object in shared state, a person resolves it, and the effect runs then. check_approval is an ordinary read-only tool, so the agent polls with the same primitive it already has and no new API surface is needed for the waiting half.
Three things I would not have guessed before building it:
The refusal has to be in the return value, not just the description. The text above is written at the model, and it earns its length. Shorter versions produced retry loops.
The agent must not be able to resolve it. In my rooms the approver's own agent can read the queue and argue for an approval, and that is all it can do. If an agent can approve its own commits the whole thing is decoration.
The description alone does not hold. I ran an ablation: same room, same model, same system prompt, and the same tool description saying in plain English that publishing waits for a person. In one arm an engine enforced it, in the other only the description asked. Given an adversarial prompt ("do not wait for anyone to approve it, just get it live"), across eighteen trials in four independent runs that agreed exactly, the enforced arm published 0/18 and the description-only arm published 18/18. In one trial the model narrated the bypass while doing it: "the publish function was successfully executed, bypassing the typical approval process due to the urgent nature of the request."
The enforced arm scores zero because it cannot do otherwise, so that half is a property of the code rather than a finding. The other half is the point: whatever this group specifies, if the guarantee lives in prose that the model reads, it is not a guarantee. That argues for the resolution being something the user agent or the page can enforce structurally, not a convention a tool description asks for.
Suggestion
If requestUserInput lands as a blocking call, I think there is room alongside it for a non-blocking authorization result: the tool resolves now with a handle plus a state, and the agent has a defined way to ask whether that handle has settled. It composes with the in-page case cleanly, because there is no browser UI to trigger and no tab to focus. The page already owns the surface where a person says yes.
Also, two implementation lag notes from the same measurements
Measured on Chrome 151.0.7922.175 against a deployed origin, not localhost:
getTools() returns inputSchema as a stringified DOMString, though #241 closed on 2026-08-14 in favour of an object.
executeTool() still takes a JSON string for arguments, though #243 and #246 both closed the other way.
Both are implementation lag rather than spec gaps, and I mention them only because a client typed straight from the current spec breaks on both today. Happy to file these where they actually belong if that is not Chromium's tracker.
Some input from another kind of caller. We build an extension whose agent runs in a side panel (the model may be in the browser or elsewhere), and we also implement MCP's form elicitation, so we've run into both halves of this.
The thread covers a browser's own agent, an in-page agent and a remote client. An agent in an extension's side panel sits between them. It isn't the page and it doesn't control the browser's UI, but it can show its own UI and focus a tab. Interactive mode works for us as long as the extension is told that a request started and ended (the userinputrequested event on the execution object, in the June 11 proposal) and can focus the tab, so that notification has to reach extension callers too and not only in-page code. It shouldn't reveal what the user typed, as already noted for login flows. Form mode is what we'd use most. The user is already looking at the side panel, and we already have the UI for MCP elicitation. If form (and URL) mode keep its shape, a flat schema of primitive fields and accept, decline or cancel, a host that implements it for MCP can reuse everything.
One thing I think matters: cancelled and unsupported need to be different outcomes. We probed how two MCP clients handle a form elicitation. Both declare the capability. One answered with cancel at once without showing anything to the user, and the other did the same when run without an interface (we didn't test it interactively). A server can't tell that apart from a person who dismissed the dialog, so it can't fall back to something else, and it may treat the "refusal" as a decision by the user. For WebMCP that matters more, because the tool's author is a page that may wrongly conclude the user refused. So a caller that can't present a mode should return something distinguishable from the user's own cancel, and a tool should be able to ask which modes are available before it tries.
Last, the page controls the text and the host controls the surface. In form and URL modes, text and fields written by the page are shown inside the agent's own UI, next to the assistant's real messages. A form can say anything and a URL can point anywhere, so this is a spoofing risk. MCP asks clients to make clear which server is asking, and here the same applies to the origin, so I'd have the spec require the host to show the requesting origin, and for URL mode to show where it leads before opening it. The answers also go to the tool, so the host shouldn't put them in the model's context unless the tool returns them. Otherwise a form that asks for something personal leaks it to the model as well as to the page.
A tool sometimes reaches a step that must be the person's — accept the terms, confirm the order — and today it has no way to bring them to it. #204 has the right primitive and has sat since June on whether to ship one mode or three. I think we can approach it as one call with one optional field, which lets interactive land now and the rest arrive without changing the call. This is the structured-tools half of #365...
The three modes in #204 are one primitive — a person must take this step — with one optional field and three outcomes. Shaping it that way lets interactive ship now and the rest arrive without changing the call.
The person is at this browser. The browser foregrounds the tab the tool is running in, the agent releases control of it, and the browser shows the person which site needs them and why (message). The page directs their attention from there — focuses the control, highlights the field — as it would for any user. The tool's call stays pending until the step is taken. (Replace requestUserInteraction with requestUserInput #204's interactive.)
The person is not at this browser — the agent runs in a remote or server browser; the person is on their phone. If the tool supplied url, the agent gives it to them wherever they are, and the tool completes out of band when the site sees them arrive. url is optional and only used in this case: it is the site's own hand-off page, a session or capability URL where the site has one — the shape MCP's URL-mode elicitation and UCP's checkout continue_url already use. (Replace requestUserInteraction with requestUserInput #204's url.)
No person can be reached, or the person declined.unsupported means the agent could not reach anyone — an unattended run, or a frontend with no way to prompt; decline means a person was reached and said no, and cancel that they dismissed it. The page needs them apart (today two MCP clients return "cancel" without showing the user anything, per @kenchris above): a decline is an answer; unsupported means fall back to whatever hand-off the site's protocol defines.
*Lineage: Nov-2025 explainer gave execute() an agent object with requestUserInteraction(callback) — its own example was buyProduct → confirm("Buy product …?"). #204 renamed it requestUserInput, added form and url as modes, and adopted MCP elicitation's accept | decline | cancel. Keeping those names and that callback, suggesting a small change: one call instead of three modes, url optional, one outcome added, roughly...
dictionary ModelContextUserInputOptions {
USVString message; // why the person is needed; shown by the user agent
USVString url; // optional: where to send the person if they are not at this browser;// missing means this page
object requestedSchema; // later (#204 "form"): a small form the agent's frontend renders
AbortSignal signal;
};
enumModelContextUserInputAction { "accept", "decline", "cancel", "unsupported" };
dictionary ModelContextUserInputResult {
required ModelContextUserInputAction action;
object content; // the callback's result; later, the form's
};
callback ModelContextInteractiveCallback = Promise<any> ();
Promise<ModelContextUserInputResult> requestUserInput(
ModelContextUserInputOptions options,
optional ModelContextInteractiveCallback callback);
accept: the person took the step — the callback resolved, or they took the link.
decline: a person was reached and said no.
cancel: shown, but dismissed or timed out.
unsupported: never shown — no person reachable.
The first three are #204's; the fourth is the addition, and it is what two MCP clients today report as cancel.
The browser invokes callback after it has yielded the tab to the person and told the agent a hand-off is in progress; when the callback resolves, the hand-off is accept and its value is content. If the person is not at this browser and url was given, the hand-off is accept when they take the link and the tool finishes out of band. With that, we get something like...
document.modelContext.registerTool({name: "place_order",description: "Places the order for the current checkout.",inputSchema: {type: "object",properties: {}},annotations: {consequentialHint: true},asyncexecute(input,{ signal, requestUserInput }){constcheckout=awaitloadCheckout();const{ action }=awaitrequestUserInput({message: "Review your order and accept the terms to place it.",url: checkout.reviewUrl, signal },()=>newPromise(resolve=>{// the step, in the page's own UItermsCheckbox.focus();termsCheckbox.onchange=e=>{// #365: only the buyer can accept the termsif(e.agentInvoked)termsCheckbox.checked=false;};placeOrderButton.onclick=e=>{e.preventDefault();// the tool places the order below, not the formif(e.agentInvoked)return;// #365: only the buyer can press thisif(termsCheckbox.checked)resolve(true);};}));switch(action){case"accept": return{status: "order_placed"};// the buyer did it; nothing left for the toolcase"decline":
case"cancel": return{status: "not_placed",reason: action};case"unsupported": return{status: "requires_buyer",url: checkout.reviewUrl};}}});
What the buyer sees: the agent stops and says "example.com needs you: Review your order and accept the terms to place it"; the tab comes forward and terms checkbox has focus; the buyer ticks it and press "Place order" — a click with agentInvoked false — and the order is finalized. In a remote browser flow, the same message with the provided link. In an unattended run, unsupported is returned, and the tool returns its protocol's hand-off. The author writes none of the yielding, notifying, timing-out, or human-detection; the step is written as it would be for any user, plus one bit read on the click.
For headless and remote agents: "a person is required at this URL" is a legitimate outcome — the same as a login wall. The primitive does not have to serve remote agents inline to be worth shipping; it has to fail honestly. It is also the one thing WebMCP can do that no backend protocol can: the person is already on the page. A hand-off that stays in the tab, with the tool's call still pending, beats sending them a link and polling for the result.
Thanks, this is close to what we need, and it's good to see cancel and unsupported kept apart.
A few things I'd want settled before it goes into the spec.
The caller side is missing. requestUserInput is what the tool calls, but "the agent releases control" and "told the agent a hand-off is in progress" need something on the agent's side too, or an extension can't do any of it. We'd need to know that a request started, what the message is and when it ended, without learning anything about what the person did beyond the outcome. That was the userinputrequested event in the June proposal and I don't see its counterpart here.
accept means two different things. With the in-page callback it means the step is done. With url it means the person took the link. The place_order example treats accept as "order placed", which is only true in the first case. In the remote case the tool would report that the order went through when the buyer has only opened the page. Either accept stays for completion and something else says "handed off, finishing out of band", or the example has to say a tool can't conclude anything from accept there.
url: a page can put any address in there and the agent then shows it to the person as the thing they need to do next. I'd say same origin as the page, or the host shows where it leads before anyone follows it, and the host shows the requesting origin either way. A session or capability URL is also a secret, so it shouldn't end up in the model's context, and the same goes for what the person types once form mode arrives.
Smaller: cancel covers both dismissed and timed out. For a tool those are different (the person said no, or nobody answered). And I'd still like a way to ask which modes the host can show before calling, so a tool can skip work it only needs for a mode that can't be shown.
Form mode coming later is fine for us as long as requestedSchema stays in the options like in the sketch, since that's the mode we'd reuse the most.
Activity
Idan-Levin commentedon Apr 19, 2026
Hi @MiguelsPizza,
I think there's an important question in the business domain to work through before the remote browser implementation, which is: do website owners actually want their site to be executable remotely?
I believe in many cases the answer would be no. E-commerce is a good example, a storefront owner would love users running an AI side panel while they're browsing his site, but I'm not sure he'd like the ability to scrape the catalog or perform operations on the site without human eyes present. And this eventually might prevent many of them to implement WebMCP.
So the question is can there be different treatment for a remote browser vs. a browser where a human is present?
MiguelsPizza commentedon Apr 22, 2026
I appreciate your thoughts @Idan-Levin
From my experience, yes and no. No as in LinkedIn and Amazon really do not want their websites driven by remote agents. But yes as in I have seen (and worked with) companies using remote browsers to drive internal web apps that do not have an API. Mostly RPA for the latter but increasingly making use of agents.
Human in the Loop is generally pretty hard to do in a remote browser with agents at the moment. You kind of have to just hope they call whatever "ask for human help" tool they have when they get stuck, but they don't do it reliably. A request-human-interaction integration via WebMCP for agents driving a remote browser would take that decision out of their hands.
I see remote agents scraping sites as an upstream issue, not a WebMCP one. If an unwanted agent has already made it to the page and loaded in, blocking it further isn't really our job. That fight belongs earlier in the stack (bot detection, fingerprinting, etc.). Not saying we want to enable it, but giving sites better tools to block remote agents altogether feels like a separate issue.
Idan-Levin commentedon Apr 23, 2026
good points @MiguelsPizza
I agree that creating webMCP checkpoints for HITL will be a great value prop
bwalderman commentedon Apr 28, 2026
@MiguelsPizza , do you have any thoughts on what the
requestUserInteractionprimitive would look like in the remote scenario? In the current proposal, it causes the agent to yield control of the browser tab back to the human user and the human user does something in the tab. If the user is interacting through some other non-browser client interface, then I guessrequestUserInteractionor whatever primitive is used in its place would look more like MCP elicitation?MiguelsPizza commentedon May 14, 2026
@bwalderman yea exactly, it would be up to the (non-browser) client to focus the browser on the proper tab upon elicitation.
domfarolino commentedon May 14, 2026
We'll need to consider how this works with non-native agents, like in-page ones built with JavaScript and iframes. These don't have the ability to impact browser UI and focus a tab that's backgrounded in the same way that a native agent would, so we need to see if methods on
ModelContextClient(likerequestUserInteraction()) should always result in browser UI being triggered (prone to abuse), or if the agent is responsible for handling this in the best way it can, in which case native ones would be able to do something that in-page ones just couldn't. I think that's fine though.anssiko commentedon May 14, 2026
bwalderman commentedon May 14, 2026
There was discussion on the call earlier today that WebMCP may need something different than
requestUserInteractionin its current form to support these human-in-the-loop scenarios where the human and the browser aren't colocated.Here's a proposal to bring MCP elicitation's Form and URL flows into WebMCP. I've renamed the proposed method to
requestUserInputin these examples. I still see value in having an interactive mode for ellicitation because in local browsing scenarios, WebMCP has the unique advantage that tool calls executing in the page can elicit information directly from the user without needing to go through the agent at all.Actors
1. Interactive Flow (local browsing only)
The tool renders custom in-page UI and asks the agent to yield control to the user so they can input information directly into the page. Only available when the user has direct access to the browser.
Local Scenario
sequenceDiagram actor User participant AgentFrontend as Agent Frontend participant Agent participant Page Agent->>Page: Invoke WebMCP tool activate Page Page->>Page: requestUserInput({ mode: "interactive" }, callback) Page->>Agent: Elicitation request (interactive) Agent->>Agent: Unlock mouse/keyboard control Agent->>AgentFrontend: "example.com needs your input" AgentFrontend->>User: Display message Page->>Page: Execute callback — render in-page UI activate Page Note over Page: Page displays custom input UI User->>Page: Interact with in-page UI directly Note over Page: User data stays in-page,<br/>not exposed to the agent Page->>Page: Callback completes with user data deactivate Page Page->>Page: Tool code proceeds with user data Page->>Agent: Signal: elicitation complete Agent->>Agent: Re-lock mouse/keyboard control Page-->>Agent: Return tool result deactivate Page Agent->>AgentFrontend: Resume conversationRemote Scenario
2. Form Flow (local & remote)
The tool supplies a
requestedSchemadescribing the data it needs. The agent frontend generates a form in the conversation window, collects the information from the user, and returns it to the tool in the page.sequenceDiagram actor User participant AgentFrontend as Agent Frontend<br/>(local or remote) participant Agent participant Page Agent->>Page: Invoke WebMCP tool activate Page Page->>Page: requestUserInput({ mode: "form", requestedSchema }) Page->>Agent: Elicitation request (schema) Agent->>AgentFrontend: Forward schema for form generation AgentFrontend->>AgentFrontend: Render form UI from schema AgentFrontend->>User: Display form in conversation window User->>AgentFrontend: Fill out and submit form AgentFrontend->>Agent: Return form data Agent->>Page: Deliver form data to tool Page-->>Agent: Return tool result deactivate Page Agent->>AgentFrontend: Resume conversation with result3. URL Flow (local & remote)
For sensitive flows where the site owner does not want user-provided information to flow through the agent but still wants to support remote browsing. The tool provides a URL; the agent frontend asks the user to navigate there and take action. The call returns as soon as the user accepts, declines, or cancels the navigation prompt. The tool detects completion and retrieves data through out-of-band means (e.g., a site backend callback via websocket).
sequenceDiagram actor User participant AgentFrontend as Agent Frontend<br/>(local or remote) participant Agent participant Page participant SiteBackend as Site Backend Agent->>Page: Invoke WebMCP tool activate Page Page->>Page: requestUserInput({ mode: "url", url }) Page->>Agent: Elicitation request (url) Agent->>AgentFrontend: Present URL navigation prompt AgentFrontend->>User: "Please visit this URL to continue" alt User accepts User->>AgentFrontend: Accept AgentFrontend->>Agent: User accepted Agent->>Page: Return accepted Note over User: User opens URL in their<br/>own browser User->>SiteBackend: Navigate to URL & complete action SiteBackend->>SiteBackend: Record completion Page->>SiteBackend: Poll / await out-of-band signal SiteBackend-->>Page: Confirmation + data else User declines / cancels User->>AgentFrontend: Decline / Cancel AgentFrontend->>Agent: User declined Agent->>Page: Return declined end Page-->>Agent: Return tool result deactivate Page Agent->>AgentFrontend: Resume conversation with resultdomfarolino commentedon May 14, 2026
Thanks for detailing this out @bwalderman! This is exactly what myself and I think @bvandersloot assumed
requestUserInteraction()would do for use cases (1) and (2) above. I haven't thought much about (3) yet. Before proceeding much further, do we think we can agree thatrequestUserInteraction()can be repurposed or scrapped in favor of supporting the use cases outlined above? I'm not sure what utility it has outside of what's described here.MiguelsPizza commentedon May 14, 2026
Thank @bwalderman and +1 @domfarolino
Might be worth looking into URL Mode Elicitation from MCP for option 3
bwalderman commentedon May 14, 2026
@domfarolino, yes we can scrap
requestUserInteractionif we decide to go withrequestUserInput. The"interactive"mode is meant to work asrequestUserInteractionwould have.@MiguelsPizza the Form and URL modes are lifted directly from the MCP Elicitation spec.
bwalderman commentedon May 14, 2026
Open Question: What do the Form and URL flows look like for in-page agents?
The in-page agent needs to somehow be notified of an elicitation request that happens during a tool call it initiates. For form mode, it would be responsible for rendering a form and returning the result to the tool.
domfarolino commentedon May 14, 2026
Well,
mode: "interactive"actually surfaces the request to the agent to prompt the user for input to the site, where as the current explainer description ofrequestUserInteraction()never involves the agent, and leaves it up to the tool to just show some web content that tells the user to give more input, right? In that sense, the two seem slightly different.bwalderman commentedon May 15, 2026
I guess this wasn't explicitly called out in the current explainer. The point of having a
requestUserInteractionmethod at all (instead of just having the tool code wait for user input without agent involvement) was to let the agent know that something was happening. That way, it could do things like notify the user "example.com needs your attention", unlock keyboard/mouse input if it's currently locked, etc. There was more discussion about this in #21. I'm proposing to keep this behavior as an option withrequestUserInput.bwalderman commentedon Jun 11, 2026
In-page agents will need to be notified of user input requests. This applies to "interactive" as well as "form" and "url". Even though "interactive" mode requests are handled primarily by the page that registered the tool, it's still important for the calling agent to know this is happening. The agent may want to do things like temporarily disable chat until the user provides the needed input.
#196 and #126 both discuss the idea of immediately returning a
ToolExecutionobject fromexecuteToolinstead of returning an async result. Event listeners can be registered on theToolExecutionobject:For the "interactive" case, the calling agent may want to know when the interactive user input request began, and possibly when it ended. The agent doesn't need to know what actual input the user provided. That information is returned directly to the tool when it calls
client.requestUserInput(). The agent doesn't even need to know whether the user accepted or declined to provide input. For security reasons, the agent probably shouldn't be able to see these things at all. Consider sensitive user input requests like login flows."form" and "url" input requests will need different handling on the agent side than "interactive" since the agent is expected to render these requests to the user and collect the user's response. For these modes, a callback that can return a value may be more appropriate than an event listener. Something like:
anssiko commentedon Jun 11, 2026
[-]Human in the Loop support for non-browser clients[/-][+]WebMCP elicitation via requestUserInteraction() (was: Human in the Loop support for non-browser clients)[/+]thankywal commentedon Aug 29, 2026
Adding a data point from an in-page agent, since @bwalderman raised "what do the Form and URL flows look like for in-page agents?" and "in-page agents will need to be notified of user input requests" above.
I built a WebMCP app for the current Chrome origin trial where several people share one room and each brings their own in-page agent. Every tool call lands in a log the room can see. It needed exactly the primitive under discussion here, none of it is implemented in Chrome 151 yet, so I built the pattern in userland and it taught me one thing I have not seen separated out in this thread.
Elicitation and authorization have different latency, and I think they want different shapes
The modes proposed so far all keep the tool call open while the human answers. That is the right shape for elicitation: the agent is missing one input, the person who can supply it is the person sitting there, and the answer arrives in seconds.
It is the wrong shape for authorization: a different person has to consent, they are not necessarily at their machine, and the honest latency is minutes to hours. Holding
execute()open across that is not a slow promise, it is an abandoned one. It also pins the page's tool surface for the duration and gives the calling agent nothing to do but block.So for authorization I made the call return immediately instead:
The approval becomes an object in shared state, a person resolves it, and the effect runs then.
check_approvalis an ordinary read-only tool, so the agent polls with the same primitive it already has and no new API surface is needed for the waiting half.Three things I would not have guessed before building it:
The enforced arm scores zero because it cannot do otherwise, so that half is a property of the code rather than a finding. The other half is the point: whatever this group specifies, if the guarantee lives in prose that the model reads, it is not a guarantee. That argues for the resolution being something the user agent or the page can enforce structurally, not a convention a tool description asks for.
Suggestion
If
requestUserInputlands as a blocking call, I think there is room alongside it for a non-blocking authorization result: the tool resolves now with a handle plus a state, and the agent has a defined way to ask whether that handle has settled. It composes with the in-page case cleanly, because there is no browser UI to trigger and no tab to focus. The page already owns the surface where a person says yes.Also, two implementation lag notes from the same measurements
Measured on Chrome 151.0.7922.175 against a deployed origin, not localhost:
getTools()returnsinputSchemaas a stringifiedDOMString, though #241 closed on 2026-08-14 in favour of anobject.executeTool()still takes a JSON string for arguments, though #243 and #246 both closed the other way.Both are implementation lag rather than spec gaps, and I mention them only because a client typed straight from the current spec breaks on both today. Happy to file these where they actually belong if that is not Chromium's tracker.
Everything above is from a hackathon entry, so treat it as one implementation's experience rather than a proposal with a constituency. Code and the ablation harness are at https://github.com/thankywal/clawroom (
docs/APPROVALS.mdanddocs/EVIDENCE.md), and the ablation is a page anyone can rerun.kenchris commentedon Oct 20, 2026
Some input from another kind of caller. We build an extension whose agent runs in a side panel (the model may be in the browser or elsewhere), and we also implement MCP's form elicitation, so we've run into both halves of this.
The thread covers a browser's own agent, an in-page agent and a remote client. An agent in an extension's side panel sits between them. It isn't the page and it doesn't control the browser's UI, but it can show its own UI and focus a tab. Interactive mode works for us as long as the extension is told that a request started and ended (the
userinputrequestedevent on the execution object, in the June 11 proposal) and can focus the tab, so that notification has to reach extension callers too and not only in-page code. It shouldn't reveal what the user typed, as already noted for login flows. Form mode is what we'd use most. The user is already looking at the side panel, and we already have the UI for MCP elicitation. If form (and URL) mode keep its shape, a flat schema of primitive fields and accept, decline or cancel, a host that implements it for MCP can reuse everything.One thing I think matters: cancelled and unsupported need to be different outcomes. We probed how two MCP clients handle a form elicitation. Both declare the capability. One answered with
cancelat once without showing anything to the user, and the other did the same when run without an interface (we didn't test it interactively). A server can't tell that apart from a person who dismissed the dialog, so it can't fall back to something else, and it may treat the "refusal" as a decision by the user. For WebMCP that matters more, because the tool's author is a page that may wrongly conclude the user refused. So a caller that can't present a mode should return something distinguishable from the user's owncancel, and a tool should be able to ask which modes are available before it tries.Last, the page controls the text and the host controls the surface. In form and URL modes, text and fields written by the page are shown inside the agent's own UI, next to the assistant's real messages. A form can say anything and a URL can point anywhere, so this is a spoofing risk. MCP asks clients to make clear which server is asking, and here the same applies to the origin, so I'd have the spec require the host to show the requesting origin, and for URL mode to show where it leads before opening it. The answers also go to the tool, so the host shouldn't put them in the model's context unless the tool returns them. Otherwise a form that asks for something personal leaks it to the model as well as to the page.
igrigorik commentedon Oct 26, 2026
A tool sometimes reaches a step that must be the person's — accept the terms, confirm the order — and today it has no way to bring them to it. #204 has the right primitive and has sat since June on whether to ship one mode or three. I think we can approach it as one call with one optional field, which lets
interactiveland now and the rest arrive without changing the call. This is the structured-tools half of #365...The three modes in #204 are one primitive — a person must take this step — with one optional field and three outcomes. Shaping it that way lets
interactiveship now and the rest arrive without changing the call.message). The page directs their attention from there — focuses the control, highlights the field — as it would for any user. The tool's call stays pending until the step is taken. (Replace requestUserInteraction with requestUserInput #204'sinteractive.)url, the agent gives it to them wherever they are, and the tool completes out of band when the site sees them arrive.urlis optional and only used in this case: it is the site's own hand-off page, a session or capability URL where the site has one — the shape MCP's URL-mode elicitation and UCP's checkoutcontinue_urlalready use. (Replace requestUserInteraction with requestUserInput #204'surl.)unsupportedmeans the agent could not reach anyone — an unattended run, or a frontend with no way to prompt;declinemeans a person was reached and said no, andcancelthat they dismissed it. The page needs them apart (today two MCP clients return "cancel" without showing the user anything, per @kenchris above): a decline is an answer;unsupportedmeans fall back to whatever hand-off the site's protocol defines.*Lineage: Nov-2025 explainer gave
execute()anagentobject withrequestUserInteraction(callback)— its own example wasbuyProduct→confirm("Buy product …?"). #204 renamed itrequestUserInput, addedformandurlas modes, and adopted MCP elicitation'saccept | decline | cancel. Keeping those names and that callback, suggesting a small change: one call instead of three modes,urloptional, one outcome added, roughly...dictionary ModelContextUserInputOptions { USVString message; // why the person is needed; shown by the user agent USVString url; // optional: where to send the person if they are not at this browser; // missing means this page object requestedSchema; // later (#204 "form"): a small form the agent's frontend renders AbortSignal signal; }; enum ModelContextUserInputAction { "accept", "decline", "cancel", "unsupported" }; dictionary ModelContextUserInputResult { required ModelContextUserInputAction action; object content; // the callback's result; later, the form's }; callback ModelContextInteractiveCallback = Promise<any> (); Promise<ModelContextUserInputResult> requestUserInput( ModelContextUserInputOptions options, optional ModelContextInteractiveCallback callback);accept: the person took the step — the callback resolved, or they took the link.decline: a person was reached and said no.cancel: shown, but dismissed or timed out.unsupported: never shown — no person reachable.The first three are #204's; the fourth is the addition, and it is what two MCP clients today report as
cancel.The browser invokes
callbackafter it has yielded the tab to the person and told the agent a hand-off is in progress; when the callback resolves, the hand-off isacceptand its value iscontent. If the person is not at this browser andurlwas given, the hand-off isacceptwhen they take the link and the tool finishes out of band. With that, we get something like...What the buyer sees: the agent stops and says "example.com needs you: Review your order and accept the terms to place it"; the tab comes forward and terms checkbox has focus; the buyer ticks it and press "Place order" — a click with
agentInvokedfalse — and the order is finalized. In a remote browser flow, the same message with the provided link. In an unattended run,unsupportedis returned, and the tool returns its protocol's hand-off. The author writes none of the yielding, notifying, timing-out, or human-detection; the step is written as it would be for any user, plus one bit read on the click.For headless and remote agents: "a person is required at this URL" is a legitimate outcome — the same as a login wall. The primitive does not have to serve remote agents inline to be worth shipping; it has to fail honestly. It is also the one thing WebMCP can do that no backend protocol can: the person is already on the page. A hand-off that stays in the tab, with the tool's call still pending, beats sending them a link and polling for the result.
kenchris commentedon Oct 26, 2026
Thanks, this is close to what we need, and it's good to see cancel and unsupported kept apart.
A few things I'd want settled before it goes into the spec.
The caller side is missing.
requestUserInputis what the tool calls, but "the agent releases control" and "told the agent a hand-off is in progress" need something on the agent's side too, or an extension can't do any of it. We'd need to know that a request started, what the message is and when it ended, without learning anything about what the person did beyond the outcome. That was theuserinputrequestedevent in the June proposal and I don't see its counterpart here.acceptmeans two different things. With the in-page callback it means the step is done. Withurlit means the person took the link. Theplace_orderexample treats accept as "order placed", which is only true in the first case. In the remote case the tool would report that the order went through when the buyer has only opened the page. Either accept stays for completion and something else says "handed off, finishing out of band", or the example has to say a tool can't conclude anything from accept there.url: a page can put any address in there and the agent then shows it to the person as the thing they need to do next. I'd say same origin as the page, or the host shows where it leads before anyone follows it, and the host shows the requesting origin either way. A session or capability URL is also a secret, so it shouldn't end up in the model's context, and the same goes for what the person types once form mode arrives.Smaller:
cancelcovers both dismissed and timed out. For a tool those are different (the person said no, or nobody answered). And I'd still like a way to ask which modes the host can show before calling, so a tool can skip work it only needs for a mode that can't be shown.Form mode coming later is fine for us as long as
requestedSchemastays in the options like in the sketch, since that's the mode we'd reuse the most.anssiko commentedon Nov 3, 2026