conductor mcp serve exposes your registered Conductor workflows as
Model Context Protocol (MCP) tools, so
any MCP-compatible host — Claude Code, VS Code, Cursor, or your own agent —
can discover and invoke them directly, with a typed input schema derived
from each workflow's own input: block.
This page is about Conductor being an MCP server. For the mirror image — Conductor workflows calling MCP tools via
runtime.mcp_servers— see MCP Tools.
If you already have a registry configured, there is no workflow editing required (G1):
# One-time: register a source of workflows, if you haven't already
conductor registry add official myorg/conductor-workflows --default
# Start the server
conductor mcp serveEvery workflow in every configured registry is exposed as a tool by
default. Point your MCP host at the command above (see
Host Configuration below) and its tools/list
request will return one tool per workflow, named after the workflow's own
(slugified) name — no conductor_ prefix, no registry-qualified name,
unless two registries publish a workflow with the same name (see
Naming and collisions).
conductor mcp serve speaks stdio only in v1 (Limits,
DD9) — the transport every current MCP host spawns a local server over.
Configure your host to run the command as a subprocess.
{
"mcpServers": {
"conductor": {
"command": "conductor",
"args": ["mcp", "serve"]
}
}
}Add this to .mcp.json in your project root, or register it globally with
claude mcp add.
Add to .vscode/mcp.json (or your user MCP settings):
{
"servers": {
"conductor": {
"type": "stdio",
"command": "conductor",
"args": ["mcp", "serve"]
}
}
}Add to .cursor/mcp.json:
{
"mcpServers": {
"conductor": {
"command": "conductor",
"args": ["mcp", "serve"]
}
}
}Any of these can narrow or widen what gets exposed by adding flags to
args, e.g. ["mcp", "serve", "--registry", "official", "--allow", "release-*"].
See conductor mcp serve in the
CLI reference for every flag.
Whether a given workflow becomes a tool is decided by a four-rung ladder, evaluated in this order — each rung outranks the ones below it (DD4):
--deny <glob>(repeatable) — the highest-precedence rung. A match excludes a workflow unconditionally, even one an--allowpattern also matches.--allow <glob>(repeatable) — a non-empty--allowlist switches the server into allow-list mode: only matching workflows are candidates, and a match overrides a workflow's ownmcp.expose: false.- The workflow's own
mcp.expose(see Themcp:Block below) —falseopts a specific workflow out. - Default: exposed. A workflow with no
mcp:block, and not matched by--deny/--allow, is exposed. This is deliberate (DD4): opt-in exposure would expose zero tools from any existing registry, since no workflow anywhere carries anmcp:block yet.
--registry <glob> (repeatable) sits one level above this ladder — it
narrows which registries are even considered before the ladder runs.
--workflow-dir <path> (repeatable) adds workflows from a local directory
alongside, or instead of, any registry.
The startup summary the server prints to stderr (see
Startup Summary) names every workflow it does
expose, its source registry, and its pinned identity — a workflow
silently gaining exposure because a registry gained one is only silent
if nothing says otherwise, and this is the channel that says otherwise.
It does not enumerate workflows the ladder excluded (via --deny, a
non-matching --allow, or mcp.expose: false); those are simply absent
from the published tool list.
A workflow opts into non-default MCP behavior with a mcp: block under
workflow:. Every field defaults to the value that keeps an existing
workflow (with no block at all) exposed exactly as before — nothing here
is required:
workflow:
name: review-pr
description: Reviews a pull request across correctness, tests, and security.
mcp:
expose: true # candidate for exposure (the default)
mode: async # async (default) | sync | auto -- a default, not a mandate
read_only: false
destructive: true
estimated_minutes: 8| Field | Default | Meaning |
|---|---|---|
expose |
true |
Whether this workflow is a candidate for exposure at all (rung 3 of the ladder above). |
mode |
async |
The invocation mode a caller's omitted _wait_seconds resolves to. sync resolves to the server's --max-wait-seconds ceiling rather than an unbounded wait; async/auto return immediately. This is a default a caller can still override per call, not a mandate. |
read_only |
false |
Surfaced as the generated tool's readOnlyHint annotation. |
destructive |
false |
Surfaced as the generated tool's destructiveHint annotation. |
estimated_minutes |
unset | A client-side hint for how long a run typically takes; must be positive when set. |
WorkflowDef forbids unknown keys, so a typo (expse: false) is a schema
error conductor validate reports, not a silently-ignored value (FR11).
See examples/mcp-serve.yaml for a complete, runnable example.
The server groups its tools into named toolsets, decided once at startup
from --toolsets and never re-evaluated per connection or per request
(DD3):
| Toolset | Default | Contents |
|---|---|---|
workflows |
on | One generated tool per exposed workflow (or, above --max-direct-tools, the two-tool discovery pair — see Discovery Mode). |
runs |
on | conductor_run_status, conductor_await_run, conductor_cancel_run, conductor_list_runs — see The Run Lifecycle. |
introspect |
off | conductor_run_events, conductor_node_detail, conductor_plan_tree — event-level and per-step detail for a run. |
diagnose |
off | conductor_doctor, conductor_validate_workflow, conductor_run_logs — environment health, pre-flight validation, and links to a run's raw logs. |
introspect and diagnose add zero tools to the default footprint —
enable them explicitly with
--toolsets workflows --toolsets runs --toolsets introspect --toolsets diagnose
(the option is repeatable, one toolset name per occurrence) when you need
failure diagnosis from the same MCP connection that started the run.
Above --max-direct-tools (default 25) exposed workflows, the server
serves a fixed two-tool discovery pair instead of one tool per
workflow, so a large registry degrades predictably rather than silently
overflowing a host's tool-count limit (FR9, G7):
conductor_find_workflow(query)— searches the exposed catalogue by name/description and returns matching workflow identities.conductor_run_workflow(name, inputs, ...)— invokes a workflow found viaconductor_find_workflowby name. No tool anywhere accepts a filesystem path, URL, or registry source as a parameter (NFR3) — only a name already present in the frozen catalogue.
Whether the server is in direct or discovery mode is decided once at startup from the exposed count, never re-decided later, and is named in the startup summary.
Invoking a workflow tool (in either direct or discovery mode) never
executes the workflow inside the server process. It always forks a
detached conductor run ... --web child, the same launch path
conductor run --web-bg uses. The run is detached in every mode — the
only thing that ever varies is whether the tool call waits for it before
returning (G3, G4):
-
_wait_seconds— every generated workflow tool accepts this reserved parameter, and the intended answer is to leave it unset. Omitted, the call returns a run handle within seconds while the workflow keeps going, so the caller can report the run back and move on.> 0blocks up to that many seconds for a terminal state, capped by the server's--max-wait-secondsceiling regardless of the value requested;0is an explicit return-immediately. Omitted, a workflow declaringmcp.mode: syncis the one case that still blocks. Reaching a human gate ends a bounded wait early — see Limits.Setting
_wait_secondsdoes not make the workflow "run properly" — it only holds the caller's turn open. A model that reaches for it on a long workflow will block for the full ceiling and find the run still going afterwards. If you want the guarantee rather than the convention, start the server with--max-wait-seconds 0: the ceiling applies to every blocking path, so no invocation can block at all, whatever a caller requests or a workflow declares. -
Immediately, at a gate, on failure, or when a bounded wait's deadline is reached, the result is a handle. It always includes the run's
run_id, the dashboardurl(G4) and theportit bound, the pinned workflow identity, and anobserveblock of commands for watching the run from a terminal (conductor fleet,conductor status,conductor stop --port <port>). See the result shapes below for what a completed run returns instead. -
conductor_run_status(run_id)— status, current step, and (at a gate) the gate's prompt, options, and the dashboard approval URL. Works for a live run, a run parked at a gate, and a run whose process has already exited. -
conductor_await_run(run_id, wait_seconds=60)— blocks, bounded by--max-wait-seconds, returning on a terminal status or on reaching a gate; its timeout text names the approval URL as the next action. -
conductor_cancel_run(run_id, force=false)— stops a run through the same graceful-then-force ladderconductor stopuses, so a checkpoint is written when the graceful rung succeeds. Reports an already-terminal run as a distinct, non-error outcome. -
conductor_list_runs(status?, workflow?, limit=20)— over live and completed runs alike;status="at-gate"is the query that surfaces every parked run.
A run_id is answerable through this lifecycle before, during, at a
gate, and after the run — including once the launching process has long
since exited, via the terminal run record every run writes on completion
(see conductor status / conductor fleet list
and R1's scope change below).
--max-concurrent-runs (default 0, unbounded) bounds how many runs
this server process has launched and still has live; over the cap, a
launch is rejected with a message pointing at
conductor_list_runs/conductor_cancel_run, never queued. Restarting the
server resets the count — the server owns no execution state of its own,
so there is nothing else for it to remember.
Stdout is the JSON-RPC transport (DD9); nothing may write to it but
protocol bytes. So the server's one channel for operator-facing
information is stderr — the startup summary (FR10), printed once per
process start and also written through the standard logging module at
warning level for anomalies, so a host's own MCP log aggregation captures
them too:
conductor mcp serve: exposing 12 workflow(s) in direct mode (--max-direct-tools=25).
Toolsets enabled: runs, workflows.
review_pr <- official/review-pr (pin: a1b2c3d4e5f6)
summarize_topic <- official/summarize-topic (pin: 9f8e7d6c5b4a)
...
It names, in order: the exposed count and direct-vs-discovery mode; every
published tool with its source registry and pinned identity; every
workflow exposed with a degraded schema and why (see
the schema resolution ladder note below);
and every tool-name collision it qualified, naming both registries. This
is the only place a stdio server has to say "here is what I did" — use
conductor doctor to see the same
information without attaching a host at all.
Tool names are bare — the slugified workflow name, with no
conductor_ prefix — unless two configured registries publish a workflow
with the same name, in which case both are qualified with their
registry (never just the loser), or --tool-prefix is set, which prefixes
every generated workflow tool name (DD10). The static run-lifecycle,
introspect, and diagnose tools always carry their conductor_ prefix,
since there is nothing to disambiguate.
The following are deliberate boundaries, not gaps to be worked around:
WorkflowDef.output is a dict[str, str] of untyped Jinja2 templates, so
an honest outputSchema cannot currently be published — no invocation
result declares one (spec-legal, and what most MCP servers do), which
means a calling model learns the result shape by reading a result rather
than by contract. Three distinct shapes exist:
- Immediate, at-gate, failed, or timed-out — a handle:
structuredContentcarriesrun_id,status, the dashboardurl, and the run's pinned workflow identity (plus agate/errorblock where relevant). - Inline completion (the rendered
output:dict serialized to 50 KB or less) —structuredContentis theoutput:dict itself, with no wrapping envelope, plus a human-readable text fallback. - Spilled completion (serialized
output:over 50 KB) — the output is written to a file andstructuredContentis{run_id, status, note}plus aresource_linkto the file; the output is never embedded inline.
Deriving an outputSchema from referenced agents' typed OutputFields is
a tracked follow-up, not part of this release.
The server never passes --skip-gates to a launched run. A run that
reaches a human gate reports status: "at-gate" with the gate's prompt,
options, and a dashboard approval URL, and stays there until a human
resolves it via the dashboard, conductor gate respond, or the Fleet TUI
— indefinitely, if nobody is watching. This is intentional: a gate is a
control the workflow's author deliberately placed in the path, and the
caller being a model rather than a person is the strongest argument for
keeping it, not for removing it. conductor_await_run returns as soon as
it reaches the gate rather than burning its budget waiting for one to
resolve itself, and conductor_list_runs(status="at-gate") enumerates
every parked run so none of them are silently forgotten. v1 ships no park
timeout and no reaper.
conductor_run_logs (the diagnose toolset) returns resource_link
content blocks pointing at a run's .bg.stderr.log, .bg.stdout.log, and
.events.jsonl, plus bounded metadata (size, modified_at, exists)
and the terminal record's error type/message — never file contents,
regardless of size. Conductor has no redaction layer today, and these
files (including runtime.tool_output spill files) may contain secrets;
returning only paths routes the read through the consent boundary the
user already configured for their host's own file tools, rather than
Conductor becoming an unredacted exfiltration surface. A host with no
file-reading tool simply cannot follow the link — better than the
alternative of a globbed temp directory, but still a real limitation.
conductor_run_events (the introspect toolset) reduces each tool call's
arguments and result to {name, status, byte_size} by default —
byte_size is computed from the original serialized payload, so a caller
learns the size of what it is not being shown. Pass --introspect-full
at server startup to restore the original arguments and results. This
does not affect conductor_node_detail, which returns a run's prompt
and output in full regardless — that toolset's activity lines never
carried tool payloads to begin with.
No Streamable HTTP transport, no OAuth, in v1. stdio is what every current
MCP host already spawns, needs no auth story beyond process ownership,
and has no observed tool-call timeout. web/auth.py's existing token-file
model is the intended reuse point when HTTP transport lands.
Every conductor mcp serve invocation launches a real conductor run
under the hood, and that run now writes a terminal record on exit —
which means conductor status, conductor fleet list, and the Fleet
TUI's History screen all gained a completed-runs section as a side effect
of this feature shipping, not something opt-in to this server. See the
CHANGELOG and
conductor status /
conductor fleet list for the
full scope-change description and the --live flag that restores the
pre-change behavior.
- MCP Tools — the mirror image: Conductor workflows
calling MCP tools via
runtime.mcp_servers. conductor mcp serve— full CLI flag reference.- Fleet Manager —
conductor fleet, terminal records, and retention, which every MCP-launched run participates in identically to a CLI-launched one. - Workflow Syntax Reference — the full schema,
including the
mcp:block. conductor doctor— inspect what a server would expose without attaching a host.