kubectl-mcp-server 1.13.0__py3-none-any.whl → 1.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,8 +7,38 @@ import logging
7
7
  import asyncio
8
8
  import argparse
9
9
  import traceback
10
+ import json
11
+ import shutil
12
+ import fnmatch
13
+ from typing import Any, Dict, List, Optional
14
+
10
15
  from ..mcp_server import MCPServer
16
+ from .errors import (
17
+ ErrorCode,
18
+ format_cli_error,
19
+ tool_not_found_error,
20
+ tool_execution_error,
21
+ invalid_json_error,
22
+ missing_argument_error,
23
+ unknown_subcommand_error,
24
+ k8s_context_error,
25
+ dependency_missing_error,
26
+ )
27
+ from .output import (
28
+ format_tools_list,
29
+ format_tool_schema,
30
+ format_tools_search,
31
+ format_resources_list,
32
+ format_prompts_list,
33
+ format_call_result,
34
+ format_server_info,
35
+ format_context_info,
36
+ format_doctor_results,
37
+ format_error,
38
+ format_success,
39
+ )
11
40
 
41
+ # Logging setup
12
42
  log_file = os.environ.get("MCP_LOG_FILE")
13
43
  log_level = logging.DEBUG if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true") else logging.INFO
14
44
 
@@ -27,38 +57,393 @@ logger = logging.getLogger("kubectl-mcp-cli")
27
57
 
28
58
 
29
59
  async def serve_stdio():
30
- """Serve the MCP server over stdio transport."""
31
60
  server = MCPServer("kubernetes")
32
61
  await server.serve_stdio()
33
62
 
34
63
 
35
64
  async def serve_sse(host: str, port: int):
36
- """Serve the MCP server over SSE transport."""
37
65
  server = MCPServer("kubernetes")
38
66
  await server.serve_sse(host=host, port=port)
39
67
 
40
68
 
41
69
  async def serve_http(host: str, port: int):
42
- """Serve the MCP server over HTTP transport."""
43
70
  server = MCPServer("kubernetes")
44
71
  await server.serve_http(host=host, port=port)
45
72
 
46
73
 
