feat(mcp): add support for production deployment with uvicorn (#8958)

This commit is contained in:
Rubén De la Torre Vico
2025-10-21 16:03:24 +02:00
committed by GitHub
parent 000cb93157
commit 34554d6123
9 changed files with 159 additions and 38 deletions

View File

@@ -182,19 +182,19 @@ Configure the server using environment variables:
|----------|-------------|----------|---------| |----------|-------------|----------|---------|
| `PROWLER_APP_API_KEY` | Prowler API key | Only for STDIO mode | - | | `PROWLER_APP_API_KEY` | Prowler API key | Only for STDIO mode | - |
| `PROWLER_API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com` | | `PROWLER_API_BASE_URL` | Custom Prowler API endpoint | No | `https://api.prowler.com` |
| `PROWLER_MCP_MODE` | Default transport mode (overwritten by `--transport` argument) | No | `stdio` | | `PROWLER_MCP_TRANSPORT_MODE` | Default transport mode (overwritten by `--transport` argument) | No | `stdio` |
<CodeGroup> <CodeGroup>
```bash macOS/Linux ```bash macOS/Linux
export PROWLER_APP_API_KEY="pk_your_api_key_here" export PROWLER_APP_API_KEY="pk_your_api_key_here"
export PROWLER_API_BASE_URL="https://api.prowler.com" export PROWLER_API_BASE_URL="https://api.prowler.com"
export PROWLER_MCP_MODE="http" export PROWLER_MCP_TRANSPORT_MODE="http"
``` ```
```bash Windows PowerShell ```bash Windows PowerShell
$env:PROWLER_APP_API_KEY="pk_your_api_key_here" $env:PROWLER_APP_API_KEY="pk_your_api_key_here"
$env:PROWLER_API_BASE_URL="https://api.prowler.com" $env:PROWLER_API_BASE_URL="https://api.prowler.com"
$env:PROWLER_MCP_MODE="http" $env:PROWLER_MCP_TRANSPORT_MODE="http"
``` ```
</CodeGroup> </CodeGroup>
@@ -209,7 +209,7 @@ For convenience, create a `.env` file in the `mcp_server` directory:
```bash .env ```bash .env
PROWLER_APP_API_KEY=pk_your_api_key_here PROWLER_APP_API_KEY=pk_your_api_key_here
PROWLER_API_BASE_URL=https://api.prowler.com PROWLER_API_BASE_URL=https://api.prowler.com
PROWLER_MCP_MODE=stdio PROWLER_MCP_TRANSPORT_MODE=stdio
``` ```
When using Docker, pass the environment file: When using Docker, pass the environment file:
@@ -228,6 +228,35 @@ uvx /path/to/prowler/mcp_server/
This is particularly useful when configuring MCP clients that need to launch the server from a specific path. This is particularly useful when configuring MCP clients that need to launch the server from a specific path.
## 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/).
### Entrypoint Script
The source tree includes `entrypoint.sh` to simplify switching between the
standard CLI runner and the ASGI app. The first argument selects the mode and
any additional flags are passed straight through:
```bash
# Default CLI experience (prowler-mcp console script)
./entrypoint.sh main --transport http --host 0.0.0.0
# ASGI app via uvicorn
./entrypoint.sh uvicorn --host 0.0.0.0 --port 9000
```
Omitting the mode defaults to `main`, matching the `prowler-mcp` console script.
When `uvicorn` mode is selected, the script exports `PROWLER_MCP_TRANSPORT_MODE=http` automatically.
This is the default entrypoint for the Docker container.
## Next Steps ## Next Steps
Now that you have the Prowler MCP Server installed, proceed to configure your MCP client: Now that you have the Prowler MCP Server installed, proceed to configure your MCP client:

View File

@@ -1,3 +1,3 @@
PROWLER_APP_API_KEY="pk_your_api_key_here" PROWLER_APP_API_KEY="pk_your_api_key_here"
PROWLER_API_BASE_URL="https://api.prowler.com" PROWLER_API_BASE_URL="https://api.prowler.com"
PROWLER_MCP_MODE="stdio" PROWLER_MCP_TRANSPORT_MODE="stdio"

View File

@@ -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) - 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) - 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) - 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)

