Skip to content
mcpd
no daemon
Source

HTTP API

The whole HTTP surface on one page. Generated from the specification the server routes are built from, so an endpoint documented here is an endpoint that exists.

Generated file

Every endpoint section on this page is produced by tools/codegen from spec/mcpd.spec.json. Do not edit it by hand. Change the spec, run make gen, and the server, the SDKs, the OpenAPI document and this page all move together. make check fails the build when any of them drift apart.

There are 72 endpoints under specification 1.0.0. Four of them accept signals, four answer a Kubernetes probe or a Prometheus scrape, and the other sixty-four read back what was stored, attach a judgement to it, or delete it. If you would rather generate a client than read this, spec/openapi.json is emitted from the same file.

Before the endpoints

Four things are true of the API as a whole, so they are stated here once rather than repeated under every endpoint below.

Address

There is no hosted API and no base URL to look up. The API is served by your own daemon on whatever address it listens on, which is 0.0.0.0:4317 unless you set --listen. The operator interface is served from the same port, so the address in your browser is the address your client uses.

a read against a local daemon

curl -s 'http://localhost:4317/v1/api/traces?project=default&limit=5'

Keys and scope

Present a key as Authorization: Bearer <key>, or as the x-mcpd-key header if a proxy has taken the first one. There are six scopes, and each endpoint below names the one it takes. ingest writes signals and reads nothing. read reads stored signals, which is where the prompts are. write changes stored data: scores, feedback, datasets, experiments and acknowledgements. execute starts, cancels and signals runs. worker claims steps and reports what happened to them. admin deletes, and reads the audit log. Handing a fleet of agents a key that can also read back every stored prompt is the mistake the split exists to prevent.

Configure them with --ingest-key, --read-key, --write-key, --admin-key, --execute-key and --worker-key. Four of them fall back: an unset write list accepts the ingest keys, an unset admin list accepts the read keys, and unset execute and worker lists accept the write keys, so a deployment configured before the narrower scopes existed keeps the reach it had. With no keys set at all, every scope is open: that is what makes the first run take five minutes, and it is also why the daemon warns at startup when it is listening on anything other than loopback with a scope open. Security has the full table.

Failures

Every non-2xx response is JSON with the same three fields: a stable error code to branch on, a message written for whoever has to fix it, and field when one parameter is at fault. A response that only says 500 costs the reader an hour, so nothing here returns one without saying what happened.

Paging

Lists page by cursor, never by offset. A response carries next_cursor until the last page, and you pass it back as cursor. Offsets skip and repeat rows when new traces arrive between two requests, which on an ingest path that is always writing is most of the time. Cursors are opaque: read them, do not build them.

What a write goes through

The four ingest endpoints differ only in what they decode. After that they share one pipeline, and its order is the part worth knowing: redaction runs before storage, not as a cleanup job afterwards. A value a rule matches is never written down, so there is no window in which it existed on disk and no job that has to find it again.

Ingest pipeline
  1. Decode

    OTLP or native, gzip or plain

  2. Sample

    Decided once, at the root

  3. Redact

    Before anything is written

  4. Price

    From the configured price book

  5. Store

    One transaction per batch

Redaction is stage three, not a cleanup job. A value that a rule matches never reaches storage at all.

The endpoints

Grouped as the specification groups them. A method takes its colour from what the request does to your data: a read is nominal, a write asks for attention, a delete destroys.

Ingest

Receive OpenTelemetry traces

POST/v1/traces

The standard OTLP/HTTP trace endpoint. Accepts protobuf (application/x-protobuf) and JSON (application/json), gzip-encoded or uncompressed. Point any OpenTelemetry SDK here and mcpd will record its spans, mapping GenAI and OpenInference semantic conventions onto mcpd’s own model. This is the endpoint that makes mcpd framework-agnostic: nothing about it requires an mcpd SDK.

Request body: Otlp.ExportTraceServiceRequest

Response: Otlp.ExportTraceServiceResponse

Authentication: an API key with ingest scope.


Receive OpenTelemetry logs

POST/v1/logs

The standard OTLP/HTTP log endpoint. Records correlate to spans automatically through their trace and span ids.

Request body: Otlp.ExportLogsServiceRequest

Response: Otlp.ExportLogsServiceResponse

Authentication: an API key with ingest scope.


Receive OpenTelemetry metrics

POST/v1/metrics

The standard OTLP/HTTP metric endpoint, including exemplars, which mcpd turns into links back to the exact span that produced a data point.

Request body: Otlp.ExportMetricsServiceRequest

Response: Otlp.ExportMetricsServiceResponse

Authentication: an API key with ingest scope.


Receive mcpd signals directly

POST/v1/ingest

Accepts mcpd’s own shapes for all five signals in one batch, posted as a single IngestBatch object, gzip-encoded or uncompressed. Use this when you want the full model without translating through OTLP: token detail, cost, scores and feedback all survive intact. Supply ingest_key on a batch to make retries idempotent.

Request body: IngestBatch

Response: IngestResult

Authentication: an API key with ingest scope.


Traces

List traces

GET/v1/api/traces

