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, writes the result to the MCPServer’s referenced Secret, and stamps the parent MCPServer with an annotation that records who triggered the flow.
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 and stamping 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 leaves two visible side-effects on the MCPServer:
kubectl get mcpserver notion-mcp -o jsonpath='{.metadata.annotations}'{
"ark.mckinsey.com/mcp-auth-authorized-by": "cli",
"ark.mckinsey.com/mcp-auth-authorized-at": "2026-05-19T14:32:11Z"
}authorized-by records who triggered the flow: cli for CLI logins (and any unauthenticated in-cluster caller), or the authenticated user’s resolved identity for dashboard-initiated flows (captured from ark-api’s impersonation middleware at auth/start time).
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; the
authorized-byannotation surfaces who last authorized. - No chainsaw e2e tests. End-to-end tests are blocked on a TLS-capable mock MCP server. This is tracked as a follow-up.
- MCPServer delete+recreate. Deleting and recreating an MCPServer leaves stale
authorized-by/authorized-atannotations on the old resource. The new resource starts clean.
Related
ark mcp auth login/logout— CLI reference- MCPServer reference —
status.authorizationstate machine