diff --git a/docs/yml-specification.md b/docs/yml-specification.md index 0e5fcdc..31f7b24 100644 --- a/docs/yml-specification.md +++ b/docs/yml-specification.md @@ -100,6 +100,51 @@ properties: description: "Tool-specific environment variables" additionalProperties: type: string + name: + type: string + description: "Custom tool name (defaults to function name)" + version: + type: string + description: "Tool version" + author: + type: string + description: "Tool author" + tags: + type: array + description: "Tool tags for categorization" + items: + type: string + category: + type: string + description: "Tool category" + timeout: + type: integer + description: "Tool timeout in seconds (default: 300)" + retries: + type: integer + description: "Number of retries on failure (default: 0)" + examples: + type: array + description: "Usage examples" + items: + type: object + properties: + description: + type: string + description: "Example description" + command: + type: string + description: "Example command" + dependencies: + type: array + description: "Required system dependencies" + items: + type: string + permissions: + type: array + description: "Required permissions" + items: + type: string resources: type: object @@ -640,6 +685,102 @@ prompts: default: "" ``` +## Enhanced Tool Features + +### Tool Metadata and Organization + +```yaml +tools: + DatabaseBackup: + name: "db-backup" # Custom tool name + version: "2.1.0" + author: "DevOps Team" + category: "database" + tags: ["backup", "database", "maintenance"] + cmd: "mysqldump {{ database }} > {{ backup_file }}" + desc: "Create a database backup" + timeout: 600 # 10 minutes + retries: 2 + dependencies: ["mysqldump", "mysql"] + permissions: ["database:read", "file:write"] + examples: + - description: "Backup production database" + command: "db-backup --database=prod_db --backup_file=prod_backup.sql" + - description: "Backup with timestamp" + command: "db-backup --database=test_db --backup_file=test_$(date +%Y%m%d).sql" + args: + - name: database + help: "Database name to backup" + type: string + - name: backup_file + help: "Output backup file path" + type: string + default: "backup.sql" +``` + +### Advanced Tool Configuration + +```yaml +tools: + SystemMonitor: + name: "system-monitor" + version: "1.0.0" + author: "System Admin" + category: "monitoring" + tags: ["system", "monitoring", "health"] + cmd: | + {% if metric == "cpu" %} + top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 + {% elif metric == "memory" %} + free | grep Mem | awk '{printf "%.2f", $3/$2 * 100.0}' + {% elif metric == "disk" %} + df -h {{ path or "/" }} | awk 'NR==2{print $5}' | cut -d'%' -f1 + {% endif %} + desc: "Monitor system metrics (CPU, memory, disk usage)" + timeout: 30 + retries: 1 + dependencies: ["top", "free", "df"] + examples: + - description: "Check CPU usage" + command: "system-monitor --metric=cpu" + - description: "Check memory usage" + command: "system-monitor --metric=memory" + - description: "Check disk usage for specific path" + command: "system-monitor --metric=disk --path=/var" + args: + - name: metric + help: "Metric to monitor" + type: string + choices: ["cpu", "memory", "disk"] + - name: path + help: "Path for disk usage check" + type: string + default: "/" +``` + +### Tool with Custom Name and Tags + +```yaml +tools: + FileSearch: + name: "find-files" # Custom name different from function name + tags: ["file", "search", "utility"] + cmd: "find {{ path }} -name '{{ pattern }}' -type f" + desc: "Search for files matching a pattern" + examples: + - description: "Find Python files" + command: "find-files --path=/home/user --pattern='*.py'" + args: + - name: path + help: "Directory to search" + type: string + default: "." + - name: pattern + help: "File pattern to match" + type: string + default: "*" +``` + ## Best Practices 1. **Use descriptive names**: Choose clear, meaningful names for tools, resources, prompts, and arguments @@ -650,4 +791,10 @@ prompts: 6. **Test templates**: Validate Jinja2 templates before deployment 7. **Resource URIs**: Use meaningful URIs for resources (e.g., `system://info`, `file://config`) 8. **Prompt clarity**: Make prompts clear and specific with good structure -9. **MIME types**: Specify appropriate MIME types for resources when possible \ No newline at end of file +9. **MIME types**: Specify appropriate MIME types for resources when possible +10. **Use metadata**: Leverage version, author, category, and tags for better organization +11. **Set timeouts**: Configure appropriate timeouts for long-running operations +12. **Handle retries**: Use retries for operations that might fail due to temporary issues +13. **Document dependencies**: List all required system dependencies +14. **Provide examples**: Include usage examples to help users understand tool functionality +15. **Specify permissions**: Document required permissions for security and access control \ No newline at end of file diff --git a/enhanced_server/README.md b/enhanced_server/README.md new file mode 100644 index 0000000..a0b1aa9 --- /dev/null +++ b/enhanced_server/README.md @@ -0,0 +1,123 @@ +# enhanced-mcp-server + +Enhanced MCP server demonstrating new features + +## Installation + +### Option 1: Using Virtual Environment (Recommended) + +1. **Create a virtual environment**: +```bash +python3 -m venv venv +``` + + +2. **Activate the virtual environment**: + ```bash + source venv/bin/activate + ``` + +3. **Install dependencies**: +```bash +pip install -r requirements.txt +``` + +4. **Run the server**: +```bash +python enhanced_mcp_server_server.py +``` + +5. **Deactivate when done** (optional): +```bash +deactivate +``` + +### Option 2: System-wide Installation + +1. **Install dependencies**: +```bash +pip install -r requirements.txt +``` + +2. **Run the server**: +```bash +python enhanced_mcp_server_server.py +``` + + +## Tools + + +### DatabaseBackup + +Create a database backup with compression and validation + +**Function**: `databasebackup` + +**Arguments**: +- `database` (string): Database name- `backup_file` (string): Path to a file or directory +**Command**: `mysqldump {{ database }} > {{ backup_file }}` + + +### SystemMonitor + +Monitor system metrics (CPU, memory, disk usage) + +**Function**: `systemmonitor` + +**Arguments**: +- `metric` (string): Metric to monitor [choices: ['cpu', 'memory', 'disk']]- `path` (string): Path for disk usage check [default: /] +**Command**: `{% if metric == "cpu" %} +top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 +{% elif metric == "memory" %} +free | grep Mem | awk '{printf "%.2f", $3/$2 * 100.0}' +{% elif metric == "disk" %} +df -h {{ path or "/" }} | awk 'NR==2{print $5}' | cut -d'%' -f1 +{% endif %} +` + + +### FileSearch + +Search for files matching a pattern with optional size filtering + +**Function**: `filesearch` + +**Arguments**: +- `path` (string): Path to a file or directory- `pattern` (string): File pattern to match [default: *]- `size` (string): File size filter (e.g., +100M, -1G) +**Command**: `find {{ path }} -name '{{ pattern }}' -type f {% if size %} -size {{ size }}{% endif %}` + + +### LogAnalyzer + +Analyze log files with various output formats and filtering + +**Function**: `loganalyzer` + +**Arguments**: +- `log_file` (string): Path to a file or directory- `format` (string): Output format [default: raw] [choices: ['text', 'json', 'raw']]- `lines` (number): Number of lines to show [default: 100]- `pattern` (string): Search pattern for text format- `filter` (string): JQ filter for JSON format +**Command**: `{% if format == "json" %} +jq '{{ filter }}' {{ log_file }} +{% elif format == "text" %} +grep "{{ pattern }}" {{ log_file }} | tail -{{ lines or 100 }} +{% else %} +tail -{{ lines or 100 }} {{ log_file }} +{% endif %} +` + + +## Configuration + +This server was generated from a YAML configuration file. The server exposes shell commands as MCP tools with the following features: + +- Jinja2 template support for dynamic command generation +- Argument validation with patterns and choices +- Environment variable support +- Error handling and timeout protection + +## Server Information + +- **Name**: enhanced-mcp-server +- **Version**: 2.0.0 +- **Description**: Enhanced MCP server demonstrating new features +- **Tools**: 4 \ No newline at end of file diff --git a/enhanced_server/enhanced_mcp_server_server.py b/enhanced_server/enhanced_mcp_server_server.py new file mode 100644 index 0000000..da6600f --- /dev/null +++ b/enhanced_server/enhanced_mcp_server_server.py @@ -0,0 +1,586 @@ +"""Generated FastMCP server from YAML configuration.""" + +import os +import subprocess +import tempfile +import shlex +from datetime import datetime +from typing import Any, Dict, List, Optional +from fastmcp import FastMCP +from jinja2 import Template, Environment + +def execute_command(cmd: str, env_vars: Optional[Dict[str, str]] = None, timeout: int = 300) -> Dict[str, Any]: + """Execute a shell command and return the result.""" + try: + # Prepare environment + env = os.environ.copy() + if env_vars: + env.update(env_vars) + + # Execute command + result = subprocess.run( + cmd, + shell=True, + capture_output=True, + text=True, + env=env, + timeout=timeout + ) + + return { + "success": result.returncode == 0, + "stdout": result.stdout, + "stderr": result.stderr, + "returncode": result.returncode + } + except subprocess.TimeoutExpired: + return { + "success": False, + "stdout": "", + "stderr": "Command timed out after 5 minutes", + "returncode": -1 + } + except Exception as e: + return { + "success": False, + "stdout": "", + "stderr": str(e), + "returncode": -1 + } + +def render_template(template_str: str, **kwargs) -> str: + """Render Jinja2 template with provided variables.""" + try: + # Add built-in functions + context = { + 'now': datetime.now, + **kwargs + } + + template = Template(template_str) + return template.render(**context) + except Exception as e: + raise ValueError(f"Template rendering error: {e}") + +# Initialize FastMCP server +mcp = FastMCP(name="enhanced-mcp-server") + +# Server configuration +SERVER_NAME = "enhanced-mcp-server" +SERVER_DESC = "Enhanced MCP server demonstrating new features" +SERVER_VERSION = "2.0.0" +os.environ["LOG_LEVEL"] = "INFO" +os.environ["MAX_CONCURRENT"] = "10" + + +@mcp.tool(name="db-backup", tags=['backup', 'database', 'maintenance']) +def databasebackup(database: str, backup_file: str) -> Dict[str, Any]: + """ + Create a database backup with compression and validation + + Metadata: + - Version: 2.1.0 + - Author: DevOps Team + - Category: database + + Parameters: + - database (string): Database name + Pattern: ^[a-zA-Z][a-zA-Z0-9_]*$ + - backup_file (string): Path to a file or directory + Pattern: ^[^\0]+$ + + Examples: + - Backup production database: + db-backup --database=prod_db --backup_file=prod_backup.sql + - Backup with timestamp: + db-backup --database=test_db --backup_file=test_$(date +%Y%m%d).sql + + Dependencies: + - mysqldump + - mysql + + Required Permissions: + - database:read + - file:write + + Returns: + Dict[str, Any]: Command execution result with 'success', 'stdout', 'stderr', and 'returncode' fields. + """ + try: + # Check dependencies + missing_deps = [] + try: + subprocess.run(["which", "mysqldump"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("mysqldump") + try: + subprocess.run(["which", "mysql"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("mysql") + if missing_deps: + return { + "success": False, + "stdout": "", + "stderr": f"Missing dependencies: {', '.join(missing_deps)}", + "returncode": -1 + } + # Validate database pattern + import re + if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", str(database)): + raise ValueError(f"Invalid database: must match pattern ^[a-zA-Z][a-zA-Z0-9_]*$") + # Validate backup_file pattern + import re + if not re.match(r"^[^\0]+$", str(backup_file)): + raise ValueError(f"Invalid backup_file: must match pattern ^[^\0]+$") + + # Render command template + cmd = render_template("""mysqldump {{ database }} > {{ backup_file }}""", database=database, backup_file=backup_file) + + # Execute command with retries + env_vars = {} + + max_retries = 2 + timeout = 600 + + for attempt in range(max_retries + 1): + try: + result = execute_command(cmd, env_vars, timeout=timeout) + if result["success"] or attempt == max_retries: + return result + else: + if attempt < max_retries: + import time + time.sleep(1) # Brief delay before retry + except Exception as e: + if attempt == max_retries: + raise e + import time + time.sleep(1) # Brief delay before retry + + return result + except Exception as e: + return { + "success": False, + "stdout": "", + "stderr": f"Error in DatabaseBackup: {str(e)}", + "returncode": -1 + } + + +@mcp.tool(name="system-monitor", tags=['system', 'monitoring', 'health']) +def systemmonitor(metric: str, path: str = "/") -> Dict[str, Any]: + """ + Monitor system metrics (CPU, memory, disk usage) + + Metadata: + - Version: 1.0.0 + - Author: System Admin + - Category: monitoring + + Parameters: + - metric (string): Metric to monitor + Allowed values: cpu, memory, disk + - path (string): Path for disk usage check + Default: / + + Examples: + - Check CPU usage: + system-monitor --metric=cpu + - Check memory usage: + system-monitor --metric=memory + - Check disk usage for specific path: + system-monitor --metric=disk --path=/var + + Dependencies: + - top + - free + - df + + Returns: + Dict[str, Any]: Command execution result with 'success', 'stdout', 'stderr', and 'returncode' fields. + """ + try: + # Check dependencies + missing_deps = [] + try: + subprocess.run(["which", "top"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("top") + try: + subprocess.run(["which", "free"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("free") + try: + subprocess.run(["which", "df"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("df") + if missing_deps: + return { + "success": False, + "stdout": "", + "stderr": f"Missing dependencies: {', '.join(missing_deps)}", + "returncode": -1 + } + # Validate metric choices + if metric not in ['cpu', 'memory', 'disk']: + raise ValueError(f"Invalid metric: must be one of ['cpu', 'memory', 'disk']") + + # Render command template + cmd = render_template("""{% if metric == \"cpu\" %} +top -bn1 | grep \"Cpu(s)\" | awk '{print $2}' | cut -d'%' -f1 +{% elif metric == \"memory\" %} +free | grep Mem | awk '{printf \"%.2f\", $3/$2 * 100.0}' +{% elif metric == \"disk\" %} +df -h {{ path or \"/\" }} | awk 'NR==2{print $5}' | cut -d'%' -f1 +{% endif %} +""", metric=metric, path=path) + + # Execute command with retries + env_vars = {} + + max_retries = 1 + timeout = 30 + + for attempt in range(max_retries + 1): + try: + result = execute_command(cmd, env_vars, timeout=timeout) + if result["success"] or attempt == max_retries: + return result + else: + if attempt < max_retries: + import time + time.sleep(1) # Brief delay before retry + except Exception as e: + if attempt == max_retries: + raise e + import time + time.sleep(1) # Brief delay before retry + + return result + except Exception as e: + return { + "success": False, + "stdout": "", + "stderr": f"Error in SystemMonitor: {str(e)}", + "returncode": -1 + } + + +@mcp.tool(name="find-files", tags=['file', 'search', 'utility']) +def filesearch(path: str, size: str, pattern: str = "*") -> Dict[str, Any]: + """ + Search for files matching a pattern with optional size filtering + + Metadata: + - Version: 1.0.0 + - Author: File System Team + - Category: file-system + + Parameters: + - path (string): Path to a file or directory + Pattern: ^[^\0]+$ + - pattern (string): File pattern to match + Default: * + - size (string): File size filter (e.g., +100M, -1G) + + Examples: + - Find Python files: + find-files --path=/home/user --pattern='*.py' + - Find large files: + find-files --path=/var --pattern='*' --size='+100M' + + Dependencies: + - find + + Returns: + Dict[str, Any]: Command execution result with 'success', 'stdout', 'stderr', and 'returncode' fields. + """ + try: + # Check dependencies + missing_deps = [] + try: + subprocess.run(["which", "find"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("find") + if missing_deps: + return { + "success": False, + "stdout": "", + "stderr": f"Missing dependencies: {', '.join(missing_deps)}", + "returncode": -1 + } + # Validate path pattern + import re + if not re.match(r"^[^\0]+$", str(path)): + raise ValueError(f"Invalid path: must match pattern ^[^\0]+$") + + # Render command template + cmd = render_template("""find {{ path }} -name '{{ pattern }}' -type f {% if size %} -size {{ size }}{% endif %}""", path=path, pattern=pattern, size=size) + + # Execute command with retries + env_vars = {} + + max_retries = 1 + timeout = 120 + + for attempt in range(max_retries + 1): + try: + result = execute_command(cmd, env_vars, timeout=timeout) + if result["success"] or attempt == max_retries: + return result + else: + if attempt < max_retries: + import time + time.sleep(1) # Brief delay before retry + except Exception as e: + if attempt == max_retries: + raise e + import time + time.sleep(1) # Brief delay before retry + + return result + except Exception as e: + return { + "success": False, + "stdout": "", + "stderr": f"Error in FileSearch: {str(e)}", + "returncode": -1 + } + + +@mcp.tool(name="log-analyzer", tags=['logs', 'analysis', 'debugging']) +def loganalyzer(log_file: str, pattern: str, filter: str, format: str = "raw", lines: float = 100) -> Dict[str, Any]: + """ + Analyze log files with various output formats and filtering + + Metadata: + - Version: 1.2.0 + - Author: Monitoring Team + - Category: logging + + Parameters: + - log_file (string): Path to a file or directory + Pattern: ^[^\0]+$ + - format (string): Output format + Default: raw + Allowed values: text, json, raw + - lines (number): Number of lines to show + Default: 100 + - pattern (string): Search pattern for text format + - filter (string): JQ filter for JSON format + + Examples: + - Show last 50 lines of error log: + log-analyzer --log_file=/var/log/error.log --lines=50 + - Filter JSON logs for errors: + log-analyzer --log_file=/var/log/app.json --format=json --filter='.level == "error"' + - Search for specific pattern: + log-analyzer --log_file=/var/log/access.log --format=text --pattern='404' + + Dependencies: + - jq + - grep + - tail + + Required Permissions: + - file:read + + Returns: + Dict[str, Any]: Command execution result with 'success', 'stdout', 'stderr', and 'returncode' fields. + """ + try: + # Check dependencies + missing_deps = [] + try: + subprocess.run(["which", "jq"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("jq") + try: + subprocess.run(["which", "grep"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("grep") + try: + subprocess.run(["which", "tail"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("tail") + if missing_deps: + return { + "success": False, + "stdout": "", + "stderr": f"Missing dependencies: {', '.join(missing_deps)}", + "returncode": -1 + } + # Validate log_file pattern + import re + if not re.match(r"^[^\0]+$", str(log_file)): + raise ValueError(f"Invalid log_file: must match pattern ^[^\0]+$") + # Validate format choices + if format not in ['text', 'json', 'raw']: + raise ValueError(f"Invalid format: must be one of ['text', 'json', 'raw']") + + # Render command template + cmd = render_template("""{% if format == \"json\" %} +jq '{{ filter }}' {{ log_file }} +{% elif format == \"text\" %} +grep \"{{ pattern }}\" {{ log_file }} | tail -{{ lines or 100 }} +{% else %} +tail -{{ lines or 100 }} {{ log_file }} +{% endif %} +""", log_file=log_file, format=format, lines=lines, pattern=pattern, filter=filter) + + # Execute command with retries + env_vars = {} + + max_retries = 0 + timeout = 60 + + for attempt in range(max_retries + 1): + try: + result = execute_command(cmd, env_vars, timeout=timeout) + if result["success"] or attempt == max_retries: + return result + else: + if attempt < max_retries: + import time + time.sleep(1) # Brief delay before retry + except Exception as e: + if attempt == max_retries: + raise e + import time + time.sleep(1) # Brief delay before retry + + return result + except Exception as e: + return { + "success": False, + "stdout": "", + "stderr": f"Error in LogAnalyzer: {str(e)}", + "returncode": -1 + } + + +# Resource handlers + +@mcp.resource("system://info") +def systeminfo() -> str: + """ + Current system information and status + + Returns: + str: The resource content. + """ + try: + + # Execute command + cmd = render_template("""echo \"=== System Information ===\" +uname -a +echo \"\" +echo \"=== CPU Info ===\" +lscpu | grep -E \"(Model name|CPU\(s\)|Thread|Core)\" +echo \"\" +echo \"=== Memory Info ===\" +free -h +echo \"\" +echo \"=== Disk Usage ===\" +df -h +""", ) + env_vars = {} + result = execute_command(cmd, env_vars) + if not result["success"]: + raise ValueError(f"Command failed: {result['stderr']}") + content = result["stdout"] + + return content + except Exception as e: + raise ValueError(f"Error in SystemInfo: {str(e)}") + + +@mcp.resource("template://{template_name}") +def configtemplate(template_name: str) -> str: + """ + Load configuration template + + Parameters: + - template_name (string): Template name + Allowed values: nginx, apache, mysql, redis + + Returns: + str: The resource content. + """ + try: + # Validate template_name choices + if template_name not in ['nginx', 'apache', 'mysql', 'redis']: + raise ValueError(f"Invalid template_name: must be one of ['nginx', 'apache', 'mysql', 'redis']") + + # Read from file + file_path = render_template("""templates/{{ template_name }}.conf""", template_name=template_name) + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + except FileNotFoundError: + raise ValueError(f"Resource file not found: {file_path}") + except Exception as e: + raise ValueError(f"Error reading resource file {file_path}: {str(e)}") + + return content + except Exception as e: + raise ValueError(f"Error in ConfigTemplate: {str(e)}") + + +# Prompt handlers + +@mcp.prompt() +def codereview(code: str, language: str = "python", focus_areas: str = "", include_examples: bool = True) -> str: + """ + Generate a comprehensive code review prompt + + Parameters: + - language (string): Programming language + Default: python + Allowed values: python, javascript, java, go, rust, typescript + - code (string): Code to review + - focus_areas (string): Specific areas to focus on + Default: + - include_examples (boolean): Include improvement examples + Default: True + + Returns: + str: The generated prompt content. + """ + try: + # Validate language choices + if language not in ['python', 'javascript', 'java', 'go', 'rust', 'typescript']: + raise ValueError(f"Invalid language: must be one of ['python', 'javascript', 'java', 'go', 'rust', 'typescript']") + + # Use direct template content + content = render_template("""You are a senior software engineer reviewing the following {{ language }} code: + +```{{ language }} +{{ code }} +``` + +Please provide a thorough code review focusing on: +- Code quality and best practices +- Performance implications +- Security considerations +- Maintainability and readability +- Test coverage recommendations + +{% if focus_areas %} +Pay special attention to: {{ focus_areas }} +{% endif %} + +{% if include_examples %} +Please provide specific examples of improvements where applicable. +{% endif %} +""", language=language, code=code, focus_areas=focus_areas, include_examples=include_examples) + + return content + except Exception as e: + raise ValueError(f"Error in CodeReview: {str(e)}") + + +if __name__ == "__main__": + print(f"Starting {SERVER_NAME} v{SERVER_VERSION}") + print(f"Description: {SERVER_DESC}") + mcp.run() \ No newline at end of file diff --git a/enhanced_server/requirements.txt b/enhanced_server/requirements.txt new file mode 100644 index 0000000..4b34ceb --- /dev/null +++ b/enhanced_server/requirements.txt @@ -0,0 +1,3 @@ +fastmcp>=0.1.0 +jinja2>=3.0.0 +pyyaml>=6.0 \ No newline at end of file diff --git a/example_enhanced_config.yml b/example_enhanced_config.yml new file mode 100644 index 0000000..75f5904 --- /dev/null +++ b/example_enhanced_config.yml @@ -0,0 +1,229 @@ +server: + name: "enhanced-mcp-server" + desc: "Enhanced MCP server demonstrating new features" + version: "2.0.0" + env: + LOG_LEVEL: "INFO" + MAX_CONCURRENT: "10" + +args: + FilePath: + help: "Path to a file or directory" + type: string + pattern: "^[^\\0]+$" + + DatabaseName: + help: "Database name" + type: string + pattern: "^[a-zA-Z][a-zA-Z0-9_]*$" + +tools: + DatabaseBackup: + name: "db-backup" + version: "2.1.0" + author: "DevOps Team" + category: "database" + tags: ["backup", "database", "maintenance"] + cmd: "mysqldump {{ database }} > {{ backup_file }}" + desc: "Create a database backup with compression and validation" + timeout: 600 # 10 minutes + retries: 2 + dependencies: ["mysqldump", "mysql"] + permissions: ["database:read", "file:write"] + examples: + - description: "Backup production database" + command: "db-backup --database=prod_db --backup_file=prod_backup.sql" + - description: "Backup with timestamp" + command: "db-backup --database=test_db --backup_file=test_$(date +%Y%m%d).sql" + args: + - name: database + help: "Database name to backup" + ref: DatabaseName + - name: backup_file + help: "Output backup file path" + ref: FilePath + default: "backup.sql" + + SystemMonitor: + name: "system-monitor" + version: "1.0.0" + author: "System Admin" + category: "monitoring" + tags: ["system", "monitoring", "health"] + cmd: | + {% if metric == "cpu" %} + top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 + {% elif metric == "memory" %} + free | grep Mem | awk '{printf "%.2f", $3/$2 * 100.0}' + {% elif metric == "disk" %} + df -h {{ path or "/" }} | awk 'NR==2{print $5}' | cut -d'%' -f1 + {% endif %} + desc: "Monitor system metrics (CPU, memory, disk usage)" + timeout: 30 + retries: 1 + dependencies: ["top", "free", "df"] + examples: + - description: "Check CPU usage" + command: "system-monitor --metric=cpu" + - description: "Check memory usage" + command: "system-monitor --metric=memory" + - description: "Check disk usage for specific path" + command: "system-monitor --metric=disk --path=/var" + args: + - name: metric + help: "Metric to monitor" + type: string + choices: ["cpu", "memory", "disk"] + - name: path + help: "Path for disk usage check" + type: string + default: "/" + + FileSearch: + name: "find-files" + version: "1.0.0" + author: "File System Team" + category: "file-system" + tags: ["file", "search", "utility"] + cmd: "find {{ path }} -name '{{ pattern }}' -type f {% if size %} -size {{ size }}{% endif %}" + desc: "Search for files matching a pattern with optional size filtering" + timeout: 120 + retries: 1 + dependencies: ["find"] + examples: + - description: "Find Python files" + command: "find-files --path=/home/user --pattern='*.py'" + - description: "Find large files" + command: "find-files --path=/var --pattern='*' --size='+100M'" + args: + - name: path + help: "Directory to search" + ref: FilePath + default: "." + - name: pattern + help: "File pattern to match" + type: string + default: "*" + - name: size + help: "File size filter (e.g., +100M, -1G)" + type: string + + LogAnalyzer: + name: "log-analyzer" + version: "1.2.0" + author: "Monitoring Team" + category: "logging" + tags: ["logs", "analysis", "debugging"] + cmd: | + {% if format == "json" %} + jq '{{ filter }}' {{ log_file }} + {% elif format == "text" %} + grep "{{ pattern }}" {{ log_file }} | tail -{{ lines or 100 }} + {% else %} + tail -{{ lines or 100 }} {{ log_file }} + {% endif %} + desc: "Analyze log files with various output formats and filtering" + timeout: 60 + retries: 0 + dependencies: ["jq", "grep", "tail"] + permissions: ["file:read"] + examples: + - description: "Show last 50 lines of error log" + command: "log-analyzer --log_file=/var/log/error.log --lines=50" + - description: "Filter JSON logs for errors" + command: "log-analyzer --log_file=/var/log/app.json --format=json --filter='.level == \"error\"'" + - description: "Search for specific pattern" + command: "log-analyzer --log_file=/var/log/access.log --format=text --pattern='404'" + args: + - name: log_file + help: "Path to log file" + ref: FilePath + - name: format + help: "Output format" + type: string + choices: ["text", "json", "raw"] + default: "raw" + - name: lines + help: "Number of lines to show" + type: number + default: 100 + - name: pattern + help: "Search pattern for text format" + type: string + - name: filter + help: "JQ filter for JSON format" + type: string + +resources: + SystemInfo: + uri: "system://info" + name: "System Information" + description: "Current system information and status" + mime_type: "text/plain" + cmd: | + echo "=== System Information ===" + uname -a + echo "" + echo "=== CPU Info ===" + lscpu | grep -E "(Model name|CPU\(s\)|Thread|Core)" + echo "" + echo "=== Memory Info ===" + free -h + echo "" + echo "=== Disk Usage ===" + df -h + dependencies: ["uname", "lscpu", "free", "df"] + + ConfigTemplate: + uri: "template://{{ template_name }}" + name: "Configuration Template" + description: "Load configuration template" + mime_type: "text/plain" + file: "templates/{{ template_name }}.conf" + args: + - name: template_name + help: "Template name" + type: string + choices: ["nginx", "apache", "mysql", "redis"] + +prompts: + CodeReview: + name: "Code Review Prompt" + description: "Generate a comprehensive code review prompt" + template: | + You are a senior software engineer reviewing the following {{ language }} code: + + ```{{ language }} + {{ code }} + ``` + + Please provide a thorough code review focusing on: + - Code quality and best practices + - Performance implications + - Security considerations + - Maintainability and readability + - Test coverage recommendations + + {% if focus_areas %} + Pay special attention to: {{ focus_areas }} + {% endif %} + + {% if include_examples %} + Please provide specific examples of improvements where applicable. + {% endif %} + args: + - name: language + help: "Programming language" + choices: ["python", "javascript", "java", "go", "rust", "typescript"] + default: "python" + - name: code + help: "Code to review" + type: string + - name: focus_areas + help: "Specific areas to focus on" + type: string + default: "" + - name: include_examples + help: "Include improvement examples" + type: boolean + default: true \ No newline at end of file diff --git a/shellmcp/models.py b/shellmcp/models.py index d706cf5..9906ecf 100644 --- a/shellmcp/models.py +++ b/shellmcp/models.py @@ -75,6 +75,18 @@ class ToolConfig(BaseModel): args: Optional[List[ToolArgument]] = Field(None, description="Argument definitions") env: Optional[Dict[str, str]] = Field(None, description="Tool-specific environment variables") + # Enhanced metadata fields + name: Optional[str] = Field(None, description="Custom tool name (defaults to function name)") + version: Optional[str] = Field(None, description="Tool version") + author: Optional[str] = Field(None, description="Tool author") + tags: Optional[List[str]] = Field(None, description="Tool tags for categorization") + category: Optional[str] = Field(None, description="Tool category") + timeout: Optional[int] = Field(None, description="Tool timeout in seconds (default: 300)") + retries: Optional[int] = Field(None, description="Number of retries on failure (default: 0)") + examples: Optional[List[Dict[str, Any]]] = Field(None, description="Usage examples") + dependencies: Optional[List[str]] = Field(None, description="Required system dependencies") + permissions: Optional[List[str]] = Field(None, description="Required permissions") + model_config = {"populate_by_name": True} diff --git a/shellmcp/template_utils.py b/shellmcp/template_utils.py index b9cf399..50abb13 100644 --- a/shellmcp/template_utils.py +++ b/shellmcp/template_utils.py @@ -39,10 +39,41 @@ def escape_double_quotes(text: str) -> str: return text.replace('"', '\\"') +def format_examples(examples: list) -> str: + """Format examples for documentation.""" + if not examples: + return "" + + formatted = [] + for i, example in enumerate(examples, 1): + desc = example.get('description', f'Example {i}') + cmd = example.get('command', 'N/A') + formatted.append(f" {i}. {desc}: {cmd}") + + return '\n'.join(formatted) + + +def format_dependencies(deps: list) -> str: + """Format dependencies list.""" + if not deps: + return "" + return '\n'.join(f" - {dep}" for dep in deps) + + +def format_permissions(perms: list) -> str: + """Format permissions list.""" + if not perms: + return "" + return '\n'.join(f" - {perm}" for perm in perms) + + def get_jinja_filters(): """Get dictionary of custom Jinja2 filters.""" return { 'python_type': python_type, 'python_value': python_value, 'escape_double_quotes': escape_double_quotes, + 'format_examples': format_examples, + 'format_dependencies': format_dependencies, + 'format_permissions': format_permissions, } \ No newline at end of file diff --git a/shellmcp/templates/server.py.j2 b/shellmcp/templates/server.py.j2 index 4a6858c..8d0fbb2 100644 --- a/shellmcp/templates/server.py.j2 +++ b/shellmcp/templates/server.py.j2 @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional from fastmcp import FastMCP from jinja2 import Template, Environment -def execute_command(cmd: str, env_vars: Optional[Dict[str, str]] = None) -> Dict[str, Any]: +def execute_command(cmd: str, env_vars: Optional[Dict[str, str]] = None, timeout: int = 300) -> Dict[str, Any]: """Execute a shell command and return the result.""" try: # Prepare environment @@ -24,7 +24,7 @@ def execute_command(cmd: str, env_vars: Optional[Dict[str, str]] = None) -> Dict capture_output=True, text=True, env=env, - timeout=300 # 5 minute timeout + timeout=timeout ) return { @@ -91,10 +91,28 @@ os.environ["{{ key }}"] = "{{ value }}" {% endfor %} {% set param_str = (params_without_defaults + params_with_defaults)|join(", ") %} -@mcp.tool() +{% if tool.name %} +@mcp.tool(name="{{ tool.name }}"{% if tool.tags %}, tags={{ tool.tags }}{% endif %}) +{% else %} +@mcp.tool(){% if tool.tags %} +# Tags: {{ tool.tags|join(', ') }}{% endif %} +{% endif %} def {{ func_name }}({{ param_str }}) -> Dict[str, Any]: """ {{ tool.desc }} +{% if tool.version or tool.author or tool.category %} + + Metadata: +{% if tool.version %} + - Version: {{ tool.version }} +{% endif %} +{% if tool.author %} + - Author: {{ tool.author }} +{% endif %} +{% if tool.category %} + - Category: {{ tool.category }} +{% endif %} +{% endif %} {% if tool.help_cmd and help_outputs.get(tool_name) %} Help: @@ -118,12 +136,51 @@ def {{ func_name }}({{ param_str }}) -> Dict[str, Any]: Pattern: {{ arg.pattern }} {% endif %} {% endfor %} +{% endif %} +{% if tool.examples %} + + Examples: +{% for example in tool.examples %} + - {{ example.description or 'Example' }}: + {{ example.command or 'N/A' }} +{% endfor %} +{% endif %} +{% if tool.dependencies %} + + Dependencies: +{% for dep in tool.dependencies %} + - {{ dep }} +{% endfor %} +{% endif %} +{% if tool.permissions %} + + Required Permissions: +{% for perm in tool.permissions %} + - {{ perm }} +{% endfor %} {% endif %} Returns: Dict[str, Any]: Command execution result with 'success', 'stdout', 'stderr', and 'returncode' fields. """ try: +{% if tool.dependencies %} + # Check dependencies + missing_deps = [] +{% for dep in tool.dependencies %} + try: + subprocess.run(["which", "{{ dep }}"], check=True, capture_output=True) + except subprocess.CalledProcessError: + missing_deps.append("{{ dep }}") +{% endfor %} + if missing_deps: + return { + "success": False, + "stdout": "", + "stderr": f"Missing dependencies: {', '.join(missing_deps)}", + "returncode": -1 + } +{% endif %} {% for arg in resolved_args %} {% if arg.pattern %} # Validate {{ arg.name }} pattern @@ -141,14 +198,31 @@ def {{ func_name }}({{ param_str }}) -> Dict[str, Any]: # Render command template cmd = render_template("""{{ tool.cmd|escape_double_quotes }}""", {% for arg in resolved_args %}{{ arg.name }}={{ arg.name }}{% if not loop.last %}, {% endif %}{% endfor %}) - # Execute command + # Execute command with retries env_vars = {} {% if tool.env %} {% for key, value in tool.env.items() %} env_vars["{{ key }}"] = "{{ value }}" {% endfor %} {% endif %} - result = execute_command(cmd, env_vars) + + max_retries = {{ tool.retries or 0 }} + timeout = {{ tool.timeout or 300 }} + + for attempt in range(max_retries + 1): + try: + result = execute_command(cmd, env_vars, timeout=timeout) + if result["success"] or attempt == max_retries: + return result + else: + if attempt < max_retries: + import time + time.sleep(1) # Brief delay before retry + except Exception as e: + if attempt == max_retries: + raise e + import time + time.sleep(1) # Brief delay before retry return result except Exception as e: diff --git a/tests/test_models.py b/tests/test_models.py index 3f898ae..5f48623 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -149,6 +149,55 @@ def test_tool_config_with_args(): assert tool.args[1].type == "boolean" +def test_tool_config_with_metadata(): + """Test tool config with enhanced metadata.""" + tool = ToolConfig( + cmd="echo {{ message }}", + desc="Echo a message", + name="custom-echo", + version="1.2.0", + author="Test Author", + category="utility", + tags=["echo", "test", "utility"], + timeout=60, + retries=2, + examples=[ + {"description": "Simple echo", "command": "custom-echo --message='Hello'"}, + {"description": "Echo with variable", "command": "custom-echo --message='$USER'"} + ], + dependencies=["echo"], + permissions=["user:read"] + ) + assert tool.name == "custom-echo" + assert tool.version == "1.2.0" + assert tool.author == "Test Author" + assert tool.category == "utility" + assert tool.tags == ["echo", "test", "utility"] + assert tool.timeout == 60 + assert tool.retries == 2 + assert len(tool.examples) == 2 + assert tool.dependencies == ["echo"] + assert tool.permissions == ["user:read"] + + +def test_tool_config_defaults(): + """Test tool config with default values for optional fields.""" + tool = ToolConfig( + cmd="echo test", + desc="Test tool" + ) + assert tool.name is None + assert tool.version is None + assert tool.author is None + assert tool.category is None + assert tool.tags is None + assert tool.timeout is None + assert tool.retries is None + assert tool.examples is None + assert tool.dependencies is None + assert tool.permissions is None + + # YMLConfig tests def test_valid_yml_config():