Skip to content

MCP: Tools you didn't write

Self-serve track. Not part of the live 90-minute block; do it any time after Foundations. Run with make mcp (server: make mcp-server).

Every tool so far has lived in your own file. Real agents pull most of their tools from elsewhere: services someone else built and deployed, that your agent connects to and calls.

In a hurry? These three steps are the whole challenge. Everything below is the why and the how.

  1. Run make mcp-server in one terminal (leave it on http://localhost:4321/mcp), then make mcp in another, and watch TripMate plan a trip but fail to book a hotel.
  2. Edit start/agent.py: do TODO 1 (import MCPToolset), TODO 2 (connect with MCPToolset("http://localhost:4321/mcp")), TODO 3 (add toolsets=[hotel_server] to the agent).
  3. Done when the hotel tool appears in “tools called this run” and the plan includes a real hotel from the server, with no change to the agent loop.

The standard for that is MCP, the Model Context Protocol. An MCP server exposes a set of tools over the wire, and your agent connects to it and calls them as if you’d written them yourself. This challenge wires one in.

You build this in two runs. First run TripMate with only its local tools and watch it fail to book a hotel. Then connect to an MCP server and watch the hotel tool appear next to the local ones, with no other change to the agent.

Forget hotels. Connecting to any MCP server over HTTP is the same three moves, whatever it serves:

from pydantic_ai.mcp import MCPToolset

# 1. connect to the server (the URL's http:// implies Streamable HTTP transport)
server = MCPToolset("http://example.com:8000/mcp")

# 2. register it as a toolset
agent = Agent(model, toolsets=[server])

# 3. use the agent; `async with` opens and closes the connection around the run
async with agent:
    result = await agent.run(prompt)

That toolsets=[server] is the whole integration: a remote tool sits next to the local ones and the model cannot tell which is which. Below you write these three moves for TripMate’s hotel server.

Open start/agent.py. It is the familiar shape: three local @agent.tool_plain functions and an Agent, with async with agent: already wrapping the run. You write three pieces against the find_hotel server on http://localhost:4321/mcp: import MCPToolset (TODO 1), connect (TODO 2), and register toolsets=[hotel_server] on the agent (TODO 3).

make mcp           # the agent
make mcp-server    # the server, in a second terminal, leave it running
  1. Run the agent on its own. No server needed yet:

    make mcp

    TripMate looks up the traveller, gets the weather, and prices the flight, but it has no hotel tool. It says it cannot book a hotel or skips it, and nothing hotel-shaped appears in “tools called this run”. That gap is what the MCP server fills.

  2. Start the MCP server in a second terminal. Leave it running:

    make mcp-server

    You should see the server announce itself on http://localhost:4321/mcp. This is a separate program, a process you did not write into your agent.

  3. Import the client and connect to the server (TODOs 1 and 2). Type the import for MCPToolset from pydantic_ai.mcp, then construct one pointing at the server’s URL (port 4321) and keep it in a variable. Each MCP server is a toolset: the agent lists and calls its tools over HTTP. The TODO comments in start/agent.py mark where each piece goes; write them yourself rather than uncommenting.

  4. Register the toolset on the agent (TODO 3). Add toolsets=[hotel_server] to the Agent(...) call so the server’s tool joins the local ones. That one argument is the whole integration.

    Run make mcp again. The hotel tool is now discovered from the server and shows up in “tools called this run” next to the local tools, and the trip plan includes a real hotel under £200. It has no [tool fired] line of its own, because it does not run in the agent process: it runs in the server, in the other terminal. To the model it’s just another tool.

  5. Verify what you’ve got. With no server the run plans a trip but cannot book a hotel. After the three TODOs and the server running, the hotel tool appears in “tools called this run” and the plan includes a hotel that came from the server, not the model. You should be able to say why the agent loop did not change to add a tool from another process. make solution-mcp (with the server running) runs the finished version if you want to compare.

  • “Could not reach the MCP server.” You did not start it. Run make mcp-server in a separate terminal first, and leave it running. The starter’s except tells you this.
  • Port 4321 in use. Something else is on it. Change port= in mcp_server.py and the URL in the client to match.
  • No [tool fired] find_hotel line. Expected: that log lives in the local tools. Confirm the call in the “tools called this run” list, or in the console trace, where MCP tool calls appear like any other span.
What is async with agent doing?

An MCP server is a live connection (here, HTTP). async with agent: opens connections to every server in the agent’s toolsets before the run and closes them after, which is more efficient than reconnecting per call. With no MCP servers it is a harmless no-op, which is why the starter already uses it: wiring the server in just gives it something to open.

The two paths share this one server

The MCP server does not know or care what language its clients are written in. The Python agent in this folder and the TypeScript agent in ../../vercel-ai-sdk connect to the same kind of server and call the same hotel tool: a tool published once and usable by any agent in any language.

These self-serve tracks sit outside the numbered path. If you came here from resilience, head back to the main tracks, foundations f1–f7 and patterns p1–p7, when you’re ready.