The trace list. Filter on status, kind, name, tags, session, thread, user, environment, model, time window, duration, cost and score, and sort by start time, duration, cost, span count or score. Unlike a plain span dump, this reads the materialised trace projection rather than aggregating spans on every request.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
q string · Full-text search across span names, prompts and completions. This is the field that answers ‘find the run where the model said that’.
status SpanStatus · Only traces with this outcome.
kind SpanKind · Only traces whose root is this kind.
name string · Exact root span name.
tags string[] · Traces carrying all of these tags.
session_id string · Every trace in one conversation.
thread_id string · Every trace in one thread.
user_id string · Every trace for one end user. Stored ids are hashed, and this value is hashed the same way before matching, so pass the plain id you sent.
environment string · Restrict to one environment.
model string · Traces that used this model.
from timestamp · Start of the time window, inclusive.
to timestamp · End of the time window, exclusive.
min_duration_ms u64 · Only traces at least this slow.
min_cost f64 · Only traces at least this expensive.
has_score string · Only traces carrying a score from this scorer.
score_below f64 · Combined with has_score: only traces where that scorer came in under this value. This is the query that finds bad answers.
sort string start_time One of start_time, duration, cost, span_count, score. Defaults to start_time.
order string desc asc or desc. Defaults to desc.
limit u32 50 Page size, 1 to 500.
cursor string · Opaque cursor from a previous page.

Response: TraceListResponse

Authentication: an API key with read scope.


Get one trace with every span

GET/v1/api/traces/{trace_id}

Returns the trace projection and the full span tree in one response, ordered so a client can render the waterfall without a second request or a client-side sort.

Path parameters

Name Type Description
trace_id string The trace to fetch.

Query parameters

Name Type Default Description
include string · Comma-separated extras: logs, scores, feedback, metrics. Fetching a run and everything attached to it is one request.

Response: TraceDetail

Authentication: an API key with read scope.


Delete a trace and everything attached to it

DELETE/v1/api/traces/{trace_id}

Removes the trace, its spans, logs, metrics, scores and feedback. Used to satisfy an erasure request. The deletion is recorded in the audit log.

Path parameters

Name Type Description
trace_id string The trace to delete.

Response: DeleteResult

Authentication: an API key with admin scope.


List the projects that have data

GET/v1/api/projects

Every project mcpd has stored a signal for. Projects are not created up front: a project exists as soon as something arrives carrying its name, which is why this reads the data rather than a registry.

Response: String

Authentication: an API key with read scope.


Discovery

The values a filter dimension takes

GET/v1/api/discovery

What to offer in a filter box. A filter is only usable if you know what to type into it, and the values are not a fixed list: they are whatever the services reporting to this project happen to send. This reads the distinct-value list the daemon maintains as signals arrive, scoped to one project, prefix-matched for typeahead and ordered by value. Ask for one dimension at a time. One limit is worth knowing before you build against it: metric_label_key lists every label key seen anywhere in the project, not the keys of one named metric, because the list is keyed by the label and not by the metric that carried it.

Query parameters

Name Type Default Description
project string required Which project to read. Every dimension is scoped to one.
facet Facet required Which dimension to list. Required.
key string · For metric_label_value, the label key whose values you want. Required for that facet, because the answer would otherwise be every label’s values mixed together. Ignored by every other facet.
prefix string · Offer only values starting with this. Matched literally and case sensitively, because these values are what a filter will be compared against and folding here would offer one that then matches nothing.
limit u32 100 How many values to return, up to 1000. A filter box shows a list somebody reads; ten thousand environments is not a longer list, it is a slower request that renders the same first page.

Response: DiscoveryResponse

Other responses

Status Body When
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


What has arrived since you last looked

GET/v1/api/deltas

Refresh a list in place instead of running its query again. Hold the cursor from the last poll, send it back, and get the rows that have arrived since, oldest first. The watermark is a row’s arrival time and its id together, because a batch writes every row in it with one timestamp and a watermark that could only say ‘after this nanosecond’ would either replay the whole batch or skip the rest of it. A row written with an arrival time behind the watermark, which a sender can cause by setting received_at itself, is not returned; that is the cost of a watermark and no cursor shape avoids it.

Query parameters

Name Type Default Description
project string required Which project to poll.
signal DeltaSignal traces Which signal to poll. Defaults to traces.
cursor string · The cursor the previous poll returned, unchanged. Absent starts from since, or from the oldest row the project holds when that is absent too. A cursor this daemon did not issue is refused rather than guessed at.
since timestamp · Where to start when there is no cursor yet. Ignored once cursor is set.
limit u32 100 Page size, 1 to 1000.

Response: DeltaResponse

Other responses

Status Body When
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


Spans

Search spans across traces

GET/v1/api/spans

Search individual steps rather than whole runs. This is how you answer ‘which tool call is slow’ or ‘show me every call to this model that errored’, across every trace at once.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
q string · Full-text search across name, input and output.
kind SpanKind · Only this kind of step.
status SpanStatus · Only this outcome.
name string · Exact span name.
model string · Only spans against this model.
trace_id string · Restrict to one trace.
from timestamp · Start of the window.
to timestamp · End of the window.
min_duration_ms u64 · Only spans at least this slow.
sort string start_time start_time, duration or cost.
order string desc asc or desc.
limit u32 50 Page size, 1 to 500.
cursor string · Opaque cursor.

Response: SpanListResponse

Authentication: an API key with read scope.


Logs

Search logs

GET/v1/api/logs

Log search, filtered by minimum severity, text, time window, trace and span. Because logs carry trace ids, every result links straight to the run that produced it.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
q string · Full-text search across the message and structured fields.
level LogLevel · Minimum severity, inclusive.
trace_id string · Only logs from this run.
span_id string · Only logs from this step.
from timestamp · Start of the window.
to timestamp · End of the window.
order string desc asc or desc. Defaults to desc, newest first, which is where an investigation starts.
limit u32 100 Page size, 1 to 1000.
cursor string · Opaque cursor.

Response: LogListResponse

Authentication: an API key with read scope.


Metrics

Query a metric as a time series

GET/v1/api/metrics

