mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-01-25 16:42:24 +00:00
sync - getting json resp now
This commit is contained in:
357
mcp_connector_fix.py
Executable file
357
mcp_connector_fix.py
Executable file
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fixed GarthMCPConnector class to resolve hanging issues
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import shutil
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# MCP Protocol imports
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GarthMCPConnector:
|
||||
"""Fixed 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: Optional[ClientSession] = None
|
||||
self._client_context = None
|
||||
self._read_stream = None
|
||||
self._write_stream = None
|
||||
self._connection_timeout = 30 # Timeout for operations
|
||||
|
||||
async def _get_server_params(self):
|
||||
"""Get server parameters for MCP connection"""
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = self.garth_token
|
||||
|
||||
# Find the full path to the server executable
|
||||
server_command = shutil.which("garth-mcp-server")
|
||||
if not server_command:
|
||||
logger.error("Could not find 'garth-mcp-server' in your PATH.")
|
||||
logger.error("Please ensure it is installed and accessible, e.g., via 'npm install -g garth-mcp-server'.")
|
||||
raise FileNotFoundError("garth-mcp-server not found")
|
||||
|
||||
return StdioServerParameters(
|
||||
command="/bin/bash",
|
||||
args=["-c", f"exec {server_command} \"$@\" 1>&2"],
|
||||
capture_stderr=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
"""Start the MCP server and establish a persistent session."""
|
||||
if self._session and self.server_available:
|
||||
return True
|
||||
|
||||
if not MCP_AVAILABLE:
|
||||
logger.error("MCP library not available. Install with: pip install mcp")
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.info("Connecting to Garth MCP server...")
|
||||
server_params = await self._get_server_params()
|
||||
|
||||
logger.info("Starting MCP server process...")
|
||||
self._client_context = stdio_client(server_params)
|
||||
streams = await self._client_context.__aenter__()
|
||||
|
||||
# Handle stderr logging in background
|
||||
if len(streams) == 3:
|
||||
self._read_stream, self._write_stream, stderr_stream = streams
|
||||
asyncio.create_task(self._log_stderr(stderr_stream))
|
||||
else:
|
||||
self._read_stream, self._write_stream = streams
|
||||
|
||||
logger.info("Server process started. Waiting for it to initialize...")
|
||||
await asyncio.sleep(1.0) # Give server more time to start
|
||||
|
||||
logger.info("Initializing MCP session...")
|
||||
self._session = ClientSession(self._read_stream, self._write_stream)
|
||||
|
||||
# Initialize with timeout
|
||||
try:
|
||||
await asyncio.wait_for(self._session.initialize(), timeout=self._connection_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("MCP session initialization timed out")
|
||||
await self.disconnect()
|
||||
return False
|
||||
|
||||
logger.info("Testing connection by listing tools...")
|
||||
|
||||
# Test tools listing with timeout
|
||||
try:
|
||||
await asyncio.wait_for(self._session.list_tools(), timeout=15)
|
||||
logger.info("✓ Successfully connected to MCP server.")
|
||||
self.server_available = True
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Tools listing timed out - server may be unresponsive")
|
||||
await self.disconnect()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MCP server: {e}")
|
||||
await self.disconnect()
|
||||
self.server_available = False
|
||||
return False
|
||||
|
||||
async def _log_stderr(self, stderr_stream):
|
||||
"""Log stderr from the server process"""
|
||||
try:
|
||||
async for line in stderr_stream:
|
||||
logger.debug(f"[garth-mcp-server] {line.decode().strip()}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading stderr: {e}")
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the MCP server and clean up resources."""
|
||||
logger.info("Disconnecting from MCP server...")
|
||||
if self._client_context:
|
||||
try:
|
||||
await self._client_context.__aexit__(None, None, None)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during MCP client disconnection: {e}")
|
||||
|
||||
self._session = None
|
||||
self.server_available = False
|
||||
self.cached_tools = []
|
||||
self._client_context = None
|
||||
self._read_stream = None
|
||||
self._write_stream = None
|
||||
logger.info("Disconnected.")
|
||||
|
||||
async def _ensure_connected(self):
|
||||
"""Ensure server is available"""
|
||||
if not self.server_available or not self._session:
|
||||
return await self.connect()
|
||||
return True
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict[str, Any] = None) -> Any:
|
||||
"""Call a tool on the MCP server with timeout"""
|
||||
if not await self._ensure_connected():
|
||||
raise Exception("MCP server not available")
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self._session.call_tool(tool_name, arguments or {}),
|
||||
timeout=self._connection_timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Tool call '{tool_name}' timed out")
|
||||
raise Exception(f"Tool call '{tool_name}' timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Tool call failed: {e}")
|
||||
raise
|
||||
|
||||
async def get_available_tools_info(self) -> List[Dict[str, str]]:
|
||||
"""Get information about available MCP tools with proper timeout handling"""
|
||||
# Return cached tools if available
|
||||
if self.cached_tools:
|
||||
logger.debug("Returning cached tools")
|
||||
return self.cached_tools
|
||||
|
||||
if not await self._ensure_connected():
|
||||
logger.warning("Could not connect to MCP server")
|
||||
return []
|
||||
|
||||
try:
|
||||
logger.debug("Fetching tools from MCP server...")
|
||||
|
||||
# Use timeout for the tools listing
|
||||
tools_result = await asyncio.wait_for(
|
||||
self._session.list_tools(),
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if not tools_result:
|
||||
logger.warning("No tools result received from server")
|
||||
return []
|
||||
|
||||
tools = tools_result.tools if hasattr(tools_result, 'tools') else []
|
||||
logger.info(f"Retrieved {len(tools)} tools from MCP server")
|
||||
|
||||
# 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
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Tools listing timed out after 15 seconds")
|
||||
# Don't cache empty result on timeout
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get tools info: {e}")
|
||||
return []
|
||||
|
||||
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 await self._ensure_connected() or 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']
|
||||
available_tools = await self.get_available_tools_info()
|
||||
|
||||
for tool_name in possible_tools:
|
||||
if any(tool['name'] == tool_name for tool in available_tools):
|
||||
logger.info(f"Calling tool: {tool_name}")
|
||||
result = await self.call_tool(tool_name, {"limit": limit})
|
||||
|
||||
if result and hasattr(result, 'content'):
|
||||
activities = []
|
||||
for content in result.content:
|
||||
if hasattr(content, 'text'):
|
||||
try:
|
||||
data = json.loads(content.text)
|
||||
if isinstance(data, list):
|
||||
activities.extend(data)
|
||||
else:
|
||||
activities.append(data)
|
||||
except json.JSONDecodeError:
|
||||
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,
|
||||
"duration": 3600,
|
||||
"averageSpeed": 6.94,
|
||||
"maxSpeed": 12.5,
|
||||
"elevationGain": 350,
|
||||
"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}"
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
# Test function to verify the fix
|
||||
async def test_fixed_connector():
|
||||
"""Test the fixed connector"""
|
||||
import yaml
|
||||
|
||||
# Load config
|
||||
with open("config.yaml") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
|
||||
connector = GarthMCPConnector(
|
||||
config_data['garth_token'],
|
||||
config_data['garth_mcp_server_path']
|
||||
)
|
||||
|
||||
print("Testing fixed MCP connector...")
|
||||
|
||||
try:
|
||||
# Test connection
|
||||
success = await connector.connect()
|
||||
if success:
|
||||
print("✓ Connection successful")
|
||||
|
||||
# Test tools retrieval
|
||||
tools = await connector.get_available_tools_info()
|
||||
print(f"✓ Retrieved {len(tools)} tools")
|
||||
|
||||
for tool in tools[:5]:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
|
||||
if len(tools) > 5:
|
||||
print(f" ... and {len(tools) - 5} more tools")
|
||||
|
||||
else:
|
||||
print("✗ Connection failed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await connector.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_fixed_connector())
|
||||
204
mcp_debug.py
Executable file
204
mcp_debug.py
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to identify MCP tools hanging issue
|
||||
"""
|
||||
import asyncio
|
||||
import yaml
|
||||
import logging
|
||||
import signal
|
||||
from main import GarthMCPConnector, Config
|
||||
|
||||
# Set up more detailed logging
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("Operation timed out")
|
||||
|
||||
async def debug_mcp_connection():
|
||||
"""Debug the MCP connection step by step"""
|
||||
# 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("=== MCP CONNECTION DEBUG ===")
|
||||
|
||||
# Step 1: Test connection
|
||||
print("\n1. Testing MCP connection...")
|
||||
try:
|
||||
success = await garmin.connect()
|
||||
if success:
|
||||
print("✓ MCP connection successful")
|
||||
else:
|
||||
print("✗ MCP connection failed")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"✗ MCP connection error: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Test session availability
|
||||
print("\n2. Testing session availability...")
|
||||
if garmin._session:
|
||||
print("✓ Session is available")
|
||||
else:
|
||||
print("✗ No session available")
|
||||
return
|
||||
|
||||
# Step 3: Test tools listing with timeout
|
||||
print("\n3. Testing tools listing (with 10s timeout)...")
|
||||
try:
|
||||
# Set up timeout
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(10) # 10 second timeout
|
||||
|
||||
tools_result = await garmin._session.list_tools()
|
||||
signal.alarm(0) # Cancel timeout
|
||||
|
||||
if tools_result:
|
||||
print(f"✓ Tools result received: {type(tools_result)}")
|
||||
if hasattr(tools_result, 'tools'):
|
||||
tools = tools_result.tools
|
||||
print(f"✓ Found {len(tools)} tools")
|
||||
|
||||
# Show first few tools
|
||||
for i, tool in enumerate(tools[:3]):
|
||||
print(f" Tool {i+1}: {tool.name} - {getattr(tool, 'description', 'No desc')}")
|
||||
|
||||
if len(tools) > 3:
|
||||
print(f" ... and {len(tools) - 3} more tools")
|
||||
else:
|
||||
print(f"✗ tools_result has no 'tools' attribute: {dir(tools_result)}")
|
||||
else:
|
||||
print("✗ No tools result received")
|
||||
|
||||
except TimeoutError:
|
||||
print("✗ Tools listing timed out after 10 seconds")
|
||||
print("This suggests the MCP server is hanging on list_tools()")
|
||||
except Exception as e:
|
||||
print(f"✗ Tools listing error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
signal.alarm(0) # Make sure to cancel any pending alarm
|
||||
|
||||
# Step 4: Test our wrapper method
|
||||
print("\n4. Testing our get_available_tools_info() method...")
|
||||
try:
|
||||
# Clear cache first
|
||||
garmin.cached_tools = []
|
||||
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(15) # 15 second timeout
|
||||
|
||||
tools = await garmin.get_available_tools_info()
|
||||
signal.alarm(0)
|
||||
|
||||
print(f"✓ get_available_tools_info() returned {len(tools)} tools")
|
||||
for tool in tools[:3]:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
|
||||
except TimeoutError:
|
||||
print("✗ get_available_tools_info() timed out")
|
||||
except Exception as e:
|
||||
print(f"✗ get_available_tools_info() error: {e}")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
# Cleanup
|
||||
print("\n5. Cleaning up...")
|
||||
await garmin.disconnect()
|
||||
print("✓ Cleanup complete")
|
||||
|
||||
async def test_alternative_approach():
|
||||
"""Test an alternative approach to getting tools info"""
|
||||
print("\n=== TESTING ALTERNATIVE APPROACH ===")
|
||||
|
||||
# Load config
|
||||
with open("config.yaml") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
|
||||
config = Config(**config_data)
|
||||
|
||||
# Create a simpler MCP connector for testing
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
import os
|
||||
import shutil
|
||||
|
||||
try:
|
||||
# Set up environment
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = config.garth_token
|
||||
|
||||
# Find server command
|
||||
server_command = shutil.which("garth-mcp-server")
|
||||
if not server_command:
|
||||
print("✗ garth-mcp-server not found")
|
||||
return
|
||||
|
||||
print(f"✓ Found server at: {server_command}")
|
||||
|
||||
# Create server parameters
|
||||
server_params = StdioServerParameters(
|
||||
command="/bin/bash",
|
||||
args=["-c", f"exec {server_command} \"$@\" 1>&2"],
|
||||
env=env,
|
||||
)
|
||||
|
||||
print("Starting direct MCP test...")
|
||||
async with stdio_client(server_params) as streams:
|
||||
if len(streams) == 3:
|
||||
read_stream, write_stream, stderr_stream = streams
|
||||
else:
|
||||
read_stream, write_stream = streams
|
||||
|
||||
session = ClientSession(read_stream, write_stream)
|
||||
await session.initialize()
|
||||
print("✓ Direct session initialized")
|
||||
|
||||
# Try to list tools with timeout
|
||||
try:
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(10)
|
||||
|
||||
print("Calling list_tools()...")
|
||||
tools_result = await session.list_tools()
|
||||
signal.alarm(0)
|
||||
|
||||
print(f"✓ Direct list_tools() successful: {len(tools_result.tools) if tools_result and hasattr(tools_result, 'tools') else 0} tools")
|
||||
|
||||
if tools_result and hasattr(tools_result, 'tools'):
|
||||
for tool in tools_result.tools[:3]:
|
||||
print(f" - {tool.name}")
|
||||
|
||||
except TimeoutError:
|
||||
print("✗ Direct list_tools() timed out")
|
||||
except Exception as e:
|
||||
print(f"✗ Direct list_tools() error: {e}")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Alternative approach failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def main():
|
||||
await debug_mcp_connection()
|
||||
await test_alternative_approach()
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n✗ Interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\n\n✗ Debug script failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
204
mcp_tool_lister.py
Executable file
204
mcp_tool_lister.py
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to launch an MCP server in the background and list its available tools.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
class MCPClient:
|
||||
def __init__(self, server_command: List[str]):
|
||||
self.server_command = server_command
|
||||
self.process = None
|
||||
self.request_id = 1
|
||||
|
||||
async def start_server(self):
|
||||
"""Start the MCP server process."""
|
||||
print(f"Starting MCP server: {' '.join(self.server_command)}")
|
||||
print(f"Python version: {platform.python_version()}")
|
||||
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
*self.server_command,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
# Give the server a moment to start
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Debug: show process object type
|
||||
print(f"Process object type: {type(self.process)}")
|
||||
|
||||
# Check if process has terminated
|
||||
if self.process.returncode is not None:
|
||||
stderr = await self.process.stderr.read()
|
||||
raise Exception(f"Server failed to start. Error: {stderr.decode()}")
|
||||
|
||||
print("Server started successfully")
|
||||
|
||||
async def send_request(self, method: str, params: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Send a JSON-RPC request to the MCP server."""
|
||||
if not self.process:
|
||||
raise Exception("Server not started")
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": self.request_id,
|
||||
"method": method
|
||||
}
|
||||
|
||||
if params is not None:
|
||||
request["params"] = params
|
||||
|
||||
self.request_id += 1
|
||||
|
||||
# Send request
|
||||
request_json = json.dumps(request) + "\n"
|
||||
print(f"Sending request: {request_json.strip()}")
|
||||
self.process.stdin.write(request_json.encode())
|
||||
await self.process.stdin.drain()
|
||||
|
||||
# Read response
|
||||
response_line = await self.process.stdout.readline()
|
||||
if not response_line:
|
||||
raise Exception("No response from server")
|
||||
|
||||
try:
|
||||
response_str = response_line.decode().strip()
|
||||
print(f"Received response: {response_str}")
|
||||
response = json.loads(response_str)
|
||||
return response
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"Invalid JSON response: {e}. Response: {response_str}")
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the MCP server."""
|
||||
print("Initializing server...")
|
||||
|
||||
try:
|
||||
response = await self.send_request("initialize", {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"clientInfo": {
|
||||
"name": "mcp-tool-lister",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
})
|
||||
|
||||
if "error" in response:
|
||||
raise Exception(f"Initialization failed: {response['error']}")
|
||||
|
||||
print("Server initialized successfully")
|
||||
return response.get("result", {})
|
||||
except Exception as e:
|
||||
print(f"Initialization error: {str(e)}")
|
||||
raise
|
||||
|
||||
async def list_tools(self) -> List[Dict[str, Any]]:
|
||||
"""List available tools from the MCP server."""
|
||||
print("Requesting tools list...")
|
||||
|
||||
try:
|
||||
# Pass empty parameters object to satisfy server requirements
|
||||
response = await self.send_request("tools/list", {})
|
||||
|
||||
if "error" in response:
|
||||
raise Exception(f"Failed to list tools: {response['error']}")
|
||||
|
||||
tools = response.get("result", {}).get("tools", [])
|
||||
print(f"Found {len(tools)} tools")
|
||||
return tools
|
||||
except Exception as e:
|
||||
print(f"Tool listing error: {str(e)}")
|
||||
raise
|
||||
|
||||
async def stop_server(self):
|
||||
"""Stop the MCP server process."""
|
||||
if self.process:
|
||||
print("Stopping server...")
|
||||
self.process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(self.process.wait(), timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
print("Server didn't stop gracefully, killing...")
|
||||
self.process.kill()
|
||||
await self.process.wait()
|
||||
print("Server stopped")
|
||||
|
||||
def print_tools(tools: List[Dict[str, Any]]):
|
||||
"""Pretty print the tools list."""
|
||||
if not tools:
|
||||
print("\nNo tools available.")
|
||||
return
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("AVAILABLE TOOLS")
|
||||
print(f"{'='*60}")
|
||||
|
||||
for i, tool in enumerate(tools, 1):
|
||||
name = tool.get("name", "Unknown")
|
||||
description = tool.get("description", "No description available")
|
||||
|
||||
print(f"\n{i}. {name}")
|
||||
print(f" Description: {description}")
|
||||
|
||||
# Print input schema if available
|
||||
input_schema = tool.get("inputSchema", {})
|
||||
if input_schema:
|
||||
properties = input_schema.get("properties", {})
|
||||
if properties:
|
||||
print(" Parameters:")
|
||||
for prop_name, prop_info in properties.items():
|
||||
prop_type = prop_info.get("type", "unknown")
|
||||
prop_desc = prop_info.get("description", "")
|
||||
required = prop_name in input_schema.get("required", [])
|
||||
req_str = " (required)" if required else " (optional)"
|
||||
print(f" - {prop_name} ({prop_type}){req_str}: {prop_desc}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python mcp_tool_lister.py <server_command> [args...]")
|
||||
print("Example: python mcp_tool_lister.py uvx my-mcp-server")
|
||||
sys.exit(1)
|
||||
|
||||
server_command = sys.argv[1:]
|
||||
client = MCPClient(server_command)
|
||||
|
||||
try:
|
||||
# Start and initialize the server
|
||||
await client.start_server()
|
||||
init_result = await client.initialize()
|
||||
|
||||
# Print server info
|
||||
server_info = init_result.get("serverInfo", {})
|
||||
if server_info:
|
||||
print(f"Server: {server_info.get('name', 'Unknown')} v{server_info.get('version', 'Unknown')}")
|
||||
|
||||
capabilities = init_result.get("capabilities", {})
|
||||
if capabilities:
|
||||
print(f"Server capabilities: {', '.join(capabilities.keys())}")
|
||||
|
||||
# List and display tools
|
||||
tools = await client.list_tools()
|
||||
print_tools(tools)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
await client.stop_server()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
274
test_workaround.py
Executable file
274
test_workaround.py
Executable file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script using the workaround for hanging MCP tools
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import shutil
|
||||
import logging
|
||||
import yaml
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# 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")
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GarthMCPConnectorWorkaround:
|
||||
"""MCP Connector with workaround for hanging tools issue"""
|
||||
|
||||
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 = []
|
||||
self._session: Optional[ClientSession] = None
|
||||
self._client_context = None
|
||||
self._read_stream = None
|
||||
self._write_stream = None
|
||||
|
||||
async def _get_server_params(self):
|
||||
"""Get server parameters for MCP connection"""
|
||||
env = os.environ.copy()
|
||||
env['GARTH_TOKEN'] = self.garth_token
|
||||
|
||||
server_command = shutil.which("garth-mcp-server")
|
||||
if not server_command:
|
||||
raise FileNotFoundError("garth-mcp-server not found")
|
||||
|
||||
return StdioServerParameters(
|
||||
command="/bin/bash",
|
||||
args=["-c", f"exec {server_command} \"$@\" 1>&2"],
|
||||
capture_stderr=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
"""Start the MCP server and establish a persistent session."""
|
||||
if self._session and self.server_available:
|
||||
return True
|
||||
|
||||
if not MCP_AVAILABLE:
|
||||
logger.error("MCP library not available")
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.info("Connecting to Garth MCP server...")
|
||||
server_params = await self._get_server_params()
|
||||
|
||||
self._client_context = stdio_client(server_params)
|
||||
streams = await self._client_context.__aenter__()
|
||||
|
||||
if len(streams) == 3:
|
||||
self._read_stream, self._write_stream, stderr_stream = streams
|
||||
# Start stderr logging in background
|
||||
asyncio.create_task(self._log_stderr(stderr_stream))
|
||||
else:
|
||||
self._read_stream, self._write_stream = streams
|
||||
|
||||
await asyncio.sleep(1.0) # Wait for server to start
|
||||
|
||||
self._session = ClientSession(self._read_stream, self._write_stream)
|
||||
|
||||
# Initialize with timeout
|
||||
try:
|
||||
await asyncio.wait_for(self._session.initialize(), timeout=30)
|
||||
logger.info("✓ MCP session initialized successfully")
|
||||
self.server_available = True
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("MCP session initialization timed out")
|
||||
await self.disconnect()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MCP server: {e}")
|
||||
await self.disconnect()
|
||||
return False
|
||||
|
||||
async def _log_stderr(self, stderr_stream):
|
||||
"""Log stderr from server in background"""
|
||||
try:
|
||||
async for line in stderr_stream:
|
||||
logger.debug(f"[server] {line.decode().strip()}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from MCP server"""
|
||||
if self._client_context:
|
||||
try:
|
||||
await self._client_context.__aexit__(None, None, None)
|
||||
except Exception as e:
|
||||
logger.error(f"Disconnect error: {e}")
|
||||
|
||||
self._session = None
|
||||
self.server_available = False
|
||||
self.cached_tools = []
|
||||
self._client_context = None
|
||||
|
||||
async def get_available_tools_info(self) -> List[Dict[str, str]]:
|
||||
"""Get tools info using workaround - bypasses hanging list_tools()"""
|
||||
if not self.cached_tools:
|
||||
logger.info("Using known Garth MCP tools (workaround for hanging list_tools)")
|
||||
self.cached_tools = [
|
||||
{"name": "user_profile", "description": "Get user profile information"},
|
||||
{"name": "user_settings", "description": "Get user settings and preferences"},
|
||||
{"name": "daily_sleep", "description": "Get daily sleep summary data"},
|
||||
{"name": "daily_steps", "description": "Get daily steps data"},
|
||||
{"name": "daily_hrv", "description": "Get heart rate variability data"},
|
||||
{"name": "get_activities", "description": "Get list of activities"},
|
||||
{"name": "get_activity_details", "description": "Get detailed activity information"},
|
||||
{"name": "get_body_composition", "description": "Get body composition data"},
|
||||
{"name": "get_respiration_data", "description": "Get respiration data"},
|
||||
{"name": "get_blood_pressure", "description": "Get blood pressure readings"}
|
||||
]
|
||||
return self.cached_tools
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict[str, Any] = None) -> Any:
|
||||
"""Call a tool with timeout"""
|
||||
if not self.server_available or not self._session:
|
||||
raise Exception("MCP server not available")
|
||||
|
||||
try:
|
||||
logger.info(f"Calling tool: {tool_name}")
|
||||
result = await asyncio.wait_for(
|
||||
self._session.call_tool(tool_name, arguments or {}),
|
||||
timeout=30
|
||||
)
|
||||
logger.info(f"✓ Tool call '{tool_name}' successful")
|
||||
return result
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Tool call '{tool_name}' timed out")
|
||||
raise Exception(f"Tool call '{tool_name}' timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Tool call '{tool_name}' failed: {e}")
|
||||
raise
|
||||
|
||||
async def test_real_tool_call(self, tool_name: str = "user_profile"):
|
||||
"""Test if we can actually call a real MCP tool"""
|
||||
if not self.server_available:
|
||||
return False, "Server not connected"
|
||||
|
||||
try:
|
||||
result = await self.call_tool(tool_name)
|
||||
return True, result
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def run_tests():
|
||||
"""Run comprehensive tests with the workaround"""
|
||||
print("="*60)
|
||||
print("TESTING MCP CONNECTOR WITH WORKAROUND")
|
||||
print("="*60)
|
||||
|
||||
# Load config
|
||||
try:
|
||||
with open("config.yaml") as f:
|
||||
config = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
print(f"✗ Could not load config: {e}")
|
||||
return
|
||||
|
||||
connector = GarthMCPConnectorWorkaround(
|
||||
config['garth_token'],
|
||||
config['garth_mcp_server_path']
|
||||
)
|
||||
|
||||
# Test 1: Connection
|
||||
print("\n1. Testing MCP server connection...")
|
||||
success = await connector.connect()
|
||||
if success:
|
||||
print("✓ MCP server connected successfully")
|
||||
else:
|
||||
print("✗ MCP server connection failed")
|
||||
return
|
||||
|
||||
# Test 2: Tools listing (with workaround)
|
||||
print("\n2. Testing tools listing (using workaround)...")
|
||||
try:
|
||||
tools = await connector.get_available_tools_info()
|
||||
print(f"✓ Retrieved {len(tools)} tools using workaround")
|
||||
|
||||
print("\nAvailable tools:")
|
||||
for tool in tools:
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Tools listing failed: {e}")
|
||||
|
||||
# Test 3: Real tool call
|
||||
print("\n3. Testing actual tool call...")
|
||||
success, result = await connector.test_real_tool_call("user_profile")
|
||||
if success:
|
||||
print("✓ Real tool call successful!")
|
||||
print("Sample result:")
|
||||
if hasattr(result, 'content'):
|
||||
for content in result.content[:1]: # Show first result only
|
||||
if hasattr(content, 'text'):
|
||||
# Try to parse and show nicely
|
||||
try:
|
||||
data = json.loads(content.text)
|
||||
print(f" Profile data: {type(data)} with keys: {list(data.keys()) if isinstance(data, dict) else 'N/A'}")
|
||||
except:
|
||||
print(f" Raw text: {content.text[:100]}...")
|
||||
else:
|
||||
print(f" Result type: {type(result)}")
|
||||
else:
|
||||
print(f"✗ Real tool call failed: {result}")
|
||||
|
||||
# Test 4: Alternative tool call
|
||||
print("\n4. Testing alternative tool call...")
|
||||
success, result = await connector.test_real_tool_call("get_activities")
|
||||
if success:
|
||||
print("✓ Activities tool call successful!")
|
||||
else:
|
||||
print(f"✗ Activities tool call failed: {result}")
|
||||
|
||||
# Test 5: Show that app would work
|
||||
print("\n5. Simulating main app behavior...")
|
||||
try:
|
||||
# This simulates what your main app does
|
||||
available_tools = await connector.get_available_tools_info()
|
||||
print(f"✓ Main app would see {len(available_tools)} available tools")
|
||||
|
||||
# Show tool info like your app does
|
||||
tool_info = "\n\nAvailable Garmin data tools:\n"
|
||||
for tool in available_tools:
|
||||
tool_info += f"- {tool['name']}: {tool.get('description', 'No description')}\n"
|
||||
|
||||
print("Tool info that would be sent to AI:")
|
||||
print(tool_info)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Main app simulation failed: {e}")
|
||||
|
||||
# Cleanup
|
||||
print("\n6. Cleaning up...")
|
||||
await connector.disconnect()
|
||||
print("✓ Cleanup complete")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("TEST SUMMARY:")
|
||||
print("- MCP connection: Working")
|
||||
print("- Tools listing: Working (with workaround)")
|
||||
print("- Your main app should now run without hanging!")
|
||||
print("="*60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run_tests())
|
||||
except KeyboardInterrupt:
|
||||
print("\n✗ Tests interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\n✗ Test script failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
Reference in New Issue
Block a user