# hexagonal-agents > This skill should be used when building web applications where an AI agent dynamically generates HTML UI, using the hexagonal/ports-and-adapters architecture with HTMX for interactivity and MCP tools for data operations. - Author: Joshua Oliphant - Repository: JoshuaOliphant/claude-plugins - Version: 20260203181753 - Stars: 3 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/JoshuaOliphant/claude-plugins - Web: https://mule.run/skillshub/@@JoshuaOliphant/claude-plugins~hexagonal-agents:20260203181753 --- --- name: hexagonal-agents description: This skill should be used when building web applications where an AI agent dynamically generates HTML UI, using the hexagonal/ports-and-adapters architecture with HTMX for interactivity and MCP tools for data operations. --- # Hexagonal Agent Application Skill Build web applications where an AI agent serves as the UI layer, dynamically generating HTML in response to user messages. This architecture combines the hexagonal (ports-and-adapters) pattern with the Claude Agent SDK. --- ## Philosophy This skill applies the **message-passing paradigm** from Smalltalk and object-oriented programming to AI agents. In this model, the agent is a "prompt object" that receives semantic messages from users and responds with appropriate behavior—in this case, generating UI. **Key Principles:** 1. **Semantic Late Binding**: The agent interprets user intent at runtime, choosing which tools to call and what UI to generate. This provides flexibility traditional code cannot match. 2. **Separation of Concerns**: Tools handle data operations (CRUD), the agent handles presentation logic (generating HTML), and the HTTP adapter handles transport (FastAPI/HTMX). 3. **Hexagonal Architecture**: The agent sits at the center, with ports (tool interfaces) connecting to adapters (MCP servers, HTTP endpoints). This makes testing and evolution straightforward. 4. **Single Source of Truth**: The skill file defines the agent's entire UI vocabulary. When you want to change behavior, you modify the skill—not scattered code. --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Base HTML Template (static shell) │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ #content (HTMX swap target) │ │ │ │ │ │ ← Agent-generated HTML goes here │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ Input form: hx-post="/agent" │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ POST /agent {message: "..."} ┌─────────────────────────────────────────────────────────────┐ │ FastAPI Server (HTTP Adapter) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ @app.post("/agent") │ │ │ │ → agent.process(message) │ │ │ │ ← returns HTML string │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Agent (ClaudeSDKClient) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ System Prompt = skill file + HTML output rules │ │ │ │ MCP Server = your tool definitions │ │ │ │ │ │ │ │ Loop: │ │ │ │ 1. Send user message │ │ │ │ 2. Agent may call tools (handled by SDK) │ │ │ │ 3. Agent generates response │ │ │ │ 4. Extract text content as HTML │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Tools (MCP Server) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ @tool("name", "description", {schema}) │ │ │ │ async def name(args) -> {"content": [...]} │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` **Components:** - **Browser**: Displays static shell + agent-generated HTML, uses HTMX for partial updates - **FastAPI**: Receives HTTP requests, passes messages to agent, returns HTML - **Agent**: Receives messages, calls tools for data, generates HTML responses - **Tools**: Pure data operations (list, create, update, delete) returning structured JSON --- ## Quick Start Workflow When a user wants to build a hexagonal agent application, follow these steps: ### Step 1: Initialize Project Structure Run the initialization script or create manually: ```bash uv run scripts/init_hexagonal_app.py my-app-name --domain items ``` This creates: ``` my-app-name/ ├── pyproject.toml ├── README.md ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI application │ ├── agent.py # Agent wrapper │ ├── tools.py # MCP tool definitions │ └── skills/ │ └── ui.md # UI skill file └── data/ # Created at runtime ``` ### Step 2: Define Your Tools Edit `app/tools.py` to define what operations the agent can perform. Each tool: - Does ONE thing (single responsibility) - Returns structured JSON (not formatted strings) - Has a clear description of WHEN to use it ### Step 3: Create the Skill File Edit `app/skills/ui.md` to teach the agent: - What UI components to use - When to call which tools - How to handle different user intents ### Step 4: Configure the Agent The agent wrapper in `app/agent.py` connects everything: - Loads the skill file into the system prompt - Registers tools via MCP server - Extracts HTML from responses ### Step 5: Run and Iterate ```bash cd my-app-name uv run uvicorn app.main:app --reload ``` Open `http://localhost:8000` and interact with your agent. --- ## Detailed Implementation ### Tool Design Tools are the agent's interface to data. They define what actions are possible. **Tool Anatomy:** ```python from claude_agent_sdk import tool, create_sdk_mcp_server from typing import Any import json @tool( "list_items", # Name: verb_noun format "Get all items. Returns array of items with id, name, status.", # Description {} # Schema: parameter definitions ) async def list_items(args: dict[str, Any]) -> dict[str, Any]: """List all items.""" items = load_items() # Your data access logic return { "content": [{ "type": "text", "text": json.dumps({"items": items, "count": len(items)}) }] } ``` **Design Principles:** 1. **Single Responsibility**: Each tool does one thing. `list_items` lists, `create_item` creates. 2. **Structured Returns**: Return JSON with all fields the agent needs for UI generation. Don't format strings—let the agent decide presentation. 3. **Clear Descriptions**: The description tells the agent WHEN to use this tool. Be specific: "Get all items" vs "Search items by query". 4. **Error Handling**: Return errors in a consistent format with `is_error: True`. **Return Format:** ```python # Success { "content": [{"type": "text", "text": json.dumps(data)}] } # Error { "content": [{"type": "text", "text": json.dumps({"error": "message"})}], "is_error": True } ``` **Parameter Schema:** ```python # Required string {"name": str} # Optional (can be None) {"description": str | None} # With defaults (in function signature) @tool("search", "Search items", {"query": str, "limit": int | None}) async def search(query: str, limit: int = 10): ... ``` **Creating the MCP Server:** ```python def create_tools_server(): return create_sdk_mcp_server( name="app_tools", version="1.0.0", tools=[ list_items, get_item, create_item, update_item, delete_item, ] ) ``` --- ### Skill File Design The skill file is the heart of the hexagonal agent pattern. It teaches the agent how to generate UI. **Critical Requirements:** 1. **Raw HTML Output**: LLMs default to markdown. You MUST explicitly state "output raw HTML only" multiple times. 2. **Complete Component Patterns**: Show full HTML examples with all classes and HTMX attributes. Don't just list class names. 3. **HTMX Integration**: Every interactive element needs `hx-post`, `hx-target`, and `hx-vals`. 4. **Tool-to-UI Mapping**: Explain when to call each tool and what UI to render with the results. **Skill File Structure:** ```markdown # Application UI Skill You are an AI application that generates user interfaces... ## Critical Output Rules 1. Output ONLY raw HTML — never wrap in markdown code fences 2. Never include ```html or ``` markers 3. All output must be valid HTML fragments ... ## Design System ### Colors (Tailwind classes) - Background: bg-slate-900 (page), bg-slate-800 (cards) ... ## Component Patterns ### Card Container ```html
``` ## Available Tools 1. **list_items** — Get all items. Call when user wants to see items. ... ## Response Patterns ### User wants to see items 1. Call list_items tool 2. If items exist: render with Page Header + Item List 3. If no items: render Empty State ... ``` **HTMX Requirements:** Every button must have: ```html ``` Every form must have: ```html
``` --- ### Agent Wrapper The agent wrapper connects the SDK to your application. **Key Responsibilities:** 1. Load skill file into system prompt 2. Create and register MCP tools server 3. Process messages and extract HTML 4. Handle errors gracefully **Implementation Pattern:** ```python from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, TextBlock from pathlib import Path from app.tools import create_tools_server SKILL_PATH = Path(__file__).parent / "skills" / "ui.md" class Agent: def __init__(self): self.tools_server = create_tools_server() self.client = None self._connected = False # Tool names must match: mcp__{server_name}__{tool_name} self._allowed_tools = [ "mcp__app_tools__list_items", "mcp__app_tools__create_item", # ... add all your tools ] async def _ensure_connected(self): if self._connected: return skill_content = SKILL_PATH.read_text() options = ClaudeAgentOptions( system_prompt=skill_content, mcp_servers={"app_tools": self.tools_server}, allowed_tools=self._allowed_tools, permission_mode="acceptEdits", ) self.client = ClaudeSDKClient(options=options) await self.client.connect() self._connected = True async def process(self, message: str) -> str: await self._ensure_connected() await self.client.query(message) html_parts = [] async for msg in self.client.receive_response(): for block in msg.content: if isinstance(block, TextBlock): html_parts.append(block.text) return self._clean_html("\n".join(html_parts)) def _clean_html(self, html: str) -> str: """Strip markdown fences if present.""" html = html.strip() if html.startswith("```html"): html = html[7:] elif html.startswith("```"): html = html[3:] if html.endswith("```"): html = html[:-3] return html.strip() ``` **MCP Server Naming:** Tool names in `allowed_tools` must follow exact format: ``` mcp__{server_key}__{tool_name} ``` Where `server_key` is the key used in `mcp_servers={"server_key": server}`. --- ### HTTP Adapter The FastAPI application serves as the HTTP adapter in the hexagonal architecture. **Base Template:** ```python BASE_TEMPLATE = ''' {title}
{content}
''' ``` **Agent Endpoint:** ```python @app.post("/agent", response_class=HTMLResponse) async def handle_message(request: Request): form_data = await request.form() message = str(form_data.get("message", "")).strip() if not message: return '

Please enter a message.

' # Append form fields to message extra_fields = [f"{k}={v}" for k, v in form_data.items() if k != "message" and v] if extra_fields: message = f"{message} [{', '.join(extra_fields)}]" try: html = await agent.process(message) return html except Exception as e: return f'''

