
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
Pydantic AI connects to MCP servers in two layers. The MCP capability is the recommended entry point: give it a URL and the agent gets the server's tools, with an option to let the model provider call the server natively. MCPToolset is the lower-level piece underneath, for when you need to manage connections yourself or pass advanced transport settings.
This tutorial uses both. You will connect a server with the capability, test tools in memory with MCPToolset, load several servers from a JSON file, and set per-user credentials. Code was checked against the Pydantic AI MCP client docs and MCP capability docs on 2 November 2026, when pydantic-ai 2.64.0 was current on PyPI.
Key Takeaways
- Install
pydantic-ai, orpydantic-ai-slim[mcp], which pulls in FastMCP 4 as the MCP client. - Start with
MCP("https://...")in an agent'scapabilities; drop toMCPToolsetwhen you need lifecycle or transport control. MCPToolsetaccepts a URL, a script path, a FastMCP transport, afastmcp.Client, or an in-process FastMCP server, which makes unit tests easy.- Use
tool_prefixto avoid name clashes between servers, and pass a function forauthorization_tokenwhen each user has their own credentials. - FastMCP 4's modern protocol mode does not support server-initiated sampling or elicitation; you must use its legacy mode for those.
Prerequisites
- Python 3.11 or later (
pydantic-ai2.64.0 on PyPI declares>=3.11). - An API key for your model provider.
- An MCP server to connect to. The steps below build one with FastMCP.
Install
pip install "pydantic-ai-slim[mcp]"
Or with uv:
uv add "pydantic-ai-slim[mcp]"
The docs note this installs FastMCP 4, and that FastMCP 4 is "built on httpx2 rather than legacy HTTPX." If you pass custom auth or http_client objects, build them with httpx2.
Build a test server
The client docs still show a server that imports from mcp.server.fastmcp import FastMCP. That path no longer exists in the official MCP Python SDK v2, so use the standalone fastmcp package instead, as the FastMCP quickstart does:
# notes_server.py
from fastmcp import FastMCP
mcp = FastMCP("Notes")
NOTES = {"welcome": "Hello from the notes server."}
@mcp.tool
def list_notes() -> list[str]:
"""List the titles of all saved notes."""
return sorted(NOTES)
@mcp.tool
def read_note(title: str) -> str:
"""Read one note by its exact title."""
return NOTES.get(title, f"No note titled {title!r}.")
if __name__ == "__main__":
mcp.run(transport="http", port=8000)
Run python notes_server.py. It serves http://localhost:8000/mcp. Our FastMCP tutorial goes further.
Minimal working example: the MCP capability
import asyncio
from pydantic_ai import Agent
from pydantic_ai.capabilities import MCP
agent = Agent(
'openai:gpt-6.1-sol',
capabilities=[MCP('http://localhost:8000/mcp', id='notes')],
)
async def main():
result = await agent.run('What does the welcome note say?')
print(result.output)
asyncio.run(main())
The model string comes from the docs; use any model Pydantic AI supports. By default the capability "runs the MCP server locally," meaning your process makes the MCP calls, which keeps "credentials, hooks, and tracing under your control."
Without id=, the capability uses the URL's hostname as its ID. Two servers on the same host would collide, so give each an explicit id.
Native MCP
Some model providers can call MCP servers themselves. MCP(url, native=True) prefers that and falls back to a local connection. MCP(url, native=True, local=False) is native-only. With native calls the provider, not your code, connects to the server, so the server must be one the provider can reach. Pydantic AI's MCP Server Tool docs list which providers support it.
MCPToolset for more control
Use MCPToolset directly when you want to share one connection across agents or set transport options. Register it with toolsets=:
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset
toolset = MCPToolset('http://localhost:8000/mcp')
agent = Agent('openai:gpt-6.1-sol', toolsets=[toolset])
What MCPToolset accepts as its first argument:
| Input | Transport |
|---|---|
| URL string | Streamable HTTP (SSE if the path ends in /sse) |
| Path to a Python or Node.js script | stdio |
StdioTransport, StreamableHttpTransport, SSETransport | As configured |
A prebuilt fastmcp.Client | As configured, including OAuth |
| An in-process FastMCP server | In memory, no network |
For stdio with full control over the command, the docs use:
from fastmcp.client.transports import StdioTransport
from pydantic_ai.mcp import MCPToolset
toolset = MCPToolset(StdioTransport(command='python', args=['mcp_server.py']))
Lifecycle
You can wrap runs in async with agent: to open all MCP connections (and start stdio subprocesses) once. If you do not, the docs say "the toolset will be opened and closed automatically as needed." For a busy app, open once and reuse.
Test tools in memory
Pass a FastMCP server object straight to MCPToolset. There is no subprocess and no port, which makes it ideal for unit tests:
from fastmcp import FastMCP
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset
fastmcp_server = FastMCP('my_server')
@fastmcp_server.tool()
async def add(a: int, b: int) -> int:
return a + b
toolset = MCPToolset(fastmcp_server)
agent = Agent('openai:gpt-6.1-sol', toolsets=[toolset])
Add a tool
Add a decorated function to the server. The agent sees it on the next connection. Write the docstring as a tool description, because that is what the model reads.
Several servers from one config file
load_mcp_toolsets() reads the same mcpServers JSON shape that desktop clients use:
{
"mcpServers": {
"notes": { "url": "http://localhost:8000/mcp" },
"files": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
}
}
}
from pydantic_ai import Agent
from pydantic_ai.mcp import load_mcp_toolsets
toolsets = load_mcp_toolsets('mcp_config.json')
agent = Agent('openai:gpt-6.1-sol', toolsets=toolsets)
The loader expands ${VAR} and ${VAR:-default} so keys stay in your environment. Two warnings from the docs: unknown keys are ignored, so "disabled": true "does not skip a server," and "type" does not choose the transport. Only a /sse suffix makes a URL use SSE. Our MCP config generator produces valid mcpServers blocks.
Name clashes and per-user tokens
Two servers with a search tool will clash. The capability's tool_prefix fixes that:
agent = Agent(
'openai:gpt-6.1-sol',
capabilities=[
MCP('https://docs.example.com/mcp', id='docs', tool_prefix=True),
MCP('https://tickets.example.com/mcp', tool_prefix='tickets'),
],
)
The docs say prefixing by default "is planned for v3." The capability also takes an allowed_tools list, which matches the server's own tool names even when a prefix is set.
When each user has their own token, pass a function instead of a fixed string. It runs at the start of each run with the run context:
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import MCP
@dataclass
class Deps:
mcp_token: str
def mcp_token(ctx: RunContext[Deps]) -> str:
return f'Bearer {ctx.deps.mcp_token}'
agent = Agent(
'openai:gpt-6.1-sol',
deps_type=Deps,
capabilities=[MCP('https://mcp.example.com/api', authorization_token=mcp_token)],
)
The docs say each run then connects "in an MCP session of its own, so concurrent runs never share a session between users." A shared MCPToolset, by contrast, "connects to the server as a single identity."
Test with the MCP Inspector
Check the server without any model:
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http --method tools/list
For watching the agent's tool calls, the docs suggest Logfire. Our MCP Inspector lesson covers scripted checks.
Pitfalls
- v1 server imports.
from mcp.server.fastmcp import FastMCPfails on MCP SDK v2. Usefastmcpormcp.server.MCPServer. - Sampling or elicitation silently off. FastMCP 4's modern mode does not support server-initiated sampling or elicitation.
MCPToolsetwarns when you configure them. httpxvshttpx2. Custom auth or HTTP clients must match FastMCP 4'shttpx2.- SSE by accident. A URL ending in
/sseswitches to the deprecated SSE transport. See SSE vs Streamable HTTP.
Next step
The free MCP course explains sampling, elicitation and transports in plain English. If you want a typed, well-tested MCP server for your Pydantic AI agents, see our MCP development service.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Should I use the MCP capability or MCPToolset?
The docs recommend the MCP capability "for most use cases." Use MCPToolset when you need to manage the client lifecycle, share one server across several agents, or pass advanced transport or client settings.
Can Pydantic AI also build MCP servers?
Yes. The MCP docs have a server section, and Pydantic AI supports MCP sampling as both client and server. For a plain tool server, FastMCP is the simplest route.
Does Pydantic AI support MCP background tasks?
Yes, with caveats. The docs describe task-augmented execution for FastMCP 3 servers and the newer Tasks extension for FastMCP 4, which needs the pydantic-ai-slim[mcp-tasks] extra to select explicitly.
Why is my "disabled" server still loading?
load_mcp_toolsets() ignores keys it does not recognize. The docs say disabled does not skip a server, so remove the entry from the file instead.











