restrcuted repo

This commit is contained in:
2025-09-26 06:52:27 -07:00
parent 997028f0e1
commit 15974bbac5
26 changed files with 17 additions and 12 deletions

0
mcp/__init__.py Normal file
View File

572
mcp/custom_garth_mcp.py Normal file
View File

@@ -0,0 +1,572 @@
#!/usr/bin/env python3
"""
Custom Garth MCP Implementation
Direct wrapper around the Garth module with MCP-like interface
"""
import asyncio
import json
import logging
import os
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Union
from pathlib import Path
try:
import garth
GARTH_AVAILABLE = True
except ImportError:
GARTH_AVAILABLE = False
garth = None
from ..core.cache_manager import CacheManager
logger = logging.getLogger(__name__)
class GarthTool:
"""Represents a single Garth-based tool"""
def __init__(self, name: str, description: str, parameters: Dict[str, Any] = None):
self.name = name
self.description = description
self.parameters = parameters or {}
def __repr__(self):
return f"GarthTool(name='{self.name}')"
class CustomGarthMCP:
"""
Custom MCP-like interface for Garth
Provides tools for accessing Garmin Connect data with local caching
"""
def __init__(self, garth_token: str = None, cache_ttl: int = 300):
self.garth_token = garth_token or os.getenv("GARTH_TOKEN")
self.cache = CacheManager(default_ttl=cache_ttl)
self.session = None
self._tools = []
self._setup_tools()
if not GARTH_AVAILABLE:
logger.error("Garth module not available")
raise ImportError("Garth module not available. Install with: pip install garth")
def _setup_tools(self):
"""Setup available tools based on working endpoints"""
self._tools = [
GarthTool(
name="user_profile",
description="Get user profile information (social profile)",
parameters={}
),
GarthTool(
name="user_settings",
description="Get user statistics/settings information",
parameters={}
),
GarthTool(
name="get_activities",
description="Get list of activities from Garmin Connect",
parameters={
"start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
"limit": {"type": "integer", "description": "Maximum number of activities (default: 20)"}
}
),
GarthTool(
name="get_activities_by_date",
description="Get activities for a specific date",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)", "required": True}
}
),
GarthTool(
name="get_activity_details",
description="Get detailed information for a specific activity",
parameters={
"activity_id": {"type": "string", "description": "Activity ID", "required": True}
}
),
GarthTool(
name="daily_steps",
description="Get daily summary data (may include steps if available)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"},
"days": {"type": "integer", "description": "Number of days"}
}
),
GarthTool(
name="weekly_steps",
description="Get multi-day summary data (weekly equivalent)",
parameters={
"date": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
"weeks": {"type": "integer", "description": "Number of weeks"}
}
),
GarthTool(
name="get_body_composition",
description="Get body composition/weight data (may have parameter requirements)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"}
}
),
GarthTool(
name="snapshot",
description="Get comprehensive data snapshot using working endpoints only",
parameters={
"start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
"end_date": {"type": "string", "description": "End date (YYYY-MM-DD)"}
}
),
# Note: These tools are available but will return helpful error messages
# since the endpoints don't work
GarthTool(
name="daily_sleep",
description="Get daily sleep data (currently unavailable - endpoint not working)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"},
"days": {"type": "integer", "description": "Number of days"}
}
),
GarthTool(
name="daily_stress",
description="Get daily stress data (currently unavailable - endpoint not working)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"},
"days": {"type": "integer", "description": "Number of days"}
}
),
GarthTool(
name="daily_body_battery",
description="Get daily body battery data (currently unavailable - endpoint not working)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"},
"days": {"type": "integer", "description": "Number of days"}
}
),
GarthTool(
name="daily_hrv",
description="Get daily heart rate variability data (currently unavailable - endpoint not working)",
parameters={
"date": {"type": "string", "description": "Date (YYYY-MM-DD)"},
"days": {"type": "integer", "description": "Number of days"}
}
),
GarthTool(
name="get_devices",
description="Get connected devices info (limited - returns user profile as fallback)",
parameters={}
)
]
async def initialize(self):
"""Initialize Garth session"""
if not self.garth_token:
logger.error("No GARTH_TOKEN provided")
raise ValueError("GARTH_TOKEN is required")
try:
# Configure Garth
garth.configure()
# Try to use saved session first
session_path = Path.home() / ".garth"
if session_path.exists():
try:
garth.resume(str(session_path))
# Test the session
await self._test_session()
logger.info("Resumed existing Garth session")
except Exception as e:
logger.warning(f"Could not resume session: {e}")
await self._create_new_session()
else:
await self._create_new_session()
logger.info("Garth session initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Garth: {e}")
raise
async def _create_new_session(self):
"""Create new Garth session using token"""
try:
# Use the token to login
# Note: You might need to adjust this based on your token format
garth.login_oauth(token=self.garth_token)
# Save session
session_path = Path.home() / ".garth"
garth.save(str(session_path))
except Exception as e:
logger.error(f"Failed to create Garth session: {e}")
raise
async def _test_session(self):
"""Test if current session is valid"""
# Try multiple endpoints to find one that works
test_endpoints = [
"/userprofile-service/socialProfile",
"/user-service/users/settings",
"/modern/currentuser-service/user/profile",
"/userstats-service/statistics"
]
for endpoint in test_endpoints:
try:
garth.connectapi(endpoint)
logger.debug(f"Session test successful with {endpoint}")
return True
except Exception as e:
logger.debug(f"Session test failed for {endpoint}: {e}")
continue
logger.debug("All session tests failed")
raise Exception("No working endpoints found")
async def cleanup(self):
"""Cleanup resources"""
# Garth doesn't require explicit cleanup
pass
def list_tools(self) -> List[GarthTool]:
"""List available tools"""
return self._tools
async def has_tool(self, tool_name: str) -> bool:
"""Check if tool is available"""
return any(tool.name == tool_name for tool in self._tools)
async def call_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
"""Call a specific tool"""
# Check cache first
cache_key = f"{tool_name}:{hash(str(sorted(parameters.items())))}"
cached_result = self.cache.get(cache_key)
if cached_result is not None:
logger.debug(f"Cache hit for {tool_name}")
return cached_result
try:
result = await self._execute_tool(tool_name, parameters)
# Cache result with appropriate TTL
ttl = self._get_cache_ttl(tool_name)
self.cache.set(cache_key, result, ttl=ttl)
logger.debug(f"Tool {tool_name} executed successfully")
return result
except Exception as e:
logger.error(f"Error calling tool {tool_name}: {e}")
raise
def _get_cache_ttl(self, tool_name: str) -> int:
"""Get appropriate cache TTL for different tools"""
ttl_map = {
"user_profile": 3600, # 1 hour - rarely changes
"user_settings": 3600, # 1 hour - rarely changes
"get_devices": 1800, # 30 minutes - occasionally changes
"get_activities": 300, # 5 minutes - changes frequently
"daily_steps": 3600, # 1 hour - daily data
"daily_sleep": 3600, # 1 hour - daily data
"daily_stress": 3600, # 1 hour - daily data
"daily_body_battery": 3600, # 1 hour - daily data
"daily_hrv": 3600, # 1 hour - daily data
"weekly_steps": 1800, # 30 minutes - weekly data
"get_body_composition": 1800, # 30 minutes
"snapshot": 600, # 10 minutes - comprehensive data
}
return ttl_map.get(tool_name, 300) # Default 5 minutes
async def _execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
"""Execute the actual tool call"""
# User profile and settings - use working endpoints
if tool_name == "user_profile":
# Use the working social profile endpoint
return garth.connectapi("/userprofile-service/socialProfile")
elif tool_name == "user_settings":
# Try user stats as fallback since user settings doesn't work
try:
return garth.connectapi("/userstats-service/statistics")
except Exception:
# If stats don't work, return the social profile as user info
return garth.connectapi("/userprofile-service/socialProfile")
# Activities - use working endpoint
elif tool_name == "get_activities":
start_date = parameters.get("start_date")
limit = parameters.get("limit", 20)
params = {"limit": limit}
if start_date:
params["startDate"] = start_date
return garth.connectapi("/activitylist-service/activities/search/activities", params=params)
elif tool_name == "get_activities_by_date":
date = parameters["date"]
start = f"{date}T00:00:00.000Z"
end = f"{date}T23:59:59.999Z"
return garth.connectapi("/activitylist-service/activities/search/activities", params={
"startDate": start,
"endDate": end,
"limit": 100
})
elif tool_name == "get_activity_details":
activity_id = parameters["activity_id"]
raw_data = garth.connectapi(f"/activity-service/activity/{activity_id}")
# Normalize activity data for consistent field access
normalized_data = self._normalize_activity_data(raw_data)
return normalized_data
# Daily metrics - many don't work, so provide fallbacks
elif tool_name == "daily_steps":
date = parameters.get("date", datetime.now().strftime("%Y-%m-%d"))
days = parameters.get("days", 1)
# Try usersummary first, if that fails, try to get from activities
try:
return garth.connectapi("/usersummary-service/usersummary/daily", params={
"startDate": date,
"numOfDays": days
})
except Exception as e:
logger.warning(f"Daily steps via usersummary failed: {e}")
# Fallback: get activities for the date and sum steps if available
try:
activities = garth.connectapi("/activitylist-service/activities/search/activities", params={
"startDate": f"{date}T00:00:00.000Z",
"endDate": f"{date}T23:59:59.999Z",
"limit": 50
})
return {"fallback_activities": activities, "date": date, "message": "Daily steps not available, showing activities instead"}
except Exception as e2:
raise Exception(f"Both daily steps and activities failed: {e}, {e2}")
elif tool_name == "daily_sleep":
# Sleep endpoint doesn't work, return error with helpful message
raise Exception("Daily sleep endpoint not available. Sleep data may be accessible through other means.")
elif tool_name == "daily_stress":
# Stress endpoint doesn't work
raise Exception("Daily stress endpoint not available.")
elif tool_name == "daily_body_battery":
# Body battery endpoint doesn't work
raise Exception("Daily body battery endpoint not available.")
elif tool_name == "daily_hrv":
# HRV endpoint doesn't work
raise Exception("HRV endpoint not available.")
# Weekly metrics
elif tool_name == "weekly_steps":
# Weekly endpoint doesn't work, try to get multiple days instead
date = parameters.get("date", datetime.now().strftime("%Y-%m-%d"))
weeks = parameters.get("weeks", 1)
days = weeks * 7
try:
return garth.connectapi("/usersummary-service/usersummary/daily", params={
"startDate": date,
"numOfDays": days
})
except Exception as e:
raise Exception(f"Weekly steps not available: {e}")
# Device info
elif tool_name == "get_devices":
# Device registration doesn't work, return user profile as fallback
profile = garth.connectapi("/userprofile-service/socialProfile")
return {"message": "Device registration endpoint not available", "user_profile": profile}
# Body composition
elif tool_name == "get_body_composition":
# Weight service gives 400 error, likely needs parameters
date = parameters.get("date", datetime.now().strftime("%Y-%m-%d"))
try:
return garth.connectapi("/weight-service/weight/dateRange", params={
"startDate": date,
"endDate": date
})
except Exception as e:
raise Exception(f"Body composition endpoint error: {e}")
# Comprehensive snapshot - use working endpoints only
elif tool_name == "snapshot":
start_date = parameters.get("start_date", datetime.now().strftime("%Y-%m-%d"))
end_date = parameters.get("end_date", start_date)
snapshot = {}
try:
# User profile - we know this works
snapshot["user_profile"] = garth.connectapi("/userprofile-service/socialProfile")
except Exception as e:
logger.warning(f"Could not get user profile: {e}")
try:
# Activities for date range - we know this works
snapshot["activities"] = garth.connectapi("/activitylist-service/activities/search/activities", params={
"startDate": f"{start_date}T00:00:00.000Z",
"endDate": f"{end_date}T23:59:59.999Z",
"limit": 100
})
except Exception as e:
logger.warning(f"Could not get activities: {e}")
try:
# User stats - we know this works
snapshot["user_stats"] = garth.connectapi("/userstats-service/statistics")
except Exception as e:
logger.warning(f"Could not get user stats: {e}")
# Try some daily data (even though many endpoints don't work)
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
days = (end_dt - start_dt).days + 1
snapshot["daily_summary"] = garth.connectapi("/usersummary-service/usersummary/daily", params={
"startDate": start_date,
"numOfDays": days
})
except Exception as e:
logger.warning(f"Could not get daily summary: {e}")
if not snapshot:
raise Exception("Could not retrieve any data for snapshot")
return snapshot
else:
raise ValueError(f"Unknown tool: {tool_name}")
def _normalize_activity_data(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize activity data to ensure consistent field structure"""
if not isinstance(raw_data, dict):
logger.warning("Invalid activity data received")
return {}
# Ensure summaryDTO exists
summary_dto = raw_data.get('summaryDTO', {})
if not isinstance(summary_dto, dict):
summary_dto = {}
# Define expected fields with defaults
expected_fields = {
'averageSpeed': None,
'maxSpeed': None,
'averageHR': None,
'maxHR': None,
'averagePower': None,
'maxPower': None,
'normalizedPower': None,
'trainingStressScore': None,
'elevationGain': None,
'elevationLoss': None,
'distance': None,
'duration': None,
}
# Fill missing fields
for field, default in expected_fields.items():
if field not in summary_dto:
summary_dto[field] = default
logger.debug(f"Set default for missing field: {field}")
# Update raw_data with normalized summaryDTO
raw_data['summaryDTO'] = summary_dto
# Add activity type indicator for indoor detection
activity_type = raw_data.get('activityType', {}).get('typeKey', '').lower()
raw_data['is_indoor'] = 'indoor' in activity_type or 'trainer' in activity_type
logger.debug(f"Normalized activity data for ID {raw_data.get('activityId', 'unknown')}")
return raw_data
def print_tools(self):
"""Pretty print available tools"""
print(f"\n{'='*60}")
print("AVAILABLE GARTH TOOLS")
print(f"{'='*60}")
for i, tool in enumerate(self._tools, 1):
print(f"\n{i}. {tool.name}")
print(f" Description: {tool.description}")
if tool.parameters:
print(" Parameters:")
for param, info in tool.parameters.items():
param_type = info.get("type", "string")
param_desc = info.get("description", "")
required = info.get("required", False)
req_str = " (required)" if required else " (optional)"
print(f" - {param} ({param_type}){req_str}: {param_desc}")
print(f"\n{'='*60}")
@property
def is_available(self) -> bool:
"""Check if MCP server is available"""
return GARTH_AVAILABLE and self.garth_token is not None
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
return self.cache.get_stats()
def clear_cache(self):
"""Clear all cached data"""
self.cache.clear()
# Utility functions
async def create_garth_mcp(garth_token: str = None, cache_ttl: int = 300) -> CustomGarthMCP:
"""Create and initialize a CustomGarthMCP instance"""
mcp = CustomGarthMCP(garth_token=garth_token, cache_ttl=cache_ttl)
await mcp.initialize()
return mcp
async def test_garth_mcp():
"""Test the custom Garth MCP implementation"""
print("🚀 Testing Custom Garth MCP Implementation")
print("=" * 50)
try:
# Initialize
mcp = await create_garth_mcp()
# List tools
tools = mcp.list_tools()
print(f"✅ Found {len(tools)} tools")
# Test user profile
profile = await mcp.call_tool("user_profile", {})
print("✅ Got user profile")
print(f" User: {profile.get('displayName', 'Unknown')}")
# Test activities
activities = await mcp.call_tool("get_activities", {"limit": 5})
print(f"✅ Got {len(activities)} activities")
# Test cache
cached_profile = await mcp.call_tool("user_profile", {}) # Should hit cache
print("✅ Cache working")
# Show cache stats
cache_stats = mcp.get_cache_stats()
print(f"📊 Cache has {cache_stats['total_entries']} entries")
print("\n🎉 All tests passed!")
except Exception as e:
print(f"❌ Test failed: {e}")
raise
if __name__ == "__main__":
asyncio.run(test_garth_mcp())

169
mcp/garth_diagnostics.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Garth API Diagnostics
Test various Garmin Connect API endpoints to see what's available
"""
import json
from pathlib import Path
try:
import garth
print("✅ Garth module available")
except ImportError:
print("❌ Garth module not found")
exit(1)
def test_endpoints():
"""Test various Garmin Connect API endpoints"""
print("🔍 Testing Garmin Connect API Endpoints")
print("=" * 50)
# Load session
session_path = Path.home() / ".garth"
if not session_path.exists():
print("❌ No Garth session found. Run setup_garth.py first.")
return
try:
garth.resume(str(session_path))
print("✅ Session loaded")
except Exception as e:
print(f"❌ Could not load session: {e}")
return
# List of endpoints to test
endpoints_to_test = [
# User/Profile endpoints
("/userprofile-service/socialProfile", "Social Profile"),
("/user-service/user", "User Service"),
("/user-service/users/settings", "User Settings"),
("/modern/currentuser-service/user/profile", "Modern User Profile"),
("/userprofile-service/userprofile", "User Profile Alt"),
# Activity endpoints
("/activitylist-service/activities/search/activities?limit=1", "Recent Activities"),
("/activity-service/activity", "Activity Service"),
# Wellness endpoints
("/wellness-service/wellness", "Wellness Service"),
("/wellness-service/wellness/dailySleep", "Daily Sleep"),
("/wellness-service/wellness/dailyStress", "Daily Stress"),
("/wellness-service/wellness/dailyBodyBattery", "Daily Body Battery"),
# Stats endpoints
("/userstats-service/statistics", "User Statistics"),
("/usersummary-service/usersummary/daily", "Daily Summary"),
("/usersummary-service/usersummary/weekly", "Weekly Summary"),
# Device endpoints
("/device-service/deviceRegistration", "Device Registration"),
("/device-service/deviceService/app-info", "Device App Info"),
# HRV and health
("/hrv-service/hrv", "HRV Service"),
("/weight-service/weight/dateRange", "Weight Service"),
# Other services
("/badge-service/badge", "Badge Service"),
("/golf-service/golf", "Golf Service"),
("/content-service/content", "Content Service"),
]
working_endpoints = []
failed_endpoints = []
for endpoint, name in endpoints_to_test:
try:
print(f"\n📡 Testing: {name}")
print(f" Endpoint: {endpoint}")
# Extract base endpoint and parameters
if "?" in endpoint:
base_endpoint, params_str = endpoint.split("?", 1)
# Parse simple parameters
params = {}
if params_str:
for param in params_str.split("&"):
if "=" in param:
key, value = param.split("=", 1)
params[key] = value
result = garth.connectapi(base_endpoint, params=params)
else:
result = garth.connectapi(endpoint)
if result is not None:
if isinstance(result, dict):
keys_info = f"Dict with {len(result)} keys: {list(result.keys())[:5]}"
elif isinstance(result, list):
keys_info = f"List with {len(result)} items"
else:
keys_info = f"{type(result).__name__}: {str(result)[:50]}"
print(f" ✅ SUCCESS: {keys_info}")
working_endpoints.append((endpoint, name, result))
else:
print(f" ⚠️ SUCCESS but empty response")
working_endpoints.append((endpoint, name, None))
except Exception as e:
error_str = str(e)
if "404" in error_str:
print(f" ❌ NOT FOUND (404)")
elif "403" in error_str:
print(f" 🔒 FORBIDDEN (403) - May need different auth")
elif "401" in error_str:
print(f" 🚫 UNAUTHORIZED (401) - Auth expired?")
elif "500" in error_str:
print(f" 💥 SERVER ERROR (500)")
else:
print(f" ❌ ERROR: {error_str[:100]}")
failed_endpoints.append((endpoint, name, str(e)))
# Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"\n✅ Working Endpoints ({len(working_endpoints)}):")
for endpoint, name, _ in working_endpoints:
print(f" {name}: {endpoint}")
print(f"\n❌ Failed Endpoints ({len(failed_endpoints)}):")
for endpoint, name, error in failed_endpoints:
short_error = error.split('\n')[0][:50]
print(f" {name}: {short_error}")
# Show sample data from best working endpoint
if working_endpoints:
print(f"\n📋 SAMPLE DATA from first working endpoint:")
endpoint, name, data = working_endpoints[0]
print(f"Endpoint: {name} ({endpoint})")
if data:
if isinstance(data, dict):
# Show first few key-value pairs
for i, (key, value) in enumerate(data.items()):
if i >= 10: # Limit to first 10 items
print(f" ... and {len(data) - 10} more fields")
break
value_str = str(value)[:100]
print(f" {key}: {value_str}")
elif isinstance(data, list) and len(data) > 0:
print(f" List with {len(data)} items")
if isinstance(data[0], dict):
print(f" First item keys: {list(data[0].keys())}")
else:
print(f" First item: {str(data[0])[:100]}")
else:
print(f" Data: {str(data)[:200]}")
print(f"\n💡 Recommendation:")
if working_endpoints:
best_endpoint = working_endpoints[0]
print(f"Use '{best_endpoint[0]}' for user profile data")
else:
print("No working endpoints found. Check authentication.")
if __name__ == "__main__":
test_endpoints()

284
mcp/mcp_client.py Normal file
View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
Updated MCP Client - Uses our custom Garth implementation
Fallback to standard MCP if needed, but prefer custom implementation
"""
import logging
from typing import List, Dict, Any, Optional
from ..config import Config
# Try to import both implementations
try:
from .custom_garth_mcp import CustomGarthMCP, GarthTool
CUSTOM_GARTH_AVAILABLE = True
except ImportError:
CUSTOM_GARTH_AVAILABLE = False
CustomGarthMCP = None
GarthTool = None
try:
from pydantic_ai.mcp import MCPServerStdio
import shutil
import os
STANDARD_MCP_AVAILABLE = True
except ImportError:
STANDARD_MCP_AVAILABLE = False
MCPServerStdio = None
logger = logging.getLogger(__name__)
class MCPClient:
"""
Enhanced MCP Client that prefers custom Garth implementation
Falls back to standard MCP if needed
"""
def __init__(self, config: Config):
self.config = config
self.garth_mcp = None
self.standard_mcp = None
self.available_tools = []
self._initialized = False
self._use_custom = True
# Decide which implementation to use
if CUSTOM_GARTH_AVAILABLE and config.garth_token:
logger.info("Using custom Garth MCP implementation")
self._use_custom = True
elif STANDARD_MCP_AVAILABLE and config.garth_token:
logger.info("Falling back to standard MCP implementation")
self._use_custom = False
else:
logger.warning("No MCP implementation available")
async def initialize(self):
"""Initialize the preferred MCP implementation"""
if not self.config.garth_token:
logger.warning("No GARTH_TOKEN provided. MCP tools will be unavailable.")
return
try:
if self._use_custom and CUSTOM_GARTH_AVAILABLE:
await self._initialize_custom_garth()
elif STANDARD_MCP_AVAILABLE:
await self._initialize_standard_mcp()
else:
logger.error("No MCP implementation available")
return
self._initialized = True
logger.info("MCP client initialized successfully")
except Exception as e:
logger.error(f"MCP initialization failed: {e}")
# Try fallback if custom failed
if self._use_custom and STANDARD_MCP_AVAILABLE:
logger.info("Trying fallback to standard MCP")
try:
self._use_custom = False
await self._initialize_standard_mcp()
self._initialized = True
logger.info("Fallback MCP initialization successful")
except Exception as fallback_error:
logger.error(f"Fallback MCP initialization also failed: {fallback_error}")
async def _initialize_custom_garth(self):
"""Initialize custom Garth MCP"""
self.garth_mcp = CustomGarthMCP(
garth_token=self.config.garth_token,
cache_ttl=self.config.cache_ttl
)
await self.garth_mcp.initialize()
logger.info("Custom Garth MCP initialized")
async def _initialize_standard_mcp(self):
"""Initialize standard MCP (fallback)"""
if not self.config.garth_token:
raise ValueError("GARTH_TOKEN required for standard MCP")
# Set up environment
os.environ["GARTH_TOKEN"] = self.config.garth_token
env = os.environ.copy()
env["GARTH_TOKEN"] = self.config.garth_token
# Find server executable
server_executable = shutil.which(self.config.garth_mcp_server_path)
if not server_executable:
raise FileNotFoundError(f"'{self.config.garth_mcp_server_path}' not found in PATH")
self.standard_mcp = MCPServerStdio(
command=server_executable,
args=["garth-mcp-server"],
env=env,
)
logger.info("Standard MCP initialized")
async def cleanup(self):
"""Cleanup MCP resources"""
if self.garth_mcp:
await self.garth_mcp.cleanup()
# Standard MCP cleanup is handled by the agent
async def list_tools(self) -> List[Any]:
"""List available MCP tools"""
if not self._initialized:
return []
try:
if self._use_custom and self.garth_mcp:
if not self.available_tools:
self.available_tools = self.garth_mcp.list_tools()
return self.available_tools
elif self.standard_mcp:
if not self.available_tools:
self.available_tools = await self.standard_mcp.list_tools()
return self.available_tools
except Exception as e:
logger.error(f"Error listing tools: {e}")
return []
return []
def list_tools_sync(self) -> List[Any]:
"""Synchronous version for compatibility"""
if self._use_custom and self.garth_mcp:
return self.garth_mcp.list_tools()
return []
async def has_tool(self, tool_name: str) -> bool:
"""Check if a specific tool is available"""
if not self._initialized:
return False
try:
if self._use_custom and self.garth_mcp:
return await self.garth_mcp.has_tool(tool_name)
elif self.standard_mcp:
tools = await self.list_tools()
return any(tool.name == tool_name for tool in tools)
except Exception as e:
logger.error(f"Error checking tool {tool_name}: {e}")
return False
return False
async def call_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
"""Call a specific MCP tool"""
if not self._initialized:
raise RuntimeError("MCP client not initialized")
try:
if self._use_custom and self.garth_mcp:
result = await self.garth_mcp.call_tool(tool_name, parameters)
logger.debug(f"Custom MCP tool {tool_name} called successfully")
return result
elif self.standard_mcp:
result = await self.standard_mcp.direct_call_tool(tool_name, parameters)
# Handle different result formats
if hasattr(result, 'output'):
return result.output
elif hasattr(result, 'content'):
return result.content
else:
return result
except Exception as e:
logger.error(f"Error calling tool {tool_name}: {e}")
raise
def get_tool_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
"""Get information about a specific tool"""
tools = self.list_tools_sync() if self._use_custom else []
for tool in tools:
if tool.name == tool_name:
if self._use_custom and isinstance(tool, GarthTool):
return {
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters
}
else:
# Standard MCP tool
return {
"name": tool.name,
"description": getattr(tool, 'description', ''),
"parameters": getattr(tool, 'inputSchema', {}).get('properties', {})
}
return None
def print_tools(self):
"""Pretty print available tools"""
if self._use_custom and self.garth_mcp:
self.garth_mcp.print_tools()
return
tools = self.list_tools_sync()
if not tools:
print("No MCP tools available")
return
print(f"\n{'='*60}")
print("AVAILABLE MCP TOOLS")
print(f"{'='*60}")
for i, tool in enumerate(tools, 1):
print(f"\n{i}. {tool.name}")
if hasattr(tool, 'description') and tool.description:
print(f" Description: {tool.description}")
if hasattr(tool, 'inputSchema') and tool.inputSchema:
properties = tool.inputSchema.get("properties", {})
if properties:
print(" Parameters:")
required = tool.inputSchema.get("required", [])
for param, info in properties.items():
param_type = info.get("type", "unknown")
param_desc = info.get("description", "")
req_str = " (required)" if param in required else " (optional)"
print(f" - {param} ({param_type}){req_str}: {param_desc}")
print(f"\n{'='*60}")
@property
def is_available(self) -> bool:
"""Check if MCP is available and initialized"""
return self._initialized and (
(self._use_custom and self.garth_mcp is not None) or
(not self._use_custom and self.standard_mcp is not None)
)
@property
def implementation_type(self) -> str:
"""Get the type of MCP implementation being used"""
if self._use_custom:
return "custom_garth"
else:
return "standard_mcp"
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics (only available with custom implementation)"""
if self._use_custom and self.garth_mcp:
return self.garth_mcp.get_cache_stats()
else:
return {"message": "Cache stats only available with custom implementation"}
def clear_cache(self):
"""Clear cache (only available with custom implementation)"""
if self._use_custom and self.garth_mcp:
self.garth_mcp.clear_cache()
logger.info("Cache cleared")
else:
logger.warning("Cache clearing only available with custom implementation")
# Compatibility methods for existing code
@property
def mcp_server(self):
"""Compatibility property for existing code using agent integration"""
if self._use_custom:
# For custom implementation, we can't provide direct agent integration
# Return None to indicate tools should be called directly
return None
else:
return self.standard_mcp