createCloudAdapter
Creates an AgentAdapter backed by your /api/chat endpoint: the model runs on your server and every tool call runs on the page, one at a time, so the agent cursor plays.
Creates an AgentAdapter backed by your /api/chat endpoint: the model runs on your server and every tool call runs on the page, one at a time, so the agent cursor plays. You rarely call it yourself: <AgentChat>, useAgentChat, useWebAgent and agent.invoke share one createCloudAdapter() when given no adapter. Create your own for a different endpoint, headers, step budget or label, or to offer it beside an on-device adapter.
Import
import { createCloudAdapter, turnstileHeaders } from "@webmcp-agents/chat";
import type { CloudAdapterOptions, TurnstileHeadersOptions } from "@webmcp-agents/chat";
Example
An adapter for a protected endpoint, passed to the chat. Create it once, at module scope, not during render.
import { AgentChat, createCloudAdapter } from "@webmcp-agents/chat";
import { shopper } from "@/agents";
// Once, at module scope: the adapter checks the endpoint on first
// use and keeps its status.
const cloud = createCloudAdapter({
api: "/api/assistant",
headers: () => ({ "x-csrf": document.cookie }),
maxSteps: 6,
label: "Shop assistant",
});
export function Assistant() {
return <AgentChat adapter={cloud} agent={shopper} />;
}
Options
createCloudAdapter(options?) takes a CloudAdapterOptions; every option is optional.
| Name | Type | Default | Description |
|---|---|---|---|
api |
string |
"/api/chat" |
The chat endpoint. A GET to it reports whether chat is configured and which model runs; a POST runs one model step. See createChatHandler. |
id |
string |
"cloud" |
The adapter’s id; the switcher remembers the person’s choice by it. Set one per endpoint when a page offers several cloud adapters. |
maxSteps |
number |
8 |
Tool steps per user message. One more request follows the last step and the server answers it without tools, so the model sums up. An agent’s own maxSteps wins. Keep it equal to the handler’s maxSteps. |
headers |
Record<string, string> | (() => Record<string, string>) |
— | Extra request headers (auth, CSRF), or a function returning them for each request. content-type: application/json is always sent. |
sessionHeaders |
() => Record<string, string> | Promise<…> |
— | Headers for the GET that starts a chat session, awaited only when the server answers 401 session_required: a bot check’s token, e.g. turnstileHeaders. A throw is logged, and the error reads “Couldn’t start a chat session”. |
fetch |
typeof fetch |
globalThis.fetch |
The function that makes requests. For tests and custom transports. |
label |
string |
from the endpoint | The name in the adapter switcher. By default the model the endpoint’s GET reports, for example “GPT-5.5 (cloud)” or “Claude Opus 5.5 via OpenRouter (cloud)”; Anthropic through its own SDK keeps “Claude (cloud)”; “Cloud” until the first check answers. |
Turnstile
turnstileHeaders({ siteKey }) is a sessionHeaders that solves a Cloudflare Turnstile challenge, for a server with
session: { turnstile }:
const cloud = createCloudAdapter({
sessionHeaders: turnstileHeaders({ siteKey: import.meta.env.VITE_TURNSTILE_SITE_KEY }),
});
| Name | Type | Default | Description |
|---|---|---|---|
siteKey |
string |
required | The widget’s site key (public, from the Cloudflare dashboard). |
action |
string |
"web_agents_chat" |
The action the token carries; the server checks it when its action is set. |
header |
string |
"x-turnstile-token" |
The header the token goes in. |
timeout |
number |
60000 |
Milliseconds to wait for a token (a person may have to click). |
Nothing loads until a message needs a session: then Cloudflare’s script is injected (once per page), the widget renders
bottom-centre with appearance: "interaction-only" (invisible unless Cloudflare wants a click), and it is removed as
soon as it answers. Its host has data-web-agents-ui, so clicking it never makes the agent’s cursor yield. Every session
takes a fresh token (they are single-use). A page with a Content-Security-Policy must allow
https://challenges.cloudflare.com in script-src and frame-src. agent.invoke and other adapters without
sessionHeaders share the browser’s session cookie, so they work once the chat has started a session.
Returns
An AgentAdapter with id (default "cloud"), a label getter, status(), subscribe() and run(). It has no prepare() and no models().
- Status. Starts as
loading. The firststatus()call in a browser sends aGETtoapi:{ enabled: true }makes itready; anything else isunavailable(“Chat isn’t configured on this site.”); a network failure isunavailable(“Couldn’t reach the assistant…”). After a failure it checks again on a laterstatus()call, at most every 10 s. - Run. Each step POSTs
{ messages, tools, page: { url }, agent }, readingreq.toolsfresh.agentis the page’s agent id only (req.agent?.id): instructions never travel. A sub-agent’s nested run (agenton a tool part) is stripped from the history first; the server sees the delegate call and its result. The reply streams back asAgentStreamParts. Tool calls run throughreq.callTool()in order and their results go out with the next step. The run ends withfinish("stop","max-steps"or"aborted") or anerror.
Notes
- HTTP failures become
errorparts with a code the widget explains: 400, 413 and 415 arebad_request, 429 israte_limited, a 503 witherror: "chat_disabled"ischat_disabled(and the status turnsunavailable). A 503 withreason: "daily_budget", or a 429 withRetry-Afterover an hour, reads as a daily limit (rate_limited); any other 503 reads as “The assistant is busy”. A thrownfetchor a broken stream isnetwork. - Sessions (
createChatHandler({ session })) need nothing here: the availability check is theGETthat sets the cookie,fetchsends it to a same-origin endpoint, and a401 session_required(a run before the check answered, an expired session) makes the adapterGETagain and retry the request once. If that fails too, the error is “Couldn’t start a chat session. Reload the page and try again.” - Tool results are truncated (4000 characters of text) when they are recorded, never later, so each request repeats the previous one’s prefix: Claude’s signed reasoning stays valid and the prompt cache keeps hitting.
- Past 160 messages (
LIMITS.messages) the oldest are dropped, starting the window at a user message. - A tool call whose input did not parse arrives as
tool-callthentool-error(Error [invalid_input]: …) and is never run. - No provider SDK is in the client bundle; the API key stays on the server.
See also
- Add the chat, the page that teaches it
createChatHandler, the server sidewebAgentsChatDev,/api/chatinvite dev<AgentChat>- Adapter types