74
+ def get_all_tools() -> List[Dict[str, Any]]:
75
+ server = MCPServer("kubernetes")
76
+
77
+ async def _get():
78
+ tools = await server.server.list_tools()
79
+ return [
80
+ {
81
+ "name": t.name,
82
+ "description": t.description or "",
83
+ "inputSchema": t.inputSchema if hasattr(t, 'inputSchema') else {},
84
+ "category": _get_tool_category(t.name),
85
+ }
86
+ for t in tools
87
+ ]
88
+
89
+ return asyncio.run(_get())
90
+
91
+
92
+ def _get_tool_category(tool_name: str) -> str:
93
+ """Determine tool category from name."""
94
+ categories = {
95
+ "pod": "pods",
96
+ "deployment": "deployments",
97
+ "statefulset": "deployments",
98
+ "daemonset": "deployments",
99
+ "replicaset": "deployments",
100
+ "namespace": "core",
101
+ "configmap": "core",
102
+ "secret": "core",
103
+ "service": "networking",
104
+ "ingress": "networking",
105
+ "network": "networking",
106
+ "pvc": "storage",
107
+ "pv": "storage",
108
+ "storage": "storage",
109
+ "rbac": "security",
110
+ "role": "security",
111
+ "serviceaccount": "security",
112
+ "helm": "helm",
113
+ "apply": "operations",
114
+ "patch": "operations",
115
+ "delete": "operations",
116
+ "scale": "operations",
117
+ "rollout": "operations",
118
+ "context": "cluster",
119
+ "cluster": "cluster",
120
+ "node": "cluster",
121
+ "metric": "diagnostics",
122
+ "compare": "diagnostics",
123
+ "event": "diagnostics",
124
+ "cost": "cost",
125
+ "browser": "browser",
126
+ "screenshot": "browser",
127
+ "ui": "ui",
128
+ "dashboard": "ui",
129
+ }
130
+
131
+ name_lower = tool_name.lower()
132
+ for keyword, category in categories.items():
133
+ if keyword in name_lower:
134
+ return category
135
+
136
+ return "other"
137
+
138
+
139
+ def cmd_tools(args):
140
+ tools = get_all_tools()
141
+
142
+ if args.name:
143
+ # Show specific tool schema
144
+ tool = next((t for t in tools if t["name"] == args.name), None)
145
+ if not tool:
146
+ available = [t["name"] for t in tools]
147
+ print(format_cli_error(tool_not_found_error(args.name, available)), file=sys.stderr)
148
+ return ErrorCode.CLIENT_ERROR
149
+
150
+ print(format_tool_schema(tool, as_json=args.json))
151
+ else:
152
+ # List all tools
153
+ print(format_tools_list(tools, with_descriptions=args.with_descriptions, as_json=args.json))
154
+
155
+ return ErrorCode.SUCCESS
156
+
157
+
158
+ def get_all_resources() -> List[Dict[str, Any]]:
159
+ server = MCPServer("kubernetes")
160
+
161
+ async def _get():
162
+ resources = await server.server.list_resources()
163
+ return [
164
+ {
165
+ "uri": r.uri,
166
+ "name": r.name,
167
+ "description": r.description or "",
168
+ "mimeType": getattr(r, 'mimeType', None),
169
+ }
170
+ for r in resources
171
+ ]
172
+
173
+ return asyncio.run(_get())
174
+
175
+
176
+ def cmd_resources(args):
177
+ resources = get_all_resources()
178
+ print(format_resources_list(resources, as_json=args.json))
179
+ return ErrorCode.SUCCESS
180
+
181
+
182
+ def get_all_prompts() -> List[Dict[str, Any]]:
183
+ server = MCPServer("kubernetes")
184
+
185
+ async def _get():
186
+ prompts = await server.server.list_prompts()
187
+ return [
188
+ {
189
+ "name": p.name,
190
+ "description": p.description or "",
191
+ "arguments": [
192
+ {"name": a.name, "description": a.description, "required": a.required}
193
+ for a in (p.arguments or [])
194
+ ],
195
+ }
196
+ for p in prompts
197
+ ]
198
+
199
+ return asyncio.run(_get())
200
+
201
+
202
+ def cmd_prompts(args):
203
+ prompts = get_all_prompts()
204
+ print(format_prompts_list(prompts, as_json=args.json))
205
+ return ErrorCode.SUCCESS
206
+
207
+
208
+ def cmd_call(args):
209
+ tools = get_all_tools()
210
+ tool = next((t for t in tools if t["name"] == args.tool), None)
211
+
212
+ if not tool:
213
+ available = [t["name"] for t in tools]
214
+ print(format_cli_error(tool_not_found_error(args.tool, available)), file=sys.stderr)
215
+ return ErrorCode.CLIENT_ERROR
216
+
217
+ # Parse JSON arguments
218
+ json_args = args.args
219
+
220
+ # Read from stdin if no args provided and stdin is not a tty
221
+ if not json_args and not sys.stdin.isatty():
222
+ json_args = sys.stdin.read().strip()
223
+
224
+ # Default to empty object
225
+ if not json_args:
226
+ json_args = "{}"
227
+
228
+ try:
229
+ tool_args = json.loads(json_args)
230
+ except json.JSONDecodeError as e:
231
+ print(format_cli_error(invalid_json_error(json_args, str(e))), file=sys.stderr)
232
+ return ErrorCode.CLIENT_ERROR
233
+
234
+ # Execute the tool
235
+ server = MCPServer("kubernetes")
236
+
237
+ async def _call():
238
+ result = await server.server.call_tool(args.tool, tool_args)
239
+ return result
240
+
241
+ try:
242
+ result = asyncio.run(_call())
243
+ print(format_call_result(result, as_json=args.json))
244
+ return ErrorCode.SUCCESS
245
+ except Exception as e:
246
+ print(format_cli_error(tool_execution_error(args.tool, str(e))), file=sys.stderr)
247
+ return ErrorCode.SERVER_ERROR
248
+
249
+
250
+ def cmd_grep(args):
251
+ tools = get_all_tools()
252
+ pattern = args.pattern
253
+
254
+ # Support glob patterns
255
+ if not pattern.startswith("*") and not pattern.endswith("*"):
256
+ # Make it a contains search by default
257
+ pattern = f"*{pattern}*"
258
+
259
+ matches = [
260
+ t for t in tools
261
+ if fnmatch.fnmatch(t["name"].lower(), pattern.lower())
262
+ or fnmatch.fnmatch((t.get("description") or "").lower(), pattern.lower())
263
+ ]
264
+
265
+ print(format_tools_search(matches, args.pattern, with_descriptions=args.with_descriptions))
266
+ return ErrorCode.SUCCESS
267
+
268
+
269
+ def cmd_info(args):
270
+ from .. import __version__
271
+
272
+ tools = get_all_tools()
273
+ resources = get_all_resources()
274
+ prompts = get_all_prompts()
275
+
276
+ # Get current k8s context
277
+ context = None
278
+ try:
279
+ from kubernetes import config
280
+ _, active_context = config.list_kube_config_contexts()
281
+ context = active_context.get("name") if active_context else None
282
+ except Exception:
283
+ pass
284
+
285
+ print(format_server_info(
286
+ version=__version__,
287
+ tool_count=len(tools),
288
+ resource_count=len(resources),
289
+ prompt_count=len(prompts),
290
+ context=context,
291
+ as_json=getattr(args, 'json', False)
292
+ ))
293
+ return ErrorCode.SUCCESS
294
+
295
+
296
+ def cmd_context(args):
297
+ try:
298
+ from kubernetes import config
299
+ import subprocess
300
+
301
+ contexts, active_context = config.list_kube_config_contexts()
302
+ current = active_context.get("name") if active_context else None
303
+ available = [c.get("name") for c in contexts] if contexts else []
304
+
305
+ if args.name:
306
+ # Switch context
307
+ if args.name not in available:
308
+ print(format_cli_error(k8s_context_error(args.name, available)), file=sys.stderr)
309
+ return ErrorCode.K8S_ERROR
310
+
311
+ result = subprocess.run(
312
+ ["kubectl", "config", "use-context", args.name],
313
+ capture_output=True,
314
+ text=True
315
+ )
316
+ if result.returncode == 0:
317
+ print(format_success(f"Switched to context: {args.name}"))
318
+ return ErrorCode.SUCCESS
319
+ else:
320
+ print(format_error(result.stderr.strip()), file=sys.stderr)
321
+ return ErrorCode.K8S_ERROR
322
+ else:
323
+ # Show current context
324
+ print(format_context_info(current or "(none)", available, as_json=getattr(args, 'json', False)))
325
+ return ErrorCode.SUCCESS
326
+
327
+ except Exception as e:
328
+ print(format_error(f"Failed to get contexts: {e}"), file=sys.stderr)
329
+ return ErrorCode.K8S_ERROR
330
+
331
+
332
+ def cmd_doctor(args):
333
+ checks = []
334
+
335
+ # Check kubectl
336
+ kubectl_path = shutil.which("kubectl")
337
+ if kubectl_path:
338
+ try:
339
+ import subprocess
340
+ result = subprocess.run(["kubectl", "version", "--client", "-o", "json"],
341
+ capture_output=True, text=True, timeout=5)
342
+ if result.returncode == 0:
343
+ version_info = json.loads(result.stdout)
344
+ version = version_info.get("clientVersion", {}).get("gitVersion", "unknown")
345
+ checks.append({"name": "kubectl", "status": "ok", "version": version, "details": kubectl_path})
346
+ else:
347
+ checks.append({"name": "kubectl", "status": "warning", "details": "kubectl found but version check failed"})
348
+ except Exception as e:
349
+ checks.append({"name": "kubectl", "status": "warning", "details": str(e)})
350
+ else:
351
+ checks.append({"name": "kubectl", "status": "error", "details": "kubectl not found in PATH"})
352
+
353
+ # Check helm
354
+ helm_path = shutil.which("helm")
355
+ if helm_path:
356
+ try:
357
+ import subprocess
358
+ result = subprocess.run(["helm", "version", "--short"], capture_output=True, text=True, timeout=5)
359
+ if result.returncode == 0:
360
+ version = result.stdout.strip()
361
+ checks.append({"name": "helm", "status": "ok", "version": version, "details": helm_path})
362
+ else:
363
+ checks.append({"name": "helm", "status": "warning", "details": "helm found but version check failed"})
364
+ except Exception as e:
365
+ checks.append({"name": "helm", "status": "warning", "details": str(e)})
366
+ else:
367
+ checks.append({"name": "helm", "status": "warning", "details": "helm not found (optional)"})
368
+
369
+ # Check Kubernetes connection
370
+ try:
371
+ from kubernetes import client, config
372
+ config.load_kube_config()
373
+ v1 = client.CoreV1Api()
374
+ v1.list_namespace(limit=1)
375
+ _, active_context = config.list_kube_config_contexts()
376
+ context_name = active_context.get("name") if active_context else "unknown"
377
+ checks.append({"name": "kubernetes", "status": "ok", "version": context_name, "details": "Connected"})
378
+ except Exception as e:
379
+ checks.append({"name": "kubernetes", "status": "error", "details": str(e)})
380
+
381
+ # Check agent-browser (optional)
382
+ browser_path = shutil.which("agent-browser")
383
+ if browser_path:
384
+ try:
385
+ import subprocess
386
+ result = subprocess.run(["agent-browser", "--version"], capture_output=True, text=True, timeout=5)
387
+ if result.returncode == 0:
388
+ version = result.stdout.strip()
389
+ checks.append({"name": "agent-browser", "status": "ok", "version": version, "details": browser_path})
390
+ else:
391
+ checks.append({"name": "agent-browser", "status": "ok", "details": browser_path})
392
+ except Exception:
393
+ checks.append({"name": "agent-browser", "status": "ok", "details": browser_path})
394
+ else:
395
+ enabled = os.environ.get("MCP_BROWSER_ENABLED", "").lower() in ("1", "true")
396
+ if enabled:
397
+ checks.append({"name": "agent-browser", "status": "warning",
398
+ "details": "MCP_BROWSER_ENABLED=true but agent-browser not found"})
399
+ else:
400
+ checks.append({"name": "agent-browser", "status": "ok",
401
+ "details": "Not installed (optional, set MCP_BROWSER_ENABLED=true to use)"})
402
+
403
+ # Check Python dependencies
404
+ try:
405
+ import fastmcp
406
+ checks.append({"name": "fastmcp", "status": "ok", "version": getattr(fastmcp, '__version__', 'installed')})
407
+ except ImportError:
408
+ checks.append({"name": "fastmcp", "status": "error", "details": "fastmcp not installed"})
409
+
410
+ print(format_doctor_results(checks, as_json=getattr(args, 'json', False)))
411
+
412
+ # Return error code if any critical checks failed
413
+ has_errors = any(c["status"] == "error" for c in checks)
414
+ return ErrorCode.CLIENT_ERROR if has_errors else ErrorCode.SUCCESS
415
+
416
+
47
417
  def main():
