Icuvisor.app - opensource local MCP server, shipped as a signed binary for Mac and Windows

icuvisor is an open-source, MIT-licensed MCP server that connects intervals.icu to Claude, ChatGPT, Pi, Cursor, and other MCP-compatible clients. The repo is hosted on Github, and end-user docs / installers live at https://icuvisor.app.

What’s different from other MCP servers

The goal is to make this usable by athletes who don’t want to install Python tooling, edit JSON config by hand, or hand their API key to a hosted service. Concretely:

  • Single signed Go binary. Install via .dmg / .msi / .deb / Homebrew / Scoop / Winget. No uv, no pip, no hatchling errors. The first-launch flow asks for an API key, detects your athlete ID and timezone, and writes the MCP config for your chosen client(s) automatically.

  • API key lives in the OS keychain (macOS Keychain, Windows Credential Manager, libsecret) — not in a .env file that ends up in dotfile backups or repos.

  • Terse-by-default responses. Every read tool returns the smallest useful payload; heavy fields (streams, raw samples) require an explicit include_full: true. Nulls are stripped (wellness rows with N/A device fields shrink dramatically). The target is fitting a full year of overview data inside a single free-tier Claude conversation without running out of context.

  • Scale labels embedded in the response itselffeel is 1–5, sleepQuality is 1–4, sleepScore is 0–100. These are in _meta.scales on every row, because some MCP clients don’t pass the tool description back to the model at inference time and the LLM would otherwise compare numbers on incompatible scales.

  • Sleep dual-scale handling. sleepQuality (athlete-entered 1–4) and sleepScore (device-imported 0–100) are surfaced as distinct fields with provenance, never collapsed into a single ambiguous “sleep rating.”

  • Wellness provenance + freshness. Bridged fields like readiness carry a _meta.provenance per field (polar | garmin | oura | …) and a _meta.stale: true flag when the bridge hasn’t refreshed in 24h (the Polar-bridge-only-refreshes-when-you-open-the-website problem).

  • Strava-imported activities are detected and returned as a structured unavailable: { reason: "strava_tos" } with a workaround, instead of empty N/A fields the model might hallucinate over.

  • Unit and timezone normalization. Distances render in the athlete’s preferred_units (miles or km) with the unit name in the field key, so no LLM unit drift. All times are in the athlete’s configured timezone.

  • Delete safety is not LLM-controllable. delete_event, delete_events_by_date_range, delete_workout, etc. are gated by an ICUVISOR_DELETE_MODE env var read at startup (none / safe / full). Forbidden tools aren’t registered with the MCP server at all — the model can’t see them, can’t be talked into them, and can’t invent a confirm: true flag. A per-call confirmation argument controlled by the model is not a credible safety guard.

  • Coach mode with per-athlete tool ACLs. Paste a coach-scoped API key, list athletes, and grant each athlete a specific tool subset (e.g. read-only for a prospective client, full read+write for an active one). The athlete_id argument selects who the call targets; it is never a credential. The coach key never leaves the local binary.

  • Workout DSL round-trip. intervals.icu emits structured workout_doc on reads but silently ignores it on writes — uploads need to be re-encoded into the description-string DSL. icuvisor owns that serializer so you pass structured steps and never see the asymmetry. Lossy fields are surfaced as a _meta.lossy_fields warning rather than dropped silently.

  • Free-text descriptions are written through verbatim. Athlete/coach notes in description are not silently normalized into structured blocks on edit.

  • MCP Resources for long-form schemas. The workout-syntax DSL, event categories, custom-item content schemas, and the live athlete profile ship as Resources rather than inline tool descriptions, so the per-session tool-description budget stays small.

  • Toolset tiers. Defaults to a curated core subset (around 17 tools) covering the daily-use path; the full surface is one env var (ICUVISOR_TOOLSET=full) away. Smaller catalog → fewer tool-selection mistakes on smaller models.

  • MCP Prompts (training analysis, recovery check, weekly planning, race-week taper, coach roster triage) for clients that surface prompts.

