hosted-auth is the HTTP app the Docker image serves. It checks the bearer on each request, validates OAuth tokens against opik-backend, serves the OAuth discovery documents and records who the caller is. Open it to learn what a host sees when a token is missing, expired or an API key, and where to change that.
Every request to the MCP path needs Authorization: Bearer <token>. A token
that starts with OAUTH_ACCESS_TOKEN_PREFIX (in src/opik_mcp/identity/context.py)
is an OAuth token. Any other bearer is an API key.
| OAuth token | API key | |
|---|---|---|
| Checked by opik-mcp | Introspected on every request, with a short cache | No |
| Forwarded to opik-backend | The full Authorization header |
The full Authorization header |
| Workspace | Taken from the token by opik-backend; an inbound Comet-Workspace header is forwarded and checked against it |
The inbound Comet-Workspace header, else OPIK_WORKSPACE (or COMET_WORKSPACE), else default |
| Dead credential | HTTP 401 invalid_token, so the host refreshes. A backend 401 on a data call first comes back as a tool error inside HTTP 200; the retry gets the 401 |
A tool error inside HTTP 200 that says to check OPIK_API_KEY and OPIK_WORKSPACE |
Whether a hosted API-key call should fall back to OPIK_WORKSPACE is an open
question in runtime.
/.well-known/ or /mcp/.well-known/ (_is_unauth_path).Bearer or an empty token gets 401
{"error": "unauthorized"}.invalid_token, and
nothing is forwarded. Hosts run the refresh_token grant on this code.WWW-Authenticate challenge whose resource_metadata
is the protected-resource URL at the host root of OPIK_MCP_RESOURCE_URI,
or the relative path when that is unset (_resource_metadata_url). The served
app always sends it. A URL under the resource path would hit the auth middleware (#139).initialize succeeds, and the
first tool call carries the backend’s answer as a tool error./.well-known/oauth-protected-resource serves the RFC 9728 document. The
paths in _PROXIED_OAUTH_PATHS are proxied to OPIK_MCP_AS_URL. Both
answer 503 when that setting is unset./health/ready sends a HEAD to COMET_URL_OVERRIDE (the default Comet URL if empty).
A 5xx, a timeout or a network error is not ready; a malformed URL is a 500 on purpose.src/opik_mcp/identity/store.py is checked first. A miss
posts the inbound header to /opik/auth-oauth under the Opik REST base
(introspect_oauth_token in src/opik_mcp/identity/oauth.py).OPIK_MCP_OAUTH_VALIDATION_CACHE_TTL_S, never
past expires_at minus EXPIRY_SKEW_MARGIN_S. A cached token whose
expires_at has passed gets 401 without a backend call.resource that differs from OPIK_MCP_RESOURCE_URI is logged and served.note_backend_401 in src/opik_mcp/client/base.py. It drops the cached
validation, and the tool error (OAUTH_TOKEN_EXPIRED_HINT) tells the model
to retry. The retry gets the invalid_token 401 and the host refreshes.opik-mcp never chooses an OAuth call’s workspace. opik-backend rejects a forwarded
header that does not match the token (McpOAuthService.verifyWorkspaceHeaderMatchesToken,
per the comment on inbound_workspace). The introspected name is display-only
(resolved_workspace_name); its precedence is in tool-surface.
Caller identity feeds analytics and never decides access. The rule is in
caller_identity_with_outcome (src/opik_mcp/identity/caller.py): an OAuth
token uses what introspection stored for it, an inbound API key is a miss, and
stdio resolves the install’s own key through src/opik_mcp/identity/account.py
on cloud Comet only.
host -> AuthRejectionMiddleware -> BearerAuthMiddleware
(shape check, OAuth validation, ContextVars set)
-> MCP session manager -> session task
-> install_request_auth_rebinding (ContextVars from this request)
-> tool -> resolve_opik_config -> OpikClient -> opik-backend
build_app in src/opik_mcp/server/app/factory.py,
the OAuth and HTTP fields of Settings in src/opik_mcp/config.py.BearerAuthMiddleware.dispatch._validate_oauth_bearer, then the two modules above._PROXIED_OAUTH_PATHS.Boundaries: runtime owns the outbound client and
resolve_opik_config, analytics the rejection
event and identity fields, release the image and
chart, tool-surface the instructions and links.
tools/call, because the SDK forks the
session task from initialize and a tool would forward the handshake’s token
after a refresh (#182).Comet-Workspace header (#151).classify_bearer and again inside
resolve_opik_config. Both must match opik-backend’s prefix. On a mismatch
a real OAuth token takes the API-key path and the backend answers 403.classify_bearer returns. The session store is keyed by the full
Authorization header. A lookup with the other input silently misses._validate_oauth_bearer caches a valid answer before it compares
resource. A rejecting audience check has to run before
remember_validation, or the next request is served from cache.tests/identity/test_http_auth.py, tests/identity/test_oauth_passthrough_mode.py.test_upstream_401_in_api_key_mode_keeps_the_api_key_wording.invalid_token, fail-open, the cache and its eviction:
tests/identity/test_oauth_token_validation.py, tests/identity/test_oauth_identity.py.tests/identity/test_oauth_token_rotation.py.test_resolve_opik_config_oauth_detection_is_prefix_not_substring.tests/identity/test_oauth_protected_resource.py,
tests/identity/test_resource_metadata_url.py, tests/identity/test_oauth_redirect.py.tests/server/test_http_path_config.py, tests/server/test_health.py.tests/identity/test_credential_identity.py.test_a_forwarded_api_key_bearer_is_not_resolved_as_our_own;
the install’s own key: tests/identity/test_account_identity.py.tests/analytics/test_auth_rejected.py.invalid_token, with a validation cache, so hosts refresh (#182).