Almanac MCP Server
Self-hosted fitness tracking MCP server that gives AI assistants access to your nutrition, training, weight, sleep, and accomplishment data via 77 tools and 5 resources. Enables natural-language logging and querying of personal health metrics through Claude or ChatGPT.
README
Almanac
A precise, self-hosted fitness record your own AI agent can read.
Almanac keeps an accurate account of what you eat, lift, weigh, and sleep, then hands it to the assistant you already use over MCP. Ask how your cut is going or what to train today, and the answer comes from your own numbers, not a guess. It runs on your hardware, so the record stays yours.
No outside assistant needed: the web dashboard does everything, and two chat surfaces are built in — an AI meal assistant that turns "chicken burrito bowl" into editable macros, and a read-only insights coach. One API, one SQLite file, so every surface sees the same data.
Screenshots
The web dashboard. Nutrition phase and TDEE, today's macros and meals, a seven-day macro grid, weight trend, sleep, and the training panel with a recommended session.
<p align="center"> <img src="screenshots/demo-dashboard.png" alt="Almanac dashboard" width="984"> </p>
The built-in AI meal assistant. Describe what you ate and it returns editable entries, looking up unfamiliar foods and asking about portion size when it changes the math.
<p align="center"> <img src="screenshots/demo-meal-lookup.png" alt="AI meal assistant parsing a meal description into editable macros" width="984"> </p>
The AI insights coach. A read-only read of your logged history: nutrition adherence, TDEE drift, training volume, and sleep.
<p align="center"> <img src="screenshots/demo-insights-chat.png" alt="AI insights coach summarizing nutrition, training, and sleep" width="984"> </p>
Logging and reviewing through an outside assistant over MCP:
| <img src="screenshots/chat3.png" alt="Training recommendation and accomplishments" width="420"> | <img src="screenshots/chat4.png" alt="Resistance training volume analysis" width="420"> |
| <img src="screenshots/chat2.png" alt="Cut progress overview" width="420"> | <img src="screenshots/chat1.png" alt="Daily check-in" width="420"> |
Architecture
Browser ──► nginx ──► oauth2-proxy ──► almanac-web (Vue 3 SPA)
│
├──► almanac-api (Fastify + SQLite)
│
Claude / ChatGPT ─────────┴──► almanac-mcp (77 tools, 5 resources)
│
└──► almanac-api
Four containers behind nginx. oauth2-proxy handles browser SSO (Google, GitHub, or any OIDC provider it supports); MCP traffic bypasses SSO and authenticates via OAuth 2.1 or personal access tokens (PATs). All auth paths converge on the same PAT format stored in SQLite.
See reference/mcp-auth.md for the full authentication architecture.
Features
Nutrition
- Meal logging — log meals with kcal, protein, carbs, and fats. Edit, delete, and review meals by date.
- Stored meals — save a meal definition once (name + macros) and log it as eaten in one tap.
- Nutrition phases — cut, bulk, or maintenance phases with daily kcal targets (static or TDEE-relative) and macro splits. The create form suggests a split from your bodyweight and target; a guided cold-start collects what a TDEE estimate needs.
- Macro analytics — today's totals vs. target, historical summaries by date or range, rolling 7-day averages, and on-track / at-risk / off-track verdicts.
- TDEE estimation — three-tier basis: profile baseline (Mifflin BMR × activity), measured intake (back-calculated from weight trend), or user assertion. Calibrates from baseline to measured over ~14 days of weigh-ins.
Body composition
- Weight logging — daily weigh-ins with optional notes.
- Weight trend — exponential-weighted moving average with 14-day and 30-day views, change rate, and confidence levels.
Training
- Workout templates — build reusable templates with ordered exercises and defaults, directly in the app or via an assistant. Start a session from a template, then skip, override, or add exercises as you go. Starter programs (Push/Pull/Legs, Upper/Lower) seed a whole split for new users.
- Set tracking — log reps, weight, and RPE (1–10) per set. Duration and estimated kcal per workout.
- Exercise library — custom exercises organized into muscle groups. Archive exercises you no longer use.
- Stim state / recovery — per-muscle-group recovery tracking (0–100) with phase classification (too_soon → prime → detrained). Multi-phase decay model with hours-since-last and trainable-capacity signals.
- Workout recommendations — template recommendation engine scores which workout to do next based on current recovery state across all muscle groups.
Cardio, steps, and alcohol
- Cardio — log sessions by modality (bike, run, ruck, etc.) with duration, distance, average HR, and estimated kcal (Keytel / METs formulas).
- Steps — daily step count with automatic kcal estimation. Override estimates when you have better data.
- Alcohol — session-based logging (start/end, drink count in US standard drinks, kcal estimate). Overlays onto daily energy balance.
Sleep
- Sleep logging — hours and quality (1–5) per night, with timezone-aware midnight crossing.
- Sleep debt — rolling debt calculation over a configurable window (default 14 days) against a baseline.
Accomplishments
- Wins — milestones derived automatically from your logs: logging and workout streaks, calorie-adherence streaks, body-weight milestones off the smoothed trend, strength PRs, sleep recovery, and the moment your TDEE flips to measured. Each shows its previous best and earns the moment a log completes it. Lifetime milestones (100th workout, total kilograms lifted, meals and weigh-ins logged) are backdated to the day you crossed them.
Web UI
- Daily dashboard — a calorie ring and draining protein/carb/fat bars, current vs. phase TDEE, deficit/surplus and on-target adherence, today's meals and movement, weekly macro grid, weight sparkline, sleep debt, phase progress, and an earned-wins section above the workout picker.
- Editable cards — meals, weight, sleep, cardio, and steps are all add/edit/delete directly in the dashboard, not just in chat. Edits update the ring, bars, week grid, and trends right away.
- Phase controls — start, edit, and stop a nutrition phase from the dashboard, with a live TDEE estimate and macro suggestions in the create form.
- Workout panel — template picker, build/edit templates and starter programs, active session with live set entry, add-exercise-mid-session, end/save dialog, and last-session reference.
- Calendar — month view in Workouts or Intake mode. Workouts mode shows per-template tallies, recovery pills (too_soon, prime, etc.), and a forward recommendation window; Intake mode tints each day by adherence. Tap a day (or step with
‹ ›) to view and edit any past day; the URL reflects the day (?date=…). - Copy stats for LLM — one button copies a full markdown briefing of your current picture (phase, TDEE, today, trends, recent workouts, a 14-day history table) to paste into any chat.
- AI Meal Assistant — an in-app chat panel where you describe what you ate and get editable proposal cards to log. It matches your stored-meal library first, estimates otherwise, and can web-search unfamiliar foods. A daily token budget shows "~N logs left"; both budget and search have configurable caps. Optional — see the LLM
.envsection. - AI insights coach — a second panel that reads your logged history back to you: nutrition adherence, TDEE drift, training volume and split balance, and sleep. Read-only by design (no write tools, no web search). Transcripts persist per day with
◀ ▶navigation, and opening a fresh day auto-asks for a quick read. Runs a stronger model than the meal parser (ALMANAC_LLM_INSIGHTS_MODEL). - Settings — profile editing, activity level, timezone and unit (metric/imperial) selectors, PAT creation/revocation, and the MCP URL for connecting an assistant.
- Mobile responsive — single 768 px breakpoint, swipeable panels via CSS scroll-snap, contextual sticky header, and 36 px touch targets.
MCP integration
- 77 tools, 5 resources — full CRUD for every entity (including stored meals and
log_meal_from_stored), plus derived signals (stim state, TDEE, sleep debt, day status, calendar),get_next_best_actionfor onboarding/next-step guidance, andget_accomplishmentsso an assistant can surface your earned wins in chat. - Works with Claude (mobile, Desktop, Code) and ChatGPT — say "log a 350 kcal breakfast" and it shows up in the web UI.
- OAuth 2.1 — Claude mobile and ChatGPT connect through the standard MCP OAuth flow, using whichever SSO provider you configured. No manual token setup.
- PAT auth — personal access tokens for Claude Code or any HTTP client.
- Idempotent logging — safe to retry meal, weight, and sleep log calls.
Auth
- Three-layer allowlist — oauth2-proxy (browser SSO), API (account provisioning), and MCP (OAuth flow) all enforce the same
allowed-users.txtfile. - OAuth tokens are real PATs — minted via the API, stored in SQLite, visible and revocable in the web Settings panel.
- Per-user data isolation — every record is scoped to its owner. Reads and writes are enforced against the authenticated user at the data layer, so one account never sees or touches another's data.
Requirements
- Node 20 or newer
- pnpm 9 or newer
- Docker (for production; optional for local dev)
SQLite ships bundled via better-sqlite3.
Quickstart — local dev
The local dev script starts all services (API, web, MCP, oauth2-proxy) in one command:
1. Install
pnpm install
2. Configure .env
cp .env.example .env
Edit .env and set the required Google OAuth credentials and your dev email. See .env.example for documentation on each variable. The stack ships configured for Google; to use another provider, see the oauth2-proxy provider docs.
3. Start everything
scripts/local-dev/up.sh
This starts:
- almanac-api on
:3001(Fastify, trusts proxy headers) - almanac-web on
:5173(Vite dev server) - almanac-mcp on
:3030(Streamable HTTP + OAuth 2.1) - oauth2-proxy on
:4180(Docker container, SSO)
Stop everything with scripts/local-dev/down.sh.
Without Docker (no Google OAuth)
If you don't need the real Google sign-in path, skip .env, docker, and
oauth2-proxy entirely:
scripts/local-dev/dev-noauth.sh you@example.com # web on 127.0.0.1
scripts/local-dev/dev-noauth.sh you@example.com --lan # web on 0.0.0.0 (other devices)
This runs the API + web with header-trust auth: the Vite dev proxy injects
the x-forwarded-email header that oauth2-proxy would emit in prod, so the UI
needs no login and acts as the email you pass. Migrations run automatically on
API boot. Ctrl-C stops both.
--lancaveat: binding the web server to0.0.0.0means anyone on your network is authenticated as that email. Use it only on a trusted network.
For MCP in this mode, run it in stdio transport against the local API (mint a PAT in the web Settings panel first) — the script prints the exact command on startup.
Demo instance (populated with fake data)
To see the UI fully populated — every panel non-empty, both AI surfaces unlocked — without touching your real data:
scripts/local-dev/demo.sh # 127.0.0.1
scripts/local-dev/demo.sh --lan # LAN, for phone testing
scripts/local-dev/demo.sh --days 90 # longer history
This seeds a throwaway SQLite file and runs the API + web on :3099/:5199, so
it can run alongside your normal dev stack. The data is anchored relative to
today (an active cut phase, 40 days of meals, weigh-ins, sleep, steps, a PPL
split with session history), so it never goes stale. It sources .env for
ANTHROPIC_API_KEY — without one the UI still renders but the AI panels report
llm_available: false. Ctrl-C stops it; rm -f /tmp/almanac-demo.sqlite* to
delete the data.
Screenshots
scripts/local-dev/screenshot.mjs captures the running UI headlessly, driving
your system Chrome via playwright-core (no bundled browser download). Capture
height is independent of your display, so a full-page dashboard shot works on
any screen:
node scripts/local-dev/screenshot.mjs # full dashboard
node scripts/local-dev/screenshot.mjs --preset both # desktop + mobile
node scripts/local-dev/screenshot.mjs --scene meal-lookup # AI modal (real LLM call)
Defaults to 984 px wide at 1×, matching the screenshots above. --scene clicks a
modal open before capturing; --help lists the available scenes.
4. Connect Claude Code to MCP
Register the MCP server by URL:
{
"mcpServers": {
"almanac": {
"type": "url",
"url": "https://almanac.example.com/mcp",
"headers": {
"Authorization": "Bearer alm_XXXXX"
}
}
}
}
For local dev with a PAT, point at http://localhost:4180/mcp. For OAuth-capable clients (Claude mobile, ChatGPT), just use the public URL — the OAuth flow handles everything automatically.
5. Verify
Open Claude Code. The almanac tools should show up under the almanac server. Ask Claude to "log a 350 kcal breakfast" — the meal should appear in the web UI and via get_macros_today.
.env reference
Core
| Variable | Purpose | Required when |
|---|---|---|
ALMANAC_DB_PATH |
SQLite file location | always |
ALMANAC_API_PORT / _HOST |
Where API listens | always |
ALMANAC_API_URL |
Where MCP reaches API | always |
ALMANAC_TRUST_PROXY_HEADERS |
API trusts X-Forwarded-Email from oauth2-proxy |
behind a proxy |
ALMANAC_ALLOWED_EMAILS |
Email allowlist — file path or comma-separated. Shared by the API and MCP server. | production |
ALMANAC_WEB_PORT |
Port the Vite dev server binds (default 5173). Set when running two stacks side by side. |
local dev |
ALMANAC_DEV_EMAIL |
Email the Vite dev proxy injects as X-Forwarded-Email. The API auto-provisions this user. |
local dev |
ALMANAC_LOG_LEVEL |
Pino level override (fatal…silent). Unset uses debug in dev, info in prod. |
optional |
MCP
| Variable | Purpose | Required when |
|---|---|---|
ALMANAC_MCP_TRANSPORT |
stdio, http, or sse (legacy) |
always |
ALMANAC_MCP_PORT / _HOST |
Where MCP listens (HTTP/SSE only) | http/sse |
ALMANAC_MCP_CLIENT_TOKEN |
Static PAT for stdio transport | stdio |
OAuth 2.1 (MCP + browser SSO)
The stack ships configured for Google as the SSO provider, so the variables below name Google credentials. oauth2-proxy also supports GitHub, GitLab, and any generic OIDC provider — swap --provider in docker-compose.yml and supply that provider's client ID and secret in the same variables.
| Variable | Purpose | Required when |
|---|---|---|
OAUTH2_PROXY_CLIENT_ID |
Google OAuth client ID (shared by oauth2-proxy and MCP) | production |
OAUTH2_PROXY_CLIENT_SECRET |
Google OAuth client secret | production |
OAUTH2_PROXY_COOKIE_SECRET |
oauth2-proxy session cookie encryption key | production |
OAUTH2_PROXY_REDIRECT_URL |
oauth2-proxy callback URL (https://domain/oauth2/callback) |
production |
ALMANAC_MCP_OAUTH_CLIENT_ID |
Google client ID for MCP OAuth (typically ${OAUTH2_PROXY_CLIENT_ID}) |
MCP OAuth mode |
ALMANAC_MCP_OAUTH_CLIENT_SECRET |
Google client secret for MCP OAuth | MCP OAuth mode |
ALMANAC_MCP_PUBLIC_URL |
Public URL for MCP OAuth issuer (https://domain) |
MCP OAuth mode |
ALMANAC_FIRST_LOGIN_EMAIL |
One-shot: binds an existing email-less users.id = 1 to this email on first boot |
only when adopting a pre-auth database |
When do you need
ALMANAC_FIRST_LOGIN_EMAIL? Almost never. When you sign in through OAuth or oauth2-proxy, the API provisions your account from your verified email automatically — no setup. This variable only matters when ausers.id = 1row already exists without an email, which happens if data was created before going through the auth flow: a local-dev database, or records written directly via the API/MCP during early setup. Setting it links that orphaned row to your real email on the next boot, so signing in resolves to your existing data instead of creating a fresh, empty account. On a clean database, or if you only ever use OAuth, you never set it. It's a one-shot: confirm the binding, then remove the var.
Watchtower auto-deploy notifications (deploy-only, optional)
The watchtower compose service emails on container updates/errors via shoutrrr SMTP. Set WATCHTOWER_EMAIL_TO to turn notifications on; leave it blank and watchtower runs silently.
| Variable | Purpose | Default |
|---|---|---|
WATCHTOWER_EMAIL_TO |
Recipient. Blank = notifications off | unset (silent) |
WATCHTOWER_EMAIL_FROM |
Sender address | — |
WATCHTOWER_EMAIL_SERVER |
SMTP host | — |
WATCHTOWER_EMAIL_PORT |
SMTP port | 25 |
WATCHTOWER_EMAIL_HELO |
HELO/EHLO hostname — must be an FQDN; a strict postfix rejects shoutrrr's localhost default with 504 5.5.2 … need fully-qualified hostname |
— |
LLM / AI surfaces (optional)
Read by the API only. Both AI surfaces — the AI Meal Assistant and the AI insights coach — are gated behind the same ALMANAC_LLM_ENABLED switch (off by default), so they stay dark until explicitly turned on. The prod docker-compose.yml already forwards these from the host .env to the almanac-api service.
The two surfaces use separate models: meal parsing is a cheap extraction task and stays on Haiku, while the coach does harder multi-signal reasoning and defaults to Sonnet.
| Variable | Purpose | Default |
|---|---|---|
ALMANAC_LLM_ENABLED |
Master switch for both AI surfaces (meal chat + insights coach) | false |
ANTHROPIC_API_KEY |
Anthropic key. Without it both AI surfaces are hidden (llm_available=false) |
— |
ALMANAC_LLM_PROVIDER |
Provider seam. Only anthropic is supported; any other value fails at boot |
anthropic |
ALMANAC_LLM_MODEL |
Model for the meal assistant (the cheap parser) | claude-haiku-4-5 |
ALMANAC_LLM_INSIGHTS_MODEL |
Model for the insights coach — harder reasoning, so a stronger default | claude-sonnet-4-6 |
ALMANAC_LLM_DEFAULT_DAILY_TOKEN_LIMIT |
Soft daily token limit — drives "~N logs left"; warns but never blocks | unset (no soft limit) |
ALMANAC_LLM_HARD_DAILY_TOKEN_CAP |
Hard daily token ceiling — 429 circuit-breaker | unset (no hard cap) |
ALMANAC_LLM_TOKENS_PER_SEARCH |
Flat token charge per web search when no recent search history to average | 2500 |
ALMANAC_LLM_HARD_DAILY_SEARCH_CAP |
Max web searches per user-local day; at the cap search is disabled but meals still log | unset (uncapped) |
Turning it on also needs: (1) the per-user flag
llm_logging_enabled = 1(admin API/MCP tool, orUPDATE users SET llm_logging_enabled = 1 WHERE email = '…'), and (2) web search enabled for your org in the Anthropic Console (Settings → Privacy). Web searches draw a flat charge from the same daily token budget; the real token cost is still recorded but the budget is billed the flat per-search amount.
Production deployment
Almanac runs as four Docker Compose services (web, API, MCP, oauth2-proxy) behind host nginx with TLS. Only oauth2-proxy binds a host port; the three almanac services stay internal to the Docker network. Images build in CI and publish to GHCR on each release tag, so the server pulls prebuilt images and never builds locally, with an optional watchtower service for hands-off updates.
The email allowlist is enforced at three independent layers, all reading the
same allowed-users.txt: oauth2-proxy for browser traffic, the API for account
provisioning, and the MCP server during the OAuth flow.
Full walkthrough (DNS, TLS, nginx, first boot, updates, backups, rollback) is in the deploy runbook.
Testing
pnpm -r test # full test suite across all packages
pnpm -r typecheck # tsc --noEmit workspace-wide
pnpm lint # Biome
pnpm format # Biome
Per-package: pnpm --filter @almanac/<pkg> test.
Troubleshooting
- API returns 403 for a new user. The email is not in
allowed-users.txt. Add it and restart the API container (or wait for oauth2-proxy to hot-reload the file). ALMANAC_DB_PATHended up in the wrong place. Classicpnpm --filtercwd trap — relative path resolved againstpackages/<pkg>/, not the workspace root. Use an absolute path.
Project layout
almanac/
├── packages/
│ ├── core/ # SQLite, migrations, repos, domain types, signals, schemas
│ ├── api/ # Fastify HTTP server, zod request/response validation
│ ├── mcp/ # MCP server, MCP tools + resources, OAuth 2.1
│ └── web/ # Vue 3 SPA, Vite, Pinia
├── deploy/ # nginx config, post-migration smoke test
├── docs/ # showcase landing page (GitHub Pages)
├── reference/ # MCP architecture + authentication reference
├── scripts/
│ └── local-dev/ # up.sh / down.sh, dev-noauth.sh, demo.sh, screenshot.mjs
├── docker-compose.yml
├── allowed-users.txt # shared email allowlist
├── .env.example
└── pnpm-workspace.yaml
License
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.