Skip to content

Architecture

Note the two MCP arrows, pointing opposite ways. On the left, someone else’s client calls our server. On the right, our agent calls someone else’s. Same protocol, both ends — see Learn · MCP.

What happens when you ask a question, in order:

The critical line is the fourth one. The connection closes before the agent has done anything. Every timeout problem in Learn · Why deployment is hard dies right there.

Three tables. Two of them do all the work.

runs is the job. steps is the thought process — and the reason the UI can say “Agent is searching…” instead of showing a spinner. memories is there for when you add RAG; nothing writes to it yet.

deploying-agentic-ai-apps-workshop/
├── app/ the deployed application — its own uv project
│ ├── pyproject.toml dependencies, pinned by app/uv.lock
│ ├── main.py the API — /runs vs /runs/naive
│ ├── config.py environment variables, validated at startup
│ ├── db.py Supabase over its REST API
│ ├── llm.py raw Gemini calls (used by manual_loop only)
│ ├── agent/
│ │ ├── research.py the agent — Pydantic AI
│ │ ├── manual_loop.py the same loop by hand, for reading
│ │ └── steps.py step logging + the step ceiling
│ ├── tools/
│ │ ├── wikipedia.py the tools themselves
│ │ └── server.py the same tools, over MCP
│ ├── tests/ 37 tests, no network or keys needed
│ └── http/ ready-made requests for driving the API by hand
├── client/ the client — Vite + React + TypeScript
│ └── src/
│ ├── api.ts the five HTTP calls
│ ├── useConversation.ts accept-and-poll + threads
│ └── useHealth.ts which model is answering
├── database/schema.sql three tables
├── deploy/render.yaml infrastructure as a file
├── slides/ the presentation deck
├── website/ this site
└── docs/ run of show, prep notes, free-tier notes

One service, not three. The API, the agent, and the MCP server all run in one FastAPI process. Splitting them would be more “correct” and would triple the number of things that can be asleep when you need them. On a free tier, fewer moving parts wins.

The database is the message bus. There’s no Redis, no queue. The agent writes rows; the client reads rows. It’s simpler than it sounds and it means every run leaves a complete, inspectable trace you can read in the Supabase table editor.

The frontend is a real app, not a demo page. Vite + React + TypeScript, with its own package.json. A hand-written HTML file would be shorter, but it would quietly skip the part that actually breaks: cancelling the poll loop when the component unmounts, and not stacking requests when a cold server is slow to answer. Those live in client/src/useConversation.ts. The fetch calls next door in api.ts are identical from Vue, Flutter, or curl — it’s the code around them that’s worth reading.