Skip to content
📌 Enable SQLite data replication to S3 with Litestream sync
Remote API and MCP

Remote API and MCP

Overview

By default, the OpenRun server is managed over a unix domain socket: the CLI on the same machine connects with no credentials, protected by file system permissions. Two optional remote surfaces extend management beyond the local machine:

  • rest — the management REST API over TCP, used by the OpenRun CLI running on another machine.
  • mcp — a Model Context Protocol endpoint at https://<host>:<https_port>/_openrun/mcp, which lets AI clients like Claude Code operate OpenRun: list and create apps, promote, manage services and bindings, run syncs and more.

Both surfaces are off by default ([api.rest] enable / [api.mcp] enable). When enabled, they are served only over HTTPS (or through a TLS-terminating proxy listed in security.trusted_proxies) — a plaintext request gets a plain 404, never a redirect. Every remote call authenticates with a bearer credential (an API key, or an OAuth token from a browser login) and runs as that credential’s user identity with RBAC enforced. RBAC is always on: the default grant gives every authenticated principal read access to apps, and the admin account has full authority, so a fresh install needs no RBAC setup before enabling remote access.

Server Setup

Add to the server config and restart:

openrun.toml
[api.rest]
enable = true
auth = ["admin"]   # login mechanisms for openrun login: "admin" (default), "builtin",
                   # or an [auth.*]/[saml.*] name. At least one is required

[api.mcp]
enable = true
auth = ["admin"]   # login mechanisms for MCP OAuth clients

API keys work on any enabled surface whatever its login mechanisms. The console manages the external url, the token lifetimes and the REST surface on its API Access page; its MCP page (Configuration → MCP) holds the MCP settings, shows the state of every MCP endpoint, enables the management endpoint with one click once the prerequisites below hold, and gives the connect commands for Claude Code and Codex.

