Documentation · v0.2
Deploying mcpdef
mcpdef is one static binary. Run it on a VM with Docker/Podman, or on Kubernetes with the bundled Helm chart. In both cases you provide one mcpdef.toml declaring the MCP servers to front (see CONFIG.md), and the gateway serves clients over Streamable HTTP on :7878 while the optional read-only admin server (Prometheus /metrics + a status UI) runs on :7879.
Security defaults. The admin server has no auth of its own, so it is never reachable off-host by default: compose binds it to loopback (
127.0.0.1) only, and the Helm chart leaves it off (admin.enabled=false) — turn it on withadmin.enabled=trueand[gateway.admin] enabled = trueinconfig, and keep it in-cluster / behind a NetworkPolicy or auth proxy. The binary has no in-process TLS — put a TLS-terminating reverse proxy (and[gateway.auth]) in front of:7878for internet exposure. The audit ledger persists only if you give it a durable volume.
On a cloud VM (Docker or Podman) #
Files: compose.yaml and mcpdef.example.toml, reproduced in full under Reference files.
mkdir mcpdef && cd mcpdef # save compose.yaml and mcpdef.example.toml here
cp mcpdef.example.toml mcpdef.toml # declare your MCP servers
docker compose up -d # or: podman-compose up -d
docker compose logs -f mcpdef # "mcpdef X.Y.Z ready · N upstream(s) · listening …"
- Clients:
http://<vm-ip>:7878/mcp(front it with your own TLS proxy). - Admin UI / metrics: published to
127.0.0.1:7879only — reach it over an SSH tunnel:ssh -L 7879:127.0.0.1:7879 user@vm, then openhttp://localhost:7879. - The audit ledger lives in the
mcpdef-auditnamed volume (persists across restarts). Back it up; it is your tamper-evident record.
Upgrade: bump the image: tag in compose.yaml, docker compose pull && docker compose up -d. State (the ledger) survives.
On Kubernetes (Helm) #
Chart: deploy/helm/mcpdef in the source tree (Chart.yaml, values.yaml, and the templates it renders).
# from the source tree
helm install mcpdef ./deploy/helm/mcpdef \
--namespace mcpdef --create-namespace \
--set-file config=./my-mcpdef.toml # your config (declares the upstreams)
Or edit values.yaml's inline config and helm install mcpdef ./deploy/helm/mcpdef -n mcpdef --create-namespace.
Key values (values.yaml):
| Value | Default | Purpose |
|---|---|---|
image.repository / image.tag | mancube/mcpdef / chart appVersion | the image |
config | admin-disabled stub | the full mcpdef.toml (declare your servers here) |
admin.enabled | false | publish the unauthenticated admin/metrics port (:7879); also set [gateway.admin] enabled = true in config |
persistence.enabled | false | give the audit ledger a PVC (else emptyDir, lost on restart); requires replicaCount: 1 (RWO, single-writer) |
serviceMonitor.enabled | false | create a Prometheus-Operator ServiceMonitor for /metrics (requires admin.enabled=true) |
service.type | ClusterIP | expose via your own Ingress/LoadBalancer + TLS |
resources, replicaCount | 1 replica | note: each replica keeps its own ledger; persistence.enabled pins this to 1 |
Reach it:
# MCP endpoint (in-cluster): http://mcpdef.mcpdef.svc:7878/mcp
# admin UI + /metrics (only when admin.enabled=true):
kubectl -n mcpdef port-forward svc/mcpdef 7879:7879
# → http://localhost:7879
Metrics & Grafana #
Metrics live on the admin listener, so enable it first: --set admin.enabled=true (and [gateway.admin] enabled = true in config). Keep it private.
- Prometheus Operator:
--set serviceMonitor.enabled=trueand Prometheus scrapesmcpdef/metricsautomatically. - Plain Prometheus: scrape the
adminport —static_configs: [{ targets: ["mcpdef.mcpdef.svc:7879"] }]. - Grafana: import
deploy/grafana/mcpdef-dashboard.jsonfrom the source tree ("mcpdef — gateway governance": call rate by decision, denies by rule, calls by server, latency p50/p95, upstreams/uptime). It prompts for your Prometheus data source on import.
Notes #
- Single replica by default. The gateway serialises upstream calls and each replica keeps its own audit ledger / pin store (no cross-replica coordination in OSS) — scale out only if you aggregate the ledgers downstream.
- Exposing it. For traffic from outside the cluster, front
:7878with an Ingress that terminates TLS and enable[gateway.auth](OAuth 2.1) inconfig. Do not expose:7879without an auth proxy.
Reference files #
The files this page refers to, exactly as they ship in the source tree.
compose.yaml #
# Run mcpdef on a single cloud VM with Docker or Podman.
#
# 1. cp mcpdef.example.toml mcpdef.toml # then declare your MCP servers in it
# 2. docker compose up -d # or: podman-compose up -d
# 3. point MCP clients at http://<vm>:7878/mcp
# view the status UI at http://127.0.0.1:7879 (SSH-tunnel it; no auth)
#
# The admin port is bound to loopback (127.0.0.1) — it has no auth, so it is not
# exposed off-box. Put a TLS-terminating reverse proxy + [gateway.auth] in front
# of :7878 for real internet exposure (the binary has no in-process TLS).
services:
mcpdef:
image: mancube/mcpdef:0.2.1
command: ["run", "--http", "--config", "/etc/mcpdef/mcpdef.toml"]
ports:
- "7878:7878" # MCP Streamable HTTP (bring your own TLS proxy)
- "127.0.0.1:7879:7879" # admin /metrics + status UI — loopback only
volumes:
- ./mcpdef.toml:/etc/mcpdef/mcpdef.toml:ro
- mcpdef-audit:/var/lib/mcpdef # tamper-evident audit ledger (persisted)
restart: unless-stopped
volumes:
mcpdef-audit:
mcpdef.example.toml #
# Copy to mcpdef.toml and declare the MCP servers to front. Full reference:
# docs/CONFIG.md. This example enables the read-only admin server (metrics + UI).
[gateway]
listen = "0.0.0.0:7878"
audit = "/var/lib/mcpdef/audit.log" # persisted via the compose volume
[gateway.admin]
enabled = true
listen = "0.0.0.0:7879" # published to 127.0.0.1 only by compose
# --- declare your MCP servers (deny-by-default allowlist) ---
# A remote Streamable-HTTP MCP server:
# [[server]]
# id = "internal"
# transport = "streamable-http"
# url = "https://mcp.internal.example/mcp"
# tools = ["list_issues", "get_file_contents"]
# deny = ["delete_*"]
# A local stdio MCP server (its binary must exist in the mcpdef image — for
# arbitrary stdio servers, prefer wrapping them as their own HTTP service):
# [[server]]
# id = "local"
# transport = "stdio"
# command = ["mcp-server-foo", "--flag"]
# tools = ["*"]