10 KiB
Prowler MCP Server - AI Agent Ruleset
Complete guide for AI agents and developers working on the Prowler MCP Server - the Model Context Protocol server that provides AI agents access to the Prowler ecosystem.
Project Overview
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 hosts, allowing interaction with Prowler's security capabilities through natural language.
Critical Rules
Tool Implementation
- ALWAYS: Extend
BaseToolABC for new Prowler App tools (auto-registration) - ALWAYS: Use
@mcp.tool()decorator for Hub/Docs tools (manual registration) - NEVER: Manually register BaseTool subclasses (auto-discovered via
load_all_tools()) - NEVER: Import tools directly in server.py (tool_loader handles discovery)
Models
- ALWAYS: Use
MinimalSerializerMixinfor LLM-optimized responses - ALWAYS: Implement
from_api_response()factory method for API transformations - ALWAYS: Use two-tier models (Simplified for lists, Detailed for single items)
- NEVER: Return raw API responses (transform to simplified models)
API Client
- ALWAYS: Use singleton
ProwlerAPIClientviaself.api_clientin tools - ALWAYS: Use
build_filter_params()for query parameter normalization - NEVER: Create new httpx clients in tools (use shared client)
Architecture
Three Sub-Servers Pattern
The main server (server.py) orchestrates three independent sub-servers with prefixed tool namespacing:
# server.py imports sub-servers with prefixes
await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub")
await prowler_mcp_server.import_server(app_mcp_server, prefix="prowler_app")
await prowler_mcp_server.import_server(docs_mcp_server, prefix="prowler_docs")
This pattern ensures:
- Failures in one sub-server do not block others
- Clear tool namespacing for LLM disambiguation
- Independent development and testing
Tool Naming Convention
All tools follow a consistent naming pattern with prefixes:
prowler_hub_*- Prowler Hub catalog and compliance toolsprowler_docs_*- Prowler documentation search and retrievalprowler_app_*- Prowler Cloud and App (Self-Managed) management tools
Tool Registration Patterns
Pattern 1: Prowler Hub/Docs (Direct Decorators)
# prowler_hub/server.py or prowler_documentation/server.py
hub_mcp_server = FastMCP("prowler-hub")
@hub_mcp_server.tool()
async def get_checks(providers: str | None = None) -> dict:
"""Tool docstring becomes LLM description."""
# Direct implementation
response = prowler_hub_client.get("/check", params=params)
return response.json()
Pattern 2: Prowler App (BaseTool Auto-Registration)
# prowler_app/tools/findings.py
class FindingsTools(BaseTool):
async def search_security_findings(
self,
severity: list[str] = Field(default=[], description="Filter by severity")
) -> dict:
"""Docstring becomes LLM description."""
response = await self.api_client.get("/api/v1/findings")
return SimplifiedFinding.from_api_response(response).model_dump()
NOTE: Only public methods of BaseTool subclasses are registered as tools.
Tech Stack
- Language: Python 3.12+
- MCP Framework: FastMCP 2.13.1
- HTTP Client: httpx (async)
- Validation: Pydantic with MinimalSerializerMixin
- Package Manager: uv
Project Structure
mcp_server/
├── README.md # User documentation
├── AGENTS.md # This file - AI agent guidelines
├── CHANGELOG.md # Version history
├── pyproject.toml # Project metadata and dependencies
├── Dockerfile # Container image definition
├── entrypoint.sh # Docker entrypoint script
└── prowler_mcp_server/
├── __init__.py # Version info
├── main.py # CLI entry point
├── server.py # Main FastMCP server orchestration
├── lib/
│ └── logger.py # Structured logging
├── prowler_hub/
│ └── server.py # Hub tools (10 tools, no auth)
├── prowler_app/
│ ├── server.py # App server initialization
│ ├── tools/
│ │ ├── base.py # BaseTool abstract class
│ │ ├── findings.py # Findings tools
│ │ ├── providers.py # Provider tools
│ │ ├── scans.py # Scan tools
│ │ ├── resources.py # Resource tools
│ │ └── muting.py # Muting tools
│ ├── models/
│ │ ├── base.py # MinimalSerializerMixin
│ │ ├── findings.py # Finding models
│ │ ├── providers.py # Provider models
│ │ ├── scans.py # Scan models
│ │ ├── resources.py # Resource models
│ │ └── muting.py # Muting models
│ └── utils/
│ ├── api_client.py # ProwlerAPIClient singleton
│ ├── auth.py # ProwlerAppAuth (STDIO/HTTP)
│ └── tool_loader.py # Auto-discovery and registration
└── prowler_documentation/
├── server.py # Documentation tools (2 tools, no auth)
└── search_engine.py # Mintlify API integration
Commands
NOTE: To run a python command always use uv run <command> from within the mcp_server/ directory.
Development
# 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 from anywhere using uvx
uvx /path/to/prowler/mcp_server/
Development Patterns
Adding New Tools to Prowler App
- Create or extend a tool class in
prowler_app/tools/:
# prowler_app/tools/new_feature.py
from pydantic import Field
from prowler_mcp_server.prowler_app.tools.base import BaseTool
from prowler_mcp_server.prowler_app.models.new_feature import FeatureResponse
class NewFeatureTools(BaseTool):
async def list_features(
self,
status: str | None = Field(default=None, description="Filter by status")
) -> dict:
"""List all features with optional filtering.
Returns a simplified list of features optimized for LLM consumption.
"""
params = {}
if status:
params["filter[status]"] = status
clean_params = self.api_client.build_filter_params(params)
response = await self.api_client.get("/api/v1/features", params=clean_params)
return FeatureResponse.from_api_response(response).model_dump()
- Create corresponding models in
prowler_app/models/:
# prowler_app/models/new_feature.py
from pydantic import Field
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
class SimplifiedFeature(MinimalSerializerMixin):
"""Lightweight feature for list operations."""
id: str
name: str
status: str
class DetailedFeature(SimplifiedFeature):
"""Extended feature with complete details."""
description: str | None = None
created_at: str
updated_at: str
@classmethod
def from_api_response(cls, data: dict) -> "DetailedFeature":
"""Transform API response to model."""
attributes = data.get("attributes", {})
return cls(
id=data["id"],
name=attributes["name"],
status=attributes["status"],
description=attributes.get("description"),
created_at=attributes["created_at"],
updated_at=attributes["updated_at"],
)
- No registration needed - the tool loader auto-discovers BaseTool subclasses
Adding Tools to Prowler Hub/Docs
Use the @mcp.tool() decorator directly:
# prowler_hub/server.py
@hub_mcp_server.tool()
async def new_hub_tool(param: str) -> dict:
"""Tool description for LLM."""
response = prowler_hub_client.get("/endpoint")
return response.json()
Code Quality Standards
Tool Docstrings
Tool docstrings become AI agent descriptions. Write them in a clear, concise manner focusing on LLM-relevant behavior:
async def search_security_findings(
self,
severity: list[str] = Field(default=[], description="Filter by severity levels")
) -> dict:
"""Search security findings with advanced filtering.
Returns a lightweight list of findings optimized for LLM consumption.
Use get_finding_details for complete information about a specific finding.
"""
Model Design
- Use
MinimalSerializerMixinto exclude None/empty values - Implement
from_api_response()for consistent API transformation - Create two-tier models: Simplified (lists) and Detailed (single items)
Error Handling
Return structured error responses rather than raising exceptions:
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"}
QA Checklist Before Commit
- Tool docstrings are clear and describe LLM-relevant behavior
- Models use
MinimalSerializerMixinfor LLM optimization - API responses are transformed to simplified models
- No hardcoded secrets or API keys
- Error handling returns structured responses
- New tools are auto-discovered (BaseTool subclass) or properly decorated
- Parameter descriptions use Pydantic
Field()with clear descriptions
References
- Root Project Guide:
../AGENTS.md - FastMCP Documentation: https://gofastmcp.com/llms.txt
- Prowler API Documentation: https://api.prowler.com/api/v1/docs