feat(mcp_server): update API base URL environment variable to include complete path (#9542)

This commit is contained in:
Rubén De la Torre Vico
2025-12-15 11:04:44 +01:00
committed by GitHub
parent 6761f0ffd0
commit e0cf8bffd4
13 changed files with 72 additions and 77 deletions

View File

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

View File

@@ -2,6 +2,12 @@
All notable changes to the **Prowler MCP Server** are documented in this file.
## [0.2.1] (UNRELEASED)
### Changed
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9300)
## [0.2.0] (Prowler v5.15.0)
### Added

View File

@@ -78,7 +78,7 @@ With environment variables:
```bash
docker run --rm -i \
-e PROWLER_APP_API_KEY="pk_your_api_key" \
-e PROWLER_API_BASE_URL="https://api.prowler.com" \
-e API_BASE_URL="https://api.prowler.com/api/v1" \
prowlercloud/prowler-mcp
```
@@ -144,10 +144,10 @@ 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_TRANSPORT_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 `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 API_BASE_URL="https://api.prowler.com/api/v1"
export PROWLER_MCP_TRANSPORT_MODE="http"
```
@@ -319,7 +319,7 @@ For STDIO mode, authentication is handled via environment variables using an API
export PROWLER_APP_API_KEY="pk_your_api_key_here"
# Optional - for custom API endpoint, in case not provided Prowler Cloud API will be used
export PROWLER_API_BASE_URL="https://api.prowler.com"
export API_BASE_URL="https://api.prowler.com/api/v1"
```
#### HTTP Mode Authentication
@@ -370,7 +370,7 @@ For local execution, configure your MCP client to launch the server directly. Be
"args": ["/path/to/prowler/mcp_server/"],
"env": {
"PROWLER_APP_API_KEY": "pk_your_api_key_here",
"PROWLER_API_BASE_URL": "https://api.prowler.com" // Optional, in case not provided Prowler Cloud API will be used
"API_BASE_URL": "https://api.prowler.com/api/v1" // Optional, in case not provided Prowler Cloud API will be used
}
}
}
@@ -387,7 +387,7 @@ For local execution, configure your MCP client to launch the server directly. Be
"args": [
"run", "--rm", "-i",
"--env", "PROWLER_APP_API_KEY=pk_your_api_key_here",
"--env", "PROWLER_API_BASE_URL=https://api.prowler.com", // Optional, in case not provided Prowler Cloud API will be used
"--env", "API_BASE_URL=https://api.prowler.com/api/v1", // Optional, in case not provided Prowler Cloud API will be used
"prowler-mcp"
]
}

View File

@@ -33,7 +33,7 @@ class BaseTool(ABC):
async def search_security_findings(self, severity: list[str] = Field(...)):
# Implementation with access to self.api_client
response = await self.api_client.get("/api/v1/findings")
response = await self.api_client.get("/findings")
return response
"""

View File

@@ -6,13 +6,14 @@ across all cloud providers.
from typing import Any, Literal
from pydantic import Field
from prowler_mcp_server.prowler_app.models.findings import (
DetailedFinding,
FindingsListResponse,
FindingsOverview,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class FindingsTools(BaseTool):
@@ -122,11 +123,11 @@ class FindingsTools(BaseTool):
if date_range is None:
# No dates provided - use latest findings endpoint
endpoint = "/api/v1/findings/latest"
endpoint = "/findings/latest"
params = {}
else:
# Dates provided - use historical findings endpoint
endpoint = "/api/v1/findings"
endpoint = "/findings"
params = {
"filter[inserted_at__gte]": date_range[0],
"filter[inserted_at__lte]": date_range[1],
@@ -228,7 +229,7 @@ class FindingsTools(BaseTool):
# Get API response and transform to detailed format
api_response = await self.api_client.get(
f"/api/v1/findings/{finding_id}", params=params
f"/findings/{finding_id}", params=params
)
detailed_finding = DetailedFinding.from_api_response(
api_response.get("data", {})
@@ -281,7 +282,7 @@ class FindingsTools(BaseTool):
# Get API response and transform to simplified format
api_response = await self.api_client.get(
"/api/v1/overviews/findings", params=clean_params
"/overviews/findings", params=clean_params
)
overview = FindingsOverview.from_api_response(api_response)

View File

@@ -8,13 +8,14 @@ This module provides tools for managing finding muting in Prowler, including:
import json
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.muting import (
DetailedMuteRule,
MutelistResponse,
MuteRulesListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class MutingTools(BaseTool):
@@ -53,9 +54,7 @@ class MutingTools(BaseTool):
}
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/processors", params=clean_params
)
api_response = await self.api_client.get("/processors", params=clean_params)
data = api_response.get("data", [])
@@ -145,7 +144,7 @@ Structure:
}
api_response = await self.api_client.post(
"/api/v1/processors", json_data=create_body
"/processors", json_data=create_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
@@ -163,7 +162,7 @@ Structure:
}
api_response = await self.api_client.patch(
f"/api/v1/processors/{existing_mutelist['id']}", json_data=update_body
f"/processors/{existing_mutelist['id']}", json_data=update_body
)
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
return mutelist.model_dump()
@@ -194,7 +193,7 @@ Structure:
# Delete the mutelist
mutelist_id = existing_mutelist["id"]
await self.api_client.delete(f"/api/v1/processors/{mutelist_id}")
await self.api_client.delete(f"/processors/{mutelist_id}")
return {
"success": True,
@@ -276,9 +275,7 @@ Structure:
params["filter[search]"] = search
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/mute-rules", params=clean_params
)
api_response = await self.api_client.get("/mute-rules", params=clean_params)
simplified_response = MuteRulesListResponse.from_api_response(api_response)
return simplified_response.model_dump()
@@ -311,7 +308,7 @@ Structure:
}
api_response = await self.api_client.get(
f"/api/v1/mute-rules/{rule_id}", params=params
f"/mute-rules/{rule_id}", params=params
)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
@@ -363,9 +360,7 @@ Structure:
}
}
api_response = await self.api_client.post(
"/api/v1/mute-rules", json_data=create_body
)
api_response = await self.api_client.post("/mute-rules", json_data=create_body)
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
@@ -432,10 +427,9 @@ Structure:
}
api_response = await self.api_client.patch(
f"/api/v1/mute-rules/{rule_id}", json_data=update_body
f"/mute-rules/{rule_id}", json_data=update_body
)
self.logger.info(f"API response: {api_response}")
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
return detailed_rule.model_dump()
@@ -463,7 +457,7 @@ Structure:
"""
self.logger.info(f"Deleting mute rule {rule_id}...")
result = await self.api_client.delete(f"/api/v1/mute-rules/{rule_id}")
result = await self.api_client.delete(f"/mute-rules/{rule_id}")
if result.get("success"):
return {

View File

@@ -6,12 +6,13 @@ including searching, connecting, and deleting providers.
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.providers import (
ProviderConnectionStatus,
ProvidersListResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ProvidersTools(BaseTool):
@@ -100,9 +101,7 @@ class ProvidersTools(BaseTool):
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get(
"/api/v1/providers", params=clean_params
)
api_response = await self.api_client.get("/providers", params=clean_params)
simplified_response = ProvidersListResponse.from_api_response(api_response)
# Fetch secret_type for each provider that has a secret
@@ -306,9 +305,7 @@ class ProvidersTools(BaseTool):
self.logger.info(f"Deleting provider {provider_id}...")
try:
# Initiate the deletion task
task_response = await self.api_client.delete(
f"/api/v1/providers/{provider_id}"
)
task_response = await self.api_client.delete(f"/providers/{provider_id}")
task_id = task_response.get("data", {}).get("id")
# Poll until task completes (with 60 second timeout)
@@ -345,7 +342,7 @@ class ProvidersTools(BaseTool):
"""
self.logger.info(f"Checking if provider {provider_uid} exists...")
response = await self.api_client.get(
"/api/v1/providers", params={"filter[uid]": provider_uid}
"/providers", params={"filter[uid]": provider_uid}
)
providers = response.get("data", [])
@@ -391,7 +388,7 @@ class ProvidersTools(BaseTool):
if alias:
provider_body["data"]["attributes"]["alias"] = alias
await self.api_client.post("/api/v1/providers", json_data=provider_body)
await self.api_client.post("/providers", json_data=provider_body)
provider_id = await self._check_provider_exists(provider_uid)
if provider_id is None:
@@ -418,7 +415,7 @@ class ProvidersTools(BaseTool):
}
}
result = await self.api_client.patch(
f"/api/v1/providers/{prowler_provider_id}", json_data=update_body
f"/providers/{prowler_provider_id}", json_data=update_body
)
if result.get("data", {}).get("attributes", {}).get("alias") != alias:
raise Exception(f"Provider {prowler_provider_id} alias update failed")
@@ -450,7 +447,7 @@ class ProvidersTools(BaseTool):
"""
try:
response = await self.api_client.get(
"/api/v1/providers/secrets",
"/providers/secrets",
params={"filter[provider]": prowler_provider_id},
)
secrets = response.get("data", [])
@@ -481,7 +478,7 @@ class ProvidersTools(BaseTool):
"""
try:
response = await self.api_client.get(
f"/api/v1/providers/secrets/{secret_id}",
f"/providers/secrets/{secret_id}",
params={"fields[provider-secrets]": "secret_type"},
)
secret_type = (
@@ -536,7 +533,7 @@ class ProvidersTools(BaseTool):
}
try:
response = await self.api_client.patch(
f"/api/v1/providers/secrets/{existing_secret_id}",
f"/providers/secrets/{existing_secret_id}",
json_data=update_body,
)
self.logger.info("Credentials updated successfully")
@@ -567,7 +564,7 @@ class ProvidersTools(BaseTool):
try:
response = await self.api_client.post(
"/api/v1/providers/secrets", json_data=secret_body
"/providers/secrets", json_data=secret_body
)
self.logger.info("Credentials added successfully")
return response
@@ -588,7 +585,7 @@ class ProvidersTools(BaseTool):
try:
# Initiate the connection test task
task_response = await self.api_client.post(
f"/api/v1/providers/{prowler_provider_id}/connection", json_data={}
f"/providers/{prowler_provider_id}/connection", json_data={}
)
task_id = task_response.get("data", {}).get("id")
@@ -619,5 +616,5 @@ class ProvidersTools(BaseTool):
Provider data dictionary
"""
return await self.api_client.get(
f"/api/v1/providers/{prowler_provider_id}",
f"/providers/{prowler_provider_id}",
)

View File

@@ -6,13 +6,14 @@ across all providers.
from typing import Any
from pydantic import Field
from prowler_mcp_server.prowler_app.models.resources import (
DetailedResource,
ResourcesListResponse,
ResourcesMetadataResponse,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ResourcesTools(BaseTool):
@@ -121,11 +122,11 @@ class ResourcesTools(BaseTool):
if date_range is None:
# No dates provided - use latest resources endpoint
endpoint = "/api/v1/resources/latest"
endpoint = "/resources/latest"
params = {}
else:
# Dates provided - use historical resources endpoint
endpoint = "/api/v1/resources"
endpoint = "/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],
@@ -206,9 +207,8 @@ class ResourcesTools(BaseTool):
# Get API response and transform to detailed format
api_response = await self.api_client.get(
f"/api/v1/resources/{resource_id}", params=params
f"/resources/{resource_id}", params=params
)
self.logger.info(f"API response: {api_response}")
detailed_resource = DetailedResource.from_api_response(
api_response.get("data", {})
)
@@ -265,13 +265,13 @@ class ResourcesTools(BaseTool):
if date_range is None:
# No dates provided - use latest metadata endpoint
metadata_endpoint = "/api/v1/resources/metadata/latest"
list_endpoint = "/api/v1/resources/latest"
metadata_endpoint = "/resources/metadata/latest"
list_endpoint = "/resources/latest"
params = {}
else:
# Dates provided - use historical endpoints
metadata_endpoint = "/api/v1/resources/metadata"
list_endpoint = "/api/v1/resources"
metadata_endpoint = "/resources/metadata"
list_endpoint = "/resources"
params = {
"filter[updated_at__gte]": date_range[0],
"filter[updated_at__lte]": date_range[1],

View File

@@ -5,6 +5,8 @@ This module provides tools for managing and monitoring Prowler security scans.
from typing import Any, Literal
from pydantic import Field
from prowler_mcp_server.prowler_app.models.scans import (
DetailedScan,
ScanCreationResult,
@@ -12,7 +14,6 @@ from prowler_mcp_server.prowler_app.models.scans import (
ScheduleCreationResult,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from pydantic import Field
class ScansTools(BaseTool):
@@ -119,7 +120,7 @@ class ScansTools(BaseTool):
clean_params = self.api_client.build_filter_params(params)
api_response = await self.api_client.get("/api/v1/scans", params=clean_params)
api_response = await self.api_client.get("/scans", params=clean_params)
simplified_response = ScansListResponse.from_api_response(api_response)
return simplified_response.model_dump()
@@ -163,9 +164,7 @@ class ScansTools(BaseTool):
"fields[scans]": "name,trigger,state,progress,duration,unique_resource_count,started_at,completed_at,scheduled_at,next_scan_at,inserted_at"
}
api_response = await self.api_client.get(
f"/api/v1/scans/{scan_id}", params=params
)
api_response = await self.api_client.get(f"/scans/{scan_id}", params=params)
detailed_scan = DetailedScan.from_api_response(api_response["data"])
return detailed_scan.model_dump()
@@ -213,9 +212,7 @@ class ScansTools(BaseTool):
# Create scan (returns Task)
self.logger.info(f"Creating scan for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/scans", json_data=request_data
)
task_response = await self.api_client.post("/scans", json_data=request_data)
scan_id = (
task_response.get("data", {})
@@ -228,7 +225,7 @@ class ScansTools(BaseTool):
raise Exception("No scan_id returned from scan creation")
self.logger.info(f"Scan created successfully: {scan_id}")
scan_response = await self.api_client.get(f"/api/v1/scans/{scan_id}")
scan_response = await self.api_client.get(f"/scans/{scan_id}")
scan_info = DetailedScan.from_api_response(scan_response["data"])
return ScanCreationResult(
@@ -273,7 +270,7 @@ class ScansTools(BaseTool):
"""
self.logger.info(f"Creating daily schedule for provider {provider_id}")
task_response = await self.api_client.post(
"/api/v1/schedules/daily",
"/schedules/daily",
json_data={
"data": {
"type": "daily-schedules",
@@ -316,7 +313,7 @@ class ScansTools(BaseTool):
2. Use this tool with the scan 'id' and new name
"""
api_response = await self.api_client.patch(
f"/api/v1/scans/{scan_id}",
f"/scans/{scan_id}",
json_data={
"data": {
"type": "scans",

View File

@@ -228,7 +228,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
)
# Fetch current task state
response = await self.get(f"/api/v1/tasks/{task_id}")
response = await self.get(f"/tasks/{task_id}")
task_data = response.get("data", {})
task_attrs = task_data.get("attributes", {})
state = task_attrs.get("state")

View File

@@ -15,7 +15,7 @@ class ProwlerAppAuth:
def __init__(
self,
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("API_BASE_URL", "https://api.prowler.com/api/v1"),
):
self.base_url = base_url.rstrip("/")
logger.info(f"Using Prowler App API base URL: {self.base_url}")