How to Connect a Pydantic AI Agent to MCP Servers with FastMCPToolset

Learn how to connect a Pydantic AI agent to local and remote MCP servers with FastMCPToolset. Wrap FastMCP instances, Python scripts, Streamable HTTP endpoints, and multi-server MCP configs with clean tool naming.

If you already have MCP servers running, the hard part is probably over.

What comes next is mostly wiring. You need your agent to discover the right tools, call them through the right transport, and stay readable once you have more than one server in the mix. That is the job FastMCPToolset handles inside Pydantic AI.

This is the bit that tends to surprise people: you do not need a giant integration layer just to let a Pydantic AI agent talk to MCP. The official FastMCP client can already connect to a Python script, a remote HTTP endpoint, a server object in the same process, or a full JSON MCP config. Pydantic AI simply wraps that client as a toolset.

If you want to build the server side first, start with our earlier guide on building a Python MCP server with FastMCP. This article picks up from there and focuses on the agent side.

What you’ll learn:

  • When FastMCPToolset is the better choice than the standard MCPServer client
  • How to connect a Pydantic AI agent to an in-process FastMCP server
  • How to connect to local STDIO servers and remote Streamable HTTP servers
  • How to load several MCP servers from one config and keep tool names sane
  • What usually breaks first when you wire real servers into an agent

Time required: 30-40 minutes
Difficulty level: Intermediate

Step 1: Know What FastMCPToolset Actually Buys You

Pydantic AI exposes two main MCP client paths:

  • FastMCPToolset, which uses the FastMCP client
  • MCPServer, which uses the lower-level MCP SDK client

The official docs position FastMCPToolset as the more convenient route when you want FastMCP’s extra client features, including transport flexibility and tool transformation. In practice, that means it is usually the path of least resistance if:

  • you already use FastMCP elsewhere
  • you want to load servers from a JSON MCP config
  • you want to connect to a mix of local and remote MCP servers without much ceremony

There is one real limitation to keep in mind. The Pydantic AI docs note that FastMCPToolset does not yet support elicitation or sampling. If your workflow depends on either of those MCP features, use the standard MCPServer client instead.

For everything else, FastMCPToolset is a very clean adapter. It lets your agent treat MCP tools like any other toolset in Pydantic AI, which means you can still filter, prefix, combine, or override them when the tool surface gets crowded.

Step 2: Install the Right Packages

The Pydantic AI docs recommend installing the FastMCP extra:

uv init pydantic-ai-mcp-demo
cd pydantic-ai-mcp-demo

uv add "pydantic-ai-slim[fastmcp]" fastmcp

If you prefer pip, this is the equivalent:

python -m venv .venv
source .venv/bin/activate
pip install "pydantic-ai-slim[fastmcp]" fastmcp

You will also need a model provider configured for Pydantic AI. For a simple OpenAI setup:

export OPENAI_API_KEY="your_api_key_here"

Nothing MCP-specific is required beyond that unless your servers themselves need credentials.

Step 3: The Fastest Path Is an In-Process FastMCP Server

If your agent and MCP server live in the same Python codebase, this is the nicest setup by far.

The FastMCPToolset docs show that you can pass a FastMCP server object directly into the toolset. That avoids the subprocess overhead of STDIO and the network hop of HTTP. It is especially handy for tests, internal tools, and single-repo agent systems.

import asyncio

from fastmcp import FastMCP
from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

release_server = FastMCP("release_ops")


@release_server.tool()
async def get_release_status(service: str) -> dict[str, str]:
    fake_db = {
        "checkout-api": {"status": "degraded", "owner": "payments-platform"},
        "search-api": {"status": "healthy", "owner": "discovery-team"},
    }
    return fake_db.get(
        service,
        {"status": "unknown", "owner": "unassigned"},
    )


@release_server.tool()
async def list_open_incidents() -> list[dict[str, str]]:
    return [
        {"service": "checkout-api", "severity": "high", "state": "investigating"},
        {"service": "billing-worker", "severity": "medium", "state": "monitoring"},
    ]


agent = Agent(
    "openai:gpt-5.2",
    system_prompt=(
        "You are a release assistant. "
        "Use MCP tools whenever the user asks about live service state."
    ),
    toolsets=[FastMCPToolset(release_server)],
)


async def main() -> None:
    result = await agent.run(
        "Which incidents are still open, and is checkout-api part of the problem?"
    )
    print(result.output)


asyncio.run(main())

This pattern is worth using longer than people usually do. If the server is local to the same app and you do not need process isolation, keep it simple.

