Skip to main content
Glama

Gemini MCP Server

base_test.pyโ€ข14.6 kB
#!/usr/bin/env python3 """ Base Test Class for Communication Simulator Tests Provides common functionality and utilities for all simulator tests. """ import json import logging import os import subprocess from typing import Optional from .log_utils import LogUtils class BaseSimulatorTest: """Base class for all communication simulator tests""" def __init__(self, verbose: bool = False): self.verbose = verbose self.test_files = {} self.test_dir = None # Configure logging first log_level = logging.DEBUG if verbose else logging.INFO logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") self.logger = logging.getLogger(self.__class__.__name__) self.python_path = self._get_python_path() def _get_python_path(self) -> str: """Get the Python path for the virtual environment""" current_dir = os.getcwd() # Try .venv first (modern convention) venv_python = os.path.join(current_dir, ".venv", "bin", "python") if os.path.exists(venv_python): return venv_python # Try venv as fallback venv_python = os.path.join(current_dir, "venv", "bin", "python") if os.path.exists(venv_python): return venv_python # Try .zen_venv as fallback zen_venv_python = os.path.join(current_dir, ".zen_venv", "bin", "python") if os.path.exists(zen_venv_python): return zen_venv_python # Fallback to system python if venv doesn't exist self.logger.warning("Virtual environment not found, using system python") return "python" def setup_test_files(self): """Create test files for the simulation""" # Test Python file python_content = '''""" Sample Python module for testing MCP conversation continuity """ def fibonacci(n): """Calculate fibonacci number recursively""" if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2) def factorial(n): """Calculate factorial iteratively""" result = 1 for i in range(1, n + 1): result *= i return result class Calculator: """Simple calculator class""" def __init__(self): self.history = [] def add(self, a, b): result = a + b self.history.append(f"{a} + {b} = {result}") return result def multiply(self, a, b): result = a * b self.history.append(f"{a} * {b} = {result}") return result ''' # Test configuration file config_content = """{ "database": { "host": "localhost", "port": 5432, "name": "testdb", "ssl": true }, "cache": { "redis_url": "redis://localhost:6379", "ttl": 3600 }, "logging": { "level": "INFO", "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" } }""" # Create files in the current project directory current_dir = os.getcwd() self.test_dir = os.path.join(current_dir, "test_simulation_files") os.makedirs(self.test_dir, exist_ok=True) test_py = os.path.join(self.test_dir, "test_module.py") test_config = os.path.join(self.test_dir, "config.json") with open(test_py, "w") as f: f.write(python_content) with open(test_config, "w") as f: f.write(config_content) # Ensure absolute paths for MCP server compatibility self.test_files = {"python": os.path.abspath(test_py), "config": os.path.abspath(test_config)} self.logger.debug(f"Created test files with absolute paths: {list(self.test_files.values())}") def call_mcp_tool(self, tool_name: str, params: dict) -> tuple[Optional[str], Optional[str]]: """Call an MCP tool via standalone server""" try: # Prepare the MCP initialization and tool call sequence init_request = { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "clientInfo": {"name": "communication-simulator", "version": "1.0.0"}, }, } # Send initialized notification initialized_notification = {"jsonrpc": "2.0", "method": "notifications/initialized"} # Prepare the tool call request tool_request = { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": tool_name, "arguments": params}, } # Combine all messages messages = [ json.dumps(init_request, ensure_ascii=False), json.dumps(initialized_notification, ensure_ascii=False), json.dumps(tool_request, ensure_ascii=False), ] # Join with newlines as MCP expects input_data = "\n".join(messages) + "\n" # Call the standalone MCP server directly server_cmd = [self.python_path, "server.py"] self.logger.debug(f"Calling MCP tool {tool_name} with proper initialization") # Execute the command with proper handling for async responses # For consensus tool and other long-running tools, we need to ensure # the subprocess doesn't close prematurely result = subprocess.run( server_cmd, input=input_data, text=True, capture_output=True, timeout=3600, # 1 hour timeout check=False, # Don't raise on non-zero exit code ) if result.returncode != 0: self.logger.error(f"Standalone server failed with return code {result.returncode}") self.logger.error(f"Stderr: {result.stderr}") # Still try to parse stdout as the response might have been written before the error self.logger.debug(f"Attempting to parse stdout despite error: {result.stdout[:500]}") # Parse the response - look for the tool call response response_data = self._parse_mcp_response(result.stdout, expected_id=2) if not response_data: return None, None # Extract continuation_id if present continuation_id = self._extract_continuation_id(response_data) return response_data, continuation_id except subprocess.TimeoutExpired: self.logger.error(f"MCP tool call timed out after 1 hour: {tool_name}") return None, None except Exception as e: self.logger.error(f"MCP tool call failed: {e}") return None, None def _parse_mcp_response(self, stdout: str, expected_id: int = 2) -> Optional[str]: """Parse MCP JSON-RPC response from stdout""" try: lines = stdout.strip().split("\n") for line in lines: if line.strip() and line.startswith("{"): response = json.loads(line) # Look for the tool call response with the expected ID if response.get("id") == expected_id and "result" in response: # Extract the actual content from the response result = response["result"] # Handle new response format with 'content' array if isinstance(result, dict) and "content" in result: content_array = result["content"] if isinstance(content_array, list) and len(content_array) > 0: return content_array[0].get("text", "") # Handle legacy format elif isinstance(result, list) and len(result) > 0: return result[0].get("text", "") elif response.get("id") == expected_id and "error" in response: self.logger.error(f"MCP error: {response['error']}") return None # If we get here, log all responses for debugging self.logger.warning(f"No valid tool call response found for ID {expected_id}") self.logger.warning(f"Full stdout: {stdout}") self.logger.warning(f"Total stdout lines: {len(lines)}") for i, line in enumerate(lines[:10]): # Log first 10 lines self.logger.warning(f"Line {i}: {line[:100]}...") return None except json.JSONDecodeError as e: self.logger.error(f"Failed to parse MCP response: {e}") self.logger.debug(f"Stdout that failed to parse: {stdout}") return None def _extract_continuation_id(self, response_text: str) -> Optional[str]: """Extract continuation_id from response metadata""" try: # Parse the response text as JSON to look for continuation metadata response_data = json.loads(response_text) # Look for continuation_id in various places if isinstance(response_data, dict): # Check for direct continuation_id field (new workflow tools) if "continuation_id" in response_data: return response_data["continuation_id"] # Check metadata metadata = response_data.get("metadata", {}) if "thread_id" in metadata: return metadata["thread_id"] # Check follow_up_request follow_up = response_data.get("follow_up_request", {}) if follow_up and "continuation_id" in follow_up: return follow_up["continuation_id"] # Check continuation_offer continuation_offer = response_data.get("continuation_offer", {}) if continuation_offer and "continuation_id" in continuation_offer: return continuation_offer["continuation_id"] self.logger.debug(f"No continuation_id found in response: {response_data}") return None except json.JSONDecodeError as e: self.logger.debug(f"Failed to parse response for continuation_id: {e}") return None def run_command(self, cmd: list[str], check: bool = True, capture_output: bool = False, **kwargs): """Run a shell command with logging""" if self.verbose: self.logger.debug(f"Running: {' '.join(cmd)}") return subprocess.run(cmd, check=check, capture_output=capture_output, **kwargs) def create_additional_test_file(self, filename: str, content: str) -> str: """Create an additional test file for mixed scenario testing""" if not hasattr(self, "test_dir") or not self.test_dir: raise RuntimeError("Test directory not initialized. Call setup_test_files() first.") file_path = os.path.join(self.test_dir, filename) with open(file_path, "w") as f: f.write(content) # Return absolute path for MCP server compatibility return os.path.abspath(file_path) def cleanup_test_files(self): """Clean up test files""" if hasattr(self, "test_dir") and self.test_dir and os.path.exists(self.test_dir): import shutil shutil.rmtree(self.test_dir) self.logger.debug(f"Removed test files directory: {self.test_dir}") # ============================================================================ # Log Utility Methods (delegate to LogUtils) # ============================================================================ def get_server_logs_since(self, since_time: Optional[str] = None) -> str: """Get server logs from both main and activity log files.""" return LogUtils.get_server_logs_since(since_time) def get_recent_server_logs(self, lines: int = 500) -> str: """Get recent server logs from the main log file.""" return LogUtils.get_recent_server_logs(lines) def get_server_logs_subprocess(self, lines: int = 500) -> str: """Get server logs using subprocess (alternative method).""" return LogUtils.get_server_logs_subprocess(lines) def check_server_logs_for_errors(self, lines: int = 500) -> list[str]: """Check server logs for error messages.""" return LogUtils.check_server_logs_for_errors(lines) def extract_conversation_usage_logs(self, logs: str) -> list[dict[str, int]]: """Extract token budget calculation information from logs.""" return LogUtils.extract_conversation_usage_logs(logs) def extract_conversation_token_usage(self, logs: str) -> list[int]: """Extract conversation token usage values from logs.""" return LogUtils.extract_conversation_token_usage(logs) def extract_thread_creation_logs(self, logs: str) -> list[dict[str, str]]: """Extract thread creation logs with parent relationships.""" return LogUtils.extract_thread_creation_logs(logs) def extract_history_traversal_logs(self, logs: str) -> list[dict[str, any]]: """Extract conversation history traversal logs.""" return LogUtils.extract_history_traversal_logs(logs) def validate_file_deduplication_in_logs(self, logs: str, tool_name: str, test_file: str) -> bool: """Validate that logs show file deduplication behavior.""" return LogUtils.validate_file_deduplication_in_logs(logs, tool_name, test_file) def search_logs_for_pattern( self, pattern: str, logs: Optional[str] = None, case_sensitive: bool = False ) -> list[str]: """Search logs for a specific pattern.""" return LogUtils.search_logs_for_pattern(pattern, logs, case_sensitive) def get_log_file_info(self) -> dict[str, dict[str, any]]: """Get information about log files.""" return LogUtils.get_log_file_info() def run_test(self) -> bool: """Run the test - to be implemented by subclasses""" raise NotImplementedError("Subclasses must implement run_test()") @property def test_name(self) -> str: """Get the test name - to be implemented by subclasses""" raise NotImplementedError("Subclasses must implement test_name property") @property def test_description(self) -> str: """Get the test description - to be implemented by subclasses""" raise NotImplementedError("Subclasses must implement test_description property")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BeehiveInnovations/gemini-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server