MCP OAuth Callback
When a user authorizes an MCP server with ark mcp auth login, the IdP redirects the browser back to ark-api with the authorization code. ark-api exchanges the code for tokens and writes the result to the MCPServer’s referenced Secret.
The redirect target is a single externally reachable URL that ark-api must own. This page covers configuring ARK_API_PUBLIC_CALLBACK_URL for both public ingress and air-gapped clusters.
Env vars
| Env var | Default | Description |
|---|---|---|
ARK_API_PUBLIC_CALLBACK_URL | http://127.0.0.1:34780/v1/mcp/auth/callback | Externally reachable URL the IdP redirects back to. MUST be HTTPS unless the host is a loopback literal (127.0.0.1, [::1] bracketed per RFC 3986 §3.2.2, or localhost). The path /v1/mcp/auth/callback is appended automatically if the URL has no path. The chart default is a loopback literal that ark mcp auth login makes reachable through its automatic ark-api port-forward (port 34780). Set to empty to disable the four auth/* endpoints (they return 503). |
ARK_API_MCP_AUTH_CACHE_TTL_SECONDS | 600 | TTL of in-flight flow entries stored in the Secret. After this window a late callback returns “unknown state”. |
ARK_API_MCP_AUTH_DCR_TIMEOUT_SECONDS | 15 | HTTP timeout for the RFC 7591 registration POST. |
ARK_API_MCP_AUTH_TOKEN_TIMEOUT_SECONDS | 15 | HTTP timeout for the token-exchange POST. |
ARK_API_DASHBOARD_URL | unset | Base URL of the dashboard, used to redirect the browser back after a dashboard-initiated flow. The redirect target is <ARK_API_DASHBOARD_URL>/mcp, so the value MUST include any path prefix under which the dashboard is served (e.g. https://ark.example.com/dashboard behind an X-Forwarded-Prefix). Same scheme rules as ARK_API_PUBLIC_CALLBACK_URL (HTTPS unless the host is a loopback literal). Validated at startup. Required only for the dashboard redirect-completion path — the CLI flow is unaffected. When unset, dashboard flows fall back to the HTML completion page. |
ark-api refuses to start if ARK_API_PUBLIC_CALLBACK_URL is set to a non-HTTPS public host. Unbracketed IPv6 literals are also rejected. It also logs a warning when the host is a non-literal that merely resolves to loopback (e.g. a nip.io name): such a value is accepted by ark-api but rejected by RFC 8252-strict IdPs (Notion, Google) as a redirect URI — prefer a loopback literal or a public HTTPS URL.
Public ingress
For clusters with an Ingress in front of ark-api, set the env var to the public HTTPS URL:
env:
- name: ARK_API_PUBLIC_CALLBACK_URL
value: https://ark.example.com/v1/mcp/auth/callbackThe same URL MUST be one of the redirect URIs registered with the IdP (ark-api enforces this against the RFC 7591 DCR response).
Air-gapped port-forward
Clusters without public ingress run the flow through kubectl port-forward. The IdP redirects to a loopback URL on the operator’s workstation, where kubectl forwards the connection into the cluster.
CLI flow. ark mcp auth login does this automatically: it already port-forwards ark-api to 127.0.0.1:34780, which is exactly the host and port of the chart default callback URL. No manual port-forward is needed for the CLI flow.
Dashboard flow. Under devspace dev the callback port is forwarded automatically (services/ark-api/devspace.yaml forwards 34780:8000), so the dashboard flow completes with no manual step on a local install. For any other deployment the dashboard does not port-forward: run a manual port-forward matching the callback URL, or override ARK_API_PUBLIC_CALLBACK_URL with a public HTTPS ingress.
kubectl port-forward --address 127.0.0.1,::1 svc/ark-api 34780:80The --address 127.0.0.1,::1 flag binds the listener to both IPv4 and IPv6 loopback. This avoids a class of bugs where the OS resolves localhost to ::1 but kubectl only listens on 127.0.0.1 (or vice versa) and the IdP redirect times out.
Configure ark-api to advertise the same loopback URL (matching the forwarded port):
env:
- name: ARK_API_PUBLIC_CALLBACK_URL
value: http://127.0.0.1:34780/v1/mcp/auth/callbackUse bracketed IPv6 if the cluster prefers v6:
env:
- name: ARK_API_PUBLIC_CALLBACK_URL
value: http://[::1]:34780/v1/mcp/auth/callbackThe loopback carve-out also accepts the literal localhost:
env:
- name: ARK_API_PUBLIC_CALLBACK_URL
value: http://localhost:34780/v1/mcp/auth/callbackRegister the chosen URL with the IdP as a redirect URI before running the flow.
Authenticate an MCP server from the dashboard
The dashboard MCP servers page (/mcp) drives the same ark-api OAuth flow as the CLI, so a user who sees a server in the Required state can authorize it without dropping to a terminal. Each card surfaces the server’s authorization state as a badge (Auth required, Authorized, Discovery failed) and exposes the relevant actions.
- Authenticate (state
Required) — starts the flow and full-page-navigates the browser to the IdP. After login/consent the IdP redirects to ark-api’s callback, which redirects the browser back to the dashboard. If the server has nospec.authorization.tokenSecretRef.name, ark-api provisions it as<name>-oauthautomatically — no manualkubectledit is needed. - Re-authenticate (state
Authorized) — same as Authenticate but forces a fresh flow against an already-authorized server (last login wins). The action is emphasised when the authorization is near expiry; the card shows an expiry indication derived fromstatus.authorization.expiresAt. - Sign out (state
Authorized) — opens a confirmation dialog and, on confirm, revokes the authorization (clears the tokens, keeps the Secret) so the card returns toRequired.
Completion confirmation. On return from the IdP, ark-api redirects to <ARK_API_DASHBOARD_URL>/mcp with the flow’s auth_id (success) or an auth_error (failure / expired flow). The dashboard then polls the same auth/status endpoint the CLI uses until the controller reconciles the MCPServer to Authorized, showing a success, failure, or expired toast. No token material reaches the browser — the redirect carries only the server name, namespace, an opaque auth_id, and (on failure) an error code and a length-capped description.
Configuration. The dashboard flow requires ARK_API_DASHBOARD_URL (see Env vars) so ark-api knows where to redirect the browser. The value MUST include any path prefix under which the dashboard is served. When it is unset, dashboard-initiated flows fall back to ark-api’s HTML completion page (the CLI flow is unaffected either way). As with the CLI, the IdP redirect still lands on ARK_API_PUBLIC_CALLBACK_URL, which must be reachable from the browser. Under devspace dev this is handled automatically by the ark-api callback port-forward; other deployments use a public HTTPS ingress or a manual loopback port-forward.
RBAC
ark-api needs get/create/patch/update/delete on Secrets in every namespace it serves, plus get/update on mcpservers (for reading status.authorization, provisioning spec.authorization.tokenSecretRef, and clearing legacy auth annotations). The chart applies these rules automatically; see services/ark-api/chart/templates/rbac.yaml.
Callback URL changes
If you change ARK_API_PUBLIC_CALLBACK_URL after servers have been authorized, the IdP’s stored redirect_uri will no longer match. Run ark mcp auth login --force against any previously-authorized server to re-register with the new URL.
Verifying
A successful login is reflected in the MCPServer’s status.authorization.state:
kubectl get mcpserver notion-mcp -o jsonpath='{.status.authorization.state}'Authorizedark-api does not record the identity of the caller who triggered the flow. The referenced Secret holds the token material; ark-api never logs token bodies, refresh tokens, client secrets, or PKCE verifiers.
External executor header-resolution workaround
When an agent targets an external ExecutionEngine (claude-agent-sdk, LangChain, or any marketplace executor) and authorizes an MCPServer that is configured solely via spec.authorization.tokenSecretRef — whether via ark mcp auth login or the dashboard Authenticate action, which drive the same flow — the SDK’s _resolve_mcp_server() will not inject the Bearer token on tool calls.
Why this happens: the controller already reads spec.authorization.tokenSecretRef for tool discovery and injects the token on each MCP call. External executors build their MCPServerConfig at query dispatch time and read only spec.headers[] — they do not read spec.authorization. So the MCPServer discovers tools correctly, but the executor’s tool calls will 401.
Workaround: add a redundant spec.headers[] entry pointing at the same Secret and key:
spec:
authorization:
tokenSecretRef:
name: notion-mcp-oauth # set by ark mcp auth login
headers:
- name: Authorization
value:
valueFrom:
secretKeyRef:
name: notion-mcp-oauth # same Secret as tokenSecretRef.name
key: access_token # or the accessTokenKey override if setThe redundancy is intentional: the controller reads spec.authorization for discovery; this header entry lets the SDK read the same token for dispatch. The default key is access_token unless spec.authorization.tokenSecretRef.accessTokenKey overrides it.
This workaround becomes unnecessary once the mcp-auth-sdk-header-resolution capability lands; that capability will teach _resolve_mcp_server() to read spec.authorization directly.
Known limitations
- Single-user token model. Only one set of tokens is stored per MCPServer. In multi-user clusters the last login wins.
- No chainsaw e2e tests. End-to-end tests are blocked on a TLS-capable mock MCP server. This is tracked as a follow-up.
Related
ark mcp auth login/logout— CLI reference- MCPServer reference —
status.authorizationstate machine