Aggregates raw points into time buckets and returns a series per label combination. Supports sum, count, mean, min, max and quantiles, so a chart is one request rather than a client-side fold over raw points.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
name string required Metric name. Required.
from timestamp required Start of the window. Required.
to timestamp required End of the window. Required.
bucket string · Bucket width, such as 1m, 5m, 1h. Chosen automatically when omitted.
agg string mean sum, count, mean, min, max, p50, p90, p95 or p99.
group_by string[] · Label keys to split the series on.
filter string · Label equality filters, as key:value pairs joined by commas: model:gpt-4o,status:ok. A point matches when every named label has that value. A colon, not =, so the value needs no escaping in a query string. A key cannot contain : or ,, and a value cannot contain ,. A JSON object here matches no point, and the answer is an empty series.

Response: MetricSeriesResponse

Authentication: an API key with read scope.


Evaluation

List scores

GET/v1/api/scores

Every machine evaluation, filterable by scorer, source, run, experiment, value range and time.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
scorer_id string · Only this scorer.
source ScoreSource · Only scores from this source.
trace_id string · Only scores for this run.
experiment_id string · Only scores recorded by this experiment.
min_value f64 · Lower bound, inclusive.
max_value f64 · Upper bound, inclusive.
from timestamp · Start of the window.
to timestamp · End of the window.
limit u32 50 Page size.
cursor string · Opaque cursor.

Response: ScoreListResponse

Authentication: an API key with read scope.


Attach a score

POST/v1/api/scores

Records a machine evaluation against a span or a trace. Supply min and max: a bare number with no declared scale cannot be compared to anything.

Request body: Score

Response: Score

Authentication: an API key with write scope.


Attach human feedback

POST/v1/api/feedback

Records a person’s judgement of a run or a step. Call this from your own product’s thumbs-up control and the judgement lands on the exact trace that produced the answer.

Request body: Feedback

Response: Feedback

Authentication: an API key with write scope.


List human feedback

GET/v1/api/feedback

The human judgements on one run, read from the trace detail. trace_id is required and is refused when absent: mcpd cannot yet list a whole project’s feedback, and answering an empty list instead would read as though nobody had judged anything. Every other parameter is refused by name as well, so a filter this endpoint does not apply cannot come back as a plausible wrong answer.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
trace_id string required The run whose judgements to read. Required.
limit u32 50 Page size.

Response: FeedbackListResponse

Authentication: an API key with read scope.


List the built-in scorers

GET/v1/api/scorers

Every scorer mcpd ships, with what it measures and whether it needs configuration. Scorers are deterministic code, not model calls, so they cost nothing to run and give the same answer twice.

Response: ScorerInfo

Authentication: an API key with read scope.


Run scorers over stored runs

POST/v1/api/score

Scores runs that are already stored, and records the results. Naming a scorer that does not exist fails the request before any work happens; a scorer that cannot answer on a given run is reported in skipped rather than failing it.

Request body: ScoreRunRequest

Response: ScoreRunResult

Authentication: an API key with write scope.


Compare two experiments

GET/v1/api/experiments/compare

Reports the per-scorer delta between two experiments beside the sample size on each side, and refuses to call a comparison decidable below twenty observations.

Query parameters

Name Type Default Description
baseline string required The experiment to compare against.
candidate string required The experiment being judged.
project string · Which project to read.

Response: ExperimentComparison

Authentication: an API key with read scope.


Analytics

Headline numbers for a time window

GET/v1/api/analytics/summary

Run count, error rate, latency quantiles, token totals and cost for a window, optionally split by a dimension. One request backs the overview screen.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
from timestamp · Start of the window. Defaults to 24 hours before to.
to timestamp · End of the window. Defaults to now.
group_by string · Split by model, entity_name, environment, status or session_id.

Response: AnalyticsSummary

Authentication: an API key with read scope.


Cost broken down

GET/v1/api/analytics/cost

Spend over a window, split by model, provider, entity, environment, status, session, user, service or run name, with token detail behind each figure. Answers ‘where is the money going’ without exporting to a spreadsheet.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
from timestamp · Start of the window. Defaults to seven days before to.
to timestamp · End of the window. Defaults to now.
group_by string model model, provider, entity_name, environment, status, session_id, user_id, service_name or name.
bucket string · Time bucket for a series, such as 1h or 1d. Omit for totals only.

Response: CostBreakdown

Authentication: an API key with read scope.


Datasets

List datasets

GET/v1/api/datasets

Every dataset in the project, with its case count.

Query parameters

Name Type Default Description
project string · Which project to read.

Response: Dataset

Authentication: an API key with read scope.


Create a dataset

POST/v1/api/datasets

A dataset is a named set of cases to run a version of an agent against. Leave dataset_id empty and mcpd assigns one.

Request body: Dataset

Response: Dataset

Authentication: an API key with write scope.


List the cases in a dataset

GET/v1/api/datasets/{dataset_id}/items

The cases themselves, each with its input, its expected answer when it has one, and the run it was captured from.

Path parameters

Name Type Description
dataset_id string Which dataset.

Query parameters

Name Type Default Description
limit u32 500 Page size.

Response: DatasetItem

Authentication: an API key with read scope.


Add cases to a dataset

POST/v1/api/datasets/{dataset_id}/items

Adds cases written by hand or exported from somewhere else. Leave each item_id empty and mcpd assigns one.

Path parameters

Name Type Description
dataset_id string Which dataset.

Request body: DatasetItem

Response: ItemsAdded

Authentication: an API key with write scope.


Capture production runs as cases

POST/v1/api/datasets/{dataset_id}/capture

