Server specs + registry
MCPServerSpec.stdio(...)
Launches an MCP server as a subprocess and talks to it over stdio.
from loomflow.mcp import MCPServerSpec
spec = MCPServerSpec.stdio(
name="git", # registry id
command="uvx", # the binary to launch
args=["mcp-server-git", "--repo", "/Users/me/code/myrepo"],
env={"GIT_AUTHOR_NAME": "agent"}, # optional extra env
)The name becomes the prefix for tool-name disambiguation
(git.commit etc.).
MCPServerSpec.http(...)
Talks to a hosted MCP server over Streamable HTTP transport.
spec = MCPServerSpec.http(
name="hosted",
url="https://example.com/mcp/",
headers={
"Authorization": "Bearer sk-...",
"X-Tenant-Id": "acme",
},
)The framework keeps a single connection open per server for the lifetime of the registry; reconnects on transient failures.
MCPRegistry. Combine multiple servers
from loomflow import Agent
from loomflow.mcp import MCPRegistry, MCPServerSpec
registry = MCPRegistry([
MCPServerSpec.stdio("git", "uvx", ["mcp-server-git", "--repo", "."]),
MCPServerSpec.stdio("fs", "uvx", ["mcp-server-filesystem", "--root", "."]),
MCPServerSpec.http("hosted", url="https://example.com/mcp/"),
])
agent = Agent("...", model="claude-opus-4-7", tools=registry)MCPRegistry implements the ToolHost protocol. Pass it directly
to Agent(tools=...).
Tool-name conflict resolution
If two servers expose a tool with the same simple name:
- Both get qualified,
git.statusandfs.status. - The agent sees both qualified names in the tool list.
- Either qualified or bare form is accepted at call time; the registry strips the prefix before forwarding.
If only one server has the name, it stays bare:
git.commit # only git ships commit
status, fs.status, git.status # both ship status; both qualifiedListing tools at runtime
tools = await registry.list_tools()
for t in tools:
print(t.name, t.description)list_tools is a coroutine. The registry queries each server’s
tools/list endpoint on first call and caches the result.
Fault isolation + unavailable
One dead server must not make every other server unusable. The registry connects all clients in parallel and isolates per-server failures at every stage:
- Connect failures are logged, not raised — the healthy servers’ tools register normally.
- Listing failures skip that server’s tools from the index; its
name is recorded in
registry.unavailableand the pull is retried on the nextrefresh(). - Call-time transport failures (broken pipe, server restart, connection drop) trigger a reconnect-and-retry — once, and only for errors that look like connection failures. A mid-execution error may mean the server already performed a side effect, so it is surfaced instead of silently re-run.
await registry.connect()
if registry.unavailable:
log.warning("degraded MCP servers: %s", registry.unavailable)listChanged → automatic refresh
MCP servers can announce that their tool list changed
(notifications/tools/list_changed) — a server that gains or loses
capabilities mid-run. The registry handles this automatically: the
notification flags the server stale, and the next registry operation
(list, call) re-pulls that server only and rebuilds the index.
No polling, no restart, no re-listing of healthy siblings.
To observe changes, subscribe to the diff stream:
async for event in registry.watch():
print(event.kind, event.tool, event.server) # "added" / "removed" / "updated"watch() registers eagerly (no events lost between subscribe and
iterate) and each subscriber has a bounded buffer — a slow consumer
drops events rather than blocking refreshes.
Lazy initialization
MCPRegistry connects to its servers lazily. The subprocess
launches and the HTTP connection opens on the first agent.run. This
keeps Agent(...) synchronous and avoids paying for unused servers
in tests.
Cleanup
For long-lived processes, hold the registry across runs and let Python’s garbage collector clean up. For short-lived processes (scripts), the framework’s atexit handler closes stdio subprocesses and HTTP connections cleanly.
Errors
| Error | Cause |
|---|---|
MCPError | Connect-phase failure: SDK not installed, bad spec, unsupported transport, unknown server / resource / prompt. |
ToolResult(ok=False) | A tool call failed (unknown tool, server-side isError, transport failure after the one reconnect retry). The error text reaches the model as the tool result; the run continues. |
Server failures never crash a run: a server that can’t list tools is
simply absent from the index (and named in unavailable), and a
failing tool call surfaces to the model as an error result it can
react to.
Multiple instances of the same server. Two stdio specs with the
same command but different name and different --repo (etc.) run
as two independent subprocesses. Useful for “git for repo A” and
“git for repo B” in the same agent.