Skip to content

Repository files navigation

Coach

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.

Why

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

Architecture (one screen)

                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.

Stack

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

17 MCP tools, grouped

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

Repo layout

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

Setup (your own deployment)

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 + web

For 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.

Two things this project taught me

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.

License

MIT. Use, fork, learn. Health data is yours; the code is yours under MIT.

About

Personal sport performance coaching agent. MCP server + OAuth 2.1 + multi-source health-data resolver + literature retrieval, talking to Claude. Open-sourced showcase of a working deployment.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages