Skip to main content
This guide explains how to extend the Prowler MCP Server with new tools and features.
New to Prowler MCP Server? Start with the user documentation:
  • Overview - Key capabilities, use cases, and deployment options
  • Installation - Install locally or use the managed server
  • Configuration - Configure Claude Desktop, Cursor, and other MCP hosts
  • Tools Reference - Complete list of all available tools

Introduction

The Prowler MCP Server brings the entire Prowler ecosystem to AI assistants through the Model Context Protocol (MCP). It enables seamless integration with AI tools like Claude Desktop, Cursor, and other MCP clients. The server follows a modular architecture with three independent sub-servers:
Sub-ServerAuth RequiredDescription
Prowler AppYesFull access to Prowler Cloud and Self-Managed features
Prowler HubNoSecurity checks catalog with over 1000 checks, fixers, and 70+ compliance frameworks
Prowler DocumentationNoFull-text search and retrieval of official documentation
For a complete list of tools and their descriptions, see the Tools Reference.

Architecture Overview

The MCP Server architecture is illustrated in the Overview documentation. AI assistants connect through the MCP protocol to access Prowler’s three main components.

Server Structure

The main server orchestrates three sub-servers with prefixed namespacing:
mcp_server/prowler_mcp_server/
├── server.py                 # Main orchestrator
├── main.py                   # CLI entry point
├── prowler_hub/
├── prowler_app/
│   ├── tools/                # Tool implementations
│   ├── models/               # Pydantic models
│   └── utils/                # API client, auth, loader
└── prowler_documentation/

Tool Registration Patterns

The MCP Server uses two patterns for tool registration:
  1. Direct Decorators (Prowler Hub/Docs): Tools are registered using @mcp.tool() decorators
  2. Auto-Discovery (Prowler App): All public methods of BaseTool subclasses are auto-registered

Adding Tools to Prowler App

Step 1: Create the Tool Class

Create a new file or add to an existing file in prowler_app/tools/:
# prowler_app/tools/new_feature.py
from typing import Any

from pydantic import Field

from prowler_mcp_server.prowler_app.models.new_feature import (
    FeatureListResponse,
    DetailedFeature,
)
from prowler_mcp_server.prowler_app.tools.base import BaseTool


class NewFeatureTools(BaseTool):
    """Tools for managing new features."""

    async def list_features(
        self,
        status: str | None = Field(
            default=None,
            description="Filter by status (active, inactive, pending)"
        ),
        page_size: int = Field(
            default=50,
            description="Number of results per page (1-100)"
        ),
    ) -> dict[str, Any]:
        """List all features with optional filtering.

        Returns a lightweight list of features optimized for LLM consumption.
        Use get_feature for complete information about a specific feature.
        """
        # Validate parameters
        self.api_client.validate_page_size(page_size)

        # Build query parameters
        params: dict[str, Any] = {"page[size]": page_size}
        if status:
            params["filter[status]"] = status

        # Make API request
        clean_params = self.api_client.build_filter_params(params)
        response = await self.api_client.get("/api/v1/features", params=clean_params)

        # Transform to LLM-friendly format
        return FeatureListResponse.from_api_response(response).model_dump()

    async def get_feature(
        self,
        feature_id: str = Field(description="The UUID of the feature"),
    ) -> dict[str, Any]:
        """Get detailed information about a specific feature.

        Returns complete feature details including configuration and metadata.
        """
        try:
            response = await self.api_client.get(f"/api/v1/features/{feature_id}")
            return DetailedFeature.from_api_response(response["data"]).model_dump()
        except Exception as e:
            self.logger.error(f"Failed to get feature {feature_id}: {e}")
            return {"error": str(e), "status": "failed"}

Step 2: Create the Models

Create corresponding models in prowler_app/models/:
# prowler_app/models/new_feature.py
from typing import Any

from pydantic import Field

from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin


class SimplifiedFeature(MinimalSerializerMixin):
    """Lightweight feature for list operations."""

    id: str = Field(description="Unique feature identifier")
    name: str = Field(description="Feature name")
    status: str = Field(description="Current status")

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "SimplifiedFeature":
        """Transform API response to simplified format."""
        attributes = data.get("attributes", {})
        return cls(
            id=data["id"],
            name=attributes["name"],
            status=attributes["status"],
        )


class DetailedFeature(SimplifiedFeature):
    """Extended feature with complete details."""

    description: str | None = Field(default=None, description="Feature description")
    configuration: dict[str, Any] | None = Field(default=None, description="Configuration")
    created_at: str = Field(description="Creation timestamp")
    updated_at: str = Field(description="Last update timestamp")

    @classmethod
    def from_api_response(cls, data: dict[str, Any]) -> "DetailedFeature":
        """Transform API response to detailed format."""
        attributes = data.get("attributes", {})
        return cls(
            id=data["id"],
            name=attributes["name"],
            status=attributes["status"],
            description=attributes.get("description"),
            configuration=attributes.get("configuration"),
            created_at=attributes["created_at"],
            updated_at=attributes["updated_at"],
        )


class FeatureListResponse(MinimalSerializerMixin):
    """Response wrapper for feature list operations."""

    count: int = Field(description="Total number of features")
    features: list[SimplifiedFeature] = Field(description="List of features")

    @classmethod
    def from_api_response(cls, response: dict[str, Any]) -> "FeatureListResponse":
        """Transform API response to list format."""
        data = response.get("data", [])
        features = [SimplifiedFeature.from_api_response(item) for item in data]
        return cls(count=len(features), features=features)