An error occurred. Please try again.

''' ``` **Form Data Flow:** 1. User submits form with fields + hidden message 2. FastAPI extracts all form data 3. Extra fields appended to message: `"action [field1=value1, field2=value2]"` 4. Agent parses and uses values with appropriate tool --- ## UI Design System This skill uses a dark, atmospheric aesthetic with refined typography. ### Color Palette ``` Backgrounds: - Page: bg-slate-950 - Cards: bg-slate-900 - Inputs: bg-slate-800 - Highlight: bg-slate-800/50 Borders: - Default: border-slate-800 - Emphasis: border-slate-700 - Focus: ring-indigo-500 Accent (use sparingly): - Primary: bg-indigo-600 hover:bg-indigo-500 - Links: text-indigo-400 - Focus: ring-indigo-500 Text: - Headings: text-white - Body: text-slate-300 - Muted: text-slate-400 - Success: text-emerald-400 - Warning: text-amber-400 - Danger: text-red-400 ``` ### Typography ``` Headings: font-['Space_Grotesk'] font-bold Body: Default sans (Inter) Sizes: - Page title: text-2xl font-bold - Section: text-xl font-semibold - Card title: text-lg font-medium - Body: default - Caption: text-sm text-slate-400 ``` ### Component Examples **Page Header:** ```html

Title

Supporting context

``` **Card:** ```html
``` **List Item (Interactive):** ```html
A

Item Name

Description

``` **Primary Button:** ```html ``` **Form:** ```html
``` **Empty State:** ```html
📚

No items yet

Get started by creating your first item

``` **Success Alert:** ```html

Success message

``` **Error Alert:** ```html

Error message

``` --- ## Evaluation Patterns Use `pydantic-evals` to systematically test your hexagonal agent application. ### Three Evaluation Targets 1. **Tool Usage**: Did the agent call the right tools with correct parameters? 2. **State Outcome**: Is the data correct after the action? 3. **UI Quality**: Does the HTML make sense for this context? ### Evaluator Types | Type | Use For | Example | |------|---------|---------| | Code-based | Deterministic checks | Tool was called, state changed | | Model-based | Subjective quality | UI appropriateness | | Human | Calibration | Periodic review | ### Basic Test Case ```python from pydantic_evals import Case, Dataset Case( name="create_item_natural_language", inputs="Create a task called 'Buy groceries'", metadata={"category": "create", "initial_state": {}}, evaluators=[ ToolWasCalled(tool_name="create_item"), StateCountDelta(collection="items", delta=1), ContainsHTMXAttributes(), ], ) ``` ### Key Principles - **Grade outcomes, not paths**: Don't require specific tool sequences - **Start with real failures**: Convert bugs into test cases - **Run multiple trials**: Agent behavior varies; use `num_trials=3` - **Graduate passing tests**: Move capability tests to regression suite at 95%+ pass rate --- ## Debugging Guide ### Agent outputs markdown instead of HTML **Symptom:** Response wrapped in ```html ... ``` **Fix:** 1. Verify skill file has "Output ONLY raw HTML" rule prominently 2. Add reminder at end of system prompt: "Never use markdown code fences" 3. The agent's `_clean_html` method strips fences as fallback ### Tool not being called **Symptom:** Agent responds conversationally instead of using tools **Fix:** 1. Check tool is in `allowed_tools` list (exact format: `mcp__servername__toolname`) 2. Verify tool description clearly states when to use it 3. Add explicit instruction: "When user asks X, call tool Y" 4. Print registered tools to debug: `print(list(mcp_server.tools.keys()))` ### HTMX not working **Symptom:** Buttons cause full page reload or nothing happens **Fix:** 1. Check HTMX script loaded in base template 2. Verify hx-post, hx-target, hx-vals all present 3. hx-vals must be valid JSON: `hx-vals='{"message":"..."}'` 4. hx-target must match element ID: `hx-target="#content"` ### Form data not reaching agent **Symptom:** Agent doesn't see form field values **Fix:** 1. Form fields need `name` attribute 2. Hidden message field must exist 3. Verify FastAPI extracts form fields and appends to message ### Blank response **Symptom:** Empty content area after request **Fix:** 1. Check agent properly extracts TextBlock content 2. Look for ResultMessage.is_error being True 3. Add logging to see what messages are received --- ## Domain Adaptation Checklist To build a hexagonal agent app for your domain: ### 1. Define Your Entities What are you managing? Books, tasks, recipes, tickets, products? ### 2. Replace Tools (`tools.py`) - Change entity names (items → books, tasks, etc.) - Define fields specific to your domain - Keep CRUD operations standard - Add domain-specific operations (search, filter, aggregate) ### 3. Update Skill File (`skills/ui.md`) - Update tool list and descriptions - Adjust response patterns for your domain - Customize empty state messages and icons - Add domain-specific components (star ratings, status badges) ### 4. Update Agent (`agent.py`) - Change `_allowed_tools` list to match your tools ### 5. Update Welcome Content (`main.py`) - Domain-appropriate title - Initial action buttons for your use case ### 6. Test Common Flows - [ ] List view (empty state) - [ ] List view (with items) - [ ] Create item (with form) - [ ] Create item (natural language) - [ ] View single item - [ ] Update item - [ ] Delete item - [ ] Search/filter --- ## Running Your Application ```bash # Navigate to your app cd my-app-name # Set API key export ANTHROPIC_API_KEY=your_key_here # Run with uvicorn uv run uvicorn app.main:app --reload # Open browser open http://localhost:8000 ``` --- ## Evolving Your Application As your app matures, consider these advanced patterns documented in the references: ### Multi-Agent Architecture When your app needs specialized domain expertise, evolve to multiple agents: - **UI Agent**: Handles user interaction, generates HTML - **Specialist Agents**: Handle recommendations, analytics, etc. - **Message Passing**: Agents communicate via semantic messages, not method calls See `references/multi_agent_patterns.md` for complete implementation. ### Progressive UI Caching (Saved Views) Reduce latency and API costs by caching agent-generated views: - **Static Views**: Cache forms, welcome screens - **Data-Driven Views**: Templates with fresh data at serve time - **Fast Path**: Match saved views before calling agents See `references/saved_views.md` for implementation. ### SQLite Persistence Migrate from JSON files to SQLite for ACID compliance: - Proper transactions - Concurrent access safety - Query capabilities - Auto-migration from JSON See `references/sqlite_persistence.md` for implementation. ### Enhanced Loading UX Improve perceived performance with better feedback: - Animated loading indicators with status messages - Form disabling during requests - Content dimming during loading - Bouncing dots for agent activity See `references/enhanced_ux.md` for patterns. --- ## Files Reference When implementing, refer to these reference files: ### Core Architecture - `references/architecture.md` — Deep dive on hexagonal architecture - `references/sdk_reference.md` — Claude Agent SDK API details ### UI & Components - `references/component_library.md` — Extended UI component patterns - `references/enhanced_ux.md` — Advanced loading states and visual feedback ### Advanced Patterns - `references/multi_agent_patterns.md` — Multi-agent message-passing architecture - `references/saved_views.md` — Progressive UI caching (fast/slow path) - `references/sqlite_persistence.md` — SQLite database patterns ### Testing & Evaluation - `references/eval_patterns.md` — Comprehensive evaluation examples --- ## Summary The hexagonal agent pattern provides: 1. **Clean separation**: Tools handle data, agent handles UI, HTTP handles transport 2. **Flexibility**: Change UI behavior by editing skill file, not code 3. **Testability**: Mock tools for unit tests, use evals for integration 4. **Evolvability**: Swap implementations at any boundary Start simple: one entity, basic CRUD, minimal UI. Iterate from working software.