diff --git a/.cursor/rules/mcp-server-configuration.mdc b/.cursor/rules/mcp-server-configuration.mdc new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/.cursor/rules/mcp-server-configuration.mdc @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/config/mcp_servers.yaml b/config/mcp_servers.yaml new file mode 100644 index 0000000..36b4483 --- /dev/null +++ b/config/mcp_servers.yaml @@ -0,0 +1,120 @@ +# MCP Server Configuration +# This file defines MCP server configurations for different environments + +# Default configuration that applies to all servers unless overridden +default: + workers: 4 + timeout: 30 + ssl_enabled: false + log_level: "INFO" + middleware: + - SecurityMiddleware: + enabled: true + - MetricsMiddleware: + enabled: true + metrics_path: "/metrics" + +# Environment-specific server configurations +environments: + development: + servers: + main: + host: "localhost" + port: 8000 + workers: 2 + ssl_enabled: false + log_level: "DEBUG" + debug: true + middleware: + - SecurityMiddleware: + enabled: true + rate_limit: + requests: 1000 + period: 60 + - MetricsMiddleware: + enabled: true + enable_timing: true + secondary: + host: "localhost" + port: 8001 + workers: 1 + ssl_enabled: false + debug: true + + test: + servers: + main: + host: "localhost" + port: 9000 + workers: 1 + timeout: 10 + ssl_enabled: false + log_level: "DEBUG" + debug: true + middleware: + - SecurityMiddleware: + enabled: false + - MetricsMiddleware: + enabled: true + track_endpoints: true + + staging: + servers: + main: + host: "0.0.0.0" + port: 8000 + workers: 4 + ssl_enabled: true + ssl_cert: "/etc/ssl/certs/mcp-staging.crt" + ssl_key: "/etc/ssl/private/mcp-staging.key" + middleware: + - SecurityMiddleware: + enabled: true + rate_limit: + requests: 500 + period: 60 + allowed_hosts: + - "api-staging.example.com" + - MetricsMiddleware: + enabled: true + + production: + servers: + main: + host: "0.0.0.0" + port: 443 + workers: 8 + timeout: 30 + ssl_enabled: true + ssl_cert: "/etc/ssl/certs/mcp.crt" + ssl_key: "/etc/ssl/private/mcp.key" + log_level: "WARNING" + middleware: + - SecurityMiddleware: + enabled: true + rate_limit: + requests: 200 + period: 60 + allowed_hosts: + - "api.example.com" + - MetricsMiddleware: + enabled: true + analytics: + host: "0.0.0.0" + port: 8080 + workers: 4 + ssl_enabled: true + ssl_cert: "/etc/ssl/certs/mcp-analytics.crt" + ssl_key: "/etc/ssl/private/mcp-analytics.key" + log_level: "WARNING" + middleware: + - SecurityMiddleware: + enabled: true + rate_limit: + requests: 500 + period: 60 + allowed_hosts: + - "analytics.example.com" + - MetricsMiddleware: + enabled: true + enable_timing: true \ No newline at end of file diff --git a/docs/mcp_server_configuration.md b/docs/mcp_server_configuration.md new file mode 100644 index 0000000..eb8e89e --- /dev/null +++ b/docs/mcp_server_configuration.md @@ -0,0 +1,193 @@ +# MCP Server Configuration + +This document describes how to configure and manage MCP servers using the server configuration system. + +## Overview + +The MCP server configuration system allows you to define server configurations for different environments in a centralized YAML file. This makes it easy to manage multiple server configurations across different environments (development, test, staging, production) and ensures consistency in server setup. + +## Configuration File + +Server configurations are defined in `config/mcp_servers.yaml`. This file contains: + +1. **Default configuration**: Applied to all servers unless overridden +2. **Environment-specific configurations**: Settings for each environment +3. **Server-specific configurations**: Settings for individual servers within each environment + +### Example Configuration + +```yaml +# Default configuration that applies to all servers unless overridden +default: + workers: 4 + timeout: 30 + ssl_enabled: false + log_level: "INFO" + middleware: + - SecurityMiddleware: + enabled: true + - MetricsMiddleware: + enabled: true + metrics_path: "/metrics" + +# Environment-specific server configurations +environments: + development: + servers: + main: + host: "localhost" + port: 8000 + workers: 2 + ssl_enabled: false + log_level: "DEBUG" + debug: true + secondary: + host: "localhost" + port: 8001 + workers: 1 + ssl_enabled: false + debug: true + + # Other environments... +``` + +## Configuration Structure + +### Default Configuration + +The `default` section contains settings that apply to all servers unless overridden by environment-specific or server-specific settings. Common defaults include: + +- `workers`: Number of worker processes +- `timeout`: Request timeout in seconds +- `ssl_enabled`: Whether SSL is enabled +- `log_level`: Logging level +- `middleware`: List of middleware to apply + +### Environment-Specific Configuration + +Each environment (development, test, staging, production) has its own section under `environments`. Within each environment, there's a `servers` section that contains configurations for individual servers. + +### Server-Specific Configuration + +Each server within an environment has its own configuration. Server-specific settings override default settings. Common server settings include: + +- `host`: Server hostname or IP address +- `port`: Server port +- `workers`: Number of worker processes +- `timeout`: Request timeout in seconds +- `ssl_enabled`: Whether SSL is enabled +- `ssl_cert`: Path to SSL certificate (if SSL is enabled) +- `ssl_key`: Path to SSL private key (if SSL is enabled) +- `log_level`: Logging level +- `debug`: Whether debug mode is enabled +- `middleware`: List of middleware to apply + +## Using the Configuration + +### Server Configuration Manager + +The `ServerConfigManager` class in `tools/server_config_manager.py` provides methods to load, validate, and manage server configurations: + +```python +from tools.server_config_manager import ServerConfigManager + +# Create a manager instance +manager = ServerConfigManager() + +# Get all environments +environments = manager.get_environments() + +# Get all servers in an environment +servers = manager.get_servers('development') + +# Get configuration for a specific server +config = manager.get_server_config('development', 'main') + +# Validate a server configuration +errors = manager.validate_config('development', 'main') +``` + +### Convenience Functions + +For common operations, you can use the convenience functions: + +```python +from tools.server_config_manager import get_server_config, validate_server_config + +# Get configuration for a server +config = get_server_config('development', 'main') + +# Validate a server configuration +errors = validate_server_config('development', 'main') +``` + +## Command-Line Interface + +The server configuration manager includes a command-line interface for common operations: + +```bash +# List available environments +python tools/server_config_manager.py list-environments + +# List servers in an environment +python tools/server_config_manager.py list-servers development + +# Get configuration for a server +python tools/server_config_manager.py get-config development main + +# Validate a server configuration +python tools/server_config_manager.py validate development main +``` + +## Example Usage + +See `examples/mcp_server_example.py` for an example of how to use the server configuration with an MCP server: + +```bash +# Run the example with default settings (development environment, main server) +python examples/mcp_server_example.py + +# Run the example with a specific environment and server +python examples/mcp_server_example.py --env production --server analytics +``` + +## Best Practices + +1. **Environment Isolation**: Keep configurations for different environments separate and appropriate for their context. +2. **Security**: Use environment variables for sensitive data like SSL keys and passwords. +3. **Validation**: Always validate configurations before using them. +4. **Documentation**: Document any custom settings or requirements for specific servers. +5. **Version Control**: Keep the configuration file in version control, but use environment variables for sensitive data. + +## Troubleshooting + +### Common Issues + +1. **Configuration Not Found**: Ensure the configuration file exists at `config/mcp_servers.yaml`. +2. **Invalid Configuration**: Use the validation tools to check for configuration errors. +3. **Missing Required Fields**: Ensure all required fields (host, port, workers, timeout) are specified. +4. **SSL Configuration**: If SSL is enabled, ensure ssl_cert and ssl_key are specified. + +### Debugging + +Enable debug logging to see more information about the configuration loading process: + +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +## Extending the Configuration + +To add new configuration options: + +1. Update the YAML file with the new options +2. Update the validation logic in `ServerConfigManager.validate_config()` +3. Update the documentation to reflect the new options + +## Security Considerations + +1. **SSL Configuration**: Always enable SSL in production environments. +2. **Sensitive Data**: Use environment variables for sensitive data like SSL keys and passwords. +3. **Access Control**: Restrict access to the configuration file to authorized users. +4. **Validation**: Always validate configurations before using them to prevent security issues. \ No newline at end of file diff --git a/examples/mcp_server_example.py b/examples/mcp_server_example.py new file mode 100644 index 0000000..d0707dd --- /dev/null +++ b/examples/mcp_server_example.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +MCP Server Example + +This example demonstrates how to use the server configuration manager +to configure and start an MCP server. +""" + +import os +import sys +import asyncio +import logging +from typing import Dict, Any + +# Add parent directory to path to import tools +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from tools.server_config_manager import get_server_config + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Mock classes for demonstration (similar to the test classes) +class MCPServer: + def __init__(self, config: Dict[str, Any]): + self.config = config + self.middleware = [] + self.routes = {} + self.status = "initialized" + logger.info(f"Initialized MCPServer with config: {config}") + + def add_middleware(self, middleware): + self.middleware.append(middleware) + logger.info(f"Added middleware: {middleware.__class__.__name__}") + + def add_route(self, path: str, handler): + self.routes[path] = handler + logger.info(f"Added route: {path}") + + def is_configured(self) -> bool: + return all(k in self.config for k in ["host", "port", "workers", "timeout"]) + + def get_status(self) -> str: + return self.status + + async def start(self): + self.status = "running" + host = self.config.get("host", "localhost") + port = self.config.get("port", 8000) + logger.info(f"Server started on {host}:{port}") + + async def shutdown(self): + self.status = "stopped" + logger.info("Server shutdown") + +class SecurityMiddleware: + def __init__(self, **kwargs): + self.enabled = kwargs.get("enabled", True) + self.rate_limit = kwargs.get("rate_limit", {}) + self.allowed_hosts = kwargs.get("allowed_hosts", []) + logger.info(f"Initialized SecurityMiddleware: enabled={self.enabled}, rate_limit={self.rate_limit}, allowed_hosts={self.allowed_hosts}") + +class MetricsMiddleware: + def __init__(self, **kwargs): + self.enabled = kwargs.get("enabled", True) + self.enable_timing = kwargs.get("enable_timing", False) + self.track_endpoints = kwargs.get("track_endpoints", False) + self.metrics_path = kwargs.get("metrics_path", "/metrics") + self.metrics = {} + logger.info(f"Initialized MetricsMiddleware: enabled={self.enabled}, enable_timing={self.enable_timing}, track_endpoints={self.track_endpoints}") + +async def health_check(request): + """Simple health check endpoint.""" + return {"status": "healthy"} + +async def echo(request): + """Echo endpoint that returns the request data.""" + return {"echo": request.get("data", {})} + +async def main(): + """Main function to demonstrate server configuration and startup.""" + # Get environment from command line or use default + import argparse + parser = argparse.ArgumentParser(description='MCP Server Example') + parser.add_argument('--env', default='development', help='Environment (development, test, staging, production)') + parser.add_argument('--server', default='main', help='Server name (main, secondary, analytics, etc.)') + args = parser.parse_args() + + try: + # Get server configuration for the specified environment + config = get_server_config(args.env, args.server) + logger.info(f"Using server configuration for {args.server} in {args.env} environment") + + # Create server instance + server = MCPServer(config) + + # Add middleware from configuration + for middleware_config in config.get("middleware", []): + for middleware_name, middleware_options in middleware_config.items(): + if middleware_name == "SecurityMiddleware": + server.add_middleware(SecurityMiddleware(**middleware_options)) + elif middleware_name == "MetricsMiddleware": + server.add_middleware(MetricsMiddleware(**middleware_options)) + + # Add routes + server.add_route("/health", health_check) + server.add_route("/echo", echo) + + # Start server + if server.is_configured(): + await server.start() + + # In a real application, we would keep the server running + # For this example, we'll just wait a bit and then shut down + logger.info("Server running. Press Ctrl+C to stop...") + try: + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + logger.info("Received shutdown signal") + finally: + await server.shutdown() + else: + logger.error("Server is not properly configured") + + except Exception as e: + logger.error(f"Error starting server: {e}") + sys.exit(1) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 807c9a3..b1c9466 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ python-dotenv>=1.0.1 pydantic>=2.10.6 pytz>=2024.1 # Added for timezone support +PyYAML>=6.0.1 # Added for YAML configuration files # Web and HTTP aiohttp>=3.9.3 diff --git a/tools/server_config_manager.py b/tools/server_config_manager.py new file mode 100644 index 0000000..56605b0 --- /dev/null +++ b/tools/server_config_manager.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +Server Configuration Manager + +This utility loads and manages MCP server configurations from YAML files. +It provides functions to get server configurations for different environments +and validate the configurations. +""" + +import os +import yaml +import logging +from typing import Dict, Any, List, Optional +from pathlib import Path + +logger = logging.getLogger(__name__) + +class ServerConfigManager: + """Manages MCP server configurations.""" + + def __init__(self, config_path: Optional[str] = None): + """ + Initialize the server configuration manager. + + Args: + config_path: Path to the server configuration file. + If None, uses the default path. + """ + self.config_path = config_path or os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "config", + "mcp_servers.yaml" + ) + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + """ + Load the server configuration from the YAML file. + + Returns: + Dict containing the server configurations. + + Raises: + FileNotFoundError: If the configuration file doesn't exist. + yaml.YAMLError: If the YAML file is invalid. + """ + try: + with open(self.config_path, 'r') as f: + config = yaml.safe_load(f) + logger.info(f"Loaded server configuration from {self.config_path}") + return config + except FileNotFoundError: + logger.error(f"Server configuration file not found: {self.config_path}") + raise + except yaml.YAMLError as e: + logger.error(f"Invalid YAML in server configuration: {e}") + raise + + def get_environments(self) -> List[str]: + """ + Get a list of available environments. + + Returns: + List of environment names. + """ + return list(self.config.get('environments', {}).keys()) + + def get_servers(self, environment: str) -> Dict[str, Dict[str, Any]]: + """ + Get all server configurations for a specific environment. + + Args: + environment: The environment name (e.g., 'development', 'production'). + + Returns: + Dict of server configurations for the specified environment. + + Raises: + ValueError: If the environment doesn't exist. + """ + if environment not in self.get_environments(): + raise ValueError(f"Environment '{environment}' not found in configuration") + + return self.config.get('environments', {}).get(environment, {}).get('servers', {}) + + def get_server_config(self, environment: str, server_name: str) -> Dict[str, Any]: + """ + Get configuration for a specific server in an environment. + + Args: + environment: The environment name (e.g., 'development', 'production'). + server_name: The name of the server (e.g., 'main', 'analytics'). + + Returns: + Dict containing the server configuration with defaults applied. + + Raises: + ValueError: If the environment or server doesn't exist. + """ + servers = self.get_servers(environment) + if server_name not in servers: + raise ValueError(f"Server '{server_name}' not found in environment '{environment}'") + + # Get default configuration + default_config = self.config.get('default', {}) + + # Merge with server-specific configuration (server config takes precedence) + server_config = servers[server_name] + merged_config = {**default_config, **server_config} + + # Handle middleware separately to allow proper merging + if 'middleware' in default_config and 'middleware' in server_config: + # For now, just use the server's middleware if specified + # A more sophisticated merge could be implemented if needed + pass + + return merged_config + + def validate_config(self, environment: str, server_name: str) -> List[str]: + """ + Validate a server configuration. + + Args: + environment: The environment name. + server_name: The name of the server. + + Returns: + List of validation errors. Empty list if valid. + """ + errors = [] + try: + config = self.get_server_config(environment, server_name) + + # Check required fields + required_fields = ['host', 'port', 'workers', 'timeout'] + for field in required_fields: + if field not in config: + errors.append(f"Missing required field: {field}") + + # Validate SSL configuration + if config.get('ssl_enabled', False): + if 'ssl_cert' not in config: + errors.append("SSL enabled but ssl_cert not specified") + if 'ssl_key' not in config: + errors.append("SSL enabled but ssl_key not specified") + + # Validate port range + port = config.get('port') + if port is not None and (port < 1 or port > 65535): + errors.append(f"Invalid port number: {port}") + + # Validate workers + workers = config.get('workers') + if workers is not None and workers < 1: + errors.append(f"Invalid number of workers: {workers}") + + # Validate timeout + timeout = config.get('timeout') + if timeout is not None and timeout < 1: + errors.append(f"Invalid timeout: {timeout}") + + except ValueError as e: + errors.append(str(e)) + + return errors + + def save_config(self, config: Dict[str, Any], backup: bool = True) -> None: + """ + Save the configuration to the YAML file. + + Args: + config: The configuration to save. + backup: Whether to create a backup of the existing file. + + Raises: + IOError: If the file cannot be written. + """ + if backup and os.path.exists(self.config_path): + backup_path = f"{self.config_path}.bak" + try: + with open(self.config_path, 'r') as src, open(backup_path, 'w') as dst: + dst.write(src.read()) + logger.info(f"Created backup of server configuration at {backup_path}") + except IOError as e: + logger.error(f"Failed to create backup: {e}") + raise + + try: + with open(self.config_path, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + logger.info(f"Saved server configuration to {self.config_path}") + except IOError as e: + logger.error(f"Failed to save server configuration: {e}") + raise + +def get_server_config(environment: str, server_name: str = 'main') -> Dict[str, Any]: + """ + Convenience function to get a server configuration. + + Args: + environment: The environment name. + server_name: The name of the server (defaults to 'main'). + + Returns: + Dict containing the server configuration. + """ + manager = ServerConfigManager() + return manager.get_server_config(environment, server_name) + +def validate_server_config(environment: str, server_name: str = 'main') -> List[str]: + """ + Convenience function to validate a server configuration. + + Args: + environment: The environment name. + server_name: The name of the server (defaults to 'main'). + + Returns: + List of validation errors. Empty list if valid. + """ + manager = ServerConfigManager() + return manager.validate_config(environment, server_name) + +if __name__ == "__main__": + import argparse + import sys + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Parse command line arguments + parser = argparse.ArgumentParser(description='MCP Server Configuration Manager') + subparsers = parser.add_subparsers(dest='command', help='Command to execute') + + # List environments command + list_env_parser = subparsers.add_parser('list-environments', help='List available environments') + + # List servers command + list_servers_parser = subparsers.add_parser('list-servers', help='List servers in an environment') + list_servers_parser.add_argument('environment', help='Environment name') + + # Get server config command + get_config_parser = subparsers.add_parser('get-config', help='Get server configuration') + get_config_parser.add_argument('environment', help='Environment name') + get_config_parser.add_argument('server', help='Server name', default='main', nargs='?') + + # Validate server config command + validate_parser = subparsers.add_parser('validate', help='Validate server configuration') + validate_parser.add_argument('environment', help='Environment name') + validate_parser.add_argument('server', help='Server name', default='main', nargs='?') + + args = parser.parse_args() + + try: + manager = ServerConfigManager() + + if args.command == 'list-environments': + environments = manager.get_environments() + print("Available environments:") + for env in environments: + print(f" - {env}") + + elif args.command == 'list-servers': + servers = manager.get_servers(args.environment) + print(f"Servers in environment '{args.environment}':") + for server_name in servers: + print(f" - {server_name}") + + elif args.command == 'get-config': + config = manager.get_server_config(args.environment, args.server) + print(f"Configuration for server '{args.server}' in environment '{args.environment}':") + print(yaml.dump(config, default_flow_style=False)) + + elif args.command == 'validate': + errors = manager.validate_config(args.environment, args.server) + if errors: + print(f"Validation errors for server '{args.server}' in environment '{args.environment}':") + for error in errors: + print(f" - {error}") + sys.exit(1) + else: + print(f"Server '{args.server}' in environment '{args.environment}' has a valid configuration.") + + else: + parser.print_help() + sys.exit(1) + + except Exception as e: + logger.error(f"Error: {e}") + sys.exit(1) \ No newline at end of file