What it doesn’t do (yet)

  • No Strava / TrainingPeaks direct ingestion — connect your device directly to intervals.icu (or add another MCP server alongside).

  • No hosted relay yet. Everything runs on your machine; your API key and data never leave it.

  • The analyzer family (analyze_trend, compute_zone_time, etc.) is planned for v0.6 — for now the LLM does the math from the read tools.

Status

Beta. Check the roadmap and the PRD, which explains the design choices above in more detail.

I’d love feedback, especially on the installation process and documentation, tool catalog, the response shapes, and anything that feels wrong against how you actually use intervals.icu. Issues and PRs on Github welcome.

Also thanks to the intervals.icu community and other MCP connector authors; a lot of the design decisions here are direct responses to problems people raised on other posts and projects.

6 Likes

Hi Ricardo, thx for making such a tool. I tried to install this tool but there is no way to install it. I asked another AI tool what the problem is but still, I can’t install this.
I have no knowledge in coding, can you explain me how to install your tool?
TIA

Hi! What platform are you on? Windows? Mac? Which install method did you use? Any error message from the installer? What steps have you done when installing?

Hi Ricardo, just wanted to say this is an amazing tool. I had small Python tool that I used to parse my intervals.icu data, but this mcp server has basically replaced that.

1 Like

UPDATE

Get it working following this steps: Connect Claude Desktop – icuvisor — Talk to your intervals.icu data

Because it mentions Mac OS and gives MacOS paths, I tought it wouldn’t work on windows.

But the “claude_desktop_config.json” is in this location: %appdata%\claude

Ricardo, maybe you can update the documentation to include the correct info for windows users as well.

/UPDATE

Hi @Ricardo_N_Cabral

I tried Claude and ChatGPT, but no luck in connecting.

Any ideas please?

I did the setup and followed Connect Claude Desktop – icuvisor — Talk to your intervals.icu data

But that is apparently only for MacOS. I’m on windows 11

Then I tried to connect it to chatGPT.

fist I had to enable Dev tools.

Then I could create an app.

