Skip to content

Local, tunnel or production ​

There are three ways to run bundlemcp, and only the last one is meant for other people to use. This page says what each one is for, what it leaves insecure, and which settings a real deployment has to provide.

SetupForReachable fromHow
LocalTrying the admin UI, scripts and agents on your own machineYour machine onlydocker compose … up — the Quickstart
Local + tunnelTesting with claude.ai, ChatGPT or Claude Desktop before you deployAnyone with the tunnel URLdeploy/stack.sh up — below
ProductionReal usersYour domain, over HTTPSYour own compose file or orchestrator — Production deployment

What the two local setups take shortcuts on ​

Both local setups run the compose stack in deploy/, which is built to start with one command, not to be safe. It:

  • falls back to published secrets. Without deploy/.env, the service key bmcp_sk_devservicekey…, the token encryption key, the identity provider's admin password and its client secret are the same in every checkout. Plain docker compose up uses them, which is safe only because every port the stack publishes listens on 127.0.0.1. deploy/stack.sh replaces the first three in deploy/.env with generated values before every start, and refuses to open a tunnel while the running gateway still accepts the published service key.
  • brings a development identity provider. Keycloak with the login admin / admin, and a realm that accepts any redirect URI so that a tunnel's changing URL still works.
  • speaks plain HTTP on localhost. In the tunnel setup the tunnel adds HTTPS in front.
  • trusts every private address as a proxy, so X-Forwarded-For from inside the compose network is believed.
  • lets the gateway reach its own loopback, because the OAuth demo upstream runs there. A production gateway never allows that.

None of that matters while only you can reach the stack. A tunnel makes it reachable from the internet, so keep a tunnel up only while you are testing, and never put data you care about into it.

Local + tunnel ​

claude.ai and ChatGPT connect to an MCP server from their own servers on the internet, so http://localhost:8000 is useless to them: they need a public HTTPS URL. A tunnel provides one. It is a small program that opens an outgoing connection to a tunnel service, which then forwards requests for a public URL back to your machine.

deploy/stack.sh sets this up. It starts the same stack as the quickstart, adds a reverse proxy (Caddy) that serves the gateway and the admin UI under one address, and a tunnel in front of the proxy:

bash
cp deploy/.env.example deploy/.env    # once; TUNNEL=quick is the default
deploy/stack.sh up                    # or: make up

When it is done it prints the public URL and the connector URLs:

text
bundlemcp is up.

  Admin UI         https://<random-words>.trycloudflare.com
  Gateway          https://<random-words>.trycloudflare.com
  ...
  claude.ai / Claude Desktop connector URL:   https://<random-words>.trycloudflare.com/claude/mcp
  ChatGPT connector URL:                      https://<random-words>.trycloudflare.com/chatgpt/mcp

Paste the connector URL into claude.ai or ChatGPT, as described in Connect claude.ai and Connect ChatGPT. The seed already has an endpoint for each.

Why the script and not just cloudflared ​

The gateway has to know its public URL before it starts: every OAuth address it hands to claude.ai or ChatGPT — issuer, authorization URL, token URL, redirect URI — is built from BUNDLEMCP_PUBLIC_URL. A client that is sent to http://localhost:8000 for login cannot follow. The script starts the tunnel first, reads the URL the tunnel was given, and only then starts the gateway with that URL. Starting a tunnel by hand in front of a running stack skips that step and breaks every OAuth login.

Choosing a tunnel ​

TUNNEL in deploy/.env selects it:

TUNNELWhat you getWhat you need
quick (default)A Cloudflare quick tunnel: a random https://….trycloudflare.com URLNothing — no account
ngrokngrok with a static domain: the same URL every timeNGROK_AUTHTOKEN and NGROK_DOMAIN from the ngrok dashboard (the free plan includes one domain)
noneNo tunnel; the stack stays on localhostNothing

A quick tunnel's URL changes whenever the tunnel container restarts. Run deploy/stack.sh up again afterwards so the gateway learns the new URL, and reconnect claude.ai or ChatGPT, which remembered the old one. If that happens often, use ngrok's static domain.

Everyday commands ​

