Browse docs

Maintained by

OOMOL OpenConnector self-hosting guide

OOMOL OpenConnector is an open-source, self-hostable connector service. Use it when agents or internal tools need to call real external services while provider credentials, permissions, and execution history stay in your own environment. It exposes typed actions for services such as GitHub, Gmail, Notion, Hacker News, Ably, Abstract, and A-Leads through MCP or HTTP.

Agents see schemas, scopes, execution status, and safe account labels; raw provider tokens, action permissions, and execution history stay inside your deployment boundary.

What it gives you

  • A runtime that exposes provider actions through MCP, HTTP, OpenAPI, and a web console.
  • Credential storage for API keys, custom credentials, OAuth2 connections, and no-auth providers.
  • Typed action schemas so agents can discover what they can call before they call it.
  • Connection identity and scopes so users and agents can see which account an action will run as.
  • Temporary file transit for actions that need file URLs.
  • Recent run logs with redacted input summaries and provider errors.
  • A provider catalog with local executors that load only when an action is used.

Start by choosing where to run the runtime, then configure storage, access control, provider connections, and the agent-facing MCP or HTTP entry point.

Choose a deployment target

TargetUse it whenStorage
Docker ComposeYou want the fastest local or single-server deployment.Docker volume mounted at /app/data, with SQLite at /app/data/connect.sqlite.
Source runtimeYou are developing OOMOL OpenConnector or provider executors.Local ./data/connect.sqlite unless OOMOL_CONNECT_DATA_DIR is set.
Cloudflare WorkersYou want a Cloudflare-hosted runtime state and metadata deployment.D1 for runtime records and R2 for temporary transit files.

Prepare the runtime

Before deploying, decide these values:

ValueWhy it matters
OOMOL_CONNECT_ADMIN_TOKENProtects the web console, /api, and local API reference when they are reachable outside your own shell.
OOMOL_CONNECT_ENCRYPTION_KEYEncrypts stored provider credentials and OAuth client secrets. Keep it in your secret manager.
OOMOL_CONNECT_ORIGINSets the public origin used for OAuth callback URLs. Required when the browser reaches the runtime through a tunnel, domain, or Worker URL.
Runtime tokensAuthenticate agent and client calls to /v1 and /mcp. Create them from the web console Access tab or the admin API.
Action policyLimits which actions can execute through /v1 and MCP. Use OOMOL_CONNECT_ALLOWED_ACTIONS and OOMOL_CONNECT_BLOCKED_ACTIONS.

Treat the runtime database as sensitive. Without OOMOL_CONNECT_ENCRYPTION_KEY, OOMOL OpenConnector still works, but provider secrets are stored in a sensitive local SQLite file.

Run with Docker Compose

Clone the OOMOL OpenConnector repository, enter the project directory, and start the runtime:

git clone https://github.com/oomol-lab/open-connector.git
cd open-connector
docker compose up --build

Open the web console:

http://localhost:3000

Open the generated API reference:

http://localhost:3000/docs

Verify the runtime with a no-auth action:

curl -s -X POST http://localhost:3000/v1/actions/hackernews.get_top_stories \
  -H 'content-type: application/json' \
  -d '{"input":{}}'

Docker Compose stores runtime state in the connector-data volume. The container stores SQLite at:

/app/data/connect.sqlite

For a private server deployment, set at least the admin token and encryption key before starting:

Run these examples in Bash. Enter real credentials only at the hidden prompts; do not paste them into shell commands or command logs.

(
  set -eu
  read -r -s -p "OOMOL_CONNECT_ADMIN_TOKEN: " OOMOL_CONNECT_ADMIN_TOKEN
  echo
  test -n "$OOMOL_CONNECT_ADMIN_TOKEN"
  export OOMOL_CONNECT_ADMIN_TOKEN
  read -r -s -p "OOMOL_CONNECT_ENCRYPTION_KEY: " OOMOL_CONNECT_ENCRYPTION_KEY
  echo
  test -n "$OOMOL_CONNECT_ENCRYPTION_KEY"
  export OOMOL_CONNECT_ENCRYPTION_KEY
  docker compose up --build
)

When the runtime is exposed through a public domain or tunnel, require both admin and runtime authentication and set the public origin:

