What works through mount()
The integration boundary is Burr’s Application. Anything supported by
ApplicationBuilder passes through mount() without adapter changes, including
parallelism, persistence, telemetry, and library coexistence. Anything missing
from this table has not been exercised yet.
| Burr surface | Through mount() | Demo / evidence |
|---|---|---|
@action, with_transitions, with_state, with_entrypoint | Yes (core path) | every demo |
Condition.expr / .when / .default | Yes | coffee_order, chargen, incident_response |
with_tracker(LocalTrackingClient) | Yes; surfaced at theodosia://trace | every narrative demo |
with_state_persister(BaseStatePersister) | Yes | sqlite_persister |
with_typed_state(Pydantic) | Yes; JSON schema exported via theodosia://graph state_schema | typed_state_loan |
@pydantic_action decorator | Yes; subset-model machinery surfaces the action’s typed slice | pydantic_actions |
with_identifiers(partition_key=...) (multi-tenancy) | Yes; surfaces in theodosia://session.partition_key | partition_key_tenants |
with_parallel_executor(...) | Yes (default thread-pool); RayExecutor swap documented inline | burr_map_parallel |
MapStates / parallel sub-runs | Yes | burr_map_parallel |
| Streaming actions | Yes; emitted as MCP progress notifications | streaming_narrate |
Async actions (async def @action) | Yes | parallel_research, mellea_qiskit_migration |
| Sub-Application composition | Yes; theodosia://subruns indexes spawn_subapp calls | incident_response, subgraphs |
OpenTelemetry (OpenTelemetryBridge) | Yes | with_otel |
User-defined lifecycle hooks (PreRunStepHook / PostRunStepHook / etc.) | Yes; via ApplicationBuilder.with_hooks(...) | pipeline_hooks |
Async hooks + envelope hooks (PreRunStepHookAsync, PostApplicationCreateHook, PreRunExecuteCallHookAsync, etc.) | Yes; awaited around each action; envelope hooks wrap every execute boundary including MCP step | async_hooks |
@streaming_action.pydantic + streaming hooks (PreStartStreamHook, PostStreamItemHook, PostEndStreamHook) | Yes; chunks typed by stream_type, hooks fire when streaming actions are driven via MCP step (adapter uses app.astream_result) | streaming_hooks |
Span tracing hooks (PreStartSpanHook, PostEndSpanHook, DoLogAttributeHook) via the __tracer parameter | Yes; user-defined hook captures sub-span trees and attribute logs alongside OpenTelemetryBridge | custom_telemetry, with_otel |
ApplicationBuilder.initialize_from(persister, fork_from_app_id=..., fork_from_sequence_id=...) (builder-level state forking) | Yes; two Applications share an initial state via a persister, then walk independently with their own uids | state_forking, sqlite_persister |
AsyncBaseStatePersister + PersisterHookAsync | Yes; await persister.save(...) runs inline on the MCP step path (adapter drives astep, hooks fire async) | async_persister |
@trace decorator (auto-span any function called from an action) | Yes; nested call graph maps onto the span tree, inputs/outputs auto-logged as attributes | trace_decorator |
Burr’s prebuilt StateAndResultsFullLogger (zero-config JSONL audit log) | Yes; one JSONL row per action with post-step state + result + timing | full_logger |
FastMCP ctx.sample from inside an action body | Yes; theodosia.current_mcp_context() returns the FastMCP Context so actions can delegate LLM work to the connected agent’s model | caller_sample |
FastMCP ctx.elicit from inside an action body | Yes; action bodies can pop interactive user confirmation prompts mid-step for safety-rail gates | elicit_confirm |
Output schema on the step tool | Yes; clients see a typed response contract (discriminator error + per-shape fields) in the MCP tool listing | always-on |
| FastMCP middleware (timing, structured logging, rate limiting, custom) | Yes; mounted server is a regular FastMCP server, so server.add_middleware(...) after mount(...) works | with_middleware |
with_graph(Graph) / with_graphs(...) (reusable graph fragments) | Yes; same Graph object embedded in multiple Applications | subgraph_composition |
Class-based Action subclasses (escape from @action) | Yes; one class, configured instances | class_action |
| Hamilton driver inside an action body | Yes (no special integration) | hamilton_features |
app.run(halt_after=...) auto-routing | Burr-level only | MCP path always uses agent-chosen actions via step |
Lifting an existing FastMCP server
Section titled “Lifting an existing FastMCP server”If the starting point is a flat FastMCP server rather than a Burr graph,
burr_app_from_fastmcp(...) lifts the tools into an Application so they gain
transition enforcement and an audit trail. This path is less exercised than
building a graph directly; treat it as advanced.
from fastmcp import FastMCPfrom theodosia import ServingMode, ToolSpec, burr_app_from_fastmcp, mount
flat = FastMCP("legacy")
@flat.toolasync def create_order(item: str) -> dict: return {"order_id": "ORD-1", "item": item}
@flat.toolasync def pay(order_id: str, amount: float) -> dict: return {"paid": True, "receipt": "R-99"}
@flat.toolasync def fulfill(order_id: str) -> dict: return {"status": "fulfilled"}
app = await burr_app_from_fastmcp( flat, entrypoint="create_order", initial_state={"order_id": None, "paid": False}, tool_specs={ "create_order": ToolSpec(writes=["order_id"], merge_result=True), "pay": ToolSpec(reads=["order_id"], writes=["paid"], merge_result=True), "fulfill": ToolSpec(reads=["order_id", "paid"]), }, transitions=[("create_order", "pay"), ("pay", "fulfill")],)
server = mount(app, mode=ServingMode.STEP, name="lifted")You declare the state machine explicitly because parameter names do not tell you
which tools mutate shared state. What carries over without declaration: parameter
names, types, defaults, docstrings, and async/sync nature. ToolSpec also accepts
state_update (a callable from result to state mutations) and rename (change
the action’s name in the graph). tests/test_importing.py exercises every knob.