The server refuses to start with a surface enabled unless the transport prerequisites are met:

  • An HTTPS listener (on by default, port 25223) or security.trusted_proxies for a TLS-terminating proxy.
  • A canonical https origin in api.external_url, defaulting to security.callback_url when that is set, and otherwise to the HTTPS listener on the default app domain (https://<system.default_domain>:<https.port>, logged at startup). This value backs the OAuth metadata and token bindings — treat it as stable; changing it invalidates outstanding tokens. Set it explicitly whenever clients reach the server by a name other than the default domain.
openrun.toml
[security]
callback_url = "https://openrun.example.com:25223"

For real deployments, configure automatic TLS certificates; the default self-signed certificate works for the OpenRun CLI (with skip_cert_check) but most MCP clients reject it.

The [api] section is also part of the dynamic config, so the surfaces can be enabled and reconfigured at runtime without a restart (the same transport prerequisites are validated on update).

Connect the Remote CLI

On the remote machine, point the client at the server:

client openrun.toml
server_uri = "https://openrun.example.com:25223"

[client]
skip_cert_check = false  # true only for self-signed certs

The global --server-uri flag (or the OPENRUN_SERVER_URI env variable) takes precedence over server_uri in the config file, which allows one client config to be used against multiple servers:

openrun --server-uri https://staging.example.com:25223 app list

Then either log in interactively:

openrun login             # opens the browser, authenticates against [api.rest] auth,
                          # stores short-lived tokens in the OS keychain
openrun app list
openrun logout            # revokes the session server side

When [api.rest] auth lists more than one mechanism the login page offers the choice; openrun login --auth <name> selects an [auth.*] or saml_<name> login directly. Listing the auth provider of an app lets the users of that app log in from the CLI and run its actions as themselves.

or use an API key (for CI and headless use):

# on the server (or as any user holding apikey:manage:self):
openrun apikey create --desc "ci deploy"

# on the remote machine:
export OPENRUN_API_KEY=orun_pat_...   # or set client.api_key in the config file
openrun app list

openrun login tokens rotate automatically (access tokens live 1 hour, refreshed transparently) and the whole grant expires after api.grant_max_ttl (90 days), after which a fresh login is required. API keys expire after 90 days by default; --expires changes that and --expires=never is required for a non-expiring key.

Connect an MCP Client

This section covers OpenRun’s own management MCP endpoint. OpenRun serves three kinds of MCP endpoint:

EndpointToolsLoginOn by default
/_openrun/app_mcpthe actions of every app the user can run, one tool eachthe app login named by ?auth=; the server’s default app auth without ityes
<app>/mcpthe actions of one app, or the MCP server the app deploysthe app’s own authyes, for an app with actions
/_openrun/mcpmanagement: apps, versions, services, secrets, config, and the generic run_action[api.mcp] authno

A fresh install has no apps and therefore no action tools: the first two endpoints list nothing until an app with actions is deployed, which the client shows as an empty tool list, not a failed connection.

The MCP endpoint is https://<external_url host>/_openrun/mcp. There are two ways to authenticate:

OAuth (browser login) — for clients that support MCP OAuth, like Claude Code, add the server and the client discovers the OpenRun authorization server, identifies itself, and opens a browser login (against the mechanisms in [api.mcp] auth: admin, builtin, the name of an [auth.*] OAuth/OIDC provider, or saml_<name> for a [saml.*] provider; with several mechanisms the login page offers a choice, with a single federated one it goes straight to that provider). The consent page defaults MCP sessions to read-only scopes (*:read plus app:read_detail, so the client can inspect apps, versions, files and logs but change nothing); choosing broader scopes at consent is an explicit act.

Clients identify themselves in one of three ways, in the order the MCP spec recommends: a pre-registered client id (the OpenRun CLI uses openrun-cli), a Client ID Metadata Document (the client id is an https URL serving a JSON document with the client’s name and redirect URIs; OpenRun fetches and validates it on demand and caches it per its Cache-Control headers), or Dynamic Client Registration (/_openrun/oauth/register), which the MCP spec has deprecated but OpenRun keeps for older clients. Metadata document clients are accepted from any domain by default; api.cimd_allowed_domains and api.cimd_denied_domains restrict them, and documents on private or loopback addresses are refused unless api.cimd_allow_private_hosts is set (development only). Document fetches always connect directly and ignore HTTPS_PROXY, so the address check applies to the document host itself. Only public clients (token_endpoint_auth_method absent or none) are supported. The consent page names the document URL and warns when a client only redirects to localhost, since any website can publish such a document.

claude mcp add --transport http openrun https://openrun.example.com:25223/_openrun/mcp

The endpoint url can name the login to use: https://<host>/_openrun/mcp?auth=<name>, where the name is one of [api.mcp] auth (system is accepted for admin). The login page then offers that login only, a single federated one is started right away, and credentials of the other listed logins are not accepted through it. This only selects the login: the token is a normal MCP token, valid at the endpoint with or without the param.

claude mcp add --transport http openrun "https://openrun.example.com:25223/_openrun/mcp?auth=google_openrun"

API key — mint an MCP-bound key and pass it as a bearer header:

openrun apikey create --resource mcp --desc "claude on laptop"
claude mcp add --transport http openrun https://openrun.example.com:25223/_openrun/mcp \
  --header "Authorization: Bearer orun_pat_..."

An MCP-only key with no explicit --scopes defaults to *:read app:read_detail — the AI client can inspect everything its user can read, including app details, versions, files and logs, but cannot change anything. Mint a write-capable key deliberately with --scopes "*". A key bound to mcp is rejected at the REST surface and vice versa (--resource all for a key valid on both).

Destructive tools (delete, promote, version switch, …) support dry_run, and for clients that support elicitation the server runs a dry-run preview and asks for confirmation before applying the change. A client which cannot confirm is refused by default ([api.mcp] destructive_confirm = "require"); "ask" runs the call at once for such clients and "skip" never asks. A key created with openrun apikey create --confirm skip skips the prompt where the mode is ask, for headless automation. See side-effect hints for the modes.

API Keys

openrun apikey create [--user builtin:alice] [--expires 30d|never] \
    [--scopes "app:*,sync:read"] [--resource rest|mcp|all] [--desc "text"]
openrun apikey list [--all]     # --all (every user's keys) requires admin
openrun apikey delete <id>
  • The key value is shown once at creation and stored only as a hash.
  • --user defaults to the caller. Creating a key for another user requires admin and is prominently audited.
  • Scopes are a ceiling, not a grant: the key can never do more than its user’s RBAC grants allow, and scopes only narrow that further. A * scope deliberately does not cover secret:reveal, binding:reveal, app:approve, config:update or admin — those must be named literally in --scopes, so a key carrying reveal- or admin-class authority is visible as such.
  • A key created by a remote (credential-authenticated) caller can only be as powerful as the creating credential: scopes and resources must be subsets, and the new key cannot outlive its parent.
  • Expired and revoked credentials are pruned automatically 30 days after they die.

Keys for common AI client tasks (each is still limited by the user’s RBAC grants):

openrun apikey create --resource mcp                                   # inspect: read-only management (the default scopes)
openrun apikey create --resource mcp --scopes "app:*"                  # deploy: create, reload, promote, delete apps (approve needs app:approve named)
openrun apikey create --resource mcp --scopes "*:read,app:read_detail,app:access"   # inspect and run actions through run_action
openrun apikey create --user builtin:alice --resource app_mcp:builtin --scopes app:access   # run actions through /_openrun/app_mcp as the builtin user alice (without --user the key is the caller's: admin on the unix socket)
openrun apikey create --resource app:/orders                           # one app's own MCP endpoint

Four layers decide whether a call succeeds, and their errors name themselves: the client’s own approval policy (Codex and Claude Code ask before a tool runs, independently of OpenRun; destructive_confirm = "skip" only removes OpenRun’s confirmation prompt), the credential’s scopes (credential scope ceiling), the user’s RBAC grants (does not have ... permission), and the operations disabled for the surface (disabled for the mcp API surface). Running an app’s actions also needs the app’s plugin permissions approved.

What MCP Can Do

The MCP tool set mirrors the management API: apps (list/create/delete/approve/reload/promote/preview/versions), sync, services, bindings, secrets metadata, config read, API key self-management and more. RBAC decides per identity what actually succeeds, and every call is audited with the invoker type and credential id.

The list_actions, get_action, run_action and suggest_action tools run app actions as the calling user, under the checks the app’s form UI applies to that user (the app’s login provider, app:access and the action’s permit). A credential limited by scopes needs app:access in its scopes: the read only default of openrun apikey create --resource mcp does not include it (--scopes app:access). The result of run_action has the rows of a download or image result with their urls, not the files: use the MCP endpoint of the app for actions whose files the client should receive. To keep AI clients away from actions, add run_action to disable_apis. The runs of async actions are managed with list_action_runs, get_action_run and cancel_action_run.

Dangerous operations are disabled for MCP by default and are not even registered as tools:

OperationWhy
stop_server, restart_serverserver lifecycle
secret_reveal, secret_rekeyreading back / re-encrypting secret values
binding_show_accountrevealing service account credentials
provider_install, provider_uninstallinstalls binaries the server executes
user_add, user_deletebuiltin user management
config_updatecan grant every other permission
create_apikey_other, delete_apikey_othercredentials for other users

Operations can be opted back in, or additionally disabled, per surface:

openrun.toml
[api.mcp]
enable_apis  = []                # opt default-disabled operations back in (logged at startup)
disable_apis = ["secret_create"] # turn additional operations off
destructive_confirm = "require"  # require, ask or skip for destructive tools called by clients which cannot confirm

[api.rest]                       # the same overrides for the remote REST surface
disable_apis = []

Note on secrets: secret_create is enabled for MCP by default, which means secret values typed into an AI client transit that client and the model’s context by design. Disable it (as above) if secret writes must never pass through an AI client. Reading values back stays disabled unless explicitly enabled.

Defaults Summary

SettingDefault
api.rest enable, api.mcp enablefalse — no management surface
api.app_mcp enabletrue — the actions of all apps are served at /_openrun/app_mcp (not with security.unsafe_disable_rbac)
api.rest auth, api.mcp auth["admin"] — the admin account is the only login mechanism
/_openrun/app_mcp login?auth= on the url, default security.app_default_auth_type (none: no login, open apps only)
TransportHTTPS only (or security.trusted_proxies); plaintext is a 404
RBACAlways on; default grant gives every principal app:access + app:read
API key expiry (api.pat_default_ttl)90 days; --expires=never must be explicit
API key resource (--resource)rest
MCP-only key scopes*:read app:read_detail (read-only) unless --scopes given
OAuth access token (api.access_token_ttl)1 hour
OAuth refresh token (api.refresh_token_ttl)30 days per rotation
Absolute login lifetime (api.grant_max_ttl)90 days, then log in again
MCP consent scope presetread-only
MCP dangerous operationsdisabled (table above)
Destructive tool confirmationon, for clients supporting elicitation
Credential cleanupexpired/revoked keys pruned after 30 days

Security Best Practices

  • One identity per person, one key per client. Create keys with --user builtin:<name> (or the SSO principal) and a --desc naming where it lives. Audit events record the acting user and the credential id, so shared keys destroy attribution.
  • Prefer openrun login for humans. It stores rotating short-lived tokens in the OS keychain and is revoked with one openrun logout. Reserve long-lived API keys for automation.
  • Keep AI clients read-only until proven. The MCP defaults (read-only key scopes, read-only consent preset, destructive confirmation, disabled dangerous ops) are the intended starting point; widen deliberately, one capability at a time.
  • Name dangerous scopes explicitly. Since * never matches secret:reveal, binding:reveal, app:approve, config:update or admin, any credential carrying that authority shows it in openrun apikey list — review for keys with literal dangerous scopes.
  • Bind keys to one surface. The rest/mcp resource binding stops a key leaked from an MCP client config being replayed against the REST API and vice versa; avoid --resource all.
  • Let keys expire. Keep the 90 day default, rotate CI keys, and audit openrun apikey list --all for stale never keys. Deleting a key takes effect immediately.
  • Use real certificates. skip_cert_check and self-signed certs are for trials; automatic TLS makes real certificates easy. Only list actual TLS-terminating proxies in security.trusted_proxies, and bind the plaintext listener to a private address in that setup.
  • Watch the audit log. Every MCP call, to this endpoint, to an app’s MCP endpoint and to /_openrun/app_mcp, is an audit event of type mcp naming the method, the tool, the client and the credential, so “what did this AI client call” is one filter. Every remote call carries invoker=rest or invoker=mcp plus cred=<id> in the audit detail, and calls refused by the per-surface operation policy are audited too — “what has MCP done (or tried)” is a single filter.
  • Keep unsafe_* flags off in production. security.unsafe_disable_rbac and friends are dev-only; they cannot be set through the dynamic config API, and disabling RBAC also disables the remote surfaces.
  • Narrow the default grant if apps should not be reachable by every authenticated user. Replace the default * grant with group-scoped grants in the RBAC config.

Config Reference

openrun.toml
[api]
external_url = ""                # canonical https origin; defaults to security.callback_url
access_token_ttl = "1h"          # OAuth access token lifetime
refresh_token_ttl = "720h"       # OAuth refresh token lifetime per rotation (30d)
grant_max_ttl = "2160h"          # absolute OAuth grant lifetime (90d)
federated_identity_ttl = "720h"  # provider group snapshot max age
pat_default_ttl = "2160h"        # default API key expiry (90d)

[api.mcp]                        # the MCP endpoint
enable = false
auth = ["admin"]                 # login mechanisms: "admin", "builtin", [auth.*]/[saml.*] names (never empty)
enable_apis = []                 # default-disabled operations opted back in
disable_apis = []                # additional operations turned off
destructive_confirm = "require"  # what happens to a destructive tool call from a client which cannot confirm

[api.rest]                       # the management REST API over TCP (remote CLI)
enable = false
auth = ["admin"]                 # login mechanisms for openrun login (never empty)
enable_apis = []
disable_apis = []

[api.app_mcp]                    # /_openrun/app_mcp: the actions of all apps as MCP tools
enable = true                    # on by default
list_ttl = "3m"                  # freshness hint of the tool list
max_tools = 300                  # the tool list fails above this, narrow the apps glob
allowed_auth = []                # logins the auth url param may name; empty = any configured one

[api.app_mcp] is the MCP endpoint for the actions of all apps. It has no auth list: the login comes from the url (?auth=), by default security.app_default_auth_type. Unlike the two management surfaces it is on by default: it gives a user nothing the apps’ own pages do not, each action runs under the app’s checks. It is not served with security.unsafe_disable_rbac set, and a login needs an OAuth issuer origin (https, or http on localhost for development); without one only the token-less auth=none view works. With it on, the OAuth authorization server endpoints (/_openrun/oauth/*) are active on the server.