Skip to content
mcpd
no daemon
Source

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.

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. 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:

FamilyKinds
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_generation is one logical generation: everything from the first request to the final answer.
  • model_step is one step inside it. A generation that calls tools twice has three steps.
  • model_inference is the provider round-trip alone, with no processors and no tool execution inside it.
  • model_chunk is 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

trace_id Store Search Export

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.

FieldWhat it holds
projectThe tenancy key. Every signal belongs to exactly one.
environmentWhere it ran, such as production or staging.
service_nameThe emitting service.
service_versionIts version, so a regression can be pinned to a release.
session_idA user-facing session. Groups many traces into one conversation.
thread_idA thread within that session.
user_idThe end user the run was for.
resource_idA tenant or workspace inside your own application.
run_idYour own identifier for this run.
request_idThe inbound request that caused the work.
experiment_idSet when an experiment produced the run.
releaseA 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.

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