# Overview

FlowCP turns any API with a Swagger/OpenAPI document into a hosted MCP (Model Context Protocol) server — so AI clients like Claude Desktop, Cursor, and ChatGPT can call your APIs on behalf of your end users, using each user's own OAuth session. Connect any OpenAPI-documented API with the **Generic OpenAPI** provider, or use the **Bubble** provider for extra Bubble-specific conveniences.

## What you can do with FlowCP

* Connect an API in seconds — paste a Swagger/OpenAPI URL, or connect an app with just its App ID
* Review and customize which API endpoints become MCP tools
* Configure OAuth so every MCP call runs as the authenticated end user — never a shared admin token
* Publish your MCP server and copy a one-line config snippet into any AI client
* Monitor execution logs and manage end-user connections

## Quickstart

New here? Start with the [Quickstart guide](/getting-started/quickstart) to go from sign-up to a live MCP server in about five minutes.

## Navigation

* **Getting started** — What FlowCP is, the quickstart, and core concepts
* **Guides** — Step-by-step walkthroughs for each workflow
* **Security** — How FlowCP enforces user-context OAuth and default-deny safety
* **Reference** — Tool risk levels and execution log fields
* **Support** — Troubleshooting common errors


# What is FlowCP?

FlowCP is a hosted platform that converts any API with a Swagger/OpenAPI document into an MCP (Model Context Protocol) server. AI clients — Claude Desktop, Cursor, ChatGPT, and any other MCP-compatible tool — can then call your endpoints as if they were native AI tools.

FlowCP supports any provider through an adapter model. The **Generic OpenAPI** provider connects any API that publishes a Swagger/OpenAPI document, and the **Bubble** provider adds Bubble-specific conveniences (connect by App ID, automatic Swagger discovery, Live/Main branches). Both produce the same kind of hosted MCP server.

## The core promise

Every MCP call runs in the end user's own OAuth session. FlowCP never uses a shared admin token to act on a user's behalf. When a user's AI client calls a FlowCP tool, it is their account making the API call — with their permissions and their data visibility.

## How it fits together

```
Your API  (any Swagger/OpenAPI document; app, etc.)
      │
      │ Swagger / OpenAPI document
      ▼
   FlowCP
      │  imports endpoints → generates MCP tools
      │  enforces per-user OAuth
      ▼
  MCP server  (/mcp/<server-id>)
      │
      ▼
  AI client (Claude Desktop, Cursor, ChatGPT…)
```

1. You connect an API. For a generic provider you paste the Swagger/OpenAPI URL; for Bubble you enter your Bubble App ID and FlowCP discovers the Swagger spec automatically.
2. FlowCP parses every API endpoint and creates a corresponding MCP tool.
3. You review the tools, enable the ones you want, and configure OAuth.
4. You publish the server and copy a config snippet into your AI client.
5. When your AI client calls a tool, FlowCP resolves the requesting user's token, validates the input, and proxies the call to your API.

## Who is FlowCP for?

* **API teams and SaaS builders** who want to expose their product to AI workflows without writing a custom MCP integration.
* **Bubble developers and agencies** who want to connect apps (or multiple client apps) to AI clients with zero glue code.
* **Anyone with an OpenAPI-documented API** who wants per-user, OAuth-scoped AI access.

## Next steps

Follow the [Quickstart](/getting-started/quickstart) to get a live MCP server running in about five minutes.


# Quickstart

Get from sign-up to a live MCP server in about five minutes.

> **Tip:** The dashboard's **Getting started** checklist tracks these steps for you and links to each page as you go — see [Onboarding checklist](/guides/onboarding-checklist).

## Prerequisites

Pick the path that matches your API:

* **Generic OpenAPI** — a reachable Swagger/OpenAPI document URL (and OAuth `authorize`/`token` URLs if your API uses OAuth).
* **Bubble** — a Bubble app with its API enabled (Bubble Settings → API → Enable the Bubble API) and a Bubble OAuth app configured (Bubble Settings → API → 3rd Party OAuth / SAML Access).

***

## Step 1 — Sign up and create a workspace

Go to the FlowCP dashboard and sign up. A workspace is created for you automatically. All apps and servers you create live inside this workspace.

***

## Step 2 — Connect your API

1. Click **Connect App** in the sidebar (or navigate to `/connect`). The sidebar entry appears only while the workspace has no app yet; once you have one, use **Connect new app** in the header's app selector.
2. Choose a **Provider** — **Generic OpenAPI** or **Bubble**.
3. Enter a **Display name** (used only inside FlowCP).
4. Then, depending on the provider:

   **Generic OpenAPI**

   * Paste your **Swagger / OpenAPI URL** (e.g. `https://api.example.com/openapi.json`).
   * Click **Continue to import**. FlowCP fetches the document to confirm it is reachable, then imports it into a single **Default** branch. There is no ownership-verification step.

   **Bubble**

   * Enter your **Bubble App ID** — the subdomain of your Bubble URL. For example, if your app lives at `myapp.bubbleapps.io`, enter `myapp`.
   * Click **Continue to verification**, then add a Bubble page with the verification token slug and click **Check Verification**.
   * After verification, FlowCP creates two **versions** — **Live** and **Main** (the `/version-test/` development version).

See [Managing branches](/guides/manage-branches) to add or remove branches.

> **Switching apps:** Once you have one or more apps, use the **app selector** in the header (to the left of the breadcrumb) to switch between them or to connect a new app. The sidebar always shows the menu for the currently selected app, organized into **Build** (Tools, Skills, Widgets, Prompts, Resources), **Configure** (Servers, MCP Auth, Settings), **Monitor** (Health, Stats, Logs, Alerts), and **Support** (Feedback).

***

## Step 3 — Review the import

Open the branch's **Tools** to see all endpoints found in the imported document. If an import fails, see [Troubleshooting](/support/troubleshooting).

***

## Step 4 — Enable tools

Navigate to the **Tools** tab. Every endpoint starts **disabled** by default — FlowCP never exposes a tool until you explicitly turn it on.

1. Enable each endpoint you want AI clients to be able to call.
2. Optionally rename the tool or edit its description to give the AI clearer context.
3. Check the **risk level** badge: `read`, `write`, or `destructive`. Destructive tools (`DELETE` endpoints) require the **Require confirmation** toggle to be on before you can deploy.

***

## Step 5 — Configure OAuth

Navigate to **MCP Auth** in the sidebar, click **Add config**, and choose **User OAuth**.

1. Enter the **Client ID** and **Client Secret** for your API's OAuth app. For a generic provider, also enter the **authorize** and **token** URLs and any scopes. For Bubble, find these under Bubble Settings → API → 3rd Party OAuth / SAML Access.
2. Copy the **OAuth redirect URI** shown on the page (`https://api.flowcp.io/oauth/callback`) and add it to your OAuth app's allowed redirect URLs.
3. Click **Save**.

***

## Step 6 — Chat-test in the Playground

