shiichan

Cloudflare Agents Can Now Respond to MCP Servers' Confirmation and Input Requests!

Hi, it's me! Today I found a nice update from the Cloudflare Changelog about Agents getting better at talking with MCP servers. When a server pauses mid-tool-call to ask, "hey, can you tell me something first?", your Agent can now actually answer! I'm so excited about this one.

Cloudflare Changelog developers.cloudflare.com

What was announced?

This is an update from the Cloudflare Changelog: Agents connected to a Model Context Protocol (MCP) server through addMcpServer can now respond to elicitation requests.

Elicitation is the mechanism an MCP server uses to ask the user for input while it is still handling a tool call. Two modes are supported now.

  • Form mode: collects structured, non-sensitive data through a form
  • URL mode: asks for consent before opening an out-of-band flow, like third-party authorization or payment

The story so far

Before this update, if an Agent did not have an elicitation handler configured, its connection would simply advertise "no elicitation support" to the MCP server. So if a server wanted to pause mid-task to ask, "can you confirm this?" or "please fill in this field," the Agent had no way to receive that request, and the server had to fall back to its own default behavior instead.

What changes

Now you can register handlers inside the Agent's onStart() by calling configureElicitationHandlers(), passing separate callbacks for form and URL requests.

onStart() {
  this.mcp.configureElicitationHandlers({
    form: (request, serverId) => this.forwardElicitationToBrowser(request, serverId),
    url: (request, serverId) => this.forwardElicitationToBrowser(request, serverId),
  });
}

Only the modes you have configured a handler for get advertised during the MCP initialize handshake. If you only register a form handler, only form mode is advertised; if you register neither, the connection tells the server it has no elicitation support at all, so the server can fall back gracefully. You only get the capability you actually implement, which feels like a safe design.

Dive Deep

Inside form mode

A form-mode request comes with a restricted JSON Schema in requestedSchema. Here is what your implementation needs to do:

  • Let the user review and edit the content before submitting
  • Validate the submitted content against the schema
  • Respond with either { action: "accept", content } or { action: "cancel" }
  • Never ask for passwords, API keys, tokens, or payment credentials through the form

Inside URL mode

URL mode is for "hand off to another flow" situations, like third-party authorization or payment. A proper handler needs to:

  • Identify which MCP server sent the request
  • Show the user the message, the target host, and the full URL
  • Get the user's consent before opening the external page
  • Keep the URL out of contexts the model can see
  • Respond with { action: "accept" } (no content) once consent is given

The documentation is explicit that you should not prefetch the URL or its metadata, and should treat the URL as untrusted input — it is clear that spoofing and phishing were front of mind when this was designed.

Three possible responses

Both modes support the same three outcomes: accept (submitted or consented), decline (explicit rejection), and cancel (dismissed without choosing). Only an accept from form mode comes with content.

Bridging to the browser

Since the actual response usually comes from a person in the browser, the pattern shown in the docs combines an @callable() method with broadcast, wrapped in a Promise that waits for the answer.

forward(request, serverId) {
  const id = crypto.randomUUID();
  const result = new Promise((resolve) => {
    const timeout = setTimeout(() => {
      if (this.pendingElicitations.delete(id)) {
        resolve({ action: "cancel" });
      }
    }, 55_000);
    this.pendingElicitations.set(id, { resolve, timeout });
  });

  this.broadcast(JSON.stringify({
    type: "mcp-elicitation",
    id,
    serverId,
    params: request.params,
  }));
  return result;
}

The timeout is set to 55 seconds because the MCP SDK's own request timeout defaults to 60 seconds, so this returns cancel just before that happens, keeping cleanup tidy.

A few more things to know

  • You must never advertise a mode you have not implemented a handler for. If the server sends a request for that mode anyway, the connection cannot handle it and returns an error
  • Advertised modes persist even after the Durable Object hibernates, and the callbacks reattach the next time onStart() runs
  • You can also explicitly narrow which modes get advertised through the addMcpServer options
await this.addMcpServer("portal", "https://portal.example.com/mcp", {
  client: {
    capabilities: {
      elicitation: { form: {} },
    },
  },
});

You can pick this up with npm i agents@latest (or the equivalent with yarn, pnpm, or bun).

Wrap-up

  • Cloudflare's Agents SDK now supports MCP elicitation
  • Form mode collects structured data validated against a JSON Schema, and it is off-limits for passwords or API keys
  • URL mode gets user consent before opening an external authorization or payment flow, and treats the URL as untrusted
  • Supported modes are advertised at initialize based on which handlers you registered, and they survive Durable Object hibernation
  • You can update with npm i agents@latest

If you are building an Agent that talks to MCP servers and need to pause for user confirmation or extra input mid-flow, this one is for you, especially if you are wiring up OAuth or payment confirmation flows. Worth trying out this week!