(
  set -eu
  export OOMOL_CONNECT_ORIGIN="https://connect.example.com"
  read -r -s -p "OOMOL_CONNECT_ADMIN_TOKEN: " OOMOL_CONNECT_ADMIN_TOKEN
  echo
  test -n "$OOMOL_CONNECT_ADMIN_TOKEN"
  export OOMOL_CONNECT_ADMIN_TOKEN
  read -r -s -p "OOMOL_CONNECT_RUNTIME_TOKEN: " OOMOL_CONNECT_RUNTIME_TOKEN
  echo
  test -n "$OOMOL_CONNECT_RUNTIME_TOKEN"
  export OOMOL_CONNECT_RUNTIME_TOKEN
  read -r -s -p "OOMOL_CONNECT_ENCRYPTION_KEY: " OOMOL_CONNECT_ENCRYPTION_KEY
  echo
  test -n "$OOMOL_CONNECT_ENCRYPTION_KEY"
  export OOMOL_CONNECT_ENCRYPTION_KEY
  docker compose up --build
)

The environment-provided runtime token is useful for bootstrapping a public deployment. If you prefer console-created oct_… tokens, keep the runtime private while creating the first token, then expose it publicly.

The Docker image binds to 0.0.0.0 inside the container. Control external access with your host firewall, reverse proxy, or container platform.

Protect admin and runtime access

Admin HTTP clients call /api, /docs, or the web console with:

Authorization: Bearer replace-with-an-admin-token

Create runtime tokens for agents and SDK-style clients from the web console Access tab. The token is shown once; only a hash is stored. A runtime without tokens must remain on localhost or a private network.

You can also create one through the admin API:

The authenticated request examples below require Python 3 and curl 7.76.0 or newer, which supports the —fail-with-body option used throughout these examples. They read secrets with hidden prompts and pass them to curl through standard input, without placing credentials in command-line arguments or shell history.

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
payload = {'name': 'Claude Desktop'}
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
config += "data = " + json.dumps(json.dumps(payload)) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'POST', 'http://localhost:3000/api/runtime-tokens', '--header', 'content-type: application/json'],
    input=config, text=True, check=True,
)
PY

Runtime clients then call /v1 or /mcp with:

Authorization: Bearer oct_...

For bootstrap scripts and backward compatibility, OOMOL_CONNECT_RUNTIME_TOKEN is still accepted:

(
  set -eu
  read -r -s -p "OOMOL_CONNECT_ADMIN_TOKEN: " OOMOL_CONNECT_ADMIN_TOKEN
  echo
  test -n "$OOMOL_CONNECT_ADMIN_TOKEN"
  export OOMOL_CONNECT_ADMIN_TOKEN
  read -r -s -p "OOMOL_CONNECT_RUNTIME_TOKEN: " OOMOL_CONNECT_RUNTIME_TOKEN
  echo
  test -n "$OOMOL_CONNECT_RUNTIME_TOKEN"
  export OOMOL_CONNECT_RUNTIME_TOKEN
  docker compose up --build
)

Limit the actions agents can execute:

OOMOL_CONNECT_ALLOWED_ACTIONS="hackernews.*,github.get_current_user" docker compose up --build

Block specific actions even when a broader allowlist includes them:

OOMOL_CONNECT_ALLOWED_ACTIONS="github.*" \
OOMOL_CONNECT_BLOCKED_ACTIONS="github.delete_repository" \
docker compose up --build

Connect an API-key provider

GitHub is a compact API-key example because it can use a personal access token.

Inspect the provider contract:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'GET', 'http://localhost:3000/api/providers/github'],
    input=config, text=True, check=True,
)
PY

Store the default GitHub connection:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
payload = {'authType': 'api_key', 'values': {'apiKey': None}}
payload["values"]["apiKey"] = getpass.getpass("GitHub API key: ")
if not payload["values"]["apiKey"]:
    raise SystemExit("An API key is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
config += "data = " + json.dumps(json.dumps(payload)) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'PUT', 'http://localhost:3000/api/connections/github', '--header', 'content-type: application/json'],
    input=config, text=True, check=True,
)
PY

Call GitHub through the runtime:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("Runtime token: ")
if not token:
    raise SystemExit("A token is required")
