kubectl-mcp-server 1.12.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.
Files changed (45) hide show
  1. kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
  2. kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
  3. kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
  4. kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
  5. kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
  6. kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
  7. kubectl_mcp_tool/__init__.py +21 -0
  8. kubectl_mcp_tool/__main__.py +46 -0
  9. kubectl_mcp_tool/auth/__init__.py +13 -0
  10. kubectl_mcp_tool/auth/config.py +71 -0
  11. kubectl_mcp_tool/auth/scopes.py +148 -0
  12. kubectl_mcp_tool/auth/verifier.py +82 -0
  13. kubectl_mcp_tool/cli/__init__.py +9 -0
  14. kubectl_mcp_tool/cli/__main__.py +10 -0
  15. kubectl_mcp_tool/cli/cli.py +111 -0
  16. kubectl_mcp_tool/diagnostics.py +355 -0
  17. kubectl_mcp_tool/k8s_config.py +289 -0
  18. kubectl_mcp_tool/mcp_server.py +530 -0
  19. kubectl_mcp_tool/prompts/__init__.py +5 -0
  20. kubectl_mcp_tool/prompts/prompts.py +823 -0
  21. kubectl_mcp_tool/resources/__init__.py +5 -0
  22. kubectl_mcp_tool/resources/resources.py +305 -0
  23. kubectl_mcp_tool/tools/__init__.py +28 -0
  24. kubectl_mcp_tool/tools/browser.py +371 -0
  25. kubectl_mcp_tool/tools/cluster.py +315 -0
  26. kubectl_mcp_tool/tools/core.py +421 -0
  27. kubectl_mcp_tool/tools/cost.py +680 -0
  28. kubectl_mcp_tool/tools/deployments.py +381 -0
  29. kubectl_mcp_tool/tools/diagnostics.py +174 -0
  30. kubectl_mcp_tool/tools/helm.py +1561 -0
  31. kubectl_mcp_tool/tools/networking.py +296 -0
  32. kubectl_mcp_tool/tools/operations.py +501 -0
  33. kubectl_mcp_tool/tools/pods.py +582 -0
  34. kubectl_mcp_tool/tools/security.py +333 -0
  35. kubectl_mcp_tool/tools/storage.py +133 -0
  36. kubectl_mcp_tool/utils/__init__.py +17 -0
  37. kubectl_mcp_tool/utils/helpers.py +80 -0
  38. tests/__init__.py +9 -0
  39. tests/conftest.py +379 -0
  40. tests/test_auth.py +256 -0
  41. tests/test_browser.py +349 -0
  42. tests/test_prompts.py +536 -0
  43. tests/test_resources.py +343 -0
  44. tests/test_server.py +384 -0
  45. tests/test_tools.py +659 -0
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP server implementation for kubectl-mcp-tool.
4
+
5
+ Compatible with:
6
+ - Claude Desktop
7
+ - Cursor AI
8
+ - Windsurf
9
+ - Docker MCP Toolkit (https://docs.docker.com/ai/mcp-catalog-and-toolkit/toolkit/)
10
+
11
+ FastMCP Migration Notes:
12
+ ------------------------
13
+ Currently using: fastmcp (gofastmcp.com) - standalone package with extra features
14
+ To revert to official Anthropic MCP SDK:
15
+ 1. Change requirements.txt: fastmcp>=3.0.0 -> mcp>=1.8.0
16
+ 2. Change import below: from fastmcp import FastMCP -> from mcp.server.fastmcp import FastMCP
17
+ 3. Change ToolAnnotations import: from fastmcp.tools import ToolAnnotations -> from mcp.types import ToolAnnotations
18
+ """
19
+
20
+ import json
21
+ import sys
22
+ import logging
23
+ import asyncio
24
+ import os
25
+ import platform
26
+ from typing import List, Optional, Any
27
+
28
+ # Import k8s_config early to patch kubernetes config for in-cluster support
29
+ # This must be done before any tools are imported
30
+ import kubectl_mcp_tool.k8s_config # noqa: F401
31
+
32
+ from kubectl_mcp_tool.tools import (
33
+ register_helm_tools,
34
+ register_pod_tools,
35
+ register_core_tools,
36
+ register_cluster_tools,
37
+ register_deployment_tools,
38
+ register_security_tools,
39
+ register_networking_tools,
40
+ register_storage_tools,
41
+ register_operations_tools,
42
+ register_diagnostics_tools,
43
+ register_cost_tools,
44
+ register_browser_tools,
45
+ is_browser_available,
46
+ )
47
+ from kubectl_mcp_tool.resources import register_resources
48
+ from kubectl_mcp_tool.prompts import register_prompts
49
+ from kubectl_mcp_tool.auth import get_auth_config, create_auth_verifier
50
+
51
+ if platform.system() == "Windows":
52
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
53
+
54
+ import warnings
55
+ warnings.filterwarnings(
56
+ "ignore",
57
+ category=RuntimeWarning,
58
+ message=r".*found in sys.modules after import of package.*"
59
+ )
60
+
61
+ _log_file = os.environ.get("MCP_LOG_FILE")
62
+ _log_level = logging.DEBUG if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true") else logging.INFO
63
+
64
+ _handlers: List[logging.Handler] = []
65
+ if _log_file:
66
+ try:
67
+ os.makedirs(os.path.dirname(_log_file), exist_ok=True)
68
+ _handlers.append(logging.FileHandler(_log_file))
69
+ except (OSError, ValueError):
70
+ _handlers.append(logging.StreamHandler(sys.stderr))
71
+ else:
72
+ _handlers.append(logging.StreamHandler(sys.stderr))
73
+
74
+ logging.basicConfig(
75
+ level=_log_level,
76
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
77
+ handlers=_handlers
78
+ )
79
+ logger = logging.getLogger("mcp-server")
80
+
81
+ for handler in logging.root.handlers[:]:
82
+ if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout:
83
+ logging.root.removeHandler(handler)
84
+
85
+ # FastMCP 3 from gofastmcp.com (standalone package)
86
+ # To revert to official SDK: from mcp.server.fastmcp import FastMCP
87
+ try:
88
+ from fastmcp import FastMCP
89
+ except ImportError:
90
+ logger.error("FastMCP not found. Installing...")
91
+ import subprocess
92
+ try:
93
+ subprocess.check_call(
94
+ [sys.executable, "-m", "pip", "install", "fastmcp>=3.0.0b1"],
95
+ stdout=subprocess.DEVNULL,
96
+ stderr=subprocess.DEVNULL
97
+ )
98
+ from fastmcp import FastMCP
99
+ except Exception as e:
100
+ logger.error(f"Failed to install FastMCP: {e}")
101
+ raise
102
+
103
+
104
+ class MCPServer:
105
+ """MCP server implementation."""
106
+
107
+ def __init__(self, name: str, non_destructive: bool = False):
108
+ """Initialize the MCP server.
109
+
110
+ Args:
111
+ name: Server name for identification
112
+ non_destructive: If True, block destructive operations
113
+
114
+ Environment Variables:
115
+ MCP_AUTH_ENABLED: Enable OAuth 2.1 authentication (default: false)
116
+ MCP_AUTH_ISSUER: OAuth 2.0 Authorization Server URL
117
+ MCP_AUTH_JWKS_URI: JWKS endpoint (optional, derived from issuer)
118
+ MCP_AUTH_AUDIENCE: Expected token audience (default: kubectl-mcp-server)
119
+ MCP_AUTH_REQUIRED_SCOPES: Required scopes (default: mcp:tools)
120
+ """
121
+ self.name = name
122
+ self.non_destructive = non_destructive
123
+ self._dependencies_checked = False
124
+ self._dependencies_available = None
125
+
126
+ # Load authentication configuration
127
+ self.auth_config = get_auth_config()
128
+ auth_verifier = self._setup_auth()
129
+
130
+ # Initialize FastMCP with optional authentication
131
+ if auth_verifier:
132
+ logger.info("Initializing MCP server with authentication enabled")
133
+ self.server = FastMCP(name=name, auth=auth_verifier)
134
+ else:
135
+ self.server = FastMCP(name=name)
136
+
137
+ self.setup_tools()
138
+ self.setup_resources()
139
+ self.setup_prompts()
140
+
141
+ def _setup_auth(self) -> Optional[Any]:
142
+ """Set up authentication if enabled."""
143
+ if not self.auth_config.enabled:
144
+ logger.debug("Authentication disabled")
145
+ return None
146
+
147
+ try:
148
+ verifier = create_auth_verifier(self.auth_config)
149
+ if verifier:
150
+ logger.info(f"Authentication configured with issuer: {self.auth_config.issuer_url}")
151
+ return verifier
152
+ except Exception as e:
153
+ logger.error(f"Failed to configure authentication: {e}")
154
+ if self.auth_config.enabled:
155
+ raise
156
+ return None
157
+
158
+ @property
159
+ def dependencies_available(self) -> bool:
160
+ """Lazy check for dependencies (only runs once, on first access)."""
161
+ if not self._dependencies_checked:
162
+ self._dependencies_available = self._check_dependencies()
163
+ self._dependencies_checked = True
164
+ if not self._dependencies_available:
165
+ logger.warning("Some dependencies are missing. Certain operations may not work correctly.")
166
+ return self._dependencies_available
167
+
168
+ def setup_tools(self):
169
+ """Set up the tools for the MCP server by calling all registration functions."""
170
+ # Register all tool modules
171
+ register_helm_tools(self.server, self.non_destructive, self._check_helm_availability)
172
+ register_pod_tools(self.server, self.non_destructive)
173
+ register_core_tools(self.server, self.non_destructive)
174
+ register_cluster_tools(self.server, self.non_destructive)
175
+ register_deployment_tools(self.server, self.non_destructive)
176
+ register_security_tools(self.server, self.non_destructive)
177
+ register_networking_tools(self.server, self.non_destructive)
178
+ register_storage_tools(self.server, self.non_destructive)
179
+ register_operations_tools(self.server, self.non_destructive)
180
+ register_diagnostics_tools(self.server, self.non_destructive)
181
+ register_cost_tools(self.server, self.non_destructive)
182
+
183
+ # Register optional browser tools if enabled and available
184
+ if is_browser_available():
185
+ register_browser_tools(self.server, self.non_destructive)
186
+ logger.info("Browser automation tools enabled (MCP_BROWSER_ENABLED=true)")
187
+ else:
188
+ logger.debug("Browser tools disabled (set MCP_BROWSER_ENABLED=true to enable)")
189
+
190
+ def setup_resources(self):
191
+ """Set up MCP resources for Kubernetes data exposure."""
192
+ register_resources(self.server)
193
+
194
+ def setup_prompts(self):
195
+ """Set up MCP prompts."""
196
+ register_prompts(self.server)
197
+
198
+ def _check_dependencies(self) -> bool:
199
+ """Check if required dependencies are available."""
200
+ kubectl_ok = self._check_kubectl_availability()
201
+ if not kubectl_ok:
202
+ logger.warning("kubectl is not available in PATH")
203
+ return kubectl_ok
204
+
205
+ def _check_tool_availability(self, tool: str) -> bool:
206
+ """Check if a tool (kubectl, helm) is available and working."""
207
+ try:
208
+ import subprocess
209
+ import shutil
210
+ if shutil.which(tool) is None:
211
+ return False
212
+ if tool == "kubectl":
213
+ subprocess.check_output(
214
+ [tool, "version", "--client", "--output=json"],
215
+ stderr=subprocess.PIPE,
216
+ timeout=2
217
+ )
218
+ elif tool == "helm":
219
+ subprocess.check_output(
220
+ [tool, "version", "--short"],
221
+ stderr=subprocess.PIPE,
222
+ timeout=2
223
+ )
224
+ return True
225
+ except (subprocess.SubprocessError, subprocess.TimeoutExpired, FileNotFoundError):
226
+ return False
227
+
228
+ def _check_kubectl_availability(self) -> bool:
229
+ """Check if kubectl is available."""
230
+ return self._check_tool_availability("kubectl")
231
+
232
+ def _check_helm_availability(self) -> bool:
233
+ """Check if helm is available."""
234
+ return self._check_tool_availability("helm")
235
+
236
+ def _check_destructive(self):
237
+ """Check if destructive operations are allowed.
238
+
239
+ Returns None if allowed, error dict if blocked.
240
+ """
241
+ if self.non_destructive:
242
+ return {"success": False, "error": "Operation blocked: non-destructive mode enabled"}
243
+ return None
244
+
245
+ def _mask_secrets(self, text: str) -> str:
246
+ """Mask sensitive data in text output.
247
+
248
+ Masks base64-encoded secrets, passwords, tokens, and API keys.
249
+ """
250
+ import re
251
+
252
+ # Mask base64-encoded data (common in Kubernetes secrets)
253
+ # Match data fields with base64 values (at least 16 chars)
254
+ text = re.sub(
255
+ r'(data:\s*\n(?:\s+\w+:\s*)[A-Za-z0-9+/=]{16,})',
256
+ lambda m: re.sub(r':\s*[A-Za-z0-9+/=]{16,}', ': [MASKED]', m.group(0)),
257
+ text
258
+ )
259
+
260
+ # Mask password fields
261
+ text = re.sub(
262
+ r'(password|passwd|secret|credential)(\s*[=:]\s*)["\']?[^"\'\s]+["\']?',
263
+ r'\1\2[MASKED]',
264
+ text,
265
+ flags=re.IGNORECASE
266
+ )
267
+
268
+ # Mask token fields
269
+ text = re.sub(
270
+ r'(token|api[_-]?key|auth[_-]?key)(\s*[=:]\s*)["\']?[^"\'\s]+["\']?',
271
+ r'\1\2[MASKED]',
272
+ text,
273
+ flags=re.IGNORECASE
274
+ )
275
+
276
+ # Mask Bearer tokens
277
+ text = re.sub(
278
+ r'(Bearer\s+)[A-Za-z0-9._-]+',
279
+ r'\1[MASKED]',
280
+ text
281
+ )
282
+
283
+ # Mask JWT tokens (three base64 sections separated by dots)
284
+ text = re.sub(
285
+ r'eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*',
286
+ '[MASKED]',
287
+ text
288
+ )
289
+
290
+ return text
291
+
292
+ async def serve_stdio(self):
293
+ """Serve the MCP server over stdio transport."""
294
+ if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true"):
295
+ logger.debug("Starting MCP server with stdio transport")
296
+ logger.debug(f"Working directory: {os.getcwd()}")
297
+ kube_config = os.environ.get('KUBECONFIG', '~/.kube/config')
298
+ expanded_path = os.path.expanduser(kube_config)
299
+ logger.debug(f"KUBECONFIG: {expanded_path}")
300
+ logger.debug(f"Dependencies: {'available' if self.dependencies_available else 'missing'}")
301
+
302
+ await self.server.run_stdio_async()
303
+
304
+ async def serve_sse(self, host: str = "0.0.0.0", port: int = 8000):
305
+ """Serve the MCP server over SSE transport.
306
+
307
+ Uses FastMCP 3's create_sse_app() to create a Starlette ASGI application
308
+ that handles Server-Sent Events for MCP communication.
309
+
310
+ SSE Endpoints:
311
+ - GET /sse: SSE connection endpoint for receiving server events
312
+ - POST /messages/: Endpoint for sending messages to the server
313
+ """
314
+ logger.info(f"Starting MCP server with SSE transport on {host}:{port}")
315
+
316
+ try:
317
+ import uvicorn
318
+ except ImportError:
319
+ logger.error("SSE transport requires 'uvicorn'. Install with: pip install uvicorn")
320
+ raise ImportError("Missing dependency for SSE transport. Run: pip install uvicorn")
321
+
322
+ try:
323
+ # FastMCP 3 uses create_sse_app() to create a Starlette ASGI app
324
+ from fastmcp.server.http import create_sse_app
325
+
326
+ # Create the SSE Starlette application
327
+ # message_path: POST endpoint for client messages
328
+ # sse_path: GET endpoint for SSE event stream
329
+ app = create_sse_app(
330
+ self.server,
331
+ message_path="/messages/",
332
+ sse_path="/sse"
333
+ )
334
+
335
+ logger.info(f"SSE endpoints: GET /sse (events), POST /messages/ (messages)")
336
+
337
+ # Run with uvicorn
338
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
339
+ server = uvicorn.Server(config)
340
+ await server.serve()
341
+
342
+ except ImportError as e:
343
+ # Fallback for older FastMCP versions that might have run_sse_async
344
+ logger.warning(f"create_sse_app not available: {e}. Trying legacy API...")
345
+ try:
346
+ await self.server.run_sse_async(host=host, port=port)
347
+ except (TypeError, AttributeError):
348
+ try:
349
+ await self.server.run_sse_async(port=port)
350
+ except (TypeError, AttributeError):
351
+ await self.server.run_sse_async()
352
+
353
+ async def serve_http(self, host: str = "0.0.0.0", port: int = 8000):
354
+ """
355
+ Serve the MCP server over HTTP transport (streamable HTTP).
356
+ This is an alternative to SSE that some clients prefer.
357
+ """
358
+ logger.info(f"Starting MCP server with HTTP transport on {host}:{port}")
359
+
360
+ try:
361
+ # Check if FastMCP supports streamable HTTP
362
+ if hasattr(self.server, 'run_http_async'):
363
+ await self.server.run_http_async(host=host, port=port)
364
+ elif hasattr(self.server, 'run_streamable_http_async'):
365
+ await self.server.run_streamable_http_async(host=host, port=port)
366
+ else:
367
+ # Fall back to implementing HTTP transport manually using ASGI
368
+ logger.info("FastMCP does not have built-in HTTP support, using custom implementation")
369
+ await self._serve_http_custom(host=host, port=port)
370
+ except TypeError as e:
371
+ logger.warning(f"HTTP transport parameter issue: {e}. Trying alternative signatures...")
372
+ # Try without parameters
373
+ if hasattr(self.server, 'run_http_async'):
374
+ await self.server.run_http_async()
375
+ elif hasattr(self.server, 'run_streamable_http_async'):
376
+ await self.server.run_streamable_http_async()
377
+ else:
378
+ await self._serve_http_custom(host=host, port=port)
379
+
380
+ async def _serve_http_custom(self, host: str = "0.0.0.0", port: int = 8000):
381
+ """
382
+ Custom HTTP server implementation using uvicorn and Starlette.
383
+ Provides HTTP/JSON-RPC transport for MCP.
384
+ """
385
+ try:
386
+ from starlette.applications import Starlette
387
+ from starlette.responses import JSONResponse
388
+ from starlette.routing import Route
389
+ import uvicorn
390
+ except ImportError:
391
+ logger.error("HTTP transport requires 'starlette' and 'uvicorn'. Install with: pip install starlette uvicorn")
392
+ raise ImportError("Missing dependencies for HTTP transport. Run: pip install starlette uvicorn")
393
+
394
+ async def handle_mcp_request(request):
395
+ """Handle incoming MCP JSON-RPC requests."""
396
+ try:
397
+ body = await request.json()
398
+ logger.debug(f"Received MCP request: {body}")
399
+
400
+ # Get the method and params from the JSON-RPC request
401
+ method = body.get("method", "")
402
+ params = body.get("params", {})
403
+ request_id = body.get("id")
404
+
405
+ # Handle different MCP methods
406
+ if method == "initialize":
407
+ result = {
408
+ "protocolVersion": "2024-11-05",
409
+ "capabilities": {
410
+ "tools": {"listChanged": True},
411
+ "resources": {"subscribe": False, "listChanged": True}
412
+ },
413
+ "serverInfo": {
414
+ "name": self.name,
415
+ "version": "1.2.0"
416
+ }
417
+ }
418
+ elif method == "tools/list":
419
+ # Get list of tools from FastMCP
420
+ tools = []
421
+ if hasattr(self.server, '_tool_manager') and hasattr(self.server._tool_manager, 'tools'):
422
+ for name, tool in self.server._tool_manager.tools.items():
423
+ tools.append({
424
+ "name": name,
425
+ "description": tool.description if hasattr(tool, 'description') else "",
426
+ "inputSchema": tool.parameters if hasattr(tool, 'parameters') else {}
427
+ })
428
+ result = {"tools": tools}
429
+ elif method == "tools/call":
430
+ tool_name = params.get("name", "")
431
+ tool_args = params.get("arguments", {})
432
+
433
+ # Execute the tool
434
+ if hasattr(self.server, '_tool_manager'):
435
+ try:
436
+ tool_result = await self.server._tool_manager.call_tool(tool_name, tool_args)
437
+ result = {"content": [{"type": "text", "text": json.dumps(tool_result)}]}
438
+ except Exception as e:
439
+ result = {"content": [{"type": "text", "text": f"Error: {str(e)}"}], "isError": True}
440
+ else:
441
+ result = {"content": [{"type": "text", "text": "Tool manager not available"}], "isError": True}
442
+ elif method == "ping":
443
+ result = {}
444
+ else:
445
+ result = {"error": f"Unknown method: {method}"}
446
+
447
+ response = {
448
+ "jsonrpc": "2.0",
449
+ "id": request_id,
450
+ "result": result
451
+ }
452
+ return JSONResponse(response)
453
+ except Exception as e:
454
+ logger.error(f"Error handling MCP request: {e}")
455
+ return JSONResponse({
456
+ "jsonrpc": "2.0",
457
+ "id": None,
458
+ "error": {"code": -32603, "message": str(e)}
459
+ }, status_code=500)
460
+
461
+ async def health_check(request):
462
+ """Health check endpoint."""
463
+ return JSONResponse({"status": "healthy", "server": self.name})
464
+
465
+ app = Starlette(
466
+ routes=[
467
+ Route("/", handle_mcp_request, methods=["POST"]),
468
+ Route("/mcp", handle_mcp_request, methods=["POST"]),
469
+ Route("/health", health_check, methods=["GET"]),
470
+ ]
471
+ )
472
+
473
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
474
+ server = uvicorn.Server(config)
475
+ await server.serve()
476
+
477
+
478
+ if __name__ == "__main__":
479
+ import argparse
480
+ import signal
481
+
482
+ parser = argparse.ArgumentParser(description="Run the Kubectl MCP Server.")
483
+ parser.add_argument(
484
+ "--transport",
485
+ type=str,
486
+ choices=["stdio", "sse", "http", "streamable-http"],
487
+ default="stdio",
488
+ help="Communication transport to use (stdio, sse, http, or streamable-http). Default: stdio.",
489
+ )
490
+ parser.add_argument(
491
+ "--port",
492
+ type=int,
493
+ default=8000,
494
+ help="Port to use for SSE/HTTP transport. Default: 8000.",
495
+ )
496
+ parser.add_argument(
497
+ "--host",
498
+ type=str,
499
+ default="0.0.0.0",
500
+ help="Host to bind to for SSE/HTTP transport. Default: 0.0.0.0.",
501
+ )
502
+ args = parser.parse_args()
503
+
504
+ server_name = "kubectl_mcp_server"
505
+ mcp_server = MCPServer(name=server_name)
506
+
507
+ # Handle signals gracefully with immediate exit
508
+ def signal_handler(sig, frame):
509
+ print("\nShutting down server...", file=sys.stderr)
510
+ os._exit(0)
511
+
512
+ signal.signal(signal.SIGINT, signal_handler)
513
+ signal.signal(signal.SIGTERM, signal_handler)
514
+
515
+ try:
516
+ if args.transport == "stdio":
517
+ logger.info(f"Starting {server_name} with stdio transport.")
518
+ asyncio.run(mcp_server.serve_stdio())
519
+ elif args.transport == "sse":
520
+ logger.info(f"Starting {server_name} with SSE transport on {args.host}:{args.port}.")
521
+ asyncio.run(mcp_server.serve_sse(host=args.host, port=args.port))
522
+ elif args.transport in ("http", "streamable-http"):
523
+ logger.info(f"Starting {server_name} with HTTP transport on {args.host}:{args.port}.")
524
+ asyncio.run(mcp_server.serve_http(host=args.host, port=args.port))
525
+ except KeyboardInterrupt:
526
+ print("\nShutting down server...", file=sys.stderr)
527
+ except SystemExit:
528
+ pass # Clean exit
529
+ except Exception as e:
530
+ logger.error(f"Server exited with error: {e}", exc_info=True)
@@ -0,0 +1,5 @@
1
+ from .prompts import register_prompts
2
+
3
+ __all__ = [
4
+ "register_prompts",
5
+ ]