mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat: add AI skills pack for Claude Code and OpenCode (#9728)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com> Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com> Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
@@ -1,310 +1,88 @@
|
||||
# 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.**
|
||||
> **Skills Reference**: For detailed patterns, use the [`prowler-mcp`](../skills/prowler-mcp/SKILL.md) skill.
|
||||
|
||||
## 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.
|
||||
The Prowler MCP Server provides AI agents access to the Prowler ecosystem through the Model Context Protocol (MCP). It integrates with Claude Desktop, Cursor, and other MCP hosts.
|
||||
|
||||
---
|
||||
|
||||
## Critical Rules
|
||||
## CRITICAL RULES
|
||||
|
||||
### Tool Implementation
|
||||
|
||||
- **ALWAYS**: Extend `BaseTool` ABC 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)
|
||||
- ALWAYS: Extend `BaseTool` ABC for Prowler App tools (auto-registration)
|
||||
- ALWAYS: Use `@mcp.tool()` decorator for Hub/Docs tools
|
||||
- NEVER: Manually register BaseTool subclasses
|
||||
- NEVER: Import tools directly in server.py
|
||||
|
||||
### Models
|
||||
|
||||
- **ALWAYS**: Use `MinimalSerializerMixin` for 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)
|
||||
- ALWAYS: Use `MinimalSerializerMixin` for LLM-optimized responses
|
||||
- ALWAYS: Implement `from_api_response()` factory method
|
||||
- ALWAYS: Two-tier models (Simplified for lists, Detailed for single items)
|
||||
- NEVER: Return raw API responses
|
||||
|
||||
### API Client
|
||||
|
||||
- **ALWAYS**: Use singleton `ProwlerAPIClient` via `self.api_client` in tools
|
||||
- **ALWAYS**: Use `build_filter_params()` for query parameter normalization
|
||||
- **NEVER**: Create new httpx clients in tools (use shared client)
|
||||
- ALWAYS: Use singleton `ProwlerAPIClient` via `self.api_client`
|
||||
- ALWAYS: Use `build_filter_params()` for query parameters
|
||||
- NEVER: Create new httpx clients in tools
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
## ARCHITECTURE
|
||||
|
||||
### Three Sub-Servers Pattern
|
||||
|
||||
The main server (`server.py`) orchestrates three independent sub-servers with prefixed tool namespacing:
|
||||
### Three Sub-Servers
|
||||
|
||||
```python
|
||||
# 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 tools
|
||||
- `prowler_docs_*` - Prowler documentation search and retrieval
|
||||
- `prowler_app_*` - Prowler Cloud and App (Self-Managed) management tools
|
||||
|
||||
### Tool Registration Patterns
|
||||
|
||||
**Pattern 1: Prowler Hub/Docs (Direct Decorators)**
|
||||
|
||||
```python
|
||||
# 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)**
|
||||
|
||||
```python
|
||||
# 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.
|
||||
### Tool Naming
|
||||
- `prowler_hub_*` - Catalog and compliance (no auth)
|
||||
- `prowler_docs_*` - Documentation search (no auth)
|
||||
- `prowler_app_*` - Cloud/App management (auth required)
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## TECH STACK
|
||||
|
||||
- **Language**: Python 3.12+
|
||||
- **MCP Framework**: FastMCP 2.13.1
|
||||
- **HTTP Client**: httpx (async)
|
||||
- **Validation**: Pydantic with MinimalSerializerMixin
|
||||
- **Package Manager**: uv
|
||||
Python 3.12+ | FastMCP 2.13.1 | httpx (async) | Pydantic | uv
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## 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
|
||||
mcp_server/prowler_mcp_server/
|
||||
├── server.py # Main orchestration
|
||||
├── prowler_hub/server.py # Hub tools (no auth)
|
||||
├── prowler_app/
|
||||
│ ├── server.py
|
||||
│ ├── tools/{feature}.py # BaseTool subclasses
|
||||
│ ├── models/{feature}.py # Pydantic models
|
||||
│ └── utils/api_client.py # ProwlerAPIClient
|
||||
└── prowler_documentation/
|
||||
└── server.py # Docs tools (no auth)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
NOTE: To run a python command always use `uv run <command>` from within the `mcp_server/` directory.
|
||||
|
||||
### Development
|
||||
## COMMANDS
|
||||
|
||||
```bash
|
||||
# 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/
|
||||
cd mcp_server && uv run prowler-mcp # STDIO mode
|
||||
cd mcp_server && uv run prowler-mcp --transport http --port 8000 # HTTP mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Patterns
|
||||
## QA CHECKLIST
|
||||
|
||||
### Adding New Tools to Prowler App
|
||||
|
||||
1. **Create or extend a tool class** in `prowler_app/tools/`:
|
||||
|
||||
```python
|
||||
# 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()
|
||||
```
|
||||
|
||||
2. **Create corresponding models** in `prowler_app/models/`:
|
||||
|
||||
```python
|
||||
# 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"],
|
||||
)
|
||||
```
|
||||
|
||||
3. **No registration needed** - the tool loader auto-discovers BaseTool subclasses
|
||||
|
||||
### Adding Tools to Prowler Hub/Docs
|
||||
|
||||
Use the `@mcp.tool()` decorator directly:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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 `MinimalSerializerMixin` to 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:
|
||||
|
||||
```python
|
||||
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 `MinimalSerializerMixin` for LLM optimization
|
||||
- [ ] API responses are transformed to simplified models
|
||||
- [ ] No hardcoded secrets or API keys
|
||||
- [ ] Tool docstrings describe LLM-relevant behavior
|
||||
- [ ] Models use `MinimalSerializerMixin`
|
||||
- [ ] API responses transformed to simplified models
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] 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
|
||||
- [ ] Parameter descriptions use Pydantic `Field()`
|
||||
|
||||
Reference in New Issue
Block a user