Concepts
A handful of words carry the whole data model. The rest of the documentation assumes you know them, so this page defines each one and says what it is for.
Span
A span is one unit of work. It has a name, a kind, a start time, usually an end time, and a status. It can carry what went in, what came out, how many tokens it used, what it cost, and an error if it failed.
Keep the name low-cardinality. search_docs is a name;
search_docs(query=hello) is a name and a value glued together,
and it makes grouping by name useless the moment you have two runs.
Trace
A trace is every span from one run, joined by a 128-bit trace id. Inside the
trace, parent_span_id gives the tree its shape: a tool call made
inside a model step is a child of that step, so the nesting on screen is the
nesting that actually happened.
Ids are OpenTelemetry-shaped, 32 lowercase hex characters for a trace and 16 for a span. That is not decoration. It is what lets a trace that started in your web tier and ended in your agent stay one trace across both, and what lets mcpd sit beside whatever else already reads your telemetry.
A span whose parent came from a foreign tracing system carries it in
external_parent_span_id instead. mcpd keeps that value for
correlation but never builds its tree from it, because a parent id that
names a span mcpd never received would hang the whole subtree under nothing.
- agent support_agent fail
- model chat
- tool returns_policy
- model chat
- tool returns_policy
- model chat
- tool returns_policy
The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.
mcpd also stores a projection of each trace: the span count, the error count, total tokens, total cost, how many model calls and how many tool calls. That row is recomputed from the spans on every write rather than incremented, because spans arrive out of order, get retried and get updated in place. A counter drifts under all three, and a span count that is visibly wrong in the list makes a reader distrust every other number on the page.
Root spans and event spans
A root span is one with no parent inside the trace. It is normally an
agent_run or a workflow_run, and it is where the
trace gets its name, its tags and its correlation. Tags are only meaningful
on a root, where they label the whole run.
An event span is a point in time rather than a duration, so it has no end and no duration. A streamed chunk is the usual case. The distinction is stored rather than inferred from a missing end time, because "this never had an end" and "this has not finished yet" are different facts and only one of them is a problem.
A trace is complete once its root has ended and no span is still open. Until then it is still listed, with the best name available, rather than hidden until it finishes. The run that never finished is often the one you came to look at.
Span kind
The kind says what an operation is. It decides how the span is drawn, which attributes mean anything on it, and whether cost accounting looks at it. mcpd defines 33 kinds, in families:
| Family | Kinds |
|---|---|
| Agent | agent_run |
| Model | model_generation, model_step,
model_inference, model_chunk |
| Tools | tool_call, client_tool_call,
provider_tool_call, mcp_tool_call |
| Workflow | workflow_run, workflow_step,
workflow_conditional, workflow_parallel,
workflow_loop, and the rest
|
| Retrieval | rag_ingestion, rag_embedding,
rag_vector_operation, rag_action,
retrieval, memory_operation,
graph_action |
| Evaluation | scorer_run, scorer_step,
guardrail, processor_run |
| Other | mapping, skill_resolution,
workspace_action, http_request,
generic |
Four kinds bill tokens: model_generation,
model_step, model_inference and
rag_embedding. Those are the spans cost accounting reads.
generic is always valid and never inferred, so a span mcpd
cannot classify is labelled honestly rather than guessed at.
The data model lists
every kind with what it means.
The model hierarchy
Four of those kinds nest inside each other, and the nesting is the most useful thing in the model:
-
model_generationis one logical generation: everything from the first request to the final answer. -
model_stepis one step inside it. A generation that calls tools twice has three steps. -
model_inferenceis the provider round-trip alone, with no processors and no tool execution inside it. -
model_chunkis one streamed chunk, recorded as an event.
The reason for four levels rather than one is that "the model call was slow" is almost never true as stated. One call from your code is not one HTTP round trip once tools are involved: it is a request, a tool, another request, a guardrail, another request. Collapse that into a single span and a run that spent six seconds in your own retry loop looks identical to one that spent six seconds waiting on a provider. Separating inference from step from generation is what makes the difference readable, and the first chunk event is what gives you time to first token.
Record token usage once, on the span that made the provider call. The trace projection sums input and output tokens across every span in the trace, so the same counts attached to a generation and again to each of its steps will double the token total and double the cost on screen. The hierarchy is for timing; the tokens belong to whichever level actually spoke to the provider.
The six signals
Spans are one of six things mcpd stores. Logs are the lines a run emitted.
Metrics are numeric observations, as counters, gauges or histograms. Scores
are a machine's evaluation of a span or a trace. Feedback is a person's. An
aspect note is one sentence about a run, written after it finished. A model
writes most of them. The daemon writes the outcome notes, from
the run's status and other facts it already holds.
-
Spans
What ran, nested, with timings
-
Logs
Lines the run emitted
-
Metrics
Numbers, with exemplars back to a run
-
Scores
Machine evaluation
-
Feedback
What a person thought
-
Aspect notes
A sentence a model wrote about the run
All six carry the trace id they belong to, most of them the span id as well, and all six pass through the same redaction pipeline before anything is written. A secret leaks out of a log line exactly as well as out of a prompt.
A score always declares its scale: the value, and the minimum and maximum it sits between. Every SDK requires both at the call site, because a bare 0.7 with no scale attached cannot be compared with anything, including the same scorer's output last week.
Feedback is kept in a separate place from scores, and never averaged with
them. It comes in five shapes: thumbs, rating,
correction, tag and comment. A
correction is the most valuable of the five, because it is the only signal
that carries the right answer.
Correlation
Correlation is the context a signal belongs to. It is the difference between a pile of runs and a set you can slice.
| Field | What it holds |
|---|---|
project | The tenancy key. Every signal belongs to exactly one. |
environment | Where it ran, such as production or staging. |
service_name | The emitting service. |
service_version | Its version, so a regression can be pinned to a release. |
session_id | A user-facing session. Groups many traces into one conversation. |
thread_id | A thread within that session. |
user_id | The end user the run was for. |
resource_id | A tenant or workspace inside your own application. |
run_id | Your own identifier for this run. |
request_id | The inbound request that caused the work. |
experiment_id | Set when an experiment produced the run. |
release | A release name or a git sha. |
project is the only required field. When a sender does not name
one, the daemon files the signal under its --project default, so
nothing is ever stored with nowhere to belong.
Set session_id even when it looks redundant. One agent run is a
trace, but a complaint is almost always about a conversation, and without a
session id there is nothing that turns eleven separate traces back into the
exchange the person actually had.
The trace list filters on environment,
session_id, thread_id and user_id
directly, alongside status, kind, name, tags, model, time range, minimum
duration, minimum cost and score.
Finding a run puts them together.
Links
A span has one parent and any number of links. A link points at another span
or another trace and says why: follows_from,
retry_of, replay_of, or whatever you name it.
Links exist because parentage cannot express fan-in. A batch job caused by twenty requests has twenty links and one parent. Forcing that into the tree would either invent nineteen duplicate subtrees or throw nineteen of the causes away.
Next
- Every entity and field in full: Data model.
- How these words appear in code: the SDKs.
- What mcpd infers when the spans arrive as OTLP: OpenTelemetry.
- Turning the vocabulary into a query: Finding a run.