Turns runs that already happened into permanent cases. This is what datasets are for: the run that went wrong in production on Tuesday becomes a regression test that every later version has to pass, and it keeps its trace id, so the case always points back at the run it came from.

Path parameters

Name Type Description
dataset_id string The dataset to add to.

Query parameters

Name Type Default Description
project string · Which project the runs are in.

Request body: CaptureRequest

Response: CaptureResult

Authentication: an API key with write scope.


List experiments

GET/v1/api/experiments

Every recorded run of a dataset against a version of an agent.

Query parameters

Name Type Default Description
project string · Which project to read.

Response: Experiment

Authentication: an API key with read scope.


Record an experiment

POST/v1/api/experiments

Registers a run of a dataset so its scores can be compared against another version later. Scores recorded with this experiment’s id are what compare reads.

Request body: Experiment

Response: Experiment

Authentication: an API key with write scope.


Operations

Liveness

GET/healthz

Returns 200 as long as the process is running. Wire this to a Kubernetes liveness probe. It never touches storage, so a slow database cannot cause a restart loop.

Response: HealthStatus

Authentication: none; this endpoint is public.


Readiness

GET/readyz

Returns 200 once storage is reachable and migrations have completed. Wire this to a Kubernetes readiness probe so traffic is not sent to an instance that cannot serve it.

Response: HealthStatus

Authentication: none; this endpoint is public.


Prometheus metrics about mcpd itself

GET/metrics

mcpd’s own counters in Prometheus text format: signals accepted, rejected, over quota and unbounded, duplicate batches, traces sampled out, ingest and query requests handled, request errors, audit rows that could not be written, and uptime. An observability tool that cannot be observed is a liability.

Response: String

Authentication: none; this endpoint is public.


What this deployment’s storage backend can do

GET/v1/api/capabilities

The capability set of the store behind this daemon. mcpd has one storage trait and three backends, and they differ: discovery, delta polling, per-signal retention and case folding outside ASCII are not on all three. Read this once and offer only what the deployment answers, rather than discovering a difference from an empty list. An endpoint whose capability is false refuses with 501 and names the same field, so nothing here can disagree with what the daemon actually does.

Response: StoreCapabilities

Authentication: an API key with read scope.


Audit

Read the audit log

GET/v1/api/audit

Every recorded read of stored payloads and every deletion, newest first. Writing an audit row never fails the request it describes: a row that cannot be written is logged and counted as mcpd_audit_write_failures_total on /metrics, and the read still returns.

Query parameters

Name Type Default Description
project string · Which project to read. Defaults to the account’s own project for an account credential, else the daemon’s default project.
action string · Only rows recording this action, such as delete_trace.
from timestamp · Start of the window.
to timestamp · End of the window.
limit u32 100 Page size, 1 to 1000.

Response: AuditEvent

Authentication: an API key with admin scope.


Privacy

Erase everything recorded about one person

DELETE/v1/api/subjects/{user_id}

Removes every run in the project attributed to this user, with the spans, logs, metrics, scores and feedback attached to those runs, and any log line recorded against the user on its own. On a daemon that serves durable runs, it also removes every ended run whose correlation carries this user_id, with its whole tree of subworkflow runs and everything stored for them; a run that has not ended is counted in runs_in_flight and left whole. This answers a deletion request in one call, instead of finding each run by hand and deleting it trace by trace. Only the project in the request is touched, and only if the key reaches it: the same user id in a project the key cannot open is left alone and is not counted, so a count can never disclose that the subject exists elsewhere. Returns a count per signal, so the reply to the person can say what went. The erasure is recorded in the audit log.

Path parameters

Name Type Description
user_id string The person to erase, written as the user_id your instrumentation set on the run. Send it as you sent it at ingest; the daemon applies the same hashing it applied then.

Query parameters

Name Type Default Description
project string · Which project to erase within. Defaults to the account’s own project for an account credential, else the daemon’s default project.

Response: ErasureResult

Authentication: an API key with admin scope.


Runs

Store a definition

POST/v1/api/definitions

Stores one version of a definition. The canonical body is the document with sorted keys and no insignificant whitespace, minus version. An identical body already stored is confirmed at its version rather than stored again, so posting the same code twice is a no-op, and a version is immutable once stored. A body the definition schema refuses is a 400 naming the field.

Query parameters

Name Type Default Description
project string · Which project. A selector: refused with 403 on a mismatch with the key’s grant, filled in for a single-project key, and a 400 when a multi-project key names none.

Request body: DefinitionDocument

Response: DefinitionVersion

Other responses

Status Body When
409 · A stored version holds a different body under the number you named. A stored version never changes: post the body under a new version, or leave version absent and let the daemon assign one.

Authentication: an API key with write scope.


List definitions

GET/v1/api/definitions

Every stored version of every definition in the project, with its body.

Query parameters

Name Type Default Description
project string · Which project. A selector: refused with 403 on a mismatch with the key’s grant, filled in for a single-project key, and a 400 when a multi-project key names none.

Response: Definition

Authentication: an API key with read scope.


Get one definition

GET/v1/api/definitions/{id}

One definition. Without version, the highest stored version, which is the one a run started now would pin.

Path parameters

Name Type Description
id string The definition’s identifier.

Query parameters

Name Type Default Description
project string · Which project. A selector: refused with 403 on a mismatch with the key’s grant, filled in for a single-project key, and a 400 when a multi-project key names none.
version u32 · Which version. Absent means the highest stored.

Response: Definition

Other responses

Status Body When
404 · No definition with that id in the project, or none at the named version.

Authentication: an API key with read scope.


Start a run

POST/v1/api/runs

