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
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
|
|||||||
@@ -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 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 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...")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user