Iris

io.github.iris-eval由 GitHub 帳號 iris-eval 發布v0.21.0更新於 Nov 9, 2026

Stop shipping agents on vibes. Score every agent output for quality, safety, and cost.

概覽

AI 產生的概覽

Iris 在本機記錄 AI 代理的執行,以確定性規則評估輸出的品質、安全與成本,並可選擇接入 LLM 評審。

可以做什麼
  • 記錄並保存代理執行,包含輸入、輸出、工具呼叫、span、token 與成本,可在同一次呼叫中直接評分
  • 依內建的完整性、相關性、安全與成本規則評分,並讀取是否放行的結論及其依據與證據
  • 搜尋、篩選和刪除已存軌跡,並讀取能力、規則準確率、軌跡、評估、儀表板摘要與稽核等資源
  • 比較兩次執行或重複案例的迴歸、不穩定與通過率,並依目前規則重新評分一次執行
  • 管理自訂規則:查看規則清單、部署新規則、停用或刪除規則
  • 可選用自己的金鑰呼叫 LLM 評審輸出,並查核引用是否被來源支持
適用情境
適合建置或維運 AI 代理、希望每次執行都有可稽核數字而非憑感覺判斷的情境:在依據回答行動前先檢查、把變更與上一次執行比較,或在 CI 中以品質與安全規則做把關。
執行需求
以本機 stdio 程序執行,可用 npx 或全域 npm 安裝;需要 Node.js 22.13 或更新版本。資料存放在 Iris 主目錄下的 SQLite(預設 ~/.iris,可用 IRIS_HOME 或 IRIS_DB_PATH 變更)。不需要帳號。LLM 評審與引用查核需要 IRIS_ANTHROPIC_API_KEY 或 IRIS_OPENAI_API_KEY。將 HTTP 傳輸或儀表板綁定到回送位址以外需要 IRIS_API_KEY 或 IRIS_API_KEY_FILE。
安裝前請注意
軌跡會把代理的輸入、輸出與工具呼叫存到本機磁碟,敏感內容會進入資料庫。LLM 評審會用你的金鑰把被評文本傳送給 Anthropic 或 OpenAI 並產生費用,受 IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL 與 IRIS_LLM_JUDGE_DAILY_BUDGET_USD 限制。只有設定 IRIS_CITATION_ALLOW_FETCH 時引用查核才會抓取被引頁面,並受 IRIS_CITATION_DOMAINS 限制。OTel 匯出與 webhook 會把資料送到本機之外。delete_trace 與 delete_rule 會刪除資料,已部署規則會改變之後每次評估。IRIS_ALLOW_UNAUTHENTICATED 會在非回送綁定上取消 API 金鑰要求。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Iris,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

Iris — stop shipping agents on vibes

[Glama Score] [Install in Cursor] [Install in VS Code] [npm version] [GitHub stars] [CI] [OpenSSF Scorecard] [OpenSSF Best Practices] [License: MIT] [Docker] [PulseMCP] [mcp.so]

Iris scores every agent run for quality, safety, and cost — on your machine, with no SDK and no account. Most agent projects check quality by running a few remembered prompts and eyeballing the output. Iris replaces that with numbers you can audit: your agent's runs land in a SQLite database on your disk, 26 built-in rules score them deterministically — PII, prompt injection, hallucination markers, cost thresholds, and the agent's own tool calls — free, with no LLM calls, and an optional LLM judge with a hard per-eval cost cap handles the semantic questions. Every rule is inspectable and editable, because a judge you can't audit is just vibes with a number on it. MIT licensed, no telemetry. Nothing leaves your machine unless you turn one of these on: an OpenTelemetry endpoint (IRIS_OTEL_ENDPOINT), which exports traces to the collector you name; the LLM judge with your own key, which sends the text it judges to that provider, and whose citation check fetches the pages an output cites; or a webhook, which posts ids, the verdict and rule names, never the text, to the address you set.

Requires Node.js 22.13 or later. Check with node --version.

[The demo: Failures, a failure opened, two runs compared]

The demo database, recorded by scripts/demo-media.mts; the source is demo.mp4. A still: dashboard-overview.png.

A failure on screen in 60 seconds

No agent wiring, no config — one command:

bash
npx @iris-eval/mcp-server --demo

This seeds a demo database — five small agents, two weeks of runs, every verdict the engine's own — and serves the dashboard against it at http://localhost:6920 (your browser opens automatically on first run). The dashboard lands on Failures: what failed, worst and newest first, each card naming the rule and its evidence. Worth clicking into — a PII leak caught by the safety rules, a hidden directive in a forum post that the summarizer complied with, a number the source document never said, two runs on the same twelve questions compared with an interval (Runs), a deployed custom rule and a paused one with their audit rows, and a failed LLM-judge score with its rationale.

Demo data lives in its own database (demo.db in your Iris home directory — ~/.iris on macOS/Linux, %USERPROFILE%\.iris on Windows) and never mixes with your real traces. Remove all of it with one command:

bash
npx @iris-eval/mcp-server --demo-clear

Hook up your own agent

First, prove the install works on this machine — it runs offline and opens nothing of yours:

bash
npx @iris-eval/mcp-server --self-test   # exit 0 = healthy

Then add Iris to your MCP client. One command writes the client's own config file, keeps every other server in it, and pins the version you ran:

bash
npx -y @iris-eval/mcp-server install claude-code

The clients: claude-code, claude-desktop, cursor, windsurf, continue, vscode, cline, zed, codex, gemini. install --list shows the ones found on this machine, the Iris each one runs and the file it reads; install <client> --uninstall takes Iris out again. Every client shares one database, so after an upgrade move them all at once with install --upgrade (Updating). Restart the client to load it.

Claude Desktop: one click. Each release from 0.20.0 on attaches iris-eval.mcpb, an MCP Bundle: download the latest, open it, and Claude Desktop shows an install dialog. Nothing on it is required — an Anthropic or OpenAI key for the LLM judge is optional, and the dashboard is a switch that starts off. The bundle holds the npm package and its dependencies, so nothing else needs installing: Claude Desktop runs it under the Node it ships when that Node is 22.13 or newer (Claude Desktop 1.1.6679 ships 24.13), and Iris stores traces with Node's built-in SQLite, in the same ~/.iris every other install uses. The release notes show how to verify its signature and build attestation.

It runs in any MCP client, and every client it names has a row with what was actually checked. Verified on every CI run: Claude Code, Gemini CLI — the real client starts Iris from the config the installer wrote and reports it connected (claude mcp list, gemini mcp list), on Linux, macOS and Windows; Claude Code's capture plugin hooks are driven through the real scripts too. Claimed from each client's own MCP documentation — the installer writes the configuration shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect: Claude Desktop, Cursor, Devin Desktop (Windsurf), Continue, VS Code, Cline, Zed, OpenAI Codex CLI. Every row with its source and the date it was read: https://iris-eval.com/clients. By hand instead, one block, dashboard included:

json
{  "mcpServers": {    "iris-eval": {      "command": "npx",      "args": ["-y", "@iris-eval/mcp-server", "--dashboard"]    }  }}

Your client lists Iris's twelve tools on connect, and the dashboard serves at http://localhost:6920. Now paste this to your agent:

Log that last task to Iris and evaluate the output.

The trace lands on the dashboard with its scores. Prefer the MCP server headless? Drop --dashboard from the args — you can open the same dashboard any time with npx @iris-eval/mcp-server --dashboard.

One thing worth knowing up front: MCP tools are called when the model decides to call them. Iris doesn't intercept your agent, so traces are logged when your agent asks it to log them — either because you told it to, or because your code calls the tools directly. Ask your agent to "log this to Iris and evaluate it" and it will. If you want capture that doesn't depend on the model choosing, POST /api/v1/traces does exactly that — your code sends the trace over plain HTTP, no model in the loop (see docs/http-ingest.md). The CLI and host hooks on the roadmap will be thin clients over the same endpoint.

Capture over HTTP (no model in the loop)

The ingest endpoint lives on the dashboard port — 6920 by default, not the MCP transport port — and it exists only while the dashboard is running. Pass --dashboard (or set IRIS_DASHBOARD=true); --transport http on its own does not start it, and a request to the transport port returns 404. With the dashboard up, anything that can send an HTTP request can log a trace — and optionally run the deterministic evals in the same request. GET /api/v1/capabilities on the same port says what this server can judge, what each rule needs, the judge state with the steps that enable it, and the limits — the same object the MCP resource iris://capabilities serves — so an HTTP caller has the frame an MCP client gets at initialize:

bash
curl -s -X POST "http://127.0.0.1:6920/api/v1/traces" \  -H "Content-Type: application/json" \  -d '{    "agent_name": "support-bot",    "input": "What is the refund policy?",    "output": "Refunds are available within 30 days of purchase.",    "evaluate": true,    "eval_type": "safety"  }'

