MCP server
leoflow-mcp is Leoflow’s Model Context Protocol server
(ADR 0050). Point an LLM agent — Claude Desktop,
Claude Code, or any MCP client — at your control plane and it can read and reason
about your DAGs, runs, task instances, and logs: “which task failed in last
night’s sales run, and why?”
The MCP server is read-only and carries the caller’s own token — its blast radius equals your existing API rights. No tool triggers, clears, edits, or deletes anything. See the security model below.
leoflow-mcp over stdio, and you are talking MCP in two commands.
Run it now →
Tools
High-value, read-only actions — list DAGs, diagnose a run in one call, search a task log.
See the tools →
Resources
Addressable URIs the agent can read — run detail, task instances, sanitized logs and DAG source.
Browse resources →
Wire a client
Drop leoflow-mcp into Claude Desktop or Claude Code and ask "list my DAGs".
Connect a client →Why it matters
Operating an orchestrator is a diagnosis loop: something failed, and you chase it across runs, task instances, and logs. The MCP server hands that loop to an agent. Instead of clicking through the grid or chaining four API calls, you ask a question in plain language and the agent walks the same read surface you would — safely, because it can only see what your token already authorizes.
It is a separate, read-only process. It reaches the control plane only through
the Airflow-compatible /api/v2 (via the typed pkg/client),
carrying the caller’s token, and is never compiled into leoflow-server. It
holds no database, Redis, or Kubernetes access of its own. Built on the official
modelcontextprotocol/go-sdk.
Running it
leoflow-mcp ships alongside the other binaries (installed by the one-command
install, or go build ./cmd/leoflow-mcp). It has two
transports.
export LEOFLOW_SERVER_URL=http://localhost:8088 # your Lite control plane
export LEOFLOW_TOKEN="$(leoflow auth create-token \
--server http://localhost:8088 \
--username admin@leoflow.local --password <your-admin-password>)"
leoflow-mcp # speaks MCP over stdin/stdout
On the stdio transport the process token is the caller’s identity: the
server reads it once from LEOFLOW_TOKEN and every /api/v2 call carries it. Logs
go to stderr — stdout is the MCP protocol channel and carries nothing else.
This is the transport an MCP client (Claude Desktop / Code) launches for you; you
rarely run it by hand.
leoflow-mcp --transport http --listen :9099 --server https://leoflow.internal
The HTTP transport serves POST /mcp (plus GET /healthz) and is stateless:
identity is a per-request bearer, never an ambient process token (ADR 0050 D9).
A request without an Authorization: Bearer <jwt> header is refused — the server
never falls back to a process credential. LEOFLOW_TOKEN is ignored in this mode.
Flags and environment
| Flag | Env | Default | Purpose |
|---|---|---|---|
--server | LEOFLOW_SERVER_URL | http://localhost:8080 | Control-plane base URL (/api/v2 origin). For Lite, use http://localhost:8088. |
--transport | LEOFLOW_MCP_TRANSPORT | stdio | stdio or http. |
--listen | LEOFLOW_MCP_LISTEN | :9099 | Listen address for the http transport. |
| — | LEOFLOW_TOKEN | — | Bearer JWT for the stdio transport (ignored on http). |
--version | — | — | Print the version and exit. |
Auth: getting a token
The MCP passes the caller’s Leoflow JWT through to /api/v2 and never mints one
(ADR 0050 D9). Obtain one from the control plane with your admin login:
leoflow auth create-token \
--server http://localhost:8088 \
--username admin@leoflow.local \
--password <your-admin-password>
Use that token as LEOFLOW_TOKEN (stdio) or as the request Authorization: Bearer
header (http). Tokens are short-lived; treat them as secrets (never log them, never
commit them).
Tools
Tools are the surface most MCP clients render first (ADR 0050 D7), so the server leads with a few high-value ones. All are read-only.
| Tool | What it does | Key inputs |
|---|---|---|
list_dags | List registered DAGs with their paused state (compact). | limit (default 25, max 200), tag |
diagnose_run | Diagnose one DAG run in a single call — its state, which task instances failed, a truncated tail of each failed task’s log, the tasks each failure blocks downstream, and any dbt models involved. Replaces chaining list-runs → get-run → list-tasks → get-logs. | dag_id, run_id, log_tail_lines (default 40, max 200) |
search_logs | Search one task attempt’s log for a case-insensitive substring, returning matching lines with line numbers instead of the whole log. | dag_id, run_id, task_id, try_number (default 1), query, max_matches (default 20, max 100) |
Resources
Addressable, read-only resources — the agent picks the URI; the control plane authorizes each read via the pass-through token, so a resource can only surface what the caller may already see. Log and source reads are sanitized and truncated by construction (untrusted content, ADR 0050 D10).
| Resource URI | Returns |
|---|---|
dag://list | All registered DAGs (compact). |
run://detail/{dag_id}/{run_id} | A DAG run’s detail (state, type, timing). |
task://instances/{dag_id}/{run_id} | The task instances of a run (state, try, duration). |
log://task/{dag_id}/{run_id}/{task_id}/{try_number} | A task attempt’s log, last lines only, sanitized. |
dag://source/{dag_id} | The DAG’s dag.py source, sanitized and size-capped. |
dag://spec/{dag_id} | The compiled dag.json artifact (the structured task graph). |
health://control-plane | Control-plane health: component status, executor capability, and version. |
Wiring an MCP client (Claude Desktop)
Add leoflow-mcp to your client’s MCP server config. For Claude Desktop
(claude_desktop_config.json):
{
"mcpServers": {
"leoflow": {
"command": "leoflow-mcp",
"env": {
"LEOFLOW_SERVER_URL": "http://localhost:8088",
"LEOFLOW_TOKEN": "<paste a JWT from `leoflow auth create-token`>"
}
}
}
}
If leoflow-mcp is not on the launcher’s PATH, use its absolute path as
command (e.g. ~/.leoflow/bin/leoflow-mcp). Restart the client, and Leoflow’s
tools and resources appear. Start with “list my DAGs” or “diagnose the latest
failed run of <dag_id>”.
A single leoflow-mcp targets exactly one control plane (--server). To reach
several environments, add one MCP-client entry per environment — the server never
routes by environment (ADR 0050 D4).
Security: the blast radius
The MVP exposes reads only — no tool triggers, clears, edits, or deletes
anything. Because every /api/v2 call carries the caller’s token and the server
holds no credentials of its own, a bug in tool code can only do what that token
already authorizes: its blast radius equals your own API rights. Log and DAG-source
reads are sanitized and size-capped by construction, treated as untrusted content
(ADR 0050 D10).
See also
- ADR 0050 — Model Context Protocol server: the full design, the security posture, and the untrusted-content threat model.
- Go packages →
pkg/client: the typed/api/v2client the MCP is built on. - HTTP API (Scalar): the
/api/v2surface itself.