claude-mpm 3.9.7__py3-none-any.whl → 3.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/services/__init__.py +28 -0
- claude_mpm/services/mcp_gateway/__init__.py +122 -0
- claude_mpm/services/mcp_gateway/config/__init__.py +17 -0
- claude_mpm/services/mcp_gateway/config/config_loader.py +232 -0
- claude_mpm/services/mcp_gateway/config/config_schema.py +234 -0
- claude_mpm/services/mcp_gateway/config/configuration.py +371 -0
- claude_mpm/services/mcp_gateway/core/__init__.py +51 -0
- claude_mpm/services/mcp_gateway/core/base.py +315 -0
- claude_mpm/services/mcp_gateway/core/exceptions.py +239 -0
- claude_mpm/services/mcp_gateway/core/interfaces.py +476 -0
- claude_mpm/services/mcp_gateway/registry/__init__.py +9 -0
- claude_mpm/services/mcp_gateway/server/__init__.py +9 -0
- claude_mpm/services/mcp_gateway/tools/__init__.py +9 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/METADATA +2 -1
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/RECORD +21 -9
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/WHEEL +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.9.7.dist-info → claude_mpm-3.9.8.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,234 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            MCP Gateway Configuration Schema
         | 
| 3 | 
            +
            ================================
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Defines and validates the configuration schema for MCP Gateway.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Part of ISS-0034: Infrastructure Setup - MCP Gateway Project Foundation
         | 
| 8 | 
            +
            """
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            from typing import Any, Dict, List, Optional
         | 
| 11 | 
            +
            from dataclasses import dataclass
         | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
            @dataclass
         | 
| 15 | 
            +
            class MCPConfigSchema:
         | 
| 16 | 
            +
                """
         | 
| 17 | 
            +
                Configuration schema definition for MCP Gateway.
         | 
| 18 | 
            +
                
         | 
| 19 | 
            +
                This class defines the structure and validation rules for
         | 
| 20 | 
            +
                MCP Gateway configuration.
         | 
