mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(mcp): add support for production deployment with uvicorn (#8958)
This commit is contained in:
committed by
GitHub
parent
000cb93157
commit
34554d6123
@@ -1,3 +1,3 @@
|
||||
PROWLER_APP_API_KEY="pk_your_api_key_here"
|
||||
PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
PROWLER_MCP_MODE="stdio"
|
||||
PROWLER_MCP_TRANSPORT_MODE="stdio"
|
||||
|
||||
@@ -13,4 +13,5 @@ All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
- Add new MCP Server for Prowler Documentation [(#8795)](https://github.com/prowler-cloud/prowler/pull/8795)
|
||||
- API key support for STDIO mode and enhanced HTTP mode authentication [(#8823)](https://github.com/prowler-cloud/prowler/pull/8823)
|
||||
- Add health check endpoint [(#8905)](https://github.com/prowler-cloud/prowler/pull/8905)
|
||||
- Update Prowler Documentation MCP Server to use Mintlify API [(#8915)](https://github.com/prowler-cloud/prowler/pull/8915)
|
||||
- Update Prowler Documentation MCP Server to use Mintlify API [(#8916)](https://github.com/prowler-cloud/prowler/pull/8916)
|
||||
- Add custom production deployment using uvicorn [(#8958)](https://github.com/prowler-cloud/prowler/pull/8958)
|
||||
|
||||
@@ -47,13 +47,12 @@ COPY --from=builder --chown=prowler /app/prowler_mcp_server /app/prowler_mcp_ser
|
||||
# 3. Project metadata file (may be needed by some packages at runtime)
|
||||
COPY --from=builder --chown=prowler /app/pyproject.toml /app/pyproject.toml
|
||||
|
||||
# 4. Entrypoint helper script for selecting runtime mode
|
||||
COPY --from=builder --chown=prowler /app/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
# Add virtual environment to PATH so prowler-mcp command is available
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Entry point for the MCP server
|
||||
# Default to stdio mode, but allow overriding via command arguments
|
||||
# Examples:
|
||||
# docker run -p 8000:8000 prowler-mcp --transport http --host 0.0.0.0 --port 8000
|
||||
# docker run prowler-mcp --transport stdio
|
||||
ENTRYPOINT ["prowler-mcp"]
|
||||
CMD ["--transport", "stdio"]
|
||||
# Entrypoint wrapper defaults to CLI mode; override with `uvicorn` to run ASGI app
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["main"]
|
||||
|
||||
@@ -144,11 +144,11 @@ uv run prowler-mcp --transport http
|
||||
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
For self-deployed MCP remote server, you can use also configure the server to use a custom API base URL with the environment variable `PROWLER_API_BASE_URL`; and the transport mode with the environment variable `PROWLER_MCP_MODE`.
|
||||
For self-deployed MCP remote server, you can use also configure the server to use a custom API base URL with the environment variable `PROWLER_API_BASE_URL`; and the transport mode with the environment variable `PROWLER_MCP_TRANSPORT_MODE`.
|
||||
|
||||
```bash
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
export PROWLER_MCP_MODE="http"
|
||||
export PROWLER_MCP_TRANSPORT_MODE="http"
|
||||
```
|
||||
|
||||
### Using uv directly
|
||||
@@ -190,6 +190,16 @@ docker run --rm --env-file ./.env -p 8000:8000 -it prowler-mcp --transport http
|
||||
docker run --rm --env-file ./.env -p 8080:8080 -it prowler-mcp --transport http --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production deployments that require customization, it is recommended to use the ASGI application that can be found in `prowler_mcp_server.server`. This can be run with uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn prowler_mcp_server.server:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
For more details on production deployment options, see the [FastMCP production deployment guide](https://gofastmcp.com/deployment/http#production-deployment) and [uvicorn settings](https://www.uvicorn.org/settings/).
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
The Prowler MCP server supports the following command line arguments:
|
||||
@@ -482,6 +492,10 @@ If you want to have it globally available, add the example server to Cursor's co
|
||||
|
||||
If you want to have it only for the current project, add the example server to the project's root in a new `.cursor/mcp.json` file.
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed documentation about the Prowler MCP Server, including guides, tutorials, and use cases, visit the [official Prowler documentation](https://docs.prowler.com).
|
||||
|
||||
## License
|
||||
|
||||
This project follows the repository’s main license. See the [LICENSE](../LICENSE) file at the repository root.
|
||||
This project follows the repository's main license. See the [LICENSE](../LICENSE) file at the repository root.
|
||||
|
||||
50
mcp_server/entrypoint.sh
Executable file
50
mcp_server/entrypoint.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./entrypoint.sh [main|uvicorn] [args...]
|
||||
|
||||
Modes:
|
||||
main (default) Run prowler-mcp
|
||||
uvicorn Run uvicorn prowler_mcp_server.server:app
|
||||
|
||||
All additional arguments are forwarded to the selected command.
|
||||
EOF
|
||||
}
|
||||
|
||||
mode="main"
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
case "$1" in
|
||||
main|cli)
|
||||
mode="main"
|
||||
shift
|
||||
;;
|
||||
uvicorn|asgi)
|
||||
mode="uvicorn"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
mode="main"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "$mode" in
|
||||
main)
|
||||
exec prowler-mcp "$@"
|
||||
;;
|
||||
uvicorn)
|
||||
export PROWLER_MCP_TRANSPORT_MODE="http"
|
||||
exec uvicorn prowler_mcp_server.server:app "$@"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
from prowler_mcp_server.server import setup_main_server
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
@@ -13,7 +11,7 @@ def parse_arguments():
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "http"],
|
||||
default=os.getenv("PROWLER_MCP_MODE", "stdio"),
|
||||
default=None,
|
||||
help="Transport method (default: stdio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -35,13 +33,26 @@ def main():
|
||||
try:
|
||||
args = parse_arguments()
|
||||
|
||||
# Set up server with configuration
|
||||
prowler_mcp_server = asyncio.run(setup_main_server(transport=args.transport))
|
||||
print(f"args.transport: {args.transport}")
|
||||
|
||||
if args.transport is None:
|
||||
args.transport = os.getenv("PROWLER_MCP_TRANSPORT_MODE", "stdio")
|
||||
else:
|
||||
os.environ["PROWLER_MCP_TRANSPORT_MODE"] = args.transport
|
||||
|
||||
from prowler_mcp_server.server import prowler_mcp_server
|
||||
|
||||
if args.transport == "stdio":
|
||||
prowler_mcp_server.run(transport="stdio")
|
||||
prowler_mcp_server.run(transport=args.transport, show_banner=False)
|
||||
elif args.transport == "http":
|
||||
prowler_mcp_server.run(transport="http", host=args.host, port=args.port)
|
||||
prowler_mcp_server.run(
|
||||
transport=args.transport,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
show_banner=False,
|
||||
)
|
||||
else:
|
||||
logger.error(f"Invalid transport: {args.transport}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down Prowler MCP server...")
|
||||
|
||||
@@ -14,7 +14,7 @@ class ProwlerAppAuth:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = os.getenv("PROWLER_MCP_MODE", "stdio"),
|
||||
mode: str = os.getenv("PROWLER_MCP_TRANSPORT_MODE", "stdio"),
|
||||
base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
@@ -33,7 +33,14 @@ class ProwlerAppAuth:
|
||||
raise ValueError("Prowler App API key format is incorrect")
|
||||
|
||||
def _parse_jwt(self, token: str) -> Optional[Dict]:
|
||||
"""Parse JWT token and return payload, similar to JS parseJwt function."""
|
||||
"""Parse JWT token and return payload
|
||||
|
||||
Args:
|
||||
token: JWT token to parse
|
||||
|
||||
Returns:
|
||||
Parsed JWT payload, or None if parsing fails
|
||||
"""
|
||||
if not token:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server")
|
||||
|
||||
async def setup_main_server(transport: str) -> FastMCP:
|
||||
|
||||
async def setup_main_server():
|
||||
"""Set up the main Prowler MCP server with all available integrations."""
|
||||
|
||||
# Initialize main Prowler MCP server
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server")
|
||||
|
||||
# Import Prowler Hub tools with prowler_hub_ prefix
|
||||
try:
|
||||
logger.info("Importing Prowler Hub server...")
|
||||
@@ -21,12 +20,10 @@ async def setup_main_server(transport: str) -> FastMCP:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import Prowler Hub server: {e}")
|
||||
|
||||
# Import Prowler App tools with prowler_app_ prefix
|
||||
try:
|
||||
logger.info("Importing Prowler App server...")
|
||||
|
||||
if os.getenv("PROWLER_MCP_MODE", None) is None:
|
||||
os.environ["PROWLER_MCP_MODE"] = transport
|
||||
|
||||
if not os.path.exists(
|
||||
os.path.join(os.path.dirname(__file__), "prowler_app", "server.py")
|
||||
):
|
||||
@@ -44,6 +41,7 @@ async def setup_main_server(transport: str) -> FastMCP:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import Prowler App server: {e}")
|
||||
|
||||
# Import Prowler Documentation tools with prowler_docs_ prefix
|
||||
try:
|
||||
logger.info("Importing Prowler Documentation server...")
|
||||
from prowler_mcp_server.prowler_documentation.server import docs_mcp_server
|
||||
@@ -53,9 +51,21 @@ async def setup_main_server(transport: str) -> FastMCP:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import Prowler Documentation server: {e}")
|
||||
|
||||
# Add health check endpoint
|
||||
@prowler_mcp_server.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request):
|
||||
return JSONResponse({"status": "healthy", "service": "prowler-mcp-server"})
|
||||
|
||||
return prowler_mcp_server
|
||||
# Add health check endpoint
|
||||
@prowler_mcp_server.custom_route("/health", methods=["GET"])
|
||||
async def health_check() -> JSONResponse:
|
||||
"""Health check endpoint."""
|
||||
return JSONResponse({"status": "healthy", "service": "prowler-mcp-server"})
|
||||
|
||||
|
||||
# Get or create the event loop
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# If we have a running loop, schedule the setup as a task
|
||||
loop.create_task(setup_main_server())
|
||||
except RuntimeError:
|
||||
# No running loop, use asyncio.run (for standalone execution)
|
||||
asyncio.run(setup_main_server())
|
||||
|
||||
app = prowler_mcp_server.http_app()
|
||||
|
||||
Reference in New Issue
Block a user