Topic 3: Agentic patterns
An agentic pattern is an arrangement of models, hosts, and servers: who decides what, who talks to which server, and whose context holds what. Three are common enough to name.
Single agent with many servers
The simplest arrangement: one Host, one model, several servers. The host namespaces each server's tools (notes__search_notes, calendar__check_availability) and the model chooses among all of them. The canonical Host already supports this, since it takes a dict of clients.
"""Single agent, many servers: one Host, two stdio servers, one model choosing among all tools."""
from __future__ import annotations
import anyio
from mcp import Client
from examples.m10_common import KeywordAgent, stdio_params
from notes_assistant.host import Host
QUESTIONS = [
"What do my notes say about the half-life of caffeine?",
"Is the sleep lab free the week of 19 October?",
"How many participants will the nap study have, and is the sleep lab free the week of 19 October?",
]
async def main() -> None:
async with (
Client(stdio_params("-m notes_assistant.server")) as notes,
Client(stdio_params("examples/m10_calendar_server.py")) as calendar,
):
host = Host({"notes": notes, "calendar": calendar}, chat_fn=KeywordAgent())
specs = await host.load_tools()
print("the model sees:", [s["function"]["name"] for s in specs])
for question in QUESTIONS:
answer = await host.ask(question)
print(f"\nQ: {question}")
print(" calls:", [f"{c.name}({', '.join(f'{k}={v!r}' for k, v in c.arguments.items())})" for c in answer.calls])
print(" A:", answer.text.replace("\n", " ")[:110])
if __name__ == "__main__":
anyio.run(main)Code explained
- In simple words: one assistant with two phone lines, one to the notes and one to the calendar.
- What happens:
- Two stdio subprocesses start, one per server, each with its own
Client. Host({"notes": notes, "calendar": calendar}, ...)gives the model both tool sets with prefixes, plus the host's ownread_resource.- Three questions go through the same host: one for each server, and one compound question that needs both.
- Two stdio subprocesses start, one per server, each with its own
- Comes out:text
the model sees: ['notes__search_notes', 'notes__create_note', 'calendar__check_availability', 'calendar__list_bookings', 'read_resource'] Q: What do my notes say about the half-life of caffeine? calls: ["notes__search_notes(query='half life caffeine')", "read_resource(uri='notes://coffee-and-focus')"] A: Half-life is about 5 hours, so a 4 pm coffee still leaves half the dose at 9 pm. [coffee-and-focus] Q: Is the sleep lab free the week of 19 October? calls: ["calendar__check_availability(equipment='sleep-lab', week_of='2026-10-19')"] A: { "equipment": "sleep-lab", "week_of": "2026-10-19", "free": true, "booked_by": null } Q: How many participants will the nap study have, and is the sleep lab free the week of 19 October? calls: ["calendar__check_availability(equipment='sleep-lab', week_of='2026-10-19')"] A: { "equipment": "sleep-lab", "week_of": "2026-10-19", "free": true, "booked_by": null }The first two questions are routed correctly with no help: the calendar question's words ("sleep", "lab", "free", "week") overlap the calendar tool's description far more than the notes tool's. The third shows the pattern's limit with a weak model: the compound question gets one tool call and a half answer. The calendar part won on word overlap; the nap study part was silently dropped. A strong real model usually makes both calls, but "usually" is the problem: as questions and tool lists grow, one context has to hold every tool definition, every intermediate result, and the whole plan.