| 21 | 
            +
                """
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                # Schema version for migration support
         | 
| 24 | 
            +
                SCHEMA_VERSION = "1.0.0"
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                # Configuration schema definition
         | 
| 27 | 
            +
                SCHEMA = {
         | 
| 28 | 
            +
                    "mcp": {
         | 
| 29 | 
            +
                        "type": "object",
         | 
| 30 | 
            +
                        "required": True,
         | 
| 31 | 
            +
                        "properties": {
         | 
| 32 | 
            +
                            "server": {
         | 
| 33 | 
            +
                                "type": "object",
         | 
| 34 | 
            +
                                "required": True,
         | 
| 35 | 
            +
                                "properties": {
         | 
| 36 | 
            +
                                    "name": {"type": "string", "required": True},
         | 
| 37 | 
            +
                                    "version": {"type": "string", "required": True},
         | 
| 38 | 
            +
                                    "description": {"type": "string", "required": False},
         | 
| 39 | 
            +
                                    "communication": {
         | 
| 40 | 
            +
                                        "type": "object",
         | 
| 41 | 
            +
                                        "required": True,
         | 
| 42 | 
            +
                                        "properties": {
         | 
| 43 | 
            +
                                            "type": {
         | 
| 44 | 
            +
                                                "type": "string",
         | 
| 45 | 
            +
                                                "required": True,
         | 
| 46 | 
            +
                                                "enum": ["stdio", "websocket", "http"]
         | 
| 47 | 
            +
                                            },
         | 
| 48 | 
            +
                                            "timeout": {
         | 
| 49 | 
            +
                                                "type": "number",
         | 
| 50 | 
            +
                                                "required": False,
         | 
| 51 | 
            +
                                                "min": 1,
         | 
| 52 | 
            +
                                                "max": 3600
         | 
| 53 | 
            +
                                            },
         | 
| 54 | 
            +
                                            "buffer_size": {
         | 
| 55 | 
            +
                                                "type": "integer",
         | 
| 56 | 
            +
                                                "required": False,
         | 
| 57 | 
            +
                                                "min": 1024,
         | 
| 58 | 
            +
                                                "max": 1048576
         | 
| 59 | 
            +
                                            },
         | 
| 60 | 
            +
                                        }
         | 
| 61 | 
            +
                                    },
         | 
| 62 | 
            +
                                    "capabilities": {
         | 
| 63 | 
            +
                                        "type": "object",
         | 
| 64 | 
            +
                                        "required": False,
         | 
| 65 | 
            +
                                        "properties": {
         | 
| 66 | 
            +
                                            "tools": {"type": "boolean", "required": False},
         | 
| 67 | 
            +
                                            "resources": {"type": "boolean", "required": False},
         | 
| 68 | 
            +
                                            "prompts": {"type": "boolean", "required": False},
         | 
| 69 | 
            +
                                        }
         | 
| 70 | 
            +
                                    },
         | 
| 71 | 
            +
                                }
         | 
| 72 | 
            +
                            },
         | 
| 73 | 
            +
                            "tools": {
         | 
| 74 | 
            +
                                "type": "object",
         | 
| 75 | 
            +
                                "required": False,
         | 
| 76 | 
            +
                                "properties": {
         | 
| 77 | 
            +
                                    "enabled": {"type": "boolean", "required": False},
         | 
| 78 | 
            +
                                    "auto_discover": {"type": "boolean", "required": False},
         | 
| 79 | 
            +
                                    "discovery_paths": {
         | 
| 80 | 
            +
                                        "type": "array",
         | 
| 81 | 
            +
                                        "required": False,
         | 
| 82 | 
            +
                                        "items": {"type": "string"}
         | 
| 83 | 
            +
                                    },
         | 
| 84 | 
            +
                                    "timeout_default": {
         | 
| 85 | 
            +
                                        "type": "number",
         | 
| 86 | 
            +
                                        "required": False,
         | 
| 87 | 
            +
                                        "min": 1,
         | 
| 88 | 
            +
                                        "max": 300
         | 
| 89 | 
            +
                                    },
         | 
| 90 | 
            +
                                    "max_concurrent": {
         | 
| 91 | 
            +
                                        "type": "integer",
         | 
| 92 | 
            +
                                        "required": False,
         | 
| 93 | 
            +
                                        "min": 1,
         | 
| 94 | 
            +
                                        "max": 100
         | 
| 95 | 
            +
                                    },
         | 
| 96 | 
            +
                                }
         | 
| 97 | 
            +
                            },
         | 
| 98 | 
            +
                            "logging": {
         | 
| 99 | 
            +
                                "type": "object",
         | 
| 100 | 
            +
                                "required": False,
         | 
| 101 | 
            +
                                "properties": {
         | 
| 102 | 
            +
                                    "level": {
         | 
| 103 | 
            +
                                        "type": "string",
         | 
| 104 | 
            +
                                        "required": False,
         | 
| 105 | 
            +
                                        "enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
         | 
| 106 | 
            +
                                    },
         | 
| 107 | 
            +
                                    "file": {"type": "string", "required": False},
         | 
| 108 | 
            +
                                    "max_size": {"type": "string", "required": False},
         | 
| 109 | 
            +
                                    "max_files": {
         | 
| 110 | 
            +
                                        "type": "integer",
         | 
| 111 | 
            +
                                        "required": False,
         | 
| 112 | 
            +
                                        "min": 1,
         | 
| 113 | 
            +
                                        "max": 100
         | 
| 114 | 
            +
                                    },
         | 
| 115 | 
            +
                                    "format": {
         | 
| 116 | 
            +
                                        "type": "string",
         | 
| 117 | 
            +
                                        "required": False,
         | 
| 118 | 
            +
                                        "enum": ["json", "text"]
         | 
| 119 | 
            +
                                    },
         | 
| 120 | 
            +
                                }
         | 
| 121 | 
            +
                            },
         | 
| 122 | 
            +
                            "security": {
         | 
| 123 | 
            +
                                "type": "object",
         | 
| 124 | 
            +
                                "required": False,
         | 
| 125 | 
            +
                                "properties": {
         | 
| 126 | 
            +
                                    "validate_schemas": {"type": "boolean", "required": False},
         | 
| 127 | 
            +
                                    "sanitize_inputs": {"type": "boolean", "required": False},
         | 
| 128 | 
            +
                                    "max_request_size": {
         | 
| 129 | 
            +
                                        "type": "integer",
         | 
| 130 | 
            +
                                        "required": False,
         | 
| 131 | 
            +
                                        "min": 1024,
         | 
| 132 | 
            +
                                        "max": 104857600  # 100MB max
         | 
| 133 | 
            +
                                    },
         | 
| 134 | 
            +
                                    "allowed_tools": {
         | 
| 135 | 
            +
                                        "type": "array",
         | 
| 136 | 
            +
                                        "required": False,
         | 
| 137 | 
            +
                                        "items": {"type": "string"}
         | 
| 138 | 
            +
                                    },
         | 
| 139 | 
            +
                                    "blocked_tools": {
         | 
| 140 | 
            +
                                        "type": "array",
         | 
| 141 | 
            +
                                        "required": False,
         | 
| 142 | 
            +
                                        "items": {"type": "string"}
         | 
| 143 | 
            +
                                    },
         | 
| 144 | 
            +
                                }
         | 
| 145 | 
            +
                            },
         | 
| 146 | 
            +
                        }
         | 
| 147 | 
            +
                    }
         | 
| 148 | 
            +
                }
         | 
| 149 | 
            +
             | 
| 150 | 
            +
             | 
| 151 | 
            +
            def validate_config(config: Dict[str, Any], schema: Optional[Dict[str, Any]] = None) -> List[str]:
         | 
| 152 | 
            +
                """
         | 
| 153 | 
            +
                Validate configuration against schema.
         | 
| 154 | 
            +
                
         | 
| 155 | 
            +
                Args:
         | 
| 156 | 
            +
                    config: Configuration dictionary to validate
         | 
| 157 | 
            +
                    schema: Schema to validate against (uses default if not provided)
         | 
| 158 | 
            +
                    
         | 
| 159 | 
            +
                Returns:
         | 
| 160 | 
            +
                    List of validation errors (empty if valid)
         | 
