#
tokens: 19274/50000 18/18 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── garmin_mcp
│       ├── __init__.py
│       ├── activity_management.py
│       ├── challenges.py
│       ├── data_management.py
│       ├── devices.py
│       ├── gear_management.py
│       ├── health_wellness.py
│       ├── training.py
│       ├── user_profile.py
│       ├── weight_management.py
│       ├── womens_health.py
│       └── workouts.py
├── test_mcp_server.py
├── tests
│   ├── test_garmin.py
│   └── test_mcp_debug.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | __pycache__
2 | .env
3 | .venv/
4 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/taxuspt-garmin-mcp-badge.png)](https://mseep.ai/app/taxuspt-garmin-mcp)
 2 | 
 3 | # Garmin MCP Server
 4 | 
 5 | This Model Context Protocol (MCP) server connects to Garmin Connect and exposes your fitness and health data to Claude and other MCP-compatible clients.
 6 | 
 7 | ## Features
 8 | 
 9 | - List recent activities
10 | - Get detailed activity information
11 | - Access health metrics (steps, heart rate, sleep)
12 | - View body composition data
13 | 
14 | ## Setup
15 | 
16 | 1. Install the required packages on a new environment:
17 | 
18 | ```bash
19 | uv sync
20 | ```
21 | 
22 | ## Running the Server
23 | 
24 | ### With Claude Desktop
25 | 
26 | 1. Create a configuration in Claude Desktop:
27 | 
28 | Edit your Claude Desktop configuration file:
29 | 
30 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
31 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
32 | 
33 | Add this server configuration:
34 | 
35 | ```json
36 | {
37 |   "mcpServers": {
38 |     "garmin": {
39 |       "command": "uvx",
40 |       "args": [
41 |         "--python",
42 |         "3.12",
43 |         "--from",
44 |         "git+https://github.com/Taxuspt/garmin_mcp",
45 |         "garmin-mcp"
46 |       ],
47 |       "env": {
48 |         "GARMIN_EMAIL": "YOUR_GARMIN_EMAIL",
49 |         "GARMIN_PASSWORD": "YOUR_GARMIN_PASSWORD"
50 |       }
51 |     }
52 |   }
53 | }
54 | ```
55 | 
56 | Replace the path with the absolute path to your server file.
57 | 
58 | 2. Restart Claude Desktop
59 | 
60 | ### With MCP Inspector
61 | 
62 | For testing, you can use the MCP Inspector from the project root:
63 | 
64 | ```bash
65 | npx @modelcontextprotocol/inspector uv run garmin-mcp
66 | ```
67 | 
68 | ## Usage Examples
69 | 
70 | Once connected in Claude, you can ask questions like:
71 | 
72 | - "Show me my recent activities"
73 | - "What was my sleep like last night?"
74 | - "How many steps did I take yesterday?"
75 | - "Show me the details of my latest run"
76 | 
77 | ## Security Note
78 | 
79 | ## Troubleshooting
80 | 
81 | If you encounter login issues:
82 | 
83 | 1. Verify your credentials are correct
84 | 2. Check if Garmin Connect requires additional verification
85 | 3. Ensure the garminconnect package is up to date
86 | 
87 | For other issues, check the Claude Desktop logs at:
88 | 
89 | - macOS: `~/Library/Logs/Claude/mcp-server-garmin.log`
90 | - Windows: `%APPDATA%\Claude\logs\mcp-server-garmin.log`
91 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = [ "hatchling",]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "garmin-mcp"
 7 | version = "0.1.0"
 8 | description = "MCP server to access Garmin data"
 9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | dependencies = [
12 |     "python-dotenv==1.0.1",
13 |     "garminconnect==0.2.25",
14 |     "requests==2.32.3",
15 |     "mcp==1.3.0",
16 |     "garth==0.5.2",
17 | ]
18 | 
19 | [project.scripts]
20 | garmin-mcp = "garmin_mcp:main"
21 | 
22 | [tool.uv.sources]
23 | garmin-mcp = { workspace = true }
24 | 
```

--------------------------------------------------------------------------------
/tests/test_mcp_debug.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Debug version of the MCP server for direct testing
 3 | """
 4 | 
 5 | import asyncio
 6 | import datetime
 7 | import os
 8 | from pathlib import Path
 9 | 
10 | from dotenv import load_dotenv
11 | from garminconnect import Garmin
12 | 
13 | # Load environment variables from .env file
14 | env_path = Path(__file__).parent / '.env'
15 | load_dotenv(dotenv_path=env_path)
16 | 
17 | # Direct test function
18 | async def test_direct():
19 |     # Get credentials from environment
20 |     email = os.environ.get("GARMIN_EMAIL")
21 |     password = os.environ.get("GARMIN_PASSWORD")
22 | 
23 |     print(f"Logging in with email: {email}")
24 | 
25 |     try:
26 |         # Create and initialize Garmin client
27 |         client = Garmin(email, password)
28 |         client.login()
29 |         print("Login successful!")
30 | 
31 |         # Test activities
32 |         print("\nGetting recent activities...")
33 |         activities = client.get_activities(0, 2)
34 | 
35 |         if activities:
36 |             print(f"Found {len(activities)} activities")
37 |             for idx, activity in enumerate(activities, 1):
38 |                 print(f"\n--- Activity {idx} ---")
39 |                 print(f"Name: {activity.get('activityName', 'Unknown')}")
40 |         else:
41 |             print("No activities found")
42 | 
43 |         print("\nTest completed successfully!")
44 | 
45 |     except Exception as e:
46 |         print(f"Error: {str(e)}")
47 | 
48 | if __name__ == "__main__":
49 |     asyncio.run(test_direct())
50 | 
```

--------------------------------------------------------------------------------
/src/garmin_mcp/user_profile.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | User Profile functions for Garmin Connect MCP Server
 3 | """
 4 | import datetime
 5 | from typing import Any, Dict, List, Optional, Union
 6 | 
 7 | # The garmin_client will be set by the main file
 8 | garmin_client = None
 9 | 
10 | 
11 | def configure(client):
12 |     """Configure the module with the Garmin client instance"""
13 |     global garmin_client
14 |     garmin_client = client
15 | 
16 | 
17 | def register_tools(app):
18 |     """Register all user profile tools with the MCP server app"""
19 |     
20 |     @app.tool()
21 |     async def get_full_name() -> str:
22 |         """Get user's full name from profile"""
23 |         try:
24 |             full_name = garmin_client.get_full_name()
25 |             return full_name
26 |         except Exception as e:
27 |             return f"Error retrieving user's full name: {str(e)}"
28 | 
29 |     @app.tool()
30 |     async def get_unit_system() -> str:
31 |         """Get user's preferred unit system from profile"""
32 |         try:
33 |             unit_system = garmin_client.get_unit_system()
34 |             return unit_system
35 |         except Exception as e:
36 |             return f"Error retrieving unit system: {str(e)}"
37 |     
38 |     @app.tool()
39 |     async def get_user_profile() -> str:
40 |         """Get user profile information"""
41 |         try:
42 |             profile = garmin_client.get_user_profile()
43 |             if not profile:
44 |                 return "No user profile information found."
45 |             return profile
46 |         except Exception as e:
47 |             return f"Error retrieving user profile: {str(e)}"
48 | 
49 |     @app.tool()
50 |     async def get_userprofile_settings() -> str:
51 |         """Get user profile settings"""
52 |         try:
53 |             settings = garmin_client.get_userprofile_settings()
54 |             if not settings:
55 |                 return "No user profile settings found."
56 |             return settings
57 |         except Exception as e:
58 |             return f"Error retrieving user profile settings: {str(e)}"
59 | 
60 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/gear_management.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Gear management functions for Garmin Connect MCP Server
 3 | """
 4 | import datetime
 5 | from typing import Any, Dict, List, Optional, Union
 6 | 
 7 | # The garmin_client will be set by the main file
 8 | garmin_client = None
 9 | 
10 | 
11 | def configure(client):
12 |     """Configure the module with the Garmin client instance"""
13 |     global garmin_client
14 |     garmin_client = client
15 | 
16 | 
17 | def register_tools(app):
18 |     """Register all gear management tools with the MCP server app"""
19 |     
20 |     @app.tool()
21 |     async def get_gear(user_profile_id: str) -> str:
22 |         """Get all gear registered with the user account
23 |         
24 |         Args:
25 |             user_profile_id: User profile ID (can be obtained from get_device_last_used)
26 |         """
27 |         try:
28 |             gear = garmin_client.get_gear(user_profile_id)
29 |             if not gear:
30 |                 return "No gear found."
31 |             return gear
32 |         except Exception as e:
33 |             return f"Error retrieving gear: {str(e)}"
34 | 
35 |     @app.tool()
36 |     async def get_gear_defaults(user_profile_id: str) -> str:
37 |         """Get default gear settings
38 |         
39 |         Args:
40 |             user_profile_id: User profile ID (can be obtained from get_device_last_used)
41 |         """
42 |         try:
43 |             defaults = garmin_client.get_gear_defaults(user_profile_id)
44 |             if not defaults:
45 |                 return "No gear defaults found."
46 |             return defaults
47 |         except Exception as e:
48 |             return f"Error retrieving gear defaults: {str(e)}"
49 |     
50 |     @app.tool()
51 |     async def get_gear_stats(gear_uuid: str) -> str:
52 |         """Get statistics for specific gear
53 |         
54 |         Args:
55 |             gear_uuid: UUID of the gear item
56 |         """
57 |         try:
58 |             stats = garmin_client.get_gear_stats(gear_uuid)
59 |             if not stats:
60 |                 return f"No stats found for gear with UUID {gear_uuid}."
61 |             return stats
62 |         except Exception as e:
63 |             return f"Error retrieving gear stats: {str(e)}"
64 | 
65 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/womens_health.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Women's health functions for Garmin Connect MCP Server
 3 | """
 4 | import datetime
 5 | from typing import Any, Dict, List, Optional, Union
 6 | 
 7 | # The garmin_client will be set by the main file
 8 | garmin_client = None
 9 | 
10 | 
11 | def configure(client):
12 |     """Configure the module with the Garmin client instance"""
13 |     global garmin_client
14 |     garmin_client = client
15 | 
16 | 
17 | def register_tools(app):
18 |     """Register all women's health tools with the MCP server app"""
19 |     
20 |     @app.tool()
21 |     async def get_pregnancy_summary() -> str:
22 |         """Get pregnancy summary data"""
23 |         try:
24 |             summary = garmin_client.get_pregnancy_summary()
25 |             if not summary:
26 |                 return "No pregnancy summary data found."
27 |             return summary
28 |         except Exception as e:
29 |             return f"Error retrieving pregnancy summary: {str(e)}"
30 |     
31 |     @app.tool()
32 |     async def get_menstrual_data_for_date(date: str) -> str:
33 |         """Get menstrual data for a specific date
34 |         
35 |         Args:
36 |             date: Date in YYYY-MM-DD format
37 |         """
38 |         try:
39 |             data = garmin_client.get_menstrual_data_for_date(date)
40 |             if not data:
41 |                 return f"No menstrual data found for {date}."
42 |             return data
43 |         except Exception as e:
44 |             return f"Error retrieving menstrual data: {str(e)}"
45 |     
46 |     @app.tool()
47 |     async def get_menstrual_calendar_data(start_date: str, end_date: str) -> str:
48 |         """Get menstrual calendar data between specified dates
49 |         
50 |         Args:
51 |             start_date: Start date in YYYY-MM-DD format
52 |             end_date: End date in YYYY-MM-DD format
53 |         """
54 |         try:
55 |             data = garmin_client.get_menstrual_calendar_data(start_date, end_date)
56 |             if not data:
57 |                 return f"No menstrual calendar data found between {start_date} and {end_date}."
58 |             return data
59 |         except Exception as e:
60 |             return f"Error retrieving menstrual calendar data: {str(e)}"
61 | 
62 |     return app
```

--------------------------------------------------------------------------------
/test_mcp_server.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Test script for MCP server functionality
 3 | This script tests the MCP server directly without needing Claude Desktop
 4 | """
 5 | 
 6 | import asyncio
 7 | import sys
 8 | import json
 9 | from pathlib import Path
10 | from dotenv import load_dotenv
11 | 
12 | # Import MCP client for testing
13 | from mcp import ClientSession, StdioServerParameters
14 | from mcp.client.stdio import stdio_client
15 | 
16 | # Load environment variables
17 | load_dotenv()
18 | 
19 | 
20 | async def test_mcp_server():
21 |     """Test MCP server by simulating a client connection"""
22 |     # Path to the server script
23 |     server_script = Path(__file__).parent / "garmin_mcp_server.py"
24 | 
25 |     if not server_script.exists():
26 |         print(f"ERROR: Server script not found at {server_script}")
27 |         return
28 | 
29 |     print(f"Testing MCP server at: {server_script}")
30 | 
31 |     # Create server parameters
32 |     server_params = StdioServerParameters(
33 |         command="python",
34 |         args=[str(server_script)],
35 |         env=None,  # Uses current environment which includes .env variables
36 |     )
37 | 
38 |     try:
39 |         # Connect to server
40 |         print("Connecting to MCP server...")
41 |         async with stdio_client(server_params) as (read, write):
42 |             async with ClientSession(read, write) as session:
43 |                 # Initialize the connection
44 |                 print("Initializing connection...")
45 |                 await session.initialize()
46 | 
47 |                 # List available tools
48 |                 print("\nListing available tools:")
49 |                 tools = await session.list_tools()
50 |                 for tool in tools.tools:
51 |                     print(f"  - {tool.name}: {tool.description}")
52 | 
53 |                 # Test each tool with sample parameters
54 |                 print("\nTesting tools:")
55 | 
56 |                 # Test list_activities
57 |                 print("\nTesting list_activities...")
58 |                 try:
59 |                     result = await session.call_tool(
60 |                         "list_activities", arguments={"limit": 2}
61 |                     )
62 |                     print(f"Result: {result.content[0].text[:500]}...")
63 |                 except Exception as e:
64 |                     print(f"ERROR: {str(e)}")
65 | 
66 |                 # Test get_steps_data
67 |                 print("\nTesting get_steps_data...")
68 |                 try:
69 |                     result = await session.call_tool(
70 |                         "get_steps_data", arguments={}  # Uses default date (today)
71 |                     )
72 |                     print(f"Result: {result.content[0].text[:500]}...")
73 |                 except Exception as e:
74 |                     print(f"ERROR: {str(e)}")
75 | 
76 |                 print("\nMCP server test completed")
77 | 
78 |     except Exception as e:
79 |         print(f"ERROR: Failed to connect to MCP server: {str(e)}")
80 | 
81 | 
82 | if __name__ == "__main__":
83 |     asyncio.run(test_mcp_server())
84 | 
```

--------------------------------------------------------------------------------
/src/garmin_mcp/workouts.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Workout-related functions for Garmin Connect MCP Server
 3 | """
 4 | import datetime
 5 | from typing import Any, Dict, List, Optional, Union
 6 | 
 7 | # The garmin_client will be set by the main file
 8 | garmin_client = None
 9 | 
10 | 
11 | def configure(client):
12 |     """Configure the module with the Garmin client instance"""
13 |     global garmin_client
14 |     garmin_client = client
15 | 
16 | 
17 | def register_tools(app):
18 |     """Register all workout-related tools with the MCP server app"""
19 |     
20 |     @app.tool()
21 |     async def get_workouts() -> str:
22 |         """Get all workouts"""
23 |         try:
24 |             workouts = garmin_client.get_workouts()
25 |             if not workouts:
26 |                 return "No workouts found."
27 |             return workouts
28 |         except Exception as e:
29 |             return f"Error retrieving workouts: {str(e)}"
30 |     
31 |     @app.tool()
32 |     async def get_workout_by_id(workout_id: int) -> str:
33 |         """Get details for a specific workout
34 |         
35 |         Args:
36 |             workout_id: ID of the workout to retrieve
37 |         """
38 |         try:
39 |             workout = garmin_client.get_workout_by_id(workout_id)
40 |             if not workout:
41 |                 return f"No workout found with ID {workout_id}."
42 |             return workout
43 |         except Exception as e:
44 |             return f"Error retrieving workout: {str(e)}"
45 |     
46 |     @app.tool()
47 |     async def download_workout(workout_id: int) -> str:
48 |         """Download a workout as a FIT file (this will return a message about how to access the file)
49 |         
50 |         Args:
51 |             workout_id: ID of the workout to download
52 |         """
53 |         try:
54 |             workout_data = garmin_client.download_workout(workout_id)
55 |             if not workout_data:
56 |                 return f"No workout data found for workout with ID {workout_id}."
57 |             
58 |             # Since we can't return binary data directly, we'll inform the user
59 |             return f"Workout data for ID {workout_id} is available. The data is in FIT format and would need to be saved to a file."
60 |         except Exception as e:
61 |             return f"Error downloading workout: {str(e)}"
62 |     
63 |     @app.tool()
64 |     async def upload_workout(workout_json: str) -> str:
65 |         """Upload a workout from JSON data
66 |         
67 |         Args:
68 |             workout_json: JSON string containing workout data
69 |         """
70 |         try:
71 |             result = garmin_client.upload_workout(workout_json)
72 |             return result
73 |         except Exception as e:
74 |             return f"Error uploading workout: {str(e)}"
75 |             
76 |     @app.tool()
77 |     async def upload_activity(file_path: str) -> str:
78 |         """Upload an activity from a file (this is just a placeholder - file operations would need special handling)
79 |         
80 |         Args:
81 |             file_path: Path to the activity file (.fit, .gpx, .tcx)
82 |         """
83 |         try:
84 |             # This is a placeholder - actual implementation would need to handle file access
85 |             return f"Activity upload from file path {file_path} is not supported in this MCP server implementation."
86 |         except Exception as e:
87 |             return f"Error uploading activity: {str(e)}"
88 | 
89 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/devices.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Device-related functions for Garmin Connect MCP Server
 3 | """
 4 | import datetime
 5 | from typing import Any, Dict, List, Optional, Union
 6 | 
 7 | # The garmin_client will be set by the main file
 8 | garmin_client = None
 9 | 
10 | 
11 | def configure(client):
12 |     """Configure the module with the Garmin client instance"""
13 |     global garmin_client
14 |     garmin_client = client
15 | 
16 | 
17 | def register_tools(app):
18 |     """Register all device-related tools with the MCP server app"""
19 |     
20 |     @app.tool()
21 |     async def get_devices() -> str:
22 |         """Get all Garmin devices associated with the user account"""
23 |         try:
24 |             devices = garmin_client.get_devices()
25 |             if not devices:
26 |                 return "No devices found."
27 |             return devices
28 |         except Exception as e:
29 |             return f"Error retrieving devices: {str(e)}"
30 | 
31 |     @app.tool()
32 |     async def get_device_last_used() -> str:
33 |         """Get information about the last used Garmin device"""
34 |         try:
35 |             device = garmin_client.get_device_last_used()
36 |             if not device:
37 |                 return "No last used device found."
38 |             return device
39 |         except Exception as e:
40 |             return f"Error retrieving last used device: {str(e)}"
41 |     
42 |     @app.tool()
43 |     async def get_device_settings(device_id: str) -> str:
44 |         """Get settings for a specific Garmin device
45 |         
46 |         Args:
47 |             device_id: Device ID
48 |         """
49 |         try:
50 |             settings = garmin_client.get_device_settings(device_id)
51 |             if not settings:
52 |                 return f"No settings found for device ID {device_id}."
53 |             return settings
54 |         except Exception as e:
55 |             return f"Error retrieving device settings: {str(e)}"
56 | 
57 |     @app.tool()
58 |     async def get_primary_training_device() -> str:
59 |         """Get information about the primary training device"""
60 |         try:
61 |             device = garmin_client.get_primary_training_device()
62 |             if not device:
63 |                 return "No primary training device found."
64 |             return device
65 |         except Exception as e:
66 |             return f"Error retrieving primary training device: {str(e)}"
67 |     
68 |     @app.tool()
69 |     async def get_device_solar_data(device_id: str, date: str) -> str:
70 |         """Get solar data for a specific device
71 |         
72 |         Args:
73 |             device_id: Device ID
74 |             date: Date in YYYY-MM-DD format
75 |         """
76 |         try:
77 |             solar_data = garmin_client.get_device_solar_data(device_id, date)
78 |             if not solar_data:
79 |                 return f"No solar data found for device ID {device_id} on {date}."
80 |             return solar_data
81 |         except Exception as e:
82 |             return f"Error retrieving solar data: {str(e)}"
83 |     
84 |     @app.tool()
85 |     async def get_device_alarms() -> str:
86 |         """Get alarms from all Garmin devices"""
87 |         try:
88 |             alarms = garmin_client.get_device_alarms()
89 |             if not alarms:
90 |                 return "No device alarms found."
91 |             return alarms
92 |         except Exception as e:
93 |             return f"Error retrieving device alarms: {str(e)}"
94 | 
95 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/data_management.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Data management functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all data management tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def add_body_composition(
 22 |         date: str,
 23 |         weight: float,
 24 |         percent_fat: Optional[float] = None,
 25 |         percent_hydration: Optional[float] = None,
 26 |         visceral_fat_mass: Optional[float] = None,
 27 |         bone_mass: Optional[float] = None,
 28 |         muscle_mass: Optional[float] = None,
 29 |         basal_met: Optional[float] = None,
 30 |         active_met: Optional[float] = None,
 31 |         physique_rating: Optional[int] = None,
 32 |         metabolic_age: Optional[float] = None,
 33 |         visceral_fat_rating: Optional[int] = None,
 34 |         bmi: Optional[float] = None
 35 |     ) -> str:
 36 |         """Add body composition data
 37 |         
 38 |         Args:
 39 |             date: Date in YYYY-MM-DD format
 40 |             weight: Weight in kg
 41 |             percent_fat: Body fat percentage
 42 |             percent_hydration: Hydration percentage
 43 |             visceral_fat_mass: Visceral fat mass
 44 |             bone_mass: Bone mass
 45 |             muscle_mass: Muscle mass
 46 |             basal_met: Basal metabolic rate
 47 |             active_met: Active metabolic rate
 48 |             physique_rating: Physique rating
 49 |             metabolic_age: Metabolic age
 50 |             visceral_fat_rating: Visceral fat rating
 51 |             bmi: Body Mass Index
 52 |         """
 53 |         try:
 54 |             result = garmin_client.add_body_composition(
 55 |                 date,
 56 |                 weight=weight,
 57 |                 percent_fat=percent_fat,
 58 |                 percent_hydration=percent_hydration,
 59 |                 visceral_fat_mass=visceral_fat_mass,
 60 |                 bone_mass=bone_mass,
 61 |                 muscle_mass=muscle_mass,
 62 |                 basal_met=basal_met,
 63 |                 active_met=active_met,
 64 |                 physique_rating=physique_rating,
 65 |                 metabolic_age=metabolic_age,
 66 |                 visceral_fat_rating=visceral_fat_rating,
 67 |                 bmi=bmi
 68 |             )
 69 |             return result
 70 |         except Exception as e:
 71 |             return f"Error adding body composition data: {str(e)}"
 72 |     
 73 |     @app.tool()
 74 |     async def set_blood_pressure(
 75 |         systolic: int,
 76 |         diastolic: int,
 77 |         pulse: int,
 78 |         notes: Optional[str] = None
 79 |     ) -> str:
 80 |         """Set blood pressure values
 81 |         
 82 |         Args:
 83 |             systolic: Systolic pressure (top number)
 84 |             diastolic: Diastolic pressure (bottom number)
 85 |             pulse: Pulse rate
 86 |             notes: Optional notes
 87 |         """
 88 |         try:
 89 |             result = garmin_client.set_blood_pressure(
 90 |                 systolic, diastolic, pulse, notes=notes
 91 |             )
 92 |             return result
 93 |         except Exception as e:
 94 |             return f"Error setting blood pressure values: {str(e)}"
 95 |     
 96 |     @app.tool()
 97 |     async def add_hydration_data(
 98 |         value_in_ml: int,
 99 |         cdate: str,
100 |         timestamp: str
101 |     ) -> str:
102 |         """Add hydration data
103 |         
104 |         Args:
105 |             value_in_ml: Amount of liquid in milliliters
106 |             cdate: Date in YYYY-MM-DD format
107 |             timestamp: Timestamp in YYYY-MM-DDThh:mm:ss.sss format
108 |         """
109 |         try:
110 |             result = garmin_client.add_hydration_data(
111 |                 value_in_ml=value_in_ml,
112 |                 cdate=cdate,
113 |                 timestamp=timestamp
114 |             )
115 |             return result
116 |         except Exception as e:
117 |             return f"Error adding hydration data: {str(e)}"
118 | 
119 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/weight_management.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Weight management functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all weight management tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def get_weigh_ins(start_date: str, end_date: str) -> str:
 22 |         """Get weight measurements between specified dates
 23 |         
 24 |         Args:
 25 |             start_date: Start date in YYYY-MM-DD format
 26 |             end_date: End date in YYYY-MM-DD format
 27 |         """
 28 |         try:
 29 |             weigh_ins = garmin_client.get_weigh_ins(start_date, end_date)
 30 |             if not weigh_ins:
 31 |                 return f"No weight measurements found between {start_date} and {end_date}."
 32 |             return weigh_ins
 33 |         except Exception as e:
 34 |             return f"Error retrieving weight measurements: {str(e)}"
 35 | 
 36 |     @app.tool()
 37 |     async def get_daily_weigh_ins(date: str) -> str:
 38 |         """Get weight measurements for a specific date
 39 |         
 40 |         Args:
 41 |             date: Date in YYYY-MM-DD format
 42 |         """
 43 |         try:
 44 |             weigh_ins = garmin_client.get_daily_weigh_ins(date)
 45 |             if not weigh_ins:
 46 |                 return f"No weight measurements found for {date}."
 47 |             return weigh_ins
 48 |         except Exception as e:
 49 |             return f"Error retrieving daily weight measurements: {str(e)}"
 50 |     
 51 |     @app.tool()
 52 |     async def delete_weigh_ins(date: str, delete_all: bool = True) -> str:
 53 |         """Delete weight measurements for a specific date
 54 |         
 55 |         Args:
 56 |             date: Date in YYYY-MM-DD format
 57 |             delete_all: Whether to delete all measurements for the day
 58 |         """
 59 |         try:
 60 |             result = garmin_client.delete_weigh_ins(date, delete_all=delete_all)
 61 |             return result
 62 |         except Exception as e:
 63 |             return f"Error deleting weight measurements: {str(e)}"
 64 |     
 65 |     @app.tool()
 66 |     async def add_weigh_in(weight: float, unit_key: str = "kg") -> str:
 67 |         """Add a new weight measurement
 68 |         
 69 |         Args:
 70 |             weight: Weight value
 71 |             unit_key: Unit of weight ('kg' or 'lb')
 72 |         """
 73 |         try:
 74 |             result = garmin_client.add_weigh_in(weight=weight, unitKey=unit_key)
 75 |             return result
 76 |         except Exception as e:
 77 |             return f"Error adding weight measurement: {str(e)}"
 78 |     
 79 |     @app.tool()
 80 |     async def add_weigh_in_with_timestamps(
 81 |         weight: float, 
 82 |         unit_key: str = "kg", 
 83 |         date_timestamp: str = None, 
 84 |         gmt_timestamp: str = None
 85 |     ) -> str:
 86 |         """Add a new weight measurement with specific timestamps
 87 |         
 88 |         Args:
 89 |             weight: Weight value
 90 |             unit_key: Unit of weight ('kg' or 'lb')
 91 |             date_timestamp: Local timestamp in format YYYY-MM-DDThh:mm:ss
 92 |             gmt_timestamp: GMT timestamp in format YYYY-MM-DDThh:mm:ss
 93 |         """
 94 |         try:
 95 |             if date_timestamp is None or gmt_timestamp is None:
 96 |                 # Generate timestamps if not provided
 97 |                 now = datetime.datetime.now()
 98 |                 date_timestamp = now.strftime('%Y-%m-%dT%H:%M:%S')
 99 |                 gmt_timestamp = now.astimezone(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S')
100 |                 
101 |             result = garmin_client.add_weigh_in_with_timestamps(
102 |                 weight=weight,
103 |                 unitKey=unit_key,
104 |                 dateTimestamp=date_timestamp,
105 |                 gmtTimestamp=gmt_timestamp
106 |             )
107 |             return result
108 |         except Exception as e:
109 |             return f"Error adding weight measurement with timestamps: {str(e)}"
110 | 
111 |     return app
```

--------------------------------------------------------------------------------
/tests/test_garmin.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Test functions for the Garmin Connect API integration
  3 | This script allows you to test the connection and API functions without MCP
  4 | """
  5 | 
  6 | import os
  7 | import datetime
  8 | from pathlib import Path
  9 | import json
 10 | 
 11 | from dotenv import load_dotenv
 12 | from garminconnect import Garmin
 13 | 
 14 | # Load environment variables from .env file
 15 | env_path = Path(__file__).parent / '.env'
 16 | load_dotenv(dotenv_path=env_path)
 17 | 
 18 | def test_garmin_login():
 19 |     """Test Garmin Connect login"""
 20 |     email = os.environ.get("GARMIN_EMAIL")
 21 |     password = os.environ.get("GARMIN_PASSWORD")
 22 | 
 23 |     if not email or not password:
 24 |         print("ERROR: GARMIN_EMAIL and GARMIN_PASSWORD environment variables must be set in .env file")
 25 |         return False
 26 | 
 27 |     print(f"Attempting to login with email: {email}")
 28 | 
 29 |     try:
 30 |         client = Garmin(email, password)
 31 |         client.login()
 32 |         print("SUCCESS: Login successful")
 33 |         return client
 34 |     except Exception as e:
 35 |         print(f"ERROR: Login failed: {str(e)}")
 36 |         print("\nNote: Garmin Connect might require additional verification.")
 37 |         print("If this is the first time using this API, try logging in through the official Garmin website first.")
 38 |         return False
 39 | 
 40 | def test_activities(client, limit=3):
 41 |     """Test retrieving activities"""
 42 |     if not client:
 43 |         return
 44 | 
 45 |     try:
 46 |         activities = client.get_activities(0, limit)
 47 |         print(f"\nRetrieved {len(activities)} activities:")
 48 | 
 49 |         for idx, activity in enumerate(activities, 1):
 50 |             print(f"\n--- Activity {idx} ---")
 51 |             print(f"Name: {activity.get('activityName', 'Unknown')}")
 52 |             print(f"Type: {activity.get('activityType', {}).get('typeKey', 'Unknown')}")
 53 |             print(f"Date: {activity.get('startTimeLocal', 'Unknown')}")
 54 |             print(f"ID: {activity.get('activityId', 'Unknown')}")
 55 | 
 56 |         if activities:
 57 |             # Save the first activity ID for testing get_activity_details
 58 |             return activities[0].get('activityId')
 59 |     except Exception as e:
 60 |         print(f"ERROR: Failed to retrieve activities: {str(e)}")
 61 | 
 62 | def test_activity_details(client, activity_id):
 63 |     """Test retrieving activity details"""
 64 |     if not client or not activity_id:
 65 |         return
 66 | 
 67 |     try:
 68 |         activity = client.get_activity_details(activity_id)
 69 |         print(f"\nActivity Details for ID {activity_id}:")
 70 |         print(json.dumps(activity, indent=2)[:1000] + "... (truncated)")
 71 |     except Exception as e:
 72 |         print(f"ERROR: Failed to retrieve activity details: {str(e)}")
 73 | 
 74 | def test_health_data(client):
 75 |     """Test retrieving health data for today"""
 76 |     if not client:
 77 |         return
 78 | 
 79 |     today = datetime.date.today().strftime("%Y-%m-%d")
 80 |     print(f"\nTesting health data for {today}:")
 81 | 
 82 |     # Test steps data
 83 |     try:
 84 |         steps_data = client.get_steps_data(today)
 85 |         print("\nSteps Data:")
 86 |         print(f"Steps: {steps_data.get('steps', 0)}")
 87 |         print(f"Goal: {steps_data.get('dailyStepGoal', 0)}")
 88 |     except Exception as e:
 89 |         print(f"ERROR: Failed to retrieve steps data: {str(e)}")
 90 | 
 91 |     # Test heart rate data
 92 |     try:
 93 |         hr_data = client.get_heart_rates(today)
 94 |         print("\nHeart Rate Data:")
 95 |         print(f"Resting HR: {hr_data.get('restingHeartRate', 0)} bpm")
 96 |     except Exception as e:
 97 |         print(f"ERROR: Failed to retrieve heart rate data: {str(e)}")
 98 | 
 99 |     # Test sleep data
100 |     try:
101 |         sleep_data = client.get_sleep_data(today)
102 |         daily_sleep_data = sleep_data.get('dailySleepDTO', sleep_data)
103 |         print("\nSleep Data:")
104 |         sleep_score = daily_sleep_data.get('sleepScoreTotal', 0)
105 |         print(f"Sleep Score: {sleep_score}")
106 |     except Exception as e:
107 |         print(f"ERROR: Failed to retrieve sleep data: {str(e)}")
108 | 
109 | if __name__ == "__main__":
110 |     client = test_garmin_login()
111 |     if client:
112 |         activity_id = test_activities(client)
113 |         if activity_id:
114 |             test_activity_details(client, activity_id)
115 |         test_health_data(client)
116 | 
```

--------------------------------------------------------------------------------
/src/garmin_mcp/training.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Training and performance functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all training-related tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def get_progress_summary_between_dates(
 22 |         start_date: str, end_date: str, metric: str
 23 |     ) -> str:
 24 |         """Get progress summary for a metric between dates
 25 | 
 26 |         Args:
 27 |             start_date: Start date in YYYY-MM-DD format
 28 |             end_date: End date in YYYY-MM-DD format
 29 |             metric: Metric to get progress for (e.g., "elevationGain", "duration", "distance", "movingDuration")
 30 |         """
 31 |         try:
 32 |             summary = garmin_client.get_progress_summary_between_dates(
 33 |                 start_date, end_date, metric
 34 |             )
 35 |             if not summary:
 36 |                 return f"No progress summary found for {metric} between {start_date} and {end_date}."
 37 |             return summary
 38 |         except Exception as e:
 39 |             return f"Error retrieving progress summary: {str(e)}"
 40 |     
 41 |     @app.tool()
 42 |     async def get_hill_score(start_date: str, end_date: str) -> str:
 43 |         """Get hill score data between dates
 44 | 
 45 |         Args:
 46 |             start_date: Start date in YYYY-MM-DD format
 47 |             end_date: End date in YYYY-MM-DD format
 48 |         """
 49 |         try:
 50 |             hill_score = garmin_client.get_hill_score(start_date, end_date)
 51 |             if not hill_score:
 52 |                 return f"No hill score data found between {start_date} and {end_date}."
 53 |             return hill_score
 54 |         except Exception as e:
 55 |             return f"Error retrieving hill score data: {str(e)}"
 56 |     
 57 |     @app.tool()
 58 |     async def get_endurance_score(start_date: str, end_date: str) -> str:
 59 |         """Get endurance score data between dates
 60 | 
 61 |         Args:
 62 |             start_date: Start date in YYYY-MM-DD format
 63 |             end_date: End date in YYYY-MM-DD format
 64 |         """
 65 |         try:
 66 |             endurance_score = garmin_client.get_endurance_score(start_date, end_date)
 67 |             if not endurance_score:
 68 |                 return f"No endurance score data found between {start_date} and {end_date}."
 69 |             return endurance_score
 70 |         except Exception as e:
 71 |             return f"Error retrieving endurance score data: {str(e)}"
 72 |     
 73 |     @app.tool()
 74 |     async def get_training_effect(activity_id: int) -> str:
 75 |         """Get training effect data for a specific activity
 76 |         
 77 |         Args:
 78 |             activity_id: ID of the activity to retrieve training effect for
 79 |         """
 80 |         try:
 81 |             effect = garmin_client.get_training_effect(activity_id)
 82 |             if not effect:
 83 |                 return f"No training effect data found for activity with ID {activity_id}."
 84 |             return effect
 85 |         except Exception as e:
 86 |             return f"Error retrieving training effect data: {str(e)}"
 87 |     
 88 |     @app.tool()
 89 |     async def get_max_metrics(date: str) -> str:
 90 |         """Get max metrics data (like VO2 Max and fitness age)
 91 |         
 92 |         Args:
 93 |             date: Date in YYYY-MM-DD format
 94 |         """
 95 |         try:
 96 |             metrics = garmin_client.get_max_metrics(date)
 97 |             if not metrics:
 98 |                 return f"No max metrics data found for {date}."
 99 |             return metrics
100 |         except Exception as e:
101 |             return f"Error retrieving max metrics data: {str(e)}"
102 |     
103 |     @app.tool()
104 |     async def get_hrv_data(date: str) -> str:
105 |         """Get Heart Rate Variability (HRV) data
106 |         
107 |         Args:
108 |             date: Date in YYYY-MM-DD format
109 |         """
110 |         try:
111 |             hrv_data = garmin_client.get_hrv_data(date)
112 |             if not hrv_data:
113 |                 return f"No HRV data found for {date}."
114 |             return hrv_data
115 |         except Exception as e:
116 |             return f"Error retrieving HRV data: {str(e)}"
117 |     
118 |     @app.tool()
119 |     async def get_fitnessage_data(date: str) -> str:
120 |         """Get fitness age data
121 |         
122 |         Args:
123 |             date: Date in YYYY-MM-DD format
124 |         """
125 |         try:
126 |             fitness_age = garmin_client.get_fitnessage_data(date)
127 |             if not fitness_age:
128 |                 return f"No fitness age data found for {date}."
129 |             return fitness_age
130 |         except Exception as e:
131 |             return f"Error retrieving fitness age data: {str(e)}"
132 |     
133 |     @app.tool()
134 |     async def request_reload(date: str) -> str:
135 |         """Request reload of epoch data
136 |         
137 |         Args:
138 |             date: Date in YYYY-MM-DD format
139 |         """
140 |         try:
141 |             result = garmin_client.request_reload(date)
142 |             return result
143 |         except Exception as e:
144 |             return f"Error requesting data reload: {str(e)}"
145 | 
146 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/challenges.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Challenges and badges functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all challenges-related tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def get_goals(goal_type: str = "active") -> str:
 22 |         """Get Garmin Connect goals (active, future, or past)
 23 | 
 24 |         Args:
 25 |             goal_type: Type of goals to retrieve. Options: "active", "future", or "past"
 26 |         """
 27 |         try:
 28 |             goals = garmin_client.get_goals(goal_type)
 29 |             if not goals:
 30 |                 return f"No {goal_type} goals found."
 31 |             return goals
 32 |         except Exception as e:
 33 |             return f"Error retrieving {goal_type} goals: {str(e)}"
 34 | 
 35 |     @app.tool()
 36 |     async def get_personal_record() -> str:
 37 |         """Get personal records for user"""
 38 |         try:
 39 |             records = garmin_client.get_personal_record()
 40 |             if not records:
 41 |                 return "No personal records found."
 42 |             return records
 43 |         except Exception as e:
 44 |             return f"Error retrieving personal records: {str(e)}"
 45 | 
 46 |     @app.tool()
 47 |     async def get_earned_badges() -> str:
 48 |         """Get earned badges for user"""
 49 |         try:
 50 |             badges = garmin_client.get_earned_badges()
 51 |             if not badges:
 52 |                 return "No earned badges found."
 53 |             return badges
 54 |         except Exception as e:
 55 |             return f"Error retrieving earned badges: {str(e)}"
 56 | 
 57 |     @app.tool()
 58 |     async def get_adhoc_challenges(start: int = 0, limit: int = 100) -> str:
 59 |         """Get adhoc challenges data
 60 | 
 61 |         Args:
 62 |             start: Starting index for challenges retrieval
 63 |             limit: Maximum number of challenges to retrieve
 64 |         """
 65 |         try:
 66 |             challenges = garmin_client.get_adhoc_challenges(start, limit)
 67 |             if not challenges:
 68 |                 return "No adhoc challenges found."
 69 |             return challenges
 70 |         except Exception as e:
 71 |             return f"Error retrieving adhoc challenges: {str(e)}"
 72 | 
 73 |     @app.tool()
 74 |     async def get_available_badge_challenges(start: int = 1, limit: int = 100) -> str:
 75 |         """Get available badge challenges data
 76 | 
 77 |         Args:
 78 |             start: Starting index for challenges retrieval (starts at 1)
 79 |             limit: Maximum number of challenges to retrieve
 80 |         """
 81 |         try:
 82 |             challenges = garmin_client.get_available_badge_challenges(start, limit)
 83 |             if not challenges:
 84 |                 return "No available badge challenges found."
 85 |             return challenges
 86 |         except Exception as e:
 87 |             return f"Error retrieving available badge challenges: {str(e)}"
 88 | 
 89 |     @app.tool()
 90 |     async def get_badge_challenges(start: int = 1, limit: int = 100) -> str:
 91 |         """Get badge challenges data
 92 | 
 93 |         Args:
 94 |             start: Starting index for challenges retrieval (starts at 1)
 95 |             limit: Maximum number of challenges to retrieve
 96 |         """
 97 |         try:
 98 |             challenges = garmin_client.get_badge_challenges(start, limit)
 99 |             if not challenges:
100 |                 return "No badge challenges found."
101 |             return challenges
102 |         except Exception as e:
103 |             return f"Error retrieving badge challenges: {str(e)}"
104 | 
105 |     @app.tool()
106 |     async def get_non_completed_badge_challenges(start: int = 1, limit: int = 100) -> str:
107 |         """Get non-completed badge challenges data
108 | 
109 |         Args:
110 |             start: Starting index for challenges retrieval (starts at 1)
111 |             limit: Maximum number of challenges to retrieve
112 |         """
113 |         try:
114 |             challenges = garmin_client.get_non_completed_badge_challenges(start, limit)
115 |             if not challenges:
116 |                 return "No non-completed badge challenges found."
117 |             return challenges
118 |         except Exception as e:
119 |             return f"Error retrieving non-completed badge challenges: {str(e)}"
120 | 
121 |     @app.tool()
122 |     async def get_race_predictions() -> str:
123 |         """Get race predictions for user"""
124 |         try:
125 |             predictions = garmin_client.get_race_predictions()
126 |             if not predictions:
127 |                 return "No race predictions found."
128 |             return predictions
129 |         except Exception as e:
130 |             return f"Error retrieving race predictions: {str(e)}"
131 | 
132 |     @app.tool()
133 |     async def get_inprogress_virtual_challenges(start_date: str, end_date: str) -> str:
134 |         """Get in-progress virtual challenges/expeditions between dates
135 | 
136 |         Args:
137 |             start_date: Start date in YYYY-MM-DD format
138 |             end_date: End date in YYYY-MM-DD format
139 |         """
140 |         try:
141 |             challenges = garmin_client.get_inprogress_virtual_challenges(
142 |                 start_date, end_date
143 |             )
144 |             if not challenges:
145 |                 return f"No in-progress virtual challenges found between {start_date} and {end_date}."
146 |             return challenges
147 |         except Exception as e:
148 |             return f"Error retrieving in-progress virtual challenges: {str(e)}"
149 | 
150 |     return app
```

--------------------------------------------------------------------------------
/src/garmin_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Modular MCP Server for Garmin Connect Data
  3 | """
  4 | 
  5 | import os
  6 | 
  7 | import requests
  8 | from mcp.server.fastmcp import FastMCP
  9 | 
 10 | from garth.exc import GarthHTTPError
 11 | from garminconnect import Garmin, GarminConnectAuthenticationError
 12 | 
 13 | # Import all modules
 14 | from garmin_mcp import activity_management
 15 | from garmin_mcp import health_wellness
 16 | from garmin_mcp import user_profile
 17 | from garmin_mcp import devices
 18 | from garmin_mcp import gear_management
 19 | from garmin_mcp import weight_management
 20 | from garmin_mcp import challenges
 21 | from garmin_mcp import training
 22 | from garmin_mcp import workouts
 23 | from garmin_mcp import data_management
 24 | from garmin_mcp import womens_health
 25 | 
 26 | def get_mfa() -> str:
 27 |     """Get MFA code from user input"""
 28 |     print("\nGarmin Connect MFA required. Please check your email/phone for the code.")
 29 |     return input("Enter MFA code: ")
 30 | 
 31 | # Get credentials from environment
 32 | email = os.environ.get("GARMIN_EMAIL")
 33 | password = os.environ.get("GARMIN_PASSWORD")
 34 | tokenstore = os.getenv("GARMINTOKENS") or "~/.garminconnect"
 35 | tokenstore_base64 = os.getenv("GARMINTOKENS_BASE64") or "~/.garminconnect_base64"
 36 | 
 37 | 
 38 | def init_api(email, password):
 39 |     """Initialize Garmin API with your credentials."""
 40 | 
 41 |     try:
 42 |         # Using Oauth1 and OAuth2 token files from directory
 43 |         print(
 44 |             f"Trying to login to Garmin Connect using token data from directory '{tokenstore}'...\n"
 45 |         )
 46 | 
 47 |         # Using Oauth1 and Oauth2 tokens from base64 encoded string
 48 |         # print(
 49 |         #     f"Trying to login to Garmin Connect using token data from file '{tokenstore_base64}'...\n"
 50 |         # )
 51 |         # dir_path = os.path.expanduser(tokenstore_base64)
 52 |         # with open(dir_path, "r") as token_file:
 53 |         #     tokenstore = token_file.read()
 54 | 
 55 |         garmin = Garmin()
 56 |         garmin.login(tokenstore)
 57 | 
 58 |     except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError):
 59 |         # Session is expired. You'll need to log in again
 60 |         print(
 61 |             "Login tokens not present, login with your Garmin Connect credentials to generate them.\n"
 62 |             f"They will be stored in '{tokenstore}' for future use.\n"
 63 |         )
 64 |         try:
 65 |             garmin = Garmin(
 66 |                 email=email, password=password, is_cn=False, prompt_mfa=get_mfa
 67 |             )
 68 |             garmin.login()
 69 |             # Save Oauth1 and Oauth2 token files to directory for next login
 70 |             garmin.garth.dump(tokenstore)
 71 |             print(
 72 |                 f"Oauth tokens stored in '{tokenstore}' directory for future use. (first method)\n"
 73 |             )
 74 |             # Encode Oauth1 and Oauth2 tokens to base64 string and safe to file for next login (alternative way)
 75 |             token_base64 = garmin.garth.dumps()
 76 |             dir_path = os.path.expanduser(tokenstore_base64)
 77 |             with open(dir_path, "w") as token_file:
 78 |                 token_file.write(token_base64)
 79 |             print(
 80 |                 f"Oauth tokens encoded as base64 string and saved to '{dir_path}' file for future use. (second method)\n"
 81 |             )
 82 |         except (
 83 |             FileNotFoundError,
 84 |             GarthHTTPError,
 85 |             GarminConnectAuthenticationError,
 86 |             requests.exceptions.HTTPError,
 87 |         ) as err:
 88 |             print(err)
 89 |             return None
 90 | 
 91 |     return garmin
 92 | 
 93 | 
 94 | def main():
 95 |     """Initialize the MCP server and register all tools"""
 96 | 
 97 |     # Initialize Garmin client
 98 |     garmin_client = init_api(email, password)
 99 |     if not garmin_client:
100 |         print("Failed to initialize Garmin Connect client. Exiting.")
101 |         return
102 | 
103 |     print("Garmin Connect client initialized successfully.")
104 | 
105 |     # Configure all modules with the Garmin client
106 |     activity_management.configure(garmin_client)
107 |     health_wellness.configure(garmin_client)
108 |     user_profile.configure(garmin_client)
109 |     devices.configure(garmin_client)
110 |     gear_management.configure(garmin_client)
111 |     weight_management.configure(garmin_client)
112 |     challenges.configure(garmin_client)
113 |     training.configure(garmin_client)
114 |     workouts.configure(garmin_client)
115 |     data_management.configure(garmin_client)
116 |     womens_health.configure(garmin_client)
117 | 
118 |     # Create the MCP app
119 |     app = FastMCP("Garmin Connect v1.0")
120 | 
121 |     # Register tools from all modules
122 |     app = activity_management.register_tools(app)
123 |     app = health_wellness.register_tools(app)
124 |     app = user_profile.register_tools(app)
125 |     app = devices.register_tools(app)
126 |     app = gear_management.register_tools(app)
127 |     app = weight_management.register_tools(app)
128 |     app = challenges.register_tools(app)
129 |     app = training.register_tools(app)
130 |     app = workouts.register_tools(app)
131 |     app = data_management.register_tools(app)
132 |     app = womens_health.register_tools(app)
133 | 
134 |     # Add activity listing tool directly to the app
135 |     @app.tool()
136 |     async def list_activities(limit: int = 5) -> str:
137 |         """List recent Garmin activities"""
138 |         try:
139 |             activities = garmin_client.get_activities(0, limit)
140 | 
141 |             if not activities:
142 |                 return "No activities found."
143 | 
144 |             result = f"Last {len(activities)} activities:\n\n"
145 |             for idx, activity in enumerate(activities, 1):
146 |                 result += f"--- Activity {idx} ---\n"
147 |                 result += f"Activity: {activity.get('activityName', 'Unknown')}\n"
148 |                 result += (
149 |                     f"Type: {activity.get('activityType', {}).get('typeKey', 'Unknown')}\n"
150 |                 )
151 |                 result += f"Date: {activity.get('startTimeLocal', 'Unknown')}\n"
152 |                 result += f"ID: {activity.get('activityId', 'Unknown')}\n\n"
153 | 
154 |             return result
155 |         except Exception as e:
156 |             return f"Error retrieving activities: {str(e)}"
157 | 
158 |     # Run the MCP server
159 |     app.run()
160 | 
161 | 
162 | if __name__ == "__main__":
163 |     main()
164 | 
```

--------------------------------------------------------------------------------
/src/garmin_mcp/activity_management.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Activity Management functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all activity management tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def get_activities_by_date(start_date: str, end_date: str, activity_type: str = "") -> str:
 22 |         """Get activities data between specified dates, optionally filtered by activity type
 23 |         
 24 |         Args:
 25 |             start_date: Start date in YYYY-MM-DD format
 26 |             end_date: End date in YYYY-MM-DD format
 27 |             activity_type: Optional activity type filter (e.g., cycling, running, swimming)
 28 |         """
 29 |         try:
 30 |             activities = garmin_client.get_activities_by_date(start_date, end_date, activity_type)
 31 |             if not activities:
 32 |                 return f"No activities found between {start_date} and {end_date}" + \
 33 |                        (f" for activity type '{activity_type}'" if activity_type else "")
 34 |             
 35 |             return activities
 36 |         except Exception as e:
 37 |             return f"Error retrieving activities by date: {str(e)}"
 38 | 
 39 |     @app.tool()
 40 |     async def get_activities_fordate(date: str) -> str:
 41 |         """Get activities for a specific date
 42 |         
 43 |         Args:
 44 |             date: Date in YYYY-MM-DD format
 45 |         """
 46 |         try:
 47 |             activities = garmin_client.get_activities_fordate(date)
 48 |             if not activities:
 49 |                 return f"No activities found for {date}"
 50 |             
 51 |             return activities
 52 |         except Exception as e:
 53 |             return f"Error retrieving activities for date: {str(e)}"
 54 | 
 55 |     @app.tool()
 56 |     async def get_activity(activity_id: int) -> str:
 57 |         """Get basic activity information
 58 |         
 59 |         Args:
 60 |             activity_id: ID of the activity to retrieve
 61 |         """
 62 |         try:
 63 |             activity = garmin_client.get_activity(activity_id)
 64 |             if not activity:
 65 |                 return f"No activity found with ID {activity_id}"
 66 |             
 67 |             return activity
 68 |         except Exception as e:
 69 |             return f"Error retrieving activity: {str(e)}"
 70 | 
 71 |     @app.tool()
 72 |     async def get_activity_splits(activity_id: int) -> str:
 73 |         """Get splits for an activity
 74 |         
 75 |         Args:
 76 |             activity_id: ID of the activity to retrieve splits for
 77 |         """
 78 |         try:
 79 |             splits = garmin_client.get_activity_splits(activity_id)
 80 |             if not splits:
 81 |                 return f"No splits found for activity with ID {activity_id}"
 82 |             
 83 |             return splits
 84 |         except Exception as e:
 85 |             return f"Error retrieving activity splits: {str(e)}"
 86 | 
 87 |     @app.tool()
 88 |     async def get_activity_typed_splits(activity_id: int) -> str:
 89 |         """Get typed splits for an activity
 90 |         
 91 |         Args:
 92 |             activity_id: ID of the activity to retrieve typed splits for
 93 |         """
 94 |         try:
 95 |             typed_splits = garmin_client.get_activity_typed_splits(activity_id)
 96 |             if not typed_splits:
 97 |                 return f"No typed splits found for activity with ID {activity_id}"
 98 |             
 99 |             return typed_splits
100 |         except Exception as e:
101 |             return f"Error retrieving activity typed splits: {str(e)}"
102 | 
103 |     @app.tool()
104 |     async def get_activity_split_summaries(activity_id: int) -> str:
105 |         """Get split summaries for an activity
106 |         
107 |         Args:
108 |             activity_id: ID of the activity to retrieve split summaries for
109 |         """
110 |         try:
111 |             split_summaries = garmin_client.get_activity_split_summaries(activity_id)
112 |             if not split_summaries:
113 |                 return f"No split summaries found for activity with ID {activity_id}"
114 |             
115 |             return split_summaries
116 |         except Exception as e:
117 |             return f"Error retrieving activity split summaries: {str(e)}"
118 | 
119 |     @app.tool()
120 |     async def get_activity_weather(activity_id: int) -> str:
121 |         """Get weather data for an activity
122 |         
123 |         Args:
124 |             activity_id: ID of the activity to retrieve weather data for
125 |         """
126 |         try:
127 |             weather = garmin_client.get_activity_weather(activity_id)
128 |             if not weather:
129 |                 return f"No weather data found for activity with ID {activity_id}"
130 |             
131 |             return weather
132 |         except Exception as e:
133 |             return f"Error retrieving activity weather data: {str(e)}"
134 | 
135 |     @app.tool()
136 |     async def get_activity_hr_in_timezones(activity_id: int) -> str:
137 |         """Get heart rate data in different time zones for an activity
138 |         
139 |         Args:
140 |             activity_id: ID of the activity to retrieve heart rate time zone data for
141 |         """
142 |         try:
143 |             hr_zones = garmin_client.get_activity_hr_in_timezones(activity_id)
144 |             if not hr_zones:
145 |                 return f"No heart rate time zone data found for activity with ID {activity_id}"
146 |             
147 |             return hr_zones
148 |         except Exception as e:
149 |             return f"Error retrieving activity heart rate time zone data: {str(e)}"
150 | 
151 |     @app.tool()
152 |     async def get_activity_gear(activity_id: int) -> str:
153 |         """Get gear data used for an activity
154 |         
155 |         Args:
156 |             activity_id: ID of the activity to retrieve gear data for
157 |         """
158 |         try:
159 |             gear = garmin_client.get_activity_gear(activity_id)
160 |             if not gear:
161 |                 return f"No gear data found for activity with ID {activity_id}"
162 |             
163 |             return gear
164 |         except Exception as e:
165 |             return f"Error retrieving activity gear data: {str(e)}"
166 | 
167 |     @app.tool()
168 |     async def get_activity_exercise_sets(activity_id: int) -> str:
169 |         """Get exercise sets for strength training activities
170 |         
171 |         Args:
172 |             activity_id: ID of the activity to retrieve exercise sets for
173 |         """
174 |         try:
175 |             exercise_sets = garmin_client.get_activity_exercise_sets(activity_id)
176 |             if not exercise_sets:
177 |                 return f"No exercise sets found for activity with ID {activity_id}"
178 |             
179 |             return exercise_sets
180 |         except Exception as e:
181 |             return f"Error retrieving activity exercise sets: {str(e)}"
182 | 
183 |     return app
184 | 
```

--------------------------------------------------------------------------------
/src/garmin_mcp/health_wellness.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Health & Wellness Data functions for Garmin Connect MCP Server
  3 | """
  4 | import datetime
  5 | from typing import Any, Dict, List, Optional, Union
  6 | 
  7 | # The garmin_client will be set by the main file
  8 | garmin_client = None
  9 | 
 10 | 
 11 | def configure(client):
 12 |     """Configure the module with the Garmin client instance"""
 13 |     global garmin_client
 14 |     garmin_client = client
 15 | 
 16 | 
 17 | def register_tools(app):
 18 |     """Register all health and wellness tools with the MCP server app"""
 19 |     
 20 |     @app.tool()
 21 |     async def get_stats(date: str) -> str:
 22 |         """Get daily activity stats
 23 |         
 24 |         Args:
 25 |             date: Date in YYYY-MM-DD format
 26 |         """
 27 |         try:
 28 |             stats = garmin_client.get_stats(date)
 29 |             if not stats:
 30 |                 return f"No stats found for {date}"
 31 |             
 32 |             return stats
 33 |         except Exception as e:
 34 |             return f"Error retrieving stats: {str(e)}"
 35 | 
 36 |     @app.tool()
 37 |     async def get_user_summary(date: str) -> str:
 38 |         """Get user summary data (compatible with garminconnect-ha)
 39 |         
 40 |         Args:
 41 |             date: Date in YYYY-MM-DD format
 42 |         """
 43 |         try:
 44 |             summary = garmin_client.get_user_summary(date)
 45 |             if not summary:
 46 |                 return f"No user summary found for {date}"
 47 |             
 48 |             return summary
 49 |         except Exception as e:
 50 |             return f"Error retrieving user summary: {str(e)}"
 51 | 
 52 |     @app.tool()
 53 |     async def get_body_composition(start_date: str, end_date: str = None) -> str:
 54 |         """Get body composition data for a single date or date range
 55 |         
 56 |         Args:
 57 |             start_date: Date in YYYY-MM-DD format or start date if end_date provided
 58 |             end_date: Optional end date in YYYY-MM-DD format for date range
 59 |         """
 60 |         try:
 61 |             if end_date:
 62 |                 composition = garmin_client.get_body_composition(start_date, end_date)
 63 |                 if not composition:
 64 |                     return f"No body composition data found between {start_date} and {end_date}"
 65 |             else:
 66 |                 composition = garmin_client.get_body_composition(start_date)
 67 |                 if not composition:
 68 |                     return f"No body composition data found for {start_date}"
 69 |             
 70 |             return composition
 71 |         except Exception as e:
 72 |             return f"Error retrieving body composition data: {str(e)}"
 73 | 
 74 |     @app.tool()
 75 |     async def get_stats_and_body(date: str) -> str:
 76 |         """Get stats and body composition data
 77 |         
 78 |         Args:
 79 |             date: Date in YYYY-MM-DD format
 80 |         """
 81 |         try:
 82 |             data = garmin_client.get_stats_and_body(date)
 83 |             if not data:
 84 |                 return f"No stats and body composition data found for {date}"
 85 |             
 86 |             return data
 87 |         except Exception as e:
 88 |             return f"Error retrieving stats and body composition data: {str(e)}"
 89 | 
 90 |     @app.tool()
 91 |     async def get_steps_data(date: str) -> str:
 92 |         """Get steps data
 93 |         
 94 |         Args:
 95 |             date: Date in YYYY-MM-DD format
 96 |         """
 97 |         try:
 98 |             steps_data = garmin_client.get_steps_data(date)
 99 |             if not steps_data:
100 |                 return f"No steps data found for {date}"
101 |             
102 |             return steps_data
103 |         except Exception as e:
104 |             return f"Error retrieving steps data: {str(e)}"
105 | 
106 |     @app.tool()
107 |     async def get_daily_steps(start_date: str, end_date: str) -> str:
108 |         """Get steps data for a date range
109 |         
110 |         Args:
111 |             start_date: Start date in YYYY-MM-DD format
112 |             end_date: End date in YYYY-MM-DD format
113 |         """
114 |         try:
115 |             steps_data = garmin_client.get_daily_steps(start_date, end_date)
116 |             if not steps_data:
117 |                 return f"No daily steps data found between {start_date} and {end_date}"
118 |             
119 |             return steps_data
120 |         except Exception as e:
121 |             return f"Error retrieving daily steps data: {str(e)}"
122 | 
123 |     @app.tool()
124 |     async def get_training_readiness(date: str) -> str:
125 |         """Get training readiness data
126 |         
127 |         Args:
128 |             date: Date in YYYY-MM-DD format
129 |         """
130 |         try:
131 |             readiness = garmin_client.get_training_readiness(date)
132 |             if not readiness:
133 |                 return f"No training readiness data found for {date}"
134 |             
135 |             return readiness
136 |         except Exception as e:
137 |             return f"Error retrieving training readiness data: {str(e)}"
138 | 
139 |     @app.tool()
140 |     async def get_body_battery(start_date: str, end_date: str) -> str:
141 |         """Get body battery data
142 |         
143 |         Args:
144 |             start_date: Start date in YYYY-MM-DD format
145 |             end_date: End date in YYYY-MM-DD format
146 |         """
147 |         try:
148 |             battery_data = garmin_client.get_body_battery(start_date, end_date)
149 |             if not battery_data:
150 |                 return f"No body battery data found between {start_date} and {end_date}"
151 |             
152 |             return battery_data
153 |         except Exception as e:
154 |             return f"Error retrieving body battery data: {str(e)}"
155 | 
156 |     @app.tool()
157 |     async def get_body_battery_events(date: str) -> str:
158 |         """Get body battery events data
159 |         
160 |         Args:
161 |             date: Date in YYYY-MM-DD format
162 |         """
163 |         try:
164 |             events = garmin_client.get_body_battery_events(date)
165 |             if not events:
166 |                 return f"No body battery events found for {date}"
167 |             
168 |             return events
169 |         except Exception as e:
170 |             return f"Error retrieving body battery events: {str(e)}"
171 | 
172 |     @app.tool()
173 |     async def get_blood_pressure(start_date: str, end_date: str) -> str:
174 |         """Get blood pressure data
175 |         
176 |         Args:
177 |             start_date: Start date in YYYY-MM-DD format
178 |             end_date: End date in YYYY-MM-DD format
179 |         """
180 |         try:
181 |             bp_data = garmin_client.get_blood_pressure(start_date, end_date)
182 |             if not bp_data:
183 |                 return f"No blood pressure data found between {start_date} and {end_date}"
184 |             
185 |             return bp_data
186 |         except Exception as e:
187 |             return f"Error retrieving blood pressure data: {str(e)}"
188 | 
189 |     @app.tool()
190 |     async def get_floors(date: str) -> str:
191 |         """Get floors climbed data
192 |         
193 |         Args:
194 |             date: Date in YYYY-MM-DD format
195 |         """
196 |         try:
197 |             floors_data = garmin_client.get_floors(date)
198 |             if not floors_data:
199 |                 return f"No floors data found for {date}"
200 |             
201 |             return floors_data
202 |         except Exception as e:
203 |             return f"Error retrieving floors data: {str(e)}"
204 | 
205 |     @app.tool()
206 |     async def get_training_status(date: str) -> str:
207 |         """Get training status data
208 |         
209 |         Args:
210 |             date: Date in YYYY-MM-DD format
211 |         """
212 |         try:
213 |             status = garmin_client.get_training_status(date)
214 |             if not status:
215 |                 return f"No training status data found for {date}"
216 |             
217 |             return status
218 |         except Exception as e:
219 |             return f"Error retrieving training status data: {str(e)}"
220 | 
221 |     @app.tool()
222 |     async def get_rhr_day(date: str) -> str:
223 |         """Get resting heart rate data
224 |         
225 |         Args:
226 |             date: Date in YYYY-MM-DD format
227 |         """
228 |         try:
229 |             rhr_data = garmin_client.get_rhr_day(date)
230 |             if not rhr_data:
231 |                 return f"No resting heart rate data found for {date}"
232 |             
233 |             return rhr_data
234 |         except Exception as e:
235 |             return f"Error retrieving resting heart rate data: {str(e)}"
236 | 
237 |     @app.tool()
238 |     async def get_heart_rates(date: str) -> str:
239 |         """Get heart rate data
240 |         
241 |         Args:
242 |             date: Date in YYYY-MM-DD format
243 |         """
244 |         try:
245 |             hr_data = garmin_client.get_heart_rates(date)
246 |             if not hr_data:
247 |                 return f"No heart rate data found for {date}"
248 |             
249 |             return hr_data
250 |         except Exception as e:
251 |             return f"Error retrieving heart rate data: {str(e)}"
252 | 
253 |     @app.tool()
254 |     async def get_hydration_data(date: str) -> str:
255 |         """Get hydration data
256 |         
257 |         Args:
258 |             date: Date in YYYY-MM-DD format
259 |         """
260 |         try:
261 |             hydration_data = garmin_client.get_hydration_data(date)
262 |             if not hydration_data:
263 |                 return f"No hydration data found for {date}"
264 |             
265 |             return hydration_data
266 |         except Exception as e:
267 |             return f"Error retrieving hydration data: {str(e)}"
268 | 
269 |     @app.tool()
270 |     async def get_sleep_data(date: str) -> str:
271 |         """Get sleep data
272 |         
273 |         Args:
274 |             date: Date in YYYY-MM-DD format
275 |         """
276 |         try:
277 |             sleep_data = garmin_client.get_sleep_data(date)
278 |             if not sleep_data:
279 |                 return f"No sleep data found for {date}"
280 |             
281 |             return sleep_data
282 |         except Exception as e:
283 |             return f"Error retrieving sleep data: {str(e)}"
284 | 
285 |     @app.tool()
286 |     async def get_stress_data(date: str) -> str:
287 |         """Get stress data
288 |         
289 |         Args:
290 |             date: Date in YYYY-MM-DD format
291 |         """
292 |         try:
293 |             stress_data = garmin_client.get_stress_data(date)
294 |             if not stress_data:
295 |                 return f"No stress data found for {date}"
296 |             
297 |             return stress_data
298 |         except Exception as e:
299 |             return f"Error retrieving stress data: {str(e)}"
300 | 
301 |     @app.tool()
302 |     async def get_respiration_data(date: str) -> str:
303 |         """Get respiration data
304 |         
305 |         Args:
306 |             date: Date in YYYY-MM-DD format
307 |         """
308 |         try:
309 |             respiration_data = garmin_client.get_respiration_data(date)
310 |             if not respiration_data:
311 |                 return f"No respiration data found for {date}"
312 |             
313 |             return respiration_data
314 |         except Exception as e:
315 |             return f"Error retrieving respiration data: {str(e)}"
316 | 
317 |     @app.tool()
318 |     async def get_spo2_data(date: str) -> str:
319 |         """Get SpO2 (blood oxygen) data
320 |         
321 |         Args:
322 |             date: Date in YYYY-MM-DD format
323 |         """
324 |         try:
325 |             spo2_data = garmin_client.get_spo2_data(date)
326 |             if not spo2_data:
327 |                 return f"No SpO2 data found for {date}"
328 |             
329 |             return spo2_data
330 |         except Exception as e:
331 |             return f"Error retrieving SpO2 data: {str(e)}"
332 | 
333 |     @app.tool()
334 |     async def get_all_day_stress(date: str) -> str:
335 |         """Get all-day stress data
336 |         
337 |         Args:
338 |             date: Date in YYYY-MM-DD format
339 |         """
340 |         try:
341 |             stress_data = garmin_client.get_all_day_stress(date)
342 |             if not stress_data:
343 |                 return f"No all-day stress data found for {date}"
344 |             
345 |             return stress_data
346 |         except Exception as e:
347 |             return f"Error retrieving all-day stress data: {str(e)}"
348 | 
349 |     @app.tool()
350 |     async def get_all_day_events(date: str) -> str:
351 |         """Get daily wellness events data
352 |         
353 |         Args:
354 |             date: Date in YYYY-MM-DD format
355 |         """
356 |         try:
357 |             events = garmin_client.get_all_day_events(date)
358 |             if not events:
359 |                 return f"No daily wellness events found for {date}"
360 |             
361 |             return events
362 |         except Exception as e:
363 |             return f"Error retrieving daily wellness events: {str(e)}"
364 | 
365 |     return app
```