- Complete Rentcast API integration with all endpoints - Intelligent caching system with hit/miss tracking - Rate limiting with exponential backoff - User confirmation system with MCP elicitation support - Docker Compose setup with dev/prod modes - PostgreSQL database for persistence - Comprehensive test suite foundation - Full project structure and documentation
18 KiB
mcrentcast - Rentcast MCP Server
Implement an MCP server the implements the Rentcast API
-
allow users to set their API key via 'tool'
-
'cache' all API responses, each request costs $$!
- track cache hits and misses
- always include the 'cache age' of the returned data in the reply
- property records should be cached unless the client specifies to 'expire' the cache record
- provide tool to "expire" cache
- protect this tool from being called too often (exponential backoff) - in the case of 'mis-use'
-
set 'api call' limits per day/month
- use reasonable limits by default
- allow client to change them
- configurable (by mcp client) exponential backoff on all 'rentcast api' requests (on cache miss)
- enabled by default, set sane values (3 calls per minute)
-
Confirmation:
- when rentcast cache is 'missed' we want to make sure the user wants to pay for the request
- if client supports https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation "double check" by issuing an elicitation, asking if they want to make the call, including old and new values, and estimated cost (if any). If client doesnt support 'mcp elicitation', use a 'regular response' (without making the call) and ask the 'mcp client' to get permission from their 'user', and retry the call if granted. behind the scenes (in the 'fastmcp session') track calls to 'confirmed tools' (including 'parameter hash' and time). use this to tell if call is to "confirm" a previous operation, or a new operation.
- store 'properties' in a json structure
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)
## Python Testing Framework with Syntax Highlighting
Use pytest with comprehensive test recording, beautiful HTML reports, and syntax highlighting:
**Setup with uv:**
```bash
# Install test dependencies
uv add --dev pytest pytest-asyncio pytest-html pytest-cov ruff
```
**pyproject.toml dev dependencies:**
```toml
[dependency-groups]
dev = [
"pytest>=8.4.0",
"pytest-asyncio>=1.1.0",
"pytest-html>=4.1.0",
"pytest-cov>=4.0.0",
"ruff>=0.1.0",
]
```
**pytest.ini configuration:**
```ini
[tool:pytest]
addopts =
-v --tb=short
--html=reports/test_report.html --self-contained-html
--cov=src --cov-report=html:reports/coverage_html
--capture=no --log-cli-level=INFO
--log-cli-format="%(asctime)s [%(levelname)8s] %(name)s: %(message)s"
--log-cli-date-format="%Y-%m-%d %H:%M:%S"
testpaths = .
markers =
unit: Unit tests
integration: Integration tests
smoke: Smoke tests for basic functionality
performance: Performance and benchmarking tests
agent: Expert agent system tests
```
**Advanced Test Framework Features:**
**1. TestReporter Class for Rich I/O Capture:**
```python
from test_enhanced_reporting import TestReporter
def test_with_beautiful_output():
reporter = TestReporter("My Test")
# Log inputs with automatic syntax highlighting
reporter.log_input("json_data", {"key": "value"}, "Sample JSON data")
reporter.log_input("python_code", "def hello(): return 'world'", "Sample function")
# Log processing steps with timing
reporter.log_processing_step("validation", "Checking data integrity", 45.2)
# Log outputs with quality scores
reporter.log_output("result", {"status": "success"}, quality_score=9.2)
# Log quality metrics
reporter.log_quality_metric("accuracy", 0.95, threshold=0.90, passed=True)
# Complete test
reporter.complete()
```
**2. Automatic Syntax Highlighting:**
- **JSON**: Color-coded braces, strings, numbers, keywords
- **Python**: Keyword highlighting, string formatting, comment styling
- **JavaScript**: ES6 features, function detection, syntax coloring
- **Auto-detection**: Automatically identifies and formats code vs data
**3. Interactive HTML Reports:**
- **Expandable Test Details**: Click any test row to see full logs
- **Professional Styling**: Clean, content-focused design with Inter fonts
- **Comprehensive Logging**: Inputs, processing steps, outputs, quality metrics
- **Performance Metrics**: Timing, success rates, assertion tracking
**4. Custom conftest.py Configuration:**
```python
# Enhance pytest-html reports with custom styling and data
def pytest_html_report_title(report):
report.title = "🏠 Your App - Test Results"
def pytest_html_results_table_row(report, cells):
# Add custom columns, styling, and interactive features
# Full implementation in conftest.py
```
**5. Running Tests:**
```bash
# Basic test run with beautiful HTML report
uv run pytest
# Run specific test categories
uv run pytest -m smoke
uv run pytest -m "unit and not slow"
# Run with coverage
uv run pytest --cov=src --cov-report=html
# Run single test with full output
uv run pytest test_my_feature.py -v -s
```
**6. Test Organization:**
```
tests/
├── conftest.py # pytest configuration & styling
├── test_enhanced_reporting.py # TestReporter framework
├── test_syntax_showcase.py # Syntax highlighting examples
├── agents/ # Agent system tests
├── knowledge/ # Knowledge base tests
└── server/ # API/server tests
```
## MCP (Model Context Protocol) Server Architecture
Use FastMCP >=v2.12.2 for building powerful MCP servers with expert agent systems:
**Installation with uv:**
```bash
uv add fastmcp pydantic
```
**Basic FastMCP Server Setup:**
```python
from fastmcp import FastMCP
from fastmcp.elicitation import request_user_input
from pydantic import BaseModel, Field
app = FastMCP("Your Expert System")
class ConsultationRequest(BaseModel):
scenario: str = Field(..., description="Detailed scenario description")
expert_type: str = Field(None, description="Specific expert to consult")
context: Dict[str, Any] = Field(default_factory=dict)
enable_elicitation: bool = Field(True, description="Allow follow-up questions")
@app.tool()
async def consult_expert(request: ConsultationRequest) -> Dict[str, Any]:
"""Consult with specialized expert agents using dynamic LLM sampling."""
# Implementation with agent dispatch, knowledge search, elicitation
return {"expert": "FoundationExpert", "analysis": "...", ...}
```
**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.reverse_proxy: "{{upstreams}}"
when necessary, use "prefix or suffix" to make labels unique/ordered, see how a prefix is used below in the 'reverse_proxy' labels: ```
caddy: $DOMAIN caddy.@ws.0_header: Connection Upgrade caddy.@ws.1_header: Upgrade websocket caddy.0_reverse_proxy: @ws {{upstreams}} caddy.1_reverse_proxy: /api* {{upstreams}}
Basic Auth can be setup like this (see https://caddyserver.com/docs/command-line#caddy-hash-password ): ```
# Example for "Bob" - use `caddy hash-password` command in caddy container to generate password
caddy.basicauth: /secret/*
caddy.basicauth.Bob: $$2a$$14$$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
You can enable on_demand_tls by adding the follwing labels: ```
labels: caddy_0: yourbasedomain.com caddy_0.reverse_proxy: '{{upstreams 8080}}'
https://caddyserver.com/on-demand-tls
caddy.on_demand_tls:
caddy.on_demand_tls.ask: http://yourinternalcontainername:8080/v1/tls-domain-check # Replace with a full domain if you don't have the service on the same docker network.
caddy_1: https:// # Get all https:// requests (happens if caddy_0 match is false)
caddy_1.tls_0.on_demand:
caddy_1.reverse_proxy: http://yourinternalcontainername:3001 # Replace with a full domain if you don't have the service on the same docker network.
## Common Pitfalls to Avoid
1. **Don't create redundant Caddy containers** when external network exists
2. **Don't forget `PUBLIC_` prefix** for client-side env vars
3. **Don't import client-only packages** at build time
4. **Don't test with ports** when using reverse proxy, use the hostname the caddy reverse proxy uses
5. **Don't hardcode domains in configs** - use `process.env.PUBLIC_DOMAIN` everywhere
6. **Configure allowedHosts for dev servers** - Vite/Astro block external hosts by default