48
- """Main entry point for the CLI."""
49
418
  parser = argparse.ArgumentParser(
50
- description="kubectl-mcp-tool - MCP server for Kubernetes",
419
+ prog="kubectl-mcp-server",
420
+ description="MCP server for Kubernetes with 127+ tools, 8 resources, and 8 prompts",
51
421
  formatter_class=argparse.RawDescriptionHelpFormatter,
52
422
  epilog="""
53
423
  Examples:
54
- kubectl-mcp serve # stdio transport (Claude Desktop/Cursor)
55
- kubectl-mcp serve --transport sse # SSE transport
56
- kubectl-mcp serve --transport http # HTTP transport
57
- kubectl-mcp diagnostics # Run cluster diagnostics
424
+ kubectl-mcp-server serve # Start stdio server (Claude/Cursor)
425
+ kubectl-mcp-server serve --transport http # Start HTTP server
426
+ kubectl-mcp-server tools # List all tools
427
+ kubectl-mcp-server tools -d # List tools with descriptions
428
+ kubectl-mcp-server tools get_pods # Show tool schema
429
+ kubectl-mcp-server grep "*pod*" # Search for pod-related tools
430
+ kubectl-mcp-server call get_pods '{"namespace": "default"}' # Call a tool
431
+ echo '{"namespace": "kube-system"}' | kubectl-mcp-server call get_pods
432
+ kubectl-mcp-server context # Show k8s context
433
+ kubectl-mcp-server doctor # Check dependencies
434
+
435
+ Environment Variables:
436
+ MCP_DEBUG=true Enable debug logging
437
+ MCP_BROWSER_ENABLED=true Enable browser automation tools
438
+ NO_COLOR=1 Disable colored output
58
439
  """
59
440
  )