Step 4: Connect to a Local Python or Node MCP Server over STDIO

Once the server is no longer in the same process, the next step is usually STDIO. That is the default local MCP setup for a lot of desktop tools, and FastMCPToolset supports it in two ways:

  • pass a script path directly
  • pass an explicit StdioTransport when you need more control

The short version looks like this:

from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

toolset = FastMCPToolset("calendar_server.py")

agent = Agent(
    "openai:gpt-5.2",
    toolsets=[toolset],
)

That works when your server can launch with default settings. Real projects usually need one more step, because STDIO servers often depend on environment variables, a working directory, or both.

The FastMCP transport docs call this out pretty clearly: STDIO servers do not inherit your shell environment by default. If the server needs API keys, database URLs, or feature flags, pass them explicitly.

import os

from fastmcp.client.transports import StdioTransport
from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

transport = StdioTransport(
    command="python",
    args=["calendar_server.py"],
    cwd="/Users/you/agent-servers/calendar",
    env={
        "GOOGLE_API_KEY": os.environ["GOOGLE_API_KEY"],
        "CALENDAR_ID": os.environ["CALENDAR_ID"],
    },
)

toolset = FastMCPToolset(transport)

agent = Agent(
    "openai:gpt-5.2",
    toolsets=[toolset],
)

Use the explicit transport whenever any of these are true:

  • the server needs secrets
  • the script lives outside your current working directory
  • you want to disable or tune session reuse
  • you want the launch config to be obvious when someone reads the code later

That last point matters more than it sounds. A lot of agent bugs are really launch bugs.

Step 5: Use Streamable HTTP for Remote or Production MCP Servers

For local development, STDIO is fine. For production, HTTP is usually the cleaner fit.

The FastMCP client docs recommend Streamable HTTP for production deployments, while keeping SSE mainly for backward compatibility. In other words, if you are publishing a remote MCP endpoint today, aim for Streamable HTTP unless you already have an SSE-based setup you must keep.

The simplest Pydantic AI wiring looks like this:

from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

toolset = FastMCPToolset("https://mcp.example.com/mcp")

agent = Agent(
    "openai:gpt-5.2",
    toolsets=[toolset],
)

If the server requires a bearer token or custom headers, build the FastMCP client explicitly and pass that into the toolset:

import os

from fastmcp import Client
from fastmcp.client.auth import BearerAuth
from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

client = Client(
    "https://mcp.example.com/mcp",
    auth=BearerAuth(os.environ["MCP_API_TOKEN"]),
)

agent = Agent(
    "openai:gpt-5.2",
    toolsets=[FastMCPToolset(client)],
)

This is also a good reminder that FastMCPToolset is not limited to servers built with FastMCP. The Pydantic docs explicitly say it can connect to local and remote MCP servers whether or not those servers were built with FastMCP Server.

That makes it a useful default when your stack is mixed.

Step 6: Connect One Agent to Several MCP Servers from a Single Config

This is the feature that makes FastMCPToolset really pleasant in day-to-day agent work.

Instead of manually building one toolset per server, you can hand it a JSON MCP config. The Pydantic AI docs show this directly, and the FastMCP transport docs note that tools from config-based multi-server clients are namespaced by server. That namespacing solves a lot of collisions before they happen.

Here is a realistic example:

from pydantic_ai import Agent
from pydantic_ai.toolsets.fastmcp import FastMCPToolset

mcp_config = {
    "mcpServers": {
        "docs": {
            "url": "https://docs.example.com/mcp",
            "transport": "http",
        },
        "repo": {
            "command": "python",
            "args": ["repo_server.py"],
            "cwd": "/Users/you/agent-servers/repo",
            "env": {
                "REPO_ROOT": "/Users/you/work/my-app",
            },
        },
        "time": {
            "command": "uvx",
            "args": ["mcp-run-python", "stdio"],
        },
    }
}

toolset = FastMCPToolset(mcp_config)

agent = Agent(
    "openai:gpt-5.2",
    system_prompt=(
        "You are an engineering assistant. "
        "Use docs tools for reference lookups, repo tools for local code context, "
        "and time tools when the user asks date-sensitive questions."
    ),
    toolsets=[toolset],
)

In a setup like this, the tool names exposed to the model will usually read more like:

  • docs_search_docs
  • repo_list_changed_files
  • time_now

That is much easier for the model to reason about than three unrelated servers all exposing a tool named search.

It is also much easier for you to debug when the agent picks the wrong tool, because the server origin is right there in the tool name.

