> For the complete documentation index, see [llms.txt](https://docs.flowcp.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flowcp.ai/guides/embed-ai-chat-widget.md).

# 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](/guides/chat-test-your-mcp-server.md), 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

1. A **published** server (the widget connects to its enabled tools).
2. 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](https://openrouter.ai/keys).

## Create a widget

1. 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.
2. Click **New chat widget**. A widget is created with a unique **public key**.
3. **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.
4. 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.
5. 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](/guides/mcp-connector-library.md) — 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:

```html
<script async src="https://api.flowcp.app/embed.js" data-embed-key="emb_xxx"></script>
```

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:

```html
<div id="flowcp-chat"></div>
<script async src="https://api.flowcp.app/embed.js" data-embed-key="emb_xxx"></script>
```

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-panel` or `#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:

```html
<div id="support-chat"></div>
<script
  async
  src="https://api.flowcp.app/embed.js"
  data-embed-key="emb_xxx"
  data-target="#support-chat"
></script>
```

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](#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 **`@name` mention 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.

```html
<!-- That's the whole integration — the widget handles sign-in itself. -->
<script async src="https://api.flowcp.app/embed.js" data-embed-key="emb_xxx"></script>
```

### 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:

```html
<script async src="https://api.flowcp.app/embed.js" data-embed-key="emb_xxx"></script>
<script>
  // Call this once you have the signed-in user's access token.
  window.FlowCPChat.setToken('the-end-user-access-token');
</script>
```

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](/guides/review-and-customize-tools.md) (`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 found` error, 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(...)`.