| 161 | 
            +
                """
         | 
| 162 | 
            +
                if schema is None:
         | 
| 163 | 
            +
                    schema = MCPConfigSchema.SCHEMA
         | 
| 164 | 
            +
                
         | 
| 165 | 
            +
                errors = []
         | 
| 166 | 
            +
                
         | 
| 167 | 
            +
                def validate_value(value: Any, spec: Dict[str, Any], path: str) -> None:
         | 
| 168 | 
            +
                    """Recursively validate a value against its specification."""
         | 
| 169 | 
            +
                    
         | 
| 170 | 
            +
                    # Check type
         | 
| 171 | 
            +
                    expected_type = spec.get("type")
         | 
| 172 | 
            +
                    if expected_type:
         | 
| 173 | 
            +
                        if expected_type == "object" and not isinstance(value, dict):
         | 
| 174 | 
            +
                            errors.append(f"{path}: Expected object, got {type(value).__name__}")
         | 
| 175 | 
            +
                            return
         | 
| 176 | 
            +
                        elif expected_type == "array" and not isinstance(value, list):
         | 
| 177 | 
            +
                            errors.append(f"{path}: Expected array, got {type(value).__name__}")
         | 
| 178 | 
            +
                            return
         | 
| 179 | 
            +
                        elif expected_type == "string" and not isinstance(value, str):
         | 
| 180 | 
            +
                            errors.append(f"{path}: Expected string, got {type(value).__name__}")
         | 
| 181 | 
            +
                            return
         | 
| 182 | 
            +
                        elif expected_type == "number" and not isinstance(value, (int, float)):
         | 
| 183 | 
            +
                            errors.append(f"{path}: Expected number, got {type(value).__name__}")
         | 
| 184 | 
            +
                            return
         | 
| 185 | 
            +
                        elif expected_type == "integer" and not isinstance(value, int):
         | 
| 186 | 
            +
                            errors.append(f"{path}: Expected integer, got {type(value).__name__}")
         | 
| 187 | 
            +
                            return
         | 
| 188 | 
            +
                        elif expected_type == "boolean" and not isinstance(value, bool):
         | 
| 189 | 
            +
                            errors.append(f"{path}: Expected boolean, got {type(value).__name__}")
         | 
| 190 | 
            +
                            return
         | 
| 191 | 
            +
                    
         | 
| 192 | 
            +
                    # Check enum values
         | 
| 193 | 
            +
                    if "enum" in spec and value not in spec["enum"]:
         | 
| 194 | 
            +
                        errors.append(f"{path}: Value '{value}' not in allowed values: {spec['enum']}")
         | 
| 195 | 
            +
                    
         | 
| 196 | 
            +
                    # Check numeric constraints
         | 
| 197 | 
            +
                    if isinstance(value, (int, float)):
         | 
| 198 | 
            +
                        if "min" in spec and value < spec["min"]:
         | 
| 199 | 
            +
                            errors.append(f"{path}: Value {value} is less than minimum {spec['min']}")
         | 
| 200 | 
            +
                        if "max" in spec and value > spec["max"]:
         | 
| 201 | 
            +
                            errors.append(f"{path}: Value {value} is greater than maximum {spec['max']}")
         | 
| 202 | 
            +
                    
         | 
| 203 | 
            +
                    # Validate object properties
         | 
| 204 | 
            +
                    if expected_type == "object" and isinstance(value, dict):
         | 
| 205 | 
            +
                        properties = spec.get("properties", {})
         | 
| 206 | 
            +
                        for prop_name, prop_spec in properties.items():
         | 
| 207 | 
            +
                            prop_path = f"{path}.{prop_name}"
         | 
| 208 | 
            +
                            
         | 
| 209 | 
            +
                            if prop_name in value:
         | 
| 210 | 
            +
                                validate_value(value[prop_name], prop_spec, prop_path)
         | 
| 211 | 
            +
                            elif prop_spec.get("required", False):
         | 
| 212 | 
            +
                                errors.append(f"{prop_path}: Required field missing")
         | 
| 213 | 
            +
                    
         | 
| 214 | 
            +
                    # Validate array items
         | 
| 215 | 
            +
                    if expected_type == "array" and isinstance(value, list):
         | 
| 216 | 
            +
                        item_spec = spec.get("items", {})
         | 
| 217 | 
            +
                        for i, item in enumerate(value):
         | 
| 218 | 
            +
                            validate_value(item, item_spec, f"{path}[{i}]")
         | 
| 219 | 
            +
                
         | 
| 220 | 
            +
                # Start validation from root
         | 
| 221 | 
            +
                validate_value(config, {"type": "object", "properties": schema}, "config")
         | 
| 222 | 
            +
                
         | 
| 223 | 
            +
                return errors
         | 
| 224 | 
            +
             | 
| 225 | 
            +
             | 
| 226 | 
            +
            def generate_config_template() -> Dict[str, Any]:
         | 
| 227 | 
            +
                """
         | 
| 228 | 
            +
                Generate a configuration template with all possible options.
         | 
| 229 | 
            +
                
         | 
| 230 | 
            +
                Returns:
         | 