CommandDoes
deploy/stack.sh urlPrints the URL the stack is published at, and warns if the tunnel has moved since
deploy/stack.sh logsFollows the logs of every container
deploy/stack.sh downStops everything; the database is kept
deploy/stack.sh rebuildDeletes the stack's data volumes, rebuilds the images and starts fresh from the seed
deploy/stack.sh secretsGenerates any secret deploy/.env lacks or still holds at its published value, without starting

Every command has a make alias (make up, make url, …).

If up stops with "the gateway still accepts the service key published in this repository", the stack's database was created before the script generated a key, for example by a plain docker compose up. A service key that already exists is never replaced by a new BUNDLEMCP_INITIAL_SERVICE_KEY, so revoke the published one under API keys in the admin UI, or start from empty volumes with deploy/stack.sh rebuild.

Reaching the stack from another device on your network

Every host port binds to 127.0.0.1, so other machines cannot reach the stack directly. To open it on purpose, and only on a network you trust, write your own compose file that republishes the ports you need, for example ports: !override ["8000:8000"] under gateway, keep it out of the repository, and pass it after the stack's own: docker compose -f deploy/docker-compose.yml -f <your file> up -d.

Signing in through the tunnel works on this machine only

The development identity provider is published at http://localhost:8080. When you sign in — to the admin UI through the tunnel URL, or during claude.ai's OAuth flow — your browser is sent there, which only works in a browser on the machine running the stack. A production deployment uses an identity provider with its own public URL.

Production ​

A production deployment replaces every shortcut above. Production deployment walks through the reverse proxy, health checks, backups and scaling. These are the settings it must provide; the Configuration reference lists every other one with its default.

Gateway: always set ​

A production gateway needs every one of these. Each has a fallback so the gateway starts on a developer's machine, and in production that fallback is wrong. What each variable does, and exactly what goes wrong without it, is on the Configuration page; follow the variable's name.

VariableSet it to
BUNDLEMCP_DATABASE_URLpostgresql+asyncpg://<user>:<password>@<host>:5432/<db>
BUNDLEMCP_REDIS_URLredis://<host>:6379/0
BUNDLEMCP_PUBLIC_URLThe HTTPS URL clients reach, e.g. https://mcp.example.com
BUNDLEMCP_TRUSTED_PROXIESThe CIDRs your reverse proxy connects from, e.g. ["10.0.0.0/8"]
BUNDLEMCP_OIDC_DISCOVERY_URLYour identity provider's …/.well-known/openid-configuration (Identity provider)
BUNDLEMCP_OIDC_CLIENT_IDThe client id you registered for the gateway
BUNDLEMCP_OIDC_CLIENT_SECRETThe secret your identity provider issued
BUNDLEMCP_ADMIN_EMAILS["you@example.com"]
BUNDLEMCP_INITIAL_SERVICE_KEYA key you generate

Gateway: set when it applies ​

Each of these is needed only in a particular situation. If the second column does not describe your deployment, leave the variable unset; what the gateway does then is on the Configuration page, under the variable's name.

VariableYou need it whenSet it to
BUNDLEMCP_TOKEN_ENCRYPTION_KEYSAt least one upstream server uses upstream OAuth: people or an admin sign in to it, or the gateway logs in with a client id and secret.A list with one key you generate. Keep it safe.
BUNDLEMCP_WEB_ORIGINSThe admin UI is served from a different address than the gateway.The admin UI's address, e.g. ["https://admin.example.com"]
BUNDLEMCP_BOOTSTRAP_FILEYou create upstream servers, bundles and endpoints from a bootstrap file.The file's path inside the gateway container
BUNDLEMCP_BOOTSTRAP_STRICTYou use a bootstrap file.true
BUNDLEMCP_DCR_ALLOWED_REDIRECT_URISYou want only certain MCP clients, such as claude.ai, to connect. Unset allows any client; people still have to sign in either way.The clients' return addresses, e.g. ["https://claude.ai/api/mcp/auth_callback"]
BUNDLEMCP_EGRESS_ALLOW_CIDRSAn upstream server or your identity provider is on the gateway's loopback or in a link-local / cloud-metadata range.Only the exact ranges you need, e.g. ["127.0.0.1/32"]

Admin UI container ​

VariableSet it to
NUXT_PUBLIC_API_BASEThe gateway's public URL, the same value as BUNDLEMCP_PUBLIC_URL

Generating the secrets ​

How to generate the service key and the token encryption key is under Secrets you generate.