Skip to content
LoglunedebugSign in

Connect an AI agent to Loglune

Updated

Loglune keeps every published feature-flag ruleset, so an agent can read any version, compare two, find the one that changed a user's value, and reproduce what production served at an instant. An agent reaches it two ways with the same operator token: the MCP server at https://loglune.com/api/mcp (Streamable HTTP) and the loglune CLI.

Setup

Issue an operator token at /app/tokens and keep it in the LOGLUNE_OPERATOR_TOKEN environment variable. Every setup below reads it from there (VS Code asks for it), so no config file or shell history holds the token.

CLI

npm install --global @loglune/cli
read -rs LOGLUNE_OPERATOR_TOKEN && export LOGLUNE_OPERATOR_TOKEN
loglune inspect

Claude Code

claude mcp add --transport http --scope project loglune https://loglune.com/api/mcp --header 'Authorization: Bearer ${LOGLUNE_OPERATOR_TOKEN}'

.mcp.json

{
  "mcpServers": {
    "loglune": {
      "type": "http",
      "url": "https://loglune.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${LOGLUNE_OPERATOR_TOKEN}"
      }
    }
  }
}

Cursor (~/.cursor/mcp.json)

{
  "mcpServers": {
    "loglune": {
      "url": "https://loglune.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${env:LOGLUNE_OPERATOR_TOKEN}"
      }
    }
  }
}

VS Code (.vscode/mcp.json)