| 231 | 
            +
                    Configuration template dictionary
         | 
| 232 | 
            +
                """
         | 
| 233 | 
            +
                from ..config.configuration import MCPConfiguration
         | 
| 234 | 
            +
                return MCPConfiguration.DEFAULT_CONFIG
         | 
| @@ -0,0 +1,371 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            MCP Gateway Configuration Implementation
         | 
| 3 | 
            +
            ========================================
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Manages configuration for the MCP Gateway service.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Part of ISS-0034: Infrastructure Setup - MCP Gateway Project Foundation
         | 
| 8 | 
            +
            """
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            import os
         | 
| 11 | 
            +
            from typing import Any, Dict, Optional
         | 
| 12 | 
            +
            from pathlib import Path
         | 
| 13 | 
            +
            import yaml
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            from claude_mpm.services.mcp_gateway.core.interfaces import IMCPConfiguration
         | 
| 16 | 
            +
            from claude_mpm.services.mcp_gateway.core.base import BaseMCPService
         | 
| 17 | 
            +
            from claude_mpm.services.mcp_gateway.core.exceptions import MCPConfigurationError
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 20 | 
            +
            class MCPConfiguration(BaseMCPService, IMCPConfiguration):
         | 
| 21 | 
            +
                """
         | 
| 22 | 
            +
                MCP Gateway configuration management service.
         | 
| 23 | 
            +
                
         | 
| 24 | 
            +
                This service handles loading, validation, and access to MCP Gateway configuration.
         | 
| 25 | 
            +
                It supports YAML-based configuration files and environment variable overrides.
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                WHY: Configuration is centralized in a service to ensure consistent access
         | 
| 28 | 
            +
                patterns, validation, and the ability to reload configuration at runtime.
         | 
| 29 | 
            +
                The service pattern also allows for dependency injection of configuration
         | 
| 30 | 
            +
                into other MCP services.
         | 
| 31 | 
            +
                """
         | 
| 32 | 
            +
                
         | 
| 33 | 
            +
                DEFAULT_CONFIG = {
         | 
| 34 | 
            +
                    "mcp": {
         | 
| 35 | 
            +
                        "server": {
         | 
| 36 | 
            +
                            "name": "claude-mpm-gateway",
         | 
| 37 | 
            +
                            "version": "1.0.0",
         | 
| 38 | 
            +
                            "description": "Claude MPM MCP Gateway Server",
         | 
| 39 | 
            +
                            "communication": {
         | 
| 40 | 
            +
                                "type": "stdio",  # stdio, websocket, or http
         | 
| 41 | 
            +
                                "timeout": 30,  # seconds
         | 
| 42 | 
            +
                                "buffer_size": 8192,
         | 
| 43 | 
            +
                            },
         | 
| 44 | 
            +
                            "capabilities": {
         | 
| 45 | 
            +
                                "tools": True,
         | 
| 46 | 
            +
                                "resources": False,  # Not yet implemented
         | 
| 47 | 
            +
                                "prompts": False,    # Not yet implemented
         | 
| 48 | 
            +
                            },
         | 
| 49 | 
            +
                        },
         | 
| 50 | 
            +
                        "tools": {
         | 
| 51 | 
            +
                            "enabled": True,
         | 
| 52 | 
            +
                            "auto_discover": True,
         | 
| 53 | 
            +
                            "discovery_paths": [
         | 
| 54 | 
            +
                                "~/.claude/mcp/tools",
         | 
| 55 | 
            +
                                "./mcp_tools",
         | 
| 56 | 
            +
                            ],
         | 
| 57 | 
            +
                            "timeout_default": 30,  # seconds
         | 
| 58 | 
            +
                            "max_concurrent": 10,
         | 
| 59 | 
            +
                        },
         | 
| 60 | 
            +
                        "logging": {
         | 
| 61 | 
            +
                            "level": "INFO",
         | 
| 62 | 
            +
                            "file": "~/.claude/logs/mcp_gateway.log",
         | 
| 63 | 
            +
                            "max_size": "10MB",
         | 
| 64 | 
            +
                            "max_files": 5,
         | 
| 65 | 
            +
                            "format": "json",  # json or text
         | 
| 66 | 
            +
                        },
         | 
| 67 | 
            +
                        "security": {
         | 
| 68 | 
            +
                            "validate_schemas": True,
         | 
| 69 | 
            +
                            "sanitize_inputs": True,
         | 
| 70 | 
            +
                            "max_request_size": 1048576,  # 1MB
         | 
| 71 | 
            +
                            "allowed_tools": [],  # Empty means all tools allowed
         | 
| 72 | 
            +
                            "blocked_tools": [],
         | 
| 73 | 
            +
                        },
         | 
| 74 | 
            +
                    }
         | 
| 75 | 
            +
                }
         | 
| 76 | 
            +
                
         | 
| 77 | 
            +
                def __init__(self, config_path: Optional[Path] = None):
         | 
| 78 | 
            +
                    """
         | 
| 79 | 
            +
                    Initialize MCP configuration service.
         | 
| 80 | 
            +
                    
         | 
| 81 | 
            +
                    Args:
         | 
| 82 | 
            +
                        config_path: Optional path to configuration file
         | 
| 83 | 
            +
                    """
         | 
