Metadata-Version: 2.5
Name: caddie-mcp
Version: 1.0.1
Summary: LLM-optimized MCP connector for Garmin golf and health data
Author: Marcos Briones Alvarez
License: MIT
License-File: LICENSE
Keywords: claude,garmin,golf,llm,mcp,strokes-gained
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.12
Requires-Dist: duckdb>=1.1.0
Requires-Dist: garminconnect>=0.3.8
Requires-Dist: mcp>=1.2.0
Requires-Dist: pydantic>=2.8
Requires-Dist: pytz
Description-Content-Type: text/markdown

# Caddie

**The AI caddie that has actually seen every shot you've hit.**

Caddie is an MCP server that connects Claude (or any MCP-capable LLM) to your
Garmin golf data — every round, every GPS shot, strokes gained, real club
distances, plays-like yardages, and the sleep/recovery context behind each
score. Ask anything: *"why do I blow up on par 5s?"*, *"map my drives at my
home course"*, *"does bad sleep actually cost me strokes?"*

- **Local-first and private**: your data syncs once into a DuckDB file on
  your machine; credentials and health data never leave it.
- **Unofficial**: Caddie uses the same private API as the Garmin Connect
  app (via [python-garminconnect](https://github.com/cyberjunky/python-garminconnect)).
  It reads your own account only — and it can break whenever Garmin changes
  things, until the ecosystem catches up. Know that going in.
- **Support**: best-effort, issues and PRs welcome. By the maker of OneClub.

## How it works

```
Garmin Connect  ──sync──►  local DuckDB cache  ──MCP tools──►  your LLM
```

Your data is synced once into a local database (`~/.caddiemcp/golf.duckdb`)
and every question is answered from the cache — fast, private, and kind to
Garmin's servers. Credentials never leave your machine.

## Setup

**From source** (this directory):

```bash
python3 -m venv .venv && .venv/bin/pip install -e .
.venv/bin/caddie-mcp login              # one-time; MFA supported
.venv/bin/caddie-mcp sync --max-rounds 50
```

Prefer a browser to a terminal? `caddie-mcp login --web` opens a local
page (127.0.0.1 only) with proper email/password/MFA fields — same token
store, friendlier surface.

**From the built wheel** (no checkout needed — `dist/` ships both):

```bash
pipx install ./dist/caddie_mcp-1.0.0-py3-none-any.whl   # or pip install
caddie-mcp login && caddie-mcp sync
```

Once published to PyPI, this becomes `uvx caddie-mcp` / `pipx install caddie-mcp`.

### Claude Desktop / Claude Code

Add to `claude_desktop_config.json` (or `.mcp.json` for Claude Code):

```json
{
  "mcpServers": {
    "caddie-mcp": {
      "command": "/ABSOLUTE/PATH/TO/caddie-mcp",
      "args": ["serve"]
    }
  }
}
```

Use `which caddie-mcp` (pipx) or `/path/to/caddie-mcp/.venv/bin/caddie-mcp`
(source install) for the command path. This repo's `.mcp.json` already wires
it up for Claude Code sessions started in this folder.

### Remote server (multi-user, HTTP)

The same server runs over streamable-HTTP with bearer-token auth and full
per-user isolation — each user gets their own database and Garmin token
store under `~/.caddiemcp/users/<name>/` (or `$CADDIEMCP_HOME`).

```bash
caddie-mcp user add marcos          # prints the access token (shown once)
caddie-mcp serve-http --host 0.0.0.0 --port 8724
```

Users link their own Garmin account in a browser — no admin terminal
needed: send them `https://your-host/garmin/login`, where they enter their
access token plus Garmin credentials (two-step MFA supported); tokens land
in their private server directory. Admin-assisted
`caddie-mcp user login <name>` still works as a fallback.

Clients connect to `http://host:8724/mcp` with header
`Authorization: Bearer <token>` — e.g. for Claude Code:

```bash
claude mcp add --transport http golf http://host:8724/mcp \
  --header "Authorization: Bearer <token>"
```

Docker:

```bash
python -m build                     # wheel must exist in dist/
docker build -t caddie-mcp .
docker run -d -v caddie_data:/data -p 8724:8724 caddie-mcp
docker exec -it <container> caddie-mcp user add marcos
docker exec -it <container> caddie-mcp user login marcos
```

Security notes: tokens are 256-bit secrets stored only as SHA-256 hashes and
compared in constant time; rotate with `caddie-mcp user rotate-token <name>`.
Run behind TLS (Caddy/nginx/Cloudflare Tunnel) — tokens travel in headers.
`--no-auth` mode exists for localhost-only single-user use and refuses to
bind to non-loopback addresses. `/health` is the only unauthenticated route.
The `/data` volume contains Garmin credentials — treat backups accordingly.

### claude.ai custom connectors (OAuth)

claude.ai connects to remote MCP servers via OAuth (dynamic client
registration + PKCE), not bearer headers. Enable it with:

```bash
caddie-mcp serve-http --host 0.0.0.0 --oauth \
  --public-url https://golf.example.com
```

Then add `https://golf.example.com/mcp` as a custom connector in claude.ai.
On first connect the user lands on a consent page and pastes their
caddie-mcp access token once; after that, OAuth tokens (1 h access +
rotating refresh, stored hashed) keep the connection alive, scoped to that
user's data. Static bearer tokens keep working alongside OAuth. A real
domain with TLS is required — claude.ai will not talk to plain HTTP.

### Background sync

In server mode, keep every linked user fresh automatically:

```bash
caddie-mcp serve-http --host 0.0.0.0 --sync-every 6h
```

Users are synced sequentially (incremental, with jitter and per-user error
isolation) so the Garmin API never sees a thundering herd.

### Claude Desktop Extension

`python scripts/build_mcpb.py` produces `dist/caddie-mcp.mcpb` — a
double-click install bundle for Claude Desktop that launches the published
package via `uvx`. Requires [uv](https://docs.astral.sh/uv/) on the machine
and the package on PyPI; the one-time `uvx caddie-mcp login` still happens
in a terminal (credentials never pass through the desktop app).

### Publishing to PyPI (maintainer)

```bash
.venv/bin/python -m build          # sdist + wheel into dist/
pipx run twine upload dist/*       # needs a PyPI API token
```

### Data quality & repair

`caddie-mcp repair` re-derives every computed field across the cache with no
Garmin calls (club names, lie/shot-type normalization, GPS-to-pin distances,
GIR/fairway inference). It runs automatically at the end of every sync, so
you only need it manually after upgrading the connector.

Known Garmin quirks handled: watches report `TEEBOX`/`TEE`/`UNKNOWN` enums;
putts are often a per-hole count with no green shots; the end position of
green-reaching shots is snapped to the pin (so post-shot proximity on green
hits is honestly NULL, and strokes-gained putting uses a flagged estimate).

## Tools

| Tool | What it answers |
|---|---|
| `garmin_status` | Am I connected? How much is cached vs still on Garmin? |
| `sync_garmin_data` | Pull rounds/shots/clubs from Garmin — chunked (25/call, reports `rounds_remaining`) so full multi-year histories load safely from chat |
| `list_rounds` | "Show my recent rounds" — compact summaries |
| `get_round` | One round in depth: scorecard, holes, shots |
| `get_shots` | Shot-level GPS/club/lie data; `format="geojson"` for maps |
| `get_club_stats` | Real measured distances per club vs Garmin's estimates |
| `get_strokes_gained` | Broadie strokes gained per round: off-tee / approach / short game / putting, vs your handicap level or scratch |
| `get_performance_stats` | ~40 PGA-style stats (driving, GIR by distance, scrambling, putting, scoring) with optional handicap benchmarks |
| `get_handicap` | WHS-style index, counting differentials, and trend |
| `get_course_intel` | Per-hole difficulty and usual tee club at one course |
| `get_health_context` | Sleep, HRV, Body Battery, readiness, and stress for a round's date |
| `correlate_health_performance` | "Do I score worse on bad sleep?" — paired data + honest correlation |
| `query_golf_data` | Any question, as read-only SQL over the cache |

Sync also fills `daily_health` (4 extra Garmin calls per round date):
sleep score/duration, resting HR, HRV status, Body Battery range, training
readiness, stress, steps — **plus round-window enrichment** on each round:
Body Battery at tee time and drain during play, average/max heart rate and
stress *during the round*, and the round's weather (temperature, wind,
humidity, conditions) via its matched Garmin activity.

When Garmin sends no dispersion data, shot miss offsets (dx/dy) are derived
from GPS geometry: drives laterally vs the tee→pin line, approaches/chips
relative to the pin — powering miss-direction and miss-tendency stats on any
watch with shot GPS.

Every shot also gets **plays-like distance**: terrain elevations for shot
and pin positions come from the free Open-Meteo elevation API (cached
forever in the local DB), and `plays_like_yds` applies the standard 1:1
uphill/downhill heuristic — so "my 150-yd shots" can be analyzed by what
they actually played as.

**Prompts** (packaged workflows): `post_round_report`, `practice_plan`,
`course_strategy`, `health_check_in`, `season_review`.

Resources: `schema://data-dictionary` (columns, units, enums),
`docs://metric-definitions` (how every stat is computed), and
`docs://getting-started` (recommended tool flow).

## Conventions

- Distances in **yards**, coordinates in **decimal degrees**, times in ISO 8601.
- Lies: `tee | fairway | rough | sand | green | recovery | penalty`.
- Shot types: `drive | approach | chip | putt | penalty`.
- Unknown values are `NULL`, never fabricated — the last shot of a hole has no
  measured end-distance, and tools say so.

## Security notes

- Garmin tokens live at `~/.garminconnect` (mode 0600) — treat like a password.
- The SQL tool is read-only, single-statement, SELECT-only, and capped.
- This is an unofficial connector for **your own** Garmin data.

## Development

```bash
.venv/bin/python -m pytest tests/ -q
```

---

`mcp-name: io.github.mbrionesalvarez/caddie-mcp`
