- Replace wildcard CORS origins with restricted domain list - Add comprehensive security patterns to .gitignore - Create SECURITY.md with deployment security guidelines - Restrict CORS methods and headers to minimum required - Add security documentation for production deployment
242 lines
9.9 KiB
Markdown
242 lines
9.9 KiB
Markdown
# 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
|
|
<script is:inline>
|
|
import('https://esm.run/@mlc-ai/web-llm').then(webllm => {
|
|
window.webllm = webllm;
|
|
});
|
|
</script> ```
|
|
|
|
|
|
|
|
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
|
|
|
|
|