mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-01-25 16:42:24 +00:00
274 lines
9.9 KiB
Python
Executable File
274 lines
9.9 KiB
Python
Executable File
#!/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() |