| 84 | 
            +
                    super().__init__("MCPConfiguration")
         | 
| 85 | 
            +
                    self._config_path = config_path
         | 
| 86 | 
            +
                    self._config_data: Dict[str, Any] = {}
         | 
| 87 | 
            +
                    self._is_loaded = False
         | 
| 88 | 
            +
                
         | 
| 89 | 
            +
                async def _do_initialize(self) -> bool:
         | 
| 90 | 
            +
                    """
         | 
| 91 | 
            +
                    Initialize the configuration service.
         | 
| 92 | 
            +
                    
         | 
| 93 | 
            +
                    Returns:
         | 
| 94 | 
            +
                        True if initialization successful
         | 
| 95 | 
            +
                    """
         | 
| 96 | 
            +
                    # Start with default configuration
         | 
| 97 | 
            +
                    self._config_data = self.DEFAULT_CONFIG.copy()
         | 
| 98 | 
            +
                    
         | 
| 99 | 
            +
                    # Load from file if path provided
         | 
| 100 | 
            +
                    if self._config_path:
         | 
| 101 | 
            +
                        if not self.load_config(self._config_path):
         | 
| 102 | 
            +
                            return False
         | 
| 103 | 
            +
                    
         | 
| 104 | 
            +
                    # Apply environment variable overrides
         | 
| 105 | 
            +
                    self._apply_env_overrides()
         | 
| 106 | 
            +
                    
         | 
| 107 | 
            +
                    # Validate configuration
         | 
| 108 | 
            +
                    if not self.validate():
         | 
| 109 | 
            +
                        return False
         | 
| 110 | 
            +
                    
         | 
| 111 | 
            +
                    self._is_loaded = True
         | 
| 112 | 
            +
                    self.log_info("Configuration initialized successfully")
         | 
| 113 | 
            +
                    return True
         | 
| 114 | 
            +
                
         | 
| 115 | 
            +
                def load_config(self, config_path: Path) -> bool:
         | 
| 116 | 
            +
                    """
         | 
| 117 | 
            +
                    Load configuration from a file.
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                    Args:
         | 
| 120 | 
            +
                        config_path: Path to configuration file
         | 
| 121 | 
            +
                        
         | 
| 122 | 
            +
                    Returns:
         | 
| 123 | 
            +
                        True if configuration loaded successfully
         | 
| 124 | 
            +
                    """
         | 
| 125 | 
            +
                    try:
         | 
| 126 | 
            +
                        # Expand user path
         | 
| 127 | 
            +
                        config_path = Path(config_path).expanduser()
         | 
| 128 | 
            +
                        
         | 
| 129 | 
            +
                        if not config_path.exists():
         | 
| 130 | 
            +
                            self.log_warning(f"Configuration file not found: {config_path}")
         | 
| 131 | 
            +
                            return True  # Not an error, use defaults
         | 
| 132 | 
            +
                        
         | 
| 133 | 
            +
                        with open(config_path, 'r') as f:
         | 
| 134 | 
            +
                            if config_path.suffix in ['.yaml', '.yml']:
         | 
| 135 | 
            +
                                loaded_config = yaml.safe_load(f) or {}
         | 
| 136 | 
            +
                            else:
         | 
| 137 | 
            +
                                raise MCPConfigurationError(
         | 
| 138 | 
            +
                                    f"Unsupported configuration file format: {config_path.suffix}"
         | 
| 139 | 
            +
                                )
         | 
| 140 | 
            +
                        
         | 
| 141 | 
            +
                        # Merge with existing configuration
         | 
| 142 | 
            +
                        self._merge_config(self._config_data, loaded_config)
         | 
| 143 | 
            +
                        self._config_path = config_path
         | 
| 144 | 
            +
                        
         | 
| 145 | 
            +
                        self.log_info(f"Configuration loaded from {config_path}")
         | 
| 146 | 
            +
                        return True
         | 
| 147 | 
            +
                        
         | 
| 148 | 
            +
                    except yaml.YAMLError as e:
         | 
| 149 | 
            +
                        raise MCPConfigurationError(f"Failed to parse YAML configuration: {e}")
         | 
| 150 | 
            +
                    except Exception as e:
         | 
| 151 | 
            +
                        self.log_error(f"Failed to load configuration: {e}")
         | 
| 152 | 
            +
                        return False
         | 
| 153 | 
            +
                
         | 
| 154 | 
            +
                def _merge_config(self, base: Dict[str, Any], overlay: Dict[str, Any]) -> None:
         | 
| 155 | 
            +
                    """
         | 
| 156 | 
            +
                    Recursively merge overlay configuration into base.
         | 
| 157 | 
            +
                    
         | 
| 158 | 
            +
                    Args:
         | 
| 159 | 
            +
                        base: Base configuration dictionary
         | 
| 160 | 
            +
                        overlay: Configuration to merge in
         | 
| 161 | 
            +
                    """
         | 
| 162 | 
            +
                    for key, value in overlay.items():
         | 
