mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 15:24:22 +00:00
Adds ProwlerMCP, the FastMCP subclass every sub-server is now built
from. Its tool() wraps whatever it registers -- the decorator forms and
the direct call BaseTool uses -- so a failure leaves any tool as a
ToolError, which the client reads as isError: true.
Applied at the base class rather than by hand because forgetting it is
silent: every server now sets mask_error_details=True, so an unwrapped
tool would answer "Error calling tool 'x'" and nothing else. ToolError
bypasses that masking, which is what lets the servers mask by default
and still say something useful.
No tool changes yet. Tools that still return {"error": ...} keep working
exactly as before; they are converted surface by surface in the PRs
above this one. What changes here is that a failure which used to escape
as a raw exception is now described by render_tool_error.
The rules this establishes are in AGENTS.md and the developer guide,
so the conversions have something to be checked against.
5.9 KiB
5.9 KiB
Prowler MCP Server - AI Agent Ruleset
Skills Reference: See
prowler-mcp
Auto-invoke Skills
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Action | Skill |
|---|---|
| Add changelog entry for a PR or feature | prowler-changelog |
| Committing changes | prowler-commit |
| Create PR that requires changelog entry | prowler-changelog |
| Creating a git commit | prowler-commit |
| Review changelog format and conventions | prowler-changelog |
| Update CHANGELOG.md in any component | prowler-changelog |
| Working on MCP server tools | prowler-mcp |
| Writing tests for the MCP server | prowler-test-mcp |
Project Overview
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
Tool Implementation
- ALWAYS: Build sub-servers with
ProwlerMCP, neverFastMCPdirectly. It is what applies the error contract to every tool, whichever way it is registered - ALWAYS: Extend
BaseToolABC for Prowler 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
MinimalSerializerMixinfor 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
ProwlerAPIClientviaself.api_client - ALWAYS: Use
build_filter_params()for query parameters - NEVER: Create new httpx clients in tools
Errors
One rule: ToolError is a message you wrote for the caller. Any other exception is
a bug or an upstream failure, and render_tool_error describes it.
- ALWAYS:
raise ToolError(...)for anything the caller can act on — a rejected argument, a lookup that found nothing, a workflow step they must do first. Its text reaches the client verbatim, pastmask_error_details - NEVER:
raise ValueError(...)in a tool. It is reported as a bug in this server, which is correct for a model factory rejecting an API payload and wrong for a refusal - ALWAYS: Let an upstream failure propagate untouched.
ProwlerMCP.toolwraps every registration, so it becomes aToolErrordescribing the call, the status and what the API said. There is nothing to remember to apply - NEVER:
return {"error": ...}or{"success": False}. A returned payload isisError: false, so the client is told the call succeeded - NEVER: Raise a plain exception after a write has been accepted.
ToolErroris the only kind whose message reaches the client exactly as written - ALWAYS:
render_tool_error(e)when you surface an exception yourself, so a failure is never described two different ways. Usewarn=Falsewhen embedding it in a result that already reports the outcome - ALWAYS: Return a structured result, not an error, when a write may have partially
landed (
status="unknown",deleted="unknown",safe_to_retry=False). An agent readsisError: trueas "nothing happened, safe to retry" - ALWAYS: Return a structured result for an outcome that is the tool's job to
report:
connected: false, an empty list, an idempotent no-op - NEVER: Wrap a whole tool body in
except Exception. It reports bugs in this server as API failures, and the wrapper already handles the rest
See prowler_mcp_server/lib/errors.py and
docs/developer-guide/mcp-server.mdx for the message format.
ARCHITECTURE
Three Sub-Servers
prowler_mcp_server.mount(hub_mcp_server, namespace="prowler_hub")
prowler_mcp_server.mount(app_mcp_server, namespace="prowler")
prowler_mcp_server.mount(docs_mcp_server, namespace="prowler_docs")
Tool Naming
prowler_hub_*- Catalog and compliance (no auth)prowler_docs_*- Documentation search (no auth)prowler_*- Prowler Cloud, Private Cloud & Local Server management (auth required)
TECH STACK
Python 3.12+ | FastMCP 3.4.4 | httpx (async) | Pydantic | uv | pytest
PROJECT STRUCTURE
mcp_server/prowler_mcp_server/
├── server.py # Main orchestration
├── lib/
│ ├── server.py # ProwlerMCP: base class of every sub-server
│ └── errors.py # Exception types + render_tool_error
├── 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
From mcp_server/:
cd mcp_server
uv run prowler-mcp # STDIO mode
uv run prowler-mcp --transport http --port 8000 # HTTP mode
uv run pytest # Run the test suite
uv run pytest tests/prowler_app/models # Run one area
uv run pytest --cov=./prowler_mcp_server # With coverage
From the repository root:
make test-mcp # Run the MCP test suite exactly as CI does
QA CHECKLIST
- Tool docstrings describe LLM-relevant behavior
- Models use
MinimalSerializerMixin - API responses transformed to simplified models
- No hardcoded secrets
- Failures raise (never
return {"error": ...}); outcomes that may have changed something return a structured result - Parameter descriptions use Pydantic
Field() - Tests added under
mcp_server/tests/, mirroring the source path below the package root (prowler_mcp_server/prowler_app/tools/->tests/prowler_app/tools/), as the SDK does forprowler/->tests/ uv run pytestpasses