# Basic Service Template # General Notes: Make this project and collaboration delightful! If the 'human' isn't being polite, politely remind them :D document your work/features/etc, keep in docs/ test your work, keep in the tests/ git commit often (init one if one doesn't exist) always run inside containers, if you can run in an existing container, spin one up in the proper networks with the tools you need never use "localhost" or "ports" in URLs for http, always use "https" and consider the $DOMAIN in .env ## Tech Specs Docker Compose no "version:" in docker-compose.yml Use multi-stage build $DOMAIN defined in .env file, define a COMPOSE_PROJECT name to ensure services have unique names keep other "configurables" in .env file and compose/expose to services in docker-compose.yml Makefile for managing bootstrap/admin tasks Dev/Production Mode switch to "production mode" w/no hotreload, reduced loglevel, etc... Services: Frontend Simple, alpine.js/astro.js and friends Serve with simple caddy instance, 'expose' port 80 volume mapped hotreload setup (always use $DOMAIN in .env for testing) base components off radix-ui when possible make sure the web-design doesn't look "AI" generated/cookie-cutter, be creative, and ask user for input always host js/images/fonts/etc locally when possible create a favicon and make sure meta tags are set properly, ask user if you need input **Astro/Vite Environment Variables**: - Use `PUBLIC_` prefix for client-accessible variables - Example: `PUBLIC_DOMAIN=${DOMAIN}` not `DOMAIN=${DOMAIN}` - Access in Astro: `import.meta.env.PUBLIC_DOMAIN` **In astro.config.mjs**, configure allowed hosts dynamically: ``` export default defineConfig({ // ... other config vite: { server: { host: '0.0.0.0', port: 80, allowedHosts: [ process.env.PUBLIC_DOMAIN || 'localhost', // Add other subdomains as needed ] } } });``` ## Client-Side Only Packages Some packages only work in browsers Never import these packages at build time - they'll break SSR. **Package.json**: Add normally **Usage**: Import dynamically or via CDN ```javascript // Astro - use dynamic import const webllm = await import("@mlc-ai/web-llm"); // Or CDN approach for problematic packages ``` Backend Python 3.13 uv/pyproject.toml/ruff/FastAPI 0.116.1 /PyDantic 2.11.7 /SqlAlchemy 2.0.43/sqlite See: https://docs.astral.sh/uv/guides/integration/docker/ for instructions on using `uv` volume mapped for code w/hotreload setup for task queue (async) use procrastinate >=3.5.2 https://procrastinate.readthedocs.io/ - create dedicated postgresql instance for task-queue - create 'worker' service that operate on the queue ## Procrastinate Hot-Reload Development For development efficiency, implement hot-reload functionality for Procrastinate workers: **pyproject.toml dependencies:** ```toml dependencies = [ "procrastinate[psycopg2]>=3.5.0", "watchfiles>=0.21.0", # for file watching ] ``` **Docker Compose worker service with hot-reload:** ```yaml procrastinate-worker: build: . command: /app/.venv/bin/python -m app.services.procrastinate_hot_reload volumes: - ./app:/app/app:ro # Mount source for file watching environment: - WATCHFILES_FORCE_POLLING=false # Use inotify on Linux networks: - caddy depends_on: - procrastinate-db restart: unless-stopped healthcheck: test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] interval: 30s timeout: 10s retries: 3 ``` **Hot-reload wrapper implementation:** - Uses `watchfiles` library with inotify for efficient file watching - Subprocess isolation for clean worker restarts - Configurable file patterns (defaults to `*.py` files) - Debounced restarts to handle rapid file changes - Graceful shutdown handling with SIGTERM/SIGINT - Development-only feature (disabled in production) **Advanced MCP Features:** **1. Expert Agent System Integration:** ```python # Agent Registry with 45+ specialized experts agent_registry = AgentRegistry(knowledge_base) agent_dispatcher = AgentDispatcher(agent_registry, knowledge_base) # Multi-agent coordination for complex scenarios @app.tool() async def multi_agent_conference( scenario: str, required_experts: List[str], coordination_mode: str = "collaborative" ) -> Dict[str, Any]: """Coordinate multiple experts for interdisciplinary analysis.""" return await agent_dispatcher.multi_agent_conference(...) ``` **2. Interactive Elicitation:** ```python @app.tool() async def elicit_user_input( questions: List[str], context: str = "", expert_name: str = "" ) -> Dict[str, Any]: """Request clarifying input from human user via MCP.""" user_response = await request_user_input( prompt=f"Expert {expert_name} asks:\n" + "\n".join(questions), title=f"Expert Consultation: {expert_name}" ) return {"questions": questions, "user_response": user_response} ``` **3. Knowledge Base Integration:** ```python @app.tool() async def search_knowledge_base( query: str, filters: Optional[Dict] = None, max_results: int = 10 ) -> Dict[str, Any]: """Semantic search across expert knowledge and standards.""" results = await knowledge_base.search(query, filters, max_results) return {"query": query, "results": results, "total": len(results)} ``` **4. Server Architecture Patterns:** ``` src/your_mcp/ ├── server.py # FastMCP app with tool definitions ├── agents/ │ ├── base.py # Base agent class with LLM sampling │ ├── dispatcher.py # Multi-agent coordination │ ├── registry.py # Agent discovery and management │ ├── structural.py # Structural inspection experts │ ├── mechanical.py # HVAC, plumbing, electrical experts │ └── professional.py # Safety, compliance, documentation ├── knowledge/ │ ├── base.py # Knowledge base with semantic search │ └── search_engine.py # Vector search and retrieval └── tools/ # Specialized MCP tools ``` **5. Testing MCP Servers:** ```python import pytest from fastmcp.testing import MCPTestClient @pytest.mark.asyncio async def test_expert_consultation(): client = MCPTestClient(app) result = await client.call_tool("consult_expert", { "scenario": "Horizontal cracks in basement foundation", "expert_type": "FoundationExpert" }) assert result["success"] == True assert "analysis" in result assert "recommendations" in result ``` **6. Key MCP Concepts:** - **Tools**: Functions callable by LLM clients (always describe from LLM perspective) - **Resources**: Static or dynamic content (files, documents, data) - **Sampling**: Server requests LLM to generate content using client's models - **Elicitation**: Server requests human input via client interface - **Middleware**: Request/response processing, auth, logging, rate limiting - **Progress**: Long-running operations with status updates **Essential Links:** - Server Composition: https://gofastmcp.com/servers/composition - Powerful Middleware: https://gofastmcp.com/servers/middleware - MCP Testing Guide: https://gofastmcp.com/development/tests#tests - Logging & Progress: https://gofastmcp.com/servers/logging - User Elicitation: https://gofastmcp.com/servers/elicitation - LLM Sampling: https://gofastmcp.com/servers/sampling - Authentication: https://gofastmcp.com/servers/auth/authentication - CLI Patterns: https://gofastmcp.com/patterns/cli - Full Documentation: https://gofastmcp.com/llms-full.txt All Reverse Proxied Services use external `caddy` network" services being reverse proxied SHOULD NOT have `port:` defined, just `expose` on the `caddy` network **CRITICAL**: If an external `caddy` network already exists (from caddy-docker-proxy), do NOT create additional Caddy containers. Services should only connect to the existing external network. Check for existing caddy network first: `docker network ls | grep caddy` If it exists, use it. If not, create it once globally. see https://github.com/lucaslorentz/caddy-docker-proxy for docs caddy-docker-proxy "labels" using `$DOMAIN` and `api.$DOMAIN` (etc, wildcard *.$DOMAIN record exists) ``` labels: caddy: $DOMAIN caddy.0_reverse_proxy: {{upstreams 80}} # caddy.1_reverse_proxy: /other_url other_server 80 network: - caddy ``` ## Common Pitfalls to Avoid 1. **Don't forget `PUBLIC_` prefix** for client-side env vars 2. **Don't import client-only packages** at build time 3. **Don't test with ports** when using reverse proxy, use the hostname the caddy reverse proxy uses 4. **Don't hardcode domains in configs** - use `process.env.PUBLIC_DOMAIN` everywhere 5. **Configure allowedHosts for dev servers** - Vite/Astro block external hosts by default