payload = {'input': {}}
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
config += "data = " + json.dumps(json.dumps(payload)) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'POST', 'http://localhost:3000/v1/actions/github.get_current_user', '--header', 'content-type: application/json'],
    input=config, text=True, check=True,
)
PY

Check configured connections and the safe account identity exposed to agents:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'GET', 'http://localhost:3000/api/connections'],
    input=config, text=True, check=True,
)
PY

Named connections

Add connectionName when the same provider needs multiple accounts:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
payload = {'authType': 'api_key', 'connectionName': 'work', 'values': {'apiKey': None}}
payload["values"]["apiKey"] = getpass.getpass("GitHub API key: ")
if not payload["values"]["apiKey"]:
    raise SystemExit("An API key is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
config += "data = " + json.dumps(json.dumps(payload)) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'PUT', 'http://localhost:3000/api/connections/github', '--header', 'content-type: application/json'],
    input=config, text=True, check=True,
)
PY

Select that account during execution:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("Runtime token: ")
if not token:
    raise SystemExit("A token is required")
payload = {'input': {}}
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
config += "data = " + json.dumps(json.dumps(payload)) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'POST', 'http://localhost:3000/v1/actions/github.get_current_user', '--header', 'x-oo-connector-alias: work', '--header', 'content-type: application/json'],
    input=config, text=True, check=True,
)
PY

The alias query parameter is also accepted.

Connect an OAuth provider

OAuth providers use your own provider OAuth app. First set the callback URL in the provider OAuth app. The callback URL is your OpenConnector origin plus /oauth/callback.

With the default port, GitHub uses this callback URL:

http://localhost:3000/oauth/callback

If you expose the runtime through another origin, set OOMOL_CONNECT_ORIGIN before starting the runtime, then use that origin in the callback URL:

https://connect.example.com/oauth/callback

Paste the exact callback URL into the provider OAuth app.

Store the OAuth client from the web console:

  1. Open http://localhost:3000.
  2. Open the provider page, such as GitHub.
  3. Choose Configure OAuth Client or Edit OAuth Client.
  4. Paste the provider app’s Client ID and Client Secret.
  5. Choose Save OAuth Client.

If the provider needs extra client config fields, fill them in the same OAuth client form. Use an OpenConnector console version that exposes the fields required by that provider before continuing.

After saving the OAuth client, choose Connect on the provider page. Approve the provider authorization screen. After the provider redirects back to OpenConnector, confirm that the provider page shows the account as connected.

Give tools to an agent

For MCP-capable clients, point the client at:

http://localhost:3000/mcp

The MCP server exposes discovery-oriented tools:

  • list_apps
  • search_actions
  • get_action_guide
  • execute_action

Preview MCP tool metadata:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("Runtime token: ")
if not token:
    raise SystemExit("A token is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'GET', 'http://localhost:3000/mcp/tools'],
    input=config, text=True, check=True,
)
PY

For HTTP clients, use the /v1 runtime API:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("Runtime token: ")
if not token:
    raise SystemExit("A token is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'GET', 'http://localhost:3000/v1/actions'],
    input=config, text=True, check=True,
)
PY

Each action has a local Markdown guide with the input schema, scopes, provider permissions, current connection identity, and request examples:

python3 - <<'PY'
import getpass
import json
import subprocess
import warnings

warnings.simplefilter("error", getpass.GetPassWarning)
token = getpass.getpass("OOMOL_CONNECT_ADMIN_TOKEN: ")
if not token:
    raise SystemExit("A token is required")
config = "header = " + json.dumps("authorization: Bearer " + token) + "\n"
subprocess.run(
    ['curl', '--disable', '--silent', '--show-error', '--fail-with-body', '--config', '-', '--request', 'GET', 'http://localhost:3000/api/actions/github.get_current_user/agent.md'],
    input=config, text=True, check=True,
)
PY

The web console can also copy cURL, TypeScript, and agent prompt examples for each action.

Run from source

Use the source workflow when you are developing OOMOL OpenConnector or provider executors. Use Node.js 22 or newer.

git clone https://github.com/oomol-lab/open-connector.git
cd open-connector
npm install
npm run build:web
npm run dev

