Nash MCP Server

import json import os import subprocess import sys import traceback import logging from datetime import datetime from nash_mcp.constants import MAC_SECRETS_PATH, NASH_SESSION_DIR from nash_mcp.process_manager import ProcessManager def list_session_files() -> str: """ List all Python files in the current session directory. This function is essential to check what files already exist before creating new ones. ALWAYS use this function before creating a new file to avoid duplicating functionality. USE CASES: - Before creating a new file to check if something similar already exists - When starting work on a new task to understand available resources - To discover relevant code that could be modified instead of rewritten - When fixing errors to find the file that needs editing EXAMPLES: ```python # List all existing Python files in the session list_session_files() # After seeing available files, check content of a specific file get_file_content("data_processor.py") ``` WORKFLOW: 1. ALWAYS start by listing available files with list_session_files() 2. Check content of relevant files with get_file_content() 3. Edit existing files with edit_python_file() instead of creating new ones 4. Only create new files for entirely new functionality Returns: A formatted list of Python files in the current session directory """ try: # Ensure session directory exists if not NASH_SESSION_DIR.exists(): return "No session directory found." # Find all Python files in the session directory py_files = list(NASH_SESSION_DIR.glob("*.py")) if not py_files: return "No Python files found in the current session." # Sort files by modification time (newest first) py_files.sort(key=lambda x: x.stat().st_mtime, reverse=True) # Format output result = "Python files in current session:\n\n" for file_path in py_files: mod_time = datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") result += f"- {file_path.name} (Modified: {mod_time})\n" result += '\nTo view file content: get_file_content("filename.py")' return result except Exception as e: logging.error(f"Error listing session files: {str(e)}") return f"Error listing files: {str(e)}" def get_file_content(file_name: str) -> str: """ Retrieve the contents of a Python file from the session directory. This function reads a file from the current session directory and returns its contents. This is essential for viewing the current state of a file before making edits with edit_python_file(). USE CASES: - Before making edits to an existing file - When checking the current implementation of a script - To understand the structure of a previously saved script - For reviewing code to identify parts that need modification EXAMPLES: ```python # View a file named "data_analysis.py" get_file_content("data_analysis.py") # View a file without .py extension (extension will be added automatically) get_file_content("data_analysis") ``` WORKFLOW: 1. Use get_file_content() to check if a file exists and view its current content 2. Identify the exact content you want to modify 3. Use edit_python_file() to make targeted changes by replacing specific content 4. Use execute_python() with an empty code string to run the modified file Args: file_name: The name of the file to read from the session directory Returns: The file contents as a string, or an error message if the file doesn't exist """ try: # Ensure file has .py extension if not file_name.endswith(".py"): file_name = f"{file_name}.py" file_path = NASH_SESSION_DIR / file_name if not file_path.exists(): return f"Error: File '{file_name}' not found in the current session." with open(file_path, "r") as f: content = f.read() return content except Exception as e: logging.error(f"Error reading file '{file_name}': {str(e)}") return f"Error reading file: {str(e)}" def edit_python_file(file_name: str, old_content: str, new_content: str) -> str: """ Edit a Python file by replacing specific content with new content. ALWAYS PRIORITIZE EDITING EXISTING FILES RATHER THAN CREATING NEW ONES WHEN MAKING CHANGES. This should be your first choice whenever modifying existing code - even for seemingly significant changes. This function uses exact string matching to find and replace code snippets, similar to how Claude edits files. This approach is more reliable for complex changes and matches how LLMs naturally think about editing text. USE CASES: - Fix bugs or errors in existing code - Refactor code to improve readability or maintainability - Add new features to an existing script - Update variable names, function signatures, or other identifiers - Replace entire blocks of code with improved implementations - Change algorithm implementations or logic flows - Modify large portions of files (you can replace almost the entire content if needed) ADVANTAGES: - Uses exact pattern matching, similar to how Claude handles edits - Avoids problems with line numbers shifting during edits - Can replace multi-line content with precise context - More reliable for complex edits than line-based approaches - Preserves script history and context WHEN TO EDIT vs. CREATE NEW: EDIT when (almost always): - Making any modification to existing functionality - Fixing bugs or issues in existing code - Adding new functions or classes to existing modules - Changing logic, algorithms, or implementations - Adjusting parameters or configuration values - Updating imports or dependencies - Improving error handling or adding validation - Enhancing existing features in any way - Refactoring or restructuring code - Even for major changes that affect large portions of the file CREATE NEW only when: - Creating a completely separate utility with an entirely different purpose - Explicitly asked by the user to create a new standalone file - Testing isolated functionality that shouldn't affect existing code - The existing file is explicitly described as a template or example EXAMPLES: ```python # Fix a calculation by replacing the specific function edit_python_file( "data_analysis.py", "def calculate_average(values):\n return sum(values) / len(values)", "def calculate_average(values):\n return np.mean(values) # Using numpy for better handling of edge cases" ) # Fix a bug by replacing a specific line with its surrounding context edit_python_file( "processor.py", " data = load_data()\n result = process(data)\n save_results(data) # Bug: saving wrong data", " data = load_data()\n result = process(data)\n save_results(result) # Fixed: save processed results" ) # Add a new import statement edit_python_file( "api_client.py", "import requests\nimport json", "import requests\nimport json\nimport logging" ) # Adding error handling to a function (NOTICE INDENTATION IS PRESERVED) edit_python_file( "fetch_data.py", "def fetch_user_data(user_id):\n url = f\"https://api.example.com/users/{user_id}\"\n response = requests.get(url)\n response.raise_for_status()\n return response.json()", "def fetch_user_data(user_id):\n url = f\"https://api.example.com/users/{user_id}\"\n try:\n response = requests.get(url)\n response.raise_for_status()\n return response.json()\n except requests.RequestException as e:\n logging.error(f\"Failed to fetch user data: {e}\")\n return None" ) # Major change: Replace an entire function with a completely new implementation edit_python_file( "processor.py", "def process_data(data):\n # Old inefficient implementation\n result = []\n for item in data:\n if item['value'] > 0:\n result.append(item['value'] * 2)\n return result", "def process_data(data):\n # New vectorized implementation\n import pandas as pd\n df = pd.DataFrame(data)\n return df[df['value'] > 0]['value'] * 2" ) # Even major changes that add multiple functions should use edit_python_file edit_python_file( "utils.py", "# Utility functions for data processing", "# Utility functions for data processing\n\ndef validate_input(data):\n \"\"\"Validate input data format.\"\"\"\n if not isinstance(data, list):\n raise TypeError(\"Data must be a list\")\n return True\n\ndef normalize_data(data):\n \"\"\"Normalize values to 0-1 range.\"\"\"\n min_val = min(data)\n max_val = max(data)\n return [(x - min_val) / (max_val - min_val) for x in data]" ) ``` WORKFLOW: 1. Always check if a relevant file already exists with get_file_content() 2. When modifying any existing file, use edit_python_file() 3. Identify the exact content to replace (including enough context) 4. Create the new replacement content 5. Apply the change with edit_python_file() 6. Use execute_python() to run the modified file 7. Only create new files when specifically creating a new utility BEST PRACTICES: - Include sufficient context around the text to be replaced (3-5 lines before and after) - For major rewrites, you can replace large chunks of the file or even nearly all content - Ensure the old_content exactly matches text in the file, including spacing and indentation - Make focused, targeted changes rather than multiple changes at once - When a user asks to "fix", "update", "modify", or "change" something, they typically want edits to existing files INDENTATION GUIDELINES (CRITICAL FOR PYTHON): - Always preserve correct indentation in both old_content and new_content - When adding control structures (if/else, try/except, loops), replace the entire block - Never try to insert just the opening part of a control structure without its closing part - For adding error handling, replace the entire function or block, not just parts of it - Watch for common indentation errors, especially with nested structures - When debugging indentation issues, view the entire file first with get_file_content() - For complex control flow changes, prefer replacing larger blocks to ensure consistency PATTERN RECOGNITION: - When a user asks to "fix", "update", "modify", or "change" something, they typically want edits to existing files - Use list_session_files() and get_file_content() first to check what files already exist - Only create new files when the user explicitly requests a completely new utility SAFETY FEATURES: - Creates a backup of the original file (.py.bak extension) - Returns a diff of changes made - Will only replace exact matches, preventing unintended changes Args: file_name: The name of the file to edit in the session directory old_content: The exact content to replace (must match exactly, including whitespace) new_content: The new content to insert as a replacement Returns: Success message with diff of changes, or error message if the operation fails """ try: # Ensure file has .py extension if not file_name.endswith(".py"): file_name = f"{file_name}.py" file_path = NASH_SESSION_DIR / file_name if not file_path.exists(): return f"Error: File '{file_name}' not found in the current session." # Read the original file with open(file_path, "r") as f: content = f.read() # Check if the old content exists in the file if old_content not in content: return f"Error: The specified content was not found in '{file_name}'. Please check that the content matches exactly, including whitespace and indentation." # Create a backup of the original file backup_path = file_path.with_suffix(".py.bak") with open(backup_path, "w") as f: f.write(content) # Replace the content new_file_content = content.replace(old_content, new_content) # Write the modified content back to the file with open(file_path, "w") as f: f.write(new_file_content) # Generate a unified diff for the changes from difflib import unified_diff old_lines = content.splitlines() new_lines = new_file_content.splitlines() diff = list( unified_diff( old_lines, new_lines, fromfile=f"{file_name} (original)", tofile=f"{file_name} (modified)", lineterm="", n=3, # Context lines ) ) if diff: diff_result = "\n".join(diff) else: diff_result = "No changes detected." return f"Successfully edited '{file_name}'.\n\nChanges:\n{diff_result}" except Exception as e: logging.error(f"Error editing file '{file_name}': {str(e)}") logging.error(traceback.format_exc()) return f"Error editing file: {str(e)}" def execute_python(code: str, file_name: str, args: list = None) -> str: """Execute arbitrary Python code and return the result. ⚠️ REQUIRED PARAMETERS - MUST BE PROVIDED IN A SINGLE FUNCTION CALL: ⚠️ - code: The Python code to execute (string) - file_name: Name for the Python file (string) - args: Optional command-line arguments for the script (list) CORRECT EXAMPLES: ```python # Example 1: Basic usage execute_python( code="print('Hello world')", file_name="hello.py" ) # Example 2: With imports execute_python( code="import os\nprint(os.getcwd())", file_name="show_dir.py" ) # Example 3: With command-line arguments execute_python( code="import sys\nprint(f'Arguments: {sys.argv[1:]}')", file_name="show_args.py", args=["arg1", "arg2"] ) # Example 4: Running an existing file without modifying it execute_python( code="", file_name="existing_script.py" ) ``` ❌ COMMON ERRORS - DO NOT DO THIS: ```python # ERROR: Missing required parameters execute_python() # Will fail! Missing both code and file_name # ERROR: Missing file_name parameter execute_python(code="print('Hello')") # Will fail! Missing file_name # ERROR: Trying to split function call across multiple steps # First call to "prepare" - THIS IS WRONG execute_python() # Then providing code separately - THIS DOESN'T WORK code = "print('Hello')" ``` ⚠️ MANDATORY PRE-CODING CHECKLIST - COMPLETE BEFORE WRITING ANY CODE: ⚠️ STOP! Before writing or executing ANY code, have you completed these REQUIRED checks? 1. Check available packages: list_installed_packages() - Know what libraries you can use - Avoid importing unavailable packages 2. Check available secrets: nash_secrets() - See what API keys and credentials are available - Don't write code requiring credentials you don't have 3. Check existing files: list_session_files() - See what code already exists - Avoid duplicating existing functionality 4. Review relevant file contents: get_file_content("filename.py") - Understand existing implementations - Decide whether to edit or create new These steps are MANDATORY. Skipping them is the #1 cause of inefficient code development. WHEN TO USE execute_python VS. edit_python_file: - Use edit_python_file() for MINOR or MODERATE changes to existing files - This is usually more efficient for small to medium changes - Always prefer editing when the file already exists - Use execute_python() only when: - Creating a completely new file - Changes would require replacing almost the entire existing file - Creating a brand new file results in a cleaner, smaller response FUNCTION BEHAVIOR: - This function executes standard Python code with access to imported modules - The code is saved to a file in the session directory and executed - All secrets are passed as environment variables to the subprocess - If code="" and the file exists, it will run the existing file without changes COMMAND-LINE ARGUMENTS: - The `args` parameter allows passing command-line arguments to the script - Arguments are accessed in your script using the standard sys.argv list: ```python import sys if len(sys.argv) > 1: input_file = sys.argv[1] print(f"Processing {input_file}") ``` SECRET MANAGEMENT: - Use nash_secrets() to see available API keys/credentials - Access secrets in your code with: os.environ.get('SECRET_NAME') BEST PRACTICES: - Always provide both code and file_name in a single function call - Use descriptive filenames that reflect the purpose (e.g., "data_analysis.py") - Include proper error handling with try/except blocks - Clean up any resources (files, connections) your code creates - For web automation, use operate_browser instead of custom code SECURITY CONSIDERATIONS: - Never write code that could harm the user's system - Don't leak or expose secret values in output - Avoid making unauthorized network requests - Sanitize any arguments passed to the script Args: code: Python code to execute (multi-line string) file_name: Descriptive name for the Python file (will be saved in session directory) args: Optional list of command-line arguments to pass to the script Returns: Captured stdout from code execution or detailed error message """ # Log the full code being executed logging.info(f"Executing Python code in file '{file_name}':\n{code}") # Format args for logging if provided args_str = str(args) if args else "None" if len(args_str) > 100: args_str = args_str[:100] + "..." logging.info(f"Script arguments: {args_str}") try: # Load secrets as environment variables env_vars = os.environ.copy() if MAC_SECRETS_PATH.exists(): try: with open(MAC_SECRETS_PATH, "r") as f: secrets = json.load(f) # Add secrets to environment variables for subprocess for secret in secrets: if "key" in secret and "value" in secret: env_vars[secret["key"]] = secret["value"] logging.info("Loaded secrets for Python execution") except Exception as e: # Log the error but continue execution logging.warning(f"Error loading secrets: {str(e)}") print(f"Warning: Error loading secrets: {str(e)}") else: logging.info("No secrets file found") # Ensure file name has .py extension if not file_name.endswith(".py"): file_name = f"{file_name}.py" # Create the full file path in the session directory file_path = NASH_SESSION_DIR / file_name # If code is empty and file exists, use existing file if not code and file_path.exists(): logging.info(f"Using existing file: {file_path}") else: # Write the code to the file with open(file_path, "w") as f: f.write(code) logging.info(f"Saved Python code to: {file_path}") try: # Execute the file using the same Python interpreter logging.info(f"Running Python code from: {file_path}") try: # Prepare command arguments cmd_args = [sys.executable, str(file_path)] # Add any command-line arguments if args: # Convert all arguments to strings string_args = [str(arg) for arg in args] cmd_args.extend(string_args) # Log the full command being executed logging.info(f"Launching subprocess with command: {' '.join(cmd_args)}") proc = subprocess.Popen( cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env_vars ) # Log process information proc_pid = proc.pid logging.info(f"Created subprocess with PID {proc_pid} for file {file_path}") # Track the process in the process manager process_manager = ProcessManager.get_instance() process_manager.add_pid(proc_pid) try: logging.info(f"Waiting for process {proc_pid} to complete...") stdout, stderr = proc.communicate() logging.info(f"Process {proc_pid} completed with return code {proc.returncode}") result = subprocess.CompletedProcess(cmd_args, proc.returncode, stdout, stderr) finally: # Remove from the process manager if completed successfully if hasattr(proc, "returncode") and proc.returncode == 0: process_manager.remove_pid(proc_pid) else: logging.info(f"Keeping PID {proc_pid} in process manager (non-zero or unknown return code)") # Return stdout if successful, or stderr if there was an error if result.returncode == 0: logging.info(f"Python code in {file_name} executed successfully") return result.stdout if result.stdout else f"Code in {file_name} executed successfully (no output)" else: logging.warning(f"Python code execution of {file_name} failed with return code {result.returncode}") return f"Error in {file_name} (return code {result.returncode}):\n{result.stderr}" except Exception as exec_err: # Handle any execution exceptions error_msg = f"Error executing {file_name}: {str(exec_err)}" logging.error(error_msg) logging.error(traceback.format_exc()) return error_msg except Exception as e: # Catch-all for any other exceptions in the outer try block logging.error(f"Unexpected error in execute_python for {file_name}: {str(e)}") logging.error(traceback.format_exc()) # Return error message instead of raising to prevent MCP server crash return f"Unexpected error in {file_name}: {str(e)}\nSee logs for details." except Exception as e: logging.error(f"Python execution error: {str(e)}") logging.error(traceback.format_exc()) return f"Error in {file_name}: {str(e)}\nTraceback: {traceback.format_exc()}\n\n"