|
| 1 | +from kubernetes import client, config |
| 2 | + |
| 3 | +# from mcp.server.fastmcp import FastMCP │ 4 lines yanked │ |
| 4 | +from fastmcp import FastMCP |
| 5 | +import logging |
| 6 | +import subprocess |
| 7 | +import os |
| 8 | + |
| 9 | + |
| 10 | +mcp = FastMCP("ArgoKubernetes MCP server") |
| 11 | + |
| 12 | +# Configure logging |
| 13 | +logging.basicConfig(level=logging.INFO) |
| 14 | +logger = logging.getLogger(__name__) |
| 15 | + |
| 16 | +# Global API clients that will be reinitialized when context changes |
| 17 | +coreV1 = None |
| 18 | +appsV1 = None |
| 19 | +batchV1 = None |
| 20 | +networkingV1 = None |
| 21 | +customObjectsApi = None |
| 22 | + |
| 23 | +# Global variable to track current kubectl context |
| 24 | +# current_kubectl_context = "spendor.cluster" |
| 25 | +current_kubectl_context = "k3d-sunrise" |
| 26 | +current_namespace_context = "argo" |
| 27 | + |
| 28 | +# Load initial kubeconfig (usually from ~/.kube/config) |
| 29 | +config.load_kube_config() |
| 30 | + |
| 31 | + |
| 32 | +def initialize_clients(): |
| 33 | + """Initialize or reinitialize all Kubernetes API clients.""" |
| 34 | + # Create API clients |
| 35 | + global coreV1, appsV1, batchV1, networkingV1, customObjectsApi |
| 36 | + coreV1 = client.CoreV1Api() |
| 37 | + appsV1 = client.AppsV1Api() |
| 38 | + batchV1 = client.BatchV1Api() |
| 39 | + networkingV1 = client.NetworkingV1Api() |
| 40 | + customObjectsApi = client.CustomObjectsApi() |
| 41 | + logger.info("Kubernetes API clients initialized") |
| 42 | + |
| 43 | + |
| 44 | +def sc(context=current_kubectl_context): |
| 45 | + try: |
| 46 | + global current_kubectl_context |
| 47 | + |
| 48 | + # First, switch the kubectl config context permanently |
| 49 | + subprocess.run( |
| 50 | + ["kubectl", "config", "use-context", context], |
| 51 | + capture_output=True, |
| 52 | + text=True, |
| 53 | + check=True, |
| 54 | + ) |
| 55 | + logger.info(f"kubectl config switched to: {context}") |
| 56 | + # Then load the kube config for Python client |
| 57 | + config.load_kube_config(context=context) |
| 58 | + initialize_clients() # Reinitialize clients with new context |
| 59 | + current_kubectl_context = context # Track the current context globally |
| 60 | + logger.info(f"Successfully switched to context: {context}") |
| 61 | + return {"message": f"Switched to context: {context}"} |
| 62 | + except subprocess.CalledProcessError as cmd_error: |
| 63 | + logger.error(f"Error switching kubectl context: {cmd_error.stderr}") |
| 64 | + return {"error": f"kubectl context switch failed: {cmd_error.stderr}"} |
| 65 | + except Exception as e: |
| 66 | + logger.error(f"Error switching context to {context}: {e}") |
| 67 | + return {"error": str(e)} |
| 68 | + |
| 69 | + |
| 70 | +def sn(namespace=current_namespace_context): |
| 71 | + try: |
| 72 | + global current_namespace_context |
| 73 | + # First, switch the kubectl config context permanently |
| 74 | + subprocess.run( |
| 75 | + ["kubectl", "config", "set-context", "--current", "--namespace", namespace], |
| 76 | + capture_output=True, |
| 77 | + text=True, |
| 78 | + check=True, |
| 79 | + ) |
| 80 | + logger.info(f"kubectl config switched to: {namespace}") |
| 81 | + current_namespace_context = namespace # Track the current context globally |
| 82 | + return {"message": f"Switched to namespace: {namespace}"} |
| 83 | + except subprocess.CalledProcessError as cmd_error: |
| 84 | + logger.error(f"Error switching kubectl context: {cmd_error.stderr}") |
| 85 | + return {"error": f"kubectl context switch failed: {cmd_error.stderr}"} |
| 86 | + except Exception as e: |
| 87 | + logger.error(f"Error switching namespace to {namespace}: {e}") |
| 88 | + return {"error": str(e)} |
| 89 | + |
| 90 | + |
| 91 | +@mcp.tool() |
| 92 | +def switch_context(context: str): |
| 93 | + """ |
| 94 | + Switch the active Kubernetes context to connect to a different cluster. |
| 95 | +
|
| 96 | + This tool changes the current Kubernetes context to the specified one, allowing |
| 97 | + you to switch between different clusters or namespaces. After switching, all |
| 98 | + subsequent kubectl commands and API calls will be directed to the new context. |
| 99 | + The Kubernetes API clients are automatically reinitialized for the new context. |
| 100 | +
|
| 101 | + Args: |
| 102 | + context (str): The name of the context to switch to. Must be a valid context |
| 103 | + name from your kubeconfig file. Use list_clusters() to see |
| 104 | + available contexts. |
| 105 | +
|
| 106 | + Returns: |
| 107 | + dict: A dictionary containing: |
| 108 | + - message: Success message if context switch was successful |
| 109 | + - error: Error message if the context switch failed |
| 110 | +
|
| 111 | + Example: |
| 112 | + Input: "dev-cluster" |
| 113 | + Returns: {"message": "Switched to context: dev-cluster"} |
| 114 | + """ |
| 115 | + sc(context) |
| 116 | + |
| 117 | + |
| 118 | +@mcp.tool() |
| 119 | +def switch_namespace(namespace: str): |
| 120 | + """ |
| 121 | + Switch the active Kubernetes namespace to connect to a different namespace. |
| 122 | +
|
| 123 | + This tool changes the current Kubernetes namespace to the specified one, allowing |
| 124 | + you to switch between different namespaces. After switching, all |
| 125 | + subsequent kubectl commands and API calls will be directed to the new namespace. |
| 126 | + The Kubernetes API clients are automatically reinitialized for the new namespace. |
| 127 | +
|
| 128 | + Args: |
| 129 | + namespace (str): The name of the namespace to switch to. Must be a valid namespace |
| 130 | + name from your cluster. Use list_namespaces() to see |
| 131 | + available namespaces. |
| 132 | +
|
| 133 | + Returns: |
| 134 | + dict: A dictionary containing: |
| 135 | + - message: Success message if namespace switch was successful |
| 136 | + - error: Error message if the namespace switch failed |
| 137 | +
|
| 138 | + Example: |
| 139 | + Input: "argo" |
| 140 | + Returns: {"message": "Switched to namespace: argo"} |
| 141 | + """ |
| 142 | + sn(namespace) |
| 143 | + |
| 144 | + |
| 145 | +@mcp.tool() |
| 146 | +def run_kubectl_command(command: str): |
| 147 | + """ |
| 148 | + Execute any kubectl command with full privileges (use with caution). |
| 149 | +
|
| 150 | + This tool allows execution of any kubectl command, including potentially |
| 151 | + destructive operations like delete, update, patch, apply, etc. It provides |
| 152 | + complete access to your Kubernetes cluster with the same permissions as |
| 153 | + your kubectl configuration. |
| 154 | +
|
| 155 | + WARNING: This tool can perform destructive operations. Use run_kubectl_command_ro() |
| 156 | + for safe, read-only operations when you only need to gather information. |
| 157 | +
|
| 158 | + Args: |
| 159 | + command (str): The complete kubectl command to execute. Must start with "kubectl". |
| 160 | + Examples: "kubectl delete pod nginx", "kubectl apply -f config.yaml" |
| 161 | +
|
| 162 | + Returns: |
| 163 | + str: The stdout output from the kubectl command, or an error message if the |
| 164 | + command fails or doesn't start with "kubectl". |
| 165 | +
|
| 166 | + Example: |
| 167 | + Input: "kubectl scale deployment nginx --replicas=3" |
| 168 | + Returns: "deployment.apps/nginx scaled" |
| 169 | + """ |
| 170 | + try: |
| 171 | + global current_kubectl_context |
| 172 | + |
| 173 | + # Check if command starts with kubectl |
| 174 | + if not command.startswith("kubectl "): |
| 175 | + return "Error: Command must start with 'kubectl'" |
| 176 | + |
| 177 | + # Add context flag if a context has been switched and --context is not already in command |
| 178 | + if current_kubectl_context and "--context" not in command: |
| 179 | + command_parts = command.split() |
| 180 | + command_parts.insert(1, f"--context={current_kubectl_context}") |
| 181 | + command = " ".join(command_parts) |
| 182 | + logger.info( |
| 183 | + f"Using context: {current_kubectl_context} for command: {command}" |
| 184 | + ) |
| 185 | + |
| 186 | + # Execute the full command as provided |
| 187 | + result = subprocess.run( |
| 188 | + command.split(), capture_output=True, text=True, check=True |
| 189 | + ) |
| 190 | + return result.stdout |
| 191 | + except subprocess.CalledProcessError as e: |
| 192 | + logger.error(f"Error running kubectl command: {e.stderr}") |
| 193 | + return f"Error: {e.stderr}" |
| 194 | + |
| 195 | + |
| 196 | +@mcp.tool() |
| 197 | +def run_kubectl_command_ro(command: str): |
| 198 | + """ |
| 199 | + Execute read-only kubectl commands safely for information gathering. |
| 200 | +
|
| 201 | + This tool provides a safe way to run kubectl commands that only read information |
| 202 | + from your Kubernetes cluster without making any modifications. It blocks potentially |
| 203 | + destructive operations and only allows commands that gather information. |
| 204 | +
|
| 205 | + Allowed operations: |
| 206 | + - get: Retrieve resources (pods, deployments, services, etc.) |
| 207 | + - describe: Show detailed information about resources |
| 208 | + - explain: Show documentation for resource types |
| 209 | + - logs: Retrieve the logs of the resources (pods, deployments, services, etc.) |
| 210 | + - top: Display resource usage (cpu/memory) |
| 211 | + - config view: View kubeconfig settings |
| 212 | + - config get-contexts: List available contexts |
| 213 | + - version: Show kubectl and cluster version information |
| 214 | + - api-resources: List available API resources |
| 215 | + - cluster-info: Show cluster information |
| 216 | + - debug: debugs cluster resources using interactive containers |
| 217 | + - events: prints a table of the most important information about events. |
| 218 | +
|
| 219 | + Blocked operations include: delete, update, patch, apply, create, replace, edit, |
| 220 | + scale, cordon, drain, taint, and any command with --overwrite flags. |
| 221 | +
|
| 222 | + Args: |
| 223 | + command (str): The kubectl command to execute. Must start with "kubectl" and |
| 224 | + be a read-only operation. Examples: "kubectl get pods", |
| 225 | + "kubectl describe deployment nginx" |
| 226 | +
|
| 227 | + Returns: |
| 228 | + str: The stdout output from the kubectl command, or an error message if the |
| 229 | + command fails, doesn't start with "kubectl", or contains disallowed operations. |
| 230 | +
|
| 231 | + Example: |
| 232 | + Input: "kubectl get pods -n default" |
| 233 | + Returns: "NAME READY STATUS RESTARTS AGE\nnginx 1/1 Running 0 2d" |
| 234 | + """ |
| 235 | + try: |
| 236 | + global current_kubectl_context |
| 237 | + |
| 238 | + # Check if command starts with kubectl |
| 239 | + if not command.startswith("kubectl "): |
| 240 | + return "Error: Command must start with 'kubectl'" |
| 241 | + |
| 242 | + # Extract the actual kubectl subcommand (after "kubectl ") |
| 243 | + kubectl_subcommand = command[8:] # Remove "kubectl " prefix |
| 244 | + |
| 245 | + # List of allowed command prefixes (read-only operations) |
| 246 | + allowed_prefixes = [ |
| 247 | + "get", |
| 248 | + "describe", |
| 249 | + "explain", |
| 250 | + "logs", |
| 251 | + "top", |
| 252 | + "config view", |
| 253 | + "config get-contexts", |
| 254 | + "debug", |
| 255 | + "version", |
| 256 | + "api-resources", |
| 257 | + "cluster-info", |
| 258 | + "events", |
| 259 | + ] |
| 260 | + |
| 261 | + # List of disallowed terms that might modify resources |
| 262 | + disallowed_terms = [ |
| 263 | + "delete", |
| 264 | + "update", |
| 265 | + "patch", |
| 266 | + "apply", |
| 267 | + "create", |
| 268 | + "replace", |
| 269 | + "edit", |
| 270 | + "scale", |
| 271 | + "cordon", |
| 272 | + "drain", |
| 273 | + "taint", |
| 274 | + "label --overwrite", |
| 275 | + "annotate --overwrite", |
| 276 | + ] |
| 277 | + |
| 278 | + # Check if command is allowed |
| 279 | + is_allowed = any( |
| 280 | + kubectl_subcommand.startswith(prefix) for prefix in allowed_prefixes |
| 281 | + ) |
| 282 | + has_disallowed = any(term in kubectl_subcommand for term in disallowed_terms) |
| 283 | + |
| 284 | + if not is_allowed or has_disallowed: |
| 285 | + return "Error: Only read-only kubectl commands are allowed (get, describe, etc.)" |
| 286 | + |
| 287 | + # Add context flag if a context has been switched and --context is not already in command |
| 288 | + if current_kubectl_context and "--context" not in command: |
| 289 | + command_parts = command.split() |
| 290 | + command_parts.insert(1, f"--context={current_kubectl_context}") |
| 291 | + command = " ".join(command_parts) |
| 292 | + logger.info(f"Modified command with context: {command}") |
| 293 | + else: |
| 294 | + logger.info( |
| 295 | + f"No context modification needed. Context: {current_kubectl_context}, has --context: {'--context' in command}" |
| 296 | + ) |
| 297 | + |
| 298 | + # Execute the full command as provided |
| 299 | + logger.info(f"Executing command: {command}") |
| 300 | + result = subprocess.run( |
| 301 | + command.split(), capture_output=True, text=True, check=True |
| 302 | + ) |
| 303 | + logger.info( |
| 304 | + f"Command successful, output length: {len(result.stdout)} characters" |
| 305 | + ) |
| 306 | + return result.stdout |
| 307 | + except subprocess.CalledProcessError as e: |
| 308 | + logger.error(f"Error running kubectl command: {e.stderr}") |
| 309 | + return f"Error: {e.stderr}" |
| 310 | + except Exception as e: |
| 311 | + logger.error(f"Unexpected error in run_kubectl_command_ro: {e}") |
| 312 | + return f"Error: {e}" |
| 313 | + |
| 314 | + |
| 315 | +@mcp.tool() |
| 316 | +def run_argo_command(command: str): |
| 317 | + """ |
| 318 | + Execute any argo command with full privileges (use with caution). |
| 319 | +
|
| 320 | + This tool allows execution of any argo command, including potentially |
| 321 | + destructive operations like delete, stop, terminate, submit, etc. It provides |
| 322 | + complete access to your Argo server with the same permissions as |
| 323 | + your kubectl configuration. |
| 324 | +
|
| 325 | + Args: |
| 326 | + command (str): The complete argo command to execute. Must start with "argo". |
| 327 | + Examples: "argo submit -n argo --from clusterworkflowtemplate/automlai-dualsearch" |
| 328 | +
|
| 329 | + Returns: |
| 330 | + str: The stdout output from the argo command, or an error message if the |
| 331 | + command fails or doesn't start with "argo". |
| 332 | +
|
| 333 | + Example: |
| 334 | + Input: "argo -n argo list" |
| 335 | + Returns: "automl-dualsearch-pxd7s Succeeded 51d 49s 0" |
| 336 | + """ |
| 337 | + try: |
| 338 | + global current_kubectl_context, current_namespace_context |
| 339 | + |
| 340 | + # Check if command starts with kubectl |
| 341 | + if not command.startswith("argo "): |
| 342 | + return "Error: Command must start with 'argo'" |
| 343 | + |
| 344 | + # Add context flag if a context has been switched and --context is not already in command |
| 345 | + if current_kubectl_context and "--context" not in command: |
| 346 | + command_parts = command.split() |
| 347 | + command_parts.insert(1, f"--context={current_kubectl_context}") |
| 348 | + command = " ".join(command_parts) |
| 349 | + logger.info( |
| 350 | + f"Using context: {current_kubectl_context} for command: {command}" |
| 351 | + ) |
| 352 | + |
| 353 | + if current_namespace_context and ("--namespace" or "-n") not in command: |
| 354 | + command_parts = command.split() |
| 355 | + command_parts.insert(1, f"--namespace={current_namespace_context}") |
| 356 | + command = " ".join(command_parts) |
| 357 | + logger.info( |
| 358 | + f"Using context: {current_kubectl_context} and namespace: {current_namespace_context} for command: {command}" |
| 359 | + ) |
| 360 | + |
| 361 | + # Execute the full command as provided |
| 362 | + result = subprocess.run( |
| 363 | + command.split(), capture_output=True, text=True, check=True |
| 364 | + ) |
| 365 | + return result.stdout |
| 366 | + except subprocess.CalledProcessError as e: |
| 367 | + logger.error(f"Error running kubectl command: {e.stderr}") |
| 368 | + return f"Error: {e.stderr}" |
| 369 | + |
| 370 | + |
| 371 | +# main entry point to run the MCP server |
| 372 | +async def main(): |
| 373 | + # Use run_async() in async contexts |
| 374 | + await mcp.run_async(transport="http", host="127.0.0.1", port=8000) |
| 375 | + |
| 376 | + |
| 377 | +sc() |
| 378 | +sn() |
| 379 | + |
| 380 | +main_app = mcp.http_app() |
| 381 | + |
| 382 | +# if __name__ == "__main__": |
| 383 | +# logger.info("Starting ArgoKubernetes MCP.") |
| 384 | +# mcp.run() |
0 commit comments