opik-mcp

runtime

Purpose

Runtime is how the opik-mcp process starts, where its settings come from and how a tool call reaches the Opik REST API. It answers: which credential and workspace does a call use, what does a backend error or timeout turn into, when does the process refuse to start, and why does no tool take a workspace?

What it does now

Start

Credential and workspace

resolve_opik_config() in src/opik_mcp/client/base.py returns (base_url, api_key, workspace) for every call:

OpikClient._headers sends the credential verbatim as Authorization: an inbound header (OAuth or API key) as the host sent it, usually Bearer <token>, and OPIK_API_KEY raw, with no Bearer. A missing credential sends no Authorization. A missing workspace (an OAuth bearer and no inbound header) sends no Comet-Workspace.

Open question: a hosted request with an API key and no Comet-Workspace falls back to the process’s OPIK_WORKSPACE. .claude/rules/security.md forbids an environment fallback when the caller supplied a value; does a missing header count?

Errors

Reads and lists map a non-2xx answer to a typed error in _raise_for_status, with an entity hint and an excerpt of the backend’s message.

Status Error error_kind
401 OpikAuthError auth
403 OpikPermissionError (subclass of OpikAuthError) permission
404 OpikNotFoundError not_found
400, 422 OpikValidationError validation
5xx, other OpikServerError upstream_5xx

A 2xx other than 200, or a body that is not a JSON object, is also OpikServerError. Writes get the raw response from OpikClient.write_json, which never raises on status; writes builds the error.

Transport errors stay httpx exceptions in the client. list turns a timeout into a tool error that says how to narrow the call, and any other httpx.HTTPError into “Could not reach Opik” (_as_tool_error). read catches only the typed errors around its main fetch. The optional blocks of a composite read also catch httpx.HTTPError and give up after DEADLINE_SECONDS (src/opik_mcp/read_list/decorations.py).

Timeouts and connections

How it works

read/list -> client_for_call -> make_opik_client -> resolve_opik_config
          -> OpikClient.get_* / list_* -> _get_json | _post_json -> _raise_for_status
write     -> writes/dispatch.py -> make_opik_client -> OpikClient.write_json

Decisions

Traps

Proven by

Log