| 163 | 
            +
                        if key in base and isinstance(base[key], dict) and isinstance(value, dict):
         | 
| 164 | 
            +
                            self._merge_config(base[key], value)
         | 
| 165 | 
            +
                        else:
         | 
| 166 | 
            +
                            base[key] = value
         | 
| 167 | 
            +
                
         | 
| 168 | 
            +
                def _apply_env_overrides(self) -> None:
         | 
| 169 | 
            +
                    """
         | 
| 170 | 
            +
                    Apply environment variable overrides to configuration.
         | 
| 171 | 
            +
                    
         | 
| 172 | 
            +
                    Environment variables follow the pattern: MCP_GATEWAY_<SECTION>_<KEY>
         | 
| 173 | 
            +
                    For example: MCP_GATEWAY_SERVER_NAME=my-server
         | 
| 174 | 
            +
                    """
         | 
| 175 | 
            +
                    prefix = "MCP_GATEWAY_"
         | 
| 176 | 
            +
                    
         | 
| 177 | 
            +
                    for env_key, env_value in os.environ.items():
         | 
| 178 | 
            +
                        if not env_key.startswith(prefix):
         | 
| 179 | 
            +
                            continue
         | 
| 180 | 
            +
                        
         | 
| 181 | 
            +
                        # Parse environment variable into configuration path
         | 
| 182 | 
            +
                        config_path = env_key[len(prefix):].lower().split('_')
         | 
| 183 | 
            +
                        
         | 
| 184 | 
            +
                        # Navigate to the configuration location
         | 
| 185 | 
            +
                        current = self._config_data
         | 
| 186 | 
            +
                        for i, part in enumerate(config_path[:-1]):
         | 
| 187 | 
            +
                            if part not in current:
         | 
| 188 | 
            +
                                current[part] = {}
         | 
| 189 | 
            +
                            elif not isinstance(current[part], dict):
         | 
| 190 | 
            +
                                self.log_warning(f"Cannot override non-dict config at {'.'.join(config_path[:i+1])}")
         | 
| 191 | 
            +
                                break
         | 
| 192 | 
            +
                            current = current[part]
         | 
| 193 | 
            +
                        else:
         | 
| 194 | 
            +
                            # Set the value
         | 
| 195 | 
            +
                            key = config_path[-1]
         | 
| 196 | 
            +
                            # Try to parse as JSON for complex types
         | 
| 197 | 
            +
                            try:
         | 
| 198 | 
            +
                                import json
         | 
| 199 | 
            +
                                current[key] = json.loads(env_value)
         | 
| 200 | 
            +
                            except:
         | 
| 201 | 
            +
                                # Fall back to string value
         | 
| 202 | 
            +
                                current[key] = env_value
         | 
| 203 | 
            +
                            
         | 
| 204 | 
            +
                            self.log_debug(f"Applied environment override: {env_key}")
         | 
| 205 | 
            +
                
         | 
| 206 | 
            +
                def get(self, key: str, default: Any = None) -> Any:
         | 
| 207 | 
            +
                    """
         | 
| 208 | 
            +
                    Get configuration value by key.
         | 
| 209 | 
            +
                    
         | 
| 210 | 
            +
                    Args:
         | 
| 211 | 
            +
                        key: Configuration key (supports dot notation, e.g., "mcp.server.name")
         | 
| 212 | 
            +
                        default: Default value if key not found
         | 
| 213 | 
            +
                        
         | 
| 214 | 
            +
                    Returns:
         | 
| 215 | 
            +
                        Configuration value or default
         | 
| 216 | 
            +
                    """
         | 
| 217 | 
            +
                    parts = key.split('.')
         | 
| 218 | 
            +
                    current = self._config_data
         | 
| 219 | 
            +
                    
         | 
| 220 | 
            +
                    for part in parts:
         | 
| 221 | 
            +
                        if isinstance(current, dict) and part in current:
         | 
| 222 | 
            +
                            current = current[part]
         | 
| 223 | 
            +
                        else:
         | 
| 224 | 
            +
                            return default
         | 
| 225 | 
            +
                    
         | 
| 226 | 
            +
                    return current
         | 
| 227 | 
            +
                
         | 
| 228 | 
            +
                def set(self, key: str, value: Any) -> None:
         | 
| 229 | 
            +
                    """
         | 
| 230 | 
            +
                    Set configuration value.
         | 
| 231 | 
            +
                    
         | 
| 232 | 
            +
                    Args:
         | 
| 233 | 
            +
                        key: Configuration key (supports dot notation)
         | 
| 234 | 
            +
                        value: Configuration value
         | 
| 235 | 
            +
                    """
         | 
| 236 | 
            +
                    parts = key.split('.')
         | 
| 237 | 
            +
                    current = self._config_data
         | 
| 238 | 
            +
                    
         | 
| 239 | 
            +
                    # Navigate to parent
         | 
| 240 | 
            +
                    for part in parts[:-1]:
         | 
| 241 | 
            +
                        if part not in current:
         | 
| 242 | 
            +
                            current[part] = {}
         | 
| 243 | 
            +
                        elif not isinstance(current[part], dict):
         | 
