Exhaustive reference for every field in mdp.yaml. For the narrative guide with more worked examples, see Config.
At a glance
port_range: "10000-60000"
global:
env_file: ./.mdp.env
env:
API_URL: "http://localhost:${api.port}"
services:
web:
command: npm run dev
dir: ./frontend
proxy: 3000
env:
API_URL: "http://localhost:${api.port}"
api:
command: go run ./cmd/server
dir: ./backend
proxy: 4000
depends_on: [db]
env:
DATABASE_URL: "postgres://localhost:${db.port}/app"
db:
command: docker compose up db --wait
# no proxy — DB is internal only
Top-level
| Key | Type | Default | Notes |
|---|---|---|---|
services |
map of name → service | {} |
Each key is a service name. Shown in the TUI and referenced by depends_on. |
port_range |
"MIN-MAX" string |
"10000-60000" |
Range for auto-allocating service ports (when port is 0 / unset). |
stable_ports |
bool | true |
Reuse each branch's previously-assigned ports across restarts (when still free), stored under ~/.mdp/ports/. Set false to allocate fresh ports every run. See Stable ports. |
global |
global | {} |
Project-wide env export block. |
inputs |
map of name → input | {} |
Values prompted for by mdp run -i, referenced elsewhere as ${inputs.<name>}. Declaration order is the prompt order. |
links |
map of repo → group |
{} |
Cross-repo peer-lookup groups, equivalent to the --link repo=group CLI flag. Values may reference an input (e.g. ${inputs.api_branch}). See links. |
port_range: "20000-25000" # narrow range if you want predictable ports for firewall rules
services:
web:
command: npm run dev
proxy: 3000
api:
command: ./api
proxy: 4000
global
| Key | Type | Default | Notes |
|---|---|---|---|
global.env_file |
string path | "" (none) |
Aggregate .env file to write at startup. Relative paths resolve against the mdp.yaml directory. ~ is expanded. Empty = no file written. |
global.env |
map of name → scalar | {ref: …, default: …} |
{} |
Env vars to write to env_file. Scalar values support ${svc.port} / ${svc.env.VAR} / ${@repo.svc...} interpolation with optional :-default fallback. The mapping form takes a ref: (e.g. svc.port, @repo.svc.port) and an optional default: used when the ref cannot be resolved. |
Writing an aggregate .env for external tools
global:
env_file: ./.mdp.env
env:
# Scalar form — any string, with ${...} interpolation.
API_URL: "http://localhost:${api.port}"
DB_URL: "postgres://app:app@localhost:${db.env.DB_PORT}/app"
# Mapping form — pass another service's port or env var through as-is.
API_PORT:
ref: api.port
DB_PORT:
ref: db.env.DB_PORT
After resolution .mdp.env contains:
# Managed by mdp — mdp-managed keys are updated on start.
API_PORT="42301"
API_URL="http://localhost:42301"
DB_PORT="20045"
DB_URL="postgres://app:app@localhost:20045/app"
Values written by mdp are double-quoted so the file is safe to source from a shell or load with standard .env parsers. When the file is created from scratch, keys are sorted alphabetically.
If the file already exists, mdp updates only the lines for keys it manages (in place, preserving an export prefix and any inline comment) and appends new keys at the end in sorted order — comments, blank lines, unrelated variables, and their order are left untouched. A consequence: a key removed from mdp.yaml lingers in the file with its last-written value until you delete the line by hand.
The file is written after all ports are allocated and before any service command runs. Point your editor's run config or shell tooling at it when you need the same values outside mdp.
inputs
inputs declares values that mdp run -i prompts for at startup. Each resolved value is referenced elsewhere as ${inputs.<name>}. The motivating case is supplying a cross-repo branch name interactively instead of typing --link repo=branch on the command line.
${inputs.<name>} is supported in these fields: a service's command, dir, setup, shutdown, post_start, env (scalar value, ref:, and default:), and env_file; the global.env block and global.env_file; and links values. Using it in group, tls_cert, or tls_key is a load-time error (use links for cross-repo groups); it is not expanded in other fields such as port_range or health_check.
Input names may contain only letters, digits, and underscore (so they are always referenceable as ${inputs.<name>}).
Keys under inputs.<name>:
| Key | Type | Default | Notes |
|---|---|---|---|
prompt |
string | the input name | Question shown when prompting. |
default |
string | none | Fallback used when not prompting, or when the answer is empty. Omitting the key (or writing a bare default: with no value) means no default — such an input errors under plain mdp run and must be prompted with -i. Use default: "" for an explicit empty default. Must be a plain literal — it cannot contain ${...} references. |
choices |
string | "" |
Set to groups to offer a numbered pick-list of the orchestrator's currently active groups (you may still type a custom value). Any other value is rejected at load. |
repo |
string | "" |
Only valid with choices: groups. Limits the pick-list to groups containing services from this repo — useful when the input chooses a peer repo's branch for a link. Must be a plain literal. |
inputs:
api_branch:
prompt: "Which branch is the API running on?"
default: main
choices: groups
repo: api
links:
api: ${inputs.api_branch}
services:
web:
command: npm run dev
env:
VITE_API_URL: "http://localhost:${@api.server.port}"
VITE_TARGET_BRANCH: "${inputs.api_branch}"
Resolution:
mdp run -i: each input is prompted in declaration order through an interactive terminal UI, which requires both stdin and stderr to be a real terminal (it errors if either is piped or redirected). Achoices: groupsinput shows the active groups (filtered byrepoif set) plus a final@{current}entry — "this checkout's default group" — for links that should fall back to the caller's own group (marking thedefault); use Up/Down to browse the list (each selection fills the text box) or type a custom value, and press Enter to submit whatever's in the box. Achoices: groupsinput with no matching active groups and a declareddefaultis skipped silently — exactly like plainmdp run— so-ionly prompts when there is actually something to select; with nodefaultit degrades to a free-text prompt since a value is still required. If the group listing fails (e.g. the orchestrator can't be reached), the input also falls back to a free-text prompt rather than silently taking thedefault. Submitting an input that has nodefaultwith an empty box re-prompts;EscorCtrl-Caborts the run.mdp run(no-i): every input resolves to itsdefault. An input with nodefaultis an error — provide adefaultor run with-i.
Input values are plain literals: an answer (or default) containing ${...} is rejected, so an input can never smuggle in a port or peer reference. An undeclared ${inputs.X} reference in a supported field is a load-time error, and the service name inputs is reserved.
links
links maps a peer repo name to the group its services run in — the config-file equivalent of the repeatable --link repo=group CLI flag. A value may be a literal group or an ${inputs.<name>} reference. The CLI --link flag overrides config links per repo. See Cross-group lookups.
links:
api: ${inputs.api_branch} # group chosen interactively
auth: stable # fixed group
db: "@{current}" # explicitly the caller's own group
The special value @{current} means "the caller's own group" — the lookup behaves as if the link were not declared. It is accepted anywhere a link group can come from: a literal links: value, an input answer or default, or --link repo=@{current}. Note that YAML requires quoting ("@{current}") because a plain scalar cannot start with @. A link that resolves to an empty group (e.g. from an empty input) is still an error — @{current} is the explicit way to fall back. The sentinel can never collide with a real group: git forbids @{ anywhere in a branch name (it's reserved for symbolic revisions like @{u}).
Service
Keys under services.<name>.
| Key | Type | Default | Notes |
|---|---|---|---|
command |
string | "" |
Shell command to run. If empty and port > 0, the service is treated as externally managed: registered with the proxy but not started by mdp. |
setup |
list of strings | [] |
Commands run sequentially before command. First non-zero exit fails the service and command is not started. Shares dir and env. |
shutdown |
list of strings | [] |
Commands run sequentially after command exits (for any reason). Best-effort with a 30s per-step timeout. Shares dir and env. |
post_start |
list of strings | {commands: [...], on_restart: bool} |
[] |
Commands run sequentially after the service becomes ready (TCP-reachable, plus the docker gate when health_check names compose services). Best-effort and non-blocking: failures only warn, and depends_on dependents never wait on them. Shares dir and env. See post_start hooks. |
dir |
string path | "" |
Working directory for command, setup, and shutdown. Relative paths resolve against the mdp.yaml directory. ~ is expanded. |
port |
int | 0 |
Fixed upstream port (for externally managed processes like Docker containers). When 0, a free port is allocated from port_range. Ignored when ports: is set. |
proxy |
int | 0 |
Proxy port to register this service on. 0 means do not register with any proxy — useful for DB-only services. |
group |
string | detected git branch in batch mode, else "" |
Logical grouping. Displayed as <group>/<name> in the TUI and used for cookie-based switching. |
scheme |
"http" | "https" |
"http" (auto "https" when tls_cert is set) |
Scheme the proxy uses to connect to this service's upstream. |
tls_cert |
string path | "" |
TLS certificate path. Relative paths resolve against the mdp.yaml directory. ~ is expanded. Setting this auto-infers scheme: https. |
tls_key |
string path | "" |
TLS key path. Paired with tls_cert. |
env_file |
string path | "" |
Path to write this service's resolved env vars as a .env file. Relative paths resolve against the service's dir if set, else the mdp.yaml directory. ~ is expanded. |
env |
map of name → scalar | {ref: …, default: …} |
{} |
Env vars for command, setup, and shutdown. Scalar values support: literal strings, auto (allocates a port in the corresponding ports[] entry), ${svc.port} / ${svc.NAMED_PORT} (own-mdp.yaml port references), ${@repo.svc.port} / ${@repo.svc.env.VAR} (cross-repo lookups via the orchestrator) with optional :-default fallback. The mapping form takes a ref: (e.g. api.port, @backend.api.env.URL) plus an optional default:. Cross-repo refs without a default are silently omitted when the peer is not registered. |
ports |
list of port mapping | [] |
Multi-port mode. When present, port is ignored and ports are allocated per entry. |
log_split |
string | mapping | "" |
Demultiplex combined-stream logs into per-sub-service colored lanes. Accepts the scalar "compose" (built-in docker-compose parser) or a mapping { regex: '<pattern>' } for arbitrary prefixes. See log_split. |
depends_on |
list of service names | [] |
Wait for each dependency to be TCP-reachable on its assigned port(s) before starting. 60s per-dependency timeout. Unknown names and cycles are rejected at config load. |
health_check |
health check | "docker" |
nil (TCP on port) |
Liveness probe used by the registry pruner. When unset, the default is a TCP dial of the service's registered port. The docker: [svc, ...] variant also gates startup readiness. See Detached services and health checks. |
restart |
bool | false |
Automatically restart the service's process after it exits — crash or clean exit alike. mdp run --restart enables this for every service in addition to whatever's set here. See restart. |
command — the basic case
services:
web:
command: npm run dev
dir: ./frontend
proxy: 3000
command omitted — register an externally managed process
When command is empty and port is set, mdp only registers the upstream with the proxy. You start the actual process however you like (Docker, systemd, another terminal).
services:
auth:
port: 8080 # already running somewhere on :8080
proxy: 5000 # expose it through the proxy on :5000
setup and shutdown hooks
services:
web:
setup:
- bun install
- bun run build:assets
command: bun dev
shutdown:
- rm -rf .cache/dev
dir: ./frontend
proxy: 3000
If bun install exits non-zero, web is marked failed and bun dev is never started. shutdown runs whenever command exits — clean exit, crash, or Ctrl-C.
When mdp run is connected to a terminal (unix only), hooks run on a pseudo-terminal so a hook that prompts for input doesn't hang silently. If a hook's output stalls mid-line (or right after a line ending in :, ?, or >), the hook takes focus of your terminal: the prompt is replayed, keystrokes go to the hook (Ctrl-C included), and all other service/log output is buffered in memory until focus releases — then it flushes and streaming resumes. Focus releases automatically once the hook moves past its prompt (resumes output that no longer looks like a prompt), when the hook exits, or when you press Ctrl-]. Before you type anything, an unterminated prompt holds focus even if the hook prints noise (a countdown, a periodic log line) while waiting. If several hooks need input, they take focus one at a time, with the in-between output flushed between them. Because a PTY has a single output stream, hook stderr is merged into stdout in this mode. This applies to setup, shutdown, and post_start hooks. Set MDP_NO_HOOK_PTY=1 to disable and run hooks on plain pipes; non-TTY environments (CI) and Windows always use plain pipes.
post_start hooks
post_start runs one-shot commands once the service is ready — its TCP port(s) accept connections, and, when health_check names docker compose services, those containers are running and healthy. Use it for work that needs the running service (seeding a database, warming a cache) without defining a separate service for it.
services:
db:
command: docker compose up
health_check:
docker: [postgres]
post_start:
- ./scripts/migrate.sh
- ./scripts/seed-db.sh
Semantics:
- Best-effort — a non-zero exit logs a warning and the next command still runs; the service is unaffected.
- Non-blocking —
depends_ondependents unblock on readiness as usual; they never wait forpost_startto finish. - Commands run sequentially and share the service's
dirandenv, likesetupandshutdown. - If the docker health gate never passes within the readiness timeout, the hooks are skipped with a warning.
By default the hooks run only on the initial start. To re-run them after a cross-repo peer change restarts the service, use the mapping form:
post_start:
commands:
- ./scripts/seed-db.sh
on_restart: true
port — fixed vs auto-allocated
services:
api:
command: ./api
proxy: 4000
# no `port:` → mdp picks a free one from port_range and passes it as $PORT
legacy:
command: ./legacy-server --bind=:9000
port: 9000 # legacy-server can't read $PORT, so pin it
proxy: 4100
proxy: 0 — internal-only services
services:
db:
command: docker compose up db --wait
# proxy omitted → the DB is allocated a free port but is NOT exposed
# through a proxy listener. Other services reference it via ${db.port}.
group — organise multiple branches or repos
Usually group is auto-detected from the git branch in batch mode, but you can set it explicitly when running unrelated projects side-by-side:
services:
web-main:
command: npm run dev
dir: ../main/web
proxy: 3000
group: main
web-feature:
command: npm run dev
dir: ../feature-x/web
proxy: 3000
group: feature-x
Both services register on proxy :3000; the widget / TUI switches between main/web-main and feature-x/web-feature.
scheme, tls_cert, tls_key — HTTPS upstream
services:
web:
command: npm run dev -- --https
proxy: 3000
tls_cert: ./certs/localhost.pem
tls_key: ./certs/localhost-key.pem
# scheme auto-becomes "https" — no need to set it
env_file — write one service's env to a dotfile
services:
api:
command: ./api
dir: ./backend
proxy: 4000
env_file: .mdp.env # resolves to ./backend/.mdp.env
env:
DATABASE_URL: "postgres://localhost:${db.port}/app"
The mdp-managed keys mirror exactly what the api process sees (merged into the existing file if one is already there, same rules as global.env_file — any other keys in the file are preserved but not passed to the process):
# Managed by mdp — mdp-managed keys are updated on start.
DATABASE_URL="postgres://localhost:42044/app"
PORT="20017"
env — literals, interpolation, and auto
services:
api:
command: ./api
proxy: 4000
env:
LOG_LEVEL: debug # literal
API_URL: "http://localhost:${api.port}" # self-reference
DB_URL: "postgres://localhost:${db.port}" # cross-service port
To read another service's env var, export it through global.env — service-level env values cannot use ${svc.env.VAR}.
auto is only meaningful inside multi-port services:
services:
infra:
command: docker compose up --wait
env:
API_PORT: auto # mdp assigns a free port and writes it here
DB_PORT: auto
ports:
- env: API_PORT
proxy: 4000
- env: DB_PORT
ports — multi-port services
Use this when one command exposes several ports and you want to map (or not map) each one to a proxy individually:
services:
infra:
command: docker compose up --wait
env:
API_PORT: auto
DB_PORT: auto
ports:
- env: API_PORT
proxy: 4000 # HTTP — registered with the 4000 proxy
- env: DB_PORT # no proxy — DB port is internal only
Every proxy-bearing port of a multi-port service registers under the parent service's key (<group>/<service>). The registered entry's env map carries all of the service's resolved env vars, so cross-repo refs select a specific port by env-var name: @<repo>.<service>.env.<ENV_VAR>. The bare form @<repo>.<service>.port is ambiguous when a service has more than one proxy-bearing port and will be rejected with a 409 — use the .env.<KEY> form. Two ports of the same service cannot share a proxy: value (the second would silently overwrite the first; mdp rejects the config at load time).
Other services reference the named ports as ${svc.NAME}, where NAME is the port mapping's env key:
services:
web:
command: npm run dev
proxy: 3000
env:
API_URL: "http://localhost:${infra.API_PORT}"
DB_URL: "postgres://localhost:${infra.DB_PORT}/app"
depends_on: [infra]
log_split — demultiplex combined-stream logs
When a service's command produces output from multiple sub-processes over a single stream (e.g. docker compose up), log_split parses each line and routes it to its own colored lane so each sub-process gets its own prefix. Two forms are accepted:
Built-in compose mode (scalar shorthand) — parses docker-compose's <name> | <message> format, including colorized output (--ansi=always / TTY).
services:
infra:
command: docker compose up
log_split: compose
env:
API_PORT: auto
AUTH_PORT: auto
ports:
- env: API_PORT
proxy: 4000
- env: AUTH_PORT
proxy: 5000
Custom regex mode (mapping form) — provide any Go-regexp pattern with named captures name and msg. Useful for kubectl, honcho/foreman, bracket-prefixed tools, or anything else that multiplexes logs over one stream.
services:
procfile:
command: honcho start
log_split:
regex: '^(?P<name>[a-z]+)\s*\|\s(?P<msg>.*)$'
k8s-app:
command: kubectl logs --all-containers --prefix -f pod/api
log_split:
regex: '^\[pod/(?P<name>[^/]+)/[^\]]+\]\s*(?P<msg>.*)$'
Lines that don't match the parser fall through to the outer service prefix, so top-level status output (compose's Attaching to…, kubectl's reconnect messages, etc.) still appears under the service name. Stdout and stderr share the same name-to-color map so a sub-process's two streams collapse into one lane. Inside mdp.yaml, sub-lane labels are prefixed with the service name as <service>/<sub> so it's obvious which service an inner lane belongs to. Lane labels render at full length — short labels are right-padded to 12 characters for column alignment, longer labels expand past that and push their line out rather than being truncated.
For ad-hoc commands, pass the same value as a flag:
mdp run --log-split=compose -- docker compose up
mdp run --log-split='regex:^\[(?P<name>[^\]]+)\]\s*(?P<msg>.*)$' -- some-prefixed-tool
restart — auto-restart on crash
services:
api:
command: ./api
proxy: 4000
restart: true
When restart: true, mdp relaunches the service's process whenever it exits — a crash (non-zero exit) or a clean exit (0) are treated the same, since both mean the dev server is no longer running. There's a fixed 1s pause between the exit and the relaunch; restart continues indefinitely (no retry cap or backoff curve). It never fires on an mdp-initiated shutdown (Ctrl-C, mdp stop, orchestrator going away).
mdp run --restart turns this on for every service in a batch run, in addition to (not instead of) any per-service restart: true already in mdp.yaml. For an ad hoc command (mdp run -- some-command), --restart is the only way to enable it — there's no per-command YAML.
Auto-restart is incompatible with holding a session open for a detached command (e.g. docker compose up -d): with --restart, a clean exit relaunches immediately instead of watching the port and waiting for the detached process to stop.
depends_on — wait for readiness
services:
db:
command: docker compose up db --wait
env:
DB_PORT: auto
ports:
- env: DB_PORT
api:
command: ./api
proxy: 4000
depends_on: [db]
web:
command: npm run dev
proxy: 3000
depends_on: [api, db]
Services without depends_on start in parallel. Independent branches of the dependency graph also run in parallel — only direct dependents wait. Each dependency has a 60s TCP-readiness timeout. Failed deps cause dependents to be marked failed and skipped. Cycles and references to undefined services are rejected at config load.
Detached services and health checks
By default, mdp prunes a service from its proxy when the command process exits. That's wrong for detached commands like docker compose up -d: the foreground process exits quickly, but the real service is still listening on its port.
Instead, once the process exits mdp falls back to a liveness probe and keeps the entry around until the probe fails for ~30 seconds. The default probe is a TCP dial of the service's port. Override it with health_check:
services:
# Default — TCP probe on svc.port.
web:
command: npm run dev
proxy: 3000
# Custom HTTP probe.
api:
command: bun run dev
proxy: 4000
health_check:
http: http://localhost:4000/health
# Shorthand for docker compose: runs `docker compose ps -q` in `dir`,
# healthy as long as at least one container in the project is running.
db:
command: docker compose up -d
dir: ./db
port: 5432
health_check: docker
# Named compose services: each must be running, and healthy if it defines
# a HEALTHCHECK. Also gates startup readiness — see below.
stack:
command: docker compose up -d
dir: ./stack
port: 5432
health_check:
docker: [db, redis]
# For compose projects whose services are all short-lived, probe the
# network directly instead.
workers:
command: docker compose up -d
dir: ./workers
port: 6000
health_check:
command: "docker network inspect workers_default"
Variants (mutually exclusive):
| Field | Healthy when |
|---|---|
tcp: <port> |
a TCP connection to localhost:<port> succeeds |
http: <url> |
an HTTP GET returns a 2xx or 3xx status |
command: <shell tokens> |
the command exits 0 (run in the service's dir) |
shorthand docker |
docker compose ps -q in the service's dir exits 0 with non-empty output |
docker: [svc, ...] |
every named compose service has all containers in state running, and healthy if a HEALTHCHECK is defined |
Unlike the other variants, which only affect post-exit liveness pruning, docker: [svc, ...] also gates startup readiness: the service isn't marked running (and depends_on dependents don't start) until the named compose services pass, in addition to TCP port reachability. It runs docker compose ps in the service's dir and requires Docker Compose v2.
Command parsing honors single/double quotes but does not invoke a shell, so write sh -c "..." explicitly if you need shell features. Timeouts: TCP 2s, HTTP 3s, command/docker 5s.
Port mapping
Entries in services.<name>.ports[].
| Key | Type | Required | Notes |
|---|---|---|---|
env |
string | yes | Env var name that will hold the allocated port. Reference from other services' env with ${svc.NAME}, or from global.env with ${svc.env.NAME}. |
proxy |
int | no (default 0) |
Proxy port to register this port on. 0 = no proxy (use for DB / non-HTTP ports). Two ports of the same service cannot share a proxy: value. |
protocol |
string | no (default tcp) |
Transport protocol. udp marks the port as UDP: allocation uses a UDP-aware free-port check, and the depends_on readiness probe skips it (TCP probes never succeed on UDP). Incompatible with proxy. |
services:
infra:
command: docker compose up --wait
env:
API_PORT: auto
WS_PORT: auto
DB_PORT: auto
JAEGER_AGENT_PORT: auto
ports:
- env: API_PORT
proxy: 4000
- env: WS_PORT
proxy: 4001
- env: DB_PORT # internal only, no proxy
- env: JAEGER_AGENT_PORT
protocol: udp # UDP-only; compose publishes "${JAEGER_AGENT_PORT}:6831/udp"
In the TUI this service appears as <group>/infra on both the 4000 and 4001 proxies — the parent service key is the registered identity. Internal-only ports (no proxy:) and UDP mappings are not registered against any proxy and do not appear in the TUI.
Interpolation
Service-level env values support port references only:
${svc.port}— the primary allocated port of the service namedsvc(single-port services).${svc.NAME}— a named port from a multi-port service'sports[]entry, whereNAMEis that entry'senvkey (e.g.${infra.API_PORT}).
global.env scalar values support everything above, plus:
${svc.env.VAR}— the resolved value of another service's env var.
Using ${svc.env.VAR} inside a service-level env is a startup error ("env-var references are not allowed here"). All references resolve after port allocation and before any command runs, so you can freely cross-reference services without worrying about start order.
services:
api:
command: ./api
proxy: 4000
infra:
command: docker compose up --wait
env:
GRPC_PORT: auto
ports:
- env: GRPC_PORT
web:
command: npm run dev
proxy: 3000
env:
API_URL: "http://localhost:${api.port}" # single-port primary
GRPC_URL: "localhost:${infra.GRPC_PORT}" # multi-port named
depends_on: [api, infra]
global:
env_file: ./.mdp.env
env:
# global.env adds ${svc.env.VAR} — the resolved value of another service's env var.
ADMIN_TOKEN: "${api.env.ADMIN_TOKEN}"
ref: mapping form
Scalar strings work in any env. The ref: mapping form passes the referenced value through without string-wrapping. It works in both global.env and per-service env:
global:
env_file: ./.mdp.env
env:
# Scalar — wraps the value into a string template.
API_URL: "http://localhost:${api.port}"
# Ref — passes the value through on its own.
API_PORT:
ref: api.port
ADMIN_TOKEN:
ref: api.env.ADMIN_TOKEN
Cross-repo @<repo> references
mdp run instances in different repos all register with the same singleton orchestrator. To reference a service running in another repo, prefix the reference with @<repo-name>.. The lookup is scoped to the same group as the caller by default (typically the git branch); use --link to override the lookup group per peer repo (see below).
# In frontend's mdp.yaml
services:
web:
command: npm run dev
env:
# Interpolation form with optional :-default fallback.
API_URL: "http://localhost:${@backend.api.port:-3001}"
# Ref form with optional default: field.
AUTH_TOKEN:
ref: "@backend.api.env.AUTH_TOKEN"
default: "dev-token"
Resolution semantics:
- The frontend resolves
@backend.*against the orchestrator at startup. Whatever the backend is registered as (port, exposed env vars), the frontend gets — for the same group. - If the peer is unresolved AND a default is provided, the default is used.
- If the peer is unresolved AND no default is provided:
- For
ref:form, the env var is silently omitted (graceful "ignored" behavior). - For
${...}interpolation form, you must supply:-default; otherwise it is a hard error.
- For
- The supervisor watches each cross-repo peer for changes. When the peer's port or any referenced env var changes (or the peer (de)registers), the dependent service is killed and relaunched with refreshed values.
Notes:
@selfand other reserved names have no special meaning —@<repo>is just whichever string was registered as the peer's repo (typically the basename of the directory containing the peer'smdp.yaml).- Cross-group references require
--link <repo>=<group>. Without it, the peer must be in the same group as the resolver.
Cross-group lookups via --link
Use mdp run --link <repo>=<group> to redirect cross-repo @<repo>.* references to a different group than the caller's. The flag is repeatable and last-wins per repo. Common case: a frontend on a feature branch wiring to a backend that runs on main.
# Frontend on branch derek/foo, backend on main:
mdp run --link api=main
# In the frontend's mdp.yaml, no change needed:
services:
web:
command: npm run dev
env:
VITE_API_URL: "http://localhost:${@api.server.port}"
The override applies to all @<repo>.* references resolved during this mdp run invocation, both at startup and during peer-change watching.
To avoid typing the group on every run, declare it in the links section instead — and combine it with an input to choose the branch interactively:
inputs:
api_branch:
prompt: "Which branch is the API on?"
default: main
choices: groups
repo: api
links:
api: ${inputs.api_branch}
mdp run -i then prompts for api_branch (offering the active groups containing api services plus @{current}, or skipping straight to the default when there are none), and mdp run without -i uses the main default. A --link api=<group> on the command line still overrides the config value.
To default to the caller's own group instead of a fixed branch, use the @{current} sentinel (quoted — YAML reserves a leading @):
inputs:
api_branch:
prompt: "Which branch is the API on?"
default: "@{current}"
choices: groups
links:
api: ${inputs.api_branch}
Now plain mdp run resolves @api.* references against the caller's own group, and mdp run -i lets you pick a different one. --link api=@{current} works the same way from the command line.
Path resolution
All paths support ~ expansion. Relative paths resolve as follows:
| Field | Base directory |
|---|---|
dir |
mdp.yaml directory |
tls_cert, tls_key |
mdp.yaml directory |
global.env_file |
mdp.yaml directory |
env_file (service-level) |
the service's dir if set, else the mdp.yaml directory |
Absolute paths are used as-is.
# mdp.yaml lives at /repo/mdp.yaml
services:
api:
dir: ./backend # → /repo/backend
tls_cert: ~/certs/dev.pem # → $HOME/certs/dev.pem
env_file: .mdp.env # → /repo/backend/.mdp.env (relative to dir)
web:
# no dir set
env_file: .mdp.web.env # → /repo/.mdp.web.env (falls back to mdp.yaml dir)