MCP server
The apps-mcp server lets coding agents (Claude Code, Codex, or any MCP client) launch and manage apps with natural language. It is a thin, well-described tool layer over the REST API — no business logic of its own, so agent launches behave exactly like UI launches.
Endpoint: https://apps.<cluster-domain>/mcp (streamable HTTP — the UI’s nginx proxies
/mcp to the MCP service, so the whole platform lives on one hostname).
Connecting
Section titled “Connecting”# Claude Codeclaude mcp add --transport http nebari-apps https://apps.example.ai/mcpOr in any MCP client configuration:
{ "mcpServers": { "nebari-apps": { "type": "http", "url": "https://apps.example.ai/mcp" } }}Authentication
Section titled “Authentication”Two ways to present an identity:
- Device flow (interactive) — call the
authenticatetool. It returns a Keycloak verification URL and user code; the user approves in a browser, the agent callsauthenticateagain, and the token is cached (and refreshed) per MCP session. The public device-flow client is provisioned automatically by the pack’sNebariApp(auth.deviceFlowClient). - Bearer passthrough — clients that already hold a token send a standard
Authorization: Bearer <token>header.
Enforcement is layered:
- At the MCP layer — middleware verifies the token’s signature, issuer, and expiry
against the realm JWKS before any tool logic runs. Every tool except
authenticate(the login bootstrap) is rejected for anonymous or forged callers with an error telling the agent to callauthenticate. - At the API layer — apps-api independently validates the JWT on every request, so the MCP can never act with more privilege than the calling user.
On clusters with auth disabled, authenticate reports not_required and every tool works
anonymously.
| Tool | Purpose |
|---|---|
authenticate | Start/complete the device-flow login, or confirm the cached token. |
describe_cluster | Source types, launchable namespaces, apps domain — call first to pick valid options. |
launch_app | Create + launch (static, or Python/pixi via pixi_task). Idempotent on (namespace, name) — re-launching updates instead of failing, so retries are safe. |
list_apps | Apps with phase, URL, owner; optional namespace filter. |
get_app | Full spec + status for one app. |
get_app_status | Lightweight poll: phase, URL, replicas, conditions. |
get_app_logs | Recent pod logs — first stop when phase is Failed. |
update_app | Patch spec (source, runtime, access, …). |
stop_app / start_app | Scale to zero / back up. |
restart_app | Roll the app’s pods without changing the spec; fails if the app is stopped. |
get_app_metrics | Instantaneous CPU/memory per pod (available:false when the cluster has no metrics server). |
remove_app | Delete permanently (cascades; the tool description tells agents to confirm with the user). |
Every tool returns structured JSON. Errors carry the API’s human-readable detail, and a
401 includes nextAction: call the authenticate tool, so agents can self-recover.
Example agent flow
Section titled “Example agent flow”“Launch the site in ./docs-site as a public app called docs-site”
describe_cluster→ namespaces["apps", ...], appsDomainapps.example.ailaunch_appwithsource_type: inline, the directory’s files asinline_files,public: true,subdomain: docs-siteget_app_statusuntilphase: Running- Report
https://docs-site.apps.example.aiback to the user
For a Python app (“launch the pixi app in ./py-app with task start”), step 2 adds
pixi_task: "start" and the files must include a pixi.toml (or pyproject.toml).
Pair it with the scaffolding skill: the skill generates apps with a
nebari-app.yaml manifest, and “launch it” maps that manifest onto launch_app with no
ambiguity.