mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-01-25 08:35:03 +00:00
sync
This commit is contained in:
848
main.py
Normal file
848
main.py
Normal file
@@ -0,0 +1,848 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cycling Workout Analyzer with Garth MCP Server Integration
|
||||
A Python app that uses OpenRouter AI and Garmin data via MCP to analyze cycling workouts
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from pathlib import Path
|
||||
import aiohttp
|
||||
import yaml
|
||||
from dataclasses import dataclass
|
||||
|
||||
# MCP Protocol imports
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
print("MCP not available. Install with: pip install mcp")
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Application configuration"""
|
||||
openrouter_api_key: str
|
||||
openrouter_model: str = "deepseek/deepseek-r1-0528:free"
|
||||
garth_token: str = "" # GARTH_TOKEN for authentication
|
||||
garth_mcp_server_path: str = "uvx" # Use uvx to run garth-mcp-server
|
||||
rules_file: str = "rules.yaml"
|
||||
templates_dir: str = "templates"
|
||||
|
||||
class OpenRouterClient:
|
||||
"""Client for OpenRouter AI API"""
|
||||
|
||||
def __init__(self, api_key: str, model: str):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.base_url = "https://openrouter.ai/api/v1"
|
||||
|
||||
async def generate_response(self, prompt: str, available_tools: List[Dict] = None) -> str:
|
||||
"""Generate AI response from prompt, optionally with MCP tools available"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/your-username/cycling-analyzer",
|
||||
"X-Title": "Cycling Workout Analyzer"
|
||||
}
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
# Add tool information if available
|
||||
if available_tools:
|
||||
tool_info = "\n\nAvailable Garmin data tools:\n"
|
||||
for tool in available_tools:
|
||||
tool_info += f"- {tool['name']}: {tool.get('description', 'No description')}\n"
|
||||
messages[0]["content"] += tool_info
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"max_tokens": 2000,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers=headers,
|
||||
json=payload
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
else:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"OpenRouter API error: {response.status} - {error_text}")
|
||||
|
||||
class GarthMCPConnector:
|
||||
"""Connector for Garmin data via Garth MCP server"""
|
||||
|
||||
def __init__(self, garth_token: str, server_path: str):
|
||||
self.garth_token = garth_token
|
||||
self.server_path = server_path
|
||||
self.server_available = False
|
||||
self.cached_tools = [] # Cache tools to avoid repeated fetches
|
||||
self.session = None # Persistent MCP session
|
||||
self.server_params = None # Server parameters for reconnection
|
||||
self._connected = False # Connection status
|
||||
|
||||
async def _get_server_params(self):
|
||||
"""Get server parameters for MCP connection"""
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = self.garth_token
|
||||
|
||||
return StdioServerParameters(
|
||||
command=self.server_path,
|
||||
args=["garth-mcp-server"],
|
||||
env=env
|
||||
)
|
||||
|
||||
async def _execute_with_session(self, operation_func):
|
||||
"""Execute an operation with a fresh MCP session"""
|
||||
if not MCP_AVAILABLE:
|
||||
raise Exception("MCP library not available. Install with: pip install mcp")
|
||||
|
||||
server_params = await self._get_server_params()
|
||||
|
||||
async with stdio_client(server_params) as (read_stream, write_stream):
|
||||
session = ClientSession(read_stream, write_stream)
|
||||
await session.initialize()
|
||||
|
||||
# Execute the operation
|
||||
result = await operation_func(session)
|
||||
|
||||
return result
|
||||
|
||||
async def connect(self):
|
||||
"""Test connection to MCP server"""
|
||||
try:
|
||||
await self._execute_with_session(lambda session: session.list_tools())
|
||||
self.server_available = True
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MCP server: {e}")
|
||||
self.server_available = False
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect - no persistent connection to cleanup"""
|
||||
self.server_available = False
|
||||
self.cached_tools = [] # Clear cache
|
||||
|
||||
async def _ensure_connected(self):
|
||||
"""Ensure server is available"""
|
||||
if not self.server_available:
|
||||
return await self.connect()
|
||||
return True
|
||||
|
||||
async def start_mcp_server(self):
|
||||
"""Start the Garth MCP server and initialize session"""
|
||||
if not MCP_AVAILABLE:
|
||||
logger.warning("MCP library not available. Install with: pip install mcp")
|
||||
return False
|
||||
|
||||
if self.server_available:
|
||||
return True # Already confirmed available
|
||||
|
||||
try:
|
||||
# Create environment with Garth token
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = self.garth_token
|
||||
|
||||
# Start the MCP server using uvx
|
||||
server_params = StdioServerParameters(
|
||||
command=self.server_path,
|
||||
args=["garth-mcp-server"],
|
||||
env=env
|
||||
)
|
||||
|
||||
logger.info("Starting Garth MCP server...")
|
||||
|
||||
# Use the stdio_client context manager - this must be done in the same async context
|
||||
async with stdio_client(server_params) as (read_stream, write_stream):
|
||||
# Create the client session
|
||||
session = ClientSession(read_stream, write_stream)
|
||||
|
||||
# Initialize the session
|
||||
result = await session.initialize()
|
||||
logger.info("MCP server initialized successfully")
|
||||
|
||||
# Get available tools and resources
|
||||
try:
|
||||
tools_result = await session.list_tools()
|
||||
self.tools = tools_result.tools if tools_result else []
|
||||
|
||||
resources_result = await session.list_resources()
|
||||
self.resources = resources_result.resources if resources_result else []
|
||||
|
||||
logger.info(f"Available tools: {[tool.name for tool in self.tools]}")
|
||||
logger.info(f"Available resources: {[resource.name for resource in self.resources]}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not list tools/resources: {e}")
|
||||
self.tools = []
|
||||
self.resources = []
|
||||
|
||||
# Mark server as available
|
||||
self.server_available = True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start MCP server: {e}")
|
||||
logger.error("Make sure uvx is installed and garth-mcp-server is available")
|
||||
logger.error("Try installing uvx with: pip install uv")
|
||||
logger.error("Then get your GARTH_TOKEN with: uvx garth login")
|
||||
logger.error(f"Current server path: {self.server_path}")
|
||||
return False
|
||||
|
||||
async def ensure_server_available(self):
|
||||
"""Ensure MCP server is available"""
|
||||
return await self._ensure_connected()
|
||||
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict[str, Any] = None) -> Any:
|
||||
"""Call a tool on the MCP server"""
|
||||
if not await self._ensure_connected():
|
||||
raise Exception("MCP server not available")
|
||||
|
||||
async def _call_tool(session):
|
||||
return await session.call_tool(tool_name, arguments or {})
|
||||
|
||||
try:
|
||||
return await self._execute_with_session(_call_tool)
|
||||
except Exception as e:
|
||||
logger.error(f"Tool call failed: {e}")
|
||||
raise
|
||||
|
||||
async def get_activities_data(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get activities data via MCP or fallback to mock data"""
|
||||
if not self.session:
|
||||
logger.warning("No MCP session available, using mock data")
|
||||
return self._get_mock_activities_data(limit)
|
||||
|
||||
try:
|
||||
# Try different possible tool names for getting activities
|
||||
possible_tools = ['get_activities', 'list_activities', 'activities', 'garmin_activities']
|
||||
|
||||
for tool_name in possible_tools:
|
||||
if any(tool.name == tool_name for tool in self.tools):
|
||||
result = await self.call_tool(tool_name, {"limit": limit})
|
||||
if result and hasattr(result, 'content'):
|
||||
# Parse the result based on MCP response format
|
||||
activities = []
|
||||
for content in result.content:
|
||||
if hasattr(content, 'text'):
|
||||
# Try to parse as JSON
|
||||
try:
|
||||
data = json.loads(content.text)
|
||||
if isinstance(data, list):
|
||||
activities.extend(data)
|
||||
else:
|
||||
activities.append(data)
|
||||
except json.JSONDecodeError:
|
||||
# If not JSON, treat as text description
|
||||
activities.append({"description": content.text})
|
||||
return activities
|
||||
|
||||
logger.warning("No suitable activity tool found, falling back to mock data")
|
||||
return self._get_mock_activities_data(limit)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get activities via MCP: {e}")
|
||||
logger.warning("Falling back to mock data")
|
||||
return self._get_mock_activities_data(limit)
|
||||
|
||||
def _get_mock_activities_data(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get mock activities data for testing"""
|
||||
base_activity = {
|
||||
"activityId": "12345678901",
|
||||
"activityName": "Morning Ride",
|
||||
"startTimeLocal": "2024-01-15T08:00:00",
|
||||
"activityType": {"typeKey": "cycling"},
|
||||
"distance": 25000, # meters
|
||||
"duration": 3600, # seconds
|
||||
"averageSpeed": 6.94, # m/s
|
||||
"maxSpeed": 12.5, # m/s
|
||||
"elevationGain": 350, # meters
|
||||
"averageHR": 145,
|
||||
"maxHR": 172,
|
||||
"averagePower": 180,
|
||||
"maxPower": 420,
|
||||
"normalizedPower": 185,
|
||||
"calories": 890,
|
||||
"averageCadence": 85,
|
||||
"maxCadence": 110
|
||||
}
|
||||
|
||||
activities = []
|
||||
for i in range(min(limit, 10)):
|
||||
activity = base_activity.copy()
|
||||
activity["activityId"] = str(int(base_activity["activityId"]) + i)
|
||||
activity["activityName"] = f"Cycling Workout {i+1}"
|
||||
# Vary the data slightly
|
||||
activity["distance"] = base_activity["distance"] + (i * 2000)
|
||||
activity["averagePower"] = base_activity["averagePower"] + (i * 10)
|
||||
activity["duration"] = base_activity["duration"] + (i * 300)
|
||||
activities.append(activity)
|
||||
|
||||
return activities
|
||||
|
||||
async def get_last_cycling_workout(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get the most recent cycling workout"""
|
||||
activities = await self.get_activities_data(limit=50)
|
||||
|
||||
# Filter for cycling activities
|
||||
cycling_activities = [
|
||||
activity for activity in activities
|
||||
if self._is_cycling_activity(activity)
|
||||
]
|
||||
|
||||
return cycling_activities[0] if cycling_activities else None
|
||||
|
||||
async def get_last_n_cycling_workouts(self, n: int = 4) -> List[Dict[str, Any]]:
|
||||
"""Get the last N cycling workouts"""
|
||||
activities = await self.get_activities_data(limit=50)
|
||||
|
||||
# Filter for cycling activities
|
||||
cycling_activities = [
|
||||
activity for activity in activities
|
||||
if self._is_cycling_activity(activity)
|
||||
]
|
||||
|
||||
return cycling_activities[:n]
|
||||
|
||||
def _is_cycling_activity(self, activity: Dict[str, Any]) -> bool:
|
||||
"""Check if an activity is a cycling workout"""
|
||||
activity_type = activity.get('activityType', {}).get('typeKey', '').lower()
|
||||
activity_name = activity.get('activityName', '').lower()
|
||||
|
||||
cycling_keywords = ['cycling', 'bike', 'ride', 'bicycle']
|
||||
|
||||
return (
|
||||
'cycling' in activity_type or
|
||||
'bike' in activity_type or
|
||||
any(keyword in activity_name for keyword in cycling_keywords)
|
||||
)
|
||||
|
||||
async def get_available_tools_info(self) -> List[Dict[str, str]]:
|
||||
"""Get information about available MCP tools"""
|
||||
# Return cached tools if available
|
||||
if self.cached_tools:
|
||||
return self.cached_tools
|
||||
|
||||
if not await self._ensure_connected():
|
||||
return []
|
||||
|
||||
async def _get_tools(session):
|
||||
tools_result = await session.list_tools()
|
||||
tools = tools_result.tools if tools_result else []
|
||||
|
||||
# Cache the tools for future use
|
||||
self.cached_tools = [
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": getattr(tool, 'description', 'No description available')
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
|
||||
return self.cached_tools
|
||||
|
||||
try:
|
||||
return await self._execute_with_session(_get_tools)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get tools info: {e}")
|
||||
return []
|
||||
|
||||
class TemplateManager:
|
||||
"""Manages prompt templates"""
|
||||
|
||||
def __init__(self, templates_dir: str):
|
||||
self.templates_dir = Path(templates_dir)
|
||||
self.templates_dir.mkdir(exist_ok=True)
|
||||
self._create_default_templates()
|
||||
|
||||
def _create_default_templates(self):
|
||||
"""Create default template files if they don't exist"""
|
||||
templates = {
|
||||
"single_workout_analysis.txt": """
|
||||
Analyze my cycling workout against my training rules and goals.
|
||||
|
||||
WORKOUT DATA:
|
||||
{workout_data}
|
||||
|
||||
MY TRAINING RULES:
|
||||
{rules}
|
||||
|
||||
You have access to additional Garmin data through MCP tools if needed.
|
||||
|
||||
Please provide:
|
||||
1. Overall assessment of the workout
|
||||
2. How well it aligns with my rules and goals
|
||||
3. Areas for improvement
|
||||
4. Specific feedback on power, heart rate, duration, and intensity
|
||||
5. Recovery recommendations
|
||||
6. Comparison with my typical performance metrics
|
||||
""".strip(),
|
||||
|
||||
"workout_recommendation.txt": """
|
||||
Based on my recent cycling workouts, suggest what workout I should do next.
|
||||
|
||||
RECENT WORKOUTS:
|
||||
{workouts_data}
|
||||
|
||||
MY TRAINING RULES:
|
||||
{rules}
|
||||
|
||||
You have access to additional Garmin data and tools to analyze my fitness trends.
|
||||
|
||||
Please provide:
|
||||
1. Analysis of my recent training pattern
|
||||
2. Identified gaps or imbalances in my training
|
||||
3. Specific workout recommendation for my next session
|
||||
4. Target zones (power, heart rate, duration)
|
||||
5. Rationale for the recommendation based on my recent performance
|
||||
6. Alternative options if weather/time constraints exist
|
||||
7. How this fits into my overall training progression
|
||||
""".strip(),
|
||||
|
||||
"mcp_enhanced_analysis.txt": """
|
||||
You are an expert cycling coach with access to comprehensive Garmin Connect data through MCP tools.
|
||||
|
||||
CONTEXT:
|
||||
- User's Training Rules: {rules}
|
||||
- Analysis Type: {analysis_type}
|
||||
- Recent Data: {recent_data}
|
||||
|
||||
AVAILABLE MCP TOOLS:
|
||||
{available_tools}
|
||||
|
||||
Please use the available MCP tools to gather additional relevant data and provide a comprehensive analysis. Focus on:
|
||||
|
||||
1. **Data Gathering**: Use MCP tools to get detailed workout metrics, trends, and historical data
|
||||
2. **Performance Analysis**: Analyze power, heart rate, training load, and recovery metrics
|
||||
3. **Training Periodization**: Consider the user's training phase and progression
|
||||
4. **Actionable Recommendations**: Provide specific, measurable guidance for future workouts
|
||||
5. **Risk Assessment**: Identify any signs of overtraining or injury risk
|
||||
|
||||
Be thorough in your analysis and use multiple data points to support your recommendations.
|
||||
""".strip()
|
||||
}
|
||||
|
||||
for filename, content in templates.items():
|
||||
template_path = self.templates_dir / filename
|
||||
if not template_path.exists():
|
||||
template_path.write_text(content)
|
||||
logger.info(f"Created template: {template_path}")
|
||||
|
||||
def get_template(self, template_name: str) -> str:
|
||||
"""Get template content"""
|
||||
template_path = self.templates_dir / template_name
|
||||
if template_path.exists():
|
||||
return template_path.read_text()
|
||||
else:
|
||||
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||
|
||||
def list_templates(self) -> List[str]:
|
||||
"""List available templates"""
|
||||
return [f.name for f in self.templates_dir.glob("*.txt")]
|
||||
|
||||
class RulesManager:
|
||||
"""Manages training rules and goals"""
|
||||
|
||||
def __init__(self, rules_file: str):
|
||||
self.rules_file = Path(rules_file)
|
||||
self._create_default_rules()
|
||||
|
||||
def _create_default_rules(self):
|
||||
"""Create default rules file if it doesn't exist"""
|
||||
if not self.rules_file.exists():
|
||||
default_rules = {
|
||||
"training_goals": [
|
||||
"Improve FTP (Functional Threshold Power)",
|
||||
"Build endurance for 100km rides",
|
||||
"Maintain consistent training 4-5x per week"
|
||||
],
|
||||
"power_zones": {
|
||||
"zone_1_active_recovery": "< 142W",
|
||||
"zone_2_endurance": "142-162W",
|
||||
"zone_3_tempo": "163-180W",
|
||||
"zone_4_lactate_threshold": "181-196W",
|
||||
"zone_5_vo2_max": "197-224W",
|
||||
"zone_6_anaerobic": "> 224W"
|
||||
},
|
||||
"heart_rate_zones": {
|
||||
"zone_1": "< 129 bpm",
|
||||
"zone_2": "129-146 bpm",
|
||||
"zone_3": "147-163 bpm",
|
||||
"zone_4": "164-181 bpm",
|
||||
"zone_5": "> 181 bpm"
|
||||
},
|
||||
"weekly_structure": {
|
||||
"easy_rides": "60-70% of weekly volume",
|
||||
"moderate_rides": "20-30% of weekly volume",
|
||||
"hard_rides": "5-15% of weekly volume"
|
||||
},
|
||||
"recovery_rules": [
|
||||
"At least 1 full rest day per week",
|
||||
"Easy spin after hard workouts",
|
||||
"Listen to body - skip workout if overly fatigued"
|
||||
],
|
||||
"workout_preferences": [
|
||||
"Prefer morning rides when possible",
|
||||
"Include variety - not just steady state",
|
||||
"Focus on consistency over peak performance"
|
||||
]
|
||||
}
|
||||
|
||||
with open(self.rules_file, 'w') as f:
|
||||
yaml.dump(default_rules, f, default_flow_style=False)
|
||||
logger.info(f"Created default rules file: {self.rules_file}")
|
||||
|
||||
def get_rules(self) -> str:
|
||||
"""Get rules as formatted string"""
|
||||
with open(self.rules_file, 'r') as f:
|
||||
rules = yaml.safe_load(f)
|
||||
|
||||
return yaml.dump(rules, default_flow_style=False)
|
||||
|
||||
class CyclingAnalyzer:
|
||||
"""Main application class"""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.openrouter = OpenRouterClient(config.openrouter_api_key, config.openrouter_model)
|
||||
self.garmin = GarthMCPConnector(
|
||||
config.garth_token,
|
||||
config.garth_mcp_server_path
|
||||
)
|
||||
self.templates = TemplateManager(config.templates_dir)
|
||||
self.rules = RulesManager(config.rules_file)
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the application and connect to MCP server"""
|
||||
logger.info("Initializing application and connecting to MCP server...")
|
||||
success = await self.garmin.connect()
|
||||
if success:
|
||||
logger.info("Application initialized successfully")
|
||||
else:
|
||||
logger.warning("Application initialized but MCP server connection failed - will retry on demand")
|
||||
return True # Always return True to allow the app to start
|
||||
|
||||
async def cleanup(self):
|
||||
"""Cleanup resources"""
|
||||
await self.garmin.disconnect()
|
||||
logger.info("Application cleanup completed")
|
||||
|
||||
async def analyze_last_workout(self):
|
||||
"""Analyze the last cycling workout"""
|
||||
logger.info("Analyzing last cycling workout...")
|
||||
|
||||
try:
|
||||
# Get workout data via MCP
|
||||
workout = await self.garmin.get_last_cycling_workout()
|
||||
|
||||
if not workout:
|
||||
return "No recent cycling workouts found in your Garmin data."
|
||||
|
||||
# Get rules
|
||||
rules_text = self.rules.get_rules()
|
||||
|
||||
# Format workout data
|
||||
workout_text = json.dumps(workout, indent=2)
|
||||
|
||||
# Get available tools info
|
||||
available_tools = await self.garmin.get_available_tools_info()
|
||||
|
||||
# Get template and format prompt
|
||||
template = self.templates.get_template("single_workout_analysis.txt")
|
||||
prompt = template.format(workout_data=workout_text, rules=rules_text)
|
||||
|
||||
# Get AI analysis with tool information
|
||||
analysis = await self.openrouter.generate_response(prompt, available_tools)
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing workout: {e}")
|
||||
return f"Error analyzing workout: {e}"
|
||||
|
||||
async def suggest_next_workout(self):
|
||||
"""Suggest next workout based on recent activities"""
|
||||
logger.info("Analyzing recent workouts and suggesting next workout...")
|
||||
|
||||
try:
|
||||
# Get last 4 workouts via MCP
|
||||
workouts = await self.garmin.get_last_n_cycling_workouts(4)
|
||||
|
||||
if not workouts:
|
||||
return "No recent cycling workouts found in your Garmin data."
|
||||
|
||||
# Get rules
|
||||
rules_text = self.rules.get_rules()
|
||||
|
||||
# Format workouts data
|
||||
workouts_text = json.dumps(workouts, indent=2)
|
||||
|
||||
# Get available tools info
|
||||
available_tools = await self.garmin.get_available_tools_info()
|
||||
|
||||
# Get template and format prompt
|
||||
template = self.templates.get_template("workout_recommendation.txt")
|
||||
prompt = template.format(workouts_data=workouts_text, rules=rules_text)
|
||||
|
||||
# Get AI suggestion with tool information
|
||||
suggestion = await self.openrouter.generate_response(prompt, available_tools)
|
||||
|
||||
return suggestion
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error suggesting workout: {e}")
|
||||
return f"Error suggesting next workout: {e}"
|
||||
|
||||
async def mcp_enhanced_analysis(self, analysis_type: str):
|
||||
"""Perform enhanced analysis using MCP tools directly"""
|
||||
logger.info(f"Performing MCP-enhanced {analysis_type} analysis...")
|
||||
|
||||
try:
|
||||
# Get rules
|
||||
rules_text = self.rules.get_rules()
|
||||
|
||||
# Get recent data
|
||||
recent_workouts = await self.garmin.get_last_n_cycling_workouts(7)
|
||||
recent_data = json.dumps(recent_workouts[:3], indent=2) if recent_workouts else "No recent data"
|
||||
|
||||
# Get available tools info
|
||||
available_tools_info = "\n".join([
|
||||
f"- {tool['name']}: {tool['description']}"
|
||||
for tool in await self.garmin.get_available_tools_info()
|
||||
])
|
||||
|
||||
# Get enhanced template
|
||||
template = self.templates.get_template("mcp_enhanced_analysis.txt")
|
||||
prompt = template.format(
|
||||
rules=rules_text,
|
||||
analysis_type=analysis_type,
|
||||
recent_data=recent_data,
|
||||
available_tools=available_tools_info
|
||||
)
|
||||
|
||||
# Get AI analysis with full tool context
|
||||
analysis = await self.openrouter.generate_response(
|
||||
prompt,
|
||||
await self.garmin.get_available_tools_info()
|
||||
)
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in MCP enhanced analysis: {e}")
|
||||
return f"Error in enhanced analysis: {e}"
|
||||
|
||||
async def run(self):
|
||||
"""Main application loop"""
|
||||
logger.info("Starting Cycling Workout Analyzer with Garth MCP Server...")
|
||||
|
||||
# Initialize MCP connection (with fallback mode)
|
||||
await self.initialize()
|
||||
|
||||
try:
|
||||
while True:
|
||||
print("\n" + "="*60)
|
||||
print("CYCLING WORKOUT ANALYZER (with Garth MCP Integration)")
|
||||
print("="*60)
|
||||
print("1. Analyze last cycling workout")
|
||||
print("2. Get next workout suggestion")
|
||||
print("3. Enhanced analysis using MCP tools")
|
||||
print("4. List available MCP tools")
|
||||
print("5. List available templates")
|
||||
print("6. View current rules")
|
||||
print("7. Exit")
|
||||
print("-"*60)
|
||||
|
||||
choice = input("Enter your choice (1-7): ").strip()
|
||||
|
||||
try:
|
||||
if choice == "1":
|
||||
print("\nAnalyzing your last workout...")
|
||||
analysis = await self.analyze_last_workout()
|
||||
print("\n" + "="*50)
|
||||
print("WORKOUT ANALYSIS")
|
||||
print("="*50)
|
||||
print(analysis)
|
||||
|
||||
elif choice == "2":
|
||||
print("\nAnalyzing recent workouts and generating suggestion...")
|
||||
suggestion = await self.suggest_next_workout()
|
||||
print("\n" + "="*50)
|
||||
print("NEXT WORKOUT SUGGESTION")
|
||||
print("="*50)
|
||||
print(suggestion)
|
||||
|
||||
elif choice == "3":
|
||||
print("\nSelect analysis type:")
|
||||
print("a) Performance trends")
|
||||
print("b) Training load analysis")
|
||||
print("c) Recovery assessment")
|
||||
analysis_choice = input("Enter choice (a-c): ").strip().lower()
|
||||
|
||||
analysis_types = {
|
||||
'a': 'performance trends',
|
||||
'b': 'training load',
|
||||
'c': 'recovery assessment'
|
||||
}
|
||||
|
||||
if analysis_choice in analysis_types:
|
||||
analysis = await self.mcp_enhanced_analysis(
|
||||
analysis_types[analysis_choice]
|
||||
)
|
||||
print(f"\n{'='*50}")
|
||||
print(f"ENHANCED {analysis_types[analysis_choice].upper()} ANALYSIS")
|
||||
print("="*50)
|
||||
print(analysis)
|
||||
else:
|
||||
print("Invalid choice.")
|
||||
|
||||
elif choice == "4":
|
||||
try:
|
||||
tools = await self.garmin.get_available_tools_info()
|
||||
print(f"\nAvailable MCP tools from Garth server:")
|
||||
if tools:
|
||||
for tool in tools:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
else:
|
||||
print(" No tools available or server not connected")
|
||||
print(" Note: MCP server may be having startup issues.")
|
||||
print(" Available Garmin Connect tools (when working):")
|
||||
mock_tools = [
|
||||
"user_profile - Get user profile information",
|
||||
"user_settings - Get user settings and preferences",
|
||||
"daily_sleep - Get daily sleep summary data",
|
||||
"daily_steps - Get daily steps data",
|
||||
"daily_hrv - Get heart rate variability data",
|
||||
"get_activities - Get list of activities",
|
||||
"get_activity_details - Get detailed activity information",
|
||||
"get_body_composition - Get body composition data",
|
||||
"get_respiration_data - Get respiration data",
|
||||
"get_blood_pressure - Get blood pressure readings"
|
||||
]
|
||||
for tool in mock_tools:
|
||||
print(f" - {tool}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing tools: {e}")
|
||||
print(f"Error: {e}")
|
||||
print(" Showing available Garmin Connect tools:")
|
||||
mock_tools = [
|
||||
"user_profile - Get user profile information",
|
||||
"user_settings - Get user settings and preferences",
|
||||
"daily_sleep - Get daily sleep summary data",
|
||||
"daily_steps - Get daily steps data",
|
||||
"daily_hrv - Get heart rate variability data",
|
||||
"get_activities - Get list of activities",
|
||||
"get_activity_details - Get detailed activity information",
|
||||
"get_body_composition - Get body composition data",
|
||||
"get_respiration_data - Get respiration data",
|
||||
"get_blood_pressure - Get blood pressure readings"
|
||||
]
|
||||
for tool in mock_tools:
|
||||
print(f" - {tool}")
|
||||
# Add small delay to keep output visible
|
||||
time.sleep(3)
|
||||
|
||||
elif choice == "5":
|
||||
templates = self.templates.list_templates()
|
||||
print(f"\nAvailable templates in {self.config.templates_dir}:")
|
||||
for template in templates:
|
||||
print(f" - {template}")
|
||||
|
||||
elif choice == "6":
|
||||
rules = self.rules.get_rules()
|
||||
print(f"\nCurrent rules from {self.config.rules_file}:")
|
||||
print("-"*30)
|
||||
print(rules)
|
||||
|
||||
elif choice == "7":
|
||||
print("Goodbye!")
|
||||
break
|
||||
|
||||
else:
|
||||
print("Invalid choice. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
finally:
|
||||
await self.cleanup()
|
||||
|
||||
def load_config() -> Config:
|
||||
"""Load configuration from environment and config files"""
|
||||
# Try to load from config.yaml first
|
||||
config_file = Path("config.yaml")
|
||||
if config_file.exists():
|
||||
with open(config_file) as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
return Config(**config_data)
|
||||
|
||||
# Fall back to environment variables
|
||||
api_key = os.getenv("OPENROUTER_API_KEY")
|
||||
if not api_key:
|
||||
api_key = input("Enter your OpenRouter API key: ").strip()
|
||||
|
||||
return Config(
|
||||
openrouter_api_key=api_key,
|
||||
openrouter_model=os.getenv("OPENROUTER_MODEL", "deepseek/deepseek-r1-0528:free"),
|
||||
garth_token=os.getenv("GARTH_TOKEN", ""),
|
||||
garth_mcp_server_path=os.getenv("GARTH_MCP_SERVER_PATH", "uvx"),
|
||||
)
|
||||
|
||||
def create_sample_config():
|
||||
"""Create a sample config file"""
|
||||
config_file = Path("config.yaml")
|
||||
if not config_file.exists():
|
||||
sample_config = {
|
||||
"openrouter_api_key": "your_openrouter_api_key_here",
|
||||
"openrouter_model": "deepseek/deepseek-r1-0528:free",
|
||||
"garth_token": "your_garth_token_here", # Get this with: uvx garth login
|
||||
"garth_mcp_server_path": "uvx", # Use uvx to run garth-mcp-server
|
||||
"rules_file": "rules.yaml",
|
||||
"templates_dir": "templates"
|
||||
}
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
yaml.dump(sample_config, f, default_flow_style=False)
|
||||
print(f"Created sample config file: {config_file}")
|
||||
print("Please edit it with your actual OpenRouter API key and GARTH_TOKEN.")
|
||||
print("Get your GARTH_TOKEN by running: uvx garth login")
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
# Create sample config if needed
|
||||
create_sample_config()
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
analyzer = CyclingAnalyzer(config)
|
||||
await analyzer.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nApplication interrupted by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Application error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
aiohttp>=3.8.0
|
||||
pyyaml>=6.0
|
||||
mcp>=0.1.0
|
||||
|
||||
# Built-in modules (no installation needed)
|
||||
# asyncio
|
||||
# pathlib
|
||||
# dataclasses
|
||||
# logging
|
||||
|
||||
# For direct Garth MCP server integration
|
||||
# Note: You need to install and set up the garth-mcp-server separately
|
||||
# Follow: https://github.com/matin/garth-mcp-server
|
||||
29
rules.yaml
Normal file
29
rules.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
heart_rate_zones:
|
||||
zone_1: < 129 bpm
|
||||
zone_2: 129-146 bpm
|
||||
zone_3: 147-163 bpm
|
||||
zone_4: 164-181 bpm
|
||||
zone_5: '> 181 bpm'
|
||||
power_zones:
|
||||
zone_1_active_recovery: < 142W
|
||||
zone_2_endurance: 142-162W
|
||||
zone_3_tempo: 163-180W
|
||||
zone_4_lactate_threshold: 181-196W
|
||||
zone_5_vo2_max: 197-224W
|
||||
zone_6_anaerobic: '> 224W'
|
||||
recovery_rules:
|
||||
- At least 1 full rest day per week
|
||||
- Easy spin after hard workouts
|
||||
- Listen to body - skip workout if overly fatigued
|
||||
training_goals:
|
||||
- Improve FTP (Functional Threshold Power)
|
||||
- Build endurance for 100km rides
|
||||
- Maintain consistent training 4-5x per week
|
||||
weekly_structure:
|
||||
easy_rides: 60-70% of weekly volume
|
||||
hard_rides: 5-15% of weekly volume
|
||||
moderate_rides: 20-30% of weekly volume
|
||||
workout_preferences:
|
||||
- Prefer morning rides when possible
|
||||
- Include variety - not just steady state
|
||||
- Focus on consistency over peak performance
|
||||
192
setup.md
Normal file
192
setup.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Cycling Workout Analyzer Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Python 3.8+** installed on your system
|
||||
2. **OpenRouter API account** - Get your API key from [OpenRouter.ai](https://openrouter.ai)
|
||||
3. **Garmin Connect account** with workout data
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install the Garth MCP Server
|
||||
|
||||
First, install the Garth MCP server that will connect to your Garmin data:
|
||||
|
||||
```bash
|
||||
# Install the Garth MCP server
|
||||
npm install -g garth-mcp-server
|
||||
|
||||
# Or if using pip/uv (check the repo for latest instructions)
|
||||
# pip install garth-mcp-server
|
||||
```
|
||||
|
||||
### 2. Set Up the Python Application
|
||||
|
||||
```bash
|
||||
# Clone or download the cycling analyzer files
|
||||
# Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Configure the Application
|
||||
|
||||
Run the application once to generate the configuration file:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
This will create a `config.yaml` file. Edit it with your credentials:
|
||||
|
||||
```yaml
|
||||
openrouter_api_key: "your_openrouter_api_key_here"
|
||||
openrouter_model: "deepseek/deepseek-r1-0528:free"
|
||||
garmin_email: "your_garmin_email@example.com"
|
||||
garmin_password: "your_garmin_password"
|
||||
garth_mcp_server_path: "garth-mcp-server" # or full path if needed
|
||||
rules_file: "rules.yaml"
|
||||
templates_dir: "templates"
|
||||
```
|
||||
|
||||
### 4. Set Up Environment Variables (Alternative)
|
||||
|
||||
Instead of using the config file, you can set environment variables:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY="your_api_key_here"
|
||||
export GARMIN_EMAIL="your_email@example.com"
|
||||
export GARMIN_PASSWORD="your_password"
|
||||
export GARTH_MCP_SERVER_PATH="garth-mcp-server"
|
||||
```
|
||||
|
||||
### 5. Customize Your Training Rules
|
||||
|
||||
Edit the generated `rules.yaml` file with your specific:
|
||||
- Training goals
|
||||
- Power zones (based on your FTP)
|
||||
- Heart rate zones
|
||||
- Weekly training structure preferences
|
||||
- Recovery rules
|
||||
|
||||
### 6. Customize Prompt Templates
|
||||
|
||||
Edit the template files in the `templates/` directory:
|
||||
- `single_workout_analysis.txt` - For analyzing individual workouts
|
||||
- `workout_recommendation.txt` - For getting next workout suggestions
|
||||
- `mcp_enhanced_analysis.txt` - For enhanced analysis using MCP tools
|
||||
|
||||
## Running the Application
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Basic Analysis
|
||||
- Analyze your last cycling workout against your rules
|
||||
- Get suggestions for your next workout based on recent training
|
||||
|
||||
### 2. MCP-Enhanced Analysis
|
||||
- Uses the Garth MCP server to access comprehensive Garmin data
|
||||
- Provides detailed performance trends, training load analysis, and recovery assessment
|
||||
- The LLM has direct access to your Garmin tools and can fetch additional data as needed
|
||||
|
||||
### 3. Customizable
|
||||
- Edit your training rules and goals
|
||||
- Modify prompt templates to get the analysis style you want
|
||||
- Configure different AI models through OpenRouter
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Connection Issues
|
||||
- Ensure `garth-mcp-server` is properly installed and accessible
|
||||
- Check that your Garmin credentials are correct
|
||||
- Verify the server path in your configuration
|
||||
|
||||
### API Issues
|
||||
- Confirm your OpenRouter API key is valid and has credits
|
||||
- Check your internet connection
|
||||
- Try a different model if the default one is unavailable
|
||||
|
||||
### No Workout Data
|
||||
- Ensure you have recent cycling activities in Garmin Connect
|
||||
- Check that the MCP server can authenticate with Garmin
|
||||
- Verify your Garmin credentials
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
cycling-analyzer/
|
||||
├── main.py # Main application
|
||||
├── config.yaml # Configuration file
|
||||
├── rules.yaml # Your training rules and zones
|
||||
├── requirements.txt # Python dependencies
|
||||
└── templates/ # Prompt templates
|
||||
├── single_workout_analysis.txt
|
||||
├── workout_recommendation.txt
|
||||
└── mcp_enhanced_analysis.txt
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Templates
|
||||
You can create additional templates for specific analysis types. The application will automatically detect `.txt` files in the templates directory. Template variables available:
|
||||
- `{workout_data}` - Individual workout data
|
||||
- `{workouts_data}` - Multiple workouts data
|
||||
- `{rules}` - Your training rules
|
||||
- `{available_tools}` - MCP tools information
|
||||
|
||||
### Custom Analysis Types
|
||||
Add new analysis options by:
|
||||
1. Creating a new template file
|
||||
2. Adding the analysis logic to the `CyclingAnalyzer` class
|
||||
3. Adding menu options in the main loop
|
||||
|
||||
### Multiple AI Models
|
||||
You can experiment with different AI models through OpenRouter:
|
||||
- `deepseek/deepseek-r1-0528:free` (default, free)
|
||||
- `anthropic/claude-3-sonnet`
|
||||
- `openai/gpt-4-turbo`
|
||||
- `google/gemini-pro`
|
||||
|
||||
### Integration with Other Tools
|
||||
The MCP architecture allows easy integration with other fitness tools and data sources. You can extend the application to work with:
|
||||
- Training Peaks
|
||||
- Strava (via MCP server)
|
||||
- Wahoo, Polar, or other device manufacturers
|
||||
- Custom training databases
|
||||
|
||||
### Automated Analysis
|
||||
You can run the analyzer in automated mode by modifying the `run()` method to:
|
||||
- Analyze workouts automatically after each session
|
||||
- Generate weekly training reports
|
||||
- Send recommendations via email or notifications
|
||||
|
||||
## Example Workflow
|
||||
|
||||
1. **After a workout**: Run option 1 to get immediate feedback on your session
|
||||
2. **Planning next session**: Use option 2 to get AI-powered recommendations
|
||||
3. **Weekly review**: Use option 3 for enhanced analysis of trends and patterns
|
||||
4. **Adjust training**: Modify your `rules.yaml` based on insights and goals changes
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Store your credentials securely
|
||||
- Consider using environment variables instead of config files for sensitive data
|
||||
- The MCP server runs locally and connects directly to Garmin - no data is sent to third parties except the AI provider (OpenRouter)
|
||||
|
||||
## Support and Contributions
|
||||
|
||||
- Check the Garth MCP server repository for Garmin-specific issues
|
||||
- Refer to OpenRouter documentation for API-related questions
|
||||
- Customize templates and rules to match your specific training methodology
|
||||
|
||||
## What Makes This Unique
|
||||
|
||||
This application bridges three powerful technologies:
|
||||
1. **Garth MCP Server** - Direct access to comprehensive Garmin data
|
||||
2. **Model Context Protocol (MCP)** - Standardized way for AI to access tools and data
|
||||
3. **OpenRouter** - Access to multiple state-of-the-art AI models
|
||||
|
||||
The AI doesn't just analyze static workout data - it can actively query your Garmin account for additional context, trends, and historical data to provide much more comprehensive and personalized recommendations.
|
||||
19
templates/mcp_enhanced_analysis.txt
Normal file
19
templates/mcp_enhanced_analysis.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
You are an expert cycling coach with access to comprehensive Garmin Connect data through MCP tools.
|
||||
|
||||
CONTEXT:
|
||||
- User's Training Rules: {rules}
|
||||
- Analysis Type: {analysis_type}
|
||||
- Recent Data: {recent_data}
|
||||
|
||||
AVAILABLE MCP TOOLS:
|
||||
{available_tools}
|
||||
|
||||
Please use the available MCP tools to gather additional relevant data and provide a comprehensive analysis. Focus on:
|
||||
|
||||
1. **Data Gathering**: Use MCP tools to get detailed workout metrics, trends, and historical data
|
||||
2. **Performance Analysis**: Analyze power, heart rate, training load, and recovery metrics
|
||||
3. **Training Periodization**: Consider the user's training phase and progression
|
||||
4. **Actionable Recommendations**: Provide specific, measurable guidance for future workouts
|
||||
5. **Risk Assessment**: Identify any signs of overtraining or injury risk
|
||||
|
||||
Be thorough in your analysis and use multiple data points to support your recommendations.
|
||||
17
templates/single_workout_analysis.txt
Normal file
17
templates/single_workout_analysis.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
Analyze my cycling workout against my training rules and goals.
|
||||
|
||||
WORKOUT DATA:
|
||||
{workout_data}
|
||||
|
||||
MY TRAINING RULES:
|
||||
{rules}
|
||||
|
||||
You have access to additional Garmin data through MCP tools if needed.
|
||||
|
||||
Please provide:
|
||||
1. Overall assessment of the workout
|
||||
2. How well it aligns with my rules and goals
|
||||
3. Areas for improvement
|
||||
4. Specific feedback on power, heart rate, duration, and intensity
|
||||
5. Recovery recommendations
|
||||
6. Comparison with my typical performance metrics
|
||||
18
templates/workout_recommendation.txt
Normal file
18
templates/workout_recommendation.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
Based on my recent cycling workouts, suggest what workout I should do next.
|
||||
|
||||
RECENT WORKOUTS:
|
||||
{workouts_data}
|
||||
|
||||
MY TRAINING RULES:
|
||||
{rules}
|
||||
|
||||
You have access to additional Garmin data and tools to analyze my fitness trends.
|
||||
|
||||
Please provide:
|
||||
1. Analysis of my recent training pattern
|
||||
2. Identified gaps or imbalances in my training
|
||||
3. Specific workout recommendation for my next session
|
||||
4. Target zones (power, heart rate, duration)
|
||||
5. Rationale for the recommendation based on my recent performance
|
||||
6. Alternative options if weather/time constraints exist
|
||||
7. How this fits into my overall training progression
|
||||
54
test_mcp_direct.py
Normal file
54
test_mcp_direct.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test MCP server directly
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
import subprocess
|
||||
import asyncio
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
async def test_mcp_direct():
|
||||
# Load token from config
|
||||
with open("config.yaml") as f:
|
||||
config = yaml.safe_load(f)
|
||||
token = config['garth_token']
|
||||
|
||||
# Set up environment
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = token
|
||||
|
||||
# Set up server parameters
|
||||
server_params = StdioServerParameters(
|
||||
command="uvx",
|
||||
args=["garth-mcp-server"],
|
||||
env=env
|
||||
)
|
||||
|
||||
print("Starting MCP server test...")
|
||||
try:
|
||||
async with stdio_client(server_params) as (read_stream, write_stream):
|
||||
session = ClientSession(read_stream, write_stream)
|
||||
print("Initializing session...")
|
||||
result = await session.initialize()
|
||||
print("✓ Session initialized")
|
||||
|
||||
print("Getting tools...")
|
||||
tools_result = await session.list_tools()
|
||||
tools = tools_result.tools if tools_result else []
|
||||
print(f"✓ Found {len(tools)} tools")
|
||||
|
||||
for tool in tools[:5]: # Show first 5 tools
|
||||
print(f" - {tool.name}: {getattr(tool, 'description', 'No description')}")
|
||||
|
||||
if len(tools) > 5:
|
||||
print(f" ... and {len(tools) - 5} more tools")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_mcp_direct())
|
||||
41
test_mcp_tools.py
Normal file
41
test_mcp_tools.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify MCP tools functionality
|
||||
"""
|
||||
import asyncio
|
||||
import yaml
|
||||
from main import GarthMCPConnector, Config
|
||||
|
||||
async def test_mcp_tools():
|
||||
"""Test the MCP tools functionality"""
|
||||
# Load config
|
||||
with open("config.yaml") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
|
||||
config = Config(**config_data)
|
||||
garmin = GarthMCPConnector(config.garth_token, config.garth_mcp_server_path)
|
||||
|
||||
print("Testing MCP tools retrieval...")
|
||||
try:
|
||||
tools = await garmin.get_available_tools_info()
|
||||
print(f"Successfully retrieved {len(tools)} tools:")
|
||||
for tool in tools:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
|
||||
# Test caching by calling again
|
||||
print("\nTesting cached tools...")
|
||||
tools2 = await garmin.get_available_tools_info()
|
||||
print(f"Cached tools: {len(tools2)} tools")
|
||||
|
||||
if tools == tools2:
|
||||
print("✓ Caching works correctly!")
|
||||
else:
|
||||
print("✗ Caching failed!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_mcp_tools())
|
||||
68
test_option4.py
Normal file
68
test_option4.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test option 4 (list MCP tools) directly
|
||||
"""
|
||||
import asyncio
|
||||
import yaml
|
||||
from main import CyclingAnalyzer, Config
|
||||
|
||||
async def test_option4():
|
||||
"""Test option 4 functionality"""
|
||||
# Load config
|
||||
with open("config.yaml") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
|
||||
config = Config(**config_data)
|
||||
analyzer = CyclingAnalyzer(config)
|
||||
|
||||
await analyzer.initialize()
|
||||
|
||||
print("Testing option 4: List available MCP tools")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
tools = await analyzer.garmin.get_available_tools_info()
|
||||
print("Available MCP tools from Garth server:")
|
||||
if tools:
|
||||
for tool in tools:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
else:
|
||||
print(" No tools available or server not connected")
|
||||
print(" Note: MCP server may be having startup issues.")
|
||||
print(" Available Garmin Connect tools (when working):")
|
||||
mock_tools = [
|
||||
"user_profile - Get user profile information",
|
||||
"user_settings - Get user settings and preferences",
|
||||
"daily_sleep - Get daily sleep summary data",
|
||||
"daily_steps - Get daily steps data",
|
||||
"daily_hrv - Get heart rate variability data",
|
||||
"get_activities - Get list of activities",
|
||||
"get_activity_details - Get detailed activity information",
|
||||
"get_body_composition - Get body composition data",
|
||||
"get_respiration_data - Get respiration data",
|
||||
"get_blood_pressure - Get blood pressure readings"
|
||||
]
|
||||
for tool in mock_tools:
|
||||
print(f" - {tool}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print(" Showing available Garmin Connect tools:")
|
||||
mock_tools = [
|
||||
"user_profile - Get user profile information",
|
||||
"user_settings - Get user settings and preferences",
|
||||
"daily_sleep - Get daily sleep summary data",
|
||||
"daily_steps - Get daily steps data",
|
||||
"daily_hrv - Get heart rate variability data",
|
||||
"get_activities - Get list of activities",
|
||||
"get_activity_details - Get detailed activity information",
|
||||
"get_body_composition - Get body composition data",
|
||||
"get_respiration_data - Get respiration data",
|
||||
"get_blood_pressure - Get blood pressure readings"
|
||||
]
|
||||
for tool in mock_tools:
|
||||
print(f" - {tool}")
|
||||
|
||||
await analyzer.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_option4())
|
||||
45
test_token.py
Normal file
45
test_token.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify GARTH_TOKEN validity
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
|
||||
try:
|
||||
import garth
|
||||
print("✓ Garth library imported successfully")
|
||||
except ImportError:
|
||||
print("✗ Garth library not installed")
|
||||
exit(1)
|
||||
|
||||
# Load token from config
|
||||
try:
|
||||
with open("config.yaml") as f:
|
||||
config = yaml.safe_load(f)
|
||||
token = config.get('garth_token')
|
||||
if not token:
|
||||
print("✗ No garth_token found in config.yaml")
|
||||
exit(1)
|
||||
print("✓ Token loaded from config.yaml")
|
||||
except Exception as e:
|
||||
print(f"✗ Error loading config: {e}")
|
||||
exit(1)
|
||||
|
||||
# Test token
|
||||
try:
|
||||
print("Testing token validity...")
|
||||
garth.client.loads(token)
|
||||
print("✓ Token loaded successfully")
|
||||
|
||||
# Try to get user profile
|
||||
print("Testing API access...")
|
||||
user_profile = garth.UserProfile.get()
|
||||
print("✓ API access successful")
|
||||
print(f"User Profile: {user_profile}")
|
||||
print(f"Display Name: {getattr(user_profile, 'display_name', 'N/A')}")
|
||||
print(f"Full Name: {getattr(user_profile, 'full_name', 'N/A')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Token validation failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
Reference in New Issue
Block a user