Two things, both discovered by instrumenting the failure rather than trusting the message it printed. The MCP session never failed on protocol. macOS resolves any `.local` name through mDNS first and takes a flat ~5.0s doing it, which sits exactly on httpx's 5s default connect timeout; fastmcp reported the resulting ConnectTimeout only as "Failed to initialize server session". Earlier tests used `curl --resolve`, which bypasses DNS entirely and made it look like DNS had been eliminated. Settings.http_timeout (60s) now covers the shared client, the client fastmcp builds, and MCPToolset's init_timeout. list-products, get-product and create-product now enumerate over the real gateway. Inference used a static "unused" api_key against a gateway whose openid-connect plugin requires audience ["llm-proxy"]. The model call now carries a token exchanged for llm-proxy-audience, so it is attributable to the same (agent, end user) pair as every tool call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.9 KiB
Catalogue — product management agent
Framework: PydanticAI + FastA2A · Mode: conversational · Client id: classic-models-agent-catalogue
Serves the product manager: create and update products and product lines, set prices, decide what to discontinue.
Scopes and MCP servers
| MCP | catalog (mcp-catalog-audience), sales (mcp-sales-audience) |
| Scopes | catalog:read, catalog:write, sales:read |
sales:read is there to answer "does this product actually sell?" before a
pricing or discontinuation decision. Read only — this agent never touches orders.
A2A
Calls — Approvisionnement.check_availability, to know remaining stock and
open orders before discontinuing a reference.
Exposes — nothing yet. Add skills only when another agent needs them.
Why PydanticAI here
Not arbitrary. This is the agent that writes structured data under constraints — prices, quantities, references, product line membership. Typed validation of the model's output earns its place here in a way it would not on a purely conversational agent.
FastA2A, not to_a2a()
Use FastA2A directly. Agent.to_a2a() still works in PydanticAI 1.x but emits
a deprecation warning and is removed in v2.
FastA2A is framework-agnostic, so whatever is built here is also the natural
basis for the A2A adapter in front of the n8n agents — see
classic-models-agent-flows.
Before writing code
Read classic-models-agent-platform/CLAUDE.md first. It holds the identity
model, the MCP and LLM endpoints, the token-exchange recipe and the known
blockers. This file only covers what is specific to this agent.
Non-negotiables
- Inference goes through the Kong AI gateway,
https://llm-dev.sttlab.local/llm/v1/chat/completions. Calling a provider directly bypasses the rate limits and prompt guards. - One MCP client per conversation turn, built with freshly exchanged tokens. Do not hold a long-lived MCP connection with a fixed token.
- A2A calls return answers, not data. Expose narrow decision-shaped skills,
never a generic
query. - Deliverables in English.
Running it against the lab from macOS
macOS resolves every .local name through mDNS before falling back to
/etc/hosts, and that costs a flat ~5.0s per hostname — dscacheutil -q host -a name catalog-dev.sttlab.local shows it, and so does curl -w %{time_namelookup}. httpx's default connect timeout is 5s, so the very first
request to each host lands on the boundary and fails intermittently. fastmcp
reports that as Failed to initialize server session and swallows the cause,
which is why it read as an MCP protocol problem for a long time. curl --resolve
hides it completely — do not use --resolve to "prove" DNS is fine.
Settings.http_timeout (60s) is the fix, applied to the shared httpx2 client,
to the client fastmcp builds, and to MCPToolset(init_timeout=...). In the
cluster these names resolve instantly and the timeout never binds.