Lesson 5 · Phase 2 · Agents

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.

ask_researcher()
ask_booker()

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.