From fccc4c6cc6e0ba0fccaa4ad83376712ef3756ee0 Mon Sep 17 00:00:00 2001 From: codeMonkey-shin Date: Mon, 21 Jul 2025 09:02:37 +0900 Subject: [PATCH] feat: Add --listen option for configurable host binding - Add --listen command line option to all entry points - Support LISTEN environment variable with special 'true' value for 0.0.0.0 binding - Default to 127.0.0.1 for security, use LISTEN=true for external access - Update Dockerfile to use secure defaults - Update README with Docker usage examples and environment variables - Maintain backward compatibility with existing deployments --- Dockerfile | 5 ++-- README.md | 43 +++++++++++++++++++++++++++++++--- kubectl_mcp_tool/cli.py | 17 ++++++++++---- kubectl_mcp_tool/mcp_server.py | 42 +++++++++++++++++++++++---------- run_server.py | 21 +++++++++++++---- 5 files changed, 103 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index a03e214..2fc317b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,5 +42,6 @@ EXPOSE 8000 ENV TRANSPORT=sse \ PORT=8000 -# Run the server (align with FastMCP default port 8000) -CMD ["python", "run_server.py", "--transport", "sse", "--port", "8000"] +# Run the server using environment variables +# Default binds to 127.0.0.1, set LISTEN=true to bind to 0.0.0.0 +CMD ["python", "run_server.py", "--transport", "sse"] diff --git a/README.md b/README.md index 4e589bb..2f35c87 100644 --- a/README.md +++ b/README.md @@ -182,12 +182,16 @@ docker pull rohitghumare64/kubectl-mcp-server:latest ### Running the image -The server inside the container listens on port **8000**. Bind any free host port to 8000 and mount your kubeconfig: +The server inside the container listens on port **8000** and binds to **127.0.0.1** by default. For external access, you need to set `LISTEN=true` to bind to all interfaces (0.0.0.0): ```bash -# Replace 8081 with any free port on your host -# Mount your local ~/.kube directory for cluster credentials +# For external access (bind to 0.0.0.0) +docker run -p 8081:8000 \ + -e LISTEN=true \ + -v $HOME/.kube:/root/.kube \ + rohitghumare64/kubectl-mcp-server:latest +# For localhost only (default behavior) docker run -p 8081:8000 \ -v $HOME/.kube:/root/.kube \ rohitghumare64/kubectl-mcp-server:latest @@ -195,6 +199,39 @@ docker run -p 8081:8000 \ * `-p 8081:8000` maps host port 8081 → container port 8000. * `-v $HOME/.kube:/root/.kube` mounts your kubeconfig so the server can reach the cluster. +* `-e LISTEN=true` binds to 0.0.0.0 for external access. + +### Environment Variables + +You can customize the server behavior using environment variables: + +```bash +# Bind to all interfaces (0.0.0.0) for external access +docker run -p 8000:8000 \ + -e LISTEN=true \ + -v $HOME/.kube:/root/.kube \ + rohitghumare64/kubectl-mcp-server:latest + +# Custom port with external access +docker run -p 9000:9000 \ + -e PORT=9000 \ + -e LISTEN=true \ + -v $HOME/.kube:/root/.kube \ + rohitghumare64/kubectl-mcp-server:latest + +# Use different transport (stdio for direct integration) +docker run -e TRANSPORT=stdio \ + -v $HOME/.kube:/root/.kube \ + rohitghumare64/kubectl-mcp-server:latest +``` + +Available environment variables: +- `TRANSPORT`: `sse` (default) or `stdio` +- `PORT`: Port number (default: 8000) +- `LISTEN`: + - Default: `127.0.0.1` (localhost only) + - `true`: Bind to 0.0.0.0 (all interfaces) + - Any IP address: Bind to specific address ### Building a multi-architecture image (AMD64 & ARM64) diff --git a/kubectl_mcp_tool/cli.py b/kubectl_mcp_tool/cli.py index 2704328..549600c 100644 --- a/kubectl_mcp_tool/cli.py +++ b/kubectl_mcp_tool/cli.py @@ -32,11 +32,11 @@ async def serve_stdio(): server = MCPServer("kubectl-mcp-tool") await server.serve_stdio() -async def serve_sse(port: int): +async def serve_sse(port: int, host: str = "127.0.0.1"): """Serve the MCP server over SSE transport.""" - logger.info(f"Starting standard MCP server with SSE transport on port {port}") + logger.info(f"Starting standard MCP server with SSE transport on {host}:{port}") server = MCPServer("kubectl-mcp-tool") - await server.serve_sse(port) + await server.serve_sse(port, host) def main(): """Main entry point for the CLI.""" @@ -49,6 +49,15 @@ def main(): help="Transport to use (stdio or sse)") serve_parser.add_argument("--port", type=int, default=8080, help="Port to use for SSE transport") + # Handle LISTEN environment variable logic + listen_env = os.environ.get("LISTEN", "127.0.0.1") + if listen_env.lower() == "true": + listen_default = "0.0.0.0" + else: + listen_default = listen_env + + serve_parser.add_argument("--listen", type=str, default=listen_default, + help="Host address to bind to for SSE transport. Use 0.0.0.0 to bind to all interfaces (or set LISTEN=true)") serve_parser.add_argument("--cursor", action="store_true", help="Enable Cursor compatibility mode") serve_parser.add_argument("--debug", action="store_true", @@ -88,7 +97,7 @@ def main(): if args.transport == "stdio": asyncio.run(serve_stdio()) else: - asyncio.run(serve_sse(args.port)) + asyncio.run(serve_sse(args.port, args.listen)) except Exception as e: logger.error(f"Error starting standard MCP server: {e}") if args.debug: diff --git a/kubectl_mcp_tool/mcp_server.py b/kubectl_mcp_tool/mcp_server.py index 69c2f69..2577591 100644 --- a/kubectl_mcp_tool/mcp_server.py +++ b/kubectl_mcp_tool/mcp_server.py @@ -842,20 +842,24 @@ async def serve_stdio(self): # Continue with normal server startup await self.server.run_stdio_async() - async def serve_sse(self, port: int): + async def serve_sse(self, port: int, host: str = "127.0.0.1"): """Serve the MCP server over SSE transport.""" - logger.info(f"Starting MCP server with SSE transport on port {port}") + logger.info(f"Starting MCP server with SSE transport on {host}:{port}") try: - # Newer versions of FastMCP expose a keyword argument for the port - await self.server.run_sse_async(port=port) + # Newer versions of FastMCP expose keyword arguments for port and host + await self.server.run_sse_async(port=port, host=host) except TypeError: - # Fall back to the legacy signature that takes no parameters - logger.warning( - "FastMCP.run_sse_async() does not accept a 'port' parameter in this version. " - "Falling back to the default signature (using FastMCP's internal default port)." - ) - await self.server.run_sse_async() + try: + # Try with just port parameter + await self.server.run_sse_async(port=port) + except TypeError: + # Fall back to the legacy signature that takes no parameters + logger.warning( + "FastMCP.run_sse_async() does not accept 'port' or 'host' parameters in this version. " + "Falling back to the default signature (using FastMCP's internal defaults)." + ) + await self.server.run_sse_async() if __name__ == "__main__": import asyncio @@ -878,6 +882,20 @@ async def serve_sse(self, port: int): default=8080, help="Port to use for SSE transport. Default: 8080.", ) + # Handle LISTEN environment variable logic + import os + listen_env = os.environ.get("LISTEN", "127.0.0.1") + if listen_env.lower() == "true": + listen_default = "0.0.0.0" + else: + listen_default = listen_env + + parser.add_argument( + "--listen", + type=str, + default=listen_default, + help="Host address to bind to for SSE transport. Use 0.0.0.0 to bind to all interfaces. Default: 127.0.0.1 (or set LISTEN=true for 0.0.0.0).", + ) args = parser.parse_args() server_name = "kubectl_mcp_server" @@ -892,8 +910,8 @@ async def serve_sse(self, port: int): logger.info(f"Starting {server_name} with stdio transport.") loop.run_until_complete(mcp_server.serve_stdio()) elif args.transport == "sse": - logger.info(f"Starting {server_name} with SSE transport on port {args.port}.") - loop.run_until_complete(mcp_server.serve_sse(port=args.port)) + logger.info(f"Starting {server_name} with SSE transport on {args.listen}:{args.port}.") + loop.run_until_complete(mcp_server.serve_sse(port=args.port, host=args.listen)) except KeyboardInterrupt: logger.info("Server shutdown requested by user.") except Exception as e: diff --git a/run_server.py b/run_server.py index 8d23d49..c9370fb 100755 --- a/run_server.py +++ b/run_server.py @@ -34,8 +34,21 @@ def main(): parser.add_argument( "--port", type=int, - default=8080, - help="Port to use for SSE transport. Default: 8080.", + default=int(os.environ.get("PORT", 8080)), + help="Port to use for SSE transport. Default: 8080 (or PORT env var).", + ) + # Handle LISTEN environment variable logic + listen_env = os.environ.get("LISTEN", "127.0.0.1") + if listen_env.lower() == "true": + listen_default = "0.0.0.0" + else: + listen_default = listen_env + + parser.add_argument( + "--listen", + type=str, + default=listen_default, + help="Host address to bind to for SSE transport. Use 0.0.0.0 to bind to all interfaces. Default: 127.0.0.1 (or set LISTEN=true for 0.0.0.0).", ) args = parser.parse_args() @@ -48,8 +61,8 @@ def main(): logger.info(f"Starting {server_name} with stdio transport.") loop.run_until_complete(mcp_server.serve_stdio()) elif args.transport == "sse": - logger.info(f"Starting {server_name} with SSE transport on port {args.port}.") - loop.run_until_complete(mcp_server.serve_sse(port=args.port)) + logger.info(f"Starting {server_name} with SSE transport on {args.listen}:{args.port}.") + loop.run_until_complete(mcp_server.serve_sse(port=args.port, host=args.listen)) except KeyboardInterrupt: logger.info("Server shutdown requested by user.") except Exception as e: