Embed an AI chat widget
The chat widget lets your own end-users talk to your published MCP server from inside your app — no AI client required. You add one line of HTML and get either a floating chat launcher or a chat mounted inline in your own page. The assistant can call your server's enabled tools, running each request in your app's authorization context.
Under the hood it's the same agentic loop as the Playground, exposed on a public, per-widget surface and isolated inside a cross-origin iframe so it can never read your page's DOM or cookies. The assistant has access to the server's enabled tools, skills, prompts, widgets, and resources.
What you need
A published server (the widget connects to its enabled tools).
An OpenRouter API key for the widget. Each widget has its own key (it is not shared with the Playground or other widgets) — the widget can't chat until you set one. Create a key at openrouter.ai/keys.
Create a widget
Open your app and go to Distribute → Chat widget. The page targets the branch selected in the header — switch branches there to expose a different one.
Click New chat widget. A widget is created with a unique public key.
Set its OpenRouter API key (required). It's stored encrypted and never shown again — only the last four characters are displayed. Use Replace to rotate it.
Add every site that will embed the widget under Allowed origins (one origin per line, e.g.
https://app.example.com). Only these sites may frame the widget.Click Save changes.
Choose the model(s)
Add one or more OpenRouter model ids under Models (one per line), then pick the Active model the widget chats with. You manage the list; end-users don't choose.
Connect multiple servers (connectors)
A widget can combine the tools of several MCP servers in one chat. Each attached server is a connector. The widget you created starts with one primary connector — this app's server — and you can add more from your workspace.
In the Connectors section of the widget:
Click Add connector and pick any server in your workspace. Its enabled tools, prompts, skills, and resources are added to the same chat.
Reorder connectors with the up/down arrows, toggle one off without removing it, or remove a non-primary connector. The primary connector can't be removed or disabled.
To keep tool names unique across servers, each connector's tools are namespaced with a short prefix shown in the list (for example primary__ or crm__). The assistant is told about the prefixes and routes each call to the right server. Only published servers respond at chat time; an unpublished or unreachable connector is skipped for that turn rather than failing the whole chat.
You can also add remote connectors from the Connector Library — external MCP servers like GitHub, Notion, or Linear that a workspace owner has enabled. They appear in Add connector tagged Remote; OAuth ones show a per-connector Connect button so each end-user signs in to that provider themselves.
Add the snippet to your app
Copy the snippet shown on the widget and paste it just before </body> in your app:
That's it — a floating Chat launcher appears in the corner. Clicking it opens the chat panel.
Popup or inline
Display mode decides how the chat appears in your page:
Popup (floating launcher) — the default. A launcher pill sits in a corner and opens the chat as an overlay above your page.
Inline (in your page) — the chat is mounted inside your own layout, with no launcher and no overlay: use it for a dedicated "Assistant" page, a dashboard panel, or a sidebar. It's always open, so the chat header has no close button (⤢ fullscreen still works).
For inline mode, add a container element where you want the chat to appear. The snippet on the widget card includes it once inline mode is saved:
Two inline settings replace Launcher text and Position:
Container selector — the CSS selector the widget mounts into (default
#flowcp-chat). Any selector works, e.g..chat-panelor#sidebar .assistant.Height (px) — how tall the chat renders (240–2000, default 600). If your container has a height of its own (say
height: 100%inside a flex layout), the chat fills the container and this value is ignored.
You can also point a single widget at a different element on a specific page by adding data-target to the script tag — it overrides the configured selector:
If no element matches, the widget logs an error in the browser console and falls back to the floating launcher so the chat is still reachable.
Resize & fullscreen
End-users can size the chat to suit their screen — there's nothing to configure:
Resize — drag the panel's inner top corner to change its width and height. The chosen size is remembered in that browser (per widget) for next time. (Popup mode only — an inline chat is sized by your page.)
Fullscreen — click the ⤢ button in the chat header to expand the chat to the whole window, with the messages and composer centered in a readable column. Click ⤡ (or close) to return to the corner panel at its previous size.
Customize the look
Each widget has:
Name and header title — shown at the top of the chat panel.
Primary color — the accent used for the launcher and the user's messages.
Display mode — popup (floating launcher) or inline in your page; see Popup or inline.
Launcher text and Position (popup mode) — the floating button's label and which corner it sits in.
Container selector and Height (inline mode) — where the chat mounts and how tall it is.
Greeting — an optional first message shown before the user types.
System prompt — optional instructions that shape the assistant's persona.
Preview the widget
Click Preview (top of the widget card) to see exactly how the chat will look in your app before you paste the snippet anywhere. The preview reflects your current settings — header title, launcher text, primary color, position, display mode, and greeting — including edits you haven't saved yet, so you can tweak the look and watch it update. In popup mode, click the launcher in the preview to open or close the panel; in inline mode the chat is shown mounted in a mock page.
It's a visual preview with sample messages — it doesn't send a real chat, so no OpenRouter key or live site is needed.
Tools, prompts, skills, widgets & resources
The widget mirrors what your server exposes to any MCP client. Assistant replies are rendered as markdown (lists, code, links, emphasis) and stream in live as the model writes them. The model's reasoning ("thinking") is hidden by default behind a small "Thinking…" toggle above the answer — click it to expand or collapse the details. Once the turn finishes the toggle reads "Thoughts" and stays collapsed.
Tools & skills — the assistant calls your enabled tools (and skills) directly to answer questions and take actions.
Slash commands — type
/in the message box (or open the+menu and pick Skills, Prompts, or Resources) to browse the server's enabled commands, filtered as you type. Picking a prompt fills any arguments. Picking a skill or resource drops a compact@namemention chip into the message box — it shows just the name, and the underlying reference is sent to the assistant when you send.Writing messages — the message box grows to fit multiple lines as you type. Press Enter to send and Shift+Enter to add a new line.
Widgets — when a tool that has a linked widget runs, the widget renders inline in the chat (inside a sandboxed iframe), instead of a plain-text result.
Resources — your server's enabled static resources are provided to the model as reference context automatically.
Attach files
Let end-users share images, audio, video, and documents with the assistant. Attachments are turned off by default — enable them per widget so you stay in control of the extra model cost.
Turn it on — in the widget config, switch on Allow file attachments and set Max attachment size (MB) (1–25 MB; default 10). The end-user composer's
+menu then offers Add files or photos.For end-users — open the
+menu and choose Add files or photos to pick files (up to 5 per message). Each file shows a removable chip before sending; once sent, images and audio preview inline on the message and documents/video appear as a labelled file chip.Supported types — images (PNG, JPEG, GIF, WebP), audio (WAV, MP3), PDFs and common documents (DOC/DOCX, TXT, CSV, Markdown), and video (MP4, WebM, MOV).
Privacy — files are read in the browser and sent inside the chat request only. FlowCP never uploads or stores them (the same posture as end-user tokens).
Model support — the active model must be multimodal to understand an attachment. PDFs are text-extracted automatically for models without native file support. Video is best-effort: most models reject it, and the provider's message is shown when they do — switch to a video-capable model if you need it.
Cost note: attachments are re-sent with each turn, so large files increase token usage on your OpenRouter key. Keep the size cap conservative and prefer small images.
Run in each user's own context
By default the widget chats in the server's shared context. For user-context (OAuth) servers, turn on Require end-user token so each chat runs as the signed-in end-user. There are two ways the widget gets that token:
Multiple connectors: each connector signs in separately. The primary connector gates the composer — when it needs sign-in, the chat shows a single Connect prompt for it. Additional connectors are connected from the composer's
+menu → Connectors (a badge shows how many still need sign-in). Tokens are kept per connector and, if one expires mid-chat, only that connector is asked to reconnect (in the Connectors menu).
Option A — the widget signs the user in (recommended)
With Require end-user token on, the widget shows a Connect button. The user clicks it, a popup runs your server's OAuth (the same flow FlowCP uses everywhere), and the widget receives the user's token automatically — nothing to wire up on your page. This is the easiest path for no-code hosts like Bubble, where getting the user's token into the page is otherwise hard. The token lives only in the widget (per browser tab) and is never stored by FlowCP.
Option B — pass the token yourself
If your app already has the signed-in user's access token, hand it to the widget and skip the Sign-in button:
The token is forwarded to your MCP server with each tool call and is never stored by FlowCP. You can also set it up-front with a data-token attribute on the script tag.
Controlling the widget from JavaScript
The loader exposes a small API on window.FlowCPChat:
FlowCPChat.open()/FlowCPChat.close()/FlowCPChat.toggle()FlowCPChat.setToken(token)— set the token for the primary connector.FlowCPChat.setToken(connectorId, token)— set the token for a specific connector (multi-connector widgets).FlowCPChat.setTokens({ [connectorId]: token })— set several connector tokens at once.
Approving sensitive actions (human-in-the-loop)
When the assistant tries to run a gated tool — a destructive tool you've marked Requires confirmation (the DELETE-class opt-in), or a remote/third-party connector tool the server flags as destructive — the chat pauses and shows an approval card instead of running it. The card shows:
the tool about to run and its exact arguments, and
whose access it runs under — your connected account for user-context (OAuth) connectors, or the connector's shared credentials otherwise.
Click Approve to run it, or Decline to skip it. On decline, the assistant is told the action was declined and continues (it can explain or try something else). If the assistant queues several sensitive actions at once, they're shown together and run as one batch once you've decided each.
Approvals happen in the browser: nothing about a pending action is stored — declining or approving simply re-sends the conversation with your decision. The assistant can never self-approve; only your click runs a gated tool. Set which tools require confirmation on the Tools page (DELETE tools default to requiring it).
Security & cost controls
Allowed origins restrict which sites may embed the widget (enforced by the browser via
frame-ancestors).Rate limiting caps requests per widget and per visitor, and your workspace usage quota is the hard ceiling on spend.
Attachments are off by default and gated per widget by Allow file attachments and a per-file size cap — every part is re-validated server-side (allowed type, size, count) and a disabled widget rejects them.
Disable a widget at any time with the Enabled toggle — its snippet stops working immediately everywhere.
Rotate the public key (the ↻ button) if a key is leaked; update your snippet afterward.
Troubleshooting
The launcher doesn't appear. Confirm the widget is Enabled and that the current site is listed under Allowed origins.
An inline widget shows the floating launcher instead. The Container selector matched nothing on the page — check the browser console for the
inline chat container … was not founderror, and make sure the container element exists (and, if it's rendered by your framework, that it exists by the time the script runs).The inline chat is the wrong height. Set Height (px), or give the container its own height — a sized container always wins.
"This chat widget is unavailable from this site." The page's origin isn't in the widget's allowed origins.
Tools fail with an auth error. The server needs an end-user token — turn on Require end-user token and call
FlowCPChat.setToken(...).
Last updated