Note
The Python interpreter and the run_python tool described below are in flight, not released.
They sit on branches against v2.1.1. Everything else on this page ships today.
Most of what an assistant does on your behalf is run commands. The usual way it does that is a generic shell tool: it composes a string, something executes it, the output comes back, and the whole thing evaporates when the conversation scrolls. That works, and it is also the reason you cannot answer “what did it actually do” an hour later.
flow already had the pieces to do better. It knows your workspace, it holds your secrets, it captures logs, and it records every run. Exposing that over MCP turns it from a task runner into somewhere an agent can work.
The Scope Boundary
Worth stating up front, because it shapes everything else:
flow is an AI tool provider, not an AI consumer.
The core exposes deterministic capabilities: an MCP server, published JSON schemas, an
llms.txt. It does not make model calls. No LLM parsing of natural-language commands, no
generation inside the CLI. That would put vendor keys, per-call cost, and non-determinism in the
critical path of a task runner. Anything applying a model to flow does so from outside, through
the MCP surface. Mochi is exactly that: a consumer built on top.
The Ladder
The run tools are deliberately ordered, closest fit first:
| Tool | For |
|---|---|
execute | A task you have already named. Runs the project’s real test or deploy. |
run_command | A one-off shell command. |
run_python | The one-off, when it is Python rather than shell. |
run_executable | Something richer than a single command. |
The reason run_python is its own tool rather than a flag on run_command is small and
practical: agents select tools by name, and a tool called “run_command” is not what gets reached
for when the task is Python.
Around those sit discovery and inspection tools (list_executables, get_executable,
list_workspaces, get_workspace, switch_workspace, get_info), history (get_execution_logs),
and authoring (write_flowfile, which validates against the schema server-side before writing).
There are MCP resources for workspaces, executables, flowfiles and logs, and prompts for
generating and debugging executables.
The server is built on mcp-go, and exposes Tools and Prompts but not Resources. I wanted to use Resources, but client support for them is still thin, and Tools plus Prompts gave the best experience across the most clients.
The boundary is stated honestly in the server instructions: fall back to a raw shell for things that genuinely should not be recorded, or that flow is not suited to, like anything needing a TTY.
What Running Through flow Buys You
Compared to a generic execute tool:
- Named work first. Discovery means the agent runs your actual
testexecutable rather than its own approximation of one. - Workspace resolution from a directory. Pass a path and flow walks up to the nearest
flow.yaml. Works in a fresh clone or a git worktree with nothing registered. - Secrets from the vault, injected as environment, never in the argv.
- Provenance on every run.
source,clientName,sessionId,workingDir. - Lifecycle-aware history. Written as
runningat start and upserted on completion, so a log can be read while the run is still going. - Approval gates in the workflow, via
reviewRequiredon a step, rather than depending on the client to ask. - Byte-capped structured output, so a runaway log cannot eat the context window.
Provenance Has Opinions
Three environment variables carry it: FLOW_RUN_SOURCE, FLOW_RUN_CLIENT, FLOW_RUN_SESSION.
Two decisions behind that are worth repeating.
There is no client registry. flow does not sniff for CLAUDE_CODE_SESSION_ID or any other
vendor’s variables. Those are undocumented internals that get renamed, and detection built on
them fails silently, so history quietly stops grouping and nobody notices. Each tool maps its own
variables onto the contract instead.
And identity is exported, not passed. Environment beats a flag the assistant has to remember on every call: a model can silently omit an argument, but it cannot omit a variable it never sees. Parameters are for intent, which is the only thing the model actually knows.
The Python Interpreter
The in-flight work adds python alongside the built-in POSIX shell:
executables:
- verb: run
name: report
exec:
interpreter: python
cmd: |
import json, sys
print(json.dumps({"python": sys.version_info[:2]}))
A .py file needs no interpreter field at all, since the extension implies it. The same field
works on serial and parallel steps, and inside containers, where the entrypoint follows the
interpreter rather than being hardcoded to a shell.
Nothing is embedded. There is no bundled CPython, no Starlark, no WebAssembly. flow resolves a
real interpreter on the host, preferring a project’s virtualenv over bare system Python, so an
agent running Python inside a repo gets that repo’s dependencies. The search order is
FLOW_PYTHON_BIN, then $VIRTUAL_ENV, then the workspace’s .venv, then python3 on the path.
An override that does not resolve fails rather than quietly falling back.
Two details I liked:
Inline code runs from a temporary file, never python -c. That keeps user code, which may
have interpolated secrets, out of the process table; it produces tracebacks with real line
numbers; and it sidesteps shell quoting for multi-line scripts.
PYTHONUNBUFFERED is set by default, because flow pipes stdout to a log writer rather than a
terminal, and CPython block-buffers to a pipe. Without it a long run emits nothing until it
exits, which looks hung to anyone watching, human or otherwise.
The MCP side is the reason the rest exists. run_python gives an assistant a Python runtime with
the same workspace environment and secrets, the same captured logs, and the same attributable
history entry it already gets for shell. It is the difference between an agent writing a scratch
file and an agent doing work you can audit afterwards.