Chris Farhood ecf4df88d3
build-image / test (push) Successful in 49s
build-image / build (push) Successful in 19s
security: require verified email for admin + harden sessions
Admin (email allowlist) is now gated on a verified email and re-evaluated per
request instead of trusting a frozen cookie flag; session max_age cut to 8h.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 10:39:05 -04:00
2026-07-07 07:21:22 -04:00
2026-07-07 07:21:22 -04:00

intervalsicu-mcp-ui

Web portal for the self-hosted, multi-tenant Intervals.icu MCP system.

Users sign in with Google, connect their Intervals.icu account by entering their athlete ID and API key (validated live, then stored encrypted), and get the setup steps for the MCP connector. Admins approve accounts before the connector will work for a given user.

Where it fits

Three services share one host (https://intervalsicu.farhoodlabs.com) and one Postgres database:

Service Path Role
intervalsicu-mcp /mcp FastMCP server exposing Intervals.icu data as MCP tools (multi-tenant)
intervalsicu-mcp-ui (this repo) /portal Sign-in + credential management + admin approval
intervalsicu-mcp-auth /api/auth Better Auth OAuth 2.1 / OIDC provider (Google upstream)

The portal authenticates as an OIDC client of intervalsicu-mcp-auth (public client, PKCE, token_endpoint_auth_method=none). It writes each user's encrypted Intervals.icu API key into the shared users table; the MCP server reads that same table, decrypts the key, and calls Intervals.icu on the user's behalf.

Shared coupling with the MCP server (read this)

The portal and the MCP server share the same database and the same encryption key:

  • The users table schema is owned by the MCP server's Alembic migrations. db.py here only maps it (matching column names/types). Test runs build a throwaway SQLite copy from that mapping.
  • API keys are encrypted with AES-256-GCM (crypto.py), layout nonce(12) || ct+tag, key from base64 INTERVALS_ENC_KEY. The MCP server uses the identical scheme and key. Change either the format or the key and the other service can no longer decrypt.

How it works

  • Sign in (/auth/login/auth/callback): OIDC against intervalsicu-mcp-auth. First login creates a disabled user record keyed on the OAuth subject (sub).
  • Account (/account): the user submits athlete ID + API key. intervals.py validates them with a live GET /athlete/{id} (HTTP Basic, username API_KEY); only on success is the key encrypted and stored. The page also shows the MCP connector URL and Claude setup steps.
  • Admin (/admin): emails in ADMIN_EMAILS can approve (enable), disable, or delete users. enabled is the gate the MCP server checks — until an admin approves, the connector won't work for that user.
  • /healthz returns {"status": "ok"}.

Stack

FastAPI + Uvicorn, Authlib (OIDC), Jinja2 templates, SQLAlchemy async + asyncpg (Postgres), cryptography (AES-GCM). Python 3.12+.

Development

uv sync --extra dev

uv run pytest         # tests + 80% coverage gate (SQLite + stubbed HTTP; no network)
uv run ruff check .

# run locally (set the env vars below first); --factory targets app.create_app
uv run uvicorn intervalsicu_mcp_ui.app:create_app --factory --host 0.0.0.0 --port 8080

Environment variables

Required: DATABASE_URL (e.g. postgresql+asyncpg://…), OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URL, SESSION_SECRET, INTERVALS_ENC_KEY (base64 of 32 bytes; shared with the MCP server).

Optional: OIDC_CLIENT_SECRET (leave empty for the public/PKCE client), ADMIN_EMAILS (comma-separated), INTERVALS_API_BASE_URL (default https://intervals.icu/api/v1), OIDC_SCOPES (default openid email profile), ROOT_PATH (e.g. /portal), MCP_URL (connector URL shown to users).

Generate an encryption key with python -c "from intervalsicu_mcp_ui.crypto import generate_key_b64; print(generate_key_b64())".

Deployment

Push to main triggers Gitea Actions (.gitea/workflows/build.yaml at git.farh.net): run tests, then build and push a Docker image to git.farh.net/farhoodlabs/intervalsicu-mcp-ui. Flux deploys it to Kubernetes, served behind the gateway under /portal.

S
Description
Web portal for Intervals.icu MCP: users manage their credentials; admins approve/disable users
Readme MIT 162 KiB
Languages
Python 79.4%
HTML 19.3%
Dockerfile 1.3%