pydantic-mcp-demo
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@pydantic-mcp-demoremember that my favorite color is blue"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
pydantic-mcp-demo
Serve a pydantic-ai toolset as an MCP server. One definition, two surfaces.
pip install git+https://github.com/thejens/pydantic-mcp-demo.gitWhat it looks like
Minimal — decorator style
Instantiate MCPServer, decorate tools with @server.tool(), call .run():
from dataclasses import dataclass
from pydantic_ai import RunContext
from pydantic_ai_mcp import MCPServer
@dataclass
class Deps:
api_key: str
server = MCPServer(deps=Deps(api_key="sk-…"), name="demo")
@server.tool()
async def whoami(ctx: RunContext[Deps]) -> str:
"""Return the current user."""
return f"Authenticated as {ctx.deps.api_key[:4]}…"
server.run(transport="stdio")One toolset → agent and MCP server
The whole point: define tools once, use them everywhere:
from pydantic_ai import Agent
from pydantic_ai.toolsets import FunctionToolset
from pydantic_ai_mcp import MCPServer
toolset: FunctionToolset[Deps] = FunctionToolset(id="demo")
@toolset.tool()
async def whoami(ctx: RunContext[Deps]) -> str: ...
# Agent path — unchanged
agent = Agent(model="openai:gpt-4o", toolsets=[toolset])
# MCP path — same toolset, deps called fresh per invocation
server = MCPServer(toolsets=[toolset], deps=make_deps)
server.run(transport="stdio")Session state
Persist data across MCP calls without touching FastMCP APIs. Make Deps a Pydantic model: plain fields are saved to the session store; Field(exclude=True) fields are ephemeral and rebuilt each call:
from pydantic import BaseModel, ConfigDict, Field
from pydantic_ai_mcp import MCPServer
class Deps(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
# persisted across calls ──────────────────────────────────
user_id: str | None = None
notes: dict[str, str] = Field(default_factory=dict)
# rebuilt each call (not stored) ──────────────────────────
http: httpx.AsyncClient | None = Field(default=None, exclude=True)
async def make_deps(deps: Deps) -> Deps:
if deps.user_id is None:
deps.user_id = await auth() # runs once per session, then cached
deps.http = httpx.AsyncClient() # fresh every call
return deps
server = MCPServer(
deps=make_deps,
session_deps=Deps, # enables load-before / save-after
name="my-server",
)
@server.tool()
def remember(ctx: RunContext[Deps], key: str, value: str) -> str:
ctx.deps.notes[key] = value # mutate — auto-saved after this returns
return f"Stored {key!r}"
server.run(transport="stdio")For distributed deployments, swap the in-memory store for Redis with one extra parameter:
from key_value.aio.stores.redis import RedisStore
server = MCPServer(
deps=make_deps,
session_deps=Deps,
session_state_store=RedisStore(url=os.environ["REDIS_URL"]),
)
server.run(transport="streamable-http", host="0.0.0.0", port=8000)Mount on FastAPI
MCPServer is a FastMCP subclass, so http_app() is already available:
from fastapi import FastAPI
from pydantic_ai_mcp import MCPServer
server = MCPServer(deps=make_deps, name="my-server")
@server.tool()
async def my_tool(ctx: RunContext[Deps]) -> str: ...
app = FastAPI()
app.mount("/mcp", server.http_app())Related MCP server: mcp-server
Installation
pip install git+https://github.com/thejens/pydantic-mcp-demo.git
# or
uv add git+https://github.com/thejens/pydantic-mcp-demo.gitRequires Python 3.12+ and pydantic-ai ≥ 1.107.
Usage
Tools
Register tools with @server.tool() (pydantic-ai RunContext convention), or pass pre-built FunctionToolset / AbstractToolset instances via toolsets=[…].
Prompts
Prompt functions follow the same convention — RunContext first, then named arguments. Register them with @server.prompt or pass via prompts=[…] at construction:
@server.prompt
async def expert_prompt(ctx: RunContext[Deps], domain: str) -> str:
"""System prompt that makes the model a domain expert."""
return f"You are an expert {domain} engineer."The function name becomes the prompt name; the docstring its description; remaining parameters become MCP prompt arguments.
Deps factory
For real applications, deps are created fresh per call. Pass any callable:
def make_deps() -> Deps:
return Deps(api_key=os.environ["API_KEY"])
async def make_deps_async() -> Deps:
secret = await vault.get("api_key")
return Deps(api_key=secret)
server = MCPServer(deps=make_deps, ...)Session deps
Make Deps a Pydantic model and pass it as session_deps. Before each tool call the serialisable fields are loaded from the store and the instance is passed to the factory. After the call the (possibly mutated) instance is saved back.
Plain fields — JSON-serialisable types, persisted to the session store
Field(exclude=True)fields — any type; omitted bymodel_dump()and restored to their default before the factory fills them in
All fields must have defaults; the model is instantiated with no arguments for new sessions.
This mirrors how pydantic-ai shares a mutable deps instance across all tool calls within a single agent run. session_deps extends that contract across MCP round-trips — the session is the unit of continuity instead of the run. The same toolset works unmodified with a plain pydantic-ai Agent (pass Deps() as deps).
Distributed sessions with Redis
By default session state is kept in memory. Pass session_state_store to swap the backend:
from key_value.aio.stores.redis import RedisStore
server = MCPServer(
deps=make_deps,
session_deps=Deps,
session_state_store=RedisStore(url=os.environ["REDIS_URL"]),
)RedisStore is from py-key-value-aio[redis]. Any backend implementing AsyncKeyValue works — DynamoDB, Firestore, PostgreSQL, Valkey, and more.
Multiple toolsets
server = MCPServer(
toolsets=[search_toolset, files_toolset, metrics_toolset],
deps=make_deps,
prompts=[analyst_prompt, researcher_prompt],
name="full-suite",
instructions="Company intelligence tools.",
)
server.run(transport="stdio")API
MCPServer extends FastMCP — all FastMCP methods (run(), run_async(), http_app(), add_middleware(), etc.) are inherited.
Constructor
MCPServer(
*,
toolsets: Sequence[AbstractToolset[DepsT]] = (),
deps: DepsT | Callable[[], DepsT] | Callable[[DepsT], DepsT] = None,
session_deps: type[DepsT] | None = None,
prompts: Sequence[Callable[..., Any]] | None = None,
name: str = "pydantic-ai-mcp",
bootstrap_deps: Any = None,
**fastmcp_kwargs,
)Parameter | Description |
|
|
| Deps instance, |
| Pydantic |
|
|
| Server name shown to MCP clients |
| Deps used only during startup tool discovery (safe as |
| Forwarded to |
Decorators
Decorator | Description |
| Register a pydantic-ai style tool (RunContext as first arg) |
| Register a pydantic-ai style prompt function |
Inherited from FastMCP
Method | Description |
| Start server (sync, blocks until exit) |
| Start server (async) |
| Return ASGI/Starlette app for mounting on FastAPI |
Examples
File | What it shows |
Minimal — | |
Per-call deps factory, env config, | |
Core case — one toolset in both an Agent and an MCP server | |
Session state — auth cached once, notes persisted; tools just mutate | |
Redis backing — same code as 04, one extra parameter |
How it works
MCPServer subclasses FastMCP. During ASGI lifespan startup it calls get_tools() on each registered toolset, then wraps each discovered tool in a thin FastMCP.Tool subclass that injects pydantic-ai deps and runs the tool through pydantic-ai's normal call path. On each MCP call:
If
session_depsis set: deserialise stored state → constructDeps→ pass to factoryValidate and coerce MCP args through pydantic-ai's schema validator
Build a
RunContextand calltoolset.call_tool()— the same path the agent run loop usesIf
session_depsis set: serialise the (possibly mutated)Depsinstance back to the store
Because state is passed by reference into the factory and into RunContext.deps, any mutations a tool makes to ctx.deps fields are automatically captured in step 4.
Status
Prototype exploring the pydantic-ai ↔ MCP bridge before potential upstreaming. The API may change as the session/deps contract evolves.
Feedback and issues welcome.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/thejens/pydantic-mcp-demo'
If you have feedback or need assistance with the MCP directory API, please join our Discord server