Skip to content
mcpd
no daemon
Source

Every step your agent takes, held to the end and on the record.

mcpd is a daemon, not a framework. It holds a run between steps, the retries, the timers, the week-long wait for an approval, while your own code does the work inside each step, in any language. Every step lands in a trace with what it cost.

Model latency
A strip chart recording model latency over the last hour: a baseline near 420 milliseconds with five occasional excursions towards 800 milliseconds.
A strip chart recording model latency over the last hour: a baseline near 420 milliseconds with five occasional excursions towards 800 milliseconds.

Build and run it

git clone https://github.com/mcpd/mcpd && cd mcpd && make build && ./target/release/mcpd demo

Then open localhost:4317. No account, no database to stand up, nothing to sign. No release yet: the image and the packages land with the first one. Read the five-minute quickstart.

A run you can read.

An agent that fails does not fail in the log line that mentions the error. It fails three steps earlier, when the model chose the wrong tool and then chose it again.

mcpd reconstructs the whole run as a tree: every model call, every tool call, every retrieval, with the exact prompt and completion attached to each, the latency of each, and what each one cost.

Open the interface

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.

Run drum

The same run, wound round a recorder drum. Time runs round it, one track per span. Turn it, and the pen reads the span under it.

Drag to turn the drum, or use the arrow keys 2.64 s · tool returns_policy · 90 ms

A run that survives the process that started it.

The daemon owns the run between steps. Your process owns everything inside one, and hands nothing over: not the model, not the prompt, not a credential.

Lease board

The daemon keeps one tag per step on its board. A worker holds a tag only while its heartbeats arrive. Stop the worker and, after the lease and one reclaim window, the tag goes back on the board for the other worker.

Click, or press Space, to stop the worker draft · worker-a · started · lease 25 s

The graph is data, the bodies are yours

A workflow is written once, in code. The graph goes to the daemon as a definition it validates and versions. The step bodies stay in your process, in any of six languages, for a worker to serve.

Retries, timers and waits are rows

A transient failure is retried on the step's own budget. A sleep or a wait for an approval holds the run with no process alive, through a deploy, a crash, or a week. An event answers the wait by name and key, and a repeated event is a duplicate, not a second run.

Every run is a trace

Each step is a span with its input, its output and the usage it reported, priced with the same book that prices every other span. The model call inside a step sits under it, so the search, the cost and the scorers work on a run unchanged.

Read about workflows

Instrument in the language you already write.

Six SDKs, one behaviour. Recording a span never blocks, never throws, and never grows without bound: if mcpd is unreachable your agent does not notice, which is the only acceptable way for observability to fail. Each one also carries a worker, so the steps of a durable run are served from the same process that records them.

None of them is required. Point OTEL_EXPORTER_OTLP_ENDPOINT at mcpd and an OpenTelemetry SDK you already run reaches it, with no code of ours in your application. The six are a convenience over an HTTP surface, not a condition of being recorded.

Observability that arrives attached to a framework can only record agents built with that framework. mcpd takes whatever reaches its OTLP endpoint: an agent framework, a loop you wrote yourself, or the services either one calls.

instrument a run

/**
 * One ticket, one trace.
 *
 * `withSpan` opens the span, keeps it active for everything the callback
 * starts, and ends it when the callback settles. Nothing inside has to be
 * passed a handle to be nested under it.
 */
async function handleTicket(ticket: Ticket): Promise<{ answer: string; traceId: string }> {
  let traceId = ''
  const answer = await mcpd.withSpan('support_agent', async (run) => {
    // Keep the id: a score or a piece of feedback that arrives later attaches
    // to this run by it.
    traceId = run.traceId
    // Tags label the whole run, so the trace list can filter on them.
    run.setTags(['support', 'tier-1'])
    run.setCorrelation({ sessionId: ticket.id, userId: ticket.customerId })
    run.setInput({ question: ticket.question })

    const order = await lookUpOrder(ticket)
    const answer = await draftAnswer(ticket, order)

    run.setOutput({ answer })
    return answer
  })
  return { answer, traceId }
}

Six signals, correlated by the run that produced them.

Most tools give you traces here and logs somewhere else, and leave you to match timestamps by eye. mcpd stores all six signals against the same trace and span ids, so a spike on a chart opens the run that caused it and a run shows the log lines it emitted.

Sorting bench

Six kinds of signal arrive mixed on one belt. The reader reads the trace id on each, and every piece of a run lands in that run’s drawer.

Point at a drawer, or use the arrow keys trace 4bf9 · span 3 · log 1 · metric 1 · score 1 · feedback 1

13 languages that agree.

Cross-language conformance

tests/e2e/run.mjs

Channel root tree status error model tokens score feedback ids
TypeScript
Python
Go
Elixir
Kotlin
Rust
cURL
TypeScript
Python
Go
Elixir
Kotlin
Rust

13 languages, one canonical run, one daemon. Every trace comes back the same shape.

481 checks

A conformance panel with one row per language and one column per family of check. Every cell passes. The families are: the root span is the agent run; every child hangs off the root; a failed step makes the whole run an error, and only that step; the error carries a message, on the step that failed; the model call records its model; token usage and cost survive the wire; the scores land, with their scale; the feedback lands; ids are OpenTelemetry-shaped. The suite asserts 37 checks in each of the 13 languages.

Every SDK is tested on its own, which proves each does what its authors meant and nothing about whether they match. So each one writes the same run against one daemon, and the traces are read back and compared to each other.

A behaviour that holds in one language and not another shows up there and nowhere else. It runs in CI on every change. How this compares.

Who read what, including who was turned away.

The question that decides a security review is not whether the data is safe. It is whether you can show, afterwards, who reached it.

Audit log

GET /v1/api/audit

Time Principal Action Project Result
14:02:11 a3f9c2e1b7d4 get_trace checkout ok
14:02:11 a3f9c2e1b7d4 list_spans checkout ok
14:04:38 7c1e08b53a9f list_dataset_items billing refused
14:07:02 d52a4f8c0e63 capture_into_dataset checkout ok
14:11:55 d52a4f8c0e63 delete_trace checkout ok
14:19:20 7c1e08b53a9f list_feedback billing ok

A refusal is recorded like a read. The principal is a digest of the key, never the key. Retention never trims this table.

18 actions

An audit log with six entries, each showing the time, a truncated digest of the key that acted, the action, the project and the result. One entry, a read of another project's dataset items, is recorded as refused.

Six key scopes

Emitting telemetry, reading prompts, changing stored data, destroying it, starting runs and claiming steps are different privileges. An unset write list accepts the ingest keys, and an unset admin list accepts the read keys. Unset execute and worker lists accept the write keys. Upgrading does not lock you out.

A project is a boundary

Keys, retention and sampling are all per project. A key listed under one reaches that one, and the daemon resolves who owns a dataset before it returns a single case from it.

Nothing calls home

Four outbound request sites exist in the whole daemon, and you configure every one of them: the forwarder, two webhooks and the judge. There is no update check, no telemetry and no licence server.

Which key, and which person

The ledger names the key. Give mcpd the header your ingress already sets, and the addresses allowed to set it, and it names the person as well. Sent from anywhere else, a header is a claim anyone could make, so the row stays empty rather than carry a guess.

A deletion request is one call

One DELETE removes every run recorded against a person, with the spans, logs, metrics, scores and feedback hanging off them, and answers with a count per signal. Rows in a project that key cannot open are neither touched nor counted.

Check it before you trust it

mcpd doctor grades a deployment: whether the store takes a write and gives it back, whether a scope was left open, whether a configured project has ever received a span. Anything it wants you to act on names the flag that fixes it.

What mcpd does not do.

Every tool has a list like this. Most keep it internal, and you find it out in the second week. Here it is, with the reason rather than a date.

No OTLP over gRPC.
HTTP carries traces in both encodings, and logs and metrics in JSON. gRPC is not supported at all.
No vendor exporters by name.
mcpd forwards OTLP, which reaches most destinations through one implementation and makes mcpd one hop rather than the place your data has to stay. One that speaks only its own protocol is out of reach.
No SSO or SCIM.
Keys are scoped to projects, which is a tenancy boundary, not an identity provider. A person's identity is whatever your ingress establishes in front of mcpd.
No multi-region ingest.
On Postgres or ClickHouse it runs as several replicas behind one service. That is not regional failover.

The full account, measured against a direct source audit of the incumbent, is in PARITY.md. It is updated when something lands, not when something is planned.

Five minutes to your first trace.

Run the daemon, point your agent at it, open the interface. If it takes longer than that, it is a bug and we would like the report.

Start

docker run -p 4317:4317 ghcr.io/mcpd/mcpd