Skip to content
mcpd
no daemon
Source

Install

mcpd is one binary with storage compiled in, so installing it means putting that binary somewhere and starting it. Pick whichever route below matches how you already run things.

Docker

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

The published image is the shortest route in, and the one the rest of the documentation assumes.

docker run -p 4317:4317 -v mcpd-data:/data -e MCPD_DATA=/data/mcpd.db ghcr.io/mcpd/mcpd

The volume matters. mcpd writes a SQLite database at the path MCPD_DATA names, and a container without a volume takes your traces with it when it stops. Naming the path explicitly costs one flag and removes the question of where the file went.

Every flag has an environment variable, so anything you can pass on the command line you can also set with -e. Pin a tag rather than tracking latest in anything you care about. The image is built for linux/amd64 and linux/arm64, and Docker pulls the one that matches your machine.

Docker Compose

The same container, written down. This is the form to use when mcpd sits beside your application on one host.

compose.yaml
services:
  mcpd:
    image: ghcr.io/mcpd/mcpd:latest
    restart: unless-stopped
    ports:
      - "4317:4317"
    volumes:
      - mcpd-data:/data
    environment:
      MCPD_DATA: /data/mcpd.db
      MCPD_PROJECT: default
      # Set these before this port is reachable by anything but you.
      # MCPD_INGEST_KEYS: key-for-your-agents
      # MCPD_READ_KEYS: key-for-your-team
      # MCPD_RETENTION_DAYS: 30

volumes:
  mcpd-data:

The three commented keys are the ones to fill in before this reaches a network anyone else is on. With no keys set, ingest and reads are both open, which is correct on your laptop and wrong everywhere else. Deploy covers what to set and why.

Kubernetes

Each release pushes the chart to the same registry as the image, as an OCI artefact, so there is no repository to add first. The chart refuses to install without an ingest key and a read key, so generate both first.

helm install mcpd oci://ghcr.io/mcpd/charts/mcpd -n mcpd --create-namespace --set keys.ingest=$INGEST_KEY --set keys.read=$READ_KEY

mcpd handles SIGTERM and shuts down gracefully, which is what Kubernetes sends. It exposes /healthz for liveness and /readyz for readiness, and the two are deliberately different: liveness never touches storage, so a slow database cannot restart the pod into a loop. Values, probes and the Deployment shape are on Docker and Kubernetes.

A release binary

Each release attaches an archive for Linux on x86_64 and arm64, static and built against musl so there is no libc to match, and for macOS on Apple silicon and Intel. A SHA256SUMS file and a build provenance attestation come with them. Unpack the archive for your machine and run ./mcpd; with no arguments it starts the daemon.

There is no cargo install mcpd. Nothing is published to crates.io, and a crate built from the registry could not carry the interface, which is built with npm and compiled into the binary. With a Rust toolchain, build from source instead.

From source

Clone the repository and run the distribution build. It uses full link-time optimisation and a single codegen unit, which is slower to compile and produces the smaller, faster binary. Go through make rather than cargo directly: the interface is compiled into the binary, and make dist builds it first, which is the step a plain cargo build on a fresh clone has no way to do. That step needs Node and npm on the machine as well as the Rust toolchain.

git clone https://github.com/mcpd/mcpd && cd mcpd && make dist

The binary is written to target/dist/mcpd, or target/release/mcpd if you took the faster route. Use make build while you are working on mcpd itself and make dist for anything you intend to run.

Where the data goes

--data is the path to the SQLite file, and it defaults to ./mcpd.db in the working directory. Parent directories are created for you.

Passing :memory: keeps everything in memory and writes nothing to disk. That is the right choice for a test suite, for a demo on someone else's machine, and for any run whose prompts you would rather not leave behind. The store disappears with the process, which is the point.

Leave nothing behind

mcpd --data :memory:

mcpd demo fills the store with a realistic failing run and then serves, so the interface has something in it the first time you open it.

Check what it will do before you start it

mcpd config prints the configuration this invocation resolves to, then exits. Flags, environment variables and defaults all end up in one listing, so you can see which one won without starting anything.

resolved configuration
$ mcpd configlisten          0.0.0.0:4317storage         sqlite at ./mcpd.dbconfig file     nonelog format      textproject         defaultprojects        noneretention       foreveringest auth     openread auth       openwrite auth      openadmin auth      openexecute auth    openworker auth     opensignup          offruns            sqlite at ./mcpd.runs.dbrun settings    lease_ttl_ms 30000, reclaim_window_ms 30000, stall_window_ms 300000, idempotency_window_ms 86400000, park_ttl_ms 86400000, max_depth 8, max_step_repeats 16identity        offclient ip       peer addresssampling        keep everythingquota           unlimitedredaction       onredact key      built in, which is public; set --redact-keycors            same-origin onlytimeout         30s per requestlimits          32 MiB per request, 10000 signals per batchderived         duration and token metricsaspects         offmonitors        noneobjectives      nonejudge           offforwarding      off

Verify the install

/healthz is the answer to "is it actually running". It never reads from storage, so it stays fast and stays honest about the process rather than the disk.

verify
$ curl -s localhost:4317/healthz{"status":"ok","version":"0.1.0","spec_version":"1.0.0","uptime_seconds":8,"storage":"sqlite"}$ curl -s -o /dev/null -w "%{http_code}\n" localhost:4317/readyz200

spec_version is the data-model version this build implements. A client can read it and refuse to talk to a server it does not understand, which is better than sending a batch that will be rejected field by field.

/readyz is the other half. It pings storage and answers 503 when the store is not reachable, so a load balancer stops sending reads to an instance that cannot serve them. Use /healthz for liveness and /readyz for readiness, never the same one for both.

Then open localhost:4317 and you are looking at the interface.

Next