Returns 201 with the stored trace_id and the evaluation result (in --demo mode the endpoint refuses writes with 403, so demo data never mixes with yours). The endpoint accepts the same body as the log_trace tool and sits behind the same middleware stack as the rest of the dashboard: loopback bind and the DNS-rebinding guard by default, plus Bearer auth when you set one. Two plain facts about it: it accepts unauthenticated writes unless Iris was started with --api-key (or IRIS_API_KEY) — the loopback bind is what keeps it to your machine by default, so set a key before binding beyond loopback; and what it stores is verbatim — input and output land in iris.db exactly as sent, including any text no_pii goes on to flag. Full contract, field reference, and error semantics: docs/http-ingest.md.

Capture every Claude Code turn (optional)

/plugin marketplace add iris-eval/mcp-server/plugin install iris-eval-capture@iris-eval

A second, separately installed plugin: its hooks record each turn's prompt, tool calls (the failed ones included, with the error the agent received) and final answer, keyed on the prompt, so an interrupted turn's calls and a background sub-agent's are sent too, and hand them to iris-eval ingest, detached, with critical spans redacted in the stored evaluation text — capture that does not depend on the model deciding to call a tool. A turn that ended in an API error is stored without being judged; when the model logged the turn too, both traces are kept and the plugin's names the model's. It never prints, never blocks, never sends anything anywhere. Installing iris-eval alone changes nothing about your turn loop. Limits and removal: claude-plugin-capture/README.md.

Python

bash
pip install iris-eval
python
from iris_eval import IrisClientiris = IrisClient()                       # IRIS_URL, or the running dashboard's runtime.jsoniris.evaluate_output("…", input="…", agent_name="support-bot")["verdict"]   # {"state": "pass", "basis": "clean", "by": []}

A thin client over the HTTP API of server 0.16.0 and later, versioned on its own — iris_eval.__version__ and the PyPI page carry its number, which is not the server's: log_trace(), evaluate_output(), get_traces(), get_trace(), health(), capabilities(), sync and async, typed answers, the server's own sentence on a refusal — and a pytest plugin: an iris fixture and assert_iris(output, expect="pass") that asserts on the verdict's state. packages/python/README.md.

Record every OpenAI and Anthropic call

python
from iris_eval import wrap_openaiclient = wrap_openai(OpenAI(), agent_name="support-bot")   # every call: a GenAI span to POST /v1/traces, scored
ts
import { wrapOpenAI } from '@iris-eval/sdk';const openai = wrapOpenAI(new OpenAI(), { agentName: 'support-bot' });

Wrap the provider client once and each model call becomes one OpenTelemetry GenAI span sent to the OTLP door, stored with its input, output, token usage and tool calls, and scored: capture that does not depend on the model calling a tool. wrap_openai / wrap_anthropic in the Python client; wrapOpenAI, wrapAnthropic and irisMiddleware for the Vercel AI SDK in @iris-eval/sdk. Both are not yet published (the next iris-eval release on PyPI; @iris-eval/sdk is built from source until its first npm release). Streams, the SDKs' stream helpers and tool calls are covered, the original client is not changed, and Iris being down never breaks a call — packages/sdk/README.md, packages/python/README.md.

Score every LangChain and LangGraph run

python
from iris_eval.langchain import IrisCallbackHandlergraph.invoke(inputs, config={"callbacks": [IrisCallbackHandler(agent_name="support-bot")]})

Each top-level run becomes one trace (the run, its model calls, its tool calls and its graph nodes as GenAI spans) with its input, output, tool calls, token usage and a verdict. Python in the client (pip install iris-eval, 0.2.0 and later), JavaScript as @iris-eval/langchain (in this repository, not yet published to npm). Both are proven in CI against a real LangGraph app with a scripted model; LangSmith's own OpenTelemetry export is proven the same way — docs/otel-recipes.md.

A CI gate, no server needed

bash
npx -y @iris-eval/mcp-server ingest --file traces.ndjson --evaluate --fail-on any

any fails the job on every verdict that is not a pass: a failure, and a verdict that was not checked. Name one basis instead (--fail-on detector_veto) to fail only on that one.

Or the GitHub Action (0.16.0), which fails the job on the verdicts you name, writes the receipt to the job summary and posts it as one pull-request comment updated in place: uses: iris-eval/mcp-server/.github/actions/[email protected] with traces: traces.ndjson — docs/ci-gate.md.

A fourth door (0.15.0): POST /v1/traces on the dashboard port takes the OTLP/HTTP JSON or protobuf your OpenTelemetry instrumentation already emits (the Python SDK's exporter speaks protobuf only, so this is the Python door too), and each OTLP trace becomes an Iris trace with its spans — docs/otel-integration.md; one recipe per framework (Pydantic AI, Google ADK, LangGraph via LangSmith, CrewAI, the OpenAI Agents SDK in Python and JavaScript, LlamaIndex, AutoGen, Microsoft Agent Framework, Semantic Kernel, the Vercel AI SDK and Mastra), each proved by a fixture, in docs/otel-recipes.md. ingest reads one JSON trace (or NDJSON, one per line) from stdin or a file, stores it, evaluates it under exactly the rules evaluate_output runs, prints one JSON line per trace with the verdict and its basis, and exits 1 when a verdict matches --fail-on. --dataset <id|label> restricts that gate to the case keys in a dataset (POST /api/v1/datasets promotes a run's case keys into one), so a job fails only on the cases you chose. The full recipe, the exit codes and the eight bases are in docs/ci-gate.md.

Write a rule as code

eval.plugins in config.json loads rules you wrote — an ES module whose default export is { name, kind, mechanism, version, needs, evaluate(ctx) } — pinned by the sha256 of the file, so a file that changed since you pinned it refuses startup rather than running. A loaded plugin fires like a built-in and shows on list_rules under plugins. The contract, the hash recipe and what a plugin may return: docs/plugins.md.

Use the engine in your own process

The evaluation engine is importable — no server, no database, no model:

ts
import { EvalEngine, defaultConfig } from '@iris-eval/mcp-server/engine';
const engine = new EvalEngine(defaultConfig.eval.defaultThreshold, defaultConfig.eval.ruleThresholds, defaultConfig.eval);const result = await engine.evaluateAll({ output: answer, input: prompt, toolCalls, costUsd });result.verdict.state;       // 'pass' | 'fail' | 'unknown', with result.verdict.basis and result.interpretations

The same engine, the same rules and the same composer the server runs; builtInRules(), createCustomRule(), compose() and the published-accuracy readers are exported beside it.

A typed client for the HTTP route

ts
import { createClient } from '@iris-eval/mcp-server/client';
const iris = createClient({ baseUrl: 'http://127.0.0.1:6920', apiKey: process.env.IRIS_API_KEY });const { trace_id, evaluation } = await iris.logTrace({ agent_name: 'support-bot', input, output, tool_calls, evaluate: true });evaluation?.verdict?.state;  // the same object evaluate_output returns

One body on every door: it is what log_trace and iris-eval ingest accept. A refusal throws IrisClientError with the server's own sentence and status. Both subpaths are checked from a packed tarball on every build.

Verify your install

bash
npx @iris-eval/mcp-server --self-test   # offline diagnostic; exit 0 = healthy, 1 = a check failednpx @iris-eval/mcp-server --version     # prints the bare version, e.g. 1.2.3

--self-test first creates your Iris home if it is missing and checks that it is writable (exit 1, naming the path, if it is not), reports where your database's search index is (whole, how many traces a background build has indexed so far, or no FTS5 on this SQLite), reads your database's schema (exit 1, with the fix, when this version or an MCP client pinned to an older release cannot open it), then runs its checks — storage round-trip, a planted SSN and a planted injection caught by the safety rules, dashboard boot, the DNS-rebinding guard — inside an isolated temp home. Your real database is only read, never changed. Everything Iris writes lives under one directory, your Iris home: ~/.iris by default (%USERPROFILE%\.iris on Windows), or wherever IRIS_HOME points. That is where iris.db, config.json, custom-rules.json, audit.log, preferences.json and the demo files live; point IRIS_HOME at a scratch directory to try Iris without touching your real data.

Setup by tool
ClientStatusWhat that meansRead
Claude Codeverifieda test drives the real client on every CI run2026-09-25
Claude Desktopclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Cursorclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Devin Desktop (Windsurf)claimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Continueclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
VS Codeclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Clineclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Zedclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
OpenAI Codex CLIclaimedthe installer writes the shape the client documents, and that writer is tested on the shape; nobody on the Iris side has watched it connect2026-09-25
Gemini CLIverifieda test drives the real client on every CI run2026-09-25

Every row with what was checked: iris-eval.com/clients. No client is called supported without a row.

npx -y @iris-eval/mcp-server install <client> writes each of these for you. By hand, per client:

Claude Desktop

Edit your MCP config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add the JSON config above, then restart Claude Desktop.

Claude Code
bash
claude mcp add --transport stdio iris-eval -- npx -y @iris-eval/mcp-server

Then restart the session (/clear or relaunch) for tools to load.

Windows note: Do not use cmd /c wrapper — it causes path parsing issues. The npx command works directly.

Cursor

Add the JSON config above to ~/.cursor/mcp.json (every project) or .cursor/mcp.json in a workspace, with "type": "stdio" in the iris-eval entry — Cursor's docs mark it required.

Devin Desktop (Windsurf)

Add the JSON config above to mcp_config.json: ~/.config/devin/mcp_config.json on macOS and Linux, %APPDATA%\devin\mcp_config.json on Windows.

Continue

Save the JSON config above as its own file in Continue's mcpServers folder: ~/.continue/mcpServers/iris-eval.json (every workspace) or .continue/mcpServers/iris-eval.json in one.

VS Code (native MCP)

Add to .vscode/mcp.json in your workspace (note: VS Code uses servers, not mcpServers):

json
{  "servers": {    "iris-eval": {      "command": "npx",      "args": ["-y", "@iris-eval/mcp-server"]    }  }}
Cline

Open Cline's MCP Servers panel → Configure MCP Servers, and add the mcpServers JSON config above to cline_mcp_settings.json (~/.cline/data/settings/cline_mcp_settings.json, shared by Cline in VS Code, JetBrains and the CLI).

Zed

Add to Zed settings.json:

json
{  "context_servers": {    "iris-eval": {      "command": "npx",      "args": ["-y", "@iris-eval/mcp-server"],      "env": {}    }  }}
OpenAI Codex CLI

Add to ~/.codex/config.toml:

toml
[mcp_servers.iris-eval]command = "npx"args = ["-y", "@iris-eval/mcp-server"]
Gemini CLI

Add the mcpServers JSON config above to ~/.gemini/settings.json. Gemini CLI connects to MCP servers only in folders it trusts: if gemini mcp list shows iris-eval as Disabled, run /permissions in that folder.

Anything else that speaks MCP

Iris is a standard stdio MCP server — one npx @iris-eval/mcp-server command, no SDK, no code changes. If your client supports MCP, it supports Iris. Client config formats change; when in doubt, check your client's MCP docs and point it at that command.

Other Install Methods

bash
# Global install (recommended for persistent data and faster startup)npm install -g @iris-eval/mcp-serveriris-eval --dashboard
# Docker — two servers, two ports: 3000 = MCP HTTP transport,# 6920 = dashboard (which also serves the POST /api/v1/traces ingest endpoint).# The image binds 0.0.0.0 inside the container, so a key is required (see Production).# The volume is the Iris home: the database, deployed rules and audit log persist in it.docker run -p 3000:3000 -p 6920:6920 -v iris-data:/data \  -e IRIS_API_KEY="$(openssl rand -hex 32)" ghcr.io/iris-eval/mcp-server

Tip: Global install (npm install -g) stores traces persistently at ~/.iris/iris.db. With npx, traces persist in the same location, but startup is slower due to package resolution.

What You Get

