Multi-Agent: An Agent Can Be a Tool
A researcher that can only read. A booking clerk that can only spend. A manager that does nothing itself — it delegates. Wrap whole agents as tools of a supervisor and watch a nested agent loop plan a real trip end to end.
What you'll learn
- Split risky and safe capabilities across separate MCP servers
- Wrap specialist agents as tools with @manager.tool_plain
- Understand the multi-agent, multi-tool delegation pattern
The org chart
Click each node to see its system prompt and its tools. Notice the security shape: the researcher cannot spend money, the booker cannot search the web, and the manager can do nothing except talk to its staff.
Manager
system prompt
"You are a trip manager. You have staff - you do nothing yourself. 1) ask_researcher for weather + real hotels. 2) pick ONE. 3) ask_booker to book it."
tools (each one is an agent)
ask_researcher(question: str) -> str"Ask the researcher about weather and real hotels."
A whole agent, wrapped as a tool with @manager.tool_plain
ask_booker(instruction: str) -> str"Ask the booker to book a hotel. Include the hotel name and dates."
A whole agent, wrapped as a tool with @manager.tool_plain
Two servers, deliberately split
Risky capabilities and safe capabilities live on separate MCP servers, so no single agent can ever hold both:
research_mcp = FastMCP("Research") # READ-ONLY. safe.
@research_mcp.tool()
async def check_weather(location: str) -> str:
"""Get the current weather for a location."""
async with python_weather.Client(unit=python_weather.IMPERIAL) as c:
w = await c.get(location)
return f"{location}: {w.temperature} F. Hotels: Taj Kannur, Blue Bay Resort."
booking_mcp = FastMCP("Booking") # WRITES. spends money.
@booking_mcp.tool()
async def book_hotel(hotel_name: str, dates: str, confirm: bool) -> str:
"""Book a hotel. `confirm` must be True to actually book."""
return f"CONFIRMED: {hotel_name} for {dates}." if confirm else "Not booked."The trick: @manager.tool_plain
To the manager, ask_researcher looks like any other tool — a name, a docstring, one string argument. But inside it, researcher.run() spins a complete nested agent loop, tool calls and all, before returning a single string:
manager = Agent(model, system_prompt=(
"You are a trip manager. You have staff - you do nothing yourself. "
"1) ask_researcher for weather + real hotels. 2) pick ONE. 3) ask_booker to book it."))
@manager.tool_plain
async def ask_researcher(question: str) -> str:
"""Ask the researcher about weather and real hotels.""" # <- the MANAGER reads this
print(f"\n[MANAGER -> RESEARCHER] {question}")
r = await researcher.run(question) # a FULL agent loop, nested inside one tool call
return r.output
@manager.tool_plain
async def ask_booker(instruction: str) -> str:
"""Ask the booker to book a hotel. Include the hotel name and dates."""
print(f"\n[MANAGER -> BOOKER] {instruction}")
r = await booker.run(instruction)
return r.output
result = await manager.run("Plan a weekend trip to Kannur and book a resort.")
print("\nFINAL:", result.output)That symmetry — an agent is just a tool that happens to think — is the whole multi-agent pattern. Supervisors, crews, swarms: every framework's version of this is the same nesting you just read.