{
  "inputs": [
    {
      "type": "promptString",
      "id": "loglune-token",
      "description": "Loglune operator token",
      "password": true
    }
  ],
  "servers": {
    "loglune": {
      "type": "http",
      "url": "https://loglune.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${input:loglune-token}"
      }
    }
  }
}
Auth:  LOGLUNE_OPERATOR_TOKEN (required; issue one at https://loglune.com/app/tokens)
Env:   LOGLUNE_API_URL (optional; default https://loglune.com)

What the server tells the agent

The MCP server sends these lines as its instructions when a client connects; loglune guide prints them.

  • Loglune is a feature-flag platform that keeps every published ruleset version, so it can reproduce what production served at a version or an instant.
  • Auth: an operator token (Authorization: Bearer). The user issues it at /app/tokens; never print it.
  • Start: inspect with no arguments returns the current version and its flags. Versions are consecutive integers from 1.
  • Typical flows: a ticket whose records carry a report id → triage with it (answers, changes and fixes in one read); what changed → diff; why a user got a value → bisect, then diff; why a bug report's record got a value → explain with its fingerprintId (free, reads the record), then bisect with the same fingerprintId; a listed user or a matching value is not served, or an old version is suspect → lint; whitespace or upper-case e-mails in segment lists → normalize, simulate, publish; what an instant served a context no record covers → reproduce with contextKey (billed, so only then); add or change a flag → define, simulate, then publish; record what a published version caused → annotate; a bad publish is live → kill the flag or rollback to the last good version, then annotate the bad one; undo one edit → revert, simulate, publish; drop every unpublished edit → discard; remove a flag from code → readiness.
  • confirm: reproduce (billed), define, publish, annotate, rollback, kill, revert, discard, normalize and restore need confirm: true. Ask the user before each such call.
  • Errors come back as an error result with status, error, reason and hint. Follow the hint. Do not retry 400, 403, 404, 409 or 501 unchanged; on 429 wait retryAfter seconds.
  • 403 reason scope or allowlist: this token may not run the op; ask the user for a token with that op. 401: the token is missing or revoked.
  • The MCP server needs a paid plan (Plus or Team Standard); the CLI takes the same token. Loglune-run AI (ai-*) is coming soon; ai-launch and ai-cancel answer 403 coming_soon: do the analysis yourself with these tools.

Operations

Each operation is an MCP tool of the same name and a CLI command. Operations marked confirm change state or bill: pass confirm: true (CLI: --confirm yes) only after the user agrees.

reproduce — Reproduce a ruleset (billed) (confirm)

Reproduce the ruleset production delivered, either at a version or at an instant T (the version live at T is resolved server-side). Pass exactly one of target.version or target.at. Add contextKey (and the report's attributes) to also evaluate that ruleset for the context, as POST /api/flags pinned to that version would: evaluations holds each flag's result (flagKey narrows it to one flag) with missing, the inputs its path read that you did not give — an attribute name, "now" (a time rule; use target.at), or "key" (the raw key; for flagKey's own key split pass the record's reason.bucket as bucket). A result with a non-empty missing may differ from production's. BILLED: 1 reproduction per call, evaluated or not, so ask the user before calling. For a free look at a version use inspect; for a bug report with a record, explain already reads what that context got, free.

Returns: { version, count, ruleset: { version, publishedAt, flags, segments, configValues } }; count is the reproductions billed this period; with contextKey, { version, count, evaluations: [{ flagKey, variationId, value, reason, missing }] } and no ruleset (inspect reads it free).

loglune reproduce (--version <n> | --at <ISO>) [--context <key> [--attrs <k=v,...>] [--flag <key> [--bucket <n>]]] --confirm yes
  • --version <n> — the version to reproduce
  • --at <ISO> — an instant; the version live then is reproduced
  • --context <key> — also evaluate the reproduced ruleset for this context key
  • --attrs <k=v,...> — context attributes the rules read
  • --flag <key> — evaluate only this flag
  • --bucket <n> — that flag's reason.bucket in the report, 0-99999
  • --confirm yes — required: this call is billed

Example:

loglune reproduce --at 2026-08-22T14:07:00Z --context user-42 --attrs plan=beta --flag checkout-v2 --confirm yes

MCP arguments: {"target":{"at":"2026-08-22T14:07:00Z"},"contextKey":"user-42","attributes":{"plan":"beta"},"flagKey":"checkout-v2","confirm":true}

inspect — Inspect a version

Read one published version: its flags (kind, default, rule count, description and owner when set), segment keys and config values. Omit version to read the current (latest published) version; its number is in the result. Pass flagKey to also get that flag as stored (rules, variations, rollout weights, prerequisites) and the segments its rules name; 404 flag-not-in-version when the version lacks it. Versions are consecutive integers from 1; inspect with no version returns the current one. Read-only, not billed.

Returns: { view: { version, publishedAt, flags: [{ key, kind, defaultValue, ruleCount, ... }], segmentKeys, configValues }, label, reportId, notes, flag?, segments? }; label is the note the version was published with, or null; reportId is the report (ticket) it was published for, or null; notes are the ones annotate added later, oldest first: [{ noteId, actor, at, note, reportId? }]

loglune inspect [--version <n>] [--flag <key>]
  • --version <n> — the version (default: the current one)
  • --flag <key> — also print that flag as stored and the segments its rules name

Example:

loglune inspect

MCP arguments: {}

diff — Diff two versions

Compare two published versions field by field: which flags, segments and config values were added, removed or changed, with their before and after state, plus the rollout-guard findings between the same two versions. Pass flagKey to narrow both to that flag and the segments its rules name; 404 flag-not-in-versions when neither version has it. Versions are consecutive integers from 1; inspect with no version returns the current one. Read-only, not billed.

Returns: { before, after, diff: { flags: [{ key, change, before, after }], segments, configValues }, findings: [{ kind, flagKey, severity, ... }] }; before and after echo the versions compared.

loglune diff --before <n> --after <n> [--flag <key>]
  • --before <n> — the earlier version
  • --after <n> — the later version
  • --flag <key> — only that flag and the segments its rules name

Example:

loglune diff --before 2 --after 3

MCP arguments: {"before":2,"after":3}

impact — Impact of a flag

List every flag downstream of one flag in a version (flags that name it as a prerequisite, transitively), the prerequisites it names that are missing from the version (it then serves its default), and whether it sits in a prerequisite cycle. Read-only, not billed.

Returns: { version, impact: { flagKey, impacted, missingPrerequisites, inCycle } }

loglune impact --version <n> --flag <key>
  • --version <n> — the version
  • --flag <key> — the flag key

Example:

loglune impact --version 3 --flag checkout-v2

MCP arguments: {"version":3,"flagKey":"checkout-v2"}

depgraph — Dependency graph

Read the prerequisite dependency graph of a version: for each flag, what it depends on and what depends on it. Read-only, not billed.

Returns: { version, graph: { dependsOn: { <flagKey>: [prerequisite keys] }, dependents: { <flagKey>: [dependent keys] } } }

loglune depgraph --version <n>
  • --version <n> — the version

Example:

loglune depgraph --version 3

MCP arguments: {"version":3}

rollout-guard — Rollout guard

Check the change from one version to another for risky publishes, each with a severity: defaultFlip (the default variation changed, high), rolloutSurge (a rollout gives one variation 100%, medium), keySplit (a new or reweighted split rollout with no bucketBy, so the context key of each evaluation decides the variation, medium), rebucketedSplit (a split rule kept under its id now hashes another bucketBy, so about movedShare of the contexts it reaches get another variation even at the same weights; names bucketBy before and after, medium), fullServe (a rule with no clauses, and no segment or a segment that matches everyone, serves a fixed variation other than the default, so every context reaching it loses the default, high), shadowedRule (a rule no context reaches because an earlier rule matches every context — no clauses, and no segment or a segment that matches everyone; names the rule and shadowedBy, high), caseSensitiveEmail (a new user-list entry of a segment a flag names is an e-mail with upper-case letters on a list without caseInsensitive, so the lower-cased address an app sends never matches it; names segmentKey, list and index, medium), versionStringOrdering (a new gt/gte/lt/lte clause bounded by a dotted version string, which orders as text; names ruleId, or segmentKey and ruleIndex, clauseIndex and op: use the semver op, high), paddedEntry (a new user-list entry or clause value with leading or trailing whitespace, which never matches the value an app sends without it; names segmentKey, list and index, or the clause and valueIndex, medium), flagRemoved (high), danglingPrerequisite (high). publish runs the same check against the previous version. Read-only, not billed.

Returns: { before, after, findings: [{ kind, flagKey, severity, ... }] }

loglune rollout-guard --before <n> --after <n>
  • --before <n> — the earlier version
  • --after <n> — the later version

Example:

loglune rollout-guard --before 2 --after 3

MCP arguments: {"before":2,"after":3}

lint — Lint a published version

Check one published version (default: the current one) for every shape rollout-guard flags, read against an empty version as a first publish is: not only what its publish added, so an entry pasted before a check existed, or in a publish whose guard nobody read, shows too. Kinds: keySplit, rolloutSurge, fullServe, shadowedRule, caseSensitiveEmail, versionStringOrdering, paddedEntry (a user-list entry or clause value with leading or trailing whitespace; names segmentKey, list and index, or the clause and valueIndex), danglingPrerequisite. Each finding carries text: the change that fixes it (define the entity, or normalize for whitespace and e-mail case in segment lists, then publish). Pass flagKey for one flag's findings. Use it when a report says a listed user or a matching value is not served, or before trusting an old version. Read-only, not billed.

Returns: { version, findings: [{ kind, flagKey, severity, ..., text }] }

loglune lint [--version <n>] [--flag <key>]
  • --version <n> — the published version (default: the current one)
  • --flag <key> — only the findings on this flag

Example:

loglune lint --flag new-reports

MCP arguments: {"flagKey":"new-reports"}

bisect — Bisect a flag flip

Find the published version, and the change in it, where a flag first evaluated to badValue for a context. From a bug report, pass its fingerprintId: the record fills contextKey, badValue, at, bucket and the attribute names it sent; a field you pass wins (another contextKey drops its bucket and names). An attribute the record did not send is never asked for (production lacked it). Give in attributes what the rules this context reaches read (never an unreached rule's). Pass at (the production instant) to stop at the version live then. The verdict is confirmed | since-creation | none | multiple | inconclusive | insufficient-input. since-creation: the change behind the one flip is the flag's own creation, so this context never got the good value and nothing can be rolled back (flips[0] names that version); keyListedAt, when set, is the version and event that put this user in a list the flag reads in another case or with spaces (matches as in explain's keyMatches): fix that entry or set caseInsensitive. Otherwise fix the answering rule with define, or kill. flips lists every verdict change with its version, recoveries included (a later fix shows as one: multiple). insufficient-input lists what is missing: an attribute name (add it; for a record's, hint names the values to ask for), "now" (pass at), "engine" (the record's SDK is newer, or older than a version needs: caseInsensitive 2, semver ops 3), or "key": a rule reads the raw user key or splits a rollout by it, and a report only carries an opaque key. For the flag's own key split, pass the record's reason.bucket as bucket (holds when the weights sum to a divisor of 100000; a fingerprintId passes it only if the answering rule hashed the raw key); a key clause or a prerequisite's split cannot be decided: use diff between versions. Read-only, not billed.

Returns: { result: { verdict, culprit, flips, inspected, missing, passes, keyListedAt? }, hint? }

loglune bisect --flag <key> (--fingerprint <id> | --context <key> --bad <json>) [--attrs <k=v,...>] [--at <ISO>] [--engine <n>] [--bucket <n>]
  • --flag <key> — the flag key
  • --fingerprint <id> — a bug-report record's id; fills --context, --bad, --at, --bucket and the attribute names it sent
  • --context <key> — the evaluation context key
  • --bad <json> — the wrong value observed (true, "off", 3, ...)
  • --attrs <k=v,...> — context attributes the rules read
  • --at <ISO> — the production instant T
  • --engine <n> — the engine version the report names
  • --bucket <n> — the flag's reason.bucket in the report, 0-99999

Example:

loglune bisect --flag checkout-v2 --context user-42 --bad true --attrs plan=beta

MCP arguments: {"flagKey":"checkout-v2","contextKey":"user-42","badValue":true,"attributes":{"plan":"beta"}}

explain — Explain a bug-report record

Read one bug-report record (fingerprintId from fingerprints) against the version it names: for each result, or flagKey's only, the flag (with description and owner), the rule its reason.ruleIndex points at as stored with the segments that rule names, or the prerequisite a prerequisiteFailed reason names. consistent is false when what the reason names in that version cannot serve the recorded variation (the record came from another ruleset). keyMatches lists the segment list entries the flag reads that are this record's user as written (exact), trimmed or lower-cased, by segmentKey, list and index: a lowercase or trim entry on a list without caseInsensitive never matched, so a user outside a list they belong to shows there; read the entry with inspect (flagKey, version) and fix its case or set caseInsensitive. Nothing is evaluated, so no attributes are needed. Then bisect with the same fingerprintId to find the change that caused it. Read-only, not billed.

Returns: { fingerprint: { fingerprintId, version, contextKey, results, at, reportId, attributes }, explanations: [{ flagKey, variationId, value, reason, flag, rule, segments, prerequisite, consistent, keyMatches: [{ segmentKey, list, index, transform }] }] }

loglune explain --fingerprint <id> [--flag <key>]
  • --fingerprint <id> — the record's fingerprintId (from fingerprints)
  • --flag <key> — explain only this flag's result

Example:

loglune explain --fingerprint 0b8f5c2e-7d41-4a8e-9a1f-3c6d2e9b7a10 --flag checkout-v2

MCP arguments: {"fingerprintId":"0b8f5c2e-7d41-4a8e-9a1f-3c6d2e9b7a10","flagKey":"checkout-v2"}

audit — Audit log

Read the tenant's change log (every define, oldest first) with its hash-chain verification. flagKey keeps only the changes to that flag (segments its rules name are not included: diff with flagKey shows those). Each event's publishedIn is the version that first shipped it; null means it is still in the draft, or the version before the one that covers it was pruned. A discarded edit and the discard that undid it share a version, so that version shipped neither. format ndjson returns the whole chain, one row per line with the verification last, and takes no flagKey. Read-only, not billed.

Returns: { events: [{ seq, at, actor, op, entity, targetKey, before, after, publishedIn }], chain } or { ndjson }

loglune audit [--flag <key>] [--format json|ndjson]
  • --flag <key> — only changes to this flag (json only)
  • --format json|ndjson — json (default) or one row per line

Example:

loglune audit --flag checkout-v2

MCP arguments: {"flagKey":"checkout-v2"}

fingerprints — Bug-report fingerprints

List up to 50 evaluation records from bug reports, newest first. Each names the version it was evaluated at; pass that to reproduce as target.version, and its fingerprintId to explain or bisect. A report whose call sites each sent a record shares one reportId: pass it to list only those records, and linked: the versions and notes stored with it (publish, rollback, kill, annotate take one). attributes names the attributes the context carried, never values (null: not sent). flagKey, variationId (same result), contextKey and from/to on the evaluation time (from <= at < to) narrow the list in the database, so the 50 are 50 matches; flagKey also narrows disagreements. A record holds only a hash of the user key: pass the ticket's customer ids or e-mails as userKeys (up to 50, hashed as evaluation records are, never stored) to list that customer's records, each naming the userKeys that hash to its contextKey (a 32-bit hash can be shared, so check the record's attributes). disagreements names each flag one report's records got different variations for, with each record's variation and reason. bucketSplit true: every record answered from one rollout (rule id, weights, bucketBy) unchanged across their versions, so only the hashed bucket differed. hashedBy names what that rollout hashed: 'key' means each call site hashed its own context key (e.g. an anonymous and a signed-in key); give the rollout a bucketBy from sharedAttributes, the attribute names every record sent, and check it with simulate. hashedBy an attribute not in sharedAttributes: a call site did not send it and hashed its raw key. Read-only, not billed.

Returns: { fingerprints: [{ fingerprintId, version, contextKey, results, at, ingestedAt, reportId, attributes, userKeys? }], disagreements: [{ reportId, flagKey, records: [{ fingerprintId, version, contextKey, variationId, reason }], bucketSplit, hashedBy, sharedAttributes }], linked?: { versions: [{ version, publishedAt, label }], notes: [{ noteId, version, actor, at, note }] } }

loglune fingerprints [--report-id <id>] [--flag <key>] [--variation <id>] [--context <key>] [--user-keys <k1,k2>] [--from <iso>] [--to <iso>]
  • --report-id <id> — only the records sent with this report id, plus the versions and notes linked to it
  • --flag <key> — only records with a result for this flag
  • --variation <id> — only records whose result got this variation (for --flag, when given)
  • --context <key> — only records stamped with this opaque context key
  • --user-keys <k1,k2> — only records of these raw user keys (comma-separated, up to 50; hashed, never stored)
  • --from <iso> — only records evaluated at or after this instant
  • --to <iso> — only records evaluated before this instant

Example:

loglune fingerprints

MCP arguments: {}

triage — Triage one report

Start a ticket here: one call joins what fingerprints, explain, audit and lint say about one report's records. Takes the reportId the records were sent with; userKeys (the ticket's customer ids or e-mails, hashed, never stored) keep only those users' records. flags covers flagKey if given, else the flags the records got different variations for, else every flag they carry. Per flag: answers, each record's variation and reason read against the version it names (rule or prerequisite as stored, consistent false when that version cannot serve it, keyMatches: the segment list entries that are its user as written, trimmed or lower-cased); disagreement, as in fingerprints (bucketSplit, hashedBy, sharedAttributes); changes, the 10 newest logged edits to the flag and to the segments its rules name, each with publishedIn (the version that shipped it, null while drafted); findings, the lint of the current version for the flag, each with text naming the fix; next, the fixes the records point at in words (a bucketBy for a split rollout, an entry to trim or lower-case, a record its version cannot serve, a segment rule on a shared attribute when the listed keys match none of the records). Check a fix with simulate records before publishing. 404 no-records when no record has that reportId. Read-only, not billed.

Returns: { reportId, version, fingerprints, flags: [{ flagKey, answers: [{ fingerprintId, contextKey, version, at, variationId, reason, rule, prerequisite, consistent, keyMatches }], disagreement, changes: [{ ...event, publishedIn }], findings: [{ ...finding, text }], next: [text] }], linked: { versions, notes } }

loglune triage --report-id <id> [--flag <key>] [--user-keys <k1,k2>]
  • --report-id <id> — the report id its records were sent with
  • --flag <key> — triage only this flag
  • --user-keys <k1,k2> — only the records of these raw user keys (comma-separated, up to 50; hashed, never stored)

Example:

loglune triage --report-id SUP-3120

MCP arguments: {"reportId":"SUP-3120"}

history — Billed reproductions

List the reproductions billed in one UTC month (default: the current one). Listing never bills again. Read-only.

Returns: { period, count, reproductions: [{ reproductionId, version, period, at }] }

loglune history [--period <YYYY-MM>]
  • --period <YYYY-MM> — the month (default: this month)

Example:

loglune history --period 2026-09

MCP arguments: {"period":"2026-09"}

define — Define a change (confirm)

Append one change to the draft: create, update or delete a flag, segment or config. Give after, the entity's FULL state (after.key = targetKey), or for update only patch: a JSON merge patch on the draft entity (named fields change, null removes one, arrays are replaced whole; rules or variations keyed by id patch only those items: {"rules":{"half":{"variationId":"on"},"old":null}}). Omit both for delete. Nothing goes live until publish. Changes state: ask the user first. Entity shapes: flag: {"key":"checkout-v2","kind":"bool","variations":[{"id":"on","value":true},{"id":"off","value":false}],"defaultVariationId":"off","rules":[]}. kind is bool|string|number|json. Optional prerequisites: [{"flagKey","requiredVariationId"}]; optional description (1-500 chars, what it gates) and owner (1-200 chars, who to ask first); evaluation ignores both. rule: {"id":"beta","clauses":[{"attribute":"plan","op":"in","values":["beta"]}],"variationId":"on"}. Exactly one of variationId or rollout {"bucketBy"?,"variations":[{"variationId","weight"}]}; optional segmentKeys. Clause ops: in, notIn, eq, neq, contains, gt, gte, lt, lte (strings order as text: "1.9.4" > "1.19.0"), before, after (instants), semverGt, semverGte, semverLt, semverLte (v prefix and 1.19 allowed; a non-version fails). Attribute "key" is the context key; "now" the evaluation time. Case-sensitive unless "caseInsensitive":true on in, notIn, eq, neq or contains (lower-cases both sides). segment: {"key":"beta-testers","included":["user-1"],"excluded":[],"rules":[]}; rules: [{"clauses":[...]}]. included and excluded match the context key exactly; "caseInsensitive":true compares both lists lower-cased. config: {"key":"checkout.timeout-ms","kind":"number","value":3000}.

Returns: { event: { seq, at, actor, op, entity, targetKey, before, after } } (after is the full state, also for a patch)

loglune define --op <create|update|delete> --entity <flag|segment|config> --key <key> [--payload <json> | --patch <json>] --confirm yes
  • --op <create|update|delete> — the change
  • --entity <flag|segment|config> — what changes
  • --key <key> — the entity key
  • --payload <json> — the full entity state (not for delete)
  • --patch <json> — update only: the fields to change (JSON merge patch), instead of --payload
  • --confirm yes — required: this changes state

Example:

loglune define --op update --entity segment --key beta-testers --patch '{"caseInsensitive":true}' --confirm yes

MCP arguments: {"mutationOp":"update","entity":"segment","targetKey":"beta-testers","patch":{"caseInsensitive":true},"confirm":true}

publish — Publish the draft (confirm)

Fold every change defined since the last publish into the next immutable version and make it live. Run simulate first to see what it would change. Pass label (1-200 characters) to store why it was published, such as the incident or ticket it answers; inspect returns it with the version. Pass reportId (the ticket's, as its records were sent with) to link the version to that report: fingerprints with that reportId lists it under linked. The result carries the rollout-guard findings against the previous version (guard.status first: the first version, read against an empty one, so a key split in a new flag is still named), and rejudged: the newest 50 stored bug-report records read against the new version (see simulate). Changes state: ask the user first.

Returns: { version, publishedAt, flagCount, guard: { status, against, findings }, rejudged: { status: "rejudged", records, same, changed, undetermined, changes } | { status: "unavailable" }, label?, reportId? }

loglune publish --confirm yes [--label <text>] [--report-id <id>] | loglune publish --dry-run yes [--contexts <json>] [--records <json>] [--flag <key>]
  • --confirm yes — required: this makes a new live version
  • --label <text> — a note stored with the version (1-200 characters)
  • --report-id <id> — the report (ticket) the version is published for
  • --dry-run yes — run simulate instead: nothing is written, no --confirm; takes the --contexts, --records and --flag of simulate

Example:

loglune publish --label 'Fix SUP-545' --report-id SUP-545 --confirm yes

MCP arguments: {"label":"Fix SUP-545","reportId":"SUP-545","confirm":true}

simulate — Simulate a publish

Fold the draft as publish would, writing nothing: its version number, the rollout-guard findings and field diff against the latest version (first version: guard against an empty one, diff null), and rejudged: the newest 50 stored bug-report records read against it, each at its own at and reason.bucket. Verdict per result: same, changed (variationId is the new answer) or undetermined (variationId null; missing names what the record lacks: a sent attribute, whose value it never holds, or "key" when a rule reads the raw user key). With only "key" missing, ifKeyIs gives, per list entry the flag reads that is the record's user as written, trimmed or lower-cased, the answer if the user is that entry (still undetermined: a 32-bit hash can be shared). notSent: attributes the rules read that the record did not send; production lacked them too, so their clauses failed and a split by one hashed the raw key. changes lists non-same results. To prove a fix for given users, pass contexts (raw user keys): evaluations gives each flag's draft and live result per context (live null before the first version). To judge stored records by a fix on an attribute they sent, pass records with the ticket's values per contextKey: records returns every record of those keys with all its verdicts, using only names it sent. flagKey narrows either to one flag. Read-only, not billed.

Returns: { version, flagCount, guard: { status, against, findings }, diff, rejudged: { records, same, changed, undetermined, changes: [{ fingerprintId, reportId, recordedVersion, flagKey, recordedVariationId, variationId, value, verdict, missing, notSent, ifKeyIs: [{ match, variationId, value, verdict }] }] }, evaluations?: [{ key, draft: [{ flagKey, variationId, value, reason, missing }], live }], records?: [{ fingerprintId, contextKey, reportId, recordedVersion, attributes, results: [{ flagKey, recordedVariationId, variationId, value, verdict, missing, notSent, ifKeyIs }] }] }

loglune simulate [--contexts <json>] [--records <json>] [--flag <key>]
  • --contexts <json> — contexts to evaluate the draft and live version for: [{"key":"u1","attributes":{"appVersion":"1.9.4"}}]
  • --records <json> — attribute values for stored records, by contextKey: [{"contextKey":"5d6ade90","attributes":{"accountId":"acct-2210"}}]
  • --flag <key> — evaluate only this flag for the contexts or records

Example:

loglune simulate --contexts '[{"key":"u1","attributes":{"appVersion":"1.9.4"}}]'

MCP arguments: {"contexts":[{"key":"u1","attributes":{"appVersion":"1.9.4"}}]}

annotate — Annotate a version (confirm)

Add a note (1-200 characters) to a version that is already published, such as the incident it caused, the fix, or a ticket link. Each call appends one note with who added it and when; the label the version was published with stays as it was, and nothing about the version or its evaluation changes. inspect returns the notes. Pass reportId (the ticket's, as its records were sent with) to link the note to that report: fingerprints with that reportId lists it under linked. 404 when the version does not exist. Versions are consecutive integers from 1; inspect with no version returns the current one. Changes state: ask the user first.

Returns: { version, note: { noteId, actor, at, note, reportId? } }

loglune annotate --version <n> --note <text> [--report-id <id>] --confirm yes
  • --version <n> — the published version
  • --note <text> — the note (1-200 characters)
  • --report-id <id> — the report (ticket) the note is for
  • --confirm yes — required: this adds a note

Example:

loglune annotate --version 3 --note 'Caused INC-166: billing-banner shown to free users' --report-id INC-166 --confirm yes

MCP arguments: {"version":3,"note":"Caused INC-166: billing-banner shown to free users","reportId":"INC-166","confirm":true}

rollback — Roll back to a version (confirm)

Append the edits that make the draft equal an earlier published version, then publish them at once as the next version (the log is only appended, so the rollback is itself in diff, bisect and revert). 409 draft-pending lists the unpublished edits when the draft differs from the live version, because publishing would ship them too: discard, revert or publish them first. 409 no-change when the live version already equals it; 404 when the version does not exist. The label defaults to "rollback to v<n>"; reportId links it to a report as publish does. Returns what publish returns plus the appended events. Versions are consecutive integers from 1; inspect with no version returns the current one. Changes state: ask the user first.

Returns: { version, publishedAt, flagCount, guard, rejudged, label, reportId?, events: [{ seq, op, entity, targetKey, ... }] }

loglune rollback --to <n> [--label <text>] [--report-id <id>] --confirm yes
  • --to <n> — the published version to return to
  • --label <text> — the note stored with the new version (1-200 characters)
  • --report-id <id> — the report (ticket) the new version is published for
  • --confirm yes — required: this makes a new live version

Example:

loglune rollback --to 2 --confirm yes

MCP arguments: {"to":2,"confirm":true}

kill — Kill switch (confirm)

Drop the targeting rules of one flag and serve one variation to every context (default: the flag's default variation), then publish at once as the next version. Prerequisites stay. Undo it with revert on the kill's event seq, then publish. 409 draft-pending lists the unpublished edits when the draft differs from the live version (discard drops them); 409 no-change when the flag already serves only that variation; 404 flag-not-in-draft; 400 unknown-variation lists the flag's variation ids. The label defaults to "kill <flagKey>"; reportId links it to a report as publish does. Returns what publish returns plus the appended event. Changes state: ask the user first.

Returns: { version, publishedAt, flagCount, guard, rejudged, label, reportId?, events: [{ seq, op, entity, targetKey, ... }] }

loglune kill --flag <key> [--variation <id>] [--label <text>] [--report-id <id>] --confirm yes
  • --flag <key> — the flag key
  • --variation <id> — the variation to serve (default: the flag's default)
  • --label <text> — the note stored with the new version (1-200 characters)
  • --report-id <id> — the report (ticket) the new version is published for
  • --confirm yes — required: this makes a new live version

Example:

loglune kill --flag checkout-v2 --variation off --confirm yes

MCP arguments: {"flagKey":"checkout-v2","variationId":"off","confirm":true}

revert — Revert one logged change (confirm)

Append one edit that puts the entity changed at event seq back to its state just before that event. It goes to the draft like define: run simulate, then publish. 409 superseded lists laterSeqs, the later events on the same entity: revert those first, newest first. 409 no-change when that event changed nothing; 404 no-event when no event has that seq. Event seqs come from the events of rollback, kill and define results. Changes state: ask the user first.

Returns: { event: { seq, op, entity, targetKey, before, after, actor, at } }

loglune revert --seq <n> --confirm yes
  • --seq <n> — the event seq to undo
  • --confirm yes — required: this changes the draft

Example:

loglune revert --seq 7 --confirm yes

MCP arguments: {"seq":7,"confirm":true}

discard — Discard the draft (confirm)

Append the edits that put the draft back on the live version, without publishing: the unpublished edits stay in audit, and the next publish ships nothing from them. Use it when rollback or kill answers 409 draft-pending and those edits should not ship, then call rollback or kill again. discarded lists the entities that differed; version is the live version the draft now equals. 409 no-change when the draft already equals it. Changes state: ask the user first.

Returns: { version, discarded: [{ entity, key }], events: [{ seq, op, entity, targetKey, ... }] }

loglune discard --confirm yes
  • --confirm yes — required: this changes the draft

Example:

loglune discard --confirm yes

MCP arguments: {"confirm":true}

normalize — Normalize segment lists (confirm)

Rewrite one segment (segmentKey) or every segment in the draft. trim drops leading and trailing whitespace from the included and excluded entries and from the string values of the segment's rule clauses; lowercase lower-cases the entries holding "@" (e-mail addresses), because other keys may be case-significant ids. An entry that becomes a copy of an earlier one in the same list is dropped (after: null) and the first kept. Give trim, lowercase or both. Positions are the draft's before the rewrite. The edits go to the draft like define, one per changed segment: run simulate, then publish. Use it for lint's paddedEntry and caseSensitiveEmail findings on segment lists; a flag rule's own clause values are fixed with define. 404 segment-not-in-draft; 409 no-change when nothing would change. Changes state: ask the user first.

Returns: { changes: [{ segmentKey, entries: [{ list, index, before, after }], values: [{ ruleIndex, clauseIndex, valueIndex, before, after }] }], events: [{ seq, op, entity, targetKey, ... }] }

loglune normalize [--segment <key>] [--trim yes] [--lowercase yes] --confirm yes
  • --segment <key> — only this segment (default: every segment in the draft)
  • --trim yes — drop leading and trailing whitespace
  • --lowercase yes — lower-case the e-mail entries
  • --confirm yes — required: this changes the draft

Example:

loglune normalize --segment beta-customers --trim yes --lowercase yes --confirm yes

MCP arguments: {"segmentKey":"beta-customers","trim":true,"lowercase":true,"confirm":true}

readiness — Flag removal readiness

Judge whether one flag can be removed from code, by rule (no model): ok | caution | no, with the served variation, the last change and dependents. Default: the current version. Read-only, not billed.

Returns: { readiness: { flagKey, version, verdict, servedVariationId, lastChangedAt, reasons, ... } }

loglune readiness --flag <key> [--version <n>]
  • --flag <key> — the flag key
  • --version <n> — the version (default: the current one)

Example:

loglune readiness --flag checkout-v2

MCP arguments: {"flagKey":"checkout-v2"}

restore-point — Record a restore point

Read the restore bookmark of the tenant database now, or at an instant within the last 30 days; keep it before a risky change so restore can roll back to it. Answers 501 restore_unavailable where the database has no point-in-time history (a local deployment); retrying does not help. Read-only.

Returns: { bookmark }

loglune restore-point [--at <ISO>]
  • --at <ISO> — an instant in the last 30 days (default: now)

Example:

loglune restore-point

MCP arguments: {}

restore — Restore the customer database (confirm)

Roll the tenant database back to a bookmark or an instant within the last 30 days. OVERWRITES every write after that point; keep the returned undoBookmark to reverse it. Refused (409) at or before the tenant's last data erasure. Changes state: ask the user first.

Returns: { bookmark, undoBookmark }

loglune restore (--at <ISO> | --bookmark <b>) --confirm yes
  • --at <ISO> — an instant in the last 30 days
  • --bookmark <b> — a bookmark from restore-point
  • --confirm yes — required: this overwrites data

Example:

loglune restore --bookmark 0000...abc --confirm yes

MCP arguments: {"target":{"bookmark":"0000...abc"},"confirm":true}

ai-launch, ai-status and ai-cancel (Loglune-run AI) are coming soon; ai-launch and ai-cancel answer 403 coming_soon. Until then, run your own agent over the operations above.

Errors and what to do next

A refused call comes back with its HTTP status, error, reason and a hint: an MCP error result carries them as JSON, and the CLI prints the hint under the status line. The most specific row matches first; {op} stands for the operation called.

Statuserror / reasonHint
400invalid_request / confirm{op} changes state or bills. Ask the user, then call again with confirm: true (CLI: --confirm yes).
400invalid_request / invalid-stateafter, or the entity a patch makes, does not match the entity shape; fix the fields listed in issues. after is the full entity state (see the define description). Do not retry unchanged.
400invalid_request / unknown-idA patch keyed by id names an item the draft array lacks (issues gives the path). Read the ids with inspect, or give the whole array to add an item. Do not retry unchanged.
400invalid_request / key-mismatchafter.key must equal targetKey. Do not retry unchanged.
400invalid_request / out-of-rangeThe instant must lie within the last 30 days and not in the future. Do not retry unchanged.
400invalid_request / unknown-variationThat variation is not one of the flag's; pick one from variationIds, or omit variationId to serve the flag's default. Do not retry unchanged.
400invalid_request / idempotency-keyPass idempotencyKey (a short string you reuse to replay the same launch).
400invalid_request / unknown-opop is absent or names no op this server runs (loglune help lists them; a CLI newer than the server may know more). Do not retry unchanged.
400invalid_request / not-objectThe request body must be a JSON object: {"op": "<name>", ...that op's fields}. Do not retry unchanged.
400invalid_requestThe arguments do not match the input schema of {op}; fix the fields listed in issues (CLI: loglune help {op}). Do not retry unchanged.
401anyThe operator token is missing, malformed or revoked. Ask the user for a valid token (issued at /app/tokens). Do not retry.
403forbidden / scopeThis token's scope does not cover {op} (a read token cannot write or bill; a write token cannot read). Ask the user for a token that includes {op}. Do not retry.
403forbidden / allowlist{op} was switched off when this token was issued. Ask the user for a token that includes {op}. Do not retry.
403forbidden / coming_soonLoglune-run AI is not offered yet. Do the analysis yourself with inspect, diff, bisect and readiness. Do not retry.
403forbidden / ai_disabledAI operations are turned off on this deployment. Do not retry.
403forbidden / planThe workspace plan does not include this. The user can change the plan at /app/billing. Do not retry.
403outside_retentionThat version is older than the plan keeps. Pick a newer version (inspect shows the current one). Do not retry.
403reproduction_limit_reachedThis month's reproductions are used up. inspect and diff are free; ask the user before anything else. Do not retry.
404not_found / flag-not-in-versionThat flag key is not in this version. Call inspect without flagKey to list its flags, or try another version.
404not_found / flag-not-in-versionsNeither version has that flag key. Call inspect with each version to list its flags, or diff without flagKey.
404not_found / no-fingerprintNo record has that fingerprintId in this workspace. Call fingerprints (filter by flagKey, contextKey or reportId) and copy an id from it.
404not_found / flag-not-in-fingerprintThat record has no result for this flag. Call explain with only fingerprintId to list the flags it holds.
404not_found / no-recordsNo record carries that reportId (or none of those userKeys within it). Call fingerprints with userKeys or contextKey to find the records, and check the report id the app sent with them.
404not_found / fingerprint-version-goneThe version this record names is no longer stored (pruned). bisect with contextKey and badValue still scans the kept versions.
404not_found / no-version-at-timeNothing was published at or before that instant. Use a later at.
404not_found / flag-not-in-draftThe draft has no flag with that key. Call inspect with no arguments to list the flags.
404not_found / segment-not-in-draftThe draft has no segment with that key. Call inspect with the flagKey whose rules name it to see its segments, or omit segmentKey to rewrite every segment.
404not_found / entity-not-in-draftThe draft has no entity of that kind with that key, so there is nothing to patch. Call inspect to list the flags (and with flagKey, its segments), or create it with a full after.
404not_found / no-eventNo logged change has that seq. Call audit to list the changes and their seqs.
404not_found / repo_not_boundThat repository is not approved for this workspace. Ask the user to approve it in the console.
404no_version_at_timeNothing was published at or before that instant. Use a later at.
404version_not_foundNo such version. Call inspect with no arguments for the current version; versions run from 1 to it.
404not_deliverableThat version can no longer be served (its stored ruleset is gone). Pick another version.
404anyNo such version, flag or request. Call inspect with no arguments for the current version; versions run from 1 to it.
409conflict / before-erasureThat point is at or before the workspace's last data erasure; restoring it would bring erased data back. Pick a later point. Do not retry unchanged.
409conflict / draft-pending{op} publishes, and a publish ships the whole draft, which holds the unpublished edits listed in pending. Read them with simulate, then publish them, or drop them all with discard (or one at a time with revert and the seqs audit lists), and call {op} again.
409conflict / no-changeThe draft already matches what {op} would make; nothing was written or published. Do not retry unchanged.
409conflict / supersededLater changes (laterSeqs) edited the same entity; reverting this one would throw them away. Revert those first, newest first, or define the state you want.
409conflict / idempotency-keyThis idempotency key was used with other arguments. Use a new key.
429anyA quota or spend cap is used up. Wait retryAfter seconds, then retry once.
500anyThe server failed. Retry once; if it fails again, tell the user.
501restore_unavailableThis deployment has no point-in-time history of the database (a local deployment), so restore-point and restore cannot work here. Do not retry.
503anyThe server or its AI provider is unavailable. Retry later; tell the user if it persists.

CLI output

  • --json prints { ok, status, body, hint?, retryAfter? } on stdout for every result, including failures.
  • Exit codes: 0 success (and help), 1 the request failed (HTTP error or no response), 2 usage error.
  • loglune help <command> prints one command's flags, result and example.