Step 3: Verify Auto-Discovery

No manual registration is needed. The tool_loader.py automatically discovers and registers all BaseTool subclasses. Verify your tool is loaded by checking the server logs:
INFO - Auto-registered 2 tools from NewFeatureTools
INFO - Loaded and registered: NewFeatureTools

Adding Tools to Prowler Hub/Docs

For Prowler Hub or Documentation tools, use the @mcp.tool() decorator directly:
# prowler_hub/server.py
from fastmcp import FastMCP

hub_mcp_server = FastMCP("prowler-hub")

@hub_mcp_server.tool()
async def get_new_artifact(
    artifact_id: str,
) -> dict:
    """Fetch a specific artifact from Prowler Hub.

    Args:
        artifact_id: The unique identifier of the artifact

    Returns:
        Dictionary containing artifact details
    """
    response = prowler_hub_client.get(f"/artifact/{artifact_id}")
    response.raise_for_status()
    return response.json()

Model Design Patterns

MinimalSerializerMixin

All models should use MinimalSerializerMixin to optimize responses for LLM consumption:
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin

class MyModel(MinimalSerializerMixin):
    """Model that excludes empty values from serialization."""
    required_field: str
    optional_field: str | None = None  # Excluded if None
    empty_list: list = []              # Excluded if empty
This mixin automatically excludes:
  • None values
  • Empty strings
  • Empty lists
  • Empty dictionaries

Two-Tier Model Pattern

Use two-tier models for efficient responses:
  • Simplified: Lightweight models for list operations
  • Detailed: Extended models for single-item retrieval
class SimplifiedItem(MinimalSerializerMixin):
    """Use for list operations - minimal fields."""
    id: str
    name: str
    status: str

class DetailedItem(SimplifiedItem):
    """Use for get operations - extends simplified with details."""
    description: str | None = None
    configuration: dict | None = None
    created_at: str
    updated_at: str

Factory Method Pattern

Always implement from_api_response() for API transformation:
@classmethod
def from_api_response(cls, data: dict[str, Any]) -> "MyModel":
    """Transform API response to model.

    This method handles the JSON:API format used by Prowler API,
    extracting attributes and relationships as needed.
    """
    attributes = data.get("attributes", {})
    return cls(
        id=data["id"],
        name=attributes["name"],
        # ... map other fields
    )

API Client Usage

The ProwlerAPIClient is a singleton that handles authentication and HTTP requests:
class MyTools(BaseTool):
    async def my_tool(self) -> dict:
        # GET request
        response = await self.api_client.get("/api/v1/endpoint", params={"key": "value"})

        # POST request
        response = await self.api_client.post(
            "/api/v1/endpoint",
            json_data={"data": {"type": "items", "attributes": {...}}}
        )

        # PATCH request
        response = await self.api_client.patch(
            f"/api/v1/endpoint/{id}",
            json_data={"data": {"attributes": {...}}}
        )

        # DELETE request
        response = await self.api_client.delete(f"/api/v1/endpoint/{id}")

Helper Methods

The API client provides useful helper methods:
# Validate page size (1-1000)
self.api_client.validate_page_size(page_size)

# Normalize date range with max days limit
date_range = self.api_client.normalize_date_range(date_from, date_to, max_days=2)

# Build filter parameters (handles type conversion)
clean_params = self.api_client.build_filter_params({
    "filter[status]": "active",
    "filter[severity__in]": ["high", "critical"],  # Converts to comma-separated
    "filter[muted]": True,  # Converts to "true"
})

# Poll async task until completion
result = await self.api_client.poll_task_until_complete(
    task_id=task_id,
    timeout=60,
    poll_interval=1.0
)

Best Practices

Tool Docstrings

Tool docstrings become description that is going to be read by the LLM. Provide clear usage instructions and common workflows:
async def search_items(self, status: str = Field(...)) -> dict:
    """Search items with advanced filtering.

    Returns a lightweight list optimized for LLM consumption.
    Use get_item for complete details about a specific item.

    Common workflows:
    - Find critical items: status="critical"
    - Find recent items: Use date_from parameter
    """

Error Handling

Return structured error responses instead of raising exceptions:
async def get_item(self, item_id: str) -> dict:
    try:
        response = await self.api_client.get(f"/api/v1/items/{item_id}")
        return DetailedItem.from_api_response(response["data"]).model_dump()
    except Exception as e:
        self.logger.error(f"Failed to get item {item_id}: {e}")
        return {"error": str(e), "status": "failed"}

Parameter Descriptions

Use Pydantic Field() with clear descriptions. This also helps LLMs understand the purpose of each parameter, so be as descriptive as possible:
async def list_items(
    self,
    severity: list[str] = Field(
        default=[],
        description="Filter by severity levels (critical, high, medium, low)"
    ),
    status: str | None = Field(
        default=None,
        description="Filter by status (PASS, FAIL, MANUAL)"
    ),
    page_size: int = Field(
        default=50,
        description="Results per page"
    ),
) -> dict:

Development Commands

# Navigate to MCP server directory
cd mcp_server

# Run in STDIO mode (default)
uv run prowler-mcp

# Run in HTTP mode
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8000

# Run with environment variables
PROWLER_APP_API_KEY="pk_xxx" uv run prowler-mcp
For complete installation and deployment options, see: For development I recommend to use the Model Context Protocol Inspector as MCP client to test and debug your tools.

Additional Resources