# Personal Productivity MCP Server
## Project Context
Multi-domain MCP server (career, fitness, family) for personal automation.
This is both a learning exercise to understand MCP architecture and a practical
tool for daily use.
**Purpose:** Replace manual workflows with conversational automation via Claude.
## Architecture
### Overview
Single MCP server with three domains:
- **career.*** - Job search, application tracking, interview prep
- **fitness.*** - Health metrics, workout analysis, progress tracking
- **family.*** - Kids billing, Math Academy tracking, expense management
### Design Patterns
- Each domain: read tools, write tools, analysis tools
- SQLite for all structured data
- Playwright (async) for web scraping
- Pydantic v2 for data validation
- Redis for caching (optional, start with in-memory)
## Technology Stack
- **Language:** Python 3.11+
- **Framework:** Anthropic MCP SDK
- **Scraping:** Playwright (headless Chromium)
- **Database:** SQLite (single-user is sufficient)
- **HTTP:** httpx (async)
- **Deployment:** Docker + docker-compose
## Development Principles
### Code Quality
- Comprehensive error handling with specific exception types
- Structured logging (JSON) for production debugging
- Type hints everywhere (validate with mypy)
- Docstrings for all tools (Claude needs to understand them)
### Performance
- Cache scraped jobs for 1 hour
- Cache health metrics for 15 minutes
- Batch database operations where possible
- Rate limiting on external APIs
### Security
- Store credentials in `.env` (never commit)
- Validate all inputs with Pydantic
- Sanitize URLs before scraping
- Respect robots.txt
### Testing Philosophy
- Unit tests for business logic
- Integration tests with real external services (using VCR.py for replays)
- Manual testing via Claude conversations
- Start simple, add tests as bugs are found
## Deployment Environment
### Quick Summary
- **Host:** anvil.tail0689bc.ts.net (100.72.67.122)
- **OS:** Ubuntu Server 24.04 LTS, Docker + Docker Compose
- **Network:** Tailscale mesh network for secure remote access
- **Existing Services:** n8n (port 5678), Portainer (port 9443)
- **MCP Server Port:** 8100
- **MCP Access:** Claude Desktop connects via SSH tunnel to benro@100.72.67.122
### Server Environment
**Server Information:**
- **OS:** Ubuntu Server 24.04 LTS
- **Hostname:** anvil
- **Tailscale IP:** 100.72.67.122
- **Local IP:** 192.168.1.111
- **Full domain:** anvil.tail0689bc.ts.net
**Hardware:**
- CPU: Intel i7-6820HQ (4 cores, 8 threads @ 2.70GHz)
- RAM: 16GB
- Storage: 477GB available
- GPU: NVIDIA Quadro (not currently in use)
**Docker Setup:**
- Version: Docker CE (latest)
- Docker Compose: v2+ (version line deprecated)
- Running services:
- n8n (port 5678) - workflow automation
- Portainer (port 9443) - Docker management UI
**Directory Structure:**
- `~/projects/` - Running production services
- `~/dev/` - Development projects (with dev container templates)
- `~/homelab-configs/` - Infrastructure configs (git-backed)
### Network & Access
**Connection Methods:**
- **SSH Access:** `ssh benro@100.72.67.122` (via Tailscale)
- **Tailscale:** Full mesh network enabled with MagicDNS
- **Tailscale Funnel:** Enabled for n8n (port 5678) - allows public HTTPS access
**MCP Server Access:**
- Use SSH tunnel from Claude Desktop to homelab
- MCP server on port 8100 (internal Docker, accessible via Tailscale)
- Tailscale provides encrypted access without opening router ports
**Security Considerations:**
- Tailscale provides encrypted mesh networking
- Firewall (ufw) is currently inactive
- All Docker services on isolated networks
- No direct internet exposure except via Tailscale Funnel (intentional)
**Port Mappings:**
- n8n: 5678
- Portainer: 9443
- MCP Server: 8100
- Available range: 8101-9000 for future services
### Data Storage
**Storage Location:**
- SQLite database: Docker volume `mcp-data`
- Configuration files: `~/projects/mcp-project/config/`
- Health exports: Docker volume `mcp-data` under `/data/health-exports/`
- Logs: Docker container logs (viewable via Portainer or `docker logs`)
**Backup Strategy:**
- Docker volume backups via `~/homelab-configs/scripts/backup-configs.sh`
- Periodic SQLite dumps to `~/homelab-configs/backups/`
- Git-backed configs pushed to GitHub
- Consider automated daily backups via cron
**Storage Notes:**
- 477GB total storage available
- No NAS currently (local storage only)
- Docker volumes provide persistence across container restarts
- All configs version-controlled via git
### Integration Points
**n8n Integration:**
- Running at: `https://anvil.tail0689bc.ts.net`
- MCP server can expose HTTP endpoints for n8n webhooks
- n8n workflows can trigger MCP tools
- Shared Docker network: `homelab` (to be created)
**Automation Tools:**
- n8n (primary automation platform)
- Docker Compose for service orchestration
- Tailscale for networking/access
**Potential Integrations:**
- n8n → MCP: Scheduled job searches, weekly fitness reports
- MCP → n8n: Trigger workflows on new job applications
- Gmail (OAuth configured for n8n) for job alerts, interview scheduling
**Job Search Context:**
- User is actively job searching (solutions engineer background)
- Networking via Pre-Sales Collective community
- Experience with Cyara and Botium (chatbot testing)
- Track applications, contacts, networking activities via MCP
**Other Data Sources:**
- Gmail API (via n8n OAuth)
- Google Sheets (family billing system)
- Health Auto Export (CSV)
- Hevy workout app (CSV + API)
### Development Workflow
**Using Claude Code:**
- Installed locally on Mac Mini (benjamins-mac-mini, 192.168.1.195)
- Tailscale IP: 100.73.175.1
- Connects to homelab via SSH (benro@100.72.67.122)
- Can create/manage files and containers remotely
**Deployment Preference:**
- Use Docker Compose for service definition
- Store in `~/projects/mcp-project/`
- Follow existing pattern (similar to n8n setup)
- Use dev containers for development/testing (optional)
**Testing Access:**
- MCP server accessible at: `http://100.72.67.122:8100`
- Or via domain: `http://anvil.tail0689bc.ts.net:8100`
- Health check endpoint: `/health`
### Docker Configuration
**docker-compose.yml requirements:**
```yaml
services:
mcp-server:
build: .
container_name: mcp-server
restart: unless-stopped
ports:
- "8100:8100"
volumes:
- mcp-data:/app/data # SQLite DB, health exports
- ./config:/app/config:ro # .env, credentials (read-only)
networks:
- homelab
environment:
- MCP_PORT=8100
- DATABASE_PATH=/app/data/mcp.db
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8100/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mcp-data:
networks:
homelab:
external: true # Shared with n8n for integration
```
**Network Setup:**
Before deploying, create shared network:
```bash
docker network create homelab
```
Then add to n8n's docker-compose.yml:
```yaml
networks:
homelab:
external: true
```
### Operational Commands
**Deploy and manage:**
```bash
# Deploy initial version
cd ~/projects/mcp-project
docker compose up -d
# View logs
docker compose logs -f
# Restart after changes
docker compose restart
# Stop service
docker compose down
# Rebuild after code changes
docker compose up -d --build
```
**Test endpoints:**
```bash
# Health check (from server)
curl http://localhost:8100/health
# Health check (from Mac via Tailscale)
curl http://100.72.67.122:8100/health
# Or using domain
curl http://anvil.tail0689bc.ts.net:8100/health
```
**Database backups:**
```bash
# Manual backup
docker compose exec mcp-server sqlite3 /app/data/mcp.db ".backup /app/data/mcp-backup-$(date +%Y%m%d).db"
# Copy backup to host
docker cp mcp-server:/app/data/mcp-backup-*.db ~/homelab-configs/backups/
```
**Makefile targets (to be created):**
- `make deploy` - Build and deploy
- `make logs` - Tail logs
- `make backup` - Backup database
- `make test` - Run test suite
- `make shell` - Interactive shell in container
### Deployment Constraints
- **No public internet exposure** (Tailscale only - this is intentional)
- **Single-user system** (no multi-tenancy needed)
- **Must handle Tailscale network hiccups gracefully** (retry logic, timeouts)
- **Playwright runs headless in Docker** (no X server, use `--no-sandbox` flag)
- **SQLite is perfect** for this use case (not high concurrency)
- **Container must run as non-root** user (security best practice)
## Current Development Phase
**Phase 1: Career Domain Foundation**
- [x] Docker environment setup
- [ ] MCP server scaffolding
- [ ] career.search_greenhouse_boards()
- [ ] Basic SQLite storage
- [ ] Integration test with real job board
**Next:** Add job tracking (write tools) and resume analysis
## File Organization
```
/
├── src/
│ └── mcp_server/ # Main application
│ ├── server.py # MCP server entry point
│ ├── domains/ # Domain implementations
│ │ ├── career/
│ │ ├── fitness/
│ │ └── family/
│ └── utils/ # Shared utilities
├── tests/ # Test suite
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── data/ # Runtime data (Git-ignored, in Docker volume)
│ ├── mcp.db # SQLite database
│ └── health-exports/ # Health Auto Export CSVs
├── config/ # Configuration (Git-ignored)
│ ├── .env # Credentials, API keys
│ └── .env.example # Template for .env
├── .claude/ # Claude Code configuration
│ └── commands/ # Custom slash commands
├── docker-compose.yml # Service definition
├── Dockerfile # Container image
├── Makefile # Common operations (to be created)
├── requirements.txt # Python dependencies
├── pyproject.toml # Project metadata
└── CLAUDE.md # This file
```
## Domain-Specific Notes
### Career Domain
**Real use case:** Actively job searching for Sales Engineering Leadership roles.
- Target companies: Series B-D SaaS (50-500 employees)
- Job boards: Greenhouse, Lever, custom career pages
- Background: Software Development, Cloud, Pre-Sales, Management
- Need: Early access to postings before LinkedIn spam
**Critical features:**
- Scraper must handle dynamic loading (Playwright, not BeautifulSoup)
- Track application status in SQLite (applied, interviewed, rejected, offer)
- Resume tailoring should emphasize conversational AI experience
- Store job descriptions for later analysis
**Data model:**
```python
class JobListing:
id: str
company: str
title: str
location: str
description: str
posted_date: datetime
url: str
source: str # greenhouse, lever, etc.
status: str # discovered, applied, interviewing, rejected, offer
```
### Fitness Domain
**Real use case:** Golf practice tracking and workout optimization.
- Data sources: Health Auto Export (CSV), Hevy app (CSV + API)
- Golf handicap: 26, target: 19
- Focus areas: Putting consistency, tee shot accuracy
**Critical features:**
- Parse complex Health Auto Export format (multiple CSV structures)
- Weekly progress summaries with trend analysis
- Suggest practice routines based on weaknesses
- Track workout volume and recovery
**Data model:**
```python
class WorkoutSession:
date: datetime
type: str # strength, golf, cardio
duration_minutes: int
exercises: List[Exercise]
notes: str
class GolfRound:
date: datetime
course: str
score: int
fairways_hit: int
greens_in_regulation: int
putts: int
```
### Family Domain
**Real use case:** Kids billing system (already built, integrating into MCP).
- Existing system: Google Sheets + n8n workflows
- Yay Lunch transactions (API scraping already built)
- Math Academy tracking (weekly email parsing)
**Critical features:**
- Bidirectional Google Sheets sync (read and write)
- Don't break existing workflows
- Maintain audit trail for all transactions
- Generate PDF invoices
**Data model:**
```python
class Transaction:
date: datetime
child: str # which kid
category: str # lunch, tutoring, activities
amount: Decimal
description: str
paid: bool
```
## Common Patterns
### Tool Naming Convention
`domain.action_target()`
Examples:
- `career.search_greenhouse_boards()`
- `fitness.get_workout_history()`
- `family.generate_invoice()`
### Error Handling
All tools return Result types for better error messages in Claude:
```python
from typing import Union, TypeVar, Generic
from dataclasses import dataclass
T = TypeVar('T')
@dataclass
class Ok(Generic[T]):
value: T
@dataclass
class Err:
error: str
Result = Union[Ok[T], Err]
@mcp.tool()
async def search_greenhouse_boards(company: str) -> Result[List[JobListing]]:
"""Search a company's Greenhouse job board for open positions."""
try:
jobs = await scraper.get_jobs(company)
return Ok(jobs)
except ScraperError as e:
logger.error(f"Scraper failed for {company}: {e}")
return Err(f"Could not access {company}'s careers page. They may be using a different platform.")
except Exception as e:
logger.exception(f"Unexpected error: {e}")
return Err("An unexpected error occurred. Check logs for details.")
```
### Caching Strategy
```python
from functools import lru_cache
from datetime import datetime, timedelta
# Simple in-memory cache with TTL
_cache = {}
_cache_times = {}
def cached(ttl_seconds: int):
def decorator(func):
async def wrapper(*args, **kwargs):
key = f"{func.__name__}:{args}:{kwargs}"
# Check if cached and not expired
if key in _cache:
cached_time = _cache_times[key]
if datetime.now() - cached_time < timedelta(seconds=ttl_seconds):
logger.debug(f"Cache hit: {key}")
return _cache[key]
# Cache miss or expired
logger.debug(f"Cache miss: {key}")
result = await func(*args, **kwargs)
_cache[key] = result
_cache_times[key] = datetime.now()
return result
return wrapper
return decorator
# Usage
@cached(ttl_seconds=3600) # 1 hour
@mcp.tool()
async def search_greenhouse_boards(company: str) -> Result[List[JobListing]]:
# Implementation
pass
```
### Database Patterns
```python
import sqlite3
from contextlib import contextmanager
@contextmanager
def get_db():
"""Context manager for database connections."""
conn = sqlite3.connect("/app/data/mcp.db")
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# Usage
with get_db() as conn:
cursor = conn.execute("INSERT INTO jobs (company, title) VALUES (?, ?)",
(job.company, job.title))
```
## Learning Objectives
This project teaches:
- ✅ MCP server architecture and tool design
- ✅ Multi-domain service organization
- ✅ Async Python patterns (asyncio, Playwright)
- ✅ Web scraping at scale with dynamic content
- ✅ API integration (REST, OAuth)
- ✅ Docker deployment and networking
- ✅ Error handling strategies in async code
- ✅ Testing async code with real external services
- ✅ SQLite for embedded databases
- ✅ Logging and observability
## Getting Help
- **MCP SDK docs:** https://modelcontextprotocol.io/
- **Playwright async:** https://playwright.dev/python/docs/async
- **Pydantic v2:** https://docs.pydantic.dev/latest/
- **Ask Claude Code:** It has access to this file!
## Important Reminders
- **Start simple, add complexity only when needed**
- **Commit after each working feature** (atomic commits)
- **Use `/compact` at natural breakpoints** to keep context manageable
- **Test in Docker before assuming it works** (Playwright setup differs)
- **This is a learning project** - document discoveries and ask questions!
- **Security:** Never commit `.env`, always use environment variables
- **Performance:** Profile before optimizing, measure after
- **Documentation:** Update this file as the project evolves