Starts a run and answers it as stored. The run row, its trace id, its first cursor and everything that cursor requires commit in one transaction, so a 200 means the first step is already on disk for a worker to claim. With idempotency_key, a repost within 24 hours answers the same run and starts nothing.

Request body: StartRunRequest

Response: Run

Other responses

Status Body When
404 · No definition with that id in the project, or none at the named version.

Authentication: an API key with execute scope.


List runs

GET/v1/api/runs

Runs in the project, newest first.

Query parameters

Name Type Default Description
project string · Which project. A selector: refused with 403 on a mismatch with the key’s grant, filled in for a single-project key, and a 400 when a multi-project key names none.
status RunStatus · Only runs in this status.
definition_id string · Only runs of this definition.
limit u32 50 Page size, 1 to 500.
cursor string · Opaque cursor from a previous page.

Response: RunListResponse

Authentication: an API key with read scope.


Get one run with its journal

GET/v1/api/runs/{run_id}

One run with every attempt at every step, and the deadlines and waits it currently holds.

Path parameters

Name Type Description
run_id string The run to fetch.

Response: RunDetail

Other responses

Status Body When
404 · No run with that id in a project the key reaches.

Authentication: an API key with read scope.


Cancel a run

POST/v1/api/runs/{run_id}/cancel

Ends the run and every child under it as cancelled: their deadlines and waits are deleted and every live attempt is marked failed. A worker holding a lease learns on its next call, with a 409 run_terminal. A parent of a cancelled child sees the child’s step fail and follows its on_error, unless the parent itself was the run cancelled. Cancelling a run that has already ended changes nothing and still answers 200. The body is empty.

Path parameters

Name Type Description
run_id string The run to cancel.

Response: Run

Other responses

Status Body When
404 · No run with that id in a project the key reaches.

Authentication: an API key with execute scope.


Read a run’s event log

GET/v1/api/runs/{run_id}/events

The run’s event log after a sequence number. Send Accept: text/event-stream and the same route answers server-sent events instead: one frame per row with id set to seq, held open until the run ends or you disconnect. after is the reattach point in both forms.

Path parameters

Name Type Description
run_id string The run to read.

Query parameters

Name Type Default Description
after i64 -1 Return rows with a sequence number greater than this. Exclusive; the default returns the log from its first row.
limit u32 500 The most rows to return in one page.

Response: RunEventPage

Other responses

Status Body When
404 · No run with that id in a project the key reaches.

Authentication: an API key with read scope.


Explain why a step failed

GET/v1/api/runs/{run_id}/cause

Walks backwards from one step of a run along the data dependencies its definition declares, and returns the ancestors whose output that step consumed.

A workflow template such as ${steps.classify.output.kind} is an explicit edge from one step to another, written in the document the run pinned at start, so the chain is read out of the definition rather than guessed from timing or from what happened to run first. The answer names the case a status code cannot: an ancestor that ended succeeded, with every step between it and the failure green as well, whose value the failure consumed anyway.

The step key is a query parameter rather than a path segment because a key may contain a slash, as refund/issue does, and an encoded slash inside a path segment is rewritten or refused by enough proxies to make it a bad contract.

Path parameters

Name Type Description
run_id string The run to read.

Query parameters

Name Type Default Description
step string required The step key to explain, exactly as the journal keys it, such as refund/issue or poll[7]/classify.
max_depth u32 32 How far back to walk. The walk stops there and sets truncated, so a long chain, a loop or a definition that reads its own earlier steps repeatedly cannot produce an unbounded answer.

Response: StepCause

Other responses

Status Body When
404 · No run with that id in a project the key reaches, or the run’s definition declares no step with that key.

Authentication: an API key with read scope.


Deliver an event to one run

POST/v1/api/runs/{run_id}/signal

Delivers an event to one run by id. The daemon reads the run’s open wait on event, takes its key, and routes the event as the publish endpoint would, with the same deduplication.

Path parameters

Name Type Description
run_id string The run that is waiting.

Request body: SignalRequest

Response: EventReceipt

Other responses

Status Body When
404 · No run with that id, or the run holds no open wait on that event.

Authentication: an API key with execute scope.


Publish an event

POST/v1/api/events

Delivers an event to whichever run is waiting for it, matched on project, event name and key. Delivery writes the payload as the waiting step’s output and moves the run in the same transaction. With no run waiting, the event is parked for 24 hours, and the next run that reaches a matching wait with accept_parked consumes it, oldest first.

Request body: PublishEventRequest

Response: EventReceipt

Authentication: an API key with execute scope.


Workers

Register a worker

POST/v1/api/workers/register

Stores the definitions a worker serves and tells it the lease timings. Nothing about the worker itself is stored: dispatch reads the runnable set, never a worker table, so a worker that dies needs no deregistration.

Request body: WorkerRegistration

Response: WorkerRegistered

Other responses

Status Body When
409 · A stored version holds a different body under the number you named. A stored version never changes: post the body under a new version, or leave version absent and let the daemon assign one.

Authentication: an API key with worker scope.


Claim runnable steps

POST/v1/api/steps/claim

Long-polls for runnable steps, up to wait_ms. A step is runnable when it has a pending attempt whose retry timer, if any, has fired, its run and its root are running, no other attempt of the same step is live or inside its reclaim window, and neither the run nor its root has spent max_steps or max_cost_usd. A run refused for a spent ceiling is ended failed with an error naming the ceiling.

Request body: ClaimRequest

Response: ClaimResponse

Authentication: an API key with worker scope.


Declare that a step body is about to run

POST/v1/api/steps/{lease_id}/started