441
+
442
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
443
+
60
444
  subparsers = parser.add_subparsers(dest="command", help="Command to run")
61
445
 
446
+ # serve command (existing)
62
447
  serve_parser = subparsers.add_parser("serve", help="Start the MCP server")
63
448
  serve_parser.add_argument(
64
449
  "--transport",
@@ -68,14 +453,55 @@ Examples:
68
453
  )
69
454
  serve_parser.add_argument("--host", type=str, default="0.0.0.0", help="Host for SSE/HTTP (default: 0.0.0.0)")
70
455
  serve_parser.add_argument("--port", type=int, default=8000, help="Port for SSE/HTTP (default: 8000)")
71
- serve_parser.add_argument("--debug", action="store_true", help="Enable debug logging")
72
456
 
457
+ # version command (existing)
73
458
  subparsers.add_parser("version", help="Show version")
459
+
460
+ # diagnostics command (existing)
74
461
  subparsers.add_parser("diagnostics", help="Run cluster diagnostics")
75
462
 
463
+ # tools command (new)
464
+ tools_parser = subparsers.add_parser("tools", help="List or inspect tools")
465
+ tools_parser.add_argument("name", nargs="?", help="Tool name to inspect")
466
+ tools_parser.add_argument("-d", "--with-descriptions", action="store_true", help="Include descriptions")
467
+ tools_parser.add_argument("--json", action="store_true", help="Output as JSON")
468
+
469
+ # resources command (new)
470
+ resources_parser = subparsers.add_parser("resources", help="List available resources")
471
+ resources_parser.add_argument("--json", action="store_true", help="Output as JSON")
472
+
473
+ # prompts command (new)
474
+ prompts_parser = subparsers.add_parser("prompts", help="List available prompts")
475
+ prompts_parser.add_argument("--json", action="store_true", help="Output as JSON")
476
+
477
+ # call command (new)
478
+ call_parser = subparsers.add_parser("call", help="Call a tool directly")
479
+ call_parser.add_argument("tool", help="Tool name to call")
480
+ call_parser.add_argument("args", nargs="?", help="JSON arguments (reads stdin if omitted)")
481
+ call_parser.add_argument("--json", action="store_true", help="Force JSON output")
482
+
483
+ # grep command (new)
484
+ grep_parser = subparsers.add_parser("grep", help="Search tools by pattern")
485
+ grep_parser.add_argument("pattern", help="Glob pattern to search (e.g., '*pod*')")
486
+ grep_parser.add_argument("-d", "--with-descriptions", action="store_true", help="Include descriptions")
487
+
488
+ # info command (new)
489
+ info_parser = subparsers.add_parser("info", help="Show server information")
490
+ info_parser.add_argument("--json", action="store_true", help="Output as JSON")
491
+
492
+ # context command (new)
493
+ context_parser = subparsers.add_parser("context", help="Show/switch Kubernetes context")
494
+ context_parser.add_argument("name", nargs="?", help="Context to switch to")
495
+ context_parser.add_argument("--json", action="store_true", help="Output as JSON")
496
+
497
+ # doctor command (new)
498
+ doctor_parser = subparsers.add_parser("doctor", help="Check dependencies and configuration")
499
+ doctor_parser.add_argument("--json", action="store_true", help="Output as JSON")
500
+
76
501
  args = parser.parse_args()
