Mount Burr state-machine Applications as MCP servers.
Project description
BurrMCP
BurrMCP gives an AI agent a stateful, auditable workflow it cannot step outside of. You define the workflow as a Burr state machine; BurrMCP serves it over MCP so the agent advances it one transition at a time.
Each Burr @action is reachable through one step(action, inputs) MCP tool. State lives on the server. The server enforces transitions: if the agent calls an action that isn't reachable from the current state, the response is a structured refusal listing the actions that are reachable. Every step is recorded to a replayable trace.
from burrmcp import mount
server = mount(application)
server.run()
Full documentation: msradam.github.io/burrmcp.
Install
uv pip install burrmcp # or: pip install burrmcp
From source:
git clone git@github.com:msradam/burrmcp.git
cd burrmcp
uv sync
Python 3.11 through 3.13. Optional extras: burrmcp[observability] (OpenTelemetry), burrmcp[ui] (Burr web UI), burrmcp[all].
Quickstart
Build a Burr graph, mount it, point an agent at it.
from burr.core import action, ApplicationBuilder, State
from burrmcp import mount, ServingMode
@action(reads=[], writes=["stage", "item", "qty"])
def take_order(state: State, item: str, qty: int = 1) -> State:
"""Place a new coffee order."""
return state.update(stage="ordered", item=item, qty=qty)
@action(reads=["stage"], writes=["stage", "paid_amount"])
def pay(state: State, amount: float) -> State:
"""Pay for the placed order."""
return state.update(stage="paid", paid_amount=amount)
@action(reads=["stage"], writes=["stage"])
def fulfill(state: State) -> State:
"""Mark the order fulfilled. Terminal."""
return state.update(stage="fulfilled")
app = (
ApplicationBuilder()
.with_actions(take_order=take_order, pay=pay, fulfill=fulfill)
.with_transitions(("take_order", "pay"), ("pay", "fulfill"))
.with_state(stage="new")
.with_entrypoint("take_order")
.build()
)
mount(app, mode=ServingMode.STEP, name="coffee").run()
A client that calls pay before take_order gets a structured refusal:
{
"error": "invalid_transition",
"valid_next_actions": ["take_order"],
"message": "action 'pay' is not reachable from current state. Valid actions now: ['take_order']."
}
The list of valid actions rides on the response, so a client without its own model of the graph recovers from a single error. The shipped examples/coffee_order.py extends this with an add_modifier loop and a cancel escape, demonstrating loop, branch, and escape on top of the linear path.
Why this shape
The four-tool surface (step, reset_session, fork_at, fork_from_past) is constant regardless of FSM complexity. The agent reads the action namespace from burr://graph, calls step(action=X), and the server refuses anything not reachable from the current state. The reachable action set is the graph, enforced at the protocol layer rather than asked of the model. Run burrmcp render <target> to print that graph in the terminal, or --mermaid for a diagram. See Architecture.
The integration boundary is Burr's Application: anything ApplicationBuilder supports (parallelism, persistence, typed state, hooks, telemetry, sub-applications) passes through mount() without adapter changes. See What works through mount().
Observability
Add a tracker to the builder and every step is recorded to JSONL and replayable in the Burr UI:
from burr.tracking.client import LocalTrackingClient
app = ApplicationBuilder().with_tracker(LocalTrackingClient(project="coffee-demo")) # ...
The agent reads its own audit trail through burr:// resources (graph, state, next, history, trace, session, subruns). From the terminal, the CLI reads the same tracker store:
burrmcp sessions ls # recent sessions
burrmcp sessions show <app-id> # full timeline: per-step state diff + timing
burrmcp watch [app-id] # live-tail a running session
burrmcp logs --refusals --plain # only steps that errored, pipe-friendly
Full surface (resources, CLI, UI, OpenTelemetry): Observability.
Driving other MCP servers
A Burr action can call tools on other MCP servers through BurrMCP. Pass mount(application, upstream={...}) a map of server name to a fastmcp.Client transport; inside an action body, call_upstream(server, tool, args) forwards to it.
from burrmcp import call_upstream, mount
@action(reads=[], writes=["pods"])
async def survey(state):
pods = await call_upstream("k8s", "list_pods", {"namespace": "prod"})
return state.update(pods=pods)
server = mount(build_application, upstream={"k8s": {"command": "npx", "args": ["-y", "kubernetes-mcp-server"]}})
The agent connects to one server (this one) and sees one tool (step). The upstream servers are reached from inside actions, so every upstream call advances state. See Driving other MCP servers.
Shipping your own command
A package that ships an MCP graph can expose its own command with build_cli, baking in the graph so serve needs no target:
# my_fsm_mcp/cli.py
from burrmcp.cli import build_cli, run
from my_fsm_mcp import build_application
cli = build_cli("my-fsm-mcp", application=build_application, help="My graph as an MCP server.")
def main() -> int:
return run(cli)
Then my-fsm-mcp serve, my-fsm-mcp doctor, and my-fsm-mcp sessions ls all carry your name. See CLI.
Examples
examples/ ships self-contained FSMs, each runnable as uv run python examples/<file>.py and wireable into a client via the shipped examples/*.example.json configs.
- Pure FSM:
coffee_order,triage,adventure,chargen,incident_response,local_shell,ml_training,subgraphs. - Typed state, hooks, persistence:
typed_state_loan,pydantic_actions,pipeline_hooks,async_hooks,streaming_hooks,sqlite_persister,async_persister,state_forking. - Shellout / real tooling:
unix_health,codebase_security,git_review. - LLM-in-the-graph (local model runtime):
granite_oncall,adaptive_crag,granite_guardian,mellea_qiskit_migration. - Caller-LLM / user-in-the-loop:
caller_sample,elicit_confirm. - Observability:
with_otel,custom_telemetry,trace_decorator,full_logger,with_middleware. - SKILL-to-FSM:
security_audit,differential_review,fp_check,webapp_testing. - Upstream:
upstream_filesystemdrives the official filesystem MCP server.
Some demos need a runtime (Ollama with a Granite model; bandit / detect-secrets on PATH; a git repo). Each refuses at action time with a clear message when its runtime is missing.
Validate before mounting
burrmcp doctor module:attr # static validation
burrmcp doctor module:attr --runtime # also probe the mounted wire shape
doctor exits nonzero on failures, so it slots into CI. Importable too: from burrmcp.doctor import run_checks.
Tests
uv run pytest
Six hundred and thirty-one tests, most in-process via FastMCP's in-memory client. tests/smoke/ holds opt-in real-Claude tests (deselected by default).
Acknowledgements
BurrMCP is glue between two libraries that do the hard parts:
- Apache Burr provides the state-machine
Application, the transition graph, and theLocalTrackingClient/ Burr UI replay. - FastMCP provides the MCP server, the resource and tool transforms, and the client used by the
upstreamfeature.
The SKILL demos under examples/skills/ are reproduced verbatim from Anthropic and Trail of Bits with attribution in each file.
BurrMCP is an independent project. It is not affiliated with, endorsed by, or sponsored by the Apache Software Foundation, DAGWorks, the Apache Burr project, or the FastMCP project. "Apache Burr" and "FastMCP" are the property of their respective owners and are referenced here only to describe what BurrMCP builds on.
License
Apache 2.0.
Notice
BurrMCP is independent open-source work by Adam Munawar Rahman and does not represent the views, positions, or technology roadmap 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 burrmcp-0.1.1.tar.gz.
File metadata
- Download URL: burrmcp-0.1.1.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2c3b0353d7d0d2a8ccdf94870c27a76bd97dfe0d82afb047896d2ebf06ee01c
|
|
| MD5 |
15910afe792c9a12334744810f3259d3
|
|
| BLAKE2b-256 |
095ef5389a5257850ce841a91f23b8fe33359b5dc2a67d8fe52b392e9ee2e3af
|
Provenance
The following attestation bundles were made for burrmcp-0.1.1.tar.gz:
Publisher:
release.yml on msradam/burrmcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
burrmcp-0.1.1.tar.gz -
Subject digest:
e2c3b0353d7d0d2a8ccdf94870c27a76bd97dfe0d82afb047896d2ebf06ee01c - Sigstore transparency entry: 1626905639
- Sigstore integration time:
-
Permalink:
msradam/burrmcp@4272f7944fe2a62a5a11873a62db7df93de883bd -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4272f7944fe2a62a5a11873a62db7df93de883bd -
Trigger Event:
push
-
Statement type:
File details
Details for the file burrmcp-0.1.1-py3-none-any.whl.
File metadata
- Download URL: burrmcp-0.1.1-py3-none-any.whl
- Upload date:
- Size: 68.0 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 |
5bc28ae57b0192dbd863396255c601fd7b220fe084eb19a53718da2e8295c401
|
|
| MD5 |
717dab5afd8fd5a5e6c7276178aef385
|
|
| BLAKE2b-256 |
ce406279d058faed57c9d9d75a6e697f54f24db464d50f711980047139a0f63d
|
Provenance
The following attestation bundles were made for burrmcp-0.1.1-py3-none-any.whl:
Publisher:
release.yml on msradam/burrmcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
burrmcp-0.1.1-py3-none-any.whl -
Subject digest:
5bc28ae57b0192dbd863396255c601fd7b220fe084eb19a53718da2e8295c401 - Sigstore transparency entry: 1626905693
- Sigstore integration time:
-
Permalink:
msradam/burrmcp@4272f7944fe2a62a5a11873a62db7df93de883bd -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/msradam
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4272f7944fe2a62a5a11873a62db7df93de883bd -
Trigger Event:
push
-
Statement type: