Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
GARMIN_EMAILNoOptional. Your Garmin Connect account email, for unattended sign-in. Without it, sign in with the sign_in_to_garmin tool.
GARMIN_PASSWORDNoOptional. Your Garmin Connect account password, for unattended sign-in. Without it, sign in with the sign_in_to_garmin tool.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
sign_in_to_garminA

Sign in to Garmin Connect. Call this when another Garmin tool says you are not logged in or the session expired. It opens a sign-in page in the user's browser; the password and MFA code are entered there, never in the chat. Afterwards, ask the user to tell you when they have finished, then retry.

preview_workoutA

Check a workout and show it in readable form WITHOUT saving anything. Always call this first, show the user the result, and wait for them to confirm before create_workout. Paces are minutes as decimals (4:30 = 4.5), distances metres, times seconds.

create_workoutA

Save a workout to the user's Garmin Connect workout library. Only after the user has confirmed a preview_workout of the same spec. Look exercise names up with search_exercises first; an unknown name is rejected. Afterwards, schedule_workout (if available) puts it on the calendar and push_workout_to_device (if available) sends it to the watch. Paces are minutes as decimals (4:30 = 4.5), distances metres, times seconds.

update_workoutA

Replace an existing workout with a new spec. This OVERWRITES the whole workout; confirm with the user first, ideally after preview_workout. Paces are minutes as decimals (4:30 = 4.5), distances metres, times seconds.

search_exercisesA

Find Garmin exercise names for strength, HIIT, cardio, yoga, pilates and mobility steps. Returns CATEGORY / NAME lines; use them as exercise: { category, name }.

get_user_profileC

Cached per instance; every date-scoped endpoint needs the display name.

get_user_settingsC

Cached per instance, same eviction-on-failure pattern as getUserProfile().

display_nameA

The account's display name, which date-scoped endpoints put in their URLs. Cached.

full_nameA

The account holder's full name, from the cached user profile.

user_nameB

The account's user name, from the cached user profile.

unit_systemA

measurementSystem lives on /userprofile-service/userprofile/user-settings (under userData), not on the social profile. A convenience accessor, not a data endpoint: returns undefined rather than throwing if the payload shape is missing.

get_userprofile_settingsB

GETs /userprofile-service/userprofile/settings (SINGULAR "settings", distinct from getUserSettings's "user-settings"; the two paths are one character apart and easy to transpose) Three profile endpoints, three methods: getUserProfile() reads /userprofile-service/socialProfile (and backs displayName/fullName/userName), getUserSettings() reads /userprofile-service/userprofile/user-settings (and backs unitSystem), and this one reads .../settings

has_connect_plusA

whether the account has a Garmin Connect+ subscription, read from the cached user profile (/userprofile-service/socialProfile, so usually no extra request): true when hasPremiumSocialIcon is true or userRoles holds any ROLE_SP_FEATURE_n entry. The methods in CONNECT_PLUS_METHODS (searchFoods, getCustomFoods, getCustomFoodServingUnits, createCustomFood, updateCustomFood, deleteCustomFood, logFood, quickAddFood, deleteFoodLogs) need it; without it they throw GarminConnectPlusRequiredError instead of the bare 403 Garmin sends

get_user_summaryB

The daily summary for one date: steps, distance, calories and the rest of the day's totals.

get_statsC

alias of getUserSummary

get_steps_dataA

Step counts for one date, as a series of time intervals; [] if there are none.

get_heart_ratesB

Heart-rate samples plus resting, min and max heart rate for one date.

get_sleep_dataB

Sleep data for one date: the night's summary and its sleep levels.

get_hrv_dataB

Heart-rate variability (HRV) readings and status for one date.

get_body_batteryA

Daily Body Battery reports for a date or a date range (enddate defaults to startdate).

get_body_battery_eventsC

Body Battery events for one date.

get_floorsD

throws GarminError if Garmin returns nothing

get_daily_stepsB

auto-chunks ranges over Garmin's 28-day-per-request limit into ≤28-day windows and concatenates; a single request within the limit passes its (possibly null) result through unchecked

get_daily_statsA

GETs /usersummary-service/stats/daily/{start}/{end}?statsType=CALORIES|STEPS; statsType is exactly "CALORIES" or "STEPS" (any other value, lower-case included, is a Garmin 404). Garmin 400s when end - start > 27, so longer ranges are fetched in 28-day windows and the rows concatenated; Garmin's per-request aggregations (averages) are DROPPED rather than returned wrong for a chunked range. A range with no data returns [] (Garmin answers null). Overlaps getDailySteps/getCaloriesDaily; this one returns total/active/resting calories together, and steps with totalDistance and…

get_weekly_stepsC

weeks defaults to 52, must be a positive integer

get_weekly_stressD

same weeks default/validation as getWeeklySteps

get_weekly_intensity_minutesB

Weekly intensity-minute totals between two dates.

get_stats_and_bodyC

merges getUserSummary with the body-composition totalAverage block, delegating to getBodyComposition (bodyComposition service) for the latter

set_blood_pressureC

validates systolic 70-260, diastolic 40-150, pulse (if given) 20-250, all integers; when defaults to new Date()

get_blood_pressureD

enddate defaults to startdate

delete_blood_pressureA

deletes one reading by its version, taken from getBloodPressure IRREVERSIBLE: confirm with the user before calling.

add_hydration_dataA

raw milliliters, magnitude capped at 10000, negative values allowed; no delete endpoint exists, so not safely round-trippable

get_hydration_dataC

The hydration log for one date.

get_respiration_dataC

Breathing-rate data for one date.

get_spo2_dataD

coerces a string lastSevenDaysAvgSpO2 to a number

get_intensity_minutes_dataC

Moderate and vigorous intensity minutes for one date.

get_all_day_stressB

All-day stress levels for one date.

get_stress_dataC

identical URL to getAllDayStress; an alias

get_all_day_eventsB

Daily events for one date (Garmin's dailyEvents feed).

get_sleep_dailyC

Garmin's endpoint has a documented 28-day-per-request limit; ranges beyond that are auto-chunked, de-duplicated by calendarDate, and sorted

get_rhr_dayC

Resting heart rate for one date.

get_rhr_dailyC

reshapes allMetrics.metricsMap into [{calendarDate, value}], dropping null values

get_calories_dailyA

merges active (metricId 22) and resting/BMR (metricId 23) series into [{calendarDate, active, resting, total}]

get_hrv_data_rangeB

Daily HRV summaries over a date range.

count_activitiesA

returns the envelope's totalCount, not the whole response; throws GarminError if Garmin returns nothing or a non-numeric totalCount

get_activitiesC

defaults start=0, limit=20

get_activities_for_dateD

despite the name, the path is /mobile-gateway/heartRate/..., not an activities-service path

get_activities_by_dateC

paginates internally: fetches fixed pages of 20, incrementing start by 20, until an empty page (normal end) or 2000 pages without one (throws GarminError)

get_last_activityB

delegates to getActivities(0, 1), returns the last element or null

get_activityA

One activity's summary by id. Throws GarminError if Garmin returns nothing.

get_activity_typesB

ActivityTypesResponse is ActivityType[] (154 entries observed live: {typeId, typeKey, parentTypeId, isHidden, restricted, trimmable}), an ARRAY, verified live

delete_activityB

resolves to null on success (204). Live-verified on synthetic fixtures created by the probe itself (never against pre-existing data): create → delete → poll-confirm gone via getActivities IRREVERSIBLE: confirm with the user before calling.

set_activity_nameC

resolves to null on success. Live-verified: value read back via getActivity after the call, not just that the request was accepted

set_activity_typeD

resolves to null on success. Live-verified: activityTypeDTO read back via getActivity after the call

set_activity_descriptionC

resolves to null on success. Live-verified: description read back via getActivity after the call

get_activity_event_typesA

GETs /activity-service/activity/eventTypes: nine {typeId, typeKey, sortOrder} entries (race, recreation, specialEvent, training, transportation, touring, geocaching, fitness, uncategorized)

set_activity_event_typeB

Partial PUT /activity-service/activity/{id} with {activityId, eventTypeDTO: {typeKey}}. The typeKey alone is enough; Garmin fills in typeId/sortOrder. Throws GarminError before any request for a key outside the nine

set_activity_perceived_effortA

Partial PUT of summaryDTO.directWorkoutRpe, which Garmin stores TIMES TEN: pass RPE 1-10 (integer), it sends 10-100; null clears it. Garmin itself rejects 150 with a 400 MEASUREMENT_NOT_VALID. Never read-modify-write the whole summaryDTO instead: PUTting its stored start-coordinate pair back is a 400

set_activity_feelA

Partial PUT of summaryDTO.directWorkoutFeel, one of 0/25/50/75/100 ("How did you feel?", very weak to very strong); null clears it. Garmin stores any number (30 read back as 30) that its apps cannot display, so the method refuses anything else

create_manual_activity_from_jsonB

sends payload to Garmin verbatim, no shape validation

create_manual_activityC

converts units: distanceKm * 1000 → meters, durationMin * 60 → seconds, before building the request body; startDatetime is NOT routed through formatDate (it's a full local timestamp, not a bare calendar date) and must include milliseconds, e.g. "2026-09-22T10:00:00.000"; omitting them produced a live HTTP 500 ValueInstantiationException from Garmin during verification. Live-verified: the converted summaryDTO.distance/summaryDTO.duration were read back via getActivity and matched the expected meters/seconds exactly (5.5km/30min → 5500m/1800s)

import_activityB

multipart upload to /upload-service/upload/{ext} (extension from filename, must be fit/gpx/tcx) with the load-bearing NK/origin/custom User-Agent headers that make Garmin treat it as an import rather than a device sync; a 409 is re-raised as GarminConnectionError ("Activity already exists (duplicate):...")

upload_activityA

multipart upload to the PLAIN /upload-service/upload path, no extension suffix and none of importActivity's load-bearing import headers (ordinary device-sync-shaped upload, distinct from importActivity's spoofed-client import)

get_personal_recordB

GETs /personalrecord-service/personalrecord/prs/{displayName}; no args PersonalRecords is PersonalRecord[], an ARRAY, verified live (the test account returned array[0]). Note the plural type name: the method name is singular but the payload is a list

download_activityD

ActivityDownloadFormat is "ORIGINAL" | "TCX" | "GPX" | "KML" | "CSV", default "ORIGINAL"

get_activity_splitsC

An activity's laps.

get_activity_typed_splitsB

richer detail than getActivitySplits for some activity types (e.g. Bouldering)

get_activity_split_summariesC

Summaries of an activity's splits.

get_activity_weatherC

The weather recorded for an activity.

get_activity_hr_in_timezonesB

Time spent in each heart-rate zone during an activity.

get_activity_power_in_timezonesB

Time spent in each power zone during an activity.

get_activity_detailsD

defaults maxchart=2000, maxpoly=4000, sent as maxChartSize/maxPolylineSize

get_activity_exercise_setsC

A strength activity's exercise sets.

set_activity_exercise_setsB

replace-all semantics, payload sent verbatim. IRREVERSIBLE: confirm with the user before calling.

get_activity_gearD

returns an ARRAY, verified live

get_gear_activitiesC

limit clamped to 1000; returns [] on a 404 instead of throwing

add_gear_to_activityC

on 404 re-raises as GarminConnectionError ("gear not found (likely retired/removed)")

remove_gear_from_activityC

PUT, not DELETE; same 404-handling pattern as addGearToActivity

get_progress_summary_between_datesD

defaults metric="distance", groupbyactivities=true; both dates routed through formatDate

download_health_snapshotD

routed through formatDate, routed through client.download

get_gearB

hits /gear-service/gear/filterGear?userProfilePk=...; returns an ARRAY of gear entries, verified live. This is the dedicated gear-CRUD service (src/services/gear.ts), distinct from getActivityGear (activities service, reuses the same base URL with activityId instead)

create_gearA

POSTs /gear-service/gear/v2; defaults usageType="DISTANCE", notes=""; firstUseDate routed through formatDate; gearType/usageType upper-cased via validateSportKey; converts maxUsageDistanceKm -> maxUsageDistanceMeters (round(km*1000), floor 1) and maxUsageDurationMin -> maxUsageDurationSeconds (round(min*60), floor 1), the opposite direction from addWeighIn's "send raw" rule; returns the response unchecked

get_gear_statsA

GETs /gear-service/gear/stats/{gearUUID}; gearUUID validated via validateUuid (hex, hyphens optional); returns {} on a 404 instead of throwing; other errors re-raised

delete_gearA

DELETE /gear-service/gear/v2/{gearUUID}, resolves null on success (204). The UUID must be HYPHENATED: getGear returns them WITHOUT hyphens and that form 404s, so this method re-inserts them for you when handed the bare 32-char form; only hand-rolled URLs hit the 404. IRREVERSIBLE: removes the gear AND its activity history IRREVERSIBLE: confirm with the user before calling.

set_gear_activity_defaultsA

The working way to set gear defaults (the old dedicated default-gear endpoint is dead; there is no setGearDefault). Read-modify-writes the v2 record: GETs /gear-service/gear/v2/{uuid}, replaces associatedActivityTypes with [{activityTypeKey, defaultGear: true, preferredGear: false}] per key, PUTs the whole record back. Keys are lowercase ("running"), matching createGear, NOT setGearDefault's upper-cased form. [] clears all defaults. Concurrent callers can clobber each other IRREVERSIBLE: confirm with the user before calling.

get_gear_defaultsC

GETs /gear-service/gear/user/{userProfileNumber}/activityTypes; returns an ARRAY of {uuid, activityTypePk, defaultGear} entries, verified live

list_coursesA

GETs /web-gateway/course/owner/ (trailing slash included); returns an ENVELOPE { coursesForUser: CourseSummary[] }, not a bare array

get_courseB

GETs /course-service/course/{courseId}; a missing id is a 404 GarminHttpError ("Course not found : {id}")

import_course_gpxA

multipart POST (field file) to /course-service/course/import. Parses only, saves NOTHING: the result has courseId: null and a RESAMPLED track (13 GPX points came back as 37) with null elevations/timestamps; courseName is the GPX <name>. Only .gpx accepted (only .gpx verified); throws GarminError on an empty response

create_courseA

POSTs /course-service/course with the template Garmin's web client sends. CourseInput = { name, geoPoints, coursePoints?, activityTypeId? = 1 (running), privacy? = "private" }; privacy maps to rulePK public 1 / private 2. Garmin computes distanceMeter and elevation. Throws GarminError before any request on an empty name or fewer than two geoPoints

create_course_from_gpxB

importCourseGpx then createCourse. Name defaults to the GPX <name>, then the filename without .gpx

update_courseA

READ-MODIFY-WRITE: GETs the record, overlays the changes, PUTs the whole record back to /course-service/course/{courseId}, the same request Garmin's own web editor sends on Save (observed 2026-09-24). Concurrent callers can clobber each other. Throws before any request if neither field is given or the name is blank. A just-created course may 429 "not yet ready"

delete_courseA

DELETE /course-service/course/{courseId}, resolves null (204). IRREVERSIBLE. May 429 "not yet ready" for a few seconds after creation IRREVERSIBLE: confirm with the user before calling.

download_course_gpxC

GETs /course-service/course/gpx/{courseId} through client.download

get_devicesC

undocumented per-device shape, deviceId is the field the other device methods key off of

get_device_settingsC

deviceId coerced to an int, validated positive, re-stringified before being placed in the path Two-call sequence: get a deviceId from a getDevices() entry first, then pass it here

get_primary_training_deviceC

The account's primary training device.

get_device_solar_dataB

the only raising method in this group: throws GarminConnectionError if the response is falsy or missing the deviceSolarInput key; returns resp.deviceSolarInput, NOT the envelope. enddate defaults to startdate, and singleDayView is sent "true" exactly when enddate was omitted

get_device_alarmsB

no HTTP path of its own: calls getDevices() once, then getDeviceSettings(device.deviceId) once per device (N+1 fan-out, sequential by design; do not parallelize), concatenating each device's alarms; a device with no alarms contributes nothing, never throws for that case

get_device_last_usedC

also used internally by pushWorkoutToDevice to resolve a missing deviceId

get_earned_badgesA

GETs /badge-service/badge/earned; each badge gains badgeImageUrls: { small, large }, added by this library (Garmin sends no image URL): https://connect.garmin.com/images/badges/xxhdpi/badge_<badgeUuid ?? badgeId>_sml.png (_lrg.png for large), the URL Garmin Connect's web app builds; public, no session. Every other field passes through unchanged; stays nullable (does NOT coalesce to [])

get_available_badgesB

GETs /badge-service/badge/available?showExclusiveBadge=true; same badgeImageUrls as getEarnedBadges; stays nullable

get_in_progress_badgesB

no HTTP path of its own: calls getEarnedBadges() and getAvailableBadges(), filters each with an in-progress predicate (progress truthy; if progress === target, only "in progress" when badgeLimitCount is set and badgeEarnedNumber < badgeLimitCount), then merges both filtered lists into a Map keyed by badgeId (available overwrites earned on collision, keeping the earned entry's position); never raises: a null from either call is treated as []. Badges carry badgeImageUrls (inherited from the two calls)

get_badge_detailA

GETs /badge-service/badge/detail/v3/{badgeId}, the request Garmin Connect's web app makes when a badge is opened; badgeId validated as a positive integer. Returns an OBJECT: the getEarnedBadges fields plus relatedBadges (the rest of the series, each with earnedByMe), badgeAssocType/badgeAssocDataId/badgeAssocDataName (for "activityId", the activity that earned it) and followings. Works for unearned badges. An unknown id is a 400 GarminHttpError, not a 404. The badge and each relatedBadges entry gain badgeImageUrls (see getEarnedBadges). Carries no description t…

get_adhoc_challengesB

GETs /adhocchallenge-service/adHocChallenge/historical; start validated non-negative, limit validated positive (throws GarminError otherwise); returns an ARRAY, verified live

get_badge_challengesA

GETs /badgechallenge-service/badgeChallenge/completed; same start/limit validation as getAdhocChallenges; returns an ARRAY, verified live. Live discovery: Garmin's server rejects start=0 with a 400 ("start should > 0.") on this endpoint even though the client-side check allows it (non-negative); the 400 is Garmin's server, not a wrong URL; call with start>=1 in practice

get_available_badge_challengesB

GETs /badgechallenge-service/badgeChallenge/available; same start/limit validation; same array-not-dict correction and same live start=0 -> 400 discovery as getBadgeChallenges

get_non_completed_badge_challengesC

GETs /badgechallenge-service/badgeChallenge/non-completed; same start/limit validation; same array-not-dict correction and same live start=0 -> 400 discovery as getBadgeChallenges

get_inprogress_virtual_challengesC

GETs /badgechallenge-service/virtualChallenge/inProgress; asymmetric validation: start validated POSITIVE here (rejects start=0), unlike the non-negative start on the four challenge methods above; limit validated positive; same array-not-dict correction

get_weigh_insC

Weigh-ins between two dates.

add_weigh_inC

defaults unitKey="kg", when=new Date()

delete_weigh_inA

Deletes one weigh-in, by date and its weightPk. IRREVERSIBLE: confirm with the user before calling.

add_weigh_in_with_timestampsB

same raw-value, no-conversion rule as addWeighIn; the two timestamps are COUPLED: the local instant resolves from dateTimestamp if given (naive strings read as LOCAL time) else from when (defaults new Date()), and gmtTimestamp, when omitted, is derived from THAT resolved instant, never independently from when. Both supplied strings are re-formatted rather than forwarded verbatim (a naive gmtTimestamp is read as UTC)

get_daily_weigh_insC

GETs /weight-service/weight/dayview/{cdate}?includeAll=true

delete_weigh_insA

no HTTP path of its own: calls getDailyWeighIns, then loops deleteWeighIn per entry; returns null (deletes nothing) if there are zero entries, or more than one entry and deleteAll is not true; otherwise deletes every entry that day and returns the count. IRREVERSIBLE: deletes ALL weigh-ins recorded on cdate when it proceeds IRREVERSIBLE: confirm with the user before calling.

get_body_compositionB

GETs /weight-service/weight/dateRange; enddate defaults to startdate; throws GarminError if startdate > enddate getStatsAndBody (wellness service) now delegates to this instead of inlining its own copy of the same call

add_body_compositionC

builds a .fit binary in memory (src/util/fit.ts, a hand-rolled FIT encoder) and uploads it via client.upload to /upload-service/upload; weight validated positive/finite, throws GarminError otherwise; passes client.upload's result through unchecked

get_max_metricsD

the date is repeated twice in the path (start=end=cdate)

get_max_metrics_rangeD

throws GarminError if start > end

get_functional_threshold_power_rangeC

defaults sport="RUNNING", aggregation="daily"; sport upper-cased and validated (^[A-Z_]+$); aggregation restricted to {daily,weekly,monthly,yearly}

get_lactate_thresholdC

defaults latest=true, aggregation="daily". TWO DIFFERENT branches: latest=true returns {speed_and_heart_rate, power} from two GETs; latest=false (requires startDate, throws otherwise) returns {speed, heart_rate, power} from three GETs

get_training_readinessB

Training readiness entries for one date.

get_morning_training_readinessB

delegates to getTrainingReadiness, no HTTP call of its own; filters for inputContext === "AFTER_WAKEUP_RESET", falls back to the first entry; null for a falsy or empty result

get_endurance_scoreB

TWO branches by presence of enddate: no enddate hits the single-day endpoint; with enddate hits .../stats with hard-coded aggregation="weekly"

get_running_toleranceC

defaults aggregation="weekly"; restricted to {daily,weekly} (narrower than the FTP/lactate methods)

get_race_predictionsA

TWO branches, all-or-nothing params (throws on a partial combination): no params hits .../latest/{displayName}; all three hit .../{type}/{displayName}, capped at a 366-day span

get_training_statusC

Training status for one date.

get_fitness_age_dataC

Fitness age for one date.

get_hill_scoreC

TWO branches by presence of enddate, same shape as getEnduranceScore but the range branch hard-codes aggregation="daily" (NOT "weekly"; do not conflate the two)

get_cycling_ftpA

latest value only; use getFunctionalThresholdPowerRange for history

get_heart_rate_zonesB

Heart-rate zone profiles: DEFAULT plus any per-sport ones.

set_heart_rate_zonesA

READ-MODIFY-WRITE of one profile: GETs /biometric-service/heartRateZones, overlays {sport? = "DEFAULT", trainingMethod?, maxHeartRate?, restingHeartRate?, lactateThresholdHeartRate?, zoneFloors?: [5 bpm]} with changeState: "CHANGED", PUTs [profile] (204), then returns the profile READ BACK. A sport with no profile starts from DEFAULT's. Setting restingHeartRate also turns off restingHrAutoUpdateUsed. Garmin does NOT recompute floors when the method or a heart rate changes. Send zoneFloors too. Floors must be strictly ascending (Garmin 400s `"Zone Floor values must be ascendi…

delete_heart_rate_zonesA

PUTs the stored profile back with changeState: "DELETED", which removes it so the sport falls back to DEFAULT. Refuses DEFAULT; throws for a sport with no profile instead of doing nothing IRREVERSIBLE: confirm with the user before calling.

get_power_zonesC

Power zone profiles.

get_power_zones_for_sportC

sport upper-cased and validated the same way as getFunctionalThresholdPowerRange

get_workoutsC

defaults start=0, limit=100, stays nullable (does NOT coalesce to [])

get_workout_by_idC

One workout from the library by id.

delete_workoutA

deletes the template from the workout library, irreversible IRREVERSIBLE: confirm with the user before calling.

download_workoutC

FIT-file bytes

push_workout_to_deviceB

multi-call: resolves a missing deviceId via /device-service/deviceservice/mylastused's userDeviceId, a missing workoutId via getWorkouts(0,1)'s first result (throws if none), then reads getWorkoutById(workoutId).workoutName for the push message on the final POST

get_scheduled_workoutsD

month is 1-12 on the way in, converted to 0-indexed on the wire; validates year>=2000, month 1-12

get_scheduled_workout_by_idC

uses a DIFFERENT base (/workout-service/schedule) than getScheduledWorkouts (/calendar-service)

get_next_scheduled_workoutB

computed from two getScheduledWorkouts calls (this month + next, handling Dec->Jan rollover); returns {} if nothing matches, never throws

schedule_workoutD

dateStr routed through formatDate

unschedule_workoutA

removes the calendar entry without deleting the workout template; irreversible IRREVERSIBLE: confirm with the user before calling.

get_adaptive_workoutA

GETs /workout-service/fbt-adaptive/{uuid}: a Garmin Coach workout with segments, steps and estimatedTrainingEffect. Coach workouts have no workoutId, and getWorkoutById cannot fetch them: /workout-service/workout/{uuid} is a 404. Take the uuid from getTrainingPlanWorkouts or getScheduledWorkoutSummaries

get_scheduled_workout_summariesA

POSTs the fixed GraphQL query workoutScheduleSummariesScalar(startDate, endDate): every scheduled workout in the range, plan or self-scheduled (tpType: null), as compact rows whose scheduledWorkoutId is what unscheduleWorkout takes, or null for a Garmin Coach workout, which also has tpType: null like a self-scheduled one (tell them apart by fbtAdaptivePlanId). Dates are validated before they are spliced into the query. A GraphQL error arrives as HTTP 200 with errors, turned into a GarminError. It LAGS writes: a just-scheduled workout is in the month feed at once but abse…

get_training_plan_workoutsB

POSTs GraphQL trainingPlanScalar(calendarDate, lang, firstDayOfWeek) (all three arguments are REQUIRED by Garmin) and unwraps trainingPlanWorkoutScheduleDTOS: one entry per enrolled plan, {trainingPlanId, planName, trainingPlanClassification, trainingPlanDetailsDTO, workoutScheduleSummaries}. Garmin picks the window (18 workouts across several weeks for an ITP plan); [] with no plan

list_calendar_eventsA

Events also appear in getScheduledWorkouts' month feed as itemType event; this GETs /calendar-service/events, with startDate/endDate when both dates are given (inclusive) or no params for every event on the account; one date alone throws

get_calendar_eventB

GETs /calendar-service/event/{id}; a missing id is a 404 GarminHttpError

get_shared_calendar_eventA

GETs /calendar-service/event/{uuid}/shareable: an event from Garmin's public events catalogue (Races & Events -> Find an Event), with locationStartPoint, subscribersCount, provider and subscribed. Works WITHOUT subscribing (subscribed: false). The plain /event/{uuid} is a 404. Events you create never get a shareable uuid (Garmin forces them private)

create_calendar_eventA

POSTs /calendar-service/event. CalendarEventInput = {name, date, startTime?: "HH:MM", timeZoneId?, eventType?, race?, primary?, distance?: {value, unit: meter|kilometer|yard|mile}, goalTimeSeconds?, location?, url?, note?}; only name/date are required by Garmin ('addEvent.arg3.eventName' must not be blank otherwise). Garmin forces every user-created event PRIVATE and unshareable whatever is sent, sets isTrainingEvent whenever isPrimaryEvent is, and answers an unknown eventType with a 500. It also rewrote Europe/Zagreb to Europe/Paris (same offset) while keeping `Amer…

update_calendar_eventA

READ-MODIFY-WRITE: GETs the event, overlays {name?, date?, location?, url?, note?, race?, goalTimeSeconds?} (null clears an optional one), PUTs the whole record back to /calendar-service/event/{id}; Garmin answers with no body, so it resolves to null. Concurrent callers can clobber each other

delete_calendar_eventA

DELETE /calendar-service/event/{id}, resolves null (204). IRREVERSIBLE IRREVERSIBLE: confirm with the user before calling.

get_menstrual_data_for_dateB

The menstrual-cycle day view for one date.

get_menstrual_calendar_dataC

Garmin rejects windows of 92+ inclusive days; not enforced here (caller's responsibility)

get_menstrual_last_confirmedC

The last confirmed period as of a date.

get_menstrual_cycle_summaryC

A summary of the menstrual cycle containing a date.

get_menstrual_reportsC

defaults numberOfCycles=6, nextReport=false, reportType="CYCLE", todayCalendarDate defaults to today; numberOfCycles restricted to {1,6,12} or throws GarminError

get_pregnancy_summaryC

The pregnancy-tracking summary.

update_menstrual_daily_logA

full-day replace, not a merge; at least one optional field required or throws GarminError; notes: undefined preserves the existing note, notes: "" clears it (distinct, load-bearing); discharge rejects combining "NO_DISCHARGE" with any other value IRREVERSIBLE: confirm with the user before calling.

update_menstrual_calendarA

full replace, not a merge; each cycleDatesLists group must be non-empty, consecutive calendar dates, and fall within [startdate, enddate], else throws GarminError IRREVERSIBLE: confirm with the user before calling.

init_menstrual_cycle_setupC

does NOT also update tracking-preference settings (call updateMenstrualSettings separately for that); not intended for an already-configured account

confirm_menstrual_period_startC

POSTs /periodichealth-service/menstrualcycle/{periodStartDate} directly, NOT the dayview/calendar/lastconfirmed/summary sub-paths

update_menstrual_settingsB

settings must be non-empty or throws GarminError; multi-step: GETs /userprofile-service/userprofile/user-settings, overlays settings onto the current userMenstrualCycleSettings, then PUTs the same endpoint, including id if resolvable

get_goalsA

defaults status="active", **start=1**, limit=30. start defaults to 1, NOT 0, because goal-service is 1-INDEXED and start=0 silently returns []; throws GarminError before any request for an invalid status. Paginated, multi-call: starting at start, fetches successive pages of limit entries (incrementing start by limit each call) until a page comes back empty/falsy, same fixed-page-size pattern as getActivitiesByDate; throws GarminError if MAX_PAGINATED_REQUESTS (2000) pages are fetched without ever seeing an empty one. **Sends the load-bearing `Sec-Fetch-Site: same…

get_golf_summaryC

defaults start=0, limit=100; start validated non-negative, limit validated positive (throws GarminError otherwise); query params are literally hyphenated (per-page, start), matching Garmin's own naming. An OBJECT, not an array, verified live: the test account (0 rounds recorded) returned a single pagination-envelope OBJECT {pageNumber, rowsPerPage, totalRows}, not an array

get_golf_scorecardC

GETs /gcs-golfcommunity/api/v2/scorecard/detail; hyphenated query params scorecard-ids and include-longest-shot-distance (sent as the literal string "true")

get_golf_shot_dataA

GETs /gcs-golfcommunity/api/v2/shot/scorecard/{scorecardId}/hole; holeNumbers accepts commas or hyphens as separators (spaces stripped), re-joined with - before sending as the hyphenated hole-numbers param; if any requested hole number is >9, the filter is silently dropped and all 18 holes are requested instead (Garmin's endpoint drops double-digit hole numbers from a filtered query); omitting holeNumbers also fetches all 18

get_golf_club_statsB

defaults limit=1000; validated positive; GETs /gcs-golfcommunity/api/v2/club/player; hyphenated query params per-page and include-stats (literal "true"). An ARRAY, verified live: the test account returned a JSON ARRAY of 17 club entries ({id, clubTypeId, shaftLength, flexTypeId, averageDistance, adviceDistance, retired, deleted, lastModifiedTime}), not a single object

get_golf_user_statsB

GETs /gcs-golfcommunity/api/v2/player/stats; handicap and strokes-gained overview, no params

get_nutrition_daily_food_logC

GETs /nutrition-service/food/logs/{cdate}

get_nutrition_daily_mealsC

GETs /nutrition-service/meals/{cdate}

get_nutrition_daily_settingsC

GETs /nutrition-service/settings/{cdate}

get_nutrition_food_log_rangeA

GETs /nutrition-service/food/logs/range?startDate&endDate: {dailyNutritionSummaries}, one day-log per day that has anything logged. The ONLY food-logging call that works without Connect+ (returns no days then)

search_foodsA

Requires a Garmin Connect+ subscription (check with has_connect_plus). GETs /nutrition-service/food/search?searchExpression&start&limit (defaults 0/20): {results: Food[], moreDataAvailable}; catalogue foods carry source: "FATSECRET" and several servings each. Needs Garmin Connect+, which needs a paired Garmin device (a bare 403 ForbiddenException without it)

get_custom_foodsA

Requires a Garmin Connect+ subscription (check with has_connect_plus). GETs /nutrition-service/customFood with includeContent=true: {customFoods, moreDataAvailable}. limit is capped at 20 (Garmin 400s above it). There is NO get-by-id: GET /customFood/{id} is a 405, so search by name to read one back. Needs Connect+

get_custom_food_serving_unitsB

Requires a Garmin Connect+ subscription (check with has_connect_plus). GETs /nutrition-service/metadata/customFoodServingUnits (13 units). Needs Connect+

create_custom_foodA

Requires a Garmin Connect+ subscription (check with has_connect_plus). A PUT, not a POST, to /nutrition-service/customFood; CustomFoodInput = {name, calories, servingUnit? = "G", servingSize? = 100, brand?, carbs?, protein?, fat?, fiber?, sugar?, saturatedFat?, transFat?, sodium?, cholesterol?, potassium?, calcium?, iron?, vitaminD?} per ONE serving, absolute amounts (not %DV); numbers are sent as strings, as Garmin's own client does. Returns the stored food with the foodId/servingId logFood takes. Needs Connect+

update_custom_foodC

Requires a Garmin Connect+ subscription (check with has_connect_plus). The same PUT carrying both ids. FULL REPLACE: a nutrient or brand left out is removed (verified: the brand was dropped). Needs Connect+

delete_custom_foodA

Requires a Garmin Connect+ subscription (check with has_connect_plus). DELETE /nutrition-service/customFood/{foodId}, resolves null. IRREVERSIBLE. Needs Connect+ IRREVERSIBLE: confirm with the user before calling.

log_foodA

Requires a Garmin Connect+ subscription (check with has_connect_plus). PUTs /nutrition-service/food/logs with one REGULAR_LOG item and returns the whole day's log. FoodLogInput = {date, foodId, servingId, servings? = 1, time?, meal?, source? = "GARMIN", regionCode?, languageCode?}. Pass the catalogue food's source/regionCode/languageCode for a search result. Needs a mealId, which only exists after Garmin's nutrition setup in the app (400 mealId must not be null otherwise; this method throws a clearer error first). The meal is the named one, else the one whose window holds time, else SNACKS; with a meal and no time it picks a time that…

quick_add_foodA

Requires a Garmin Connect+ subscription (check with has_connect_plus). PUTs /nutrition-service/food/logs/quickAdd: an entry by name + calories/carbs/protein/fat with no food behind it (QUICK_ADD). Same meal rules as logFood. Needs Connect+

delete_food_logsA

Requires a Garmin Connect+ subscription (check with has_connect_plus). DELETE /nutrition-service/food/logs/{date} with {logIds} as the body: any number in ONE call, regular and quick-add alike. logIds are on the entries of getNutritionDailyFoodLog. IRREVERSIBLE. Needs Connect+ IRREVERSIBLE: confirm with the user before calling.

get_training_plansC

GETs /trainingplan-service/trainingplan/plans; no params

get_training_plan_by_idC

GETs /trainingplan-service/trainingplan/phased/{planId}

get_adaptive_training_plan_by_idC

GETs /trainingplan-service/trainingplan/fbt-adaptive/{planId}, a distinct sub-path from getTrainingPlanById's phased path

delete_training_planA

DELETE /trainingplan-service/trainingplan/trainingplan/{planId} (the trainingplan segment really is doubled): the request Garmin Connect's "Quit Plan" sends, observed in the web client on 2026-10-06. Resolves null (204); the plan's scheduled workouts leave the calendar, completed activities stay. A second call is a 404 "Training plan not found with ID: …". IRREVERSIBLE IRREVERSIBLE: confirm with the user before calling.

get_lifestyle_logging_dataC

GETs /lifestylelogging-service/dailyLog/{cdate} Grouped under misc per the plan's explicit instruction, even though it superficially resembles a wellness-daily endpoint

request_reloadA

POSTs /wellness-service/wellness/epoch/request/{cdate} with no JSON body; asks Garmin to reload/recompute a day's data (Garmin offloads older data, so this forces it back)

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

C2.5/5.0

Scored across 190 tools

Disambiguation2/5

With 190 tools, several have overlapping or aliased purposes, such as get_stats/get_user_summary and get_stress_data/get_all_day_stress, which can confuse selection. Many descriptions are detailed, but the sheer breadth and near-duplicate activity/summary endpoints create unclear boundaries.

Naming Consistency4/5

Tool names are overwhelmingly snake_case with predictable verb_noun patterns like get_, create_, update_, delete_, and set_. Minor deviations exist, including noun-only accessors like display_name and user_name, plus state-style names like has_connect_plus.

Tool Count1/5

A 190-tool surface is far beyond a well-scoped MCP server and is extremely heavy for an agent to navigate. Even for a broad Garmin Connect API wrapper, this count creates severe selection and maintenance overhead.

Completeness4/5

The surface is remarkably complete across activities, wellness, nutrition, workouts, courses, gear, badges, challenges, menstrual data, devices, and calendar events. Minor gaps remain, such as no delete or update path for some read-only domains like hydration and goals.

Maintenance

ActivityMaintained
ResponsivenessNo issues