| 244 | 
            +
                            raise MCPConfigurationError(
         | 
| 245 | 
            +
                                f"Cannot set value at {key}: parent is not a dictionary"
         | 
| 246 | 
            +
                            )
         | 
| 247 | 
            +
                        current = current[part]
         | 
| 248 | 
            +
                    
         | 
| 249 | 
            +
                    # Set the value
         | 
| 250 | 
            +
                    current[parts[-1]] = value
         | 
| 251 | 
            +
                    self.log_debug(f"Configuration updated: {key} = {value}")
         | 
| 252 | 
            +
                
         | 
| 253 | 
            +
                def validate(self) -> bool:
         | 
| 254 | 
            +
                    """
         | 
| 255 | 
            +
                    Validate the current configuration.
         | 
| 256 | 
            +
                    
         | 
| 257 | 
            +
                    Returns:
         | 
| 258 | 
            +
                        True if configuration is valid
         | 
| 259 | 
            +
                    """
         | 
| 260 | 
            +
                    try:
         | 
| 261 | 
            +
                        # Check required fields
         | 
| 262 | 
            +
                        required_fields = [
         | 
| 263 | 
            +
                            "mcp.server.name",
         | 
| 264 | 
            +
                            "mcp.server.version",
         | 
| 265 | 
            +
                            "mcp.server.communication.type",
         | 
| 266 | 
            +
                        ]
         | 
| 267 | 
            +
                        
         | 
| 268 | 
            +
                        for field in required_fields:
         | 
| 269 | 
            +
                            if self.get(field) is None:
         | 
| 270 | 
            +
                                raise MCPConfigurationError(
         | 
| 271 | 
            +
                                    f"Required configuration field missing: {field}",
         | 
| 272 | 
            +
                                    config_key=field
         | 
| 273 | 
            +
                                )
         | 
| 274 | 
            +
                        
         | 
| 275 | 
            +
                        # Validate communication type
         | 
| 276 | 
            +
                        comm_type = self.get("mcp.server.communication.type")
         | 
| 277 | 
            +
                        if comm_type not in ["stdio", "websocket", "http"]:
         | 
| 278 | 
            +
                            raise MCPConfigurationError(
         | 
| 279 | 
            +
                                f"Invalid communication type: {comm_type}",
         | 
| 280 | 
            +
                                config_key="mcp.server.communication.type",
         | 
| 281 | 
            +
                                expected_type="stdio|websocket|http"
         | 
| 282 | 
            +
                            )
         | 
| 283 | 
            +
                        
         | 
| 284 | 
            +
                        # Validate numeric fields
         | 
| 285 | 
            +
                        timeout = self.get("mcp.server.communication.timeout")
         | 
| 286 | 
            +
                        if not isinstance(timeout, (int, float)) or timeout <= 0:
         | 
| 287 | 
            +
                            raise MCPConfigurationError(
         | 
| 288 | 
            +
                                "Invalid timeout value",
         | 
| 289 | 
            +
                                config_key="mcp.server.communication.timeout",
         | 
| 290 | 
            +
                                expected_type="positive number"
         | 
| 291 | 
            +
                            )
         | 
| 292 | 
            +
                        
         | 
| 293 | 
            +
                        self.log_debug("Configuration validation successful")
         | 
| 294 | 
            +
                        return True
         | 
| 295 | 
            +
                        
         | 
| 296 | 
            +
                    except MCPConfigurationError:
         | 
| 297 | 
            +
                        raise
         | 
| 298 | 
            +
                    except Exception as e:
         | 
| 299 | 
            +
                        self.log_error(f"Configuration validation failed: {e}")
         | 
| 300 | 
            +
                        return False
         | 
| 301 | 
            +
                
         | 
| 302 | 
            +
                def get_server_config(self) -> Dict[str, Any]:
         | 
| 303 | 
            +
                    """
         | 
| 304 | 
            +
                    Get MCP server configuration.
         | 
| 305 | 
            +
                    
         | 
| 306 | 
            +
                    Returns:
         | 
| 307 | 
            +
                        Server configuration dictionary
         | 
| 308 | 
            +
                    """
         | 
| 309 | 
            +
                    return self.get("mcp.server", {})
         | 
| 310 | 
            +
                
         | 
| 311 | 
            +
                def get_tools_config(self) -> Dict[str, Any]:
         | 
| 312 | 
            +
                    """
         | 
| 313 | 
            +
                    Get tools configuration.
         | 
| 314 | 
            +
                    
         | 
| 315 | 
            +
                    Returns:
         | 
| 316 | 
            +
                        Tools configuration dictionary
         | 
| 317 | 
            +
                    """
         | 
| 318 | 
            +
                    return self.get("mcp.tools", {})
         | 
| 319 | 
            +
                
         | 
| 320 | 
            +
                def save_config(self, path: Optional[Path] = None) -> bool:
         | 
| 321 | 
            +
                    """
         | 
| 322 | 
            +
                    Save current configuration to file.
         | 
| 323 | 
            +
                    
         | 
| 324 | 
            +
                    Args:
         | 
| 325 | 
            +
                        path: Path to save configuration (uses loaded path if not specified)
         | 
| 326 | 
            +
                        
         | 
| 327 | 
            +
                    Returns:
         | 
| 328 | 
            +
                        True if save successful
         | 
| 329 | 
            +
                    """
         | 
