Skip to content

Add agent tutorial with MCP server example#59

Open
Adam-D-Lewis wants to merge 10 commits into
nebari-dev:mainfrom
Adam-D-Lewis:agent-tutorial-mcp
Open

Add agent tutorial with MCP server example#59
Adam-D-Lewis wants to merge 10 commits into
nebari-dev:mainfrom
Adam-D-Lewis:agent-tutorial-mcp

Conversation

@Adam-D-Lewis

Copy link
Copy Markdown
Member

Continues #58 by @pmeier. This branch is Philip's agent-tutorial base (commit 74ba32f) plus a new MCP-server step on top. I don't have push access to update #58 directly, so this is opened as a separate PR from a fork — happy to fold it into #58 instead if that's preferred.

Adds a Tutorials → Adding an agent page (docs/tutorials/agents.py, wired into mkdocs.yml)
showing how to bring your own agent to ravnar. It builds up in three steps:

  1. Full control — subclassing the Agent ABC and driving the AG-UI event stream directly
    (WhoAmIAgent).
  2. The pydantic-ai wrapper — adapting a pydantic_ai.Agent via PydanticAiAgentWrapper,
    declared entirely through ravnar config, with an in-process whoami tool that receives the
    injected User.
  3. Connecting an MCP server — pointing the agent at a stdio MCP server via MCPToolset, again
    purely 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 is
runnable and the shown output is real.

What's in this PR

  • docs/tutorials/agents.py — the tutorial; added to the nav in mkdocs.yml.
  • pyproject.toml / uv.lock — add the mcp extra to pydantic-ai-slim so MCPToolset (and the
    docs build) have FastMCP available.
  • A one-line DefaultAgent streaming tweak to preserve whitespace between tokens.

MCP example notes

  • Uses pydantic_ai.mcp.MCPToolset — the current API. MCPServerStdio is deprecated in
    pydantic-ai ≥1.x (removed in v2) and emits a DeprecationWarning, so it is avoided.
  • The demo server is a minimal mcp.server.fastmcp.FastMCP stdio server written to a temp file so
    the page is self-contained; the prose calls out that real servers are separate processes or remote
    URLs (just pass a URL to MCPToolset instead of a script path).
  • No changes to PydanticAiAgentWrapper were needed. It already iterates agent.toolsets in
    extract_capabilities(), and pydantic-ai connects MCP servers lazily during capability extraction
    and runs. The MCP tool is discovered (with its parameter schema) and invoked end-to-end with config
    only.

One decision to confirm

Adding mcp to the pydantic-ai extra pulls fastmcp-slim and its transitive deps (authlib,
joserfc, mcp, aiofile, …) into ravnar[pydantic-ai] / ravnar[all], even for users who never
use MCP. The alternative is a dedicated ravnar[mcp] extra added only to the docs/dev groups. Happy
to split it that way if you'd prefer to keep the base extra lean — let me know.

Verification

  • mkdocs build with fail_on_warning: true executes the whole tutorial — including spawning the
    MCP subprocess and calling its tool — and completes with no warnings.
  • Existing test suite: 247 passed.

pmeier and others added 2 commits June 23, 2026 12:31
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>
@Adam-D-Lewis Adam-D-Lewis marked this pull request as draft June 23, 2026 16:32
- docs/tutorials/agents.py: wrap long calls at the tutorial's 79-col limit
- src/_ravnar/agents.py: normalize regex literal to double quotes
@Adam-D-Lewis

Copy link
Copy Markdown
Member Author

Heads-up on the red checks — two of them are fork-PR limitations, not code issues:

  • pypi"Failed to obtain OIDC token: is the id-token: write permission missing?" GitHub withholds OIDC tokens from fork PRs, so trusted publishing can't run here.
  • build-and-push"Username and password required" on the Quay.io login; repo secrets aren't exposed to fork PRs either.

Both should be green when run from an in-repo branch / post-merge on main. The one real failure, format, is fixed in 8f4d7fe.

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
@Adam-D-Lewis

Copy link
Copy Markdown
Member Author

Note: API cross-references → code spans (5e76f9b)

While reviewing the rendered preview I noticed the mkdocstrings cross-references in the tutorial (e.g. [`Agent`][ravnar.agents.Agent]) were showing up as literal [text][id] brackets rather than links — see the preview.

Why: mkdocs-jupyter renders notebook markdown cells via nbconvert, which is outside MkDocs's Markdown pipeline — and that's where autorefs (the plugin that resolves [text][id]) runs. So the syntax is never processed in .py/.ipynb cells and leaks as raw text. It works fine on regular .md pages. This is a known, still-open upstream limitation: danielfrg/mkdocs-jupyter#236.

Fix: converted all 19 occurrences to plain code spans (`Agent`, `User`, etc.), which render correctly under nbconvert. Real parenthesized links (pydantic-ai, MCP, MCPToolset) are unchanged. The refs spanned the whole tutorial, so this touches the earlier sections too, not just the MCP one.

Trade-off: this drops the hyperlinks to the API reference. Keeping them would need a custom MkDocs hook that re-injects autorefs' internal markers into the notebook HTML — doable but fragile (depends on autorefs internals, and ~6 of the targets — User, Agent.run, get_capabilities, extract_capabilities, RunContext — have no reference-page anchor to link to anyway). Happy to pursue that separately if you'd prefer the links back.

Adam-D-Lewis and others added 6 commits June 23, 2026 12:32
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).
@Adam-D-Lewis Adam-D-Lewis marked this pull request as ready for review June 23, 2026 18:59
@pmeier pmeier mentioned this pull request Jun 24, 2026
Comment thread pyproject.toml
]
pydantic-ai = [
"pydantic-ai-slim[ag-ui]>=1.87,<2",
"pydantic-ai-slim[ag-ui,mcp]>=1.87,<2",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this into the docs group.

Comment thread src/_ravnar/docs.py
Comment on lines +19 to +23
# 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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"?

Comment thread docs/tutorials/agents.py
#
# 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/tutorials/agents.py


# %% [markdown]
# ## Full control with the Agent ABC

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section needs to explain more that ravnar agents are based on AG-UI

Comment thread docs/tutorials/agents.py
# Time to send it a message.

# %%
run_agent(client, "whoami", "Who am I?")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: make this

Suggested change
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

Comment thread docs/tutorials/agents.py
mcp_server = pathlib.Path(tempfile.mkdtemp()) / "calculator.py"
mcp_server.write_text(
'''
from mcp.server.fastmcp import FastMCP

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something here, but why not use the fastmcp package directly?

Comment thread docs/tutorials/agents.py
Comment on lines +212 to +213
# 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 —

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ImportStringWithParams is not public. Can you link to the import mechanism on the config docs instead?

Comment thread docs/tutorials/agents.py
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants