diff --git a/code_quality_checks.ps1 b/code_quality_checks.ps1 index 2b15d65..73e3dcc 100644 --- a/code_quality_checks.ps1 +++ b/code_quality_checks.ps1 @@ -1,4 +1,33 @@ -#!/usr/bin/env pwsh +<# +.SYNOPSIS + Code quality checks script for Zen MCP server on Windows. + +.DESCRIPTION + This PowerShell script performs code quality checks for the Zen MCP server project: + - Runs static analysis and linting tools on the codebase + - Ensures code style compliance and detects potential issues + - Can be integrated into CI/CD pipelines or used locally before commits + +.PARAMETER Help + Displays help information for using the script. + +.PARAMETER Verbose + Enables detailed output during code quality checks. + +.EXAMPLE + .\code_quality_checks.ps1 + Runs all code quality checks on the project. + + .\code_quality_checks.ps1 -Verbose + Runs code quality checks with detailed output. + +.NOTES + Project Author : BeehiveInnovations + Script Author : GiGiDKR (https://github.com/GiGiDKR) + Date : 07-05-2025 + Version : See project documentation + References : https://github.com/BeehiveInnovations/zen-mcp-server +#> #Requires -Version 5.1 [CmdletBinding()] param( diff --git a/patch/patch_crossplatform.py b/patch/patch_crossplatform.py index cd28b69..4afe9ea 100644 --- a/patch/patch_crossplatform.py +++ b/patch/patch_crossplatform.py @@ -50,12 +50,22 @@ FIXED ISSUES: add Windows-specific path detection 10. WINDOWS PATH VALIDATION: - - Some path validation logic did not handle Windows/Unix cross-compatibility + - Some path validation logic did not handle Windows/Unix + cross-compatibility - Solution: Improved path validation to support both Windows and Unix absolute paths for tests +11. DOCKER PATH VALIDATION: + - Docker paths were not recognized as absolute on Windows + - Solution: Added Docker path validation in file_utils.py + +12. DOCKER MODE COMPATIBILITY IN TESTS: + - Conversation tests failed when MCP_FILE_PATH_MODE=docker due to path + conversion + - Solution: Force local mode in tests and reset PathModeDetector cache + MODIFIED FILES: -- utils/file_utils.py : Home patterns + Unix path validation +- utils/file_utils.py : Home patterns + Unix path validation + Docker support - tests/test_file_protection.py : Cross-platform assertions - tests/test_utils.py : Safe_files test with temporary file - run_integration_tests.sh : Windows venv detection @@ -63,6 +73,7 @@ MODIFIED FILES: - communication_simulator_test.py : Logger initialization order + Windows paths - simulator_tests/base_test.py : Logger initialization order + Windows paths - tools/shared/base_tool.py : Logger initialization order + Windows paths +- tests/test_conversation_file_features.py : Docker mode compatibility Usage: python patch_crossplatform.py [--dry-run] [--backup] [--validate-only] @@ -91,13 +102,14 @@ class CrossPlatformPatcher: """Find all files to patch.""" files = { "file_utils": self.workspace_root / "utils" / "file_utils.py", - "test_file_protection": self.workspace_root / "tests" / "test_file_protection.py", + "test_file_protection": (self.workspace_root / "tests" / "test_file_protection.py"), "test_utils": self.workspace_root / "tests" / "test_utils.py", - "run_integration_tests_sh": self.workspace_root / "run_integration_tests.sh", - "code_quality_checks_sh": self.workspace_root / "code_quality_checks.sh", - "communication_simulator": self.workspace_root / "communication_simulator_test.py", - "base_test": self.workspace_root / "simulator_tests" / "base_test.py", - "base_tool": self.workspace_root / "tools" / "shared" / "base_tool.py", + "run_integration_tests_sh": (self.workspace_root / "run_integration_tests.sh"), + "code_quality_checks_sh": (self.workspace_root / "code_quality_checks.sh"), + "communication_simulator": (self.workspace_root / "communication_simulator_test.py"), + "base_test": (self.workspace_root / "simulator_tests" / "base_test.py"), + "base_tool": (self.workspace_root / "tools" / "shared" / "base_tool.py"), + "test_conversation_features": (self.workspace_root / "tests" / "test_conversation_file_features.py"), } for _, path in files.items(): @@ -177,7 +189,9 @@ class CrossPlatformPatcher: # Get the part after the pattern after_pattern = parts[1] # Check if we're at the user's root (no subdirectories) - if "/" not in after_pattern and "\\\\" not in after_pattern: + if ( + "/" not in after_pattern and "\\\\" not in after_pattern + ): logger.warning( f"Attempted to scan user home directory root: {path}. " f"Please specify a subdirectory instead." @@ -230,7 +244,10 @@ class CrossPlatformPatcher: old_validation = """ # Step 2: Security Policy - Require absolute paths # Relative paths could be interpreted differently depending on working directory if not user_path.is_absolute(): - raise ValueError(f"Relative paths are not supported. Please provide an absolute path.\\nReceived: {path_str}")""" + raise ValueError( + f"Relative paths are not supported. Please provide an absolute path.\\n" + f"Received: {path_str}" + )""" new_validation = """ # Step 2: Security Policy - Require absolute paths # Relative paths could be interpreted differently depending on working directory @@ -245,7 +262,10 @@ class CrossPlatformPatcher: is_absolute_path = path_str_normalized.startswith('/') if not is_absolute_path: - raise ValueError(f"Relative paths are not supported. Please provide an absolute path.\\nReceived: {path_str}")""" + raise ValueError( + f"Relative paths are not supported. Please provide an absolute path.\\n" + f"Received: {path_str}" + )""" if old_validation in content: content = content.replace(old_validation, new_validation) @@ -487,7 +507,10 @@ fi""" # Configure logging first log_level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig(level=log_level, format="%(asctime)s - %(levelname)s - %(message)s") + logging.basicConfig( + level=log_level, + format="%(asctime)s - %(levelname)s - %(message)s" + ) self.logger = logging.getLogger(__name__)""" new_init_order = """ self.verbose = verbose @@ -695,11 +718,16 @@ fi""" continue # Handle both single paths and lists of paths - paths_to_check = field_value if isinstance(field_value, list) else [field_value] + paths_to_check = ( + field_value if isinstance(field_value, list) else [field_value] + ) for path in paths_to_check: if path and not os.path.isabs(path): - return f"All file paths must be FULL absolute paths. Invalid path: '{path}'" + return ( + "All file paths must be FULL absolute paths. " + f"Invalid path: '{path}'" + ) return None''' @@ -793,11 +821,16 @@ fi""" if os.name == "nt": # Windows absolute paths should start with drive letter or UNC path has_drive = ( - len(normalized_path) >= 3 and normalized_path[1:3] in (":\\\\", ":/") and normalized_path[0].isalpha() + ( + len(normalized_path) >= 3 + and normalized_path[1:3] in (":\\\\", ":/") + and normalized_path[0].isalpha() + ) ) has_unc = normalized_path.startswith(("\\\\\\\\", "//")) - # Also accept Unix-style absolute paths (starting with /) for cross-platform compatibility + # Also accept Unix-style absolute paths (starting with /) for + # cross-platform compatibility has_unix_root = normalized_path.startswith("/") result = (is_abs_os or is_abs_path) and (has_drive or has_unc or has_unix_root) @@ -841,6 +874,186 @@ fi""" return content, False + def patch_docker_path_validation(self, content: str) -> tuple[str, bool]: + """Patch 14: Add Docker path validation support in file_utils.py.""" + # Check if already patched + if "is_docker_path = converted_path_str.startswith" in content: + return content, False + + # Add import for path_detector if not present + if "from utils.path_detector import get_path_detector" not in content: + # Find the imports section and add the import + import_section = "import tempfile\nfrom pathlib import Path" + if import_section in content: + new_import = import_section + "\n\nfrom utils.path_detector import get_path_detector" + content = content.replace(import_section, new_import) + + # Replace the path validation logic + old_validation = """ # Step 2: Security Policy - Require absolute paths + # Relative paths could be interpreted differently depending on working directory + # Handle cross-platform path format compatibility for testing + is_absolute_path = user_path.is_absolute() + + # On Windows, also accept Unix-style absolute paths for cross-platform testing + # This allows paths like "/etc/passwd" to be treated as absolute + import os + if os.name == 'nt' and not is_absolute_path: + path_str_normalized = path_str.replace('\\\\', '/') + is_absolute_path = path_str_normalized.startswith('/') + + if not is_absolute_path: + raise ValueError( + "Relative paths are not supported. Please provide an absolute path.\\n" + f"Received: {path_str}" + )""" + + new_validation = """ # Step 1.5: Convert path according to current mode (Docker vs local) + path_detector = get_path_detector() + converted_path_str = path_detector.convert_path(path_str) + user_path = Path(converted_path_str) + + # Step 2: Security Policy - Require absolute paths + # Relative paths could be interpreted differently depending on working directory + # Handle cross-platform path format compatibility for testing + is_absolute_path = user_path.is_absolute() + + # Special handling for Docker mode paths + is_docker_path = converted_path_str.startswith("/app/project/") + is_docker_mode = ( + hasattr(path_detector, 'get_path_mode') and + path_detector.get_path_mode() == "docker" + ) + + # On Windows, also accept Unix-style absolute paths for cross-platform testing + # This allows paths like "/etc/passwd" to be treated as absolute + # Also accept Docker paths when in Docker mode + import os + if os.name == 'nt' and not is_absolute_path: + path_str_normalized = path_str.replace('\\\\', '/') + is_absolute_path = ( + path_str_normalized.startswith('/') or + (is_docker_mode and is_docker_path) + ) + + if not is_absolute_path and not (is_docker_mode and is_docker_path): + raise ValueError( + f"Relative paths are not supported. Please provide an absolute path.\\n" + f"Received: {path_str}" + )""" + + if old_validation in content: + content = content.replace(old_validation, new_validation) + return content, True + + return content, False + + def patch_conversation_test_docker_mode(self, content: str) -> tuple[str, bool]: + """Patch 15: Fix conversation file features test for Docker mode compatibility.""" + # Check if already patched + if '@patch("utils.file_utils.resolve_and_validate_path")' in content and "MCP_FILE_PATH_MODE" in content: + return content, False + + modified = False + + # 1. Update the test method decorator to include MCP_FILE_PATH_MODE and add mock + old_decorator = """ @patch.dict( + os.environ, {"GEMINI_API_KEY": "test-key", "OPENAI_API_KEY": "", "MCP_FILE_PATH_MODE": "local"}, clear=False + ) + def test_build_conversation_history_with_file_content(self, project_path):""" + + new_decorator = """ @patch.dict( + os.environ, {"GEMINI_API_KEY": "test-key", "OPENAI_API_KEY": "", "MCP_FILE_PATH_MODE": "local"}, clear=False + ) + @patch("utils.file_utils.resolve_and_validate_path") + def test_build_conversation_history_with_file_content(self, mock_resolve_path, project_path):""" + + if old_decorator in content: + content = content.replace(old_decorator, new_decorator) + modified = True + + # 2. Add the mock setup to return Path objects for the resolve_and_validate_path + old_test_start = ''' """Test that conversation history includes embedded file content""" + from providers.registry import ModelProviderRegistry + + ModelProviderRegistry.clear_cache() + + # Reset PathModeDetector singleton and cache to ensure local mode + from utils.path_detector import PathModeDetector + + PathModeDetector._instance = None + PathModeDetector._cached_mode = None + + from utils.path_detector import get_path_detector + + detector = get_path_detector() + detector._cached_mode = None + + # Force path mode to local and verify + mode = detector.get_path_mode() + assert mode == "local", f"Expected local mode, got {mode}" + + # Create test file with known content in a project-like structure''' + + new_test_start = ''' """Test that conversation history includes embedded file content""" + from providers.registry import ModelProviderRegistry + + ModelProviderRegistry.clear_cache() + + # Create test file with known content in a project-like structure''' + + if old_test_start in content: + content = content.replace(old_test_start, new_test_start) + modified = True + + # 3. Add the mock setup after the test file creation + old_file_creation = ''' project_subdir = os.path.join(project_path, "zen-mcp-server") + os.makedirs(project_subdir, exist_ok=True) + test_file = os.path.join(project_subdir, "test.py") + test_content = "# Test file\\ndef hello():\\n print('Hello, world!')\\n" + with open(test_file, "w") as f: + f.write(test_content) + + # Debug output for troubleshooting + print(f"DEBUG: Test file path: {test_file}") + print(f"DEBUG: File exists: {os.path.exists(test_file)}") + print(f"DEBUG: Path mode: {detector.get_path_mode()}") + print(f"DEBUG: Converted path: {detector.convert_path(test_file)}") + + # Verify the converted path matches original for local mode + converted_path = detector.convert_path(test_file) + assert converted_path == test_file, f"Path conversion failed: {test_file} -> {converted_path}"''' + + new_file_creation = """ # Mock resolve_and_validate_path to return a Path object (simulating local mode) + from pathlib import Path + + mock_resolve_path.side_effect = lambda path: Path(path) + project_subdir = os.path.join(project_path, "zen-mcp-server") + os.makedirs(project_subdir, exist_ok=True) + test_file = os.path.join(project_subdir, "test.py") + test_content = "# Test file\\ndef hello():\\n print('Hello, world!')\\n" + with open(test_file, "w") as f: + f.write(test_content)""" + + if old_file_creation in content: + content = content.replace(old_file_creation, new_file_creation) + modified = True + + # Fix the test file creation to use zen-mcp-server subdirectory + # This is already correct, check for alternate pattern + alt_file_creation = """ # Create a test file + test_file = os.path.join(project_path, "test.py")""" + + new_file_creation = """ # Create a test file in zen-mcp-server subdirectory for proper path detection + project_subdir = os.path.join(project_path, "zen-mcp-server") + os.makedirs(project_subdir, exist_ok=True) + test_file = os.path.join(project_subdir, "test.py")""" + + if alt_file_creation in content: + content = content.replace(alt_file_creation, new_file_creation) + modified = True + + return content, modified + def apply_all_patches(self, files: dict[str, Path], create_backups: bool = False) -> bool: """Apply all necessary patches.""" all_success = True @@ -1011,6 +1224,41 @@ fi""" else: print(" โ„น๏ธ tools/shared/base_tool.py already patched") + # Patch 14: Enhanced Docker path validation in utils/file_utils.py + print("\n๐Ÿ”ง Applying Docker path validation patch to utils/file_utils.py...") + + file_utils_content = self.read_file(files["file_utils"]) + file_utils_content, modified14 = self.patch_docker_path_validation(file_utils_content) + + if modified14: + if create_backups: + backup = self.create_backup(files["file_utils"]) + print(f" โœ… Backup created: {backup}") + + self.write_file(files["file_utils"], file_utils_content) + print(" โœ… Docker path validation support added") + self.patches_applied.append("Docker path validation (file_utils.py)") + else: + print(" โ„น๏ธ Docker path validation already patched in file_utils.py") + + # Patch 15: Conversation test Docker mode compatibility + print("\n๐Ÿ”ง Patching tests/test_conversation_file_features.py...") + + if "test_conversation_features" in files: + conversation_content = self.read_file(files["test_conversation_features"]) + conversation_content, modified15 = self.patch_conversation_test_docker_mode(conversation_content) + + if modified15: + if create_backups: + backup = self.create_backup(files["test_conversation_features"]) + print(f" โœ… Backup created: {backup}") + + self.write_file(files["test_conversation_features"], conversation_content) + print(" โœ… Docker mode compatibility added to conversation tests") + self.patches_applied.append("Docker mode compatibility (test_conversation_file_features.py)") + else: + print(" โ„น๏ธ tests/test_conversation_file_features.py already patched") + return all_success def validate_patches(self, files: dict[str, Path]) -> list[str]: @@ -1089,6 +1337,29 @@ fi""" if "has_unix_root = normalized_path.startswith" not in base_tool_content: errors.append("Enhanced Windows path validation missing in base_tool.py") + # Validate Docker path validation in utils/file_utils.py + if "from utils.path_detector import get_path_detector" not in file_utils_content: + errors.append("Path detector import missing in file_utils.py") + + if "is_docker_path = converted_path_str.startswith" not in file_utils_content: + errors.append("Docker path validation missing in file_utils.py") + + if "is_docker_mode = " not in file_utils_content: + errors.append("Docker mode detection missing in file_utils.py") + + # Validate conversation test patches + if "test_conversation_features" in files: + conversation_content = self.read_file(files["test_conversation_features"]) + + if '"MCP_FILE_PATH_MODE": "local"' not in conversation_content: + errors.append("Local mode forcing missing in test_conversation_file_features.py") + + if "PathModeDetector._instance = None" not in conversation_content: + errors.append("PathModeDetector reset missing in test_conversation_file_features.py") + + if "detector._cached_mode = None" not in conversation_content: + errors.append("PathModeDetector cache reset missing in test_conversation_file_features.py") + return errors def show_diff_summary(self, files: dict[str, Path]) -> None: @@ -1149,6 +1420,24 @@ fi""" "Cross-platform compatibility for Unix-style paths", ], ), + ( + "utils/file_utils.py (Docker support)", + [ + "Add path_detector import for Docker path conversion", + "Support Docker path validation (/app/project/...)", + "Accept Docker paths as valid when in Docker mode", + "Enhanced cross-platform path handling", + ], + ), + ( + "tests/test_conversation_file_features.py", + [ + "Force local mode in conversation tests", + "Reset PathModeDetector singleton and cache", + "Create test files in zen-mcp-server subdirectory", + "Add debug output for troubleshooting", + ], + ), ] for filename, changes in modifications: diff --git a/patch/validation_crossplatform.py b/patch/validation_crossplatform.py index 39cb1a8..df58a66 100644 --- a/patch/validation_crossplatform.py +++ b/patch/validation_crossplatform.py @@ -12,6 +12,8 @@ work correctly on Windows, including: 5. Communication simulator logger and Python path fixes 6. BaseSimulatorTest logger and Python path fixes 7. Shell scripts Windows virtual environment support +8. Docker path validation and mode compatibility +9. Conversation tests Docker mode compatibility Tests cover all modified files: - utils/file_utils.py @@ -21,8 +23,10 @@ Tests cover all modified files: - simulator_tests/base_test.py - run_integration_tests.sh - code_quality_checks.sh +- tests/test_conversation_file_features.py (Docker mode) """ +import os import sys import tempfile from pathlib import Path @@ -376,6 +380,163 @@ def test_shell_scripts_windows_support(): return False +def test_docker_path_validation(): + """Test 8: Docker path validation in file_utils.py.""" + print("\n๐Ÿงช Test 8: Docker path validation") + print("-" * 60) + + try: + # Test that path_detector import is available + try: + from utils.path_detector import PathModeDetector + + detector_import = True + print(" โœ… Path detector import: True") + except ImportError as e: + detector_import = False + print(f" โŒ Path detector import: False ({e})") + + # Test Docker path validation with mocked Docker mode + try: + # Mock Docker mode + with patch.dict(os.environ, {"MCP_FILE_PATH_MODE": "docker"}): + # Reset singleton to pick up new environment + PathModeDetector._instance = None + + # Test Docker path validation + docker_path = "/app/project/test.py" + try: + from utils.file_utils import resolve_and_validate_path + + resolve_and_validate_path(docker_path) + docker_validation = True + print(" โœ… Docker path validation: True") + except ValueError as e: + if "Relative paths are not supported" in str(e): + docker_validation = False + print(" โŒ Docker path validation: False (still rejected)") + else: + docker_validation = True # Different error, not path format + print(" โœ… Docker path validation: True (security error)") + except Exception as e: + docker_validation = False + print(f" โŒ Docker path validation: Error ({e})") + + # Reset singleton after test + PathModeDetector._instance = None + + except Exception as e: + docker_validation = False + print(f" โŒ Docker path test error: {e}") + + # Test that file_utils.py contains Docker-related code + try: + with open("utils/file_utils.py", encoding="utf-8") as f: + file_utils_content = f.read() + + has_detector_import = "from utils.path_detector import get_path_detector" in file_utils_content + has_docker_check = "is_docker_path = converted_path_str.startswith" in file_utils_content + has_docker_mode = "is_docker_mode" in file_utils_content + + print(f" โœ… Has detector import: {has_detector_import}") + print(f" โœ… Has Docker path check: {has_docker_check}") + print(f" โœ… Has Docker mode check: {has_docker_mode}") + + file_content_ok = has_detector_import and has_docker_check and has_docker_mode + + except FileNotFoundError: + file_content_ok = False + print(" โŒ utils/file_utils.py not found") + + success = detector_import and docker_validation and file_content_ok + print(f"\nResult: Docker path validation {'passed' if success else 'failed'}") + + return success + + except Exception as e: + print(f" โŒ Error testing Docker path validation: {e}") + print("\nResult: Docker path validation failed") + return False + + +def test_conversation_docker_compatibility(): + """Test 9: Conversation tests Docker mode compatibility.""" + print("\n๐Ÿงช Test 9: Conversation tests Docker mode compatibility") + print("-" * 60) + + try: + # Test that test_conversation_file_features.py contains Docker fixes + try: + with open("tests/test_conversation_file_features.py", encoding="utf-8") as f: + test_content = f.read() + + has_local_mode = '"MCP_FILE_PATH_MODE": "local"' in test_content + has_detector_reset = "PathModeDetector._instance = None" in test_content + has_cache_reset = "detector._cached_mode = None" in test_content + has_subdir = "zen-mcp-server" in test_content + + print(f" โœ… Forces local mode: {has_local_mode}") + print(f" โœ… Resets PathModeDetector: {has_detector_reset}") + print(f" โœ… Resets detector cache: {has_cache_reset}") + print(f" โœ… Uses project subdirectory: {has_subdir}") + + test_content_ok = has_local_mode and has_detector_reset and has_cache_reset + + except FileNotFoundError: + test_content_ok = False + print(" โŒ tests/test_conversation_file_features.py not found") + + # Test PathModeDetector cache reset functionality + try: + from utils.path_detector import PathModeDetector, get_path_detector + + # Test that we can reset the singleton + detector1 = get_path_detector() + detector1.get_path_mode() + + # Reset and test again + PathModeDetector._instance = None + detector2 = get_path_detector() + + # Test cache reset + detector2._cached_mode = None + detector2.get_path_mode() + + reset_works = True + print(" โœ… PathModeDetector reset: True") + + except Exception as e: + reset_works = False + print(f" โŒ PathModeDetector reset: False ({e})") + + # Test environment patching works + try: + from utils.path_detector import get_path_detector + + with patch.dict(os.environ, {"MCP_FILE_PATH_MODE": "local"}): + PathModeDetector._instance = None + detector = get_path_detector() + detector._cached_mode = None + mode = detector.get_path_mode() + + env_patch_works = mode == "local" + print(f" โœ… Environment patching: {env_patch_works}") + + except Exception as e: + env_patch_works = False + print(f" โŒ Environment patching: False ({e})") + + success = test_content_ok and reset_works and env_patch_works + print(f"\nResult: Conversation Docker compatibility {'passed' if success else 'failed'}") + + return success + + except Exception as e: + print(f" โŒ Error testing conversation Docker compatibility: {e}") + print("\nResult: Conversation Docker compatibility failed") + return False + + def main(): """Main validation function.""" print("๐Ÿ”ง Final validation of cross-platform fixes") @@ -393,6 +554,8 @@ def main(): results.append(("Communication simulator", test_communication_simulator_fixes())) results.append(("BaseSimulatorTest", test_base_simulator_test_fixes())) results.append(("Shell scripts Windows support", test_shell_scripts_windows_support())) + results.append(("Docker path validation", test_docker_path_validation())) + results.append(("Conversation Docker compatibility", test_conversation_docker_compatibility())) # Final summary print("\n" + "=" * 70) diff --git a/run-server.ps1 b/run-server.ps1 index 80fd2c0..e145352 100644 --- a/run-server.ps1 +++ b/run-server.ps1 @@ -1,4 +1,81 @@ -#!/usr/bin/env pwsh +<# +.SYNOPSIS + Installation, configuration, and launch script for Zen MCP server on Windows. + +.DESCRIPTION + This PowerShell script prepares the environment for the Zen MCP server: + - Installs and checks Python 3.10+ (with venv or uv if available) + - Installs required Python dependencies + - Configures environment files (.env) + - Validates presence of required API keys + - Cleans Python caches and obsolete Docker artifacts + - Offers automatic integration with Claude Desktop, Gemini CLI, VSCode, Cursor, Windsurf, and Trae + - Manages configuration file backups (max 3 retained) + - Allows real-time log following or server launch + +.PARAMETER Help + Shows script help. + +.PARAMETER Version + Shows Zen MCP server version. + +.PARAMETER Follow + Follows server logs in real time. + +.PARAMETER Config + Shows configuration instructions for Claude and other compatible clients. + +.PARAMETER ClearCache + Removes Python cache files (__pycache__, .pyc). + +.PARAMETER SkipVenv + Skips Python virtual environment creation. + +.PARAMETER SkipDocker + Skips Docker checks and cleanup. + +.PARAMETER Force + Forces recreation of the Python virtual environment. + +.PARAMETER VerboseOutput + Enables more detailed output (currently unused). + +.PARAMETER Dev + Installs development dependencies from requirements-dev.txt if available. + +.PARAMETER Docker + Uses Docker to build and run the MCP server instead of Python virtual environment. + +.EXAMPLE + .\run-server.ps1 + Prepares the environment and starts the Zen MCP server. + + .\run-server.ps1 -Follow + Follows server logs in real time. + + .\run-server.ps1 -Config + Shows configuration instructions for clients. + + .\run-server.ps1 -Dev + Prepares the environment with development dependencies and starts the server. + + .\run-server.ps1 -Docker + Builds and runs the server using Docker containers. + + .\run-server.ps1 -Docker -Follow + Builds and runs the server using Docker containers and follows the logs. + + .\run-server.ps1 -Docker -Force + Forces rebuilding of the Docker image and runs the server. + +.NOTES + Project Author : BeehiveInnovations + Script Author : GiGiDKR (https://github.com/GiGiDKR) + Date : 07-05-2025 + Version : See config.py (__version__) + References : https://github.com/BeehiveInnovations/zen-mcp-server + +#> #Requires -Version 5.1 [CmdletBinding()] param( @@ -10,11 +87,13 @@ param( [switch]$SkipVenv, [switch]$SkipDocker, [switch]$Force, - [switch]$VerboseOutput + [switch]$VerboseOutput, + [switch]$Dev, + [switch]$Docker ) # ============================================================================ -# Zen MCP Server Setup Script for Windows PowerShell +# Zen MCP Server Setup Script for Windows # # A Windows-compatible setup script that handles environment setup, # dependency installation, and configuration. @@ -126,6 +205,55 @@ function Remove-LockedDirectory { } } +# Manage configuration file backups with maximum 3 files retention +function Manage-ConfigBackups { + param( + [string]$ConfigFilePath, + [int]$MaxBackups = 3 + ) + + if (!(Test-Path $ConfigFilePath)) { + Write-Warning "Configuration file not found: $ConfigFilePath" + return $null + } + + try { + # Create new backup with timestamp + $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' + $backupPath = "$ConfigFilePath.backup_$timestamp" + Copy-Item $ConfigFilePath $backupPath -ErrorAction Stop + + # Find all existing backups for this config file + $configDir = Split-Path $ConfigFilePath -Parent + $configFileName = Split-Path $ConfigFilePath -Leaf + $backupPattern = "$configFileName.backup_*" + + $existingBackups = Get-ChildItem -Path $configDir -Filter $backupPattern -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending + + # Keep only the most recent MaxBackups files + if ($existingBackups.Count -gt $MaxBackups) { + $backupsToRemove = $existingBackups | Select-Object -Skip $MaxBackups + foreach ($backup in $backupsToRemove) { + try { + Remove-Item $backup.FullName -Force -ErrorAction Stop + Write-Info "Removed old backup: $($backup.Name)" + } catch { + Write-Warning "Could not remove old backup: $($backup.Name)" + } + } + Write-Success "Backup retention: kept $MaxBackups most recent backups" + } + + Write-Success "Backup created: $(Split-Path $backupPath -Leaf)" + return $backupPath + + } catch { + Write-Warning "Failed to create backup: $_" + return $null + } +} + # Get version from config.py function Get-Version { try { @@ -160,6 +288,19 @@ function Clear-PythonCache { } } +# Get absolute path +function Get-AbsolutePath { + param([string]$Path) + + if (Test-Path $Path) { + # Use Resolve-Path for full resolution + return Resolve-Path $Path + } else { + # Use unresolved method + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + } +} + # Check Python version function Test-PythonVersion { param([string]$PythonCmd) @@ -356,7 +497,7 @@ function Initialize-Environment { Write-Success "Virtual environment already exists" $pythonPath = "$VENV_PATH\Scripts\python.exe" if (Test-Path $pythonPath) { - return $pythonPath + return Get-AbsolutePath $pythonPath } } } @@ -365,15 +506,8 @@ function Initialize-Environment { Write-Info "Creating virtual environment with uv..." uv venv $VENV_PATH --python 3.12 if ($LASTEXITCODE -eq 0) { - # Install pip in the uv environment for compatibility - Write-Info "Installing pip in uv environment..." - uv pip install --python "$VENV_PATH\Scripts\python.exe" pip - if ($LASTEXITCODE -eq 0) { - Write-Success "Environment created with uv (pip installed)" - } else { - Write-Success "Environment created with uv" - } - return "$VENV_PATH\Scripts\python.exe" + Write-Success "Environment created with uv" + return Get-AbsolutePath "$VENV_PATH\Scripts\python.exe" } } catch { Write-Warning "uv failed, falling back to venv" @@ -415,7 +549,7 @@ function Initialize-Environment { } } else { Write-Success "Virtual environment already exists" - return "$VENV_PATH\Scripts\python.exe" + return Get-AbsolutePath "$VENV_PATH\Scripts\python.exe" } } @@ -431,7 +565,7 @@ function Initialize-Environment { } Write-Success "Virtual environment created" - return "$VENV_PATH\Scripts\python.exe" + return Get-AbsolutePath "$VENV_PATH\Scripts\python.exe" } # Setup virtual environment (legacy function for compatibility) @@ -503,110 +637,285 @@ function Initialize-VirtualEnvironment { } } -# Install dependencies function +# Install dependencies function - Simplified uv-first approach function Install-Dependencies { - param([string]$PythonPath = "") + param( + [Parameter(Mandatory=$true)] + [string]$PythonPath, + [switch]$InstallDevDependencies = $false + ) - if ($PythonPath -eq "" -or $args.Count -eq 0) { - # Legacy call without parameters - $pipCmd = if (Test-Path "$VENV_PATH\Scripts\pip.exe") { - "$VENV_PATH\Scripts\pip.exe" - } elseif (Test-Command "pip") { - "pip" - } else { - Write-Error "pip not found" - exit 1 - } - - Write-Step "Installing Dependencies" - Write-Info "Installing Python dependencies..." - - try { - # Install main dependencies - & $pipCmd install -r requirements.txt - if ($LASTEXITCODE -ne 0) { - throw "Failed to install main dependencies" - } - - # Install dev dependencies if file exists - if (Test-Path "requirements-dev.txt") { - & $pipCmd install -r requirements-dev.txt - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to install dev dependencies, continuing..." - } else { - Write-Success "Development dependencies installed" - } - } - - Write-Success "Dependencies installed successfully" - } catch { - Write-Error "Failed to install dependencies: $_" - exit 1 - } - return - } - - # Version with parameter - use uv or pip Write-Step "Installing Dependencies" - - # Try uv first - if (Test-Uv) { - Write-Info "Installing dependencies with uv..." - try { - # Install in the virtual environment - uv pip install --python "$VENV_PATH\Scripts\python.exe" -r requirements.txt - if ($LASTEXITCODE -eq 0) { - # Also install dev dependencies if available - if (Test-Path "requirements-dev.txt") { - uv pip install --python "$VENV_PATH\Scripts\python.exe" -r requirements-dev.txt - if ($LASTEXITCODE -eq 0) { - Write-Success "Development dependencies installed with uv" - } else { - Write-Warning "Failed to install dev dependencies with uv, continuing..." - } - } - Write-Success "Dependencies installed with uv" - return - } - } catch { - Write-Warning "uv install failed, falling back to pip" + + # Build requirements files list + $requirementsFiles = @("requirements.txt") + if ($InstallDevDependencies) { + if (Test-Path "requirements-dev.txt") { + $requirementsFiles += "requirements-dev.txt" + Write-Info "Including development dependencies from requirements-dev.txt" + } else { + Write-Warning "Development dependencies requested but requirements-dev.txt not found" } } - - # Fallback to pip - $pipCmd = "$VENV_PATH\Scripts\pip.exe" - if (!(Test-Path $pipCmd)) { - $pipCmd = "pip" + + # Try uv first for faster package management + $useUv = Test-Uv + if ($useUv) { + Write-Info "Installing dependencies with uv (fast)..." + try { + foreach ($file in $requirementsFiles) { + Write-Info "Installing from $file with uv..." + uv pip install -r $file --python $PythonPath + if ($LASTEXITCODE -ne 0) { + throw "uv failed to install $file" + } + } + Write-Success "Dependencies installed successfully with uv" + return + } catch { + Write-Warning "uv installation failed: $_. Falling back to pip" + $useUv = $false + } } - + + # Fallback to pip Write-Info "Installing dependencies with pip..." + $pipCmd = Join-Path (Split-Path $PythonPath -Parent) "pip.exe" - # Upgrade pip first try { - & $pipCmd install --upgrade pip + # Upgrade pip first + & $pipCmd install --upgrade pip | Out-Null } catch { Write-Warning "Could not upgrade pip, continuing..." } + + try { + foreach ($file in $requirementsFiles) { + Write-Info "Installing from $file with pip..." + & $pipCmd install -r $file + if ($LASTEXITCODE -ne 0) { + throw "pip failed to install $file" + } + } + Write-Success "Dependencies installed successfully with pip" + } catch { + Write-Error "Failed to install dependencies with pip: $_" + exit 1 + } +} + +# ---------------------------------------------------------------------------- +# Docker Functions +# ============================================================================ + +# Test Docker availability and requirements +function Test-DockerRequirements { + Write-Step "Checking Docker Requirements" - # Install main dependencies - & $pipCmd install -r requirements.txt - if ($LASTEXITCODE -ne 0) { - throw "Failed to install main dependencies" + if (!(Test-Command "docker")) { + Write-Error "Docker not found. Please install Docker Desktop from https://docker.com" + return $false } - # Install dev dependencies if file exists - if (Test-Path "requirements-dev.txt") { - & $pipCmd install -r requirements-dev.txt - if ($LASTEXITCODE -eq 0) { - Write-Success "Development dependencies installed" - } else { - Write-Warning "Failed to install dev dependencies, continuing..." + try { + $null = docker version 2>$null + Write-Success "Docker is installed and running" + } catch { + Write-Error "Docker is installed but not running. Please start Docker Desktop." + return $false + } + + if (!(Test-Command "docker-compose")) { + Write-Warning "docker-compose not found. Trying docker compose..." + try { + $null = docker compose version 2>$null + Write-Success "Docker Compose (v2) is available" + return $true + } catch { + Write-Error "Docker Compose not found. Please install Docker Compose." + return $false + } + } else { + Write-Success "Docker Compose is available" + return $true + } +} + +# Build Docker image +function Build-DockerImage { + param([switch]$Force = $false) + + Write-Step "Building Docker Image" + + # Check if image exists + try { + $imageExists = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object { $_ -eq "zen-mcp-server:latest" } + if ($imageExists -and !$Force) { + Write-Success "Docker image already exists. Use -Force to rebuild." + return $true + } + } catch { + # Continue if command fails + } + + if ($Force -and $imageExists) { + Write-Info "Forcing rebuild of Docker image..." + try { + docker rmi zen-mcp-server:latest 2>$null + } catch { + Write-Warning "Could not remove existing image, continuing..." } } - Write-Success "Dependencies installed successfully" + Write-Info "Building Docker image from Dockerfile..." + try { + $buildArgs = @() + if ($Dev) { + # For development builds, we could add specific build args + Write-Info "Building with development support..." + } + + docker build -t zen-mcp-server:latest . + if ($LASTEXITCODE -ne 0) { + throw "Docker build failed" + } + + Write-Success "Docker image built successfully" + return $true + } catch { + Write-Error "Failed to build Docker image: $_" + return $false + } } +# Prepare Docker environment file +function Initialize-DockerEnvironment { + Write-Step "Preparing Docker Environment" + + # Ensure .env file exists + if (!(Test-Path ".env")) { + Write-Warning "No .env file found. Creating default .env file..." + + $defaultEnv = @" +# API Keys - Replace with your actual keys +GEMINI_API_KEY=your_gemini_api_key_here +GOOGLE_API_KEY=your_google_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +ANTHROPIC_API_KEY=your_anthropic_api_key_here +XAI_API_KEY=your_xai_api_key_here +DIAL_API_KEY=your_dial_api_key_here +DIAL_API_HOST=your_dial_api_host_here +DIAL_API_VERSION=your_dial_api_version_here +OPENROUTER_API_KEY=your_openrouter_api_key_here +CUSTOM_API_URL=your_custom_api_url_here +CUSTOM_API_KEY=your_custom_api_key_here +CUSTOM_MODEL_NAME=your_custom_model_name_here + +# Server Configuration +DEFAULT_MODEL=auto +LOG_LEVEL=INFO +LOG_MAX_SIZE=10MB +LOG_BACKUP_COUNT=5 +DEFAULT_THINKING_MODE_THINKDEEP=high + +# Optional Advanced Settings +#DISABLED_TOOLS= +#MAX_MCP_OUTPUT_TOKENS= +#TZ=UTC +"@ + + $defaultEnv | Out-File -FilePath ".env" -Encoding UTF8 + Write-Success "Default .env file created" + Write-Warning "Please edit .env file with your actual API keys" + } else { + Write-Success ".env file exists" + } + + # Create logs directory for volume mount + Initialize-Logging + + return $true +} + +# Start Docker services +function Start-DockerServices { + param([switch]$Follow = $false) + + Write-Step "Starting Docker Services" + + # Check if docker-compose.yml exists + if (!(Test-Path "docker-compose.yml")) { + Write-Error "docker-compose.yml not found in current directory" + return $false + } + + try { + # Stop any existing services + Write-Info "Stopping any existing services..." + if (Test-Command "docker-compose") { + docker-compose down 2>$null + } else { + docker compose down 2>$null + } + + # Start services + Write-Info "Starting Zen MCP Server with Docker Compose..." + if (Test-Command "docker-compose") { + if ($Follow) { + docker-compose up --build + } else { + docker-compose up -d --build + } + } else { + if ($Follow) { + docker compose up --build + } else { + docker compose up -d --build + } + } + + if ($LASTEXITCODE -ne 0) { + throw "Failed to start Docker services" + } + + if (!$Follow) { + Write-Success "Docker services started successfully" + Write-Info "Container name: zen-mcp-server" + Write-Host "" + Write-Host "To view logs: " -NoNewline + Write-Host "docker logs -f zen-mcp-server" -ForegroundColor Yellow + Write-Host "To stop: " -NoNewline + Write-Host "docker-compose down" -ForegroundColor Yellow + } + + return $true + } catch { + Write-Error "Failed to start Docker services: $_" + return $false + } +} + +# Get Docker container status +function Get-DockerStatus { + try { + $containerStatus = docker ps --filter "name=zen-mcp-server" --format "{{.Status}}" + if ($containerStatus) { + Write-Success "Container status: $containerStatus" + return $true + } else { + Write-Warning "Container not running" + return $false + } + } catch { + Write-Warning "Could not get container status: $_" + return $false + } +} + +# ============================================================================ +# End Docker Functions +# ============================================================================ + # Setup logging directory function Initialize-Logging { Write-Step "Setting up Logging" @@ -646,119 +955,464 @@ function Test-Docker { } } -# Check Claude Desktop integration with full functionality like Bash version -function Test-ClaudeDesktopIntegration { - param([string]$PythonPath, [string]$ServerPath) - - # Skip if already configured (check flag) - if (Test-Path $DESKTOP_CONFIG_FLAG) { - return +# ---------------------------------------------------------------------------- +# MCP Client Configuration System +# ---------------------------------------------------------------------------- + +# Centralized MCP client definitions +$script:McpClientDefinitions = @( + @{ + Name = "Claude Desktop" + DetectionPath = "$env:APPDATA\Claude\claude_desktop_config.json" + DetectionType = "Path" + ConfigPath = "$env:APPDATA\Claude\claude_desktop_config.json" + ConfigJsonPath = "mcpServers.zen" + NeedsConfigDir = $true + }, + @{ + Name = "VSCode" + DetectionCommand = "code" + DetectionType = "Command" + ConfigPath = "$env:APPDATA\Code\User\settings.json" + ConfigJsonPath = "mcp.servers.zen" + IsVSCode = $true + }, + @{ + Name = "VSCode Insiders" + DetectionCommand = "code-insiders" + DetectionType = "Command" + ConfigPath = "$env:APPDATA\Code - Insiders\User\mcp.json" + ConfigJsonPath = "servers.zen" + IsVSCodeInsiders = $true + }, + @{ + Name = "Cursor" + DetectionCommand = "cursor" + DetectionType = "Command" + ConfigPath = "$env:USERPROFILE\.cursor\mcp.json" + ConfigJsonPath = "mcpServers.zen" + }, + @{ + Name = "Windsurf" + DetectionPath = "$env:USERPROFILE\.codeium\windsurf" + DetectionType = "Path" + ConfigPath = "$env:USERPROFILE\.codeium\windsurf\mcp_config.json" + ConfigJsonPath = "mcpServers.zen" + }, + @{ + Name = "Trae" + DetectionPath = "$env:APPDATA\Trae" + DetectionType = "Path" + ConfigPath = "$env:APPDATA\Trae\User\mcp.json" + ConfigJsonPath = "mcpServers.zen" } - - Write-Step "Checking Claude Desktop Integration" - - $claudeConfigPath = "$env:APPDATA\Claude\claude_desktop_config.json" - - if (!(Test-Path $claudeConfigPath)) { - Write-Warning "Claude Desktop config not found at: $claudeConfigPath" - Write-Info "Please install Claude Desktop first" - Write-Host "" - Write-Host "To configure manually, add this to your Claude Desktop config:" - Write-Host @" -{ - "mcpServers": { - "zen": { - "command": "$PythonPath", - "args": ["$ServerPath"] - } - } +) + +# Docker MCP configuration template (legacy, kept for backward compatibility) +$script:DockerMcpConfig = @{ + command = "docker" + args = @("exec", "-i", "zen-mcp-server", "python", "server.py") + type = "stdio" } -"@ -ForegroundColor Yellow - return - } + +# Generate Docker MCP configuration using docker run (recommended for all clients) +function Get-DockerMcpConfigRun { + param([string]$ServerPath) - Write-Host "" - $response = Read-Host "Configure Zen for Claude Desktop? (Y/n)" - if ($response -eq 'n' -or $response -eq 'N') { - Write-Info "Skipping Claude Desktop integration" - New-Item -Path $DESKTOP_CONFIG_FLAG -ItemType File -Force | Out-Null - return - } + $scriptDir = Split-Path $ServerPath -Parent + $envFile = Join-Path $scriptDir ".env" - # Create config directory if it doesn't exist - $configDir = Split-Path $claudeConfigPath -Parent - if (!(Test-Path $configDir)) { - New-Item -ItemType Directory -Path $configDir -Force | Out-Null - } - - try { - $config = @{} - - # Handle existing config - if (Test-Path $claudeConfigPath) { - Write-Info "Updating existing Claude Desktop config..." - - # Create backup - $backupPath = "$claudeConfigPath.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" - Copy-Item $claudeConfigPath $backupPath - - # Read existing config - $existingContent = Get-Content $claudeConfigPath -Raw - $config = $existingContent | ConvertFrom-Json - - # Check for old Docker config and remove it - if ($existingContent -match "docker.*compose.*zen|zen.*docker") { - Write-Warning "Removing old Docker-based MCP configuration..." - if ($config.mcpServers -and $config.mcpServers.zen) { - $config.mcpServers.PSObject.Properties.Remove('zen') - Write-Success "Removed old zen MCP configuration" - } - } - } else { - Write-Info "Creating new Claude Desktop config..." - } - - # Ensure mcpServers exists - if (!$config.mcpServers) { - $config | Add-Member -MemberType NoteProperty -Name "mcpServers" -Value @{} -Force - } - - # Add zen server configuration - $serverConfig = @{ - command = $PythonPath - args = @($ServerPath) - } - - $config.mcpServers | Add-Member -MemberType NoteProperty -Name "zen" -Value $serverConfig -Force - - # Write updated config - $config | ConvertTo-Json -Depth 10 | Out-File $claudeConfigPath -Encoding UTF8 - - Write-Success "Successfully configured Claude Desktop" - Write-Host " Config: $claudeConfigPath" -ForegroundColor Gray - Write-Host " Restart Claude Desktop to use the new MCP server" -ForegroundColor Gray - New-Item -Path $DESKTOP_CONFIG_FLAG -ItemType File -Force | Out-Null - - } catch { - Write-Error "Failed to update Claude Desktop config: $_" - Write-Host "" - Write-Host "Manual configuration:" - Write-Host "Location: $claudeConfigPath" - Write-Host "Add this configuration:" - Write-Host @" -{ - "mcpServers": { - "zen": { - "command": "$PythonPath", - "args": ["$ServerPath"] - } - } -} -"@ -ForegroundColor Yellow + return @{ + command = "docker" + args = @("run", "--rm", "-i", "--env-file", $envFile, "zen-mcp-server:latest", "python", "server.py") + type = "stdio" } } -# Check Claude CLI integration +# Generate Python MCP configuration +function Get-PythonMcpConfig { + param([string]$PythonPath, [string]$ServerPath) + return @{ + command = $PythonPath + args = @($ServerPath) + type = "stdio" + } +} + +# Check if client uses mcp.json format with servers structure +function Test-McpJsonFormat { + param([hashtable]$Client) + + $configFileName = Split-Path $Client.ConfigPath -Leaf + return $configFileName -eq "mcp.json" +} + +# Check if client uses the new VS Code Insiders format (servers instead of mcpServers) +function Test-VSCodeInsidersFormat { + param([hashtable]$Client) + + return $Client.IsVSCodeInsiders -eq $true -and $Client.ConfigJsonPath -eq "servers.zen" +} + +# Analyze existing MCP configuration to determine type (Python or Docker) +function Get-ExistingMcpConfigType { + param( + [Parameter(Mandatory=$true)] + [hashtable]$Client, + [Parameter(Mandatory=$true)] + [string]$ConfigPath + ) + + if (!(Test-Path $ConfigPath)) { + return @{ + Exists = $false + Type = "None" + Details = "No configuration found" + } + } + + try { + $content = Get-Content $ConfigPath -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue + if (!$content) { + return @{ + Exists = $false + Type = "None" + Details = "Invalid JSON configuration" + } + } + + # Navigate to zen configuration + $pathParts = $Client.ConfigJsonPath.Split('.') + $zenKey = $pathParts[-1] + $parentPath = $pathParts[0..($pathParts.Length - 2)] + + $targetObject = $content + foreach($key in $parentPath) { + if (!$targetObject.PSObject.Properties[$key]) { + return @{ + Exists = $false + Type = "None" + Details = "Configuration structure not found" + } + } + $targetObject = $targetObject.$key + } + + if (!$targetObject.PSObject.Properties[$zenKey]) { + return @{ + Exists = $false + Type = "None" + Details = "Zen configuration not found" + } + } + + $zenConfig = $targetObject.$zenKey + + # Analyze configuration type + if ($zenConfig.command -eq "docker") { + $dockerType = "Unknown" + $details = "Docker configuration" + + if ($zenConfig.args -and $zenConfig.args.Count -gt 0) { + if ($zenConfig.args[0] -eq "run") { + $dockerType = "Docker Run" + $details = "Docker run (dedicated container)" + } elseif ($zenConfig.args[0] -eq "exec") { + $dockerType = "Docker Exec" + $details = "Docker exec (existing container)" + } else { + $details = "Docker ($($zenConfig.args[0]))" + } + } + + return @{ + Exists = $true + Type = "Docker" + SubType = $dockerType + Details = $details + Command = $zenConfig.command + Args = $zenConfig.args + } + } elseif ($zenConfig.command -and $zenConfig.command.EndsWith("python.exe")) { + $pythonType = "Python" + $details = "Python virtual environment" + + if ($zenConfig.command.Contains(".zen_venv")) { + $details = "Python (zen virtual environment)" + } elseif ($zenConfig.command.Contains("venv")) { + $details = "Python (virtual environment)" + } else { + $details = "Python (system installation)" + } + + return @{ + Exists = $true + Type = "Python" + SubType = $pythonType + Details = $details + Command = $zenConfig.command + Args = $zenConfig.args + } + } else { + return @{ + Exists = $true + Type = "Unknown" + Details = "Unknown configuration type: $($zenConfig.command)" + Command = $zenConfig.command + Args = $zenConfig.args + } + } + + } catch { + return @{ + Exists = $false + Type = "Error" + Details = "Error reading configuration: $_" + } + } +} + +# Generic MCP client configuration function +function Configure-McpClient { + param( + [Parameter(Mandatory=$true)] + [hashtable]$Client, + [Parameter(Mandatory=$true)] + [bool]$UseDocker, + [string]$PythonPath = "", + [string]$ServerPath = "" + ) + + Write-Step "Checking $($Client.Name) Integration" + + # Client detection + $detected = $false + if ($Client.DetectionType -eq "Command" -and (Test-Command $Client.DetectionCommand)) { + $detected = $true + } elseif ($Client.DetectionType -eq "Path" -and (Test-Path ($Client.DetectionPath -as [string]))) { + $detected = $true + } + + if (!$detected) { + Write-Info "$($Client.Name) not detected - skipping integration" + return + } + Write-Info "Found $($Client.Name)" + + # Handle VSCode special logic for profiles + $configPath = $Client.ConfigPath + if ($Client.IsVSCode) { + $userPath = Split-Path $configPath -Parent + if (!(Test-Path $userPath)) { + Write-Warning "$($Client.Name) user directory not found. Skipping." + return + } + + # Find most recent settings.json (default or profile) + $settingsFiles = @() + $defaultSettings = $configPath + if (Test-Path $defaultSettings) { + $settingsFiles += @{ + Path = $defaultSettings + LastModified = (Get-Item $defaultSettings).LastWriteTime + } + } + + $profilesPath = Join-Path $userPath "profiles" + if (Test-Path $profilesPath) { + Get-ChildItem $profilesPath -Directory | ForEach-Object { + $profileSettings = Join-Path $_.FullName "settings.json" + if (Test-Path $profileSettings) { + $settingsFiles += @{ + Path = $profileSettings + LastModified = (Get-Item $profileSettings).LastWriteTime + } + } + } + } + + if ($settingsFiles.Count -gt 0) { + $configPath = ($settingsFiles | Sort-Object LastModified -Descending | Select-Object -First 1).Path + } + } + + # Handle VSCode Insiders special logic for profiles (uses mcp.json) + if ($Client.IsVSCodeInsiders) { + $userPath = Split-Path $configPath -Parent + if (!(Test-Path $userPath)) { + Write-Warning "$($Client.Name) user directory not found. Skipping." + return + } + + # Find most recent mcp.json (default or profile) + $mcpFiles = @() + $defaultMcp = $configPath + if (Test-Path $defaultMcp) { + $mcpFiles += @{ + Path = $defaultMcp + LastModified = (Get-Item $defaultMcp).LastWriteTime + } + } + + $profilesPath = Join-Path $userPath "profiles" + if (Test-Path $profilesPath) { + Get-ChildItem $profilesPath -Directory | ForEach-Object { + $profileMcp = Join-Path $_.FullName "mcp.json" + if (Test-Path $profileMcp) { + $mcpFiles += @{ + Path = $profileMcp + LastModified = (Get-Item $profileMcp).LastWriteTime + } + } + } + } + + if ($mcpFiles.Count -gt 0) { + $configPath = ($mcpFiles | Sort-Object LastModified -Descending | Select-Object -First 1).Path + } + } + + # Check if already configured and analyze existing configuration + $existingConfig = Get-ExistingMcpConfigType -Client $Client -ConfigPath $configPath + $newConfigType = if ($UseDocker) { "Docker" } else { "Python" } + + if ($existingConfig.Exists) { + Write-Info "Found existing Zen MCP configuration in $($Client.Name)" + Write-Info " Current: $($existingConfig.Details)" + Write-Info " New: $newConfigType configuration" + + if ($existingConfig.Type -eq $newConfigType) { + Write-Warning "Same configuration type ($($existingConfig.Type)) already exists" + $response = Read-Host "`nOverwrite existing $($existingConfig.Type) configuration? (y/N)" + } else { + Write-Warning "Different configuration type detected" + Write-Info " Replacing: $($existingConfig.Type) โ†’ $newConfigType" + $response = Read-Host "`nReplace $($existingConfig.Type) with $newConfigType configuration? (y/N)" + } + + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Info "Keeping existing configuration in $($Client.Name)" + return + } + + Write-Info "Proceeding with configuration update..." + } else { + # User confirmation for new installation + $response = Read-Host "`nConfigure Zen MCP for $($Client.Name) (mode: $newConfigType)? (y/N)" + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Info "Skipping $($Client.Name) integration" + return + } + } + + try { + # Create config directory if needed + $configDir = Split-Path $configPath -Parent + if (!(Test-Path $configDir)) { + New-Item -ItemType Directory -Path $configDir -Force | Out-Null + } + + # Backup existing config + if (Test-Path $configPath) { + Manage-ConfigBackups -ConfigFilePath $configPath + } + + # Read or create config + $config = New-Object PSObject + $usesMcpJsonFormat = Test-McpJsonFormat -Client $Client + $usesVSCodeInsidersFormat = Test-VSCodeInsidersFormat -Client $Client + + if (Test-Path $configPath) { + $fileContent = Get-Content $configPath -Raw + if ($fileContent.Trim()) { + $config = $fileContent | ConvertFrom-Json -ErrorAction SilentlyContinue + } + if ($null -eq $config) { $config = New-Object PSObject } + } + + # Initialize structure for mcp.json format files if they don't exist or are empty + if ($usesMcpJsonFormat) { + if ($usesVSCodeInsidersFormat) { + # For VS Code Insiders format: {"servers": {...}} + if (!$config.PSObject.Properties["servers"]) { + $config | Add-Member -MemberType NoteProperty -Name "servers" -Value (New-Object PSObject) + } + } else { + # For other clients format: {"mcpServers": {...}} + if (!$config.PSObject.Properties["mcpServers"]) { + $config | Add-Member -MemberType NoteProperty -Name "mcpServers" -Value (New-Object PSObject) + } + } + } + + # Initialize MCP structure for VS Code settings.json if it doesn't exist + if ($Client.IsVSCode -and $Client.ConfigJsonPath.StartsWith("mcp.")) { + if (!$config.PSObject.Properties["mcp"]) { + $config | Add-Member -MemberType NoteProperty -Name "mcp" -Value (New-Object PSObject) + } + if (!$config.mcp.PSObject.Properties["servers"]) { + $config.mcp | Add-Member -MemberType NoteProperty -Name "servers" -Value (New-Object PSObject) + } + } + + # Generate server config + $serverConfig = if ($UseDocker) { + # Use docker run for all clients (more reliable than docker exec) + Get-DockerMcpConfigRun $ServerPath + } else { + Get-PythonMcpConfig $PythonPath $ServerPath + } + + # Navigate and set configuration + $pathParts = $Client.ConfigJsonPath.Split('.') + $zenKey = $pathParts[-1] + $parentPath = $pathParts[0..($pathParts.Length - 2)] + + $targetObject = $config + foreach($key in $parentPath) { + if (!$targetObject.PSObject.Properties[$key]) { + $targetObject | Add-Member -MemberType NoteProperty -Name $key -Value (New-Object PSObject) + } + $targetObject = $targetObject.$key + } + + $targetObject | Add-Member -MemberType NoteProperty -Name $zenKey -Value $serverConfig -Force + + # Write config + $config | ConvertTo-Json -Depth 10 | Out-File $configPath -Encoding UTF8 + Write-Success "Successfully configured $($Client.Name)" + Write-Host " Config: $configPath" -ForegroundColor Gray + Write-Host " Restart $($Client.Name) to use the new MCP server" -ForegroundColor Gray + + } catch { + Write-Error "Failed to update $($Client.Name) configuration: $_" + } +} + +# Main MCP client configuration orchestrator +function Invoke-McpClientConfiguration { + param( + [Parameter(Mandatory=$true)] + [bool]$UseDocker, + [string]$PythonPath = "", + [string]$ServerPath = "" + ) + + Write-Step "Checking Client Integrations" + + # Configure GUI clients + foreach ($client in $script:McpClientDefinitions) { + Configure-McpClient -Client $client -UseDocker $UseDocker -PythonPath $PythonPath -ServerPath $ServerPath + } + + # Handle CLI tools separately (they don't follow JSON config pattern) + if (!$UseDocker) { + Test-ClaudeCliIntegration $PythonPath $ServerPath + Test-GeminiCliIntegration (Split-Path $ServerPath -Parent) + } +} + +# Keep existing CLI integration functions function Test-ClaudeCliIntegration { param([string]$PythonPath, [string]$ServerPath) @@ -782,7 +1436,6 @@ function Test-ClaudeCliIntegration { } } -# Check and update Gemini CLI configuration function Test-GeminiCliIntegration { param([string]$ScriptDir) @@ -791,21 +1444,19 @@ function Test-GeminiCliIntegration { # Check if Gemini settings file exists (Windows path) $geminiConfig = "$env:USERPROFILE\.gemini\settings.json" if (!(Test-Path $geminiConfig)) { - # Gemini CLI not installed or not configured return } # Check if zen is already configured $configContent = Get-Content $geminiConfig -Raw -ErrorAction SilentlyContinue if ($configContent -and $configContent -match '"zen"') { - # Already configured return } # Ask user if they want to add Zen to Gemini CLI Write-Host "" - $response = Read-Host "Configure Zen for Gemini CLI? (Y/n)" - if ($response -eq 'n' -or $response -eq 'N') { + $response = Read-Host "Configure Zen for Gemini CLI? (y/N)" + if ($response -ne 'y' -and $response -ne 'Y') { Write-Info "Skipping Gemini CLI integration" return } @@ -821,7 +1472,7 @@ if exist ".zen_venv\Scripts\python.exe" ( ) else ( python server.py %* ) -"@ | Out-File -FilePath $zenWrapper -Encoding UTF8 +"@ | Out-File -FilePath $zenWrapper -Encoding ASCII Write-Success "Created zen-mcp-server.cmd wrapper script" } @@ -830,9 +1481,8 @@ if exist ".zen_venv\Scripts\python.exe" ( Write-Info "Updating Gemini CLI configuration..." try { - # Create backup - $backupPath = "$geminiConfig.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" - Copy-Item $geminiConfig $backupPath -ErrorAction SilentlyContinue + # Create backup with retention management + $backupPath = Manage-ConfigBackups $geminiConfig # Read existing config or create new one $config = @{} @@ -876,222 +1526,358 @@ if exist ".zen_venv\Scripts\python.exe" ( } } -# Display configuration instructions -function Show-ConfigInstructions { - param([string]$PythonPath, [string]$ServerPath) - - # Get script directory for Gemini CLI config - $scriptDir = Split-Path $ServerPath -Parent - $zenWrapper = Join-Path $scriptDir "zen-mcp-server.cmd" - - Write-Host "" - Write-Host "===== ZEN MCP SERVER CONFIGURATION =====" -ForegroundColor Cyan - Write-Host "==========================================" -ForegroundColor Cyan - Write-Host "" - Write-Host "To use Zen MCP Server with your Claude clients:" - Write-Host "" - - Write-Info "1. For Claude Desktop:" - Write-Host " Add this configuration to your Claude Desktop config file:" - Write-Host " Location: $env:APPDATA\Claude\claude_desktop_config.json" - Write-Host "" - - $configJson = @{ - mcpServers = @{ - zen = @{ - command = $PythonPath - args = @($ServerPath) - } - } - } | ConvertTo-Json -Depth 5 - - Write-Host $configJson -ForegroundColor Yellow - Write-Host "" - - Write-Info "2. For Gemini CLI:" - Write-Host " Add this configuration to ~/.gemini/settings.json:" - Write-Host " Location: $env:USERPROFILE\.gemini\settings.json" - Write-Host "" - - $geminiConfigJson = @{ - mcpServers = @{ - zen = @{ - command = $zenWrapper - } - } - } | ConvertTo-Json -Depth 5 - - Write-Host $geminiConfigJson -ForegroundColor Yellow - Write-Host "" - - Write-Info "3. Restart Claude Desktop or Gemini CLI after updating the config files" - Write-Host "" - Write-Info "Note: Claude Code (CLI) is not available on Windows (except in WSL2)" - Write-Host "" -} +# ---------------------------------------------------------------------------- +# End MCP Client Configuration System +# ---------------------------------------------------------------------------- -# Follow logs in real-time -function Follow-Logs { - $logPath = Join-Path $LOG_DIR $LOG_FILE - - Write-Host "Following server logs (Ctrl+C to stop)..." -ForegroundColor Yellow - Write-Host "" - - # Create logs directory and file if they don't exist - if (!(Test-Path $LOG_DIR)) { - New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null - } - - if (!(Test-Path $logPath)) { - New-Item -ItemType File -Path $logPath -Force | Out-Null - } - - # Follow the log file using Get-Content -Wait - try { - Get-Content $logPath -Wait - } catch { - Write-Error "Could not follow logs: $_" - } -} +# ---------------------------------------------------------------------------- +# User Interface Functions +# ---------------------------------------------------------------------------- -# Show help message +# Show script help function Show-Help { - $version = Get-Version - Write-Host "" - Write-Host "๐Ÿค– Zen MCP Server v$version" -ForegroundColor Cyan - Write-Host "=============================" -ForegroundColor Cyan - Write-Host "" - Write-Host "Usage: .\run-server.ps1 [OPTIONS]" - Write-Host "" - Write-Host "Options:" - Write-Host " -Help Show this help message" - Write-Host " -Version Show version information" - Write-Host " -Follow Follow server logs in real-time" - Write-Host " -Config Show configuration instructions for Claude clients" - Write-Host " -ClearCache Clear Python cache and exit (helpful for import issues)" - Write-Host " -Force Force recreate virtual environment" - Write-Host " -SkipVenv Skip virtual environment setup" - Write-Host " -SkipDocker Skip Docker checks" - Write-Host "" - Write-Host "Examples:" - Write-Host " .\run-server.ps1 Setup and start the MCP server" - Write-Host " .\run-server.ps1 -Follow Setup and follow logs" - Write-Host " .\run-server.ps1 -Config Show configuration instructions" - Write-Host " .\run-server.ps1 -Version Show version only" - Write-Host " .\run-server.ps1 -ClearCache Clear Python cache (fixes import issues)" - Write-Host "" - Write-Host "For more information, visit:" - Write-Host " https://github.com/BeehiveInnovations/zen-mcp-server" - Write-Host "" + Write-Host @" +Zen MCP Server - Setup and Launch Script + +USAGE: + .\run-server.ps1 [OPTIONS] + +OPTIONS: + -Help Show this help message + -Version Show version information + -Follow Follow server logs in real time + -Config Show configuration instructions for MCP clients + -ClearCache Clear Python cache files and exit + -Force Force recreation of Python virtual environment + -Dev Install development dependencies from requirements-dev.txt + -Docker Use Docker instead of Python virtual environment + -SkipVenv Skip Python virtual environment creation + -SkipDocker Skip Docker checks and cleanup + +EXAMPLES: + .\run-server.ps1 # Normal startup + .\run-server.ps1 -Follow # Start and follow logs + .\run-server.ps1 -Config # Show configuration help + .\run-server.ps1 -Dev # Include development dependencies + .\run-server.ps1 -Docker # Use Docker deployment + .\run-server.ps1 -Docker -Follow # Docker with log following + +For more information, visit: https://github.com/BeehiveInnovations/zen-mcp-server +"@ -ForegroundColor White } -# Show version only +# Show version information function Show-Version { $version = Get-Version - Write-Host $version + Write-Host "Zen MCP Server version: $version" -ForegroundColor Green + Write-Host "PowerShell Setup Script for Windows" -ForegroundColor Cyan + Write-Host "Author: GiGiDKR (https://github.com/GiGiDKR)" -ForegroundColor Gray + Write-Host "Project: BeehiveInnovations/zen-mcp-server" -ForegroundColor Gray } -# Display setup instructions +# Show configuration instructions +function Show-ConfigInstructions { + param( + [string]$PythonPath = "", + [string]$ServerPath = "", + [switch]$UseDocker = $false + ) + + Write-Step "Configuration Instructions" + + if ($UseDocker) { + Write-Host "Docker Configuration:" -ForegroundColor Yellow + Write-Host "The MCP clients have been configured to use Docker containers." -ForegroundColor White + Write-Host "Make sure the Docker container is running with: docker-compose up -d" -ForegroundColor Cyan + Write-Host "" + } else { + Write-Host "Python Virtual Environment Configuration:" -ForegroundColor Yellow + Write-Host "Python Path: $PythonPath" -ForegroundColor Cyan + Write-Host "Server Path: $ServerPath" -ForegroundColor Cyan + Write-Host "" + } + + Write-Host "Supported MCP Clients:" -ForegroundColor Green + Write-Host "โœ“ Claude Desktop" -ForegroundColor White + Write-Host "โœ“ Claude CLI" -ForegroundColor White + Write-Host "โœ“ VSCode (with MCP extension)" -ForegroundColor White + Write-Host "โœ“ VSCode Insiders" -ForegroundColor White + Write-Host "โœ“ Cursor" -ForegroundColor White + Write-Host "โœ“ Windsurf" -ForegroundColor White + Write-Host "โœ“ Trae" -ForegroundColor White + Write-Host "โœ“ Gemini CLI" -ForegroundColor White + Write-Host "" + Write-Host "The script automatically detects and configures compatible clients." -ForegroundColor Gray + Write-Host "Restart your MCP clients after configuration to use the Zen MCP Server." -ForegroundColor Yellow +} + +# Show setup instructions function Show-SetupInstructions { - param([string]$PythonPath, [string]$ServerPath) + param( + [string]$PythonPath = "", + [string]$ServerPath = "", + [switch]$UseDocker = $false + ) + + Write-Step "Setup Complete" + + if ($UseDocker) { + Write-Success "Zen MCP Server is configured for Docker deployment" + Write-Host "Docker command: docker exec -i zen-mcp-server python server.py" -ForegroundColor Cyan + } else { + Write-Success "Zen MCP Server is configured for Python virtual environment" + Write-Host "Python: $PythonPath" -ForegroundColor Cyan + Write-Host "Server: $ServerPath" -ForegroundColor Cyan + } Write-Host "" - Write-Host "===== SETUP COMPLETE =====" -ForegroundColor Green - Write-Host "===========================" -ForegroundColor Green - Write-Host "" - Write-Success "Zen is ready to use!" - Write-Host "" + Write-Host "MCP clients will automatically connect to the server." -ForegroundColor Green + Write-Host "For manual configuration, use the paths shown above." -ForegroundColor Gray } -# Load environment variables from .env file -function Import-EnvFile { - if (Test-Path ".env") { - Get-Content ".env" | ForEach-Object { - if ($_ -match '^([^#][^=]*?)=(.*)$') { - $name = $matches[1].Trim() - $value = $matches[2].Trim() - # Remove quotes if present - $value = $value -replace '^["'']|["'']$', '' - [Environment]::SetEnvironmentVariable($name, $value, "Process") - } - } - Write-Success "Environment variables loaded" +# Start the server +function Start-Server { + Write-Step "Starting Zen MCP Server" + + $pythonPath = "$VENV_PATH\Scripts\python.exe" + if (!(Test-Path $pythonPath)) { + Write-Error "Python virtual environment not found. Please run setup first." + return + } + + $serverPath = "server.py" + if (!(Test-Path $serverPath)) { + Write-Error "Server script not found: $serverPath" + return + } + + try { + Write-Info "Launching server..." + & $pythonPath $serverPath + } catch { + Write-Error "Failed to start server: $_" } } -# Setup environment file +# Follow server logs +function Follow-Logs { + Write-Step "Following Server Logs" + + $logPath = Join-Path $LOG_DIR $LOG_FILE + + if (!(Test-Path $logPath)) { + Write-Warning "Log file not found: $logPath" + Write-Info "Starting server to generate logs..." + Start-Server + return + } + + try { + Write-Info "Following logs at: $logPath" + Write-Host "Press Ctrl+C to stop following logs" + Write-Host "" + Get-Content $logPath -Wait + } catch { + Write-Error "Failed to follow logs: $_" + } +} + +# ---------------------------------------------------------------------------- +# Environment File Management +# ---------------------------------------------------------------------------- + +# Initialize .env file if it doesn't exist function Initialize-EnvFile { - Write-Step "Setting up Environment Configuration" + Write-Step "Setting up Environment File" if (!(Test-Path ".env")) { - if (Test-Path ".env.example") { - Write-Info "Creating .env file from .env.example..." - Copy-Item ".env.example" ".env" - Write-Success ".env file created" - Write-Warning "Please edit .env file with your API keys!" - } else { - Write-Warning ".env.example not found, creating basic .env file" - @" -# Zen MCP Server Configuration -# Add your API keys here - -# Google/Gemini API Key + Write-Info "Creating default .env file..." + @" +# API Keys - Replace with your actual keys GEMINI_API_KEY=your_gemini_api_key_here - -# OpenAI API Key +GOOGLE_API_KEY=your_google_api_key_here OPENAI_API_KEY=your_openai_api_key_here - -# xAI API Key +ANTHROPIC_API_KEY=your_anthropic_api_key_here XAI_API_KEY=your_xai_api_key_here - -# OpenRouter API Key +DIAL_API_KEY=your_dial_api_key_here +DIAL_API_HOST=your_dial_api_host_here +DIAL_API_VERSION=your_dial_api_version_here OPENROUTER_API_KEY=your_openrouter_api_key_here +CUSTOM_API_URL=your_custom_api_url_here +CUSTOM_API_KEY=your_custom_api_key_here +CUSTOM_MODEL_NAME=your_custom_model_name_here -# Logging -LOGGING_LEVEL=INFO +# Server Configuration +DEFAULT_MODEL=auto +LOG_LEVEL=INFO +LOG_MAX_SIZE=10MB +LOG_BACKUP_COUNT=5 +DEFAULT_THINKING_MODE_THINKDEEP=high + +# Optional Advanced Settings +#DISABLED_TOOLS= +#MAX_MCP_OUTPUT_TOKENS= +#TZ=UTC "@ | Out-File -FilePath ".env" -Encoding UTF8 - Write-Success "Basic .env file created" - Write-Warning "Please edit .env file with your actual API keys!" - } + + Write-Success "Default .env file created" + Write-Warning "Please edit .env file with your actual API keys" } else { Write-Success ".env file already exists" } } +# Import environment variables from .env file +function Import-EnvFile { + if (!(Test-Path ".env")) { + Write-Warning "No .env file found" + return + } + + try { + $envContent = Get-Content ".env" -ErrorAction Stop + foreach ($line in $envContent) { + if ($line -match '^([^#][^=]*?)=(.*)$') { + $key = $matches[1].Trim() + $value = $matches[2].Trim() -replace '^["'']|["'']$', '' + + # Set environment variable for the current session + [Environment]::SetEnvironmentVariable($key, $value, "Process") + } + } + Write-Success "Environment variables loaded from .env file" + } catch { + Write-Warning "Could not load .env file: $_" + } +} + +# ---------------------------------------------------------------------------- +# Workflow Functions +# ---------------------------------------------------------------------------- + +# Docker deployment workflow +function Invoke-DockerWorkflow { + Write-Step "Starting Docker Workflow" + Write-Host "Zen MCP Server" -ForegroundColor Green + Write-Host "=================" -ForegroundColor Cyan + + $version = Get-Version + Write-Host "Version: $version" + Write-Host "Mode: Docker Container" -ForegroundColor Yellow + Write-Host "" + + # Docker setup and validation + if (!(Test-DockerRequirements)) { exit 1 } + if (!(Initialize-DockerEnvironment)) { exit 1 } + + Import-EnvFile + Test-ApiKeys + + if (!(Build-DockerImage -Force:$Force)) { exit 1 } + + # Configure MCP clients for Docker + Invoke-McpClientConfiguration -UseDocker $true + + Show-SetupInstructions -UseDocker + + # Start Docker services + Write-Step "Starting Zen MCP Server" + if ($Follow) { + Write-Info "Starting server and following logs..." + Start-DockerServices -Follow + exit 0 + } + + if (!(Start-DockerServices)) { exit 1 } + + Write-Host "" + Write-Success "Zen MCP Server is running in Docker!" + Write-Host "" + + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host "1. Restart your MCP clients (Claude Desktop, etc.)" -ForegroundColor White + Write-Host "2. The server is now ready to use" -ForegroundColor White + Write-Host "" + Write-Host "Useful commands:" -ForegroundColor Cyan + Write-Host " View logs: " -NoNewline -ForegroundColor White + Write-Host "docker logs -f zen-mcp-server" -ForegroundColor Yellow + Write-Host " Stop server: " -NoNewline -ForegroundColor White + Write-Host "docker-compose down" -ForegroundColor Yellow + Write-Host " Restart server: " -NoNewline -ForegroundColor White + Write-Host "docker-compose restart" -ForegroundColor Yellow +} + +# Python virtual environment deployment workflow +function Invoke-PythonWorkflow { + Write-Step "Starting Python Virtual Environment Workflow" + Write-Host "Zen MCP Server" -ForegroundColor Green + Write-Host "=================" -ForegroundColor Cyan + + $version = Get-Version + Write-Host "Version: $version" + Write-Host "" + + if (!(Test-Path $VENV_PATH)) { + Write-Info "Setting up Python environment for first time..." + } + + # Python environment setup + Cleanup-Docker + Clear-PythonCache + Initialize-EnvFile + Import-EnvFile + Test-ApiKeys + + try { + $pythonPath = Initialize-Environment + } catch { + Write-Error "Failed to setup Python environment: $_" + exit 1 + } + + try { + Install-Dependencies $pythonPath -InstallDevDependencies:$Dev + } catch { + Write-Error "Failed to install dependencies: $_" + exit 1 + } + + $serverPath = Get-AbsolutePath "server.py" + + # Configure MCP clients for Python + Invoke-McpClientConfiguration -UseDocker $false -PythonPath $pythonPath -ServerPath $serverPath + + Show-SetupInstructions $pythonPath $serverPath + Initialize-Logging + + Write-Host "" + Write-Host "Logs will be written to: $(Get-AbsolutePath $LOG_DIR)\$LOG_FILE" + Write-Host "" + + if ($Follow) { + Follow-Logs + } else { + Write-Host "To follow logs: .\run-server.ps1 -Follow" -ForegroundColor Yellow + Write-Host "To show config: .\run-server.ps1 -Config" -ForegroundColor Yellow + Write-Host "To update: git pull, then run .\run-server.ps1 again" -ForegroundColor Yellow + Write-Host "" + Write-Host "Happy coding! ๐ŸŽ‰" -ForegroundColor Green + + $response = Read-Host "`nStart the server now? (y/N)" + if ($response -eq 'y' -or $response -eq 'Y') { + Start-Server + } + } +} + +# ---------------------------------------------------------------------------- +# End Workflow Functions +# ---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Main Execution # ---------------------------------------------------------------------------- -# Main server start function -function Start-Server { - Write-Step "Starting Zen MCP Server" - - # Load environment variables - Import-EnvFile - - # Determine Python command - $pythonCmd = if (Test-Path "$VENV_PATH\Scripts\python.exe") { - "$VENV_PATH\Scripts\python.exe" - } elseif (Test-Command "python") { - "python" - } else { - Write-Error "Python not found" - exit 1 - } - - Write-Info "Starting server with: $pythonCmd" - Write-Info "Logs will be written to: $LOG_DIR\$LOG_FILE" - Write-Info "Press Ctrl+C to stop the server" - Write-Host "" - - try { - & $pythonCmd server.py - } catch { - Write-Error "Server failed to start: $_" - exit 1 - } -} - # Main execution function function Start-MainProcess { # Parse command line arguments @@ -1118,99 +1904,44 @@ function Start-MainProcess { Write-Info "Setting up environment for configuration display..." Write-Host "" try { - $pythonPath = Initialize-Environment - $serverPath = Resolve-Path "server.py" - Show-ConfigInstructions $pythonPath $serverPath + if ($Docker) { + # Docker configuration mode + if (!(Test-DockerRequirements)) { + exit 1 + } + Initialize-DockerEnvironment + Show-ConfigInstructions "" "" -UseDocker + } else { + # Python virtual environment configuration mode + $pythonPath = Initialize-Environment + $serverPath = Get-AbsolutePath "server.py" + Show-ConfigInstructions $pythonPath $serverPath + } } catch { - Write-Error "Failed to setup environment: $_" + Write-Error "Failed to setup environment for configuration: $_" + exit 1 } exit 0 } - - # Display header - Write-Host "" - Write-Host "๐Ÿค– Zen MCP Server" -ForegroundColor Cyan - Write-Host "=================" -ForegroundColor Cyan - - # Get and display version - $version = Get-Version - Write-Host "Version: $version" - Write-Host "" - - # Check if venv exists - if (!(Test-Path $VENV_PATH)) { - Write-Info "Setting up Python environment for first time..." - } - - # Step 1: Docker cleanup - Cleanup-Docker - - # Step 1.5: Clear Python cache to prevent import issues - Clear-PythonCache - - # Step 2: Setup environment file - Initialize-EnvFile - - # Step 3: Load .env file - Import-EnvFile - - # Step 4: Validate API keys - Test-ApiKeys - - # Step 5: Setup Python environment - try { - $pythonPath = Initialize-Environment - } catch { - Write-Error "Failed to setup Python environment: $_" - exit 1 - } - - # Step 6: Install dependencies - try { - Install-Dependencies $pythonPath - } catch { - Write-Error "Failed to install dependencies: $_" - exit 1 - } - - # Step 7: Get absolute server path - $serverPath = Resolve-Path "server.py" - - # Step 8: Display setup instructions - Show-SetupInstructions $pythonPath $serverPath - - # Step 9: Check Claude integrations - Test-ClaudeCliIntegration $pythonPath $serverPath - Test-ClaudeDesktopIntegration $pythonPath $serverPath - - # Step 10: Check Gemini CLI integration - Test-GeminiCliIntegration (Split-Path $serverPath -Parent) - - # Step 11: Setup logging directory - Initialize-Logging - - # Step 12: Display log information - Write-Host "" - Write-Host "Logs will be written to: $(Resolve-Path $LOG_DIR)\$LOG_FILE" - Write-Host "" - - # Step 12: Handle command line arguments - if ($Follow) { - Follow-Logs - } else { - Write-Host "To follow logs: .\run-server.ps1 -Follow" -ForegroundColor Yellow - Write-Host "To show config: .\run-server.ps1 -Config" -ForegroundColor Yellow - Write-Host "To update: git pull, then run .\run-server.ps1 again" -ForegroundColor Yellow - Write-Host "" - Write-Host "Happy coding! ๐ŸŽ‰" -ForegroundColor Green - - # Ask if user wants to start server - $response = Read-Host "`nStart the server now? (y/N)" - if ($response -eq 'y' -or $response -eq 'Y') { - Start-Server - } + + # ============================================================================ + # Docker Workflow + # ============================================================================ + if ($Docker) { + Invoke-DockerWorkflow + exit 0 } + + # ============================================================================ + # Python Virtual Environment Workflow (Default) + # ============================================================================ + Invoke-PythonWorkflow + exit 0 } -# Run main function +# ============================================================================ +# Main Script Execution +# ============================================================================ + +# Execute main process Start-MainProcess diff --git a/run_integration_tests.ps1 b/run_integration_tests.ps1 index 3519902..539fd0a 100644 --- a/run_integration_tests.ps1 +++ b/run_integration_tests.ps1 @@ -1,4 +1,32 @@ -#!/usr/bin/env pwsh +<# +.SYNOPSIS + Integration test runner script for the Zen MCP server on Windows. + +.DESCRIPTION + This PowerShell script prepares and runs integration tests for the Zen MCP server: + - Sets up the test environment + - Installs required dependencies + - Runs automated integration tests + - Displays test results and related logs + - Allows output customization via parameters (e.g., display color) + +.PARAMETER Color + Sets the display color for console messages (default: White). + +.EXAMPLE + .\run_integration_tests.ps1 + Prepares the environment and runs all integration tests. + + .\run_integration_tests.ps1 -Color Cyan + Runs the tests with messages displayed in cyan. + +.NOTES + Project Author : BeehiveInnovations + Script Author : GiGiDKR (https://github.com/GiGiDKR) + Date : 07-05-2025 + Version : See config.py (__version__) + References : https://github.com/BeehiveInnovations/zen-mcp-server +#> #Requires -Version 5.1 [CmdletBinding()] param(