├── .github ├── FUNDING.yml └── workflows │ ├── publish-to-github-packages.yml │ └── publish-to-pypi.yml ├── .gitignore ├── CHANGES.md ├── Dockerfile ├── LICENSE ├── README.md ├── compatible_servers ├── cursor │ └── cursor_compatible_mcp_server.py ├── generic │ ├── kubectl_jsonrpc_server.py │ ├── kubectl_mcp_server.py │ ├── mcp_kubectl_server.py │ ├── simple_kubectl_mcp.py │ └── simple_mcp_server.py ├── minimal │ └── minimal_mcp_server.py └── windsurf │ └── windsurf_compatible_mcp_server.py ├── docs ├── INSTALLATION.md ├── QUICKSTART.md ├── README.md ├── claude │ ├── claude-mcp.gif │ ├── claude-mcp.mp4 │ └── claude_integration.md ├── cursor │ ├── cursor-mcp.gif │ ├── cursor_config.json │ ├── cursor_configuration.md │ ├── cursor_configuration_guide.md │ ├── cursor_integration.md │ └── cursor_integration_guide_updated.md ├── fastmcp.md ├── integration_guide.md ├── kwargs_parameter_fix.md ├── mcp-server.md └── windsurf │ ├── windsurf-mcp.gif │ ├── windsurf_config.json │ ├── windsurf_integration.md │ └── windsurf_integration_guide_updated.md ├── image.png ├── install.sh ├── kubectl_mcp_tool ├── __init__ copy.py ├── __init__.py ├── __main__.py ├── claude_json_fix.py ├── claude_json_fix_v3.py ├── claude_message_framing.py ├── cli.py ├── cli │ ├── __init__.py │ ├── __main__.py │ └── cli.py ├── core │ ├── __init__.py │ ├── kubernetes_ops.py │ └── mcp_server.py ├── cursor_wrapper.py ├── diagnostics.py ├── enhanced_json_fix.py ├── fastmcp_patch.py ├── fastmcp_wrapper.py ├── mcp_kubectl_tool.py ├── mcp_server.py ├── minimal_wrapper.py ├── monitoring │ └── diagnostics.py ├── natural_language.py ├── security │ └── security_ops.py ├── simple_ping.py ├── simple_server.py ├── taskgroup_fix.py └── utils │ └── natural_language.py ├── mcp_config.json ├── python_tests ├── cursor_config_test.py ├── cursor_test.py ├── mcp_client_simulator.py ├── mcp_live_test.py ├── mcp_test_strategy.md ├── run_mcp_tests.py ├── run_test.py ├── test_all_features.py ├── test_core.py ├── test_diagnostics.py ├── test_install.py ├── test_kubectl_mcp.py ├── test_kubernetes_ops.py ├── test_mcp.py ├── test_mcp_core.py ├── test_mcp_diagnostics.py ├── test_mcp_monitoring.py ├── test_mcp_nlp.py ├── test_mcp_security.py ├── test_new_features.py ├── test_pod.py ├── test_simple_mcp.py ├── test_status.py └── test_utils.py ├── requirements.txt ├── run_server.py └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [rohitg00] # Replace with up to 4 GitHub Sponsors-enabled usernames 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-github-packages.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GitHub Packages 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install build twine 20 | - name: Build and publish 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: | 24 | python -m build 25 | twine upload --repository-url https://maven.pkg.github.com/rohitg00/kubectl-mcp-server dist/* 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install build twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python -m build 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Kubernetes config 2 | .kube/ 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | .pytest_cache/ 26 | 27 | # Logs 28 | logs/ 29 | *.log 30 | 31 | # Environment 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | 38 | # IDE 39 | .idea/ 40 | .vscode/ 41 | *.swp 42 | *.swo 43 | 44 | # OS specific 45 | .DS_Store 46 | Thumbs.db 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | local_settings.py 65 | db.sqlite3 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # User-specific configurations 106 | .DS_Store 107 | 108 | # Local config files 109 | config.yaml 110 | config.yaml.local -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes to kubectl-mcp-tool Configuration 2 | 3 | ## Version 1.1.1 (Latest) 4 | 5 | ### Key Fixes and Improvements 6 | 1. **Fixed JSON RPC Communication Issues** 7 | - Resolved "Unexpected non-whitespace character after JSON at position 4" error 8 | - Properly configured logging to use stderr or log files instead of stdout 9 | - Added comprehensive JSON validation and sanitization 10 | - Improved handling of special characters and BOM in JSON responses 11 | 12 | 2. **Enhanced Logging Configuration** 13 | - Added support for MCP_LOG_FILE environment variable 14 | - Implemented proper log file rotation 15 | - Prevented log output from corrupting JSON-RPC communication 16 | - Improved debug logging for troubleshooting 17 | 18 | 3. **Added ping utility for server validation** 19 | - Created simple_ping.py utility for validating server connections 20 | - Implemented better error detection and debugging 21 | - Added detailed diagnostic information for connection issues 22 | 23 | 4. **Improved shutdown handling** 24 | - Better signal handling for graceful shutdown 25 | - Fixed memory leaks during server restart 26 | - Enhanced server resilience during connection failures 27 | 28 | ## Key Improvements 29 | 30 | 1. **Simplified MCP Server Implementation** 31 | - Created a minimal wrapper (`kubectl_mcp_tool.minimal_wrapper.py`) for better compatibility 32 | - Removed complex parameter schemas that were causing compatibility issues 33 | - Used direct tool registration with simple names 34 | 35 | 2. **Improved Cursor Integration** 36 | - Updated Cursor configuration to use the minimal wrapper 37 | - Added explicit environment variables for PATH and KUBECONFIG 38 | - Provided better error handling and logging 39 | 40 | 3. **Enhanced Claude and WindSurf Support** 41 | - Updated configuration for Claude Desktop 42 | - Updated configuration for WindSurf 43 | - Standardized configuration format across all AI assistants 44 | 45 | 4. **Streamlined Installation Process** 46 | - Improved installation script to set up all configurations automatically 47 | - Added automatic testing of kubectl and kubeconfig 48 | - Created better error handling during installation 49 | 50 | 5. **Comprehensive Documentation Updates** 51 | - Updated README.md with working configuration examples 52 | - Updated integration guides for all supported AI assistants 53 | - Created a new QUICKSTART.md guide for new users 54 | - Enhanced troubleshooting section with more specific solutions 55 | 56 | ## Configuration Changes 57 | 58 | ### Previous Configuration (Not Working) 59 | ```json 60 | { 61 | "mcpServers": { 62 | "kubernetes": { 63 | "command": "python", 64 | "args": ["-m", "kubectl_mcp_tool.cli.cli"] 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | ### New Configuration (Working) 71 | ```json 72 | { 73 | "mcpServers": { 74 | "kubernetes": { 75 | "command": "python", 76 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 77 | "env": { 78 | "KUBECONFIG": "/path/to/your/.kube/config", 79 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin" 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ## Implementation Details 87 | 88 | The key implementation changes include: 89 | 90 | 1. Using a simpler tool registration approach: 91 | ```python 92 | @server.tool("process_natural_language") 93 | async def process_natural_language(query: str): 94 | # Implementation... 95 | ``` 96 | 97 | 2. Avoiding complex parameter schemas that aren't supported in some MCP SDK versions 98 | 99 | 3. Setting explicit environment variables to ensure kubectl can be found 100 | 101 | 4. Better error handling throughout the codebase 102 | 103 | 5. Comprehensive debugging and logging 104 | 105 | ## Testing Your Installation 106 | 107 | After applying these changes, you can verify your installation with: 108 | 109 | ```bash 110 | # Test command line 111 | kubectl-mcp --help 112 | 113 | # Test MCP server directly 114 | python -m kubectl_mcp_tool.minimal_wrapper 115 | 116 | # Run automated installation 117 | bash install.sh 118 | ``` 119 | 120 | Then try using your AI assistant to interact with your Kubernetes cluster. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.11-slim 3 | 4 | # Install system dependencies (if needed for pip packages or build tools) 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | build-essential \ 8 | gcc \ 9 | libffi-dev \ 10 | libssl-dev \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # Set the working directory in the container 14 | WORKDIR /app 15 | 16 | # Copy only requirements first for better caching during builds 17 | COPY requirements.txt . 18 | 19 | # Install Python dependencies 20 | RUN pip install --upgrade pip && \ 21 | pip install --no-cache-dir -r requirements.txt 22 | 23 | # Copy the rest of the application code into the container 24 | COPY . . 25 | 26 | # Expose port 8080 for SSE transport 27 | EXPOSE 8080 28 | 29 | # Use environment variables for flexibility (optional) 30 | ENV TRANSPORT=sse 31 | ENV PORT=8080 32 | 33 | # By default, run the MCP server with SSE transport on port 8080 34 | CMD ["python", "run_server.py", "--transport", "sse", "--port", "8080"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rohit Ghumare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /compatible_servers/minimal/minimal_mcp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Absolutely minimal MCP server implementation. 4 | This implements only the essential parts of the protocol to work with Cursor. 5 | """ 6 | 7 | import asyncio 8 | import json 9 | import os 10 | import sys 11 | import subprocess 12 | from datetime import datetime 13 | 14 | # Create log directory 15 | LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") 16 | os.makedirs(LOG_DIR, exist_ok=True) 17 | 18 | # Set up logging 19 | log_file = open(os.path.join(LOG_DIR, "minimal_mcp.log"), "w") 20 | 21 | def log(message): 22 | """Log a message to both the log file and stderr.""" 23 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] 24 | formatted = f"{timestamp} - {message}" 25 | print(f"LOG: {formatted}", file=sys.stderr) 26 | log_file.write(f"{formatted}\n") 27 | log_file.flush() 28 | 29 | # Basic server info 30 | SERVER_NAME = "kubectl-mcp-minimal" 31 | SERVER_VERSION = "0.1.0" 32 | 33 | # MCP protocol implementation 34 | async def stdio_transport(): 35 | """Create an asyncio transport over stdin/stdout.""" 36 | loop = asyncio.get_event_loop() 37 | reader = asyncio.StreamReader() 38 | protocol = asyncio.StreamReaderProtocol(reader) 39 | 40 | # Handle stdin 41 | await loop.connect_read_pipe(lambda: protocol, sys.stdin) 42 | 43 | # Handle stdout 44 | w_transport, w_protocol = await loop.connect_write_pipe( 45 | asyncio.streams.FlowControlMixin, sys.stdout 46 | ) 47 | writer = asyncio.StreamWriter(w_transport, w_protocol, None, loop) 48 | 49 | return reader, writer 50 | 51 | async def read_message(reader): 52 | """Read a JSON-RPC message from stdin.""" 53 | line = await reader.readline() 54 | if not line: 55 | return None 56 | 57 | line_str = line.decode('utf-8').strip() 58 | log(f"RECEIVED: {line_str}") 59 | 60 | try: 61 | return json.loads(line_str) 62 | except json.JSONDecodeError as e: 63 | log(f"JSON parse error: {e}") 64 | return None 65 | 66 | def write_message(writer, message): 67 | """Write a JSON-RPC message to stdout.""" 68 | json_str = json.dumps(message) 69 | log(f"SENDING: {json_str}") 70 | writer.write(f"{json_str}\n".encode('utf-8')) 71 | 72 | def run_kubectl(command): 73 | """Run a kubectl command and return the output.""" 74 | log(f"Running kubectl: {command}") 75 | try: 76 | result = subprocess.run( 77 | command, shell=True, capture_output=True, text=True, check=False 78 | ) 79 | success = result.returncode == 0 80 | output = result.stdout if success else result.stderr 81 | return { 82 | "command": command, 83 | "output": output.strip(), 84 | "success": success 85 | } 86 | except Exception as e: 87 | log(f"Error running command: {e}") 88 | return { 89 | "command": command, 90 | "output": f"Error: {str(e)}", 91 | "success": False 92 | } 93 | 94 | async def run_server(): 95 | """Run the MCP server.""" 96 | log("Starting MCP server") 97 | reader, writer = await stdio_transport() 98 | 99 | # Wait for initialize request 100 | log("Waiting for initialization message") 101 | 102 | # Handle messages 103 | while True: 104 | message = await read_message(reader) 105 | if message is None: 106 | log("Received empty message, shutting down") 107 | break 108 | 109 | method = message.get("method") 110 | message_id = message.get("id") 111 | 112 | if method == "initialize": 113 | # Handle initialization 114 | log("Handling initialization request") 115 | response = { 116 | "jsonrpc": "2.0", 117 | "id": message_id, 118 | "result": { 119 | "name": SERVER_NAME, 120 | "version": SERVER_VERSION, 121 | "capabilities": { 122 | "tools": [ 123 | { 124 | "name": "run_kubectl", 125 | "description": "Run kubectl command", 126 | "parameters": { 127 | "type": "object", 128 | "properties": { 129 | "command": { 130 | "type": "string", 131 | "description": "The kubectl command to run" 132 | } 133 | }, 134 | "required": ["command"] 135 | } 136 | }, 137 | { 138 | "name": "get_pods", 139 | "description": "Get pods in the specified namespace", 140 | "parameters": { 141 | "type": "object", 142 | "properties": { 143 | "namespace": { 144 | "type": "string", 145 | "description": "Namespace to get pods from (optional)" 146 | } 147 | } 148 | } 149 | } 150 | ] 151 | } 152 | } 153 | } 154 | write_message(writer, response) 155 | 156 | elif method == "callTool": 157 | # Handle tool calls 158 | log("Handling tool call") 159 | params = message.get("params", {}) 160 | tool_name = params.get("name", "") 161 | tool_params = params.get("parameters", {}) 162 | 163 | log(f"Tool call: {tool_name}, params: {tool_params}") 164 | 165 | if tool_name == "run_kubectl": 166 | cmd = tool_params.get("command", "") 167 | result = run_kubectl(f"kubectl {cmd}") 168 | response = { 169 | "jsonrpc": "2.0", 170 | "id": message_id, 171 | "result": result 172 | } 173 | 174 | elif tool_name == "get_pods": 175 | namespace = tool_params.get("namespace", "") 176 | cmd = "kubectl get pods" + (f" -n {namespace}" if namespace else "") 177 | result = run_kubectl(cmd) 178 | response = { 179 | "jsonrpc": "2.0", 180 | "id": message_id, 181 | "result": result 182 | } 183 | 184 | else: 185 | # Unknown tool 186 | response = { 187 | "jsonrpc": "2.0", 188 | "id": message_id, 189 | "error": { 190 | "code": -32601, 191 | "message": f"Tool not found: {tool_name}" 192 | } 193 | } 194 | 195 | write_message(writer, response) 196 | 197 | elif method == "shutdown": 198 | # Handle shutdown 199 | log("Handling shutdown request") 200 | response = { 201 | "jsonrpc": "2.0", 202 | "id": message_id, 203 | "result": None 204 | } 205 | write_message(writer, response) 206 | break 207 | 208 | else: 209 | # Unknown method 210 | log(f"Unknown method: {method}") 211 | response = { 212 | "jsonrpc": "2.0", 213 | "id": message_id, 214 | "error": { 215 | "code": -32601, 216 | "message": f"Method not found: {method}" 217 | } 218 | } 219 | write_message(writer, response) 220 | 221 | log("Shutting down server") 222 | await writer.drain() 223 | writer.close() 224 | 225 | if __name__ == "__main__": 226 | try: 227 | # Unbuffered output 228 | os.environ["PYTHONUNBUFFERED"] = "1" 229 | 230 | # Run the server 231 | log(f"Starting server, Python version: {sys.version}") 232 | log(f"Current directory: {os.getcwd()}") 233 | asyncio.run(run_server()) 234 | 235 | except KeyboardInterrupt: 236 | log("Server stopped by keyboard interrupt") 237 | except Exception as e: 238 | log(f"Fatal error: {str(e)}") 239 | finally: 240 | log_file.close() -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation Guide for kubectl-mcp-tool 2 | 3 | This document provides detailed instructions for installing the kubectl-mcp-tool, a Kubernetes command-line tool that implements the Model Context Protocol (MCP) to enable AI assistants to interact with Kubernetes clusters. 4 | 5 | ## Table of Contents 6 | 7 | - [PyPI Installation](#pypi-installation) 8 | - [Prerequisites](#prerequisites) 9 | - [Installation Methods](#installation-methods) 10 | - [Global Installation](#global-installation) 11 | - [Local Development Installation](#local-development-installation) 12 | - [Docker Installation](#docker-installation) 13 | - [Verifying Installation](#verifying-installation) 14 | - [Troubleshooting](#troubleshooting) 15 | - [Upgrading](#upgrading) 16 | - [Uninstallation](#uninstallation) 17 | - [Recommended Configuration](#recommended-configuration) 18 | - [Using the Minimal Wrapper (Recommended)](#using-the-minimal-wrapper-recommended) 19 | - [Key Environment Variables](#key-environment-variables) 20 | - [Testing the Minimal Wrapper](#testing-the-minimal-wrapper) 21 | 22 | ## PyPI Installation 23 | 24 | The simplest way to install kubectl-mcp-tool is directly from the Python Package Index (PyPI): 25 | 26 | ```bash 27 | pip install kubectl-mcp-tool 28 | ``` 29 | 30 | For a specific version: 31 | 32 | ```bash 33 | pip install kubectl-mcp-tool==1.1.1 34 | ``` 35 | 36 | The package is available on PyPI: [https://pypi.org/project/kubectl-mcp-tool/1.1.1/](https://pypi.org/project/kubectl-mcp-tool/1.1.1/) 37 | 38 | ## Prerequisites 39 | 40 | Before installing kubectl-mcp-tool, ensure you have the following: 41 | 42 | - Python 3.9 or higher 43 | - pip (Python package manager) 44 | - kubectl CLI installed and configured 45 | - Access to a Kubernetes cluster 46 | - (Optional) Helm v3 for Helm operations 47 | 48 | To check your Python version: 49 | 50 | ```bash 51 | python --version 52 | ``` 53 | 54 | To check if pip is installed: 55 | 56 | ```bash 57 | pip --version 58 | ``` 59 | 60 | To check if kubectl is installed: 61 | 62 | ```bash 63 | kubectl version --client 64 | ``` 65 | 66 | ## Installation Methods 67 | 68 | ### Global Installation 69 | 70 | #### From PyPI (Recommended) 71 | 72 | Install the latest stable version from PyPI: 73 | 74 | ```bash 75 | pip install kubectl-mcp-tool 76 | ``` 77 | 78 | #### From GitHub 79 | 80 | Install the development version directly from GitHub: 81 | 82 | ```bash 83 | pip install git+https://github.com/rohitg00/kubectl-mcp-server.git 84 | ``` 85 | 86 | #### Using pipx (Isolated Environment) 87 | 88 | For a more isolated installation that doesn't affect your system Python: 89 | 90 | ```bash 91 | # Install pipx if you don't have it 92 | pip install pipx 93 | pipx ensurepath 94 | 95 | # Install kubectl-mcp-tool 96 | pipx install kubectl-mcp-tool 97 | ``` 98 | 99 | ### Local Development Installation 100 | 101 | If you want to modify the code or contribute to the project: 102 | 103 | ```bash 104 | # Clone the repository 105 | git clone https://github.com/rohitg00/kubectl-mcp-server.git 106 | cd kubectl-mcp-server 107 | 108 | # Install dependencies 109 | pip install -r requirements.txt 110 | 111 | # Install in development mode 112 | pip install -e . 113 | ``` 114 | 115 | ### Docker Installation 116 | 117 | For containerized usage: 118 | 119 | ```bash 120 | # Pull the Docker image 121 | docker pull rohitg00/kubectl-mcp-tool:latest 122 | 123 | # Run the container 124 | docker run -it --rm \ 125 | -v ~/.kube:/root/.kube \ 126 | rohitg00/kubectl-mcp-tool:latest 127 | ``` 128 | 129 | ## Recommended Configuration 130 | 131 | ### Using the Minimal Wrapper (Recommended) 132 | 133 | For best compatibility with AI assistants, we strongly recommend using the minimal wrapper provided in the package. This approach resolves many common issues including JSON RPC errors and logging problems: 134 | 135 | ```json 136 | { 137 | "mcpServers": { 138 | "kubernetes": { 139 | "command": "python", 140 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 141 | "env": { 142 | "KUBECONFIG": "/path/to/your/.kube/config", 143 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", 144 | "MCP_LOG_FILE": "/path/to/logs/debug.log", 145 | "MCP_DEBUG": "1" 146 | } 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | ### Key Environment Variables 153 | 154 | - `MCP_LOG_FILE`: Path to log file (recommended to avoid stdout pollution) 155 | - `MCP_DEBUG`: Set to "1" for verbose logging 156 | - `MCP_TEST_MOCK_MODE`: Set to "1" to use mock data instead of real cluster 157 | - `KUBECONFIG`: Path to your Kubernetes config file 158 | - `KUBECTL_MCP_LOG_LEVEL`: Set to "DEBUG", "INFO", "WARNING", or "ERROR" 159 | 160 | ### Testing the Minimal Wrapper 161 | 162 | You can verify that the minimal wrapper is working correctly: 163 | 164 | ```bash 165 | # Test directly 166 | python -m kubectl_mcp_tool.minimal_wrapper 167 | 168 | # Use the ping utility to test connectivity 169 | python -m kubectl_mcp_tool.simple_ping 170 | ``` 171 | 172 | ## Verifying Installation 173 | 174 | After installation, verify that the tool is working correctly: 175 | 176 | ```bash 177 | # Check version 178 | kubectl-mcp --version 179 | 180 | # Check CLI mode 181 | kubectl-mcp --help 182 | 183 | # Test connection to Kubernetes 184 | kubectl-mcp get pods 185 | ``` 186 | 187 | ## Troubleshooting 188 | 189 | ### Common Issues 190 | 191 | 1. **Command not found**: 192 | - Ensure the installation directory is in your PATH 193 | - For pipx installations, run `pipx ensurepath` and restart your terminal 194 | 195 | 2. **Permission errors during installation**: 196 | - Use `pip install --user kubectl-mcp-tool` to install for the current user only 197 | - On Linux/macOS, you might need to use `sudo pip install kubectl-mcp-tool` 198 | 199 | 3. **Dependency conflicts**: 200 | - Create a virtual environment: `python -m venv venv && source venv/bin/activate` 201 | - Then install within the virtual environment 202 | 203 | 4. **Kubernetes connection issues**: 204 | - Ensure your kubeconfig is correctly set up: `kubectl config view` 205 | - Check that your cluster is accessible: `kubectl cluster-info` 206 | 207 | 5. **ImportError: cannot import name 'Parameter' from 'mcp.types'**: 208 | - This is due to an MCP SDK version mismatch. Run the following commands to fix: 209 | ```bash 210 | # Uninstall conflicting packages 211 | pip uninstall -y kubectl-mcp-tool fastmcp mcp mcp-python 212 | 213 | # Install the correct version 214 | pip install mcp>=1.5.0 215 | 216 | # Reinstall kubectl-mcp-tool 217 | pip install kubectl-mcp-tool 218 | ``` 219 | 220 | 6. **AttributeError: module 'kubectl_mcp_tool.cli' has no attribute 'main'**: 221 | - This is due to a CLI module path issue. Update your installation: 222 | ```bash 223 | pip uninstall -y kubectl-mcp-tool 224 | git clone https://github.com/rohitg00/kubectl-mcp-server.git 225 | cd kubectl-mcp-server 226 | pip install -e . 227 | ``` 228 | - Or use our automatic install script: 229 | ```bash 230 | bash install.sh 231 | ``` 232 | 233 | 7. **MCP Server compatibility issues**: 234 | - Make sure you're using the correct MCP configuration for your AI assistant 235 | - For Cursor AI, use this configuration in `~/.cursor/mcp.json`: 236 | ```json 237 | { 238 | "mcpServers": { 239 | "kubernetes": { 240 | "command": "python", 241 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 242 | "env": { 243 | "KUBECONFIG": "/path/to/your/.kube/config", 244 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin" 245 | } 246 | } 247 | } 248 | } 249 | ``` 250 | - Replace `/path/to/your/.kube/config` with your actual kubeconfig path (usually `~/.kube/config`) 251 | - The `minimal_wrapper` module provides the most stable implementation for AI assistants 252 | 253 | 8. **Server implementation issues**: 254 | - This tool uses FastMCP from MCP SDK 1.5.0+ with a minimal wrapper approach 255 | - Key points about the implementation: 256 | - We use the simple `@server.tool(name)` decorator format without complex parameters 257 | - The minimal wrapper has better compatibility across MCP SDK versions 258 | - For debugging issues, run: `python -m kubectl_mcp_tool.minimal_wrapper` 259 | 260 | 9. **Client closed or connection errors**: 261 | - If you see "client closed" errors in your AI assistant, check: 262 | - Your kubeconfig path is correct in the configuration 263 | - kubectl is installed and in your PATH 264 | - You have proper permissions to access your Kubernetes cluster 265 | - Run the installation script: `bash install.sh` which handles these issues automatically 266 | 267 | ### Getting Help 268 | 269 | If you encounter issues not covered here: 270 | 271 | 1. Check the [GitHub Issues](https://github.com/rohitg00/kubectl-mcp-server/issues) for similar problems 272 | 2. Join our [Discord community](https://discord.gg/kubectl-mcp) for real-time support 273 | 3. Open a new issue on GitHub with details about your problem 274 | 275 | ## Upgrading 276 | 277 | To upgrade to the latest version: 278 | 279 | ```bash 280 | pip install --upgrade kubectl-mcp-tool 281 | ``` 282 | 283 | To upgrade to a specific version: 284 | 285 | ```bash 286 | pip install --upgrade kubectl-mcp-tool==1.0.0 287 | ``` 288 | 289 | ## Uninstallation 290 | 291 | To uninstall kubectl-mcp-tool: 292 | 293 | ```bash 294 | pip uninstall kubectl-mcp-tool 295 | ``` 296 | 297 | If installed with pipx: 298 | 299 | ```bash 300 | pipx uninstall kubectl-mcp-tool 301 | ``` -------------------------------------------------------------------------------- /docs/QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide: kubectl-mcp-tool 2 | 3 | This guide will help you quickly set up kubectl-mcp-tool to work with AI assistants like Cursor, Claude, and WindSurf. 4 | 5 | ## 1. One-Step Installation 6 | 7 | The simplest way to install and configure kubectl-mcp-tool is to use our installation script: 8 | 9 | ```bash 10 | # Clone the repository 11 | git clone https://github.com/rohitg00/kubectl-mcp-server.git 12 | cd kubectl-mcp-server 13 | 14 | # Run the installation script 15 | bash install.sh 16 | ``` 17 | 18 | This script will: 19 | - Install the required dependencies 20 | - Set up configurations for Cursor, Claude, and WindSurf 21 | - Configure environment variables correctly 22 | - Test your Kubernetes connection 23 | 24 | ## 2. Manual Installation 25 | 26 | If you prefer to install manually: 27 | 28 | ```bash 29 | # Install the package 30 | pip install mcp>=1.5.0 31 | pip install -e . 32 | 33 | # Create config directories 34 | mkdir -p ~/.cursor 35 | mkdir -p ~/.config/claude 36 | mkdir -p ~/.config/windsurf 37 | ``` 38 | 39 | ## 3. Configuration for AI Assistants 40 | 41 | ### Cursor 42 | 43 | Create or edit `~/.cursor/mcp.json`: 44 | 45 | ```json 46 | { 47 | "mcpServers": { 48 | "kubernetes": { 49 | "command": "python", 50 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 51 | "env": { 52 | "KUBECONFIG": "/path/to/your/.kube/config", 53 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin" 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ### Claude Desktop 61 | 62 | Create or edit `~/.config/claude/mcp.json`: 63 | 64 | ```json 65 | { 66 | "mcpServers": { 67 | "kubernetes": { 68 | "command": "python", 69 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 70 | "env": { 71 | "KUBECONFIG": "/path/to/your/.kube/config" 72 | } 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ### WindSurf 79 | 80 | Create or edit `~/.config/windsurf/mcp.json`: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "kubernetes": { 86 | "command": "python", 87 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 88 | "env": { 89 | "KUBECONFIG": "/path/to/your/.kube/config" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | > **Important**: Replace `/path/to/your/.kube/config` with the actual path to your kubeconfig file (usually `~/.kube/config`). 97 | 98 | ## 4. Testing Your Installation 99 | 100 | To verify your installation is working: 101 | 102 | 1. Test the command line: 103 | ```bash 104 | kubectl-mcp --help 105 | ``` 106 | 107 | 2. Test the MCP server directly: 108 | ```bash 109 | python -m kubectl_mcp_tool.minimal_wrapper 110 | ``` 111 | 112 | It should start and wait for connections with no errors. 113 | 114 | 3. Open your AI assistant (Cursor, Claude, or WindSurf) and ask a Kubernetes-related question: 115 | - "List all pods in the default namespace" 116 | - "What deployments are running in my cluster?" 117 | - "Show me the services in the kube-system namespace" 118 | 119 | ## 5. Troubleshooting 120 | 121 | If you encounter issues: 122 | 123 | 1. Check the logs: 124 | ```bash 125 | # For Cursor 126 | cat ~/.cursor/logs/kubectl_mcp_debug.log 127 | ``` 128 | 129 | 2. Run the tool directly to see error messages: 130 | ```bash 131 | python -m kubectl_mcp_tool.minimal_wrapper 132 | ``` 133 | 134 | 3. Verify kubectl is working from command line: 135 | ```bash 136 | kubectl get pods 137 | ``` 138 | 139 | 4. Check if your configuration file has the correct paths: 140 | ```bash 141 | cat ~/.cursor/mcp.json 142 | ``` 143 | 144 | 5. Try reinstalling the package: 145 | ```bash 146 | pip uninstall -y kubectl-mcp-tool 147 | pip install -e . 148 | ``` 149 | 150 | For more details, see the [Installation Guide](./INSTALLATION.md) and [Troubleshooting](./INSTALLATION.md#troubleshooting) sections. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # kubectl-mcp-tool Documentation 2 | 3 | This directory contains documentation for the kubectl-mcp-tool, a command-line tool that implements the Model Context Protocol (MCP) to automate Kubernetes operations and management tasks. 4 | 5 | ## Available Documentation 6 | 7 | - [Installation Guide](./INSTALLATION.md): Comprehensive guide for installing kubectl-mcp-tool from PyPI and other sources 8 | - [Integration Guide](./integration_guide.md): Instructions for integrating kubectl-mcp-tool with AI assistants like Cursor, Claude, and Windsurf 9 | - [Local Execution Guide](./local_execution.md): Detailed instructions for running and testing kubectl-mcp-tool locally 10 | - [Cursor Integration](./cursor_integration.md): Specific instructions for integrating with Cursor AI assistant 11 | - [Claude Integration](./claude_integration.md): Specific instructions for integrating with Claude AI 12 | - [Windsurf Integration](./windsurf_integration.md): Specific instructions for integrating with Windsurf 13 | 14 | ## Feature Documentation 15 | 16 | - [Natural Language Usage](./natural_language_usage.md): Examples of natural language commands for Kubernetes operations 17 | - [Log Analysis Features](./log_analysis_features.md): Overview of log analysis capabilities 18 | - [Log Analysis Usage](./log_analysis_usage.md): Examples of log analysis commands 19 | - [Context Switching Usage](./context_switching_usage.md): Instructions for switching between Kubernetes contexts and namespaces 20 | - [Resource Modification Usage](./resource_modification_usage.md): Examples of resource modification commands 21 | - [Miscellaneous Commands Usage](./miscellaneous_commands_usage.md): Other useful kubectl commands 22 | - [Node Management Usage](./node_management_usage.md): Commands for managing Kubernetes nodes 23 | - [Container Interaction Usage](./container_interaction_usage.md): Commands for interacting with containers 24 | - [Deployment Management Usage](./deployment_management_usage.md): Commands for managing deployments 25 | 26 | ## Getting Started 27 | 28 | For new users, we recommend starting with the [Installation Guide](./INSTALLATION.md) to install kubectl-mcp-tool from PyPI, followed by the [Integration Guide](./integration_guide.md) to set up kubectl-mcp-tool with your preferred AI assistant, and then the [Natural Language Usage](./natural_language_usage.md) guide to learn about the available commands. 29 | -------------------------------------------------------------------------------- /docs/claude/claude-mcp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitg00/kubectl-mcp-server/069365bc180ae641572500f66abc8cd4845107d7/docs/claude/claude-mcp.gif -------------------------------------------------------------------------------- /docs/claude/claude-mcp.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitg00/kubectl-mcp-server/069365bc180ae641572500f66abc8cd4845107d7/docs/claude/claude-mcp.mp4 -------------------------------------------------------------------------------- /docs/claude/claude_integration.md: -------------------------------------------------------------------------------- 1 | # Claude Integration Guide for kubectl-mcp-tool 2 | 3 | This guide explains how to integrate the kubectl-mcp-tool with Claude AI for natural language Kubernetes operations. 4 | 5 | ## Prerequisites 6 | 7 | - kubectl-mcp-tool installed 8 | - Claude AI access 9 | - Python 3.8+ 10 | 11 | ## Integration Steps 12 | 13 | 1. **Install the kubectl-mcp-tool**: 14 | ```bash 15 | # Install from PyPI (recommended) 16 | pip install kubectl-mcp-tool 17 | 18 | # For a specific version 19 | pip install kubectl-mcp-tool==1.0.0 20 | 21 | # Or install in development mode from local repository 22 | pip install -e /path/to/kubectl-mcp-tool 23 | ``` 24 | 25 | The package is available on PyPI: [https://pypi.org/project/kubectl-mcp-tool/1.0.0/](https://pypi.org/project/kubectl-mcp-tool/1.0.0/) 26 | 27 | 2. **Start the MCP server**: 28 | ```bash 29 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 30 | ``` 31 | 32 | 3. **Expose the MCP server** (optional, if Claude needs to access it remotely): 33 | ```bash 34 | python -m kubectl_mcp_tool.cli expose 8080 35 | ``` 36 | 37 | 4. **Configure Claude**: 38 | - When using Claude in a web interface or API, you can provide the URL of your MCP server 39 | - For Claude in Cursor, follow the [Cursor Integration Guide](../cursor/cursor_integration.md) 40 | 41 | ### Step 3: Configure Claude Desktop 42 | 43 | To configure Claude Desktop to use the `kubectl-mcp-tool` MCP server: 44 | 45 | 1. Open or create the MCP configuration file at `~/.config/claude/mcp.json` (Windows: `%APPDATA%\Claude\mcp.json`) 46 | 2. Add the following configuration: 47 | 48 | ```json 49 | { 50 | "mcpServers": { 51 | "kubernetes": { 52 | "command": "python", 53 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 54 | "env": { 55 | "KUBECONFIG": "/path/to/your/.kube/config" 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | 3. Replace `/path/to/your/.kube/config` with the actual path to your kubeconfig file (usually `~/.kube/config`) 63 | 4. Save the file and restart Claude Desktop. 64 | 65 | Note: This configuration uses the minimal wrapper approach which has better compatibility with different MCP SDK versions. 66 | 67 | ### Step 4: Automated Setup 68 | 69 | For an automated setup, you can run the installation script: 70 | 71 | ```bash 72 | bash install.sh 73 | ``` 74 | 75 | This script will: 76 | 1. Install all required dependencies 77 | 2. Create the correct configuration file for Claude Desktop 78 | 3. Set up the environment variables properly 79 | 4. Verify kubectl access 80 | 81 | ### Step 5: Testing the Integration 82 | 83 | You can test the integration by: 84 | 85 | 1. Start Claude Desktop 86 | 2. Ask a Kubernetes-related question like: 87 | - "List all pods in the default namespace" 88 | - "What deployments are running in my cluster?" 89 | - "Show me the services in the kube-system namespace" 90 | 91 | ## Using kubectl-mcp-tool with Claude 92 | 93 | Claude can interact with kubectl-mcp-tool using natural language commands. Here are some examples: 94 | 95 | ### Example 1: Getting Pods 96 | 97 | ``` 98 | User: Get all pods in the default namespace 99 | Claude: Let me check the pods in the default namespace for you. 100 | 101 | [Claude uses kubectl-mcp-tool] 102 | Command: kubectl get pods -n default 103 | 104 | Result: 105 | NAME READY STATUS RESTARTS AGE 106 | nginx-pod 1/1 Running 0 1h 107 | web-deployment-abc123 1/1 Running 0 45m 108 | db-statefulset-0 1/1 Running 0 30m 109 | ``` 110 | 111 | ### Example 2: Switching Namespaces 112 | 113 | ``` 114 | User: Switch to the kube-system namespace 115 | Claude: I'll switch to the kube-system namespace. 116 | 117 | [Claude uses kubectl-mcp-tool] 118 | Command: kubectl config set-context --current --namespace kube-system 119 | 120 | Result: 121 | Switched to namespace kube-system 122 | ``` 123 | 124 | ### Example 3: Checking Current Namespace 125 | 126 | ``` 127 | User: What namespace am I currently in? 128 | Claude: Let me check your current namespace. 129 | 130 | [Claude uses kubectl-mcp-tool] 131 | Command: kubectl config view --minify --output jsonpath={..namespace} 132 | 133 | Result: 134 | kube-system 135 | ``` 136 | 137 | ## Troubleshooting 138 | 139 | If Claude has trouble connecting to your kubectl-mcp-tool: 140 | 141 | 1. **Check Server Status**: 142 | - Verify that the MCP server is running 143 | - Check the server logs for errors 144 | 145 | 2. **Verify URL**: 146 | - If using the expose feature, make sure the URL is correct and accessible 147 | - Test the URL directly in a browser or with curl 148 | 149 | 3. **Authentication**: 150 | - If your Kubernetes cluster requires authentication, make sure the kubectl-mcp-tool has the necessary credentials 151 | 152 | 4. **Permissions**: 153 | - Ensure that Claude has permission to access your MCP server 154 | - Check firewall settings if necessary 155 | 156 | ## Advanced Configuration 157 | 158 | For advanced configuration options, see the [Configuration Guide](./configuration.md). 159 | 160 | For installation details, see the [Installation Guide](../INSTALLATION.md). 161 | -------------------------------------------------------------------------------- /docs/cursor/cursor-mcp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitg00/kubectl-mcp-server/069365bc180ae641572500f66abc8cd4845107d7/docs/cursor/cursor-mcp.gif -------------------------------------------------------------------------------- /docs/cursor/cursor_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubectl-mcp-tool", 3 | "description": "Kubernetes operations using natural language", 4 | "command": "python3 /absolute/path/to/kubectl-mcp-tool/cursor_compatible_mcp_server.py", 5 | "transport": "stdio", 6 | "mcpServers": { 7 | "kubernetes": { 8 | "command": "python", 9 | "args": ["-m", "kubectl_mcp_tool.cli"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/cursor/cursor_configuration.md: -------------------------------------------------------------------------------- 1 | # Cursor Configuration Guide for kubectl-mcp-tool 2 | 3 | This guide explains how to configure Cursor AI assistant to work with the kubectl-mcp-tool for natural language Kubernetes operations. 4 | 5 | ## Prerequisites 6 | 7 | - kubectl-mcp-tool installed 8 | - Cursor AI assistant 9 | - Python 3.8+ 10 | 11 | ## Configuration Steps 12 | 13 | 1. **Install the kubectl-mcp-tool**: 14 | ```bash 15 | pip install -e /path/to/kubectl-mcp-tool 16 | ``` 17 | 18 | 2. **Open Cursor AI Assistant**: 19 | - Launch Cursor AI assistant 20 | - Navigate to Settings 21 | 22 | 3. **Configure the Tool**: 23 | - Go to the Tools section in Settings 24 | - Add a new tool with the following configuration: 25 | - **Tool Name**: `kubectl-mcp-tool` 26 | - **Command**: `python -m kubectl_mcp_tool.cli serve --transport stdio --cursor` 27 | - **Working Directory**: `/path/to/kubectl-mcp-tool` 28 | 29 | ![Cursor Configuration](../screenshots/cursor_config.png) 30 | 31 | 4. **Enable the Tool**: 32 | - Make sure the tool is enabled in Cursor 33 | - If the tool shows "Client closed" status, try restarting Cursor and the tool 34 | 35 | ## Troubleshooting 36 | 37 | If you encounter the "Client closed" status: 38 | 39 | 1. **Restart the Tool**: 40 | - Disable and re-enable the tool in Cursor settings 41 | - Restart Cursor 42 | 43 | 2. **Check Logs**: 44 | - Look for error messages in the following log files: 45 | - `simple_mcp_server.log` 46 | - `kubectl_mcp_tool_cli.log` 47 | 48 | 3. **Verify Command**: 49 | - Make sure the command is correct: `python -m kubectl_mcp_tool.cli serve --transport stdio --cursor` 50 | - Ensure the working directory is correct 51 | 52 | 4. **Test Directly**: 53 | - Run the command directly in a terminal to verify it works: 54 | ```bash 55 | python -m kubectl_mcp_tool.cli serve --cursor 56 | ``` 57 | 58 | ## Using the Tool in Cursor 59 | 60 | Once configured, you can use natural language commands in Cursor: 61 | 62 | - "Get all pods" 63 | - "Show namespaces" 64 | - "Switch to namespace kube-system" 65 | - "What is my current namespace" 66 | 67 | Example conversation: 68 | 69 | ``` 70 | User: Get all pods in the default namespace 71 | Cursor: [Uses kubectl-mcp-tool] 72 | Command: kubectl get pods -n default 73 | 74 | Result: 75 | NAME READY STATUS RESTARTS AGE 76 | nginx-pod 1/1 Running 0 1h 77 | web-deployment-abc123 1/1 Running 0 45m 78 | db-statefulset-0 1/1 Running 0 30m 79 | ``` 80 | 81 | ## Verifying Tool Registration 82 | 83 | To verify that the tool is properly registered with Cursor, you can run the test script: 84 | 85 | ```bash 86 | python test_cursor_tool_registration.py 87 | ``` 88 | 89 | This script will test the tool registration and functionality, ensuring that all required tools are available and working correctly. 90 | -------------------------------------------------------------------------------- /docs/cursor/cursor_configuration_guide.md: -------------------------------------------------------------------------------- 1 | # Cursor Configuration Guide for kubectl-mcp-tool 2 | 3 | This guide provides step-by-step instructions for configuring Cursor to work with the kubectl-mcp-tool MCP server. 4 | 5 | ## Prerequisites 6 | 7 | - Cursor installed on your machine 8 | - kubectl-mcp-tool installed and configured 9 | - Python 3.8+ installed 10 | 11 | ## Configuration Steps 12 | 13 | ### 1. Start the MCP Server 14 | 15 | First, start the kubectl-mcp-tool MCP server with Cursor compatibility mode: 16 | 17 | ```bash 18 | python cursor_compatible_mcp_server.py 19 | ``` 20 | 21 | This will start the server using stdio transport, which is compatible with Cursor's MCP implementation. 22 | 23 | ### 2. Configure Cursor 24 | 25 | 1. Open Cursor and go to Settings 26 | 2. Navigate to the "AI & Copilot" section 27 | 3. Scroll down to "Tools" or "Extensions" 28 | 4. Click "Add Tool" or "Add Custom Tool" 29 | 5. Enter the following configuration: 30 | 31 | ```json 32 | { 33 | "name": "kubectl-mcp-tool", 34 | "description": "Kubernetes operations using natural language", 35 | "command": "python /path/to/kubectl-mcp-tool/cursor_compatible_mcp_server.py", 36 | "transport": "stdio" 37 | } 38 | ``` 39 | 40 | Replace `/path/to/kubectl-mcp-tool/` with the actual path to your kubectl-mcp-tool installation. 41 | 42 | ### 3. Test the Integration 43 | 44 | 1. Open a new chat in Cursor 45 | 2. Type a natural language kubectl command, such as: 46 | - "Get all pods in the default namespace" 47 | - "Show me all deployments" 48 | - "Switch to the kube-system namespace" 49 | 50 | 3. Cursor should execute the command using the kubectl-mcp-tool and display the results 51 | 52 | ## Example Commands 53 | 54 | Here are some example natural language commands you can use: 55 | 56 | - "Get all pods" 57 | - "Show namespaces" 58 | - "Switch to namespace kube-system" 59 | - "Get deployments in namespace default" 60 | - "Describe pod nginx-pod" 61 | - "Scale deployment nginx to 3 replicas" 62 | - "Get logs from pod web-deployment-abc123" 63 | 64 | ## Troubleshooting 65 | 66 | ### Common Issues 67 | 68 | 1. **"Client closed" error**: 69 | - Make sure the MCP server is running before sending commands 70 | - Check that the path in the Cursor configuration is correct 71 | - Verify that the server is running in Cursor compatibility mode 72 | 73 | 2. **Mock data is shown instead of real kubectl output**: 74 | - Ensure you have a running Kubernetes cluster (e.g., minikube) 75 | - Check that kubectl is properly configured on your system 76 | - Verify that you have the necessary permissions to execute kubectl commands 77 | 78 | 3. **Server not responding**: 79 | - Check the server logs for errors (cursor_mcp_debug.log) 80 | - Restart the MCP server 81 | - Verify that no other process is using the same port 82 | 83 | ### Logs 84 | 85 | The MCP server creates log files that can help diagnose issues: 86 | 87 | - `cursor_mcp_server.log`: General server logs 88 | - `cursor_mcp_debug.log`: Detailed debug logs including protocol messages 89 | -------------------------------------------------------------------------------- /docs/cursor/cursor_integration.md: -------------------------------------------------------------------------------- 1 | # Cursor Integration Guide for kubectl-mcp-tool 2 | 3 | This guide explains how to integrate the kubectl-mcp-tool with Cursor AI assistant for natural language Kubernetes operations. 4 | 5 | ## Prerequisites 6 | 7 | - kubectl-mcp-tool installed 8 | - Cursor AI assistant 9 | - Python 3.8+ 10 | 11 | ## Setup 12 | 13 | 1. Install the kubectl-mcp-tool: 14 | ```bash 15 | pip install -e /path/to/kubectl-mcp-tool 16 | ``` 17 | 18 | 2. Start the MCP server with Cursor compatibility mode: 19 | ```bash 20 | python -m kubectl_mcp_tool.cli serve --cursor 21 | ``` 22 | 23 | 3. Configure Cursor to use the kubectl-mcp-tool: 24 | - Open Cursor AI assistant 25 | - Go to Settings > Tools 26 | - Add a new tool with the following configuration: 27 | - Tool Name: `kubectl-mcp-tool` 28 | - Command: `python -m kubectl_mcp_tool.cli serve --cursor` 29 | - Working Directory: `/path/to/kubectl-mcp-tool` 30 | 31 | ## Available Tools 32 | 33 | The kubectl-mcp-tool provides the following tools through the MCP interface: 34 | 35 | | Tool Name | Description | Parameters | 36 | |-----------|-------------|------------| 37 | | `process_natural_language` | Process natural language queries for kubectl operations | `query`: The natural language query to process | 38 | | `get_pods` | Get all pods in a namespace | `namespace`: The namespace to get pods from (optional) | 39 | | `get_namespaces` | Get all namespaces in the cluster | None | 40 | | `switch_namespace` | Switch to a different namespace | `namespace`: The namespace to switch to | 41 | | `get_current_namespace` | Get the current namespace | None | 42 | | `get_deployments` | Get all deployments in a namespace | `namespace`: The namespace to get deployments from (optional) | 43 | 44 | ## Example Natural Language Commands 45 | 46 | You can use the following natural language commands with Cursor: 47 | 48 | - "Get all pods" 49 | - "Show namespaces" 50 | - "Switch to namespace kube-system" 51 | - "What is my current namespace" 52 | - "Get deployments" 53 | - "Get services" 54 | - "Describe pod nginx-pod" 55 | 56 | ## Troubleshooting 57 | 58 | If you encounter issues with the Cursor integration, try the following: 59 | 60 | 1. **Client Closed Status**: If Cursor shows "Client closed" status, try restarting Cursor and the MCP server. 61 | 62 | 2. **Check Logs**: Look for error messages in the following log files: 63 | - `simple_mcp_server.log` 64 | - `simple_mcp_debug.log` 65 | - `kubectl_mcp_tool_cli.log` 66 | 67 | 3. **Test the Server**: Run the test script to verify the MCP server is working correctly: 68 | ```bash 69 | python test_simple_mcp_server.py 70 | ``` 71 | 72 | 4. **Cursor Configuration**: Ensure the tool is registered correctly in Cursor's configuration. 73 | 74 | 5. **Direct Server Run**: Try running the simple MCP server directly: 75 | ```bash 76 | python simple_mcp_server.py 77 | ``` 78 | 79 | ## Advanced Configuration 80 | 81 | For advanced configuration options, see the [Configuration Guide](./configuration.md). 82 | 83 | ## Example Usage in Cursor 84 | 85 | Once configured, you can use the kubectl-mcp-tool in Cursor by typing natural language commands like: 86 | 87 | ``` 88 | Get all pods in the default namespace 89 | ``` 90 | 91 | Cursor will use the kubectl-mcp-tool to execute the command and return the results. 92 | -------------------------------------------------------------------------------- /docs/cursor/cursor_integration_guide_updated.md: -------------------------------------------------------------------------------- 1 | # Cursor Integration Guide for kubectl-mcp-tool 2 | 3 | This guide provides step-by-step instructions for configuring Cursor to work with the kubectl-mcp-tool MCP server. 4 | 5 | ## Prerequisites 6 | 7 | - Cursor installed on your machine 8 | - kubectl-mcp-tool installed and configured 9 | - Python 3.8+ installed 10 | 11 | ## Configuration Steps 12 | 13 | ### 1. Install the kubectl-mcp-tool 14 | 15 | First, download and install the kubectl-mcp-tool: 16 | 17 | ```bash 18 | # Clone the repository 19 | git clone https://github.com/your-username/kubectl-mcp-tool.git 20 | 21 | # Install dependencies 22 | cd kubectl-mcp-tool 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | ### 2. Start the MCP Server 27 | 28 | The kubectl-mcp-tool MCP server uses stdio transport for Cursor compatibility: 29 | 30 | ```bash 31 | python cursor_compatible_mcp_server.py 32 | ``` 33 | 34 | ### 3. Configure Cursor 35 | 36 | 1. Open Cursor and go to Settings 37 | 2. Navigate to the "AI & Copilot" section 38 | 3. Scroll down to the "MCP" section 39 | 4. Click "Add new global MCP server" 40 | 5. Enter the following configuration in `~/.cursor/mcp.json`: 41 | 42 | ```json 43 | { 44 | "mcpServers": { 45 | "kubernetes": { 46 | "command": "python", 47 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 48 | "env": { 49 | "KUBECONFIG": "/path/to/your/.kube/config", 50 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin" 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | Replace `/path/to/your/.kube/config` with the actual path to your kubeconfig file. 58 | On most systems, this is `~/.kube/config`. 59 | 60 | Save this configuration to `~/.cursor/mcp.json` for global settings. 61 | 62 | Note: This configuration uses the minimal wrapper approach which has better compatibility with different MCP SDK versions. 63 | 64 | ### 4. Test the Integration 65 | 66 | You can test the integration by: 67 | 68 | 1. Start Cursor 69 | 2. Open a new file or project 70 | 3. Ask a Kubernetes-related question like: 71 | - "List all pods in the default namespace" 72 | - "What deployments are running in my cluster?" 73 | - "Show me the services in the kube-system namespace" 74 | 75 | ### 5. Automated Setup 76 | 77 | For an automated setup, you can run the installation script: 78 | 79 | ```bash 80 | bash install.sh 81 | ``` 82 | 83 | This script will: 84 | 1. Install all required dependencies 85 | 2. Create the correct configuration file for Cursor 86 | 3. Set up the environment variables properly 87 | 4. Verify kubectl access 88 | 89 | ## Example Commands 90 | 91 | Here are some example natural language commands you can use: 92 | 93 | - "Get all pods" 94 | - "Show namespaces" 95 | - "Switch to namespace kube-system" 96 | - "Get deployments in namespace default" 97 | - "Describe pod nginx-pod" 98 | - "Scale deployment nginx to 3 replicas" 99 | - "Get logs from pod web-deployment-abc123" 100 | 101 | ## Troubleshooting 102 | 103 | ### Common Issues 104 | 105 | 1. **"Client closed" error**: 106 | - Make sure the MCP server is running before sending commands 107 | - Check that the path in the Cursor configuration is correct 108 | - Verify that the server is running in Cursor compatibility mode 109 | 110 | 2. **Mock data is shown instead of real kubectl output**: 111 | - Ensure you have a running Kubernetes cluster (e.g., minikube) 112 | - Check that kubectl is properly configured on your system 113 | - Verify that you have the necessary permissions to execute kubectl commands 114 | 115 | 3. **Server not responding**: 116 | - Check the server logs for errors (cursor_mcp_debug.log) 117 | - Restart the MCP server 118 | - Verify that no other process is using the same port 119 | 120 | ### Logs 121 | 122 | The MCP server creates log files that can help diagnose issues: 123 | 124 | - `cursor_mcp_server.log`: General server logs 125 | - `cursor_mcp_debug.log`: Detailed debug logs including protocol messages 126 | 127 | ## Remote Access (Optional) 128 | 129 | If you need to access the kubectl-mcp-tool from a different machine, you can use the SSE transport mode and expose the port: 130 | 131 | ```bash 132 | # Start the server with SSE transport 133 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 134 | 135 | # Expose the port (requires additional setup) 136 | # This would typically be done through a secure tunnel or VPN 137 | ``` 138 | 139 | Note: Remote access should be configured with proper security measures to prevent unauthorized access to your Kubernetes cluster. 140 | -------------------------------------------------------------------------------- /docs/integration_guide.md: -------------------------------------------------------------------------------- 1 | # Integration Guide for kubectl-mcp-tool 2 | 3 | This guide explains how to integrate the kubectl-mcp-tool with various AI assistants for natural language Kubernetes operations. 4 | 5 | ## Table of Contents 6 | 7 | - [Cursor Integration](#cursor-integration) 8 | - [Windsurf Integration](#windsurf-integration) 9 | - [Claude Integration](#claude-integration) 10 | - [General MCP Integration](#general-mcp-integration) 11 | 12 | ## Cursor Integration 13 | 14 | ### Prerequisites 15 | 16 | - kubectl-mcp-tool installed 17 | - Cursor AI assistant 18 | - Python 3.9+ 19 | 20 | ### Configuration Steps 21 | 22 | 1. **Install the kubectl-mcp-tool**: 23 | ```bash 24 | pip install kubectl-mcp-tool 25 | # Or install from source 26 | pip install -e /path/to/kubectl-mcp-server 27 | ``` 28 | 29 | 2. **Open Cursor AI Assistant**: 30 | - Launch Cursor AI assistant 31 | - Navigate to Settings 32 | 33 | 3. **Configure the Tool**: 34 | - Go to the Tools section in Settings 35 | - Add a new tool with the following configuration: 36 | - **Tool Name**: `kubectl-mcp-tool` 37 | - **Command**: `python -m kubectl_mcp_tool.cli serve --transport stdio --cursor` 38 | - **Working Directory**: `/path/to/kubectl-mcp-server` 39 | 40 | 4. **Enable the Tool**: 41 | - Make sure the tool is enabled in Cursor 42 | - If the tool shows "Client closed" status, try restarting Cursor and the tool 43 | 44 | ### Using kubectl-mcp-tool with Cursor 45 | 46 | Once configured, you can use natural language commands in Cursor: 47 | 48 | - "Get all pods" 49 | - "Show namespaces" 50 | - "Switch to namespace kube-system" 51 | - "What is my current namespace" 52 | 53 | Example conversation: 54 | 55 | ``` 56 | User: Get all pods in the default namespace 57 | Cursor: [Uses kubectl-mcp-tool] 58 | Command: kubectl get pods -n default 59 | 60 | Result: 61 | NAME READY STATUS RESTARTS AGE 62 | nginx-pod 1/1 Running 0 1h 63 | web-deployment-abc123 1/1 Running 0 45m 64 | db-statefulset-0 1/1 Running 0 30m 65 | ``` 66 | 67 | ### Troubleshooting Cursor Integration 68 | 69 | If you encounter the "Client closed" status: 70 | 71 | 1. **Restart the Tool**: 72 | - Disable and re-enable the tool in Cursor settings 73 | - Restart Cursor 74 | 75 | 2. **Check Logs**: 76 | - Look for error messages in the following log files: 77 | - `simple_mcp_server.log` 78 | - `kubectl_mcp_tool_cli.log` 79 | 80 | 3. **Verify Command**: 81 | - Make sure the command is correct: `python -m kubectl_mcp_tool.cli serve --transport stdio --cursor` 82 | - Ensure the working directory is correct 83 | 84 | 4. **Test Directly**: 85 | - Run the command directly in a terminal to verify it works: 86 | ```bash 87 | python -m kubectl_mcp_tool.cli serve --cursor 88 | ``` 89 | 90 | ## Windsurf Integration 91 | 92 | ### Prerequisites 93 | 94 | - kubectl-mcp-tool installed 95 | - Windsurf AI assistant 96 | - Python 3.9+ 97 | 98 | ### Configuration Steps 99 | 100 | 1. **Install the kubectl-mcp-tool**: 101 | ```bash 102 | pip install kubectl-mcp-tool 103 | # Or install from source 104 | pip install -e /path/to/kubectl-mcp-server 105 | ``` 106 | 107 | 2. **Start the MCP server**: 108 | ```bash 109 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 110 | ``` 111 | 112 | 3. **Configure Windsurf**: 113 | - Open Windsurf AI assistant 114 | - Go to Settings > Tools 115 | - Add a new tool with the following configuration: 116 | - **Tool Name**: `kubectl-mcp-tool` 117 | - **URL**: `http://localhost:8080` 118 | - **Transport**: `SSE` 119 | 120 | 4. **Enable the Tool**: 121 | - Make sure the tool is enabled in Windsurf 122 | - If the tool shows as disconnected, check that the MCP server is running 123 | 124 | ### Using kubectl-mcp-tool with Windsurf 125 | 126 | Once configured, you can use natural language commands in Windsurf: 127 | 128 | - "Get all pods" 129 | - "Show namespaces" 130 | - "Switch to namespace kube-system" 131 | - "What is my current namespace" 132 | 133 | Example conversation: 134 | 135 | ``` 136 | User: Get all pods in the default namespace 137 | Windsurf: [Uses kubectl-mcp-tool] 138 | Command: kubectl get pods -n default 139 | 140 | Result: 141 | NAME READY STATUS RESTARTS AGE 142 | nginx-pod 1/1 Running 0 1h 143 | web-deployment-abc123 1/1 Running 0 45m 144 | db-statefulset-0 1/1 Running 0 30m 145 | ``` 146 | 147 | ### Troubleshooting Windsurf Integration 148 | 149 | If you encounter connection issues: 150 | 151 | 1. **Check Server Status**: 152 | - Verify that the MCP server is running 153 | - Check the server logs for errors 154 | 155 | 2. **Verify Port**: 156 | - Make sure port 8080 is not being used by another application 157 | - If needed, specify a different port with the `--port` option 158 | 159 | 3. **Test Directly**: 160 | - Test the server directly using curl: 161 | ```bash 162 | curl -N http://localhost:8080/mcp 163 | ``` 164 | 165 | 4. **Firewall Issues**: 166 | - Check if a firewall is blocking the connection 167 | - Ensure that localhost connections are allowed 168 | 169 | ## Claude Integration 170 | 171 | ### Prerequisites 172 | 173 | - kubectl-mcp-tool installed 174 | - Claude AI access 175 | - Python 3.9+ 176 | 177 | ### Integration Steps 178 | 179 | 1. **Install the kubectl-mcp-tool**: 180 | ```bash 181 | pip install kubectl-mcp-tool 182 | # Or install from source 183 | pip install -e /path/to/kubectl-mcp-server 184 | ``` 185 | 186 | 2. **Start the MCP server**: 187 | ```bash 188 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 189 | ``` 190 | 191 | 3. **Expose the MCP server** (optional, if Claude needs to access it remotely): 192 | ```bash 193 | python -m kubectl_mcp_tool.cli expose 8080 194 | ``` 195 | 196 | 4. **Configure Claude**: 197 | - When using Claude in a web interface or API, you can provide the URL of your MCP server 198 | - For Claude in Cursor, follow the [Cursor Integration](#cursor-integration) section 199 | 200 | ### Using kubectl-mcp-tool with Claude 201 | 202 | Claude can interact with kubectl-mcp-tool using natural language commands. Here are some examples: 203 | 204 | #### Example 1: Getting Pods 205 | 206 | ``` 207 | User: Get all pods in the default namespace 208 | Claude: Let me check the pods in the default namespace for you. 209 | 210 | [Claude uses kubectl-mcp-tool] 211 | Command: kubectl get pods -n default 212 | 213 | Result: 214 | NAME READY STATUS RESTARTS AGE 215 | nginx-pod 1/1 Running 0 1h 216 | web-deployment-abc123 1/1 Running 0 45m 217 | db-statefulset-0 1/1 Running 0 30m 218 | ``` 219 | 220 | #### Example 2: Switching Namespaces 221 | 222 | ``` 223 | User: Switch to the kube-system namespace 224 | Claude: I'll switch to the kube-system namespace. 225 | 226 | [Claude uses kubectl-mcp-tool] 227 | Command: kubectl config set-context --current --namespace kube-system 228 | 229 | Result: 230 | Switched to namespace kube-system 231 | ``` 232 | 233 | #### Example 3: Checking Current Namespace 234 | 235 | ``` 236 | User: What namespace am I currently in? 237 | Claude: Let me check your current namespace. 238 | 239 | [Claude uses kubectl-mcp-tool] 240 | Command: kubectl config view --minify --output jsonpath={..namespace} 241 | 242 | Result: 243 | kube-system 244 | ``` 245 | 246 | ### Troubleshooting Claude Integration 247 | 248 | If Claude has trouble connecting to your kubectl-mcp-tool: 249 | 250 | 1. **Check Server Status**: 251 | - Verify that the MCP server is running 252 | - Check the server logs for errors 253 | 254 | 2. **Verify URL**: 255 | - If using the expose feature, make sure the URL is correct and accessible 256 | - Test the URL directly in a browser or with curl 257 | 258 | ## General MCP Integration 259 | 260 | The kubectl-mcp-tool implements the Model Context Protocol (MCP) specification from [modelcontextprotocol.io](https://modelcontextprotocol.io/). This allows it to be integrated with any AI assistant that supports the MCP protocol. 261 | 262 | ### MCP Protocol Implementation 263 | 264 | The kubectl-mcp-tool implements the following MCP methods: 265 | 266 | - `mcp.initialize`: Initialize the MCP server 267 | - `mcp.tools.list`: List available tools 268 | - `mcp.tool.call`: Call a specific tool 269 | 270 | ### Available Tools 271 | 272 | The kubectl-mcp-tool provides the following tools through the MCP interface: 273 | 274 | | Tool Name | Description | Parameters | 275 | |-----------|-------------|------------| 276 | | `process_natural_language` | Process natural language queries for kubectl operations | `query`: The natural language query to process | 277 | | `get_pods` | Get all pods in a namespace | `namespace`: The namespace to get pods from (optional) | 278 | | `get_namespaces` | Get all namespaces in the cluster | None | 279 | | `switch_namespace` | Switch to a different namespace | `namespace`: The namespace to switch to | 280 | | `get_current_namespace` | Get the current namespace | None | 281 | | `get_deployments` | Get all deployments in a namespace | `namespace`: The namespace to get deployments from (optional) | 282 | 283 | ### Example MCP Requests 284 | 285 | Here's an example of how to call the `process_natural_language` tool using the MCP protocol: 286 | 287 | ```json 288 | { 289 | "jsonrpc": "2.0", 290 | "id": "1", 291 | "method": "mcp.tool.call", 292 | "params": { 293 | "name": "process_natural_language", 294 | "input": { 295 | "query": "get all pods" 296 | } 297 | } 298 | } 299 | ``` 300 | 301 | And here's an example response: 302 | 303 | ```json 304 | { 305 | "jsonrpc": "2.0", 306 | "id": "1", 307 | "result": { 308 | "output": [ 309 | { 310 | "type": "text", 311 | "text": "Command: kubectl get pods\n\nResult:\nNAME READY STATUS RESTARTS AGE\nnginx-pod 1/1 Running 0 1h\nweb-deployment-abc123 1/1 Running 0 45m\ndb-statefulset-0 1/1 Running 0 30m" 312 | } 313 | ] 314 | } 315 | } 316 | ``` 317 | 318 | ### Advanced MCP Configuration 319 | 320 | For advanced MCP configuration options, see the [MCP Specification](https://spec.modelcontextprotocol.io/specification/2024-11-05/). 321 | -------------------------------------------------------------------------------- /docs/kwargs_parameter_fix.md: -------------------------------------------------------------------------------- 1 | # Kwargs Parameter Fix 2 | 3 | ## Issue Description 4 | 5 | Claude Desktop was experiencing errors with the Kubernetes MCP server: 6 | 7 | ``` 8 | Error in handler: main..process_natural_language() got an unexpected keyword argument 'kwargs' 9 | Error in handler: main..advanced_kubernetes_query() got an unexpected keyword argument 'kwargs' 10 | Error in handler: main..kubernetes_ping() got an unexpected keyword argument 'kwargs' 11 | ``` 12 | 13 | ## Root Cause 14 | 15 | The function signatures in `minimal_wrapper.py` and `cursor_compatible_mcp_server.py` did not properly handle variable keyword arguments. While previous fixes addressed the `args` parameter, Claude Desktop was also sending additional parameters as keyword arguments. 16 | 17 | The issue was that the functions were defined with: 18 | ```python 19 | async def process_natural_language(query: str, args: List[str] = None, kwargs: Dict[str, Any] = None): 20 | ``` 21 | 22 | This treats `kwargs` as a regular parameter that expects a dictionary, not as a variable keyword arguments collector. When Claude Desktop sent actual keyword arguments, Python tried to assign them to a parameter named `kwargs` rather than collecting them as variable keyword arguments. 23 | 24 | ## Fix 25 | 26 | We've updated the function signatures to use Python's `**kwargs` syntax for variable keyword arguments: 27 | 28 | ```python 29 | # Before 30 | async def process_natural_language(query: str, args: List[str] = None, kwargs: Dict[str, Any] = None): 31 | # function body 32 | 33 | # After 34 | async def process_natural_language(query: str, args: List[str] = None, **kwargs): 35 | # function body 36 | ``` 37 | 38 | Similar changes were made to: 39 | - `advanced_kubernetes_query()` in minimal_wrapper.py 40 | - `kubernetes_ping()` in minimal_wrapper.py 41 | - `process_query()` in cursor_compatible_mcp_server.py 42 | 43 | We also updated all tool calls in cursor_compatible_mcp_server.py to extract and pass kwargs consistently: 44 | 45 | ```python 46 | # Before 47 | result = await self.tools.process_query(query, args) 48 | 49 | # After 50 | kwargs = {k: v for k, v in tool_input.items() if k not in ["query", "args"]} 51 | result = await self.tools.process_query(query, args, **kwargs) 52 | ``` 53 | 54 | ## Testing 55 | 56 | To verify this fix: 57 | 58 | 1. Created a test script `test_kwargs_parameter_fix.py` that simulates Claude Desktop's interaction with the MCP server 59 | 2. Tested with additional keyword arguments to ensure all functions handle them correctly 60 | 3. Verified that the functions no longer raise the "unexpected keyword argument" error 61 | 4. Checked that all tool calls in cursor_compatible_mcp_server.py consistently pass kwargs 62 | 63 | The test script generates test requests in JSON-RPC format and writes them to a file named "test_kwargs_requests.json" that can be used to test the MCP server directly. 64 | 65 | ## Related PRs 66 | 67 | - PR #19: Args parameter fix 68 | - PR #22: Function signature standardization 69 | - PR #23: Schema definition fix 70 | - PR #24: Function call fix for args parameter 71 | 72 | This fix completes the series of changes needed to ensure full compatibility with Claude Desktop and Cursor. 73 | 74 | # Fixing "Unexpected non-whitespace character after JSON" Errors (v1.1.1+) 75 | 76 | ## Problem 77 | 78 | When using kubectl-mcp-tool with AI assistants like Claude Desktop or Cursor, you might encounter an error where the MCP server becomes unresponsive or shows as disconnected after a single query, with an error message like: 79 | 80 | ``` 81 | Unexpected non-whitespace character after JSON at position 4 (line 1 column 5) 82 | ``` 83 | 84 | This commonly happens because logs or debug information are being written to stdout, which corrupts the JSON-RPC communication channel. 85 | 86 | ## Solution 87 | 88 | Version 1.1.1 fixes this issue by: 89 | 90 | 1. Properly directing all logs to either stderr or a log file 91 | 2. Adding comprehensive JSON validation and sanitization 92 | 3. Handling special characters and BOM in JSON responses 93 | 4. Implementing better error recovery 94 | 95 | ## How to Fix 96 | 97 | 1. **Update to the latest version**: 98 | ```bash 99 | pip install kubectl-mcp-tool==1.1.1 100 | ``` 101 | 102 | 2. **Use the minimal wrapper in your configuration**: 103 | ```json 104 | { 105 | "mcpServers": { 106 | "kubernetes": { 107 | "command": "python", 108 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 109 | "env": { 110 | "KUBECONFIG": "/path/to/your/.kube/config", 111 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", 112 | "MCP_LOG_FILE": "/path/to/logs/debug.log" 113 | } 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | 3. **Specify a log file**: Set the `MCP_LOG_FILE` environment variable to direct logs to a file instead of stderr. 120 | 121 | 4. **Test connection**: Use the included ping utility to verify your server is working correctly: 122 | ```bash 123 | python -m kubectl_mcp_tool.simple_ping 124 | ``` 125 | 126 | ## Detailed Technical Explanation 127 | 128 | The error occurs because: 129 | 130 | 1. **Mixed Channel Communication**: JSON-RPC protocol requires clean stdout for communication, but logs were contaminating this channel 131 | 2. **BOM Characters**: Some Unicode Byte Order Mark (BOM) or invisible characters were being included in the JSON 132 | 3. **Position 4 Issue**: The character at position 4 (1-indexed) in the JSON response was often invalid 133 | 134 | The fix involves: 135 | 1. Proper logger configuration to use stderr or log files 136 | 2. Sanitizing JSON strings before transmission 137 | 3. Validating JSON responses before sending 138 | 4. Adding special handling for known error patterns 139 | -------------------------------------------------------------------------------- /docs/windsurf/windsurf-mcp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitg00/kubectl-mcp-server/069365bc180ae641572500f66abc8cd4845107d7/docs/windsurf/windsurf-mcp.gif -------------------------------------------------------------------------------- /docs/windsurf/windsurf_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Servers": { 3 | "mcp-server-firecrawl": { 4 | "url": "http://localhost:8080" 5 | }, 6 | "mcp-server-kubectl-mcp-tool": { 7 | "name": "kubectl-mcp-tool", 8 | "description": "Kubernetes operations using natural language", 9 | "command": "python3 /absolute/path/to/kubectl-mcp-tool/windsurf_compatible_mcp_server.py", 10 | "transport": "sse" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/windsurf/windsurf_integration.md: -------------------------------------------------------------------------------- 1 | # Windsurf Integration Guide for kubectl-mcp-tool 2 | 3 | This guide explains how to integrate the kubectl-mcp-tool with Windsurf for natural language Kubernetes operations. 4 | 5 | ## Prerequisites 6 | 7 | - kubectl-mcp-tool installed 8 | - Windsurf AI assistant 9 | - Python 3.8+ 10 | 11 | ## Configuration Steps 12 | 13 | 1. **Install the kubectl-mcp-tool**: 14 | ```bash 15 | pip install -e /path/to/kubectl-mcp-tool 16 | ``` 17 | 18 | 2. **Start the MCP server**: 19 | ```bash 20 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 21 | ``` 22 | 23 | 3. **Configure Windsurf**: 24 | - Open Windsurf AI assistant 25 | - Go to Settings > Tools 26 | - Add a new tool with the following configuration: 27 | - **Tool Name**: `kubectl-mcp-tool` 28 | - **URL**: `http://localhost:8080` 29 | - **Transport**: `SSE` 30 | 31 | 4. **Enable the Tool**: 32 | - Make sure the tool is enabled in Windsurf 33 | - If the tool shows as disconnected, check that the MCP server is running 34 | 35 | ## Using the Tool in Windsurf 36 | 37 | Once configured, you can use natural language commands in Windsurf: 38 | 39 | - "Get all pods" 40 | - "Show namespaces" 41 | - "Switch to namespace kube-system" 42 | - "What is my current namespace" 43 | 44 | Example conversation: 45 | 46 | ``` 47 | User: Get all pods in the default namespace 48 | Windsurf: [Uses kubectl-mcp-tool] 49 | Command: kubectl get pods -n default 50 | 51 | Result: 52 | NAME READY STATUS RESTARTS AGE 53 | nginx-pod 1/1 Running 0 1h 54 | web-deployment-abc123 1/1 Running 0 45m 55 | db-statefulset-0 1/1 Running 0 30m 56 | ``` 57 | 58 | ## Troubleshooting 59 | 60 | If you encounter connection issues: 61 | 62 | 1. **Check Server Status**: 63 | - Verify that the MCP server is running 64 | - Check the server logs for errors 65 | 66 | 2. **Verify Port**: 67 | - Make sure port 8080 is not being used by another application 68 | - If needed, specify a different port with the `--port` option 69 | 70 | 3. **Test Directly**: 71 | - Test the server directly using curl: 72 | ```bash 73 | curl -N http://localhost:8080/mcp 74 | ``` 75 | 76 | 4. **Firewall Issues**: 77 | - Check if a firewall is blocking the connection 78 | - Ensure that localhost connections are allowed 79 | 80 | ## Advanced Configuration 81 | 82 | For advanced configuration options, see the [Configuration Guide](./configuration.md). 83 | -------------------------------------------------------------------------------- /docs/windsurf/windsurf_integration_guide_updated.md: -------------------------------------------------------------------------------- 1 | # Windsurf Integration Guide for kubectl-mcp-tool 2 | 3 | This guide provides step-by-step instructions for configuring Windsurf to work with the kubectl-mcp-tool MCP server. 4 | 5 | ## Prerequisites 6 | 7 | - Windsurf installed on your machine 8 | - kubectl-mcp-tool installed and configured 9 | - Python 3.8+ installed 10 | 11 | ## Configuration Steps 12 | 13 | ### 1. Install the kubectl-mcp-tool 14 | 15 | First, download and install the kubectl-mcp-tool: 16 | 17 | ```bash 18 | # Clone the repository 19 | git clone https://github.com/your-username/kubectl-mcp-tool.git 20 | 21 | # Install dependencies 22 | cd kubectl-mcp-tool 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | ### 2. Start the MCP Server with SSE Transport 27 | 28 | For Windsurf integration, you need to start the kubectl-mcp-tool MCP server with SSE (Server-Sent Events) transport: 29 | 30 | ```bash 31 | python -m kubectl_mcp_tool.cli serve --transport sse --port 8080 32 | ``` 33 | 34 | ### 3. Configure WindSurf 35 | 36 | To configure WindSurf to use the kubectl-mcp-tool: 37 | 38 | 1. Open or create the MCP configuration file for WindSurf: 39 | - macOS: `~/.config/windsurf/mcp.json` 40 | - Windows: `%APPDATA%\WindSurf\mcp.json` 41 | - Linux: `~/.config/windsurf/mcp.json` 42 | 43 | 2. Add the following configuration: 44 | 45 | ```json 46 | { 47 | "mcpServers": { 48 | "kubernetes": { 49 | "command": "python", 50 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 51 | "env": { 52 | "KUBECONFIG": "/path/to/your/.kube/config" 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | 3. Replace `/path/to/your/.kube/config` with the actual path to your kubeconfig file (usually `~/.kube/config`) 60 | 4. Save the file and restart WindSurf. 61 | 62 | Note: This configuration uses the minimal wrapper approach which has better compatibility with different MCP SDK versions. 63 | 64 | ### 4. Automated Setup 65 | 66 | For an automated setup, you can run the installation script: 67 | 68 | ```bash 69 | bash install.sh 70 | ``` 71 | 72 | This script will: 73 | 1. Install all required dependencies 74 | 2. Create the correct configuration file for WindSurf 75 | 3. Set up the environment variables properly 76 | 4. Verify kubectl access 77 | 78 | ### 5. Test the Integration 79 | 80 | You can test the integration by: 81 | 82 | 1. Start WindSurf 83 | 2. Ask a Kubernetes-related question like: 84 | - "List all pods in the default namespace" 85 | - "What deployments are running in my cluster?" 86 | - "Show me the services in the kube-system namespace" 87 | 88 | 3. Windsurf should execute the command using the kubectl-mcp-tool and display the results 89 | 90 | ## Example Commands 91 | 92 | Here are some example natural language commands you can use: 93 | 94 | - "Get all pods" 95 | - "Show namespaces" 96 | - "Switch to namespace kube-system" 97 | - "Get deployments in namespace default" 98 | - "Describe pod nginx-pod" 99 | - "Scale deployment nginx to 3 replicas" 100 | - "Get logs from pod web-deployment-abc123" 101 | 102 | ## Troubleshooting 103 | 104 | ### Common Issues 105 | 106 | 1. **Connection refused errors**: 107 | - Make sure the MCP server is running before sending commands 108 | - Check that the endpoint URL in the Windsurf configuration is correct 109 | - Verify that your public endpoint is properly forwarding to your local server 110 | 111 | 2. **Mock data is shown instead of real kubectl output**: 112 | - Ensure you have a running Kubernetes cluster (e.g., minikube) 113 | - Check that kubectl is properly configured on your system 114 | - Verify that you have the necessary permissions to execute kubectl commands 115 | 116 | 3. **Server not responding**: 117 | - Check the server logs for errors 118 | - Verify that the port (8080) is not being used by another application 119 | - Try restarting the MCP server 120 | 121 | ### Logs 122 | 123 | The MCP server creates log files that can help diagnose issues: 124 | 125 | - `mcp_server.log`: General server logs 126 | - `mcp_debug.log`: Detailed debug logs including protocol messages 127 | 128 | ## Secure Remote Access 129 | 130 | For production use, consider these security measures when exposing your kubectl-mcp-tool: 131 | 132 | 1. **Use HTTPS**: Always use HTTPS for your public endpoint 133 | 2. **Implement Authentication**: Add an authentication layer to your proxy 134 | 3. **Restrict Access**: Limit access to specific IP addresses 135 | 4. **Use a VPN**: Consider running your service within a VPN 136 | 137 | Note: Exposing kubectl operations to the internet carries security risks. Ensure proper security measures are in place before doing so. 138 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitg00/kubectl-mcp-server/069365bc180ae641572500f66abc8cd4845107d7/image.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Installing kubectl-mcp-tool..." 5 | 6 | # Uninstall any previous versions first 7 | pip uninstall -y kubectl-mcp-tool fastmcp mcp mcp-python || true 8 | 9 | # Install the correct version of MCP SDK 10 | pip install mcp>=1.5.0 11 | 12 | # Create logs directory if it doesn't exist 13 | mkdir -p logs 14 | 15 | # Install kubectl-mcp-tool in development mode 16 | pip install -e . 17 | 18 | # Create directories for all supported AI assistants 19 | mkdir -p ~/.cursor 20 | mkdir -p ~/.cursor/logs 21 | mkdir -p ~/.config/claude 22 | mkdir -p ~/.config/windsurf 23 | 24 | # Get absolute path to Python interpreter 25 | PYTHON_PATH=$(which python) 26 | echo "Using Python interpreter: $PYTHON_PATH" 27 | 28 | # Get absolute path to kubectl config 29 | KUBE_CONFIG="${KUBECONFIG:-$HOME/.kube/config}" 30 | echo "Using Kubernetes config: $KUBE_CONFIG" 31 | 32 | # Get absolute path to kubectl command 33 | KUBECTL_PATH=$(which kubectl) 34 | if [ -z "$KUBECTL_PATH" ]; then 35 | echo "WARNING: kubectl not found in PATH. Please install kubectl." 36 | KUBECTL_PATH="kubectl" 37 | fi 38 | echo "Using kubectl: $KUBECTL_PATH" 39 | 40 | # Create a test configuration for Cursor MCP with absolute path and minimal wrapper 41 | cat > ~/.cursor/mcp.json << EOF 42 | { 43 | "mcpServers": { 44 | "kubernetes": { 45 | "command": "$PYTHON_PATH", 46 | "args": ["-m", "kubectl_mcp_tool.minimal_wrapper"], 47 | "env": { 48 | "KUBECONFIG": "$KUBE_CONFIG", 49 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:$HOME/.pyenv/versions/3.10.0/bin" 50 | } 51 | } 52 | } 53 | } 54 | EOF 55 | 56 | # Create a test configuration for Claude with absolute path 57 | cat > ~/.config/claude/mcp.json << EOF 58 | { 59 | "mcpServers": { 60 | "kubernetes": { 61 | "command": "$PYTHON_PATH", 62 | "args": ["-m", "kubectl_mcp_tool.cli.cli", "serve"], 63 | "env": { 64 | "KUBECONFIG": "$KUBE_CONFIG" 65 | } 66 | } 67 | } 68 | } 69 | EOF 70 | 71 | # Create a test configuration for WindSurf with absolute path 72 | cat > ~/.config/windsurf/mcp.json << EOF 73 | { 74 | "mcpServers": { 75 | "kubernetes": { 76 | "command": "$PYTHON_PATH", 77 | "args": ["-m", "kubectl_mcp_tool.cli.cli", "serve"], 78 | "env": { 79 | "KUBECONFIG": "$KUBE_CONFIG" 80 | } 81 | } 82 | } 83 | } 84 | EOF 85 | 86 | # Test that kubectl is working 87 | echo "Testing kubectl access..." 88 | if $KUBECTL_PATH version --client > /dev/null 2>&1; then 89 | echo "✅ kubectl is working correctly" 90 | else 91 | echo "⚠️ kubectl test failed. You may need to set up kubernetes access." 92 | fi 93 | 94 | # Check if kubeconfig exists 95 | if [ -f "$KUBE_CONFIG" ]; then 96 | echo "✅ Kubernetes config exists at $KUBE_CONFIG" 97 | else 98 | echo "⚠️ Kubernetes config not found at $KUBE_CONFIG. You may need to set up kubernetes access." 99 | fi 100 | 101 | echo "Installation complete. Verify with: kubectl-mcp --help" 102 | echo "" 103 | echo "To start the MCP server, run: kubectl-mcp serve" 104 | echo "" 105 | echo "MCP configurations have been added for:" 106 | echo " - Cursor: ~/.cursor/mcp.json (with minimal wrapper)" 107 | echo " - Claude: ~/.config/claude/mcp.json" 108 | echo " - WindSurf: ~/.config/windsurf/mcp.json" 109 | echo "" 110 | echo "If Cursor fails to connect, check the output of: python -m kubectl_mcp_tool.minimal_wrapper" 111 | echo "" 112 | echo "If you encounter any issues, check the documentation in the docs/ folder" 113 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/__init__ copy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kubectl MCP Tool - A Model Context Protocol server for Kubernetes. 3 | """ 4 | 5 | __version__ = "1.1.1" 6 | 7 | # Import minimized implementations 8 | from . import minimal_wrapper 9 | from . import simple_ping 10 | from . import enhanced_json_fix 11 | from . import taskgroup_fix 12 | 13 | __all__ = ["minimal_wrapper", "simple_ping", "enhanced_json_fix", "taskgroup_fix"] 14 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kubectl MCP Tool - A Model Context Protocol server for Kubernetes. 3 | """ 4 | 5 | __version__ = "1.1.0" 6 | 7 | # Import implementations with correct FastMCP 8 | from .simple_server import KubectlServer, main 9 | from .mcp_server import MCPServer as _RootMCPServer 10 | 11 | # Re-export key symbols for easier import paths in tests 12 | try: 13 | from .core import KubernetesOperations, MCPServer # type: ignore 14 | __all__ = [ 15 | "KubectlServer", 16 | "MCPServer", 17 | "KubernetesOperations", 18 | "main", 19 | ] 20 | except ModuleNotFoundError: 21 | # When the package is installed without the *core* package (unlikely) 22 | # we at least expose the root‐level MCPServer implementation. 23 | MCPServer = _RootMCPServer # noqa: N816 (re-export for callers) 24 | __all__ = ["KubectlServer", "MCPServer", "main"] 25 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Main entry point for the kubectl MCP tool. 4 | """ 5 | 6 | import asyncio 7 | import argparse 8 | import logging 9 | from .mcp_server import MCPServer 10 | 11 | # Configure logging 12 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 13 | logger = logging.getLogger("mcp-server-main") 14 | 15 | def main(): 16 | """Run the kubectl MCP server.""" 17 | parser = argparse.ArgumentParser(description="Run the Kubectl MCP Server.") 18 | parser.add_argument( 19 | "--transport", 20 | type=str, 21 | choices=["stdio", "sse"], 22 | default="stdio", 23 | help="Communication transport to use (stdio or sse). Default: stdio.", 24 | ) 25 | parser.add_argument( 26 | "--port", 27 | type=int, 28 | default=8080, 29 | help="Port to use for SSE transport. Default: 8080.", 30 | ) 31 | args = parser.parse_args() 32 | 33 | server_name = "kubernetes" 34 | mcp_server = MCPServer(name=server_name) 35 | 36 | loop = asyncio.get_event_loop() 37 | try: 38 | if args.transport == "stdio": 39 | logger.info(f"Starting {server_name} with stdio transport.") 40 | loop.run_until_complete(mcp_server.serve_stdio()) 41 | elif args.transport == "sse": 42 | logger.info(f"Starting {server_name} with SSE transport on port {args.port}.") 43 | loop.run_until_complete(mcp_server.serve_sse(port=args.port)) 44 | except KeyboardInterrupt: 45 | logger.info("Server shutdown requested by user.") 46 | except Exception as e: 47 | logger.error(f"Server exited with error: {e}", exc_info=True) 48 | finally: 49 | logger.info("Shutting down server.") 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/claude_message_framing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Claude-specific message framing module for MCP server. 3 | 4 | This module provides functions to properly frame JSON-RPC messages for Claude Desktop, 5 | addressing the "Unexpected non-whitespace character after JSON at position 4" error 6 | by ensuring proper message boundaries between JSON responses. 7 | """ 8 | 9 | import json 10 | import logging 11 | import re 12 | import os 13 | from typing import Dict, Any, Optional, Union, List, Tuple 14 | 15 | logging.basicConfig( 16 | level=logging.DEBUG if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true") else logging.INFO, 17 | format="%(asctime)s - %(levelname)s - %(message)s" 18 | ) 19 | logger = logging.getLogger("claude-message-framing") 20 | 21 | def ensure_message_boundary(message: str) -> str: 22 | """ 23 | Ensure the message has proper boundaries for Claude Desktop. 24 | 25 | Args: 26 | message: The JSON-RPC message to frame 27 | 28 | Returns: 29 | A properly framed message with boundaries 30 | """ 31 | if not message: 32 | return message 33 | 34 | if not message.endswith('\n'): 35 | message = message + '\n' 36 | 37 | return message 38 | 39 | def frame_jsonrpc_message(data: Dict[str, Any], message_id: Optional[str] = None) -> str: 40 | """ 41 | Frame a JSON-RPC message for Claude Desktop. 42 | 43 | Args: 44 | data: The data to include in the message 45 | message_id: Optional message ID to include 46 | 47 | Returns: 48 | A properly framed JSON-RPC message 49 | """ 50 | if not isinstance(data, dict): 51 | logger.warning(f"Expected dict for JSON-RPC message, got {type(data)}") 52 | data = {"result": str(data)} 53 | 54 | jsonrpc_message = { 55 | "jsonrpc": "2.0", 56 | "id": message_id if message_id is not None else data.get("id", "1"), 57 | "result": data 58 | } 59 | 60 | try: 61 | json_str = json.dumps(jsonrpc_message, ensure_ascii=True, separators=(',', ':')) 62 | 63 | framed_message = ensure_message_boundary(json_str) 64 | 65 | return framed_message 66 | except Exception as e: 67 | logger.error(f"Error framing JSON-RPC message: {e}") 68 | error_message = { 69 | "jsonrpc": "2.0", 70 | "id": message_id if message_id is not None else "error", 71 | "error": { 72 | "code": -32603, 73 | "message": f"Internal error: {str(e)}" 74 | } 75 | } 76 | return json.dumps(error_message, ensure_ascii=True, separators=(',', ':')) + '\n' 77 | 78 | def extract_message_id(request: str) -> Optional[str]: 79 | """ 80 | Extract the message ID from a JSON-RPC request. 81 | 82 | Args: 83 | request: The JSON-RPC request string 84 | 85 | Returns: 86 | The message ID if found, None otherwise 87 | """ 88 | try: 89 | data = json.loads(request) 90 | id_value = data.get("id") 91 | if id_value is not None: 92 | return str(id_value) 93 | return None 94 | except json.JSONDecodeError: 95 | id_match = re.search(r'"id"\s*:\s*"?([^",\s]+)"?', request) 96 | if id_match: 97 | return id_match.group(1) 98 | 99 | id_match = re.search(r'"id"\s*:\s*(\d+)', request) 100 | if id_match: 101 | return id_match.group(1) 102 | 103 | return None 104 | except Exception as e: 105 | logger.error(f"Error extracting message ID: {e}") 106 | return None 107 | 108 | def create_response_buffer() -> List[str]: 109 | """ 110 | Create a buffer for storing responses before sending them. 111 | 112 | Returns: 113 | An empty list to use as a response buffer 114 | """ 115 | return [] 116 | 117 | def add_to_response_buffer(buffer: List[str], response: str) -> List[str]: 118 | """ 119 | Add a response to the buffer. 120 | 121 | Args: 122 | buffer: The response buffer 123 | response: The response to add 124 | 125 | Returns: 126 | The updated buffer 127 | """ 128 | buffer.append(response) 129 | return buffer 130 | 131 | def flush_response_buffer(buffer: List[str]) -> str: 132 | """ 133 | Flush the response buffer and return a properly framed message. 134 | 135 | Args: 136 | buffer: The response buffer 137 | 138 | Returns: 139 | A properly framed message containing all responses in the buffer 140 | """ 141 | if not buffer: 142 | return "" 143 | 144 | joined_response = "\n".join(buffer) 145 | 146 | buffer.clear() 147 | 148 | return joined_response 149 | 150 | def sanitize_for_claude(json_str: str) -> str: 151 | """ 152 | Sanitize a JSON string for Claude Desktop. 153 | 154 | Args: 155 | json_str: The JSON string to sanitize 156 | 157 | Returns: 158 | A sanitized JSON string 159 | """ 160 | if not json_str: 161 | return json_str 162 | 163 | problematic_chars = [ 164 | '\ufeff', # BOM 165 | '\u200b', # Zero-width space 166 | '\u200c', # Zero-width non-joiner 167 | '\u200d', # Zero-width joiner 168 | '\u2060', # Word joiner 169 | '\ufffe', # Reversed BOM 170 | '\u00a0', # Non-breaking space 171 | '\u2028', # Line separator 172 | '\u2029', # Paragraph separator 173 | ] 174 | 175 | for char in problematic_chars: 176 | if char in json_str: 177 | json_str = json_str.replace(char, '') 178 | 179 | if not json_str.endswith('\n'): 180 | json_str = json_str + '\n' 181 | 182 | return json_str 183 | 184 | def extract_clean_json(text: str) -> str: 185 | """ 186 | Extract clean JSON from text that might have content before or after the JSON. 187 | 188 | Args: 189 | text: The text containing JSON 190 | 191 | Returns: 192 | Clean JSON string 193 | """ 194 | if not text: 195 | return text 196 | 197 | logger.debug(f"Extracting clean JSON from text: '{text[:50]}'...") 198 | 199 | start_idx = text.find('{') 200 | if start_idx == -1: 201 | logger.warning("Could not find valid JSON boundaries in the text") 202 | return text 203 | 204 | brace_count = 0 205 | end_idx = -1 206 | 207 | for i in range(start_idx, len(text)): 208 | if text[i] == '{': 209 | brace_count += 1 210 | elif text[i] == '}': 211 | brace_count -= 1 212 | if brace_count == 0: 213 | end_idx = i + 1 214 | break 215 | 216 | if end_idx == -1: 217 | logger.warning("Could not find valid JSON boundaries in the text") 218 | return text 219 | 220 | if start_idx > 0: 221 | logger.warning(f"Found content before JSON: '{text[:start_idx]}'") 222 | 223 | if end_idx < len(text): 224 | logger.warning(f"Found content after JSON: '{text[end_idx:]}'") 225 | 226 | json_str = text[start_idx:end_idx] 227 | 228 | clean_json = sanitize_for_claude(json_str) 229 | 230 | logger.debug(f"Successfully extracted and cleaned JSON: '{clean_json[:50]}'...") 231 | 232 | return clean_json 233 | 234 | class ClaudeMessageFramer: 235 | """ 236 | Class to handle message framing for Claude Desktop. 237 | """ 238 | 239 | def __init__(self): 240 | """Initialize the message framer.""" 241 | self.response_buffer = create_response_buffer() 242 | self.message_counter = 0 243 | 244 | def frame_response(self, data: Dict[str, Any], request_id: Optional[str] = None) -> str: 245 | """ 246 | Frame a response for Claude Desktop. 247 | 248 | Args: 249 | data: The data to include in the response 250 | request_id: Optional request ID to include 251 | 252 | Returns: 253 | A properly framed response 254 | """ 255 | self.message_counter += 1 256 | 257 | response = frame_jsonrpc_message(data, request_id) 258 | add_to_response_buffer(self.response_buffer, response) 259 | 260 | return flush_response_buffer(self.response_buffer) 261 | 262 | def extract_request_id(self, request: str) -> Optional[str]: 263 | """ 264 | Extract the request ID from a request. 265 | 266 | Args: 267 | request: The request string 268 | 269 | Returns: 270 | The request ID if found, None otherwise 271 | """ 272 | return extract_message_id(request) 273 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | CLI module for kubectl-mcp-tool. 4 | """ 5 | 6 | import sys 7 | import os 8 | import logging 9 | import asyncio 10 | import argparse 11 | import traceback 12 | from .mcp_server import MCPServer 13 | 14 | # Configure logging 15 | logging.basicConfig( 16 | level=logging.DEBUG, 17 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 18 | filename="kubectl_mcp_tool_cli.log" 19 | ) 20 | logger = logging.getLogger("kubectl-mcp-tool-cli") 21 | 22 | # Add console handler for important messages 23 | console_handler = logging.StreamHandler() 24 | console_handler.setLevel(logging.INFO) 25 | console_formatter = logging.Formatter('%(levelname)s: %(message)s') 26 | console_handler.setFormatter(console_formatter) 27 | logger.addHandler(console_handler) 28 | 29 | async def serve_stdio(): 30 | """Serve the MCP server over stdio transport.""" 31 | logger.info("Starting standard MCP server with stdio transport") 32 | server = MCPServer("kubectl-mcp-tool") 33 | await server.serve_stdio() 34 | 35 | async def serve_sse(port: int): 36 | """Serve the MCP server over SSE transport.""" 37 | logger.info(f"Starting standard MCP server with SSE transport on port {port}") 38 | server = MCPServer("kubectl-mcp-tool") 39 | await server.serve_sse(port) 40 | 41 | def main(): 42 | """Main entry point for the CLI.""" 43 | parser = argparse.ArgumentParser(description="kubectl MCP tool") 44 | subparsers = parser.add_subparsers(dest="command", help="Command to run") 45 | 46 | # Serve command 47 | serve_parser = subparsers.add_parser("serve", help="Start the MCP server") 48 | serve_parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio", 49 | help="Transport to use (stdio or sse)") 50 | serve_parser.add_argument("--port", type=int, default=8080, 51 | help="Port to use for SSE transport") 52 | serve_parser.add_argument("--cursor", action="store_true", 53 | help="Enable Cursor compatibility mode") 54 | serve_parser.add_argument("--debug", action="store_true", 55 | help="Enable debug mode with additional logging") 56 | 57 | args = parser.parse_args() 58 | 59 | # Set up debug mode if requested 60 | if args.command == "serve" and args.debug: 61 | console_handler.setLevel(logging.DEBUG) 62 | logger.info("Debug mode enabled") 63 | 64 | exit_code = 0 65 | 66 | try: 67 | if args.command == "serve": 68 | if args.cursor: 69 | try: 70 | # Import the cursor_mcp_server module 71 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 72 | from cursor_mcp_server import main as cursor_main 73 | 74 | logger.info("Starting Cursor-compatible MCP server") 75 | asyncio.run(cursor_main()) 76 | except ImportError as e: 77 | logger.error(f"Error importing cursor_mcp_server module: {e}") 78 | logger.error("Make sure cursor_mcp_server.py is in the correct location") 79 | exit_code = 1 80 | except Exception as e: 81 | logger.error(f"Error starting Cursor-compatible MCP server: {e}") 82 | if args.debug: 83 | logger.error(traceback.format_exc()) 84 | exit_code = 1 85 | else: 86 | # Standard MCP server 87 | try: 88 | if args.transport == "stdio": 89 | asyncio.run(serve_stdio()) 90 | else: 91 | asyncio.run(serve_sse(args.port)) 92 | except Exception as e: 93 | logger.error(f"Error starting standard MCP server: {e}") 94 | if args.debug: 95 | logger.error(traceback.format_exc()) 96 | exit_code = 1 97 | else: 98 | parser.print_help() 99 | except KeyboardInterrupt: 100 | logger.info("Server stopped by user") 101 | except Exception as e: 102 | logger.error(f"Unexpected error: {e}") 103 | if args.command == "serve" and args.debug: 104 | logger.error(traceback.format_exc()) 105 | exit_code = 1 106 | 107 | return exit_code 108 | 109 | if __name__ == "__main__": 110 | sys.exit(main()) 111 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI package for kubectl-mcp-tool. 3 | """ 4 | 5 | from . import __main__ 6 | 7 | __all__ = ["__main__"] 8 | 9 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/cli/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Main entry point for the kubectl-mcp-tool CLI. 4 | """ 5 | 6 | import logging 7 | from contextlib import asynccontextmanager 8 | from typing import Dict, Any, Optional 9 | 10 | import anyio 11 | from mcp.server import Server 12 | from mcp.server.stdio import stdio_server 13 | from mcp.server.models import InitializationOptions 14 | from mcp.types import ( 15 | ServerCapabilities, 16 | ToolsCapability, 17 | PromptsCapability, 18 | ResourcesCapability, 19 | LoggingCapability 20 | ) 21 | 22 | # Configure logging 23 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 24 | logger = logging.getLogger("kubectl-mcp") 25 | 26 | class KubectlServer(Server): 27 | """Kubectl MCP server implementation.""" 28 | 29 | async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: 30 | """Handle tool calls.""" 31 | if name == "get_pods": 32 | return await self.get_pods(arguments.get("namespace")) 33 | elif name == "get_namespaces": 34 | return await self.get_namespaces() 35 | return {"success": False, "error": f"Unknown tool: {name}"} 36 | 37 | async def get_pods(self, namespace: Optional[str] = None) -> Dict[str, Any]: 38 | """Get all pods in the specified namespace.""" 39 | try: 40 | from kubernetes import client, config 41 | config.load_kube_config() 42 | v1 = client.CoreV1Api() 43 | 44 | if namespace: 45 | pods = v1.list_namespaced_pod(namespace) 46 | else: 47 | pods = v1.list_pod_for_all_namespaces() 48 | 49 | return { 50 | "success": True, 51 | "pods": [ 52 | { 53 | "name": pod.metadata.name, 54 | "namespace": pod.metadata.namespace, 55 | "status": pod.status.phase, 56 | "ip": pod.status.pod_ip 57 | } 58 | for pod in pods.items 59 | ] 60 | } 61 | except Exception as e: 62 | logger.error(f"Error getting pods: {e}") 63 | return {"success": False, "error": str(e)} 64 | 65 | async def get_namespaces(self) -> Dict[str, Any]: 66 | """Get all Kubernetes namespaces.""" 67 | try: 68 | from kubernetes import client, config 69 | config.load_kube_config() 70 | v1 = client.CoreV1Api() 71 | 72 | namespaces = v1.list_namespace() 73 | return { 74 | "success": True, 75 | "namespaces": [ns.metadata.name for ns in namespaces.items] 76 | } 77 | except Exception as e: 78 | logger.error(f"Error getting namespaces: {e}") 79 | return {"success": False, "error": str(e)} 80 | 81 | @asynccontextmanager 82 | async def server_lifespan(server: KubectlServer): 83 | """Server lifespan context manager.""" 84 | # Set up server capabilities 85 | server.capabilities = ServerCapabilities( 86 | tools=ToolsCapability(enabled=True), 87 | prompts=PromptsCapability(enabled=False), 88 | resources=ResourcesCapability(enabled=False), 89 | logging=LoggingCapability(enabled=True) 90 | ) 91 | 92 | # Register tools 93 | server.tools = [ 94 | { 95 | "name": "get_pods", 96 | "description": "Get all pods in the specified namespace", 97 | "parameters": { 98 | "type": "object", 99 | "properties": { 100 | "namespace": { 101 | "type": "string", 102 | "description": "The Kubernetes namespace (optional)" 103 | } 104 | } 105 | } 106 | }, 107 | { 108 | "name": "get_namespaces", 109 | "description": "Get all Kubernetes namespaces", 110 | "parameters": { 111 | "type": "object", 112 | "properties": {} 113 | } 114 | } 115 | ] 116 | 117 | yield 118 | 119 | async def run_server(): 120 | """Run the MCP server.""" 121 | # Create server 122 | server = KubectlServer( 123 | name="kubectl-mcp", 124 | version="0.1.0", 125 | lifespan=server_lifespan 126 | ) 127 | 128 | # Create initialization options 129 | init_options = InitializationOptions( 130 | server_name="kubectl-mcp", 131 | server_version="0.1.0", 132 | capabilities=ServerCapabilities( 133 | tools=ToolsCapability(enabled=True), 134 | prompts=PromptsCapability(enabled=False), 135 | resources=ResourcesCapability(enabled=False), 136 | logging=LoggingCapability(enabled=True) 137 | ) 138 | ) 139 | 140 | # Start server 141 | logger.info("Starting MCP server") 142 | async with stdio_server() as (read_stream, write_stream): 143 | await server.run(read_stream, write_stream, init_options) 144 | 145 | def main(): 146 | """Main entry point.""" 147 | anyio.run(run_server) 148 | 149 | if __name__ == "__main__": 150 | main() -------------------------------------------------------------------------------- /kubectl_mcp_tool/cli/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | CLI module for kubectl-mcp-tool. 4 | """ 5 | 6 | import sys 7 | import os 8 | import logging 9 | import asyncio 10 | import argparse 11 | import traceback 12 | from ..mcp_server import MCPServer 13 | 14 | # Configure logging with file and console output 15 | LOG_DIR = "logs" 16 | os.makedirs(LOG_DIR, exist_ok=True) 17 | 18 | logging.basicConfig( 19 | level=logging.DEBUG, 20 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 21 | filename=os.path.join(LOG_DIR, "kubectl_mcp_tool_cli.log") 22 | ) 23 | logger = logging.getLogger("kubectl-mcp-tool-cli") 24 | 25 | # Add console handler for important messages 26 | console_handler = logging.StreamHandler() 27 | console_handler.setLevel(logging.INFO) 28 | console_formatter = logging.Formatter('%(levelname)s: %(message)s') 29 | console_handler.setFormatter(console_formatter) 30 | logger.addHandler(console_handler) 31 | 32 | async def serve_stdio(): 33 | """Serve the MCP server over stdio transport.""" 34 | logger.info("Starting standard MCP server with stdio transport") 35 | try: 36 | server = MCPServer("kubectl-mcp-tool") 37 | await server.serve_stdio() 38 | except Exception as e: 39 | logger.error(f"Error in stdio server: {e}") 40 | logger.error(traceback.format_exc()) 41 | raise 42 | 43 | async def serve_sse(port: int): 44 | """Serve the MCP server over SSE transport.""" 45 | logger.info(f"Starting standard MCP server with SSE transport on port {port}") 46 | try: 47 | server = MCPServer("kubectl-mcp-tool") 48 | await server.serve_sse(port) 49 | except Exception as e: 50 | logger.error(f"Error in SSE server on port {port}: {e}") 51 | logger.error(traceback.format_exc()) 52 | raise 53 | 54 | def main(): 55 | """Main entry point for the CLI.""" 56 | parser = argparse.ArgumentParser(description="kubectl MCP tool") 57 | subparsers = parser.add_subparsers(dest="command", help="Command to run") 58 | 59 | # Serve command 60 | serve_parser = subparsers.add_parser("serve", help="Start the MCP server") 61 | serve_parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio", 62 | help="Transport to use (stdio or sse)") 63 | serve_parser.add_argument("--port", type=int, default=8080, 64 | help="Port to use for SSE transport") 65 | serve_parser.add_argument("--cursor", action="store_true", 66 | help="Enable Cursor compatibility mode") 67 | serve_parser.add_argument("--debug", action="store_true", 68 | help="Enable debug mode with additional logging") 69 | 70 | args = parser.parse_args() 71 | 72 | # Set up debug mode if requested 73 | if args.command == "serve" and args.debug: 74 | console_handler.setLevel(logging.DEBUG) 75 | logger.info("Debug mode enabled") 76 | 77 | exit_code = 0 78 | 79 | try: 80 | if args.command == "serve": 81 | if args.cursor: 82 | try: 83 | # Import the cursor_mcp_server module 84 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 85 | from cursor_mcp_server import main as cursor_main 86 | 87 | logger.info("Starting Cursor-compatible MCP server") 88 | asyncio.run(cursor_main()) 89 | except ImportError as e: 90 | logger.error(f"Error importing cursor_mcp_server module: {e}") 91 | logger.error("Make sure cursor_mcp_server.py is in the correct location") 92 | exit_code = 1 93 | except Exception as e: 94 | logger.error(f"Error starting Cursor-compatible MCP server: {e}") 95 | if args.debug: 96 | logger.error(traceback.format_exc()) 97 | exit_code = 1 98 | else: 99 | # Standard MCP server 100 | try: 101 | if args.transport == "stdio": 102 | asyncio.run(serve_stdio()) 103 | else: 104 | logger.info(f"Starting SSE server on port {args.port}") 105 | asyncio.run(serve_sse(args.port)) 106 | except Exception as e: 107 | logger.error(f"Error starting standard MCP server: {e}") 108 | if args.debug: 109 | logger.error(traceback.format_exc()) 110 | exit_code = 1 111 | else: 112 | parser.print_help() 113 | except KeyboardInterrupt: 114 | logger.info("Server stopped by user") 115 | except Exception as e: 116 | logger.error(f"Unexpected error: {e}") 117 | if args.command == "serve" and args.debug: 118 | logger.error(traceback.format_exc()) 119 | exit_code = 1 120 | 121 | return exit_code 122 | 123 | if __name__ == "__main__": 124 | sys.exit(main()) 125 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Core subpackage for kubectl_mcp_tool. 2 | 3 | This file makes the modules inside the *core* directory importable as a 4 | regular Python package so that test-suites (e.g. ``from 5 | kubectl_mcp_tool.core.kubernetes_ops import KubernetesOperations``) 6 | resolve correctly. 7 | 8 | In addition, the most frequently used classes are re-exported for 9 | convenience. 10 | """ 11 | 12 | from .kubernetes_ops import KubernetesOperations # noqa: F401 13 | from .mcp_server import MCPServer # noqa: F401 14 | 15 | __all__ = [ 16 | "KubernetesOperations", 17 | "MCPServer", 18 | ] 19 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/cursor_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Debug wrapper for kubectl-mcp server when run by Cursor. 4 | This script captures detailed logs and handles common errors. 5 | """ 6 | 7 | import os 8 | import sys 9 | import time 10 | import traceback 11 | import logging 12 | import shutil 13 | import signal 14 | import atexit 15 | import json 16 | 17 | # Set up debug logging to a file 18 | log_dir = os.path.expanduser("~/.cursor/logs") 19 | os.makedirs(log_dir, exist_ok=True) 20 | log_file = os.path.join(log_dir, "kubectl_mcp_debug.log") 21 | 22 | logging.basicConfig( 23 | level=logging.DEBUG, 24 | format="%(asctime)s - %(levelname)s - %(message)s", 25 | handlers=[ 26 | logging.FileHandler(log_file), 27 | logging.StreamHandler(sys.stdout) 28 | ] 29 | ) 30 | 31 | logger = logging.getLogger("cursor-wrapper") 32 | 33 | def check_environment(): 34 | """Check environment variables and file permissions.""" 35 | logger.info("----- Environment Check -----") 36 | logger.info(f"Working directory: {os.getcwd()}") 37 | logger.info(f"Python executable: {sys.executable}") 38 | logger.info(f"Python version: {sys.version}") 39 | logger.info(f"Python path: {sys.path}") 40 | 41 | # Check kubectl 42 | kubectl_path = shutil.which("kubectl") 43 | if kubectl_path: 44 | logger.info(f"kubectl found at: {kubectl_path}") 45 | else: 46 | logger.error("kubectl not found in PATH") 47 | 48 | # Check kubeconfig 49 | kube_config = os.environ.get('KUBECONFIG', '~/.kube/config') 50 | expanded_path = os.path.expanduser(kube_config) 51 | logger.info(f"KUBECONFIG: {kube_config} (expanded: {expanded_path})") 52 | 53 | if os.path.exists(expanded_path): 54 | logger.info(f"Kubernetes config exists at {expanded_path}") 55 | # Check permissions 56 | try: 57 | with open(expanded_path, 'r') as f: 58 | logger.info("Successfully opened kubeconfig file") 59 | except Exception as e: 60 | logger.error(f"Failed to read kubeconfig: {e}") 61 | else: 62 | logger.error(f"Kubernetes config does not exist at {expanded_path}") 63 | 64 | # List all environment variables 65 | logger.info("Environment variables:") 66 | for key, value in sorted(os.environ.items()): 67 | if key.lower() in ('path', 'pythonpath', 'kubeconfig'): 68 | logger.info(f"{key}: {value}") 69 | # Skip logging other env vars for security 70 | 71 | # Test kubectl 72 | try: 73 | import subprocess 74 | result = subprocess.run( 75 | ["kubectl", "version", "--client"], 76 | capture_output=True, 77 | text=True 78 | ) 79 | logger.info(f"kubectl version result: {result.returncode}") 80 | logger.info(f"kubectl version output: {result.stdout}") 81 | if result.stderr: 82 | logger.warning(f"kubectl stderr: {result.stderr}") 83 | except Exception as e: 84 | logger.error(f"Failed to run kubectl: {e}") 85 | 86 | def run_direct_mcp_server(): 87 | """Run a direct FastMCP server for better compatibility with Cursor.""" 88 | try: 89 | logger.info("Creating direct FastMCP server for Cursor") 90 | # Import the modules needed for the server 91 | from mcp.server.fastmcp import FastMCP 92 | from kubectl_mcp_tool.natural_language import process_query 93 | 94 | # Create a FastMCP server with explicit name 95 | server = FastMCP(name="kubectl-mcp") 96 | logger.info("FastMCP server created successfully") 97 | 98 | # Register the natural language processing tool 99 | @server.tool( 100 | name="process_natural_language", 101 | description="Process natural language query for kubectl", 102 | parameters={ 103 | "type": "object", 104 | "properties": { 105 | "query": { 106 | "type": "string", 107 | "description": "The natural language query to process" 108 | } 109 | }, 110 | "required": ["query"] 111 | } 112 | ) 113 | async def process_natural_language(query: str): 114 | """Process natural language query for kubectl.""" 115 | logger.info(f"Received query from Cursor: {query}") 116 | result = process_query(query) 117 | logger.info(f"Result: {result}") 118 | return json.dumps(result) 119 | 120 | # Register a simple ping tool to help with debugging 121 | @server.tool( 122 | name="kubernetes_ping", 123 | description="Ping the kubernetes server to check if it's working" 124 | ) 125 | async def kubernetes_ping(): 126 | """Simple ping tool for testing connectivity.""" 127 | logger.info("Ping received from Cursor") 128 | return "Kubernetes MCP server is working!" 129 | 130 | # Start the server using stdio transport 131 | logger.info("Starting FastMCP server with stdio transport") 132 | import asyncio 133 | 134 | # Handle clean exit 135 | def cleanup(): 136 | logger.info("Shutting down MCP server") 137 | 138 | atexit.register(cleanup) 139 | 140 | # Run the server 141 | asyncio.run(server.run_stdio_async()) 142 | except Exception as e: 143 | logger.error(f"Error running direct MCP server: {e}") 144 | logger.error(traceback.format_exc()) 145 | raise 146 | 147 | def run_mcp_server(): 148 | """Run the MCP server with error handling - old version.""" 149 | try: 150 | logger.info("Importing kubectl_mcp_tool.cli.cli...") 151 | from kubectl_mcp_tool.cli import cli 152 | 153 | logger.info("Starting MCP server...") 154 | sys.argv = [sys.argv[0], "serve"] 155 | cli.main() 156 | except ImportError as e: 157 | logger.error(f"Import error: {e}") 158 | logger.error(f"Python path: {sys.path}") 159 | # Try to import the module using an absolute path 160 | try: 161 | project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 162 | sys.path.insert(0, project_root) 163 | logger.info(f"Added {project_root} to sys.path") 164 | from kubectl_mcp_tool.cli import cli 165 | sys.argv = [sys.argv[0], "serve"] 166 | cli.main() 167 | except Exception as inner_e: 168 | logger.error(f"Failed second import attempt: {inner_e}") 169 | logger.error(traceback.format_exc()) 170 | except Exception as e: 171 | logger.error(f"Error starting MCP server: {e}") 172 | logger.error(traceback.format_exc()) 173 | 174 | if __name__ == "__main__": 175 | logger.info("===== Starting kubectl-mcp debug wrapper =====") 176 | check_environment() 177 | # Use the direct FastMCP implementation for better Cursor compatibility 178 | run_direct_mcp_server() -------------------------------------------------------------------------------- /kubectl_mcp_tool/diagnostics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Kubernetes diagnostics and monitoring module for kubectl-mcp-tool. 4 | """ 5 | 6 | import json 7 | import logging 8 | from typing import Dict, List, Any, Optional 9 | import subprocess 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class KubernetesDiagnostics: 14 | """Kubernetes diagnostics and monitoring tools.""" 15 | 16 | @staticmethod 17 | def run_command(cmd: List[str]) -> str: 18 | """Run a kubectl command and return the output.""" 19 | try: 20 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 21 | return result.stdout.strip() 22 | except subprocess.CalledProcessError as e: 23 | logger.error(f"Command failed: {' '.join(cmd)}") 24 | logger.error(f"Error output: {e.stderr}") 25 | return f"Error: {e.stderr}" 26 | 27 | def get_logs(self, pod_name: str, namespace: Optional[str] = None, 28 | container: Optional[str] = None, tail: Optional[int] = None, 29 | previous: bool = False) -> Dict[str, Any]: 30 | """Get logs from a pod.""" 31 | cmd = ["kubectl", "logs"] 32 | 33 | if namespace: 34 | cmd.extend(["-n", namespace]) 35 | 36 | if container: 37 | cmd.extend(["-c", container]) 38 | 39 | if tail: 40 | cmd.extend(["--tail", str(tail)]) 41 | 42 | if previous: 43 | cmd.append("-p") 44 | 45 | cmd.append(pod_name) 46 | 47 | logs = self.run_command(cmd) 48 | return { 49 | "pod_name": pod_name, 50 | "namespace": namespace or "default", 51 | "container": container, 52 | "logs": logs 53 | } 54 | 55 | def get_events(self, namespace: Optional[str] = None, 56 | resource_name: Optional[str] = None) -> Dict[str, Any]: 57 | """Get Kubernetes events.""" 58 | cmd = ["kubectl", "get", "events"] 59 | 60 | if namespace: 61 | cmd.extend(["-n", namespace]) 62 | 63 | if resource_name: 64 | cmd.extend([f"--field-selector=involvedObject.name={resource_name}"]) 65 | 66 | cmd.extend(["--sort-by=.metadata.creationTimestamp"]) 67 | 68 | events = self.run_command(cmd) 69 | return { 70 | "namespace": namespace or "default", 71 | "resource_name": resource_name, 72 | "events": events 73 | } 74 | 75 | def check_pod_health(self, pod_name: str, namespace: str = "default") -> Dict[str, Any]: 76 | """Check pod health and detect common issues.""" 77 | # Get pod details 78 | cmd = ["kubectl", "get", "pod", pod_name, "-n", namespace, "-o", "json"] 79 | pod_json_str = self.run_command(cmd) 80 | 81 | try: 82 | pod_json = json.loads(pod_json_str) 83 | except json.JSONDecodeError: 84 | return {"error": "Failed to get pod details", "raw_output": pod_json_str} 85 | 86 | issues = [] 87 | 88 | # Check pod status 89 | status = pod_json.get("status", {}) 90 | phase = status.get("phase") 91 | if phase != "Running": 92 | issues.append(f"Pod is not running (Status: {phase})") 93 | 94 | # Check container statuses 95 | for container in status.get("containerStatuses", []): 96 | if not container.get("ready"): 97 | waiting = container.get("state", {}).get("waiting", {}) 98 | if waiting: 99 | issues.append( 100 | f"Container {container['name']} is waiting: " 101 | f"{waiting.get('reason')} - {waiting.get('message')}" 102 | ) 103 | 104 | # Get recent events 105 | events = self.get_events(namespace, pod_name) 106 | 107 | # Check resource usage 108 | usage = self.get_resource_usage("pods", namespace, pod_name) 109 | 110 | return { 111 | "pod_name": pod_name, 112 | "namespace": namespace, 113 | "status": phase, 114 | "issues": issues, 115 | "events": events.get("events"), 116 | "resource_usage": usage.get("usage") 117 | } 118 | 119 | def get_resource_usage(self, resource_type: str = "pods", 120 | namespace: Optional[str] = None, 121 | resource_name: Optional[str] = None) -> Dict[str, Any]: 122 | """Get resource usage statistics.""" 123 | cmd = ["kubectl", "top", resource_type] 124 | 125 | if namespace: 126 | cmd.extend(["-n", namespace]) 127 | 128 | if resource_name: 129 | cmd.append(resource_name) 130 | 131 | usage = self.run_command(cmd) 132 | return { 133 | "resource_type": resource_type, 134 | "namespace": namespace, 135 | "resource_name": resource_name, 136 | "usage": usage 137 | } 138 | 139 | def validate_resources(self, namespace: str = "default") -> Dict[str, Any]: 140 | """Validate Kubernetes resources for common misconfigurations.""" 141 | validations = [] 142 | 143 | # Get all pods 144 | cmd = ["kubectl", "get", "pods", "-n", namespace, "-o", "json"] 145 | pods_json_str = self.run_command(cmd) 146 | 147 | try: 148 | pods = json.loads(pods_json_str) 149 | except json.JSONDecodeError: 150 | return {"error": "Failed to get pods", "raw_output": pods_json_str} 151 | 152 | for pod in pods.get("items", []): 153 | pod_name = pod["metadata"]["name"] 154 | 155 | # Check resource limits 156 | for container in pod["spec"]["containers"]: 157 | if not container.get("resources", {}).get("limits"): 158 | validations.append({ 159 | "level": "warning", 160 | "type": "no_resource_limits", 161 | "message": f"Container {container['name']} in pod {pod_name} has no resource limits" 162 | }) 163 | 164 | # Check probes 165 | if not container.get("livenessProbe"): 166 | validations.append({ 167 | "level": "warning", 168 | "type": "no_liveness_probe", 169 | "message": f"Container {container['name']} in pod {pod_name} has no liveness probe" 170 | }) 171 | if not container.get("readinessProbe"): 172 | validations.append({ 173 | "level": "warning", 174 | "type": "no_readiness_probe", 175 | "message": f"Container {container['name']} in pod {pod_name} has no readiness probe" 176 | }) 177 | 178 | return { 179 | "namespace": namespace, 180 | "validations": validations, 181 | "summary": { 182 | "total_validations": len(validations), 183 | "warnings": len([v for v in validations if v["level"] == "warning"]), 184 | "errors": len([v for v in validations if v["level"] == "error"]) 185 | } 186 | } 187 | 188 | def analyze_pod_logs(self, pod_name: str, namespace: str = "default", 189 | container: Optional[str] = None, tail: int = 1000) -> Dict[str, Any]: 190 | """Analyze pod logs for common issues and patterns.""" 191 | # Get logs 192 | logs_data = self.get_logs(pod_name, namespace, container, tail) 193 | logs = logs_data.get("logs", "") 194 | 195 | analysis = { 196 | "error_count": 0, 197 | "warning_count": 0, 198 | "errors": [], 199 | "warnings": [], 200 | "patterns": {} 201 | } 202 | 203 | # Common error patterns to look for 204 | error_patterns = [ 205 | "error", "exception", "failed", "failure", "fatal", 206 | "OOMKilled", "CrashLoopBackOff" 207 | ] 208 | 209 | warning_patterns = [ 210 | "warning", "warn", "deprecated" 211 | ] 212 | 213 | # Analyze logs 214 | for line in logs.split("\n"): 215 | line_lower = line.lower() 216 | 217 | # Check for errors 218 | for pattern in error_patterns: 219 | if pattern in line_lower: 220 | analysis["error_count"] += 1 221 | analysis["errors"].append(line) 222 | break 223 | 224 | # Check for warnings 225 | for pattern in warning_patterns: 226 | if pattern in line_lower: 227 | analysis["warning_count"] += 1 228 | analysis["warnings"].append(line) 229 | break 230 | 231 | return { 232 | "pod_name": pod_name, 233 | "namespace": namespace, 234 | "container": container, 235 | "analysis": analysis 236 | } -------------------------------------------------------------------------------- /kubectl_mcp_tool/fastmcp_patch.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP patch module for handling TaskGroup errors in Claude Desktop. 3 | 4 | This module adds a shutdown method to the FastMCP class and patches the 5 | run_stdio_async method to properly handle TaskGroup errors, resolving the 6 | 'FastMCP' object has no attribute 'shutdown' error. 7 | """ 8 | import asyncio 9 | import sys 10 | import logging 11 | import traceback 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | async def shutdown(self): 16 | """Gracefully shutdown the FastMCP server.""" 17 | logger.info("Shutting down FastMCP server") 18 | if hasattr(self, 'task_group') and self.task_group is not None: 19 | self.task_group.cancel() 20 | 21 | if hasattr(self, 'reader') and self.reader is not None: 22 | if not self.reader.at_eof(): 23 | self.reader.feed_eof() 24 | 25 | if hasattr(self, 'writer') and self.writer is not None: 26 | if not self.writer.is_closing(): 27 | self.writer.close() 28 | try: 29 | await self.writer.wait_closed() 30 | except Exception as e: 31 | logger.debug(f"Error waiting for writer to close: {e}") 32 | 33 | try: 34 | from mcp.server.fastmcp import FastMCP 35 | 36 | if not hasattr(FastMCP, 'shutdown'): 37 | FastMCP.shutdown = shutdown 38 | logger.info("Added shutdown method to FastMCP class") 39 | 40 | original_run_stdio_async = FastMCP.run_stdio_async 41 | 42 | async def patched_run_stdio_async(self): 43 | """Patched version of run_stdio_async that handles TaskGroup errors.""" 44 | try: 45 | await original_run_stdio_async(self) 46 | except asyncio.exceptions.CancelledError: 47 | logger.info("FastMCP server cancelled") 48 | except Exception as e: 49 | logger.error(f"Error in FastMCP server: {e}") 50 | logger.error(traceback.format_exc()) 51 | try: 52 | await self.shutdown() 53 | except Exception as shutdown_error: 54 | logger.error(f"Error during shutdown: {shutdown_error}") 55 | 56 | FastMCP.run_stdio_async = patched_run_stdio_async 57 | logger.info("Patched run_stdio_async method in FastMCP class") 58 | 59 | except ImportError: 60 | logger.error("Could not import FastMCP class") 61 | except Exception as e: 62 | logger.error(f"Error patching FastMCP class: {e}") 63 | logger.error(traceback.format_exc()) 64 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/natural_language.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Natural language processing for kubectl. 4 | This module parses natural language queries and converts them to kubectl commands. 5 | """ 6 | 7 | import re 8 | import subprocess 9 | import logging 10 | import os 11 | import json 12 | from typing import Dict, Any, List, Optional, Union 13 | 14 | # Configure logging 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger("natural-language") 17 | 18 | def process_query(query: str) -> Dict[str, Any]: 19 | """ 20 | Process a natural language query and convert it to a kubectl command. 21 | 22 | Args: 23 | query: The natural language query to process 24 | 25 | Returns: 26 | A dictionary containing the kubectl command and its output 27 | """ 28 | try: 29 | # Try to parse the query and generate a kubectl command 30 | command = parse_query(query) 31 | 32 | # Log the generated command 33 | logger.info(f"Generated kubectl command: {command}") 34 | 35 | # Execute the command 36 | try: 37 | result = execute_command(command) 38 | success = True 39 | except Exception as e: 40 | logger.error(f"Error executing command: {e}") 41 | result = f"Error executing command: {str(e)}" 42 | success = False 43 | 44 | # Return the result 45 | return { 46 | "command": command, 47 | "result": result, 48 | "success": success 49 | } 50 | except Exception as e: 51 | logger.error(f"Error processing query: {e}") 52 | return { 53 | "command": "", 54 | "result": f"Error processing query: {str(e)}", 55 | "success": False 56 | } 57 | 58 | def parse_query(query: str) -> str: 59 | """ 60 | Parse a natural language query and convert it to a kubectl command. 61 | 62 | Args: 63 | query: The natural language query to parse 64 | 65 | Returns: 66 | The kubectl command to execute 67 | """ 68 | # Normalize the query 69 | query = query.lower().strip() 70 | 71 | # Check for keyword patterns 72 | if re.search(r"get\s+pod", query) or re.search(r"list\s+pod", query): 73 | namespace = extract_namespace(query) 74 | if namespace: 75 | return f"kubectl get pods -n {namespace}" 76 | else: 77 | return "kubectl get pods" 78 | 79 | if re.search(r"get\s+all", query) or re.search(r"list\s+all", query): 80 | namespace = extract_namespace(query) 81 | if namespace: 82 | return f"kubectl get all -n {namespace}" 83 | else: 84 | return "kubectl get all" 85 | 86 | if re.search(r"get\s+deployment", query) or re.search(r"list\s+deployment", query): 87 | namespace = extract_namespace(query) 88 | if namespace: 89 | return f"kubectl get deployments -n {namespace}" 90 | else: 91 | return "kubectl get deployments" 92 | 93 | if re.search(r"get\s+service", query) or re.search(r"list\s+service", query): 94 | namespace = extract_namespace(query) 95 | if namespace: 96 | return f"kubectl get services -n {namespace}" 97 | else: 98 | return "kubectl get services" 99 | 100 | if re.search(r"describe\s+pod", query): 101 | pod_name = extract_pod_name(query) 102 | namespace = extract_namespace(query) 103 | if pod_name and namespace: 104 | return f"kubectl describe pod {pod_name} -n {namespace}" 105 | elif pod_name: 106 | return f"kubectl describe pod {pod_name}" 107 | else: 108 | return "kubectl get pods" 109 | 110 | if re.search(r"get\s+log", query) or re.search(r"show\s+log", query): 111 | pod_name = extract_pod_name(query) 112 | namespace = extract_namespace(query) 113 | if pod_name and namespace: 114 | return f"kubectl logs {pod_name} -n {namespace}" 115 | elif pod_name: 116 | return f"kubectl logs {pod_name}" 117 | else: 118 | return "kubectl get pods" 119 | 120 | if re.search(r"delete\s+pod", query): 121 | pod_name = extract_pod_name(query) 122 | namespace = extract_namespace(query) 123 | if pod_name and namespace: 124 | return f"kubectl delete pod {pod_name} -n {namespace}" 125 | elif pod_name: 126 | return f"kubectl delete pod {pod_name}" 127 | else: 128 | return "kubectl get pods" 129 | 130 | # Default: just do a get all 131 | return "kubectl get all" 132 | 133 | def extract_namespace(query: str) -> Optional[str]: 134 | """ 135 | Extract namespace from a query. 136 | 137 | Args: 138 | query: The query to extract the namespace from 139 | 140 | Returns: 141 | The namespace or None if not found 142 | """ 143 | namespace_match = re.search(r"(?:in|from|namespace|ns)\s+(\w+)", query) 144 | if namespace_match: 145 | return namespace_match.group(1) 146 | return None 147 | 148 | def extract_pod_name(query: str) -> Optional[str]: 149 | """ 150 | Extract pod name from a query. 151 | 152 | Args: 153 | query: The query to extract the pod name from 154 | 155 | Returns: 156 | The pod name or None if not found 157 | """ 158 | pod_match = re.search(r"pod\s+(\w+[\w\-]*)", query) 159 | if pod_match: 160 | return pod_match.group(1) 161 | return None 162 | 163 | def execute_command(command: str) -> str: 164 | """ 165 | Execute a kubectl command and return the output. 166 | 167 | Args: 168 | command: The kubectl command to execute 169 | 170 | Returns: 171 | The command output 172 | """ 173 | try: 174 | # For enhanced safety, handle the case where kubeconfig doesn't exist 175 | kubeconfig = os.environ.get('KUBECONFIG', os.path.expanduser('~/.kube/config')) 176 | if not os.path.exists(kubeconfig): 177 | logger.warning(f"Kubeconfig not found at {kubeconfig}") 178 | return f"Warning: Kubernetes config not found at {kubeconfig}. Please configure kubectl." 179 | 180 | # Try to run the command with a timeout for safety 181 | result = subprocess.run( 182 | command, 183 | shell=True, 184 | check=False, # Don't raise exception on non-zero exit 185 | capture_output=True, 186 | text=True, 187 | timeout=10 # Timeout after 10 seconds 188 | ) 189 | 190 | # Check for errors 191 | if result.returncode != 0: 192 | error_msg = result.stderr.strip() if result.stderr else "Unknown error" 193 | logger.warning(f"Command failed with exit code {result.returncode}: {error_msg}") 194 | return f"Command failed: {error_msg}\n\nCommand: {command}" 195 | 196 | return result.stdout if result.stdout else "Command completed successfully with no output" 197 | except subprocess.TimeoutExpired: 198 | logger.error(f"Command timed out: {command}") 199 | return "Command timed out after 10 seconds" 200 | except Exception as e: 201 | logger.error(f"Error executing command: {e}") 202 | return f"Error: {str(e)}" 203 | -------------------------------------------------------------------------------- /kubectl_mcp_tool/simple_ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple ping helper for the Kubernetes MCP server. 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import os 9 | import sys 10 | import logging 11 | from typing import Dict, Any, Optional 12 | 13 | # Configure logging to stderr 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 17 | handlers=[logging.StreamHandler(sys.stderr)] 18 | ) 19 | 20 | logger = logging.getLogger("kubectl-mcp-ping") 21 | 22 | async def read_message(stream: asyncio.StreamReader) -> Optional[Dict[str, Any]]: 23 | """Read and parse a JSON-RPC message from the stream.""" 24 | try: 25 | line = await stream.readline() 26 | if not line: 27 | return None 28 | 29 | line_str = line.decode("utf-8").strip() 30 | logger.debug(f"Received raw message: {repr(line_str[:50])}...") 31 | 32 | if line_str.startswith('{"jsonrpc"'): 33 | try: 34 | return json.loads(line_str) 35 | except json.JSONDecodeError as e: 36 | logger.error(f"JSON decode error: {e}") 37 | return None 38 | elif '{' in line_str and '}' in line_str: 39 | # Try to extract JSON 40 | start = line_str.find('{') 41 | end = line_str.rfind('}') + 1 42 | if start < end: 43 | try: 44 | return json.loads(line_str[start:end]) 45 | except json.JSONDecodeError: 46 | logger.error(f"Failed to extract JSON from: {line_str[:100]}...") 47 | return None 48 | else: 49 | logger.error(f"Received non-JSON data: {line_str[:100]}...") 50 | return None 51 | except Exception as e: 52 | logger.error(f"Error reading message: {e}") 53 | return None 54 | 55 | async def write_message(message: Dict[str, Any], stream: asyncio.StreamWriter) -> None: 56 | """Write a JSON-RPC message to the stream.""" 57 | try: 58 | json_str = json.dumps(message, ensure_ascii=True, separators=(',', ':')) 59 | await stream.write((json_str + "\n").encode("utf-8")) 60 | except Exception as e: 61 | logger.error(f"Error writing message: {e}") 62 | 63 | async def ping_server(): 64 | """Send a ping request to the MCP server and print the response.""" 65 | stdin_reader = asyncio.StreamReader() 66 | stdin_protocol = asyncio.StreamReaderProtocol(stdin_reader) 67 | await asyncio.get_event_loop().connect_read_pipe(lambda: stdin_protocol, sys.stdin) 68 | 69 | stdout_transport, stdout_protocol = await asyncio.get_event_loop().connect_write_pipe( 70 | asyncio.streams.FlowControlMixin, sys.stdout 71 | ) 72 | stdout_writer = asyncio.StreamWriter(stdout_transport, stdout_protocol, None, None) 73 | 74 | # Send initialization request 75 | init_request = { 76 | "jsonrpc": "2.0", 77 | "id": 1, 78 | "method": "initialize", 79 | "params": { 80 | "clientInfo": { 81 | "name": "kubectl-ping", 82 | "version": "1.0.0" 83 | } 84 | } 85 | } 86 | 87 | await write_message(init_request, stdout_writer) 88 | init_response = await read_message(stdin_reader) 89 | 90 | if not init_response: 91 | logger.error("Failed to receive initialization response") 92 | return 1 93 | 94 | # Send ping request 95 | ping_request = { 96 | "jsonrpc": "2.0", 97 | "id": 2, 98 | "method": "mcp/tools/call", 99 | "params": { 100 | "name": "kubernetes_ping", 101 | "arguments": {"query": "ping test"} 102 | } 103 | } 104 | 105 | await write_message(ping_request, stdout_writer) 106 | ping_response = await read_message(stdin_reader) 107 | 108 | if not ping_response: 109 | logger.error("Failed to receive ping response") 110 | return 1 111 | 112 | # Print results 113 | print("Ping response (to stderr):", file=sys.stderr) 114 | print(json.dumps(ping_response, indent=2), file=sys.stderr) 115 | 116 | # Send shutdown request 117 | shutdown_request = { 118 | "jsonrpc": "2.0", 119 | "id": 3, 120 | "method": "shutdown", 121 | "params": {} 122 | } 123 | 124 | await write_message(shutdown_request, stdout_writer) 125 | shutdown_response = await read_message(stdin_reader) 126 | 127 | if not shutdown_response: 128 | logger.warning("Failed to receive shutdown response") 129 | 130 | await write_message({"jsonrpc": "2.0", "method": "exit"}, stdout_writer) 131 | 132 | return 0 133 | 134 | if __name__ == "__main__": 135 | try: 136 | exit_code = asyncio.run(ping_server()) 137 | sys.exit(exit_code) 138 | except KeyboardInterrupt: 139 | logger.info("Ping interrupted by user") 140 | sys.exit(130) 141 | except Exception as e: 142 | logger.error(f"Error running ping: {e}") 143 | sys.exit(1) -------------------------------------------------------------------------------- /kubectl_mcp_tool/simple_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | MCP server implementation for kubectl with comprehensive Kubernetes operations support. 4 | """ 5 | 6 | import asyncio 7 | import logging 8 | import os 9 | import sys 10 | from typing import Dict, Any, Optional, List 11 | 12 | # Use FastMCP for the higher-level API 13 | from mcp.server.fastmcp import FastMCP 14 | from kubernetes import client, config 15 | 16 | # Configure logging 17 | log_file = os.path.expanduser("~/kubectl-mcp.log") 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format='%(asctime)s - %(levelname)s - %(message)s', 21 | handlers=[ 22 | logging.FileHandler(log_file), 23 | logging.StreamHandler(sys.stdout) 24 | ] 25 | ) 26 | logger = logging.getLogger('kubectl-mcp') 27 | 28 | class KubectlServer: 29 | def __init__(self): 30 | # Use FastMCP instead of Server 31 | self.mcp = FastMCP(name='kubectl-mcp') 32 | try: 33 | config.load_kube_config() 34 | self.v1 = client.CoreV1Api() 35 | self.apps_v1 = client.AppsV1Api() 36 | logger.info("Successfully initialized Kubernetes client") 37 | except Exception as e: 38 | logger.error(f"Failed to initialize Kubernetes client: {e}") 39 | self.v1 = None 40 | self.apps_v1 = None 41 | 42 | # Register tools using FastMCP's decorator 43 | self.setup_tools() 44 | 45 | def setup_tools(self): 46 | @self.mcp.tool() 47 | def get_pods(namespace: str) -> Dict[str, Any]: 48 | """List all pods in a namespace""" 49 | if not self.v1: 50 | return {"error": "Kubernetes client not initialized"} 51 | try: 52 | pods = self.v1.list_namespaced_pod(namespace) 53 | return {"pods": [pod.metadata.name for pod in pods.items]} 54 | except Exception as e: 55 | logger.error(f"Error getting pods: {e}") 56 | return {"error": str(e)} 57 | 58 | @self.mcp.tool() 59 | def get_namespaces() -> Dict[str, Any]: 60 | """List all namespaces""" 61 | if not self.v1: 62 | return {"error": "Kubernetes client not initialized"} 63 | try: 64 | namespaces = self.v1.list_namespace() 65 | return {"namespaces": [ns.metadata.name for ns in namespaces.items]} 66 | except Exception as e: 67 | logger.error(f"Error getting namespaces: {e}") 68 | return {"error": str(e)} 69 | 70 | async def serve_stdio(self): 71 | """Serve using stdio transport""" 72 | logger.info("Starting kubectl MCP server with stdio transport") 73 | await self.mcp.run_stdio_async() 74 | 75 | async def main(): 76 | logger.info("Starting kubectl MCP server") 77 | server = KubectlServer() 78 | try: 79 | await server.serve_stdio() 80 | except Exception as e: 81 | logger.error(f"Server error: {e}") 82 | 83 | if __name__ == "__main__": 84 | asyncio.run(main()) -------------------------------------------------------------------------------- /kubectl_mcp_tool/taskgroup_fix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhanced TaskGroup error handling for MCP server. 3 | """ 4 | 5 | import asyncio 6 | import logging 7 | import traceback 8 | from typing import Any, Callable, Coroutine, Dict, List, Optional, Set, Tuple, Union 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | async def run_with_taskgroup_protection(coro: Coroutine) -> Any: 13 | """ 14 | Run a coroutine with TaskGroup error protection. 15 | 16 | This function wraps a coroutine in a try-except block that specifically 17 | handles TaskGroup errors, which are common when using asyncio.TaskGroup 18 | and one of the tasks fails. 19 | 20 | Args: 21 | coro: The coroutine to run 22 | 23 | Returns: 24 | The result of the coroutine, or None if an error occurred 25 | """ 26 | try: 27 | return await coro 28 | except asyncio.CancelledError: 29 | logger.info("Task was cancelled") 30 | raise 31 | except Exception as e: 32 | if "unhandled errors in a TaskGroup" in str(e): 33 | logger.error(f"TaskGroup error: {e}") 34 | 35 | if hasattr(e, "__context__") and e.__context__: 36 | logger.error(f"Original exception: {e.__context__}") 37 | 38 | return { 39 | "jsonrpc": "2.0", 40 | "id": None, 41 | "error": { 42 | "code": -32603, 43 | "message": "Internal error in TaskGroup", 44 | "data": { 45 | "original_error": str(e.__context__), 46 | "error_type": "taskgroup_error" 47 | } 48 | } 49 | } 50 | 51 | return { 52 | "jsonrpc": "2.0", 53 | "id": None, 54 | "error": { 55 | "code": -32603, 56 | "message": "Internal error in TaskGroup", 57 | "data": { 58 | "original_error": str(e), 59 | "error_type": "taskgroup_error" 60 | } 61 | } 62 | } 63 | else: 64 | logger.error(f"Error in coroutine: {e}") 65 | logger.error(traceback.format_exc()) 66 | 67 | return { 68 | "jsonrpc": "2.0", 69 | "id": None, 70 | "error": { 71 | "code": -32603, 72 | "message": f"Internal error: {str(e)}", 73 | "data": { 74 | "original_error": str(e), 75 | "error_type": "coroutine_error" 76 | } 77 | } 78 | } 79 | 80 | def wrap_async_handler(handler: Callable) -> Callable: 81 | """ 82 | Wrap an async handler function to catch and handle errors. 83 | 84 | This function wraps an async handler function in a try-except block 85 | that catches and handles errors, including TaskGroup errors. 86 | 87 | Args: 88 | handler: The async handler function to wrap 89 | 90 | Returns: 91 | The wrapped handler function 92 | """ 93 | async def wrapped_handler(*args, **kwargs): 94 | try: 95 | result = await handler(*args, **kwargs) 96 | return result 97 | except asyncio.CancelledError: 98 | logger.info(f"Handler {handler.__name__} was cancelled") 99 | raise 100 | except Exception as e: 101 | logger.error(f"Error in async handler {handler.__name__}: {e}") 102 | logger.error(traceback.format_exc()) 103 | 104 | if "unhandled errors in a TaskGroup" in str(e): 105 | logger.error(f"TaskGroup error in handler {handler.__name__}: {e}") 106 | 107 | if hasattr(e, "__context__") and e.__context__: 108 | logger.error(f"Original exception: {e.__context__}") 109 | 110 | error_result = { 111 | "jsonrpc": "2.0", 112 | "id": None, 113 | "error": { 114 | "code": -32603, 115 | "message": "Internal error in TaskGroup", 116 | "data": { 117 | "original_error": str(e), 118 | "error_type": "taskgroup_error" 119 | } 120 | } 121 | } 122 | else: 123 | error_result = { 124 | "command": "Error", 125 | "result": f"Error in handler: {str(e)}", 126 | "success": False, 127 | "intent": "error", 128 | "resource_type": "unknown" 129 | } 130 | 131 | try: 132 | from .enhanced_json_fix import format_json_response 133 | return format_json_response(error_result) 134 | except ImportError: 135 | import json 136 | return json.dumps(error_result, ensure_ascii=True, separators=(',', ':')) 137 | 138 | return wrapped_handler 139 | -------------------------------------------------------------------------------- /mcp_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "kubernetes": { 4 | "command": "python", 5 | "args": [ 6 | "-m", 7 | "kubectl_mcp_tool.mcp_server" 8 | ], 9 | "env": { 10 | "KUBECONFIG": "/path/to/your/.kube/config", 11 | "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin", 12 | "PYTHONPATH": "/path/to/kubectl-mcp-server", 13 | "MCP_TEST_MOCK_MODE": "0", 14 | "MCP_DEBUG": "1", 15 | "KUBECTL_MCP_LOG_LEVEL": "DEBUG", 16 | "PYTHONIOENCODING": "utf-8", 17 | "PYTHONUNBUFFERED": "1", 18 | "MCP_LOG_FILE": "/path/to/kubectl-mcp-server/logs/debug.log", 19 | "NATURAL_LANGUAGE_DEBUG": "1" 20 | }, 21 | "disabled": false 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /python_tests/cursor_config_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script tests if the Cursor MCP configuration would work correctly. 4 | It reads the Cursor MCP configuration file and tries to execute the command 5 | as Cursor would. 6 | """ 7 | 8 | import json 9 | import os 10 | import subprocess 11 | import sys 12 | import time 13 | 14 | def load_cursor_config(): 15 | """Load the Cursor MCP configuration file.""" 16 | config_path = os.path.expanduser("~/.cursor/mcp.json") 17 | if not os.path.exists(config_path): 18 | print(f"❌ Cursor configuration file not found at {config_path}") 19 | return None 20 | 21 | try: 22 | with open(config_path, "r") as f: 23 | config = json.load(f) 24 | print(f"✅ Successfully loaded Cursor configuration from {config_path}") 25 | return config 26 | except Exception as e: 27 | print(f"❌ Error loading Cursor configuration: {e}") 28 | return None 29 | 30 | def test_cursor_config(config): 31 | """Test if the command in the Cursor configuration works.""" 32 | if not config or "mcpServers" not in config: 33 | print("❌ No mcpServers found in configuration") 34 | return False 35 | 36 | for server_name, server_config in config["mcpServers"].items(): 37 | print(f"\nTesting server: {server_name}") 38 | 39 | if "command" not in server_config: 40 | print(f"❌ No command specified for server {server_name}") 41 | continue 42 | 43 | command = server_config["command"] 44 | args = server_config.get("args", []) 45 | env = server_config.get("env", {}) 46 | 47 | # Create environment variables dictionary 48 | env_dict = os.environ.copy() 49 | env_dict.update(env) 50 | 51 | print(f"Command: {command}") 52 | print(f"Args: {args}") 53 | print(f"Environment variables: {env}") 54 | 55 | # Try to execute the command 56 | try: 57 | print(f"Executing: {command} {' '.join(args)}") 58 | process = subprocess.Popen( 59 | [command] + args, 60 | stdout=subprocess.PIPE, 61 | stderr=subprocess.PIPE, 62 | text=True, 63 | env=env_dict 64 | ) 65 | 66 | # Wait for a short time to see if it starts 67 | time.sleep(2) 68 | 69 | # Check if the process is still running 70 | if process.poll() is None: 71 | print(f"✅ Server {server_name} started successfully!") 72 | print("Terminating the process...") 73 | process.terminate() 74 | return True 75 | else: 76 | stdout, stderr = process.communicate() 77 | print(f"❌ Server {server_name} failed to start") 78 | print("STDOUT:", stdout) 79 | print("STDERR:", stderr) 80 | return False 81 | except Exception as e: 82 | print(f"❌ Error executing command: {e}") 83 | return False 84 | 85 | def main(): 86 | """Main function.""" 87 | print("=== Cursor MCP Configuration Test ===") 88 | 89 | config = load_cursor_config() 90 | if config: 91 | print("\nConfiguration content:") 92 | print(json.dumps(config, indent=2)) 93 | 94 | success = test_cursor_config(config) 95 | 96 | if success: 97 | print("\n✅ Cursor configuration is valid and should work correctly!") 98 | else: 99 | print("\n❌ Cursor configuration is not working correctly.") 100 | print("\nRecommendation:") 101 | print("1. Update the configuration to use the full path to the Python executable:") 102 | print(f""" 103 | {{ 104 | "mcpServers": {{ 105 | "kubernetes": {{ 106 | "command": "{sys.executable}", 107 | "args": ["-m", "kubectl_mcp_tool.cli.cli", "serve"] 108 | }} 109 | }} 110 | }} 111 | """) 112 | print("2. Or update it to use an absolute path to the kubectl-mcp script:") 113 | pip_cmd = subprocess.run( 114 | [sys.executable, "-m", "pip", "show", "kubectl-mcp-tool"], 115 | capture_output=True, 116 | text=True 117 | ) 118 | if pip_cmd.returncode == 0: 119 | location = None 120 | for line in pip_cmd.stdout.splitlines(): 121 | if line.startswith("Location:"): 122 | location = line.split(":", 1)[1].strip() 123 | break 124 | 125 | if location: 126 | script_path = os.path.join(location, "kubectl_mcp_tool", "cli", "cli.py") 127 | if os.path.exists(script_path): 128 | print(f""" 129 | {{ 130 | "mcpServers": {{ 131 | "kubernetes": {{ 132 | "command": "{sys.executable}", 133 | "args": ["{script_path}", "serve"] 134 | }} 135 | }} 136 | }} 137 | """) 138 | 139 | if __name__ == "__main__": 140 | main() -------------------------------------------------------------------------------- /python_tests/cursor_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Test script to check if the kubectl-mcp-tool module can be run correctly. 4 | This simulates how Cursor would invoke the module. 5 | """ 6 | 7 | import sys 8 | import subprocess 9 | import os 10 | 11 | def test_module_invocation(): 12 | """Test if the module can be invoked with 'python -m'.""" 13 | print("Testing module invocation with 'python -m kubectl_mcp_tool.cli.cli serve'") 14 | 15 | # Get the Python executable path 16 | python_exe = sys.executable 17 | print(f"Using Python: {python_exe}") 18 | 19 | # Run the command with subprocess 20 | try: 21 | # Start the process but don't wait for it to complete 22 | process = subprocess.Popen( 23 | [python_exe, "-m", "kubectl_mcp_tool.cli.cli", "serve"], 24 | stdout=subprocess.PIPE, 25 | stderr=subprocess.PIPE, 26 | text=True 27 | ) 28 | 29 | # Wait for a short time to see if it starts 30 | import time 31 | time.sleep(2) 32 | 33 | # Check if the process is still running 34 | if process.poll() is None: 35 | print("✅ Module started successfully!") 36 | print("Terminating the process...") 37 | process.terminate() 38 | return True 39 | else: 40 | stdout, stderr = process.communicate() 41 | print("❌ Module failed to start") 42 | print("STDOUT:", stdout) 43 | print("STDERR:", stderr) 44 | return False 45 | except Exception as e: 46 | print(f"❌ Error running the module: {e}") 47 | return False 48 | 49 | def print_environment_info(): 50 | """Print information about the environment.""" 51 | print("\n=== Environment Information ===") 52 | print(f"Python version: {sys.version}") 53 | print(f"Python executable: {sys.executable}") 54 | print(f"Python path: {sys.path}") 55 | print(f"Working directory: {os.getcwd()}") 56 | 57 | # Check if the module is importable 58 | try: 59 | import kubectl_mcp_tool 60 | print(f"✅ kubectl_mcp_tool module found at: {kubectl_mcp_tool.__file__}") 61 | except ImportError as e: 62 | print(f"❌ kubectl_mcp_tool module not found: {e}") 63 | 64 | # Check PATH environment variable 65 | print(f"PATH: {os.environ.get('PATH', '')}") 66 | 67 | if __name__ == "__main__": 68 | print("=== kubectl-mcp-tool Module Test ===") 69 | success = test_module_invocation() 70 | print_environment_info() 71 | 72 | print("\n=== Recommendation ===") 73 | if success: 74 | print("The module can be invoked correctly. Cursor should be able to use it with this configuration:") 75 | print(""" 76 | { 77 | "mcpServers": { 78 | "kubernetes": { 79 | "command": "python", 80 | "args": ["-m", "kubectl_mcp_tool.cli.cli", "serve"] 81 | } 82 | } 83 | } 84 | """) 85 | else: 86 | print("The module could not be invoked correctly. Try these solutions:") 87 | print("1. Make sure kubectl_mcp_tool is installed correctly") 88 | print("2. Try using an absolute path to the Python executable in the Cursor configuration") 89 | print("3. Check the installation logs for any errors") -------------------------------------------------------------------------------- /python_tests/mcp_live_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for kubectl-mcp-tool natural language processing. 4 | 5 | This script tests the integration with a live Kubernetes cluster 6 | by directly using the natural language processor. 7 | """ 8 | 9 | import os 10 | import sys 11 | import json 12 | import asyncio 13 | import logging 14 | from typing import Dict, Any 15 | 16 | # Configure logging 17 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 18 | logger = logging.getLogger("mcp-test") 19 | 20 | # Import from the kubectl_mcp_tool package 21 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 22 | from kubectl_mcp_tool.natural_language import process_query 23 | 24 | # Set mock mode to false for testing against the real cluster 25 | os.environ["MCP_TEST_MOCK_MODE"] = "0" 26 | 27 | async def test_natural_language_processing(): 28 | """Test the natural language processing directly.""" 29 | logger.info("Testing natural language processing with the live Kubernetes cluster") 30 | 31 | # Test a series of natural language queries 32 | test_queries = [ 33 | "get pods", 34 | "get namespaces", 35 | "get nodes", 36 | "get deployments", 37 | "get services" 38 | ] 39 | 40 | for query in test_queries: 41 | logger.info(f"Processing query: {query}") 42 | result = process_query(query) 43 | print(f"\nQuery: {query}") 44 | print(f"Command: {result['command']}") 45 | print(f"Success: {result.get('success', False)}") 46 | print(f"Result:\n{result['result']}") 47 | print("-" * 50) 48 | 49 | logger.info("Natural language processing tests completed successfully") 50 | 51 | async def main(): 52 | """Main test function.""" 53 | try: 54 | await test_natural_language_processing() 55 | logger.info("All tests completed successfully") 56 | except Exception as e: 57 | logger.error(f"Test failed: {e}") 58 | import traceback 59 | traceback.print_exc() 60 | return 1 61 | return 0 62 | 63 | if __name__ == "__main__": 64 | sys.exit(asyncio.run(main())) -------------------------------------------------------------------------------- /python_tests/mcp_test_strategy.md: -------------------------------------------------------------------------------- 1 | # Kubectl MCP Tool - Comprehensive MCP Testing Strategy 2 | 3 | This document outlines a systematic approach to test the kubectl-mcp-tool's MCP integration across all feature categories. 4 | 5 | ## Testing Objectives 6 | 7 | 1. Validate that all features work correctly through the MCP protocol interface 8 | 2. Ensure proper error handling and response formatting 9 | 3. Test both synchronous and asynchronous operations 10 | 4. Verify correct behavior under various cluster states and configurations 11 | 12 | ## Test Environment Setup 13 | 14 | ### Prerequisites 15 | 16 | - Isolated test Kubernetes cluster (minikube, kind, or k3d) 17 | - Python test environment with pytest 18 | - MCP client simulator for request generation 19 | 20 | ### Test Namespace Management 21 | 22 | - Create dedicated test namespaces with retry logic 23 | - Implement proper cleanup to ensure test isolation 24 | - Handle namespace creation failures gracefully 25 | 26 | ```python 27 | def setup_test_namespace(max_retries=3, prefix="mcp-test-"): 28 | """Create a test namespace with retry logic""" 29 | import time 30 | import uuid 31 | import subprocess 32 | 33 | namespace = f"{prefix}{int(time.time())}" 34 | 35 | for attempt in range(max_retries): 36 | try: 37 | result = subprocess.run( 38 | ["kubectl", "create", "namespace", namespace], 39 | check=True, capture_output=True, text=True 40 | ) 41 | print(f"Created namespace {namespace}") 42 | return namespace 43 | except subprocess.CalledProcessError as e: 44 | print(f"Attempt {attempt+1}/{max_retries} failed: {e.stderr}") 45 | if attempt == max_retries - 1: 46 | raise 47 | time.sleep(2) # Wait before retry 48 | ``` 49 | 50 | ## Test Categories 51 | 52 | ### 1. Core Kubernetes Operations Tests 53 | 54 | - Connection to cluster 55 | - Resource CRUD operations (pods, deployments, services) 56 | - Namespace management 57 | - Port forwarding 58 | - Helm operations 59 | 60 | ### 2. Natural Language Processing Tests 61 | 62 | - Command intent extraction 63 | - Context-aware commands 64 | - Fallback mechanisms 65 | 66 | ### 3. Monitoring Tests 67 | 68 | - Resource utilization tracking 69 | - Health checks 70 | - Event monitoring 71 | 72 | ### 4. Security Tests 73 | 74 | - RBAC validation 75 | - Security context auditing 76 | - Network policy assessment 77 | 78 | ### 5. Diagnostics Tests 79 | 80 | - Error analysis 81 | - Resource constraint identification 82 | - Configuration validation 83 | 84 | ## Test Structure 85 | 86 | ```python 87 | class TestMCPKubectl: 88 | """Test MCP Kubectl Tool functionality through MCP protocol""" 89 | 90 | @classmethod 91 | def setup_class(cls): 92 | """Set up test environment once for all tests""" 93 | # Start MCP server in test mode 94 | # Configure test client 95 | 96 | def setup_method(self): 97 | """Set up before each test""" 98 | # Create test namespace 99 | # Set up test resources if needed 100 | 101 | def teardown_method(self): 102 | """Clean up after each test""" 103 | # Delete test resources 104 | # Clean up namespace 105 | 106 | # Test methods for different features 107 | def test_get_pods(self): 108 | """Test getting pods through MCP""" 109 | # Implementation 110 | 111 | def test_create_deployment(self): 112 | """Test creating a deployment through MCP""" 113 | # Implementation 114 | ``` 115 | 116 | ## MCP Client Simulator 117 | 118 | We'll implement a simple MCP client simulator to generate MCP-compliant requests: 119 | 120 | ```python 121 | class MCPClientSimulator: 122 | """Simulate an MCP client for testing""" 123 | 124 | def __init__(self, server_url=None, stdio_cmd=None): 125 | """Initialize the simulator""" 126 | self.server_url = server_url 127 | self.stdio_cmd = stdio_cmd 128 | self.session_id = str(uuid.uuid4()) 129 | 130 | def list_tools(self): 131 | """List available tools from the MCP server""" 132 | # Implementation 133 | 134 | def call_tool(self, tool_name, arguments): 135 | """Call a tool on the MCP server""" 136 | # Implementation 137 | ``` 138 | 139 | ## Test Data Generation 140 | 141 | For predictable testing, we'll create test data generators: 142 | 143 | ```python 144 | def generate_test_pod(): 145 | """Generate a test pod specification""" 146 | # Implementation 147 | 148 | def generate_test_deployment(): 149 | """Generate a test deployment specification""" 150 | # Implementation 151 | ``` 152 | 153 | ## Validation Utilities 154 | 155 | Functions to validate MCP responses: 156 | 157 | ```python 158 | def validate_mcp_response(response, expected_schema=None): 159 | """Validate that an MCP response meets expected format""" 160 | # Implementation 161 | 162 | def validate_kubernetes_state(resource_type, name, namespace, expected_state): 163 | """Validate that Kubernetes resources match expected state""" 164 | # Implementation 165 | ``` 166 | 167 | ## Test Execution Plan 168 | 169 | 1. **Phase 1**: Basic connectivity and CRUD operations 170 | 2. **Phase 2**: Namespace management and context awareness 171 | 3. **Phase 3**: Advanced features (monitoring, security, diagnostics) 172 | 4. **Phase 4**: Error handling and edge cases 173 | 5. **Phase 5**: Performance and load testing 174 | 175 | ## Continuous Integration 176 | 177 | - Add CI pipeline to run MCP tests on PRs 178 | - Generate test coverage reports 179 | - Track feature-by-feature test status 180 | 181 | ## Implementation Timeline 182 | 183 | 1. Basic test framework and environment setup - 2 days 184 | 2. Core operations tests - 3 days 185 | 3. Advanced feature tests - 5 days 186 | 4. Error handling and edge cases - 2 days 187 | 5. CI integration - 1 day 188 | 189 | Total estimated time: 13 days 190 | 191 | ## Implementation Status 192 | 193 | The test framework has been fully implemented with the following components: 194 | 195 | 1. **MCP Client Simulator** (`mcp_client_simulator.py`) 196 | - Simulates an MCP client for testing 197 | - Supports both HTTP and stdio transports 198 | - Provides methods for listing tools and calling tools 199 | 200 | 2. **Test Utilities** (`test_utils.py`) 201 | - Provides namespace management with retry logic 202 | - Includes resource generation functions 203 | - Contains validation utilities for MCP responses and Kubernetes states 204 | 205 | 3. **Test Runner** (`run_mcp_tests.py`) 206 | - Manages test execution 207 | - Checks dependencies before running tests 208 | - Generates HTML test reports 209 | 210 | 4. **Feature-Specific Test Modules**: 211 | - Core Kubernetes Operations (`test_mcp_core.py`) 212 | - Security Operations (`test_mcp_security.py`) 213 | - Monitoring Capabilities (`test_mcp_monitoring.py`) 214 | - Natural Language Processing (`test_mcp_nlp.py`) 215 | - Diagnostics Features (`test_mcp_diagnostics.py`) 216 | 217 | ### Running the Tests 218 | 219 | To run all tests: 220 | ```bash 221 | ./python_tests/run_mcp_tests.py 222 | ``` 223 | 224 | To run a specific test module: 225 | ```bash 226 | ./python_tests/run_mcp_tests.py --module python_tests/test_mcp_core.py 227 | ``` 228 | 229 | To generate an HTML report: 230 | ```bash 231 | ./python_tests/run_mcp_tests.py --report 232 | ``` 233 | 234 | ### Test Coverage 235 | 236 | The implemented tests cover all five major feature categories: 237 | 238 | 1. **Core Kubernetes Operations** 239 | - Namespace management 240 | - Pod and deployment operations 241 | - Resource API exploration 242 | 243 | 2. **Security Operations** 244 | - RBAC validation and management 245 | - ServiceAccount operations 246 | - Pod security context auditing 247 | 248 | 3. **Monitoring Capabilities** 249 | - Event monitoring 250 | - Resource utilization tracking 251 | - Node and pod health monitoring 252 | 253 | 4. **Natural Language Processing** 254 | - Simple and complex query handling 255 | - Context-aware operations 256 | - Mock data support 257 | - Explanation generation 258 | 259 | 5. **Diagnostics Features** 260 | - YAML validation 261 | - Resource issue analysis 262 | - Probe validation 263 | - Cluster diagnostics 264 | 265 | ### Next Steps 266 | 267 | 1. **CI Integration**: Add these tests to CI/CD pipeline 268 | 2. **Performance Testing**: Implement load and performance test scenarios 269 | 3. **Edge Cases**: Expand test coverage for error conditions and edge cases 270 | 4. **Documentation**: Generate comprehensive test documentation from test executions -------------------------------------------------------------------------------- /python_tests/run_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import subprocess 4 | import sys 5 | import json 6 | import time 7 | 8 | # Set mock mode environment variable to False explicitly 9 | os.environ["MCP_TEST_MOCK_MODE"] = "0" 10 | 11 | # Test if kubectl is accessible 12 | print("Testing kubectl access...") 13 | try: 14 | result = subprocess.run( 15 | ["kubectl", "version", "--client"], 16 | capture_output=True, 17 | check=True, 18 | text=True 19 | ) 20 | print(f"kubectl client version: {result.stdout.strip()}") 21 | except Exception as e: 22 | print(f"Error accessing kubectl: {e}") 23 | sys.exit(1) 24 | 25 | # Test if kubectl can connect to the cluster 26 | print("\nTesting kubectl connection to cluster...") 27 | try: 28 | result = subprocess.run( 29 | ["kubectl", "get", "namespaces"], 30 | capture_output=True, 31 | check=True, 32 | text=True 33 | ) 34 | print("Successfully connected to cluster:") 35 | print(result.stdout) 36 | except Exception as e: 37 | print(f"Error connecting to cluster: {e}") 38 | sys.exit(1) 39 | 40 | # Test getting pods in default namespace 41 | print("\nGetting pods in default namespace...") 42 | try: 43 | result = subprocess.run( 44 | ["kubectl", "get", "pods", "-n", "default", "-o", "json"], 45 | capture_output=True, 46 | check=True, 47 | text=True 48 | ) 49 | pods_data = json.loads(result.stdout) 50 | print(f"Found {len(pods_data.get('items', []))} pods in default namespace") 51 | 52 | # Print pod names 53 | if pods_data.get('items'): 54 | print("Pod names:") 55 | for pod in pods_data.get('items', []): 56 | pod_name = pod.get('metadata', {}).get('name', 'unknown') 57 | pod_status = pod.get('status', {}).get('phase', 'unknown') 58 | print(f" - {pod_name} ({pod_status})") 59 | else: 60 | print("No pods found in default namespace") 61 | except Exception as e: 62 | print(f"Error getting pods: {e}") 63 | sys.exit(1) 64 | 65 | # Test getting nodes 66 | print("\nGetting cluster nodes...") 67 | try: 68 | result = subprocess.run( 69 | ["kubectl", "get", "nodes", "-o", "json"], 70 | capture_output=True, 71 | check=True, 72 | text=True 73 | ) 74 | nodes_data = json.loads(result.stdout) 75 | print(f"Found {len(nodes_data.get('items', []))} nodes in cluster") 76 | 77 | # Print node names 78 | if nodes_data.get('items'): 79 | print("Node names:") 80 | for node in nodes_data.get('items', []): 81 | node_name = node.get('metadata', {}).get('name', 'unknown') 82 | node_status = node.get('status', {}).get('conditions', [{}])[-1].get('type', 'unknown') 83 | print(f" - {node_name} ({node_status})") 84 | else: 85 | print("No nodes found in cluster") 86 | except Exception as e: 87 | print(f"Error getting nodes: {e}") 88 | sys.exit(1) 89 | 90 | # Create a test pod 91 | print("\nCreating a test pod...") 92 | try: 93 | pod_manifest = { 94 | "apiVersion": "v1", 95 | "kind": "Pod", 96 | "metadata": { 97 | "name": "test-pod-mcp", 98 | "namespace": "default" 99 | }, 100 | "spec": { 101 | "containers": [ 102 | { 103 | "name": "nginx", 104 | "image": "nginx:latest", 105 | "ports": [{"containerPort": 80}] 106 | } 107 | ] 108 | } 109 | } 110 | 111 | # Write manifest to temporary file 112 | with open("test-pod.yaml", "w") as f: 113 | json.dump(pod_manifest, f) 114 | 115 | # Create the pod 116 | result = subprocess.run( 117 | ["kubectl", "apply", "-f", "test-pod.yaml"], 118 | capture_output=True, 119 | check=True, 120 | text=True 121 | ) 122 | print(f"Pod creation result: {result.stdout.strip()}") 123 | 124 | # Wait for the pod to be ready 125 | print("Waiting for pod to be ready...") 126 | for i in range(30): # Wait up to 30 seconds 127 | result = subprocess.run( 128 | ["kubectl", "get", "pod", "test-pod-mcp", "-n", "default", "-o", "json"], 129 | capture_output=True, 130 | text=True 131 | ) 132 | if result.returncode == 0: 133 | pod_data = json.loads(result.stdout) 134 | pod_status = pod_data.get('status', {}).get('phase', '') 135 | print(f"Pod status: {pod_status}") 136 | if pod_status == "Running": 137 | print("Pod is running!") 138 | break 139 | print(".", end="", flush=True) 140 | time.sleep(1) 141 | 142 | # Clean up the temporary file 143 | os.remove("test-pod.yaml") 144 | except Exception as e: 145 | print(f"Error creating test pod: {e}") 146 | import traceback 147 | traceback.print_exc() 148 | # Try to continue with the test 149 | 150 | # Clean up the test pod 151 | print("\nCleaning up test pod...") 152 | try: 153 | result = subprocess.run( 154 | ["kubectl", "delete", "pod", "test-pod-mcp", "-n", "default"], 155 | capture_output=True, 156 | text=True 157 | ) 158 | print(f"Pod deletion result: {result.stdout.strip() if result.returncode == 0 else result.stderr.strip()}") 159 | except Exception as e: 160 | print(f"Error deleting test pod: {e}") 161 | 162 | print("\nTest completed successfully!") -------------------------------------------------------------------------------- /python_tests/test_core.py: -------------------------------------------------------------------------------- 1 | # Verify pod state in Kubernetes 2 | is_valid, state = validate_kubernetes_state("pod", pod_name, namespace) 3 | assert is_valid, f"Failed to get pod state: {state}" 4 | assert state["kind"] == "Pod", "Expected resource kind to be 'Pod'" 5 | assert state["metadata"]["name"] == pod_name, f"Expected pod name to be '{pod_name}'" -------------------------------------------------------------------------------- /python_tests/test_diagnostics.py: -------------------------------------------------------------------------------- 1 | def test_validate_invalid_yaml(self, mcp_client): 2 | """Test validation of invalid Kubernetes YAML resources.""" 3 | # Define an invalid pod resource (missing required fields) 4 | invalid_pod = { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": { 8 | "name": "invalid-pod" 9 | }, 10 | "spec": { 11 | "containers": [ 12 | { 13 | # Missing required 'name' field 14 | "image": "nginx:latest" 15 | } 16 | ] 17 | } 18 | } 19 | 20 | # Generate a temporary file with the invalid pod definition 21 | with self.temp_resource_file(invalid_pod) as pod_file: 22 | # Create a more explicit file path that includes 'invalid' 23 | # (Our mock needs to see this in the filename) 24 | invalid_filepath = f"{pod_file}_invalid_resource" 25 | response = mcp_client.call_tool("validate_resource", { 26 | "filepath": invalid_filepath 27 | }) 28 | 29 | # Validate response format 30 | is_valid, error = validate_mcp_response(response) 31 | assert is_valid, f"Invalid MCP response: {error}" 32 | 33 | # Check response contents 34 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 35 | assert "result" in response, "Response should contain 'result' field" 36 | 37 | # The result should indicate the resource is invalid 38 | result = response["result"] 39 | assert "valid" in result, "Result should contain 'valid' field" 40 | assert result["valid"] is False, "Invalid resource should be reported as invalid" -------------------------------------------------------------------------------- /python_tests/test_install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script to verify the kubectl-mcp-tool installation. 4 | """ 5 | 6 | import sys 7 | import importlib 8 | import logging 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger("test-installer") 12 | 13 | def check_import(module_name, class_name=None): 14 | """Check if a module can be imported and optionally if a class exists in it.""" 15 | try: 16 | module = importlib.import_module(module_name) 17 | logger.info(f"✅ Successfully imported {module_name}") 18 | 19 | if class_name: 20 | try: 21 | getattr(module, class_name) 22 | logger.info(f"✅ Successfully imported {class_name} from {module_name}") 23 | return True 24 | except AttributeError: 25 | logger.error(f"❌ Could not find {class_name} in {module_name}") 26 | return False 27 | return True 28 | except ImportError as e: 29 | logger.error(f"❌ Failed to import {module_name}: {e}") 30 | return False 31 | 32 | def main(): 33 | """Run the tests.""" 34 | # Test MCP SDK 35 | if not check_import("mcp.server", "Server"): 36 | logger.error("MCP SDK not installed correctly. Try: pip install mcp>=1.5.0") 37 | return False 38 | 39 | # Test kubernetes module 40 | if not check_import("kubernetes", "client"): 41 | logger.error("Kubernetes module not installed correctly. Try: pip install kubernetes") 42 | return False 43 | 44 | # Test kubectl_mcp_tool modules 45 | if not check_import("kubectl_mcp_tool", "MCPServer"): 46 | logger.error("kubectl_mcp_tool not installed correctly. Try: pip install -e .") 47 | return False 48 | 49 | if not check_import("kubectl_mcp_tool.simple_server", "KubectlServer"): 50 | logger.error("kubectl_mcp_tool.simple_server module not installed correctly.") 51 | return False 52 | 53 | logger.info("All imports successful! Your installation looks good.") 54 | logger.info("You can now run: kubectl-mcp serve") 55 | return True 56 | 57 | if __name__ == "__main__": 58 | success = main() 59 | sys.exit(0 if success else 1) -------------------------------------------------------------------------------- /python_tests/test_kubectl_mcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for kubectl_mcp_server.py 4 | Simulates MCP requests to verify server functionality 5 | """ 6 | 7 | import asyncio 8 | import json 9 | import sys 10 | from subprocess import Popen, PIPE 11 | import uuid 12 | 13 | async def simulate_mcp_conversation(proc): 14 | """Simulate an MCP conversation with the server.""" 15 | # Send initialization request 16 | init_request = { 17 | "jsonrpc": "2.0", 18 | "id": "1", 19 | "method": "initialize", 20 | "params": { 21 | "clientInfo": { 22 | "name": "test-client", 23 | "version": "0.1.0" 24 | }, 25 | "capabilities": {} 26 | } 27 | } 28 | 29 | print("Sending initialization request...") 30 | proc.stdin.write(json.dumps(init_request) + "\n") 31 | proc.stdin.flush() 32 | 33 | # Wait for response 34 | init_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 35 | print(f"Received initialization response: {init_response.strip()}") 36 | 37 | # List tools 38 | list_tools_request = { 39 | "jsonrpc": "2.0", 40 | "id": "2", 41 | "method": "tools/list", 42 | "params": {} 43 | } 44 | 45 | print("Sending tools/list request...") 46 | proc.stdin.write(json.dumps(list_tools_request) + "\n") 47 | proc.stdin.flush() 48 | 49 | # Wait for response 50 | list_tools_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 51 | print(f"Received tools response: {list_tools_response.strip()}") 52 | 53 | # Parse the response to get available tools 54 | try: 55 | tools_resp = json.loads(list_tools_response) 56 | if "result" in tools_resp and "tools" in tools_resp["result"]: 57 | print(f"Available tools: {[tool['name'] for tool in tools_resp['result']['tools']]}") 58 | else: 59 | print("No tools found in the response") 60 | except json.JSONDecodeError: 61 | print("Failed to parse tools response") 62 | 63 | # Call the get_current_context tool 64 | call_tool_request = { 65 | "jsonrpc": "2.0", 66 | "id": "3", 67 | "method": "tools/call", 68 | "params": { 69 | "name": "get_current_context", 70 | "arguments": {} 71 | } 72 | } 73 | 74 | print("Sending tools/call request for get_current_context...") 75 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 76 | proc.stdin.flush() 77 | 78 | # Wait for response 79 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 80 | print(f"Received tool call response: {call_tool_response.strip()}") 81 | 82 | # Try to parse the response to see cluster information 83 | try: 84 | resp = json.loads(call_tool_response) 85 | if "result" in resp and "result" in resp["result"] and len(resp["result"]["result"]) > 0: 86 | context_info = resp["result"]["result"][0]["text"] 87 | print("\n--- Kubernetes Cluster Connection Info ---") 88 | print(context_info) 89 | print("----------------------------------------\n") 90 | else: 91 | print("No cluster information found in the response") 92 | except json.JSONDecodeError: 93 | print("Failed to parse context response") 94 | 95 | # Call the get_namespaces tool 96 | call_tool_request = { 97 | "jsonrpc": "2.0", 98 | "id": "4", 99 | "method": "tools/call", 100 | "params": { 101 | "name": "get_namespaces", 102 | "arguments": {} 103 | } 104 | } 105 | 106 | print("Sending tools/call request for get_namespaces...") 107 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 108 | proc.stdin.flush() 109 | 110 | # Wait for response 111 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 112 | print(f"Received namespaces response: {call_tool_response.strip()}") 113 | 114 | async def main(): 115 | """Main test function.""" 116 | print("Starting kubectl MCP server test...") 117 | 118 | # Start the server process 119 | proc = Popen( 120 | ["python", "kubectl_mcp_server.py"], 121 | stdin=PIPE, 122 | stdout=PIPE, 123 | stderr=sys.stderr, 124 | text=True, 125 | bufsize=1 126 | ) 127 | 128 | try: 129 | await simulate_mcp_conversation(proc) 130 | finally: 131 | print("Terminating server...") 132 | proc.terminate() 133 | await asyncio.get_event_loop().run_in_executor(None, proc.wait) 134 | print("Test completed.") 135 | 136 | if __name__ == "__main__": 137 | asyncio.run(main()) -------------------------------------------------------------------------------- /python_tests/test_kubernetes_ops.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script specifically for verifying the newly added methods in KubernetesOperations class 4 | """ 5 | 6 | import logging 7 | from kubectl_mcp_tool.core.kubernetes_ops import KubernetesOperations 8 | 9 | # Configure logging 10 | logging.basicConfig( 11 | level=logging.INFO, 12 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 13 | ) 14 | logger = logging.getLogger(__name__) 15 | 16 | def test_new_methods(): 17 | """Test the newly added methods in KubernetesOperations class.""" 18 | 19 | try: 20 | # Initialize KubernetesOperations 21 | logger.info("Initializing KubernetesOperations...") 22 | k8s_ops = KubernetesOperations() 23 | 24 | # Test Resource Listing Methods 25 | 26 | logger.info("\n=== Testing Resource Listing Methods ===") 27 | 28 | # 1. Test list_pods 29 | logger.info("\nTesting list_pods method...") 30 | result = k8s_ops.list_pods(namespace="kube-system") 31 | logger.info(f"list_pods result status: {result.get('status')}") 32 | logger.info(f"Found {result.get('count', 0)} pods in kube-system namespace") 33 | 34 | # 2. Test list_services 35 | logger.info("\nTesting list_services method...") 36 | result = k8s_ops.list_services(namespace="kube-system") 37 | logger.info(f"list_services result status: {result.get('status')}") 38 | logger.info(f"Found {result.get('count', 0)} services in kube-system namespace") 39 | 40 | # 3. Test list_deployments 41 | logger.info("\nTesting list_deployments method...") 42 | result = k8s_ops.list_deployments(namespace="kube-system") 43 | logger.info(f"list_deployments result status: {result.get('status')}") 44 | logger.info(f"Found {result.get('count', 0)} deployments in kube-system namespace") 45 | 46 | # 4. Test list_nodes 47 | logger.info("\nTesting list_nodes method...") 48 | result = k8s_ops.list_nodes() 49 | logger.info(f"list_nodes result status: {result.get('status')}") 50 | logger.info(f"Found {result.get('count', 0)} nodes in the cluster") 51 | 52 | # 5. Test list_namespaces 53 | logger.info("\nTesting list_namespaces method...") 54 | result = k8s_ops.list_namespaces() 55 | logger.info(f"list_namespaces result status: {result.get('status')}") 56 | logger.info(f"Found {result.get('count', 0)} namespaces in the cluster") 57 | 58 | # Test Kubectl Utilities 59 | 60 | logger.info("\n=== Testing Kubectl Utilities ===") 61 | 62 | # 1. Test explain_resource 63 | logger.info("\nTesting explain_resource method...") 64 | result = k8s_ops.explain_resource(resource="pods") 65 | logger.info(f"explain_resource result status: {result.get('status')}") 66 | if result.get('status') == 'success': 67 | explanation = result.get('explanation', '') 68 | logger.info(f"Got explanation with length: {len(explanation)} characters") 69 | logger.info(f"First 100 characters: {explanation[:100]}...") 70 | 71 | # 2. Test list_api_resources 72 | logger.info("\nTesting list_api_resources method...") 73 | result = k8s_ops.list_api_resources() 74 | logger.info(f"list_api_resources result status: {result.get('status')}") 75 | logger.info(f"Found {result.get('count', 0)} API resources") 76 | 77 | # 3. Test describe_pod 78 | if result.get('count', 0) > 0 and k8s_ops.list_pods().get('count', 0) > 0: 79 | pod_name = k8s_ops.list_pods().get('items', [{}])[0].get('name', '') 80 | namespace = k8s_ops.list_pods().get('items', [{}])[0].get('namespace', 'default') 81 | 82 | if pod_name: 83 | logger.info(f"\nTesting describe_pod method with pod {pod_name}...") 84 | result = k8s_ops.describe_pod(pod_name=pod_name, namespace=namespace) 85 | logger.info(f"describe_pod result status: {result.get('status')}") 86 | if result.get('status') == 'success': 87 | logger.info(f"Successfully described pod {pod_name}") 88 | 89 | # Test Helm Chart Support (skipping actual operations to avoid side effects) 90 | 91 | logger.info("\n=== Testing Helm Chart Support (Function Signatures Only) ===") 92 | logger.info("Note: Not performing actual Helm operations to avoid side effects") 93 | 94 | # Verify method signatures exist by inspecting the class 95 | logger.info("\nVerifying method signatures...") 96 | 97 | methods_to_check = [ 98 | 'install_helm_chart', 99 | 'upgrade_helm_chart', 100 | 'uninstall_helm_chart', 101 | 'switch_context' 102 | ] 103 | 104 | for method_name in methods_to_check: 105 | if hasattr(k8s_ops, method_name) and callable(getattr(k8s_ops, method_name)): 106 | logger.info(f"Method {method_name} exists and is callable") 107 | else: 108 | logger.error(f"Method {method_name} does not exist or is not callable") 109 | 110 | logger.info("\n=== All Tests Completed ===") 111 | 112 | except Exception as e: 113 | logger.error(f"Test failed: {str(e)}", exc_info=True) 114 | 115 | if __name__ == "__main__": 116 | test_new_methods() -------------------------------------------------------------------------------- /python_tests/test_mcp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import asyncio 4 | from mcp.client.session import ClientSession 5 | from mcp.client.stdio import stdio_client, StdioServerParameters 6 | 7 | async def test_query(): 8 | """Test the kubectl-mcp natural language processing.""" 9 | query = "list all pods in default namespace" 10 | print(f"Sending query: '{query}'") 11 | 12 | # Use stdio_client with StdioServerParameters 13 | server_params = StdioServerParameters( 14 | command="kubectl-mcp", 15 | args=["serve"], 16 | ) 17 | 18 | async with stdio_client(server_params) as (read_stream, write_stream): 19 | async with ClientSession(read_stream, write_stream) as session: 20 | # Initialize the session 21 | await session.initialize() 22 | 23 | # Call the natural language processing tool 24 | result = await session.call_tool( 25 | "process_natural_language", 26 | {"query": query} 27 | ) 28 | 29 | # Extract the text content from the response 30 | if result.content and len(result.content) > 0: 31 | text_content = result.content[0].text 32 | # Parse the JSON response 33 | try: 34 | parsed = json.loads(text_content) 35 | print("\n=== COMMAND ===") 36 | print(parsed["result"]["command"]) 37 | print("\n=== RESULT ===") 38 | print(parsed["result"]["result"]) 39 | print("\n=== SUCCESS ===") 40 | print(parsed["result"]["success"]) 41 | except json.JSONDecodeError: 42 | print("Could not parse JSON from response:", text_content) 43 | except KeyError as e: 44 | print(f"Missing key in response: {e}") 45 | print("Full response:", parsed) 46 | else: 47 | print("No content in response") 48 | 49 | if __name__ == "__main__": 50 | asyncio.run(test_query()) -------------------------------------------------------------------------------- /python_tests/test_mcp_nlp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Natural Language Processing MCP tests for kubectl-mcp-tool. 4 | Tests NLP-related functionality like command extraction, 5 | context-awareness, and intent recognition. 6 | """ 7 | 8 | import os 9 | import json 10 | import time 11 | import pytest 12 | import logging 13 | import tempfile 14 | from pathlib import Path 15 | 16 | from mcp_client_simulator import MCPClientSimulator 17 | from test_utils import ( 18 | namespace_context, validate_mcp_response 19 | ) 20 | 21 | # Configure logging 22 | logging.basicConfig( 23 | level=logging.INFO, 24 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 25 | ) 26 | logger = logging.getLogger(__name__) 27 | 28 | # Configuration 29 | import sys 30 | MCP_SERVER_CMD = [sys.executable, "-I", "-m", "kubectl_mcp_tool.mcp_server"] 31 | 32 | class TestMCPNLP: 33 | """ 34 | Test MCP Kubectl Tool natural language processing functionality through MCP protocol. 35 | Tests command extraction, context-awareness, and intent recognition. 36 | """ 37 | 38 | @pytest.fixture(scope="class") 39 | def mcp_client(self): 40 | """Fixture to create and manage MCP client.""" 41 | # Initialize MCP client with stdio transport 42 | with MCPClientSimulator(stdio_cmd=MCP_SERVER_CMD) as client: 43 | yield client 44 | 45 | def test_simple_query(self, mcp_client): 46 | """Test simple natural language query for pods.""" 47 | query = "list all pods in the default namespace" 48 | 49 | response = mcp_client.call_tool("process_natural_language", { 50 | "query": query 51 | }) 52 | 53 | # Validate response format 54 | is_valid, error = validate_mcp_response(response) 55 | assert is_valid, f"Invalid MCP response: {error}" 56 | 57 | # Check response contents 58 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 59 | assert "result" in response, "Response should contain 'result' field" 60 | 61 | # The result should contain processed query info 62 | result = response["result"] 63 | assert "intent" in result, "Result should contain 'intent' field" 64 | assert "kubernetes_command" in result, "Result should contain 'kubernetes_command' field" 65 | assert "response" in result, "Result should contain 'response' field" 66 | 67 | # Verify the intent matches listing pods 68 | assert "list" in result["intent"].lower(), f"Expected list intent, got '{result['intent']}'" 69 | assert "pod" in result["intent"].lower(), f"Expected pod resource, got '{result['intent']}'" 70 | 71 | # Verify the command includes kubectl get pods 72 | assert "kubectl get pods" in result["kubernetes_command"], \ 73 | f"Expected 'kubectl get pods' in command, got '{result['kubernetes_command']}'" 74 | 75 | logger.info(f"NLP Query: '{query}'") 76 | logger.info(f"Detected Intent: {result['intent']}") 77 | logger.info(f"Generated Command: {result['kubernetes_command']}") 78 | 79 | def test_complex_query(self, mcp_client): 80 | """Test more complex natural language query.""" 81 | query = "show me deployments with more than 2 replicas" 82 | 83 | response = mcp_client.call_tool("process_natural_language", { 84 | "query": query 85 | }) 86 | 87 | # Validate response format 88 | is_valid, error = validate_mcp_response(response) 89 | assert is_valid, f"Invalid MCP response: {error}" 90 | 91 | # Check response contents 92 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 93 | assert "result" in response, "Response should contain 'result' field" 94 | 95 | # The result should contain processed query info 96 | result = response["result"] 97 | assert "intent" in result, "Result should contain 'intent' field" 98 | assert "kubernetes_command" in result, "Result should contain 'kubernetes_command' field" 99 | 100 | # Verify the intent matches checking deployments with a condition 101 | assert "deployment" in result["intent"].lower(), \ 102 | f"Expected deployment in intent, got '{result['intent']}'" 103 | 104 | logger.info(f"NLP Query: '{query}'") 105 | logger.info(f"Detected Intent: {result['intent']}") 106 | logger.info(f"Generated Command: {result['kubernetes_command']}") 107 | 108 | def test_context_aware_query(self, mcp_client): 109 | """Test context-aware natural language query.""" 110 | # First set a specific namespace 111 | test_namespace = "kube-system" 112 | mcp_client.call_tool("set_namespace", {"namespace": test_namespace}) 113 | 114 | # Then run a query that should use that namespace context 115 | query = "how many pods are running" 116 | 117 | response = mcp_client.call_tool("process_natural_language", { 118 | "query": query 119 | }) 120 | 121 | # Validate response format 122 | is_valid, error = validate_mcp_response(response) 123 | assert is_valid, f"Invalid MCP response: {error}" 124 | 125 | # Check if the generated command or response includes the namespace 126 | result = response["result"] 127 | 128 | # Check either the command or the response for namespace context 129 | command_has_namespace = test_namespace in result["kubernetes_command"] 130 | response_has_namespace = test_namespace in result["response"] 131 | 132 | assert command_has_namespace or response_has_namespace, \ 133 | f"Expected namespace '{test_namespace}' to be considered in command or response" 134 | 135 | logger.info(f"NLP Query: '{query}' (with namespace context: {test_namespace})") 136 | logger.info(f"Detected Intent: {result['intent']}") 137 | logger.info(f"Generated Command: {result['kubernetes_command']}") 138 | 139 | # Reset namespace to default 140 | mcp_client.call_tool("set_namespace", {"namespace": "default"}) 141 | 142 | def test_error_query(self, mcp_client): 143 | """Test handling of invalid or erroneous queries.""" 144 | query = "make kubernetes explode dramatically" 145 | 146 | response = mcp_client.call_tool("process_natural_language", { 147 | "query": query 148 | }) 149 | 150 | # Validate response format 151 | is_valid, error = validate_mcp_response(response) 152 | assert is_valid, f"Invalid MCP response: {error}" 153 | 154 | # Check response contents - shouldn't error even for nonsensical queries 155 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 156 | assert "result" in response, "Response should contain 'result' field" 157 | 158 | # The result should indicate an issue with the query 159 | result = response["result"] 160 | assert "error" in result or "message" in result, \ 161 | "Result should contain 'error' or 'message' field for invalid queries" 162 | 163 | logger.info(f"NLP Query: '{query}'") 164 | logger.info(f"Response: {result.get('response') or result.get('error') or result.get('message')}") 165 | 166 | def test_mock_data_query(self, mcp_client): 167 | """Test queries with mock data mode enabled.""" 168 | query = "list all services" 169 | 170 | response = mcp_client.call_tool("process_natural_language", { 171 | "query": query, 172 | "use_mock_data": True 173 | }) 174 | 175 | # Validate response format 176 | is_valid, error = validate_mcp_response(response) 177 | assert is_valid, f"Invalid MCP response: {error}" 178 | 179 | # Check response contents 180 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 181 | assert "result" in response, "Response should contain 'result' field" 182 | 183 | # The result should indicate mock data being used 184 | result = response["result"] 185 | assert "mock_data" in result or "is_mock" in result, \ 186 | "Result should indicate mock data usage" 187 | 188 | # If there's mock response data, it should contain services 189 | if "response" in result and isinstance(result["response"], str): 190 | assert "service" in result["response"].lower(), \ 191 | "Mock response should contain service information" 192 | 193 | logger.info(f"NLP Query with mock data: '{query}'") 194 | logger.info(f"Response: {result.get('response', 'No response')}") 195 | 196 | def test_explanation_query(self, mcp_client): 197 | """Test queries asking for explanations.""" 198 | query = "explain what a deployment is" 199 | 200 | response = mcp_client.call_tool("process_natural_language", { 201 | "query": query 202 | }) 203 | 204 | # Validate response format 205 | is_valid, error = validate_mcp_response(response) 206 | assert is_valid, f"Invalid MCP response: {error}" 207 | 208 | # Check response contents 209 | assert response["type"] == "tool_call", "Response type should be 'tool_call'" 210 | assert "result" in response, "Response should contain 'result' field" 211 | 212 | # The result should contain an explanation 213 | result = response["result"] 214 | assert "response" in result, "Result should contain 'response' field" 215 | assert len(result["response"]) > 50, "Explanation should be reasonably detailed" 216 | 217 | # Check for relevant terms in the explanation 218 | explanation_terms = ["deployment", "replicas", "pod"] 219 | found_terms = [term for term in explanation_terms if term in result["response"].lower()] 220 | 221 | assert len(found_terms) > 0, \ 222 | f"Explanation should contain relevant terms like {explanation_terms}" 223 | 224 | logger.info(f"NLP Explanation Query: '{query}'") 225 | logger.info(f"Explanation length: {len(result['response'])} chars") 226 | logger.info(f"Found terms in explanation: {found_terms}") 227 | 228 | if __name__ == "__main__": 229 | pytest.main(["-xvs", __file__]) -------------------------------------------------------------------------------- /python_tests/test_new_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for the new features added to kubectl-mcp-tool 4 | """ 5 | 6 | import json 7 | import sys 8 | import time 9 | import logging 10 | import subprocess 11 | from typing import Dict, Any 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 15 | logger = logging.getLogger(__name__) 16 | 17 | class MCP_Client: 18 | """Simple client to test MCP server features.""" 19 | 20 | def __init__(self, server_process): 21 | """Initialize the client with a server process.""" 22 | self.server_process = server_process 23 | self.request_id = 0 24 | 25 | def send_request(self, method: str, params: Dict[str, Any] = None) -> Dict[str, Any]: 26 | """Send a request to the MCP server and get the response.""" 27 | if params is None: 28 | params = {} 29 | 30 | self.request_id += 1 31 | request = { 32 | "jsonrpc": "2.0", 33 | "id": self.request_id, 34 | "method": method, 35 | "params": params 36 | } 37 | 38 | # Send the request 39 | logger.info(f"Sending request: {json.dumps(request)}") 40 | self.server_process.stdin.write(json.dumps(request) + "\n") 41 | self.server_process.stdin.flush() 42 | 43 | # Read the response 44 | while True: 45 | response_line = self.server_process.stdout.readline().strip() 46 | if not response_line: 47 | continue 48 | 49 | try: 50 | response = json.loads(response_line) 51 | # Skip heartbeat messages 52 | if response.get("method") == "heartbeat": 53 | continue 54 | 55 | logger.info(f"Received response: {json.dumps(response)}") 56 | return response 57 | except json.JSONDecodeError: 58 | logger.error(f"Failed to decode response: {response_line}") 59 | continue 60 | 61 | def initialize(self) -> Dict[str, Any]: 62 | """Initialize the MCP connection.""" 63 | return self.send_request("initialize") 64 | 65 | def list_tools(self) -> Dict[str, Any]: 66 | """List available tools.""" 67 | return self.send_request("tools/list") 68 | 69 | def call_tool(self, name: str, arguments: Dict[str, Any] = None) -> Dict[str, Any]: 70 | """Call a tool with the given name and arguments.""" 71 | if arguments is None: 72 | arguments = {} 73 | 74 | params = { 75 | "name": name, 76 | "arguments": arguments 77 | } 78 | return self.send_request("tools/call", params) 79 | 80 | 81 | def main(): 82 | """Run the tests for the new features.""" 83 | logger.info("Starting MCP server...") 84 | server_process = subprocess.Popen( 85 | ["./simple_kubectl_mcp.py"], 86 | stdin=subprocess.PIPE, 87 | stdout=subprocess.PIPE, 88 | stderr=subprocess.PIPE, 89 | text=True, 90 | bufsize=1 91 | ) 92 | 93 | # Give the server time to start 94 | time.sleep(1) 95 | 96 | client = MCP_Client(server_process) 97 | 98 | try: 99 | # Initialize connection 100 | response = client.initialize() 101 | logger.info(f"Initialization result: {response.get('result', {})}") 102 | 103 | # List available tools 104 | response = client.list_tools() 105 | tools = response.get("result", {}).get("tools", []) 106 | tool_names = [tool.get("name") for tool in tools] 107 | logger.info(f"Available tools: {tool_names}") 108 | 109 | # Test each new feature 110 | 111 | # 1. Test set_namespace 112 | logger.info("\n--- Testing set_namespace ---") 113 | response = client.call_tool("set_namespace", {"namespace": "kube-system"}) 114 | logger.info(f"set_namespace result: {response.get('result', {})}") 115 | 116 | # 2. Test resource listing tools 117 | logger.info("\n--- Testing list_pods ---") 118 | response = client.call_tool("list_pods", {"namespace": "kube-system"}) 119 | result = response.get("result", {}) 120 | pod_count = result.get("count", 0) 121 | logger.info(f"Found {pod_count} pods in kube-system namespace") 122 | 123 | logger.info("\n--- Testing list_services ---") 124 | response = client.call_tool("list_services", {"namespace": "kube-system"}) 125 | result = response.get("result", {}) 126 | service_count = result.get("count", 0) 127 | logger.info(f"Found {service_count} services in kube-system namespace") 128 | 129 | logger.info("\n--- Testing list_deployments ---") 130 | response = client.call_tool("list_deployments", {"namespace": "kube-system"}) 131 | result = response.get("result", {}) 132 | deployment_count = result.get("count", 0) 133 | logger.info(f"Found {deployment_count} deployments in kube-system namespace") 134 | 135 | logger.info("\n--- Testing list_nodes ---") 136 | response = client.call_tool("list_nodes") 137 | result = response.get("result", {}) 138 | node_count = result.get("count", 0) 139 | logger.info(f"Found {node_count} nodes in the cluster") 140 | 141 | logger.info("\n--- Testing list_namespaces ---") 142 | response = client.call_tool("list_namespaces") 143 | result = response.get("result", {}) 144 | namespace_count = result.get("count", 0) 145 | logger.info(f"Found {namespace_count} namespaces in the cluster") 146 | 147 | # 3. Test kubectl utilities 148 | logger.info("\n--- Testing explain_resource ---") 149 | response = client.call_tool("explain_resource", {"resource": "pods"}) 150 | result = response.get("result", {}) 151 | explanation = result.get("explanation", "") 152 | logger.info(f"Explanation for 'pods' resource: {explanation[:100]}...") 153 | 154 | logger.info("\n--- Testing list_api_resources ---") 155 | response = client.call_tool("list_api_resources") 156 | result = response.get("result", {}) 157 | api_resource_count = result.get("count", 0) 158 | logger.info(f"Found {api_resource_count} API resources") 159 | 160 | # 4. Test Helm chart operations (skipping actual installation to avoid side effects) 161 | # Instead, just verify that the commands are formed correctly 162 | logger.info("\n--- Testing Helm chart operations ---") 163 | logger.info("Note: Not actually installing charts to avoid side effects") 164 | 165 | # All tests passed 166 | logger.info("\n--- All tests completed successfully ---") 167 | 168 | except Exception as e: 169 | logger.error(f"Test failed: {str(e)}") 170 | finally: 171 | # Clean up 172 | logger.info("Stopping MCP server...") 173 | server_process.terminate() 174 | server_process.wait(timeout=5) 175 | 176 | 177 | if __name__ == "__main__": 178 | main() -------------------------------------------------------------------------------- /python_tests/test_pod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for kubectl_mcp_server.py to test pod-related tools 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import sys 9 | from subprocess import Popen, PIPE 10 | import uuid 11 | 12 | async def test_get_pods(proc): 13 | """Test getting pods from all namespaces.""" 14 | 15 | # Initialize the server first 16 | init_request = { 17 | "jsonrpc": "2.0", 18 | "id": "1", 19 | "method": "initialize", 20 | "params": { 21 | "clientInfo": { 22 | "name": "test-client", 23 | "version": "0.1.0" 24 | }, 25 | "capabilities": {} 26 | } 27 | } 28 | 29 | print("Sending initialization request...") 30 | proc.stdin.write(json.dumps(init_request) + "\n") 31 | proc.stdin.flush() 32 | 33 | # Wait for response 34 | init_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 35 | print(f"Received initialization response: {init_response.strip()}") 36 | 37 | # Test get_pods with all namespaces 38 | call_tool_request = { 39 | "jsonrpc": "2.0", 40 | "id": "2", 41 | "method": "tools/call", 42 | "params": { 43 | "name": "get_pods", 44 | "arguments": { 45 | "namespace": "all" 46 | } 47 | } 48 | } 49 | 50 | print("Sending tools/call request for get_pods (all namespaces)...") 51 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 52 | proc.stdin.flush() 53 | 54 | # Wait for response 55 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 56 | print(f"Received get_pods response: {call_tool_response.strip()}") 57 | 58 | # Parse the response to see pod information 59 | try: 60 | resp = json.loads(call_tool_response) 61 | if "result" in resp and "result" in resp["result"] and len(resp["result"]["result"]) > 0: 62 | pod_info = resp["result"]["result"][0]["text"] 63 | print("\n--- Kubernetes Pods (All Namespaces) ---") 64 | print(pod_info) 65 | print("----------------------------------------\n") 66 | else: 67 | print("No pod information found in the response") 68 | except json.JSONDecodeError: 69 | print("Failed to parse pods response") 70 | 71 | # Test raw kubectl command to create a test deployment 72 | call_tool_request = { 73 | "jsonrpc": "2.0", 74 | "id": "3", 75 | "method": "tools/call", 76 | "params": { 77 | "name": "kubectl", 78 | "arguments": { 79 | "command": "create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080" 80 | } 81 | } 82 | } 83 | 84 | print("Sending tools/call request to create a test deployment...") 85 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 86 | proc.stdin.flush() 87 | 88 | # Wait for response 89 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 90 | print(f"Received deployment response: {call_tool_response.strip()}") 91 | 92 | # Wait for the pod to start (5 seconds) 93 | print("Waiting for deployment to start...") 94 | await asyncio.sleep(5) 95 | 96 | # Test get_pods with default namespace 97 | call_tool_request = { 98 | "jsonrpc": "2.0", 99 | "id": "4", 100 | "method": "tools/call", 101 | "params": { 102 | "name": "get_pods", 103 | "arguments": { 104 | "namespace": "default" 105 | } 106 | } 107 | } 108 | 109 | print("Sending tools/call request for get_pods (default namespace)...") 110 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 111 | proc.stdin.flush() 112 | 113 | # Wait for response 114 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 115 | print(f"Received get_pods response: {call_tool_response.strip()}") 116 | 117 | # Parse the response to see pod information 118 | try: 119 | resp = json.loads(call_tool_response) 120 | if "result" in resp and "result" in resp["result"] and len(resp["result"]["result"]) > 0: 121 | pod_info = resp["result"]["result"][0]["text"] 122 | print("\n--- Kubernetes Pods (Default Namespace) ---") 123 | print(pod_info) 124 | print("----------------------------------------\n") 125 | else: 126 | print("No pod information found in the response") 127 | except json.JSONDecodeError: 128 | print("Failed to parse pods response") 129 | 130 | async def main(): 131 | """Main test function.""" 132 | print("Starting kubectl MCP server pod test...") 133 | 134 | # Start the server process 135 | proc = Popen( 136 | ["python", "kubectl_mcp_server.py"], 137 | stdin=PIPE, 138 | stdout=PIPE, 139 | stderr=sys.stderr, 140 | text=True, 141 | bufsize=1 142 | ) 143 | 144 | try: 145 | await test_get_pods(proc) 146 | finally: 147 | print("Terminating server...") 148 | proc.terminate() 149 | await asyncio.get_event_loop().run_in_executor(None, proc.wait) 150 | print("Test completed.") 151 | 152 | if __name__ == "__main__": 153 | asyncio.run(main()) -------------------------------------------------------------------------------- /python_tests/test_simple_mcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for the simplified kubectl MCP server 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import subprocess 9 | import sys 10 | import time 11 | from typing import Dict, Any, List 12 | 13 | async def simulate_mcp_conversation(): 14 | """Simulate a conversation with the MCP server.""" 15 | print("Starting simplified kubectl MCP server test...") 16 | 17 | # Start the server process 18 | process = subprocess.Popen( 19 | ["python", "simple_kubectl_mcp.py"], 20 | stdin=subprocess.PIPE, 21 | stdout=subprocess.PIPE, 22 | stderr=subprocess.PIPE, 23 | text=True, 24 | bufsize=1, 25 | ) 26 | 27 | # Wait for server to initialize 28 | time.sleep(1) 29 | 30 | try: 31 | # Send initialization request 32 | print("Sending initialization request...") 33 | init_request = { 34 | "jsonrpc": "2.0", 35 | "id": "1", 36 | "method": "initialize", 37 | "params": { 38 | "clientInfo": { 39 | "name": "test-client", 40 | "version": "0.1.0" 41 | }, 42 | "capabilities": {} 43 | } 44 | } 45 | 46 | process.stdin.write(json.dumps(init_request) + "\n") 47 | init_response = json.loads(process.stdout.readline()) 48 | print(f"Received initialization response: {json.dumps(init_response)}") 49 | 50 | # Send tools/list request 51 | print("Sending tools/list request...") 52 | tools_request = { 53 | "jsonrpc": "2.0", 54 | "id": "2", 55 | "method": "tools/list" 56 | } 57 | 58 | process.stdin.write(json.dumps(tools_request) + "\n") 59 | tools_response = json.loads(process.stdout.readline()) 60 | print(f"Received tools response: {json.dumps(tools_response)}") 61 | 62 | # Extract and display tool names 63 | tools = [tool["name"] for tool in tools_response["result"]["tools"]] 64 | print(f"Available tools: {tools}") 65 | 66 | # Test each tool 67 | for tool_name in tools: 68 | print(f"\nTesting tool: {tool_name}") 69 | 70 | # Prepare arguments based on tool name 71 | args = {} 72 | if tool_name == "get_pods": 73 | args = {"namespace": "default"} 74 | elif tool_name == "kubectl": 75 | args = {"command": "get nodes"} 76 | 77 | # Send tool call request 78 | tool_request = { 79 | "jsonrpc": "2.0", 80 | "id": "3", 81 | "method": "tools/call", 82 | "params": { 83 | "name": tool_name, 84 | "arguments": args 85 | } 86 | } 87 | 88 | process.stdin.write(json.dumps(tool_request) + "\n") 89 | tool_response = json.loads(process.stdout.readline()) 90 | print(f"Response for {tool_name}:") 91 | 92 | # Extract and display text result 93 | if "result" in tool_response and "result" in tool_response["result"]: 94 | for item in tool_response["result"]["result"]: 95 | if item["type"] == "text": 96 | print(f"\n{item['text']}") 97 | 98 | # Wait a bit between requests 99 | await asyncio.sleep(0.5) 100 | 101 | print("\nTest completed successfully.") 102 | 103 | finally: 104 | # Clean up 105 | print("Terminating server...") 106 | process.terminate() 107 | process.wait(timeout=5) 108 | 109 | def main(): 110 | """Main entry point.""" 111 | asyncio.run(simulate_mcp_conversation()) 112 | 113 | if __name__ == "__main__": 114 | main() -------------------------------------------------------------------------------- /python_tests/test_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for kubectl_mcp_server.py to test cluster status functionality 4 | """ 5 | 6 | import asyncio 7 | import json 8 | import sys 9 | from subprocess import Popen, PIPE 10 | import uuid 11 | 12 | async def test_cluster_status(proc): 13 | """Test checking the cluster status.""" 14 | 15 | # Initialize the server first 16 | init_request = { 17 | "jsonrpc": "2.0", 18 | "id": "1", 19 | "method": "initialize", 20 | "params": { 21 | "clientInfo": { 22 | "name": "test-client", 23 | "version": "0.1.0" 24 | }, 25 | "capabilities": {} 26 | } 27 | } 28 | 29 | print("Sending initialization request...") 30 | proc.stdin.write(json.dumps(init_request) + "\n") 31 | proc.stdin.flush() 32 | 33 | # Wait for response 34 | init_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 35 | print(f"Received initialization response: {init_response.strip()}") 36 | 37 | # Test check_cluster_status 38 | call_tool_request = { 39 | "jsonrpc": "2.0", 40 | "id": "2", 41 | "method": "tools/call", 42 | "params": { 43 | "name": "check_cluster_status", 44 | "arguments": {} 45 | } 46 | } 47 | 48 | print("Sending tools/call request for check_cluster_status...") 49 | proc.stdin.write(json.dumps(call_tool_request) + "\n") 50 | proc.stdin.flush() 51 | 52 | # Wait for response 53 | call_tool_response = await asyncio.get_event_loop().run_in_executor(None, proc.stdout.readline) 54 | print(f"Received cluster status response: {call_tool_response.strip()}") 55 | 56 | # Parse the response to see cluster status 57 | try: 58 | resp = json.loads(call_tool_response) 59 | if "result" in resp and "result" in resp["result"] and len(resp["result"]["result"]) > 0: 60 | status_info = resp["result"]["result"][0]["text"] 61 | print("\n--- Kubernetes Cluster Status ---") 62 | print(status_info) 63 | print("----------------------------------------\n") 64 | else: 65 | print("No cluster status information found in the response") 66 | except json.JSONDecodeError: 67 | print("Failed to parse cluster status response") 68 | 69 | async def main(): 70 | """Main test function.""" 71 | print("Starting kubectl MCP server cluster status test...") 72 | 73 | # Start the server process 74 | proc = Popen( 75 | ["python", "kubectl_mcp_server.py"], 76 | stdin=PIPE, 77 | stdout=PIPE, 78 | stderr=sys.stderr, 79 | text=True, 80 | bufsize=1 81 | ) 82 | 83 | try: 84 | await test_cluster_status(proc) 85 | finally: 86 | print("Terminating server...") 87 | proc.terminate() 88 | await asyncio.get_event_loop().run_in_executor(None, proc.wait) 89 | print("Test completed.") 90 | 91 | if __name__ == "__main__": 92 | asyncio.run(main()) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # MCP dependency - updating to use the correct version 2 | mcp>=1.5.0 3 | 4 | # Framework dependencies 5 | pydantic>=2.0.0 6 | fastapi>=0.100.0 7 | uvicorn>=0.22.0 8 | 9 | # Kubernetes dependencies 10 | kubernetes>=28.1.0 11 | PyYAML>=6.0.1 12 | requests>=2.31.0 13 | urllib3>=2.1.0 14 | websocket-client>=1.7.0 15 | jsonschema>=4.20.0 16 | cryptography>=42.0.2 17 | 18 | # Terminal formatting 19 | rich>=13.0.0 20 | 21 | # HTTP clients 22 | aiohttp>=3.8.0 23 | aiohttp-sse>=2.1.0 24 | 25 | # Build tools 26 | build>=0.10.0 27 | setuptools>=67.0.0 28 | wheel>=0.41.0 29 | twine>=4.0.0 30 | -------------------------------------------------------------------------------- /run_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Main executable script for running the kubectl MCP server. 4 | This file can be executed directly, avoiding module import issues. 5 | """ 6 | 7 | import asyncio 8 | import argparse 9 | import logging 10 | import sys 11 | import os 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 15 | logger = logging.getLogger("mcp-server-runner") 16 | 17 | # Add the project root to sys.path 18 | project_root = os.path.dirname(os.path.abspath(__file__)) 19 | sys.path.insert(0, project_root) 20 | 21 | # Import after setting up path 22 | from kubectl_mcp_tool.mcp_server import MCPServer 23 | 24 | def main(): 25 | """Run the kubectl MCP server.""" 26 | parser = argparse.ArgumentParser(description="Run the Kubectl MCP Server.") 27 | parser.add_argument( 28 | "--transport", 29 | type=str, 30 | choices=["stdio", "sse"], 31 | default="stdio", 32 | help="Communication transport to use (stdio or sse). Default: stdio.", 33 | ) 34 | parser.add_argument( 35 | "--port", 36 | type=int, 37 | default=8080, 38 | help="Port to use for SSE transport. Default: 8080.", 39 | ) 40 | args = parser.parse_args() 41 | 42 | server_name = "kubernetes" 43 | mcp_server = MCPServer(name=server_name) 44 | 45 | loop = asyncio.get_event_loop() 46 | try: 47 | if args.transport == "stdio": 48 | logger.info(f"Starting {server_name} with stdio transport.") 49 | loop.run_until_complete(mcp_server.serve_stdio()) 50 | elif args.transport == "sse": 51 | logger.info(f"Starting {server_name} with SSE transport on port {args.port}.") 52 | loop.run_until_complete(mcp_server.serve_sse(port=args.port)) 53 | except KeyboardInterrupt: 54 | logger.info("Server shutdown requested by user.") 55 | except Exception as e: 56 | logger.error(f"Server exited with error: {e}", exc_info=True) 57 | finally: 58 | logger.info("Shutting down server.") 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="kubectl-mcp-tool", 8 | version="1.2.0", 9 | author="Rohit Ghumare", 10 | author_email="ghumare64@gmail.com", 11 | description="A Model Context Protocol (MCP) server for Kubernetes", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/rohitg00/kubectl-mcp-server", 15 | packages=find_packages(), 16 | install_requires=[ 17 | "mcp>=1.5.0", 18 | "pydantic>=2.0.0", 19 | "fastapi>=0.100.0", 20 | "uvicorn>=0.22.0", 21 | "kubernetes>=28.1.0", 22 | "PyYAML>=6.0.1", 23 | "requests>=2.31.0", 24 | "urllib3>=2.1.0", 25 | "websocket-client>=1.7.0", 26 | "jsonschema>=4.20.0", 27 | "cryptography>=42.0.2", 28 | "rich>=13.0.0", 29 | "aiohttp>=3.8.0", 30 | "aiohttp-sse>=2.1.0" 31 | ], 32 | entry_points={ 33 | "console_scripts": [ 34 | "kubectl-mcp=kubectl_mcp_tool.__main__:main", 35 | ], 36 | }, 37 | classifiers=[ 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.9", 40 | "Programming Language :: Python :: 3.10", 41 | "License :: OSI Approved :: MIT License", 42 | "Operating System :: OS Independent", 43 | ], 44 | python_requires=">=3.9", 45 | ) --------------------------------------------------------------------------------