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:
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:
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 agentAnswering 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:
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.
MCPToolsetstartedorders_server.pyas a child process, asked which tools it had, and offeredorder_statusto 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:
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:
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=:
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:
from pydantic_ai.capabilities import MCP
MCP(local="orders_server.py")Toolset vs capability
| Way in | What you pass | When to use |
|---|---|---|
toolsets=[MCPToolset(...)] | A script path or a URL | Add 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.
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.Related
- Previous: Multi-agent delegation: agents that call agents
- Next: Harness Coder: a ready-made coding agent
- See also: Function tools: letting the model look things up
- Reference: MCP client
- 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 inpeek.
You understood something today that you didn't yesterday.