Skip to main content
Glama
apple-workouts-mcp

apple-workouts-mcp

by yashhooda1

apple-workouts-mcp

ci python license: MIT

An MCP server that lets Claude, OpenAI Agents, ChatGPT (where custom MCP is available), or any MCP client build structured runs, strength sessions and multi-week training plans and put them on your Apple Watch — the Apple counterpart to mcp-garmin.

"Put a 3×2mi tempo at 6:15–6:25 on Tuesday, Strength B on Thursday,
 and a 12-miler with the last 3 at half-marathon pace on Sunday."
   →  built, published, scheduled on the Watch by HoodaRun.

HoodaRun Web — browser athlete studio

HoodaRun now has a responsive web dashboard in the same MCP server, accessible from a Mac, PC, iPad or phone. No JavaScript build pipeline or third-party frontend service is required. The browser app can preview and publish structured running sessions and multiweek plans, strength workouts, plant-based meal/grocery lists, a Texas race calendar, and inspect the real workout feed and iPhone sync status.

pip install -e ".[dev]"
TRANSPORT=streamable-http HOST=127.0.0.1 PORT=8000 apple-workouts-mcp
# Open http://127.0.0.1:8000/app

For remote hosting, use HTTPS, set MCP_AUTH_TOKEN and persistent WORKOUT_FEED_DIR storage. The website's Connect server field accepts that same token without saving it in browser storage. The public page has no workout data; every browser API action is authenticated and protected. Plans always require preview and a separate deliberate publish action.

See Website setup and deployment for the routes, screens, and security limitations.

Important: a website cannot call WorkoutKit or HealthKit on your Apple Watch. HoodaRun on your paired iPhone remains necessary to turn the shared JSON feed into scheduled Apple Watch workouts. Publishing to the feed is not the same as confirming a workout on the Watch.

Related MCP server: Garmin Claude MCP

Siri AI / Apple Intelligence / Shortcuts

HoodaRun also exposes typed Siri AI / Apple Shortcuts actions through Apple's App Intents, with a safe private bridge to the same workout feed used by Claude and OpenAI MCP tools. Siri is not a native generic MCP client: App Intents are the supported Apple integration path.

Features include next-workout lookup (local iCloud or CloudKit), requesting an iPhone-to-Watch sync, previewing and confirming new easy runs or interval sessions (including standing vs jogging rest), querying Texas races and getting simple plant-based meal ideas.

Add ios/HoodaRun/HoodaRunSiriIntents.swift to the HoodaRun iPhone app target. For actions that reach the Python server, configure HTTPS plus MCP_AUTH_TOKEN under HoodaRun's existing Remote Server settings. Voice-created workouts require an explicit confirmation before they are published. The new POST /api/siri endpoint is protected by the same bearer-token middleware as MCP and the feed.

Read the Siri AI integration guide for setup, supported phrases, and the iOS 27/watchOS 27 hardware testing limitations.

How it reaches the Watch

Apple has no cloud API that writes to a Watch. Structured workouts get there through WorkoutKit, which only runs on the iPhone. So the work is split:

AI MCP client ──MCP──▶ apple-workouts-mcp ──writes──▶ workouts.json
                                                   │  (iCloud Drive, or GET /feed.json)
                                                   ▼
                                  HoodaRun (iPhone) ── WorkoutKit ──▶ Apple Watch
                                                   │
                         status.json ◀──writes─────┘  (what actually landed)
  • The server compiles each workout into JSON that maps 1:1 onto WorkoutKit (CustomWorkout → warmup / IntervalBlocks / cooldown, HeartRateZoneAlert, SpeedRangeAlert, time / distance / open goals).

  • HoodaRun reads the feed, schedules new or changed workouts with WorkoutScheduler, removes ones that left the feed, and writes status.json back — so list_scheduled can tell you what's really on your wrist.

  • Completed runs reach Strava through Strava's own Apple Health sync; the server reads them back (read-only, your own Strava API app) for fitness assessment.

Nothing third-party sits in the middle: iCloud Drive for local use, or your own HTTP host.

Quick start (Mac + iCloud Drive)

git clone https://github.com/yashhooda1/mcp-apple-workouts
cd mcp-apple-workouts
pip install -e ".[dev]"
pytest -q                     # offline: compiles workouts + feed round-trips

Add to Claude Desktop's MCP config (see examples/claude_desktop_config.json):

{ "mcpServers": { "apple-workouts": { "command": "apple-workouts-mcp" } } }

On a Mac the feed defaults to iCloud Drive › HoodaRun › MCP (WORKOUT_FEED_DIR overrides). In HoodaRun: Settings › Claude plans › Choose feed folder… and pick that folder once. HoodaRun syncs on launch and from Sync now.

OpenAI / ChatGPT + advanced runner tools

