Files
Chris Farhood 8fee4717dd
build-image / test (push) Successful in 11s
build-image / build (push) Successful in 21s
docs: rewrite CLAUDE.md + README for the multi-tenant fork
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:22:03 -04:00

103 lines
5.0 KiB
Markdown

# Intervals.icu MCP Server
A self-hosted, multi-tenant Model Context Protocol (MCP) server that exposes the
[Intervals.icu](https://intervals.icu) API — activities, events, wellness, power curves, gear, and
custom items — as MCP tools for Claude. It runs as a remote, OAuth-authenticated connector at
`https://intervalsicu.farhoodlabs.com/mcp`, where each user's Intervals.icu API key and athlete id are
stored encrypted and looked up per request from their OAuth identity.
This is a fork of the upstream single-tenant project
([mvilanova/intervals-mcp-server](https://github.com/mvilanova/intervals-mcp-server)), reworked for
remote multi-user operation.
## How it fits together
The system is three services on one Kubernetes host (`intervalsicu.farhoodlabs.com`), deployed via Flux
GitOps and sharing one CloudNativePG Postgres database:
| Service | Path | What it does |
| --- | --- | --- |
| **intervalsicu-mcp** (this repo) | `/mcp` | The MCP server. Verifies OAuth JWTs and serves Intervals.icu data as MCP tools over streamable-HTTP. |
| **intervalsicu-mcp-ui** | `/portal` | Portal where users sign in (Google via Better Auth) and set their Intervals.icu credentials; admins approve accounts. |
| **intervalsicu-mcp-auth** | `/api/auth` | Better Auth OAuth 2.1 server: dynamic client registration (DCR), PKCE, EdDSA-signed JWTs + JWKS, Google login. |
**Connection flow:** the Claude connector self-registers at the auth server (DCR), the user logs in with
Google, and Claude receives a JWT access token whose audience is the `/mcp` URL. This server verifies
that JWT against the auth server's JWKS and resolves the caller's stored Intervals.icu credentials by the
token subject. A user who hasn't been approved or hasn't set credentials gets a friendly "account isn't
ready" message.
## Connecting Claude
Add a **custom remote connector** in Claude pointing at:
```
https://intervalsicu.farhoodlabs.com/mcp
```
Name it **without a dot** — use `intervals`, not `intervals.icu`. Claude Desktop and mobile fail to load
the tools if the connector name contains a dot. Claude handles registration and Google login for you.
Your account must be approved in the portal and have Intervals.icu credentials set.
### Available tools
`get_activities`, `get_activity_details`, `get_activity_intervals`, `get_activity_streams`,
`get_activity_messages`, `add_activity_message`, `get_events`, `get_event_by_id`, `add_or_update_event`,
`delete_event`, `delete_events_by_date_range`, `get_wellness_data`, `get_athlete_power_curves`,
`get_gear_list`, `get_custom_items`, `get_custom_item_by_id`, `create_custom_item`, `update_custom_item`,
`delete_custom_item`.
## Local development
Requires Python 3.12+ and [`uv`](https://docs.astral.sh/uv/).
```bash
uv venv --python 3.12 && source .venv/bin/activate
uv sync --all-extras # install with dev extras
uv run pytest # tests (enforces a >=90% coverage gate)
ruff check . # lint
mypy src tests # type check
```
Run locally over stdio (single-user, using the env fallback credentials below):
```bash
python -m intervals_mcp_server.server
```
For local HTTP without OAuth, set `MCP_TRANSPORT=http` and leave `MCP_ISSUER` / `MCP_RESOURCE` /
`MCP_JWKS_URI` unset — auth is disabled when those aren't all present, and tools use the `API_KEY` /
`ATHLETE_ID` env fallback.
## Environment variables
**Local / single-user fallback** (used only when there is no OAuth context):
- `API_KEY` — an Intervals.icu API key (Settings → API on intervals.icu).
- `ATHLETE_ID` — your athlete id, e.g. `i12345` (from the intervals.icu URL). Must match `i?\d+`.
- `INTERVALS_API_BASE_URL` — optional, defaults to `https://intervals.icu/api/v1`.
**Multi-tenant / deployment:**
- `DATABASE_URL` — async Postgres DSN (`postgresql+asyncpg://…`) for the shared user store.
- `INTERVALS_ENC_KEY` — base64-encoded 32-byte AES-256-GCM key used to encrypt stored API keys. Shared
with the UI service so both read/write the same ciphertext.
- `MCP_ISSUER`, `MCP_RESOURCE`, `MCP_JWKS_URI` — OAuth: token issuer, the `/mcp` resource URL, and the
JWKS endpoint (an internal in-cluster URL, to bypass Cloudflare). Setting all three enables auth.
- `MCP_TRANSPORT``stdio` (default), `sse`, or `http`/`streamable-http`. Deployment uses `http`.
- `MCP_STATELESS_HTTP`, `MCP_JSON_RESPONSE` — HTTP tuning. The deployment sets both `true` (stateless +
JSON) so the distributed connector can hit any replica; stateful/SSE breaks it.
- `FASTMCP_HOST`, `FASTMCP_PORT`, `FASTMCP_LOG_LEVEL` — HTTP bind + logging.
## Deployment
Pushes to `main` trigger Gitea CI (`.gitea/workflows/build.yaml`): it runs the test suite with the
coverage gate, then builds and pushes a container image to
`git.farh.net/farhoodlabs/intervalsicu-mcp`. Flux reconciles the new image onto the cluster. Database
migrations run as an Alembic (`alembic upgrade head`) initContainer before the server starts.
## License
GNU General Public License v3.0 (inherited from the upstream project).