Skip to content
mcpd
no daemon
Source

Quickstart

Daemon running, agent instrumented, trace on screen. Nothing to sign up for and nothing to install alongside it.

1. Run the daemon

No release yet.

The image, the chart and the packages the commands on this site name are the intended distribution. None of them has been published. What works today is a build from source, which needs Rust and Node:

Build and run it

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

mcpd is one process with storage compiled in. There is no database to stand up and no configuration file to write.

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

The volume is optional but worth adding: without it, the traces go away when the container does. Prefer a binary? Install it directly.

what you should see
$ docker run -p 4317:4317 ghcr.io/mcpd/mcpdWARN listening on 0.0.0.0:4317 with open ingest. Anyone who can reach this port can write to it. Set --ingest-key (or MCPD_INGEST_KEYS) before exposing it.WARN listening on 0.0.0.0:4317 with an open read API. Stored prompts and completions are readable by anyone who can reach this port. Set --read-key to require a key.WARN listening on 0.0.0.0:4317 with an open run surface. Anyone who can reach this port can start runs and claim steps. Set --execute-key and --worker-key before exposing it.INFO mcpd 0.1.0 listening on http://0.0.0.0:4317INFO   interface   http://0.0.0.0:4317/INFO   OTLP        http://0.0.0.0:4317/v1/tracesINFO   native      http://0.0.0.0:4317/v1/ingestINFO   runs        http://0.0.0.0:4317/v1/api/runsINFO   storage     sqlite at /data/mcpd.db

The warnings come first, and they are deliberate. On your laptop they do not matter. Before this port is reachable by anything else, read Deploy.

2. Send it something

Two ways in, and you do not have to choose ours. If your agent already emits OpenTelemetry, point it at mcpd and write no new code at all:

export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Otherwise install the SDK for your language. It has no required dependencies, and recording a span never blocks and never throws.

connect

import { Mcpd } from '@mcpd/sdk'

// Every field here also reads an environment variable, so the same code runs
// unchanged in development and in production. With no arguments at all this
// talks to a daemon on http://localhost:4317.
const mcpd = new Mcpd({
  project: 'support',
  environment: 'development',
  serviceName: 'support-agent',
  serviceVersion: '1.4.0',
})

3. Instrument a run

Wrap the work your agent does. A span records what went in, what came out, how long it took and what it cost; children nest inside it automatically, so a tool call made inside the block lands in the right place in the tree without being told where it belongs.

record 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 }
}

4. Read it

Open localhost:4317. Your run is in the list. Open it and you get the whole tree: every model call with its prompt and completion, every tool call with its arguments and result, timings, tokens and cost.

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.

5. Find the interesting one

The list is not where you will spend your time. Search is. mcpd indexes every prompt and completion, so you can go straight to the run where the model said the thing a customer complained about:

search and analyse

// The same client reads. This is the query that finds bad answers: every run
// that a scorer put below the bar.
const suspect = await mcpd.listTraces({
  hasScore: 'answer_relevance',
  scoreBelow: 0.5,
  limit: 10,
})
console.log(`runs scoring under 0.5: ${suspect.traces.length}`)

const recent = await mcpd.listTraces({ limit: 5, sort: 'start_time', order: 'desc' })
for (const trace of recent.traces) {
  const seconds = ((trace.durationNs ?? 0) / 1e9).toFixed(2)
  console.log(
    `${trace.traceId.slice(0, 8)}  ${trace.name.padEnd(16)} ${trace.status.padEnd(6)} ` +
      `${seconds}s  ${trace.spanCount} spans  $${(trace.cost?.totalCost ?? 0).toFixed(6)}`,
  )
}

// One run, with every span in it, ordered so a waterfall renders directly.
const detail = await mcpd.getTrace(traceId, { include: ['scores', 'feedback'] })
for (const span of detail.spans) {
  console.log(`  ${span.kind.padEnd(18)} ${span.name}`)
}

Where to go next

You now have the whole loop. The pages worth reading next depend on what you are doing: