CLI
pip install theodosia registers a theodosia console script with three groups of
commands: serve, validate, and observe.
serve and doctor
Section titled “serve and doctor”theodosia serve module:attr # mount an Application or factory as an MCP servertheodosia serve module:attr --name x # override the MCP server nametheodosia doctor module:attr # static validation before mountingtheodosia doctor module:attr --runtime # also mount in-process and probe the wire shapeThe target is a module:attr string (the shape uvicorn and gunicorn use). The
attr may be a built Application or a callable returning one. --app-dir
prepends a directory to sys.path before importing; it is repeatable.
doctor exits 0 when there are no failures (warnings and info notes do not
block) and 1 otherwise, so it slots into CI. It is importable too:
from theodosia.doctor import run_checks.
render
Section titled “render”render draws the mounted state machine without starting a server or needing
Graphviz. It reads the graph statically from the same target serve takes.
theodosia render module:attr # terminal view of the graphtheodosia render module:attr --conditions # show transition conditions on edgestheodosia render module:attr --mermaid # emit Mermaid stateDiagram sourcetheodosia render module:attr --dot # emit Graphviz DOT sourceThe default terminal view marks the entry action (▶), terminals (■), and
self-loops (↺), and lists each action’s reachable next actions:
coffee-order · 5 action(s) · entry: take_order──────────────────────────────────────────────────── ▶ take_order → pay · add_modifier · cancel add_modifier ↺ → pay · add_modifier · cancel pay → fulfill ■ fulfill (terminal) ■ cancel (terminal)--mermaid emits source that GitHub renders inline, so a project can keep its
README diagram in sync with the actual graph instead of hand-drawing it. --dot
emits Graphviz DOT for the image-rendering path. Burr’s own Application.visualize()
covers the Graphviz image output; render adds the lightweight, paste-friendly
forms it does not.
Observability
Section titled “Observability”Every mounted server with a LocalTrackingClient writes a per-session JSONL log
under ~/.burr. These commands read it directly, so they work against any
session, including one running right now in another process.
theodosia sessions ls # recent sessions, most recent firsttheodosia sessions show <app-id> # full timeline: per-step state diff + timingtheodosia sessions tail [app-id] # live-tail a running sessiontheodosia watch [app-id] # alias for `sessions tail`theodosia logs [app-id] # compact one-line-per-step, greppabletheodosia logs --refusals --plain # only steps that errored, pipe-friendlyapp-id defaults to the most-recently-touched session and accepts a uuid prefix.
--burr-home points at a tracker root other than ~/.burr. --json on ls and
show emits machine output.
theodosia uiLaunches Burr’s web UI. Prefers a local apache-burr[start] install; otherwise
bootstraps via uvx. Permanent install: uv pip install 'theodosia[ui]'.
Shipping your own command
Section titled “Shipping your own command”build_cli returns a Typer app rebranded for a downstream package, with the
graph baked in so serve/doctor need no target.
from theodosia.cli import build_cli, runfrom my_fsm_mcp import build_application
cli = build_cli( "my-fsm-mcp", application=build_application, # Application, factory, or "module:attr" help="My graph as an MCP server.", server_name="my-fsm-mcp", # default MCP server name ui_extra="my-fsm-mcp[ui]", # named in the `ui` install hint burr_home="~/.my-fsm-mcp", # default tracker root for observability)
def main() -> int: return run(cli)[project.scripts]my-fsm-mcp = "my_fsm_mcp.cli:main"run(cli) wraps the Typer app with the same exit-code handling theodosia uses.
What rebrands: the command name (from the installed console script), the root
help text, the server name, the ui install hint, and the default tracker root.
What does not: the on-disk tracker format and the Burr UI, which are Burr’s. A
rebranded CLI is a thin shell over theodosia and Burr, not a fork of them.
application is optional. When omitted, the CLI behaves like the default
theodosia and requires a module:attr target on serve/doctor.
To let the rebranded server drive other MCP servers, pass upstream. It is
forwarded to mount, so action bodies can call call_upstream(server, tool, args):
cli = build_cli( "my-fsm-mcp", application=build_application, upstream={"fs": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]}},)See Driving other MCP servers for the action-body side.
Drive it from any MCP client
Section titled “Drive it from any MCP client”theodosia serve module:attr is a stdio MCP server, so any MCP client can launch
it. The same server is driven by each client through its own config mechanism.
Verified against a fresh pip install theodosia:
# fast-agent (Python REPL). Define the server in fastagent.config.yaml:# mcp: { servers: { vending: { command: theodosia, args: [serve, vending:build_application] } } }uvx fast-agent-mcp go --servers vending -m "Buy a soda: choose, pay 2 dollars, dispense."
# Claude Code (reads .mcp.json's mcpServers block):claude -p "Buy a soda: choose, pay 2 dollars, dispense." \ --mcp-config .mcp.json --allowedTools mcp__vending__step
# Gemini CLI (reads .gemini/settings.json, same mcpServers schema):GEMINI_CLI_TRUST_WORKSPACE=true gemini --yolo \ -p "Buy a soda: choose, pay 2 dollars, dispense."Claude and Gemini share the same mcpServers block (command + args), just in
different files (.mcp.json vs .gemini/settings.json). fast-agent uses its own
fastagent.config.yaml. Gemini’s headless mode needs
GEMINI_CLI_TRUST_WORKSPACE=true to clear its folder-trust gate.
Multiple graphs
Section titled “Multiple graphs”Two ways to serve more than one graph:
- Separate servers. Run one
theodosia serveper graph. Each is an independent MCP server with its own state, history, andtheodosia://resources. The URItheodosia://graphis identical on every server but is read against a specific server connection, so there is no collision; the client namespaces tools by server (mcp__coffee__stepvsmcp__triage__step). - One server,
mount_multi.mount_multi({"coffee": ..., "triage": ...})composes several graphs into one server. Tools becomecoffee_step/triage_step, and resources carry the namespace in the URI:theodosia://coffee/graph,theodosia://triage/next. A parenttheodosia://appsresource lists the mounted names. Seeexamples/multi_graph.py.
The URI overlap across separate servers is not a problem: a resource URI is
unique only within its server, and every read is addressed to one server, so two
servers both exposing theodosia://graph never collide. Only a single server holding
two graphs needs distinct URIs, which is what mount_multi provides.