A personal sport-performance coaching agent that combines my training, sleep, HRV, and nutrition data with on-demand retrieval from scientific literature — built end-to-end and reachable from the Claude iOS app over an authenticated, MCP endpoint.
Status: working personal deployment. This repo is the open-sourced, sanitized version for reference. The system runs on my Mac mini, ingests from Strava every 15 minutes, and is the thing I actually talk to about training.
Most fitness chatbots are surface-level: "you slept 6h, aim for 8." I wanted an agent that:
- Knows me — longitudinal access to every workout, sleep night, HRV reading, and nutrition log
- Is scientifically grounded — pulls from PubMed / Semantic Scholar / OpenAlex, grades evidence quality, scores per-paper applicability to my profile, and synthesizes a tiered verdict instead of citation-dumping
- Is honest about uncertainty — every prescription is labeled
Established/Likely/Worth trying/Mixed/Folk - Lives on my hardware — health data never leaves my Mac mini except as the prompts I send to Claude
Claude iOS / desktop / web
│
OAuth 2.1 + DCR + PKCE
│
┌───────────────────┴───────────────────┐
│ Tailscale Funnel (TLS, public edge) │
└───────────────────┬───────────────────┘
│ Tailscale-Funnel-Request: ?1
┌─────────────▼─────────────┐
│ Caddy on 127.0.0.1:8080 │ ← path ACL: /mcp, /oauth, /.well-known/* public;
└─────┬──────────┬──────────┘ /api, / (PWA) tailnet-only
│ │
┌───────────▼─┐ ┌───▼──────────┐ ┌─────────────┐
│ MCP server │ │ Hono REST │ │ Next.js PWA │
│ bun :8002 │ │ API :8001 │ │ :8003 │
└──────┬──────┘ └──────┬───────┘ └──────┬──────┘
│ 17 tools │ │
│ bearer auth │ SSE invalidate │
▼ ▼ │
┌──────────────────────────────────┐ │
│ coach.db (workouts, sleep, │◄─────────┘
│ HRV, nutrition, │
│ profile, oauth, │
│ audit) │
│ evidence.db (papers, claims, │
│ gradings, │
│ applicability) │
└──────────────┬───────────────────┘
│
┌────────▼────────┐
│ Ingester │ → every 15min: Strava (real), Apple Health (XML import),
│ (Effect │ Manual (PWA), Whoop/Oura/Coros (stubs ready for creds)
│ Schedule) │
└─────────────────┘
See ARCHITECTURE.md for the resolver pattern, OAuth implementation, and Caddy path-ACL details.
| Layer | Choice |
|---|---|
| Runtime | Bun |
| Language | TypeScript end-to-end |
| Effect system | effect (Schedule, Stream, typed errors) |
| API | Hono + Zod |
| MCP | @modelcontextprotocol/sdk over node:http |
| OAuth | Hand-rolled OAuth 2.1 + DCR + PKCE + RFC 9728 protected-resource metadata |
| Database | SQLite × 2 via Drizzle, WAL mode |
| Frontend | Next.js 15 PWA, TanStack Query + Persist, Recharts |
| Edge | Tailscale Funnel (TLS) + Caddy (path-based ACL) |
| Backups | Litestream → Backblaze B2 + iCloud |
| Process supervisor | macOS launchd |
Read ping · list_sources · query_canonical · get_profile
list_papers · list_pending_uploads · fetch_pending_upload
search_literature · fetch_paper · what_do_we_know
Write upsert_profile · log_subjective · log_meal · prescribe
index_paper · set_source_state · sync_source
apps/
api/ Hono REST API + OAuth 2.1 server
mcp/ MCP server + bearer-auth middleware
ingester/ APScheduler-equivalent in Effect
web/ Next.js 15 PWA
packages/
db/ Drizzle schemas + migrations
core/ Source plugin interface, resolver, evidence, ingest
config/
sources.yaml Runtime state for sources
coach/
system-prompt.md The persona definition (customize for yourself)
project-instructions.md Claude Project setup
ops/
caddy/ Path-ACL Caddyfile
launchd/ 6 user-agent plists
litestream.yml
scripts/
strava-auth.ts One-time OAuth helper
seed-library.ts Bootstrap evidence DB with curated reviews
smoke-{resolver,evidence,oauth}.ts End-to-end smoke tests
bun install
cp .env.example .env # fill in Strava + a passphrase (>=16 chars)
bun run strava:auth # one-time OAuth, paste refresh token
bun run db:generate
bun run db:migrate
bun run seed:library # 15 curated reviews (ISSN, ACSM, AASM, …)
bun run dev # api + mcp + ingester + webFor public iOS-app access via Tailscale Funnel + Caddy, see the section in ARCHITECTURE.md.
Then in the Claude app → Settings → Connectors → Add custom:
- URL:
https://<host>.<your-tailnet>.ts.net/mcp - OAuth is discovered automatically; type your passphrase once on the consent page.
Tailscale's serve vs funnel per-path split is a fiction. I expected tailscale serve --set-path=/api … to keep /api private while tailscale funnel --set-path=/mcp … exposed only /mcp publicly. Empirically: once Funnel is enabled on a hostname, every Serve-configured path becomes publicly reachable. I confirmed by curling /api/health from cellular with Tailscale off — got a 200 with health data. The architecture pivoted to Caddy on 127.0.0.1:8080 enforcing the ACL itself, matching the Tailscale-Funnel-Request header that Tailscale unconditionally adds to public requests. The lesson generalises: trust nothing about a security boundary until you've tested the negative case.
MCP OAuth discovery requires RFC 9728 protected-resource metadata, not just RFC 8414. Adding the auth server metadata at /.well-known/oauth-authorization-server wasn't enough — Claude's MCP client could authenticate but couldn't discover where the auth server lived from the resource server. The fix was a second metadata endpoint at /.well-known/oauth-protected-resource plus a resource_metadata=<URL> parameter on the 401's WWW-Authenticate header. Without it, the connector errors with a misleading "Couldn't reach the MCP server" — the server was reached; the client just couldn't bootstrap the auth flow. The MCP SDK doesn't generate this for you.
MIT. Use, fork, learn. Health data is yours; the code is yours under MIT.