mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(mcp): add HTTP transport support (#8784)
This commit is contained in:
committed by
GitHub
parent
5cfe140b7b
commit
4e143cf013
@@ -1,4 +1,5 @@
|
||||
PROWLER_APP_EMAIL="your_registered@email.com"
|
||||
PROWLER_APP_PASSWORD="your_user_pass"
|
||||
PROWLER_APP_TENANT_ID="optional_tenant_to_login"
|
||||
PROWLER_API_BASE_URL=https://api.prowler.com
|
||||
PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
PROWLER_MCP_MODE="stdio"
|
||||
|
||||
@@ -51,4 +51,9 @@ COPY --from=builder --chown=prowler /app/pyproject.toml /app/pyproject.toml
|
||||
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"]
|
||||
|
||||
@@ -43,6 +43,43 @@ docker run --rm --env-file ./.env -it prowler-mcp
|
||||
|
||||
## Running
|
||||
|
||||
The Prowler MCP server supports two transport modes:
|
||||
- **STDIO mode** (default): For direct integration with MCP clients like Claude Desktop
|
||||
- **HTTP mode**: For remote access over HTTP with Bearer token authentication
|
||||
|
||||
### Transport Modes
|
||||
|
||||
#### STDIO Mode (Default)
|
||||
|
||||
STDIO mode is the standard MCP transport for direct client integration:
|
||||
|
||||
```bash
|
||||
cd prowler/mcp_server
|
||||
uv run prowler-mcp
|
||||
# or
|
||||
uv run prowler-mcp --transport stdio
|
||||
```
|
||||
|
||||
#### HTTP Mode (Remote Server)
|
||||
|
||||
HTTP mode allows the server to run as a remote service accessible over HTTP:
|
||||
|
||||
```bash
|
||||
cd prowler/mcp_server
|
||||
# Run on default host and port (127.0.0.1:8000)
|
||||
uv run prowler-mcp --transport http
|
||||
|
||||
# Run on custom host and port
|
||||
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`.
|
||||
|
||||
```bash
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
export PROWLER_MCP_MODE="http"
|
||||
```
|
||||
|
||||
### Using uv directly
|
||||
|
||||
After installation, start the MCP server via the console script:
|
||||
@@ -60,13 +97,61 @@ uvx /path/to/prowler/mcp_server/
|
||||
|
||||
### Using Docker
|
||||
|
||||
Run the pre-built Docker container:
|
||||
#### STDIO Mode (Default)
|
||||
|
||||
Run the pre-built Docker container in STDIO mode:
|
||||
|
||||
```bash
|
||||
cd prowler/mcp_server
|
||||
docker run --rm --env-file ./.env -it prowler-mcp
|
||||
```
|
||||
|
||||
#### HTTP Mode (Remote Server)
|
||||
|
||||
Run as a remote HTTP server:
|
||||
|
||||
```bash
|
||||
cd prowler/mcp_server
|
||||
# Run on port 8000 (accessible from host)
|
||||
docker run --rm --env-file ./.env -p 8000:8000 -it prowler-mcp --transport http --host 0.0.0.0 --port 8000
|
||||
|
||||
# Run on custom port
|
||||
docker run --rm --env-file ./.env -p 8080:8080 -it prowler-mcp --transport http --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
The Prowler MCP server supports the following command line arguments:
|
||||
|
||||
```
|
||||
prowler-mcp [--transport {stdio,http}] [--host HOST] [--port PORT]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `--transport {stdio,http}`: Transport method (default: stdio)
|
||||
- `stdio`: Standard input/output transport for direct MCP client integration
|
||||
- `http`: HTTP transport for remote server access
|
||||
- `--host HOST`: Host to bind to for HTTP transport (default: 127.0.0.1)
|
||||
- `--port PORT`: Port to bind to for HTTP transport (default: 8000)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Default STDIO mode
|
||||
prowler-mcp
|
||||
|
||||
# Explicit STDIO mode
|
||||
prowler-mcp --transport stdio
|
||||
|
||||
# HTTP mode with default host and port (127.0.0.1:8000)
|
||||
prowler-mcp --transport http
|
||||
|
||||
# HTTP mode accessible from any network interface
|
||||
prowler-mcp --transport http --host 0.0.0.0
|
||||
|
||||
# HTTP mode with custom port
|
||||
prowler-mcp --transport http --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Prowler Hub
|
||||
@@ -130,9 +215,16 @@ All tools are exposed under the `prowler_app` prefix.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
### Prowler Cloud and Prowler App (Self-Managed) Authentication
|
||||
|
||||
For Prowler Cloud and Prowler App (Self-Managed) features, you need to set the following environment variables:
|
||||
> [!IMPORTANT]
|
||||
> Authentication is not needed for using Prowler Hub features.
|
||||
|
||||
The Prowler MCP server supports different authentication in Prowler Cloud and Prowler App (Self-Managed) methods depending on the transport mode:
|
||||
|
||||
#### STDIO Mode Authentication
|
||||
|
||||
For STDIO mode, authentication is handled via environment variables:
|
||||
|
||||
```bash
|
||||
# Required for Prowler Cloud and Prowler App (Self-Managed) authentication
|
||||
@@ -146,11 +238,36 @@ export PROWLER_APP_TENANT_ID="your-tenant-id"
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
```
|
||||
|
||||
#### HTTP Mode Authentication
|
||||
|
||||
For HTTP mode (remote server), authentication is handled via Bearer tokens. You need to obtain a JWT token from Prowler Cloud/App and include the generated token in the MCP client configuration. To get a valid token, you can use the following command (replace the email and password with your own credentials):
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.prowler.com/api/v1/tokens \
|
||||
-H "Content-Type: application/vnd.api+json" \
|
||||
-H "Accept: application/vnd.api+json" \
|
||||
-d '{
|
||||
"data": {
|
||||
"type": "tokens",
|
||||
"attributes": {
|
||||
"email": "your-email@example.com",
|
||||
"password": "your-password"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
The response will be a JWT token that you can use to [authenticate your MCP client](#http-mode-configuration-remote-server).
|
||||
|
||||
### MCP Client Configuration
|
||||
|
||||
Configure your MCP client, like Claude Desktop, Cursor, etc, to launch the server. Below are examples for both direct execution and Docker deployment; consult your client's documentation for exact locations.
|
||||
Configure your MCP client, like Claude Desktop, Cursor, etc, to connect to the server. The configuration depends on whether you're running in STDIO mode (local) or HTTP mode (remote).
|
||||
|
||||
#### Using uvx (Direct Execution)
|
||||
#### STDIO Mode Configuration
|
||||
|
||||
For local execution, configure your MCP client to launch the server directly. Below are examples for both direct execution and Docker deployment; consult your client's documentation for exact locations.
|
||||
|
||||
##### Using uvx (Direct Execution)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -169,7 +286,7 @@ Configure your MCP client, like Claude Desktop, Cursor, etc, to launch the serve
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Docker
|
||||
##### Using Docker
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -189,6 +306,29 @@ Configure your MCP client, like Claude Desktop, Cursor, etc, to launch the serve
|
||||
}
|
||||
```
|
||||
|
||||
#### HTTP Mode Configuration (Remote Server)
|
||||
|
||||
For HTTP mode, you can configure your MCP client to connect to a remote Prowler MCP server.
|
||||
|
||||
**Important Limitations:**
|
||||
- HTTP mode support varies by client - some clients may not support HTTP transport yet.
|
||||
- Some MCP clients like Claude Desktop only support OAuth authentication for HTTP connections, which is not currently supported by our MCP server.
|
||||
|
||||
Example configuration for clients that support HTTP transport:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"url": "http://mcp.prowler.com/mcp", // Replace with your own MCP server URL, by default when server is run in local it is http://localhost:8000/mcp
|
||||
"headers": {
|
||||
"Authorization": "Bearer <your-jwt-token-here>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Desktop (macOS/Windows)
|
||||
|
||||
Add the example server to Claude Desktop's config file, then restart the app.
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
from prowler_mcp_server.server import prowler_mcp_server, setup_main_server
|
||||
from prowler_mcp_server.server import setup_main_server
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(description="Prowler MCP Server")
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=["stdio", "http"],
|
||||
default=os.getenv("PROWLER_MCP_MODE", "stdio"),
|
||||
help="Transport method (default: stdio)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="127.0.0.1",
|
||||
help="Host to bind to for HTTP transport (default: 127.0.0.1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port to bind to for HTTP transport (default: 8000)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the MCP server."""
|
||||
try:
|
||||
asyncio.run(setup_main_server())
|
||||
prowler_mcp_server.run()
|
||||
args = parse_arguments()
|
||||
|
||||
# Set up server with configuration
|
||||
prowler_mcp_server = asyncio.run(setup_main_server(transport=args.transport))
|
||||
|
||||
if args.transport == "stdio":
|
||||
prowler_mcp_server.run(transport="stdio")
|
||||
elif args.transport == "http":
|
||||
prowler_mcp_server.run(transport="http", host=args.host, port=args.port)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down Prowler MCP server...")
|
||||
sys.exit(0)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"""Authentication manager for Prowler App API."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
@@ -7,6 +5,7 @@ from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
import httpx
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from prowler_mcp_server import __version__
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
|
||||
@@ -14,26 +13,32 @@ from prowler_mcp_server.lib.logger import logger
|
||||
class ProwlerAppAuth:
|
||||
"""Handles authentication and token management for Prowler App API."""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = os.getenv(
|
||||
"PROWLER_API_BASE_URL", "https://api.prowler.com"
|
||||
).rstrip("/")
|
||||
self.email = os.getenv("PROWLER_APP_EMAIL")
|
||||
self.password = os.getenv("PROWLER_APP_PASSWORD")
|
||||
self.tenant_id = os.getenv("PROWLER_APP_TENANT_ID", None)
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = os.getenv("PROWLER_MCP_MODE", "stdio"),
|
||||
base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
logger.info(f"Using Prowler App API base URL: {self.base_url}")
|
||||
self.mode = mode
|
||||
|
||||
if mode == "stdio": # STDIO mode
|
||||
self.email = os.getenv("PROWLER_APP_EMAIL")
|
||||
self.password = os.getenv("PROWLER_APP_PASSWORD")
|
||||
self.tenant_id = os.getenv("PROWLER_APP_TENANT_ID", None)
|
||||
|
||||
if not self.email or not self.password:
|
||||
raise ValueError(
|
||||
"PROWLER_APP_EMAIL and PROWLER_APP_PASSWORD environment variables are required"
|
||||
)
|
||||
else:
|
||||
self.email = None
|
||||
self.password = None
|
||||
self.tenant_id = None
|
||||
|
||||
self.access_token: Optional[str] = None
|
||||
self.refresh_token: Optional[str] = None
|
||||
|
||||
self._validate_credentials()
|
||||
|
||||
def _validate_credentials(self):
|
||||
"""Validate that all required credentials are present."""
|
||||
if not self.email:
|
||||
raise ValueError("PROWLER_APP_EMAIL environment variable is required")
|
||||
if not self.password:
|
||||
raise ValueError("PROWLER_APP_PASSWORD environment variable is required")
|
||||
|
||||
def _parse_jwt(self, token: str) -> Optional[Dict]:
|
||||
"""Parse JWT token and return payload, similar to JS parseJwt function."""
|
||||
if not token:
|
||||
@@ -63,56 +68,80 @@ class ProwlerAppAuth:
|
||||
async def authenticate(self) -> str:
|
||||
"""Authenticate with Prowler App API and return access token."""
|
||||
logger.info("Starting authentication with Prowler App API")
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# Prepare JSON:API formatted request body
|
||||
auth_attributes = {"email": self.email, "password": self.password}
|
||||
if self.tenant_id:
|
||||
auth_attributes["tenant_id"] = self.tenant_id
|
||||
if self.mode == "stdio":
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# Prepare JSON:API formatted request body
|
||||
auth_attributes = {"email": self.email, "password": self.password}
|
||||
if self.tenant_id:
|
||||
auth_attributes["tenant_id"] = self.tenant_id
|
||||
|
||||
request_body = {
|
||||
"data": {
|
||||
"type": "tokens",
|
||||
"attributes": auth_attributes,
|
||||
request_body = {
|
||||
"data": {
|
||||
"type": "tokens",
|
||||
"attributes": auth_attributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v1/tokens",
|
||||
json=request_body,
|
||||
headers={
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
"Accept": "application/vnd.api+json",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v1/tokens",
|
||||
json=request_body,
|
||||
headers={
|
||||
"Content-Type": "application/vnd.api+json",
|
||||
"Accept": "application/vnd.api+json",
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
# Extract token from JSON:API response format
|
||||
self.access_token = (
|
||||
data.get("data", {}).get("attributes", {}).get("access")
|
||||
)
|
||||
self.refresh_token = (
|
||||
data.get("data", {}).get("attributes", {}).get("refresh")
|
||||
)
|
||||
data = response.json()
|
||||
# Extract token from JSON:API response format
|
||||
self.access_token = (
|
||||
data.get("data", {}).get("attributes", {}).get("access")
|
||||
)
|
||||
self.refresh_token = (
|
||||
data.get("data", {}).get("attributes", {}).get("refresh")
|
||||
)
|
||||
|
||||
logger.debug(f"Access token: {self.access_token}")
|
||||
if not self.access_token:
|
||||
raise ValueError("Token not found in response")
|
||||
|
||||
if not self.access_token:
|
||||
raise ValueError("Token not found in response")
|
||||
logger.info("Authentication successful")
|
||||
|
||||
logger.info("Authentication successful")
|
||||
return self.access_token
|
||||
|
||||
return self.access_token
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
f"Authentication failed with HTTP status {e.response.status_code}: {e.response.text}"
|
||||
)
|
||||
raise ValueError(f"Authentication failed: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication failed with error: {e}")
|
||||
raise ValueError(f"Authentication failed: {e}")
|
||||
elif self.mode == "http":
|
||||
headers = get_http_headers()
|
||||
access_token = headers.get("authorization", None)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
f"Authentication failed with HTTP status {e.response.status_code}: {e.response.text}"
|
||||
)
|
||||
raise ValueError(f"Authentication failed: {e.response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Authentication failed with error: {e}")
|
||||
raise ValueError(f"Authentication failed: {e}")
|
||||
# Validate the token
|
||||
if access_token:
|
||||
if access_token.startswith("Bearer "):
|
||||
access_token = access_token.replace("Bearer ", "")
|
||||
|
||||
payload = self._parse_jwt(access_token)
|
||||
if not payload:
|
||||
raise ValueError("Invalid JWT token format")
|
||||
|
||||
# Check if token is expired
|
||||
now = int(datetime.now().timestamp())
|
||||
exp = payload.get("exp", 0)
|
||||
if exp <= now:
|
||||
raise ValueError("Token has expired")
|
||||
else:
|
||||
raise ValueError("No authorization token provided")
|
||||
|
||||
# Don't cache tokens in HTTP mode to prevent sharing between clients
|
||||
return access_token
|
||||
else:
|
||||
raise ValueError(f"Invalid mode: {self.mode}")
|
||||
|
||||
async def refresh_access_token(self) -> str:
|
||||
"""Refresh the access token using the refresh token."""
|
||||
@@ -161,28 +190,33 @@ class ProwlerAppAuth:
|
||||
async def get_valid_token(self) -> str:
|
||||
"""Get a valid access token, checking JWT expiry."""
|
||||
|
||||
current_token = self.access_token
|
||||
need_new_token = True
|
||||
|
||||
if current_token:
|
||||
payload = self._parse_jwt(current_token)
|
||||
|
||||
if payload:
|
||||
now = int(datetime.now().timestamp())
|
||||
time_left = payload.get("exp", 0) - now
|
||||
|
||||
if time_left > 120: # 2 minutes margin
|
||||
need_new_token = False
|
||||
|
||||
if need_new_token:
|
||||
token = await self.authenticate()
|
||||
|
||||
# Verify the new token
|
||||
payload = self._parse_jwt(token)
|
||||
|
||||
return token
|
||||
if self.mode == "http":
|
||||
# In HTTP mode, always authenticate fresh to prevent token sharing between clients
|
||||
return await self.authenticate()
|
||||
else:
|
||||
return current_token
|
||||
# In STDIO mode, cache tokens for efficiency
|
||||
current_token = self.access_token
|
||||
need_new_token = True
|
||||
|
||||
if current_token:
|
||||
payload = self._parse_jwt(current_token)
|
||||
|
||||
if payload:
|
||||
now = int(datetime.now().timestamp())
|
||||
time_left = payload.get("exp", 0) - now
|
||||
|
||||
if time_left > 120: # 2 minutes margin
|
||||
need_new_token = False
|
||||
|
||||
if need_new_token:
|
||||
token = await self.authenticate()
|
||||
|
||||
# Verify the new token
|
||||
payload = self._parse_jwt(token)
|
||||
|
||||
return token
|
||||
else:
|
||||
return current_token
|
||||
|
||||
def get_headers(self, token: str) -> Dict[str, str]:
|
||||
"""Get headers for API requests with authentication."""
|
||||
|
||||
@@ -15,7 +15,6 @@ from typing import Optional
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
|
||||
|
||||
class OpenAPIToMCPGenerator:
|
||||
@@ -758,7 +757,6 @@ class OpenAPIToMCPGenerator:
|
||||
|
||||
# Check excluded tags
|
||||
if any(tag in self.exclude_tags for tag in tags):
|
||||
logger.debug(f"Excluding endpoint {path} due to tag {tags}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -3,13 +3,13 @@ import os
|
||||
from fastmcp import FastMCP
|
||||
from prowler_mcp_server.lib.logger import logger
|
||||
|
||||
# Initialize main Prowler MCP server
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server")
|
||||
|
||||
|
||||
async def setup_main_server():
|
||||
async def setup_main_server(transport: str) -> FastMCP:
|
||||
"""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...")
|
||||
@@ -23,6 +23,9 @@ async def setup_main_server():
|
||||
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")
|
||||
):
|
||||
@@ -39,3 +42,5 @@ async def setup_main_server():
|
||||
logger.info("Successfully imported Prowler App server")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import Prowler App server: {e}")
|
||||
|
||||
return prowler_mcp_server
|
||||
|
||||
Reference in New Issue
Block a user