npm install and npm run dev create local generated files when they are missing or stale.

When running from source, runtime state is stored in:

./data/connect.sqlite

Use another data directory with:

OOMOL_CONNECT_DATA_DIR=/path/to/data npm run dev

Set the same admin, encryption, origin, runtime token, and action policy environment variables described above.

Deploy to Cloudflare Workers

Cloudflare Workers is supported as a metadata and runtime-state deployment target.

Clone the repository, create the Cloudflare resources, and deploy:

git clone https://github.com/oomol-lab/open-connector.git
cd open-connector
cp wrangler.example.jsonc wrangler.local.jsonc
npm install
npm run generate:catalog
npm run build:web
npx wrangler d1 create oomol-connect
npx wrangler r2 bucket create oomol-connect-transit-files

Before deploying, update the ignored wrangler.local.jsonc file with the D1 database_id returned by Cloudflare.

Keep the Worker private during initial setup: in the local Wrangler configuration, set workers_dev and preview_urls to false and leave public routes and custom domains unconfigured. Do not allow public access until all three secrets below have been set.

npx wrangler d1 migrations apply oomol-connect --remote --config wrangler.local.jsonc
npm run deploy:cloudflare

Set secrets with Wrangler:

npx wrangler secret put OOMOL_CONNECT_ADMIN_TOKEN --config wrangler.local.jsonc
npx wrangler secret put OOMOL_CONNECT_RUNTIME_TOKEN --config wrangler.local.jsonc
npx wrangler secret put OOMOL_CONNECT_ENCRYPTION_KEY --config wrangler.local.jsonc

After setting all three secrets, enable the intended public route in the local configuration and redeploy.

Set OOMOL_CONNECT_ORIGIN to the public Worker origin in wrangler.local.jsonc. The admin token protects the console and /api; the runtime token protects /v1 and /mcp from the first public request.

Cloudflare uses the same environment variable names for origin, auth tokens, action policy, transit file limits, and credential encryption. PORT, HOST, and OOMOL_CONNECT_DATA_DIR are local Node-only settings.

The Worker runtime serves catalog metadata, /api and /v1 metadata endpoints, connections, runtime tokens, OAuth config and state, R2-backed transit files, and the generated provider action executor registry. Configure an R2 lifecycle rule for the transit bucket if you want unread expired transit files cleaned up automatically.

Operate the deployment

Keep these records available for support and recovery:

RecordWhere to find it
Runtime databaseDocker volume, local OOMOL_CONNECT_DATA_DIR, or Cloudflare D1.
Temporary transit filesOOMOL_CONNECT_DATA_DIR/files for local runtime, or Cloudflare R2.
Admin token and encryption keyYour secret manager. OOMOL OpenConnector does not store the encryption key for you.
Runtime token prefixWeb console Access tab or /api/runtime-tokens. Full runtime tokens are shown only once.
Execution historyWeb console recent runs or GET /api/runs.

For file-upload actions, local transit files are stored under OOMOL_CONNECT_DATA_DIR/files and cleaned up by age. Tune the lifetime and upload size with OOMOL_CONNECT_TRANSIT_FILE_TTL_SECONDS and OOMOL_CONNECT_TRANSIT_FILE_MAX_BYTES.

Troubleshooting

SymptomWhat to check
The web console or /api returns unauthorized.Send Authorization: Bearer <admin-token> and confirm OOMOL_CONNECT_ADMIN_TOKEN matches the running environment.
/v1 or /mcp returns unauthorized.Use a runtime token created from the Access tab or POST /api/runtime-tokens. Admin tokens are for admin surfaces, not runtime clients.
OAuth redirects to the wrong host.Set OOMOL_CONNECT_ORIGIN to the origin users open in the browser, restart the runtime, then use <openconnector-origin>/oauth/callback in the provider app.
An action cannot find credentials.Check /api/connections, the selected x-oo-connector-alias, and whether the connection is still available.
An action is blocked.Check OOMOL_CONNECT_ALLOWED_ACTIONS and OOMOL_CONNECT_BLOCKED_ACTIONS. Blocked actions win over broader allowlists.
Provider credentials fail after working earlier.Reconnect the provider if the token expired and no refresh token is available, or verify that the encryption key still matches the stored records.