Before deploying, open **Playground** under your server to talk to a model that calls your tools for real. Save your own [OpenRouter](https://openrouter.ai/keys) API key, click **Connect account** to authorize, then chat and watch the tool calls and results. See [Chat-test your MCP server](/guides/chat-test-your-mcp-server) for details.

***

## Step 7 — Test in development, then deploy

Navigate to **Deploy** under your server.

**Test first:** copy the **Development URL** snippet and add it to your AI client. The development endpoint is available immediately — no deploy needed, and only workspace members can reach it.

**Deploy when ready:**

1. If any enabled destructive tools are missing the confirmation flag, the deploy button is blocked — fix those first.
2. Click **Deploy to Production**. The status changes to **Production**.
3. The **Production URL** panel appears. Select the tab for your AI client (Claude Desktop, Cursor, or ChatGPT) and copy the JSON.

***

## Step 8 — Add the config to your AI client

Paste the copied JSON into your AI client's config file:

* **Claude Desktop** — `claude_desktop_config.json`
* **Cursor** — `.cursor/mcp.json` (or Cursor's MCP settings panel)
* **ChatGPT** — the MCP server settings in the ChatGPT desktop app

Restart the AI client. Your tools should now appear in the tools list.

***

## What's next?

* [Monitor execution logs](/guides/publish-your-mcp-server) to see every tool call
* Read about [how user-context OAuth works](/security/user-context-oauth)


# Core concepts

## Workspace

A workspace is the top-level container for everything in FlowCP. All apps, servers, OAuth configurations, and end-user connections belong to a single workspace. If you are an agency managing multiple client apps, each client should have its own workspace.

## App

An **App** record in FlowCP represents one connected API. It stores the Swagger/OpenAPI document URL, the API base URL, the **provider** (which adapter handles it), and the OAuth credentials. You can connect multiple apps inside a single workspace.

The **provider** determines how FlowCP discovers and talks to the API:

* **Generic OpenAPI** (`openapi`) — you paste the Swagger/OpenAPI document URL directly. FlowCP makes no assumptions about path conventions or response envelopes.
* **Bubble** (`bubble`) — you enter a Bubble App ID and FlowCP derives the Swagger URL, reads app metadata (custom domain, favicon), and applies Bubble-specific endpoint and pagination handling.

## Branch (MCP Server)

Each app has one or more **branches**, and each branch is its own MCP server with an independent tool set and publish state.

* **Generic OpenAPI** apps start with a single **Default** branch pointing at the document you connected.
* **Bubble**-connected apps create two branches automatically after verification: **Live** (your live Bubble URL) and **Main** (the default development branch, served at `/version-test/`). Bubble development branches live at `/version-<id>/`, so you can add a branch for any named Bubble branch.

Each branch has its own `server-id` and `/mcp/<server-id>` endpoint, and any branch — including the defaults — can be deleted. See [Managing branches](/guides/manage-branches).

## Swagger Import

When you provision an app, FlowCP fetches and parses its Swagger/OpenAPI document **for each branch**, using that branch's URL. The import job resolves all endpoint definitions and creates a set of MCP tools for that branch's server. Imports are idempotent — re-importing updates existing tools rather than duplicating them.

## MCP Tool

An MCP tool is the FlowCP representation of one API endpoint. It stores:

* **Name** — a human-readable identifier used by AI clients
* **Description** — passed to the AI as context about what the tool does
* **Method and path** — the underlying API call
* **Input schema** — a Zod-validated JSON schema describing expected parameters
* **Risk level** — `read`, `write`, or `destructive`
* **Enabled** — whether AI clients can invoke this tool

Tools start **disabled** and must be explicitly enabled.

## MCP Prompt

An MCP prompt is a reusable, parameterized message template a server offers to clients via the MCP `prompts/list` and `prompts/get` methods. Unlike tools (which the model calls), prompts are **user-controlled** — clients surface them to the user, often as slash-commands. A prompt stores a message body with `{{argName}}` placeholders, a set of declared arguments the user fills in, and optional embedded resources. Like tools, prompts start **disabled** and must be explicitly enabled. See [Offer prompts](/guides/offer-prompts).

## Documentation Page

A documentation page is a markdown page of your product's own docs that the server serves to AI clients at `docs://<slug>`, alongside two built-in tools: `search_docs` (find pages by title, description, or heading) and `read_doc` (return one page's text). With docs connected, an agent answers "how does X work?" from your documentation rather than from guesswork. Pages can be written in the dashboard or committed to a `Docs/` directory in a connected repository, and like tools they start **disabled**. See [Serve product documentation](/guides/serve-product-docs).

## MCP Server

An MCP server is the published endpoint at `/mcp/<server-id>` that AI clients connect to. A server represents one **branch** of an App and exposes that branch's enabled tools. An App can have several servers (one per branch), each published independently. Servers have three statuses:

| Status      | Meaning                                       |
| ----------- | --------------------------------------------- |
| `draft`     | Not yet published. AI clients cannot connect. |
| `published` | Live and accepting MCP connections.           |
| `disabled`  | Suspended. Connections are rejected.          |

## OAuth Provider Config

An OAuth provider config stores a named set of OAuth credentials (Client ID and Client Secret) plus the authorize/token URLs. FlowCP uses these to run the OAuth authorization code flow when an end user first connects their AI client. For Bubble apps the authorize/token URLs are derived from the app; for generic providers you enter them directly. The client secret is encrypted at rest. Access tokens are never stored by FlowCP — the AI client holds them and presents them on every request.

An App can have **several named configs**, and each **branch** ([MCP server](#mcp-server)) is assigned exactly one. A config can be shared by many branches, but a branch with no config assigned will not authenticate. New branches inherit the app's default (first) config. See [Configure OAuth](/guides/configure-oauth) and [Manage branches](/guides/manage-branches).

## Risk Level

Every tool is classified by risk:

* **read** — Safe GET calls that only retrieve data
* **write** — POST/PATCH/PUT calls that create or modify data
* **destructive** — DELETE calls that remove data

See [Tool risk levels](/reference/tool-risk-levels) for details.


# Connect a Bubble app

This how-to walks you all the way from your Bubble app's API settings to a live MCP server that AI clients can call — with every tool running in each end-user's own Bubble OAuth context.

By the end you'll have:

* A verified Bubble app connected to FlowCP.
* Its endpoints imported as MCP tools, with the ones you want enabled.
* Bubble OAuth configured so users authenticate as themselves.
* A deployed MCP server and the config snippet to use it from Claude, Cursor, or ChatGPT.

Each step below is a summary with a link to the full reference guide if you need more detail.

> **Before you begin.** You need a Bubble app you own and access to its **Settings → API** page. Everything on the FlowCP side happens in the dashboard — no code required.

## Step 1 — Prepare your Bubble app's API settings

FlowCP imports your tools from Bubble's Swagger schema and authenticates users through Bubble's OAuth, so both must be turned on in Bubble first. In the Bubble editor, open **Settings → API** and:

1. Enable **Enable the Bubble API**.
2. Uncheck **Hide Swagger Schema** — if it's checked, FlowCP can't read your endpoint list.
3. Make sure the endpoints you want to expose are **Public** or accessible to authenticated users.
4. Enable **3rd Party OAuth / SAML Access**, then note the **Client ID** and generate a **Client Secret**. You'll paste these into FlowCP in [Step 5](#step-5-set-up-bubble-oauth).

> **Tip:** FlowCP derives Bubble's authorize and token URLs automatically from your app, so you never enter them by hand.

## Step 2 — Connect the app in FlowCP

In the FlowCP dashboard, click **Connect App** in the sidebar (or navigate to `/connect`). If you already have an app connected, the sidebar entry is gone — open the **app selector** in the header and choose **Connect new app** instead. Keep the **Bubble** provider tab selected (it's the default). On the **App details** step, fill in:

| Field             | What to enter                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------- |
| **Display name**  | A label for this app inside FlowCP (e.g. "My SaaS").                                      |
| **Bubble App ID** | The subdomain of your Bubble app. If your app is at `myapp.bubbleapps.io`, enter `myapp`. |

Click **Continue to verification**. FlowCP reads your app's Bubble metadata (`/api/1.1/meta`) to detect a custom Live domain if you have one.

For the full field reference, see [Connecting a Bubble app](/guides/connect-your-api#connecting-a-bubble-app).

## Step 3 — Verify ownership

Bubble apps require a one-time ownership check before import. On the **Verify ownership** step, pick a method:

* **Bubble Page** — copy the verification token, create a new page in the Bubble editor whose **URL slug is exactly that token** (the page content doesn't matter), and publish it.
* **Meta Tag** — paste the provided `<meta name="flowcp-verification" …>` tag into Bubble's **Settings → SEO / metatags** header section and publish.

If your app uses Bubble run-mode protection, enter the optional **Username** and **Password** so FlowCP can reach the protected URLs. Then click **Check Verification**. FlowCP probes both your live and version-test versions; on success you'll see a green **App verified** card and advance to import.

You can finish or redo this later under **Settings → Verify** (`/apps/<app-id>/settings/verify`). See [Verifying Bubble app ownership](/guides/connect-your-api#verifying-bubble-app-ownership).

## Step 4 — Import and enable tools

After verification, FlowCP imports whichever of Bubble's two default branches are reachable, each as its own MCP server:

* **Live** → your live Swagger endpoint (custom domain if detected, otherwise `https://<app-id>.bubbleapps.io/api/1.1/meta/swagger.json`).
* **Main** → the development version at `https://<app-id>.bubbleapps.io/version-test/api/1.1/meta/swagger.json`.

The import checklist settles the **Tools** and **Resources** rows with the count imported, then redirects you to the **Tools** page.

Every tool starts **disabled** — FlowCP never exposes a tool until you turn it on. On the **Tools** page:

1. Toggle the switch on each tool row you want AI clients to call. Changes take effect immediately — no re-deploy needed for enable/disable.
2. Optionally rename a tool or edit its description to give the AI clearer context.
3. Check each tool's **risk level** badge (`read`, `write`, `destructive`). Any enabled `destructive` tool must have **Require confirmation** turned on before you can deploy. For Bubble, leave the **Required OAuth scope** field blank — Bubble's tokens carry no scopes.

For more, see [Review and customize tools](/guides/review-and-customize-tools).

## Step 5 — Set up Bubble OAuth

Now connect the Bubble OAuth app you created in Step 1 so every MCP call runs in the end-user's own authorization context.

1. In the sidebar, open **MCP Auth**.
2. Click **Add config** and choose **User OAuth** as the auth type.
3. Enter a **Name** (e.g. "Production"). For a Bubble app the provider is fixed to Bubble, so there's no provider selector and no authorize/token URLs to enter.
4. Enter the **Client ID** and **Client Secret** from Bubble's **Settings → API → 3rd Party OAuth / SAML Access**.
5. Copy the read-only **OAuth redirect URI** (`https://api.flowcp.io/oauth/callback`) and paste it into your Bubble OAuth app's allowed redirect URLs.
6. Click **Create config**.

The first config you add is assigned to any branch that doesn't have one yet. To point each branch at a specific config, open **Settings → Branches** and pick the config in that branch's **OAuth config** dropdown. A branch left on **None — not configured** fails closed and won't authenticate.

For the complete OAuth reference, see [Configure MCP Auth](/guides/configure-oauth).

> **Note:** If your Bubble app uses a custom Live domain, FlowCP sends users to Bubble OAuth on that domain for the Live server automatically. The redirect URI you register with Bubble doesn't change.

## Step 6 — Deploy your server

Each branch (**Live** and **Main**) is a separate MCP server you deploy independently. Open the server's **Deploy** page (or use the **Deploy** button in the dashboard header with the branch selected).

Deploying requires all of:

1. **At least one tool enabled.**
2. **OAuth configured** — a config with a Client ID and Secret assigned to the branch.
3. **No unguarded destructive tools** — every enabled `destructive` tool has **Require confirmation** on.

If anything is missing, the **Deploy to Production** button is blocked and lists what to fix. When ready, click **Deploy to Production**. The status changes from `Development` to `Production` and the runtime begins accepting connections at `https://runtime.flowcp.io/mcp/<server-id>`.

You can test before deploying using the always-available **Development URL** (`/mcp/dev/<dev-slug>`), reachable only by workspace members. For the full walkthrough, see [Deploy your MCP server](/guides/publish-your-mcp-server).

## Step 7 — Use the MCP server

Once deployed, the **Production URL** panel shows a copyable config snippet with a tab for each AI client:

```json
{
  "mcpServers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "type": "http"
    }
  }
}
```

Paste the snippet for your client (**Claude Desktop**, **Cursor**, or **ChatGPT**) into its MCP configuration and restart it. The first time an AI client calls a tool, FlowCP returns an authorization challenge; the user logs in with Bubble, and every subsequent call runs with that user's token. When a token expires, the client re-authorizes automatically.

To try the server without leaving the dashboard, open the **Playground** to chat with a model that calls your tools for real. See [Chat-test in the Playground](/guides/chat-test-your-mcp-server) and [Integrate with AI clients](/guides/integrate-with-ai-clients).

## Troubleshooting and next steps

* Import can't reach Swagger? Recheck **Enable the Bubble API** and **Hide Swagger Schema** in Bubble (Step 1), then retry. See [Troubleshooting](/support/troubleshooting).
* Add or remove Bubble versions (branches) under **Settings → Branches** — see [Managing branches](/guides/manage-branches).
* Watch every tool call on the server's **Logs** tab — see [Execution logs](/reference/execution-logs).
* Put an assistant **inside your Bubble app** with the chat widget: install the **FlowCP - Embedded Chat** Bubble plugin and drop its **FlowCP Chat** element on a page (embed key, display mode, page context and end-user token as fields; open, send message, prompt and set-context as workflow actions), or paste the one-line script tag yourself. Either way a **Connect** button runs your Bubble OAuth and conversation history is optional — see [Embed an AI chat widget](/guides/embed-ai-chat-widget#use-the-bubble-plugin).


# Connect your API

This guide walks through connecting an API to FlowCP and understanding the import process. FlowCP supports any API that publishes a Swagger/OpenAPI document via the **Generic OpenAPI** provider, a **Bubble** provider with extra conveniences for Bubble apps, a **Git repository** provider that reads the document from source control and re-imports automatically on PR merge, and a **Manual** provider for APIs that have no spec at all.

## Choosing a provider

In the FlowCP dashboard, click **Connect App** in the sidebar (or navigate to `/connect`) and choose a **Provider** on step 1. The sidebar entry shows only while the workspace has nothing connected yet — once you have an app, open the **app selector** in the header and choose **Connect new app**.

* **Generic OpenAPI** — connect any API by pasting its Swagger/OpenAPI document URL directly. Start with [Connecting a generic OpenAPI API](#connecting-a-generic-openapi-api).
* **Bubble** — connect a Bubble app by its app identifier; FlowCP discovers the Swagger spec, reads app metadata, and creates Live/Main branches. Start with [Connecting a Bubble app](#connecting-a-bubble-app).
* **Git repository** — connect an OpenAPI document stored in a GitHub repository (via the FlowCP GitHub App) or any git host that exposes a raw file URL. Start with [Connecting from a Git repository](#connecting-from-a-git-repository).
* **Manual** — connect an API that has **no Swagger/OpenAPI document**. You provide just the base URL and auth, then add tools by hand. Start with [Connecting a manual API](#connecting-a-manual-api).

The first three providers end at the same place: a set of imported MCP tools you review, enable, and publish. The Manual provider skips import entirely — you author tools yourself on the Tools page (see [Add a tool manually](/guides/review-and-customize-tools#add-a-tool-manually)).

## Connecting a generic OpenAPI API

Choose **Generic OpenAPI** on step 1. The wizard has two steps — there is no separate ownership-verification step.

### Step 1 — App details

| Field                     | What to enter                                                                                                                                  |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Display name**          | A label for this app inside FlowCP (e.g. "My SaaS")                                                                                            |
| **Swagger / OpenAPI URL** | The full URL to your OpenAPI 3.x (or Swagger 2.0) document, in JSON or YAML, e.g. `https://api.example.com/openapi.json` or `.../openapi.yaml` |

Optionally set an **API base URL** if the host that serves your endpoints differs from the document's origin; otherwise FlowCP uses the document's origin.

Click **Continue to import**. FlowCP **verifies reachability** by fetching the document right then: ownership is established by FlowCP being able to reach your Swagger document, so the app is only created if the URL is reachable and returns a valid OpenAPI document (JSON or YAML). If it can't be fetched, you stay on step 1 with an error explaining what to fix — nothing is created. (This is the fail-closed behavior; FlowCP never assumes an app exists for an unreachable URL.)

### Step 2 — Import

A single **Default** MCP server is created for the document you connected, and its import runs immediately. The wizard shows progress and redirects you to the **Tools** section when it finishes.

Endpoints are classified generically from the HTTP method and path shape (`GET /users` → list, `GET /users/{id}` → get, `POST /users` → create, `DELETE /users/{id}` → delete, and so on), and tool names prefer the spec's `operationId` when present.

> **OAuth for generic apps.** When you set up OAuth for a generic app, you provide the provider's `authorize` and `token` URLs (and scopes) yourself, rather than FlowCP deriving them. See [Configure OAuth](/guides/configure-oauth).

## Connecting from a Git repository

Choose **Git repository** on step 1 to import an OpenAPI document that lives in source control. Like the generic provider, the wizard has two steps and no separate ownership-verification step — ownership is established by FlowCP being able to read the file. FlowCP can also **re-import automatically when a pull request is merged** into the tracked branch.

Pick a **Source**:

* **GitHub** — connect your GitHub account once (under **Account Settings**), then pick a repo from the dropdown. The dropdown lists repositories across every GitHub account/org you've connected.
* **GitLab** — connect GitLab (gitlab.com or your self-managed instance) once under **Account Settings**, then pick a project from the dropdown.
* **Other git (raw URL)** — for Bitbucket or self-hosted git: supply a raw-file URL template with an optional access token.

> GitHub and GitLab connections live at the **account level**, not per app — see [Manage Git connections](#manage-git-connections-account-settings). Connect once and every app you create reuses that connection.

> **Document format.** The **OpenAPI file path** can point at a JSON or YAML document — `.json`, `.yaml`, and `.yml` are all supported. FlowCP parses the file by content, so the extension is informational; a YAML OpenAPI spec imports exactly like its JSON equivalent. The same applies to a Generic OpenAPI URL.

### Step 1 — App details (GitHub)

1. If you haven't connected GitHub yet, click **Connect GitHub**. You'll be sent to GitHub to install (or configure) the **FlowCP** GitHub App and choose which repositories it can read. After installing, GitHub returns you to the wizard and your connection is saved to your account.
2. Choose the **Repository** from the dropdown — it's populated from your connected GitHub account(s) with no re-authorization. FlowCP pre-fills the **Branch** with the repository's default branch.
3. Set the **OpenAPI file path** — the path to the document inside the repo, e.g. `docs/openapi.json`.

| Field                 | What to enter                                                  |
| --------------------- | -------------------------------------------------------------- |
| **Display name**      | A label for this app inside FlowCP                             |
| **Repository**        | The repo the FlowCP GitHub App can read                        |
| **Branch**            | The branch to import from (and watch for merges), e.g. `main`  |
| **OpenAPI file path** | Path to the document within the repo, e.g. `docs/openapi.json` |

### Step 1 — App details (GitLab)

1. Confirm the **GitLab instance** — `https://gitlab.com` by default, or your self-managed GitLab URL.
2. If you haven't connected this GitLab instance yet, click **Connect GitLab**. You'll be sent to GitLab to authorize FlowCP (read-only). After authorizing, GitLab returns you to the wizard and the connection is saved to your account.
3. Choose the **Project** from the dropdown — populated from your connected GitLab account with no re-authorization. FlowCP pre-fills the **Branch** with the project's default branch.
4. Set the **OpenAPI file path** — the path to the document inside the project, e.g. `docs/openapi.json`.

| Field                 | What to enter                                                     |
| --------------------- | ----------------------------------------------------------------- |
| **GitLab instance**   | `https://gitlab.com` or your self-managed GitLab URL              |
| **Project**           | The project FlowCP may read                                       |
| **Branch**            | The branch to import from (and watch for merges), e.g. `main`     |
| **OpenAPI file path** | Path to the document within the project, e.g. `docs/openapi.json` |

> FlowCP stores only a (rotating) refresh token on your **account-level GitLab connection**, encrypted at rest, and never the access token — it is refreshed on demand each time the document is read. Apps reference that connection rather than holding their own token.

### Step 1 — App details (Other git / raw URL)

| Field                 | What to enter                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Raw URL template**  | A raw-file URL with `{ref}` and `{path}` placeholders, e.g. `https://gitlab.com/group/repo/-/raw/{ref}/{path}` |
| **Access token**      | Optional. For private repos; stored encrypted and sent as a Bearer token when fetching                         |
| **Branch**            | Substituted for `{ref}`                                                                                        |
| **OpenAPI file path** | Substituted for `{path}`                                                                                       |

Click **Continue to import**. FlowCP **verifies reachability** by fetching the document right then. If it can't be fetched, you stay on step 1 with an error and nothing is created (fail-closed).

### Step 2 — Import

A single **Default** MCP server is created and its import runs immediately, exactly as for the generic provider. Endpoints are classified generically from the HTTP method and path shape.

### Import skills, widgets, prompts, resources, and docs from the repo

For **GitHub** and **GitLab** sources, the import also discovers MCP content from five fixed top-level directories in the same repository, alongside the tools it generates from the OpenAPI document. These match the [standard FlowCP folder](/guides/folder-format) layout:

| Directory    | Imports as          | File format                                                                                                             |
| ------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `Skills/`    | Skills              | `<name>.md` — `---`-fenced frontmatter (`description`, `references`, `sortOrder`) + body                                |
| `Prompts/`   | Prompts             | `<name>.md` — frontmatter (`title`, `description`, `arguments`, `resources`, `sortOrder`) + body                        |
| `Widgets/`   | Widgets             | `<name>.html` (the widget HTML) + an optional `<name>.json` sidecar (`description`, `csp`, `sortOrder`)                 |
| `Resources/` | Resources (static)  | `<name>.md` — frontmatter (`title`, `description`, `uriTemplate`, `mimeType`, `uriVariables`) + body (the content)      |
| `Docs/`      | Documentation pages | `<slug>.md` or `.mdx` — frontmatter (`title`, `description`, `sortOrder`) + body. **Nested**: the file path is the slug |

* The **file name** (without extension) becomes the item name. `Docs/` is the exception: the file's whole path below `Docs/` becomes the page slug, so `Docs/guides/webhooks.md` is served at `docs://guides/webhooks`. It is also the only directory walked recursively (up to three levels). See [Serve product documentation](/guides/serve-product-docs).
* Directory names may be Capitalized (`Skills/`, as above) or lowercase (`skills/`) — both import identically.
* This is the same on-disk format the [`flowcp sync`](/reference/cli) CLI reads and writes, so a repo you commit by hand and one you push with `flowcp sync push` import identically.
* Repo resources are always **static** (a repo file can't reference a server-specific backing tool). Add or auto-generate endpoint resources from the [Resources](/guides/review-and-customize-tools) page instead.
* Imported items arrive **disabled** — like tools, they are default-deny and won't reach your live MCP server until you enable them on the Skills / Widgets / Prompts / Resources / Documentation pages.
* Re-imports are idempotent: they update content but never change your enabled/sort-order choices, and never overwrite an item you created or edited by hand in the dashboard (same name → the repo file is skipped).
* **Other git (raw URL)** sources import tools only — FlowCP can't enumerate directories over a raw-file URL, so content import is skipped for them.

### Auto-import on PR merge

Once connected, FlowCP re-imports the document whenever a pull request is **merged into the tracked branch** — your enabled tools and customizations are preserved (re-import is idempotent; see [Re-importing](#re-importing)).

* **GitHub:** auto-import works as soon as the FlowCP GitHub App is installed — the App receives pull-request events and FlowCP acts only on merges into your tracked branch.
* **GitLab:** in GitLab → **Settings → Webhooks**, add a webhook pointing at the GitLab webhook URL shown on the app's settings page, enable the **Merge request events** trigger, and paste the per-app secret as the **Secret token**. For security the secret is never displayed — click **Copy** next to **Secret token** to copy it to your clipboard. FlowCP acts only on merge requests merged into your tracked branch.
* **Other git:** add a webhook in your git host pointing at the generic webhook URL shown on the app's settings page, using the per-app secret to sign requests. For security the secret is never displayed — click **Copy** next to **Webhook secret** to copy it. Any signed request triggers a fresh import.

You can also trigger a manual re-import at any time from the app's settings.

### Manage Git connections (Account Settings)

GitHub and GitLab connections belong to **your user account**, so you authorize once and reuse the connection across every app and workspace you belong to. Manage them under **Settings → Account → Git connections**:

* **Connect GitHub** installs the FlowCP GitHub App on an account or org. You can connect several — the connect wizard's repository dropdown aggregates repositories across all of them.
* **Connect GitLab** authorizes a GitLab instance (gitlab.com or self-managed). You can connect more than one instance.
* **Disconnect** removes FlowCP's stored connection. Existing apps that imported from it keep their resolved document, but auto re-import and the picker stop working for that connection until you reconnect. (Disconnecting GitHub here forgets the installation in FlowCP; to fully revoke access, also uninstall the FlowCP GitHub App from GitHub.)

For finer control from CI — for example to also run the MCP test suite and publish only when it passes — drive the `flowcp` CLI from a workflow instead of (or alongside) auto-import. See [Use FlowCP in GitHub Actions](/guides/github-actions).

### Editing the git source later

Everything you set when connecting is editable from **Settings → General** under the **Git source & auto-import** card:

* **Branch** and **OpenAPI file path** — change either and click **Save git source**. FlowCP re-resolves the document, validates it is reachable (saving fails if it isn't), and re-imports every branch's tools from the new location.
* **Other git (raw URL)** sources can also edit the **Raw URL template** and replace the stored **Access token** (leave the token blank to keep the current one).
* **Change repository** (GitHub) / **Change project** (GitLab) re-runs the GitHub/GitLab connect so you can point the app at a different repo or project, then returns you to the settings page to pick it and save.

The card also shows the current webhook URL (GitLab / Other git), a **Copy** button for the webhook secret (the secret itself is never displayed), and the last imported commit.

## Connecting a manual API

Choose **Manual** on step 1 when your API has **no Swagger/OpenAPI document** to import — you'll define each tool by hand instead. The wizard is a single step: there is no ownership verification and no import.

### Step 1 — App details

| Field              | What to enter                                                                                     |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| **Display name**   | A label for this app inside FlowCP (e.g. "My API")                                                |
| **API base URL**   | The origin your tools call, e.g. `https://api.example.com`. Each tool's path is appended to it    |
| **Authentication** | How tools authenticate to your API: **No auth**, **API key**, **Bearer token**, or **User OAuth** |

For **API key**, also choose the **placement** (a request header or a query parameter) and its **name** (e.g. `X-API-Key`), and enter the key. For **Bearer token**, enter the token (sent as an `Authorization: Bearer …` header). Shared credentials are stored encrypted at rest and never returned. Choose **User OAuth** to run every call in the end-user's own authorization context — you configure the OAuth provider afterward under [Configure OAuth](/guides/configure-oauth).

Click **Connect and add tools**. FlowCP creates the app (already verified — you own the credential you provided) plus its default MCP server with the auth you chose, then takes you straight to the **Tools** page. From there, use **Add tool** to author your first tool — see [Add a tool manually](/guides/review-and-customize-tools#add-a-tool-manually).

Manual tools inherit the app's base URL and auth automatically; you only specify each tool's method, path, and input parameters.

## Connecting a Bubble app

Choose **Bubble** on step 1. The Bubble provider adds automatic Swagger discovery, ownership verification, and Live/Main branches.

### Prerequisites

Your Bubble app must have its API and Swagger schema enabled before FlowCP can import it. In Bubble:

1. Go to **Settings → API**
2. Enable **"Enable the Bubble API"**
3. Uncheck **"Hide Swagger Schema"** (if checked, FlowCP cannot access the endpoint list)
4. Make sure the endpoints you want to expose are set to **Public** or accessible to authenticated users

### Step 1 — App details

| Field             | What to enter                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------- |
| **Display name**  | A label for this app inside FlowCP (e.g. "My SaaS")                                       |
| **Bubble App ID** | The subdomain of your Bubble app. If your app is at `myapp.bubbleapps.io`, enter `myapp`. |

Click **Continue to verification**. FlowCP reads your app's Bubble metadata (`/api/1.1/meta`) to detect a **custom Live domain** when one is configured. You can override it later under **Settings** for the app.

### Step 2 — Verify ownership

Prove you control the app by adding a verification page. If your Bubble app has run-mode protection enabled, you can also enter the optional username and password on this step so FlowCP can reach protected run-mode URLs. See [Verifying app ownership](#verifying-bubble-app-ownership) below.

### Step 3 — Import

After verification, FlowCP checks the Swagger endpoint for both default Bubble branches and imports whichever branches are reachable:

* **Live** → live Swagger URL (custom domain when detected, otherwise `https://<app-id>.bubbleapps.io/api/1.1/meta/swagger.json`)
* **Main** → the default development version: `https://<app-id>.bubbleapps.io/version-test/api/1.1/meta/swagger.json`

The wizard shows a real-time progress indicator for each imported branch independently. Once the available imports succeed, you are automatically redirected to the **Tools** section.

If neither Live nor Main has a reachable Swagger endpoint, the wizard displays instructions to enable it in Bubble (see [Troubleshooting](/support/troubleshooting)).

You can add more branches (e.g. a named Bubble branch served at `/version-<id>/`) or delete any branch — including the defaults — from the **Settings → Branches** page. See [Managing branches](/guides/manage-branches).

### Verifying Bubble app ownership

Before importing, FlowCP requires you to prove you control the Bubble app. This is a one-time step per app.

During the connect flow, step 2 (**Verify ownership**) walks you through this. You can also open **Settings → Verify** for the app (`/apps/<app-id>/settings/verify`) from the import screen (**Go to Verify**) if you need to finish or reverify later.

1. Copy the verification token shown on the page.
2. In the Bubble editor, create a new page and set its URL slug to exactly that token (the page content does not matter).
3. If your app has Bubble run-mode protection, fill the optional **Username** and **Password** fields. Bubble protected URLs follow the form `http://username:password@appid.bubbleapps.io/`; FlowCP sends the equivalent basic-auth header and stores the password encrypted for future verification and Swagger imports.
4. Publish the page in Bubble, then click **Check Verification** in FlowCP.

FlowCP probes both your live and version-test versions. Once found, the page shows a green **App Verified** card confirming ownership.

#### Reverifying

If you transfer the app to a different Bubble account or change its base URL, you can reverify at any time:

1. Open **Settings → Verify** (`/apps/<app-id>/settings/verify`), for example via **Go to Verify** on the import page.
2. Click **Reverify** — a new token is generated.
3. Create a new Bubble page with the updated token and publish it.
4. Update the optional run-mode username/password if they changed.
5. Click **Check Verification** again.

### Custom domains (Bubble)

If your Bubble app uses a custom domain for Live (e.g. `inova.us`), FlowCP detects it from Bubble's `/api/1.1/meta` response when you connect. Live OAuth, imports, and MCP tool calls then use that host instead of `*.bubbleapps.io`.

If detection fails (API disabled or meta unreachable), set the domain manually under the app's **Settings** page.

## Managing branches

After the import step completes, you are redirected to the **Tools** section where you can enable and configure the imported tools. You can also open **Settings → Branches** to review each branch server, trigger a fresh import, create a new branch, or delete one. See [Managing branches](/guides/manage-branches).

## Understanding the import status

FlowCP runs each branch's import asynchronously — the import status page polls for updates.

| Status         | Meaning                                                                         |
| -------------- | ------------------------------------------------------------------------------- |
| **Pending**    | The import job is queued.                                                       |
| **Processing** | FlowCP is fetching and parsing the Swagger spec.                                |
| **Complete**   | All endpoints have been imported and tools created.                             |
| **Failed**     | The import could not complete. See [Troubleshooting](/support/troubleshooting). |

While an import or re-import runs, the page shows a live **checklist** that steps through each kind of thing FlowCP imports, settling each row with the number it imported:

* **Tools** and **Resources** — imported for every app.
* **Skills**, **Prompts**, **Widgets**, and **Documentation** — imported only for Git (GitHub/GitLab) apps from your repo's `Skills/`, `Prompts/`, `Widgets/`, and `Docs/` directories. These rows are hidden for Bubble and Generic OpenAPI apps.

If the import fails, the checklist highlights the step that failed and the error summary is shown below it.

Once the import completes, the page lists all discovered endpoints. Proceed to the [Tools tab](/guides/review-and-customize-tools) to enable the ones you want.

## App details

Open the app's **Settings** page (`/apps/<app-id>/settings`) to edit how the app appears across the dashboard:

* **App name** — the display name for the app. Pre-filled from what you entered when connecting; edit it any time.
* **Description** — an optional short summary of what the app does and what its MCP server exposes. Leave blank to clear it.
* **Logo URL** — an optional public image URL used as the app's logo. A small preview appears next to the field; leave blank to remove the logo.
* **Website** — an optional public homepage URL. It is surfaced to MCP clients alongside the logo and description so users can find your product. Leave blank to omit it.

The description, logo, and website are advertised to MCP clients when they connect (see [How the server appears in your AI client](/guides/publish-your-mcp-server#how-the-server-appears-in-your-ai-client)).

Edit any of these fields and click **Save app details**. Changes here only affect the app's presentation — they do not touch your Swagger URLs, branches, or imported tools.

### Updating the Swagger URL (OpenAPI apps)

For apps connected from a generic OpenAPI document, the **Settings** page also has a **Swagger URL** card. Paste a new OpenAPI 3.x or Swagger 2.0 document URL into the **Swagger / OpenAPI URL** field and click **Save Swagger URL** to point the app at a different spec — for example, if the document has moved or you need to correct the URL. FlowCP checks the new URL is reachable before saving, then re-imports the app's tools from it.

Bubble apps don't have this field: their Swagger URL is derived automatically from the app's base URL and [custom domain](#custom-domains-bubble), so there's nothing to edit directly.

### Changing an app's provider

If you connected an app under the wrong provider — or you want to migrate it (for example, move an OpenAPI spec into a Git repository) — click the **Change provider** button on the **Settings** page to open the change-provider popup instead of deleting and re-creating the app. Pick the new provider (Bubble, Generic OpenAPI, or Git repository) and fill in its details, exactly as on the connect screen. For GitHub and GitLab you connect the repository or project through the same OAuth round-trip, which returns you to the settings page.

Changing the provider is **destructive** and only available to workspace owners:

* It deletes all of the app's current branches, tools, and execution logs, then re-imports tools from the new source under that provider's adapter.
* It **resets the app's OAuth configuration.** OAuth credentials and authorize/token URLs are specific to the old provider and API, so they can't authenticate the new one — they are removed and the re-provisioned branches start with **no OAuth config assigned**.
* Switching **to** Bubble resets the app to **unverified** — you must [re-verify ownership](#verifying-bubble-app-ownership) before its tools can be imported again. Switching to Generic OpenAPI or Git verifies the app immediately by reaching the document.
* The popup summarizes these effects alongside the form before anything changes. FlowCP checks the new source is reachable first (fail closed), so a bad URL leaves the app untouched.
* If any branch is **published**, unpublish it first — re-provisioning would break MCP clients connected to that server.

After switching, **set up OAuth again** from scratch on the [OAuth configuration](/guides/configure-oauth) page and assign a config to each branch — until you do, those branches fail closed and can't authorize end users.

## Re-importing

You can trigger a fresh import at any time from the app's settings. Re-importing is safe — it updates existing tool records and adds any new endpoints found in the Swagger spec. Tools you have already customized (renamed, description edited) are not overwritten unless the underlying endpoint definition changed.

Every import is recorded in the server's **spec import history** — open **Servers → History** and switch to the **Spec imports** tab to see each import, whether it changed anything, and a per-endpoint diff of what changed in the upstream API since the previous import (including a warning when enabled tools reference endpoints the API removed). See [Version History — Spec import history](/guides/version-history#spec-import-history).

The full raw spec document is retained for the three most recent valid imports per server; older imports keep the normalized endpoint list used for diffs.


# Onboarding checklist

The onboarding checklist is a guided **Getting started** panel that walks a new user through every step required to go from an empty workspace to receiving their first MCP client request.

## Where it appears

* **Getting started** — a dedicated page at `/onboarding`, reachable from the **Getting started** entry at the top of the sidebar.
* **Connect page** — the same checklist is shown as a card above the connect form at `/connect`, so it's the first thing a new user sees after signing up.

The sidebar entry and the connect-page card disappear once you dismiss the checklist (see [Dismissing and restoring](#dismissing-and-restoring)).

## The steps

Each step is **detected automatically** from your workspace data — there is nothing to tick off by hand. A step shows as complete as soon as the underlying work is done. The checklist tracks your **primary app** (the first app you created in the workspace).

| Step                           | Marked complete when                                                           | Where to go                                                      |
| ------------------------------ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| **Connect your app**           | You've connected at least one app                                              | [Connect your API](/guides/connect-your-api)                     |
| **Import your API**            | Your API's OpenAPI document has imported and validated                         | [Connect your API](/guides/connect-your-api)                     |
| **Enable a tool**              | At least one tool is enabled (tools start disabled by default)                 | [Review and customize tools](/guides/review-and-customize-tools) |
| **Configure MCP Auth**         | A server has OAuth or a shared credential assigned, or explicitly uses no auth | [Configure MCP Auth](/guides/configure-oauth)                    |
| **Publish your MCP server**    | A server branch has been published                                             | [Publish your MCP server](/guides/publish-your-mcp-server)       |
| **Receive your first request** | An AI client has made at least one tool call                                   | [Execution logs](/reference/execution-logs)                      |

Every incomplete step has an action button that links straight to the page where you complete it. A progress bar shows how many of the six steps are done.

## Dismissing and restoring

Once you've received your first request — or any time you'd rather hide the checklist — click **Dismiss**. This hides the **Getting started** sidebar entry and the card on the connect page for everyone in the workspace.

You can bring it back at any time: open `/onboarding` directly and click **Restore checklist**.

## Related

* [Quickstart](/getting-started/quickstart) — the same flow written out as a walkthrough.
* [Agent-driven onboarding](/guides/agent-driven-onboarding) — automate these steps with the FlowCP MCP server.


# Managing branches

Every app in FlowCP can expose **multiple branches**. Each branch is its own MCP server with an independent tool set, OAuth-backed execution context, and publish state, with its own `/mcp/<server-id>` endpoint.

> **Branches are most useful with the Bubble provider**, which maps them to Bubble's live and development environments (Live/Main, plus named branches). A **Generic OpenAPI** app starts with a single **Default** branch pointing at the document you connected; you can still add more branches manually if your API serves different environments at different URLs.

The rest of this guide describes the Bubble branch model in detail.

## How branches map to Bubble

Bubble serves your live app at the bare URL and development branches at `/version-<id>/` (Bubble's own URL format). FlowCP mirrors that:

| Branch             | Branch id      | Bubble URL (default)                              |
| ------------------ | -------------- | ------------------------------------------------- |
| **Live** (default) | `live`         | `https://<app-id>.bubbleapps.io/...`              |
| **Main** (default) | `version-test` | `https://<app-id>.bubbleapps.io/version-test/...` |
| Any added branch   | `version-<id>` | `https://<app-id>.bubbleapps.io/version-<id>/...` |

When you configure a **custom domain** (at connect or in **Settings**), FlowCP uses that host for **all** branches: live at the bare path and development branches at `/version-<id>/` on the same domain (for example `https://your-domain.com/version-test/api/1.1/meta/swagger.json`). Saving a custom domain updates every branch's Swagger URL and queues a fresh import for each verified branch.

After you verify your app, FlowCP automatically creates the **Live** and **Main** branches and imports tools for each.

## The branch switcher and Deploy button

The dashboard header shows a **branch switcher** next to the app selector whenever you're inside an app. It lists every branch with its status — **Production**, **Development**, or **Offline** — and switching branches changes which branch's data the Tools, Skills, Widgets, Prompts, and Resources pages show. Your selection is remembered per app.

On the far right of the header, the **Deploy** button promotes the selected branch to production. Clicking it opens a confirmation dialog; confirming runs the same safety validation as the [publish page](/guides/publish-your-mcp-server) and, on success, the branch's MCP server goes live at its production URL. If the deploy is blocked (for example, destructive tools without confirmation), the blocker message is shown so you can fix it first.

Next to the Deploy button, an **issues tracker** shows how many issues would currently block deploying the selected branch (destructive tools without confirmation, enabled resources with a missing or disabled backing tool, a shared-credential mode with no stored credential, or scope-gated tools under a non-OAuth mode). When issues exist the counter is shown in red — click it to see the full list and a shortcut to the branch's deploy settings. A `0` with a check mark means the branch is clear to deploy.

## The Branches page

Open an app, go to **Settings** in the sidebar, then open the **Branches** tab. Each branch row shows its name, branch URL, the source URL it imports from (with a copy button), current status, and its **Authentication** setting, with actions to:

* **Authentication** — choose how this branch authenticates to the upstream API: **User OAuth** (and which OAuth config), **API key**, **Static bearer**, or **None** (see [Setting a branch's authentication](#setting-a-branchs-authentication))
* **Tools** — review and enable that branch's tools
* **Deploy** — deploy or recall that branch's MCP server
* **Re-import** — fetch a fresh copy of that branch's Swagger spec (opens the import status page so you can see progress and any errors)
* **Delete** — remove the branch (see below)

For **Git repository** apps, the line under each branch shows the git-aware source — `repo/project @ branch · file path` — rather than a raw fetch URL, and every branch imports from the same git source. Change the git branch or file path from **Settings → General** (see [Editing the git source later](/guides/connect-your-api#editing-the-git-source-later)).

## Setting a branch's authentication

Each branch has an **Authentication** dropdown choosing how its hosted MCP server authenticates to the upstream API. See [Authentication modes](/guides/configure-oauth#authentication-modes) for the full comparison.

* **User OAuth** (default, recommended) — reveals an **OAuth config** dropdown listing the app's [OAuth configs](/guides/configure-oauth). Pick the config this branch should use to authenticate end users — a config can be shared by several branches, but each branch uses exactly one. A branch left on **None — not configured** is flagged **“Won't authenticate”** and its published MCP endpoint returns an error until you assign a config. New branches inherit the app's default (first) config automatically.
* **API key** — enter a shared static key and whether to send it as a header or query parameter (with the parameter name).
* **Static bearer** — enter a shared static bearer token.
* **None** — no credential is sent; all calls are unauthenticated.

> **API key, Static bearer, and None are shared-credential modes:** every end user acts through one identity, so per-user authorization is not enforced. The credential is encrypted at rest and never shown again after saving — leave the field blank when editing to keep the existing value.

## Creating a branch

In the **Create branch** form at the bottom of the page:

1. Enter a **Name** (e.g. `Staging`).
2. Enter a **Branch id** — the URL segment Bubble uses for that branch (e.g. `version-staging`). Use `live` to point a branch at the Bubble live URL.
3. Click **Create**.

FlowCP creates a new MCP server for that branch, queues a Swagger import, and takes you to the import status page while tools are generated.

## Deleting a branch

Click the trash icon on any branch row and confirm. Deleting removes that branch's MCP server, its tools, its import history, and its execution logs. The **Live** and **Main** defaults are not special — they can be deleted like any other branch, and an app may have zero branches.

> Deleting a branch immediately stops its `/mcp/<server-id>` endpoint. Any AI client configured against that server will no longer connect.


# Review and customize tools

After importing your API, FlowCP generates an MCP tool for every API endpoint it finds. This guide explains how to review, enable, and customize those tools.

## Opening the tools list

From your app's dashboard, click the **Tools** tab. The page shows:

* **Stat cards** at the top: total tools, enabled tools, disabled tools, and destructive tools
* **Toolbar** with a search box and filter buttons (All / Enabled / Disabled / Destructive)
* **Tool rows** — one per endpoint, grouped into category sections

## Tool categories

Tools are grouped into **categories** so related endpoints sit together instead of in one long flat list. Each category section shows a heading and the number of tools it contains.

A tool's category comes from the Swagger / OpenAPI spec:

* If the operation declares **tags**, the first tag is used (for example, an operation tagged `Orders` lands in the **Orders** section).
* Otherwise FlowCP falls back to the **resource** derived from the endpoint path (for example, `/obj/Customer` → **Customer**).
* Tools that resolve to neither appear under **Uncategorized**.

Categories are recomputed every time you re-import, and they are also sent to AI clients — see [how the server appears in your AI client](/guides/publish-your-mcp-server#how-the-server-appears-in-your-ai-client).

Categories also power **dynamic tool exposure**: when a server uses dynamic exposure, AI clients discover tools through the built-in `search_tools` tool, which can filter by category — so well-tagged specs make discovery much more precise. See [Tool exposure](/guides/publish-your-mcp-server#tool-exposure).

## Enabling a tool

All tools start **disabled**. To make a tool available to AI clients, toggle the switch on its row to **Enabled**.

You can enable or disable tools at any time. Changes take effect immediately — no re-publish required for enable/disable toggles (though publishing is required for the server to accept new connections in the first place).

> Skills, widgets, prompts, and resources are default-deny in the same way. If you connected your app from a [GitHub or GitLab repository](/guides/connect-your-api#import-skills-widgets-prompts-and-resources-from-the-repo), any content imported from the repo's `skills/`, `widgets/`, `prompts/`, and `resources/` directories also starts **disabled** and must be enabled on its respective page before it reaches your live MCP server.

## Add a tool manually

Not every API has a spec, and a spec doesn't always cover every endpoint. Click **Add tool** (in the page header, or in the empty state when a server has no tools) to author a tool by hand. This is the only way to add tools to a [Manual app](/guides/connect-your-api#connecting-a-manual-api), and it works alongside imported tools on any app.

In the dialog, set:

* **Name** — snake\_case identifier (e.g. `list_orders`).
* **Method** — `GET`, `POST`, `PATCH`, or `DELETE`. The risk level defaults from the method (you can override it).
* **Path** — appended to the app's base URL, e.g. `/api/1.1/obj/order/{id}`. Use `{name}` for path parameters.
* **Description**, **Risk level**, **Requires auth**, and whether the tool starts **Exposed**.
* **Input parameters** — use the visual builder to add fields one at a time (name, type, required, description). FlowCP turns these into the tool's input schema; no JSON Schema knowledge needed.

A manually-created tool inherits the app's base URL and authentication automatically — you never enter a URL or credential on the tool itself. Manual tools carry a **Manual** badge and, unlike imported tools, remain **fully editable**: you can change the method, path, parameters, and risk level later from the same dialog (open it with the **Edit** button on the row). A later re-import never overwrites a manual tool, even if a generated endpoint shares its name.

### Test a manual tool

While editing a manual tool, use **Send test call** to run it once against the live API with sample arguments. FlowCP makes the real request (using the app's base URL and auth — connect your account in the [Playground](/guides/chat-test-your-mcp-server) first for User-OAuth servers), shows you the response, and saves the response's field keys as the tool's **output schema**. Those keys then feed [response shaping](#response-shaping). Destructive tools require an explicit confirmation before the test call runs.

## Renaming and editing descriptions

Click the pencil icon on any tool row to edit:

* **Name** — the identifier shown to the AI client. Keep it short and action-oriented, e.g. `get_user_profile` or `create_invoice`.
* **Description** — the natural-language explanation passed to the AI. Good descriptions improve the AI's ability to choose the right tool. Describe what the tool does and when to use it.

## Risk level badges

Each tool displays a risk level badge:

| Badge         | Color  | Meaning                                       |
| ------------- | ------ | --------------------------------------------- |
| `read`        | Blue   | GET endpoint — retrieves data only            |
| `write`       | Yellow | POST / PATCH / PUT — creates or modifies data |
| `destructive` | Red    | DELETE — removes data permanently             |

See [Tool risk levels](/reference/tool-risk-levels) for the full classification rules.

## Authentication badges

Each tool also shows whether its endpoint requires a Bearer token, as declared in the Swagger `security` field:

| Badge    | Color  | Meaning                                                                                                          |
| -------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `Auth`   | Violet | The endpoint's OpenAPI spec declares a security requirement — a valid Bearer token must be present on every call |
| `Public` | Gray   | The endpoint has no security requirement in the spec — no token is needed to call it                             |

Most endpoints are auth-protected. The `Public` badge appears only when an endpoint explicitly opts out of security in the Swagger definition (e.g. a webhook receiver or public data endpoint).

## Require confirmation

For **destructive** tools, an additional **Require confirmation** toggle is shown. When on, the AI client must present a confirmation step to the user before the tool call is executed.

> Destructive tools with **Require confirmation** off cannot be published. The publish button is blocked until every enabled destructive tool has this toggle on.

## Required OAuth scope

For **destructive** tools, a **Required OAuth scope** field is also shown next to the confirmation toggle. Enter an OAuth scope (for example `records.delete`) to gate the tool: at request time, the runtime verifies that the end user's access token actually grants that scope before the call is dispatched. If the token is missing the scope, the call fails with an `INSUFFICIENT_SCOPE` error and never reaches your API.

* Leave the field **blank** to leave the tool ungated (the default).
* Scope enforcement fails **closed** — an ambiguous or unverifiable token is rejected rather than allowed.
* This is most useful with identity providers that issue scoped tokens (Generic OAuth2, Auth0, Clerk, Okta). Bubble's opaque tokens carry no scope model, so leave the field blank for Bubble apps.

The scope you enter must be one your identity provider is configured to issue — set it in the provider's **Scopes** field on the [OAuth configuration page](/guides/configure-oauth).

## Linking a widget

If your server has one or more [MCP widgets](/guides/build-widgets), each tool row shows a **widget selector**. Pick a widget to render it inline when the tool runs, or choose **No widget** to unlink. Linking only takes effect once both the tool and the widget are enabled; the linked tool still returns its normal text, so clients without widget support degrade cleanly. See [Build MCP widgets](/guides/build-widgets) for how to author them.

## Response shaping

Every tool row has a collapsible **Response shaping** section. Use it to trim what the AI model sees in the tool's responses — large list responses full of internal IDs, timestamps, and nested blobs burn tokens on every call and make it harder for the agent to pick the right fields. Shaping changes only the response sent to the MCP client; **the upstream API request is unchanged**.

Four settings are available:

| Setting               | Effect                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| **Keep only fields**  | Each record keeps only the listed fields (comma-separated); everything else is dropped                  |
| **Drop fields**       | Each record keeps everything except the listed fields                                                   |
| **Max items**         | List responses are capped to the first N records (1–1000)                                               |
| **Max string length** | Strings longer than N characters (20–10000) are truncated with a trailing `…`, anywhere in the response |

Details worth knowing:

* **Keep only** and **Drop** are mutually exclusive — the mode selector picks one. Field names match the **top-level keys of each record** (no dot paths for nested fields yet).
* Field projection applies to each record of a list response (including Bubble's `results` envelope, whose `cursor` / `remaining` pagination fields are preserved) or to the single object of a get-style response. Field names that don't exist in a record are silently ignored — and if **none** of the listed names match a record, FlowCP returns that record in full rather than collapsing it to an empty object, so a projection can never wipe out a result.
* Shaping runs **before** the 50 KB response cap, so a good projection means fewer truncated list responses.
* Changes take effect on the next MCP request — no re-publish needed.
* Re-running a Swagger import **preserves** your response shaping, like the enabled state.
* Leave everything empty to pass responses through untouched (the default).

### Client-requested fields

In addition to your author-configured shaping, the AI client can narrow the response itself: read tools (GET) advertise an optional **`fields`** input — an array of field names. When the model passes it, each returned record is trimmed to just those fields, on top of any shaping you configured. This is the model's escape hatch when a call returns `RESPONSE_TOO_LARGE`: it can re-issue the call asking only for the fields it needs.

* `fields` shapes only what the model sees; like your shaping, it never changes the upstream API request.
* The `fields` input advertises the record's **real field names** (drawn from the tool's response schema) as its allowed values, so the model selects valid keys instead of guessing — guessed names that match nothing are rejected before they can shape the response.
* The client can only **narrow** within what you expose — if you set **Keep only fields**, a client `fields` request is intersected with it; **Drop fields** are always removed. The client can never widen the projection.
* Oversized list responses are now **truncated** (with `_truncated` / `_omitted` markers) rather than replaced wholesale with a `RESPONSE_TOO_LARGE` error, so the model still gets a usable first page.

## Filtering and searching

Use the toolbar to narrow the list:

* Type in the search box to filter by tool name, path, description, or HTTP method.
* Use the filter buttons to show only Enabled, Disabled, or Destructive tools.

## AI-enhanced descriptions

FlowCP can use AI to write richer, more accurate descriptions for your server and all its tools. AI descriptions give AI clients better context about what each tool does and when to use it — going beyond the simple templates generated at import time.

### How to use it

1. Open the **Tools** page for your server.
2. Click **Enhance with AI** in the top-right area of the page.
3. A job is queued in the background. Descriptions update within a few seconds — refresh the page to see the results.

The server description (shown in italics below the page heading) is also generated at this time.

### Automatic enhancement on import

AI descriptions run automatically every time you import a Swagger spec, as long as an OpenRouter API key is configured. If the key is missing, tools keep their template descriptions and no error is shown.

### Configuration

Two environment variables control AI descriptions:

| Variable             | Default                               | Description                                                                    |
| -------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
| `OPENROUTER_API_KEY` | *(empty)*                             | Your OpenRouter API key. Leave blank to disable AI descriptions.               |
| `OPENROUTER_MODEL`   | `anthropic/claude-haiku-4-5-20251001` | The model used to generate descriptions. Any OpenRouter-supported model works. |

Get an API key at [openrouter.ai/keys](https://openrouter.ai/keys).

### Editing AI descriptions

AI-generated descriptions are just text — you can edit them the same way as any other description (pencil icon on the tool row). Manual edits are never overwritten unless you click **Enhance with AI** again.

## Searching and filtering list tools (Bubble)

> This section applies to **Bubble** apps. For generic OpenAPI providers, list parameters come from the endpoint's own declared query parameters in the spec — FlowCP does not inject Bubble's search schema.

Tools generated from Bubble **GET /obj/{type}** endpoints (names like `list_customers`) support the [Bubble Data API search parameters](https://manual.bubble.io/help-guides/integrations/api/the-bubble-api/the-data-api/data-api-requests#searching-for-things):

| Parameter                  | Purpose                                                                      |
| -------------------------- | ---------------------------------------------------------------------------- |
| `constraints`              | Filter records (array of `{ key, constraint_type, value }` or a JSON string) |
| `cursor`, `limit`          | Pagination                                                                   |
| `sort_field`, `descending` | Sort order                                                                   |

Example tool input for an AI client:

```json
{
  "constraints": [
    { "key": "unitname", "constraint_type": "text contains", "value": "Unit" },
    { "key": "unitnumber", "constraint_type": "greater than", "value": "3" }
  ],
  "limit": 10
}
```

If your tools were imported before search support was added, **re-run Swagger import** on the app to refresh list tool input schemas. Enable/disable state is preserved on re-import.

## Publishing after changes

Tool enable/disable changes are live immediately. Changes to names, descriptions, or the confirmation flag take effect on the next MCP request after saving. You only need to re-publish the server if you initially had it in draft status.


# Offer agent skills

Tools give an AI agent the ability to *call* your API. **Skills** give it the *know-how* to use those tools well. A skill is a short markdown playbook — a `SKILL.md` document — that teaches an agent how to accomplish a multi-step task with your server's tools, for example:

> *To create an order: first call `list_customers` to find the customer, then call `create_order` with their id, then confirm with `get_order`.*

This guide explains how to author skills and how AI clients consume them.

## How clients consume a skill

When a skill is enabled, the runtime offers it to connected MCP clients in two ways, so it works regardless of how advanced the client is:

* **As a resource** — the playbook is published at `skill://<slug>/SKILL.md`. Clients that support MCP resources list available skills and read the playbook on demand (progressive disclosure).
* **As a wrapper tool** — a read-only tool named `skill_<slug>` is also registered. Calling it returns the same playbook. This is the compatibility path for clients that don't yet surface resources or a native skills extension.

Enabled skills are also summarized in the server's `instructions`, so the model learns they exist on connect.

## Managing skills from an agent

Skills can be managed two ways: from the dashboard (below) or by an AI agent connected to the [FlowCP MCP server](/guides/agent-driven-onboarding). The agent surface exposes skills as tools — `list_skills`, `create_skill`, `update_skill`, and `delete_skill` — so an agent can author and toggle skills for a server without leaving the chat. Like the dashboard, new skills start **disabled**.

## Opening the skills list

From your app's dashboard, click the **Skills** tab. The page lists every skill on the selected server, each with an on/off switch. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Creating a skill

1. Click **New skill**.
2. Fill in the fields:
   * **Name** — a `snake_case` identifier, e.g. `create_order`. The wrapper tool is derived from it as `skill_create_order`.
   * **Description** — a one-line summary shown to the agent so it can decide when the skill is relevant.
   * **SKILL.md body** — the markdown playbook itself. Reference your tools by their exact names and spell out the steps in order.
3. Click **Create skill**.

New skills are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model).

## Generating skills with AI

If you're not sure where to start, let AI draft skills for you. Click **AI Suggestions** on the Skills page. FlowCP reads the selected server's tools and asks an LLM to propose **three** skills that chain those tools into real workflows.

Each suggestion shows its name, a one-line description, and a preview of the playbook. Click **Use this** on the one you want — it opens the skill editor **pre-filled** so you can review and adjust the wording before saving. Nothing is created until you click **Create skill**, and (like every skill) it starts disabled.

AI Suggestions require an AI provider to be configured for the platform. If it isn't, the button reports that suggestions aren't available — you can still author skills manually.

## Enabling a skill

Toggle the switch on a skill's row to **On** to offer it to AI clients. Changes take effect immediately — no re-publish required. Disabled skills are never exposed as a resource or a tool.

## Editing and deleting

* Click the **pencil** icon to edit a skill's name, description, or body. Renaming re-derives the slug and the wrapper tool name.
* Click the **trash** icon to delete a skill. This cannot be undone.

## Writing effective skills

* **Name the tools explicitly.** Use the exact tool names from the [Tools](/guides/review-and-customize-tools) page so the agent can map steps to calls.
* **Be procedural.** List steps in order, including how to handle the common failure case.
* **Keep it focused.** One skill per task or flow reads better than a single catch-all document.
* **Enable the tools it references.** A skill is only useful if the tools it mentions are themselves enabled.


# Offer resources

Tools let an AI agent *act* on your API. **Resources** let it *read* context. A resource exposes structured data to MCP clients through the standard `resources/list`, `resources/templates/list`, and `resources/read` operations — the client decides how to use it (passing it to the model, searching it, or selecting a subset).

FlowCP supports two kinds of resource:

* **Static resources** — author-written content (markdown, JSON, plain text) served from a fixed URI, e.g. `doc://house-style`. Good for guidelines, reference material, and other content that doesn't change per request.
* **Endpoint resources** — **resource templates** generated from your read-only `GET` endpoints, e.g. `app://orders/{id}`. When a client reads a concrete URI like `app://orders/42`, FlowCP performs a **live fetch** through the backing tool, in the end user's own authorization context.

## How clients consume a resource

When a resource is enabled, the runtime advertises it to connected MCP clients:

* A **static** resource appears in `resources/list` and returns its stored content on `resources/read`.
* An **endpoint** resource appears in `resources/templates/list` as a template. The client fills in the URI variables (e.g. an order id) and calls `resources/read`; FlowCP maps each variable to the backing tool's input, invokes it, and returns the live response. URI variables can offer **completion** suggestions so users discover valid values as they type.

## User-context fetch and fail-closed

Endpoint resources follow the same security model as tools. Under the default `user_oauth` auth mode, a read **requires** a valid Bearer token from the AI client and never falls back to a shared credential — see [User-context OAuth](/security/user-context-oauth). If no token is present, the read fails closed with `AUTH_REQUIRED` and no upstream call is made. Only read-only `GET` endpoints can back a resource, so `resources/read` can never trigger a write.

## Opening the resources list

From your app's dashboard, click the **Resources** tab. The page lists every resource on the selected server, each with an on/off switch and a badge showing whether it is **Static** or an **Endpoint template**. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Where endpoint resources come from

Endpoint resource templates are generated automatically when you [connect and import an API](/guides/connect-your-api). Every read-only `GET` endpoint that produces a tool also produces a matching resource template, linked to that tool. Like tools, they are created **disabled** — enable the ones you want to expose. Re-importing is safe: it preserves your enabled state and edits.

### Generating from an existing import

If your app was imported **before** the Resources feature existed, its endpoint templates won't exist yet. You don't need to re-import — click **Generate from API** on the Resources page. FlowCP re-reads the OpenAPI document captured at your last import (no upstream call), generates a resource template for every read-only `GET` endpoint, and links each to the tool it already created. The action is idempotent: existing resources keep their enabled state and edits, and new ones appear **disabled**. If the server has no read-only endpoints, nothing is created.

## Creating a static resource

1. Click **New resource**.
2. Fill in the fields:
   * **Name** — a `snake_case` identifier, e.g. `house_style`.
   * **Description** — a one-line summary shown to the client.
   * **URI** — the fixed URI the content is served at, e.g. `doc://house-style`.
   * **MIME type** — e.g. `text/markdown` or `application/json`.
   * **Content** — the body to serve.
3. Click **Create resource**.

New resources are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model).

## Enabling a resource

Toggle the switch on a resource's row to **On** to offer it to AI clients. Changes take effect immediately — no re-publish required. Disabled resources are never listed or readable.

## Editing and deleting

* Click the **pencil** icon to edit a resource. For static resources you can change the name, description, URI, MIME type, and content. For endpoint resources the backing tool and URI variables come from your imported API; you can adjust the title, description, and URI template.
* Click the **trash** icon to delete a resource. This cannot be undone.

## Publishing

Publishing is blocked if an **enabled endpoint resource** points at a tool that is missing or disabled — otherwise the read would fail closed at runtime. Enable the backing tool, or disable the resource, before publishing. See [Publish your MCP server](/guides/publish-your-mcp-server).


# Serve product documentation

Tools let an AI agent *act* on your API. **Documentation pages** teach it what your product actually means — so when a user asks "how do webhook retries work?", the agent answers from your docs instead of guessing.

Each enabled page is served as an MCP resource at `docs://<slug>`, and the runtime adds two built-in tools:

* **`search_docs`** — free-text search across your page titles, descriptions, slugs, and headings. Returns matching pages with their `docs://` URIs. Called with no query, it lists the available pages, so it doubles as a table of contents.
* **`read_doc`** — returns one page's full text by slug. This is the fallback for AI clients that don't surface resources.

Both tools appear only once you have at least one **enabled** page, so a server without documentation never advertises them.

## Why not just use a Resource?

You can put a short document in a [static resource](/guides/offer-resources), and for one or two pages that is fine. Documentation is a separate capability because a real docs corpus is much larger: pages are stored so that the runtime can list and rank hundreds of them without loading a single page body, and only fetches a body when a client actually reads that page. That is also why the documentation list in the dashboard shows each page's size — it is metadata, not content.

Use **Skills** for *procedures* ("to refund an order, first call…") and **Documentation** for *reference* ("these are the order states and what each means").

## Opening the documentation list

From your app's dashboard, click the **Documentation** tab. The page lists every documentation page on the selected server with an on/off switch, its `docs://` URI, its size, and — for pages that came from a connected repo — a **From repo** badge and the file path it was imported from. A line above the list shows how many pages are enabled. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Writing a page in the dashboard

1. Click **New page**.
2. Fill in the fields:
   * **Slug** — a path-like, lowercase identifier, e.g. `guides/webhooks`. This is what the page is served at: `docs://guides/webhooks`. Use `/` to group related pages.
   * **Title** — optional. Left empty, it defaults to the page's first markdown heading.
   * **Description** — a one-line summary. This is what an agent sees in `search_docs` results, so write it as the question the page answers.
   * **Content** — the page body, in markdown. Headings are indexed for search, so a page with clear `##` sections is easier for an agent to find.
3. Click **Create page**.

New pages are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model).

## Importing pages from a Git repository

If your app is connected to a GitHub or GitLab repository, commit markdown files to a **`Docs/`** directory and FlowCP imports them on each import run:

```
Docs/
├── overview.md              → docs://overview
├── guides/
│   ├── index.md             → docs://guides
│   └── webhooks.md          → docs://guides/webhooks
└── reference/
    └── errors.md            → docs://reference/errors
```

* The file path becomes the slug — sub-directories are the page hierarchy.
* `index.md` and `README.md` collapse to their parent directory, so `Docs/guides/index.md` is served at `docs://guides`.
* Both `.md` and `.mdx` files are imported. A lowercase `docs/` directory works too.
* Optional front matter sets `title`, `description`, and `sortOrder`:

  ```markdown
  ---
  title: 'Setting up webhooks'
  description: 'Register an endpoint and verify signatures'
  ---

  # Setting up webhooks

  …
  ```

Imported pages are created **disabled**, and re-running the import is safe: pages whose content hasn't changed are left alone, and pages you authored by hand in the dashboard are never overwritten. Editing an imported page in the dashboard works, but the next import overwrites your edit — change the file in the repo instead.

See [Connect your API](/guides/connect-your-api) for connecting a repository, and [The standard FlowCP folder](/guides/folder-format) for the full folder layout.

## Enabling a page

Toggle the switch on a page's row to **On** to offer it to AI clients. Changes take effect immediately — no re-publish required. Disabled pages are never listed, never searchable, and never readable.

## Editing and deleting

* Click the **pencil** icon to edit a page. Because the list doesn't carry page bodies, the editor loads the full page when it opens.
* Click the **trash** icon to delete a page. This cannot be undone.

## Limits

* Up to **500 pages** per server, and **200,000 characters** per page.
* The repo importer walks up to three directory levels below `Docs/` and imports up to 400 files per run.

## Documentation and version history

Documentation pages are **not** part of a server's [version history](/guides/version-history) snapshots, and restoring an earlier version leaves your pages untouched. Pages are content — version them in the repository they come from. Every documentation change is still recorded in the server's change log for auditing.

## Keeping pages in sync from your terminal

The [FlowCP CLI](/reference/cli) syncs documentation alongside your other content:

```bash
flowcp sync pull     # write Docs/**.md from the server
flowcp sync push     # upload local Docs/**.md to the server
```

An [exported server](/guides/self-host-export) also carries its documentation: each page is written to `Docs/<slug>.md`, and a self-hosted runtime serves the bodies straight from that folder.


# Offer prompts

Tools let an AI agent *call* your API; skills give it the *know-how* to use them. **Prompts** are different again: they are reusable, parameterized message templates that a client surfaces directly to the **user** — most often as slash-commands. Picking a prompt drops a ready-made starting message into the conversation, for example:

> *Summarize ticket **{{ticket\_id}}** in a **{{tone}}** tone.*

This guide explains how to author prompts and how AI clients consume them.

## How clients consume a prompt

When a prompt is enabled, the runtime advertises the MCP **prompts** capability and offers it to connected clients via the standard protocol:

* **`prompts/list`** — the client lists the available prompts, each with its title, description, and declared arguments. Clients typically render these as slash-commands the user can pick.
* **`prompts/get`** — when the user selects a prompt and fills in its arguments, the client fetches the rendered message. The runtime substitutes the argument values into the template and returns a single user message (plus any embedded resources you attached).

Because prompts are **user-controlled**, they are not called automatically by the model — the user chooses them.

## Arguments and placeholders

A prompt can declare **arguments** the user fills in at selection time. Each argument has a `snake_case` name, a description, and a required flag. Reference an argument anywhere in the body — or in an embedded resource's text — as `{{argName}}`:

```
Summarize ticket {{ticket_id}} in a {{tone}} tone.
```

At `prompts/get`, `{{ticket_id}}` and `{{tone}}` are replaced with the values the user supplied. Required arguments must be provided; optional ones left blank resolve to an empty string. A placeholder with no matching argument is left as-is, so a typo is easy to spot.

## Argument autocomplete (suggested values)

Each argument can carry an optional list of **suggested values**. When you add them, the runtime advertises the MCP **completions** capability and answers the client's `completion/complete` requests for that argument — so as the user types, the client can offer ranked autocomplete suggestions instead of a blank field.

* Enter one value per line in the argument's **Suggested values** box.
* Suggestions are matched against what the user has typed (prefix matches first, then substring), and a response returns at most **100** values.
* Suggested values are hints, not a fixed list — clients still allow free-form input, and the value is never validated against the suggestions.

Leave the box empty for free-text arguments with no suggestions.

## Embedded resources

A prompt can optionally carry **embedded resources** — inline reference blocks (`uri`, `mimeType`, `text`) returned alongside the message. Use these to attach context the model should see, such as a policy snippet or an example payload. Placeholders in a resource's text are substituted too.

## Managing prompts from an agent

Prompts can be managed two ways: from the dashboard (below) or by an AI agent connected to the [FlowCP MCP server](/guides/agent-driven-onboarding). The agent surface exposes prompts as tools — `list_prompts`, `create_prompt`, `update_prompt`, and `delete_prompt` — so an agent can author and toggle prompts for a server without leaving the chat. Like the dashboard, new prompts start **disabled**.

## Opening the prompts list

From your app's dashboard, click the **Prompts** tab. The page lists every prompt on the selected server, each with an on/off switch. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Creating a prompt

1. Click **New prompt**.
2. Fill in the fields:
   * **Name** — a `snake_case` identifier, e.g. `summarize_ticket`. This is the prompt name clients call.
   * **Title** — an optional display title shown to the user.
   * **Description** — a one-line summary so the user knows what the prompt does.
   * **Message template** — the body of the message, with `{{argName}}` placeholders for any arguments.
   * **Arguments** — add one row per argument (name, description, required), with optional **suggested values** (one per line) that power argument autocomplete.
   * **Embedded resources** — optional reference blocks returned with the message.
3. Click **Create prompt**.

New prompts are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model).

## Generating prompts with AI

To get started quickly, click **AI Suggestions** on the Prompts page. FlowCP reads the selected server's tools and asks an LLM to propose **three** prompts that help a user drive those tools, complete with `{{argName}}` placeholders and declared arguments.

Each suggestion shows its name, description, and a preview of the template. Click **Use this** on the one you want — it opens the prompt editor **pre-filled** (including its arguments) so you can review and refine it before saving. Nothing is created until you click **Create prompt**, and (like every prompt) it starts disabled.

AI Suggestions require an AI provider to be configured for the platform. If it isn't, the button reports that suggestions aren't available — you can still author prompts manually.

## Enabling a prompt

Toggle the switch on a prompt's row to **On** to offer it to AI clients. Changes take effect immediately — no re-publish required. Disabled prompts are never listed or returned.

## Editing and deleting

* Click the **pencil** icon to edit a prompt's name, title, description, body, arguments, or embedded resources. Renaming re-derives the slug.
* Click the **trash** icon to delete a prompt. This cannot be undone.

## Writing effective prompts

* **Lead with the task.** The body should read like the message a user would type to kick off the work.
* **Declare every placeholder.** Each `{{argName}}` should have a matching argument so the client can prompt the user for it.
* **Keep arguments minimal.** Ask only for what changes between uses.
* **Use embedded resources for context**, not for the instruction itself.


# Build MCP widgets

Tools let an AI agent *call* your API; **widgets** let it *show* the result as an interactive UI. A widget is an HTML document the server publishes as a `ui://` resource. When you link a widget to a tool, the AI host renders the widget inline in the chat surface instead of just printing the tool's text — a contact picker, an order summary card, a chart, and so on.

Widgets are additive and degrade gracefully: the linked tool still returns its normal JSON/text, so clients that don't support widgets keep working.

## How clients consume a widget

When a widget is enabled and linked to a tool, the runtime:

* Publishes the HTML as a resource at `ui://<slug>` (and a parallel `ui://<slug>/skybridge` variant), so both Anthropic MCP Apps hosts (Claude) and OpenAI Apps SDK hosts (ChatGPT) can render it.
* Decorates the linked tool's metadata so the host knows which resource to render when the tool runs (`_meta.ui.resourceUri` for Claude, `openai/outputTemplate` for ChatGPT).

The host renders the widget in a sandboxed iframe and brokers messages between the widget and your server.

## Managing widgets from an agent

Widgets can be managed two ways: from the dashboard (below) or by an AI agent connected to the [FlowCP MCP server](/guides/agent-driven-onboarding). The agent surface exposes widgets as tools — `list_widgets`, `create_widget`, `update_widget`, and `delete_widget` — so an agent can author and toggle widgets for a server from the chat. Like the dashboard, new widgets start **disabled**.

## Opening the widgets list

From your app's dashboard, click the **Widgets** tab. The page lists every widget on the selected server, each with an on/off switch. If your app has multiple environments (branches), use the branch buttons to switch between them.

## Starting from the library

Don't want to write a widget from scratch? Click **Browse library** to open the **Widget library** — a gallery of prebuilt Generative UI templates (an order summary card, a data table, stat cards, an image gallery, a key/value list, and a contact form). Filter by category, then click **Use template** on the one you want.

The template opens in the widget editor pre-filled with a working name, description, and HTML. Customize it however you like — the HTML is yours to edit — and click **Create widget** to save it. Library templates are just a starting point: once saved, a widget behaves exactly like one you authored by hand, including the default-deny rule (it starts **disabled** until you turn it on).

## Creating a widget

1. Click **New widget** (or start from a template — see above).
2. Fill in the fields:
   * **Name** — a `snake_case` identifier, e.g. `order_summary`. The resource is served at `ui://order-summary`.
   * **Description** — a one-line summary of what the widget shows.
   * **Widget HTML** — the HTML/CSS/JS for the component. A starter template is pre-filled.
   * **Allowed connect domains** (optional) — comma-separated origins the widget may call. The host blocks all others by default.
3. Click **Create widget**.

New widgets are created **disabled**, following the platform's default-deny model — see [Default-deny model](/security/default-deny-model).

### The ext-apps runtime

Widgets talk to the host through the `App` class from `@modelcontextprotocol/ext-apps`. Because host security policy blocks fetching scripts from a CDN, the runtime **inlines** that library for you: put the placeholder `/*__EXT_APPS_BUNDLE__*/` inside a `<script type="module">` tag and the server replaces it with the library at serve time, exposing `globalThis.ExtApps`:

```html
<script type="module">
  /*__EXT_APPS_BUNDLE__*/
  const { App } = globalThis.ExtApps;
  const app = new App({ name: 'OrderSummary', version: '1.0.0' }, {});
  app.ontoolresult = ({ content }) => {
    const data = JSON.parse(content[0].text);
    render(data);
  };
  await app.connect();
</script>
```

`app.ontoolresult` receives the linked tool's return value. Other `App` methods let the widget send a message into the conversation (`sendMessage`), call another tool (`callServerTool`), or open a link (`openLink`).

## Linking a widget to a tool

A widget only renders when a tool it's linked to runs. On the [Tools](/guides/review-and-customize-tools) page, each tool row has a **widget selector** (shown once the server has at least one widget). Pick your widget to link it, or choose **No widget** to unlink. Keep the linked tool's text output meaningful so the experience degrades cleanly on clients without widget support.

## Enabling a widget

Toggle the switch on a widget's row to **On** to publish it. Changes take effect immediately — no re-publish required. Disabled widgets are never exposed as a resource, and tools linked to a disabled widget carry no widget metadata.

## Editing and deleting

* Click the **pencil** icon to edit a widget's name, description, HTML, or allowed domains. Renaming re-derives the `ui://` slug.
* Click the **trash** icon to delete a widget. This cannot be undone. Any tool linked to it is automatically unlinked.

## Tips for effective widgets

* **One tool, one focused widget.** Small, single-purpose widgets render and reason better than a catch-all UI.
* **Mention the widget in the tool description.** It helps the agent decide when to invoke the tool.
* **Return a useful text summary too.** The tool's text content is what users on non-widget clients see.
* **Declare only the domains you need.** Leave *Allowed connect domains* empty unless the widget genuinely calls an external origin.


# Monetize your MCP server

Turn a published MCP server into a revenue source: charge the AI agent that calls your tools, **per call**, and get paid automatically. Agents pay **agentically** — autonomously, with no human checkout — using the [x402](https://x402.org) payment protocol. Each payment **settles directly to your payout address** — FlowCP never holds your funds. FlowCP's small marketplace platform fee is billed separately, onto your FlowCP subscription invoice.

> Monetization is a premium feature, available on the **Team** plan and above. Find it in the dashboard sidebar under **Monetization** (with **Pricing** and **Transactions** pages). If your plan doesn't include it, the Pricing page shows an upgrade prompt.

## How it works

1. You set a **per-call price** on the tools you want to charge for, and a **payout address** for your server.
2. When an agent calls a paid tool **without payment**, the tool returns a payment-required response carrying the price and payment details (the x402 requirements).
3. The agent's wallet constructs a signed payment and **calls the tool again** with the proof attached.
4. FlowCP verifies and settles the payment to your wallet, runs the tool, and records it under **Monetization → Transactions**. Each payment shows your share and the platform fee; the fee is billed to your subscription, not taken from the settlement.

Free tools are never charged, and a tool stays free until you give it a price.

## Enable monetization

Open **Monetization → Pricing** in the sidebar and, under **Payout configuration**:

* Toggle **Enable monetization** on.
* Enter your **Payout address** (the wallet that receives settled funds). This is required to enable a server, and must be a valid **EVM address** (`0x` followed by 40 hex characters) — the page flags a malformed address inline and keeps **Save settings** disabled until it's valid.
* Pick the settlement **Network** and the default **asset** (token) you charge in. Selecting a testnet such as **base-sepolia** shows a **"Testnet — no real funds"** badge so you don't mistake test settlements for real revenue.

The platform fee FlowCP keeps on each payment is shown here. Click **Save settings**.

## Price your tools

Under **Tool pricing**, set each tool to **Free** or **Per call**. For a paid tool, enter the **price in atomic token units** — the smallest unit of the token. For example, USDC has 6 decimals, so `10000` = 0.01 USDC. Click **Save** on the row.

Prices take effect immediately. The price is also advertised to agents in the tool's metadata, so a well-behaved agent knows the cost before it calls.

### How agents see your prices

As soon as at least one tool has a price, your server gains a built-in **`get_pricing`** tool that returns your whole price list in one call — the tool name, price, and asset for each paid tool, plus a count of the free ones. An agent can read it before planning work instead of discovering each price from a payment-required error. It costs nothing to call, and it does not appear at all while every tool is free.

Prices also show up in `search_tools` results (for servers large enough to use dynamic tool exposure), and the server's connection instructions tell the model how many tools charge and to check `get_pricing` first.

> If your own API already exposes a tool named `get_pricing`, yours wins and the built-in is skipped — agents then read prices from each tool's metadata instead. Rename yours if you'd rather have the catalog.

> If your app has more than one environment (server), use the **Environment** selector at the top of the page — payout config and prices are per server.

## Track earnings and transactions

The **Monetization → Transactions** page opens with an **earnings summary** — stat cards for **Gross earned** (everything agents paid), **Your share** (settled to your wallet), **Platform fee** (FlowCP's marketplace cut), and **Fees pending** (fee that has settled but not yet been billed to your subscription). Amounts are shown in human units (e.g. `0.01 USDC`) rather than raw atomic units.

Below the summary, the table lists every settled payment: the tool, the amount the agent paid, your share, the platform fee, the network, the status, and a **Fee billed** indicator (**Billed** once the platform fee for that payment has been charged to your subscription, **Pending** until the hourly reconciliation runs). This is your record of what each paid tool has earned.

## Test it in the Playground

You don't need a real wallet or an external agent to see the paid flow end to end. Open the **Playground** for the server and, in the config rail under **Test payments**, set a **Test wallet** (click **Generate** for a throwaway `0x…` address) and optionally turn on **Auto-pay test payments**.

Now ask the model to call a paid tool:

* With **auto-pay on**, the Playground pays the tool with a mock x402 payment from your test wallet and retries automatically; the tool call shows a **paid** receipt badge with the amount, payer, and network.
* With **auto-pay off**, the turn pauses and a **Payment required** card shows the price and asks for a test wallet — fill it in and click **Pay & retry**.

These are **mock payments** (no real funds) and only settle against the built-in mock facilitator used in development and self-host runtimes. They still produce a real **receipt**, so you can watch it appear under **Monetization → Transactions**. See [Chat-test in the Playground](/guides/chat-test-your-mcp-server) for the full Playground guide.

## Notes

* **Payment is required before the tool runs.** A paid tool never executes without a verified, settled payment — there is no "call now, pay later".
* **A payment is single-use.** Each payment proof settles once; an agent must present a fresh payment for each call.
* **Pricing is per server.** The payout address, network, and fee apply to the whole server; prices are per tool.

For the agent-developer side of this flow (the exact request/response shape), see [Agentic payments (x402)](/reference/agentic-payments).


# Configure MCP Auth

FlowCP authenticates end users with your API's OAuth 2.0 provider. This guide walks through setting up the OAuth provider configuration so your MCP server can act on behalf of real users.

An app can have **multiple named OAuth configs**, and you assign **one config to each app branch** on the [Branches page](/guides/manage-branches). A config can serve many branches, but each branch uses exactly one config. This lets, for example, a staging branch point at a staging identity provider while the live branch uses production credentials.

> **Note:** [Changing an app's provider](/guides/connect-your-api#changing-an-apps-provider) (for example, OpenAPI → Git) **resets the app's OAuth configuration** — credentials and URLs are provider-specific. After switching, set up OAuth again here and assign a config to each branch.

FlowCP supports OAuth for any provider, with first-class support for **Bubble**, **Auth0**, **Clerk**, **Okta**, **Microsoft Entra ID**, **AWS Cognito**, **Keycloak**, **WorkOS (AuthKit)**, **Zitadel**, **FusionAuth**, **Ory Hydra**, **Frontegg**, **Descope**, **Stytch**, and **Supabase**, plus a **Generic OAuth2** option for everything else. You pick a provider in each OAuth config; the rest of the setup (Client ID, Client Secret, redirect URI) is the same. Depending on the provider you supply a few provider-specific fields (see [Provider-specific settings](#provider-specific-settings)):

* **Bubble** — FlowCP derives the authorize/token URLs from your Bubble app.
* **Auth0** — enter your Auth0 **domain** (`tenant.auth0.com` or a custom domain) and, optionally, an **audience** and **organization**.
* **Clerk** — enter your Clerk **domain** (Frontend API).
* **Okta** — enter your Okta **domain** and, optionally, a custom **Authorization Server ID**.
* **Microsoft Entra ID** — enter your **directory (tenant) ID**.
* **AWS Cognito** — enter your user pool **region** and **User Pool ID** (and, optionally, a Hosted UI **domain**).
* **Keycloak** — enter your **base URL** and **realm**.
* **WorkOS (AuthKit)**, **Zitadel**, **FusionAuth**, **Ory Hydra**, **Frontegg** — enter the provider **domain** (FlowCP discovers the rest).
* **Descope** — enter your **project ID**.
* **Stytch** — enter your **project ID** and pick the **environment** (Live or Test).
* **Supabase** — enter your **project ref** (the `{ref}.supabase.co` subdomain).
* **Generic OAuth2** — enter the `authorize` and `token` URLs directly.

## Authentication modes

OAuth is the default and recommended mode, but not every API offers per-user OAuth. Each branch picks an **Authentication** mode on the [Branches page](/guides/manage-branches):

| Mode                   | How the upstream call is authenticated                                                                                                                                                                                 | Per-user context                  |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| **User OAuth**         | The end user's own OAuth access token, forwarded on every call.                                                                                                                                                        | ✅ Yes                             |
| **Enterprise managed** | A shared credential — but the **caller** is verified against your own identity provider first, and their group claims decide which tools they can use. See [Enterprise managed auth](/guides/enterprise-managed-auth). | ✅ Yes (identity + per-tool rules) |
| **API key**            | A shared static key you configure, sent as a header or query parameter.                                                                                                                                                | ❌ No                              |
| **Static bearer**      | A shared static bearer token you configure.                                                                                                                                                                            | ❌ No                              |
| **None**               | No credential is sent. All calls are unauthenticated.                                                                                                                                                                  | ❌ No                              |

> **Shared-credential modes (API key, Static bearer, None) relax the per-user guarantee.** Every end user acts through one identity, so FlowCP cannot scope calls to individual users or attribute audit-log entries to them. Use these modes only for APIs that have no per-user OAuth, and prefer **User OAuth** whenever it is available. See [User-context OAuth](/security/user-context-oauth).

The static key/token is encrypted at rest with AES-256-GCM, never returned after saving, never written to logs, and never cached. Switching a branch back to **User OAuth** clears any stored static credential.

You create configs for any of these modes on the **MCP Auth** page: click **Add config** and pick the auth type first (User OAuth, API key, Static bearer token, or No authentication). User OAuth opens the provider editor described below; the shared-credential modes open a short form for the credential and (for API key) its header/query placement. Each saved config can then be assigned to one or more branches on the [Branches page](/guides/manage-branches). The rest of this guide covers **User OAuth**.

## How the OAuth flow works

1. An end user's AI client connects to FlowCP's MCP server.
2. FlowCP checks whether the AI client has included a valid access token.
3. If not, FlowCP returns an authorization challenge. The AI client redirects the user to FlowCP's OAuth endpoint.
4. FlowCP redirects the user to your provider's authorization page.
5. The user logs in with your provider and grants access.
6. The provider redirects back to FlowCP with an authorization code.
7. FlowCP exchanges the code for access and refresh tokens and returns them to the AI client as an encrypted auth code.
8. The AI client holds the access token and presents it on every subsequent MCP tool call — FlowCP forwards it directly to your API.
9. When the token expires, your API returns 401 and FlowCP signals the AI client to re-authorize automatically.

## Step 1 — Enable OAuth with your provider

Set up an OAuth 2.0 client (sometimes called an "app" or "integration") with your API provider and note the **Client ID**, generate a **Client Secret**, and find the **authorize** and **token** endpoint URLs. Add the FlowCP redirect URI as an allowed redirect URL (see Step 2).

**Bubble:** Go to **Settings → API**, enable **3rd Party OAuth / SAML Access**, note the **Client ID**, and generate a **Client Secret**. FlowCP derives the Bubble authorize/token URLs automatically, so you do not enter them by hand.

## Step 2 — Get the FlowCP redirect URI

In the FlowCP dashboard, open your app and click **MCP Auth** in the sidebar.

The **OAuth redirect URI** field shows a read-only URL like:

```
https://api.flowcp.io/oauth/callback
```

Use the copy button next to the field to grab the URL, then paste it into your provider's allowed redirect URLs list.

## Step 3 — Add an OAuth config

On the same **MCP Auth** page in FlowCP, click **Add config**, choose **User OAuth** as the auth type, and:

1. Enter a **Name** to tell this config apart from others (e.g. “Production”, “Staging”).
2. Choose your **provider** (Bubble, Auth0, Clerk, Okta, Microsoft Entra ID, AWS Cognito, Keycloak, WorkOS/AuthKit, Zitadel, FusionAuth, Ory Hydra, Frontegg, Descope, Stytch, Supabase, or Generic OAuth2).
3. Enter the **Client ID** from your provider.
4. Enter the **Client Secret** (treated as a write-only field — once saved, the value is not displayed again).
5. Fill in the provider-specific fields below.
6. Click **Create config**.

Repeat **Add config** for each distinct set of credentials you need. FlowCP encrypts each client secret at rest using AES-256-GCM.

> **The first config you add is assigned to every branch that doesn't yet have one.** After that, assign configs to branches yourself on the [Branches page](/guides/manage-branches).

> The **provider** selector appears for apps connected via a Generic OpenAPI/Swagger URL, letting you choose Generic OAuth2, Auth0, Clerk, Okta, Microsoft Entra ID, or AWS Cognito. Apps connected as **Bubble** are fixed to the Bubble provider — there is no selector, and the authorize/token URLs are derived for you.

## Step 4 — Assign configs to branches

Open **Settings → Branches**. Each branch has an **Authentication** dropdown. With **User OAuth** selected (the default), an **OAuth config** dropdown lists your configs — pick the one that branch should authenticate with. A branch left on **None — not configured** will not authenticate — its published MCP endpoint returns an error until you assign a config. To use a shared API key or bearer token instead, pick that mode and either assign an app-level config you created on the **MCP Auth** page or enter the credential inline (see [Authentication modes](#authentication-modes)).

See [Managing branches](/guides/manage-branches) for details.

## Provider-specific settings

| Provider               | Required fields            | Optional fields                                                                     | Default scopes                              |
| ---------------------- | -------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------- |
| **Bubble**             | — (URLs derived)           | `scopes`                                                                            | —                                           |
| **Auth0**              | `domain`                   | `audience`, `orgId` (Organizations)                                                 | `openid profile email`                      |
| **Clerk**              | `domain`                   | `sameDomain` (use the `__session` cookie)                                           | `openid profile email`                      |
| **Okta**               | `domain`                   | `authServerId` (custom Authorization Server), `classic` (Okta Classic / Org server) | `openid profile email okta.users.read.self` |
| **Microsoft Entra ID** | `tenantId`                 | `scopes`                                                                            | `openid profile email offline_access`       |
| **AWS Cognito**        | `region`, `userPoolId`     | `domain` (Hosted UI domain override), `scopes`                                      | `openid profile email`                      |
| **Keycloak**           | `baseUrl`, `realm`         | `scopes`                                                                            | `openid profile email`                      |
| **WorkOS (AuthKit)**   | `domain`                   | `scopes`                                                                            | `openid profile email offline_access`       |
| **Zitadel**            | `domain`                   | `scopes`                                                                            | `openid profile email offline_access`       |
| **FusionAuth**         | `domain`                   | `scopes`                                                                            | `openid profile email offline_access`       |
| **Ory Hydra**          | `domain`                   | `scopes`                                                                            | `openid profile email offline_access`       |
| **Frontegg**           | `domain`                   | `scopes`                                                                            | `openid profile email`                      |
| **Descope**            | `projectId`                | `scopes`                                                                            | `openid profile email`                      |
| **Stytch**             | `projectId`                | `environment` (`live` / `test`), `scopes`                                           | `openid profile email`                      |
| **Supabase**           | `projectRef`               | `scopes`                                                                            | `openid profile email`                      |
| **Generic OAuth2**     | `authorizeUrl`, `tokenUrl` | `userInfoUrl`, `issuer`, `scopes`                                                   | —                                           |

For every provider except Bubble and Generic OAuth2, FlowCP resolves the authorize, token, and userinfo endpoints automatically from the provider's OIDC discovery document, so you only enter a few identifiers:

* **Auth0 / Clerk / Okta / WorkOS (AuthKit) / Zitadel / FusionAuth / Ory Hydra / Frontegg** — discovery runs against `https://{domain}/.well-known/openid-configuration`.
* **Microsoft Entra ID** — discovery runs against the v2.0 endpoint `https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration`. `tenantId` accepts a directory GUID, a verified domain, or one of `common` / `organizations` / `consumers`. The default scopes include `offline_access`, which Entra requires before it will issue a refresh token.
* **AWS Cognito** — discovery runs against the user pool issuer `https://cognito-idp.{region}.amazonaws.com/{userPoolId}`, which advertises your Hosted UI endpoints. Set the optional **Hosted UI domain** only to override the resolved endpoints. Cognito app clients that have a secret authenticate to the token endpoint with HTTP Basic automatically.
* **Keycloak** — discovery runs against the realm issuer `{baseUrl}/realms/{realm}/.well-known/openid-configuration`. Include an `/auth` path segment in `baseUrl` for legacy (≤16) deployments. Confidential clients authenticate to the token endpoint with HTTP Basic automatically.
* **Descope** — discovery runs against `https://api.descope.com/{projectId}`. Your Descope project ID is also the OAuth client ID.
* **Stytch** — discovery runs against `https://{api|test}.stytch.com/v1/public/{projectId}` (Connected Apps). Pick **Live** (`api.stytch.com`) or **Test** (`test.stytch.com`) with the environment field.
* **Supabase** — discovery runs against `https://{projectRef}.supabase.co/auth/v1/.well-known/openid-configuration` (the Auth OAuth 2.1 server). Confidential clients authenticate to the token endpoint with HTTP Basic automatically.

Okta additionally validates the returned ID token's `nonce` to protect the login round trip.

For **Generic OAuth2**, the `authorizeUrl` and `tokenUrl` you enter are authoritative — FlowCP always uses them directly. The optional `issuer` is a **fallback only**: it is used to discover endpoints (via its `/.well-known/openid-configuration`) when you leave the explicit URLs blank, and is otherwise retained only as token metadata. It never overrides URLs you have entered.

> **Do not set `issuer` to FlowCP's own origin.** Discovery against FlowCP resolves to its `/oauth/authorize` endpoint (the AI-client authorization server, not your upstream IdP) and creates a redirect loop that fails with `unknown client_id`. To use the FlowCP platform identity provider, set `authorizeUrl` to `…/id/authorize` and `tokenUrl` to `…/id/token` and leave `issuer` blank. FlowCP rejects a same-origin `issuer` (and same-origin `/oauth/*` authorize/token URLs) at save time.

## Token refresh

FlowCP supports the OAuth2 **`refresh_token`** grant, so AI clients keep working without re-prompting the user. When an MCP client's access token nears expiry it silently exchanges its refresh token at the token endpoint; FlowCP resolves the server's provider and refreshes through it (for your own provider this requires that the provider issues refresh tokens — e.g. Auth0/Okta with `offline_access`). The refresh token FlowCP issues carries its own server context, so refresh works the same whether the client calls the generic `/oauth/token` endpoint or the per-server `/oauth/mcp/:serverId/token` endpoint — no client configuration is needed and nothing is stored server-side, the AI client holds the tokens. If a refresh token is invalid or expired, FlowCP returns `invalid_grant` and the client simply re-runs the normal authorization flow.

## Custom domain (Bubble, Live)

If your Bubble app has a **custom domain** configured (detected automatically when you connect, or set under **Settings**), FlowCP sends users to Bubble OAuth on that domain for **Live** MCP servers — for example `https://inova.us/api/1.1/oauth/authorize` instead of `*.bubbleapps.io`. This avoids cross-domain login loops.

The FlowCP redirect URI (`https://api.flowcp.io/oauth/callback`) does not change. Register it with your provider as usual.

## Updating credentials

To rotate a client secret:

1. Generate a new secret with your provider.
2. On the FlowCP OAuth page, click **Edit** on the config you want to rotate.
3. Enter the new secret in the **Client Secret** field. Leave the **Client ID** unchanged. (Leaving the secret blank keeps the existing one.)
4. Click **Save changes**.

New authorizations will use the new credentials. Users who have already authorized will continue working until their current access token expires, at which point the AI client will automatically trigger a new authorization using the updated credentials.

## Deleting an OAuth config

Click the **trash** icon on a config to remove it. Any branches assigned to that config become **unassigned** and stop authenticating until you assign them another config. After deletion, no new authorizations can be completed with those credentials, and existing AI client sessions stop working once their current token expires.

## After changing an app's provider

[Changing an app's provider](/guides/connect-your-api#changing-an-apps-provider) preserves your existing OAuth configs — they are attached to the app, not to the branches it re-creates, and the new branches re-inherit the app's default config. Review them afterwards, though: the appropriate OAuth provider differs between Bubble apps (which default to the **Bubble** provider) and generic OpenAPI or Git apps (which default to **Generic OAuth2**). Update or [add a config](#step-3--add-an-oauth-config) so it matches the API you switched to.

## UserInfo endpoint

FlowCP exposes a per-server **userinfo endpoint** so an AI client can resolve the connected account behind an access token:

```
GET https://api.flowcp.io/oauth/userinfo/mcp/{serverId}
Authorization: Bearer <access token>
```

The endpoint is advertised as `userinfo_endpoint` in each server's authorization-server discovery document (`/.well-known/oauth-authorization-server/mcp/{serverId}`), so OIDC-aware clients pick it up automatically. It is **per-server** because a bare access token doesn't say which OAuth config issued it — the `serverId` resolves the config and provider endpoints. The global discovery document does not advertise a userinfo endpoint.

FlowCP forwards the token to your provider's own userinfo endpoint and returns the normalized fields `sub`, `email`, and `scope`. It never stores the token.

> **Bubble apps:** Bubble does not expose a userinfo endpoint by default, so this route returns `userinfo_unsupported` for Bubble configs unless you set a `userInfoUrl` in the config metadata. It works out of the box for **Auth0**, **Clerk**, **Okta**, **Microsoft Entra ID**, **AWS Cognito**, **Keycloak**, **WorkOS (AuthKit)**, **Zitadel**, **FusionAuth**, **Ory Hydra**, **Frontegg**, **Descope**, **Stytch**, **Supabase**, and the **Generic OAuth2** provider (with a discoverable or configured `userInfoUrl`).

## Agent authentication discovery

Once a server uses user-context OAuth, FlowCP automatically advertises how AI agents can authenticate to it via the [auth.md](/guides/agent-auth-discovery) protocol — no extra setup required.


# Enterprise managed auth

> **Enterprise plan.** Managed auth is available on the Enterprise plan.

Managed auth makes **your own identity provider the authority** for who may reach an MCP server. Instead of every end user running their own OAuth consent flow — or everyone sharing one static credential — your IdP issues access tokens, FlowCP verifies each one before running anything, and the token's group memberships decide which tools that caller can use.

This is the mode to reach for when:

* Your organization already runs Okta, Microsoft Entra ID, Auth0, Keycloak, or any other OIDC provider, and you want MCP access governed there.
* Different teams should see different tools on the same server.
* Removing someone from a group in your IdP must remove their MCP access.

## How it compares

| Mode                 | Who the caller is                             | Who decides what they can do   |
| -------------------- | --------------------------------------------- | ------------------------------ |
| **User OAuth**       | Verified by your upstream API                 | Your API's own permissions     |
| **Managed auth**     | Verified by FlowCP against **your IdP**       | **Access rules** you configure |
| **API key / bearer** | Not identified — everyone shares one identity | Nothing per-user               |
| **None**             | Not identified                                | Nothing per-user               |

Managed auth authenticates the **caller**. The upstream API call still carries the server's **shared credential**, so you configure both: an identity provider (who may connect) and a shared auth config (how FlowCP talks to your API).

## 1. Add an identity provider

On the **MCP Auth** page for your app, find **Enterprise managed auth** and add a provider:

| Field             | What to enter                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| **Name**          | A label, e.g. `Corporate IdP`. Defaults to `Default`.                                            |
| **Issuer URL**    | The exact `iss` claim your tokens carry, e.g. `https://login.microsoftonline.com/<tenant>/v2.0`. |
| **JWKS URL**      | Where your IdP publishes its signing keys.                                                       |
| **Audience**      | The `aud` claim your IdP mints for this MCP server, e.g. `api://flowcp-mcp`.                     |
| **Subject claim** | Claim carrying the stable user id. Defaults to `sub`.                                            |
| **Groups claim**  | Claim carrying group/role membership. Defaults to `groups`.                                      |

Both URLs must be public `https://` addresses — FlowCP fetches the JWKS server-side, so private and link-local addresses are rejected.

**Audience is not optional.** A token with no audience restriction can be replayed against every resource that trusts your issuer. Register this MCP server as its own API/resource in your IdP and put that identifier here.

### Provider notes

* **Microsoft Entra ID** — issuer is `https://login.microsoftonline.com/<tenant>/v2.0`, JWKS is `https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys`. Entra emits `groups` as object IDs unless you configure group claims to emit names; use whichever your rules will match on.
* **Okta** — issuer is your authorization server (`https://<domain>/oauth2/<serverId>`), JWKS is that issuer plus `/v1/keys`. Add a `groups` claim to the access token.
* **Auth0** — issuer is `https://<tenant>/`, JWKS is `.../.well-known/jwks.json`. Groups usually arrive via a custom namespaced claim; set **Groups claim** to it.
* **Keycloak** — issuer is `https://<host>/realms/<realm>`, JWKS is that plus `/protocol/openid-connect/certs`.

## 2. Assign it to a branch

On the [Branches page](/guides/manage-branches), set a branch's **Authentication** to **Enterprise managed auth**, then pick the identity provider. A branch shows "Won't authenticate" until one is assigned.

You still need a shared auth config for the upstream call. If your API needs no credential, create a shared auth config with mode **None** and assign that.

## 3. Write access rules

Access rules map claims to tools. Each rule has:

* **Claim** — which claim to read, e.g. `groups`.
* **Operator** — `contains` (claim includes the value), `one_of` (claim overlaps any listed value), or `equals` (a single-valued claim equals it).
* **Values** — what to match.
* **Effect** — `allow` or `deny`.
* **Scope** — a single tool, or the whole server.

The rules resolve like this:

1. **A server with no rules allows everything.** Tools still have to be enabled.
2. **Deny wins over allow.** A blanket allow can't punch through a targeted deny.
3. **Otherwise a matching allow is required** — a server that has rules denies any caller none of them match. This is default deny, on purpose.

A worked example. Everyone in `mcp-users` may use the server; only `mcp-admins` may use `delete_customer`:

| Scope             | Claim    | Operator   | Values       | Effect |
| ----------------- | -------- | ---------- | ------------ | ------ |
| Whole server      | `groups` | `contains` | `mcp-users`  | allow  |
| `delete_customer` | `groups` | `contains` | `mcp-admins` | allow  |

Someone in `mcp-users` but not `mcp-admins` will not even **see** `delete_customer` — denied tools are hidden from the tool list, not just blocked when called. Resources backed by a hidden tool are hidden too.

## What the client sends

The MCP client presents your IdP's access token as a normal bearer token:

```http
POST /mcp/<serverId>
Authorization: Bearer <access token from your IdP>
```

A missing token, an expired one, a wrong audience, a wrong issuer, or a bad signature all return `401` with a `WWW-Authenticate` challenge. The response never says *which* check failed — that detail stays in FlowCP's server-side logs so the endpoint can't be used to probe your tokens.

## Revoking access

Removing a user from a group in your IdP takes effect as soon as they get a new token — their old one keeps its old claims until it expires, which is why short token lifetimes matter here. Deleting the identity provider config unassigns every branch using it, and those branches then refuse all traffic until you assign a new one.

## Audit

Every tool call from a managed server records the verified issuer and a **hash** of the caller's subject in the [execution log](/reference/execution-logs), so calls are attributable to a real corporate identity. The raw subject is never stored, and claims are never logged.

## Limits

* Managed auth needs the hosted runtime. [Self-hosted bundles](/guides/self-host-export) refuse the mode rather than starting up without the ability to verify callers.
* Tools that require an OAuth **scope** can't be published on a managed server — scopes describe a per-user OAuth token, and access rules replace them here.
* Tokens must be signed with `RS256` or `ES256`. Symmetric (`HS256`) and unsigned tokens are rejected.


# Configure alerts

Alerts notify you when one of your app's published MCP servers starts misbehaving — for example, a spike in failed calls or unauthorized (401/403) requests. You choose the condition, the threshold, the time window, and where the notification is delivered: email, Slack, or both.

## Creating an alert

1. Open your app and click **Alerts** in the sidebar.
2. Click **Create alert** and fill in:
   * **Name** — a label so you can tell your rules apart (e.g. "High error rate").
   * **Condition** — what to watch for:
     * **All failures (4xx + 5xx)** — any call that returned an HTTP error.
     * **Unauthorized (401 / 403)** — calls rejected for authentication or permission reasons.
   * **Threshold** — how many matching calls must occur before the alert fires.
   * **Time window** — the rolling window matching calls are counted over (5 minutes up to 24 hours).
   * **Cooldown** — after an alert fires, how long to wait before notifying again, so a single sustained spike doesn't flood your channels.
   * **Recipients** — zero or more comma-separated email addresses to notify.
   * **Slack webhook URL** — optionally post the alert to a Slack channel.
3. Leave **Enabled** on, then click **Create alert**.

You must provide at least one delivery channel — email recipients, a Slack webhook, or both.

## Sending alerts to Slack

To post alerts into a Slack channel, create a Slack [Incoming Webhook](https://api.slack.com/messaging/webhooks) for the channel you want notified, then paste its URL (it starts with `https://hooks.slack.com/services/…`) into the **Slack webhook URL** field. When the alert fires, FlowCP posts a message naming the rule, the app, and the count that crossed the threshold, with a button linking back to this Alerts page.

For your security, the webhook URL is encrypted at rest and never shown again once saved. When editing a rule, the field shows **Connected** — paste a new URL to replace it, or click **Remove** to stop sending to Slack.

## How alerts are evaluated

FlowCP checks your alert rules continuously in the background. For each enabled rule it counts the matching calls across the app's **published** servers within the time window. When the count reaches the threshold, every email recipient is notified, a Slack message is posted (if a webhook is configured), and the rule enters its cooldown. While a rule is in cooldown it will not send another notification, even if the condition is still met.

Only **published** servers are evaluated — draft and disabled servers do not generate alerts.

## Editing and disabling

Click **Edit** on a rule to change its condition, threshold, window, cooldown, recipients, or Slack webhook. Turn off the **Enabled** switch to pause a rule without deleting it. Click the trash icon to remove a rule entirely.

## Tips

* Start with a generous threshold and tighten it once you know your app's normal traffic. The [Execution logs](/reference/execution-logs) and [Usage stats](/reference/usage-stats) pages help you pick a sensible number.
* Use a short cooldown for noisy, fast-moving incidents and a longer one for conditions you only need to hear about occasionally.


# Stream logs to external sinks

Log streaming forwards your MCP execution logs to an external observability system — **Axiom**, **ClickHouse**, **OpenTelemetry**, **Datadog**, **Grafana Loki**, **Splunk** (via the HTTP Event Collector), or any **generic HTTP** endpoint — so MCP activity sits alongside the rest of your telemetry.

Destinations are configured **per workspace**, not per app: one destination covers every app in the workspace, including apps you connect later. You can still narrow a destination to a single app when you want its logs to land somewhere separate. A workspace can have as many destinations as you like.

Log streaming is available on the **Team**, **Business** and **Enterprise** plans. When your plan unlocks it, a **Log streaming** entry appears under **Monitor** in the sidebar, next to **Logs**. The entry sits in an app's menu for convenience, but the destinations it opens are workspace-level — the same list whichever app is selected.

Because a destination forwards execution logs to an external endpoint with a credential you provide, **only workspace owners can add, edit, or remove destinations**. Members can view the current configuration but not change it.

## Adding a destination

1. Open **Monitor → Log streaming** in the sidebar.
2. Click **Add destination** and pick where the logs should go.
3. Fill in the form:
   * **Name** — a label so you can tell destinations apart (e.g. "Axiom prod").
   * **Destination** — pre-filled from your pick; change it here if you chose the wrong one.
   * **Apps** — **All apps in this workspace** (the default) or a single app.
   * The destination's own fields (see the table below).
   * **API key / token** — the credential for the destination.
   * **Only forward errors** / **Minimum status code** — optionally restrict which calls are forwarded (e.g. errors only, or status ≥ 500).
4. Leave **Enabled** on, then click **Create destination**.

### What each destination asks for

| Destination       | Fields                                  | Credential      | Where to get it                                        |
| ----------------- | --------------------------------------- | --------------- | ------------------------------------------------------ |
| **Axiom**         | Region, Dataset                         | Axiom API token | Axiom → Settings → API tokens (needs ingest rights)    |
| **ClickHouse**    | Endpoint URL, Table, User               | Password        | Your ClickHouse service credentials                    |
| **OpenTelemetry** | Endpoint URL, Auth header name *(opt.)* | Token           | Whatever your collector expects                        |
| **Datadog**       | DataDog site                            | DataDog API key | DataDog → Organization Settings → API Keys             |
| **Grafana Loki**  | Endpoint URL, User id *(opt.)*          | API token       | Grafana Cloud → Access policies, or your Loki tenant   |
| **Splunk (HEC)**  | Endpoint URL                            | HEC token       | Splunk → Settings → Data inputs → HTTP Event Collector |
| **Generic HTTP**  | Endpoint URL, Auth header name *(opt.)* | Bearer token    | Whatever token your collector expects                  |

Notes on the addressing fields:

* **Region / DataDog site** — Axiom and Datadog are addressed by an allowlisted intake host rather than a free-form URL. Pick the one matching your account (it is shown in the service's own URL).
* **Endpoint URL** — always `https://`. Splunk takes the HEC base URL (`/services/collector` is appended), Loki the base URL (`/loki/api/v1/push` is appended), and OpenTelemetry either the collector base URL or the full signal URL (`/v1/logs` is appended when missing).
* **Table** *(ClickHouse)* — an existing table, optionally database-qualified (`logs.flowcp_mcp`). Rows are inserted as `JSONEachRow`, so its columns must match the forwarded fields:

  ```sql
  CREATE TABLE logs.flowcp_mcp (
      id                  String,
      toolName            String,
      statusCode          UInt16,
      latencyMs           UInt32,
      errorCode           Nullable(String),
      mcpClientName       String,
      mcpClientVersion    Nullable(String),
      llmModel            Nullable(String),
      endUserConnectionId Nullable(String),
      createdAt           DateTime64(3)
  ) ENGINE = MergeTree ORDER BY (createdAt, id);
  ```
* **User id** *(Grafana Loki)* — Grafana Cloud authenticates the push endpoint with basic auth, where the user is your numeric stack id. Leave it blank to send the token as `Authorization: Bearer` instead.
* **Auth header name** *(OpenTelemetry / Generic HTTP, optional)* — leave blank to send `Authorization: Bearer <token>`, or set a custom header (e.g. `X-API-Key`) to send the raw token instead.

## What gets sent

Only the privacy-safe fields from the [Execution logs](/reference/execution-logs) are forwarded — never tokens, request/response bodies, or authorization headers. Each record includes the tool name, status code, latency, error code, the MCP client name/version, the LLM model (when reported), a hashed end-user key, and the timestamp.

Only **published** servers are streamed — draft and disabled servers do not generate forwarded logs.

## How delivery works

FlowCP polls for new execution logs in the background and delivers them in batches roughly once a minute, so expect up to about a minute of latency. Each destination tracks its own position, so a destination added today only forwards logs created from that point on — it does not backfill history. Failed batches are retried automatically; if delivery keeps failing, the destination shows a **Delivery error** with the latest message.

For your security, the API key/token is encrypted at rest and never shown again once saved. When editing a destination, the secret field shows **Configured** — paste a new value to replace it, or click **Remove** to clear it.

Endpoint URLs are validated to block private and reserved network ranges, so a destination can only ever reach a public host. Intake regions, dataset and table names are checked against a strict allowlist or charset for the same reason.

## Editing and removing

Click **Edit** on a destination to change its settings, its app scope or its filters, or toggle **Enabled** to pause streaming without deleting it. Click the trash icon to remove a destination entirely.


# Send events to a webhook

A webhook pushes your app's events to an endpoint you own, as they happen — a visitor starting a chat conversation, each message in that conversation, a server going live. Instead of polling the API, your system is told.

Every app can have up to **10 webhooks**, each with its own destination, its own list of events, and its own signing secret. Webhooks are available on every plan; open an app and click **Webhooks** in the sidebar.

Because a webhook sends your app's activity to an endpoint of your choosing, **only workspace owners can add, edit, or remove webhooks**. Members can view the configuration and the delivery log, but not change it.

## Adding a webhook

1. Open your app and click **Webhooks** in the sidebar.
2. Click **Add webhook** and fill in:
   * **Name** — a label so you can tell webhooks apart (e.g. "CRM sync").
   * **Destination URL** — the `https://` endpoint that receives the POSTs. It must be publicly reachable; private and internal addresses are rejected.
   * **Events** — which events to send (at least one).
   * **Include message text** — off by default. When off, message events carry metadata only. See [What gets sent](#what-gets-sent).
3. Leave **Enabled** on, then click **Create webhook**.
4. **Copy the signing secret.** It is shown once and cannot be read again. If you lose it, use **Rotate secret** to mint a new one.

Then click **Test** on the webhook to send a `webhook.test` ping and confirm your endpoint answers.

## Events

| Event                  | Fires when                                                                        |
| ---------------------- | --------------------------------------------------------------------------------- |
| `conversation.created` | A visitor starts a new chat-widget conversation.                                  |
| `message.created`      | A message is added to a conversation — one per user turn and per assistant reply. |
| `server.published`     | An MCP server is published (or redeployed) to production.                         |

`webhook.test` is also delivered when you click **Test**. It is not subscribable: it goes only to the webhook you tested.

Conversation events come from the [chat widget](/guides/embed-ai-chat-widget). They do not depend on transcript retention — a widget that stores nothing still emits them.

## What a delivery looks like

Each delivery is a `POST` with a JSON body:

```json
{
  "id": "evt_9f2c…",
  "type": "message.created",
  "createdAt": "2026-09-20T10:31:02.481Z",
  "workspaceId": "ws_…",
  "appId": "app_…",
  "data": {
    "conversation": {
      "id": "conv_…",
      "key": "6b1f…",
      "embedId": "emb_…",
      "embedName": "Support widget",
      "serverId": "srv_…",
      "origin": "https://app.example.com",
      "context": { "page": "/orders/1042" }
    },
    "message": {
      "role": "user",
      "sequence": 3,
      "contentLength": 42,
      "attachments": 1
    }
  }
}
```

and these headers:

| Header                 | Value                                                  |
| ---------------------- | ------------------------------------------------------ |
| `X-FlowCP-Signature`   | `t=<unix seconds>,v1=<hex HMAC-SHA256>` — see below.   |
| `X-FlowCP-Event`       | The event type, e.g. `message.created`.                |
| `X-FlowCP-Event-Id`    | The event id. Stable across webhooks and redeliveries. |
| `X-FlowCP-Delivery-Id` | This attempt's delivery id.                            |
| `X-FlowCP-Webhook-Id`  | Which webhook this was sent to.                        |

Answer with any `2xx` status. Anything else counts as a failure and is retried.

`conversation.key` is a one-way hash of the visitor's conversation handle — it correlates every event of one thread without exposing the handle itself.

## What gets sent

Payloads carry ids, roles, counts, names and timestamps. They never carry end-user access tokens, the visitor's conversation handle, tool arguments, or upstream API responses.

**Message text is withheld by default.** A `message.created` event normally reports the role, its position in the thread, how long the text was, how many attachments it had, and which tools an assistant turn called — but not the words. Turn on **Include message text** for a webhook to add `data.message.content` (capped at 20,000 characters) to its deliveries. Only do this for endpoints you are comfortable sending customer conversations to.

## Verifying the signature

Every request is signed with the webhook's secret, so you can prove it came from FlowCP and is not a replay. The signature is an HMAC-SHA256 over `<timestamp>.<raw request body>`, hex-encoded:

```ts
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
  const timestamp = Number(parts['t']);
  // Reject anything older than five minutes (replay protection).
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const received = Buffer.from(parts['v1'] ?? '', 'hex');
  const digest = Buffer.from(expected, 'hex');
  return received.length === digest.length && timingSafeEqual(received, digest);
}
```

Verify against the **raw** body bytes, before any JSON parsing and re-serialization — a re-serialized object will not match.

## Delivery, retries, and the delivery log

A delivery is attempted up to **5 times** with exponential backoff, spreading retries over roughly ten minutes. Click **Deliveries** on a webhook to see recent attempts with their status, HTTP response code, attempt count, and error message, and to **Redeliver** any of them. A redelivery replays the stored event with the same event id — de-duplicate on `X-FlowCP-Event-Id` if that matters to you.

Delivery history is kept for **14 days**.

If a webhook's deliveries keep failing, it shows a **Delivery error** badge with the latest message. After **20 consecutive** failed deliveries it is disabled automatically and says so; fix the endpoint and re-enable it to resume.

Events are queued before they are sent, so a delivery survives a restart on our side. Ordering is not guaranteed — use `createdAt` and `message.sequence` if you need to order events yourself.

## Rotating or removing a webhook

* **Rotate secret** mints a new signing secret and shows it once. Deliveries signed with the old secret stop verifying immediately, so update your endpoint promptly.
* **Edit** changes the destination, the events, or the content setting.
* **Delete** removes the webhook and its delivery history; no further events are sent to its endpoint.

## Related

* [Embed an AI chat widget](/guides/embed-ai-chat-widget) — the source of conversation and message events.
* [Read chat conversations](/guides/read-chat-conversations) — retained transcripts in the dashboard.
* [Stream logs to external sinks](/guides/stream-logs) — for per-call execution logs rather than product events.


# Manage servers

The **Servers** page gives you a central, at-a-glance view of all MCP servers for an app — their health, recent traffic, and public endpoints — and lets you act on each one without navigating to the individual publish page.

## Finding a server

Below the summary you can:

* **Filter by status** with the tabs — **All**, **Published**, **Draft**, or **Disabled** (each shows a count).
* **Search** by name, branch, or slug. Press **⌘K** (**Ctrl+K** on Windows/Linux) to jump straight to the search box.

## Reading a server card

Each server is shown as a card with:

* **Name** and a **status badge** — Published, Draft, or Disabled
* **Health pill** — Healthy, Degraded, Down, or Not deployed (derived from recent traffic)
* **Endpoint** — the public MCP URL, with a **Copy** button
* A short **deploy line** — when it was last deployed (or "Never deployed")
* A **metrics strip** — tool calls in the last 24h, error rate, average latency, and server errors
* **Actions** — Test, Playground, Logs, History, and **Manage**

When a server is **Degraded** or **Down**, the card surfaces a notice with the error rate and an **Investigate →** link to its logs.

## Unpublishing a server

Click **Unpublish** on any Published server to take it offline immediately. A confirmation dialog appears before the change is made. AI clients connected to that server will stop working until you re-publish.

To re-publish or view the full publish details (MCP config snippets, tool blockers), click **Manage** to open the server's publish page.

## Creating servers

Servers are created per branch. Click **New server** (or **Spin up a new server** on the empty card) to go to **Settings → Branches**, where you can add a branch.


# App settings

Each app has its own **Settings** area, reachable from the app sidebar. It is organized into tabs:

* **General** — the app's name, description, logo, website, and MCP server domain.
* **App Source** — where the app's tools come from (provider, source URL, git import) and the controls to reconnect it.
* **Branches** — manage the app's branch servers.
* **Verify** — prove ownership of the connected app.
* **Danger Zone** — irreversible actions: transferring or deleting the app.

## General

The **App details** card sets the name, description, logo URL, and website shown for this app across the dashboard and to MCP clients. The **MCP server domain** card manages a bring-your-own custom domain for the hosted MCP endpoint.

## App Source

This tab groups everything that controls where the app's tools come from. The cards shown depend on the app's provider:

* **Custom domain** (Bubble apps) — the upstream host OAuth, Swagger imports, and tool calls use instead of `bubbleapps.io`.
* **Swagger URL** (Generic OpenAPI apps) — the OpenAPI/Swagger document URL. Saving a new URL re-imports the app's tools.
* **Git source & auto-import** (Git apps) — the branch, OpenAPI file path, and repository/project the document is fetched from, plus the inbound webhook used to re-import on merge.
* **Change provider** (owners only) — the **Change provider** button opens a popup where you reconnect the app under a different provider (Bubble, Generic OpenAPI, or Git). This is destructive: it drops the app's current branches, tools, and execution logs and re-imports from the new source.

## Danger Zone

The **Danger Zone** tab holds irreversible actions. They are available to workspace **owners** only; limited members see them disabled.

### Transfer to a workspace

Move the app — along with all of its servers, tools, OAuth configuration, domains, and execution logs — into another workspace **you own**. Select the destination workspace, click **Transfer app**, and type the app name to confirm. You lose access to the app unless you are also a member of the destination.

### Transfer to another account

Hand the app to another person by their account email. It moves into that account's workspace and they become its owner. Enter the recipient's email, click **Transfer app**, and type the app name to confirm. You lose access once the transfer completes.

> **Note:** The recipient must already have a FlowCP account. The app lands in their primary (oldest) workspace.

### Delete this app

Permanently removes the app and all of its tools, servers, OAuth configuration, and end-user connections. Type the app name to confirm. This cannot be undone.


# Test your MCP server

Before you publish, run the **Tests** page to confirm every capability your MCP server exposes — tools, authentication, skills, widgets, and prompts — registers correctly. The suite runs entirely against your server's configuration, so it is safe to run as often as you like and never makes a live API call on a user's behalf.

## Running the tests

1. Open your app and go to **Servers**.
2. Click **Test** on the server (branch) you want to check. The suite runs automatically when the page opens.
3. Review the results. Click **Run tests** at any time to re-run after you make a change.

Each check reports one of four outcomes:

* **Passed** — the capability registers cleanly.
* **Failed** — the capability is broken or unsafe to publish. Fix it before publishing.
* **Warning** — the capability registers, but something may not behave as you expect (for example, a tool missing a description). Publishing is still allowed.
* **Skipped** — there was nothing to check (for example, no skills configured).

## What the suite checks

| Check                                  | Category       | Fails when                                                                         |
| -------------------------------------- | -------------- | ---------------------------------------------------------------------------------- |
| At least one tool is enabled           | Tools          | No tools are enabled — the server would expose nothing.                            |
| Tool input schemas are valid           | Tools          | A tool's stored input schema can't be turned into a valid MCP schema.              |
| Tools have descriptions                | Tools          | *(Warning)* An enabled tool has no description for AI clients to read.             |
| Authentication is configured           | Authentication | The server's auth mode has no usable credential (e.g. OAuth config or API key).    |
| Destructive tools require confirmation | Safety         | An enabled `destructive` tool can run without **Require confirmation**.            |
| Skills are valid                       | Skills         | An enabled skill has an empty body. *(Warning)* a skill name collides with a tool. |
| Widgets are valid                      | Widgets        | An enabled widget has empty HTML. *(Warning)* a tool links a disabled widget.      |
| Prompts are valid                      | Prompts        | An enabled prompt has an empty body.                                               |

## Live conformance checks

The checks above are **static** — they read your server's configuration. The **Conformance** checks go one step further: they open a real MCP session against your **published** server's runtime and confirm that what the runtime actually registers matches what you configured.

| Check                                | Category    | Fails when                                                     |
| ------------------------------------ | ----------- | -------------------------------------------------------------- |
| Runtime handshake succeeds           | Conformance | The runtime won't accept an MCP session for this server.       |
| Tools register at runtime            | Conformance | An enabled tool (or a skill's `skill_…` tool) doesn't appear.  |
| Prompts register at runtime          | Conformance | An enabled prompt doesn't appear in the runtime's prompt list. |
| Skills & widgets register at runtime | Conformance | An enabled widget or skill resource doesn't appear.            |

These checks only issue read-only protocol calls (`initialize` and the capability `…/list` methods) — they **never invoke a tool**, so like the static suite they make no upstream API call and have no side effects.

Conformance checks run only for a **published** server (the runtime serves published branches). For a draft branch they report **Skipped** with a "publish to run" note. If the runtime is temporarily unreachable, they also report **Skipped** rather than failing — a transient outage never looks like a broken server. Because of that, conformance results are **not** publish-critical: they surface real registration problems but never block the publish gate.

## Publish readiness

The summary banner tells you whether the server is ready to publish. The **publish-critical** checks are:

* At least one tool is enabled
* Tool input schemas are valid
* Authentication is configured
* Destructive tools require confirmation

If all four pass, the server clears the publish gate and you can head to the [Publish page](/guides/publish-your-mcp-server). Warnings never block publishing, but they are worth resolving so your tools behave the way AI clients expect.

## Go further: catch response-shape regressions

The Tests page confirms your capabilities *register* correctly. To also catch an upstream API silently changing a tool's **response shape**, record [golden scenarios](/guides/golden-scenario-replay) for your read-only tools and replay them after a change.

## Go further: chat-test in the Playground

The Tests page confirms your capabilities *register* correctly without making any live calls. To watch a real model *use* your tools end to end — making live requests in your own authorization context — open the [Playground](/guides/chat-test-your-mcp-server).


# Golden-scenario replay

The [Tests page](/guides/test-your-mcp-server) confirms your tools *register* correctly. **Golden scenarios** go one step further: they record what a read-only tool's response actually *looks like*, then replay it later to catch the most common silent breakage — an upstream API quietly renaming, removing, or retyping a field. Your tool still lists fine, but the payload it returns no longer matches what AI clients expect.

A scenario stores only the **structural shape** of the response — the set of keys and the kind of each value (string, number, object, array) — never the raw data. That keeps recordings free of real customer data and stable as live values change: only a genuine format change shows up as drift.

## Recording a scenario

1. Open your app, go to **Servers**, and click **Scenarios** on the server (branch) you want to cover. The server must be **published** — scenarios run against the live runtime.
2. In **Record a scenario**, pick a **read-only** tool. Only read tools are eligible, so recording and replay never change anything upstream.
3. Give the scenario a name and provide the **input** to call the tool with, as a JSON object (for example `{"limit": 10}`).
4. Click **Record scenario**. FlowCP calls the tool once, derives the response shape, and saves it as the golden baseline.

Re-recording a scenario with the same name overwrites its baseline and clears any previous replay result.

## Replaying

* Use **Replay** on a single scenario, or **Replay all** to run every scenario on the server.
* Each replay re-invokes the tool with the recorded input, derives the new response shape, and diffs it against the golden. The result is one of:

| Status      | Meaning                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------- |
| **Passed**  | The response shape matches. New fields may have appeared (backward-compatible).          |
| **Drift**   | A recorded field was **removed** or **retyped** — a likely regression.                   |
| **Error**   | The tool call itself failed (for example, the upstream returned an error).               |
| **Skipped** | The replay couldn't run (server not published, runtime unreachable, or a missing token). |

When a scenario drifts, the row lists the offending paths — for example `removed $.email` or `retyped $.id (number → string)` — so you can see exactly what changed.

## Servers that authorize each end user (`user_oauth`)

Shared-credential servers (`api_key`, `bearer`, `none`) record and replay automatically — FlowCP uses the server's configured credential.

For **`user_oauth`** servers, FlowCP never stores end-user tokens, so it can't call the tool on its own. Provide a **temporary authorization token** in the record form (and in the **Replay authorization token** field for replays). The token is used once to make the call and is **never stored** and never logged. If you don't provide one, the scenario is reported as **Skipped**.

## When to use this

* After an upstream/Bubble API change, **Replay all** to confirm none of your read tools changed shape.
* As a quick regression check before you publish a new branch.

Golden-scenario replay only issues calls against **read-only** tools, so it is always safe to run.


# Chat-test in the Playground

The **Playground** lets you talk to your MCP server with a real model before you publish. The model can call your enabled tools for real — running live requests in your own authorization context — so you can confirm the whole loop works end to end: tool descriptions, input schemas, OAuth, and the actual API responses.

You can also exercise any capability **directly**, without the model — run a tool with hand-written arguments, read a resource, or preview a prompt — the way a developer tool like the MCP Inspector does. See [Run a tool, read a resource, or preview a prompt](#run-a-tool-read-a-resource-or-preview-a-prompt).

The Playground uses [OpenRouter](https://openrouter.ai) and **your own API key**, so you choose the model and you pay for the usage.

## What you need

1. **An OpenRouter API key.** Create one at [openrouter.ai/keys](https://openrouter.ai/keys).
2. **OAuth configured** for the server's branch (see [Configure OAuth](/guides/configure-oauth)). The Playground connects with the same OAuth flow your real AI clients use.

## Open the Playground

1. Open your app and go to **Servers**.
2. Click **Playground** on the server (branch) you want to test. (You can also reach it from the **Tests** page.)

The Playground is a three-pane workspace: a **config rail** on the left (connection, model, and capabilities), the **chat** in the center, and a **Raw JSON** inspector that slides in from the right when you ask for it.

## Save your OpenRouter key

In the config rail, paste your OpenRouter key and click **Save**. The key is stored **encrypted** for your workspace and is never shown again — only the last four characters are displayed. Once saved it collapses to a compact **Connected via OpenRouter** chip; click **Manage** to **Replace** (rotate) or **Remove** it.

## Connect your account

The connection step depends on your server's **auth mode**:

* **User OAuth** (the default) — click **Connect account**. A window opens and runs your server's OAuth flow. After you authorize, the window closes and the status badge turns **Connected**. The resulting access token is held **only in your browser for this session** — it is never stored on our servers. If it expires mid-chat, the Playground asks you to reconnect.
* **API key** or **static bearer token** — no account connection is needed. The Playground notes that the server uses a shared credential, the runtime injects it server-side, and you can chat as soon as your OpenRouter key is saved.
* **No authentication** — nothing to connect; start chatting right away.

For the non-OAuth modes the status badge reads **Ready** instead of **Connected**.

## Chat

Set the model in the config rail (any OpenRouter model id, e.g. `anthropic/claude-haiku-4.5`), type a message, and send. When the model calls one of your tools, the chat shows a card with the tool name, the arguments it sent, and the result it got back. Expand the card to inspect them, or click its **code** button to open the **Raw JSON** inspector (see below). The model's replies render as Markdown. The chat header tracks the running turn and tool-call counts; **New chat** starts a fresh session.

A few things to know:

* The Playground stops after a fixed number of tool steps so a runaway model can't burn through your OpenRouter credits.
* Draft (unpublished) servers are fully testable — that's the point.
* Errors from the model or a tool are surfaced inline (including the exact reason from OpenRouter, e.g. an invalid model id); your key and tokens are never logged.

## Choose which capabilities the model can use

The config rail groups everything your server exposes into collapsible **Capabilities** sections, each with an enabled/total count:

* **Tools** — the model calls them directly. Switch individual tools **off** to test how the model behaves with a narrower toolset; the disabled tools are withheld from the model for the current chat. Each tool also has a **Run** (▷) button to call it directly — see below.
* **Skills** — your server's skill playbooks (also callable as `skill_<slug>` tools). Toggling a skill off withholds its tool the same way. Click **View playbook** to read the skill's `SKILL.md` and bundled references.
* **Prompts** — toggle which prompts are offered. Click **Prompts** in the composer (or pick a starter card on the empty screen) to insert one. If a prompt declares arguments you'll be asked to fill them, then the rendered template drops into the input for you to edit and send. Click **Preview** to see the server-rendered messages instead of inserting them.
* **Widgets** — toggle whether a tool's linked widget renders. When an enabled widget's tool runs, its HTML is shown in a sandboxed frame beneath the result.
* **Resources** — listed for reference (always part of the server's context). Click **View** to read a resource's live content.

These toggles affect **this chat only** — they don't change what your published server exposes. Use **Reset** at the top of the rail to turn everything back on.

## Inspect the raw request and response

Click the **code** button on any tool-call card to open the **Raw JSON** inspector. It shows the exact **Request** (the tool name and the arguments the model sent) and the **Response** (the result the tool returned), each with a one-click **Copy**.

## Run a tool, read a resource, or preview a prompt

Sometimes you want to test one capability **deterministically**, without a model deciding what to do. Each capability in the config rail has a direct action:

* **Run a tool.** Expand **Tools** and click the **Run** (▷) button on a tool. A dialog opens with a JSON editor seeded from the tool's input schema — edit the arguments and click **Run**. The call goes through the same auth and policy as a real request (disabled tools can't be run; a tool that **requires confirmation** shows a confirm switch you must flip first), and the result opens in the **Raw JSON** inspector. Invalid JSON is flagged inline before any call is made. While the call runs, a **Live activity** feed shows the server's MCP notifications — `logging` lines (started / completed / failed, with status and latency) and `progress` updates — streaming in as they happen.
* **Read a resource.** Expand **Resources** and click **View**. Static resources load immediately; resources with URI variables prompt you to fill them first. The content is rendered by type (JSON, Markdown, plain text, or a download link for binary content).
* **Preview a prompt.** Expand **Prompts** and click **Preview**. Fill any arguments, and the server-rendered messages are shown exactly as a client would receive them.
* **View a skill playbook.** Expand **Skills** and click **View playbook** to read the skill's `SKILL.md` and any bundled reference files.

On **User OAuth** servers, running a tool or reading an endpoint-backed resource needs your account connected (the same as chatting); prompt previews and skill playbooks don't.

## Activity: execution logs and live notifications

Click **Logs** in the chat header to open the **Activity** pane, which has two tabs:

* **Execution logs** — this server's recorded tool invocations, newest first, with the tool name, status, latency, MCP client, and model. Both chat turns and direct runs appear here. Click any row to open its full record in the **Raw JSON** inspector. These are durable execution/audit logs.
* **Live** — a rolling feed of the MCP protocol notifications (`logging` and `progress`) emitted by tool runs in the current session. This is the same stream shown in a tool's **Live activity** while it runs, kept here so you can review it afterward. **Clear** empties the feed. (Live notifications are in-session only — they are not persisted like execution logs.)

## Test payments (monetized servers)

If you've [monetized](/guides/monetize-your-mcp-server) a server, you can pay for paid tools right here — no real wallet or external agent needed. In the config rail under **Test payments**:

* **Test wallet** — enter an EVM address, or click **Generate** for a throwaway one. This is the payer for mock payments (it's saved for next time).
* **Auto-pay test payments** — when on, paid tools are paid automatically.

When the model calls a paid tool:

* **Auto-pay on** → the Playground constructs a mock x402 payment from your test wallet, settles it, and retries. The tool call shows a **paid** badge with the amount, payer, and network.
* **Auto-pay off** → the turn pauses with a **Payment required** card showing the price; fill in a test wallet and click **Pay & retry**.

These are **mock payments** — no real funds — and settle only against the built-in mock facilitator (development / self-host runtimes). Each one still writes a real receipt under **Monetization → Transactions**, so you can confirm the full earn flow works. Wiring a real on-chain wallet into the Playground isn't supported yet.

## History

Every chat is saved automatically to this server's **History** (titled from your first message). Open **History** to revisit a past session or delete it, and **New chat** to start fresh. Only the transcript is saved — never your OpenRouter key or your connection token.

When everything behaves the way you expect, head to the [Publish page](/guides/publish-your-mcp-server).


# 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), 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**. Your app can [tell it what the user is looking at](#ground-the-assistant-in-the-current-page), [make it speak first](#start-the-conversation-from-your-app), and optionally [retain conversations](#conversation-history) for resumable threads and follow-up.

## 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) — 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.ai/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.ai/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.ai/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.

A page can also **override the display mode** itself with `data-display="inline"` or `data-display="popup"` on the script tag. This wins over the dashboard setting for that page only — handy when one widget is a floating launcher on most pages but lives inline on a dedicated "Assistant" page (or inside a component such as the [Bubble plugin element](#use-the-bubble-plugin)). Any other value leaves the configured mode in charge.

## 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**. Connectors are connected from the composer's **`+`** menu → **Connectors** (a badge shows how many still need sign-in), and a one-line prompt above the composer offers **Connect** while any of them is signed out. Tokens are kept per connector and, if one expires mid-chat, only that connector is asked to reconnect.

### When does the user have to sign in?

That is the **Require sign-in before chatting** toggle, and it is separate from **Require end-user token**:

| Setting                                             | What the visitor sees                                                                                                                                                                                                   |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Require sign-in before chatting — on**            | The composer is replaced by a **Connect** prompt. Nothing can be asked until the user signs in, and the whole conversation then runs in their own context.                                                              |
| **Require sign-in before chatting — off** (default) | Anyone can chat straight away. Connectors whose tools need the user's own authorization stay **closed** until that user signs in — the assistant answers what it can and asks them to connect when a request needs one. |

Either way a connector is **never** called with anyone else's credentials: a connector that needs a per-user token and doesn't have one is simply not opened for that turn.

> **Upgrading:** widgets that gated the chat before this setting existed (those with **Require end-user token** on, or a `user_oauth` primary server) keep doing so — they were switched to **Require sign-in before chatting = on**. Turn it off to let visitors chat before they sign in.

### Option A — the widget signs the user in (recommended)

With **Require end-user token** on, the widget offers a **Connect** button (in place of the composer when **Require sign-in before chatting** is also on, otherwise just above it). 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.ai/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.ai/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.

> **Bubble apps:** Bubble does not expose a signed-in user's OAuth access token to the page, so there is nothing to hand to `setToken()` unless your app obtains a token some other way (for example, your users sign in through Auth0, Clerk or another IdP behind your Bubble front end). For a plain Bubble-OAuth server, **Option A** is the supported path: one **Connect** click per browser tab, remembered across pages.

### Controlling the widget from JavaScript

The loader exposes a small API on `window.FlowCPChat`. In Bubble, call these from a **Run javascript** action (Toolbox plugin) in any workflow.

* `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.
* `FlowCPChat.setContext({ … })` — tell the assistant what the user is looking at; see [Ground the assistant in the current page](#ground-the-assistant-in-the-current-page).
* `FlowCPChat.sendMessage(text)` — send a message **as the user** and open the panel.
* `FlowCPChat.prompt(instruction)` — have the **assistant speak first**, following an instruction the user never sees; see [Start the conversation from your app](#start-the-conversation-from-your-app).
* `FlowCPChat.reset()` — start a fresh conversation (same as the **New chat** button).
* `FlowCPChat.isOpen()` — whether the panel is currently shown (always `true` inline).

Calls made before the widget has finished loading are queued and run once it's ready, so it's safe to call them right after the snippet.

The loader also dispatches **events on `window`** so your page can react without polling: `flowcp:ready` (the chat accepts messages), `flowcp:open` and `flowcp:close` (the panel was shown or hidden, whether by your code, the launcher or the ×), and `flowcp:error` (the widget failed to load; `event.detail.message` says why).

```js
window.addEventListener('flowcp:ready', () => FlowCPChat.setContext({ page: 'home' }));
window.addEventListener('flowcp:close', () => console.log('chat closed'));
```

## Use the Bubble plugin

Bubble apps don't need the script tag, an HTML element or the Toolbox plugin: the **FlowCP - Embedded Chat** Bubble plugin ([source](https://github.com/mslma/Bubble-Plugin-FlowCP---Embedded-Chat)) wraps the widget in a single visual element.

1. Install the plugin in your Bubble app and paste the widget's public key into **Plugins → FlowCP - Embedded Chat → FlowCP embed key**. It is an app-level setting — one key for every page.
2. Drop a **FlowCP Chat** element on the page (one per page — the widget is a page-level singleton). Make sure the page's origin (`https://<app>.bubbleapps.io` and any custom domain) is under the widget's **Allowed origins**.
3. Pick a **Display mode**: **Inline (inside this element)** renders the chat in the element's box (size the element to size the chat), **Popup (floating launcher)** shows the corner launcher instead, **Widget setting** follows the dashboard.

> The plugin always loads the chat from `https://api.flowcp.ai`, and one Bubble app runs one widget. For a self-hosted FlowCP, or two widgets in the same app, use the script snippet above instead of the element.

The element exposes the host API as Bubble building blocks:

* **Fields** — **End-user token** (leave empty to let the widget's **Connect** button run your Bubble OAuth) and **Page context (JSON)**, both applied live when their value changes; **Open on page load** for popup mode. Whether users must sign in before they can chat is the widget's own **Require sign-in before chatting** setting in the FlowCP dashboard, not a plugin field.
* **Actions** — **Open / Close / Toggle chat**, **Set end-user token**, **Set page context**, **Send message**, **Prompt assistant**, **New conversation**.
* **States** — **is ready**, **is open**, **display mode**, **last error**.
* **Events** — **is ready**, **is opened**, **is closed**, **has an error**.

Build the context JSON with Bubble's `:formatted as JSON-safe` operator, for example `{"page":"invoice","invoiceId":<Current Page Invoice's unique id:formatted as JSON-safe>}`.

## Ground the assistant in the current page

By default the assistant only knows what the user types. Give it the **page context** — the record on screen, its id, the page name, a short excerpt — and it can act on it directly, for example passing a record id straight to a tool instead of asking for it:

```html
<script>
  FlowCPChat.setContext({
    page: 'project',
    projectId: 'abc123',
    projectName: 'Website redesign',
    status: 'in_review',
  });
</script>
```

* The context is a **flat object** of strings, numbers, booleans or `null` (no nested objects), up to **32 keys**, each value up to **2,000 characters**. Anything else is rejected.
* It is sent **with every turn** and appended to the system prompt as a clearly delimited **data block** — the model is told to treat the values as facts about the page, not as instructions.
* Call `setContext()` again whenever the user navigates (Bubble: a **Run javascript** action on page load or when the current record changes). Pass `null` to clear it.
* You can also set it up-front with a `data-context` attribute on the script tag: `data-context='{"page":"invoice","invoiceId":"inv_42"}'`.
* **Never put secrets or tokens in the context** — it is prompt text, not a credential channel. Use `setToken()` for the user's token.

If the widget [retains transcripts](#conversation-history), the latest context is stored with the conversation so your team can see which record a thread was about.

## Start the conversation from your app

Your app can decide when the assistant should step in — a user has been stuck on a step, a record is missing required fields, a plan is about to expire — and make it speak first, in context:

```js
// Open the panel and have the assistant write an opening message.
// The instruction is for the assistant only; the user never sees it.
FlowCPChat.setContext({ page: 'onboarding', step: 'connect-bank', attempts: 3 });
FlowCPChat.prompt(
  'The user has failed to connect their bank account three times. Offer help ' +
    'warmly, ask what error they are seeing, and mention that you can check the ' +
    'connection status for them.',
);
```

* `prompt(instruction)` opens the panel and streams the assistant's reply into the chat as its **first visible message**. The instruction itself is hidden from the user (it is part of the transcript the assistant sees, and appears marked as a **Host instruction** in retained transcripts).
* `sendMessage(text)` is the visible counterpart: it sends `text` **as if the user typed it** — handy for an "Ask the assistant about this" button next to a record.
* Both respect the widget's sign-in gate: when **Require sign-in before chatting** is on and no token has been provided, the call is ignored until the user connects (or you call `setToken()`). Call `setToken()` before `prompt()` when you have the token. With the gate off the call goes through — the assistant answers without the tools of any connector the user hasn't signed in to.
* The static **Greeting** is still shown before the first turn; `prompt()` simply adds the assistant's contextual message after it.

## Conversation history

By default the widget stores **nothing**: each turn is stateless, the transcript lives only in the visitor's browser tab, and it is gone on reload. If your product needs threads that **persist across pages and visits**, or your customer-success team needs to **read what users asked**, turn on **Retain transcripts** on the widget.

> **Conversation history is a paid feature** — it needs the **Team** plan or above, and each plan sets how long conversations may be kept (Team 30 days, Business 60, Enterprise 180). Chatting itself is on every plan, including **Starter**; only *keeping* the conversation is paid. On a plan without it, the **Retain transcripts** toggle is locked, the **Chat conversations** page and its sidebar entry are replaced by an upgrade card, the visitor's **History** list stays empty, and the widget keeps behaving as it does with retention off. See [Plans & features](/reference/plans-and-features).

With retention on:

* **Resumable threads.** The widget keeps a random, unguessable handle in the visitor's browser (per widget). When they navigate to another page of your app or come back later, the conversation is restored. The chat header gains a **New chat** button (and your app can call `FlowCPChat.reset()`) to start over.
* **The visitor's own history.** The chat header also gains a **History** button. It lists that visitor's past threads — when each was last active, how many messages it has, and a preview of their first question — and clicking one **reopens it and carries on in it**: the next message appends to the same thread rather than starting a new one. **New chat** no longer loses the previous thread; it moves it into the history list. History is **per browser**: it lists only the threads whose handle that browser still holds (up to 25), so it never enumerates other visitors' chats, and a thread belonging to a signed-in user stays hidden unless that user's token is present.
* **Bound to the signed-in user.** When the primary connector runs with an end-user token, the thread is bound to a hash of that token: it only resumes for a browser that presents the **same** user's token. A signed-out browser, or another user signing in on the same device, cannot read it — they get a fresh thread.
* **Retention window.** Set **Keep conversations for (days)** (default 30). How long you may keep them depends on the plan: **Team up to 30 days**, **Business up to 60**, **Enterprise up to 180**. Asking for more is bounded to the plan's ceiling, and older conversations are purged automatically. Moving to a shorter tier shortens the window of widgets you already configured — nothing to edit, and anything past the new window stops resuming at once.
* **Read them in the dashboard.** **Distribute → Chat conversations** lists every retained thread for the branch — see [Read chat conversations](/guides/read-chat-conversations). The entry appears in the sidebar only once a widget retains transcripts, and **View conversations** on the widget card opens it on that widget.

**What is stored — and what isn't.** A retained transcript is a **text-only projection**: the user's text, the assistant's text, the **names** of the tools the assistant called, how many attachments a message had, and the latest page context. FlowCP never stores attachment contents, tool **arguments or results** (they can carry your API's records), the model's reasoning, or the user's token. The visitor's handle and the user's token are stored only as SHA-256 hashes.

Turning retention **off** stops recording new turns but keeps what was already stored until it ages out or you **Delete all**.

**If your plan changes.** Downgrading to a plan without conversation history stops the recording immediately — every save, resume and history call re-checks the plan, so visitors get fresh threads, their **History** list comes back empty, and nothing new is written. The widget keeps its **Retain transcripts** setting (you can still switch it off), transcripts already stored are no longer readable in the dashboard, and the locked **Chat conversations** page still offers **Delete retained transcripts** so you can erase them. Upgrade again and the same widget resumes recording.

## 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) (`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.
* **Transcripts** are off by default (**Retain transcripts**) and need a paid plan (Team and above). When on, only a text projection is kept, bound to the signed-in user, for a bounded number of days — see [Conversation history](#conversation-history).
* **Sign-in** is enforced server-side: with **Require sign-in before chatting** on, a turn without the user's token is refused; with it off the turn runs but every connector that needs a per-user token stays closed — a shared credential is **never** substituted for a missing one.
* **Page context** (`setContext`) is bounded and injected as data, never as instructions; keep secrets out of it.
* **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(...)`.
* **`FlowCPChat.prompt()` / `sendMessage()` does nothing.** The composer is gated on sign-in (**Require sign-in before chatting** is on, so the widget shows **Connect**). Provide the token first with `setToken()`, or let the user connect; the call is only skipped while the gate is up.
* **The assistant says it can't do something and asks the user to sign in.** That connector has no token for this visitor. Either let them connect from the **`+`** menu → **Connectors**, or turn on **Require sign-in before chatting** so nobody chats before signing in.
* **The conversation doesn't resume.** Check **Retain transcripts** is on and saved, and that your workspace is on the **Team** plan or above — retention stops as soon as a plan without conversation history takes effect. A thread bound to a signed-in user only resumes once that user's token is present again — the widget retries the resume as soon as the token arrives. Private-browsing modes that block `localStorage` start a fresh thread on every page.
* **The Bubble element shows nothing.** Check the **last error** state (or the browser console): a second FlowCP Chat element or a pasted snippet on the same page, an origin missing from **Allowed origins**, or a display mode the cached loader doesn't yet support. The **display mode** state tells you which mode actually rendered.
* **The assistant ignores the page context.** `setContext()` must be a flat object of primitives (no nested objects) with at most 32 keys; anything else is rejected and the turn runs without context. Check the browser console for a `data-context` parse error.

## Reacting to chat events

To have another system react when a visitor starts a conversation or sends a message, add a [webhook](/guides/webhooks) on the app and subscribe to `conversation.created` / `message.created`. The events are emitted after every completed turn, whether or not the widget retains transcripts.


# Read chat conversations

**Distribute → Chat conversations** is where you read the transcripts your [chat widgets](/guides/embed-ai-chat-widget) have retained. It's the team-facing side of the same records a visitor sees in the widget's own **History** list.

The entry only appears in the sidebar once a widget on the app retains anything, so there is never a dead link to an empty page.

> **Conversation history is a paid feature** — it needs the **Team** plan or above, and the plan sets how long transcripts are kept (Team 30 days, Business 60, Enterprise 180). Chatting is on every plan, including **Starter**; only *keeping* the conversation is paid. Without it this page shows an upgrade card instead of the reader. See [Plans & features](/reference/plans-and-features).

## Turn retention on first

Transcripts are **off by default** — a chat turn is stateless and nothing is stored. To start keeping them, open **Distribute → Chat widget**, switch on **Retain transcripts** on the widget, set **Keep conversations for (days)** — up to your plan's ceiling, default 30 — and save. **Chat conversations** appears in the sidebar as soon as you do. The toggle is locked on plans without conversation history.

Turning retention back off stops recording new turns but keeps what was already stored until it ages out or you delete it.

Downgrading to a plan without conversation history stops the recording immediately: visitors get fresh threads, their **History** list comes back empty, and this page is replaced by the upgrade card. That card still offers **Delete retained transcripts**, so you can erase what was stored while the workspace was paid.

## Read a conversation

The page targets the branch selected in the header, the same as the widget page. Switch branches there to read another branch's threads.

If several widgets on the branch retain transcripts, pick one from the **Widget** dropdown. With only one, it is shown straight away.

Threads are listed newest first. Each row shows:

* when the thread was **last active** and how many **messages** it has;
* whether it was **anonymous** or from a signed-in **visitor** — a short, non-reversible id you can correlate across threads, not the person's identity;
* a **context** badge when the host page attached [page context](/guides/embed-ai-chat-widget#ground-the-assistant-in-the-current-page);
* the **origin** that drove the chat, and a preview of the first question.

Click a row for the full transcript. Host instructions sent with `FlowCPChat.prompt()` are marked **Host instruction (not shown to the visitor)**, so you can tell what your app injected from what the person actually typed.

## Delete

**Delete** on an open transcript removes that one thread. **Delete all** removes every retained transcript for the selected widget. Neither can be undone, and neither interrupts anyone: a visitor mid-chat simply starts a new record on their next message.

## What is stored — and what isn't

A retained transcript is a **text-only projection**: the user's text, the assistant's text, the **names** of the tools the assistant called, how many attachments a message had, and the latest page context.

FlowCP never stores attachment contents, tool **arguments or results** (they can carry your API's records), the model's reasoning, or the end-user's token. The visitor's conversation handle and their token are stored only as SHA-256 hashes.

That's what makes this view safe to give a customer-success team: they can read what people asked without gaining access to the records behind the answers.

## Reacting to conversations elsewhere

The dashboard is for reading. If you want another system to *act* on chat activity — open a CRM ticket, ping a Slack channel, log a lead — add a [webhook](/guides/webhooks) and subscribe to `conversation.created` and `message.created`. Those events fire whether or not transcripts are retained, and carry message text only for webhooks that explicitly opt in.


# Distribute as a chat bot

The **chat bot** channel runs a hosted AI bot in your team's chat platform that answers in-thread using your published MCP server's tools. People **@mention** the bot in a channel (or DM it); FlowCP runs the same agentic loop as the [Playground](/guides/chat-test-your-mcp-server) and [chat widget](/guides/embed-ai-chat-widget) and posts the reply back into the thread.

> **Premium feature.** Chat bots require a **Team** plan or higher (the `chat_bot` capability). The **Distribute → Chat bot** entry is hidden on plans that don't include it.

## How it works

FlowCP runs a **hosted bot gateway** that holds your bot's connection to the chat platform — Slack and Discord over persistent sockets, Telegram by long-polling, and WhatsApp via an inbound webhook. You never run any server yourself. When a message arrives, the gateway runs your server's tools in an agentic loop and replies in the same thread. Conversation context is kept per-thread for follow-ups.

## What you need

1. A **published** server (the bot connects to its enabled tools).
2. An **OpenRouter API key** for the bot. Each bot has its **own** key — create one at [openrouter.ai/keys](https://openrouter.ai/keys).
3. Credentials for your chat platform (below).

## Create a bot

1. Open your app and go to **Distribute → Chat bot**. The page targets the branch selected in the header.
2. Under **Add a bot**, choose a **platform** and click **Create bot**.
3. Enter the platform **credentials** and click **Save credentials**, then **Test connection** to confirm they work.
4. Set the bot's **OpenRouter API key** (required — the bot can't reply without it).
5. Optionally set the **model**, a **system prompt**, and attach extra **connectors** (additional MCP servers whose tools the bot can also use).
6. Flip **Enabled** on. The gateway picks the bot up within moments (no restart), and the status badge turns **active**.

## Platform setup

### Slack

Create a Slack app, enable **Socket Mode**, and copy the **Bot User OAuth Token** (`xoxb-…`) and an **App-Level Token** (`xapp-…`) with the `connections:write` scope. Subscribe to the `app_mention` and `message.im` events. No public URL is required.

### Discord

Create a Discord application + bot, enable the **Message Content** intent, and copy the **Bot Token**. Invite the bot to your server. No public URL is required.

### Telegram

Create a bot with [@BotFather](https://t.me/BotFather) and copy the **Bot Token**. The gateway uses long-polling, so no public URL is required. In groups, the bot only replies when mentioned; in private chats it always replies.

### WhatsApp

Use the WhatsApp **Cloud API**. Provide the **Access Token**, **Phone Number ID**, a **Webhook Verify Token** (any string you choose), and the **App Secret**. Point your Meta app's webhook at `https://<gateway-host>/webhooks/whatsapp/<botId>` and use the same verify token. Inbound payloads are validated with an HMAC signature.

## Identity: per-user sign-in

* Servers using **shared-credential** auth (API key, bearer, or none) work directly — every user shares the configured credential.
* A server that uses **user OAuth** runs in **each chat user's own account**. The first time a user invokes one of its tools, the bot sends them a sign-in link **privately** (a DM, or an ephemeral message only they can see) — never in the shared channel, since the link is tied to their identity. Once they complete OAuth, the bot stores their token (encrypted) and acts as them. Per-user authorization is enforced. Until a user connects, that tool is **skipped** for them (the bot never calls it anonymously or as someone else).
* **Expiring sign-ins refresh automatically.** When a stored token nears expiry (or an upstream call comes back unauthorized mid-conversation), the bot refreshes it in the background and retries — users are **not** re-prompted. A fresh sign-in link is only sent when refresh is impossible: the provider revoked access, issued no refresh token, or definitively rejected the stored grant.
* **Remote (catalog) OAuth connectors** can run per-user too. On the bot's connector list, flip **"Each user connects their own account"** on a remote OAuth connector and each chat user is sent their own sign-in for that service (driven against the remote server's own authorization server), instead of everyone sharing the bot's upstream token. This requires the remote server to support standard OAuth discovery with dynamic client registration (or a pre-registered catalog client). The optional **shared upstream token** in bot settings applies only to remote OAuth connectors left in shared mode; platform user-OAuth servers always use per-user sign-in.

## Approving destructive actions

When the assistant wants to run a **destructive or confirmation-gated** tool, the bot pauses and posts **Approve / Reject** buttons in the thread — one pair per pending action, showing the tool name, the connector, and the arguments. The action only runs after you tap **Approve**; **Reject** declines it. Only the user who asked for the action can approve or reject it — clicks from anyone else in the thread are ignored, since the action runs in the requester's own account. Once every pending action in the batch is decided, the bot resumes and replies. This works in Slack, Discord, Telegram, and WhatsApp. Approval requests expire after 30 minutes; if one lapses, just ask again.


# Ready-to-use connectors

**Ready-to-use APIs** are pre-built connectors curated by FlowCP. Instead of pasting an OpenAPI URL and configuring everything yourself, you pick a connector from a catalog and FlowCP creates a **hosted MCP server in your workspace** for that API — with its tools already generated. You bring your own OAuth credentials, and the connector stays up to date as FlowCP maintains it.

> **Not the same as the** [**Connector library (remote MCP servers)**](/guides/mcp-connector-library)**.** That catalog lets you attach *external, already-hosted* MCP servers (GitHub, Notion, …) to a chat widget. Ready-to-use connectors instead create a **first-party MCP server you own**, running on FlowCP under your own server ID, in each end-user's own authorization context.

Open **Configure → Connectors** in the sidebar and select the **Ready-to-use APIs** tab.

> **Platform admins:** the catalog is empty until you connect a connector repository and sync it. See [Set up the connector library](/guides/set-up-connector-library).

## Browse the catalog

Search or filter by category to find a connector. Each card shows the authentication type, how many endpoints (tools) it will generate, and a link to the provider's developer docs.

## Add a connector to your workspace

1. Click **Add to workspace** on a connector.
2. Give the app a name (defaults to the connector's name) and click **Create server**. FlowCP creates a new app and a **Default** MCP server with its own server ID and imports the API to generate your tools.
3. **Connect your account.** How this step works depends on the connector's authentication:
   * **User OAuth** (most connectors) — enter the **Client ID** and **Client Secret** from *your own* OAuth app with the provider. The authorization and token URLs and scopes are prefilled from the connector; you only supply your credentials. Use the **"Where do I create an OAuth app?"** link to reach the provider's developer settings. Add FlowCP's OAuth **redirect URL** (shown in the form) to your OAuth app.
   * **API key / bearer** — you'll be pointed to the app's **MCP Auth** page to add the shared workspace credential.
   * **Open** — nothing to configure.
4. Go to the app's **Tools** page and **enable** the tools you want. As always, tools start **disabled** (default-deny), and destructive tools require confirmation.
5. **Deploy** the server from its **Deploy** page and connect to it from any MCP client.

Your credentials are stored encrypted (AES-256-GCM) and are never shown again. FlowCP never ships credentials in a connector — you always bring your own.

## Staying up to date

A ready-to-use connector stays linked to the FlowCP catalog. When FlowCP updates a connector's API definition, your server's tools are re-imported automatically:

* **New tools** arrive **disabled**, so nothing is exposed without your say-so.
* **Your customizations are preserved** — enablement, confirmation settings, and any manually authored tools survive re-imports.

You can see that an app came from the catalog on its **App details** page, which notes the connector it was created from.

## Removing a connector

A ready-to-use connector is an ordinary app once created. Delete it like any other app from its **Settings → Danger** page. If FlowCP removes a connector from the catalog, your app keeps working — it simply stops receiving catalog updates.


# Set up the connector library (admin)

This guide is for **platform administrators**. It explains how to populate the [Ready-to-use APIs](/guides/ready-to-use-connectors) catalog that workspaces browse.

The catalog is empty until you point FlowCP at a git repository of connector definitions and run a sync. (Unlike the remote-connector library, it ships no built-in seed — an empty catalog on first run is expected, not a bug.)

## How it works

Connector definitions live in a git repo — one folder per connector. A platform-level **library source** records which repo/branch/directory to sync from. A background job mirrors each connector folder into a `ConnectorTemplate` row; those rows are what workspaces see under **Ready-to-use APIs**. The sync job is the only thing that reads GitHub — instantiation and re-imports read the stored spec, so they never hit rate limits.

## 1. Create the connector repo

Create a repo (for example `inovastudio/apis-library`) with one folder per connector under a root directory (default `connectors/`):

```
connectors/
  github/
    connector.yaml
    openapi.json          # or openapi.yaml / openapi.yml
  acme-crm/
    connector.yaml
    openapi.json
```

* The **folder name is the connector slug** and must match `^[a-z0-9][a-z0-9-]{1,62}$`.
* **`connector.yaml`** carries the connector's metadata and its non-secret OAuth defaults. It must **never contain a secret** — any secret-shaped key (`clientSecret`, `token`, `password`, …) causes the whole manifest to be rejected. Each workspace supplies its own credentials at instantiate time.

```yaml
formatVersion: 1
name: GitHub
description: Repos, issues, and pull requests via the GitHub REST API.
category: Developer Tools
apiBaseUrl: https://api.github.com # https only
docsUrl: https://github.com/settings/developers # where a user creates their OAuth app
authMode: user_oauth # user_oauth | api_key | bearer | none
oauth: # required when authMode: user_oauth
  provider: oauth2 # oauth2 needs explicit URLs below;
  authorizeUrl: https://github.com/login/oauth/authorize #   named providers (auth0,
  tokenUrl: https://github.com/login/oauth/access_token #   okta, …) resolve via metadata
  scopes: repo read:user
  # metadata: { }                                 # optional non-secret provider params
sortOrder: 10
# For authMode: api_key, omit `oauth` and instead give the placement:
# authConfig: { in: header, name: X-API-Key }
```

The manifest schema is enforced by `parseConnectorManifest` in `@flowcp/shared` — the single source of truth for the format. A repo CI check can import it to validate every `connector.yaml` before merge.

## 2. Install the FlowCP GitHub App on the repo

The library sync reuses the same GitHub App as git-backed app sources. Install it on the connector repo (or grant your existing org installation access to it), then note the **installation ID** — find it under GitHub → org **Settings → Applications → FlowCP → Configure** (the page URL ends in `installations/<id>`), or from your account's git connection in FlowCP.

## 3. Allowlist admin emails

The library admin endpoints are gated by an environment allowlist and fail closed when it's unset. Set it on the **API service** and redeploy:

```
ADMIN_EMAILS=you@example.com,teammate@example.com
```

## 4. Configure the source and sync

There is no admin UI or dashboard token page. The simplest way to do the one-time setup is the bundled **`setup-library`** script, run from a trusted shell that already has the API's environment — the **API service console** (e.g. on Railway). It calls the sync service directly, so it needs no token and is not subject to the `ADMIN_EMAILS` gate (that only guards the HTTP endpoints in the next section).

```bash
# 1. List the GitHub App installations this instance knows about (or print where
#    to find the id on GitHub). Run with no id first:
pnpm library:setup

# 2. Re-run with the installation id for the org that owns the connector repo.
#    Owner/repo default to inovastudio/apis-library; override with
#    LIBRARY_REPO_OWNER / LIBRARY_REPO_NAME / LIBRARY_REF / LIBRARY_ROOT_DIR.
LIBRARY_INSTALLATION_ID=<id> pnpm library:setup
```

It configures the library source (probing the repo first — this fails if the root directory is empty or unreachable, so create the repo **with** its `connectors/` folder before running), then runs one sync and prints the result, e.g. `{ scanned, upserted, failed, delisted, reimportedServers }`. Once a connector syncs, it appears on the **Ready-to-use APIs** tab immediately. A non-zero `failed` count means one folder's `connector.yaml`/spec is invalid — the per-folder error is logged with its slug.

### HTTP endpoints (alternative)

The same operations are exposed as admin HTTP endpoints, authenticated as a user whose email is in `ADMIN_EMAILS` (`ref`/`rootDir` optional, default `main` / `connectors`):

```bash
curl -X PUT https://api.flowcp.ai/admin/library/source \
  -H "Authorization: Bearer <admin token>" \
  -H "Content-Type: application/json" \
  -d '{"installationId":"<id>","repoOwner":"inovastudio","repoName":"apis-library","ref":"main","rootDir":"connectors"}'

curl -X POST  https://api.flowcp.ai/admin/library/sync      -H "Authorization: Bearer <admin token>"
curl         https://api.flowcp.ai/admin/library/templates  -H "Authorization: Bearer <admin token>"
```

An admin token is a FlowCP API token for an allowlisted user, created with the `flowcp` CLI (`flowcp token create`) — there is no dashboard page for it.

## Keeping the catalog current

* **Auto-sync on merge.** After the one-time setup, a merged pull request into the tracked branch triggers a sync automatically through the existing GitHub webhook — no manual step.
* **Fail closed per folder.** A bad manifest or spec in one connector is recorded as a `syncError` (visible in `GET /admin/library/templates`) and skipped; the rest of the catalog is unaffected, and a previously-good spec is retained.
* **Delisting.** Deleting a connector's folder marks its template `delisted` — hidden from browse and instantiate, but never deleted, so workspaces that already instantiated it keep working.

## Endpoint reference

| Method & path                  | Purpose                                         |
| ------------------------------ | ----------------------------------------------- |
| `GET /admin/library/source`    | The configured repo pointer + last sync status  |
| `PUT /admin/library/source`    | Set/replace the repo pointer (probes it first)  |
| `POST /admin/library/sync`     | Trigger a catalog sync now                      |
| `GET /admin/library/templates` | All templates, including delisted + `syncError` |

All four require an authenticated user whose email is in `ADMIN_EMAILS`.


# Connector library (remote MCP servers)

The **Connectors** catalog lets you add **external/remote MCP servers** — GitHub, Notion, Linear, Stripe, Sentry, and many more — to your workspace, then attach them to any [embedded chat widget](/guides/embed-ai-chat-widget). The assistant can then use those servers' tools alongside your own app's tools, all in one conversation.

Open it from **Configure → Connectors** in the sidebar, under the **Remote connectors** tab.

> **Looking to create a hosted server for an API instead?** See [Ready-to-use connectors](/guides/ready-to-use-connectors) (the **Ready-to-use APIs** tab) — those create a first-party MCP server you own, rather than attaching an external one.

## Browse the catalog

The catalog is a curated set of remote MCP servers. Use the left sidebar to **search** or filter by **category** (Software Development, Payments, CRM, Productivity, …), or show only the ones you've **Enabled**. Click any connector to see its overview — what it does, its authentication type, and a link to the provider's docs.

## Enable a connector for your workspace

Only **workspace owners** can enable connectors. On a connector's detail page, click **Enable for workspace**. Once enabled, the connector becomes available to attach to your chat widgets. Use **Disable for workspace** to turn it off again.

How a connector authenticates depends on its type:

* **OAuth** (most connectors) — nothing to configure here. Each **end-user signs in individually** in the chat widget (a popup runs the provider's OAuth). Their token is used only for their own requests and is **never stored** by FlowCP.
* **API key** — you provide a **shared workspace credential** when enabling. It's stored encrypted and used for everyone using that connector in your workspace. Only the last four characters are ever shown.
* **Open** — no authentication; just enable.

## Add a connector to a chat widget

In a chat widget's **Connectors** section (see [Embed an AI chat widget](/guides/embed-ai-chat-widget)), click **Add connector**. Enabled remote connectors appear alongside your platform servers (tagged **Remote**). Add one and its tools join the widget's chat, namespaced with the connector's prefix so names never collide.

For OAuth connectors, each end-user gets a **Connect&#x20;*****name*** button in the widget and signs in to that provider separately; if a connector's session expires mid-chat, only that connector asks to reconnect.

## Security

* Every request to a remote server goes through FlowCP's SSRF-safe network layer (IP-pinned, https-only).
* OAuth discovery, dynamic client registration, and the token exchange are proxied server-side; PKCE secrets never sit in the browser in the clear.
* End-user OAuth tokens are **never persisted** — they live only in the visitor's browser session and on the wire per request. Shared API keys are encrypted at rest.


# Publish your MCP server

Every MCP server has two environments: **development** (always available for testing) and **production** (where real users connect). Use the **Deploy** page to promote a server from development to production, and to recall it when needed.

## Development vs. production

|                 | Development                            | Production                                                              |
| --------------- | -------------------------------------- | ----------------------------------------------------------------------- |
| URL             | `/mcp/dev/<dev-slug>`                  | `/mcp/<server-id>` (or your [custom slug](#customizing-the-server-url)) |
| Who can connect | Workspace members only                 | Anyone with a valid token                                               |
| When available  | Always (unless the server is disabled) | Only after a successful deploy                                          |
| Status          | `Development`                          | `Production`                                                            |

The development URL is always shown on the Deploy page regardless of whether the server has been deployed. Use it to test tools, skills, widgets, and prompts before your users see anything.

## One server per branch

Each branch (the **Default** branch, or Live/Main/any branch you add) is a separate MCP server with its own endpoints, its own tool set, and its own deployment state. You deploy each branch independently — for example, you can keep **Main** in development while **Live** is in production. Use the **Deploy** button in the dashboard header (with the branch selected in the branch switcher), or open [Settings → Branches](/guides/manage-branches) and click **Deploy** on the branch you want to take live.

For Bubble apps, a version routes its calls through that version's URL (`/version-<id>/`), so a deployed **Main** server calls `https://<app-id>.bubbleapps.io/version-test/...` while **Live** calls the bare URL.

## Before you deploy

Deploying requires:

1. **At least one tool enabled** — a server with no enabled tools cannot serve useful MCP calls.
2. **OAuth configured** — the client ID and secret must be saved (see [Configure OAuth](/guides/configure-oauth)).
3. **No unguarded destructive tools** — every enabled tool with a `destructive` risk level must have **Require confirmation** turned on. If any do not, the deploy button is blocked and lists the offending tools by name.
4. **Managed auth fully configured** — a branch using [Enterprise managed auth](/guides/enterprise-managed-auth) must have both an identity provider assigned (so callers can be verified) and a shared credential for the upstream call. Managed branches also cannot publish tools that require an OAuth scope; use access rules instead.

The fastest way to check all of this at once is to run the [Tests page](/guides/test-your-mcp-server) — it reports each deploy-critical check in one place.

## Testing in development

1. Open your app and navigate to the server's **Deploy** page.
2. Copy the **Development URL** snippet for your AI client.
3. Add it to your client config (Claude Desktop, Cursor, ChatGPT).
4. Test your tools — the development URL is available immediately, with no deploy required.

The development URL uses a `-dev` suffix on the server key in the snippet to distinguish it from the production server in your client.

## Deploying to production

1. Open the server's **Deploy** page.
2. Review the status card. If there are blockers, fix them first (or run the [capability tests](/guides/test-your-mcp-server)).
3. Click **Deploy to Production**.

You can also deploy the selected branch straight from the **Deploy** button in the dashboard header. After you confirm, a progress checklist steps through **Validating → Testing → Building → Deploying → Live** and finishes with a confirmation toast. If a blocker is hit before the branch goes live, the checklist stops on the failing stage and shows the blocker message with a **Try again** button.

The status changes from `Development` to `Production`. The MCP runtime immediately begins accepting connections from any user at `/mcp/<server-id>`.

The page records the deploy timestamp so you can see when the last deploy happened.

Every deploy also saves an automatic, restorable snapshot of the configuration that went live. See [Version history](/guides/version-history) for committing named versions and rolling back to a previous state.

## The production MCP config snippet

Once deployed, the **Production URL** panel appears with tabs for each supported AI client.

### Claude Desktop

```json
{
  "mcpServers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "type": "http"
    }
  }
}
```

### Cursor

```json
{
  "mcpServers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "transport": "http"
    }
  }
}
```

### ChatGPT

```json
{
  "servers": [
    {
      "name": "my-app",
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "transport": "streamable-http"
    }
  ]
}
```

> **Using a custom domain?** If you've added a verified, active [custom domain](/guides/custom-domain) for this app, the snippet automatically targets it instead of the default host.

## Customizing the server URL

By default the production endpoint ends in the server's internal id — `https://runtime.flowcp.io/mcp/<server-id>`. You can replace that last segment with a friendly, memorable slug.

In the **Production URL** panel, find the **Server URL** field and click **Edit**. Enter a slug and click **Save**. For example, `acme-crm` gives you:

```
https://runtime.flowcp.io/mcp/acme-crm
```

Slug rules:

* Lowercase letters, numbers, and single hyphens only (no leading, trailing, or double hyphens).
* 3–50 characters.
* A handful of reserved words (such as `dev`) aren't allowed.
* Slugs are unique across the platform — if one is already taken you'll see **"That URL is already taken"** and can pick another.

The **old URL keeps working** after you set a custom slug, so any AI client already pointed at the `/mcp/<server-id>` URL won't break. Pair a custom slug with a [custom domain](/guides/custom-domain) for a fully branded endpoint like `https://mcp.acme.com/mcp/acme-crm`.

## Listing in the public app directories

Deploying makes your server connectable by anyone with the URL. To have it discovered and installed from within an AI client, submit it to that client's public directory:

* [Publish to ChatGPT](/guides/publish-to-chatgpt) — submit to the ChatGPT Apps Directory.
* [Publish to Claude](/guides/publish-to-claude) — submit to the Claude Connectors Directory.

## Recalling from production

To pull the server back to development without deleting it:

1. Navigate to the server's **Deploy** page.
2. Click **Recall from Production** (visible when the server is in `Production` status).
3. Confirm in the dialog.

The status changes back to `Development`. The MCP runtime stops accepting public connections immediately. All tools, configuration, and OAuth settings are preserved. The development URL continues to work for workspace members.

To re-deploy, click **Deploy to Production** from the same page.

## Tool exposure

The **Tool Exposure** card controls how MCP sessions see this server's tools. Large APIs can generate hundreds of tools, which floods AI clients and wastes context tokens.

* **Auto** (default) — dynamic discovery activates only when the server has more than 40 enabled tools; smaller servers expose every tool up front.
* **All tools** — always expose every enabled tool up front.
* **Dynamic** — always start sessions with only `search_tools`.

Changes apply to new MCP sessions within about a minute and affect both the development and production endpoints.

## Agent Auth (auth.md)

The **Agent Auth** card lets you publish an [auth.md](https://github.com/workos/auth.md) discovery document so AI agents can authenticate on a user's behalf. It is **off by default** and only shown for production servers. See [Agent authentication discovery](/guides/agent-auth-discovery) for details.

## Add your server to the connectors library

The **Connectors library** card lets you submit your server to FlowCP's public connectors library, where other workspaces can discover and enable it.

The card is shown only for **production** servers that use **per-user OAuth** or **no auth** — servers that rely on a shared API key or bearer token are not eligible, so their credentials are never exposed in a public catalog.

To submit, fill in:

* **Category** — a short grouping label (e.g. *Productivity*).
* **Description** — what the server lets users do.
* **Docs URL** *(optional)* — a link to your documentation.

Every submission is **reviewed** before it goes live. After you submit, the card shows the current status:

* **Pending review** — submitted and waiting on a FlowCP admin. You can withdraw it at any time.
* **Listed** — approved and visible in the connectors library.
* **Not accepted** — declined; the reviewer's notes explain why, and you can remove the submission and try again after addressing them.

## How the server appears in your AI client

When a client connects, the runtime advertises the server's identity:

* **Name and title** — labelled with your app name and environment.
* **Logo** — your app's logo/favicon.
* **Description** — AI-generated or spec-derived, overridable at any time.
* **Website** — your app's public homepage, shown as the connector link.
* **Instructions** — a map of the server's enabled tools grouped by category.

## Viewing execution logs

Navigate to the server's **Logs** tab. Each entry shows the tool name, end-user connection ID, response status, latency, and any error code. Logs from the development endpoint are tagged `development` so you can filter them separately.

See [Execution logs](/reference/execution-logs) for the full field reference.

## Re-deploying after changes

You do not need to re-deploy after changing tools — tool changes are read dynamically at request time. Re-deploy only when you want to update the server's auth mode, description, or other server-level settings.

## Running it on your own infrastructure

On a paid plan you can export a published server and run it yourself with the FlowCP standalone runtime — no FlowCP connection, Postgres, or Redis required. See [Export & self-host your MCP server](/guides/self-host-export).


# Publish to ChatGPT

This guide covers submitting your FlowCP server to the **ChatGPT Apps Directory** (and the Codex Plugin Directory) so anyone can discover and install it. OpenAI reviews every app before it goes live, so the flow is: deploy your server, test it privately in **developer mode**, then submit it through the [OpenAI Platform Dashboard](https://platform.openai.com/apps-manage) for review.

> **Just want it for yourself or your team?** You don't need to submit anything. Add the server as a custom connector in ChatGPT developer mode — see [Integrate with AI clients](/guides/integrate-with-ai-clients). Only submit if you intend the app to be **publicly** available in the countries you choose.

The canonical, always-current reference is OpenAI's [Submit and maintain your app](https://developers.openai.com/apps-sdk/deploy/submission). This page maps that flow to FlowCP.

## Prerequisites

* **A deployed production server.** Your server must be in **Production** with a publicly accessible URL — not a local or development endpoint. FlowCP's production URL (`https://runtime.flowcp.io/mcp/<server-id>`, or your verified [custom domain](/guides/custom-domain)) satisfies this. See [Deploy your MCP server](/guides/publish-your-mcp-server). Submit this single universal URL — it works for all users.
* **OAuth configured.** Run every call in the end user's own context with user-context OAuth (see [Configure MCP Auth](/guides/configure-oauth)). You'll supply the OAuth credentials in the submission form.
* **A content security policy (CSP).** OpenAI requires your app to declare the exact domains it fetches from. This matters most when you ship [MCP widgets](/guides/build-widgets).
* **Organization verification.** Complete individual or business verification in the OpenAI Platform Dashboard for the name you'll publish under — it is enforced at review.
* **App management permission.** You need the `api.apps.write` permission to create and submit drafts (organization owners have it by default).
* **A privacy policy and a test account.** Have a public privacy policy URL and a working demo account **with no MFA** so reviewers can sign in and exercise every tool end to end.

## Step 1 — Test in developer mode

1. In FlowCP, open **Distribute → MCP clients** and copy the production server URL (see [Integrate with AI clients](/guides/integrate-with-ai-clients)).
2. In ChatGPT, enable developer mode under **Settings → Apps & Connectors → Advanced settings**.
3. Add your FlowCP server as a custom connector and authorize it.
4. Exercise every tool on both the ChatGPT web and mobile apps, confirming the results match what you expect.

## Step 2 — Prepare your submission

Get these ready before you open the form:

* **Directory metadata** — a clear, specific app name and description. Avoid generic single-word names, which are often rejected.
* **Accurate tool definitions** — human-readable tool names and descriptions, and correct hint annotations. `readOnlyHint`, `destructiveHint`, and `openWorldHint` must match what each tool actually does. In FlowCP these map to your [tool risk levels](/reference/tool-risk-levels) (`read` / `write` / `destructive`) and the **Require confirmation** toggle — review them on the [Tools page](/guides/review-and-customize-tools) before submitting.
* **Privacy policy** that discloses exactly the user-related data your tools return. Don't return PII, internal identifiers, or secrets you don't disclose.
* **Listing assets** — logo, screenshots, test prompts with their expected responses, localization details, and the countries where the app should be available.

## Step 3 — Submit for review

At [platform.openai.com/apps-manage](https://platform.openai.com/apps-manage):

1. Add your MCP server details, including OAuth credentials if OAuth is selected.
2. Complete the submission form (app name, logo, description, company and privacy policy URLs, MCP and tool information, screenshots, test prompts and responses, localization) and check all confirmation boxes.
3. Click **Submit for review**. You'll get an email confirming submission with a **Case ID** to reference in any support requests.

> **Notes.** Only one version of an app can be in review or published at a time — to change a pending submission, choose **Cancel Review** and resubmit the draft. Projects with EU data residency can't submit; use a project with global data residency.

## Step 4 — Review, approval, and publishing

Your app enters the review queue; track status in the dashboard and by email. OpenAI may run automated scans and manual reviews.

* **Approved** — click **Publish** from the dashboard to list it in the App Directory (OpenAI also creates a Codex plugin). It is not listed until you publish.
* **Rejected** — you'll get feedback on which checks failed. Fix the issues and resubmit, or appeal by replying to the email with a clear rationale.

Common rejection reasons worth pre-empting:

* Reviewers can't connect using the MCP URL and/or test credentials (broken creds, expired creds, or MFA on the demo account).
* One or more test cases didn't produce the expected result.
* Tools return user-related data not disclosed in the privacy policy.
* Tool hint annotations don't match the tool's real behavior.

## Discoverability

Once published, users find your app by clicking its direct directory link (shown next to the **Published** status in the dashboard) or by searching its name. Enhanced placement — directory features or proactive suggestions — is selective and can't be requested.

## Updating a published app

OpenAI **snapshots** your tool and metadata contract when you submit. Because FlowCP reads tools dynamically, deploying a change updates your live tool results immediately but does **not** update the published snapshot at OpenAI. To change what users see:

1. Make **backward-compatible** changes only (add tools or fields; don't remove or rename them). The app's base MCP server URL can't change between versions.
2. Create a new draft **version**, submit it for review, and publish once approved.

If a server deployment would break the published contract, roll it back rather than wait for a new version to clear review.

## Learn more

* [Submit and maintain your app](https://developers.openai.com/apps-sdk/deploy/submission) — OpenAI
* [App submission guidelines](https://developers.openai.com/apps-sdk/app-submission-guidelines) — OpenAI
* [Deploy your MCP server](/guides/publish-your-mcp-server)
* [Integrate with AI clients](/guides/integrate-with-ai-clients)


# Publish to Claude

This guide covers submitting your FlowCP server to the **Claude Connectors Directory** so Claude users can discover and add it. Anthropic reviews every submission, so the flow is: deploy your server, add and test it as a custom connector, then submit it through the directory portal in Claude.ai admin settings.

> **Just want it for yourself or your team?** You don't have to submit anything. Add the server as a custom connector in Claude — see [Integrate with AI clients](/guides/integrate-with-ai-clients). Only submit to the directory if you want the connector **publicly** listed.

Because Claude connects to your remote server from Anthropic's cloud (not from the user's device), your production URL must be publicly reachable. The canonical, always-current reference is Anthropic's [Submitting to the Connectors Directory](https://claude.com/docs/connectors/building/submission).

## Prerequisites

* **A deployed production server** with a publicly accessible URL. FlowCP's production URL (`https://runtime.flowcp.io/mcp/<server-id>`, or your verified [custom domain](/guides/custom-domain)) satisfies this. See [Deploy your MCP server](/guides/publish-your-mcp-server).
* **OAuth configured** so every call runs in the end user's own context (see [Configure MCP Auth](/guides/configure-oauth)).
* **A public privacy policy URL.** A missing or incomplete privacy policy is an immediate rejection.
* **Directory management access.** The submission portal lives in Claude.ai admin settings, which require a **Team or Enterprise** organization (it isn't available on individual plans). By default only Owners and Primary owners can submit; on Enterprise, an Owner can delegate via a custom role with the **Directory management** (or Libraries) permission.
* **A populated test account.** Reviewers need credentials and step-by-step access instructions to run your server end to end.

## Step 1 — Add and test as a custom connector

1. In FlowCP, open **Distribute → MCP clients** and copy the production server URL (see [Integrate with AI clients](/guides/integrate-with-ai-clients)).
2. In Claude, go to **Settings → Connectors → Add custom connector** and paste the URL.
3. Authorize when prompted, then run **every** tool. The submission asks you to confirm you've exercised each one — either here as a custom connector or via the MCP Inspector.

## Step 2 — Prepare your submission

Have these ready before opening the portal:

* **Privacy policy URL** (required).
* **Reviewer test-account setup** — detailed enough to access your server end to end: every credential and step, including a fully populated account where relevant.
* **Setup and usage instructions** that are clear to a first-time reviewer.
* **Allowed link URIs** — if any tool opens external links in the user's browser or native apps (the `ui/open-link` capability), provide the list of link targets so users aren't prompted to confirm each one.
* **Directory metadata** — name, description, and listing details.

## Step 3 — Submit through the portal

1. Open the submission portal in **Claude.ai admin settings** (as an Owner / Primary owner, or with a delegated Directory management role).
2. Complete the submission form and submit. The portal is always open.

## Step 4 — Track review status

After you submit, your status and any reviewer feedback appear in the **submissions dashboard**. Review times vary with queue volume. If the submission is rejected, address the feedback and resubmit.

## Updating your connector

FlowCP reads tools dynamically, so tool and configuration changes take effect on your live server without a redeploy. Resubmit to the directory when you make a material change to what the connector offers or how reviewers would evaluate it.

## Learn more

* [Submitting to the Connectors Directory](https://claude.com/docs/connectors/building/submission) — Anthropic
* [Anthropic Connectors Directory FAQ](https://support.claude.com/en/articles/11596036-anthropic-connectors-directory-faq)
* [Deploy your MCP server](/guides/publish-your-mcp-server)
* [Integrate with AI clients](/guides/integrate-with-ai-clients)


# Export as a Claude Code plugin

Package your hosted MCP server as an installable [**Claude Code**](https://code.claude.com) **plugin** and download it as a `.zip` to import into Claude. A plugin bundles the connection to your server (and any skills and prompts you've authored) into a single artifact your users — or your team — can install in one step.

> **Beta.** The generated layout may still evolve.

Plugin export is a **paid feature** — it's available on the **Team plan and above**. On the free Starter plan the **Plugins** page is hidden; upgrade to enable it.

Find it under **Distribute → Plugins** for any app (on an eligible plan).

## What you get

The download is a self-contained, single-plugin marketplace folder:

```
<server-slug>/
├── .claude-plugin/
│   ├── plugin.json        name, description, version (+ a token prompt if needed)
│   └── marketplace.json   one-plugin marketplace so the folder installs as-is
├── .mcp.json              connection to your hosted MCP server (type: http)
├── skills/<slug>/SKILL.md  your enabled skills (+ their reference files)
├── commands/<slug>.md      slash commands generated from your enabled prompts
└── README.md               install instructions
```

Your **tools, widgets, and resources are not copied into the plugin** — they are served live over the MCP connection when Claude connects, so they always reflect the current server configuration.

## Requirements

* A **Team plan or higher.** Plugin export is gated behind the `plugin_export` feature; Starter workspaces can't export.
* A **published** server. A development branch can't be exported as a plugin because it has no public endpoint — [deploy it](/guides/publish-your-mcp-server) first.

## How authentication is handled

The plugin connects to your live production endpoint, so it authenticates exactly the way your server does. No secrets are ever written into the plugin.

| Server auth mode | In the plugin                                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| **User OAuth**   | Claude Code prompts each user to sign in on first use. Nothing is stored in the plugin.                              |
| **Bearer token** | Each user enters the token at install time (stored in their OS keychain) and it's sent as an `Authorization` header. |
| **API key**      | Each user enters the key at install time; it's sent in the header or query parameter your server expects.            |
| **None**         | The plugin connects directly with no credential.                                                                     |

## Export and install

1. Open **Distribute → Plugins**, confirm the auth note matches your server, and click **Download plugin (.zip)**.
2. Unzip it. In Claude Code, add the folder as a marketplace and install the plugin:

   ```
   /plugin marketplace add /absolute/path/to/<server-slug>
   /plugin install <server-slug>@<server-slug>
   ```

   Alternatively, commit the folder to a git repository and run `/plugin marketplace add <owner>/<repo>` followed by the same `install` command — handy for sharing with a team.
3. Your server's tools appear in Claude. Skills and prompts you authored show up as plugin skills and slash commands.

## Related

* [Integrate with AI clients](/guides/integrate-with-ai-clients) — connect Claude, Cursor, VS Code and others by URL or config.
* [Export & self-host your MCP server](/guides/self-host-export) — run the server itself on your own infrastructure.
* [Publish to Claude](/guides/publish-to-claude) — list your server in the Claude Connectors Directory.


# Export & self-host your MCP server

Most FlowCP servers run on FlowCP's hosted runtime. On a **paid plan** you can also **export** a published server as a portable **standard FlowCP folder** and run it on your own infrastructure — for data-residency, air-gapped deployments, or running the MCP endpoint inside your own network.

The export is **language-agnostic**: the folder is a `config.yaml` plus human-readable per-item files (tools, skills, prompts, widgets, resources, documentation pages), consumed by the FlowCP **standalone runtime** container. There is no generated per-server code and no second SDK to maintain — the same runtime that powers the hosted platform reads the folder from disk instead of from FlowCP's database.

See [The standard FlowCP folder](/guides/folder-format) for the full layout.

## What you get

`flowcp export` writes a [standard FlowCP folder](/guides/folder-format):

```
my-server/
├── config.yaml          # server identity, auth mode, apiBaseUrl, requiredEnv
├── README.md            # run instructions specific to your server
├── Tools/
│   ├── openapi.json     # the source OpenAPI document (reference)
│   └── <tool>.yaml      # one file per tool: definition + policy
├── Skills/   <name>.md
├── Prompts/  <name>.md
├── Widgets/  <name>.html (+ <name>.json sidecar)
├── Resources/ <name>.md
├── Docs/     <slug>.md  # documentation pages, nested by slug
├── docker-compose.yml   # runtime-only compose file (no Postgres, no Redis)
└── .env.example         # the environment variables to fill in
```

## Security: secrets are never exported

A folder **never** contains secrets. Just like a configuration snapshot, the export records only the *fact* that a credential exists (`hasAuthSecret`) and the *placement* of your shared credential (which header or query parameter it goes in) — never the value. You supply the actual secret at run time through the environment (`FLOWCP_UPSTREAM_SECRET`). The same default-deny, fail-closed, and redaction rules apply to the standalone runtime as to the hosted one.

## Export your server

The server must be **published** first (a draft has no stable contract to self-host).

```bash
flowcp export --server <serverId> --out ./my-server
```

You can also trigger an export programmatically:

```
GET /v1/servers/:serverId/export
```

which returns `{ server, files }`, where `files` maps each folder-relative path to its contents.

## Run it

```bash
cd my-server
cp .env.example .env     # then edit .env
docker compose up
```

Your MCP endpoint is served at `http://localhost:3002/mcp/<serverId>` (and at `/mcp`). Point your AI client at that URL.

### Environment

The variables you need depend on the server's auth mode:

| Variable                 | When                 | Description                                               |
| ------------------------ | -------------------- | --------------------------------------------------------- |
| `FLOWCP_FOLDER_PATH`     | always               | Path to the FlowCP folder (set by the compose file).      |
| `FLOWCP_RUNTIME_MODE`    | always               | `standalone` (set by the compose file).                   |
| `FLOWCP_UPSTREAM_SECRET` | `bearer` / `api_key` | The token / key sent to your upstream API.                |
| `MCP_RUNTIME_PORT`       | optional             | Listen port (default `3002`).                             |
| `MCP_RUNTIME_PUBLIC_URL` | optional             | Public URL of the server (used in OAuth metadata).        |
| `MCP_ALLOWED_ORIGINS`    | optional             | Browser origins allowed to call the server (see below).   |
| `FLOWCP_SYNC_URL`        | optional             | FlowCP API origin to poll for config updates (see below). |
| `FLOWCP_DEPLOY_TOKEN`    | with sync            | Deploy token presented when polling `FLOWCP_SYNC_URL`.    |
| `FLOWCP_SYNC_INTERVAL`   | optional             | Sync poll interval in seconds (default `60`, min `10`).   |

> Earlier exports shipped a single `flowcp.bundle.json`. The standalone runtime still boots from one via the deprecated `FLOWCP_BUNDLE_PATH` for backward compatibility, but new exports use the folder format above.

### Restricting browser origins

MCP asks servers to validate the `Origin` header so a page on another site cannot drive your server through a visitor's browser. Set `MCP_ALLOWED_ORIGINS` to the browser origins you serve:

```
MCP_ALLOWED_ORIGINS=https://chatgpt.com,https://claude.ai
```

Requests with **no** `Origin` header (Claude Desktop, Cursor, the CLI — anything that is not a browser) and same-origin requests are always allowed. Leaving the variable unset accepts every origin, which is the default; the runtime logs a warning at boot while it is unset.

## Keep a self-hosted server up to date

An exported folder is a point-in-time artifact: when you later change the server on FlowCP (enable a tool, edit a prompt, publish a new version), the self-hosted copy does not change by itself. Two update mechanisms are supported — pick whichever fits your operations.

### Option A: sync mode (pull/watch)

The standalone runtime can poll FlowCP for the latest **published** config and hot-swap it in memory — no redeploy, no restart. Enable it with two variables:

```bash
FLOWCP_SYNC_URL=https://api.flowcp.ai
FLOWCP_DEPLOY_TOKEN=flowcp_deploy_...
```

Create the deploy token first — it is a narrow, revocable credential that can only *read this one server's export snapshot* (it can never mutate anything):

```
POST   /v1/servers/:serverId/deploy-tokens          { "name": "prod-vm" }
GET    /v1/servers/:serverId/deploy-tokens
DELETE /v1/servers/:serverId/deploy-tokens/:tokenId
```

The raw token is returned **once**, at creation; store it in your deployment's secret manager. Revoking the token stops sync for that machine immediately.

How sync behaves:

* The runtime polls `GET /v1/servers/:serverId/export/snapshot` every `FLOWCP_SYNC_INTERVAL` seconds (default 60) with `If-None-Match`, so an unchanged config is a cheap `304`.
* When the published config changed, the runtime validates the new snapshot and swaps it atomically. New MCP sessions see the new tools immediately; sessions opened before the swap keep their tool set until they expire.
* **Fail closed, keep serving**: a network error, a `403` (revoked token or plan downgrade), or an invalid snapshot never takes the server down — the runtime keeps serving the last good config and retries on the next tick.
* **Secrets are never pulled.** Only the config travels; the upstream credential stays in your environment (`FLOWCP_UPSTREAM_SECRET`).
* The on-disk folder is not rewritten (it is mounted read-only). After a restart the runtime boots from the folder again and catches up on the first poll. Re-export when you want the folder itself to move forward.

### Option B: CI re-export (push)

If you prefer immutable deploys, re-run the export in CI and redeploy the container. The `flowcp` CLI authenticates with a workspace API token (`flowcp_pat_…`), which works headlessly in GitHub Actions:

```yaml
- run: flowcp export --server $SERVER_ID --out ./my-server
  env:
    FLOWCP_TOKEN: ${{ secrets.FLOWCP_TOKEN }}
- run: docker compose -f ./my-server/docker-compose.yml up -d --force-recreate
```

Runtime *image* updates are the same in both options: the standalone runtime is the shared FlowCP runtime image, so pulling a newer image tag upgrades the runtime without touching your folder.

## Supported auth modes

| Mode         | Self-host support                                                |
| ------------ | ---------------------------------------------------------------- |
| `none`       | ✅ Runs with no secret.                                           |
| `api_key`    | ✅ Supply the key via `FLOWCP_UPSTREAM_SECRET`.                   |
| `bearer`     | ✅ Supply the token via `FLOWCP_UPSTREAM_SECRET`.                 |
| `user_oauth` | ⏳ Follow-up — also needs your OAuth provider client credentials. |

Shared-credential modes (`none` / `api_key` / `bearer`) self-host today with no database or cache. Per-user OAuth self-hosting is on the roadmap.


# Self-host the platform

FlowCP runs in one of two **deployment modes**:

* **Cloud** — the managed, multi-tenant service at [flowcp.ai](https://flowcp.ai). MCP requests are metered against your plan, and your plan comes from your Stripe subscription.
* **Self-Hosted** — you run the whole FlowCP platform yourself from a single all-in-one Docker image. MCP requests are **unlimited** (no metered quota), and your plan comes from a signed **license key**.

Both modes ship the same product and the same plans (Starter, Team, Business, Enterprise). Features are gated by **plan**, not by mode — see [Plans & features](/reference/plans-and-features) for the full matrix.

> **This is not the same as per-server export.** [Export & self-host your MCP server](/guides/self-host-export) exports **one published server** as a portable [standard FlowCP folder](/guides/folder-format) and runs just its MCP endpoint on the standalone runtime — no dashboard, no database. The guide you're reading now runs the **entire platform** (dashboard + control-plane API + MCP runtime, plus Postgres and Redis) on your own infrastructure, so you can connect apps, generate tools, configure OAuth, and publish servers exactly as you would on Cloud. Pick per-server export when you only need to host a finished server's endpoint; pick the platform self-host when you want the whole product behind your own perimeter.

## Prerequisites

* **Docker** and the **Docker Compose** plugin (`docker compose`).
* A host that can expose three ports (or a reverse proxy in front of them).

No separate Postgres or Redis is required — the all-in-one stack bundles both.

## What ships

A single all-in-one image published to [`ghcr.io/inovastudio/mcp-builder`](https://ghcr.io/inovastudio/mcp-builder) that runs the dashboard, the control-plane API, and the MCP runtime in one container. The accompanying `docker-compose.selfhost.yml` adds bundled **Postgres** and **Redis** services and wires the volumes and environment for you.

## Quickstart

```bash
cp .env.selfhost.example .env     # then edit .env
docker compose -f docker-compose.selfhost.yml up -d
```

Database **migrations run automatically on boot**, so the stack is ready once the container reports healthy. Open the dashboard at <http://localhost:3000>, create your account, and connect your first app.

## Ports

| Port   | Service           | Notes                                        |
| ------ | ----------------- | -------------------------------------------- |
| `3000` | Dashboard (web)   | The UI you sign in to.                       |
| `3001` | Control-plane API | OAuth authorization server + management API. |
| `3002` | MCP runtime       | The public `/mcp/<server-id>` endpoint.      |

Put a reverse proxy / TLS terminator in front of these if you're exposing the platform beyond localhost, and set the public-URL variables below to the externally reachable origins. `API_PUBLIC_URL` and `MCP_RUNTIME_PUBLIC_URL` are read at **runtime**, so OAuth callback, login, embed, git-webhook and MCP-client URLs follow your domain — the published image needs no rebuild for a custom domain.

> **Set the public-URL variables on the dashboard (web) service too.** The dashboard resolves `API_PUBLIC_URL` and `MCP_RUNTIME_PUBLIC_URL` from **its own** environment (via `/api/config`) to render each server's public endpoint and client-config snippets. In the bundled compose stack every service shares one `.env`, so this is automatic — but if you split the web, API and runtime into separate services, a `MCP_RUNTIME_PUBLIC_URL` set only on the runtime/API won't reach the dashboard, and server URLs render as **"MCP runtime URL not configured"** instead of your domain. Set it on the web service and restart (no rebuild needed).

## Environment reference

Copy `.env.selfhost.example` to `.env` and fill in the values you need. The variables most relevant to self-hosting:

| Variable                       | Required | Description                                                                                                                                                                       |
| ------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FLOWCP_SELF_HOSTED`           | yes      | Set to `true` to run in Self-Hosted mode (unlimited requests, license-key plans, no Stripe UI).                                                                                   |
| `FLOWCP_LICENSE_KEY`           | no       | Signed license key that activates a paid plan. Blank → free **Starter** plan.                                                                                                     |
| `DATABASE_URL`                 | yes      | Postgres connection string. Points at the bundled `postgres` service by default.                                                                                                  |
| `REDIS_URL`                    | yes      | Redis connection string (BullMQ + rate limiting). Points at the bundled `redis` service.                                                                                          |
| `WEB_PUBLIC_URL`               | yes      | Public URL of the dashboard (e.g. `https://flowcp.example.com`).                                                                                                                  |
| `API_PUBLIC_URL`               | yes      | Public URL of the control-plane API (OAuth authorization-server metadata).                                                                                                        |
| `MCP_RUNTIME_PUBLIC_URL`       | yes      | Public URL of the MCP runtime (OAuth protected-resource metadata + the server endpoint / client-config snippets shown in the dashboard). Must also be present on the web service. |
| `OAUTH_ENCRYPTION_KEY`         | auto     | AES-256-GCM key for secrets at rest. Auto-generated on first boot if unset (see below).                                                                                           |
| `OAUTH_STATE_KEY`              | auto     | AES-256-GCM key for OAuth state / auth codes. Auto-generated on first boot if unset.                                                                                              |
| `SESSION_SECRET`               | auto     | HMAC key for session cookies and CSRF. Auto-generated on first boot if unset.                                                                                                     |
| `PLATFORM_OAUTH_CLIENT_SECRET` | auto     | Secret for the platform's own OAuth wrapper. Auto-generated on first boot if unset.                                                                                               |
| `MCP_ALLOWED_ORIGINS`          | no       | Comma-separated browser origins allowed to call the MCP runtime. Unset accepts every origin. See [Origin validation](#origin-validation) below.                                   |

In Self-Hosted mode no Stripe configuration is shipped or required; the Billing UI is replaced by a **License** panel.

## Origin validation

The MCP specification requires a server to validate the `Origin` header on incoming connections, which is what stops a page on an attacker's site from driving your runtime through a visitor's browser (a DNS-rebinding attack).

Set **`MCP_ALLOWED_ORIGINS`** to a comma-separated list of the browser origins that should be able to reach your runtime:

```
MCP_ALLOWED_ORIGINS=https://chatgpt.com,https://claude.ai
```

Two things are always allowed and never need listing:

* **Requests with no `Origin` header.** Claude Desktop, Cursor, and the FlowCP CLI are not browsers and send none. Origin validation only ever affects browser-initiated requests, so these clients are unaffected.
* **Same-origin requests** — where the `Origin` host matches the host the request arrived on. A custom domain serving its own embedded chat widget works without an entry.

`MCP_RUNTIME_PUBLIC_URL` and `WEB_PUBLIC_URL` are included automatically.

> **Leaving `MCP_ALLOWED_ORIGINS` unset accepts every origin.** That is the default so an upgrade cannot silently cut off a working browser client; the runtime logs a warning at boot while enforcement is off. Set the variable once you know which browser clients you serve.

A rejected request gets HTTP `403` with `{"error":"FORBIDDEN_ORIGIN"}`.

## The `/data` volume and secret auto-generation

The compose file mounts a persistent volume at **`/data`**. On first boot, any of these secrets that you did **not** provide in `.env` are generated automatically and written to **`/data/.flowcp/secrets.env`**:

* `OAUTH_ENCRYPTION_KEY`
* `OAUTH_STATE_KEY`
* `SESSION_SECRET`
* `PLATFORM_OAUTH_CLIENT_SECRET`

On every subsequent boot the stack reads the generated values back from that file, so they stay stable across restarts and upgrades.

> **Warning — back up your `/data` volume.** `OAUTH_ENCRYPTION_KEY` encrypts every secret stored in the database (OAuth client secrets, shared upstream credentials, etc.). If you lose the `/data` volume and didn't pin these keys in `.env` yourself, the auto-generated keys are gone and **previously-encrypted secrets can no longer be decrypted** — you'll have to re-enter them. For production, either back up `/data` or set the four secrets explicitly in `.env` and store them in your own secret manager.

## Activate a paid plan

Self-Hosted derives its plan from `FLOWCP_LICENSE_KEY`:

1. Obtain a signed license key for your plan (Team, Business, or Enterprise).
2. Set `FLOWCP_LICENSE_KEY=<key>` in `.env`.
3. Recreate the stack:

   ```bash
   docker compose -f docker-compose.selfhost.yml up -d
   ```

License keys are verified **offline** against a public key baked into the official image, so activation needs no extra configuration or network calls — just the key itself. (To trust a non-official signing key, set `FLOWCP_LICENSE_PUBLIC_KEY` to your own public PEM; it overrides the baked-in default.)

With no key set, the platform runs on the free **Starter** plan (core features, unlimited requests). The License panel in the dashboard shows the active plan and the features it unlocks. See [Plans & features](/reference/plans-and-features) for what each plan includes.

### Issuing license keys (platform operators)

If you run the platform and mint keys for your own installs, sign them with the private key matching the baked-in (or `FLOWCP_LICENSE_PUBLIC_KEY`) public key:

```bash
# Private key supplied at runtime — never commit it.
FLOWCP_LICENSE_PRIVATE_KEY="$(cat flowcp-license.key)" \
  pnpm license:sign --plan team --licensed-to "Acme Inc" --expires 365d
# …or: pnpm license:sign --key-file ./flowcp-license.key --plan enterprise \
#        --licensed-to "Acme Inc" --expires 2027-01-01
```

The printed token is the value end users set as `FLOWCP_LICENSE_KEY`. Generate the keypair offline (`openssl genpkey -algorithm ed25519 -out flowcp-license.key` then `openssl pkey -in flowcp-license.key -pubout`) and keep the private key in a secret manager.

## Upgrade

Pull the latest image and recreate the stack — migrations run automatically on the new boot:

```bash
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d
```

Because all secrets persist in `/data` and the schema migrates forward on boot, upgrades are a pull-and-up with no manual migration step.


# The standard FlowCP folder

The **standard FlowCP folder** is the portable, human- and git-friendly layout a server **exports to** and **imports from**. One folder fully describes a server's MCP surface — tools, skills, prompts, widgets, resources — plus the configuration the standalone runtime needs to serve it.

It is the same shape whether you:

* run `flowcp export` to download a published server,
* hand-author a server in a Git repository, or
* self-host with the FlowCP standalone runtime.

## Layout

```
my-server/
├── config.yaml          # server identity, auth mode, apiBaseUrl, requiredEnv
├── README.md            # run instructions (generated on export)
├── Tools/
│   ├── openapi.json     # the source OpenAPI document (reference / authoring input)
│   └── <tool>.yaml      # one file per tool: definition + policy
├── Skills/   <name>.md   # frontmatter + Markdown body
├── Prompts/  <name>.md   # frontmatter + Markdown body
├── Widgets/  <name>.html # widget HTML (+ <name>.json metadata sidecar)
├── Resources/ <name>.md  # frontmatter + body (the resource content)
└── Docs/     <slug>.md   # product documentation; nested, e.g.
                          #   Docs/guides/webhooks.md → docs://guides/webhooks
```

Directory names are **Capitalized** (`Tools/`, `Skills/`, …). Lowercase names (`tools/`, `skills/`, …) are also accepted when importing, so older repositories and `flowcp sync` working copies keep importing unchanged.

## `config.yaml`

Everything that isn't stored per item lives here:

```yaml
formatVersion: 1
exportedAt: 2026-06-22T00:00:00.000Z
server:
  id: srv_123
  slug: acme
  name: Acme
  description: The Acme API
  authMode: bearer        # user_oauth | api_key | bearer | none
  provider: openapi       # bubble | openapi | git
  manifestVersion: 1
  toolExposure: auto
  hasAuthSecret: true     # the *fact* a credential exists — never its value
apiBaseUrl: https://api.acme.test
requiredEnv:
  - name: FLOWCP_UPSTREAM_SECRET
    description: Bearer token sent to your upstream API on every tool call.
    required: true
```

> **Secrets are never written to a folder.** Only the fact a credential exists (`hasAuthSecret`) and its placement (in `config.yaml`'s `authConfig`) are recorded. You supply the value at run time via the environment.

## `Tools/`

A tool is generated from your OpenAPI spec, so the recommended way to author tools by hand is to drop an **`openapi.json`** in `Tools/` and let FlowCP generate them on import.

An export additionally writes one **`<tool>.yaml`** per tool — the exact definition and policy as configured in the dashboard (so any hand edits to a description, schema, risk level, or widget link are preserved):

```yaml
name: get_order
description: Get an order by id
method: GET
path: /orders/{id}
enabled: true
riskLevel: read
requiresConfirmation: false
requiredScope: orders:read
widget: order_card        # links to Widgets/order_card.html by slug
inputSchema:
  type: object
  properties:
    id: { type: string }
```

The standalone runtime reads these per-tool files directly — it does not need to regenerate tools from the OpenAPI document.

## Content files

`Skills/`, `Prompts/`, `Resources/`, and `Docs/` use Markdown with a `---`-fenced frontmatter block; `Widgets/` use an `.html` file plus an optional `.json` metadata sidecar. This is the **same on-disk format `flowcp sync` reads and writes**, so a folder committed by hand and one written by the CLI behave identically. Cross-references are stored by name/slug — an endpoint resource points at its backing tool with `tool: <tool_name>`, and a tool points at its widget with `widget: <widget_slug>`.

`Docs/` is the one nested directory: a page's slug is its file path below `Docs/` (minus the extension), so `Docs/guides/webhooks.md` is served at `docs://guides/webhooks`, and an `index.md` or `README.md` collapses to its parent directory. Only `title`, `description`, `mimeType`, `enabled`, and `sortOrder` are written to a page's frontmatter — its headings, content hash, and length are re-derived from the body on import, so a hand-edited page never carries stale metadata. See [Serve product documentation](/guides/serve-product-docs).

Internal ids are intentionally omitted; they are re-synthesized from names on import, which keeps exports stable in `git diff` and lets folders be authored entirely by hand.

## Import a folder

```bash
flowcp import folder ./my-server --server <serverId>
```

This upserts the folder's skills, prompts, widgets, resources, and documentation pages into the target server. Imported items follow the platform's **default-deny** rule. To import tools, connect the app to the folder's `Tools/openapi.json`.

## Run a folder (self-host)

Set `FLOWCP_FOLDER_PATH` to the folder and start the standalone runtime — see [Export & self-host your MCP server](/guides/self-host-export).


# Version history

Every change to a server's configuration is tracked, and the full configuration can be committed as a restorable version at any time. Open it from **Servers → History** (or via the **History** button on the Deploy page).

> **Naming note:** "branches" (Live, Main, …) are separate servers — see [Managing branches](/guides/manage-branches). The History page tracks *versions of one server's configuration over time*, like commits in git.

## What is tracked

The History page is organized into four tabs:

* **Versions** — restorable snapshots of the server's complete configuration: server settings (description, auth mode, tool exposure, agent auth), tools, skills, widgets, prompts, and resources.
* **Deployments** — every promotion to production and recall back to development.
* **Change log** — every individual configuration change (a tool enabled, a skill edited, the server published, a version restored), with who made it and when.
* **Spec imports** — every import of the server's API spec, with diffs of what changed upstream.

## How versions are created

| Trigger                   | Kind                      | When                                                                                                                                    |
| ------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Deploying to production   | `Published`               | Automatic on every deploy. Re-deploying an unchanged configuration does not create a duplicate.                                         |
| **Commit version** button | `Commit`                  | Whenever you want a named checkpoint, with a message.                                                                                   |
| Restoring a version       | `Restore` + `Pre-restore` | A restore records the restored state as a new version, and automatically saves the state it replaced as a `Pre-restore` snapshot first. |

## Restoring a version

Click **Restore** on any version. The current configuration is replaced by that version's snapshot:

* **Nothing is ever lost.** A pre-restore snapshot of the current state is saved automatically, and the restore itself is recorded as a new version — so you can undo a restore by restoring the pre-restore snapshot.
* **Later additions are removed.** Tools, skills, widgets, prompts, and resources added after the restored version are deleted from the current configuration (they remain inside later snapshots).
* **Identity fields are untouched.** The server's name, branch, URLs, deployment status, and import source are never changed by a restore.
* **Live servers stay safe.** Restoring onto a server that is in production re-runs the full publish validation; if the snapshot would put a blocked configuration live (for example a destructive tool without confirmation), the restore is rejected and nothing changes.

Use **View changes** on a version to see exactly what differs before restoring. The diff shows each changed setting and entity field with its **before → after** values — long values (skill bodies, widget HTML) are truncated and large schemas summarized (e.g. `object (12 keys)`) to keep the view readable. By default the version is compared against the current configuration; use the **Compare against** selector inside the panel to diff it against any other version instead.

## Credentials are never stored in history

Version snapshots deliberately exclude secrets. For servers using a shared credential (`api_key`/`bearer`), the snapshot records the auth *mode and placement* but never the credential itself:

* Restoring keeps whatever credential is currently stored.
* If you restore an auth mode that needs a credential and none is stored, the restore succeeds with a warning, and publishing stays blocked until you re-enter the credential under the server's Auth settings.
* If a referenced OAuth provider config or shared auth config was deleted after the snapshot was taken, the restore clears that assignment and warns you, rather than pointing at a missing config.

## Deployments

The **Deployments** tab lists every promotion to production and every recall back to development, newest first — who triggered it and when. Deploys made after version-linking shipped also show a **v{n}** chip identifying the exact version snapshot that went live (deploying creates that snapshot automatically), so you can open it in the version list or restore it later. Older deploys, recorded before the link existed, appear without a version chip.

## Change log

The **Change log** tab records each individual mutation — entity, action, author, and timestamp — so you can see exactly what happened between two versions. Secrets and request payloads are never logged; auth changes record only the mode transition.

## Spec import history

The **Spec imports** tab lists every import of the server's API spec, newest first. While versions track *your* configuration, spec imports track *the upstream API* — what your Bubble app, OpenAPI URL, or git repository actually exposed at each import.

Each import row shows:

* **Validation status** — `Valid`, `Invalid` (with the error), or `Pending`.
* **Spec summary** — the spec's title, endpoint count, and declared version.
* **No changes** — shown when a re-import produced an API identical to the previous import, so you can tell no-op re-imports apart from real changes at a glance.

Click **View changes** on an import to see what changed in the upstream API since the previous import:

* **Added endpoints** (`+`) — new in this import; their generated tools start disabled as always.
* **Removed endpoints** (`−`) — gone from the API. If any of your *enabled* tools reference a removed endpoint, a warning names them — those tools will fail at runtime until you disable them or the endpoint returns.
* **Changed endpoints** (`~`) — same method and path, but a different summary, input schema (parameters/body), output schema, or auth requirement. Input-schema changes list the affected properties.

Spec diffs compare the normalized endpoint definitions, so cosmetic reordering inside the raw document never registers as a change. Imports created before spec history was introduced cannot be diffed and do not offer **View changes**.

> **Storage note:** the full raw spec document is retained for the three most recent valid imports per server; older imports keep their normalized endpoint list (used for diffs) indefinitely.


# Custom domain

By default, your published MCP servers are served on FlowCP's host (`https://runtime.flowcp.io/mcp/<server-id>`). With a **custom domain** you can serve them on your own hostname instead — for example `https://mcp.yourcompany.com/mcp/<server-id>` — so the URL your customers configure in their AI client is fully branded as yours.

> This is different from the Bubble **Custom domain** setting, which points at your app's *upstream backend*. The custom domain here is the public **MCP endpoint** that AI clients connect to.

## How it works

You bring your own domain and point it at FlowCP with a CNAME record. FlowCP verifies you control the domain and provisions a TLS certificate for it automatically (via Cloudflare for SaaS). Once the domain is **verified** and its certificate is **active**, your app's published MCP servers are served on it.

* **One domain per app.** The domain is bound to an app and serves every published branch of that app under the same `/mcp/<server-id>` path.
* **Verified + active before it serves.** Until both ownership verification and certificate issuance complete, traffic continues on the default host. FlowCP never advertises your domain in OAuth metadata until it can terminate TLS.

## Add a custom domain

1. Open your app and go to **Settings**.
2. Find the **MCP server domain** card.
3. Enter your hostname (for example `mcp.yourcompany.com`) and click **Add domain**.

FlowCP shows the DNS records you need to publish:

* A **CNAME** record pointing your hostname at FlowCP's target host.
* One or more **ownership / certificate validation** records (a `TXT` record, or an HTTP token) when required.

## Publish the DNS records

Add the records shown in the card at your DNS provider. A typical setup is:

| Type  | Name                   | Value                 |
| ----- | ---------------------- | --------------------- |
| CNAME | `mcp.yourcompany.com`  | `mcp.flowcp.io`       |
| TXT   | `_cf-custom-hostname…` | *(value from FlowCP)* |

DNS propagation and certificate issuance usually take a few minutes. The card polls automatically while it's open, or you can click **Refresh status**. You don't have to keep the page open: a background job also re-checks still-provisioning domains periodically, so a domain that finishes verifying overnight flips to **Active** on its own.

## When it's active

Once the status badge reads **Active**:

* The card confirms the live host.
* The **Publish** page's config snippet (and the `auth.md` URL) automatically switch to your domain, so the JSON you hand to AI clients targets `https://mcp.yourcompany.com/mcp/<server-id>`.

Already-connected clients pointed at the default host keep working; new configs use the custom domain.

## Remove a custom domain

Click **Remove** on the card and confirm. Published servers immediately revert to the default host, and FlowCP de-registers the domain and its certificate. You can add the domain again later.

## Troubleshooting

* **Stuck on&#x20;*****Pending*****.** Confirm the CNAME and any validation records exactly match what the card shows. Some DNS providers flatten or proxy CNAMEs at the apex — use a subdomain like `mcp.` rather than the root domain.
* ***Failed*****&#x20;status.** Certificate provisioning failed (often a missing or proxied validation record). Fix the DNS records, then **Remove** and re-add the domain to retry.
* **That domain is reserved / already in use.** A hostname can be bound to only one workspace, and platform hosts can't be claimed. Pick a hostname you control.


# Agent-driven onboarding

FlowCP can be driven end-to-end by an AI coding agent (such as Claude Code). Instead of clicking through the dashboard, your agent connects an app, imports its API, enables tools, configures OAuth, publishes a server, and reads back the final MCP config — all through a hosted MCP server that exposes FlowCP's own control plane.

This works because FlowCP **dogfoods itself**: the platform's control-plane API is described by an OpenAPI document committed to the FlowCP GitHub repo as `openapi.json` (generated with `pnpm gen:openapi`), and that document is imported into FlowCP like any other app. The result is a published MCP server whose tools are the FlowCP operations themselves.

***

## How authentication works

The platform MCP server uses the standard MCP OAuth 2.1 flow (authorization code + PKCE, with dynamic client registration). Your AI client:

1. Connects to the platform MCP server URL.
2. Is redirected to FlowCP to authorize. You log in (or are already logged in to the dashboard) and approve.
3. Receives a workspace-scoped access token, which it presents on every tool call.

Every tool call acts **only** within the workspace bound to that token — an agent can never reach another workspace by changing a parameter.

***

## The tool workflow

Once connected, these tools are available to the agent (names match the operations in the repo's `openapi.json`):

| Tool                                   | What it does                                                         |
| -------------------------------------- | -------------------------------------------------------------------- |
| `get_context`                          | Show the user and workspace the token is bound to.                   |
| `connect_app`                          | Connect a new app (Bubble or generic OpenAPI).                       |
| `initiate_verification` / `verify_app` | Bubble ownership verification (see below).                           |
| `import_api`                           | Provision server branches and import the Swagger document.           |
| `get_import_status`                    | Poll an import until it is `valid`.                                  |
| `list_tools`                           | List the tools generated from the import.                            |
| `enable_tool`                          | Enable/disable or rename a generated tool.                           |
| `configure_oauth`                      | Set the OAuth provider config for the app.                           |
| `publish_server`                       | Publish the server (runs safety checks).                             |
| `get_mcp_config`                       | Return the hosted MCP server URL and a ready-to-paste client config. |
| `get_app_stats`                        | Read usage stats for an app (invocations, users, success/error).     |
| `list_app_logs`                        | List an app's execution logs, newest first, with optional filters.   |
| `list_skills` / `create_skill`         | List or author agent skills (SKILL.md playbooks) for a server.       |
| `update_skill` / `delete_skill`        | Enable/disable, edit, or remove a skill.                             |
| `list_widgets` / `create_widget`       | List or author interactive widgets for a server.                     |
| `update_widget` / `delete_widget`      | Enable/disable, edit, or remove a widget.                            |

A typical agent run looks like:

```
get_context
→ connect_app  (provider: "openapi", swaggerUrl: "https://my.api/openapi.json")
→ import_api   (then get_import_status until valid)
→ list_tools
→ enable_tool  (for each tool you want exposed)
→ configure_oauth
→ publish_server
→ get_mcp_config
```

### The one human-in-the-loop step

For **Bubble** apps, ownership must be proven before importing. The agent calls `initiate_verification` to get a token, relays the instructions to you, you publish the token in your Bubble app, and then the agent calls `verify_app`. **Generic OpenAPI apps skip this** — they are verified automatically when their Swagger URL is reachable, so the agent can go straight from `connect_app` to `import_api`.

### Default-deny still applies

Imported tools always start **disabled**. The agent must call `enable_tool` explicitly for each one. Enabling a destructive tool requires `requiresConfirmation: true`, otherwise `publish_server` will refuse to publish.

***

## Connecting the platform to itself

To create the platform MCP server in your own deployment, run the bootstrap script against a running API and an existing workspace:

```bash
PLATFORM_WORKSPACE_ID=<workspace-id> pnpm connect:platform
```

It connects the repo-hosted `openapi.json` (by default the raw GitHub URL) as an OpenAPI app, imports it, enables the agent tools, configures OAuth against the platform identity provider, publishes the server, and prints the MCP client config to paste into your AI client.

The relevant environment variables:

| Variable                                                    | Purpose                                                                          |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `API_PUBLIC_URL`                                            | Public origin of the control-plane API (serves `/id/*`).                         |
| `PLATFORM_OPENAPI_URL`                                      | URL of the repo-hosted `openapi.json` to import. Defaults to the raw GitHub URL. |
| `MCP_RUNTIME_PUBLIC_URL`                                    | Public origin of the MCP runtime (serves `/mcp/:serverId`).                      |
| `PLATFORM_OAUTH_CLIENT_ID` / `PLATFORM_OAUTH_CLIENT_SECRET` | Credentials the OAuth flow uses against the platform identity provider.          |
| `PLATFORM_TOKEN_TTL_S`                                      | Lifetime (seconds) of issued platform access tokens. Defaults to 3600.           |

Every published server also advertises how agents can authenticate to it via the [auth.md](/guides/agent-auth-discovery) discovery protocol.


# Agent authentication discovery (auth.md)

Any MCP server you publish on FlowCP can advertise how an AI agent can authenticate to it using [**auth.md**](https://github.com/workos/auth.md) — an open protocol that lets agents discover a service's authentication flows on behalf of a user. Discovery is built entirely on the OAuth standards FlowCP already implements.

Agent auth is **opt-in per server** and off by default, in keeping with FlowCP's default-deny model. Turn it on from the server's **Publish** page with the **Agent Auth (auth.md)** toggle. While it is off, FlowCP publishes no auth.md document and emits no `agent_auth` block — discovery stays silent.

***

## Enabling agent auth

1. Publish the server (the toggle appears once the server is live).
2. On the **Publish** page, switch on **Agent Auth (auth.md)**.
3. FlowCP shows the server's `auth.md` URL to copy and immediately begins advertising the discovery artifacts below. Toggle it off at any time to stop.

***

## What gets published

When agent auth is enabled for a server at `https://<runtime>/mcp/<serverId>`, FlowCP serves:

| Artifact                                 | URL                                                                     |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| auth.md file                             | `https://<runtime>/mcp/<serverId>/auth.md`                              |
| Protected Resource Metadata (RFC 9728)   | `https://<runtime>/.well-known/oauth-protected-resource/mcp/<serverId>` |
| Authorization Server Metadata (RFC 8414) | `https://<api>/.well-known/oauth-authorization-server/mcp/<serverId>`   |

`<serverId>` is whichever identifier the agent connected with — the server's friendly slug or its internal id. Each artifact is served under both, and the URLs inside it echo the one that was used, so an agent that validates the advertised resource against the URL it connected to sees a consistent chain either way.

The **auth.md file** is a short Markdown document that doubles as human documentation and a machine-readable runtime artifact. It lists the server's resource URL, OAuth endpoints, requested scopes, and enabled tools.

The machine source of truth is the `agent_auth` block carried in the Protected Resource Metadata (and mirrored into the Authorization Server Metadata). It advertises `skill` (the auth.md URL), the registration endpoints (`register_uri`, `claim_uri`, `revocation_uri`), and the supported identity, assertion, and credential types.

***

## The discovery chain

An agent reaches a server's auth.md the same way any MCP client discovers auth:

1. The agent calls `/mcp/<serverId>` and receives `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header.
2. It fetches the **Protected Resource Metadata**, which names the authorization server and carries the `agent_auth` block.
3. It follows `agent_auth.skill` to read `/mcp/<serverId>/auth.md`.

From there the agent authenticates with the standard OAuth 2.0 authorization-code flow (with PKCE) — exactly the [user-context OAuth](/security/user-context-oauth) FlowCP enforces on every call. See [Configure OAuth](/guides/configure-oauth) to set up the provider.

***

## Registration flows

Beyond discovery, FlowCP implements both auth.md agent-registration flows. The `agent_auth` block advertises only the flows that are actually live for a server, so an agent is never pointed at a flow that would fail:

| Identity type        | Flow                                                  | Advertised when                                                   |
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
| `anonymous`          | **User claimed** — device-style claim ceremony        | The server uses `user_oauth` mode with an OAuth config assigned   |
| `identity_assertion` | **Agent verified** — ID-JAG signed identity assertion | The workspace has at least one [trusted issuer](#trusted-issuers) |

### User claimed (`anonymous`)

A device-authorization-style ceremony for agents that have no registration of their own:

1. The agent `POST`s to `agent_auth.claim_uri` (`/oauth/agent/claim`) with a PKCE `code_challenge` and the server's `resource` URL. It receives a `device_code`, a human-readable `user_code` (`XXXX-XXXX`), a `verification_uri`, and a polling `interval`.
2. The agent shows the code to its user, who signs in to the FlowCP dashboard, opens the **Claim** page, enters the code, and reviews the consent card (which agent, which server). See [Claim an agent](/guides/claim-an-agent).
3. Approving routes the user through the app's **standard OAuth sign-in** — the same [user-context OAuth](/security/user-context-oauth) as every other flow — so the agent only ever acts in that user's authorization context.
4. The agent polls the token endpoint with `grant_type=urn:ietf:params:oauth:grant-type:device_code` (receiving `authorization_pending` / `slow_down` until the user finishes) and redeems the result with its PKCE `code_verifier`. The tokens it receives are exactly what the standard authorization-code flow would have produced.

### Agent verified (`identity_assertion`)

Agents that hold a signed JWT identity assertion (an **ID-JAG**, `urn:ietf:params:oauth:token-type:id-jag`) from an identity provider you trust can register directly:

1. The agent `POST`s to `agent_auth.register_uri` (`/oauth/agent/register`) with `identity_type=identity_assertion`, the assertion, and the server's `resource` URL.
2. FlowCP verifies the assertion strictly — signature against the issuer's JWKS, exact issuer and audience, expiry, single-use `jti`, and an optional per-issuer subject allowlist — and returns an RFC 7591-style client registration carrying the verified identity.
3. Registration establishes **identity, never access**: the registered client still obtains credentials through the standard authorization-code flow or the user-claimed ceremony above.

Every verification failure returns one generic `invalid_assertion` error, so the endpoint cannot be used to probe configuration.

### Trusted issuers

Configure trusted issuers per workspace under **Settings → Workspace → Trusted agent issuers**: a display name, the issuer URL (exact `iss` match), and its JWKS URL (both must be `https`). Optionally restrict which `sub` values may register. While the list is empty, `identity_assertion` is not advertised and agent-verified registration is refused.


# Claim an agent

When an AI agent wants to use one of your MCP servers but has no credentials of its own, it can start a **claim ceremony** (the auth.md *user claimed* flow): the agent shows you a short code, you approve it in the dashboard, and the agent receives credentials that act **in your authorization context** — never with shared credentials.

This flow is available on servers that have [agent auth discovery](/guides/agent-auth-discovery) enabled and use per-user OAuth.

***

## Approving a claim

1. Your agent displays a claim code like `BCDF-GHJK` and a link to the dashboard's **Claim** page (`/claim`). Opening the link pre-fills the code.
2. Sign in to FlowCP if you aren't already, and enter the code under **Claim an agent** → **Look up code**.
3. Review the consent card: it names the agent (if it identified itself) and the server and app it wants to access.
4. Choose **Approve and continue** to proceed through the app's own sign-in, or **Deny** to reject the request.
5. After you finish signing in, the page confirms **Agent connected**. The agent picks up its credentials automatically within a few seconds.

Claim codes expire after 15 minutes, can be used once, and grant nothing until you explicitly approve them. Denying a claim immediately tells the agent its request was rejected.

***

## Revoking access

The agent's access is the OAuth authorization you granted during the claim — revoke it the same way you revoke any app authorization: from the upstream app's own account/security settings.


# Connect to the FlowCP MCP server

FlowCP runs its own hosted MCP server so you can drive the whole platform from an AI client — connect an app, import its API, enable tools, configure OAuth, publish a server, and read back the final config — without clicking through the dashboard. This is the hosted counterpart to [Agent-driven onboarding](/guides/agent-driven-onboarding): instead of running the bootstrap script yourself, you point your AI client at the URL below.

**MCP server URL:**

```
https://mcp.flowcp.ai/mcp/cmq3c1f9100000ln8lg3jhqf4
```

The server uses the standard MCP OAuth 2.1 flow (authorization code + PKCE with dynamic client registration), so there is no API key to paste. On first use your client opens a browser, you log in to FlowCP and approve, and the client receives a workspace-scoped token it presents on every call. Every tool acts **only** within the workspace bound to that token.

***

## Add it to your AI client

The exact UI varies by client, but the config is the same — use the URL above with an HTTP transport.

### Claude Desktop

Edit `claude_desktop_config.json`:

| OS      | Path                                                              |
| ------- | ----------------------------------------------------------------- |
| macOS   | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json`                     |

```json
{
  "mcpServers": {
    "flowcp": {
      "url": "https://mcp.flowcp.ai/mcp/cmq3c1f9100000ln8lg3jhqf4",
      "type": "http"
    }
  }
}
```

Quit and relaunch Claude Desktop. On the first tool call you'll be prompted to authorize.

### Cursor

Create or edit `.cursor/mcp.json` (project-level) or the global `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "flowcp": {
      "url": "https://mcp.flowcp.ai/mcp/cmq3c1f9100000ln8lg3jhqf4",
      "transport": "http"
    }
  }
}
```

Restart Cursor after saving.

### Claude Code

```bash
claude mcp add --transport http flowcp https://mcp.flowcp.ai/mcp/cmq3c1f9100000ln8lg3jhqf4
```

### ChatGPT

In the ChatGPT desktop app: **Settings → Tools & integrations → Add MCP server**, then enter the URL above with the **Streamable HTTP** transport.

***

## Using it

Once connected, ask your agent to build an MCP server in plain language — for example:

> Connect my OpenAPI app at `https://my.api/openapi.json`, enable the read-only tools, and publish it.

The agent calls the FlowCP tools to carry that out. The available tools mirror FlowCP's control-plane operations:

| Tool                                   | What it does                                                           |
| -------------------------------------- | ---------------------------------------------------------------------- |
| `get_context`                          | Show the user and workspace the token is bound to.                     |
| `connect_app`                          | Connect a new app (Bubble or generic OpenAPI).                         |
| `initiate_verification` / `verify_app` | Bubble ownership verification.                                         |
| `import_api`                           | Provision server branches and import the Swagger document.             |
| `get_import_status`                    | Poll an import until it is `valid`.                                    |
| `list_tools`                           | List the tools generated from the import.                              |
| `enable_tool`                          | Enable/disable or rename a generated tool.                             |
| `configure_oauth`                      | Set the OAuth provider config for the app.                             |
| `publish_server`                       | Publish the server (runs safety checks).                               |
| `get_mcp_config`                       | Return the hosted MCP server URL and a ready-to-paste client config.   |
| `get_app_stats`                        | Read usage stats for an app (invocations, users, success/error rates). |
| `list_app_logs`                        | List an app's execution logs, newest first, with optional filters.     |

A typical run:

```
get_context
→ connect_app  (provider: "openapi", swaggerUrl: "https://my.api/openapi.json")
→ import_api   (then get_import_status until valid)
→ list_tools
→ enable_tool  (for each tool you want exposed)
→ configure_oauth
→ publish_server
→ get_mcp_config
```

**Default-deny still applies:** imported tools always start disabled, so the agent must `enable_tool` each one explicitly. For Bubble apps there is one human-in-the-loop step — ownership verification — described in [Agent-driven onboarding](/guides/agent-driven-onboarding).

***

## Verifying the connection

Look for the FlowCP tools in your client's tools list (e.g. the wrench icon in Claude Desktop). If the server doesn't appear:

1. Confirm the URL exactly matches the one above.
2. Complete the OAuth prompt — tools stay hidden until the client holds a token.
3. Restart the AI client; most clients only reload MCP config on startup.

See [Troubleshooting](/support/troubleshooting) for more.


# Integrate with AI clients

Once your MCP server is published, you connect it to an AI client by adding a small JSON config. This page covers setup for Claude Desktop, Cursor, ChatGPT, Grok, and VS Code.

The config snippet with your exact server URL is on the **Distribute → MCP clients** page in FlowCP — copy it from there rather than constructing it manually. The page shows the active branch's URL (production once deployed, otherwise the development URL) and a copy-ready config per client. Each client tab is self-contained: config-file clients show a copyable JSON snippet, and URL-based clients (such as ChatGPT) show the server URL with its own copy button.

***

## Claude Desktop

Claude Desktop reads MCP server config from `claude_desktop_config.json`.

**Location:**

| OS      | Path                                                              |
| ------- | ----------------------------------------------------------------- |
| macOS   | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json`                     |

**Add the snippet:**

```json
{
  "mcpServers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "type": "http"
    }
  }
}
```

If `claude_desktop_config.json` does not exist, create it. If it already contains an `mcpServers` key, add the new entry inside that object.

After saving, quit and relaunch Claude Desktop. Your tools appear in the tools panel (the wrench icon in the message composer).

***

## Cursor

Cursor supports MCP servers via a project-level or global config file.

**Project-level** (applies only to the current workspace):

Create or edit `.cursor/mcp.json` in your project root:

```json
{
  "mcpServers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "transport": "http"
    }
  }
}
```

**Global** (applies to all Cursor projects):

Open Cursor Settings → MCP and add the server from the UI, or edit the global `mcp.json` at:

| OS      | Path                             |
| ------- | -------------------------------- |
| macOS   | `~/.cursor/mcp.json`             |
| Windows | `%USERPROFILE%\.cursor\mcp.json` |

Restart Cursor after saving. Your tools appear in Cursor's Composer panel when the MCP server is connected.

***

## ChatGPT

The ChatGPT desktop app (macOS and Windows) supports MCP servers.

Open ChatGPT desktop app → Settings → Tools & integrations → Add MCP server, and enter:

* **Name**: `my-app` (or any label)
* **URL**: `https://runtime.flowcp.io/mcp/<server-id>`
* **Transport**: Streamable HTTP

Or paste the JSON snippet directly if the app offers a JSON import option:

```json
{
  "servers": [
    {
      "name": "my-bubble-app",
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "transport": "streamable-http"
    }
  ]
}
```

***

## Grok (xAI)

Grok connects to a FlowCP server over Streamable HTTP and discovers OAuth on its own — there is no client ID or secret to create by hand. Add the server from Grok's custom MCP connector settings and enter:

* **Name**: `my-app` (or any label)
* **URL**: `https://runtime.flowcp.io/mcp/<server-slug>`

On the first call Grok reads FlowCP's protected-resource metadata, registers itself dynamically (RFC 7591), and runs the authorization-code flow with PKCE. The user signs in with your API's provider and Grok stores the resulting token.

Grok checks that the resource FlowCP advertises matches the URL you entered, so paste the URL exactly as the **Deploy** page shows it — either the slug URL or the internal-id URL works, but do not mix one with the other.

***

## VS Code

VS Code (with GitHub Copilot agent mode) supports MCP servers via an `mcp.json` config file.

Run **MCP: Add Server** from the Command Palette, or create `.vscode/mcp.json` in your project:

```json
{
  "servers": {
    "my-app": {
      "url": "https://runtime.flowcp.io/mcp/<server-id>",
      "type": "http"
    }
  }
}
```

Start the server from the MCP view, then use its tools from Copilot Chat in agent mode.

***

## End-user OAuth on first use

When an end user calls a FlowCP tool for the first time from an AI client, FlowCP checks whether it has a valid access token for that user. If not, the response includes an authorization URL. The AI client typically surfaces this as a prompt: "Please authorize access to your account."

The user clicks the link, logs in with your API's provider, and grants access. After that, all subsequent tool calls from that client use their access token automatically.

Clients that support it discover all of this automatically, with no client ID or secret to configure: the first unauthenticated call returns a `WWW-Authenticate` challenge naming the server's protected-resource metadata, the client registers itself (RFC 7591), and it runs the authorization-code flow with PKCE. The challenge names the resource under the same URL you connected with, so a client that validates the two match — as Grok does — accepts either the slug URL or the internal-id URL.

***

## Verifying the connection

In the AI client, look for your tools in the tools list (exact UI varies by client). If the server does not appear:

1. Confirm the server status is **published** on the FlowCP Deploy page.
2. Check that the URL in the config points to the MCP runtime (e.g. `runtime.flowcp.io`), not the dashboard.
3. Restart the AI client — most clients only reload MCP config on startup.
4. If the client reports a resource mismatch or an invalid `client_id`, see [Troubleshooting → OAuth errors](/support/troubleshooting).

See [Troubleshooting](/support/troubleshooting) for more.


# Use FlowCP in GitHub Actions

You can drive FlowCP from a GitHub Actions workflow with the `flowcp` CLI: re-import your OpenAPI spec when it changes, run the MCP capability test suite, publish a server, or sync skills, prompts, and widgets from your repo — all on every push or pull request.

> Looking for the zero-config option? If you connect your app as a **Git repository** with the FlowCP GitHub App, FlowCP automatically re-imports your spec when a pull request merges — no workflow required. See [Connect your API](/guides/connect-your-api). Use the CLI approach below when you want CI to also **test** and **publish**, or to gate publishing on your own checks.

## 1. Create an API token

The browser device-code login (`flowcp login`) can't run on a CI runner, so authenticate with a long-lived **API token** instead. On your machine:

```bash
flowcp login                       # one time, interactive
flowcp token create --name "github-actions"
```

Copy the token (`flowcp_pat_…`) — it's shown only once.

## 2. Store it as a repository secret

In your GitHub repo: **Settings → Secrets and variables → Actions → New repository secret**. Name it `FLOWCP_TOKEN` and paste the token.

## 3. Add a workflow

Use the bundled composite action:

```yaml
# .github/workflows/flowcp.yml
name: FlowCP

on:
  push:
    branches: [main]
    paths: ['openapi.yaml'] # run when your spec changes

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Re-import the OpenAPI spec
        uses: inovastudio/mcp-builder/.github/actions/flowcp@main
        with:
          api-token: ${{ secrets.FLOWCP_TOKEN }}
          args: import run --app app_123

      - name: Run the MCP capability tests
        uses: inovastudio/mcp-builder/.github/actions/flowcp@main
        with:
          api-token: ${{ secrets.FLOWCP_TOKEN }}
          args: server test --server srv_123

      - name: Publish
        uses: inovastudio/mcp-builder/.github/actions/flowcp@main
        with:
          api-token: ${{ secrets.FLOWCP_TOKEN }}
          args: server publish --server srv_123
```

### Action inputs

| Input          | Required | Default                 | Description                                                     |
| -------------- | -------- | ----------------------- | --------------------------------------------------------------- |
| `args`         | yes      | —                       | Arguments passed to `flowcp` (e.g. `import run --app app_123`). |
| `api-token`    | yes      | —                       | Your `FLOWCP_TOKEN` secret.                                     |
| `api-url`      | no       | `https://api.flowcp.ai` | FlowCP API base URL.                                            |
| `workspace-id` | no       | resolved from token     | Workspace id, if you prefer to set it explicitly.               |
| `version`      | no       | `latest`                | The `flowcp` npm version to install.                            |

## Without the composite action

If you'd rather not depend on the action, install the CLI directly and use the environment variables:

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: 20
- run: npm install -g flowcp
- run: flowcp import run --app app_123 && flowcp server test --server srv_123
  env:
    FLOWCP_TOKEN: ${{ secrets.FLOWCP_TOKEN }}
```

`FLOWCP_TOKEN` authenticates every command; `FLOWCP_API_URL` and `FLOWCP_WORKSPACE_ID` are optional overrides.

## Useful commands in CI

| Command                               | Use                                                       |
| ------------------------------------- | --------------------------------------------------------- |
| `flowcp import run --app <id>`        | Re-import the OpenAPI document after a spec change        |
| `flowcp server test --server <id>`    | Run the MCP capability test suite (gate publishing on it) |
| `flowcp server publish --server <id>` | Make the MCP endpoint live                                |
| `flowcp sync push --prune`            | Upload local skills, prompts, and widgets to the server   |

Add `--json` to any command for machine-readable output you can pipe into `jq`.

## Security notes

* Treat `FLOWCP_TOKEN` like a password: keep it in GitHub Secrets, never in the workflow file. It is stored hashed on the server.
* A token is scoped to the workspace and user that created it.
* Rotate or revoke at any time with `flowcp token list` / `flowcp token revoke <id>`. Revocation takes effect immediately.
* Set an expiry with `flowcp token create --name ci --expires-in 90` to force periodic rotation.


# Workspace settings

The **Settings** page lets you manage your profile, connect external accounts, rename your workspace, and control who has access.

## Connected accounts

The **Connected accounts** card lets you link your Google account so you can sign in with Google in addition to (or instead of) your email and password.

### Connecting Google

1. Go to **Settings** in the sidebar.
2. In the **Connected accounts** card, click **Connect Google**.
3. Complete the Google sign-in flow. You are redirected back to Settings when done.

### Disconnecting Google

Click **Disconnect** next to Google in the **Connected accounts** card.

> **Note:** You can only disconnect Google if your account has a password set. This prevents being locked out of your account. If you signed up with Google and have no password, use the "Forgot password" flow on the login page to set one first.

## Two-factor authentication (2FA)

Protect email/password sign-in with a time-based code from an authenticator app (Google Authenticator, 1Password, Authy, etc.).

1. Go to **Settings → Account**.
2. In the **Two-factor authentication** card, click **Set up authenticator app**.
3. Scan the QR code with your authenticator app (or enter the setup key manually), then enter the 6-digit code and click **Enable 2FA**.

On your next email/password login, enter your password first, then the code from your authenticator app.

> **Note:** 2FA applies to email/password sign-in only. Google sign-in is unchanged. You need a password on your account before you can enable 2FA.

To turn 2FA off, enter your current authenticator code and your account password (if set), then click **Disable 2FA**.

***

## Renaming your workspace

1. Go to **Settings** in the sidebar.
2. In the **Workspace** card, edit the **Workspace name** field.
3. Click **Save**.

Only workspace owners can rename the workspace. Members see the name as read-only.

## Trusted agent issuers

The **Workspace** tab also lists **Trusted agent issuers** — identity providers whose signed ID-JAG assertions may register agents against this workspace's servers with [agent auth discovery](/guides/agent-auth-discovery) enabled. Add an issuer with a display name, its issuer URL, and its JWKS URL (both `https`); remove one at any time to stop accepting its assertions. While the list is empty, only the user-claimed flow ([Claim an agent](/guides/claim-an-agent)) is advertised to agents.

## Managing workspaces

You can belong to multiple workspaces. Use the workspace switcher at the top of the sidebar to see all workspaces, switch between them, or create a new one.

### Switching workspaces

Click the workspace name at the top of the sidebar, then select the workspace you want to switch to. The page reloads with the selected workspace active and all apps, tools, and settings scoped to that workspace.

### Creating a workspace

1. Click the workspace name at the top of the sidebar.
2. Click **Create workspace** at the bottom of the dropdown.
3. Enter a name and click **Create workspace**.

After creation you are automatically switched to the new workspace.

## Managing members

Use the **Workspace members** card on the Settings page to invite teammates, change their roles, or remove them.

* Only owners can add, remove, or change the role of other members.
* At least one owner must remain in every workspace.
* You can invite someone who does not have a FlowCP account yet. They appear as **Pending** until they sign up using the link in their invitation email.
* Pending invitations can be cancelled with **Cancel invite** before the invitee accepts.

### Inviting someone new to FlowCP

1. Go to **Settings → Members**.
2. Enter their email address and click **Add member**.
3. They receive an email with a **Create account** link.
4. After signup, they join your workspace directly — no separate workspace is created for them.

For details see [Connect your API](/guides/connect-your-api) to learn about sharing a workspace with your team after it is set up.


# Get support

Need help? You can reach the FlowCP team directly from inside the dashboard — no need to leave the app or find an email address.

## Sending a support request

1. Click your account avatar at the bottom of the sidebar to open the account menu.
2. Choose **Support** (it sits right under **Documentation**).
3. In the **Contact support** popup, fill in:
   * **Category** — General question, Bug or issue, Billing, or Other.
   * **Subject** — a short summary of what you need.
   * **How can we help?** — the details of your question or issue.
4. Optionally attach a screenshot or screen recording (see below).
5. Click **Send request**.

Your message is emailed to the FlowCP support team. We reply to the email address on your account, so there's nothing else to fill in.

## Attaching a screenshot or video

A picture often explains an issue faster than words. In the **Contact support** popup, click **Add image or video** and pick a file.

* Only **images and video** are accepted.
* Files must be **10 MB or smaller**.
* The file is sent along with your request as an email attachment — it is not stored anywhere else.

To swap the file, remove it with the **✕** button and add another.

> **Note:** If sending fails, you'll see an inline error in the popup. Check your connection and try again, or email us directly at <support@flowcp.ai>.


# User-context OAuth

Every MCP tool call in FlowCP runs under the OAuth token of the specific end user making the request — issued by your API's own OAuth provider, not a shared admin or service account token.

## Why this matters

Most API integrations use a single admin token to act on behalf of all users. This creates problems:

* **Overprivileged access.** An admin token typically has access to all user data. A bug or misconfiguration exposes data it should not.
* **No audit trail.** Your API's logs show the admin user, not the actual end user who triggered the action.
* **Privacy violations.** One user can inadvertently access another user's data if the integration is not carefully scoped.

FlowCP's user-context model avoids all of these. Each request carries the calling user's own token. Your API enforces its own privacy rules and permission scopes as normal — FlowCP is a transparent proxy.

## How it works

FlowCP uses a **stateless OAuth proxy** model. Tokens are never stored on FlowCP's servers — the AI client holds the access token and presents it on every request.

1. **First call.** When an end user's AI client connects to FlowCP, it has no token yet. FlowCP returns an OAuth challenge (`WWW-Authenticate` header). The AI client automatically initiates the OAuth authorization flow.
2. **Authorization.** FlowCP redirects the user to your provider's authorization page. The user logs in and grants access.
3. **Token handoff.** Your provider redirects back to FlowCP with an authorization code. FlowCP exchanges it for tokens, packages them in a short-lived encrypted auth code, and redirects the AI client back. The AI client completes the PKCE exchange and receives the access token directly.
4. **Token use.** On every tool call, the AI client sends its access token as a `Bearer` header. FlowCP validates and forwards it to your API — no database lookup required.
5. **Re-authorization.** When the token expires, your API returns 401. FlowCP propagates this as an HTTP 401 with a `WWW-Authenticate` header. Compliant AI clients automatically restart the OAuth flow to obtain a fresh token.
6. **Fail closed.** If the AI client presents an invalid or expired token and does not re-authorize, tool calls are rejected. FlowCP never falls back to an admin token.

## What FlowCP never does

* Stores access tokens or refresh tokens in its database
* Uses a shared admin or service account token for user requests
* Falls back to any other credential when a user's token is unavailable
* Logs access tokens or Authorization headers
* Allows one workspace's users to see another workspace's connections

## The Playground uses the same flow

The [Playground](/guides/chat-test-your-mcp-server) is just another OAuth client. When you click **Connect account**, it runs the exact same stateless proxy flow described above — so testing in the Playground also validates your OAuth configuration. The resulting token lives only in your browser for that session and is never persisted, just like a real AI client's token.

## Revoking access

Users can revoke FlowCP's access from their account with your provider (for Bubble, Settings → Connected apps). Once revoked, the next tool call will fail and the AI client will prompt the user to re-authorize.

## When user-context applies

User-context OAuth is the default and recommended mode, and everything above applies whenever a branch's [Authentication](/guides/configure-oauth#authentication-modes) is set to **User OAuth**.

For APIs that don't offer per-user OAuth, a branch can instead use a **shared-credential** mode — **API key**, **Static bearer**, or **None**. These are a deliberate trade-off: a single workspace-configured credential (or no credential) is used for every end user, so the per-user guarantees above — per-user scoping, per-user audit attribution, and "never fall back to a shared credential" — **do not apply** to those branches. FlowCP still encrypts the static credential at rest, keeps it out of logs and caches, and enforces every other guardrail (default-deny tools, confirmation on destructive actions, rate limits, audit logging, and publish blockers). Choose a shared-credential mode only when user-context OAuth isn't an option.


# Default-deny model

FlowCP follows a default-deny principle: every tool is disabled when first created and must be explicitly enabled before AI clients can invoke it.

## Why default deny?

When FlowCP imports your Swagger/OpenAPI spec, it may find dozens or hundreds of endpoints. Automatically enabling all of them would expose every API operation to AI clients — including internal admin endpoints, destructive operations, or endpoints that are not intended for external access.

Default deny means:

* You review and consciously enable only the endpoints you want AI clients to use
* A newly imported endpoint can never be invoked until a human turns it on
* Adding a new endpoint to your API does not automatically expose it — it starts disabled and waits for review

## How it applies

| Event                                                    | Default state                                           |
| -------------------------------------------------------- | ------------------------------------------------------- |
| New import — endpoint discovered for the first time      | Disabled                                                |
| Re-import — existing endpoint already customized         | Unchanged (your settings are preserved)                 |
| Re-import — new endpoint found that did not exist before | Disabled                                                |
| `DELETE` endpoint                                        | Disabled, with **Require confirmation** defaulted to on |
| New [skill](/guides/offer-agent-skills) authored         | Disabled until you turn it on                           |

## Enabling tools

Enable a tool from the **Tools** tab by toggling the switch on its row. You can enable multiple tools at once using the toolbar's bulk controls.

## Workspace isolation

The default-deny model is enforced at the workspace level. A tool in workspace A can never be invoked by a request authenticated to workspace B. Every storage lookup in FlowCP is scoped to the requesting workspace — this is not just a UI concern but is enforced in every database query.

## Runtime enforcement

Even if a tool's database record is marked enabled, the runtime re-validates the state on every incoming request. If a tool is disabled between requests, the next call is rejected without making an upstream API call.


# Destructive action guards

FlowCP applies extra protection to `DELETE` endpoints to prevent accidental or unintended data deletion through AI tool calls.

## What counts as destructive

Any endpoint using the HTTP `DELETE` method is classified as `destructive`. FlowCP applies this classification automatically during import — you cannot change a tool's risk level manually.

## Two-layer protection

### Layer 1 — Disabled by default

Destructive tools start disabled, just like all other tools. You must explicitly enable them.

### Layer 2 — Require confirmation

When a destructive tool is enabled, it must also have **Require confirmation** turned on. This flag signals to the MCP client that the user must confirm the action before it is executed. Most MCP-compatible AI clients (Claude Desktop, Cursor, etc.) surface this as a confirmation dialog.

## Publish blocker

If any enabled destructive tool has **Require confirmation** turned off, the **Publish Server** button is blocked. The publish page lists the offending tools by name. You must either:

* Turn on **Require confirmation** for each listed tool, or
* Disable the tool

Publishing is not possible while any enabled destructive tool lacks confirmation enforcement.

## Runtime enforcement

At runtime, FlowCP checks the `requiresConfirmation` flag on every destructive tool call. If the MCP client did not include the required confirmation signal, the call is rejected before any upstream API request is made.

## Recommendation

Enable destructive tools only when you have a clear, deliberate use case for AI clients to delete data. Consider whether the same goal can be achieved with a `write`-level operation (e.g. soft-deleting via a PATCH that sets a `deleted` field) instead of a hard delete.


# Rate limiting & probe blocking

FlowCP's public services enforce rate limits at two layers: tight, purpose-built limits on sensitive routes (login, 2FA, OAuth, embed chat, per-tool invocation), and a coarse **edge guard** that sits in front of every request as an outer ceiling and a defense against scanner traffic.

## The edge guard

Both public services — the control-plane API and the MCP runtime — run a single hook before routing that does two things:

1. **Blocks exploit-probe paths.** Requests for paths the app never serves (`/wp-admin`, `/.env`, `/.git`, `/phpmyadmin`, `/actuator`, anything ending in `.php`/`.aspx`/`.cgi`, …) get an immediate, silent `404` — no route handler runs. Each probe is also counted against a per-IP budget, so a scanner sweeping many paths quickly trips the rate limiter. Legitimate OAuth/OIDC discovery under `/.well-known/` is never treated as a probe.
2. **Applies a category-aware rate-limit backstop.** Every other request is classified and held to a per-minute ceiling. These limits are deliberately generous — they sit *on top of* the stricter per-route limits and exist to stop floods and abuse, not to throttle normal use.

## Backstop limits

| Category | What it covers                                                                                                       | Limit (per minute) |
| -------- | -------------------------------------------------------------------------------------------------------------------- | ------------------ |
| Webhook  | `/webhooks/*` (Stripe, Git)                                                                                          | 120 per source IP  |
| AI       | Playground chat & tool calls, prompt/skill suggestions, description regeneration, embed chat, MCP runtime tool calls | 30                 |
| Query    | Read endpoints (`GET`)                                                                                               | 300                |
| Backstop | Everything else (mutations, anything unmatched)                                                                      | 600                |

Limits are scoped per workspace (or per embed) when the request path identifies a tenant, and otherwise per client IP. The real client IP is used even behind a TLS-terminating proxy or CDN.

Two paths are exempt from the backstop so health checks and client auto-discovery are never throttled:

* `GET /health`
* OAuth/OIDC discovery documents under `/.well-known/…`

## When you hit a limit

Exceeding a limit returns **HTTP 429** with the error code `RATE_LIMIT_EXCEEDED`. Wait for the current minute window to roll over and retry. If you regularly hit the AI or query ceilings during legitimate use, contact support — these are abuse ceilings, not plan quotas (plan quotas are billed separately and surface as `402`).


# FlowCP CLI

The FlowCP CLI (`flowcp`) lets you manage your hosted MCP servers from the terminal or your IDE: connect apps, import OpenAPI documents, enable tools, publish servers, tail execution logs, configure auth, and sync skills, prompts, and widgets as local files.

## Installation

Install from npm (Node.js 18 or newer):

```bash
npm install -g flowcp
flowcp --help
```

From a monorepo checkout (development builds):

```bash
pnpm install
pnpm --filter flowcp build
node apps/cli/dist/bin.js --help
```

You can link a development build onto your PATH with `pnpm --filter flowcp link --global`.

## Logging in

```bash
flowcp login
```

Login uses a browser device-code flow: the CLI prints a code and a dashboard URL (`/device`). Open the URL, sign in if needed, and approve the request — the CLI receives OAuth tokens scoped to your account and current workspace and stores them in `~/.flowcp/config.json` (file mode `0600`). No password ever passes through the terminal, and the CLI holds no client secret (it is a public OAuth client secured with PKCE).

* `flowcp whoami` shows the logged-in user and workspace.
* `flowcp workspace list` / `flowcp workspace switch <id>` change the active workspace.
* `flowcp logout` removes stored credentials.

The CLI talks to `https://api.flowcp.ai` by default. Point it at a different FlowCP instance (e.g. a local dev stack) with `--api-url <url>` or the `FLOWCP_API_URL` environment variable.

## Headless auth (CI / GitHub Actions)

The device-code flow needs a browser, so it isn't usable on a CI runner. Instead, create a long-lived **API token** and pass it via an environment variable:

```bash
flowcp token create --name "github-actions"
```

The full token is shown **once** — store it as a secret. Then, on any machine without `flowcp login`, set:

| Variable              | Purpose                                                        |
| --------------------- | -------------------------------------------------------------- |
| `FLOWCP_TOKEN`        | An API token (`flowcp_pat_…`). Authenticates every command.    |
| `FLOWCP_API_URL`      | API base URL (optional; defaults to `https://api.flowcp.ai`).  |
| `FLOWCP_WORKSPACE_ID` | Workspace id (optional; resolved from the token when omitted). |
| `FLOWCP_CONFIG_DIR`   | Override the config directory (default `~/.flowcp`).           |

```bash
export FLOWCP_TOKEN=flowcp_pat_xxxxxxxx
flowcp whoami
flowcp import run --app app_123
```

Manage tokens with `flowcp token list` and `flowcp token revoke <id>`. Tokens are stored hashed; revoke immediately stops a token from working. Tokens are scoped to the workspace and user that created them. See [Use FlowCP in GitHub Actions](/guides/github-actions) for a full workflow.

## The project file: `flowcp.json`

Most commands need an app or server id. Instead of passing `--app` / `--server` every time, run `flowcp sync init --app <appId> --server <serverId>` in your project directory to create a `flowcp.json`:

```json
{
  "appId": "app_...",
  "serverId": "srv_...",
  "workspaceId": "ws_...",
  "sync": {
    "skills": "Skills",
    "prompts": "Prompts",
    "widgets": "Widgets",
    "resources": "Resources",
    "docs": "Docs"
  }
}
```

The CLI finds the nearest `flowcp.json` by walking up from the current directory.

## Command reference

| Command                                                               | Description                                                                               |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `flowcp login` / `logout` / `whoami`                                  | Authenticate the CLI                                                                      |
| `flowcp token create --name <n>` / `list` / `revoke <id>`             | Manage API tokens for headless / CI use                                                   |
| `flowcp workspace list` / `switch <id>`                               | Manage the active workspace                                                               |
| `flowcp app list` / `get [appId]` / `connect`                         | Manage connected apps                                                                     |
| `flowcp import run [--app <id>]`                                      | Provision servers + import the OpenAPI doc (waits by default; `--no-watch` to queue only) |
| `flowcp import folder <dir> [--server <id>] [--prune]`                | Import a standard FlowCP folder's content into a server                                   |
| `flowcp import status <importId> [--watch]`                           | Check or watch an import                                                                  |
| `flowcp tools list` / `enable <toolId>` / `disable <toolId>`          | Manage generated tools                                                                    |
| `flowcp server publish` / `unpublish`                                 | Publish or unpublish a server                                                             |
| `flowcp server config`                                                | Print the MCP connection config (paste into an AI client)                                 |
| `flowcp server test`                                                  | Run the MCP capability test suite                                                         |
| `flowcp export [--server <id>] [--out <dir>]`                         | Export a published server as a self-hostable standard FlowCP folder (paid plans)          |
| `flowcp logs [--follow]`                                              | Show (or tail) tool execution logs                                                        |
| `flowcp invoke "<message>"`                                           | Run one playground chat turn against the server's tools                                   |
| `flowcp oauth get` / `set`                                            | Manage the app's user-context OAuth provider config                                       |
| `flowcp auth-config list` / `set` / `delete <id>`                     | Manage shared-credential auth (api\_key / bearer / none)                                  |
| `flowcp domains list` / `add <hostname>` / `get <id>` / `remove <id>` | Manage custom domains                                                                     |
| `flowcp sync init` / `pull` / `push [--prune]`                        | Sync skills, prompts, widgets, resources, and docs as local files                         |

Run `flowcp <command> --help` for the full option list of any command.

## A typical flow

```bash
flowcp login
flowcp app connect --name "My API" --provider openapi \
  --swagger-url https://api.example.com/openapi.json \
  --api-base-url https://api.example.com
# verify ownership in the dashboard, then:
flowcp import run --app <appId>
flowcp sync init --app <appId> --server <serverId>
flowcp tools list
flowcp tools enable <toolId>
flowcp server publish
flowcp server config
flowcp logs --follow
```

## Local sync for skills, prompts, widgets, resources, and docs

`flowcp sync pull` downloads the server's skills, prompts, resources, and [documentation pages](/guides/serve-product-docs) as Markdown files with a frontmatter block, and widgets as an `.html` file plus a `.json` metadata sidecar, into the [standard FlowCP folder](/guides/folder-format) directories:

```
Skills/lookup_order.md
Prompts/errors.md
Widgets/order_card.html
Widgets/order_card.json
Resources/catalog.md
Docs/overview.md
Docs/guides/webhooks.md
```

`Docs/` is the one directory that nests: a page's file path below `Docs/` is its slug, so `Docs/guides/webhooks.md` syncs to the page served at `docs://guides/webhooks`.

Edit them locally (with your IDE and version control), then `flowcp sync push` to upload changes. New files are created, changed files are updated, and `--prune` deletes server-side items that no longer have a local file.

## Exporting a self-hostable folder

On a paid plan you can export a **published** server as a portable [standard FlowCP folder](/guides/folder-format) and run it on your own infrastructure with the FlowCP standalone runtime image — no FlowCP connection, Postgres, or Redis required:

```bash
flowcp export --server <serverId> --out ./my-server
cd my-server
cp .env.example .env   # fill in FLOWCP_UPSTREAM_SECRET and any other values
docker compose up
```

The folder is language-agnostic (`config.yaml` + per-item files + your OpenAPI document) and never contains secrets — you supply your upstream credential via the environment at run time. To import a folder's content into another server, use `flowcp import folder <dir>`. See [Export & self-host your MCP server](/guides/self-host-export).

A skill file looks like:

```markdown
---
description: 'Look up an order and summarize it'
enabled: true
---

# Lookup order

1. Call `get_order` with the order id...
```

## Scripting with `--json`

Every command accepts `--json` and prints the raw API payload, suitable for piping into `jq`:

```bash
flowcp tools list --json | jq -r '.tools[] | select(.enabled) | .name'
```

Exit codes: `0` success, `1` generic error, `2` usage/validation error, `3` authentication required, `4` not found.


# Tool risk levels

Every MCP tool in FlowCP is automatically classified into one of three risk levels based on the HTTP method of the underlying API endpoint.

## Levels

### `read`

HTTP methods: `GET`

Safe, non-mutating calls that retrieve data. No data is created, modified, or deleted. Read tools have no special restrictions beyond the default-deny model.

**Examples:** Fetch a user profile, list records, search data.

### `write`

HTTP methods: `POST`, `PATCH`, `PUT`

Calls that create or modify data. Write tools are disabled by default and must be explicitly enabled. Once enabled, no additional confirmation is required.

**Examples:** Create a record, update a field, submit a form.

### `destructive`

HTTP methods: `DELETE`

Calls that permanently remove data. Destructive tools have two extra guards on top of the standard default-deny:

1. Must have **Require confirmation** turned on to be published
2. The MCP runtime enforces the confirmation requirement on every call

**Examples:** Delete a record, remove a file, cancel a subscription.

## Classification rules

| HTTP Method | Risk Level    |
| ----------- | ------------- |
| `GET`       | `read`        |
| `POST`      | `write`       |
| `PATCH`     | `write`       |
| `PUT`       | `write`       |
| `DELETE`    | `destructive` |

Classification is applied automatically during import and cannot be changed manually. If your API exposes a `POST` endpoint that internally deletes data, FlowCP classifies it as `write` because FlowCP cannot inspect what the endpoint does internally.

## How risk levels are shown

In the Tools list, each tool row displays a colored badge:

* **Blue** — `read`
* **Yellow** — `write`
* **Red** — `destructive`

The stat card at the top of the Tools page shows a count of destructive tools, so you can quickly assess exposure.


# Account usage

The **Usage** tab under **Settings** shows how much of FlowCP your account is using. The figures are rolled up across **every workspace you belong to**, not just the workspace you are currently viewing.

## Metrics

| Metric                   | What it counts                                                                                     |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| **Workspaces**           | The number of workspaces you are a member of.                                                      |
| **Apps**                 | All connected apps across those workspaces.                                                        |
| **Published servers**    | MCP servers whose status is **published** across those workspaces.                                 |
| **MCP calls this month** | Tool invocations recorded since the start of the current calendar month (UTC).                     |
| **AI usages**            | AI suggestion calls — skill suggestions, prompt suggestions, and auto-generated tool descriptions. |

## Notes

* **MCP calls** reset at the start of each calendar month. For per-tool and per-agent breakdowns of MCP traffic, see [Usage stats](/reference/usage-stats) and [Execution logs](/reference/execution-logs), which are scoped to a single app.
* **AI usages** counts the AI *suggestion* features only. The [Playground](/guides/chat-test-your-mcp-server) is a developer testing surface and is deliberately **not** included.
* AI usages begin accumulating from the moment the feature is released; calls made before then are not counted retroactively.


# Billing & plans

The **Billing** tab under **Settings** shows your workspace's current plan, how much of your included usage you've consumed this period, a breakdown of calls per app, and the full list of plans you can compare against.

Billing is **per workspace** (unlike the account-wide [Usage](/reference/account-usage) tab).

## Plans

| Plan         | Price/mo | Included calls/mo | Published apps | Overage (per 10k) | Quota behavior     |
| ------------ | -------: | ----------------: | -------------: | ----------------: | ------------------ |
| **Free**     |       $0 |             1,000 |              1 |                 — | Hard cap           |
| **Pro**      |      $39 |            50,000 |              3 |             $1.50 | Soft cap + overage |
| **Team**     |     $149 |           250,000 |             15 |             $1.00 | Soft cap + overage |
| **Business** |     $499 |         1,500,000 |      Unlimited |             $0.60 | Soft cap + overage |

New workspaces start on **Free**. Your current plan is highlighted in the plan list on the Billing tab, and is also shown in the sidebar footer beneath your name for quick reference.

The same tiers are summarized on the public [pricing page](https://flowcp.ai/pricing), which anyone can view without signing in.

## What you're charged for

* **Successful tool calls** are the unit of usage. A call counts only when it succeeds — errors and calls rejected for being over quota are **never** counted.
* **Published apps** are the packaging unit. Each plan caps how many apps you can have published at once; connecting and importing draft apps is unrestricted.

## Current-period usage

The summary at the top of the tab shows, for the current calendar month (UTC):

| Field                 | Meaning                                                               |
| --------------------- | --------------------------------------------------------------------- |
| **Included calls**    | Successful calls used vs. the plan's monthly allowance, with a % bar. |
| **Remaining**         | Included calls not yet consumed this period.                          |
| **Overage calls**     | Successful calls beyond the included allowance (paid plans only).     |
| **Est. overage cost** | Estimated overage charge for the period, priced per 10k calls.        |

Usage resets at the start of each calendar month.

## Quota behavior

* **Hard cap (Free):** once you reach the included allowance, further tool calls are rejected until the next period or an upgrade.
* **Soft cap (paid):** calls beyond the allowance keep working and accrue as overage, billed at the plan's per-10k rate.

## Usage notifications

Workspace **owners** are emailed automatically as usage approaches the plan's included volume:

* **80% used** — a heads-up that you're approaching the included allowance.
* **100% used** — on a hard-cap plan, calls are paused until the next period or an upgrade; on a soft-cap plan, overage billing has started (the email includes the rate and the estimated charge so far).
* **Safety cap reached** — if you've set a hard ceiling on a soft-cap plan and it's hit, calls are paused until you raise or remove the cap.

Each notification is sent **at most once per billing period** and resets when a new period starts (or when you change plans mid-period).

## Payment failures

If a renewal payment fails, the workspace is marked **past due** and we email the workspace **owners** a notice with a link to update the card. Stripe keeps retrying for about two weeks:

* **Included calls keep working** the whole time — a failed payment never cuts off access you've already paid for.
* **Overage is paused.** On soft-cap plans, calls beyond your included volume are rejected while the account is past due, so an unpaid workspace doesn't keep accruing charges. The Billing tab shows a banner explaining this.
* **Recovery is automatic.** As soon as a retry succeeds — or you update the card in the Billing Portal — full access, including overage, is restored and we email a confirmation.
* If every retry fails, Stripe cancels the subscription and the workspace reverts to **Free** at the end of the period.

## Refunds

Refunds issued from the Stripe Billing Portal or dashboard are recorded and shown in your **Billing history**. Workspace owners receive a confirmation email when a refund is processed.

## Billing history

The Billing tab lists recent money-path events — failed and recovered payments, refunds, and apps unpublished after a downgrade — newest first, so you have an at-a-glance record of account activity.

## Calls by app

The Billing tab lists successful calls per connected app for the current period, so you can see which apps are driving your usage. For per-tool and per-agent detail, see [Usage stats](/reference/usage-stats) and [Execution logs](/reference/execution-logs).

## Changing plans

* **Upgrade** — on the Free plan, each paid tier shows an **Upgrade** button that opens Stripe Checkout. After payment you're returned to the Billing tab and your new allowance, app limit, and overage rate take effect.
* **Manage / change / cancel** — on a paid plan, **Manage billing** (and **Change plan** on the tier cards) opens the **Stripe Billing Portal**, where you can switch tiers, update your payment method, view invoices, or cancel. Cancelling reverts the workspace to Free at the end of the period.

> **Downgrading to a smaller plan?** If you currently have more published apps than the new plan allows, the most recently published apps are automatically unpublished to fit the new limit, and we email you which ones. Re-publish them after upgrading again or unpublishing others first.

If billing isn't configured for the deployment, the tiers show **Contact sales** instead.


# Plans & features

FlowCP ships the same four plans in both deployment modes — **Starter**, **Team**, **Business**, and **Enterprise** (keys: `starter`, `team`, `business`, `enterprise`). What changes between modes is how requests are metered and where the plan comes from; what stays the same is that **features are gated by plan**.

## Cloud vs Self-Hosted

| Aspect           | Cloud                                                         | Self-Hosted                                                       |
| ---------------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| Deployment       | Managed multi-tenant service ([flowcp.ai](https://flowcp.ai)) | You run it ([self-host the platform](/guides/self-host-platform)) |
| MCP requests     | **Metered** per plan (included volume + overage)              | **Unlimited** — no metered quota                                  |
| Plan source      | **Stripe subscription**                                       | Signed **license key** (`FLOWCP_LICENSE_KEY`)                     |
| No plan / no key | New workspaces start on **Starter**                           | No key → free **Starter** plan                                    |
| Plan management  | **Billing** tab (Stripe Checkout / Portal)                    | **License** panel (no Stripe config shipped)                      |

For Cloud request metering, included volumes, overage rates, and quota behavior, see [Billing & plans](/reference/billing-and-plans). Self-Hosted forgoes metering entirely — there is no included-call allowance or overage.

## Plan → feature matrix

Features unlock **cumulatively**: each plan includes everything below it plus its own additions.

| Plan           | Requests (Cloud / Self-Hosted)  | Premium features unlocked (cumulative)                                  |
| -------------- | ------------------------------- | ----------------------------------------------------------------------- |
| **Starter**    | Metered (free tier) / Unlimited | Core only                                                               |
| **Team**       | Metered / Unlimited             | + Environments + Version history + Conversation history + Log streaming |
| **Business**   | Metered / Unlimited             | + RBAC / advanced roles + Alerts & monitoring                           |
| **Enterprise** | Metered / Unlimited             | + Golden-scenario replay + Enterprise managed auth                      |

### Core (every plan, both modes)

Available on every plan in both Cloud and Self-Hosted:

* Connect / import APIs (Generic OpenAPI, Bubble, Git-backed sources)
* Tools, resources, prompts, widgets, and skills
* MCP serving and publishing
* User-context OAuth and all auth providers
* Playground
* Embeddable chat widget (chatting is core; **retaining** conversations is paid — see Team adds)
* Connector library
* CLI and GitHub Actions
* Per-server export folder
* Feedback
* Server tests
* Logs, health, and stats views

### Team adds

* **Environments** — separate dev/prod URLs per server.
* **Version history** — server-config version snapshots and rollback.
* **Chat-widget conversation history** — retain each visitor's transcript so threads resume across pages and visits, and read them in the dashboard. Without it the widget still chats, but stores nothing. See [Embed an AI chat widget](/guides/embed-ai-chat-widget#conversation-history).

  How long a conversation may be kept grows with the tier:

  | Plan       | Longest retention window |
  | ---------- | ------------------------ |
  | Starter    | Not retained             |
  | Team       | 30 days                  |
  | Business   | 60 days                  |
  | Enterprise | 180 days                 |

  A widget may be set to any window up to its plan's ceiling. The runtime purges against the shorter of the two, so moving to a lower tier shortens existing widgets' windows immediately, with no migration.
* **Log streaming** — forward MCP execution logs to your own observability system (Axiom, ClickHouse, OpenTelemetry, Datadog, Grafana Loki, Splunk, or any HTTP endpoint). See [Stream logs to external sinks](/guides/stream-logs).

### Business adds

* **RBAC / advanced roles** — finer-grained workspace roles.
* **Alerts & monitoring** — health alerts and monitoring.

### Enterprise adds

* **Golden-scenario replay** — recorded-scenario regression replay.

## Where the matrix is defined

The authoritative plan definitions — keys, ordering, and which feature each plan unlocks — live in code in **`PLAN_CONFIGS`** (`packages/shared/src/plans.ts`). Both modes read from the same config; the only difference is the plan **source** (Stripe subscription on Cloud vs. license key on Self-Hosted) and request metering (on for Cloud, off for Self-Hosted).

> **Custom domains** are not plan-gated. They depend on Cloudflare for SaaS being configured for the deployment (see [Custom domain](/guides/custom-domain)) and are available regardless of plan once that configuration is present.


# Agentic payments (x402)

This page describes how an **AI agent** pays for a monetized FlowCP tool. It is the consumer-side counterpart to the builder guide [Monetize your MCP server](/guides/monetize-your-mcp-server).

FlowCP implements the [x402](https://x402.org) protocol: a paid resource responds with the payment requirements, the client pays and retries with a signed proof, and the server settles the payment before serving the result. Because MCP tool calls ride JSON-RPC (not a plain HTTP GET), FlowCP carries the x402 exchange **inside the tool call** rather than as a raw HTTP 402.

## The flow

### 1. Discover the price

Call the built-in **`get_pricing`** tool. It returns everything the server charges in one read-only call, so you can budget a sequence of calls before committing to any of them:

```json
{
  "scheme": "x402",
  "network": "base-sepolia",
  "payTo": "0x…",
  "paidTools": [
    {
      "name": "create_invoice",
      "description": "Create an invoice",
      "category": "Billing",
      "price": "10000",
      "asset": "USDC",
      "priceUsd": "0.01"
    }
  ],
  "freeToolCount": 14,
  "truncated": false,
  "paymentField": "_payment",
  "requirementsMetaKey": "com.flowcp/x402"
}
```

`price` is in **atomic token units** (e.g. `10000` = 0.01 USDC at 6 decimals). `priceUsd` is a convenience rendering, and is `null` when the asset is not a USD-pegged stablecoin we recognize — in that case use `price` and `asset`.

Notes:

* **`get_pricing` exists only on a server that actually charges.** If the tool is absent, every tool on that server is free. It is itself always free to call.
* **Anything it does not list is free.** Free tools are reported as a `freeToolCount`, not enumerated — use `tools/list` for those.
* Pass `category` or `toolName` to narrow the result. If `truncated` is `true`, the list was capped and you should filter to see the rest.
* A server whose own API already has a tool named `get_pricing` keeps its own; the built-in is skipped rather than shadowing it. In that case fall back to the per-tool metadata below.

Each paid tool also advertises its own price in its MCP tool metadata under `com.flowcp/payment` — useful when your client surfaces `_meta`, and the fallback when `get_pricing` is unavailable:

```json
{
  "scheme": "x402",
  "price": "10000",
  "asset": "USDC",
  "network": "base-sepolia",
  "payTo": "0x…"
}
```

On a large server using **dynamic tool exposure**, tools are not registered until `search_tools` activates them — so they have no metadata to read yet. `search_tools` results carry `price` and `asset` for exactly this reason (`null` on a free tool), and `get_pricing` works regardless of exposure mode.

### 2. Call once, get the requirements

Call the tool normally. If you haven't attached a payment, the tool returns an **error result** whose `_meta` carries the full x402 requirements under `com.flowcp/x402`:

```json
{
  "x402Version": 1,
  "error": "Payment required to call 'create_invoice'.",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base-sepolia",
      "maxAmountRequired": "10000",
      "resource": "mcp://your-server/tools/create_invoice",
      "payTo": "0x…",
      "asset": "USDC",
      "maxTimeoutSeconds": 120
    }
  ]
}
```

### 3. Pay and retry

Construct a signed payment that satisfies `accepts[0]` and call the tool **again** with the proof in the reserved `_payment` field:

```json
{
  "<your normal tool arguments>": "…",
  "_payment": {
    "nonce": "<unique per payment>",
    "amount": "10000",
    "asset": "USDC",
    "network": "base-sepolia",
    "payTo": "0x…",
    "signature": "<authorization the facilitator verifies>"
  }
}
```

FlowCP verifies the proof with its payment facilitator, settles it, and then runs the tool and returns the normal result.

## Rules to code against

* **Pay before you get the result.** The tool does not run until payment is verified and settled. Treat the first (unpaid) call as a price quote.
* **One nonce, one call.** Each `nonce` settles at most once. Re-sending the same proof is rejected (`PAYMENT_REQUIRED` — "payment proof already used"). Use a fresh payment for every call.
* **Cover the price.** `_payment.amount` must be ≥ `maxAmountRequired`, and the `asset`, `network`, and `payTo` must match the requirement, or the call is rejected.
* **Fail closed.** A missing/invalid proof, an underpayment, or a facilitator error all reject the call — nothing runs and nothing is charged.
* **Resource reads can charge too.** An *endpoint resource* is backed by a tool. If that tool is priced, reading the resource goes through the same payment gate as calling the tool, and fails the same way without a proof.

## Errors

All payment failures surface as a tool error with code `PAYMENT_REQUIRED` (HTTP 402 semantics). When the failure is "no payment yet", the result `_meta` includes the `com.flowcp/x402` requirements so you can pay and retry.

## Facilitator

FlowCP settles through an x402 **facilitator**. There are three modes:

* **Mock** (default) — a built-in in-process facilitator, no real funds. Used by development and self-host runtimes when nothing else is configured.
* **Generic HTTP** — set `X402_FACILITATOR_URL` (and `X402_FACILITATOR_API_KEY` for a bearer credential) to point at any x402 facilitator's `/verify` + `/settle`.
* **Coinbase CDP** — set `X402_FACILITATOR_PROVIDER=cdp` plus `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET`. The runtime uses the official [Coinbase CDP facilitator](https://docs.cdp.coinbase.com/x402/core-concepts/facilitator) via the `@coinbase/x402` SDK (an optional dependency, lazily loaded). This is the real on-chain rail.

### Running a real CDP facilitator (testnet)

The CDP facilitator speaks **true x402**: the payment the agent presents must be a real signed x402 payload (an EIP-3009 authorization), not the simplified mock `_payment` shape. To bring the real rail up safely on `base-sepolia`:

1. Create CDP API keys at <https://portal.cdp.coinbase.com> and fund a testnet wallet from a [base-sepolia faucet](https://docs.base.org/tools/network-faucets).
2. Set `X402_FACILITATOR_PROVIDER=cdp`, `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`.
3. Capture a real x402 verify/settle request from an x402 client (the Coinbase x402 examples emit one) into a JSON file: `{ "paymentPayload": {…}, "paymentRequirements": {…} }`.
4. Run the verification script — it does exactly what the runtime's `CdpFacilitator` does and prints the on-chain settlement tx:

   ```bash
   CDP_API_KEY_ID=… CDP_API_KEY_SECRET=… pnpm verify:x402 ./payment-fixture.json
   # add --verify-only to check without settling
   ```

Once that prints a settlement tx, the runtime's CDP path is live. Note that FlowCP friendly network names (`base`, `base-sepolia`) are mapped to the CAIP-2 ids the SDK expects (`eip155:8453`, `eip155:84532`).

## Where the money goes

Payments **settle directly to the builder's payout address** — FlowCP never custodies funds. FlowCP's marketplace platform fee is **not** taken from the on-chain settlement; instead it is metered onto the builder's existing FlowCP subscription invoice (reported in whole USD cents to the `STRIPE_PLATFORM_FEE_METER_EVENT` meter, reconciled hourly). Each settled payment is recorded as a receipt showing the amount, the builder's share, and the platform fee — visible on the dashboard under **Monetization → Transactions**.


# Usage stats

The **App MCP Stats** page gives you an at-a-glance view of how your published MCP server is being used: how many end users it reaches, which MCP agents drive the most traffic, and which published tools are called most often. Open it from the **Stats** tab on your app's page.

## Time-range filter

By default the page shows **all-time** activity. Use the filter buttons in the top-right to narrow the whole page — the metric cards, the top agents, and the per-tool table all update together:

| Filter           | Window                              |
| ---------------- | ----------------------------------- |
| **All time**     | Every recorded invocation (default) |
| **Last 24h**     | Calls in the past 24 hours          |
| **Last 7 days**  | Calls in the past 7 days            |
| **Last 30 days** | Calls in the past 30 days           |

## Summary metrics

| Metric                   | Description                                                                                           |
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
| **Users using App MCP**  | Distinct connected end users with at least one invocation in the selected window                      |
| **Total tool calls**     | All calls across this app's published MCP tools                                                       |
| **Published tools used** | How many published tools have been called, out of the total published, with the single most-used tool |

## Success & error rate

The **Success & error rate** card shows how reliable your app's tools are for the selected period:

* A **success rate** percentage — the share of calls that returned a `2xx` or `3xx` status.
* A stacked bar splitting all calls into **Success** (2xx/3xx), **Client errors** (4xx), and **Server errors** (5xx), with a count for each.

The **Top error codes** card next to it lists the most frequent error codes across the app's published tools, with each code's count and its share of all errors. When there are no errors in the window it shows an empty state.

Both cards update together with the time-range filter. They are aggregate-only — no individual end user is ever identified.

## Top MCP agents

The **Top MCP agents** section ranks the MCP clients calling your tools — for example Claude, ChatGPT, or Cursor — by call volume for the selected time range. Each card shows the agent name, its call count, and its share of total traffic. If no calls have been made in the window, the section shows an empty state.

MCP agents are identified from the client name each MCP client reports when it connects. Calls from clients that don't report a name are grouped under *Unknown MCP Client*.

## Usage by published tool

The per-tool table breaks usage down for each enabled tool on your published servers:

| Column          | Description                              |
| --------------- | ---------------------------------------- |
| **Tool**        | Method, tool name, and API path          |
| **Calls**       | Number of invocations in the window      |
| **Users**       | Distinct end users who called the tool   |
| **Success**     | Calls that returned a `2xx`/`3xx` status |
| **Errors**      | Calls that returned an error status      |
| **Avg latency** | Mean end-to-end latency in milliseconds  |
| **Last used**   | Timestamp of the most recent invocation  |

For a full audit trail of individual invocations, see [Execution logs](/reference/execution-logs). Stats are always scoped to your workspace — you only ever see usage for your own apps and servers.

## Reading stats from an AI agent

These same stats are available as an MCP tool — `get_app_stats` — on the [FlowCP platform MCP server](/guides/connect-to-flowcp-mcp). An AI agent can read them with the same time-range filter (`range` = `24h`, `7d`, `30d`, or all-time) without opening the dashboard. The tool is scoped to the workspace bound to the agent's access token.


# Execution logs

FlowCP records every MCP tool invocation in an execution log. Logs are available from the **Logs** tab on your server's page.

## Log fields

| Field                   | Type           | Description                                                                                                  |
| ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ |
| **Tool name**           | string         | The name of the MCP tool that was called                                                                     |
| **End-user connection** | string         | The connection ID of the end user who triggered the call                                                     |
| **Status code**         | number         | The HTTP status code returned by your API (e.g. `200`, `404`, `500`)                                         |
| **Latency**             | number (ms)    | Total time from FlowCP receiving the MCP request to returning a response                                     |
| **Response tokens**     | number \| null | Estimated number of LLM tokens in the tool response (see below); empty for failed calls                      |
| **Error code**          | string \| null | A machine-readable error code if the call failed (e.g. `token_expired`, `tool_disabled`, `validation_error`) |
| **Timestamp**           | datetime       | When the call was made                                                                                       |

## Reading the logs

The Logs page shows the most recent entries first, paginated. Use the pagination controls at the bottom to load older entries.

### Status codes

API status codes follow standard HTTP semantics:

| Code range | Meaning                                           |
| ---------- | ------------------------------------------------- |
| `2xx`      | Successful call                                   |
| `4xx`      | Client error (bad input, not found, unauthorized) |
| `5xx`      | Upstream server error                             |

A `401` from your API typically means the end user's token has expired and could not be refreshed. The user must re-authorize.

### Error codes

FlowCP may return its own error codes before the upstream call is made:

| Error code              | Meaning                                                         |
| ----------------------- | --------------------------------------------------------------- |
| `tool_disabled`         | The tool was disabled between publish and the call              |
| `validation_error`      | The input did not pass the tool's Zod schema                    |
| `token_expired`         | The user's access token could not be refreshed                  |
| `token_missing`         | No connection exists for this user — they must authorize        |
| `confirmation_required` | A destructive tool was called without the required confirmation |
| `workspace_mismatch`    | Request was routed to the wrong workspace                       |

### Response tokens

Each successful call records an **estimate** of how many LLM tokens the tool's response payload consumes. This is the size of the response *after* FlowCP has applied any author-configured projection, field selection, and the hard response cap — i.e. the text the model actually receives — so it is a useful proxy for how much of an agent's context window your tools take up.

Because the MCP protocol does not reliably tell FlowCP which model is calling, the count is a **provider-agnostic estimate** computed with a standard byte-pair tokenizer (the `o200k_base` encoding used by recent GPT models). Treat it as a close approximation, not an exact per-model figure — actual token usage varies by model family.

Failed calls (`4xx`/`5xx`) do not record a token count, so the field is empty for those rows. The count is a size signal only — FlowCP never stores the response body itself (see [What is not logged](#what-is-not-logged)).

The **Stats** page aggregates this into a **Total response tokens** figure for the app and an **Avg tokens** column per tool.

## App-level log filtering

In addition to the per-server **Logs** tab, the app's **Logs** tab shows an execution log aggregated across **all of the app's published servers**. This view adds filters so you can narrow down to exactly the invocations you care about:

| Filter         | Options                                                           |
| -------------- | ----------------------------------------------------------------- |
| **Time range** | All time (default), Last 24h, Last 7 days, Last 30 days           |
| **Tool**       | Any tool that has been called in the selected window              |
| **Status**     | Success (`2xx`/`3xx`), Client error (`4xx`), Server error (`5xx`) |
| **Error code** | Any error code recorded in the selected window                    |
| **MCP client** | Any MCP client that has called the app                            |

Filter options are populated from real traffic in the selected time range. Use **Clear filters** to reset to the unfiltered, all-time view. The list is paginated newest-first; use **Load more** to fetch older entries.

The same privacy guarantees below apply to this view: it shows only aggregate audit fields and never any end-user PII beyond the truncated connection hash.

## Exporting logs

Both the per-server **Logs** tab and the app-level **Logs** tab have an **Export CSV** button that downloads the matching execution logs as a CSV file.

* On the **app-level** view, the export honors whatever filters are active — time range, tool, status, error code, and MCP client — so you can export exactly the slice you're looking at. Clear the filters to export everything in the selected time range.
* On the **per-server** view, the export contains that server's full log.
* The export is sorted newest-first and capped at the 10,000 most recent matching entries.

The CSV columns are: `createdAt` (ISO 8601), `toolName`, `statusCode`, `latencyMs`, `responseTokens`, `errorCode`, `mcpClientName`, `mcpClientVersion`, `llmModel`, and `endUserConnectionId`.

`responseTokens` is the estimated LLM token count of the response (see [Response tokens](#response-tokens)); it is empty for failed calls.

Servers using [Enterprise managed auth](/guides/enterprise-managed-auth) also record the verified identity provider (`identityIssuer`) and a SHA-256 **hash** of the caller's subject claim (`identitySubjectHash`), so calls are attributable to a real corporate identity. The raw subject is never stored, and the token's claims are never logged. Both are empty for every other auth mode.

`mcpClientVersion` is the version the client reports in the MCP `initialize` handshake; it is empty when the client does not send one. `llmModel` is a best-effort capture of the underlying language model, populated only when a client or proxy sends an `x-llm-model` header. The MCP protocol does **not** carry the model, so this column is empty for most traffic — FlowCP never guesses the model from the client name. The same privacy guarantees below apply — the export never contains tokens, request or response bodies, or any end-user PII beyond the truncated connection hash.

## Reading logs from an AI agent

The app-level log is also available as an MCP tool — `list_app_logs` — on the [FlowCP platform MCP server](/guides/connect-to-flowcp-mcp). It is cursor-paginated and accepts the same `range`, `toolName`, `status`, `errorCode`, and `client` filters, so an agent can pull an audit trail programmatically. The same privacy guarantees apply — no tokens, bodies, or PII beyond the truncated connection hash.

## What is not logged

FlowCP never logs:

* Authorization headers or Bearer tokens
* OAuth access tokens or refresh tokens
* Request or response body contents
* Personally identifiable information beyond the connection ID

The **Response tokens** count is a redacted size signal derived from the response — an integer only. FlowCP never stores or logs the response body itself.

Logs are scoped to your workspace — you can only see logs for your own apps and servers.

## Log retention

Log entries are retained for 30 days.


# User feedback

Every hosted MCP server includes a built-in `send_feedback` tool that lets MCP clients (and the AI agents driving them) send feedback about your server — a bug report, a missing capability, a confusing tool description, or praise. Submissions land on your app's **Feedback** page, where you review them and track what you've acted on.

## The `send_feedback` tool

The tool is registered automatically on every server session, in both production (`/mcp/:serverId`) and development (`/mcp/dev/:devSlug`) endpoints. It never calls your API — a submission is stored directly by FlowCP, scoped to your workspace and server.

| Input      | Type   | Required | Description                                                       |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `message`  | string | yes      | The feedback itself, up to 4,000 characters                       |
| `category` | enum   | no       | `bug`, `missing_capability`, `docs`, `praise`, or `other`         |
| `rating`   | number | no       | Overall experience with the server, `1` (poor) to `5` (excellent) |
| `toolName` | string | no       | The specific tool the feedback is about                           |

If one of your own enabled tools is already named `send_feedback`, FlowCP skips the built-in so your tool is never shadowed.

### Rate limits

Submissions are limited to **10 per hour per end user per server** (servers using a shared-credential auth mode share one bucket per server). Calls over the limit return an error to the client without storing anything.

## The Feedback page

The app's **Feedback** tab (under **Support** in the sidebar) aggregates submissions across **all of the app's servers**, including development endpoints — entries from a dev endpoint carry a **Dev** badge.

Each entry shows the message, category, rating, the tool it refers to, the MCP client name and version, the model (when reported), the submission time, and a truncated user hash — the same privacy-safe connection key the [execution logs](/reference/execution-logs) use.

### Review workflow

Every entry has a status:

| Status       | Meaning                                           |
| ------------ | ------------------------------------------------- |
| **New**      | Not yet looked at (the default)                   |
| **Reviewed** | Seen by a maintainer, action still pending        |
| **Resolved** | Addressed — fixed, shipped, or consciously closed |

Use **Mark reviewed** / **Mark resolved** on an entry to move it through the workflow, or delete it permanently with the trash button. The status tabs at the top show live counts so you can see your backlog at a glance.

### Filtering

| Filter         | Options                                                 |
| -------------- | ------------------------------------------------------- |
| **Status**     | All (default), New, Reviewed, Resolved                  |
| **Time range** | All time (default), Last 24h, Last 7 days, Last 30 days |
| **Category**   | Bug, Missing capability, Docs, Praise, Other            |

## Privacy

Feedback messages are free-text written by the client, but FlowCP never stores tokens, request bodies, or any end-user PII beyond the truncated user hash. Feedback is scoped to your workspace — you only see feedback for your own apps and servers.


# App health

The **Health** page gives an App-focused, real-time view of everything that has to be working for that app's MCP server to serve traffic: the hosted MCP server(s), the connected upstream API, and the FlowCP platform infrastructure.

Open it from the dashboard sidebar under **Monitor → Health**.

## What it shows

The page is organized into three sections.

### MCP servers

One card per MCP server belonging to the app. Each card shows:

| Field              | Description                                                         |
| ------------------ | ------------------------------------------------------------------- |
| **Status badge**   | Derived health — Operational, Degraded, or Outage                   |
| **Publish status** | The server's configured state (`draft`, `published`, or `disabled`) |
| **Recent calls**   | Number of tool invocations in the last 24 hours                     |
| **Server errors**  | Count and share of those calls that returned a 5xx error            |
| **Avg latency**    | Average tool execution latency over the window                      |

A `draft` or `disabled` server is reported as Operational because it is not serving traffic. A `published` server degrades to **Degraded** once its recent 5xx error rate passes 10%, and to **Outage** past 50%.

### APIs

Live reachability probes of the URLs that define the connected upstream API:

| Target           | What is checked                                        |
| ---------------- | ------------------------------------------------------ |
| **API base URL** | The app's request base URL responds to an HTTP request |
| **OpenAPI spec** | The Swagger / OpenAPI document URL is reachable        |

Any served response — including a 4xx — counts as reachable. A 5xx is reported as **Degraded**; a connection failure or timeout is an **Outage**. Each card shows the probed URL and round-trip latency.

### Our servers

The shared FlowCP platform infrastructure, identical to the public [system status](/reference/system-status) checks:

| Service         | What is checked                                           |
| --------------- | --------------------------------------------------------- |
| **API**         | The control-plane API process is handling requests        |
| **Database**    | PostgreSQL is reachable and responding to queries         |
| **Redis Cache** | Redis is reachable and responding to pings                |
| **MCP Runtime** | The MCP protocol server is running and accepting requests |

## Status levels

| Banner                      | Meaning                                                    |
| --------------------------- | ---------------------------------------------------------- |
| **All systems operational** | Every checked component is healthy                         |
| **Partial degradation**     | One or more components are slow or intermittently erroring |
| **Major outage**            | One or more components are unreachable or failing          |

The overall banner reflects the worst status across all three sections.

## Auto-refresh

The page re-checks everything every **20 seconds** automatically. You can also click **Refresh** to trigger an immediate check; the header shows the time of the last check.

## Programmatic health check

The underlying endpoint is workspace-scoped and returns the same payload the page renders:

```
GET /api/workspaces/:workspaceId/apps/:appId/health
```

Example response:

```json
{
  "appId": "app_123",
  "checkedAt": "2026-06-08T12:00:00.000Z",
  "overall": "ok",
  "platform": {
    "api": { "status": "ok", "latencyMs": 0 },
    "database": { "status": "ok", "latencyMs": 4 },
    "redis": { "status": "ok", "latencyMs": 1 },
    "mcpRuntime": { "status": "ok", "latencyMs": 8 }
  },
  "upstreamApis": [
    { "label": "API base URL", "url": "https://api.example.com", "status": "ok", "latencyMs": 120 }
  ],
  "mcpServers": [
    {
      "id": "server_1",
      "name": "Live",
      "slug": "live-abc",
      "configStatus": "published",
      "status": "ok",
      "invocationCount": 20,
      "serverErrorCount": 0,
      "recentErrorRate": 0,
      "avgLatencyMs": 42
    }
  ]
}
```


# System status

The system status page shows the real-time health of every FlowCP service component.

## URL

```
https://app.flowcp.ai/status
```

The page is publicly accessible — no login required. Share it with your team or bookmark it to check during incidents.

## What it shows

| Service         | What is checked                                                |
| --------------- | -------------------------------------------------------------- |
| **API**         | The control-plane API process is running and handling requests |
| **Database**    | PostgreSQL is reachable and responding to queries              |
| **Redis Cache** | Redis is reachable and responding to pings                     |
| **MCP Runtime** | The MCP protocol server is running and accepting connections   |

Each service card displays:

* **Status badge** — Operational, Degraded, or Outage
* **Latency** — Round-trip time for the health check in milliseconds
* **Error message** — Shown only when a service is degraded or down

## Status levels

| Status                      | Meaning                                                      |
| --------------------------- | ------------------------------------------------------------ |
| **All Systems Operational** | All services are healthy                                     |
| **Partial Degradation**     | One or more services are responding slowly or intermittently |
| **Major Outage**            | One or more services are unreachable                         |

## Auto-refresh

The page re-checks all services every **30 seconds** automatically. You can also click **Refresh** to trigger an immediate check.

## Programmatic health check

The underlying health endpoint is available at:

```
GET /api/health
```

Example response:

```json
{
  "service": "api",
  "status": "ok",
  "checkedAt": "2026-06-02T12:00:00.000Z",
  "services": {
    "api": { "status": "ok", "latencyMs": 0 },
    "database": { "status": "ok", "latencyMs": 4 },
    "redis": { "status": "ok", "latencyMs": 1 },
    "mcpRuntime": { "status": "ok", "latencyMs": 8 }
  }
}
```

The endpoint always returns HTTP 200 so that load balancers remain satisfied even when individual dependencies are degraded. Check the `status` field in the response body to determine overall health.


# Troubleshooting

## Import failures

### Swagger/OpenAPI document unreachable in the connect wizard

The connect wizard tried to fetch your Swagger/OpenAPI document and could not reach it. FlowCP fails closed — an app is only created once its document is reachable — so you stay on step 1 (generic provider) or on the import step (Bubble) until it succeeds.

**Generic OpenAPI provider:**

1. Confirm the URL is correct and returns a valid JSON OpenAPI document in a browser or with `curl`.
2. Make sure the document is **publicly reachable** (no VPN, IP allowlist, or auth wall in front of it).
3. Check that the response is under the 5 MB size limit and returns within \~10 seconds.
4. Fix the URL on step 1 and click **Continue to import** again.

**Bubble provider:** this usually means **Hide Swagger Schema** is enabled in your Bubble app settings.

1. Open your Bubble app in the editor.
2. Navigate to **Settings → API**.
3. Uncheck **"Hide Swagger Schema"**.
4. Save your changes.
5. Return to the FlowCP connect wizard and click **Try again**.

If the endpoint is still unreachable after enabling Swagger, confirm that:

* The Bubble API is enabled (check "Enable the Bubble API" on the same settings page).
* Your app has at least one API workflow or data type endpoint published.

### The import status stays at "Pending" for more than a minute

The import job may be stuck in the queue. Try navigating away and back to the import page. If the status has not updated after a few minutes, trigger a re-import from your app's settings.

### Import fails with "Could not fetch Swagger spec"

FlowCP could not reach your Bubble app's Swagger endpoint. Check:

1. **Bubble API is enabled.** Go to Bubble Settings → API and confirm "Enable the Bubble API" is on.
2. **App ID is correct.** The App ID is the subdomain of your Bubble URL — `myapp` for `myapp.bubbleapps.io`. Do not include `.bubbleapps.io`.
3. **Version.** For the **Main** version (or any development version), confirm your Bubble app has that version branch (`/version-<id>/`) with the API enabled.
4. **Custom domain.** FlowCP detects Live custom domains from Bubble's `/api/1.1/meta` when you connect. If Live imports or OAuth still use `*.bubbleapps.io`, open the app's **Settings** and set **Custom domain (Live)** manually, or confirm the Bubble API is enabled so meta is reachable.

### Import fails with "OpenAPI 3.x required" or "Invalid OpenAPI spec"

Bubble publishes **Swagger 2.0** at `/api/1.1/meta/swagger.json`. FlowCP converts that format automatically during import. If you still see a validation error:

The Bubble API returned a document that FlowCP could not parse. This can happen if:

* The API is configured but has no endpoints defined
* Your Bubble branch outputs a non-standard spec format

Enable at least one API endpoint in Bubble and re-import.

### Re-import fails with "URL returned HTTP 404" (OpenAPI apps)

If a generic OpenAPI app imported successfully before but a later re-import fails with **URL returned HTTP 404**, its stored Swagger URL is pointing at the wrong location. Open the app's **Settings**, find the **Swagger URL** card, re-enter the correct OpenAPI/Swagger document URL, and click **Save Swagger URL**. FlowCP verifies the URL is reachable and re-imports your tools from it.

***

## OAuth errors

### "OAuth provider not configured"

The client ID and secret have not been saved. Open the **OAuth** tab and complete the configuration before testing the flow. For a generic provider, make sure the **authorize** and **token** URLs are filled in too.

### Authorization completes with your provider but redirects to an error page

The redirect URI registered with your provider does not match the one FlowCP expects. Copy the **FlowCP OAuth redirect URI** from the OAuth page and add it exactly (including protocol and path) to your provider's allowed redirect URLs. For Bubble, that's Settings → API → 3rd Party OAuth → Allowed redirect URLs.

### ChatGPT (web) shows "There was a problem connecting" right after sign-in

You add the connector in ChatGPT on the web (`chatgpt.com`), complete the sign-in with your provider, get redirected back to ChatGPT, and then see *"There was a problem connecting FlowCP. Try again later."*

ChatGPT on the web runs in the browser, so the requests it makes back to FlowCP to finish connecting (the OAuth token exchange and the MCP handshake) are cross-origin. FlowCP now sends the cross-origin (CORS) headers those endpoints need, so this should succeed. If you still hit it:

1. Confirm the server status is **published** on the Publish page.
2. Confirm **OAuth** is configured for the app (client ID/secret saved, and for a generic provider, the authorize and token URLs).
3. Remove and re-add the connector, then retry — ChatGPT caches connector state.
4. If it still fails, open your browser's DevTools → **Network** tab and reproduce. The failed request names the exact endpoint and status code; include that when you contact support.

### The client rejects the connection with a "resource mismatch"

A strict MCP client (Grok/xAI among them) checks that the `resource` in FlowCP's protected-resource metadata is the same URL it connected to, and refuses the authorization flow when the two differ (RFC 9728 §3.3).

A server answers on both its friendly slug (`/mcp/acme-crm`) and its internal id (`/mcp/cmu1gc74r013w0tqi31aq8sxr`). FlowCP's 401 challenge now points at whichever one you connected with, so both URLs pass this check. If you are running a self-hosted FlowCP from before this change, either update it or paste the internal-id URL — the **Deploy** page shows both.

### "Invalid client\_id" / `invalid_client` after a working connection

The AI client is presenting a registration FlowCP no longer holds. Registrations from dynamic client registration now renew on every use, so an actively used connector keeps working; only one that has gone unused for 30 days is collected.

If you hit this, remove and re-add the connector in the AI client — that makes it register again and issues a fresh `client_id`. A client that caches its `client_id` will not do this on its own.

### "Token expired" errors in execution logs

The user's access token has expired. FlowCP will automatically signal the AI client to re-authorize (the AI client shows an OAuth prompt). If re-authorization does not happen automatically:

* Ask the user to restart their AI client session to trigger a new authorization prompt.
* If the client secret was rotated without updating FlowCP, update it on the OAuth page — new authorizations will use the updated credentials.

***

## Publish errors

### Publish button is disabled

At least one enabled tool is classified `destructive` and does not have **Require confirmation** turned on. The publish page lists the tool names. Go to the Tools tab, find each listed tool, and turn on **Require confirmation**.

### Server status shows "published" but AI client does not see the tools

1. **Restart the AI client.** Most clients only load MCP config on startup.
2. **Check the config URL.** The URL in your config should point to the MCP runtime (e.g. `runtime.flowcp.io/mcp/<server-id>`), not the FlowCP dashboard or API server.
3. **Verify tools are enabled.** At least one tool must be enabled for the server to return a non-empty tools list.

### Server URL shows "MCP runtime URL not configured" (self-hosted)

The dashboard renders each server's public endpoint from its own `MCP_RUNTIME_PUBLIC_URL` environment variable. If that variable is missing on the **web/dashboard** service, the Servers, MCP-clients and Publish pages show a "MCP runtime URL not configured" notice instead of a URL.

Set `MCP_RUNTIME_PUBLIC_URL` to your MCP runtime's public origin (e.g. `https://mcp.example.com`) on the web service and restart it — the value is read at runtime, so no rebuild is needed. In the bundled docker-compose stack all services share one `.env`, so this only bites split deployments where the web, API and runtime run as separate services. See [Self-host the platform](/guides/self-host-platform#ports).

***

## MCP tool call failures

### Tool call returns "tool\_disabled"

The tool was enabled at publish time but has since been disabled. Re-enable it from the Tools tab.

### Tool call returns "token\_missing"

The calling user has not authorized FlowCP yet. The MCP client should surface an authorization URL — direct the user to visit it.

### Tool call returns a 401

The user's access token is invalid or expired. FlowCP automatically signals the AI client to re-authorize — the AI client should present an OAuth prompt. Ask the user to complete the re-authorization flow in their AI client.

If the AI client does not show a prompt, restarting it usually triggers the re-authorization.

### Every Bubble tool returns 404 `BUBBLE_NOT_FOUND`

When *all* of a Bubble server's data tools fail with `404 BUBBLE_NOT_FOUND` — not one type, but every one — the request is reaching Bubble at a path Bubble does not recognize, rather than hitting a per-type permission problem.

Open the **Tools** tab and read the path shown under a failing tool. FlowCP builds the request as `<your app origin><branch>/api/1.1` + that path, so the path must be relative to the API root:

| Path shown             | Meaning                                                |
| ---------------------- | ------------------------------------------------------ |
| `/obj/Company`         | Correct — the request goes to `…/api/1.1/obj/Company`. |
| `/api/1.1/obj/Company` | The prefix is duplicated, and every call 404s.         |

The duplicated form came from Bubble specs that bake `/api/1.1` into each path instead of declaring it as `basePath`. **Re-import the spec** from the app's Settings to regenerate the tools with correct paths. The regenerated tools are named for their action (`list_companies` rather than a name derived from the spec's `operationId`) and arrive **disabled**, so disable the old tools, enable the new ones, and publish.

If the path is already correct, the 404 is Bubble's: confirm the data type is exposed in **Settings → API** on the *same version* the server points at (a branch on `version-test` reads the development version, not live) and that the app has been deployed to live since the Data API was enabled.

### Tool call returns a 403

The user's account does not have permission to perform the action. This is your API enforcing its own access rules — FlowCP cannot override them. (For Bubble, check the app's privacy settings for the relevant data type.)

***

## Still stuck?

Open an issue at the FlowCP support channel or contact your account manager. Include:

* Your workspace ID
* The app ID and server ID from the FlowCP URL
* The error code and timestamp from the execution logs