Moves the attempt from leased to started, extends the lease and closes an open reclaim window. Required under at_most_once: post it, wait for the 200, then run the body. Optional under at_least_once, where skipping it only means a crash mid-body is recorded in the journal as a requeue rather than in the trace as an error on that attempt. The body is empty.

Path parameters

Name Type Description
lease_id string The lease from the claim.

Response: LeaseState

Other responses

Status Body When
404 · No lease with that id.
409 StepConflict The lease is no longer the one that may act on this attempt, or the run has ended. reason says which, and what to do next.

Authentication: an API key with worker scope.


Keep a lease alive

POST/v1/api/steps/{lease_id}/heartbeat

Extends the lease. Post it every heartbeat_ms while the body runs, whether or not started was posted. reclaimed is true when the call found the attempt orphaned and restored it to the state it came from. The body is empty.

Path parameters

Name Type Description
lease_id string The lease from the claim.

Response: LeaseState

Other responses

Status Body When
404 · No lease with that id.
409 StepConflict The lease is no longer the one that may act on this attempt, or the run has ended. reason says which, and what to do next.

Authentication: an API key with worker scope.


Complete a step

POST/v1/api/steps/{lease_id}/complete

Ends an attempt with an output. In one transaction the attempt becomes succeeded, state is merged shallowly into the run, usage is priced and charged to the attempt, the run and the root, the step’s deadlines are deleted, the closing span is enqueued, and once every slot the cursor requires has ended the cursor advances and everything the next position requires is written.

Path parameters

Name Type Description
lease_id string The lease from the claim.

Request body: StepResult

Response: StepAck

Other responses

Status Body When
404 · No lease with that id.
409 StepConflict The lease is no longer the one that may act on this attempt, or the run has ended. reason says which, and what to do next.

Authentication: an API key with worker scope.


Fail a step

POST/v1/api/steps/{lease_id}/fail

Ends an attempt with an error. class decides what follows: retryable writes a retry timer while attempts remain, at the node’s backoff or retry_after_ms if that is larger; unservable returns the attempt to pending after one lease length without spending the retry budget; every other class applies the node’s on_error at once, as does a retryable failure with no attempts left. fail_run ends the run failed with the message; continue marks the step failed and advances.

Path parameters

Name Type Description
lease_id string The lease from the claim.

Request body: StepFailure

Response: StepAck

Other responses

Status Body When
404 · No lease with that id.
409 StepConflict The lease is no longer the one that may act on this attempt, or the run has ended. reason says which, and what to do next.

Authentication: an API key with worker scope.


Monitors

List monitors

GET/v1/api/monitors

Every configured monitor the key reaches, with the condition it is latched in and its most recent reading. A monitor names the project it watches and carries the webhook it posts to, so the list is filtered by the key’s project grant rather than returned whole. Monitors are configured in the daemon’s configuration file, so there is nothing to create here.

Response: MonitorState

Authentication: an API key with read scope.


Acknowledge a monitor

POST/v1/api/monitors/{id}/ack

Acknowledges a tripped monitor. Acknowledging silences the transition without pretending the condition cleared: the monitor stays acknowledged while it still breaches, and returns to nominal on its own when the number recovers. Acknowledging a monitor that is not tripped changes nothing. The body is empty. A key that does not reach the project the monitor watches is refused with 403, naming the monitor rather than the project that owns it.

Path parameters

Name Type Description
id string The monitor’s identifier.

Response: MonitorState

Other responses

Status Body When
404 · No monitor with that id.

Authentication: an API key with write scope.


List objectives

GET/v1/api/slos

Every configured service level objective the key reaches, filtered by the key’s project grant the way the monitor list is. Objectives are configured in the daemon’s configuration file, so there is nothing to create here.

Response: SloState

Authentication: an API key with read scope.


Acknowledge an objective

POST/v1/api/slos/{id}/ack

Acknowledges a tripped objective, under the same latching rule as a monitor: it stays acknowledged while the burn rate still breaches, and returns to nominal when it recovers. The body is empty. The project grant is checked the way it is on a monitor acknowledgement.

Path parameters

Name Type Description
id string The objective’s identifier.

Response: SloState

Other responses

Status Body When
404 · No objective with that id.

Authentication: an API key with write scope.


Intelligence

The aspects a project describes traces along

GET/v1/api/aspects

Every aspect this project has, with the bounds a clustering pass runs under.

An aspect is an axis. Four ship with the daemon: goal is what the user wanted, sentiment is the tone they expressed, behaviour is what the agent did about it, and outcome is how the interaction ended. They are ordinary rows rather than a list compiled into a client, so nothing can hold a second opinion about what exists.

outcome ships derived, which means the daemon writes its notes itself from the run’s status, the step error class, the silent producer flag on the cause report and the divergence watch, with no model anywhere. A deployment that has configured nothing therefore fills one aspect on its own, which is enough to see the whole pipeline work before a model is wired to anything.

Query parameters

Name Type Default Description
project string required Which project to read. Required.

Response: AspectCatalogue

Authentication: an API key with read scope.


The entities that have aspect notes

GET/v1/api/aspects/entities

Which agents, workflows and tools have enough recorded to be worth opening, and which are still filling up.

A row appears as soon as the first note about an entity is recorded, before any theme exists. That is deliberate: an empty screen with a count moving on it is a different thing from an empty screen, and a reader has to be able to tell them apart.

Every roll-up here is computed and none of it is optional, because mcpd serves one version of itself and has no older server to tolerate.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind · Only entities of this kind.
limit u32 50 Page size, 1 to 200.
cursor string · Opaque cursor from a previous page.

Response: EntityIndexResponse

Other responses

