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 athttps://<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:
[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 clientsAPI 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_proxiesfor a TLS-terminating proxy. - A canonical https origin in
api.external_url, defaulting tosecurity.callback_urlwhen 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.
[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:
server_uri = "https://openrun.example.com:25223"
[client]
skip_cert_check = false # true only for self-signed certsThe 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 listThen 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 sideWhen [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 listopenrun 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:
| Endpoint | Tools | Login | On by default |
|---|---|---|---|
/_openrun/app_mcp | the actions of every app the user can run, one tool each | the app login named by ?auth=; the server’s default app auth without it | yes |
<app>/mcp | the actions of one app, or the MCP server the app deploys | the app’s own auth | yes, for an app with actions |
/_openrun/mcp | management: apps, versions, services, secrets, config, and the generic run_action | [api.mcp] auth | no |
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/mcpThe 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.
--userdefaults to the caller. Creating a key for another user requiresadminand 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 coversecret:reveal,binding:reveal,app:approve,config:updateoradmin— 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 endpointFour 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:
| Operation | Why |
|---|---|
stop_server, restart_server | server lifecycle |
secret_reveal, secret_rekey | reading back / re-encrypting secret values |
binding_show_account | revealing service account credentials |
provider_install, provider_uninstall | installs binaries the server executes |
user_add, user_delete | builtin user management |
config_update | can grant every other permission |
create_apikey_other, delete_apikey_other | credentials for other users |
Operations can be opted back in, or additionally disabled, per surface:
[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
| Setting | Default |
|---|---|
api.rest enable, api.mcp enable | false — no management surface |
api.app_mcp enable | true — 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) |
| Transport | HTTPS only (or security.trusted_proxies); plaintext is a 404 |
| RBAC | Always 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 preset | read-only |
| MCP dangerous operations | disabled (table above) |
| Destructive tool confirmation | on, for clients supporting elicitation |
| Credential cleanup | expired/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--descnaming where it lives. Audit events record the acting user and the credential id, so shared keys destroy attribution. - Prefer
openrun loginfor humans. It stores rotating short-lived tokens in the OS keychain and is revoked with oneopenrun 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 matchessecret:reveal,binding:reveal,app:approve,config:updateoradmin, any credential carrying that authority shows it inopenrun apikey list— review for keys with literal dangerous scopes. - Bind keys to one surface. The
rest/mcpresource 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 --allfor staleneverkeys. Deleting a key takes effect immediately. - Use real certificates.
skip_cert_checkand self-signed certs are for trials; automatic TLS makes real certificates easy. Only list actual TLS-terminating proxies insecurity.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 typemcpnaming the method, the tool, the client and the credential, so “what did this AI client call” is one filter. Every remote call carriesinvoker=restorinvoker=mcppluscred=<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_rbacand 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
[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.