Step 7: Clean Up the Tool Surface Before It Turns into a Mess

One of the nicest parts of this setup is that a FastMCPToolset is still just a Pydantic AI toolset. That means you can compose it the same way you would compose locally defined tools.

The Pydantic AI toolset docs highlight a few patterns that become especially useful once you have multiple MCP servers attached.

Filter out tools you do not want the model to see

Maybe the server exposes dangerous admin operations, or maybe it has ten tools and only three matter for this agent.

from pydantic_ai.toolsets.fastmcp import FastMCPToolset

toolset = FastMCPToolset(mcp_config).filtered(
    lambda ctx, tool_def: not tool_def.name.endswith(("_delete", "_drop", "_truncate"))
)

This is cleaner than teaching the model “never call the destructive tools” and hoping it listens.

Add prefixes when tool origins are still ambiguous

If you are combining several toolsets that were not loaded through a namespaced MCP config, prefixed() is still useful:

from pydantic_ai import Agent, CombinedToolset

docs_toolset = FastMCPToolset("https://docs.example.com/mcp").prefixed("docs")
calendar_toolset = FastMCPToolset("calendar_server.py").prefixed("calendar")

agent = Agent(
    "openai:gpt-5.2",
    toolsets=[CombinedToolset([docs_toolset, calendar_toolset])],
)

The toolset docs show the same general idea for ordinary toolsets, and the logic transfers perfectly to MCP-backed ones.

Require approval for tools that can mutate real systems

Pydantic AI also lets you wrap toolsets with approval logic. If your MCP server can write to production systems, publish content, or delete records, add approval rather than trusting the prompt.

I would treat this as a default for anything that touches money, infrastructure, or customer data.

Step 8: Use Dynamic Toolsets When Server Choice Depends on the Run

Some agent setups are tenant-specific. Some are environment-specific. Some swap servers depending on which repo or customer the current run belongs to.

Pydantic AI supports dynamic toolsets via @agent.toolset, which is a very natural fit here.

from dataclasses import dataclass

from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.fastmcp import FastMCPToolset


@dataclass
class TenantDeps:
    docs_url: str
    repo_server_path: str


agent = Agent(
    "openai:gpt-5.2",
    deps_type=TenantDeps,
)


@agent.toolset
def tenant_mcp_servers(ctx: RunContext[TenantDeps]):
    return FastMCPToolset(
        {
            "mcpServers": {
                "docs": {
                    "url": ctx.deps.docs_url,
                    "transport": "http",
                },
                "repo": {
                    "command": "python",
                    "args": [ctx.deps.repo_server_path],
                },
            }
        }
    )

That keeps the agent definition stable while letting each run decide which servers should be mounted.

If you are building an internal agent platform, this pattern is usually better than hardcoding one giant MCP config and hoping every environment looks the same.

Step 9: Common Failure Modes and How to Avoid Them

Most MCP integration bugs are not mysterious. They are usually one of these:

1. The STDIO server cannot see the secrets it needs

This one is common because local shell commands often inherit your environment, but FastMCP STDIO transports do not do that by default. Pass env explicitly.

2. You pointed the toolset at the wrong endpoint

If the server exposes Streamable HTTP, use the MCP endpoint, usually something like https://host.example.com/mcp. If it only exposes SSE, point to the SSE URL instead. Do not mix them.

3. The model sees too many similar tools

This is what config namespacing, prefixed(), and filtered() are for. Tool choice gets worse when every server exports a search, read, or query tool with only slightly different descriptions.

4. You chose FastMCPToolset but needed elicitation or sampling

This is the one architectural mismatch worth spotting early. If those features matter for your workflow, switch to MCPServer before you build too much around the FastMCP client path.

5. You split a same-process setup into STDIO or HTTP too early

If the agent and the server live in one Python service, direct in-memory wiring is often the better place to start. Keep the transport simple until you actually need the isolation.

Final Takeaway

If all your servers already speak MCP, connecting them to Pydantic AI is refreshingly straightforward.

FastMCPToolset is the adapter that makes the pieces click: one-process FastMCP servers, local scripts, remote HTTP endpoints, and multi-server configs can all be exposed to the same agent without changing the agent model itself. That is the real win. You can change transports and server layout without rewriting your agent architecture every time.

My rule of thumb is simple:

  • use direct FastMCP objects for same-process apps and tests
  • use explicit StdioTransport for local servers that need env or cwd
  • use Streamable HTTP for remote and production servers
  • use JSON MCP config once you have more than one server

That is usually enough to keep the system understandable.

References

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.