MCP¶
Model Context Protocol (MCP) lets an Agent
use tools supplied by servers, such as filesystems, browsers, and databases.
The MCP plugin connects to a server and converts those capabilities into
ordinary Tools.
from lovia import Agent
from lovia.plugins.mcp import MCP, MCPServerStdio
agent = Agent(
name="assistant",
model="<model>",
plugins=[
MCP(MCPServerStdio(name="web", command="uvx", args=["mcp-server-fetch"]))
],
)
The mcp dependency is imported only when a connection is actually made,
so lovia.plugins.mcp is always safe to import; a missing package raises
UserError with the install hint at open time.
Servers¶
Two transports, both frozen keyword-only configs:
MCPServerStdio(command="uvx", args=["mcp-server-fetch"], env=None, name="web")
MCPServerStreamableHTTP(url="https://mcp.example.com/mcp", headers=None, name="api")
Shared options (on either config):
| Option | Default | Effect |
|---|---|---|
name |
None |
prefixes the server's tools: name="web" → web__fetch |
include_tools / exclude_tools |
None |
allowlist / denylist of raw tool names |
needs_approval |
False |
bool or predicate — gates every tool from this server through the normal approval flow |
retries / timeout / max_output_chars / result_renderer |
None |
per-tool policies, applied to each converted tool (Tools) |
auto_reconnect |
True |
reopen a dead connection and retry the call once |
close_after_run |
True |
close the connection when the run ends |
MCP(a, b, ...) takes any number of servers; prefixes keep their tool
names from colliding (a clash is reported at run start like any other
duplicate tool name).
Connection lifecycle¶
Per-run (default). Passing a server config means each run opens the
connection in plugin setup() and closes it at run end — stateless and
robust, at the cost of a subprocess/handshake per run.
Persistent. For many runs against one server, open a session yourself
and pass the live connection — MCPServerLike is satisfied by both configs
and connections:
server = MCPServerStdio(name="web", command="uvx", args=["mcp-server-fetch"])
async with server.session() as conn: # opened once
agent = Agent(name="assistant", model="<model>", plugins=[MCP(conn)])
await Runner.run(agent, "Fetch https://example.com and summarize it.")
await Runner.run(agent, "Now fetch the RFC index.") # same connection
The run never closes a connection you opened (close_after_run is False
on a live MCPConnection); its lifetime is the async with block. One
persistent connection serves sequential runs — concurrent runs over a
single MCP session are unsupported; give each concurrent worker its own.
How MCP tools behave¶
- Tool schemas are normalized into ordinary lovia
Tools (normalize_schemarepairs loose ones) — they validate, render, truncate, and appear in streaming events exactly like native tools. A customresult_rendereron the server receives the rawMCPToolResult; the default rendering isrender_mcp_content. - Failures split in two. A protocol-level tool failure (the server
answered with
isError) is rendered back to the model as[tool error] ...so it can self-correct — not raised. A transport/connection failure raisesMCPError(carryingtool_name), which ends the call like any tool exception. - Tool results may carry resources: text is inlined; images/audio become
size-stamped placeholders (never raw base64); resource links become
[resource link: uri]lines. - Scope is deliberately tools-only. MCP prompts, resource browsing, sampling, OAuth, and subscriptions are non-goals; the plugin does one thing.
Deferring large servers¶
A server with dozens of tools costs its full schema catalog in every prompt,
even when the model uses none of them. Set defer=True and the server's
tools stay out of the tool list; the agent instead gets one shared pair —
search_mcp_tools (keyword lookup returning name, description, and
parameters schema) and call_mcp_tool (invoke by exact name) — plus one
system-prompt line naming the deferred tools, so the model knows they exist
without paying for their schemas:
plugins=[MCP(
MCPServerStdio(command="...", name="github", defer=True), # 60 tools
MCPServerStdio(command="..."), # 3 tools, direct
)]
Deferral changes when schemas enter the context, not how tools run: a
deferred call is fully delegated, so the server's needs_approval,
retries, timeout, result_renderer, and max_output_chars apply
exactly as if the tool were exposed directly. Names must stay unambiguous —
a deferred name colliding with another deferred or exposed tool in the same
plugin raises UserError at setup (give servers a name= prefix).
Sharp edges¶
auto_reconnectmeans at-least-once. A call that died mid-flight is retried once on a fresh connection — a non-idempotent side effect (create_ticket) can happen twice. Setauto_reconnect=Falseon servers with mutating tools, and let the model see the error instead.- MCP tools run in parallel by default, like every tool. A server
whose tools mutate shared state has no barrier protection — wrap the
risky ones via
include_toolssplit across two server entries, or gate them withneeds_approval. needs_approvalis per server, not per tool. Splitting one server into twoMCPServerentries (same command, differentinclude_tools) is the idiom for "read tools free, write tools gated".- stdio servers inherit your process environment unless you pass
env=; there is nocwdoption — launch via a wrapper script when a server needs a working directory.
See also¶
- Plugins — the mechanism underneath
- Tools — everything a converted MCP tool inherits
- Tool approval — gating server tools
- Example:
24_mcp.py