Trace LoggingHierarchical span trees with per-tool-call latency, token usage, and cost in USD. Stored in SQLite, queryable instantly.
Output Evaluation26 built-in rules across 4 categories: completeness, relevance, safety, cost. PII detection (21 patterns: SSN, credit card, phone, email, IBAN, DOB, MRN, IP, API key, passport, plus AWS/Slack/SendGrid/GitHub/Google/npm/DigitalOcean tokens, credentials inside URLs, secret-named assignments, PEM private-key blocks and seed phrases; date of birth, medical record number, passport and seed phrase fire only beside their label, by design), prompt injection (38 patterns, phrase + structural), stub-output detection, hallucination detection (25 context-grounded fabrication/contradiction signals — pass input to ground them against the agent's source material), and six trajectory rules that read what the agent DID: an unacknowledged failed tool call, a repeated one (by call, by repeated sequence, or by target once you send tools), a call whose arguments the tool's own JSON Schema rejects and the agent never retried, a file, directory or URL the answer cites that appears in nothing the agent read, an instruction that arrived inside a TOOL RESULT and was then obeyed by a later call, and a task that took more tool calls than your step budget. A trajectory can arrive as tool_calls or as OpenTelemetry TOOL spans. Add custom rules with Zod schemas.
LLM-as-JudgeOptional semantic scoring via Anthropic or OpenAI — bring your own API key. Seven templates. With IRIS_RELEVANCE_JUDGE_MODEL set, answers_the_ask asks the relevance judge and fails an off-topic answer; without it the rule reads the ask lexically and advises. Hard per-eval cost cap (IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL, default $0.25), per-eval pricing disclosed in the result.
Cost VisibilityAggregate cost across all agents over any time window. Set budget thresholds. Get flagged when agents overspend. A trace that sends token counts and a model but no cost (most OpenTelemetry and framework traces) is priced at the model's list price and marked estimated everywhere it shows; pricing.models in config.json prices models the built-in table does not — docs/cost.md.
Web DashboardReal-time dark-mode UI that lands on the failures, worst and newest first — trace visualization with full-text search over every trace's text, eval results, cost breakdowns, and a command palette (⌘K) that searches your own rules, traces, and evals.
Local-firstEverything lives in SQLite on your disk. The self-hosted server needs no account or sign-up and sends no telemetry. Outbound HTTP happens only where you opt in: your own LLM-judge key, citation fetching, an OTel exporter you configure, or a webhook you set.

Where this is going next: the capability map — every question Iris can be asked about every subject, with what it has and what it lacks — and the three tracks.

Measured, not claimed

Every built-in rule has a published precision, recall and F1 with 95% confidence intervals, measured on a labelled corpus that lives in this repository (proof/corpus/) and regenerates with one command — npm run proof — offline, with no key and no model in the loop. Those numbers are two different kinds, and the page never adds them together: some rules are measured against labels a model gave by reading the failure itself, which measures detection; the rest are checked against their own documented definition, applied independently, which shows the code implements its formula and says nothing about whether the formula catches the failure. proof/RESULTS.md and the proof page mark each rule. CI re-runs the measurement on every pull request and fails if the committed numbers differ from what the code produces, so a rule cannot change without its numbers changing with it. The numbers are on iris-eval.com/proof and in proof/RESULTS.md; how the corpus was made, what it is not, and how to read an interval are in docs/proof.md. The corpus is synthetic and model-labelled — a human blind label is pending, and the page says so; node proof/blind-sample.mjs draws the reproducible sample that will settle it.

MCP Tools

Iris registers twelve tools that any MCP-compatible agent can invoke — trace and rule lifecycle, comparison across runs, LLM-as-judge and semantic citation verification:

  • log_trace — Log an agent execution with spans, tool calls, token usage, and cost; pass evaluate: true to score it in the same call
  • evaluate_output — Score output quality against completeness, relevance, safety, and cost rules (heuristic, deterministic, free)
  • get_traces — Query stored traces with filtering, pagination, and time-range support, and find the run where the agent said something with q: full-text search over input, output, tool-call values and metadata, ranked, with the matched words marked
  • list_rules — Enumerate deployed custom eval rules (read-only)
  • deploy_rule — Register a new custom eval rule so it fires on every evaluate_output of that category
  • delete_rule — Remove a deployed custom rule (destructive, idempotent)
  • delete_trace — Remove a single stored trace by ID (destructive, tenant-scoped)
  • evaluate_with_llm_judge — Semantic eval via LLM (Anthropic or OpenAI). Seven templates: accuracy, helpfulness, safety, correctness, faithfulness, task_completed, relevance. Cost-capped, per-eval pricing disclosed. Bring your own API key (IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY) — Iris doesn't proxy or relay LLM calls.
  • verify_citations — Extract citations from output (numbered, author-year, URLs, DOIs), fetch sources behind an SSRF-guarded + domain-allowlisted resolver, and use an LLM judge to check whether each source actually supports the cited claim. Opt-in outbound HTTP. Same BYOK requirement as evaluate_with_llm_judge.
  • compare_runs — Did a change make the agent worse? Compares two runs of stored evaluations: a paired exact test when the runs share case keys, an interval on the difference otherwise, an honest "cannot tell" with the number of cases it would take, or "equivalent within a margin". Every rule carries its own one-sided test, corrected together (Benjamini–Hochberg) so twenty rules cannot manufacture a regression
  • compare_traces — How reliably does the agent answer the same question? Per-case pass rates with intervals, flaky cases first, and an overall rate that respects repeats
  • evaluate_runs — Re-score every trace in a run under today's rules into a new run, so a rules change is never read as an agent change

Enable the LLM judge (optional; the deterministic rules never need it)

  1. Get an API key from Anthropic or OpenAI.
  2. Put it in the environment of the process that runs Iris, not only your shell. Claude Code, Claude Desktop, Cursor and most MCP clients: the "env" block of the iris-eval entry in your MCP config — "iris-eval": { "command": "npx", "args": ["-y", "@iris-eval/mcp-server"], "env": { "IRIS_ANTHROPIC_API_KEY": "sk-ant-..." } } (IRIS_OPENAI_API_KEY for an OpenAI key). Docker: -e IRIS_ANTHROPIC_API_KEY=... on the run command. HTTP or CI: export it before starting iris-eval.
  3. Restart the MCP session. A running process never sees a variable set after it started.
  4. Confirm from inside your client: read iris://capabilities — judge.enabled must be true there. A key exported in your shell is not passed to the process your client spawns unless its config lists it. On a machine, npx @iris-eval/mcp-server --self-test prints the judge line for that shell, and GET /api/v1/health reports judge.enabled on a running dashboard.
  5. Spend guard: each call is capped by IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL (default 0.25 USD) and refused before any spend if the worst case would exceed it. Every judge call Iris makes on your key draws on one daily budget, IRIS_LLM_JUDGE_DAILY_BUDGET_USD (default 1 USD per UTC day); past it, no call is made until midnight UTC. Iris calls the provider directly with your key and never proxies it.
  6. Optional: set IRIS_RELEVANCE_JUDGE_MODEL to a priced model id (claude-haiku-4-5, for example) to have answers_the_ask ask the judge whether each answer addresses its ask, and fail an off-topic one. That is one judge call per evaluation that carries an input, on your key and under the cap above; the key alone never turns it on. Each call sends that input and output to the model's provider, with the personal data and credentials no_pii flags replaced first (IRIS_RELEVANCE_JUDGE_REDACT=off sends them as they are). It draws on that daily budget and makes at most IRIS_RELEVANCE_JUDGE_MAX_CALLS_PER_REQUEST calls per request (default 20); past either, answers_the_ask reads the ask lexically and says why.

When IRIS_OTEL_ENDPOINT is configured, log_trace calls also emit a best-effort OTLP/HTTP JSON export to any OpenTelemetry collector (Jaeger, Grafana Tempo, Datadog OTLP, Honeycomb, etc). See docs/otel-integration.md.

How passed is decided

evaluate_output returns both a score and a passed flag — they answer different questions:

  • score (0..1) is the weighted average across the rules that ran — a quality gradient.
  • passed is the ship/no-ship verdict, and the score is never consulted for it. A composer reads each rule by the kind of claim it makes: a policy you configured gates; a critical detector vetoes; a critical check that was asked and could not answer makes the verdict unknown (passed: false) rather than clean; every remaining detector combines into one probability that the output is bad, weighed against the loss ratio you state in eval.falsePassCost (default 1, so the cut is 0.5). verdict.basis names the layer that decided and verdict.by the rules, and verdict.also lists every later layer that would have decided it too; interpretations[] says why a rule that failed did not decide and which setting would change that, and names any question that was not judged and the input that would let it be.

Genuine safety violations hard-fail. By default no_pii, no_injection_patterns, and no_blocklist_words are critical rules: if one fails, the eval reports passed: false no matter how well the other rules scored, and the response names the culprits in critical_failures. A leaked SSN can't be averaged away. Which built-in rules are critical is a deployment setting (eval.criticalRules / eval.nonCriticalRules); every rule result carries the effective critical flag and criticalSource, and list_rules reports the roster this server applies. Custom rules deployed with severity: "high" or "critical" hard-fail the same way; low/medium severities only affect the score. One boundary to know, stated the same way on every surface: a critical rule that skipped (missing context, a broken definition, or a regex killed at the sandbox budget) has not judged the output and does not veto — every such rule is named in critical_skipped. A gate that must fail closed treats a non-empty critical_skipped as unknown, not clean, and may treat any budgetExceeded skip in rule_results the same way.

For CI gates: if you omit eval_type, every bundle runs — completeness, relevance, safety, cost and any custom rules — and the response says eval_type: "all" with a note that the default ran, plus a per-bundle categories map. Each row carries state: the verdict, read for the rules that bundle holds, so a row never reads passed over the rules that failed the evaluation. A bundle with nothing to judge (cost without cost_usd, relevance without input) reports state: "unknown" and passed: null there — not evaluated, not failing — and never counts toward the verdict. The response always echoes the eval_type that ran, so your gate can verify coverage; key on passed for the verdict and name a bundle only when you want a narrower run.

Authoring a custom rule

Two ways to add a rule. Inline rules ride along on one evaluate_output call (custom_rules, up to 10 per call); they fire alongside whatever eval_type bundle you chose, or alone with eval_type: "custom". Deployed rules are registered once with deploy_rule, persist in custom-rules.json under your Iris home, and fire on every future evaluate_output of their evalType. The definition is the same shape either way:

FieldRequiredWhat it is
nameyes1–80 characters; appears as ruleName in results
typeyesone of regex_match · regex_no_match · min_length · max_length · contains_keywords · excludes_keywords · json_schema · cost_threshold
configyesthe keys for that type: pattern (+ optional flags) for the two regex types · min_length / max_length (a character count) · keywords (+ optional threshold, 0–1, default 1 = all must appear) for the two keyword types · {} for json_schema · max_cost in USD for cost_threshold
weightnoweight in the score; default 1

deploy_rule wraps the definition with name, an optional description, evalType (completeness · relevance · safety · cost · custom) and severity. Severity says what a failure means: low/medium only lower the score; high/critical hard-fail the evaluation — passed: false, the rule named in critical_failures — whatever the weighted score says. A rule that skips (a cost_threshold rule with no cost_usd, or a regex killed at the 100 ms sandbox budget) has not judged the output and is listed in critical_skipped instead. Deploy a critical rule that forbids internal hostnames in anything the agent says:

json
{  "name": "no_internal_hostnames",  "description": "Output must not mention internal hostnames.",  "evalType": "safety",  "severity": "critical",  "definition": {    "name": "no_internal_hostnames",    "type": "regex_no_match",    "config": { "pattern": "\\b[a-z0-9-]+\\.internal\\.example\\b", "flags": "i" }  }}

The response is the persisted rule — keep the id for delete_rule:

json
{ "rule": { "id": "rule-588823d0", "name": "no_internal_hostnames", "evalType": "safety", "severity": "critical", "enabled": true, "version": 1, "definition": { "…": "…" } } }

From the very next evaluate_output with eval_type: "safety", an output that mentions db-primary.internal.example comes back passed: false with critical_failures: ["no_internal_hostnames"] — even though all five built-in safety rules passed and the weighted score is 0.895. Regex patterns must pass a ReDoS check at deploy time and always run in a sandbox worker under a hard 100 ms deadline. list_rules shows what is deployed; the dashboard's rule composer builds the same shape from a failure you clicked on. Full reference, scoring per type, and worked examples: docs/custom-rules.md.

Full tool schemas and configuration: iris-eval.com

Self-hosted and free

Iris runs on your machine or your own infrastructure. The open-source core is free and MIT licensed, with no usage limits and no account. Hosted storage, shared team history and alerting are under consideration, not under construction; if they would help you, tell us at [email protected].

Two commitments hold: nothing that is free today will move behind a paywall, and no compliance certification will be claimed before it is held.

Examples

Community

Configuration & Security

CLI Arguments

FlagDefaultDescription
--transportstdioTransport type: stdio or http
--port3000HTTP transport port
--db-path~/.iris/iris.dbSQLite database path
--config~/.iris/config.jsonConfig file path
--api-key—API key for HTTP authentication (transport and dashboard, including POST /api/v1/traces)
--dashboardfalseEnable web dashboard. Also the only way the POST /api/v1/traces ingest endpoint starts — it never starts implicitly with --transport http
--dashboard-port6920Dashboard port
--dashboard-host127.0.0.1Dashboard bind address. Loopback by default — the dashboard is unauthenticated unless --api-key is set, so binding beyond loopback exposes your full trace history
--demofalseSeed a demo database (separate from your real traces) and serve the dashboard against it
--demo-clearfalseDelete the demo database and exit
--self-testfalseRun the offline install diagnostic in an isolated temp home, then exit (0 = healthy, 1 = a check failed). It also reads the configured database, read-only, and fails when this version or a pinned MCP client cannot open it
--purgefalseDelete every stored trace, span and evaluation from the configured database, compact the file and truncate the write-ahead log so the deleted text does not linger on disk, then exit. Deployed rules, the audit log and preferences are kept. Not reversible. Stop any running Iris server first — the file is compacted in place. Refuses to combine with --demo, --demo-clear or --self-test
--version—Print the bare version (e.g. 1.2.3) to stdout and exit 0. Reads nothing under your Iris home

Three commands take their own arguments and exit: iris-eval ingest loads traces from a file or stdin (A CI gate, no server needed), iris-eval export traces|evaluations --format csv|jsonl writes what is stored, filtered like the dashboard's lists, to stdout or --out (docs/api-reference.md), and iris-eval install <client> writes Iris into an MCP client's config — --uninstall takes it out, --list shows the clients found on this machine and the Iris each runs, --upgrade moves every client that runs Iris to this version (Hook up your own agent, Updating). None starts a server.

config.json is validated when Iris starts. A key Iris does not read — a typo such as eval.critcalRules, a key from another tool — or a value of the wrong type refuses startup with one sentence naming the full key, the key it most likely meant, or the type it wanted. Nothing in the file is silently ignored.

Environment Variables

Every variable --help documents. CLI flags take precedence over environment variables when both are set.

VariableDescription
IRIS_TRANSPORTTransport type (stdio or http)
IRIS_HOSTHTTP transport bind address (default 127.0.0.1)
IRIS_PORTHTTP transport port (1-65535, default 3000)
IRIS_HOMEDirectory for all per-user files: config.json, iris.db, custom-rules.json, audit.log, preferences.json (default ~/.iris)
IRIS_DB_PATHSQLite database path (overrides IRIS_HOME for the DB only)
IRIS_SQLITE_DRIVERWhich SQLite driver holds the database: native (better-sqlite3, the default) or node (Node's built-in node:sqlite, Node 22.13+). Unset: native, and when the native module cannot load (or is a build that would abort on this Node) Iris warns once and falls back to the built-in
IRIS_SEARCH_BUDGET_MSHow long one trace search (q) may read before it answers with the matches it found so far and search.complete: false, in milliseconds (50 to 60000, default 1000). A search holds other requests while it reads, so this is also the longest it can make them wait. Also storage.searchBudgetMs in config.json
IRIS_SEARCH_INDEXon (the default) or off. off keeps no full-text index of the traces: a write stores the trace and nothing more, and a trace search (q) reads the traces themselves within IRIS_SEARCH_BUDGET_MS, newest first, so on a large store it can answer with part of the matches (search.complete: false). Turning it off erases the index the database kept; turning it on again builds a new one in the background. Also storage.searchIndex in config.json
IRIS_LOG_LEVELLog level: debug, info, warn, error
IRIS_DASHBOARDtrue/1/yes/on enables the web dashboard; false/0/no/off disables it (also overrides dashboard.enabled in config.json)
IRIS_DASHBOARD_PORTDashboard port (1-65535, default 6920)
IRIS_WEBHOOK_URLThe receiver of the webhook that fires on a moment — merged over notify.webhook in config.json (docs/webhooks.md)
IRIS_WEBHOOK_SECRETThe webhook's signing key (any string, or whsec_ + base64); the iris format refuses to run without one
IRIS_DASHBOARD_HOSTDashboard bind address (default 127.0.0.1)
IRIS_API_KEYAPI key for HTTP authentication. Required to bind the HTTP transport or the dashboard beyond loopback (0.0.0.0, a LAN address, a container): without it the server refuses to start
IRIS_API_KEY_FILEPath to a file whose trimmed contents are the API key — the secret-file pattern Docker and Kubernetes mount, so the key never sits in an environment block. Set this or IRIS_API_KEY, not both
IRIS_ALLOW_UNAUTHENTICATEDSet to 1 to run a non-loopback bind with no key on purpose (lifts the refusal; the network is then your boundary)
IRIS_ALLOWED_ORIGINSComma-separated origin allowlist. Dashboard: CORS headers (supports globs, e.g. http://localhost:*). HTTP transport: exact-match Origin allowlist for DNS-rebinding protection (globs ignored; the server's own loopback origins are always allowed)
IRIS_MAX_INPUT_ELEMENTSThe most array elements and object keys one input may hold before it is validated (a whole number, default 100000): a tool call's arguments on every transport, and a request body on the dashboard port (the API's JSON bodies and POST /v1/traces, JSON or protobuf). A tool call over it gets an error result; a body, 413. The default clears every legitimate JSON input measured, even one that fills the whole 1 MB request. A protobuf body on POST /v1/traces has its own ceiling, one element per 5 bytes of security.requestSizeLimit (209,716 at 1 MB) or this setting when higher, so an exporter's batch within the request size is not refused for its density (the densest real exporter body measured holds one element per 7.5 bytes); raising requestSizeLimit for a collector's larger batches raises it too. Also security.maxInputElements in config.json
IRIS_MAX_TOOL_INPUT_ELEMENTSThe name of IRIS_MAX_INPUT_ELEMENTS before 0.21.0, read when the new name is unset (and security.maxToolInputElements in config.json, likewise)
IRIS_NO_AUTO_LAUNCHSet to 1 to disable the first-run dashboard auto-launch
IRIS_ANTHROPIC_API_KEYRequired by evaluate_with_llm_judge + verify_citations with provider=anthropic
IRIS_OPENAI_API_KEYRequired by evaluate_with_llm_judge + verify_citations with provider=openai
IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVALHard cost cap per LLM judge call (default 0.25). max_cost_usd can lower it for a call, never raise it
IRIS_RELEVANCE_JUDGE_MODELA priced judge model id (e.g. claude-haiku-4-5). When set, with that provider's key, answers_the_ask asks this LLM judge on every evaluation that carries an input and gates on its relevance verdict — one judge call per evaluation, under the cost cap above and the two limits below. Each such evaluation's input and output are sent to that model's provider (Anthropic or OpenAI) on your key, with the personal data and credentials no_pii flags replaced first. Unset (the default), answers_the_ask reads the ask lexically and advises, and nothing is sent (docs/llm-as-judge.md)
IRIS_LLM_JUDGE_DAILY_BUDGET_USDWhat every judge call together may spend per UTC day, per tenant (default 1): the relevance judge, evaluate_with_llm_judge and verify_citations share it. Kept in the database, so a restart does not reset it. A call is made only if its worst case fits in what is left; past that, answers_the_ask reads the ask lexically (judge.withheld: "daily_budget") and the judge tools answer IRIS_BUDGET_EXCEEDED (a citation, daily_budget_reached). 0 stops every call
IRIS_RELEVANCE_JUDGE_DAILY_BUDGET_USDThe name of IRIS_LLM_JUDGE_DAILY_BUDGET_USD before 0.21.0, when it limited the relevance judge only. Read when the new name is unset, and it then limits every judge call; the server says so at startup
IRIS_RELEVANCE_JUDGE_MAX_CALLS_PER_REQUESTRelevance judge calls one request may make (default 20): an OTLP batch or an evaluate_runs re-score judges its first 20 traces and reads the rest lexically, with judge.withheld: "request_cap"
IRIS_RELEVANCE_JUDGE_REDACTon (default): every span no_pii flags (personal data and credentials) in the input and output is replaced by a [REDACTED:<kind>#<n>] marker before they are sent to the relevance judge. off sends them as they are
IRIS_CITATION_ALLOW_FETCHSet to 1 to permit outbound HTTP in verify_citations (off by default). A tool argument can turn it off for a call, never on
IRIS_CITATION_DOMAINSComma-separated hostname allowlist for verify_citations (suffix match). A tool argument can narrow it, never add to it
IRIS_CITATION_MAX_COST_USD_TOTALThe most one verify_citations call may spend on judge calls (default 1). max_cost_usd_total can lower it for a call, never raise it
IRIS_OTEL_ENDPOINTEnable best-effort OTLP/HTTP JSON trace export to this collector URL
IRIS_OTEL_SERVICE_NAMEservice.name resource attribute for OTel export (default iris-eval)
IRIS_OTEL_HEADERSComma-separated k=v headers for OTel export (e.g. authorization=Bearer abc)
IRIS_OTEL_TIMEOUT_MSPer-export timeout (default 15000)
RATE_LIMIT_SALTWebsite only — salts the address hash the iris-eval.com playground rate limit keeps; the server never reads it

Security

When using HTTP transport, Iris includes:

  • API key authentication with timing-safe comparison (Bearer for API clients; browser sign-in to the dashboard via ?key=)
  • CORS restricted to localhost by default
  • Rate limiting per client address and minute: 600 requests to the dashboard API (security.rateLimit.api) and 20 to the MCP endpoint (security.rateLimit.mcp), both set in config.json; an MCP request over the limit gets a JSON-RPC error that names the key
  • Helmet security headers
  • Zod input validation on all routes
  • ReDoS-safe regex for custom eval rules
  • One 1MB request size limit on every transport (security.requestSizeLimit): HTTP answers 413, stdio answers a JSON-RPC error and keeps the session open
  • One limit of 100,000 array elements and object keys in an input, counted before it is validated (security.maxInputElements): a tool call's arguments on every transport get an error result over it, and a dashboard or OTLP request body a 413
  • Validation errors return at most 20 issues and the total (issueCount), so an answer stays small however wrong the request
bash
# Production deploymentiris-eval --transport http --port 3000 --api-key "$(openssl rand -hex 32)" --dashboard

With a key set, API clients — MCP clients, capture SDKs, POST /api/v1/traces — send Authorization: Bearer <key>. To open the dashboard in a browser, append the key once to any dashboard URL, http://localhost:6920/?key=<api key>: Iris exchanges it for an HttpOnly, SameSite=Lax session cookie and redirects to the same page with the key removed from the address bar. A page opened without a session shows a sign-in form that does the same exchange. The key is never stored in the browser, and sessions live only in the server process (at most 256 live at a time; a sign-in that finds them all live is refused rather than evicting one).

Production

Several keys, and rotation without a gap. security.apiKeys in config.json holds any number of further keys, each with an id and exactly one of keyFile (a file whose trimmed contents are the key) or keyHash (the sha256 hex of the key, so the config file holds no secret — printf %s "$KEY" | openssl dgst -sha256), and an optional expiresAt (ISO 8601) after which it stops matching at that instant. To rotate: add the new key, move your clients, remove the old key. Keys in config.json and in key files take effect without a restart (0.20.0): on each request the server checks whether config.json or a key file it names has changed, and if so re-reads the keys before answering. Removing a key from security.apiKeys, or deleting its key file, revokes it on the next request: that request is refused, and every browser session opened with it is signed out. A config.json that cannot be read (for example, half-written) fails closed, and until it is fixed only a key from IRIS_API_KEY or --api-key is accepted. The key in IRIS_API_KEY or --api-key itself, and whether authentication is on at all, still change only on a restart. Every key authenticates until it is removed or expires, on the Bearer path and on the browser sign-in alike; the startup log names the ids. security.rateLimit.mcpKeyBy: "apiKey" counts the MCP endpoint's per-minute budget per key instead of per client address, so several agents behind one address each get their own minute.

Several clients. --transport http serves any number of MCP clients at one address: each initialize opens a session of its own over the one engine and store, up to 256 at a time. Over stdio each client starts its own server process, and those processes share the Iris home: a rule deployed, switched off or deleted through one applies in every other within 20 ms, a label written through one is read by the others as soon, and two of them changing rules at the same moment lose neither change.

Iris refuses to start when the HTTP transport or the dashboard is bound beyond loopback — 0.0.0.0, a LAN address, a container — with no API key, and says so in one sentence naming IRIS_API_KEY. That includes a bare docker run of the image, which binds 0.0.0.0 inside the container because loopback is unreachable through a published port. Loopback without a key keeps working (with a warning on the HTTP transport): the machine boundary is the exposure control there.

bash
# The image: pass a keydocker run -p 3000:3000 -p 6920:6920 -v iris-data:/data \  -e IRIS_API_KEY="$(openssl rand -hex 32)" ghcr.io/iris-eval/mcp-server
# Compose: the file requires the variable and refuses before the container startsIRIS_API_KEY="$(openssl rand -hex 32)" docker compose up
# A network you have already fenced some other way: run open, on purposeIRIS_ALLOW_UNAUTHENTICATED=1 iris-eval --transport http --dashboard

Open by design, on a keyed server: GET /health on the transport and GET /api/v1/health on the dashboard answer without a key and outside every rate limit, in one shape: status, version, uptime, the SQLite driver, checks for storage, the deployed-rules file and the migrations (applied against known), the search index's state (search: ready, or how far a build has got as a share of the traces), and whether a judge key is present — never the key, never a trace, never a count of them. status is ok only when every check is; otherwise it is degraded with HTTP 503, which the Docker image's own HEALTHCHECK reads. Everything else needs Authorization: Bearer <key> or a browser session. Retention runs on every server: traces and evaluations older than retention.days (default 30) are deleted at startup, once the server is answering, and every retention.sweepIntervalHours, in short steps that never make a request wait long; --self-test prints this install's policy, and iris://capabilities / GET /api/v1/capabilities carry it as retention.

A webhook fires on a moment (0.16.0): notify.webhook in config.json (or IRIS_WEBHOOK_URL and IRIS_WEBHOOK_SECRET) names a receiver, and Iris posts one signed message when a verdict fails, a critical detection vetoes, a cost is an outlier, a rule's fail rate shifts, or a case is answered both ways for the first time — ids, the verdict, the rules and the numbers, never the agent's text. Signed the Standard Webhooks way and the GitHub way at once, retried with backoff, cooled down per agent and rule, never in the way of the evaluation; Slack and Discord bodies built in. docs/webhooks.md.

Your data on disk

Everything Iris stores lives under your Iris home (~/.iris, or IRIS_HOME). iris.db keeps every trace's input and output verbatim — including any text no_pii goes on to flag; detection does not redact unless you ask it to: storage.redact: "critical_spans" in config.json stores each evaluation's output with the spans a critical detector flagged replaced by [REDACTED:<pattern>] (off by default; the evidence offsets still index the text the caller saw). storage.synchronous sets when a write reaches the disk: normal (the default) syncs the write-ahead log at each checkpoint, so a crash of Iris loses nothing and the file cannot be corrupted, but a power cut or an operating-system crash can undo the writes since the last sync; full syncs every commit and keeps them through both, at about 1.5 ms more per write. At startup, and every retention.sweepIntervalHours (default 24, 0 disables the timer) after that, traces and evaluations older than retention.days (default 30, 0 disables, set in config.json) are deleted and the write-ahead log is checkpointed. One thing is kept past the window: the pinned baseline run, with its evaluations, because it is what every later run is compared against. Each sweep that keeps some of it says so in the log with the run and the counts; unpin the run and the next sweep deletes it. Dataset cases and labels are not traces or evaluations and are not swept. Deleting a trace — by delete_trace or by the sweep — erases the text of every evaluation linked to it (the output, the expected text, and the rule messages) and stamps erased_at; the verdict, the scores and the evidence offsets stay. Each delete checkpoints the write-ahead log before it returns, so the deleted text is not left readable in iris.db or iris.db-wal (if a search is reading the file at that moment, or another process is reading or writing it, the delete returns without waiting and the text leaves the file as soon as it finishes). To remove everything now, stop the server and run --purge: it deletes every stored trace, span and evaluation, compacts the database and truncates the write-ahead log so the text is gone from disk, and keeps your deployed rules, audit log and preferences. Before a release applies a migration to an existing iris.db, it copies the file next to it (iris.db.<from>-to-<to>.<time>.bak, owner-only, the newest three kept; Downgrading): the copy holds the traces as they were, so the retention sweep deletes one older than retention.days and --purge deletes them all. The server does the copy and the migrations after it has answered its client, on a thread of its own: tool calls, resource reads and HTTP requests that arrive meanwhile wait for them, at most 30 s each, and are then refused with a sentence saying what the server is doing (IRIS_STORAGE_ERROR, retryable; HTTP 503 with Retry-After). Health answers throughout and says what the upgrade is doing. From 0.19.0 at 100,000 traces that are each an agent loop, the copy and the migrations took about 6 s. iris-eval ingest, --purge and --self-test still upgrade before they do anything else.

Iris does not encrypt its data at rest. iris.db and its write-ahead-log files are created owner-only (mode 600), and the Iris home directory is created mode 700 (on Windows, file ACLs govern instead). The database stores no LLM provider keys: IRIS_ANTHROPIC_API_KEY and IRIS_OPENAI_API_KEY are read from the environment and never written to disk. It does store trace inputs and outputs verbatim, so put the Iris home on an encrypted disk or volume (FileVault, BitLocker, LUKS, or an encrypted cloud volume for the Docker image's /data mount).

An export — the Export button on the dashboard's Traces and Evaluations pages, GET /api/v1/traces/export and /api/v1/evaluations/export, or iris-eval export — carries this stored text as it is, the same as the dashboard shows it: trace input and output verbatim, evaluation output with storage.redact applied. Treat an exported file like the database it came from.

Troubleshooting

First move: run the self-test

bash
npx @iris-eval/mcp-server --self-test

It checks storage, the deterministic evals, and the dashboard in an isolated temp home and prints a per-step verdict — the failure output names the broken step. Exit code 0 means the install is healthy.

Iris won't start / ERR_MODULE_NOT_FOUND

You may have a cached older version. Clear the npx cache and retry:

bash
npx --yes @iris-eval/mcp-server@latest

Or install globally to avoid cache issues entirely:

bash
npm install -g @iris-eval/mcp-server@latest

npm install --ignore-scripts broke the SQLite binding

Iris stores traces with better-sqlite3, a native module that fetches or compiles its binding in an install script. If that script was skipped — --ignore-scripts on the command line, ignore-scripts=true in an .npmrc (common on corporate machines), or a registry mirror that strips postinstall — startup fails with a long "Could not locate the bindings file" dump listing a dozen paths it tried. Rebuild that one module:

bash
npm rebuild better-sqlite3# for a global install:npm rebuild -g better-sqlite3

Tools not showing up in Claude Code

MCP tools only load at session start. After adding iris-eval, restart the session with /clear or relaunch the terminal.

Version check

bash
npx @iris-eval/mcp-server --version

The first startup log line also carries it (Starting Iris MCP server vX.Y.Z), and --self-test prints it in its summary. For a global install, npm ls -g @iris-eval/mcp-server shows the installed version.

Updating

Every MCP client on a machine shares one database, ~/.iris/iris.db, and install pins each client to the release that wrote its config. When a release changes the database's schema, the first process of that release to open the file upgrades it, and from then on a client still pinned to an older release refuses to start. So move every client in one step, before or right after you upgrade:

bash
npx -y @iris-eval/mcp-server@latest install --upgrade

It finds every client config on this machine that runs Iris, moves each pin to that release (keeping anything you added to the entry, such as --dashboard or an env block), leaves alone a pin to a newer release and an entry that runs something other than the npm package, and lists what it did. Restart the clients it names. install --list shows which Iris each client runs.

Two installs live outside those files: the Claude Desktop extension (iris-eval.mcpb) moves when you open a newer bundle, and the Claude Code plugins with claude plugin marketplace update iris-eval and then claude plugin update iris-eval@iris-eval (and claude plugin update iris-eval-capture@iris-eval for the capture plugin).

Upgrading from 0.19.x to 0.20.0. 0.20.0 adds the search index and other additions to the database (migrations 015 and later). Once any 0.20.0 process has opened ~/.iris/iris.db (the Claude Desktop extension, npx iris-eval, or npx @iris-eval/mcp-server with no version), a client pinned to 0.19.x stops with This database was migrated by a newer Iris (…) — migration(s) 015-trace-search, … are unknown to v0.19.0. Upgrade Iris, …. That message comes from 0.19.x and cannot change; the fix is the command above. Before the upgrade, 0.20.0 copies the file next to it, so going back is possible too (below).

A start that upgrades the database prints what it did on stderr: the copy it took, which older releases can no longer open the file, and any client on this machine pinned to one of them, with the command. --self-test reads the database without changing it and says the same before you start anything.

For a global install, npm update -g @iris-eval/mcp-server, then iris-eval install --upgrade.

Downgrading

A release that upgraded the database copies it first, next to it: iris.db.<from>-to-<to>.<time>.bak in your Iris home (<from> is the release that last changed the file's schema, <to> the one that upgraded it; the startup line printed the exact path). To go back:

  1. Stop every MCP client and any other Iris process that uses the database.
  2. Keep the upgraded file, in case you come back: rename iris.db to iris.db.upgraded, and delete iris.db-wal and iris.db-shm if they are there.
  3. Copy the backup to iris.db: cp ~/.iris/iris.db.0.19.0-to-0.20.0.<time>.bak ~/.iris/iris.db.
  4. Pin every client back to the older release: npx -y @iris-eval/[email protected] install <client> for each one (install --upgrade never moves a client back).

Traces stored after the upgrade are in iris.db.upgraded, not in the backup. If no copy was taken (the startup line says why, for example a full disk), the older release cannot open the upgraded file, and the way forward is install --upgrade.

The storage driver

On a platform with no prebuilt better-sqlite3, the install still succeeds. better-sqlite3 is an optional dependency: when npm can neither download a prebuilt binary for your Node and platform nor compile one (compiling needs Python and a C++ toolchain — Visual Studio's C++ build tools on Windows), npm prints the build error, skips the module, and finishes the install. Iris then runs on Node's built-in SQLite, and says so: startup prints one line on stderr naming why, and --self-test shows driver node: better-sqlite3 is not installed …. To get the native driver back, install it where a prebuild or a toolchain exists (npm install better-sqlite3 in the project; for a global install, install Iris again with npm install -g @iris-eval/mcp-server once a toolchain is available). CI installs the packed server with the native build forced to fail on every change, and requires the install to finish and the self-test to store and read a trace on the built-in.

Iris keeps everything in one SQLite file, opened by better-sqlite3 — a native addon that is downloaded or compiled for your Node and platform. When that module cannot load, Iris falls back to Node's built-in SQLite (node:sqlite, Node 22.13 or later) with one warning on stderr, so a missing prebuild is a slower start rather than a dead one. It does the same, before loading it, for a better-sqlite3 compiled on your machine against Node 24.19 or later headers and run on Node 24.19 to 24.21: there such a binary aborts the whole process the first time it frees a statement (Assertion failed: (env) != nullptr, nodejs/node#65446). Node 24.22.0 carries the fix, and Iris loads the binary there. npm rebuild better-sqlite3 replaces it with the prebuilt binary, which is safe on every version. IRIS_SQLITE_DRIVER=node chooses the built-in on purpose, native forbids the fallback. The built-in is opened with extension loading off and trusted_schema off; Node prints its own ExperimentalWarning: SQLite is an experimental feature line on stderr when it loads, and Iris does not silence it. --self-test and GET /health name the driver in use; every number on the proof page was measured on the native driver, and the test suite runs on both in CI.

Node.js version

Iris requires Node.js 22.13 or later. Node 20 reached end of life on 2026-04-30 and is not supported; Node 18 went in April 2025.

The floor is 22.13 rather than 22.0 because 22.13.0 is the first release that ships node:sqlite. That makes it the first version on which every supported Iris install has a second storage driver: when the native better-sqlite3 addon will not load, Iris falls back to Node's built-in SQLite instead of failing to start. Below 22.13 — and on Node 20, for its whole life — there was only ever one driver, and a missing prebuild was a dead start.

bash
node --version  # Must be v22.13.0 or newer

Windows: cmd /c not needed

Claude Code's /doctor may suggest wrapping npx with cmd /c. This is not needed and causes path parsing issues. Use npx directly:

bash
# Correctclaude mcp add --transport stdio iris-eval -- npx -y @iris-eval/mcp-server
# Wrong (causes /c to be parsed as a path)claude mcp add --transport stdio iris-eval -- cmd /c "npx -y @iris-eval/mcp-server"

If Iris is useful to you, consider starring the repo — it helps others find it.

[Star on GitHub]

MIT Licensed.

來源:README.md,提交 dbc0e8e

工具

12

log_trace

Log Trace

Store one agent execution (input, output, tool calls, spans, cost, tokens) and get the trace_id later calls key on. What it does. Writes one immutable trace to local storage; nothing is deduplicated. Only agent_name is required; store tool_calls, tools, cost and input/output so the rules can judge them. evaluate: true (with output) also scores it in this call, exactly as evaluate_output would. Full detail: iris://capabilities. When not to use it. To score a stored trace (evaluate_output with its trace_id). To change one (delete_trace, then log again). Returns. JSON: trace_id, status, cost_usd, cost_source, cost_estimate, evaluation. Errors. IRIS_STORAGE_ERROR; IRIS_INVALID_ARGUMENT when evaluate is true without output. Unknown or malformed arguments are refused, naming the valid keys. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. evaluate_output — score the stored output; get_traces — query what was logged; delete_trace — remove one trace.

輸入參數runinputspanstoolsoutputcase_keycost_usdevaluatemetadataeval_typeframeworktimestamp另有 5 個

evaluate_output

Evaluate Output

Score an output with deterministic rules: a ship verdict with its basis, per-rule evidence, what was unjudged. What it does. eval_type picks a bundle or all (the default). A rule missing its input SKIPS, never passes: input is REQUIRED when eval_type="relevance"; tool_calls, tools, cost_usd, expected feed the rest. trace_id alone scores the stored trace. Full detail: iris://capabilities. When not to use it. For semantic judgment (evaluate_with_llm_judge). As an input firewall: the rules read the output. Returns. JSON: id, trace_id, reference_trace_id, verdict, coverage, provenance, run_id, interpretations, erased_at, eval_type, score, passed, rule_results, rules_evaluated, rules_skipped, insufficient_data, critical_failures, critical_skipped, categories, note, rules_changed. Errors. IRIS_UNKNOWN_TRACE (nothing written); IRIS_STORAGE_ERROR. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. log_trace — record the run first; list_rules — what each rule needs.

輸入參數inputtoolsoutputcost_usdexpectedtrace_ideval_typetool_callstoken_usagecustom_rulesexpected_trajectory

get_traces

Get Traces

Query stored traces with filters, pagination and sorting; optionally with the dashboard summary. What it does. Read-only, local. Exact-match agent_name and framework, inclusive since and until, min_score and max_score on each trace's latest evaluation, and q: full-text search, ranked, with a snippet. limit is 1..1000 (default 50). A crossed range is refused, never returned as an empty page. Full detail: iris://capabilities. When not to use it. To score a trace (evaluate_output) or create one (log_trace). As a live stream: poll with backoff. Returns. JSON: untrusted, traces, total, limit, offset, summary, search. Errors. IRIS_STORAGE_ERROR. Out-of-range bounds are refused, naming the values; no match is total 0, not an error. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. log_trace — record an execution; evaluate_output — score one output; delete_trace — remove one trace.

輸入參數qlimitsinceuntiloffsetsessionsort_byframeworkmax_scoremin_scoreagent_namesort_order另有 2 個

compare_runs

Compare Runs

Did this change make the agent worse? Compares two runs: worse, better, equivalent, or too little evidence to tell. What it does. Pairs by case_key (McNemar exact), else Newcombe. A rule counts only where it ran; no `better` if the second run was judged on less. Refuses unlike runs unless force. Local, no model call. Full detail: iris://capabilities. When not to use it. To score one output (evaluate_output). As a gate: it reports; blocking is your policy. Returns. JSON: comparable, incomparable_because, forced, method, before, after, difference, paired, worse, better, improvement_withheld, call, smallest_detectable, equivalent_within, coverage, critical_rises, rules_tested, regressions, improvements, discordant, discordant_total, summary, dataset. Errors. IRIS_INVALID_ARGUMENT (empty run id, no baseline, bad margin or dataset); IRIS_STORAGE_ERROR. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. log_trace — record into a run; get_traces — find a run's traces.

輸入參數after必填forcebeforedatasetequivalence_margin

compare_traces

Compare Traces

How reliably does the agent answer the same question? Per-case pass rates, flaky cases, and an interval that respects repeats. What it does. Groups evaluations by case_key (or session) and reports each case's pass rate with a Wilson interval, the flaky cases least reliable first, and an overall rate over cases, with an exact interval, rather than over pooled attempts. Deterministic, local, no model call. Full detail: iris://capabilities. When not to use it. To compare two runs (compare_runs). To score an output (evaluate_output). Returns. JSON: case_key, group_by, question, cases, attempts, overall, pooled, flaky_cases, by_case, summary. Errors. IRIS_STORAGE_ERROR. No matching evaluations is cases 0, not an error. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. compare_runs — compare two runs against each other; log_trace — record an execution with a case_key; get_traces — read the traces themselves.

輸入參數runsessioncase_keygroup_byquestionmin_attempts

evaluate_runs

Evaluate Runs

Re-score every trace in a run under the current rules into a new run, so a rules change is compared, not overwritten. What it does. Writes the verdicts into a NEW run; the source run is never modified. Traces already scored under the current ruleset are skipped, so a repeat call does no work. Local, no model call. Full detail: iris://capabilities. When not to use it. To score a new execution (log_trace, then evaluate_output). Returns. JSON: source_run, run, ruleset_hash, traces, evaluated, already_current, failed, passed, not_checked, summary, rules_changed. Errors. IRIS_INVALID_ARGUMENT (empty run id, or the target run holds verdicts); IRIS_UNKNOWN_TRACE (source run empty); IRIS_STORAGE_ERROR. Unscorable traces are listed in failed. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. compare_runs — compare the two runs after; evaluate_output — re-score one trace; get_traces — read a run.

輸入參數run必填intolabel

list_rules

List Rules

The rule inventory: every built-in rule with what it needs, its effective criticality and published accuracy, plus every deployed custom rule. What it does. Read-only, no network. built_in is never narrowed by the filters; a rule whose needs are absent skips, never passes. Read critical and criticalSource before trusting passed: true. rules is the custom-rule store, filterable by eval_type and enabled_only. Full detail: iris://capabilities. When not to use it. To add, remove or pause a rule (deploy_rule, delete_rule); built-in rules cannot be. Returns. JSON: untrusted, rules, total, enabled_count, built_in, quarantined, plugins. Errors. IRIS_INTERNAL_ERROR if the store cannot be read; a missing store is an empty list. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. deploy_rule — add a custom rule; delete_rule — remove, disable or re-enable one; evaluate_output — run the rules.

輸入參數eval_typeenabled_only

deploy_rule

Deploy Custom Rule

Deploy a custom rule that fires on every future evaluate_output call of its bundle — persisted, active immediately, audited. What it does. Stores the rule and loads it into the running engine. eval_type says WHEN it fires (that bundle, and eval_type="all"); severity says what a failure does: low and medium lower the score, high and critical fail the verdict. definition.type picks the check, definition.config its keys. A taken name is refused unless replace is true. Full detail: iris://capabilities. When not to use it. For a one-off check (custom_rules on evaluate_output). To pause a rule (delete_rule with enabled: false). Returns. JSON: rule, replaced, warning. Errors. IRIS_DUPLICATE_RULE; IRIS_INVALID_RULE_CONFIG, naming the field; IRIS_STORAGE_ERROR. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. list_rules — see what is deployed; delete_rule — remove or disable; evaluate_output — where it fires.

輸入參數name必填replaceevalTypeseverityeval_typedefinition必填descriptionsourceMomentIdsource_moment_id

delete_rule

Delete or Disable Custom Rule

Remove a deployed custom rule — or, with enabled, disable or re-enable it without removing it — effective on the next evaluate_output call. What it does. Without enabled: deletes the rule and writes an audit entry; deleted is false when no rule has that id. With enabled: false stops it firing and keeps it stored, true re-enables it. Past evaluations are untouched. Full detail: iris://capabilities. When not to use it. On built-in rules (they are not in the store). To replace a rule (deploy_rule with replace: true). Returns. JSON: deleted, rule_id, toggled, enabled, rule. Errors. IRIS_STORAGE_ERROR. A malformed rule_id is refused. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. deploy_rule — add or replace a rule; list_rules — find the id; evaluate_output — where the rule fires.

輸入參數enabledrule_id必填

delete_trace

Delete Trace

Remove one stored trace by id; its spans go with it, and every evaluation linked to it keeps its verdict and loses its text. What it does. Linked evaluations keep their verdicts and scores; their text is erased and erased_at is stamped. deleted is false when no trace has that id. Every deletion is audited at iris://audit. Full detail: iris://capabilities. When not to use it. To expire data in bulk (retention.days). To change a trace: traces are immutable. Returns. JSON: deleted, trace_id. Errors. IRIS_STORAGE_ERROR. A malformed trace_id is refused. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. log_trace — store a trace; get_traces — find the trace to delete; delete_rule — the equivalent for custom rules.

輸入參數trace_id必填

evaluate_with_llm_judge

Evaluate With LLM Judge

Score an output with an LLM judge on your own key: a 0..1 score, a rationale, the spend. What it does. Calls Anthropic or OpenAI with IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY; Iris never proxies. correctness needs expected, relevance input, faithfulness source_material. Worst-case cost is checked against max_cost_usd first. Full detail: iris://capabilities. When not to use it. For length, keyword, PII or cost checks (evaluate_output, free). Returns. JSON: id, reference_trace_id, score, passed, verdict, pass_threshold, self_reported_pass, disagreement, rationale, dimensions, model, provider, template, input_tokens, output_tokens, cost_usd, latency_ms, raw_response_id, warnings. Errors. IRIS_JUDGE_NOT_ENABLED (recovery has the steps); IRIS_JUDGE_UNKNOWN_MODEL; IRIS_UNKNOWN_TRACE; IRIS_BUDGET_EXCEEDED; IRIS_PROVIDER_ERROR. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. evaluate_output — the free deterministic path; verify_citations — citation grounding.

輸入參數inputmodel必填output必填expectedprovidertemplate必填trace_idtimeout_msagent_modeltemperaturemax_cost_usdsource_material另有 1 個

verify_citations

Verify Citations

Is each citation in an output supported by its source? Extracts, fetches (opt-in, SSRF-guarded) and judges on your key. What it does. Fetches only if the operator set IRIS_CITATION_ALLOW_FETCH=1, blocking private addresses. One judge call per resolved citation, within max_cost_usd_total. overall_score: supported / judged; null if none judged. Full detail: iris://capabilities. When not to use it. With no citations (evaluate_output). Without IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY. Returns. JSON: id, reference_trace_id, overall_score, passed, verdict, total_unsupported, total_citations_found, total_resolved, total_judged, total_supported, total_cost_usd, warnings, citations. Errors. IRIS_JUDGE_NOT_ENABLED, IRIS_JUDGE_UNKNOWN_MODEL, IRIS_UNKNOWN_TRACE (before any spend); IRIS_JUDGE_FAILED when every judge call failed. Failures return {"error":{code,message,recovery}} with isError; follow recovery. Siblings. evaluate_with_llm_judge — semantic scoring; evaluate_output — the free deterministic path.

輸入參數model必填output必填providertrace_idallow_fetchmax_citationsdomain_allowlistmax_cost_usd_totalper_source_max_bytesper_source_timeout_ms

版本歷史

9
  1. v0.21.0最新Oct 26, 2026
  2. v0.20.0Oct 4, 2026
  3. v0.19.0Sep 26, 2026
  4. v0.18.0Sep 24, 2026
  5. v0.17.0Sep 23, 2026
  6. v0.16.0Sep 22, 2026
  7. v0.15.0Sep 21, 2026
  8. v0.14.0Sep 21, 2026
  9. v0.13.0Sep 16, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分
  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分