| 330 | 
            +
                    save_path = path or self._config_path
         | 
| 331 | 
            +
                    if not save_path:
         | 
| 332 | 
            +
                        self.log_error("No path specified for saving configuration")
         | 
| 333 | 
            +
                        return False
         | 
| 334 | 
            +
                    
         | 
| 335 | 
            +
                    try:
         | 
| 336 | 
            +
                        save_path = Path(save_path).expanduser()
         | 
| 337 | 
            +
                        save_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 338 | 
            +
                        
         | 
| 339 | 
            +
                        with open(save_path, 'w') as f:
         | 
| 340 | 
            +
                            yaml.dump(self._config_data, f, default_flow_style=False, sort_keys=True)
         | 
| 341 | 
            +
                        
         | 
| 342 | 
            +
                        self.log_info(f"Configuration saved to {save_path}")
         | 
| 343 | 
            +
                        return True
         | 
| 344 | 
            +
                        
         | 
| 345 | 
            +
                    except Exception as e:
         | 
| 346 | 
            +
                        self.log_error(f"Failed to save configuration: {e}")
         | 
| 347 | 
            +
                        return False
         | 
| 348 | 
            +
                
         | 
| 349 | 
            +
                def reload(self) -> bool:
         | 
| 350 | 
            +
                    """
         | 
| 351 | 
            +
                    Reload configuration from file.
         | 
| 352 | 
            +
                    
         | 
| 353 | 
            +
                    Returns:
         | 
| 354 | 
            +
                        True if reload successful
         | 
| 355 | 
            +
                    """
         | 
| 356 | 
            +
                    if not self._config_path:
         | 
| 357 | 
            +
                        self.log_warning("No configuration file to reload")
         | 
| 358 | 
            +
                        return True
         | 
| 359 | 
            +
                    
         | 
| 360 | 
            +
                    # Reset to defaults
         | 
| 361 | 
            +
                    self._config_data = self.DEFAULT_CONFIG.copy()
         | 
| 362 | 
            +
                    
         | 
| 363 | 
            +
                    # Reload from file
         | 
| 364 | 
            +
                    if not self.load_config(self._config_path):
         | 
| 365 | 
            +
                        return False
         | 
| 366 | 
            +
                    
         | 
| 367 | 
            +
                    # Reapply environment overrides
         | 
| 368 | 
            +
                    self._apply_env_overrides()
         | 
| 369 | 
            +
                    
         | 
| 370 | 
            +
                    # Revalidate
         | 
| 371 | 
            +
                    return self.validate()
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            MCP Gateway Core Module
         | 
| 3 | 
            +
            =======================
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Core interfaces and base classes for the MCP Gateway service.
         | 
| 6 | 
            +
            """
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            from .interfaces import (
         | 
| 9 | 
            +
                IMCPServer,
         | 
| 10 | 
            +
                IMCPToolRegistry,
         | 
| 11 | 
            +
                IMCPConfiguration,
         | 
| 12 | 
            +
                IMCPToolAdapter,
         | 
| 13 | 
            +
                IMCPLifecycle,
         | 
| 14 | 
            +
                IMCPCommunication,
         | 
| 15 | 
            +
            )
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            from .base import (
         | 
| 18 | 
            +
                BaseMCPService,
         | 
| 19 | 
            +
                MCPServiceState,
         | 
| 20 | 
            +
            )
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            from .exceptions import (
         | 
| 23 | 
            +
                MCPException,
         | 
| 24 | 
            +
                MCPConfigurationError,
         | 
| 25 | 
            +
                MCPToolNotFoundError,
         | 
| 26 | 
            +
                MCPServerError,
         | 
| 27 | 
            +
                MCPCommunicationError,
         | 
| 28 | 
            +
                MCPValidationError,
         | 
| 29 | 
            +
            )
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            __all__ = [
         | 
| 32 | 
            +
                # Interfaces
         | 
| 33 | 
            +
                "IMCPServer",
         | 
| 34 | 
            +
                "IMCPToolRegistry",
         | 
| 35 | 
            +
                "IMCPConfiguration",
         | 
| 36 | 
            +
                "IMCPToolAdapter",
         | 
| 37 | 
            +
                "IMCPLifecycle",
         | 
| 38 | 
            +
                "IMCPCommunication",
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                # Base classes
         | 
| 41 | 
            +
                "BaseMCPService",
         | 
| 42 | 
            +
                "MCPServiceState",
         | 
| 43 | 
            +
                
         | 
| 44 | 
            +
                # Exceptions
         | 
| 45 | 
            +
                "MCPException",
         | 
| 46 | 
            +
                "MCPConfigurationError",
         | 
| 47 | 
            +
                "MCPToolNotFoundError",
         | 
| 48 | 
            +
                "MCPServerError",
         | 
| 49 | 
            +
                "MCPCommunicationError",
         | 
| 50 | 
            +
                "MCPValidationError",
         | 
| 51 | 
            +
            ]
         |