# 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 | [](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
```