Pydantic AIPydantic AI 2.51 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
29 small wins to finish your pathNext lesson →

MCP servers: tools from another program

An MCP server is a separate program that offers tools over the Model Context Protocol, and Pydantic AI hands every one of them to your agent's model with one line.

Last updated: 28 Sep, 2026 · Pydantic AI 2.51

The tools in Function tools: letting the model look things up were functions in your own file. MCP, the Model Context Protocol, is a standard way for one program to offer tools to any AI application: an IDE, a chat client, or your own agent. You connect a server once and its tools sit next to your agent's own.

A small server with one tool

This server, written with the official MCP SDK, serves a single order_status tool over standard input and output when the file runs as a program:

python
from mcp.server import MCPServer

server = MCPServer("Orders")

ORDERS = {"A-1001": "shipped on 3 March", "A-1002": "still being packed"}


@server.tool()
def order_status(order_id: str) -> str:
    """Where an order is, by its id."""
    return ORDERS.get(order_id, "no such order")


if __name__ == "__main__":
    server.run()

@server.tool() registers the function, and its name, description and schema come from the function's name, docstring and type hints. The [mcp] extra installs both the SDK and the client Pydantic AI uses.

Attaching the server to an agent

MCPToolset(Path(...)) says the argument is a script to start. A URL string such as "http://localhost:8000/mcp" connects to a server that is already running over HTTP instead:

python
from pydantic_ai.mcp import MCPToolset

orders = MCPToolset(Path("orders_server.py"))   # a script to start
agent = Agent(shop_model, toolsets=[orders])     # its tools join the agent

Answering a ticket with the server's tool

The agent runs, the stand-in calls the first tool it is offered, and the call travels to the other program and back:

Example
from pathlib import Path

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset

from shop_model import shop_model

orders = MCPToolset(Path("orders_server.py"))
agent = Agent(shop_model, toolsets=[orders])
print(agent.run_sync("Where is A-1001?").output)

What the run produced

  • The reply came from the server. MCPToolset started orders_server.py as a child process, asked which tools it had, and offered order_status to the model.
  • The stand-in called it by position. It called the first tool it was offered, the same way it called your own tool earlier, so the answer is real, derived from the ticket.
  • The model could not tell the difference. A server's tool and one of yours reach the model in the same shape.

Reading what the model is offered

A stand-in that prints the tool definitions shows the name, description and schema all came from the server:

Example
from pydantic_ai import ModelResponse, TextPart
from pydantic_ai.models.function import FunctionModel


def peek(messages, info):
    for tool in info.function_tools:
        print(tool.name, "|", tool.description, "|", tool.parameters_json_schema["required"])
    return ModelResponse(parts=[TextPart("ok")])


Agent(FunctionModel(peek), toolsets=[orders]).run_sync("hi")

Keeping the server running across tickets

Each run_sync above started the server and stopped it again. async with agent keeps every MCP server running for the length of the block, which is what an app answering many tickets wants:

Exampletickets.py
import asyncio
from pathlib import Path

from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset

from shop_model import shop_model

agent = Agent(shop_model, toolsets=[MCPToolset(Path("orders_server.py"))])


async def main():
    async with agent:
        for ticket in ["Where is A-1001?", "Where is A-1002?"]:
            print((await agent.run(ticket)).output)


asyncio.run(main())

Attaching a server as a capability

A capability is a bundle of tools and behaviour an agent takes on through capabilities=[...], which Harness Coder: a ready-made coding agent covers next. The MCP capability is the usual way in. Given a URL it connects to that server; for a script, pass the toolset as local=:

Example
from pydantic_ai.capabilities import MCP

agent = Agent(shop_model, capabilities=[MCP(local=MCPToolset(Path("orders_server.py")))])
print(agent.run_sync("Where is A-1002?").output)

A bare path is refused

Pass a plain string to local= and the capability tells you what to do instead:

Example
from pydantic_ai.capabilities import MCP

MCP(local="orders_server.py")

Toolset vs capability

Way inWhat you passWhen to use
toolsets=[MCPToolset(...)]A script path or a URLAdd one or more servers to a single agent
capabilities=[MCP(...)]A URL, or local=MCPToolset(...)Bundle the server with other agent behaviour, or let a provider make the calls with native=True

Where you use MCP servers

  • Reusing a tool server another team already runs, without copying its code.
  • Giving several agents the same tools from one place.
  • Connecting a hosted server over HTTP that you do not run yourself.
One event loop per toolset
A toolset that run_sync has already used cannot be reused inside asyncio.run in the same program: the request for its tools waits and then fails with MCPError: Request 'tools/list' timed out. Pick one style per program, run_sync or async with.
Try it yourself
  • Add a second tool to orders_server.py, refund_status(order_id: str) -> str, and print what the model is offered.
  • Ask about an order the server does not know, such as A-9999, and read the reply.
  • Wrap the toolset as MCPToolset(Path("orders_server.py")).prefixed("orders") and see the tool's new name in peek.

You understood something today that you didn't yesterday.