# Opik MCP Server **The official Model Context Protocol (MCP) server for [Opik](https://github.com/comet-ml/opik), the open-source LLM observability and evaluation platform, built by [Comet](https://www.comet.com).** Plug your AI host (Claude Code, Cursor, VS Code Copilot, Codex, opencode, or any MCP client) directly into your Opik workspace: read traces, log scores, and save prompt versions, all from the chat. Built for LLM engineers who already run Opik and want to drive it from the same AI assistant they code with. ``` You: "Which traces in project 'demo' failed today?" Claude: → list(entity_type="trace", project_name="demo") → "Three traces failed…" You: "Score trace 7f2e… 0.9 on helpfulness with reason 'great recovery'." Claude: → write(score.create) → done ``` --- ## Quick start One command sets Opik up in the AI clients on your machine, Claude Code included. It finds your Opik (the one saved in `~/.opik.config`, one running on localhost, or it asks which), registers the MCP server, installs the Opik skill pack, and checks the connection. On Opik Cloud there is no API key to handle: your client signs in in the browser. It needs [`uv`](https://docs.astral.sh/uv/) and no Opik SDK: ```bash uvx opik@latest mcp configure ``` Without a terminal, as from a coding agent or a script, name the client: ```bash uvx opik@latest mcp configure --ai-client claude-code --skills ``` `--ai-client` also takes `codex`, `cursor`, `vscode` and `opencode`. Run that way it uses the Opik saved in `~/.opik.config`, API key included, or one answering on localhost, and otherwise stops and asks for `--deployment`. A coding agent asked to "install opik mcp" gets the full steps, including what to ask the user, from `uvx opik-mcp@latest --help`. They are the same as [Install with a coding agent](#install-with-a-coding-agent) below. Registering the server by hand, with the client commands under [Set it up by hand](#set-it-up-by-hand-no-opik-check-no-skill-pack), skips the check of which Opik you use and the skill pack, so it is for clients the installer doesn't support. | Your Opik | Server | Transport | Sign-in | |---|---|---|---| | Opik Cloud (`www.comet.com`) | [the hosted server](#opik-cloud-the-hosted-server), run by Comet | Streamable HTTP | in the browser (OAuth); no API key | | Self-hosted Comet, open-source Opik | [the local server](#self-hosted-and-open-source-opik-the-local-server), this package | stdio | env vars; an API key only where the deployment needs one | Clients load MCP servers when a session starts, so start a new session afterwards. Setup guide, troubleshooting and FAQ: [comet.com/docs/opik/mcp-server](https://www.comet.com/docs/opik/mcp-server). --- ## Opik Cloud: the hosted server Comet runs the server at `https://www.comet.com/opik/api/v1/mcp`. Your client connects over HTTP and opens a browser sign-in the first time. There is nothing to install, no API key, and no workspace to set: the server works in the workspace you pick when you sign in. After adding it, start a new session and ask: **"list my Opik projects"**. The [Quick start](#quick-start) command sets this up for Claude Code, Codex, Cursor, VS Code Copilot and opencode, with the skill pack. The buttons below add it to Cursor or VS Code in one click; other ways to register it by hand are under [Set it up by hand](#set-it-up-by-hand-no-opik-check-no-skill-pack). [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=opik-mcp&config=eyJ1cmwiOiJodHRwczovL3d3dy5jb21ldC5jb20vb3Bpay9hcGkvdjEvbWNwIn0%3D) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=opik-mcp&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fwww.comet.com%2Fopik%2Fapi%2Fv1%2Fmcp%22%7D) --- ## Self-hosted and open-source Opik: the local server `opik-mcp` runs on your machine: the client starts it with `uvx opik-mcp` and talks to it over stdio. The [Quick start](#quick-start) command sets it up from your `~/.opik.config` or a local Opik; to register it by hand, see [Set it up by hand](#set-it-up-by-hand-no-opik-check-no-skill-pack). Install [`uv`](https://docs.astral.sh/uv/) once; it fetches the package, and Python 3.13 if needed, on first use: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux # or: brew install uv ``` `uvx opik-mcp` reuses the copy uv has cached, so it starts quickly; `uv cache clean opik-mcp` makes the next start fetch the newest release. `uvx opik-mcp@latest` asks PyPI on every start, which adds about a second and a half. Codex waits `mcp_optional_startup_grace_ms` (1 s by default) for servers before it builds the first tool list, so a slower start can leave the tools out of the first turn. The one-off commands in this README (`opik mcp configure`, `opik mcp status`, `--help`, `--check`) carry `@latest`, so they never run a copy uv cached months ago. Env vars point the server at your Opik: | Your Opik | Env vars | |---|---| | Open-source Opik on this machine | `OPIK_URL=http://localhost:5173/api` | | Open-source Opik on a server | `OPIK_URL=https:///api`, plus `OPIK_API_KEY` only if the deployment adds authentication | | Self-hosted Comet platform (the Opik UI is at `https:///opik`) | `COMET_URL_OVERRIDE=https://`, `OPIK_WORKSPACE`, `OPIK_API_KEY` | - Open source serves its API at `/api` and has one workspace, `default`, so it needs no `OPIK_WORKSPACE`. `COMET_URL_OVERRIDE` would point the server at `/opik/api`, which open source does not serve. - A self-hosted Comet has named workspaces. Set `OPIK_WORKSPACE` to the segment after `/opik/` in your Opik URL; left out, reads come from your account's default workspace, with no error to say so. - A self-hosted Comet that runs the MCP OAuth server (off by default) can use the hosted flow instead, at `https:///opik/api/v1/mcp`. - Substitute every value. The server refuses a placeholder workspace such as `` or `${input:OPIK_WORKSPACE}`; a placeholder URL is sent as-is and fails to connect. - `OPIK_MCP_ANALYTICS_SOURCE=""` in the `env` block opts a self-hosted install out of the cloud-Comet source label on telemetry events. Before adding the server to a client, check the settings: run it with the same env and `--check`. It makes one call to Opik and prints the workspace and how many projects it sees, or what to fix with a link to the docs, and exits 0 or 1. A client can't tell you this before a new session: Claude Code shows Connected even with a wrong key, and Codex starts nothing until then. ```bash OPIK_URL=http://localhost:5173/api uvx opik-mcp@latest --check ``` Then register it with your client: [the local server, by hand](#local-server-self-hosted-and-open-source-opik). --- ## Install with a coding agent For an AI agent asked to install the server, as in "install opik mcp". The aim is the server registered for the user's Opik, the Opik skill pack installed, and the user signed in. Work out which Opik from this machine, and ask the user only what it can't tell you: one question per message, each answer before the next. The installer in step 3 does the whole setup in one run, so settle steps 1 and 2 before you run it. `uvx opik-mcp@latest --help` prints these steps. 1. **See what is there.** ```bash uvx opik@latest mcp status ``` It shows the Opik saved in `~/.opik.config` (URL and workspace, never the key) and the clients that already have the server. In Claude Code, also run `claude mcp list`: it shows claude.ai connectors and entries under other names. It also prints each entry's command, which for an old npx entry can hold an API key: don't repeat it. - A server for `https://www.comet.com/opik/api/v1/mcp` under any name, such as a claude.ai connector, means it is registered: don't add a second one. Install the skill pack on its own with `npx skills add comet-ml/opik-skills -g --all`. Go to step 4 if the server shows `! Needs authentication`. - The installer replaces an existing `opik-mcp` entry without asking, such as one for the old npx server. Ask the user before you run it. 2. **Find the user's Opik.** - Saved in `~/.opik.config`: use it, and tell the user which one you are setting up. The installer reads it, API key included, so don't ask for a key. - No `~/.opik.config`: `curl -s http://localhost:5173/api/is-alive/ping`. An answer is an open-source Opik on this machine. In a sandboxed shell, such as Codex's, no answer can mean the shell has no network rather than no Opik. - Neither: ask, and don't pick one yourself. An Opik URL in the project, such as `OPIK_URL_OVERRIDE` in `.env`, makes it a yes-or-no question. Otherwise: "Which Opik do you use: Opik Cloud (www.comet.com), an open-source Opik (its URL), or a self-hosted Comet platform (its URL and workspace)?" 3. **Run the installer** for the client you run in. It installs the Opik skill pack with the server, with or without `--skills`; keep the flag in a command you give the user to run later. On Opik Cloud it opens the browser sign-in and waits a minute for it, so tell the user first. ```bash uvx opik@latest mcp configure --ai-client claude-code --skills ``` `--ai-client` also takes `codex`, `cursor`, `vscode` and `opencode`. For an Opik saved in `~/.opik.config` or running on localhost, run it as it is: `--deployment` sets the saved config aside, API key included. With neither, add the flag for the Opik the user named in step 2: | The user's Opik | Add | |---|---| | Opik Cloud, `www.comet.com` | `--deployment cloud` | | Open-source Opik | `--deployment local`, and `--url ` when it is not on localhost | | A self-hosted Comet platform, `/opik/` in its URL | `--deployment self-hosted --url --workspace `, with `OPIK_API_KEY` in the shell | If it fails, as when the Opik doesn't answer, tell the user what it printed and what to check, such as a VPN or the URL. Don't switch to another Opik. 4. **Sign-in, for Opik Cloud.** If the installer reports the server as not signed in yet, give the user `claude mcp login opik-mcp` to run, or `/mcp` in a Claude Code session, rather than running it yourself: it waits on the browser. A claude.ai connector signs in from `/mcp`. The sign-in asks for a workspace; name the one saved in `~/.opik.config`, if any. 5. **Finish.** Say what was installed and what is left. Name the skill pack: the [Opik skills](https://github.com/comet-ml/opik-skills#skills-in-this-pack), installed for every project, which teach the agent to instrument, evaluate and debug with Opik. Clients load MCP servers and skills when a session starts: ask the user to start a new session, then try **"list my Opik projects"**. On every path: - Don't ask the user to paste an API key into the chat. The hosted server needs none; otherwise take it from the shell environment (`"$OPIK_API_KEY"`), or let the user run the command. - Don't guess the workspace. It is the segment after `comet.com/opik/` (or `/opik/` on a self-hosted Comet) in the user's Opik URL. - Don't print `~/.claude.json` or `~/.opik.config`: both can hold API keys. `uvx opik@latest mcp status` and `claude mcp list` show what you need, and the installer reads the config itself. - If a permission check blocks a command, don't work around it. Give the user the command to run (in Claude Code, `!` before it runs it from the prompt). For a client the installer doesn't know, register by hand with the commands in [Set it up by hand](#set-it-up-by-hand-no-opik-check-no-skill-pack): the hosted server for Opik Cloud, the local server for the rest. - Before registering the local server, run it with the env you will pass and `--check`, with `OPIK_API_KEY` exported in the shell rather than written on the command: `OPIK_URL=http://localhost:5173/api uvx opik-mcp@latest --check`. It exits 0 and names the workspace it sees, or prints what to fix. Once registered, Claude Code reports Connected even with a wrong key, and Codex has no check. - Claude Code refuses to add over an existing `opik-mcp` entry, so remove it first with `claude mcp remove opik-mcp --scope user`; Codex's `add` replaces it. - After registering, in Claude Code, read only the status line: `claude mcp get opik-mcp | grep Status`. The full output prints the `env` block, API key included. The hosted server shows `! Needs authentication` until `claude mcp login opik-mcp` has run. Codex has nothing that starts the server before a session; `codex mcp get opik-mcp` shows what was stored, with env values masked. --- ## Set it up by hand (no Opik check, no skill pack) The [Quick start](#quick-start) command does all of this for Claude Code, Codex, Cursor, VS Code Copilot and opencode, and also checks which Opik you use and installs the skill pack. Register by hand only for another client, or to see what the installer writes. A coding agent installing for a user runs the installer: see [Install with a coding agent](#install-with-a-coding-agent). ### Hosted server (Opik Cloud) #### Claude Code, by hand ```bash claude mcp add --scope user --transport http opik-mcp https://www.comet.com/opik/api/v1/mcp ``` Then sign in from `/mcp` → **Authenticate** in a Claude Code session. It opens a browser, so the person at the keyboard does it; from their own terminal, `claude mcp login opik-mcp` does the same. `--scope user` makes the server available in every project; without it, Claude Code registers it for the current directory only. Over SSH, `claude mcp login opik-mcp --no-browser` prints the sign-in URL to open on your own machine; the last step needs an interactive terminal (`ssh -t`). #### Codex, by hand ```bash codex mcp add opik-mcp --url https://www.comet.com/opik/api/v1/mcp ``` `add` opens the sign-in; `codex mcp login opik-mcp` opens it again. In `~/.codex/config.toml` the same server is: ```toml [mcp_servers.opik-mcp] url = "https://www.comet.com/opik/api/v1/mcp" ``` #### Cursor, by hand Use the button under [the hosted server](#opik-cloud-the-hosted-server), or add to `~/.cursor/mcp.json`: ```json { "mcpServers": { "opik-mcp": { "url": "https://www.comet.com/opik/api/v1/mcp" } } } ``` #### VS Code Copilot, by hand Use the button under [the hosted server](#opik-cloud-the-hosted-server), or add to `.vscode/mcp.json` in your workspace or to your user `mcp.json` (**MCP: Open User Configuration**): ```json { "servers": { "opik-mcp": { "type": "http", "url": "https://www.comet.com/opik/api/v1/mcp" } } } ``` #### Other clients Any client that takes a URL can use the hosted server: ```bash npx add-mcp https://www.comet.com/opik/api/v1/mcp --name opik-mcp ``` A client that can only start local commands can reach it through `npx -y mcp-remote https://www.comet.com/opik/api/v1/mcp`. #### Opik Cloud with an API key Where nobody can complete the browser sign-in, as when an agent runs unattended from a script or a client has no MCP OAuth support, use an API key from [`comet.com/api/my/settings/`](https://www.comet.com/api/my/settings/) instead, with [the local server](#self-hosted-and-open-source-opik-the-local-server) pointed at Opik Cloud: ```bash claude mcp add --scope user opik-mcp \ --env OPIK_API_KEY="$OPIK_API_KEY" \ --env OPIK_WORKSPACE= \ -- uvx opik-mcp ``` The other clients take the same two variables in their `env` block. Set `OPIK_WORKSPACE` to the segment after `comet.com/opik/` in your Opik URL (`https://www.comet.com/opik/acme-ai/projects` → `acme-ai`). Left out, the server sends `default`, which Comet resolves to your account's default workspace, so reads can come from the wrong workspace without an error. ### Local server (self-hosted and open-source Opik) The examples use a local open-source Opik. For a self-hosted Comet, swap in the three variables from [the table](#self-hosted-and-open-source-opik-the-local-server). After adding the server, start a new session and ask: **"list my Opik projects"**. #### Claude Code, by hand ```bash claude mcp add --scope user opik-mcp --env OPIK_URL=http://localhost:5173/api -- uvx opik-mcp ``` On a self-hosted Comet, with the key in your shell's `OPIK_API_KEY`: ```bash claude mcp add --scope user opik-mcp \ --env COMET_URL_OVERRIDE=https:// \ --env OPIK_WORKSPACE= \ --env OPIK_API_KEY="$OPIK_API_KEY" \ -- uvx opik-mcp ``` Or edit `~/.claude.json` directly: ```json { "mcpServers": { "opik-mcp": { "type": "stdio", "command": "uvx", "args": ["opik-mcp"], "env": { "OPIK_URL": "http://localhost:5173/api" } } } } ``` `claude mcp get opik-mcp` shows `✔ Connected` once the server starts. That does not prove the URL or key are right, because only a tool call reaches Opik; `--check` ([the local server](#self-hosted-and-open-source-opik-the-local-server)) does. It also prints the `env` block, API key included. #### Codex, by hand ```bash codex mcp add opik-mcp --env OPIK_URL=http://localhost:5173/api -- uvx opik-mcp ``` Or edit `~/.codex/config.toml`: ```toml [mcp_servers.opik-mcp] command = "uvx" args = ["opik-mcp"] env = { OPIK_URL = "http://localhost:5173/api" } # On a self-hosted Comet, forward the key from the environment Codex starts in, # so it stays out of this file: # env_vars = ["OPIK_API_KEY"] # Time allowed for the server to start (default 10 s); the first start # downloads the package. startup_timeout_sec = 30 ``` Codex gives a local server only the variables in `env` and the names in `env_vars`, so a key exported in your shell does not reach it otherwise. #### Cursor, by hand Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project), or use **Cmd+Shift+J → Features → Model Context Protocol**: ```json { "mcpServers": { "opik-mcp": { "type": "stdio", "command": "uvx", "args": ["opik-mcp"], "env": { "OPIK_URL": "http://localhost:5173/api" } } } } ``` > **Cursor 60s timeout.** Cursor enforces a hard tool-call timeout that doesn't > reset on progress notifications. See [Known host limits](#known-host-limits). #### VS Code Copilot, by hand Add to `.vscode/mcp.json` in your workspace, or to your user `mcp.json` (**MCP: Open User Configuration**): ```json { "servers": { "opik-mcp": { "type": "stdio", "command": "uvx", "args": ["opik-mcp"], "env": { "OPIK_URL": "http://localhost:5173/api" } } } } ``` ### MCP Inspector (manual testing) ```bash OPIK_URL=http://localhost:5173/api npx @modelcontextprotocol/inspector uvx opik-mcp ``` --- ## Coming from `npx opik-mcp`? The TypeScript server (npm `opik-mcp@2`) is deprecated and stops serving requests on **2026-11-15**. For a new install, run the [Quick start](#quick-start) command; `uvx opik-mcp` is the command a client runs to start the server, not an installer. To move an existing config on Opik Cloud, switch to the hosted server and drop the API key. Otherwise, in your MCP client config, replace `npx -y opik-mcp` with `uvx opik-mcp`. Some env vars were renamed and command-line flags are no longer read: see the [migration guide](https://github.com/comet-ml/opik-mcp/blob/main/legacy/typescript/MIGRATION.md). Support policy: [`DEPRECATED.md`](https://github.com/comet-ml/opik-mcp/blob/main/legacy/typescript/DEPRECATED.md). The TypeScript source is at the git tag `legacy-typescript-final`. --- ## Tools `opik-mcp` exposes a small, outcome-oriented surface that covers the full lifecycle (read → annotate → curate → author → iterate). | Tool | Purpose | |---|---| | [`read`](#read) | Universal read by id / name / `opik://` URI | | [`list`](#list) | Universal list with optional name filter + pagination | | [`write`](#write) | Universal write — log traces/spans, score, comment, save prompts, manage datasets & experiments | | [`schema`](#schema) | Introspect write-operation schemas (used by the LLM to construct valid payloads) | | `read_skill` | Read one of the Opik agent skills bundled with this server | ### `read` One tool for any "show me X" question. Takes an `entity_type` plus an `id` (UUID or, for nameable types, a name) or a full `opik://` URI. Composite reads (`trace`, `prompt`, `thread`, `agent_insights_issue`) inline their children so a single call returns the full picture. The record you name comes back whole. Inlined children do not: their bodies are fetched with the backend's `truncate=true`, so a field over ~10 KB is cut in ClickHouse and base64 images are replaced with `"[image]"` — one attachment echoed across 200 spans would otherwise cost more than everything else in the read. The answer says so in `spanBodies` / `messageBodies`, and any child is whole again through its own `read("span", id)` or `read("trace", trace_id)`, which hit endpoints that have no `truncate` parameter at all. An inlined collection is also bounded in length: 200 spans, 200 turns, 100 prompt versions. Past that, `spansTruncated` / `messagesTruncated` / `versionsTruncated` is `true` and a `moreSpans` / `moreMessages` / `moreVersions` line beside it carries the count and the exact `list(...)` call that continues from where the inlined part stopped. **Supported entities:** `project`, `trace`, `span`, `dataset`, `dataset_item`, `experiment`, `prompt`, `thread`, `agent_insights_issue`. Name-based lookup is available for `project`, `experiment`, `prompt`, `dataset` (slower — two API calls — and may return multiple matches). `thread` and `agent_insights_issue` are project-scoped: pass `project_id` or `project_name`, or a link/URI that carries the project. `dataset` and `dataset_item` were called `test_suite` and `test_suite_item` before; the old names still resolve, but they are not advertised and new code should use the new ones. ```python read(entity_type="trace", id="7f2e3c8a-…") read(entity_type="project", id="demo") # name lookup read(entity_type="trace", id="opik://traces/7f2e3c8a-…") read(entity_type="agent_insights_issue", id="", project_id="") read( entity_type="agent_insights_issue", id="https://www.comet.com/opik//projects//diagnostics?issue=", ) ``` A link copied from the Opik UI works as the `id`: a thread link or a Diagnostics page link carries the project, so no `project_id` is needed and the entity type is taken from the link. A `project` read answers "how is my project doing" in one call. It returns `{project, summary, vocabulary, contains, url}`: the record, then the four figures the Logs page shows as cards (trace count, error rate, average duration, total cost) for the last 7 days against the 7 before, SDK traffic only, as on screen. `since` / `until` move that window; `since="30d"` is what the UI opens on. A rate or an average over a period with no traces comes back as `null`, because 0% errors on a week with no traffic reads as a healthy week. `vocabulary` is the map you need before you can ask anything else: the project's feedback score names, its token usage keys, and the automation rules scoring its traces. These are the names that go into a filter or into `series=` below, and guessing them returns an empty page that reads like good news. Score names and rules are capped, always report the true total, and name the call that returns the rest; usage keys are listed in full, since nothing else enumerates them. `contains` names the freshest experiment, dataset, prompt version and optimization run, so "what has been happening here" does not need four more calls. A part that failed to load says so instead of looking empty, and an empty one is omitted. An `agent_insights_issue` read returns `{issue, example_trace_ids, details}`: the Diagnostics issue record (name, description, cause, suggested fix, severity, status), the deduplicated ids of the traces that exhibit it (the same sample the Diagnostics page shows — open one with `read("trace", id)`), and the per-day breakdown. Trace bodies are not inlined, so the read stays one backend call. `since` / `until` narrow the per-day rows; the default is all-time. When the server knows the Opik URL and the session's workspace, the read also carries `url` (the issue's Diagnostics page) and `trace_url_template` (a deep link for any of the example traces), so the assistant can hand you something clickable; under an OAuth session whose workspace could not be resolved the links are omitted rather than guessed. Traces themselves carry no URL — a link for one is not derivable from the fields a `read` or `list` returns, and a guessed shape 404s. The session instructions name a template for it instead, `.../v1/session/redirect/projects/?trace_id={trace_id}&path=...`, so the assistant fills in an id and hands you a link. It goes through opik-backend's redirect, which resolves the project and the workspace from the trace, so it works where a direct project URL cannot, an OAuth session with an unresolved workspace included. It is the same link the Python SDK prints for a trace. ### `list` Browse or search a collection with pagination. Project-scoped types (`trace`, `span`, `thread`, `agent_insights_issue`, `dataset_item`, `prompt_version`) need their parent: a project UUID or name, a dataset UUID, or a prompt UUID. ```python list(entity_type="experiment", page=1, size=25) list(entity_type="experiment", name="rerank") # name substring filter list(entity_type="agent_insights_issue", project_name="demo") # open Diagnostics issues list(entity_type="agent_insights_issue", project_id="", status="resolved") list(entity_type="trace", project_name="demo") # latest traces of one project list( entity_type="trace", project_name="demo", filters="error_info is_not_empty AND duration > 5000" ) list( entity_type="span", project_name="demo", # spans across the whole project filters='type = "llm" AND usage.total_tokens > 10000', ) list( entity_type="thread", project_name="demo", filters="number_of_messages > 20 AND feedback_scores.helpfulness < 0.5", ) list(entity_type="experiment", filters='dataset_id = "" AND tags contains "baseline"') ``` **Filters.** `trace`, `span`, `thread`, `experiment` and `dataset_item` take an OQL string, the same grammar as the SDK's `search_traces(filter_string=…)`: ``` [.] [AND ...] ops: = != > >= < <= contains not_contains starts_with ends_with is_empty is_not_empty in not_in ``` Strings go in double quotes, numbers are bare, `duration` is in milliseconds, dates are ISO-8601 instants with a timezone (`"2026-09-08T10:00:00Z"`). Scores and dictionaries take a key: `feedback_scores.accuracy < 0.5`, `metadata.environment = "prod"`. `AND` is the only connector. Like the UI's Logs page, trace, span and thread lists add `source = "sdk"` so evaluator, playground and experiment traces stay out of the way; name `source` yourself to see them. The first output line echoes the filter that was applied. A bad filter fails before reaching the backend with what is needed to fix it: the position of a syntax error, the closest field name, the valid operators for the field's type, or the expected value format. Fields with a closed set of values (`source`, span `type`, thread `status`, `visibility_mode`) are checked against it too, every element of an `in` list included. `source` is the one the backend validates itself, and it answers an unknown value with a 500 rather than a 400, so `source = "SDK"` would otherwise be an opaque server error for a capital letter. The rest are compared as strings and answer with an empty page, which reads as "no matches" when it means "no such value". Ask `schema("list.trace")` (or `list.span`, `list.thread`, `list.experiment`) for the full field reference, accepted values included. **Finding one case in a dataset.** `list(entity_type="dataset_item", dataset_id=…)` filters on the case itself: `data.` for the keys the dataset was built with, `full_data` for a substring of the whole payload (a full scan — name a key when you can), plus `id`, `tags`, `source`, `trace_id`, `span_id` and the timestamps. `data.` takes the six string operators only (`=`, `!=`, `contains`, `not_contains`, `starts_with`, `ends_with`); the backend answers a comparison with a 400, so this one is refused before the call. The endpoint has no sorting and no free-text search — `sort` is refused rather than dropped. `read(entity_type="dataset_item", id=…)` returns one case whole, which is how a value the table cut is read back. ```python list(entity_type="dataset_item", dataset_id="", filters='data.question contains "install"') list( entity_type="dataset_item", dataset_id="", filters='trace_id = ""' ) # the case made from that trace read(entity_type="dataset_item", id="") # the case, uncut ``` With `experiment_ids` the same list is the comparison instead — the cases with each run attached — and it filters on the runs (`feedback_scores.`, `output`, `duration`). The two are different field sets on two backend endpoints: `schema("list.dataset_item_case")` is the dataset's own cases, `schema("list.dataset_item")` the comparison. **Sort.** `trace`, `span`, `thread` and `experiment` take `sort=" [asc|desc]"`, `desc` by default and one field only: `sort="duration desc"`, `sort="total_estimated_cost"`, `sort="feedback_scores.accuracy asc"`, `sort="usage.total_tokens"`. The field is checked against the entity's sortable list before the call, because the backend silently ignores fields it cannot sort by. On very large workspaces the backend drops sorting altogether; the header says so when that happens. `dataset_item` sorts only as a comparison (with `experiment_ids`): the items endpoint takes no sorting parameter, so a sort on a plain listing is refused rather than dropped. **Time window and search.** `trace`, `span` and `thread` take `since` and `until`, each a relative span (`"30m"`, `"1h"`, `"7d"`) or an ISO-8601 instant with a timezone, so "the last hour" needs no clock arithmetic. The window is by record creation time, which is cheap for the backend and agrees with `start_time` within seconds for live traffic. For an exact bound, put `start_time` in `filters`. The same three types take `search`, free text matched anywhere in id, name, input, output, metadata, tags and thread id. Search scans the whole project on the backend, so the first call on a large project can take tens of seconds. Those calls get a 60-second timeout. Adding `since` makes them fast again. **Reading the table.** Durations are labelled `duration_ms` / `ttft_ms` and shown as whole milliseconds; the field stays `duration` in `filters` and `sort`. Timestamps are shown to the second and costs as plain decimals. Project rows carry `last_updated_trace_at` so you can see which project has live traffic; thread rows carry the first message. An empty page under a time window says when the project's last trace landed, and an empty page under the default `source = "sdk"` says how to see the other sources. A misspelled `project_name` comes back with the closest existing name. ```python list( entity_type="trace", project_name="demo", since="1h", filters="error_info is_not_empty", sort="duration desc", ) list(entity_type="trace", project_name="demo", search="order-42") ``` **Diagnostics issues.** `agent_insights_issue` is the Diagnostics page over the MCP: the recurring failures Opik's Diagnostics job grouped for a project, ranked as the UI ranks them (most recently seen first). Columns are `severity`, `status`, `total_occurrences` (all-time sum), `latest_count` (the most recent report day, the number the issue's own description refers to) and `last_seen`. Open issues are listed by default; pass `status="resolved"` or `"closed"` for the rest. `read` and `list` also answer to `issue`, which is what the UI calls these; the long name is the one in the `entity_type` enum, so that one entity does not appear there twice. Counts are all-time so they match the UI; the same `since` / `until` as for traces narrow the window, truncated to UTC report days because Diagnostics aggregates per day. An empty list says why it is empty, because "nothing is broken" and "nobody turned Diagnostics on" read the same otherwise. There are five states: Diagnostics is unavailable on this deployment, not enabled for this project, turned off, enabled but not scanned recently, or enabled and clean with the time of the last scan. The ones you can act on name the call to make, and every state links the project's Diagnostics page. A non-empty list dates itself. The issues are whatever the last scan grouped, so the reply ends with `Report covers data through