103 lines
5.0 KiB
Markdown
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).
|