Status Body When
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


How far the pipeline has got for one entity

GET/v1/api/aspects/progress

The honest empty state. For one entity: how many of its traces carry notes, and per aspect how many notes exist, what state that aspect is in, and how many more notes it needs before a clustering pass will run. Every number is counted from stored rows, so nothing here is an estimate of what is coming.

An aspect reads collecting below the floor of 50 notes, processing once it has them and no snapshot covers it yet, and ready when one does. Read this before concluding that a themes screen is broken: the three states look identical on screen and mean different things.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.

Response: AspectProgress

Other responses

Status Body When
404 · No entity of that kind and name in a project this key reaches.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


One entity’s clustering snapshots

GET/v1/api/themes/snapshots

The timeline. Each snapshot is one clustering pass over a moving window of notes, and every theme query names exactly one of them.

There are two presentations. inventory returns consecutive snapshots for a caller walking the series. landmarks returns a bounded, time balanced selection across the whole range, with the oldest and the newest snapshot in that range always included and the rest sampled by time, which is what a timeline of a couple of dozen points needs; each row then says which of the two reasons put it there.

Windows overlap. Two snapshots describe overlapping sets of traces, so their counts must never be added together.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.
aspects string[] · Only snapshots that cover all of these aspects. Unset means every snapshot for the entity, whatever it covers.
presentation string inventory inventory for consecutive snapshots, landmarks for a time balanced selection across the range.
from timestamp · Only snapshots whose cutoff falls at or after this. It bounds the range a landmark selection is drawn from as well as what is returned.
to timestamp · Only snapshots whose cutoff falls before this.
limit u32 24 How many snapshots to return, 1 to 100. A landmark selection of more than a couple of dozen points is more than a timeline can draw and more than a reader can scrub.
cursor string · Opaque cursor from a previous page.

Response: SnapshotListResponse

Other responses

Status Body When
404 · No entity of that kind and name in a project this key reaches.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


Themes across two to four aspects at once

GET/v1/api/themes/flow

The cross-aspect view: one stage per aspect, its nodes, and the links between neighbouring stages. A link’s count is the number of traces assigned to both of its ends.

This shows association and not causation. A ribbon between a goal theme and an outcome theme means the two were written about the same runs. It does not mean one caused the other.

The order of aspects is meaningful and is not a display preference. Links only ever join neighbouring stages, so goal beside outcome answers a different question from goal beside behaviour, and asking for the same aspects in another order asks another question. Fewer than two aspects is refused by name rather than answered with an empty flow, because one stage has no links in it.

Each stage carries at most one noise node and at most one overflow node. Noise is the notes that joined no theme, and it is not a failure bucket: it is rare requests, wording nothing else matched and patterns that have not recurred yet, so a large noise share means varied traffic or too few notes. Overflow is everything past theme_limit_per_stage, folded into one node so the stage totals still add up.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.
aspects string[] required Two to four aspect names, in the order the stages are wanted. The order changes the answer.
snapshot_id string required Which snapshot to read. Every count in the response belongs to this one window.
theme_limit_per_stage u32 8 How many themes to return per stage, 1 to 100, before the rest are folded into the overflow node. The fold happens on the server because only the server knows what it truncated.
filter string[] · Drill in. Repeat as filter=aspect:theme_id, or filter=aspect:noise, up to four times with a different aspect each time. The daemon recomputes the counts and the shares over the traces that survive every filter, so the answer does not change shape with the size of the snapshot and a client never has to fetch the assignments and filter them itself.

Response: ThemeFlow

Other responses

Status Body When
404 · No such snapshot for that entity, or aspects names an aspect the project does not have.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


One theme in one snapshot

GET/v1/api/themes/{theme_id}

A theme’s label, lifecycle state and counts in one snapshot, returned beside the snapshot’s own counts so a caller can write 28 of 70 traces in this window without a second request.

A theme this snapshot does not hold is a 200 with no theme rather than a 404. Scrubbing a timeline back past a theme’s birth is a reasonable question, and the answer is that the theme was not there yet. The 404 is kept for a theme id this entity has never had, which is a different mistake and deserves a different answer.

Path parameters

Name Type Description
theme_id string The theme to read. noise is reserved and is not a theme: read a stage’s noise from the flow.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.
aspect string required Which aspect the theme was clustered along.
snapshot_id string required Which snapshot to read it in.

Response: ThemeDetail

Other responses

Status Body When
404 · No such snapshot, or a theme id this entity has never had. A theme that exists and is absent from this snapshot is a 200 with no theme.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


The notes behind a theme

GET/v1/api/themes/{theme_id}/examples

The sentences that put traces in this theme, each with the run it was written about, so a reader can go from a theme to the runs behind it.

Ordered by how much each note’s wording overlaps the theme’s label, most alike first, with the note id breaking a tie so the order is total and the same on every read and every replica.

Pass noise as the theme id to read the notes that joined no theme in this snapshot. There is no separate noise route: noise is a node in the flow and its notes are read here, because a second pair of routes returning an identically shaped answer would be two more rows in this reference and no new question answered.

The text is payload. This endpoint takes a read key, it is recorded in the audit ledger like every other endpoint that hands back what a user wrote, and the notes it returns are swept by retention and erased with their subject.

Path parameters

Name Type Description
theme_id string The theme to read, or noise for the notes in this snapshot that joined no theme.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.
aspect string required Which aspect the theme was clustered along.
snapshot_id string required Which snapshot to read it in.
filter string[] · Drill in. Repeat as filter=aspect:theme_id, or filter=aspect:noise, up to four times with a different aspect each time. The daemon recomputes the counts and the shares over the traces that survive every filter, so the answer does not change shape with the size of the snapshot and a client never has to fetch the assignments and filter them itself.
limit u32 50 Page size, 1 to 100.
cursor string · Opaque cursor from a previous page.