View File

@@ -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) # 3. Project metadata file (may be needed by some packages at runtime)
COPY --from=builder --chown=prowler /app/pyproject.toml /app/pyproject.toml 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 # Add virtual environment to PATH so prowler-mcp command is available
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
# Entry point for the MCP server # Entrypoint wrapper defaults to CLI mode; override with `uvicorn` to run ASGI app
# Default to stdio mode, but allow overriding via command arguments ENTRYPOINT ["/app/entrypoint.sh"]
# Examples: CMD ["main"]
# 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"]

View File

@@ -144,11 +144,11 @@ uv run prowler-mcp --transport http
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8080 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 ```bash
export PROWLER_API_BASE_URL="https://api.prowler.com" export PROWLER_API_BASE_URL="https://api.prowler.com"
export PROWLER_MCP_MODE="http" export PROWLER_MCP_TRANSPORT_MODE="http"
``` ```
### Using uv directly ### 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 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 ## Command Line Arguments
The Prowler MCP server supports the following 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. 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 ## License
This project follows the repositorys 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
View 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

View File

@@ -1,10 +1,8 @@
import argparse import argparse
import asyncio
import os import os
import sys import sys
from prowler_mcp_server.lib.logger import logger from prowler_mcp_server.lib.logger import logger
from prowler_mcp_server.server import setup_main_server
def parse_arguments(): def parse_arguments():
@@ -13,7 +11,7 @@ def parse_arguments():
parser.add_argument( parser.add_argument(
"--transport", "--transport",
choices=["stdio", "http"], choices=["stdio", "http"],
default=os.getenv("PROWLER_MCP_MODE", "stdio"), default=None,
help="Transport method (default: stdio)", help="Transport method (default: stdio)",
) )
parser.add_argument( parser.add_argument(
@@ -35,13 +33,26 @@ def main():
try: try:
args = parse_arguments() args = parse_arguments()
# Set up server with configuration print(f"args.transport: {args.transport}")
prowler_mcp_server = asyncio.run(setup_main_server(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": if args.transport == "stdio":
prowler_mcp_server.run(transport="stdio") prowler_mcp_server.run(transport=args.transport, show_banner=False)
elif args.transport == "http": 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: except KeyboardInterrupt:
logger.info("Shutting down Prowler MCP server...") logger.info("Shutting down Prowler MCP server...")

View File

@@ -14,7 +14,7 @@ class ProwlerAppAuth:
def __init__( def __init__(
self, 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"), base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
): ):
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
@@ -33,7 +33,14 @@ class ProwlerAppAuth:
raise ValueError("Prowler App API key format is incorrect") raise ValueError("Prowler App API key format is incorrect")
def _parse_jwt(self, token: str) -> Optional[Dict]: 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: if not token:
return None return None

View File

@@ -1,16 +1,15 @@
import asyncio
import os import os
from fastmcp import FastMCP from fastmcp import FastMCP
from prowler_mcp_server.lib.logger import logger from prowler_mcp_server.lib.logger import logger
from starlette.responses import JSONResponse 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.""" """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 # Import Prowler Hub tools with prowler_hub_ prefix
try: try:
logger.info("Importing Prowler Hub server...") logger.info("Importing Prowler Hub server...")
@@ -21,12 +20,10 @@ async def setup_main_server(transport: str) -> FastMCP:
except Exception as e: except Exception as e:
logger.error(f"Failed to import Prowler Hub server: {e}") logger.error(f"Failed to import Prowler Hub server: {e}")
# Import Prowler App tools with prowler_app_ prefix
try: try:
logger.info("Importing Prowler App server...") 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( if not os.path.exists(
os.path.join(os.path.dirname(__file__), "prowler_app", "server.py") 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: except Exception as e:
logger.error(f"Failed to import Prowler App server: {e}") logger.error(f"Failed to import Prowler App server: {e}")
# Import Prowler Documentation tools with prowler_docs_ prefix
try: try:
logger.info("Importing Prowler Documentation server...") logger.info("Importing Prowler Documentation server...")
from prowler_mcp_server.prowler_documentation.server import docs_mcp_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: except Exception as e:
logger.error(f"Failed to import Prowler Documentation server: {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()