Skip to content

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.

pip install "lovia[mcp]"

The plugin supports the MCP Python SDK 1.18+ and 2.x.

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).

Web research with Parallel

Parallel Search MCP provides web search and page fetching over Streamable HTTP, with no account or API key required for its free tier. Anonymous search uses Fast mode; free usage has rate limits. Install lovia[mcp] as above and configure your model environment variables, then run this as a Python script:

from lovia import Agent, model_from_env
from lovia.plugins.mcp import MCP, MCPServerStreamableHTTP

server = MCPServerStreamableHTTP(
    name="parallel",
    url="https://search.parallel.ai/mcp",
    headers={"User-Agent": "lovia-parallel-mcp"},
)
agent = Agent(
    name="researcher",
    model=model_from_env(),
    instructions="Search for sources, fetch relevant pages, and cite their URLs.",
    plugins=[MCP(server)],
)
result = agent.run_sync(
    "Find the latest stable Python release and summarize its official release notes."
)
print(result.output)

The plugin exposes parallel__web_search and parallel__web_fetch. Search finds candidate sources; fetch reads selected URLs. Only agents given this plugin use the server. From async code, use await Runner.run(agent, ...) instead of run_sync().

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_schema repairs loose ones) — they validate, render, truncate, and appear in streaming events exactly like native tools. A custom result_renderer on the server receives the raw MCPToolResult; the default rendering is render_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 raises MCPError (carrying tool_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_reconnect means 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. Set auto_reconnect=False on 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_tools split across two server entries, or gate them with needs_approval.
  • needs_approval is per server, not per tool. Splitting one server into two MCPServer entries (same command, different include_tools) is the idiom for "read tools free, write tools gated".
  • stdio servers inherit your process environment unless you pass env=; there is no cwd option — launch via a wrapper script when a server needs a working directory.

See also