when I want to use the URL (http://127.0.0.1:8765/mcp), I get the message that the URL is unsafe (probably because it’s not HTTPS)

Thanks, you are right. The Claude Desktop manual fallback docs were too macOS-focused.

On Windows, Claude Desktop uses:

%APPDATA%\Claude\claude_desktop_config.json

and the usual icuvisor command path after install is:

C:\Users\<you>\AppData\Local\Programs\icuvisor\icuvisor.exe

So the manual Claude Desktop block should look like this, replacing , athlete ID, and timezone:

  {
    "mcpServers": {
      "icuvisor": {
        "command": "C:\\Users\\<you>\\AppData\\Local\\Programs\\icuvisor\\icuvisor.exe",
        "env": {
          "INTERVALS_ICU_ATHLETE_ID": "i12345",
          "ICUVISOR_TIMEZONE": "Europe/Brussels",
          "ICUVISOR_TRANSPORT": "stdio"
        }
      }
    }
  }

Do not put the API key in that JSON file. icuvisor setup stores it in Windows Credential Manager.

For ChatGPT: the http://127.0.0.1:8765/mcp error is expected if you are using a remote/custom connector UI. That UI needs a public HTTPS endpoint reachable from OpenAI, and it cannot call localhost on your PC. icuvisor’s local HTTP endpoint is intentionally loopback-only. Use a local MCP/stdio-capable client for now (Claude Desktop, ChatGPT app, Codex, Claude Code).

To anyone interested or having difficulties installing icuvisor on the local machine, there is now a hosted free MCP server available at https://connect.icuvisor.app/

Hello, I’m really interesting in using your "app.

I tried to install it on windows, somehow when I use the Powershell comand I can’t write anything when it asks the Intervals.icu API. Once I get to the athlete id it works again. But it says that it can’t connect. Do you know a work around?

Edit: Found the config.json solution. Everything is put in there but Claude says he isn’t able to connect because probably athlete id or api code is missing. Both are in the config.json and cross checked. Claude says: “You need to reconnect ICU Visor: the MCP token has expired or was revoked. Once you’ve done that, I’m ready to run the full recovery check immediately.”

Thanks for the update. The blank API-key prompt in PowerShell is expected: it is masked input, so nothing appears while typing or pasting. Press Enter after pasting the key.

The later “MCP token expired or was revoked” message sounds like Claude is using the hosted Claude.ai connector, not your local Windows config. Local config.json only works when Claude Desktop launches icuvisor.exe; Claude.ai in the browser cannot read that file.

If you are using Claude.ai in the browser, reconnect icuvisor from Claude’s Connector settings and sign in through the hosted Intervals.icu OAuth flow. Do not use your Intervals API key there.

If you are using Claude Desktop on Windows, make sure %APPDATA%\Claude\claude_desktop_config.json points to:

%LOCALAPPDATA%\Programs\icuvisor\icuvisor.exe

Then fully quit/reopen Claude Desktop and start a new chat. You can also run:

icuvisor version
icuvisor diagnostics

Please don’t paste your API key or full config file here. The redacted diagnostics output is safe to share.

If a non-local setup is ok with you, try the new hosted icuvisor at https://connect.icuvisor.app/

Hi @Ricardo_N_Cabral
I’ve connected this hosted version of MCP, but claude.ai doesn’t want to get the data from intervals via mcp. Here are some responses from claude:

Can you help with this?

so what are the steps you're trying to do to achieve results of my request?

19:43

Good question! Here's exactly what I'm doing:

Calling the Intervals MCP tool (intervals:get_activities) with a date range (last 2 weeks) to fetch your recent activities

The MCP server at https://connect.icuvisor.app/mcp receives the request and is supposed to use your OAuth token to call the Intervals.icu API on your behalf

Return the results to me so I can summarize your latest training session

The problem is at step 2 — the MCP server keeps returning a credentials error, meaning it either:

Doesn't have your OAuth token stored correctly, or

The token it has is expired/invalid
Great question! Here's what a typical MCP tool response looks like when it's working correctly. In our case, all we're getting back is an error object, but a successful response would look something like this:

For get_athlete_profile:

{
  "athlete_id": "i225175",
  "name": "Your Name",
  "timezone": "Europe/Kiev",
  "preferred_units": "metric",
  "sports": ["Ride", "Run"],
  "ftp": 250,
  "hr_max": 185,
  "thresholds": { ... }
}


For get_activities:

{
  "activities": [
    {
      "id": "abc123",
      "name": "Morning Run",
      "date": "2026-06-27",
      "sport": "Run",
      "distance_m": 10500,
      "duration_s": 3120,
      "training_load": 68,
      "avg_hr": 142
    }
  ],
  "_meta": { "next_page_token": null }
}


What we're actually getting:

{
  "error": "could not fetch athlete profile; check intervals.icu credentials and athlete ID"
}


So the MCP server is reachable, but it's rejecting every request at the auth layer before it even tries to call the Intervals.icu API. The connection handshake worked, but the stored credentials aren't being forwarded with the actual tool calls.

Does that help clarify where the problem is

Here’s exactly what the actual tool call looks like that I’m sending:

Tool name: intervals:get_activities

Parameters:

{
  "oldest": "2026-06-14",
  "newest": "2026-06-28",
  "page_size": 5
}

That’s it — just a date range and how many results to return. I have no way to pass your athlete ID (i225075) or any credentials in the call itself. Those are supposed to be handled automatically by the MCP server (connect.icuvisor.app) using the OAuth token it stored when you connected.

So the flow is:

Me → intervals:get_activities {oldest, newest} 
  → MCP server (icuvisor) adds your auth token 
    → Intervals.icu API /athlete/i225075/activities 
      → data back to me

The break is between the MCP server and Intervals.icu — it’s either not storing or not forwarding your OAuth token in that second arrow. That’s why no matter what parameters I send, it always fails at the same point.

It works now. Thanks for your help! What a nice tool. Really helpful. Looking forward to use it for my future training :grinning_face:

1 Like

Thanks for the debug info Ihor. Can you try reconnecting icuvisor custom MCP on Claude? Some tools were still asking for the wrong permissions from intervals.icu and failing. This apparently only happens to some users. Sorry for all this trial and error.

Hi Ricardo,
Nice, it works now! Thank you :slight_smile:

The tool name was wrong before — it's icuvisor:get_activities, not intervals:get_activities. Let me try now:

Get activities

It's working now! I can see your activities — but there's a new issue: all of them are marked as Strava-blocked, meaning Intervals.icu imported them via Strava but Strava's API is now restricting access to the full data.

Here's what I can see so far:

Date

Activity ID




1 Like

Thanks to everyone who tried the early hosted flow and reported the weird Claude/OAuth errors. A few fixes are live since then:

  • Hosted MCP is still at https://connect.icuvisor.app/mcp and now has better reconnect / re-consent handling for expired tokens, missing scopes, and stale tool catalogs. If you tried it early and got profile/credential errors, disconnect and reconnect the connector.
  • Hosted settings are available at https://connect.icuvisor.app/settings for timezone, toolset, write safety, revoking client grants, or deleting hosted account data.
  • The core toolset has also grown: a smaller compact profile for weaker/local models, get_data_quality_report for diagnosing Strava-restricted/missing data, new season-planning helpers, prompt packs for weekly review / race week / ride analysis / coach triage, and better HRV freshness + swim-yard/pace handling.

I set it up localy on Windows and created workouts for one week for test. I like it for now and I’ll keep testing it. Great work :+1: :ok_hand:

1 Like

Hi,

I ran into a bug in the validate_workout tool: it rejects workout descriptions that use the syntax documented in the original Workout builder post — i.e. a title line, a blank line, then step lines, repeated for warmup / main set / cooldown, e.g.:

Warmup
- 20m 60% 90-100rpm

Main set 6x
- 4m 100% 40-50 rpm
- 5m recovery at 40%

Cooldown
- 20m 60% 90-100rpm

validate_workout returns a PARSE_ERROR on this input, with the message:

“structured workout step lines must form a single contiguous block; multiple blocks separated by prose are not supported by the intervals.icu DSL”

This appears to be incorrect — this is exactly the format shown in the official workout builder documentation, and I confirmed that submitting it directly as the event description (bypassing validate_workout/workout_doc) is parsed correctly by the actual intervals.icu upstream: the resulting event has the correct repeat_reps structure and correct total duration.

So it looks like validate_workout’s local reimplementation of the DSL parser is out of sync with the real upstream parser — it doesn’t allow blank lines / header text between step blocks, even though upstream does.

Steps to reproduce:

  1. Call validate_workout with the multi-block description above (or with an equivalent workout_doc containing a reps group between two flat step groups).
  2. Observe valid: false with the “must form a single contiguous block” error.
  3. Submit the same text directly as an event description via add_or_update_event (no workout_doc).
  4. Fetch the event back — it has the correct structured steps with repeat_reps set, matching what the intervals.icu web UI shows.

Would be great if validate_workout could be updated to accept this documented syntax so it doesn’t produce false negatives.

Thanks!

Hi Cestni,

Thanks for finding and reporting this issue. This was a bug in icuvisor’s validator, not in the workout itself. It was treating the workout steps as if they had to be one continuous block, but Intervals accepts headings and blank lines between sections, including the repeat format in your example. That meant a valid workout was being rejected before anything was sent upstream.

I’ve fixed it in v1.5.3. Could you retry validate_workout with the same description? It should now return valid and preserve the original formatting. If it still rejects anything, please send the validation error and I’ll take another look.

1 Like