├── .dockerignore ├── .github └── workflows │ ├── ci.yml │ ├── integration-tests.yml │ └── release.yml ├── .gitignore ├── CLAUDE.md ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── deploy └── docker │ ├── Dockerfile │ ├── docker-compose.yml │ └── security_config.yaml ├── docs ├── architecture.md ├── claude-integration.md ├── cloud-providers.md ├── environment-variables.md ├── getting-started.md ├── security.md ├── spec.md └── supported-tools.md ├── pyproject.toml ├── src └── k8s_mcp_server │ ├── __init__.py │ ├── __main__.py │ ├── cli_executor.py │ ├── config.py │ ├── errors.py │ ├── prompts.py │ ├── security.py │ ├── server.py │ └── tools.py ├── tests ├── __init__.py ├── conftest.py ├── helpers.py ├── integration │ ├── __init__.py │ ├── conftest.py │ └── test_k8s_tools.py └── unit │ ├── __init__.py │ ├── test_cli_executor.py │ ├── test_errors.py │ ├── test_k8s_tools.py │ ├── test_main.py │ ├── test_prompts.py │ ├── test_security.py │ ├── test_server.py │ ├── test_tool_specific.py │ └── test_tools.py └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version Control 2 | .git/ 3 | .github/ 4 | .gitignore 5 | .gitattributes 6 | 7 | # Docker 8 | .dockerignore 9 | docker-compose*.yml 10 | Dockerfile* 11 | 12 | # Documentation 13 | docs/ 14 | 15 | # Markdown files except README.md 16 | *.md 17 | !README.md 18 | 19 | # Python 20 | __pycache__/ 21 | *.py[cod] 22 | *.$py.class 23 | *.so 24 | .Python 25 | *.egg-info/ 26 | *.egg 27 | .installed.cfg 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | 41 | # Virtual Environments 42 | .envrc 43 | .env 44 | .venv/ 45 | env/ 46 | ENV/ 47 | venv/ 48 | 49 | # Testing and Coverage 50 | .coverage 51 | .pytest_cache/ 52 | .tox/ 53 | .nox/ 54 | htmlcov/ 55 | tests/ 56 | 57 | # Development and IDE 58 | .idea/ 59 | .vscode/ 60 | .ruff_cache/ 61 | .mypy_cache/ 62 | .aider* 63 | *.swp 64 | *.swo 65 | 66 | # OS Generated 67 | .DS_Store 68 | Thumbs.db 69 | 70 | # Logs 71 | logs/ 72 | *.log -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'deploy/**' 7 | - '*.md' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 13 | strategy: 14 | matrix: 15 | python-version: ["3.13"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install uv 26 | run: | 27 | # Install uv using the official installation method 28 | curl -LsSf https://astral.sh/uv/install.sh | sh 29 | 30 | # Add uv to PATH 31 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 32 | 33 | - name: Install dependencies using uv 34 | run: | 35 | # Install dependencies using uv with the lock file and the --system flag 36 | uv pip install --system -e ".[dev]" 37 | 38 | - name: Lint 39 | run: make lint 40 | continue-on-error: true # Display errors but don't fail build for lint warnings 41 | 42 | - name: Test 43 | run: make test 44 | 45 | - name: Upload coverage to Codecov 46 | uses: codecov/codecov-action@v4 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | file: ./coverage.xml 50 | fail_ci_if_error: false 51 | verbose: true -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | # Run on pull requests that aren't just docs/deployment changes 5 | pull_request: 6 | paths-ignore: 7 | - '*.md' 8 | - 'docs/**' 9 | - 'deploy/docker/**' 10 | 11 | # Manual trigger 12 | workflow_dispatch: 13 | inputs: 14 | debug_enabled: 15 | description: 'Run the workflow with tmate debugging enabled' 16 | required: false 17 | default: false 18 | type: boolean 19 | 20 | jobs: 21 | integration-tests: 22 | runs-on: ubuntu-latest 23 | if: "!contains(github.event.head_commit.message, '[skip integration]')" 24 | 25 | strategy: 26 | matrix: 27 | python-version: ["3.13"] 28 | # Don't cancel other matrix jobs if one fails 29 | fail-fast: false 30 | 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Python ${{ matrix.python-version }} 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | 40 | - name: Install uv 41 | run: | 42 | # Install uv using the official installation method 43 | curl -LsSf https://astral.sh/uv/install.sh | sh 44 | 45 | # Add uv to PATH 46 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 47 | 48 | - name: Install dependencies using uv 49 | run: | 50 | # Install dependencies using uv with the lock file and the --system flag 51 | uv pip install --system -e ".[dev]" 52 | 53 | - name: Install KWOK tool 54 | run: | 55 | # KWOK repository 56 | KWOK_REPO=kubernetes-sigs/kwok 57 | # Get latest 58 | KWOK_LATEST_RELEASE=$(curl "https://api.github.com/repos/${KWOK_REPO}/releases/latest" | jq -r '.tag_name') 59 | 60 | wget -O kwokctl -c "https://github.com/${KWOK_REPO}/releases/download/${KWOK_LATEST_RELEASE}/kwokctl-linux-amd64" 61 | chmod +x kwokctl 62 | sudo mv kwokctl /usr/local/bin/kwokctl 63 | 64 | - name: Create KWOK Cluster 65 | run: | 66 | # Create a KWOK cluster for testing with specific Kubernetes version 67 | kwokctl create cluster --name=kwok-test --wait=1m 68 | 69 | - name: Install K8s CLI tools 70 | run: | 71 | # Install Helm 72 | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash 73 | 74 | # Install istioctl (latest version) 75 | ISTIO_VERSION=$(curl -s https://api.github.com/repos/istio/istio/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 76 | curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} sh - 77 | sudo mv istio-${ISTIO_VERSION}/bin/istioctl /usr/local/bin/ 78 | 79 | # Install ArgoCD CLI 80 | curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 81 | sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd 82 | rm argocd-linux-amd64 83 | 84 | # Verify all tools are installed 85 | echo "Verifying installed CLI tools:" 86 | kubectl version --client 87 | helm version 88 | istioctl version --remote=false 89 | argocd version --client 90 | 91 | - name: Setup tmate debug session 92 | uses: mxschmitt/action-tmate@v3 93 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} 94 | with: 95 | limit-access-to-actor: true 96 | 97 | - name: Run integration tests 98 | run: | 99 | # Run integration tests with the KWOK cluster 100 | # Set environment variable to use the cluster we created directly 101 | export K8S_MCP_TEST_USE_KWOK=false # We already created the cluster 102 | export K8S_MCP_TEST_USE_EXISTING_CLUSTER=true 103 | 104 | # Run the tests 105 | pytest -v -m integration 106 | 107 | - name: Cleanup KWOK Cluster 108 | if: always() 109 | run: | 110 | kwokctl delete cluster --name=kwok-test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | tags: 9 | - '[0-9]+.[0-9]+.[0-9]+' 10 | paths-ignore: 11 | - 'tests/**' 12 | - '*.md' 13 | 14 | jobs: 15 | build-and-push: 16 | runs-on: ubuntu-latest 17 | if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" 18 | 19 | permissions: 20 | contents: read 21 | packages: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Set up Python 3.13 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.13" 30 | 31 | - name: Install uv 32 | run: | 33 | # Install uv using the official installation method 34 | curl -LsSf https://astral.sh/uv/install.sh | sh 35 | 36 | # Add uv to PATH 37 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 38 | 39 | - name: Install dependencies and run tests 40 | run: | 41 | # Install dependencies using uv with the lock file and the --system flag 42 | uv pip install --system -e ".[dev]" 43 | # Run linting and tests to verify before release 44 | ruff check . 45 | ruff format --check . 46 | pytest -v -k "not integration" 47 | 48 | - name: Upload coverage to Codecov 49 | uses: codecov/codecov-action@v4 50 | with: 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | file: ./coverage.xml 53 | fail_ci_if_error: false 54 | verbose: true 55 | 56 | - name: Log in to GitHub Container Registry 57 | uses: docker/login-action@v3 58 | with: 59 | registry: ghcr.io 60 | username: ${{ github.actor }} 61 | password: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | - name: Extract metadata for Docker 64 | id: meta 65 | uses: docker/metadata-action@v5 66 | with: 67 | images: ghcr.io/${{ github.repository }} 68 | tags: | 69 | type=semver,pattern={{version}} 70 | type=semver,pattern={{major}}.{{minor}} 71 | type=semver,pattern={{major}} 72 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} 73 | type=sha,format=short,enable=${{ github.ref != format('refs/heads/{0}', 'main') }} 74 | 75 | - name: Set up Docker Buildx 76 | uses: docker/setup-buildx-action@v3 77 | 78 | - name: Build and push multi-architecture Docker image 79 | uses: docker/build-push-action@v6 80 | with: 81 | context: . 82 | file: ./deploy/docker/Dockerfile 83 | push: true 84 | build-args: | 85 | PYTHON_VERSION=3.13-slim 86 | KUBECTL_VERSION=v1.33.0 87 | HELM_VERSION=v3.17.3 88 | ISTIO_VERSION=1.25.2 89 | ARGOCD_VERSION=v2.14.11 90 | AWS_CLI_VERSION=1.32.0 91 | GCLOUD_VERSION=519.0.0 92 | AZURE_CLI_VERSION=2.71.0 93 | platforms: linux/amd64,linux/arm64 94 | tags: ${{ steps.meta.outputs.tags }} 95 | labels: ${{ steps.meta.outputs.labels }} 96 | cache-from: type=gha 97 | cache-to: type=gha,mode=max 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python artifacts 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Logs 24 | logs/ 25 | *.log 26 | 27 | # Unit test / coverage reports 28 | htmlcov/ 29 | .tox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | *.cover 36 | .hypothesis/ 37 | .pytest_cache/ 38 | 39 | # Virtual environments 40 | venv/ 41 | env/ 42 | ENV/ 43 | .venv/ 44 | 45 | # IDE settings 46 | .idea/ 47 | .vscode/ 48 | *.swp 49 | *.swo 50 | .DS_Store 51 | 52 | # Environment variables 53 | .env 54 | .envrc 55 | .aider* 56 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # K8s MCP Server Development Guide 2 | 3 | ## Build & Test Commands 4 | 5 | - Install dependencies: `uv pip install -e .` (or use `make install`) 6 | - Install dev dependencies: `uv pip install -e ".[dev]"` (or use `make dev-install`) 7 | - Run server: `python -m k8s_mcp_server` 8 | - Run unit tests only (default): `pytest` (or use `make test`) 9 | - Run all tests (including integration): `pytest -o addopts=""` (or use `make test-all`) 10 | - Run unit tests explicitly: `pytest -m unit` (or use `make test-unit`) 11 | - Run integration tests only: `pytest -m integration` (or use `make test-integration`) 12 | - Run single test: `pytest tests/path/to/test_file.py::test_function_name -v` 13 | - Run linter: `ruff check src/ tests/` (or use `make lint`) 14 | - Format code: `ruff format src/ tests/` (or use `make format`) 15 | 16 | ## Technical Stack 17 | 18 | - **Python version**: Python 3.13+ 19 | - **Package management**: `uv` for fast, reliable package management 20 | - **Project config**: `pyproject.toml` for configuration and dependency management 21 | - **Environment**: Use virtual environment in `.venv` for dependency isolation 22 | - **Dependencies**: Separate production and dev dependencies in `pyproject.toml` 23 | - **Linting**: `ruff` for style and error checking 24 | - **Type checking**: Use VS Code with Pylance for static type checking 25 | - **Project layout**: Organize code with `src/` layout 26 | 27 | ## Code Style Guidelines 28 | 29 | - **Formatting**: Black-compatible formatting via `ruff format` with 120 char line length 30 | - **Imports**: Sort imports with `ruff` (stdlib, third-party, local) 31 | - **Type hints**: Use native Python type hints (e.g., `list[str]` not `List[str]`) 32 | - **Documentation**: Google-style docstrings for all modules, classes, functions 33 | - **Naming**: snake_case for variables/functions, PascalCase for classes 34 | - **Function length**: Keep functions short (< 30 lines) and single-purpose 35 | - **PEP 8**: Follow PEP 8 style guide (enforced via `ruff`) 36 | 37 | ## Python Best Practices 38 | 39 | - **File handling**: Prefer `pathlib.Path` over `os.path` 40 | - **Debugging**: Use `logging` module instead of `print` 41 | - **Error handling**: Use specific exceptions with context messages and proper logging 42 | - **Data structures**: Use list/dict comprehensions for concise, readable code 43 | - **Function arguments**: Avoid mutable default arguments 44 | - **Data containers**: Leverage `dataclasses` to reduce boilerplate 45 | - **Configuration**: Use environment variables for configuration 46 | - **K8s validation**: Validate all commands before execution (must start with allowed prefixes) 47 | - **Security**: Never store/log credentials, set command timeouts 48 | 49 | ## Development Patterns & Best Practices 50 | 51 | - **Favor simplicity**: Choose the simplest solution that meets requirements 52 | - **DRY principle**: Avoid code duplication; reuse existing functionality 53 | - **Configuration management**: Use environment variables for different environments 54 | - **Focused changes**: Only implement explicitly requested or fully understood changes 55 | - **Preserve patterns**: Follow existing code patterns when fixing bugs 56 | - **File size**: Keep files under 300 lines; refactor when exceeding this limit 57 | - **Test coverage**: Write comprehensive unit and integration tests with `pytest`; include fixtures 58 | - **Modular design**: Create reusable, modular components 59 | - **Logging**: Implement appropriate logging levels (debug, info, error) 60 | - **Error handling**: Implement robust error handling for production reliability 61 | - **Security best practices**: Follow input validation and data protection practices 62 | - **Performance**: Optimize critical code sections when necessary 63 | - **Dependency management**: Add libraries only when essential 64 | 65 | ## Development Workflow 66 | 67 | - **Version control**: Commit frequently with clear messages 68 | - **Impact assessment**: Evaluate how changes affect other codebase areas 69 | - **Documentation**: Keep documentation up-to-date for complex logic and features -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Alexei Ledenev 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install dev-install lint lint-fix format test clean docker-build docker-run docker-compose 2 | 3 | # Python related commands with uv 4 | install: 5 | uv pip install -e . 6 | 7 | dev-install: 8 | uv pip install -e ".[dev]" 9 | 10 | lint: 11 | ruff check . 12 | ruff format --check . 13 | 14 | lint-fix: 15 | ruff check --fix . 16 | ruff format . 17 | 18 | format: 19 | ruff format . 20 | 21 | test: 22 | pytest -v -m 'not integration' 23 | 24 | test-unit: 25 | pytest -v -m unit 26 | 27 | test-integration: 28 | pytest -v -m integration 29 | 30 | test-all: 31 | pytest -v -o addopts="" 32 | 33 | test-coverage: 34 | pytest --cov=k8s_mcp_server --cov-report=term-missing 35 | 36 | clean: 37 | rm -rf build/ dist/ *.egg-info/ .pytest_cache/ .coverage htmlcov/ .ruff_cache/ __pycache__/ 38 | find . -type d -name __pycache__ -exec rm -rf {} + 39 | find . -type d -name '*.egg-info' -exec rm -rf {} + 40 | 41 | # Docker related commands 42 | docker-build: 43 | docker build -t k8s-mcp-server -f deploy/docker/Dockerfile . 44 | 45 | docker-run: 46 | docker run -p 8080:8080 -v ~/.kube:/home/appuser/.kube:ro k8s-mcp-server 47 | 48 | docker-compose: 49 | docker-compose -f deploy/docker/docker-compose.yml up -d 50 | 51 | docker-compose-down: 52 | docker-compose -f deploy/docker/docker-compose.yml down 53 | 54 | # Multi-architecture build (requires Docker Buildx) 55 | docker-buildx: 56 | docker buildx create --name mybuilder --use 57 | docker buildx build --platform linux/amd64,linux/arm64 -t k8s-mcp-server -f deploy/docker/Dockerfile . 58 | 59 | # Help 60 | help: 61 | @echo "Available targets:" 62 | @echo " install - Install the package using uv" 63 | @echo " dev-install - Install the package with development dependencies using uv" 64 | @echo " lint - Run linters (ruff)" 65 | @echo " lint-fix - Run linters with automatic fixes" 66 | @echo " format - Format code with ruff" 67 | @echo " test - Run unit tests only (default)" 68 | @echo " test-all - Run all tests including integration tests" 69 | @echo " test-unit - Run unit tests only" 70 | @echo " test-integration - Run integration tests only (requires K8s)" 71 | @echo " test-coverage - Run tests with coverage report" 72 | @echo " clean - Remove build artifacts" 73 | @echo " docker-build - Build Docker image" 74 | @echo " docker-run - Run server in Docker with kubeconfig mounted" 75 | @echo " docker-compose - Run server using Docker Compose" 76 | @echo " docker-compose-down - Stop Docker Compose services" 77 | @echo " docker-buildx - Build multi-architecture Docker image" 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # K8s MCP Server 2 | 3 | [![CI Status](https://github.com/alexei-led/k8s-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/alexei-led/k8s-mcp-server/actions/workflows/ci.yml) 4 | [![Release Status](https://github.com/alexei-led/k8s-mcp-server/actions/workflows/release.yml/badge.svg)](https://github.com/alexei-led/k8s-mcp-server/actions/workflows/release.yml) 5 | [![codecov](https://codecov.io/gh/alexei-led/k8s-mcp-server/graph/badge.svg?token=eCaXPJ0olS)](https://codecov.io/gh/alexei-led/k8s-mcp-server) 6 | [![Image Tags](https://ghcr-badge.egpl.dev/alexei-led/k8s-mcp-server/tags?color=%2344cc11&ignore=latest&n=4&label=image+tags&trim=)](https://github.com/alexei-led/k8s-mcp-server/pkgs/container/k8s-mcp-server/versions) 7 | [![Image Size](https://ghcr-badge.egpl.dev/alexei-led/k8s-mcp-server/size?color=%2344cc11&tag=latest&label=image+size&trim=)](https://github.com/alexei-led/k8s-mcp-server/pkgs/container/k8s-mcp-server) 8 | [![Python Version](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 10 | 11 | K8s MCP Server is a Docker-based server implementing [Anthropic's Model Context Protocol (MCP)](https://www.anthropic.com/news/introducing-mcp) that enables Claude to run Kubernetes CLI tools (`kubectl`, `istioctl`, `helm`, `argocd`) in a secure, containerized environment. 12 | 13 | ## Demo: Deploy and Troubleshoot WordPress 14 | 15 | **Session 1:** Using k8s-mcp-server and Helm CLI to deploy a WordPress application in the claude-demo namespace, then intentionally breaking it by scaling the MariaDB StatefulSet to zero. 16 | 17 | **Session 2:** Troubleshooting session where we use k8s-mcp-server to diagnose the broken WordPress site through kubectl commands, identify the missing database issue, and fix it by scaling up the StatefulSet and configuring ingress access.. 18 | 19 | [Demo](https://private-user-images.githubusercontent.com/1898375/428398164-5ddce5bc-ec92-459b-a506-5d4442618a81.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDMzNDE0OTEsIm5iZiI6MTc0MzM0MTE5MSwicGF0aCI6Ii8xODk4Mzc1LzQyODM5ODE2NC01ZGRjZTViYy1lYzkyLTQ1OWItYTUwNi01ZDQ0NDI2MThhODEubXA0P1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDMzMCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAzMzBUMTMyNjMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YmUyNDExMGUzOGRlN2QxNWViMzhhOTE4Y2U1ZmRjMTQxYTI0OGNlNTFjNTRlMjFjNmQ3NTNhNGFmODNkODIzMSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.hwKERwuQRXxHEYJ9d_fQ__XL1gj8l76nO6Yy6M4Uov8) 20 | 21 | ## How It Works 22 | 23 | ```mermaid 24 | flowchart LR 25 | A[User] --> |Asks K8s question| B[Claude] 26 | B --> |Sends command via MCP| C[K8s MCP Server] 27 | C --> |Executes kubectl, helm, etc.| D[Kubernetes Cluster] 28 | D --> |Returns results| C 29 | C --> |Returns formatted results| B 30 | B --> |Analyzes & explains| A 31 | ``` 32 | 33 | Claude can help users by: 34 | - Explaining complex Kubernetes concepts 35 | - Running commands against your cluster 36 | - Troubleshooting issues 37 | - Suggesting optimizations 38 | - Crafting Kubernetes manifests 39 | 40 | ## Quick Start with Claude Desktop 41 | 42 | Get Claude helping with your Kubernetes clusters in under 2 minutes: 43 | 44 | 1. **Create or update your Claude Desktop configuration file**: 45 | - **macOS**: Edit `$HOME/Library/Application Support/Claude/claude_desktop_config.json` 46 | - **Windows**: Edit `%APPDATA%\Claude\claude_desktop_config.json` 47 | - **Linux**: Edit `$HOME/.config/Claude/claude_desktop_config.json` 48 | 49 | ```json 50 | { 51 | "mcpServers": { 52 | "kubernetes": { 53 | "command": "docker", 54 | "args": [ 55 | "run", 56 | "-i", 57 | "--rm", 58 | "-v", 59 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 60 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 61 | ] 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | 2. **Restart Claude Desktop** 68 | - After restart, you'll see the Tools icon (🔨) in the bottom right of your input field 69 | - This indicates Claude can now access K8s tools via the MCP server 70 | 71 | 3. **Start using K8s tools directly in Claude Desktop**: 72 | - "What Kubernetes contexts do I have available?" 73 | - "Show me all pods in the default namespace" 74 | - "Create a deployment with 3 replicas of nginx:1.21" 75 | - "Explain what's wrong with my StatefulSet 'database' in namespace 'prod'" 76 | - "Deploy the bitnami/wordpress chart with Helm and set service type to LoadBalancer" 77 | 78 | > **Note**: Claude Desktop will automatically route K8s commands through the MCP server, allowing natural conversation about your clusters without leaving the Claude interface. 79 | 80 | > **Cloud Providers**: For AWS EKS, GKE, or Azure AKS, you'll need additional configuration. See the [Cloud Provider Support](./docs/cloud-providers.md) guide. 81 | 82 | ## Features 83 | 84 | - **Multiple Kubernetes Tools**: `kubectl`, `helm`, `istioctl`, and `argocd` in one container 85 | - **Cloud Providers**: Native support for AWS EKS, Google GKE, and Azure AKS 86 | - **Security**: Runs as non-root user with strict command validation 87 | - **Command Piping**: Support for common Unix tools like `jq`, `grep`, and `sed` 88 | - **Easy Configuration**: Simple environment variables for customization 89 | 90 | ## Documentation 91 | 92 | - [Getting Started Guide](./docs/getting-started.md) - Detailed setup instructions 93 | - [Cloud Provider Support](./docs/cloud-providers.md) - EKS, GKE, and AKS configuration 94 | - [Supported Tools](./docs/supported-tools.md) - Complete list of all included CLI tools 95 | - [Environment Variables](./docs/environment-variables.md) - Configuration options 96 | - [Security Features](./docs/security.md) - Security modes and custom rules 97 | - [Claude Integration](./docs/claude-integration.md) - Detailed Claude Desktop setup 98 | - [Architecture](./docs/architecture.md) - System architecture and components 99 | - [Detailed Specification](./docs/spec.md) - Complete technical specification 100 | 101 | ## Usage Examples 102 | 103 | Once connected, you can ask Claude to help with Kubernetes tasks using natural language: 104 | 105 | ```mermaid 106 | flowchart TB 107 | subgraph "Basic Commands" 108 | A1["Show me all pods in the default namespace"] 109 | A2["Get all services across all namespaces"] 110 | A3["Display the logs for the nginx pod"] 111 | end 112 | 113 | subgraph "Troubleshooting" 114 | B1["Why is my deployment not starting?"] 115 | B2["Describe the failing pod and explain the error"] 116 | B3["Check if my service is properly connected to the pods"] 117 | end 118 | 119 | subgraph "Deployments & Configuration" 120 | C1["Deploy the Nginx Helm chart"] 121 | C2["Create a deployment with 3 replicas of nginx:latest"] 122 | C3["Set up an ingress for my service"] 123 | end 124 | 125 | subgraph "Advanced Operations" 126 | D1["Check the status of my Istio service mesh"] 127 | D2["Set up a canary deployment with 20% traffic to v2"] 128 | D3["Create an ArgoCD application for my repo"] 129 | end 130 | ``` 131 | 132 | Claude can understand your intent and run the appropriate kubectl, helm, istioctl, or argocd commands based on your request. It can then explain the output in simple terms or help you troubleshoot issues. 133 | 134 | ## Advanced Claude Desktop Configuration 135 | 136 | Configure Claude Desktop to optimize your Kubernetes workflow: 137 | 138 | ### Target Specific Clusters and Namespaces 139 | 140 | ```json 141 | { 142 | "mcpServers": { 143 | "kubernetes": { 144 | "command": "docker", 145 | "args": [ 146 | "run", "-i", "--rm", 147 | "-v", "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 148 | "-e", "K8S_CONTEXT=production-cluster", 149 | "-e", "K8S_NAMESPACE=my-application", 150 | "-e", "K8S_MCP_TIMEOUT=600", 151 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 152 | ] 153 | } 154 | } 155 | } 156 | ``` 157 | 158 | ### Connect to AWS EKS Clusters 159 | 160 | ```json 161 | { 162 | "mcpServers": { 163 | "kubernetes": { 164 | "command": "docker", 165 | "args": [ 166 | "run", "-i", "--rm", 167 | "-v", "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 168 | "-v", "/Users/YOUR_USER_NAME/.aws:/home/appuser/.aws:ro", 169 | "-e", "AWS_PROFILE=production", 170 | "-e", "AWS_REGION=us-west-2", 171 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 172 | ] 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | ### Connect to Google GKE Clusters 179 | 180 | ```json 181 | { 182 | "mcpServers": { 183 | "kubernetes": { 184 | "command": "docker", 185 | "args": [ 186 | "run", "-i", "--rm", 187 | "-v", "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 188 | "-v", "/Users/YOUR_USER_NAME/.config/gcloud:/home/appuser/.config/gcloud:ro", 189 | "-e", "CLOUDSDK_CORE_PROJECT=my-gcp-project", 190 | "-e", "CLOUDSDK_COMPUTE_REGION=us-central1", 191 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 192 | ] 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | ### Connect to Azure AKS Clusters 199 | 200 | ```json 201 | { 202 | "mcpServers": { 203 | "kubernetes": { 204 | "command": "docker", 205 | "args": [ 206 | "run", "-i", "--rm", 207 | "-v", "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 208 | "-v", "/Users/YOUR_USER_NAME/.azure:/home/appuser/.azure:ro", 209 | "-e", "AZURE_SUBSCRIPTION=my-subscription-id", 210 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 211 | ] 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | ### Permissive Security Mode 218 | 219 | ```json 220 | { 221 | "mcpServers": { 222 | "kubernetes": { 223 | "command": "docker", 224 | "args": [ 225 | "run", "-i", "--rm", 226 | "-v", "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 227 | "-e", "K8S_MCP_SECURITY_MODE=permissive", 228 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 229 | ] 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | > For detailed security configuration options, see [Security Documentation](./docs/security.md). 236 | 237 | ## License 238 | 239 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: true 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | status: 9 | project: 10 | default: 11 | # basic settings 12 | target: auto 13 | threshold: 1% 14 | base: auto 15 | # advanced settings 16 | branches: null 17 | if_no_uploads: error 18 | if_not_found: success 19 | if_ci_failed: error 20 | only_pulls: false 21 | flags: null 22 | paths: null 23 | patch: 24 | default: 25 | # basic settings 26 | target: auto 27 | threshold: 1% 28 | base: auto 29 | # advanced settings 30 | branches: null 31 | if_no_uploads: error 32 | if_not_found: success 33 | if_ci_failed: error 34 | only_pulls: false 35 | flags: null 36 | paths: null 37 | 38 | parsers: 39 | gcov: 40 | branch_detection: 41 | conditional: yes 42 | loop: yes 43 | method: no 44 | macro: no 45 | 46 | comment: 47 | layout: "reach,diff,flags,files,footer" 48 | behavior: default 49 | require_changes: false 50 | require_base: no 51 | require_head: yes 52 | 53 | ignore: 54 | - "tests/" 55 | - "examples/" 56 | -------------------------------------------------------------------------------- /deploy/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage build with platform-specific configuration 2 | ARG PYTHON_VERSION=3.13-slim 3 | 4 | # =========== BUILDER STAGE =========== 5 | FROM --platform=${TARGETPLATFORM} python:${PYTHON_VERSION} AS builder 6 | 7 | # Install build dependencies 8 | RUN apt-get update && apt-get install -y --no-install-recommends \ 9 | build-essential \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # Set up working directory 14 | WORKDIR /build 15 | 16 | # Copy package definition files 17 | COPY pyproject.toml README.md LICENSE ./ 18 | COPY src/ ./src/ 19 | 20 | # Install package and dependencies with pip wheel 21 | RUN pip install --no-cache-dir wheel && \ 22 | pip wheel --no-cache-dir --wheel-dir=/wheels -e . 23 | 24 | # =========== FINAL STAGE =========== 25 | FROM --platform=${TARGETPLATFORM} python:${PYTHON_VERSION} 26 | 27 | # Set target architecture argument 28 | ARG TARGETPLATFORM 29 | ARG TARGETARCH 30 | 31 | # Step 1: Install system packages - keeping all original packages 32 | RUN apt-get update && apt-get install -y --no-install-recommends \ 33 | unzip \ 34 | curl \ 35 | wget \ 36 | less \ 37 | groff \ 38 | jq \ 39 | gnupg \ 40 | tar \ 41 | gzip \ 42 | zip \ 43 | vim \ 44 | net-tools \ 45 | dnsutils \ 46 | openssh-client \ 47 | grep \ 48 | sed \ 49 | gawk \ 50 | findutils \ 51 | && apt-get clean \ 52 | && rm -rf /var/lib/apt/lists/* 53 | 54 | # Step 2: Install kubectl based on architecture 55 | # Use specific kubectl version (e.g., v1.33.0) 56 | ARG KUBECTL_VERSION=v1.33.0 57 | RUN if [ "${TARGETARCH}" = "arm64" ]; then \ 58 | curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/arm64/kubectl"; \ 59 | else \ 60 | curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"; \ 61 | fi \ 62 | && chmod +x kubectl \ 63 | && mv kubectl /usr/local/bin/ 64 | 65 | # Step 3: Install Helm 66 | # Use specific Helm version 67 | 68 | ARG HELM_VERSION=v3.17.3 69 | RUN if [ "${TARGETARCH}" = "arm64" ]; then \ 70 | curl -LO "https://get.helm.sh/helm-${HELM_VERSION}-linux-arm64.tar.gz" && \ 71 | tar -zxvf helm-${HELM_VERSION}-linux-arm64.tar.gz && \ 72 | mv linux-arm64/helm /usr/local/bin/helm && \ 73 | rm -rf linux-arm64 helm-${HELM_VERSION}-linux-arm64.tar.gz; \ 74 | else \ 75 | curl -LO "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" && \ 76 | tar -zxvf helm-${HELM_VERSION}-linux-amd64.tar.gz && \ 77 | mv linux-amd64/helm /usr/local/bin/helm && \ 78 | rm -rf linux-amd64 helm-${HELM_VERSION}-linux-amd64.tar.gz; \ 79 | fi && chmod +x /usr/local/bin/helm 80 | 81 | # Step 4: Install istioctl 82 | # Use specific Istio version 83 | ARG ISTIO_VERSION=1.25.2 84 | RUN if [ "${TARGETARCH}" = "arm64" ]; then \ 85 | ISTIO_ARCH="arm64"; \ 86 | else \ 87 | ISTIO_ARCH="amd64"; \ 88 | fi \ 89 | && curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} TARGET_ARCH=${ISTIO_ARCH} sh - \ 90 | && mv istio-*/bin/istioctl /usr/local/bin/ \ 91 | && rm -rf istio-* 92 | 93 | # Step 5: Install ArgoCD CLI 94 | # Use specific ArgoCD version 95 | ARG ARGOCD_VERSION=v2.14.11 96 | RUN if [ "${TARGETARCH}" = "arm64" ]; then \ 97 | curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/download/${ARGOCD_VERSION}/argocd-linux-arm64; \ 98 | else \ 99 | curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/download/${ARGOCD_VERSION}/argocd-linux-amd64; \ 100 | fi \ 101 | && chmod +x argocd \ 102 | && mv argocd /usr/local/bin/ 103 | 104 | # Step 6: Install AWS CLI for EKS authentication 105 | ARG AWS_CLI_VERSION=1.32.0 106 | RUN pip install --no-cache-dir awscli==${AWS_CLI_VERSION} && \ 107 | aws --version 108 | 109 | # Step 7: Install minimal Google Cloud SDK for GKE authentication 110 | ARG GCLOUD_VERSION=519.0.0 111 | RUN if [ "${TARGETARCH}" = "arm64" ]; then \ 112 | curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-arm.tar.gz && \ 113 | tar -xzf google-cloud-cli-linux-arm.tar.gz -C /opt && \ 114 | rm google-cloud-cli-linux-arm.tar.gz; \ 115 | else \ 116 | curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz && \ 117 | tar -xzf google-cloud-cli-linux-x86_64.tar.gz -C /opt && \ 118 | rm google-cloud-cli-linux-x86_64.tar.gz; \ 119 | fi && \ 120 | /opt/google-cloud-sdk/install.sh --quiet --usage-reporting=false --path-update=false \ 121 | --additional-components gke-gcloud-auth-plugin && \ 122 | ln -s /opt/google-cloud-sdk/bin/gcloud /usr/local/bin/gcloud && \ 123 | ln -s /opt/google-cloud-sdk/bin/gke-gcloud-auth-plugin /usr/local/bin/gke-gcloud-auth-plugin && \ 124 | # Set up GKE authentication plugin 125 | echo "export USE_GKE_GCLOUD_AUTH_PLUGIN=True" >> /etc/profile.d/gke_auth.sh && \ 126 | gcloud --version 127 | 128 | # Step 8: Install Azure CLI for AKS authentication 129 | ARG AZURE_CLI_VERSION=2.71.0 130 | RUN pip install --no-cache-dir azure-cli==${AZURE_CLI_VERSION} && \ 131 | az --version 132 | 133 | # Set up application directory, user, and permissions 134 | RUN mkdir -p /app/logs && chmod 750 /app/logs \ 135 | && groupadd -g 10001 appgroup \ 136 | && useradd -m -s /bin/bash -u 10001 -g appgroup appuser \ 137 | && mkdir -p /home/appuser/.kube \ 138 | && mkdir -p /home/appuser/.aws \ 139 | && mkdir -p /home/appuser/.config/gcloud \ 140 | && mkdir -p /home/appuser/.azure \ 141 | && chmod 700 /home/appuser/.kube \ 142 | && chmod 700 /home/appuser/.aws \ 143 | && chmod 700 /home/appuser/.config/gcloud \ 144 | && chmod 700 /home/appuser/.azure 145 | 146 | WORKDIR /app 147 | 148 | # Copy application code 149 | COPY pyproject.toml README.md LICENSE ./ 150 | COPY src/ ./src/ 151 | COPY deploy/docker/security_config.yaml ./security_config.yaml 152 | 153 | # Copy wheels from builder and install 154 | COPY --from=builder /wheels /wheels 155 | RUN pip install --no-cache-dir --no-index --find-links=/wheels k8s-mcp-server && \ 156 | rm -rf /wheels 157 | 158 | # Set ownership after all files have been copied 159 | RUN chown -R appuser:appgroup /app \ 160 | && chown -R appuser:appgroup /home/appuser \ 161 | && chmod -R o-rwx /app /home/appuser 162 | 163 | # Switch to non-root user 164 | USER appuser 165 | 166 | # Set all environment variables in one layer 167 | ENV HOME="/home/appuser" \ 168 | PATH="/usr/local/bin:${PATH}" \ 169 | PYTHONUNBUFFERED=1 \ 170 | K8S_MCP_TRANSPORT=stdio \ 171 | K8S_MCP_SECURITY_MODE=strict \ 172 | K8S_MCP_SECURITY_CONFIG=/app/security_config.yaml \ 173 | USE_GKE_GCLOUD_AUTH_PLUGIN=True 174 | 175 | # Add metadata following OCI Image Specification 176 | LABEL maintainer="Alexei Ledenev" \ 177 | description="Kubernetes Multi-Command Proxy Server" \ 178 | org.opencontainers.image.title="K8s MCP Server" \ 179 | org.opencontainers.image.description="Kubernetes Multi-Command Proxy Server for Anthropic's MCP" \ 180 | org.opencontainers.image.authors="Alexei Ledenev" \ 181 | org.opencontainers.image.vendor="Alexei Ledenev" \ 182 | org.opencontainers.image.licenses="MIT" \ 183 | org.opencontainers.image.source="https://github.com/alexei-led/k8s-mcp-server" \ 184 | org.opencontainers.image.documentation="https://github.com/alexei-led/k8s-mcp-server/README.md" \ 185 | org.opencontainers.image.version="1.3.0" \ 186 | org.opencontainers.image.created="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" 187 | 188 | # Expose the service port (only needed if using SSE transport) 189 | EXPOSE 8080 190 | 191 | # Set command to run the server with proper signal handling 192 | # The ENTRYPOINT format ensures proper signal forwarding to the Python process 193 | ENTRYPOINT ["python", "-m", "k8s_mcp_server"] 194 | -------------------------------------------------------------------------------- /deploy/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | k8s-mcp-server: 3 | # Use either local build or official image from GitHub Packages 4 | build: 5 | context: ../../ 6 | dockerfile: ./deploy/docker/Dockerfile 7 | # Alternatively, use the pre-built multi-arch image 8 | # image: ghcr.io/yourusername/k8s-mcp-server:latest 9 | # Uncomment the ports section only if using SSE transport 10 | # ports: 11 | # - "8080:8080" 12 | volumes: 13 | - ~/.kube:/home/appuser/.kube:ro # Mount Kubernetes configs as read-only 14 | # Uncomment to use custom security config 15 | # - ./security_config.yaml:/security_config.yaml:ro 16 | # Uncomment for AWS EKS authentication 17 | # - ~/.aws:/home/appuser/.aws:ro # Mount AWS credentials as read-only 18 | # Uncomment for GCP GKE authentication 19 | # - ~/.config/gcloud:/home/appuser/.config/gcloud:ro # Mount GCP credentials as read-only 20 | # Uncomment for Azure AKS authentication 21 | # - ~/.azure:/home/appuser/.azure:ro # Mount Azure credentials as read-only 22 | environment: 23 | # Kubernetes settings 24 | - K8S_CONTEXT= # Leave empty to use current context or specify a context 25 | - K8S_NAMESPACE=default # Default namespace for commands 26 | 27 | # Security settings 28 | # - K8S_MCP_SECURITY_MODE=strict # strict (default) or permissive 29 | # - K8S_MCP_SECURITY_CONFIG=/security_config.yaml 30 | 31 | # Server settings 32 | - K8S_MCP_TIMEOUT=300 # Default timeout in seconds 33 | # - K8S_MCP_MAX_OUTPUT=100000 # Uncomment to set max output size 34 | - K8S_MCP_TRANSPORT=stdio # Transport protocol (stdio or sse) 35 | 36 | # AWS EKS settings (uncomment to use) 37 | # - AWS_PROFILE=default # AWS profile to use 38 | # - AWS_REGION=us-west-2 # AWS region for EKS cluster 39 | 40 | # GCP GKE settings (uncomment to use) 41 | # - CLOUDSDK_CORE_PROJECT=my-project # GCP project ID 42 | # - CLOUDSDK_COMPUTE_REGION=us-central1 # GCP region 43 | # - CLOUDSDK_COMPUTE_ZONE=us-central1-a # GCP zone 44 | # - USE_GKE_GCLOUD_AUTH_PLUGIN=True # Enable GKE auth plugin 45 | 46 | # Azure AKS settings (uncomment to use) 47 | # - AZURE_SUBSCRIPTION=my-subscription # Azure subscription 48 | # - AZURE_DEFAULTS_LOCATION=eastus # Azure region 49 | restart: unless-stopped 50 | # To build multi-architecture images: 51 | # 1. Set up Docker buildx: docker buildx create --name mybuilder --use 52 | # 2. Build and push the multi-arch image: 53 | # docker buildx build --platform linux/amd64,linux/arm64 -t yourrepo/k8s-mcp-server:latest --push . 54 | -------------------------------------------------------------------------------- /deploy/docker/security_config.yaml: -------------------------------------------------------------------------------- 1 | # K8s MCP Server Security Configuration 2 | # This is a template for customizing security rules. 3 | # To use this file, uncomment the relevant sections in docker-compose.yml. 4 | 5 | # Potentially dangerous command patterns (prefix-based) 6 | dangerous_commands: 7 | kubectl: 8 | - "kubectl delete" 9 | - "kubectl drain" 10 | - "kubectl replace --force" 11 | - "kubectl exec" 12 | - "kubectl port-forward" 13 | - "kubectl cp" 14 | - "kubectl cordon" 15 | - "kubectl uncordon" 16 | - "kubectl taint" 17 | istioctl: 18 | - "istioctl experimental" 19 | - "istioctl proxy-config" 20 | - "istioctl dashboard" 21 | helm: 22 | - "helm delete" 23 | - "helm uninstall" 24 | - "helm rollback" 25 | - "helm upgrade" 26 | argocd: 27 | - "argocd app delete" 28 | - "argocd cluster rm" 29 | - "argocd repo rm" 30 | - "argocd app set" 31 | 32 | # Safe pattern overrides (prefix-based) 33 | safe_patterns: 34 | kubectl: 35 | - "kubectl delete pod" 36 | - "kubectl delete deployment" 37 | - "kubectl delete service" 38 | - "kubectl delete configmap" 39 | - "kubectl delete secret" 40 | - "kubectl exec --help" 41 | - "kubectl exec -it" 42 | - "kubectl exec pod" 43 | - "kubectl exec deployment" 44 | - "kubectl port-forward --help" 45 | - "kubectl cp --help" 46 | istioctl: 47 | - "istioctl experimental -h" 48 | - "istioctl experimental --help" 49 | - "istioctl proxy-config --help" 50 | - "istioctl dashboard --help" 51 | helm: 52 | - "helm delete --help" 53 | - "helm uninstall --help" 54 | - "helm rollback --help" 55 | - "helm upgrade --help" 56 | argocd: 57 | - "argocd app delete --help" 58 | - "argocd cluster rm --help" 59 | - "argocd repo rm --help" 60 | - "argocd app set --help" 61 | 62 | # Advanced regex pattern rules for more complex validations 63 | regex_rules: 64 | kubectl: 65 | # Prevent deleting all resources at once 66 | - pattern: "kubectl\\s+delete\\s+(-[A-Za-z]+\\s+)*--all\\b" 67 | description: "Deleting all resources of a type" 68 | error_message: "Deleting all resources is restricted. Specify individual resources to delete." 69 | 70 | # Prevent operations across all namespaces 71 | - pattern: "kubectl\\s+delete\\s+(-[A-Za-z]+\\s+)*--all-namespaces\\b" 72 | description: "Deleting resources in all namespaces" 73 | error_message: "Deleting resources across all namespaces is restricted." 74 | 75 | # Restrict dangerous file operations via kubectl exec 76 | - pattern: "kubectl\\s+exec\\s+.*\\s+-[^-]*c\\s+.*(rm|mv|cp|curl|wget|chmod)\\b" 77 | description: "Dangerous file operations in exec" 78 | error_message: "File system operations within kubectl exec are restricted." 79 | 80 | # Restrict operations in kube-system namespace 81 | - pattern: "kubectl\\s+.*\\s+--namespace=kube-system\\b" 82 | description: "Operations in kube-system namespace" 83 | error_message: "Operations in kube-system namespace are restricted." 84 | 85 | helm: 86 | # Prevent uninstalling with skipped hooks 87 | - pattern: "helm\\s+uninstall\\s+.*\\s+--no-hooks\\b" 88 | description: "Helm uninstall without hooks" 89 | error_message: "Using --no-hooks with helm uninstall is restricted as it may skip important cleanup." -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Kubernetes MCP Server Architecture 2 | 3 | This document describes the architecture, components, and security model of the K8s MCP Server. 4 | 5 | ## System Overview 6 | 7 | The K8s MCP Server enables AI assistants like Claude to execute Kubernetes CLI tools through a secure, containerized environment: 8 | 9 | ```mermaid 10 | flowchart LR 11 | A[Claude Desktop] <--> |MCP Protocol| B[K8s MCP Server] 12 | B <--> |CLI Commands| C[Kubernetes Cluster] 13 | D[User] <--> |Questions & Commands| A 14 | ``` 15 | 16 | The server acts as a bridge between AI assistants and Kubernetes infrastructure, allowing users to manage their clusters through natural language interactions. 17 | 18 | ## Component Architecture 19 | 20 | The Kubernetes MCP Server consists of the following key components: 21 | 22 | ```mermaid 23 | flowchart TD 24 | A[MCP Client\nAI Assistant] <--> |HTTP/WebSocket| B[FastMCP Server] 25 | B --> C[Tool Commands\n& Validation] 26 | C --> D[CLI Executor] 27 | D --> E[Kubernetes CLI Tools] 28 | E --> F[Kubernetes Cluster] 29 | ``` 30 | 31 | ### Data Flow 32 | 33 | 1. The MCP Client (Claude) sends a request to the FastMCP Server 34 | 2. The FastMCP Server validates the request and routes it to the appropriate tool handler 35 | 3. The Tool Commands component validates the command against security policies 36 | 4. The CLI Executor runs the command securely and captures outputs 37 | 5. The Kubernetes CLI tools communicate with the Kubernetes cluster 38 | 6. Results flow back through the components to the MCP Client 39 | 40 | ## Component Responsibilities 41 | 42 | ### FastMCP Server 43 | - Implements MCP protocol endpoints 44 | - Handles tool requests and responses 45 | - Manages client connections 46 | - Registers prompt templates 47 | - Handles MCP resources 48 | 49 | ### Tool Commands & Validation 50 | - Processes documentation requests 51 | - Processes execution requests 52 | - Validates commands against security policies 53 | - Validates parameters and formats responses 54 | - Handles tool-specific logic (kubectl, helm, istioctl, argocd) 55 | 56 | ### CLI Executor 57 | - Executes CLI commands securely 58 | - Captures standard output and error streams 59 | - Handles timeouts 60 | - Injects context and namespace when appropriate 61 | - Manages subprocess lifecycle 62 | 63 | ## Security Model 64 | 65 | Security principles for the Kubernetes MCP Server include: 66 | 67 | ### Command Validation 68 | - Allowlist-based approach for permitted commands 69 | - Validation of all command inputs against injection attacks 70 | - Pipe chain validation for authorized utilities only 71 | - Specific validation for potentially dangerous commands like `kubectl exec` 72 | 73 | ```mermaid 74 | flowchart LR 75 | A[Command Input] --> B{Security\nFilter} 76 | B -->|Allowed| C[Execute] 77 | B -->|Blocked| D[Reject] 78 | 79 | subgraph Security Filter 80 | E[Allowlist Check] 81 | F[Injection Analysis] 82 | G[Command Pattern] 83 | H[Parameter Validation] 84 | end 85 | ``` 86 | 87 | ### Execution Security 88 | - Execution timeouts to prevent resource exhaustion 89 | - Proper handling of command errors and timeouts 90 | - Secure subprocess execution 91 | - Non-root container user 92 | 93 | ### Authentication Security 94 | - Basic detection of authentication errors 95 | - Appropriate error messages for authentication issues 96 | - No storage of sensitive credentials 97 | - Read-only mount of credentials 98 | 99 | ## Error Handling Framework 100 | 101 | A consistent error handling approach ensures clear communication: 102 | 103 | ### Error Categories 104 | - Command validation errors 105 | - Authentication errors 106 | - Execution errors 107 | - Timeout errors 108 | - Internal system errors 109 | 110 | ### Standard Error Format 111 | ```typescript 112 | type ErrorDetailsNested = { 113 | command?: string; 114 | exit_code?: number; 115 | stderr?: string; 116 | }; 117 | 118 | type ErrorDetails = { 119 | message: string; 120 | code: string; 121 | details?: ErrorDetailsNested; 122 | }; 123 | 124 | type CommandResult = { 125 | status: "success" | "error"; 126 | output: string; 127 | exit_code?: number; 128 | execution_time?: number; 129 | error?: ErrorDetails; 130 | }; 131 | ``` 132 | 133 | ### Common Error Messages 134 | - Invalid tool: "Tool not found. Available tools: kubectl, helm, istioctl, argocd." 135 | - Restricted command: "Command is restricted for security reasons." 136 | - Context errors: "Context not found in kubeconfig. Available contexts: [list]." 137 | - Timeout errors: "Command timed out after N seconds." 138 | 139 | ## Resource Management 140 | 141 | The server implements the MCP resources pattern to provide persistent context for interactions: 142 | 143 | ```mermaid 144 | flowchart TD 145 | A[Cluster Context\nk8s-context://] --> B[Namespace\nk8s-namespace://] 146 | B --> C[Deployment\nk8s-deployment://] 147 | B --> D[Service\nk8s-service://] 148 | B --> E[ConfigMap\nk8s-configmap://] 149 | B --> F[Helm Release\nhelm-release://] 150 | B --> G[Istio Virtual Service\nistio-virtualservice://] 151 | ``` 152 | 153 | Resources provide: 154 | - Hierarchical context structure 155 | - Persistent state across requests 156 | - Dynamic updates and subscriptions 157 | - Streamlined command operations 158 | 159 | ## Integration Patterns 160 | 161 | The K8s MCP Server supports multiple integration patterns: 162 | 163 | ### Local Integration 164 | ```mermaid 165 | flowchart LR 166 | A[User's Machine] --> |Claude Desktop| B[Local K8s MCP Server] 167 | B --> |Local Kubeconfig| C[Kubernetes Clusters] 168 | ``` 169 | 170 | ### Remote Integration 171 | ```mermaid 172 | flowchart LR 173 | A[User] --> |Claude Desktop| B[K8s MCP Server\nCloud Deployment] 174 | B --> |Managed Kubeconfig| C[Kubernetes Clusters] 175 | ``` 176 | 177 | ### Enterprise Integration 178 | ```mermaid 179 | flowchart LR 180 | A[Multiple Users] --> |Claude Desktop| B[Centralized K8s MCP Server] 181 | B --> |Identity-aware Access| C[Multiple Clusters] 182 | ``` 183 | 184 | ## Configuration Principles 185 | 186 | Configuration for the Kubernetes MCP Server follows these principles: 187 | 188 | ### Core Configuration Areas 189 | - Server settings (host, port, logging) 190 | - Tool settings (paths, allowed commands) 191 | - Security settings (restrictions, allowed pipes) 192 | - Timeout settings (default and maximum) 193 | - Context and namespace settings 194 | 195 | ### Configuration Layering 196 | - Default sensible configurations built-in 197 | - Configuration overrides through environment variables 198 | - Environment-specific settings 199 | 200 | ## Deployment Architecture 201 | 202 | The K8s MCP Server is designed to be deployed as a Docker container, allowing for: 203 | 204 | ### Local Deployment 205 | - Run alongside Claude Desktop on the user's machine 206 | - Access to local Kubernetes configurations 207 | - Integration with local cloud provider credentials 208 | 209 | ### Remote Deployment 210 | - Run on a server or in a cloud environment 211 | - Accessible to Claude via network 212 | - Centralized management of configurations and security policies 213 | 214 | ## Performance Considerations 215 | 216 | The server is designed with these performance characteristics: 217 | 218 | - Lightweight container design 219 | - Minimal resource requirements 220 | - Fast command execution 221 | - Efficient handling of command outputs 222 | - Support for timeout configurations to prevent resource exhaustion -------------------------------------------------------------------------------- /docs/claude-integration.md: -------------------------------------------------------------------------------- 1 | # Integrating with Claude Desktop 2 | 3 | This guide details how to integrate K8s MCP Server with Claude Desktop. 4 | 5 | ## Setting Up Claude Desktop 6 | 7 | 1. **Locate the Claude Desktop configuration file**: 8 | - macOS: `/Users/YOUR_USER_NAME/Library/Application Support/Claude/claude_desktop_config.json` 9 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 10 | 11 | 2. **Edit the configuration file** to include the K8s MCP Server: 12 | ```json 13 | { 14 | "mcpServers": { 15 | "kubernetes": { 16 | "command": "docker", 17 | "args": [ 18 | "run", 19 | "-i", 20 | "--rm", 21 | "-v", 22 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 23 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 24 | ] 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | > **Note**: Make sure to replace `/Users/YOUR_USER_NAME/.kube` with the absolute path to your Kubernetes configuration directory, and update the image name if using a custom image. 31 | 32 | 3. **Restart Claude Desktop** to apply the changes 33 | - After restarting, you should see a hammer 🔨 icon in the bottom right corner of the input box 34 | - This indicates that the K8s MCP Server is available for use 35 | 36 | ## Common Configuration Examples 37 | 38 | ### Basic Configuration with Specific Namespace 39 | 40 | ```json 41 | { 42 | "mcpServers": { 43 | "kubernetes": { 44 | "command": "docker", 45 | "args": [ 46 | "run", 47 | "-i", 48 | "--rm", 49 | "-v", 50 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 51 | "-e", 52 | "K8S_CONTEXT=my-cluster", 53 | "-e", 54 | "K8S_NAMESPACE=my-namespace", 55 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 56 | ] 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ### Permissive Mode Configuration 63 | 64 | To run in permissive mode (allow all commands, including potentially dangerous ones): 65 | 66 | ```json 67 | { 68 | "mcpServers": { 69 | "kubernetes": { 70 | "command": "docker", 71 | "args": [ 72 | "run", 73 | "-i", 74 | "--rm", 75 | "-v", 76 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 77 | "-e", 78 | "K8S_MCP_SECURITY_MODE=permissive", 79 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 80 | ] 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ### Custom Security Configuration 87 | 88 | To use a custom security configuration file: 89 | 90 | ```json 91 | { 92 | "mcpServers": { 93 | "kubernetes": { 94 | "command": "docker", 95 | "args": [ 96 | "run", 97 | "-i", 98 | "--rm", 99 | "-v", 100 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 101 | "-v", 102 | "/path/to/my-security-config.yaml:/app/security_config.yaml:ro", 103 | "-e", 104 | "K8S_MCP_SECURITY_CONFIG=/app/security_config.yaml", 105 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 106 | ] 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ## Using Kubernetes Tools in Claude 113 | 114 | Once configured, you can ask Claude to perform Kubernetes operations: 115 | 116 | - "Show me the pods in my default namespace using kubectl" 117 | - "Help me deploy a new application with Helm" 118 | - "Check the status of my Istio service mesh" 119 | - "List all my Kubernetes deployments" 120 | 121 | Claude will automatically use the appropriate Kubernetes CLI tools via the K8s MCP Server. 122 | 123 | ## Troubleshooting 124 | 125 | 1. **Missing Tools Icon**: If you don't see the hammer icon, check that: 126 | - Claude Desktop is properly restarted 127 | - The configuration file is correctly formatted 128 | - The specified paths are accessible 129 | 130 | 2. **Permission Issues**: Make sure that: 131 | - Your `.kube` directory has correct permissions (600 for the files) 132 | - The Docker container has read access to your configuration files 133 | 134 | 3. **Command Execution Fails**: Verify that: 135 | - Your Kubernetes context is valid and accessible 136 | - The command isn't being blocked by security rules (consider permissive mode for testing) 137 | - The timeout is sufficient for the command to complete -------------------------------------------------------------------------------- /docs/cloud-providers.md: -------------------------------------------------------------------------------- 1 | # Cloud Provider Configuration 2 | 3 | K8s MCP Server provides support for major cloud-managed Kubernetes services (EKS, GKE, AKS). This document details how to configure each provider. 4 | 5 | ## Amazon EKS 6 | 7 | K8s MCP Server includes AWS CLI for seamless integration with Amazon Elastic Kubernetes Service (EKS). 8 | 9 | ### Prerequisites 10 | 11 | 1. AWS CLI credentials configured in your `~/.aws` directory 12 | 2. Access to an EKS cluster 13 | 3. `kubectl` configured to access your EKS cluster 14 | 15 | ### Configuration 16 | 17 | 1. **Mount AWS credentials** in your Claude Desktop configuration: 18 | ```json 19 | { 20 | "mcpServers": { 21 | "k8s-mcp-server": { 22 | "command": "docker", 23 | "args": [ 24 | "run", 25 | "-i", 26 | "--rm", 27 | "-v", 28 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 29 | "-v", 30 | "/Users/YOUR_USER_NAME/.aws:/home/appuser/.aws:ro", 31 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 32 | ] 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | 2. **Set AWS environment variables** (optional): 39 | ```json 40 | { 41 | "mcpServers": { 42 | "k8s-mcp-server": { 43 | "command": "docker", 44 | "args": [ 45 | "run", 46 | "-i", 47 | "--rm", 48 | "-v", 49 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 50 | "-v", 51 | "/Users/YOUR_USER_NAME/.aws:/home/appuser/.aws:ro", 52 | "-e", 53 | "AWS_PROFILE=my-profile", 54 | "-e", 55 | "AWS_REGION=us-west-2", 56 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 57 | ] 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | 3. **Update kubeconfig** for EKS cluster access (if not already done): 64 | ```bash 65 | aws eks update-kubeconfig --name my-cluster --region us-west-2 66 | ``` 67 | 68 | ### Troubleshooting EKS 69 | 70 | 1. **Authentication issues**: 71 | - Ensure AWS credentials are properly mounted 72 | - Check that your AWS profile has sufficient permissions 73 | - Verify your EKS cluster exists in the specified region 74 | 75 | 2. **Connection issues**: 76 | - Check that your kubeconfig is correctly configured for EKS 77 | - Ensure your network can reach the EKS API server 78 | 79 | ## Google GKE 80 | 81 | K8s MCP Server includes Google Cloud SDK and GKE auth plugin for working with Google Kubernetes Engine. 82 | 83 | ### Prerequisites 84 | 85 | 1. gcloud CLI credentials configured in your `~/.config/gcloud` directory 86 | 2. Access to a GKE cluster 87 | 3. `kubectl` configured to access your GKE cluster 88 | 89 | ### Configuration 90 | 91 | 1. **Mount GCP credentials** in your Claude Desktop configuration: 92 | ```json 93 | { 94 | "mcpServers": { 95 | "k8s-mcp-server": { 96 | "command": "docker", 97 | "args": [ 98 | "run", 99 | "-i", 100 | "--rm", 101 | "-v", 102 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 103 | "-v", 104 | "/Users/YOUR_USER_NAME/.config/gcloud:/home/appuser/.config/gcloud:ro", 105 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 106 | ] 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | 2. **Set GCP environment variables** (optional): 113 | ```json 114 | { 115 | "mcpServers": { 116 | "k8s-mcp-server": { 117 | "command": "docker", 118 | "args": [ 119 | "run", 120 | "-i", 121 | "--rm", 122 | "-v", 123 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 124 | "-v", 125 | "/Users/YOUR_USER_NAME/.config/gcloud:/home/appuser/.config/gcloud:ro", 126 | "-e", 127 | "CLOUDSDK_CORE_PROJECT=my-gcp-project", 128 | "-e", 129 | "CLOUDSDK_COMPUTE_REGION=us-central1", 130 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 131 | ] 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | 3. **Update kubeconfig** for GKE cluster access (if not already done): 138 | ```bash 139 | gcloud container clusters get-credentials my-cluster --region=us-central1 140 | ``` 141 | 142 | ### Troubleshooting GKE 143 | 144 | 1. **Authentication issues**: 145 | - Ensure GCP credentials are properly mounted 146 | - Check that your GCP service account or user has sufficient permissions 147 | - Verify your GKE cluster exists in the specified project and region 148 | 149 | 2. **Connection issues**: 150 | - Check that your kubeconfig is correctly configured for GKE 151 | - Ensure your network can reach the GKE API server 152 | 153 | ## Microsoft AKS 154 | 155 | K8s MCP Server includes Azure CLI for working with Azure Kubernetes Service (AKS). 156 | 157 | ### Prerequisites 158 | 159 | 1. Azure CLI credentials configured in your `~/.azure` directory 160 | 2. Access to an AKS cluster 161 | 3. `kubectl` configured to access your AKS cluster 162 | 163 | ### Configuration 164 | 165 | 1. **Mount Azure credentials** in your Claude Desktop configuration: 166 | ```json 167 | { 168 | "mcpServers": { 169 | "k8s-mcp-server": { 170 | "command": "docker", 171 | "args": [ 172 | "run", 173 | "-i", 174 | "--rm", 175 | "-v", 176 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 177 | "-v", 178 | "/Users/YOUR_USER_NAME/.azure:/home/appuser/.azure:ro", 179 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 180 | ] 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | 2. **Set Azure environment variables** (optional): 187 | ```json 188 | { 189 | "mcpServers": { 190 | "k8s-mcp-server": { 191 | "command": "docker", 192 | "args": [ 193 | "run", 194 | "-i", 195 | "--rm", 196 | "-v", 197 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 198 | "-v", 199 | "/Users/YOUR_USER_NAME/.azure:/home/appuser/.azure:ro", 200 | "-e", 201 | "AZURE_SUBSCRIPTION=my-subscription-id", 202 | "-e", 203 | "AZURE_DEFAULTS_LOCATION=eastus", 204 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 205 | ] 206 | } 207 | } 208 | } 209 | ``` 210 | 211 | 3. **Update kubeconfig** for AKS cluster access (if not already done): 212 | ```bash 213 | az aks get-credentials --resource-group myResourceGroup --name myAKSCluster 214 | ``` 215 | 216 | ### Troubleshooting AKS 217 | 218 | 1. **Authentication issues**: 219 | - Ensure Azure credentials are properly mounted 220 | - Check that your Azure account has sufficient permissions 221 | - Verify your AKS cluster exists in the specified resource group 222 | 223 | 2. **Connection issues**: 224 | - Check that your kubeconfig is correctly configured for AKS 225 | - Ensure your network can reach the AKS API server -------------------------------------------------------------------------------- /docs/environment-variables.md: -------------------------------------------------------------------------------- 1 | # K8s MCP Server Environment Variables 2 | 3 | This document details all environment variables that can be configured when running the K8s MCP Server container. 4 | 5 | ## Core Server Configuration 6 | 7 | | Variable | Description | Default | Required | 8 | |----------|-------------|---------|----------| 9 | | `K8S_MCP_TIMEOUT` | Default timeout for commands in seconds | `300` | No | 10 | | `K8S_MCP_MAX_OUTPUT` | Maximum output size in characters | `100000` | No | 11 | | `K8S_MCP_TRANSPORT` | Transport protocol to use ("stdio" or "sse") | `stdio` | No | 12 | | `K8S_CONTEXT` | Kubernetes context to use | *current context* | No | 13 | | `K8S_NAMESPACE` | Default Kubernetes namespace | `default` | No | 14 | | `K8S_MCP_SECURITY_MODE` | Security mode ("strict" or "permissive") | `strict` | No | 15 | | `K8S_MCP_SECURITY_CONFIG` | Path to security configuration YAML file | `/app/security_config.yaml` | No | 16 | 17 | ## AWS EKS Configuration 18 | 19 | | Variable | Description | Default | Required for EKS | 20 | |----------|-------------|---------|-----------------| 21 | | `AWS_PROFILE` | AWS profile to use for authentication | `default` | No | 22 | | `AWS_REGION` | AWS region for EKS cluster | - | Yes, if not in kubeconfig | 23 | | `AWS_ACCESS_KEY_ID` | AWS access key ID (alternative to profile) | - | Only if not using profile | 24 | | `AWS_SECRET_ACCESS_KEY` | AWS secret access key (alternative to profile) | - | Only if not using profile | 25 | | `AWS_SESSION_TOKEN` | AWS session token for temporary credentials | - | Only if using temporary credentials | 26 | 27 | ## GCP GKE Configuration 28 | 29 | | Variable | Description | Default | Required for GKE | 30 | |----------|-------------|---------|-----------------| 31 | | `CLOUDSDK_CORE_PROJECT` | GCP project ID | - | Yes | 32 | | `CLOUDSDK_COMPUTE_REGION` | GCP region | - | Yes, if not using zone | 33 | | `CLOUDSDK_COMPUTE_ZONE` | GCP zone | - | Yes, if not using region | 34 | | `USE_GKE_GCLOUD_AUTH_PLUGIN` | Enable GKE auth plugin | `True` | No (enabled by default) | 35 | 36 | ## Azure AKS Configuration 37 | 38 | | Variable | Description | Default | Required for AKS | 39 | |----------|-------------|---------|-----------------| 40 | | `AZURE_SUBSCRIPTION` | Azure subscription ID | - | Yes | 41 | | `AZURE_DEFAULTS_LOCATION` | Azure region | - | No | 42 | | `AZURE_TENANT_ID` | Azure tenant ID (alternative to login) | - | Only if not using Azure CLI login | 43 | | `AZURE_CLIENT_ID` | Azure client ID (alternative to login) | - | Only if not using Azure CLI login | 44 | | `AZURE_CLIENT_SECRET` | Azure client secret (alternative to login) | - | Only if not using Azure CLI login | 45 | 46 | ## Docker Volume Mounts 47 | 48 | | Volume | Container Path | Purpose | Required | 49 | |--------|---------------|---------|----------| 50 | | `~/.kube` | `/home/appuser/.kube` | Kubernetes configuration | Yes | 51 | | `~/.aws` | `/home/appuser/.aws` | AWS credentials | Only for EKS | 52 | | `~/.config/gcloud` | `/home/appuser/.config/gcloud` | GCP credentials | Only for GKE | 53 | | `~/.azure` | `/home/appuser/.azure` | Azure credentials | Only for AKS | 54 | | Custom security config | `/app/security_config.yaml` | Custom security rules | No | 55 | 56 | ## Usage Examples 57 | 58 | ### Basic Configuration 59 | ```bash 60 | docker run -i --rm \ 61 | -v ~/.kube:/home/appuser/.kube:ro \ 62 | -e K8S_NAMESPACE=production \ 63 | -e K8S_MCP_TIMEOUT=600 \ 64 | ghcr.io/alexei-led/k8s-mcp-server:latest 65 | ``` 66 | 67 | ### AWS EKS Configuration 68 | ```bash 69 | docker run -i --rm \ 70 | -v ~/.kube:/home/appuser/.kube:ro \ 71 | -v ~/.aws:/home/appuser/.aws:ro \ 72 | -e AWS_PROFILE=production \ 73 | -e AWS_REGION=us-west-2 \ 74 | ghcr.io/alexei-led/k8s-mcp-server:latest 75 | ``` 76 | 77 | ### GCP GKE Configuration 78 | ```bash 79 | docker run -i --rm \ 80 | -v ~/.kube:/home/appuser/.kube:ro \ 81 | -v ~/.config/gcloud:/home/appuser/.config/gcloud:ro \ 82 | -e CLOUDSDK_CORE_PROJECT=my-project \ 83 | -e CLOUDSDK_COMPUTE_REGION=us-central1 \ 84 | ghcr.io/alexei-led/k8s-mcp-server:latest 85 | ``` 86 | 87 | ### Azure AKS Configuration 88 | ```bash 89 | docker run -i --rm \ 90 | -v ~/.kube:/home/appuser/.kube:ro \ 91 | -v ~/.azure:/home/appuser/.azure:ro \ 92 | -e AZURE_SUBSCRIPTION=my-subscription-id \ 93 | -e AZURE_DEFAULTS_LOCATION=eastus \ 94 | ghcr.io/alexei-led/k8s-mcp-server:latest 95 | ``` -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with K8s MCP Server 2 | 3 | This guide will help you quickly set up and configure K8s MCP Server to work with your Kubernetes clusters. 4 | 5 | ## Prerequisites 6 | 7 | Before starting, ensure you have: 8 | 9 | - Docker installed on your system 10 | - Valid Kubernetes configuration file (`~/.kube/config`) 11 | - Claude Desktop application installed (if using with Claude) 12 | 13 | ## Quick Start 14 | 15 | ### 1. Run K8s MCP Server Using Docker 16 | 17 | The simplest way to run K8s MCP Server: 18 | 19 | ```bash 20 | docker run -i --rm \ 21 | -v ~/.kube:/home/appuser/.kube:ro \ 22 | ghcr.io/alexei-led/k8s-mcp-server:latest 23 | ``` 24 | 25 | ### 2. Configure Claude Desktop 26 | 27 | To integrate with Claude Desktop, edit your Claude Desktop configuration file (see [Claude Integration](./claude-integration.md) for details). 28 | 29 | ### 3. Test the Connection 30 | 31 | Once configured, prompt Claude with a simple Kubernetes command like: 32 | 33 | "Show me the pods in my default namespace using kubectl" 34 | 35 | ## Basic Configuration Options 36 | 37 | K8s MCP Server can be configured using environment variables: 38 | 39 | ```bash 40 | docker run -i --rm \ 41 | -v ~/.kube:/home/appuser/.kube:ro \ 42 | -e K8S_CONTEXT=my-cluster \ 43 | -e K8S_NAMESPACE=my-namespace \ 44 | -e K8S_MCP_TIMEOUT=600 \ 45 | ghcr.io/alexei-led/k8s-mcp-server:latest 46 | ``` 47 | 48 | Common environment variables: 49 | 50 | | Variable | Description | Default | 51 | |----------|-------------|---------| 52 | | `K8S_CONTEXT` | Kubernetes context to use | *current context* | 53 | | `K8S_NAMESPACE` | Default namespace | `default` | 54 | | `K8S_MCP_TIMEOUT` | Command timeout (seconds) | `300` | 55 | | `K8S_MCP_SECURITY_MODE` | Security mode ("strict"/"permissive") | `strict` | 56 | 57 | See [Environment Variables](./environment-variables.md) for a complete list. 58 | 59 | ## Cloud Provider Configuration 60 | 61 | K8s MCP Server supports major cloud Kubernetes providers: 62 | 63 | ### AWS EKS 64 | 65 | ```bash 66 | docker run -i --rm \ 67 | -v ~/.kube:/home/appuser/.kube:ro \ 68 | -v ~/.aws:/home/appuser/.aws:ro \ 69 | -e AWS_REGION=us-west-2 \ 70 | ghcr.io/alexei-led/k8s-mcp-server:latest 71 | ``` 72 | 73 | ### Google GKE 74 | 75 | ```bash 76 | docker run -i --rm \ 77 | -v ~/.kube:/home/appuser/.kube:ro \ 78 | -v ~/.config/gcloud:/home/appuser/.config/gcloud:ro \ 79 | -e CLOUDSDK_CORE_PROJECT=my-project \ 80 | -e CLOUDSDK_COMPUTE_REGION=us-central1 \ 81 | ghcr.io/alexei-led/k8s-mcp-server:latest 82 | ``` 83 | 84 | ### Azure AKS 85 | 86 | ```bash 87 | docker run -i --rm \ 88 | -v ~/.kube:/home/appuser/.kube:ro \ 89 | -v ~/.azure:/home/appuser/.azure:ro \ 90 | -e AZURE_SUBSCRIPTION=my-subscription-id \ 91 | ghcr.io/alexei-led/k8s-mcp-server:latest 92 | ``` 93 | 94 | For more detailed information about cloud provider configuration, see [Cloud Provider Documentation](./cloud-providers.md). 95 | 96 | ## Security Configuration 97 | 98 | By default, K8s MCP Server runs in strict security mode, which prevents potentially dangerous Kubernetes commands. 99 | 100 | To run in permissive mode: 101 | 102 | ```bash 103 | docker run -i --rm \ 104 | -v ~/.kube:/home/appuser/.kube:ro \ 105 | -e K8S_MCP_SECURITY_MODE=permissive \ 106 | ghcr.io/alexei-led/k8s-mcp-server:latest 107 | ``` 108 | 109 | For custom security rules, see [Security Documentation](./security.md). 110 | 111 | ## What's Next? 112 | 113 | - Learn about [Claude Integration](./claude-integration.md) 114 | - Explore [Supported CLI Tools](./supported-tools.md) 115 | - Understand [Security Features](./security.md) 116 | - See all [Environment Variables](./environment-variables.md) -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security Configuration 2 | 3 | K8s MCP Server includes several safety features and security configurations to ensure safe operation when interacting with Kubernetes clusters. 4 | 5 | ## Security Modes 6 | 7 | K8s MCP Server supports two security modes: 8 | 9 | - **Strict Mode** (default): All commands are validated against security rules 10 | - **Permissive Mode**: Security validation is skipped, allowing all commands to execute 11 | 12 | ### Setting Security Mode 13 | 14 | To run in permissive mode (allow all commands): 15 | 16 | ```json 17 | { 18 | "mcpServers": { 19 | "k8s-mcp-server": { 20 | "command": "docker", 21 | "args": [ 22 | "run", 23 | "-i", 24 | "--rm", 25 | "-v", 26 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 27 | "-e", 28 | "K8S_MCP_SECURITY_MODE=permissive", 29 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 30 | ] 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | ## Security Features 37 | 38 | - **Isolation**: When running in Docker, the server operates in an isolated container environment 39 | - **Read-only access**: All credentials and configuration files are mounted as read-only 40 | - **Non-root execution**: All processes run as a non-root user inside the container 41 | - **Command validation**: Potentially dangerous commands require explicit resource names 42 | - **Context separation**: Automatic context and namespace injection for commands 43 | 44 | ## Customizing Security Rules 45 | 46 | Security rules can be customized using a YAML configuration file. This allows for more flexibility than the built-in rules. 47 | 48 | 1. **Create a Security Configuration File**: 49 | Create a YAML file with your custom rules (e.g., `security_config.yaml`): 50 | 51 | ```yaml 52 | # Security configuration for k8s-mcp-server 53 | 54 | # Potentially dangerous command patterns (prefix-based) 55 | dangerous_commands: 56 | kubectl: 57 | - "kubectl delete" 58 | - "kubectl drain" 59 | # Add your custom dangerous commands here 60 | 61 | # Safe pattern overrides (prefix-based) 62 | safe_patterns: 63 | kubectl: 64 | - "kubectl delete pod" 65 | - "kubectl delete deployment" 66 | # Add your custom safe patterns here 67 | 68 | # Advanced regex pattern rules 69 | regex_rules: 70 | kubectl: 71 | - pattern: "kubectl\\s+delete\\s+(-[A-Za-z]+\\s+)*--all\\b" 72 | description: "Deleting all resources of a type" 73 | error_message: "Deleting all resources is restricted. Specify individual resources to delete." 74 | # Add your custom regex rules here 75 | ``` 76 | 77 | 2. **Mount the Configuration File in Docker**: 78 | ```json 79 | { 80 | "mcpServers": { 81 | "k8s-mcp-server": { 82 | "command": "docker", 83 | "args": [ 84 | "run", 85 | "-i", 86 | "--rm", 87 | "-v", 88 | "/Users/YOUR_USER_NAME/.kube:/home/appuser/.kube:ro", 89 | "-v", 90 | "/path/to/security_config.yaml:/app/security_config.yaml:ro", 91 | "-e", 92 | "K8S_MCP_SECURITY_CONFIG=/app/security_config.yaml", 93 | "ghcr.io/alexei-led/k8s-mcp-server:latest" 94 | ] 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ## Configuration Structure 101 | 102 | The security configuration YAML file has three main sections: 103 | 104 | 1. **dangerous_commands**: Dictionary of command prefixes that are considered dangerous for each tool 105 | 2. **safe_patterns**: Dictionary of command prefixes that override dangerous commands (exceptions) 106 | 3. **regex_rules**: Advanced regex patterns for more complex validation rules 107 | 108 | Each regex rule should include: 109 | - **pattern**: Regular expression pattern to match against commands 110 | - **description**: Description of what the rule checks for 111 | - **error_message**: Custom error message to display when the rule is violated 112 | 113 | ## Examples 114 | 115 | **Example 1: Restricting Namespace Operations** 116 | 117 | ```yaml 118 | regex_rules: 119 | kubectl: 120 | - pattern: "kubectl\\s+.*\\s+--namespace=kube-system\\b" 121 | description: "Operations in kube-system namespace" 122 | error_message: "Operations in kube-system namespace are restricted." 123 | ``` 124 | 125 | **Example 2: Allowing Additional Safe Patterns** 126 | 127 | ```yaml 128 | safe_patterns: 129 | kubectl: 130 | - "kubectl delete pod" 131 | - "kubectl delete job" 132 | - "kubectl delete cronjob" 133 | ``` 134 | 135 | **Example 3: Restricting Dangerous File System Access** 136 | 137 | ```yaml 138 | regex_rules: 139 | kubectl: 140 | - pattern: "kubectl\\s+exec\\s+.*\\s+-[^-]*c\\s+.*(rm|mv|cp|curl|wget|chmod)\\b" 141 | description: "Dangerous file operations in exec" 142 | error_message: "File system operations within kubectl exec are restricted." 143 | ``` -------------------------------------------------------------------------------- /docs/supported-tools.md: -------------------------------------------------------------------------------- 1 | # Supported Tools and Commands 2 | 3 | K8s MCP Server provides access to several Kubernetes CLI tools and additional utilities for command piping. 4 | 5 | ## Kubernetes CLI Tools 6 | 7 | ### kubectl 8 | 9 | The standard command-line tool for interacting with Kubernetes clusters. 10 | 11 | **Examples:** 12 | ```bash 13 | kubectl get pods 14 | kubectl get deployments 15 | kubectl describe pod my-pod 16 | kubectl get services 17 | kubectl logs my-pod 18 | kubectl apply -f deployment.yaml 19 | ``` 20 | 21 | ### helm 22 | 23 | The package manager for Kubernetes, used to install and manage applications. 24 | 25 | **Examples:** 26 | ```bash 27 | helm list 28 | helm install my-release my-chart 29 | helm upgrade my-release my-chart 30 | helm uninstall my-release 31 | helm repo add bitnami https://charts.bitnami.com/bitnami 32 | helm search repo nginx 33 | ``` 34 | 35 | ### istioctl 36 | 37 | The command-line tool for the Istio service mesh, used to manage and configure Istio. 38 | 39 | **Examples:** 40 | ```bash 41 | istioctl analyze 42 | istioctl proxy-status 43 | istioctl dashboard 44 | istioctl x describe pod my-pod 45 | istioctl profile list 46 | istioctl version 47 | ``` 48 | 49 | ### argocd 50 | 51 | The command-line tool for ArgoCD, a GitOps continuous delivery tool for Kubernetes. 52 | 53 | **Examples:** 54 | ```bash 55 | argocd app list 56 | argocd app get my-app 57 | argocd app sync my-app 58 | argocd cluster list 59 | argocd repo list 60 | argocd project list 61 | ``` 62 | 63 | ## Cloud Provider CLI Tools 64 | 65 | ### AWS CLI 66 | 67 | The AWS Command Line Interface for managing AWS services, including EKS. 68 | 69 | **Examples:** 70 | ```bash 71 | aws eks list-clusters 72 | aws eks describe-cluster --name my-cluster 73 | aws eks update-kubeconfig --name my-cluster --region us-west-2 74 | ``` 75 | 76 | ### Google Cloud SDK (gcloud) 77 | 78 | The Google Cloud command-line tool for managing GCP services, including GKE. 79 | 80 | **Examples:** 81 | ```bash 82 | gcloud container clusters list 83 | gcloud container clusters describe my-cluster 84 | gcloud container clusters get-credentials my-cluster --region us-central1 85 | ``` 86 | 87 | ### Azure CLI (az) 88 | 89 | The Azure command-line tool for managing Azure services, including AKS. 90 | 91 | **Examples:** 92 | ```bash 93 | az aks list 94 | az aks show --name my-cluster --resource-group my-group 95 | az aks get-credentials --name my-cluster --resource-group my-group 96 | ``` 97 | 98 | ## Utility Tools for Command Piping 99 | 100 | The Docker image includes a rich set of tools that can be used for command piping and output processing: 101 | 102 | ### Text Processing 103 | - **jq**: JSON processor (`kubectl get pods -o json | jq '.items[].metadata.name'`) 104 | - **grep**: Pattern matching (`kubectl get pods | grep Running`) 105 | - **sed**: Stream editor (`kubectl get pods | sed 's/Running/UP/g'`) 106 | - **awk**: Text processing (`kubectl get pods | awk '{print $1}'`) 107 | - **findutils**: Collection of find utilities 108 | - **gawk**: GNU awk implementation 109 | 110 | ### Network Utilities 111 | - **curl**: HTTP client (`curl -s http://pod-ip:port | jq .`) 112 | - **wget**: Alternative HTTP client 113 | - **net-tools**: Network utilities (includes `netstat`, `ifconfig`) 114 | - **dnsutils**: DNS utilities (includes `dig`, `nslookup`) 115 | 116 | ### File Management 117 | - **tar**: Archive utility 118 | - **gzip**: Compression utility 119 | - **zip/unzip**: Zip file utilities 120 | 121 | ### Other Utilities 122 | - **less**: Pager for viewing output 123 | - **vim**: Text editor 124 | - **openssh-client**: SSH client utilities 125 | - **gnupg**: Encryption utilities 126 | 127 | ## Command Piping Examples 128 | 129 | The server supports Unix command piping to filter and transform output: 130 | 131 | ```bash 132 | # Extract pod names as a list 133 | kubectl get pods -o json | jq -r '.items[].metadata.name' 134 | 135 | # Find deployments in a non-Running state 136 | kubectl get deployments | grep -v Running 137 | 138 | # Format pod listing with custom columns 139 | kubectl get pods | awk '{printf "%-30s %-10s\n", $1, $3}' 140 | 141 | # Get container images running in the cluster 142 | kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}' | tr -s '[[:space:]]' '\n' | sort | uniq 143 | 144 | # Count pods by status 145 | kubectl get pods | grep -v NAME | awk '{print $3}' | sort | uniq -c 146 | 147 | # Fetch and process an API 148 | kubectl run curl --image=curlimages/curl -i --rm -- curl -s https://api.example.com | jq . 149 | ``` 150 | 151 | ## API Functions 152 | 153 | Each CLI tool has documentation and execution functions: 154 | 155 | ### Documentation Functions 156 | - `describe_kubectl(command=None)`: Get documentation for kubectl commands 157 | - `describe_helm(command=None)`: Get documentation for Helm commands 158 | - `describe_istioctl(command=None)`: Get documentation for Istio commands 159 | - `describe_argocd(command=None)`: Get documentation for ArgoCD commands 160 | 161 | ### Execution Functions 162 | - `execute_kubectl(command, timeout=None)`: Execute kubectl commands 163 | - `execute_helm(command, timeout=None)`: Execute Helm commands 164 | - `execute_istioctl(command, timeout=None)`: Execute Istio commands 165 | - `execute_argocd(command, timeout=None)`: Execute ArgoCD commands -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "k8s-mcp-server" 7 | version = "1.3.0" 8 | description = "MCP Server for Kubernetes CLI tools (kubectl, istioctl, helm, argocd)" 9 | readme = "README.md" 10 | requires-python = ">=3.13" 11 | license = {text = "MIT"} 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | ] 17 | dependencies = [ 18 | "mcp", 19 | "pydantic>=2.0.0", 20 | "psutil>=5.9.0", 21 | "pyyaml>=6.0.0", 22 | ] 23 | 24 | [project.optional-dependencies] 25 | dev = [ 26 | "ruff", 27 | "pytest", 28 | "pytest-asyncio", 29 | "pytest-cov", 30 | ] 31 | 32 | [tool.setuptools.packages.find] 33 | where = ["src"] 34 | 35 | [tool.black] 36 | line-length = 160 37 | target-version = ['py313'] 38 | 39 | [tool.ruff] 40 | line-length = 160 41 | target-version = 'py313' 42 | 43 | [tool.ruff.lint] 44 | select = ["E", "F", "W", "I", "N", "UP", "B", "A"] 45 | ignore = [] 46 | 47 | [tool.pytest.ini_options] 48 | testpaths = ["tests"] 49 | python_files = "test_*.py" 50 | # Skip integration tests by default, run with coverage 51 | addopts = "-m 'not integration' --cov=k8s_mcp_server" 52 | asyncio_mode = "auto" 53 | asyncio_default_fixture_loop_scope = "function" 54 | markers = [ 55 | "integration: marks tests as integration tests requiring a Kubernetes cluster", 56 | "unit: marks tests as unit tests not requiring external dependencies" 57 | ] 58 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | """K8s MCP Server. 2 | 3 | A server for Anthropic's MCP (Model Control Protocol) that allows running 4 | Kubernetes CLI tools such as kubectl, istioctl, helm, and argocd in a 5 | safe, containerized environment. 6 | """ 7 | 8 | __version__ = "1.3.0" 9 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point for K8s MCP Server. 2 | 3 | Running this module will start the K8s MCP Server. 4 | """ 5 | 6 | import logging 7 | import signal 8 | import sys 9 | 10 | # Configure logging before importing server 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 14 | handlers=[logging.StreamHandler(sys.stderr)], 15 | ) 16 | logger = logging.getLogger("k8s-mcp-server") 17 | 18 | 19 | def handle_interrupt(signum, frame): 20 | """Handle keyboard interrupt (Ctrl+C) gracefully.""" 21 | logger.info(f"Received signal {signum}, shutting down gracefully...") 22 | sys.exit(0) 23 | 24 | 25 | # Using FastMCP's built-in CLI handling 26 | def main(): 27 | """Run the K8s MCP Server.""" 28 | # Set up signal handler for graceful shutdown 29 | signal.signal(signal.SIGINT, handle_interrupt) 30 | signal.signal(signal.SIGTERM, handle_interrupt) 31 | try: 32 | # Import here to avoid circular imports 33 | from k8s_mcp_server.config import MCP_TRANSPORT 34 | from k8s_mcp_server.server import mcp 35 | 36 | # Validate transport protocol 37 | if MCP_TRANSPORT not in ["stdio", "sse"]: 38 | logger.error(f"Invalid transport protocol: {MCP_TRANSPORT}. Using stdio instead.") 39 | transport = "stdio" 40 | else: 41 | transport = MCP_TRANSPORT 42 | 43 | # Start the server 44 | logger.info(f"Starting K8s MCP Server with {transport} transport") 45 | mcp.run(transport=transport) 46 | except KeyboardInterrupt: 47 | logger.info("Keyboard interrupt received. Shutting down gracefully...") 48 | sys.exit(0) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/config.py: -------------------------------------------------------------------------------- 1 | """Configuration settings for the K8s MCP Server. 2 | 3 | This module contains configuration settings for the K8s MCP Server. 4 | 5 | Environment variables: 6 | - K8S_MCP_TIMEOUT: Custom timeout in seconds (default: 300) 7 | - K8S_MCP_MAX_OUTPUT: Maximum output size in characters (default: 100000) 8 | - K8S_MCP_TRANSPORT: Transport protocol to use ("stdio" or "sse", default: "stdio") 9 | - K8S_CONTEXT: Kubernetes context to use (default: current context) 10 | - K8S_NAMESPACE: Kubernetes namespace to use (default: "default") 11 | - K8S_MCP_SECURITY_MODE: Security mode for command validation ("strict", "permissive", default: "strict") 12 | - K8S_MCP_SECURITY_CONFIG: Path to YAML config file for security rules (default: None) 13 | """ 14 | 15 | import os 16 | from pathlib import Path 17 | 18 | # Command execution settings 19 | DEFAULT_TIMEOUT = int(os.environ.get("K8S_MCP_TIMEOUT", "300")) 20 | MAX_OUTPUT_SIZE = int(os.environ.get("K8S_MCP_MAX_OUTPUT", "100000")) 21 | 22 | # Server settings 23 | MCP_TRANSPORT = os.environ.get("K8S_MCP_TRANSPORT", "stdio") # Transport protocol: stdio or sse 24 | 25 | # Kubernetes specific settings 26 | K8S_CONTEXT = os.environ.get("K8S_CONTEXT", "") # Empty means use current context 27 | K8S_NAMESPACE = os.environ.get("K8S_NAMESPACE", "default") 28 | 29 | # Security settings 30 | SECURITY_MODE = os.environ.get("K8S_MCP_SECURITY_MODE", "strict") # strict or permissive 31 | SECURITY_CONFIG_PATH = os.environ.get("K8S_MCP_SECURITY_CONFIG", None) 32 | 33 | # Supported CLI tools 34 | SUPPORTED_CLI_TOOLS = { 35 | "kubectl": { 36 | "check_cmd": "kubectl version --client", 37 | "help_flag": "--help", 38 | }, 39 | "istioctl": { 40 | "check_cmd": "istioctl version --remote=false", 41 | "help_flag": "--help", 42 | }, 43 | "helm": { 44 | "check_cmd": "helm version", 45 | "help_flag": "--help", 46 | }, 47 | "argocd": { 48 | "check_cmd": "argocd version --client", 49 | "help_flag": "--help", 50 | }, 51 | } 52 | 53 | # Instructions displayed to client during initialization 54 | INSTRUCTIONS = """ 55 | K8s MCP Server provides a simple interface to Kubernetes CLI tools. 56 | 57 | Supported CLI tools: 58 | - kubectl: Kubernetes command-line tool 59 | - istioctl: Command-line tool for Istio service mesh 60 | - helm: Kubernetes package manager 61 | - argocd: GitOps continuous delivery tool for Kubernetes 62 | 63 | Available tools: 64 | - Use describe_kubectl, describe_helm, describe_istioctl, or describe_argocd to get documentation for CLI tools 65 | - Use execute_kubectl, execute_helm, execute_istioctl, or execute_argocd to run commands 66 | 67 | Command execution supports Unix pipes (|) to filter or transform output: 68 | Example: kubectl get pods -o json | jq '.items[].metadata.name' 69 | Example: helm list | grep mysql 70 | 71 | Use the built-in prompt templates for common Kubernetes tasks: 72 | - k8s_resource_status: Check status of Kubernetes resources 73 | - k8s_deploy_application: Deploy an application to Kubernetes 74 | - k8s_troubleshoot: Troubleshoot Kubernetes resources 75 | - k8s_resource_inventory: List all resources in cluster 76 | - istio_service_mesh: Manage Istio service mesh 77 | - helm_chart_management: Manage Helm charts 78 | - argocd_application: Manage ArgoCD applications 79 | - k8s_security_check: Security analysis for Kubernetes resources 80 | - k8s_resource_scaling: Scale Kubernetes resources 81 | - k8s_logs_analysis: Analyze logs from Kubernetes resources 82 | """ 83 | 84 | # Application paths 85 | BASE_DIR = Path(__file__).parent.parent.parent 86 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/errors.py: -------------------------------------------------------------------------------- 1 | """Error handling for K8s MCP Server. 2 | 3 | This module provides standardized error handling for the K8s MCP Server, 4 | including exception classes and helper functions for creating structured error responses. 5 | """ 6 | 7 | from typing import Any 8 | 9 | from k8s_mcp_server.tools import CommandResult, ErrorDetails, ErrorDetailsNested 10 | 11 | 12 | class K8sMCPError(Exception): 13 | """Base class for all K8s MCP Server exceptions.""" 14 | 15 | def __init__(self, message: str, code: str = "INTERNAL_ERROR", details: dict[str, Any] | None = None): 16 | """Initialize K8sMCPError. 17 | 18 | Args: 19 | message: Error message 20 | code: Error code 21 | details: Additional error details 22 | """ 23 | super().__init__(message) 24 | self.message = message 25 | self.code = code 26 | self.details = details or {} 27 | 28 | 29 | class CommandValidationError(K8sMCPError): 30 | """Exception raised when a command fails validation.""" 31 | 32 | def __init__(self, message: str, details: dict[str, Any] | None = None): 33 | """Initialize CommandValidationError. 34 | 35 | Args: 36 | message: Error message 37 | details: Additional error details 38 | """ 39 | super().__init__(message, "VALIDATION_ERROR", details) 40 | 41 | 42 | class CommandExecutionError(K8sMCPError): 43 | """Exception raised when a command fails to execute.""" 44 | 45 | def __init__(self, message: str, details: dict[str, Any] | None = None): 46 | """Initialize CommandExecutionError. 47 | 48 | Args: 49 | message: Error message 50 | details: Additional error details 51 | """ 52 | super().__init__(message, "EXECUTION_ERROR", details) 53 | 54 | 55 | class AuthenticationError(K8sMCPError): 56 | """Exception raised when authentication fails.""" 57 | 58 | def __init__(self, message: str, details: dict[str, Any] | None = None): 59 | """Initialize AuthenticationError. 60 | 61 | Args: 62 | message: Error message 63 | details: Additional error details 64 | """ 65 | super().__init__(message, "AUTH_ERROR", details) 66 | 67 | 68 | class CommandTimeoutError(K8sMCPError): 69 | """Exception raised when a command times out.""" 70 | 71 | def __init__(self, message: str, details: dict[str, Any] | None = None): 72 | """Initialize CommandTimeoutError. 73 | 74 | Args: 75 | message: Error message 76 | details: Additional error details 77 | """ 78 | super().__init__(message, "TIMEOUT_ERROR", details) 79 | 80 | 81 | def create_error_result(error: K8sMCPError, command: str | None = None, exit_code: int | None = None, stderr: str | None = None) -> CommandResult: 82 | """Create a CommandResult with error details from a K8sMCPError. 83 | 84 | Args: 85 | error: The exception that occurred 86 | command: The command that caused the error 87 | exit_code: The exit code of the command 88 | stderr: Standard error output from the command 89 | 90 | Returns: 91 | CommandResult with error details 92 | """ 93 | # Create nested error details 94 | nested_details = ErrorDetailsNested() 95 | if command: 96 | nested_details["command"] = command 97 | if exit_code is not None: 98 | nested_details["exit_code"] = exit_code 99 | if stderr: 100 | nested_details["stderr"] = stderr 101 | 102 | # Add any custom details from the exception 103 | for key, value in error.details.items(): 104 | if key not in nested_details: 105 | nested_details[key] = value 106 | 107 | # Create error details 108 | error_details = ErrorDetails(message=str(error), code=error.code, details=nested_details) 109 | 110 | # Create command result 111 | return CommandResult(status="error", output=str(error), error=error_details, exit_code=exit_code) 112 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/prompts.py: -------------------------------------------------------------------------------- 1 | """Kubernetes CLI prompt definitions for the K8s MCP Server. 2 | 3 | This module provides a collection of useful prompt templates for common Kubernetes operations. 4 | These prompts help ensure consistent best practices and efficient Kubernetes resource management. 5 | """ 6 | 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def register_prompts(mcp): 13 | """Register all prompts with the MCP server instance. 14 | 15 | Args: 16 | mcp: The FastMCP server instance 17 | """ 18 | logger.info("Registering Kubernetes prompt templates") 19 | 20 | @mcp.prompt() 21 | def k8s_resource_status(resource_type: str, namespace: str = "default") -> str: 22 | """Generate kubectl commands to check the status of Kubernetes resources. 23 | 24 | Args: 25 | resource_type: Type of Kubernetes resource (e.g., pods, deployments) 26 | namespace: Kubernetes namespace to target 27 | 28 | Returns: 29 | Formatted prompt string for resource status checks 30 | """ 31 | return f"""Generate kubectl commands to check the status of {resource_type} 32 | in the {namespace} namespace. 33 | 34 | Include commands to: 35 | 1. List all {resource_type} with basic information 36 | 2. Show detailed status and conditions 37 | 3. Check recent events related to these resources 38 | 4. Identify any issues or potential problems 39 | 5. Retrieve logs if applicable 40 | 41 | Structure the commands to be easily executed and parsed.""" 42 | 43 | @mcp.prompt() 44 | def k8s_deploy_application(app_name: str, image: str, namespace: str = "default", replicas: int = 1) -> str: 45 | """Generate kubectl commands to deploy an application. 46 | 47 | Args: 48 | app_name: Name for the application 49 | image: Container image to deploy 50 | namespace: Kubernetes namespace for deployment 51 | replicas: Number of replicas to run 52 | 53 | Returns: 54 | Formatted prompt string for application deployment 55 | """ 56 | return f"""Generate kubectl commands to deploy an application named '{app_name}' 57 | using the image '{image}' with {replicas} replicas in the {namespace} namespace. 58 | 59 | Include commands to: 60 | 1. Create necessary Kubernetes resources (Deployment, Service, etc.) 61 | 2. Set appropriate resource limits and requests 62 | 3. Configure health checks and probes 63 | 4. Apply proper labels and annotations 64 | 5. Verify the deployment was successful 65 | 6. Test connectivity to the deployed application 66 | 67 | The deployment should follow Kubernetes best practices for security and reliability.""" 68 | 69 | @mcp.prompt() 70 | def k8s_troubleshoot(resource_type: str, resource_name: str, namespace: str = "default") -> str: 71 | """Generate kubectl commands for troubleshooting Kubernetes resources. 72 | 73 | Args: 74 | resource_type: Type of Kubernetes resource (e.g., pod, deployment) 75 | resource_name: Name of the specific resource to troubleshoot 76 | namespace: Kubernetes namespace containing the resource 77 | 78 | Returns: 79 | Formatted prompt string for troubleshooting commands 80 | """ 81 | return f"""Generate kubectl commands to troubleshoot issues with the {resource_type} 82 | named '{resource_name}' in the {namespace} namespace. 83 | 84 | Include commands to: 85 | 1. Check the current status and configuration of the resource 86 | 2. View events related to this resource 87 | 3. Examine logs and error messages 88 | 4. Verify dependencies and related resources 89 | 5. Check networking and security settings 90 | 6. Compare against best practices for this resource type 91 | 92 | Organize the commands in a systematic troubleshooting flow from basic to advanced checks.""" 93 | 94 | @mcp.prompt() 95 | def k8s_resource_inventory(namespace: str = "") -> str: 96 | """Generate kubectl commands to inventory Kubernetes cluster resources. 97 | 98 | Args: 99 | namespace: Optional namespace to limit inventory (empty for all namespaces) 100 | 101 | Returns: 102 | Formatted prompt string for resource inventory commands 103 | """ 104 | scope = f"in the {namespace} namespace" if namespace else "across all namespaces" 105 | return f"""Generate kubectl commands to create a comprehensive inventory 106 | of resources {scope}. 107 | 108 | Include commands to: 109 | 1. List all resource types available in the cluster 110 | 2. Count resources by type 111 | 3. Show resource utilization and allocation 112 | 4. Identify critical system components 113 | 5. Check for resources without proper labels or annotations 114 | 6. Export the inventory in a structured format (JSON or YAML) 115 | 116 | Structure the commands to build a complete picture of the cluster state.""" 117 | 118 | @mcp.prompt() 119 | def k8s_security_check(namespace: str = "") -> str: 120 | """Generate kubectl commands for security analysis of Kubernetes resources. 121 | 122 | Args: 123 | namespace: Optional namespace to limit checks (empty for all namespaces) 124 | 125 | Returns: 126 | Formatted prompt string for security check commands 127 | """ 128 | scope = f"in the {namespace} namespace" if namespace else "across the entire cluster" 129 | return f"""Generate kubectl commands to perform a security assessment 130 | of Kubernetes resources {scope}. 131 | 132 | Include commands to check for: 133 | 1. Pods running as root or with privileged security contexts 134 | 2. Resources without proper RBAC restrictions 135 | 3. Secrets that might be exposed or improperly managed 136 | 4. Network policies and service configurations 137 | 5. Container image vulnerabilities and best practices 138 | 6. Resource configurations against CIS benchmarks 139 | 140 | Explain the security implications of each check and provide remediation suggestions.""" 141 | 142 | @mcp.prompt() 143 | def k8s_resource_scaling(resource_type: str, resource_name: str, namespace: str = "default") -> str: 144 | """Generate kubectl commands for scaling Kubernetes resources. 145 | 146 | Args: 147 | resource_type: Type of resource to scale (e.g., deployment, statefulset) 148 | resource_name: Name of the specific resource to scale 149 | namespace: Kubernetes namespace containing the resource 150 | 151 | Returns: 152 | Formatted prompt string for scaling commands 153 | """ 154 | return f"""Generate kubectl commands to scale the {resource_type} 155 | named '{resource_name}' in the {namespace} namespace. 156 | 157 | Include commands to: 158 | 1. Check current scaling parameters and resource utilization 159 | 2. Scale the resource manually with appropriate safeguards 160 | 3. Set up Horizontal Pod Autoscaling if applicable 161 | 4. Monitor the scaling operation 162 | 5. Verify application health during and after scaling 163 | 6. Rollback in case of issues 164 | 165 | Provide commands for both scaling up and down with appropriate checks and validations.""" 166 | 167 | @mcp.prompt() 168 | def k8s_logs_analysis(pod_name: str, namespace: str = "default", container: str = "") -> str: 169 | """Generate kubectl commands for analyzing logs from Kubernetes resources. 170 | 171 | Args: 172 | pod_name: Name of the pod to analyze logs from 173 | namespace: Kubernetes namespace containing the pod 174 | container: Optional container name for multi-container pods 175 | 176 | Returns: 177 | Formatted prompt string for log analysis commands 178 | """ 179 | container_clause = f" container '{container}' in" if container else "" 180 | return f"""Generate kubectl commands to analyze logs from{container_clause} 181 | pod '{pod_name}' in the {namespace} namespace. 182 | 183 | Include commands to: 184 | 1. Retrieve recent logs with appropriate timestamps 185 | 2. Filter logs for errors and warnings 186 | 3. Search for specific patterns or events 187 | 4. Follow logs in real-time for monitoring 188 | 5. Extract and analyze specific log sections 189 | 6. Export logs for offline analysis 190 | 191 | Provide commands that can help identify common issues like application errors, 192 | crashes, resource constraints, or performance problems.""" 193 | 194 | @mcp.prompt() 195 | def istio_service_mesh(namespace: str = "default") -> str: 196 | """Generate istioctl commands for managing Istio service mesh. 197 | 198 | Args: 199 | namespace: Kubernetes namespace to target 200 | 201 | Returns: 202 | Formatted prompt string for Istio management commands 203 | """ 204 | return f"""Generate istioctl commands to manage and analyze the Istio service mesh 205 | in the {namespace} namespace. 206 | 207 | Include commands to: 208 | 1. Analyze the mesh for issues and configuration problems 209 | 2. Inspect traffic routing and service configurations 210 | 3. Check security policies and mTLS settings 211 | 4. Monitor mesh performance and health 212 | 5. Debug common service mesh problems 213 | 6. Visualize the service topology 214 | 215 | The commands should follow Istio best practices and focus on operational excellence.""" 216 | 217 | @mcp.prompt() 218 | def helm_chart_management(release_name: str = "", namespace: str = "default") -> str: 219 | """Generate helm commands for managing Helm charts and releases. 220 | 221 | Args: 222 | release_name: Optional specific release to manage 223 | namespace: Kubernetes namespace for Helm operations 224 | 225 | Returns: 226 | Formatted prompt string for Helm management commands 227 | """ 228 | release_specific = f"for release '{release_name}'" if release_name else "for all releases" 229 | return f"""Generate helm commands to manage Helm charts {release_specific} 230 | in the {namespace} namespace. 231 | 232 | Include commands to: 233 | 1. List and inspect installed releases 234 | 2. Manage release lifecycle (install, upgrade, rollback) 235 | 3. Validate chart templates and values 236 | 4. Check release history and status 237 | 5. Debug common Helm deployment issues 238 | 6. Manage Helm repositories 239 | 240 | The commands should follow Helm best practices and include proper validation steps.""" 241 | 242 | @mcp.prompt() 243 | def argocd_application(app_name: str = "", namespace: str = "argocd") -> str: 244 | """Generate argocd commands for managing ArgoCD applications. 245 | 246 | Args: 247 | app_name: Optional specific application to manage 248 | namespace: Kubernetes namespace where ArgoCD is installed 249 | 250 | Returns: 251 | Formatted prompt string for ArgoCD management commands 252 | """ 253 | app_specific = f"for application '{app_name}'" if app_name else "for all applications" 254 | return f"""Generate argocd commands to manage GitOps deployments {app_specific} 255 | with ArgoCD in the {namespace} namespace. 256 | 257 | Include commands to: 258 | 1. List and inspect applications and their statuses 259 | 2. Manage application lifecycle (create, sync, delete) 260 | 3. Monitor sync status and health 261 | 4. Troubleshoot deployment issues 262 | 5. Handle rollbacks and recovery 263 | 6. Manage application configurations and settings 264 | 265 | The commands should follow ArgoCD and GitOps best practices for continuous delivery.""" 266 | 267 | logger.info("Successfully registered all Kubernetes prompt templates") 268 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/security.py: -------------------------------------------------------------------------------- 1 | """Security utilities for K8s MCP Server. 2 | 3 | This module provides security validation for Kubernetes CLI commands, 4 | including validation of command structure, dangerous command detection, 5 | and pipe command validation. 6 | """ 7 | 8 | import logging 9 | import re 10 | import shlex 11 | from dataclasses import dataclass 12 | from pathlib import Path 13 | 14 | import yaml 15 | 16 | from k8s_mcp_server.config import SECURITY_CONFIG_PATH, SECURITY_MODE 17 | from k8s_mcp_server.tools import ( 18 | ALLOWED_K8S_TOOLS, 19 | is_pipe_command, 20 | is_valid_k8s_tool, 21 | split_pipe_command, 22 | validate_unix_command, 23 | ) 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | # Default dictionary of potentially dangerous commands for each CLI tool 28 | DEFAULT_DANGEROUS_COMMANDS: dict[str, list[str]] = { 29 | "kubectl": [ 30 | "kubectl delete", # Global delete without specific resource 31 | "kubectl drain", 32 | "kubectl replace --force", 33 | "kubectl exec", # Handled specially to prevent interactive shells 34 | "kubectl port-forward", # Could expose services externally 35 | "kubectl cp", # File system access 36 | "kubectl delete pods --all", # Added for test - delete all pods 37 | ], 38 | "istioctl": [ 39 | "istioctl experimental", 40 | "istioctl proxy-config", # Can access sensitive information 41 | "istioctl dashboard", # Could expose services 42 | ], 43 | "helm": [ 44 | "helm delete", 45 | "helm uninstall", 46 | "helm rollback", 47 | "helm upgrade", # Could break services 48 | ], 49 | "argocd": [ 50 | "argocd app delete", 51 | "argocd cluster rm", 52 | "argocd repo rm", 53 | "argocd app set", # Could modify application settings 54 | ], 55 | } 56 | 57 | # Default dictionary of safe patterns that override the dangerous commands 58 | DEFAULT_SAFE_PATTERNS: dict[str, list[str]] = { 59 | "kubectl": [ 60 | "kubectl delete pod", 61 | "kubectl delete deployment", 62 | "kubectl delete service", 63 | "kubectl delete configmap", 64 | "kubectl delete secret", 65 | # Specific exec commands that are safe 66 | "kubectl exec --help", 67 | "kubectl exec -it", # Allow interactive mode that's explicitly requested 68 | "kubectl exec pod", 69 | "kubectl exec deployment", 70 | "kubectl port-forward --help", 71 | "kubectl cp --help", 72 | ], 73 | "istioctl": [ 74 | "istioctl experimental -h", 75 | "istioctl experimental --help", 76 | "istioctl proxy-config --help", 77 | "istioctl dashboard --help", 78 | ], 79 | "helm": [ 80 | "helm delete --help", 81 | "helm uninstall --help", 82 | "helm rollback --help", 83 | "helm upgrade --help", 84 | ], 85 | "argocd": [ 86 | "argocd app delete --help", 87 | "argocd cluster rm --help", 88 | "argocd repo rm --help", 89 | "argocd app set --help", 90 | ], 91 | } 92 | 93 | 94 | @dataclass 95 | class ValidationRule: 96 | """Represents a command validation rule.""" 97 | 98 | pattern: str 99 | description: str 100 | error_message: str 101 | regex: bool = False 102 | 103 | 104 | @dataclass 105 | class SecurityConfig: 106 | """Security configuration for command validation.""" 107 | 108 | dangerous_commands: dict[str, list[str]] 109 | safe_patterns: dict[str, list[str]] 110 | regex_rules: dict[str, list[ValidationRule]] = None 111 | 112 | def __post_init__(self): 113 | """Initialize default values.""" 114 | if self.regex_rules is None: 115 | self.regex_rules = {} 116 | 117 | 118 | # Load security configuration from YAML file if available 119 | def load_security_config() -> SecurityConfig: 120 | """Load security configuration from YAML file or use defaults.""" 121 | dangerous_commands = DEFAULT_DANGEROUS_COMMANDS.copy() 122 | safe_patterns = DEFAULT_SAFE_PATTERNS.copy() 123 | regex_rules = {} 124 | 125 | if SECURITY_CONFIG_PATH: 126 | config_path = Path(SECURITY_CONFIG_PATH) 127 | if config_path.exists(): 128 | try: 129 | with open(config_path) as f: 130 | config_data = yaml.safe_load(f) 131 | 132 | # Update dangerous commands 133 | if "dangerous_commands" in config_data: 134 | for tool, commands in config_data["dangerous_commands"].items(): 135 | dangerous_commands[tool] = commands 136 | 137 | # Update safe patterns 138 | if "safe_patterns" in config_data: 139 | for tool, patterns in config_data["safe_patterns"].items(): 140 | safe_patterns[tool] = patterns 141 | 142 | # Load regex rules 143 | if "regex_rules" in config_data: 144 | for tool, rules in config_data["regex_rules"].items(): 145 | regex_rules[tool] = [] 146 | for rule in rules: 147 | regex_rules[tool].append( 148 | ValidationRule( 149 | pattern=rule["pattern"], 150 | description=rule["description"], 151 | error_message=rule.get("error_message", f"Command matches restricted pattern: {rule['pattern']}"), 152 | regex=True, 153 | ) 154 | ) 155 | 156 | logger.info(f"Loaded security configuration from {config_path}") 157 | except Exception as e: 158 | logger.error(f"Error loading security configuration: {str(e)}") 159 | logger.warning("Using default security configuration") 160 | 161 | return SecurityConfig(dangerous_commands=dangerous_commands, safe_patterns=safe_patterns, regex_rules=regex_rules) 162 | 163 | 164 | # Initialize security configuration 165 | SECURITY_CONFIG = load_security_config() 166 | 167 | 168 | def is_safe_exec_command(command: str) -> bool: 169 | """Check if a kubectl exec command is safe to execute. 170 | 171 | We consider a kubectl exec command safe if: 172 | 1. It's explicitly interactive (-it, -ti flags) and the user is aware of this 173 | 2. It executes a specific command rather than opening a general shell 174 | 3. It uses shells (bash/sh) only with specific commands (-c flag) 175 | 176 | Args: 177 | command: The kubectl exec command 178 | 179 | Returns: 180 | True if the command is safe, False otherwise 181 | """ 182 | if not command.startswith("kubectl exec"): 183 | return True # Not an exec command 184 | 185 | # Special cases: help and version are always safe 186 | if " --help" in command or " -h" in command or " version" in command: 187 | return True 188 | 189 | # Check for explicit interactive mode 190 | has_interactive = any(flag in command for flag in [" -i ", " --stdin ", " -it ", " -ti ", " -t ", " --tty "]) 191 | 192 | # List of dangerous shell commands that should not be executed without arguments 193 | dangerous_shell_patterns = [ 194 | " -- sh", 195 | " -- bash", 196 | " -- /bin/sh", 197 | " -- /bin/bash", 198 | " -- zsh", 199 | " -- /bin/zsh", 200 | " -- ksh", 201 | " -- /bin/ksh", 202 | " -- csh", 203 | " -- /bin/csh", 204 | " -- /usr/bin/bash", 205 | " -- /usr/bin/sh", 206 | " -- /usr/bin/zsh", 207 | " -- /usr/bin/ksh", 208 | " -- /usr/bin/csh", 209 | ] 210 | 211 | # Check if any of the dangerous shell patterns are present 212 | has_shell_pattern = False 213 | for pattern in dangerous_shell_patterns: 214 | if pattern in command + " ": # Add space to match end of command 215 | has_shell_pattern = True 216 | # If shell is used with -c flag to run a specific command, that's acceptable 217 | if f"{pattern} -c " in command or f"{pattern.strip()} -c " in command: 218 | return True 219 | 220 | # Safe conditions: 221 | # 1. Not using a shell at all 222 | # 2. Interactive mode is explicitly requested (user knows they're getting a shell) 223 | if not has_shell_pattern: 224 | return True # Not using a shell 225 | 226 | if has_interactive and has_shell_pattern: 227 | # If interactive is explicitly requested and using a shell, 228 | # we consider it an intentional interactive shell request 229 | return True 230 | 231 | # Default: If using a shell without explicit command (-c) and not explicitly 232 | # requesting interactive mode, consider it unsafe 233 | return False 234 | 235 | 236 | def validate_k8s_command(command: str) -> None: 237 | """Validate that the command is a proper Kubernetes CLI tool command. 238 | 239 | Args: 240 | command: The Kubernetes CLI command to validate 241 | 242 | Raises: 243 | ValueError: If the command is invalid 244 | """ 245 | logger.debug(f"Validating K8s command: {command}") 246 | 247 | # Skip validation in permissive mode 248 | if SECURITY_MODE.lower() == "permissive": 249 | logger.warning(f"Running in permissive security mode, skipping validation for: {command}") 250 | return 251 | 252 | cmd_parts = shlex.split(command) 253 | if not cmd_parts: 254 | raise ValueError("Empty command") 255 | 256 | cli_tool = cmd_parts[0] 257 | if not is_valid_k8s_tool(cli_tool): 258 | raise ValueError(f"Command must start with a supported CLI tool: {', '.join(ALLOWED_K8S_TOOLS)}") 259 | 260 | if len(cmd_parts) < 2: 261 | raise ValueError(f"Command must include a {cli_tool} action") 262 | 263 | # Special case for kubectl exec 264 | if cli_tool == "kubectl" and "exec" in cmd_parts: 265 | if not is_safe_exec_command(command): 266 | raise ValueError("Interactive shells via kubectl exec are restricted. Use explicit commands or proper flags (-it, --command, etc).") 267 | 268 | # Apply regex rules for more advanced pattern matching 269 | if cli_tool in SECURITY_CONFIG.regex_rules: 270 | for rule in SECURITY_CONFIG.regex_rules[cli_tool]: 271 | pattern = re.compile(rule.pattern) 272 | if pattern.search(command): 273 | raise ValueError(rule.error_message) 274 | 275 | # Check against dangerous commands 276 | if cli_tool in SECURITY_CONFIG.dangerous_commands: 277 | for dangerous_cmd in SECURITY_CONFIG.dangerous_commands[cli_tool]: 278 | if command.startswith(dangerous_cmd): 279 | # Check if it matches a safe pattern 280 | if cli_tool in SECURITY_CONFIG.safe_patterns: 281 | if any(command.startswith(safe_pattern) for safe_pattern in SECURITY_CONFIG.safe_patterns[cli_tool]): 282 | logger.debug(f"Command matches safe pattern: {command}") 283 | return # Safe pattern match, allow command 284 | 285 | raise ValueError( 286 | f"This command ({dangerous_cmd}) is restricted for safety reasons. Please use a more specific form with resource type and name." 287 | ) 288 | 289 | logger.debug(f"Command validation successful: {command}") 290 | 291 | 292 | def validate_pipe_command(pipe_command: str) -> None: 293 | """Validate a command that contains pipes. 294 | 295 | This checks both Kubernetes CLI commands and Unix commands within a pipe chain. 296 | 297 | Args: 298 | pipe_command: The piped command to validate 299 | 300 | Raises: 301 | ValueError: If any command in the pipe is invalid 302 | """ 303 | logger.debug(f"Validating pipe command: {pipe_command}") 304 | 305 | commands = split_pipe_command(pipe_command) 306 | 307 | if not commands: 308 | raise ValueError("Empty command") 309 | 310 | # First command must be a Kubernetes CLI command 311 | validate_k8s_command(commands[0]) 312 | 313 | # Subsequent commands should be valid Unix commands 314 | for i, cmd in enumerate(commands[1:], 1): 315 | cmd_parts = shlex.split(cmd) 316 | if not cmd_parts: 317 | raise ValueError(f"Empty command at position {i} in pipe") 318 | 319 | if not validate_unix_command(cmd): 320 | raise ValueError( 321 | f"Command '{cmd_parts[0]}' at position {i} in pipe is not allowed. " 322 | f"Only kubectl, istioctl, helm, argocd commands and basic Unix utilities are permitted." 323 | ) 324 | 325 | logger.debug(f"Pipe command validation successful: {pipe_command}") 326 | 327 | 328 | def reload_security_config() -> None: 329 | """Reload security configuration from file. 330 | 331 | This allows for dynamic reloading of security rules without restarting the server. 332 | """ 333 | global SECURITY_CONFIG 334 | SECURITY_CONFIG = load_security_config() 335 | logger.info("Security configuration reloaded") 336 | 337 | 338 | def validate_command(command: str) -> None: 339 | """Centralized validation for all commands. 340 | 341 | This is the main entry point for command validation. 342 | 343 | Args: 344 | command: The command to validate 345 | 346 | Raises: 347 | ValueError: If the command is invalid 348 | """ 349 | logger.debug(f"Validating command: {command}") 350 | 351 | # Skip validation in permissive mode 352 | if SECURITY_MODE.lower() == "permissive": 353 | logger.warning(f"Running in permissive security mode, skipping validation for: {command}") 354 | return 355 | 356 | if is_pipe_command(command): 357 | validate_pipe_command(command) 358 | else: 359 | validate_k8s_command(command) 360 | 361 | logger.debug(f"Command validation successful: {command}") 362 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/server.py: -------------------------------------------------------------------------------- 1 | """Main server implementation for K8s MCP Server. 2 | 3 | This module defines the MCP server instance and tool functions for Kubernetes CLI interaction, 4 | providing a standardized interface for kubectl, istioctl, helm, and argocd command execution 5 | and documentation. 6 | """ 7 | 8 | import asyncio 9 | import logging 10 | import sys 11 | 12 | from mcp.server.fastmcp import Context, FastMCP 13 | from pydantic import Field 14 | from pydantic.fields import FieldInfo 15 | 16 | from k8s_mcp_server import __version__ 17 | from k8s_mcp_server.cli_executor import ( 18 | check_cli_installed, 19 | execute_command, 20 | get_command_help, 21 | ) 22 | from k8s_mcp_server.config import DEFAULT_TIMEOUT, INSTRUCTIONS, SUPPORTED_CLI_TOOLS 23 | from k8s_mcp_server.errors import ( 24 | AuthenticationError, 25 | CommandExecutionError, 26 | CommandTimeoutError, 27 | CommandValidationError, 28 | create_error_result, 29 | ) 30 | from k8s_mcp_server.prompts import register_prompts 31 | from k8s_mcp_server.tools import CommandHelpResult, CommandResult 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | 36 | # Function to run startup checks in synchronous context 37 | def run_startup_checks() -> dict[str, bool]: 38 | """Run startup checks to ensure Kubernetes CLI tools are installed. 39 | 40 | Returns: 41 | Dictionary of CLI tools and their installation status 42 | """ 43 | logger.info("Running startup checks...") 44 | 45 | # Check if each supported CLI tool is installed 46 | cli_status = {} 47 | for cli_tool in SUPPORTED_CLI_TOOLS: 48 | if asyncio.run(check_cli_installed(cli_tool)): 49 | logger.info(f"{cli_tool} is installed and available") 50 | cli_status[cli_tool] = True 51 | else: 52 | logger.warning(f"{cli_tool} is not installed or not in PATH") 53 | cli_status[cli_tool] = False 54 | 55 | # Verify at least kubectl is available 56 | if not cli_status.get("kubectl", False): 57 | logger.error("kubectl is required but not found. Please install kubectl.") 58 | sys.exit(1) 59 | 60 | return cli_status 61 | 62 | 63 | # Call the startup checks 64 | cli_status = run_startup_checks() 65 | 66 | # Create the FastMCP server following FastMCP best practices 67 | mcp = FastMCP( 68 | name="K8s MCP Server", 69 | instructions=INSTRUCTIONS, 70 | version=__version__, 71 | settings={"cli_status": cli_status}, 72 | ) 73 | 74 | # Register prompt templates 75 | register_prompts(mcp) 76 | 77 | 78 | async def _execute_tool_command(tool: str, command: str, timeout: int | None, ctx: Context | None) -> CommandResult: 79 | """Internal implementation for executing tool commands. 80 | 81 | Args: 82 | tool: The CLI tool name (kubectl, istioctl, helm, argocd) 83 | command: The command to execute 84 | timeout: Optional timeout in seconds 85 | ctx: Optional MCP context for request tracking 86 | 87 | Returns: 88 | CommandResult containing output and status 89 | """ 90 | logger.info(f"Executing {tool} command: {command}" + (f" with timeout: {timeout}" if timeout else "")) 91 | 92 | # Check if tool is installed 93 | if not cli_status.get(tool, False): 94 | message = f"{tool} is not installed or not in PATH" 95 | if ctx: 96 | await ctx.error(message) 97 | return CommandResult(status="error", output=message) 98 | 99 | # Handle Pydantic Field default for timeout 100 | actual_timeout = timeout 101 | if isinstance(timeout, FieldInfo) or timeout is None: 102 | actual_timeout = DEFAULT_TIMEOUT 103 | 104 | # Add tool prefix if not present 105 | if not command.strip().startswith(tool): 106 | command = f"{tool} {command}" 107 | 108 | if ctx: 109 | is_pipe = "|" in command 110 | message = "Executing" + (" piped" if is_pipe else "") + f" {tool} command" 111 | await ctx.info(message + (f" with timeout: {actual_timeout}s" if actual_timeout else "")) 112 | 113 | try: 114 | result = await execute_command(command, timeout=actual_timeout) 115 | 116 | if result["status"] == "success": 117 | if ctx: 118 | await ctx.info(f"{tool} command executed successfully") 119 | else: 120 | if ctx: 121 | await ctx.warning(f"{tool} command failed") 122 | 123 | return result 124 | except CommandValidationError as e: 125 | logger.warning(f"{tool} command validation error: {e}") 126 | if ctx: 127 | await ctx.error(f"Command validation error: {str(e)}") 128 | return create_error_result(e, command=command) 129 | except CommandExecutionError as e: 130 | logger.warning(f"{tool} command execution error: {e}") 131 | if ctx: 132 | await ctx.error(f"Command execution error: {str(e)}") 133 | return create_error_result(e, command=command) 134 | except AuthenticationError as e: 135 | logger.warning(f"{tool} command authentication error: {e}") 136 | if ctx: 137 | await ctx.error(f"Authentication error: {str(e)}") 138 | return create_error_result(e, command=command) 139 | except CommandTimeoutError as e: 140 | logger.warning(f"{tool} command timeout error: {e}") 141 | if ctx: 142 | await ctx.error(f"Command timed out: {str(e)}") 143 | return create_error_result(e, command=command) 144 | except Exception as e: 145 | logger.error(f"Error in execute_{tool}: {e}") 146 | if ctx: 147 | await ctx.error(f"Unexpected error: {str(e)}") 148 | error = CommandExecutionError(f"Unexpected error: {str(e)}", {"command": command}) 149 | return create_error_result(error, command=command) 150 | 151 | 152 | # Tool-specific command documentation functions 153 | @mcp.tool() 154 | async def describe_kubectl( 155 | command: str | None = Field(description="Specific kubectl command to get help for", default=None), 156 | ctx: Context | None = None, 157 | ) -> CommandHelpResult: 158 | """Get documentation and help text for kubectl commands. 159 | 160 | Args: 161 | command: Specific command or subcommand to get help for (e.g., 'get pods') 162 | ctx: Optional MCP context for request tracking 163 | 164 | Returns: 165 | CommandHelpResult containing the help text 166 | """ 167 | logger.info(f"Getting kubectl documentation for command: {command or 'None'}") 168 | 169 | # Check if kubectl is installed 170 | if not cli_status.get("kubectl", False): 171 | message = "kubectl is not installed or not in PATH" 172 | if ctx: 173 | await ctx.error(message) 174 | return CommandHelpResult(help_text=message, status="error") 175 | 176 | try: 177 | if ctx: 178 | await ctx.info(f"Fetching kubectl help for {command or 'general usage'}") 179 | 180 | result = await get_command_help("kubectl", command) 181 | if ctx and result.status == "error": 182 | await ctx.error(f"Error retrieving kubectl help: {result.help_text}") 183 | return result 184 | except Exception as e: 185 | logger.error(f"Error in describe_kubectl: {e}") 186 | if ctx: 187 | await ctx.error(f"Unexpected error retrieving kubectl help: {str(e)}") 188 | return CommandHelpResult(help_text=f"Error retrieving kubectl help: {str(e)}", status="error", error={"message": str(e), "code": "INTERNAL_ERROR"}) 189 | 190 | 191 | @mcp.tool() 192 | async def describe_helm( 193 | command: str | None = Field(description="Specific Helm command to get help for", default=None), 194 | ctx: Context | None = None, 195 | ) -> CommandHelpResult: 196 | """Get documentation and help text for Helm commands. 197 | 198 | Args: 199 | command: Specific command or subcommand to get help for (e.g., 'list') 200 | ctx: Optional MCP context for request tracking 201 | 202 | Returns: 203 | CommandHelpResult containing the help text 204 | """ 205 | logger.info(f"Getting Helm documentation for command: {command or 'None'}") 206 | 207 | # Check if Helm is installed 208 | if not cli_status.get("helm", False): 209 | message = "helm is not installed or not in PATH" 210 | if ctx: 211 | await ctx.error(message) 212 | return CommandHelpResult(help_text=message, status="error") 213 | 214 | try: 215 | if ctx: 216 | await ctx.info(f"Fetching Helm help for {command or 'general usage'}") 217 | 218 | result = await get_command_help("helm", command) 219 | if ctx and result.status == "error": 220 | await ctx.error(f"Error retrieving Helm help: {result.help_text}") 221 | return result 222 | except Exception as e: 223 | logger.error(f"Error in describe_helm: {e}") 224 | if ctx: 225 | await ctx.error(f"Unexpected error retrieving Helm help: {str(e)}") 226 | return CommandHelpResult(help_text=f"Error retrieving Helm help: {str(e)}", status="error", error={"message": str(e), "code": "INTERNAL_ERROR"}) 227 | 228 | 229 | @mcp.tool() 230 | async def describe_istioctl( 231 | command: str | None = Field(description="Specific Istio command to get help for", default=None), 232 | ctx: Context | None = None, 233 | ) -> CommandHelpResult: 234 | """Get documentation and help text for Istio commands. 235 | 236 | Args: 237 | command: Specific command or subcommand to get help for (e.g., 'analyze') 238 | ctx: Optional MCP context for request tracking 239 | 240 | Returns: 241 | CommandHelpResult containing the help text 242 | """ 243 | logger.info(f"Getting istioctl documentation for command: {command or 'None'}") 244 | 245 | # Check if istioctl is installed 246 | if not cli_status.get("istioctl", False): 247 | message = "istioctl is not installed or not in PATH" 248 | if ctx: 249 | await ctx.error(message) 250 | return CommandHelpResult(help_text=message, status="error") 251 | 252 | try: 253 | if ctx: 254 | await ctx.info(f"Fetching istioctl help for {command or 'general usage'}") 255 | 256 | result = await get_command_help("istioctl", command) 257 | if ctx and result.status == "error": 258 | await ctx.error(f"Error retrieving istioctl help: {result.help_text}") 259 | return result 260 | except Exception as e: 261 | logger.error(f"Error in describe_istioctl: {e}") 262 | if ctx: 263 | await ctx.error(f"Unexpected error retrieving istioctl help: {str(e)}") 264 | return CommandHelpResult(help_text=f"Error retrieving istioctl help: {str(e)}", status="error", error={"message": str(e), "code": "INTERNAL_ERROR"}) 265 | 266 | 267 | @mcp.tool() 268 | async def describe_argocd( 269 | command: str | None = Field(description="Specific ArgoCD command to get help for", default=None), 270 | ctx: Context | None = None, 271 | ) -> CommandHelpResult: 272 | """Get documentation and help text for ArgoCD commands. 273 | 274 | Args: 275 | command: Specific command or subcommand to get help for (e.g., 'app') 276 | ctx: Optional MCP context for request tracking 277 | 278 | Returns: 279 | CommandHelpResult containing the help text 280 | """ 281 | logger.info(f"Getting ArgoCD documentation for command: {command or 'None'}") 282 | 283 | # Check if ArgoCD is installed 284 | if not cli_status.get("argocd", False): 285 | message = "argocd is not installed or not in PATH" 286 | if ctx: 287 | await ctx.error(message) 288 | return CommandHelpResult(help_text=message, status="error") 289 | 290 | try: 291 | if ctx: 292 | await ctx.info(f"Fetching ArgoCD help for {command or 'general usage'}") 293 | 294 | result = await get_command_help("argocd", command) 295 | if ctx and result.status == "error": 296 | await ctx.error(f"Error retrieving ArgoCD help: {result.help_text}") 297 | return result 298 | except Exception as e: 299 | logger.error(f"Error in describe_argocd: {e}") 300 | if ctx: 301 | await ctx.error(f"Unexpected error retrieving ArgoCD help: {str(e)}") 302 | return CommandHelpResult(help_text=f"Error retrieving ArgoCD help: {str(e)}", status="error", error={"message": str(e), "code": "INTERNAL_ERROR"}) 303 | 304 | 305 | # Tool-specific command execution functions 306 | @mcp.tool( 307 | description="Execute kubectl commands with support for Unix pipes.", 308 | ) 309 | async def execute_kubectl( 310 | command: str = Field(description="Complete kubectl command to execute (including any pipes and flags)"), 311 | timeout: int | None = Field(description="Maximum execution time in seconds (default: 300)", default=None), 312 | ctx: Context | None = None, 313 | ) -> CommandResult: 314 | """Execute kubectl commands with support for Unix pipes. 315 | 316 | Executes kubectl commands with proper validation, error handling, and resource limits. 317 | Supports piping output to standard Unix utilities for filtering and transformation. 318 | 319 | Security considerations: 320 | - Commands are validated against security policies 321 | - Dangerous operations require specific resource names 322 | - Interactive shells via kubectl exec are restricted 323 | 324 | Examples: 325 | kubectl get pods 326 | kubectl get pods -o json | jq '.items[].metadata.name' 327 | kubectl describe pod my-pod 328 | kubectl logs my-pod -c my-container 329 | 330 | Args: 331 | command: Complete kubectl command to execute (can include Unix pipes) 332 | timeout: Optional timeout in seconds 333 | ctx: Optional MCP context for request tracking 334 | 335 | Returns: 336 | CommandResult containing output and status with structured error information 337 | """ 338 | return await _execute_tool_command("kubectl", command, timeout, ctx) 339 | 340 | 341 | @mcp.tool( 342 | description="Execute Helm commands with support for Unix pipes.", 343 | ) 344 | async def execute_helm( 345 | command: str = Field(description="Complete Helm command to execute (including any pipes and flags)"), 346 | timeout: int | None = Field(description="Maximum execution time in seconds (default: 300)", default=None), 347 | ctx: Context | None = None, 348 | ) -> CommandResult: 349 | """Execute Helm commands with support for Unix pipes. 350 | 351 | Executes Helm commands with proper validation, error handling, and resource limits. 352 | Supports piping output to standard Unix utilities for filtering and transformation. 353 | 354 | Security considerations: 355 | - Commands are validated against security policies 356 | - Dangerous operations like delete/uninstall require confirmation 357 | 358 | Examples: 359 | helm list 360 | helm status my-release 361 | helm get values my-release 362 | helm get values my-release -o json | jq '.global' 363 | 364 | Args: 365 | command: Complete Helm command to execute (can include Unix pipes) 366 | timeout: Optional timeout in seconds 367 | ctx: Optional MCP context for request tracking 368 | 369 | Returns: 370 | CommandResult containing output and status with structured error information 371 | """ 372 | return await _execute_tool_command("helm", command, timeout, ctx) 373 | 374 | 375 | @mcp.tool( 376 | description="Execute Istio commands with support for Unix pipes.", 377 | ) 378 | async def execute_istioctl( 379 | command: str = Field(description="Complete Istio command to execute (including any pipes and flags)"), 380 | timeout: int | None = Field(description="Maximum execution time in seconds (default: 300)", default=None), 381 | ctx: Context | None = None, 382 | ) -> CommandResult: 383 | """Execute Istio commands with support for Unix pipes. 384 | 385 | Executes istioctl commands with proper validation, error handling, and resource limits. 386 | Supports piping output to standard Unix utilities for filtering and transformation. 387 | 388 | Security considerations: 389 | - Commands are validated against security policies 390 | - Experimental commands and proxy-config access are restricted 391 | 392 | Examples: 393 | istioctl version 394 | istioctl analyze 395 | istioctl proxy-status 396 | istioctl dashboard kiali 397 | 398 | Args: 399 | command: Complete Istio command to execute (can include Unix pipes) 400 | timeout: Optional timeout in seconds 401 | ctx: Optional MCP context for request tracking 402 | 403 | Returns: 404 | CommandResult containing output and status with structured error information 405 | """ 406 | return await _execute_tool_command("istioctl", command, timeout, ctx) 407 | 408 | 409 | @mcp.tool( 410 | description="Execute ArgoCD commands with support for Unix pipes.", 411 | ) 412 | async def execute_argocd( 413 | command: str = Field(description="Complete ArgoCD command to execute (including any pipes and flags)"), 414 | timeout: int | None = Field(description="Maximum execution time in seconds (default: 300)", default=None), 415 | ctx: Context | None = None, 416 | ) -> CommandResult: 417 | """Execute ArgoCD commands with support for Unix pipes. 418 | 419 | Executes ArgoCD commands with proper validation, error handling, and resource limits. 420 | Supports piping output to standard Unix utilities for filtering and transformation. 421 | 422 | Security considerations: 423 | - Commands are validated against security policies 424 | - Destructive operations like app delete and repo removal are restricted 425 | 426 | Examples: 427 | argocd app list 428 | argocd app get my-app 429 | argocd cluster list 430 | argocd repo list 431 | 432 | Args: 433 | command: Complete ArgoCD command to execute (can include Unix pipes) 434 | timeout: Optional timeout in seconds 435 | ctx: Optional MCP context for request tracking 436 | 437 | Returns: 438 | CommandResult containing output and status with structured error information 439 | """ 440 | return await _execute_tool_command("argocd", command, timeout, ctx) 441 | -------------------------------------------------------------------------------- /src/k8s_mcp_server/tools.py: -------------------------------------------------------------------------------- 1 | """Command utilities for K8s MCP Server. 2 | 3 | This module provides core utilities for validating and working with Kubernetes commands, 4 | including helper functions for command parsing and validation. It focuses on the command 5 | structure and validation requirements, not execution logic. 6 | """ 7 | 8 | import shlex 9 | from dataclasses import dataclass 10 | from typing import Literal, NotRequired, TypedDict 11 | 12 | # List of allowed Unix commands that can be used in a pipe 13 | ALLOWED_UNIX_COMMANDS = [ 14 | # File operations 15 | "cat", 16 | "ls", 17 | "cd", 18 | "pwd", 19 | "cp", 20 | "mv", 21 | "rm", 22 | "mkdir", 23 | "touch", 24 | "chmod", 25 | "chown", 26 | # Text processing 27 | "grep", 28 | "sed", 29 | "awk", 30 | "cut", 31 | "sort", 32 | "uniq", 33 | "wc", 34 | "head", 35 | "tail", 36 | "tr", 37 | "find", 38 | # System information 39 | "ps", 40 | "top", 41 | "df", 42 | "du", 43 | "uname", 44 | "whoami", 45 | "date", 46 | "which", 47 | "echo", 48 | # Networking 49 | "ping", 50 | "ifconfig", 51 | "netstat", 52 | "curl", 53 | "wget", 54 | "dig", 55 | "nslookup", 56 | "ssh", 57 | "scp", 58 | # Other utilities 59 | "man", 60 | "less", 61 | "tar", 62 | "gzip", 63 | "gunzip", 64 | "zip", 65 | "unzip", 66 | "xargs", 67 | "jq", # JSON processor 68 | "yq", # YAML processor 69 | "tee", 70 | "column", # Table formatting 71 | "watch", # Repeat command execution 72 | ] 73 | 74 | # List of allowed Kubernetes CLI tools 75 | ALLOWED_K8S_TOOLS = [ 76 | "kubectl", 77 | "istioctl", 78 | "helm", 79 | "argocd", 80 | ] 81 | 82 | 83 | class ErrorDetailsNested(TypedDict, total=False): 84 | """Type definition for nested error details.""" 85 | 86 | command: str 87 | exit_code: int 88 | stderr: str 89 | 90 | 91 | class ErrorDetails(TypedDict, total=False): 92 | """Type definition for detailed error information matching the spec.""" 93 | 94 | message: str 95 | code: str 96 | details: ErrorDetailsNested # Use the nested type here 97 | 98 | 99 | class CommandResult(TypedDict): 100 | """Type definition for command execution results following the specification.""" 101 | 102 | status: Literal["success", "error"] 103 | output: str 104 | exit_code: NotRequired[int] 105 | execution_time: NotRequired[float] 106 | error: NotRequired[ErrorDetails] 107 | 108 | 109 | @dataclass 110 | class CommandHelpResult: 111 | """Type definition for command help results.""" 112 | 113 | help_text: str 114 | status: str = "success" 115 | error: ErrorDetails | None = None 116 | 117 | 118 | def is_valid_k8s_tool(command: str) -> bool: 119 | """Check if a command starts with a valid Kubernetes CLI tool. 120 | 121 | Args: 122 | command: The command to check 123 | 124 | Returns: 125 | True if the command starts with a valid Kubernetes CLI tool, False otherwise 126 | """ 127 | cmd_parts = shlex.split(command) 128 | if not cmd_parts: 129 | return False 130 | 131 | return cmd_parts[0] in ALLOWED_K8S_TOOLS 132 | 133 | 134 | def validate_unix_command(command: str) -> bool: 135 | """Validate that a command is an allowed Unix command. 136 | 137 | Args: 138 | command: The Unix command to validate 139 | 140 | Returns: 141 | True if the command is valid, False otherwise 142 | """ 143 | cmd_parts = shlex.split(command) 144 | if not cmd_parts: 145 | return False 146 | 147 | # Check if the command is in the allowed list 148 | return cmd_parts[0] in ALLOWED_UNIX_COMMANDS 149 | 150 | 151 | def is_pipe_command(command: str) -> bool: 152 | """Check if a command contains a pipe operator. 153 | 154 | Args: 155 | command: The command to check 156 | 157 | Returns: 158 | True if the command contains a pipe operator, False otherwise 159 | """ 160 | # Simple check for pipe operator that's not inside quotes 161 | in_single_quote = False 162 | in_double_quote = False 163 | 164 | for i, char in enumerate(command): 165 | if char == "'" and (i == 0 or command[i - 1] != "\\"): 166 | in_single_quote = not in_single_quote 167 | elif char == '"' and (i == 0 or command[i - 1] != "\\"): 168 | in_double_quote = not in_double_quote 169 | elif char == "|" and not in_single_quote and not in_double_quote: 170 | return True 171 | 172 | return False 173 | 174 | 175 | def split_pipe_command(pipe_command: str) -> list[str]: 176 | """Split a piped command into individual commands. 177 | 178 | Args: 179 | pipe_command: The piped command string 180 | 181 | Returns: 182 | List of individual command strings 183 | """ 184 | if not pipe_command: 185 | return [""] # Return a list with an empty string for empty input 186 | 187 | commands = [] 188 | current_command = "" 189 | in_single_quote = False 190 | in_double_quote = False 191 | 192 | for i, char in enumerate(pipe_command): 193 | if char == "'" and (i == 0 or pipe_command[i - 1] != "\\"): 194 | in_single_quote = not in_single_quote 195 | current_command += char 196 | elif char == '"' and (i == 0 or pipe_command[i - 1] != "\\"): 197 | in_double_quote = not in_double_quote 198 | current_command += char 199 | elif char == "|" and not in_single_quote and not in_double_quote: 200 | commands.append(current_command.strip()) 201 | current_command = "" 202 | else: 203 | current_command += char 204 | 205 | if current_command.strip(): 206 | commands.append(current_command.strip()) 207 | 208 | return commands 209 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for the K8s MCP Server.""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test fixtures for the K8s MCP Server tests.""" 2 | 3 | from unittest.mock import AsyncMock, patch 4 | 5 | import pytest 6 | 7 | 8 | # Standard mock types for consistent mocking 9 | @pytest.fixture 10 | def standard_async_mock(): 11 | """Create a standard AsyncMock with consistent default settings.""" 12 | return AsyncMock() 13 | 14 | 15 | @pytest.fixture 16 | def successful_command_mock(): 17 | """Create a mock for a successful command execution.""" 18 | mock = AsyncMock() 19 | mock.return_value = {"status": "success", "output": "Command executed successfully"} 20 | return mock 21 | 22 | 23 | @pytest.fixture 24 | def error_command_mock(): 25 | """Create a mock for a failed command execution.""" 26 | mock = AsyncMock() 27 | mock.return_value = {"status": "error", "output": "Command execution failed"} 28 | return mock 29 | 30 | 31 | @pytest.fixture 32 | def mock_k8s_cli_installed(): 33 | """Fixture that mocks the check_cli_installed function to always return True.""" 34 | with patch("k8s_mcp_server.cli_executor.check_cli_installed", return_value=True): 35 | yield 36 | 37 | 38 | @pytest.fixture 39 | def mock_k8s_cli_status(): 40 | """Fixture that mocks the CLI status dictionary to show all tools as installed.""" 41 | status = {"kubectl": True, "istioctl": True, "helm": True, "argocd": True} 42 | with patch("k8s_mcp_server.server.cli_status", status): 43 | yield 44 | 45 | 46 | @pytest.fixture 47 | def mock_k8s_tools(monkeypatch): 48 | """Mock all K8s CLI tools as installed. 49 | 50 | This provides a single fixture to mock the CLI tool status and related checks. 51 | """ 52 | # Mock CLI status 53 | status = {"kubectl": True, "istioctl": True, "helm": True, "argocd": True} 54 | monkeypatch.setattr("k8s_mcp_server.server.cli_status", status) 55 | 56 | # Mock installed check function 57 | monkeypatch.setattr("k8s_mcp_server.cli_executor.check_cli_installed", lambda _: True) 58 | 59 | return status 60 | 61 | 62 | @pytest.fixture 63 | def mock_execute_command(): 64 | """Fixture that mocks the execute_command function.""" 65 | mock = AsyncMock() 66 | mock.return_value = {"status": "success", "output": "Mocked command output"} 67 | with patch("k8s_mcp_server.cli_executor.execute_command", mock): 68 | yield mock 69 | 70 | 71 | @pytest.fixture 72 | def mock_get_command_help(): 73 | """Fixture that mocks the get_command_help function.""" 74 | from k8s_mcp_server.tools import CommandHelpResult 75 | 76 | mock = AsyncMock() 77 | mock.return_value = CommandHelpResult(help_text="Mocked help text", status="success") 78 | with patch("k8s_mcp_server.server.get_command_help", mock): 79 | yield mock 80 | 81 | 82 | def mock_command_execution(return_value=None): 83 | """Create a context manager that mocks command execution.""" 84 | if return_value is None: 85 | return_value = {"status": "success", "output": "Mocked command output"} 86 | 87 | return patch("k8s_mcp_server.cli_executor.execute_command", new_callable=AsyncMock, return_value=return_value) 88 | 89 | 90 | # We use the default event_loop fixture provided by pytest-asyncio 91 | # If we need custom loop handling, we can use pytest_asyncio.event_loop_policy fixture instead 92 | # @pytest.fixture 93 | # def event_loop(): 94 | # """Fixture that yields an event loop for async tests.""" 95 | # loop = asyncio.get_event_loop_policy().new_event_loop() 96 | # yield loop 97 | # loop.close() 98 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """Helper utilities for K8s MCP Server tests.""" 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | import os 7 | import subprocess 8 | import time 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | async def assert_command_executed(mock_obj, expected_command=None): 16 | """Assert that a command was executed with the mock.""" 17 | assert mock_obj.called, "Command execution was not called" 18 | 19 | if expected_command: 20 | called_with = mock_obj.call_args[0][0] 21 | assert expected_command in called_with, f"Expected {expected_command} in {called_with}" 22 | 23 | return mock_obj.call_args 24 | 25 | 26 | def create_test_pod_manifest(name="test-pod", namespace=None, image="nginx:alpine", labels=None, annotations=None): 27 | """Create a test pod manifest for integration tests. 28 | 29 | Args: 30 | name: Pod name 31 | namespace: Kubernetes namespace 32 | image: Container image 33 | labels: Optional dict of labels to add 34 | annotations: Optional dict of annotations to add 35 | 36 | Returns: 37 | Dictionary with the pod manifest 38 | """ 39 | metadata = {"name": name} 40 | 41 | if namespace: 42 | metadata["namespace"] = namespace 43 | 44 | if labels: 45 | metadata["labels"] = labels 46 | 47 | if annotations: 48 | metadata["annotations"] = annotations 49 | 50 | return { 51 | "apiVersion": "v1", 52 | "kind": "Pod", 53 | "metadata": metadata, 54 | "spec": { 55 | "containers": [ 56 | {"name": name.replace("-", ""), "image": image, "resources": {"limits": {"memory": "128Mi", "cpu": "100m"}}, "ports": [{"containerPort": 80}]} 57 | ] 58 | }, 59 | } 60 | 61 | 62 | def create_test_deployment_manifest(namespace, name="test-deployment", replicas=1, image="nginx:alpine", labels=None): 63 | """Create a test deployment manifest for integration tests. 64 | 65 | Args: 66 | namespace: Kubernetes namespace 67 | name: Deployment name 68 | replicas: Number of replicas 69 | image: Container image 70 | labels: Optional dict of labels to add 71 | 72 | Returns: 73 | YAML manifest as string 74 | """ 75 | app_label = name 76 | 77 | # Format additional labels if provided 78 | labels_yaml = f" app: {app_label}\n" 79 | if labels: 80 | for k, v in labels.items(): 81 | labels_yaml += f" {k}: {v}\n" 82 | 83 | return f"""apiVersion: apps/v1 84 | kind: Deployment 85 | metadata: 86 | name: {name} 87 | namespace: {namespace} 88 | spec: 89 | replicas: {replicas} 90 | selector: 91 | matchLabels: 92 | app: {app_label} 93 | template: 94 | metadata: 95 | labels: 96 | {labels_yaml} spec: 97 | containers: 98 | - name: {name.replace("-", "")} 99 | image: {image} 100 | resources: 101 | limits: 102 | memory: "128Mi" 103 | cpu: "100m" 104 | ports: 105 | - containerPort: 80 106 | """ 107 | 108 | 109 | def create_test_service_manifest(namespace, name="test-service", selector=None, port=80, target_port=80, service_type="ClusterIP"): 110 | """Create a test service manifest for integration tests. 111 | 112 | Args: 113 | namespace: Kubernetes namespace 114 | name: Service name 115 | selector: Dict of pod selector labels (defaults to app=name) 116 | port: Service port 117 | target_port: Target port on pods 118 | service_type: Kubernetes service type 119 | 120 | Returns: 121 | YAML manifest as string 122 | """ 123 | if selector is None: 124 | selector = {"app": name} 125 | 126 | # Format selector 127 | selector_yaml = " selector:\n" 128 | for k, v in selector.items(): 129 | selector_yaml += f" {k}: {v}\n" 130 | 131 | return f"""apiVersion: v1 132 | kind: Service 133 | metadata: 134 | name: {name} 135 | namespace: {namespace} 136 | spec: 137 | type: {service_type} 138 | {selector_yaml} ports: 139 | - port: {port} 140 | targetPort: {target_port} 141 | """ 142 | 143 | 144 | async def wait_for_pod_ready(namespace: str, name: str = "test-pod", timeout: int = 30, context: str = None) -> bool: 145 | """Wait for a pod to be ready or running, useful in integration tests. 146 | 147 | Args: 148 | namespace: Kubernetes namespace 149 | name: Pod name 150 | timeout: Timeout in seconds 151 | context: Kubernetes context (optional) 152 | 153 | Returns: 154 | True if pod is ready/running, False if timeout 155 | """ 156 | logger.info(f"Waiting for pod {name} in namespace {namespace} to be ready (timeout: {timeout}s)") 157 | start_time = asyncio.get_event_loop().time() 158 | last_phase = None 159 | check_interval = 1 # Initial check interval 160 | retries = 0 161 | 162 | while (asyncio.get_event_loop().time() - start_time) < timeout: 163 | try: 164 | from k8s_mcp_server.server import execute_kubectl 165 | 166 | # Build command with optional context 167 | context_arg = f" --context={context}" if context else "" 168 | cmd = f"get pod {name} -n {namespace}{context_arg} -o json" 169 | 170 | result = await execute_kubectl(command=cmd) 171 | 172 | if result["status"] == "success": 173 | try: 174 | pod_data = json.loads(result["output"]) 175 | phase = pod_data.get("status", {}).get("phase", "Unknown") 176 | 177 | # If phase changed, log it 178 | if phase != last_phase: 179 | logger.info(f"Pod {name} phase: {phase}") 180 | last_phase = phase 181 | 182 | # Check if pod is running or completed 183 | if phase in ("Running", "Succeeded"): 184 | logger.info(f"Pod {name} is now {phase}") 185 | return True 186 | 187 | # Check for failures 188 | if phase == "Failed": 189 | logger.warning(f"Pod {name} has failed state: {pod_data.get('status', {})}") 190 | return False 191 | 192 | except json.JSONDecodeError: 193 | logger.warning(f"Could not parse pod JSON: {result['output'][:200]}...") 194 | else: 195 | # Check if error is because pod doesn't exist yet 196 | error_msg = result.get("error", {}).get("message", "") 197 | if "not found" in error_msg and retries < 5: 198 | logger.info(f"Pod {name} not found yet, retrying...") 199 | else: 200 | logger.warning(f"Error checking pod status: {error_msg}") 201 | except Exception as e: 202 | logger.warning(f"Exception while waiting for pod: {str(e)}") 203 | 204 | # Increase check interval with backoff, capped at 3 seconds 205 | retries += 1 206 | check_interval = min(3, 0.5 * retries) 207 | await asyncio.sleep(check_interval) 208 | 209 | logger.warning(f"Timeout waiting for pod {name} to be ready") 210 | return False 211 | 212 | 213 | async def wait_for_deployment_ready(namespace: str, name: str, timeout: int = 60, expected_replicas: int = 1, context: str = None) -> bool: 214 | """Wait for a deployment to be ready, useful in integration tests. 215 | 216 | Args: 217 | namespace: Kubernetes namespace 218 | name: Deployment name 219 | timeout: Timeout in seconds 220 | expected_replicas: Expected number of ready replicas 221 | context: Kubernetes context (optional) 222 | 223 | Returns: 224 | True if deployment is ready, False if timeout 225 | """ 226 | logger.info(f"Waiting for deployment {name} to have {expected_replicas} ready replicas (timeout: {timeout}s)") 227 | start_time = asyncio.get_event_loop().time() 228 | last_status = None 229 | check_interval = 1 # Initial check interval 230 | 231 | while (asyncio.get_event_loop().time() - start_time) < timeout: 232 | try: 233 | from k8s_mcp_server.server import execute_kubectl 234 | 235 | # Build command with optional context 236 | context_arg = f" --context={context}" if context else "" 237 | cmd = f"get deployment {name} -n {namespace}{context_arg} -o json" 238 | 239 | result = await execute_kubectl(command=cmd) 240 | 241 | if result["status"] == "success": 242 | try: 243 | deployment_data = json.loads(result["output"]) 244 | status = deployment_data.get("status", {}) 245 | available_replicas = status.get("availableReplicas", 0) 246 | ready_replicas = status.get("readyReplicas", 0) 247 | 248 | # Create status summary 249 | status_summary = f"available={available_replicas}, ready={ready_replicas}, expected={expected_replicas}" 250 | 251 | # If status changed, log it 252 | if status_summary != last_status: 253 | logger.info(f"Deployment {name} status: {status_summary}") 254 | last_status = status_summary 255 | 256 | # Check if deployment is ready 257 | if ready_replicas >= expected_replicas: 258 | logger.info(f"Deployment {name} is ready with {ready_replicas} replicas") 259 | return True 260 | 261 | except json.JSONDecodeError: 262 | logger.warning(f"Could not parse deployment JSON: {result['output'][:200]}...") 263 | else: 264 | logger.warning(f"Error checking deployment status: {result.get('error', {}).get('message', '')}") 265 | except Exception as e: 266 | logger.warning(f"Exception while waiting for deployment: {str(e)}") 267 | 268 | # Use exponential backoff for check interval, capped at 5 seconds 269 | check_interval = min(5, check_interval * 1.5) 270 | await asyncio.sleep(check_interval) 271 | 272 | logger.warning(f"Timeout waiting for deployment {name} to be ready") 273 | return False 274 | 275 | 276 | def capture_k8s_diagnostics(namespace: str, context: str = None, pod_name: str = None, output_dir: Path | None = None) -> dict[str, Any]: 277 | """Capture Kubernetes diagnostics for debugging. 278 | 279 | Args: 280 | namespace: Kubernetes namespace 281 | context: Kubernetes context (optional) 282 | pod_name: Specific pod name (optional) 283 | output_dir: Directory to save diagnostic files (optional) 284 | 285 | Returns: 286 | Dictionary with diagnostic information 287 | """ 288 | diagnostics = {"timestamp": time.time(), "namespace": namespace, "context": context, "pod_name": pod_name, "results": {}} 289 | 290 | kubeconfig = os.environ.get("KUBECONFIG") 291 | context_args = ["--context", context] if context else [] 292 | kubeconfig_args = ["--kubeconfig", kubeconfig] if kubeconfig else [] 293 | namespace_args = ["--namespace", namespace] if namespace else [] 294 | 295 | # Define commands to run for diagnostics 296 | commands = [ 297 | {"name": "describe_namespace", "cmd": ["kubectl", "describe", "namespace", namespace] + context_args + kubeconfig_args, "skip_if_no_pod": False}, 298 | {"name": "get_pods", "cmd": ["kubectl", "get", "pods"] + namespace_args + context_args + kubeconfig_args, "skip_if_no_pod": False}, 299 | {"name": "describe_pod", "cmd": ["kubectl", "describe", "pod", pod_name] + namespace_args + context_args + kubeconfig_args, "skip_if_no_pod": True}, 300 | { 301 | "name": "get_pod_yaml", 302 | "cmd": ["kubectl", "get", "pod", pod_name, "-o", "yaml"] + namespace_args + context_args + kubeconfig_args, 303 | "skip_if_no_pod": True, 304 | }, 305 | { 306 | "name": "get_events", 307 | "cmd": ["kubectl", "get", "events", "--sort-by=.lastTimestamp"] + namespace_args + context_args + kubeconfig_args, 308 | "skip_if_no_pod": False, 309 | }, 310 | ] 311 | 312 | # Run diagnostic commands 313 | for cmd_spec in commands: 314 | if pod_name is None and cmd_spec["skip_if_no_pod"]: 315 | continue 316 | 317 | try: 318 | result = subprocess.run(cmd_spec["cmd"], capture_output=True, text=True, timeout=10) 319 | cmd_result = {"returncode": result.returncode, "stdout": result.stdout, "stderr": result.stderr, "success": result.returncode == 0} 320 | 321 | # Save outputs to files if output_dir is provided 322 | if output_dir: 323 | output_dir.mkdir(exist_ok=True, parents=True) 324 | output_file = output_dir / f"{cmd_spec['name']}.txt" 325 | with open(output_file, "w") as f: 326 | f.write(f"COMMAND: {' '.join(cmd_spec['cmd'])}\n") 327 | f.write(f"RETURN CODE: {result.returncode}\n\n") 328 | f.write("STDOUT:\n") 329 | f.write(result.stdout or "") 330 | f.write("\n\nSTDERR:\n") 331 | f.write(result.stderr or "") 332 | 333 | except subprocess.TimeoutExpired: 334 | cmd_result = {"returncode": -1, "stdout": "", "stderr": "Command timed out", "success": False} 335 | except Exception as e: 336 | cmd_result = {"returncode": -1, "stdout": "", "stderr": f"Error: {str(e)}", "success": False} 337 | 338 | diagnostics["results"][cmd_spec["name"]] = cmd_result 339 | 340 | return diagnostics 341 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the K8s MCP Server.""" 2 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | # File: tests/integration/conftest.py 2 | import os 3 | import subprocess 4 | import tempfile 5 | import time 6 | import uuid 7 | from collections.abc import Generator 8 | from contextlib import contextmanager 9 | 10 | import pytest 11 | 12 | 13 | class KubernetesClusterManager: 14 | """Manager class for Kubernetes cluster operations during tests.""" 15 | 16 | def __init__(self): 17 | self.context = os.environ.get("K8S_CONTEXT") 18 | self.use_existing = os.environ.get("K8S_MCP_TEST_USE_EXISTING_CLUSTER", "false").lower() == "true" 19 | self.skip_cleanup = os.environ.get("K8S_SKIP_CLEANUP", "").lower() == "true" 20 | 21 | def get_context_args(self): 22 | """Get the command line arguments for kubectl context.""" 23 | return ["--context", self.context] if self.context else [] 24 | 25 | def verify_connection(self): 26 | """Verify connection to the Kubernetes cluster.""" 27 | try: 28 | cmd = ["kubectl", "cluster-info"] + self.get_context_args() 29 | result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=20) 30 | print(f"Cluster connection verified:\n{result.stdout[:200]}...") 31 | return True 32 | except Exception as e: 33 | print(f"Cluster connection failed: {str(e)}") 34 | return False 35 | 36 | def create_namespace(self, name=None): 37 | """Create a test namespace with optional name.""" 38 | if name is None: 39 | name = f"k8s-mcp-test-{uuid.uuid4().hex[:8]}" 40 | 41 | try: 42 | cmd = ["kubectl", "create", "namespace", name] + self.get_context_args() 43 | subprocess.run(cmd, check=True, capture_output=True, timeout=10) 44 | print(f"Created test namespace: {name}") 45 | return name 46 | except subprocess.CalledProcessError as e: 47 | if b"AlreadyExists" in e.stderr: 48 | print(f"Namespace {name} already exists, reusing") 49 | return name 50 | raise 51 | 52 | def delete_namespace(self, name): 53 | """Delete the specified namespace.""" 54 | if self.skip_cleanup: 55 | print(f"Skipping cleanup of namespace {name} as requested") 56 | return 57 | 58 | try: 59 | cmd = ["kubectl", "delete", "namespace", name, "--wait=false"] + self.get_context_args() 60 | subprocess.run(cmd, check=True, capture_output=True, timeout=10) 61 | print(f"Deleted test namespace: {name}") 62 | except Exception as e: 63 | print(f"Warning: Failed to delete namespace {name}: {str(e)}") 64 | 65 | @contextmanager 66 | def temp_namespace(self): 67 | """Context manager for a temporary namespace.""" 68 | name = self.create_namespace() 69 | try: 70 | yield name 71 | finally: 72 | self.delete_namespace(name) 73 | 74 | 75 | @pytest.fixture(scope="session") 76 | def k8s_cluster(): 77 | """Fixture that provides a KubernetesClusterManager.""" 78 | manager = KubernetesClusterManager() 79 | 80 | # Skip tests if we can't connect to the cluster 81 | if not manager.verify_connection(): 82 | pytest.skip("Cannot connect to Kubernetes cluster") 83 | 84 | return manager 85 | 86 | 87 | @pytest.fixture 88 | def k8s_namespace(k8s_cluster): 89 | """Fixture that provides a temporary namespace for tests.""" 90 | with k8s_cluster.temp_namespace() as name: 91 | yield name 92 | 93 | 94 | @pytest.fixture(scope="session", name="integration_cluster") 95 | def integration_cluster_fixture() -> Generator[str]: 96 | """Fixture to ensure a K8s cluster is available for integration tests. 97 | 98 | By default, creates a KWOK cluster for testing. This behavior can be overridden 99 | by setting the K8S_MCP_TEST_USE_EXISTING_CLUSTER environment variable to 'true'. 100 | 101 | Returns: 102 | str: The Kubernetes context name to use for tests 103 | """ 104 | use_existing = os.environ.get("K8S_MCP_TEST_USE_EXISTING_CLUSTER", "false").lower() == "true" 105 | use_kwok = os.environ.get("K8S_MCP_TEST_USE_KWOK", "true").lower() == "true" 106 | 107 | if use_existing: 108 | print("\nAttempting to use existing KUBECONFIG context for integration tests.") 109 | try: 110 | # Verify connection to the existing cluster 111 | cmd = ["kubectl", "cluster-info"] 112 | context = os.environ.get("K8S_CONTEXT") 113 | if context: 114 | cmd.extend(["--context", context]) 115 | 116 | result = subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=20) 117 | print(f"Existing cluster connection verified:\n{result.stdout[:200]}...") # Print snippet 118 | 119 | # Return the current context if not explicitly specified 120 | if not context: 121 | context = subprocess.run(["kubectl", "config", "current-context"], check=True, capture_output=True, text=True).stdout.strip() 122 | 123 | yield context 124 | print("\nSkipping cluster teardown (using existing cluster).") 125 | 126 | except FileNotFoundError: 127 | pytest.fail("`kubectl` command not found. Cannot verify existing cluster connection.", pytrace=False) 128 | except subprocess.TimeoutExpired: 129 | pytest.fail("Timed out connecting to the existing Kubernetes cluster. Check KUBECONFIG or cluster status.", pytrace=False) 130 | except subprocess.CalledProcessError as e: 131 | pytest.fail( 132 | f"Failed to connect to the existing Kubernetes cluster (Command: {' '.join(e.cmd)}). Check KUBECONFIG or cluster status.\nError: {e.stderr}", 133 | pytrace=False, 134 | ) 135 | except Exception as e: 136 | pytest.fail(f"An unexpected error occurred while verifying the existing cluster: {e}", pytrace=False) 137 | 138 | elif use_kwok: 139 | # Create a KWOK cluster for integration tests 140 | print("\nSetting up KWOK cluster for integration tests...") 141 | 142 | # Check if kwokctl is installed 143 | try: 144 | subprocess.run(["kwokctl", "--version"], check=True, capture_output=True, text=True) 145 | except FileNotFoundError: 146 | pytest.fail("kwokctl not found. Please install KWOK following the instructions at https://kwok.sigs.k8s.io/docs/user/install/", pytrace=False) 147 | 148 | # Create a unique cluster name 149 | cluster_name = f"k8s-mcp-test-{uuid.uuid4().hex[:8]}" 150 | kubeconfig_dir = tempfile.mkdtemp(prefix="kwok-kubeconfig-") 151 | kubeconfig_path = os.path.join(kubeconfig_dir, "kubeconfig") 152 | 153 | try: 154 | # Create KWOK cluster 155 | print(f"Creating KWOK cluster: {cluster_name}") 156 | create_cmd = ["kwokctl", "create", "cluster", "--name", cluster_name, "--kubeconfig", kubeconfig_path] 157 | subprocess.run(create_cmd, check=True, timeout=60) 158 | 159 | # Store the original KUBECONFIG value to restore later 160 | original_kubeconfig = os.environ.get("KUBECONFIG") 161 | 162 | # Set KUBECONFIG environment variable for the tests 163 | os.environ["KUBECONFIG"] = kubeconfig_path 164 | 165 | # Give the cluster a moment to fully initialize 166 | print("Waiting for KWOK cluster to initialize...") 167 | time.sleep(5) 168 | 169 | # Get the context name 170 | context_cmd = ["kubectl", "--kubeconfig", kubeconfig_path, "config", "current-context"] 171 | context = subprocess.run(context_cmd, check=True, capture_output=True, text=True).stdout.strip() 172 | 173 | print(f"KWOK cluster '{cluster_name}' created with context '{context}'") 174 | 175 | # Yield the context name to tests 176 | yield context 177 | 178 | # Teardown 179 | print(f"\nTearing down KWOK cluster: {cluster_name}") 180 | delete_cmd = ["kwokctl", "delete", "cluster", "--name", cluster_name] 181 | subprocess.run(delete_cmd, check=True, timeout=60) 182 | 183 | # Restore original KUBECONFIG if it existed 184 | if original_kubeconfig: 185 | os.environ["KUBECONFIG"] = original_kubeconfig 186 | else: 187 | os.environ.pop("KUBECONFIG", None) 188 | 189 | # Clean up the temporary directory 190 | try: 191 | import shutil 192 | 193 | shutil.rmtree(kubeconfig_dir, ignore_errors=True) 194 | except Exception as e: 195 | print(f"Warning: Failed to clean up temporary directory: {e}") 196 | 197 | except FileNotFoundError as e: 198 | pytest.fail(f"Command not found: {e}", pytrace=False) 199 | except subprocess.TimeoutExpired: 200 | pytest.fail("Timed out creating or deleting KWOK cluster", pytrace=False) 201 | except subprocess.CalledProcessError as e: 202 | pytest.fail(f"Failed to create or manage KWOK cluster: {e.stderr if hasattr(e, 'stderr') else str(e)}", pytrace=False) 203 | except Exception as e: 204 | pytest.fail(f"An unexpected error occurred while managing KWOK cluster: {e}", pytrace=False) 205 | 206 | # Attempt cleanup on failure 207 | try: 208 | subprocess.run(["kwokctl", "delete", "cluster", "--name", cluster_name], check=False, timeout=30) 209 | except Exception: 210 | pass 211 | else: 212 | # Assume cluster is provided by CI/external setup 213 | print("\nAssuming K8s cluster is provided by CI environment or external setup.") 214 | context = os.environ.get("K8S_CONTEXT") 215 | 216 | if not context: 217 | try: 218 | context = subprocess.run(["kubectl", "config", "current-context"], check=True, capture_output=True, text=True).stdout.strip() 219 | except Exception: 220 | context = None 221 | 222 | yield context 223 | print("\nSkipping cluster teardown (managed externally).") 224 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the K8s MCP Server.""" 2 | -------------------------------------------------------------------------------- /tests/unit/test_errors.py: -------------------------------------------------------------------------------- 1 | """Tests for the error handling module.""" 2 | 3 | from k8s_mcp_server.errors import ( 4 | AuthenticationError, 5 | CommandExecutionError, 6 | CommandTimeoutError, 7 | CommandValidationError, 8 | K8sMCPError, 9 | create_error_result, 10 | ) 11 | 12 | 13 | def test_base_error(): 14 | """Test the base error class.""" 15 | error = K8sMCPError("Test error") 16 | assert str(error) == "Test error" 17 | assert error.code == "INTERNAL_ERROR" 18 | assert error.details == {} 19 | 20 | error_with_details = K8sMCPError("Test error", "TEST_CODE", {"key": "value"}) 21 | assert str(error_with_details) == "Test error" 22 | assert error_with_details.code == "TEST_CODE" 23 | assert error_with_details.details == {"key": "value"} 24 | 25 | 26 | def test_command_validation_error(): 27 | """Test the command validation error class.""" 28 | error = CommandValidationError("Invalid command") 29 | assert str(error) == "Invalid command" 30 | assert error.code == "VALIDATION_ERROR" 31 | assert error.details == {} 32 | 33 | error_with_details = CommandValidationError("Invalid command", {"command": "kubectl get pods"}) 34 | assert str(error_with_details) == "Invalid command" 35 | assert error_with_details.code == "VALIDATION_ERROR" 36 | assert error_with_details.details == {"command": "kubectl get pods"} 37 | 38 | 39 | def test_command_execution_error(): 40 | """Test the command execution error class.""" 41 | error = CommandExecutionError("Command failed") 42 | assert str(error) == "Command failed" 43 | assert error.code == "EXECUTION_ERROR" 44 | assert error.details == {} 45 | 46 | 47 | def test_authentication_error(): 48 | """Test the authentication error class.""" 49 | error = AuthenticationError("Auth failed") 50 | assert str(error) == "Auth failed" 51 | assert error.code == "AUTH_ERROR" 52 | assert error.details == {} 53 | 54 | 55 | def test_timeout_error(): 56 | """Test the timeout error class.""" 57 | error = CommandTimeoutError("Command timed out") 58 | assert str(error) == "Command timed out" 59 | assert error.code == "TIMEOUT_ERROR" 60 | assert error.details == {} 61 | 62 | 63 | def test_create_error_result(): 64 | """Test the create_error_result function.""" 65 | error = CommandValidationError("Invalid command", {"command": "kubectl get pods"}) 66 | result = create_error_result(error, command="kubectl get pods", exit_code=1, stderr="Error output") 67 | 68 | assert result["status"] == "error" 69 | assert result["output"] == "Invalid command" 70 | assert result["exit_code"] == 1 71 | assert result["error"]["message"] == "Invalid command" 72 | assert result["error"]["code"] == "VALIDATION_ERROR" 73 | assert result["error"]["details"]["command"] == "kubectl get pods" 74 | assert result["error"]["details"]["exit_code"] == 1 75 | assert result["error"]["details"]["stderr"] == "Error output" 76 | 77 | 78 | def test_create_error_result_with_custom_details(): 79 | """Test that custom details from the error are included in the result.""" 80 | error = CommandExecutionError("Command failed", {"custom_key": "custom_value"}) 81 | result = create_error_result(error) 82 | 83 | assert result["status"] == "error" 84 | assert result["output"] == "Command failed" 85 | assert result["error"]["message"] == "Command failed" 86 | assert result["error"]["code"] == "EXECUTION_ERROR" 87 | assert result["error"]["details"]["custom_key"] == "custom_value" 88 | -------------------------------------------------------------------------------- /tests/unit/test_k8s_tools.py: -------------------------------------------------------------------------------- 1 | """Tests for all K8s CLI tool functions in the server module.""" 2 | 3 | import asyncio 4 | from unittest.mock import AsyncMock, patch 5 | 6 | import pytest 7 | 8 | from k8s_mcp_server.cli_executor import CommandExecutionError, CommandValidationError 9 | from k8s_mcp_server.server import ( 10 | describe_argocd, 11 | describe_helm, 12 | describe_istioctl, 13 | describe_kubectl, 14 | execute_argocd, 15 | execute_helm, 16 | execute_istioctl, 17 | execute_kubectl, 18 | ) 19 | 20 | # Tests for describe_* functions 21 | # ============================== 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "describe_func, tool_name, command", 26 | [ 27 | (describe_kubectl, "kubectl", "get"), 28 | (describe_helm, "helm", "list"), 29 | (describe_istioctl, "istioctl", "analyze"), 30 | (describe_argocd, "argocd", "app"), 31 | ], 32 | ) 33 | @pytest.mark.asyncio 34 | async def test_describe_tool(describe_func, tool_name, command, mock_get_command_help, mock_k8s_cli_status): 35 | """Test the describe_* tools.""" 36 | result = await describe_func(command=command) 37 | 38 | assert hasattr(result, "help_text") 39 | assert result.help_text == "Mocked help text" 40 | mock_get_command_help.assert_called_once_with(tool_name, command) 41 | 42 | 43 | @pytest.mark.parametrize( 44 | "describe_func, tool_name", 45 | [ 46 | (describe_kubectl, "kubectl"), 47 | (describe_helm, "helm"), 48 | (describe_istioctl, "istioctl"), 49 | (describe_argocd, "argocd"), 50 | ], 51 | ) 52 | @pytest.mark.asyncio 53 | async def test_describe_tool_with_context(describe_func, tool_name, mock_get_command_help, mock_k8s_cli_status): 54 | """Test the describe_* tools with context.""" 55 | mock_context = AsyncMock() 56 | result = await describe_func(command="test", ctx=mock_context) 57 | 58 | assert hasattr(result, "help_text") 59 | assert result.help_text == "Mocked help text" 60 | mock_context.info.assert_called_once() 61 | 62 | 63 | @pytest.mark.parametrize( 64 | "describe_func", 65 | [ 66 | describe_kubectl, 67 | describe_helm, 68 | describe_istioctl, 69 | describe_argocd, 70 | ], 71 | ) 72 | @pytest.mark.asyncio 73 | async def test_describe_tool_with_error(describe_func, mock_k8s_cli_status): 74 | """Test the describe_* tools when get_command_help raises an error.""" 75 | error_mock = AsyncMock(side_effect=Exception("Test error")) 76 | 77 | with patch("k8s_mcp_server.server.get_command_help", error_mock): 78 | result = await describe_func(command="test") 79 | 80 | assert hasattr(result, "help_text") 81 | assert "Error retrieving" in result.help_text 82 | assert "Test error" in result.help_text 83 | 84 | 85 | # Tests for execute_* functions 86 | # ============================== 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "execute_func, tool_name, command", 91 | [ 92 | (execute_kubectl, "kubectl", "get pods"), 93 | (execute_helm, "helm", "list"), 94 | (execute_istioctl, "istioctl", "analyze"), 95 | (execute_argocd, "argocd", "app list"), 96 | ], 97 | ) 98 | @pytest.mark.asyncio 99 | async def test_execute_tool(execute_func, tool_name, command, mock_execute_command, mock_k8s_cli_status): 100 | """Test the execute_* tools.""" 101 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 102 | result = await execute_func(command=command) 103 | 104 | assert result == mock_execute_command.return_value 105 | mock_execute_command.assert_called_once() 106 | 107 | 108 | @pytest.mark.parametrize( 109 | "execute_func", 110 | [ 111 | execute_kubectl, 112 | execute_helm, 113 | execute_istioctl, 114 | execute_argocd, 115 | ], 116 | ) 117 | @pytest.mark.asyncio 118 | async def test_execute_tool_with_context(execute_func, mock_execute_command, mock_k8s_cli_status): 119 | """Test the execute_* tools with context.""" 120 | mock_context = AsyncMock() 121 | 122 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 123 | result = await execute_func(command="test", ctx=mock_context) 124 | 125 | assert result == mock_execute_command.return_value 126 | mock_execute_command.assert_called_once() 127 | mock_context.info.assert_called() 128 | 129 | 130 | @pytest.mark.parametrize( 131 | "execute_func", 132 | [ 133 | execute_kubectl, 134 | execute_helm, 135 | execute_istioctl, 136 | execute_argocd, 137 | ], 138 | ) 139 | @pytest.mark.asyncio 140 | async def test_execute_tool_with_validation_error(execute_func, mock_k8s_cli_status): 141 | """Test the execute_* tools when validation fails.""" 142 | error_mock = AsyncMock(side_effect=CommandValidationError("Invalid command")) 143 | 144 | with patch("k8s_mcp_server.server.execute_command", error_mock): 145 | result = await execute_func(command="test") 146 | 147 | assert "status" in result 148 | assert "output" in result 149 | assert result["status"] == "error" 150 | assert "Invalid command" in result["output"] 151 | assert "error" in result 152 | assert result["error"]["code"] == "VALIDATION_ERROR" 153 | 154 | 155 | @pytest.mark.parametrize( 156 | "execute_func", 157 | [ 158 | execute_kubectl, 159 | execute_helm, 160 | execute_istioctl, 161 | execute_argocd, 162 | ], 163 | ) 164 | @pytest.mark.asyncio 165 | async def test_execute_tool_with_execution_error(execute_func, mock_k8s_cli_status): 166 | """Test the execute_* tools when execution fails.""" 167 | error_mock = AsyncMock(side_effect=CommandExecutionError("Execution failed")) 168 | 169 | with patch("k8s_mcp_server.server.execute_command", error_mock): 170 | result = await execute_func(command="test") 171 | 172 | assert "status" in result 173 | assert "output" in result 174 | assert result["status"] == "error" 175 | assert "Execution failed" in result["output"] 176 | assert "error" in result 177 | assert result["error"]["code"] == "EXECUTION_ERROR" 178 | 179 | 180 | @pytest.mark.asyncio 181 | async def test_tool_command_preprocessing(mock_execute_command, mock_k8s_cli_status): 182 | """Test automatic tool prefix addition.""" 183 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 184 | # Test without tool prefix 185 | await execute_kubectl("get pods") 186 | called_command = mock_execute_command.call_args[0][0] 187 | assert called_command.startswith("kubectl") 188 | 189 | # Test with existing prefix 190 | mock_execute_command.reset_mock() 191 | await execute_kubectl("kubectl get pods") 192 | called_command = mock_execute_command.call_args[0][0] 193 | assert called_command == "kubectl get pods" 194 | 195 | 196 | @pytest.mark.asyncio 197 | async def test_concurrent_command_execution(mock_k8s_cli_status): 198 | """Test parallel command execution safety.""" 199 | 200 | # Patch execute_command within the server module's scope 201 | with patch("k8s_mcp_server.server.execute_command", new_callable=AsyncMock) as mock_exec: 202 | mock_exec.return_value = {"status": "success", "output": "test"} 203 | 204 | async def run_command(): 205 | return await execute_kubectl("get pods") 206 | 207 | # Run 10 concurrent commands 208 | results = await asyncio.gather(*[run_command() for _ in range(10)]) 209 | assert all(r["status"] == "success" for r in results) 210 | assert mock_exec.call_count == 10 211 | 212 | 213 | @pytest.mark.asyncio 214 | async def test_long_running_command(mock_k8s_cli_status): 215 | """Test timeout handling for near-limit executions.""" 216 | # Patch execute_command within the server module's scope 217 | with patch("k8s_mcp_server.server.execute_command", new_callable=AsyncMock) as mock_exec: 218 | mock_exec.return_value = {"status": "error", "output": "Command timed out after 0.1 seconds"} 219 | result = await execute_kubectl("get pods", timeout=0.1) 220 | assert "timed out" in result["output"].lower() 221 | # Check that the timeout value was passed correctly to the patched function 222 | mock_exec.assert_called_once_with("kubectl get pods", timeout=0.1) 223 | -------------------------------------------------------------------------------- /tests/unit/test_main.py: -------------------------------------------------------------------------------- 1 | """Tests for the main module.""" 2 | 3 | import os 4 | import signal 5 | from unittest.mock import call, patch 6 | 7 | import pytest 8 | 9 | 10 | @pytest.mark.unit 11 | def test_main_function(): 12 | """Test the main function that starts the MCP server.""" 13 | # Mock the server's run method to prevent actually starting a server 14 | with patch("k8s_mcp_server.server.mcp.run") as mock_run: 15 | # Test with default transport (stdio) 16 | with patch.dict(os.environ, {"K8S_MCP_TRANSPORT": "stdio"}): 17 | # Import after patching to avoid actual execution 18 | from importlib import reload 19 | 20 | import k8s_mcp_server.__main__ 21 | import k8s_mcp_server.config 22 | 23 | # Reload the module to pick up the environment variable 24 | reload(k8s_mcp_server.config) 25 | reload(k8s_mcp_server.__main__) 26 | 27 | # Call the main function 28 | k8s_mcp_server.__main__.main() 29 | mock_run.assert_called_once_with(transport="stdio") 30 | 31 | # Reset the mock for the next test 32 | mock_run.reset_mock() 33 | 34 | # Test with custom transport from environment variable 35 | with patch.dict(os.environ, {"K8S_MCP_TRANSPORT": "sse"}): 36 | # Reload the modules to pick up the new environment variable 37 | reload(k8s_mcp_server.config) 38 | reload(k8s_mcp_server.__main__) 39 | 40 | # Call the main function 41 | k8s_mcp_server.__main__.main() 42 | mock_run.assert_called_once_with(transport="sse") 43 | 44 | # Reset the mock for the next test 45 | mock_run.reset_mock() 46 | 47 | # Test with invalid transport from environment variable (should default to stdio) 48 | with patch.dict(os.environ, {"K8S_MCP_TRANSPORT": "invalid"}): 49 | # Reload the modules to pick up the new environment variable 50 | reload(k8s_mcp_server.config) 51 | reload(k8s_mcp_server.__main__) 52 | 53 | # Call the main function 54 | k8s_mcp_server.__main__.main() 55 | mock_run.assert_called_once_with(transport="stdio") 56 | 57 | 58 | @pytest.mark.unit 59 | def test_graceful_shutdown_handler(): 60 | """Test the graceful shutdown handler for SIGINT signal.""" 61 | from importlib import reload 62 | 63 | import k8s_mcp_server.__main__ 64 | 65 | # Reload to ensure we have the latest version 66 | reload(k8s_mcp_server.__main__) 67 | 68 | # Mock sys.exit to prevent the test from exiting 69 | with patch("sys.exit") as mock_exit: 70 | # Create a mock logger 71 | with patch("k8s_mcp_server.__main__.logger") as mock_logger: 72 | # Call the interrupt handler 73 | k8s_mcp_server.__main__.handle_interrupt(signal.SIGINT, None) 74 | 75 | # Verify the logger was called with the correct message 76 | mock_logger.info.assert_called_once_with(f"Received signal {signal.SIGINT}, shutting down gracefully...") 77 | 78 | # Verify sys.exit was called with 0 79 | mock_exit.assert_called_once_with(0) 80 | 81 | 82 | @pytest.mark.unit 83 | def test_keyboard_interrupt_handling(): 84 | """Test that keyboard interrupts are handled gracefully.""" 85 | # Import required modules 86 | from importlib import reload 87 | 88 | import k8s_mcp_server.__main__ 89 | 90 | # Reload to ensure we have the latest version 91 | reload(k8s_mcp_server.__main__) 92 | 93 | # Mock sys.exit to prevent the test from exiting 94 | with patch("sys.exit") as mock_exit: 95 | # Create a mock logger 96 | with patch("k8s_mcp_server.__main__.logger") as mock_logger: 97 | # Mock the server run method to raise KeyboardInterrupt 98 | with patch("k8s_mcp_server.server.mcp.run", side_effect=KeyboardInterrupt): 99 | # Call the main function 100 | k8s_mcp_server.__main__.main() 101 | 102 | # Verify the logger was called with the shutdown message 103 | mock_logger.info.assert_any_call("Keyboard interrupt received. Shutting down gracefully...") 104 | 105 | # Verify sys.exit was called with 0 106 | mock_exit.assert_called_once_with(0) 107 | 108 | 109 | @pytest.mark.unit 110 | def test_signal_handler_registration(): 111 | """Test that the signal handler is registered correctly.""" 112 | # Import required modules 113 | from importlib import reload 114 | 115 | import k8s_mcp_server.__main__ 116 | 117 | # Reload to ensure we have the latest version 118 | reload(k8s_mcp_server.__main__) 119 | 120 | # Mock signal.signal to verify it's called correctly 121 | with patch("signal.signal") as mock_signal: 122 | # Mock server.mcp.run to prevent execution 123 | with patch("k8s_mcp_server.server.mcp.run"): 124 | # Call the main function 125 | k8s_mcp_server.__main__.main() 126 | 127 | # Verify both signal handlers were registered 128 | assert mock_signal.call_count == 2 129 | mock_signal.assert_has_calls( 130 | [call(signal.SIGINT, k8s_mcp_server.__main__.handle_interrupt), call(signal.SIGTERM, k8s_mcp_server.__main__.handle_interrupt)], any_order=True 131 | ) 132 | -------------------------------------------------------------------------------- /tests/unit/test_prompts.py: -------------------------------------------------------------------------------- 1 | """Tests for the prompts module.""" 2 | 3 | from unittest.mock import MagicMock 4 | 5 | from k8s_mcp_server.prompts import register_prompts 6 | 7 | 8 | def test_register_prompts(): 9 | """Test that prompts register correctly.""" 10 | # Create a mock MCP server 11 | mock_mcp = MagicMock() 12 | 13 | # Call the register_prompts function 14 | register_prompts(mock_mcp) 15 | 16 | # Verify that prompt was called for each prompt template 17 | expected_prompt_count = 10 # Update this if you change the number of prompts 18 | assert mock_mcp.prompt.call_count == expected_prompt_count 19 | 20 | # Verify that all prompts were registered with the server 21 | # Each call to mcp.prompt() returns a decorator, which is then called with the function 22 | for i in range(expected_prompt_count): 23 | assert mock_mcp.prompt.return_value.call_count >= i + 1 24 | 25 | 26 | def test_prompt_templates(): 27 | """Test that prompt templates generate expected strings.""" 28 | # Create a mock MCP server that captures the decorated functions 29 | prompt_functions = {} 30 | 31 | def mock_prompt_decorator(func): 32 | prompt_functions[func.__name__] = func 33 | return func 34 | 35 | mock_mcp = MagicMock() 36 | mock_mcp.prompt.return_value = mock_prompt_decorator 37 | 38 | # Register the prompts 39 | register_prompts(mock_mcp) 40 | 41 | # Test k8s_resource_status prompt 42 | status_prompt = prompt_functions["k8s_resource_status"] 43 | result = status_prompt("pods", "monitoring") 44 | assert "status of pods" in result 45 | assert "in the monitoring namespace" in result 46 | 47 | # Test k8s_deploy_application prompt 48 | deploy_prompt = prompt_functions["k8s_deploy_application"] 49 | result = deploy_prompt("nginx", "nginx:latest", "web", 3) 50 | assert "deploy an application named 'nginx'" in result 51 | assert "image 'nginx:latest'" in result 52 | assert "with 3 replicas" in result 53 | assert "in the web namespace" in result 54 | 55 | # Test k8s_troubleshoot prompt 56 | troubleshoot_prompt = prompt_functions["k8s_troubleshoot"] 57 | result = troubleshoot_prompt("pod", "web-server", "default") 58 | assert "troubleshoot issues with the pod" in result 59 | assert "named 'web-server'" in result 60 | assert "in the default namespace" in result 61 | 62 | # Test k8s_resource_inventory prompt with namespace 63 | inventory_prompt = prompt_functions["k8s_resource_inventory"] 64 | result = inventory_prompt("kube-system") 65 | assert "in the kube-system namespace" in result 66 | 67 | # Test k8s_resource_inventory prompt without namespace (all namespaces) 68 | result = inventory_prompt() 69 | assert "across all namespaces" in result 70 | 71 | # Test istio_service_mesh prompt 72 | istio_prompt = prompt_functions["istio_service_mesh"] 73 | result = istio_prompt("istio-system") 74 | assert "manage and analyze the Istio service mesh" in result 75 | assert "in the istio-system namespace" in result 76 | 77 | # Test helm_chart_management prompt with release name 78 | helm_prompt = prompt_functions["helm_chart_management"] 79 | result = helm_prompt("mysql", "database") 80 | assert "for release 'mysql'" in result 81 | assert "in the database namespace" in result 82 | 83 | # Test argocd_application prompt 84 | argocd_prompt = prompt_functions["argocd_application"] 85 | result = argocd_prompt("my-app") 86 | assert "for application 'my-app'" in result 87 | assert "in the argocd namespace" in result 88 | 89 | # Test argocd_application prompt without app name 90 | result = argocd_prompt() 91 | assert "for all applications" in result 92 | 93 | # Test k8s_security_check prompt with namespace 94 | security_prompt = prompt_functions["k8s_security_check"] 95 | result = security_prompt("production") 96 | assert "in the production namespace" in result 97 | 98 | # Test k8s_security_check prompt without namespace (all namespaces) 99 | result = security_prompt() 100 | assert "across the entire cluster" in result 101 | 102 | # Test k8s_resource_scaling prompt 103 | scaling_prompt = prompt_functions["k8s_resource_scaling"] 104 | result = scaling_prompt("deployment", "api-server", "services") 105 | assert "scale the deployment" in result 106 | assert "named 'api-server'" in result 107 | assert "in the services namespace" in result 108 | 109 | # Test k8s_logs_analysis prompt with container 110 | logs_prompt = prompt_functions["k8s_logs_analysis"] 111 | result = logs_prompt("backend", "app", "api") 112 | assert "container 'api' in" in result 113 | assert "pod 'backend'" in result 114 | assert "in the app namespace" in result 115 | 116 | # Test k8s_logs_analysis prompt without container 117 | result = logs_prompt("backend", "app") 118 | assert "container" not in result 119 | assert "pod 'backend'" in result 120 | -------------------------------------------------------------------------------- /tests/unit/test_security.py: -------------------------------------------------------------------------------- 1 | """Tests for the security module.""" 2 | 3 | import os 4 | from unittest.mock import MagicMock, mock_open, patch 5 | 6 | import pytest 7 | import yaml 8 | 9 | import k8s_mcp_server.security 10 | from k8s_mcp_server.security import ( 11 | DEFAULT_DANGEROUS_COMMANDS, 12 | DEFAULT_SAFE_PATTERNS, 13 | SecurityConfig, 14 | ValidationRule, 15 | is_safe_exec_command, 16 | load_security_config, 17 | reload_security_config, 18 | validate_command, 19 | validate_k8s_command, 20 | validate_pipe_command, 21 | ) 22 | 23 | 24 | def test_validation_rule_class(): 25 | """Test the ValidationRule dataclass.""" 26 | # Create a validation rule instance 27 | rule = ValidationRule( 28 | pattern="kubectl get", 29 | description="Get Kubernetes resources", 30 | error_message="Invalid get command", 31 | ) 32 | 33 | # Check the attributes 34 | assert rule.pattern == "kubectl get" 35 | assert rule.description == "Get Kubernetes resources" 36 | assert rule.error_message == "Invalid get command" 37 | 38 | 39 | def test_is_safe_exec_command_edge_cases(): 40 | """Test is_safe_exec_command with edge cases.""" 41 | # Edge case: empty command 42 | assert is_safe_exec_command("") is True # Not an exec command 43 | 44 | # Edge case: exec with quotes 45 | assert is_safe_exec_command("kubectl exec pod-name -- 'echo hello'") is True 46 | 47 | # Edge case: exec with double quotes 48 | assert is_safe_exec_command('kubectl exec pod-name -- "echo hello"') is True 49 | 50 | # Edge case: exec with various shells 51 | assert is_safe_exec_command("kubectl exec pod-name -- csh") is False 52 | assert is_safe_exec_command("kubectl exec pod-name -- ksh") is False 53 | assert is_safe_exec_command("kubectl exec pod-name -- zsh") is False 54 | 55 | # Edge case: exec with full paths 56 | assert is_safe_exec_command("kubectl exec pod-name -- /usr/bin/bash") is False 57 | assert is_safe_exec_command("kubectl exec -it pod-name -- /usr/bin/bash") is True 58 | 59 | # Edge case: exec with complex commands 60 | assert is_safe_exec_command("kubectl exec pod-name -- bash -c 'for i in {1..5}; do echo $i; done'") is True 61 | 62 | # Edge case: exec with shell but with -c flag 63 | assert is_safe_exec_command("kubectl exec pod-name -- /bin/bash -c 'ls -la'") is True 64 | assert is_safe_exec_command("kubectl exec pod-name -- sh -c 'find / -name config'") is True 65 | 66 | 67 | def test_security_config_class(): 68 | """Test the SecurityConfig dataclass.""" 69 | # Create a SecurityConfig instance 70 | dangerous_commands = {"kubectl": ["kubectl delete"]} 71 | safe_patterns = {"kubectl": ["kubectl delete pod"]} 72 | regex_rules = {"kubectl": [ValidationRule(pattern="kubectl\\s+delete\\s+--all", description="Delete all", error_message="Cannot delete all resources")]} 73 | 74 | config = SecurityConfig(dangerous_commands=dangerous_commands, safe_patterns=safe_patterns, regex_rules=regex_rules) 75 | 76 | # Assert the values were set correctly 77 | assert config.dangerous_commands == dangerous_commands 78 | assert config.safe_patterns == safe_patterns 79 | assert config.regex_rules == regex_rules 80 | 81 | # Test default initialization for regex_rules 82 | config2 = SecurityConfig(dangerous_commands=dangerous_commands, safe_patterns=safe_patterns) 83 | 84 | assert config2.regex_rules == {} 85 | 86 | 87 | def test_dangerous_and_safe_commands(): 88 | """Test the DEFAULT_DANGEROUS_COMMANDS and DEFAULT_SAFE_PATTERNS dictionaries.""" 89 | # Check that all CLI tools in DEFAULT_DANGEROUS_COMMANDS have corresponding DEFAULT_SAFE_PATTERNS 90 | for cli_tool in DEFAULT_DANGEROUS_COMMANDS: 91 | assert cli_tool in DEFAULT_SAFE_PATTERNS, f"{cli_tool} exists in DEFAULT_DANGEROUS_COMMANDS but not in DEFAULT_SAFE_PATTERNS" 92 | 93 | # Check for specific patterns we expect to be in the dictionaries 94 | assert "kubectl delete" in DEFAULT_DANGEROUS_COMMANDS["kubectl"] 95 | assert "kubectl exec" in DEFAULT_DANGEROUS_COMMANDS["kubectl"] 96 | assert "kubectl delete pod" in DEFAULT_SAFE_PATTERNS["kubectl"] 97 | assert "kubectl exec -it" in DEFAULT_SAFE_PATTERNS["kubectl"] 98 | 99 | # Check for Helm dangerous commands 100 | assert "helm delete" in DEFAULT_DANGEROUS_COMMANDS["helm"] 101 | assert "helm delete --help" in DEFAULT_SAFE_PATTERNS["helm"] 102 | 103 | 104 | def test_validate_k8s_command_edge_cases(): 105 | """Test validate_k8s_command with edge cases.""" 106 | # Commands with exec shells should be checked by is_safe_exec_command 107 | with pytest.raises(ValueError): 108 | validate_k8s_command("kubectl exec pod-name -- /bin/bash") 109 | 110 | # But commands with exec and explicit interactive flags should be allowed 111 | validate_k8s_command("kubectl exec -it pod-name -- /bin/bash") 112 | 113 | # Commands with exec and -c flag should be allowed 114 | validate_k8s_command("kubectl exec pod-name -- /bin/bash -c 'ls -la'") 115 | 116 | # Command with help should be allowed 117 | validate_k8s_command("kubectl exec --help") 118 | 119 | # Command with empty string should raise ValueError 120 | with pytest.raises(ValueError): 121 | validate_k8s_command("") 122 | 123 | # Check that non-kubectl commands are verified properly 124 | validate_k8s_command("helm list") 125 | validate_k8s_command("istioctl version") 126 | 127 | # Test dangerous commands 128 | with pytest.raises(ValueError): 129 | validate_k8s_command("helm delete") 130 | 131 | # Test safe override of dangerous command 132 | validate_k8s_command("helm delete --help") 133 | 134 | 135 | def test_regex_pattern_validation(): 136 | """Test the regex pattern validation functionality.""" 137 | # Create simplified test rules with simpler patterns that are easier to test 138 | kubectl_rule1 = ValidationRule( 139 | pattern=r"--all", # Simplified to just match --all flag 140 | description="Delete all resources", 141 | error_message="Cannot delete all resources", 142 | regex=True, 143 | ) 144 | 145 | kubectl_rule2 = ValidationRule( 146 | pattern=r"--namespace=kube-system", # Simplified to match namespace directly 147 | description="Operations in kube-system", 148 | error_message="Operations in kube-system restricted", 149 | regex=True, 150 | ) 151 | 152 | # Test that regex patterns match correctly 153 | import re 154 | 155 | # Matches for rule 1 156 | pattern1 = re.compile(kubectl_rule1.pattern) 157 | assert pattern1.search("kubectl delete pods --all") 158 | assert pattern1.search("kubectl delete -n default --all") 159 | assert not pattern1.search("kubectl delete pod my-pod") 160 | assert not pattern1.search("kubectl get pods") 161 | 162 | # Matches for rule 2 163 | pattern2 = re.compile(kubectl_rule2.pattern) 164 | assert pattern2.search("kubectl get pods --namespace=kube-system") 165 | assert pattern2.search("kubectl describe pod mypod --namespace=kube-system") 166 | assert not pattern2.search("kubectl get pods --namespace=default") 167 | assert not pattern2.search("kubectl get pods") 168 | 169 | 170 | def test_validate_k8s_command_with_mocked_regex_rules(): 171 | """Test validate_k8s_command with regex validation rules using direct mocking.""" 172 | # Save original mode and ensure we're in strict mode 173 | original_mode = os.environ.get("K8S_MCP_SECURITY_MODE", "strict") 174 | os.environ["K8S_MCP_SECURITY_MODE"] = "strict" 175 | 176 | try: 177 | # For delete --all 178 | with patch( 179 | "k8s_mcp_server.security.SECURITY_CONFIG.regex_rules", 180 | { 181 | "kubectl": [ 182 | ValidationRule( 183 | pattern=r"kubectl\s+delete\s+(-[A-Za-z]+\s+)*--all\b", 184 | description="Delete all resources", 185 | error_message="Cannot delete all resources", 186 | regex=True, 187 | ) 188 | ] 189 | }, 190 | ): 191 | with patch("re.compile") as mock_re_compile: 192 | # Set up the mock to return a pattern that will match 193 | mock_pattern = MagicMock() 194 | mock_pattern.search.return_value = True # This will trigger the ValueError 195 | mock_re_compile.return_value = mock_pattern 196 | 197 | # This should raise an error due to our mocked regex match 198 | with pytest.raises(ValueError, match="Cannot delete all resources"): 199 | validate_k8s_command("kubectl delete pods --all") 200 | 201 | # Clean pass with no regex rules 202 | with patch("k8s_mcp_server.security.SECURITY_CONFIG.regex_rules", {}): 203 | validate_k8s_command("kubectl get pods") 204 | finally: 205 | # Restore original mode 206 | if original_mode: 207 | os.environ["K8S_MCP_SECURITY_MODE"] = original_mode 208 | else: 209 | os.environ.pop("K8S_MCP_SECURITY_MODE", None) 210 | 211 | 212 | def test_validate_pipe_command_edge_cases(): 213 | """Test validate_pipe_command with edge cases.""" 214 | # Pipe command with kubectl exec should still be checked for safety 215 | with pytest.raises(ValueError): 216 | validate_pipe_command("kubectl exec pod-name -- /bin/bash | grep root") 217 | 218 | # But pipe command with kubectl exec and -it should be allowed 219 | validate_pipe_command("kubectl exec -it pod-name -- /bin/bash -c 'ls -la' | grep root") 220 | 221 | # Test pipe commands with missing parts 222 | with pytest.raises(ValueError): 223 | validate_pipe_command("| grep root") # Missing first command 224 | 225 | # Test with empty commands list 226 | with patch("k8s_mcp_server.security.split_pipe_command", return_value=[]): 227 | with pytest.raises(ValueError, match="Empty command"): 228 | validate_pipe_command("kubectl get pods | grep nginx") 229 | 230 | 231 | def test_load_security_config(): 232 | """Test loading security configuration from YAML file.""" 233 | # Define test data 234 | test_config = { 235 | "dangerous_commands": {"kubectl": ["kubectl delete", "kubectl drain"]}, 236 | "safe_patterns": {"kubectl": ["kubectl delete pod", "kubectl delete service"]}, 237 | "regex_rules": {"kubectl": [{"pattern": "kubectl\\s+delete\\s+--all", "description": "Delete all resources", "error_message": "This is dangerous"}]}, 238 | } 239 | 240 | # Mock open to return test YAML data 241 | yaml_data = yaml.dump(test_config) 242 | 243 | with patch("k8s_mcp_server.security.SECURITY_CONFIG_PATH", "dummy_path.yaml"): 244 | with patch("pathlib.Path.exists", return_value=True): 245 | with patch("builtins.open", mock_open(read_data=yaml_data)): 246 | # Call the function 247 | config = load_security_config() 248 | 249 | # Verify the results 250 | assert "kubectl" in config.dangerous_commands 251 | assert "kubectl delete" in config.dangerous_commands["kubectl"] 252 | assert "kubectl delete pod" in config.safe_patterns["kubectl"] 253 | assert len(config.regex_rules["kubectl"]) == 1 254 | assert config.regex_rules["kubectl"][0].pattern == "kubectl\\s+delete\\s+--all" 255 | assert config.regex_rules["kubectl"][0].regex is True 256 | 257 | 258 | def test_load_security_config_file_not_found(): 259 | """Test loading security config when file doesn't exist.""" 260 | with patch("k8s_mcp_server.security.SECURITY_CONFIG_PATH", "nonexistent_file.yaml"): 261 | with patch("pathlib.Path.exists", return_value=False): 262 | # Call the function 263 | config = load_security_config() 264 | 265 | # Verify default values are used 266 | assert config.dangerous_commands == DEFAULT_DANGEROUS_COMMANDS 267 | assert config.safe_patterns == DEFAULT_SAFE_PATTERNS 268 | assert config.regex_rules == {} 269 | 270 | 271 | def test_load_security_config_error_handling(): 272 | """Test error handling when loading config file.""" 273 | with patch("k8s_mcp_server.security.SECURITY_CONFIG_PATH", "invalid_file.yaml"): 274 | with patch("pathlib.Path.exists", return_value=True): 275 | with patch("builtins.open", mock_open(read_data="invalid: yaml: content:")): 276 | with patch("yaml.safe_load", side_effect=yaml.YAMLError("Invalid YAML")): 277 | # Call the function 278 | config = load_security_config() 279 | 280 | # Verify default values are used after error 281 | assert config.dangerous_commands == DEFAULT_DANGEROUS_COMMANDS 282 | assert config.safe_patterns == DEFAULT_SAFE_PATTERNS 283 | 284 | 285 | def test_reload_security_config(): 286 | """Test reloading security configuration.""" 287 | # Mock the load_security_config function to return a known value 288 | test_config = SecurityConfig( 289 | dangerous_commands={"kubectl": ["test command"]}, 290 | safe_patterns={"kubectl": ["test safe pattern"]}, 291 | ) 292 | 293 | with patch("k8s_mcp_server.security.load_security_config", return_value=test_config): 294 | # Get the global SECURITY_CONFIG before modification 295 | current_config_before = k8s_mcp_server.security.SECURITY_CONFIG 296 | 297 | try: 298 | # Call reload function 299 | reload_security_config() 300 | 301 | # Get the SECURITY_CONFIG directly from the module 302 | current_config = k8s_mcp_server.security.SECURITY_CONFIG 303 | 304 | # Verify config was updated 305 | assert "kubectl" in current_config.dangerous_commands 306 | assert "test command" in current_config.dangerous_commands["kubectl"] 307 | assert "test safe pattern" in current_config.safe_patterns["kubectl"] 308 | finally: 309 | # Restore original config 310 | k8s_mcp_server.security.SECURITY_CONFIG = current_config_before 311 | 312 | 313 | def test_permissive_security_mode(): 314 | """Test that permissive security mode bypasses validation.""" 315 | # Patch the security mode to permissive 316 | with patch("k8s_mcp_server.security.SECURITY_MODE", "permissive"): 317 | # These commands would normally be rejected 318 | validate_command("kubectl delete") 319 | validate_command("kubectl exec pod-name -- /bin/bash") 320 | 321 | # Even with pipe commands 322 | validate_command("kubectl delete | grep result") 323 | 324 | 325 | def test_validate_command(): 326 | """Test the main validate_command function.""" 327 | # Save original settings 328 | original_mode = os.environ.get("K8S_MCP_SECURITY_MODE", "strict") 329 | os.environ["K8S_MCP_SECURITY_MODE"] = "strict" 330 | 331 | try: 332 | # Test with pipe command 333 | with patch("k8s_mcp_server.security.is_pipe_command", return_value=True): 334 | with patch("k8s_mcp_server.security.validate_pipe_command") as mock_validate_pipe: 335 | validate_command("kubectl get pods | grep nginx") 336 | mock_validate_pipe.assert_called_once_with("kubectl get pods | grep nginx") 337 | 338 | # Test with non-pipe command 339 | with patch("k8s_mcp_server.security.is_pipe_command", return_value=False): 340 | with patch("k8s_mcp_server.security.validate_k8s_command") as mock_validate_k8s: 341 | validate_command("kubectl get pods") 342 | mock_validate_k8s.assert_called_once_with("kubectl get pods") 343 | 344 | # Test dangerous commands with direct mocking 345 | with patch("k8s_mcp_server.security.is_pipe_command", return_value=False): 346 | with patch("k8s_mcp_server.security.validate_k8s_command", side_effect=ValueError("Test error")): 347 | with pytest.raises(ValueError, match="Test error"): 348 | validate_command("kubectl delete") 349 | 350 | with patch("k8s_mcp_server.security.is_pipe_command", return_value=False): 351 | with patch("k8s_mcp_server.security.validate_k8s_command", side_effect=ValueError("Shell error")): 352 | with pytest.raises(ValueError, match="Shell error"): 353 | validate_command("kubectl exec pod-name -- /bin/bash") 354 | finally: 355 | # Restore original settings 356 | if original_mode: 357 | os.environ["K8S_MCP_SECURITY_MODE"] = original_mode 358 | else: 359 | os.environ.pop("K8S_MCP_SECURITY_MODE", None) 360 | -------------------------------------------------------------------------------- /tests/unit/test_tool_specific.py: -------------------------------------------------------------------------------- 1 | """Tests for tool-specific functions in the server module.""" 2 | 3 | from unittest.mock import AsyncMock, patch 4 | 5 | import pytest 6 | 7 | from k8s_mcp_server.cli_executor import CommandValidationError 8 | from k8s_mcp_server.server import ( 9 | describe_argocd, 10 | describe_helm, 11 | describe_istioctl, 12 | describe_kubectl, 13 | execute_argocd, 14 | execute_helm, 15 | execute_istioctl, 16 | execute_kubectl, 17 | ) 18 | 19 | 20 | @pytest.mark.unit 21 | @pytest.mark.asyncio 22 | async def test_describe_kubectl(mock_get_command_help, mock_k8s_cli_status): 23 | """Test the describe_kubectl tool.""" 24 | # Test with valid command 25 | result = await describe_kubectl(command="get") 26 | 27 | assert result.help_text == "Mocked help text" 28 | mock_get_command_help.assert_called_once_with("kubectl", "get") 29 | 30 | # Test without command (general help) 31 | mock_get_command_help.reset_mock() 32 | result = await describe_kubectl() 33 | 34 | assert result.help_text == "Mocked help text" 35 | mock_get_command_help.assert_called_once() 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_describe_kubectl_with_context(mock_get_command_help, mock_k8s_cli_status): 40 | """Test the describe_kubectl tool with context.""" 41 | # Create a mock context 42 | mock_context = AsyncMock() 43 | 44 | # Test with valid command 45 | result = await describe_kubectl(command="get", ctx=mock_context) 46 | 47 | assert result.help_text == "Mocked help text" 48 | mock_get_command_help.assert_called_once_with("kubectl", "get") 49 | mock_context.info.assert_called_once() 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_describe_kubectl_with_error(mock_k8s_cli_status): 54 | """Test the describe_kubectl tool when get_command_help raises an error.""" 55 | # Create a mock that raises an exception 56 | error_mock = AsyncMock(side_effect=Exception("Test error")) 57 | 58 | with patch("k8s_mcp_server.server.get_command_help", error_mock): 59 | result = await describe_kubectl(command="get") 60 | 61 | assert "Error retrieving kubectl help" in result.help_text 62 | assert "Test error" in result.help_text 63 | 64 | 65 | @pytest.mark.asyncio 66 | async def test_describe_helm(mock_get_command_help, mock_k8s_cli_status): 67 | """Test the describe_helm tool.""" 68 | # Test with valid command 69 | result = await describe_helm(command="list") 70 | 71 | assert result.help_text == "Mocked help text" 72 | mock_get_command_help.assert_called_once_with("helm", "list") 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_describe_istioctl(mock_get_command_help, mock_k8s_cli_status): 77 | """Test the describe_istioctl tool.""" 78 | # Test with valid command 79 | result = await describe_istioctl(command="analyze") 80 | 81 | assert result.help_text == "Mocked help text" 82 | mock_get_command_help.assert_called_once_with("istioctl", "analyze") 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_describe_argocd(mock_get_command_help, mock_k8s_cli_status): 87 | """Test the describe_argocd tool.""" 88 | # Test with valid command 89 | result = await describe_argocd(command="app") 90 | 91 | assert result.help_text == "Mocked help text" 92 | mock_get_command_help.assert_called_once_with("argocd", "app") 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_execute_kubectl(mock_execute_command, mock_k8s_cli_status): 97 | """Test the execute_kubectl tool.""" 98 | # Test with valid command 99 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 100 | result = await execute_kubectl(command="get pods") 101 | 102 | assert result == mock_execute_command.return_value 103 | mock_execute_command.assert_called_once() 104 | 105 | # Test with command that doesn't start with kubectl 106 | mock_execute_command.reset_mock() 107 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 108 | result = await execute_kubectl(command="describe pod my-pod") 109 | 110 | assert result == mock_execute_command.return_value 111 | mock_execute_command.assert_called_once() 112 | 113 | 114 | @pytest.mark.asyncio 115 | async def test_execute_kubectl_with_context(mock_execute_command, mock_k8s_cli_status): 116 | """Test the execute_kubectl tool with context.""" 117 | # Create a mock context 118 | mock_context = AsyncMock() 119 | 120 | # Test with valid command 121 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 122 | result = await execute_kubectl(command="get pods", ctx=mock_context) 123 | 124 | assert result == mock_execute_command.return_value 125 | mock_execute_command.assert_called_once() 126 | mock_context.info.assert_called() 127 | 128 | 129 | @pytest.mark.asyncio 130 | async def test_execute_kubectl_with_validation_error(mock_k8s_cli_status): 131 | """Test the execute_kubectl tool when validation fails.""" 132 | # Create a mock that raises a validation error 133 | error_mock = AsyncMock(side_effect=CommandValidationError("Invalid command")) 134 | 135 | with patch("k8s_mcp_server.server.execute_command", error_mock): 136 | result = await execute_kubectl(command="get pods") 137 | 138 | assert "status" in result 139 | assert "output" in result 140 | assert result["status"] == "error" 141 | assert "Invalid command" in result["output"] 142 | assert "error" in result 143 | assert result["error"]["code"] == "VALIDATION_ERROR" 144 | 145 | 146 | @pytest.mark.asyncio 147 | async def test_execute_helm(mock_execute_command, mock_k8s_cli_status): 148 | """Test the execute_helm tool.""" 149 | # Test with valid command 150 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 151 | result = await execute_helm(command="list") 152 | 153 | assert result == mock_execute_command.return_value 154 | mock_execute_command.assert_called_once() 155 | 156 | 157 | @pytest.mark.asyncio 158 | async def test_execute_istioctl(mock_execute_command, mock_k8s_cli_status): 159 | """Test the execute_istioctl tool.""" 160 | # Test with valid command 161 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 162 | result = await execute_istioctl(command="analyze") 163 | 164 | assert result == mock_execute_command.return_value 165 | mock_execute_command.assert_called_once() 166 | 167 | 168 | @pytest.mark.asyncio 169 | async def test_execute_argocd(mock_execute_command, mock_k8s_cli_status): 170 | """Test the execute_argocd tool.""" 171 | # Test with valid command 172 | with patch("k8s_mcp_server.server.execute_command", mock_execute_command): 173 | result = await execute_argocd(command="app list") 174 | 175 | assert result == mock_execute_command.return_value 176 | mock_execute_command.assert_called_once() 177 | -------------------------------------------------------------------------------- /tests/unit/test_tools.py: -------------------------------------------------------------------------------- 1 | """Tests for the tools module.""" 2 | 3 | from k8s_mcp_server.tools import ( 4 | is_pipe_command, 5 | is_valid_k8s_tool, 6 | split_pipe_command, 7 | validate_unix_command, 8 | ) 9 | 10 | 11 | def test_is_valid_k8s_tool(): 12 | """Test the is_valid_k8s_tool function.""" 13 | # Valid tools 14 | assert is_valid_k8s_tool("kubectl") is True 15 | assert is_valid_k8s_tool("istioctl") is True 16 | assert is_valid_k8s_tool("helm") is True 17 | assert is_valid_k8s_tool("argocd") is True 18 | 19 | # Invalid tools 20 | assert is_valid_k8s_tool("aws") is False 21 | assert is_valid_k8s_tool("docker") is False 22 | assert is_valid_k8s_tool("kubect") is False # Typo 23 | assert is_valid_k8s_tool("") is False 24 | assert is_valid_k8s_tool("KUBECTL") is False # Case sensitive 25 | 26 | 27 | def test_validate_unix_command(): 28 | """Test the validate_unix_command function.""" 29 | # Valid Unix commands 30 | assert validate_unix_command("grep error") is True 31 | assert validate_unix_command("cat file.txt") is True 32 | assert validate_unix_command("jq .items") is True 33 | assert validate_unix_command("sort") is True 34 | 35 | # Invalid Unix commands 36 | assert validate_unix_command("invalidcommand") is False 37 | assert validate_unix_command("") is False 38 | assert validate_unix_command("rm -rf /") is True # Allowed but would be caught by validation later 39 | assert validate_unix_command("kubectl get pods") is False # Not a Unix command 40 | 41 | 42 | def test_is_pipe_command(): 43 | """Test the is_pipe_command function.""" 44 | # Commands with pipes 45 | assert is_pipe_command("kubectl get pods | grep nginx") is True 46 | assert is_pipe_command("helm list | grep mysql | wc -l") is True 47 | assert is_pipe_command("istioctl analyze | grep Warning") is True 48 | 49 | # Commands without pipes 50 | assert is_pipe_command("kubectl get pods") is False 51 | assert is_pipe_command("helm list") is False 52 | assert is_pipe_command("") is False 53 | 54 | # Commands with quoted pipes (should not be detected as pipe commands) 55 | assert is_pipe_command("kubectl describe pod 'nginx|app'") is False 56 | assert is_pipe_command('echo "This | is not a pipe"') is False 57 | 58 | 59 | def test_split_pipe_command(): 60 | """Test the split_pipe_command function.""" 61 | # Simple pipe command 62 | assert split_pipe_command("kubectl get pods | grep nginx") == ["kubectl get pods", "grep nginx"] 63 | 64 | # Multiple pipe command 65 | assert split_pipe_command("kubectl get pods | grep nginx | wc -l") == ["kubectl get pods", "grep nginx", "wc -l"] 66 | 67 | # Command with quotes 68 | assert split_pipe_command("kubectl get pods -l 'app=nginx' | grep Running") == [ 69 | "kubectl get pods -l 'app=nginx'", 70 | "grep Running", 71 | ] 72 | 73 | # Command with no pipes 74 | assert split_pipe_command("kubectl get pods") == ["kubectl get pods"] 75 | 76 | # Empty command 77 | assert split_pipe_command("") == [""] 78 | 79 | # Complex command with nested quotes 80 | complex_cmd = 'kubectl get pods -o jsonpath="{.items[*].metadata.name}" | grep "^nginx-" | sort' 81 | expected = ['kubectl get pods -o jsonpath="{.items[*].metadata.name}"', 'grep "^nginx-"', "sort"] 82 | assert split_pipe_command(complex_cmd) == expected 83 | --------------------------------------------------------------------------------