77
502
 
78
- if hasattr(args, 'debug') and args.debug:
503
+ # Enable debug logging
504
+ if args.debug:
79
505
  logging.getLogger().setLevel(logging.DEBUG)
80
506
  os.environ["MCP_DEBUG"] = "1"
81
507
 
@@ -87,24 +513,57 @@ Examples:
87
513
  asyncio.run(serve_sse(args.host, args.port))
88
514
  elif args.transport in ("http", "streamable-http"):
89
515
  asyncio.run(serve_http(args.host, args.port))
516
+
90
517
  elif args.command == "version":
91
518
  from .. import __version__
92
- print(f"kubectl-mcp-tool version {__version__}")
519
+ print(f"kubectl-mcp-server version {__version__}")
520
+
93
521
  elif args.command == "diagnostics":
94
522
  from ..diagnostics import run_diagnostics
95
- import json
96
523
  results = run_diagnostics()
97
524
  print(json.dumps(results, indent=2))
98
- else:
525
+
526
+ elif args.command == "tools":
527
+ return cmd_tools(args)
528
+
529
+ elif args.command == "resources":
530
+ return cmd_resources(args)
531
+
532
+ elif args.command == "prompts":
533
+ return cmd_prompts(args)
534
+
535
+ elif args.command == "call":
536
+ return cmd_call(args)
537
+
538
+ elif args.command == "grep":
539
+ return cmd_grep(args)
540
+
541
+ elif args.command == "info":
542
+ return cmd_info(args)
543
+
544
+ elif args.command == "context":
545
+ return cmd_context(args)
546
+
547
+ elif args.command == "doctor":
548
+ return cmd_doctor(args)
549
+
550
+ elif args.command is None:
99
551
  parser.print_help()
552
+
553
+ else:
554
+ # Unknown subcommand
555
+ print(format_cli_error(unknown_subcommand_error(args.command)), file=sys.stderr)
556
+ return ErrorCode.CLIENT_ERROR
557
+
100
558
  except KeyboardInterrupt:
101
559
  pass
102
560
  except Exception as e:
103
561
  logger.error(f"Error: {e}")
104
- if hasattr(args, 'debug') and args.debug:
562
+ if args.debug:
105
563
  logger.error(traceback.format_exc())
106
- return 1
107
- return 0
564
+ return ErrorCode.SERVER_ERROR
565
+
566
+ return ErrorCode.SUCCESS
108
567
 
109
568
 
110
569
  if __name__ == "__main__":