Response: ThemeExampleListResponse

Other responses

Status Body When
404 · No such snapshot, or a theme id this entity has never had.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


One theme’s life across snapshots

GET/v1/api/themes/{theme_id}/history

Every stored snapshot that holds this theme, with its state, counts and coverage at each point, and the themes it split from, split into, merged from or merged into.

This is the only theme route that names no snapshot, because the series is the answer. Points come back oldest first, so nothing has to be reordered before it is drawn or differenced.

Retention bounds what can be answered. The oldest point returned is the oldest snapshot still stored, which is a lower bound on the theme’s age and not necessarily its birth, so a series that starts at the edge of the window says when mcpd started looking rather than when the theme started.

Windows overlap, so read the points as a series and never add their counts together.

Path parameters

Name Type Description
theme_id string The theme to follow.

Query parameters

Name Type Default Description
project string required Which project to read. Required.
entity_kind EntityKind required The kind of entity to read. An entity is the pair of a kind and a name, which is the identity spans already carry.
entity_name string required The name of that entity.
aspect string required Which aspect the theme was clustered along.
limit u32 100 Page size, 1 to 500.
cursor string · Opaque cursor from a previous page.

Response: ThemeHistory

Other responses

Status Body When
404 · A theme id this entity has never had.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


What was written about one trace

GET/v1/api/traces/{trace_id}/aspects

Every aspect note recorded about one trace, in catalogue order. A trace id crosses service boundaries, so two projects can each hold a run under it: name the project to read that one. Without it, the projects the key reaches are tried in order and the first that holds the trace answers.

Read it beside the trace itself. A note is the sentence that put this run in each of its themes, so it is the shortest route from a theme back to what happened, and it is also the place to check a theme you do not believe: a note proves a definition said this about the trace at a version, and the trace proves the rest.

An empty list is the ordinary answer for a recent trace. Writing a note lags ingestion.

Path parameters

Name Type Description
trace_id string The trace to read notes for.
project string The project whose run of this trace to read. Left out, the first project the key reaches that holds the trace.

Response: TraceAspectNotes

Other responses

Status Body When
404 · No trace with that id in a project this key reaches.
501 · This deployment’s storage backend does not implement what this endpoint needs. The body names the capability in its capability field, using the same word GET /v1/api/capabilities publishes as theme_analysis, so a caller can read the capability set once and stop asking rather than retrying a refusal that will never change.

Authentication: an API key with read scope.


Accounts

Create an account

POST/v1/api/account/signup

Creates an account and a new project that belongs to it, and logs it in. The project has a random name, and no other account or key can reach it. The email is checked for shape only; there is no verification mail. Limited per client address: a client that signs up too often is answered with 429 and a Retry-After header. Answers 404 when the daemon was not started with --signup.

Request body: AccountCredentials

Response: AccountSession

Other responses

Status Body When
400 · The email is not an address, or the password is shorter than 10 characters or longer than 1024 bytes.
404 · This daemon does not offer sign-up.
409 · An account with this email already exists. Log in instead.
429 · Too many sign-ups from this address. Wait for the number of seconds in Retry-After.

Authentication: none; this endpoint is public.


Log in

POST/v1/api/account/login

Starts a session for an account. A wrong password and an unknown email get the same 401 with the same message, so the answer does not say which emails have accounts. Limited per client address and per email. Answers 404 when the daemon was not started with --signup.

Request body: AccountCredentials

Response: AccountSession

Other responses

Status Body When
401 · The email and password do not match an account.
404 · This daemon does not offer accounts.
429 · Too many attempts from this address or for this email. Wait for the number of seconds in Retry-After.

Authentication: none; this endpoint is public.


Log out

POST/v1/api/account/logout

Ends the session whose token the request carries. The account’s API keys keep working. The body is empty. Answers 404 when the daemon was not started with --signup.

Response: DeleteResult

Authentication: an account session token from POST /v1/api/account/login. An API key is not accepted.


Read the logged-in account

GET/v1/api/account

The email and the project of the account whose session the request carries. Answers 404 when the daemon was not started with --signup.

Response: Account

Authentication: an account session token from POST /v1/api/account/login. An API key is not accepted.


List the account’s API keys

GET/v1/api/account/keys

Every API key the account holds, newest first. No secret is returned: the daemon keeps only digests. Answers 404 when the daemon was not started with --signup.

Response: AccountKey

Authentication: an account session token from POST /v1/api/account/login. An API key is not accepted.


Create an API key

POST/v1/api/account/keys

Creates an API key with one scope on the account’s project. The secret is in this response and nowhere else. A key reaches the account’s project only: a signal it writes that names another project is stored in the account’s project instead. An account holds at most 50 keys. Answers 404 when the daemon was not started with --signup.

Request body: CreateAccountKeyRequest

Response: CreatedAccountKey

Other responses

Status Body When
400 · The scope is not one of the six, the name is too long, or the account already holds 50 keys.

Authentication: an account session token from POST /v1/api/account/login. An API key is not accepted.


Revoke an API key

DELETE/v1/api/account/keys/{key_id}

Revokes one of the account’s API keys. It stops working at once. Answers 404 when the daemon was not started with --signup.

Path parameters

Name Type Description
key_id string The key’s id, as listed.

Response: DeleteResult

Other responses

Status Body When
404 · The account holds no key with that id.

Authentication: an account session token from POST /v1/api/account/login. An API key is not accepted.