The server speaks standard MCP, so it works with more than Claude. This branch adds OpenAI Agents SDK (local stdio) and OpenAI Responses API (remote HTTP) examples. ChatGPT custom MCP connections depend on your workspace and supported authentication or a Secure MCP Tunnel; a local stdio server cannot be plugged straight into ChatGPT on the web.

See OpenAI/ChatGPT setup and security for the working example commands, authentication caveats, and watchOS limitations.

Additional planning tools cover an adult sub-5 mile, sub-17 5K, sub-1:20 half and the existing sub-3 marathon generator. Road plans use your current mileage and optional recent time-trial benchmark rather than assuming a stretch goal already equals current fitness.

create_road_race_plan(distance="5k", goal_time="16:59",
    current_weekly_miles=35, benchmark_time="18:15",
    benchmark_distance="5k", weeks=16, dry_run=True)
build_meal_grocery_plan(diet="lacto_vegetarian", training_load="high")
list_texas_races(area="Austin", distance="5K")

The meal planner returns seven quick-cook menus, grocery categories and plant-based B12/iron guidance. The race calendar uses published 2026–27 organizer dates and URLs for Austin, Dallas/Fort Worth and Houston; it does not pretend to fetch live registration inventory or infer future annual dates.

Optional: Strava history

  1. Create an API app at https://www.strava.com/settings/api (callback domain localhost) and set STRAVA_CLIENT_ID / STRAVA_CLIENT_SECRET.

  2. Run apple-workouts-strava-auth once. The rotating refresh token is saved to STRAVA_TOKENS (default ~/.hooda-workouts/strava_tokens.json, mode 600).

Without Strava, pass current_weekly_miles to the plan tools.

Remote (claude.ai custom connector)

Deploy with the Dockerfile / railway.toml, attach a volume at /data, set MCP_AUTH_TOKEN (openssl rand -hex 32). The same bearer token gates all three routes:

Route

Who

POST /mcp

Claude

MCP streamable HTTP

GET /feed.json

HoodaRun

the workout feed

POST /status

HoodaRun

what's scheduled on the Watch

GET /calendar.ics

any client with the token

the feed as an iCalendar

In HoodaRun use Remote server (HTTP) with your https:// host and token (stored in the Keychain).

Claude and ChatGPT connectors (OAuth). Set PUBLIC_BASE_URL to the server's https origin and the server also becomes its own OAuth 2.1 authorization server (discovery, dynamic client registration, PKCE S256, rotating refresh tokens). Add https://<host>/mcp as a custom connector in Claude or ChatGPT and sign in with your server password (OAUTH_OWNER_PASSWORD, default MCP_AUTH_TOKEN). The static token keeps working for HoodaRun and Claude Desktop.

Apple Calendar. Set CALENDAR_TOKEN and subscribe to webcal://<host>/calendar/<CALENDAR_TOKEN>.ics, or turn on Add to Apple Calendar in HoodaRun to get editable events each sync. Pick one. See Calendar and connectors.

Tools

Tool

Access

Description

list_activities

read

Recent Strava activities

assess_fitness

read

Summarise recent training; size the gap to a goal time

list_scheduled

read

Feed workouts, each flagged on_watch from HoodaRun's report

sync_status

read

Last HoodaRun sync: scheduled, pending, errors

calendar_subscription

read

Apple Calendar subscription URL and setup

preview_workout

read

The exact WorkoutKit-shaped JSON for a spec

list_marathons / pick_random_marathon

read

International marathon catalogue

create_workout

write

Structured run on a date (optional time, indoor/outdoor)

create_strength_workout

write

Guided strength/core session on a date

unschedule_workout

write

Remove one workout by id

clear_scheduled

write

Remove a date range (dry-run first)

create_training_plan

plan

Publish a plan you wrote — runs and lifts, by date

create_marathon_plan

plan

Any marathon, including sub-3, scaled to available fitness

create_hs_track_plan

plan

High-school 800 / 1600 / 3200 / 5K blocks

Every generated plan schedules two strength/core sessions a week and returns per-week nutrition, hydration and recovery guidance.

Workout spec

Same schema as mcp-garmin, so prompts and plans port unchanged:

{"name": "4x1mi threshold", "location": "outdoor", "steps": [
  {"kind": "warmup", "duration": "15:00", "target": "hr:2"},
  {"repeat": 4, "steps": [
    {"kind": "run", "duration": "1mi", "target": "pace:6:35-6:55"},
    {"kind": "recovery", "duration": "2:30", "target": "hr:2"}]},
  {"kind": "cooldown", "duration": "10:00", "target": "hr:2"}]}
  • Durations: "15:00", "90s", "1mi", "400m", "1km", "open" (lap button)

  • Targets: "hr:2" → HeartRateZoneAlert, "pace:M:SS-M:SS" (per mile) → SpeedRangeAlert, or none

  • Strength: {"name": …, "exercises": [{"category": "PLANK", "exercise": "SIDE_PLANK", "sets": 3, "hold": "30s", "rest_s": 30}]} — rep sets are open steps you end with the Action button; holds and rest are timed

