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

# 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.md) for the full layout.

## What you get

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

```
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.app
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.
