Architecture¶
Overview¶
beefree-agent-demo/
├── backend/
│ ├── config.py # pydantic-settings - typed env vars
│ ├── mcp_mock.py # Mock MCP server (port 8001)
│ ├── agent.py # PydanticAI agent - model selection + toolsets
│ ├── main.py # FastAPI - WebSocket /ws/chat
│ └── pyproject.toml # uv project - Python 3.13+
└── frontend/
└── src/
├── routes/ # TanStack Router - / to /chat
├── components/ # ChatPanel, JsonPreviewPanel, Layout
├── store/ # Zustand - messages, emailJson, isStreaming
└── api/ # useChat (WebSocket hook), Zod schemas
WebSocket message protocol¶
All messages are JSON.
Client to server:
Server to client:
{ "type": "token", "content": "Sure" }
{ "type": "token", "content": ", I'll create" }
{ "type": "tool_result", "tool_name": "create_email", "content": { ... } }
{ "type": "done" }
{ "type": "error", "content": "..." }
The frontend appends token deltas into the current message bubble, and pipes tool_result content directly into the JSON preview panel.
MCP mock server¶
mcp_mock.py implements the MCP Streamable HTTP transport manually - a single POST /mcp endpoint that handles JSON-RPC 2.0 methods:
| Method | Description |
|---|---|
initialize |
Handshake - returns server capabilities |
notifications/initialized |
Client acknowledgement - no-op |
tools/list |
Returns the four tool schemas |
tools/call |
Dispatches to the appropriate handler |
Tool results are returned as MCP content parts:
The text value is JSON-stringified BEE format. The frontend's Zod schemas validate this after parsing.
Agent wiring¶
agent.py builds a pydantic_ai.Agent with MCPServerStreamableHTTP as its toolset:
mcp_server = MCPServerStreamableHTTP(f"{settings.BEEFREE_MCP_URL}/mcp")
agent = Agent(model, toolsets=[mcp_server], system_prompt=SYSTEM_PROMPT)
PydanticAI handles the MCP initialize + tools/list handshake transparently on the first run. Tool calls made by the LLM are routed through PydanticAI to the MCP server and back.
Provider selection¶
match settings.LLM_PROVIDER:
case "openai": model = "openai:gpt-4o"
case "gemini": model = "google-gla:gemini-1.5-pro"
case _: model = "anthropic:claude-sonnet-4-5"
PydanticAI's model string format handles credential lookup from the environment automatically for each provider.