Skip to content

Driving other MCP servers

theodosia is normally the MCP server the agent talks to. With upstream, it also opens MCP client sessions to other servers (Kubernetes, Grafana, filesystem, and so on). A Burr action calls those servers’ tools from inside its Python body via call_upstream(server, tool, args).

from theodosia import call_upstream, mount
from burr.core import action
@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"]}},
)

Each value in the upstream map is anything fastmcp.Client accepts as a transport: a URL string, an mcp-config dict, a transport object, or a bare {"command", "args", "env", "cwd"} stdio spec. The bare stdio spec is mapped to an explicit StdioTransport so the upstream tool names are not namespaced the way an mcp-config dict would prefix them.

  • Single surface. The agent connects to one server (this one) and sees one tool (step). The upstream servers are never exposed to it. There is no separate “query the cluster” surface for a weak model to get absorbed in.
  • Every call is a ledger entry. The upstream call happens inside an action, so it advances state by construction. The graph cannot fall out of sync with what actually happened.
  • Any server. MCP is a standard protocol and fastmcp.Client speaks every transport (stdio, http, sse). theodosia does not need to know what the upstream server is.
  • No arg-guessing. The action author writes the call explicitly. There is no per-backend name or argument inference.

UpstreamManager lazily opens and caches one fastmcp.Client session per server, keyed by name, opened on first use and kept open for the manager’s lifetime. Calls are serialized per manager with an asyncio.Lock, since a single Client session is not guaranteed safe under concurrent calls and Burr steps are serialized per session anyway. mount() binds a manager around each step via bind_upstream and resets it afterward.

For tests or harness embeddings, bind_upstream accepts any object with an async call(server, tool, args) method, so you can bind an already-open session instead of the built-in manager.

examples/upstream_filesystem.py is a code-audit FSM that drives the official filesystem MCP server this way: list files, read a candidate, flag findings, report. The agent only ever calls step.