Add agent tutorial with MCP server example#59
Conversation
Extends the "Adding an agent" tutorial with a section that connects a pydantic-ai agent to an MCP server, plus small polish to the existing sections. - New "Connecting an MCP server" section: a minimal stdio FastMCP server exposing an `add` tool, wired to the agent via `MCPToolset` purely through ravnar config. The wrapper discovers the MCP tool and streams the call with no code changes. - Use the public `ravnar.*` namespace in tutorial imports (was `_ravnar.*`). - Clarify the `TestModel` tool-call output explanation. - Add the `mcp` extra to `pydantic-ai-slim` so `MCPToolset` and the docs build have FastMCP available; refresh uv.lock. Verified: `mkdocs build` executes the tutorial end-to-end (including the MCP subprocess) with fail_on_warning, and the existing 247 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/tutorials/agents.py: wrap long calls at the tutorial's 79-col limit - src/_ravnar/agents.py: normalize regex literal to double quotes
|
Heads-up on the red checks — two of them are fork-PR limitations, not code issues:
Both should be green when run from an in-repo branch / post-merge on |
mkdocstrings cross-references ([`Name`][dotted.path]) don't resolve inside mkdocs-jupyter markdown cells: nbconvert renders those cells outside MkDocs's Markdown pipeline where autorefs lives, so all 19 showed as literal [text][id] brackets. Convert them to plain code spans, which render cleanly under nbconvert. Real parenthesized links (pydantic-ai, MCP, MCPToolset) are unchanged. Upstream limitation: danielfrg/mkdocs-jupyter#236
Note: API cross-references → code spans (
|
Each executed-docs cell builds a real Ravnar app, whose lifespan configures logging at INFO — surfacing httpx "HTTP Request" INFO lines and repeated OpenTelemetry "already instrumented" warnings as cell output. Pin the docs Client's level to ERROR so rendered tutorials show only each example's own output. Production logging is unaffected.
Add a framing line to the intro and a summary bullet pointing at ravnar's other built-in agents (AgnoAgentWrapper, SSEAgent, and the Agent ABC), so readers see pydantic-ai is the featured example, not a requirement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MCP was a separate top-level section, which framed it as its own feature. Instead, show it as just another tool on the wrapper: after the in-process whoami example, attach an MCPToolset to the *same* agent alongside tools=[whoami]. Capabilities then list both whoami and add, and a run invokes both — reinforcing that the wrapper treats an MCP server's tools identically to in-process ones. Verified end-to-end (both tools listed and called).
The tutorial is entirely config-driven, so 'configuring' fits better than 'adding'. Updates the H1, the nav entry, and folds the now-removed MCP section's intro mention into the wrapper approach.
The tutorial said an MCP server's tools 'cannot access' ravnar's injected User. That overstates it: they don't receive it *automatically* the way an in-process tool does, but it can be forwarded explicitly (e.g. via MCPToolset's process_tool_call hook, which gets the RunContext). Soften the wording accordingly.
The wrapper section registered, inspected, and ran an agent twice: first with only the in-process whoami tool, then again with whoami plus an MCP server's tools. The second agent was a superset of the first, so the whoami-only pass was redundant. Present a single 'assistant' agent that has both kinds of tool from the start: introduce the in-process tool, introduce the MCP server, then one config -> one capabilities check (both tools) -> one run (both called).
| ] | ||
| pydantic-ai = [ | ||
| "pydantic-ai-slim[ag-ui]>=1.87,<2", | ||
| "pydantic-ai-slim[ag-ui,mcp]>=1.87,<2", |
| # Keep the executed-docs output focused on each example: silence ravnar's | ||
| # runtime logging (httpx request logs, repeated OTel "already instrumented" | ||
| # warnings from building a fresh app per cell) that nbconvert would | ||
| # otherwise capture as cell output. | ||
| config.observability.logging.level = l2sl.LogLevel("error") |
There was a problem hiding this comment.
I do want to keep the access logs. Wondering why the auth tutorial currently has them. Maybe OTEL has a flag like "instrument if not already done"?
| # | ||
| # 1. **Full control** — subclassing the `Agent` ABC directly. | ||
| # 2. **Using a wrapper** — adapting an existing [pydantic-ai](https://ai.pydantic.dev/) agent via | ||
| # `PydanticAiAgentWrapper`, and giving it the tools of an [MCP](https://modelcontextprotocol.io/) server. |
There was a problem hiding this comment.
In #60 I fixed the cross references. You can now use the standard mkdocstring syntax of [target][] or [display][target] for references. It doesn't yet work for external references, e.g. pydantic_ai.Agent, but please use it for internal references.
|
|
||
|
|
||
| # %% [markdown] | ||
| # ## Full control with the Agent ABC |
There was a problem hiding this comment.
This section needs to explain more that ravnar agents are based on AG-UI
| # Time to send it a message. | ||
|
|
||
| # %% | ||
| run_agent(client, "whoami", "Who am I?") |
There was a problem hiding this comment.
Nit: make this
| run_agent(client, "whoami", "Who am I?") | |
| run_agent(client, "whoami", "Hello!") |
to avoid giving the impression that the agent is actually responding to a question
| mcp_server = pathlib.Path(tempfile.mkdtemp()) / "calculator.py" | ||
| mcp_server.write_text( | ||
| ''' | ||
| from mcp.server.fastmcp import FastMCP |
There was a problem hiding this comment.
Maybe I'm missing something here, but why not use the fastmcp package directly?
| # Now we register the agent purely through configuration — no Python instantiation needed. ravnar's | ||
| # `ImportStringWithParams` mechanism resolves nested definitions recursively, so we declare the whole agent tree — |
There was a problem hiding this comment.
The ImportStringWithParams is not public. Can you link to the import mechanism on the config docs instead?
| run_agent(client, "assistant", "What is 2 + 3, and who am I?") | ||
|
|
||
| # %% [markdown] | ||
| # `TestModel` calls each tool with placeholder arguments, so `add` returns `0` rather than `5`, and `whoami` returns |
There was a problem hiding this comment.
This is bad for a tutorial. Can we use a tool that actually does the right thing? Maybe even something simple like greet without input arguments.
Adds a Tutorials → Adding an agent page (
docs/tutorials/agents.py, wired intomkdocs.yml)showing how to bring your own agent to ravnar. It builds up in three steps:
AgentABC and driving the AG-UI event stream directly(
WhoAmIAgent).pydantic_ai.AgentviaPydanticAiAgentWrapper,declared entirely through ravnar config, with an in-process
whoamitool that receives theinjected
User.MCPToolset, againpurely through config. The wrapper auto-discovers the server's tools and streams the calls.
Because the docs build executes these pages (
mkdocs-jupyter,execute: true), every example isrunnable and the shown output is real.
What's in this PR
docs/tutorials/agents.py— the tutorial; added to the nav inmkdocs.yml.pyproject.toml/uv.lock— add themcpextra topydantic-ai-slimsoMCPToolset(and thedocs build) have FastMCP available.
DefaultAgentstreaming tweak to preserve whitespace between tokens.MCP example notes
pydantic_ai.mcp.MCPToolset— the current API.MCPServerStdiois deprecated inpydantic-ai ≥1.x (removed in v2) and emits a
DeprecationWarning, so it is avoided.mcp.server.fastmcp.FastMCPstdio server written to a temp file sothe page is self-contained; the prose calls out that real servers are separate processes or remote
URLs (just pass a URL to
MCPToolsetinstead of a script path).PydanticAiAgentWrapperwere needed. It already iteratesagent.toolsetsinextract_capabilities(), and pydantic-ai connects MCP servers lazily during capability extractionand runs. The MCP tool is discovered (with its parameter schema) and invoked end-to-end with config
only.
One decision to confirm
Adding
mcpto thepydantic-aiextra pullsfastmcp-slimand its transitive deps (authlib,joserfc,mcp,aiofile, …) intoravnar[pydantic-ai]/ravnar[all], even for users who neveruse MCP. The alternative is a dedicated
ravnar[mcp]extra added only to the docs/dev groups. Happyto split it that way if you'd prefer to keep the base extra lean — let me know.
Verification
mkdocs buildwithfail_on_warning: trueexecutes the whole tutorial — including spawning theMCP subprocess and calling its tool — and completes with no warnings.