Things WorkoutKit decides, not us

  • Scheduled-workout cap. WorkoutKit only holds WorkoutScheduler.maxAllowedScheduledWorkoutCount workouts at once (shared with anything HoodaRun schedules itself). HoodaRun fills free slots with the soonest sessions and reports the rest as pending; each sync rolls forward, so open the app every few days during a long block.

  • One warmup and one cooldown. A leading warmup and trailing cooldown become WorkoutKit's; everything else becomes interval blocks, in order.

  • Strength is scheduled as Traditional Strength Training, falling back to Functional Strength if the Watch doesn't support it as a custom workout.

Building on the web (yashhooda.ai + CloudKit)

The HoodaRun section on yashhooda.ai is a workout builder that saves to the visitor's own private CloudKit database after they sign in with their Apple ID (CloudKit JS). HoodaRun reads those records alongside the MCP feed, schedules them with the same code path, and writes scheduledRev back so the site shows On Watch. Web workouts use the identical hooda.workouts.custom/v1 JSON.

One-time setup in CloudKit Console:

  1. Container iCloud.ai.yashhooda.HoodaRun (or change containerID in CloudKitPlanStore.swift and CK.container in the site section to match HoodaRun's real container). Add it under HoodaRun › Signing & Capabilities › iCloud › CloudKit.

  2. Record type PlannedWorkout with String fields date, time, name, sport, workoutJSON, rev, source, scheduledRev, and Date/Time syncedAt.

  3. Indexes: date Queryable + Sortable, recordName Queryable.

  4. Deploy the schema to Production.

  5. API Access › new web API token, sign-in callback/allowed origin https://yashhooda.ai. Paste it into CK.apiToken in the site section; until then the builder runs in preview mode (build + copy JSON, no sign-in).

Apple Watch Ultra 4 / watchOS 27 notes

WorkoutKit schedules via the iPhone companion, not from a Python process running on the Watch. The JSON schema is watch-model neutral. Ultra 4/watchOS 27 are target devices, not hardware verified in this repository; a paired iPhone, signed HoodaRun app and actual device tests are required.

The local server never directly reads Apple Health or touches a watch without the app syncing. Published feed items may remain pending due to WorkoutKit's schedule cap. See platform details.

HoodaRun integration

Add the three files in ios/HoodaRun/ to the HoodaRun target (iOS 18+ / watchOS 11+, WorkoutKit, CloudKit):

  • MCPPlanSync.swift — feed models, JSON → CustomWorkout, the sync actor

  • CloudKitPlanStore.swift — reads/marks workouts saved from yashhooda.ai

  • MCPSyncSettingsView.swift — source picker + Sync now

Then two insertions in the app:

// In HoodaRun's settings list:
NavigationLink("Claude plans") { MCPSyncSettingsView() }

// In the App struct, sync whenever HoodaRun comes to the foreground:
@Environment(\.scenePhase) private var scenePhase
// ...on the root WindowGroup content:
.onChange(of: scenePhase) { _, phase in
    if phase == .active { Task { try? await MCPPlanSync.shared.sync() } }
}

The sync only ever removes workouts it scheduled itself (tracked by id), so workouts made in HoodaRun's own builder are never touched.

License

MIT

Available Tools

14 tools
assess_fitnessB

Read-only: summarise recent training (from Strava), and size the gap to a marathon goal.

With goal_time ('3:15:00') and race_date ('2027-04-19') also returns what the goal asks for, what a safe ramp reaches, and — when those disagree — the goal this runway does support.

Inputs: goal_time, race_date, weeks_of_history, current_weekly_miles.

clear_scheduledA

Remove every feed workout in [start_date, end_date] inclusive.

Destructive. Defaults to dry_run=True — call again with dry_run=False to commit. Only touches workouts this server published; workouts made in HoodaRun's own builder are never removed.

Inputs: end_date (required), start_date (required), dry_run.

create_hs_track_planB

Build a high-school track block: 800 / 1600 / 3200 / 5k.

goal_time defaults to the barrier — sub-2:00 800, sub-4:30 1600, sub-10:00 3200, sub-15:00 5K. With current_pr the report sizes the gap honestly. training_age ('new' | 'developing' | 'experienced') sets a hard mileage ceiling; Sunday is always rest; no doubles; nutrition guidance is adequacy-only. Take it to the athlete's school coach before running it. dry_run=True validates without publishing.

Inputs: time, event, weeks, dry_run, +7 more.

create_marathon_planA

Build a marathon block for ANY race and ANY goal, scaled to current fitness.

Reads recent Strava history (unless use_strava_history=False or current_weekly_miles is supplied), compares it to what the goal asks for, and ramps from where the runner actually is — 8%/week ceiling, 3-up-1-down, 3-week taper, ending on race day. If the goal doesn't fit the runway, the plan targets the goal that does and says so; force_goal=True overrides. Two strength sessions a week; weekly fuelling/recovery guidance in the report. dry_run=True validates without publishing.

Inputs: goal_time (required), race_date (required), race_name (required), time, +8 more.

create_strength_workoutB

Create a guided strength/core workout and schedule it for a date.

Each exercise takes reps (an open set you end with the Action button) or hold ('45s', timed), plus sets and rest_s. Each set shows on the Watch by name, e.g. 'Romanian Deadlift · 8 reps', with a timed rest between sets.

Inputs: spec (required), schedule_date (required), time.

create_training_planA

Publish a multi-week plan you wrote yourself.

items is a list of {"date": "YYYY-MM-DD", "spec": } — a spec with exercises is strength, otherwise running. Re-pushing the same plan name updates it in place, and (replace=True) drops sessions from the earlier push that are no longer in the plan. dry_run=True validates and reports without publishing.

Inputs: name (required), items (required), time, dry_run, +1 more.

create_workoutA

Create a structured running workout and schedule it for a date.

schedule_date is 'YYYY-MM-DD'; time is optional 24h 'HH:MM'. The same name on the same date overwrites instead of duplicating. Targets: 'hr:2' (HR zone) or 'pace:6:35-6:55' (min/mi window); durations '15:00', '1mi', '400m', 'open'. Set spec.location='indoor' for treadmill sessions.

Inputs: spec (required), schedule_date (required), time.

list_activitiesB

Recent activities from Strava (read-only): id, name, type, start, distance, HR.

Inputs: days, limit.

list_marathonsC

Marathons in the catalogue whose next running falls inside the window. Dates come from each race's scheduling rule — confirm with the organiser.

Inputs: max_weeks_out, min_weeks_out.

list_scheduledB

Workouts in the feed (optionally within [start_date, end_date]), each marked with whether HoodaRun has confirmed it's scheduled on the Watch.

Inputs: end_date, start_date.

pick_random_marathonC

Pick a marathon at random from the catalogue — the 'surprise me' path.

Inputs: seed, max_weeks_out, min_weeks_out.

preview_workoutA

Compile a workout or strength spec and return the exact WorkoutKit-shaped JSON HoodaRun would receive, without publishing anything.

Inputs: spec (required).

sync_statusA

What HoodaRun last reported: when it synced, what's on the Watch, what's waiting for a free slot (WorkoutKit caps scheduled workouts), and errors.

Inputs: no named parameters.

unschedule_workoutA

Remove one workout from the feed by id; HoodaRun removes it from the Watch at its next sync.

Inputs: workout_id (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 14 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes: feed reads (list_scheduled, sync_status), feed mutations (unschedule_workout, clear_scheduled), and creation tools are well separated. The only real overlap is among the plan builders (create_training_plan vs create_marathon_plan vs create_hs_track_plan) — an agent could plausibly reach for the generic plan publisher when a marathon or track block is wanted, though the descriptions do clarify.

    Naming Consistency4/5

    Almost all names are snake_case verb_noun (create_workout, list_scheduled, preview_workout, clear_scheduled). Minor deviations: sync_status is noun-only and pick_random_marathon is verb_adj_noun, but overall the pattern is predictable.

    Tool Count5/5

    14 tools sit comfortably in the ideal band for a workout scheduling/generation server. Each tool covers a genuine capability — read feed, mutate feed, preview, sync, plan generation, catalogue — with no filler.

    Completeness4/5

    Lifecycle coverage is strong: create, preview/validate, list, unschedule single, clear range, plus sync status and plan publishing. Gaps are minor — no explicit get-by-id or update-existing-workout tool, though same-name/date overwrite and unschedule-then-recreate provide workarounds.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      Lets Claude read a runner's recent activities and wellness signals — sleep, HRV, resting heart rate — and then design, schedule, edit, or delete structured workouts. Planned sessions are written to an intervals.icu calendar that syncs through Garmin Connect onto the user's watch, without exposing Garmin credentials.
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Lets Claude read recent Garmin training activities and wellness signals such as sleep, HRV, and resting heart rate, then design and push structured workouts onto the user's calendar for syncing to their watch. Workouts can also be listed, edited, and deleted through natural language.
      MIT
    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      Lets Claude read recent training history and wellness signals, then design structured workouts and write, edit, or remove them on an intervals.icu calendar so they sync through to a Garmin watch. It brokers all of this through intervals.icu's documented API, so no Garmin credentials are ever shared.
      MIT
    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables Claude to read recent activities and wellness data, then design, create, update, and delete structured workouts that sync through intervals.icu to Garmin Connect and the user's watch.
      MIT