Mount Burr state-machine Applications as MCP servers.
Project description
Theodosia
Theodosia mounts a Burr Application as an MCP server. Every Burr action is reachable through a single step(action, inputs) tool; the server checks reachability against the graph before each action runs, refuses out-of-order calls with the legal next moves, and records every attempt.
Install
Python 3.11, 3.12, or 3.13 (Burr does not yet support 3.14). On a fresh Python 3.14 install you will see "no version that satisfies the requirement theodosia"; create a 3.11–3.13 venv first.
uv venv --python 3.13 # or: python3.13 -m venv .venv
uv pip install theodosia # or: pip install theodosia
Optional extras: theodosia[observability], theodosia[ui], theodosia[claude], theodosia[mellea], theodosia[all].
On a slim Docker image (python:3.13-slim, Alpine) the install pulls a psutil build that needs gcc and python3-dev. Either use the full python:3.13 image, or apt-get install -y gcc python3-dev before pip install.
Try it without an API key
theodosia primer
Walks the coffee-order FSM through Theodosia's step tool in-process and prints the timeline with state diffs and one structured refusal. No LLM, no network, same output every run.
Quickstart
from burr.core import ApplicationBuilder, State, action
from burr.core.action import Condition
from theodosia import mount
@action(reads=[], writes=["item", "stage"])
def take_order(state: State, item: str) -> State:
return state.update(item=item, stage="ordered")
@action(reads=["stage"], writes=["stage"])
def pay(state: State, amount: float) -> State:
return state.update(stage="paid")
def build_application():
is_ordered = Condition.expr("stage == 'ordered'")
return (
ApplicationBuilder()
.with_actions(take_order=take_order, pay=pay)
.with_transitions(("take_order", "pay", is_ordered))
.with_state(item=None, stage="empty")
.with_entrypoint("take_order")
.build()
)
if __name__ == "__main__":
mount(build_application, name="coffee").run()
Save as coffee.py and run python coffee.py to serve over stdio. Pass a factory (a callable returning a built Application) so each MCP session gets its own isolated state. Passing an already-built Application works too but shares state across sessions.
Exercise the server in-process with FastMCP's Client:
import asyncio
from fastmcp import Client
from coffee import build_application
async def main():
async with Client(mount(build_application, name="coffee")) as client:
r = await client.call_tool("step", {"action": "pay", "inputs": {"amount": 5.0}})
print(r.structured_content)
r = await client.call_tool("step", {"action": "take_order", "inputs": {"item": "mocha"}})
r = await client.call_tool("step", {"action": "pay", "inputs": {"amount": 5.0}})
print(r.structured_content)
asyncio.run(main())
A client that calls pay before take_order gets a structured refusal it can recover from:
{ "error": "invalid_transition", "valid_next_actions": ["take_order"] }
Primitives
mount(application, *, hooks=[...], middleware=[...], upstream=..., personas=...)wraps a BurrApplication(or factory) as a FastMCP server. Returns the server; call.run()or pass to FastMCP's in-memoryClient. Optional kwargs forward BurrLifecycleAdapter, FastMCPMiddleware, upstream MCP clients, andPERSONA.mdidentity layers.- The four MCP tools every mounted server exposes:
step(action, inputs),reset_session,fork_at(sequence_id),fork_from_past(app_id, sequence_id). The action namespace lives instep's argument schema; FSM complexity changes the schema, not the tool count. FastMCP'sResourcesAsToolstransform addslist_resourcesandread_resourcefor clients that lack nativeresources/read. - Structured refusals from
step:invalid_transition,unknown_action,validation_failed,action_timeout,action_error. Every refusal carriesvalid_next_actions.fork_at/fork_from_pastreturn their ownerrorcodes on the same wire shape. theodosia://resources for inspection:graph,state,next,history,subruns,trace,session.upstreamlets a Burr action body call tools on other MCP servers throughcall_upstream(server, tool, args). The agent driving Theodosia never sees those servers; it only seesstep.
Full reference (Persona, Assembly, hooks, middleware, tracker, drive_claude) lives in the docs.
Scope
Theodosia does not include an agent, a model, or a workflow engine. It mounts an existing Burr Application and gates an MCP client's access to it. The rails are only as tight as the graph you author.
Benchmarks (preliminary)
Theodosia's headline property is structural and holds regardless of any accuracy number: every step an agent takes, and every step it was refused, lands in a typed, replayable, hash-chained ledger. The benchmarks below are supporting evidence.
ITBench SRE (Claude Haiku 4.5, 35 scenarios). On IBM Research's ITBench SRE benchmark, scored by the benchmark's own judge, gating the agent's procedure with Theodosia improved root-cause entity F1 over the raw agent (0.561 vs 0.408, paired p<0.01). Same model, same prompt, same tools, same data; the only change is the gate. Full methodology, the FSM, and the seven validity controls: theodosia-bench.
Small local models (5 open-weight models, M4 Mac Mini, Ollama). Five models ≤8B were tested against three FSM compliance checks: navigate a multi-stage FSM to terminal, self-correct from a structured refusal using valid_next_actions, and receive a correct isError=True signal on a raised exception.
| Model | Parameters | FSM navigation | Self-correction | Error signal |
|---|---|---|---|---|
| Granite 4.1 8B | 8.8B | ✓ | ✓ | ✓ |
| Qwen3 8B | 8B | ✓ | ✓ | ✓ |
| Nemotron Nano 4B | 3.5B active | ✓ | ✓ | — |
| Llama 3.1 8B | 8B | ✗ | ✗ | ✗ |
| Phi4-mini | 3.8B | ✗ | ✗ | ✗ |
Nemotron Nano's "—" on error signal: the model declined to pass the deliberately invalid inputs, so the exception was never triggered. Llama 3.1 stalled before the terminal state and ignored tools entirely on the refusal test. Phi4-mini emitted no tool calls across all three tests. Parameter count does not predict protocol compliance; the models trained explicitly on structured tool-calling formats pass, the others do not. The tests/smoke/test_ollama.py suite is reproducible against any Ollama instance.
Command line
theodosia serve module:app # mount as MCP server (stdio, default)
theodosia serve module:app --transport http --port 8000 # serve over HTTP
theodosia render module:app # draw the state machine in the terminal
theodosia doctor module:app # statically validate the graph; exits nonzero for CI
theodosia sessions show <id> # full timeline: per-step state diff + timing
theodosia sessions diff <a> <b> # cross-session: action-path divergence + final-state diff
theodosia watch # live-tail a running session
theodosia logs --refusals # only the steps that were refused
theodosia status # tracker storage + recent activity snapshot
theodosia verify # recompute the ledger hash chain
theodosia primer # offline first-touch, no API key
theodosia ui # open the Burr UI
A downstream package can ship its own command (my-fsm serve, my-fsm doctor, ...) with build_cli.
Documentation
Full docs at msradam.github.io/theodosia.
| Page | Covers |
|---|---|
| Introduction | What Theodosia is and the primitives you reach for |
| Build your own agent | End-to-end: write a Burr graph, serve it, drive it with an MCP client |
| Authoring a graph | The Burr building blocks: @action, Condition, with_transitions |
| Architecture | mount(), the four-tool surface, the action-selection bridge |
| Refusals | The five refusal shapes and how the agent recovers |
| Sessions | Per-session isolation, fork_at, fork_from_past, partition keys |
| Security model | The trust boundary, what the ledger does and does not prove |
| Observability | The theodosia:// resources, CLI, Burr UI, OpenTelemetry |
| Personas | PERSONA.md identity layer mounted as MCP prompts |
| Upstream | Calling other MCP servers from action bodies |
| Compatibility | What works through mount() (typed state, persistence, hooks, parallelism, telemetry) |
| CLI | serve / doctor / render / sessions / watch / logs / status / report / primer, and build_cli |
| Deployment recipes | Copy-pasteable configs: Claude Code, Cursor, mcphost, fast-agent, HTTP, SSE, Lambda, Kubernetes |
| ITBench study | Controlled raw-vs-gated study on IBM Research's ITBench SRE, scored by the benchmark's own judge |
| Case study | Kimi K2.6 on Grafana o11y-bench: free-ranging vs gated |
| Research foundation | The published evidence the design rests on |
Examples and tests
examples/ ships self-contained FSMs covering pure-FSM, typed state, hooks, persistence, real shellouts, LLM-in-the-graph, SKILL-to-FSM, upstream, and multi-graph. Each runs with uv run python examples/<file>.py. The test suite runs with uv run pytest.
Related projects
- theodosia-rehearsal: drop-in demos that exercise the framework end to end.
- Philip: lifts declarative artifacts (Ansible YAML, Mermaid
stateDiagram-v2, Excalidraw) into BurrApplicationinstances thattheodosia.mount()serves directly.
Acknowledgements
Theodosia is glue between two libraries that do the hard parts: Apache Burr provides the state-machine Application, the transition graph, and the tracking UI; FastMCP provides the MCP server, the transforms, and the client behind upstream. The SKILL demos under examples/skills/ are reproduced verbatim from Anthropic and Trail of Bits with attribution.
Theodosia is an independent project, not affiliated with or endorsed by the Apache Software Foundation, DAGWorks, the Apache Burr project, or FastMCP.
License
Apache 2.0. Theodosia is independent open-source work by Adam Munawar Rahman and does not represent the views of IBM Corporation or any other employer. See NOTICE.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file theodosia-0.6.0.tar.gz.
File metadata
- Download URL: theodosia-0.6.0.tar.gz
- Upload date:
- Size: 5.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c62ce6eb015329ee45d9a8ec0f9e44226960d53679db363bcefadf8337af28d3
|
|
| MD5 |
083d546ef57be7e2797e43ad00bf6ed4
|
|
| BLAKE2b-256 |
1b66cba0616de5843a3309f703e936cd8a2ed680d1b63a2c8eebd45e1e3f8bd8
|
Provenance
The following attestation bundles were made for theodosia-0.6.0.tar.gz:
Publisher:
release.yml on msradam/theodosia
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theodosia-0.6.0.tar.gz -
Subject digest:
c62ce6eb015329ee45d9a8ec0f9e44226960d53679db363bcefadf8337af28d3 - Sigstore transparency entry: 1919781431
- Sigstore integration time:
-
Permalink:
msradam/theodosia@dfb62d44f2794cfb048f601d90de6586b4ca8f04 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dfb62d44f2794cfb048f601d90de6586b4ca8f04 -
Trigger Event:
push
-
Statement type:
File details
Details for the file theodosia-0.6.0-py3-none-any.whl.
File metadata
- Download URL: theodosia-0.6.0-py3-none-any.whl
- Upload date:
- Size: 138.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26bd37e0e769f982370897b896054dca02a5e287ab385f5949375370abf87d99
|
|
| MD5 |
ddf69e0c668635ac3c84e64d9741b57e
|
|
| BLAKE2b-256 |
07dda293ed2c67a860e309466e612afc0b21ab409a210a8221fd1156ea4533c3
|
Provenance
The following attestation bundles were made for theodosia-0.6.0-py3-none-any.whl:
Publisher:
release.yml on msradam/theodosia
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
theodosia-0.6.0-py3-none-any.whl -
Subject digest:
26bd37e0e769f982370897b896054dca02a5e287ab385f5949375370abf87d99 - Sigstore transparency entry: 1919781769
- Sigstore integration time:
-
Permalink:
msradam/theodosia@dfb62d44f2794cfb048f601d90de6586b4ca8f04 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dfb62d44f2794cfb048f601d90de6586b4ca8f04 -
Trigger Event:
push
-
Statement type: