/prowler-test-mcp
Testing patterns for the Prowler MCP Server: in-memory FastMCP clients, the ProwlerAPIClient singleton, JSON:API model builders and mocked httpx transports. Trigger: When writing tests under mcp_server/tests/ (tools, models, api_client, auth, sub-servers).
$ npx -y skills add prowler-cloud/prowler --skill prowler-test-mcp --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/prowler-test-mcp
Context preview
The summary Claude sees to decide when to auto-load this skill.
Testing patterns for the Prowler MCP Server: in-memory FastMCP clients, the ProwlerAPIClient singleton, JSON:API model builders and mocked httpx transports. Trigger: When writing tests under mcp_server/tests/ (tools, models, api_client, auth, sub-servers).
SKILL.md
prowler-test-mcp.SKILL.mdname: prowler-test-mcp
description: >
Testing patterns for the Prowler MCP Server: in-memory FastMCP clients, the
ProwlerAPIClient singleton, JSON:API model builders and mocked httpx transports.
Trigger: When writing tests under mcp_server/tests/ (tools, models, api_client, auth, sub-servers).
license: Apache-2.0
metadata:
author: prowler-cloud
version: "1.0.0"
scope: [root, mcp_server]
auto_invoke:
- "Writing Prowler MCP server tests"
- "Testing MCP tools or models"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, TaskCritical Rules
- ALWAYS drive tools through an in-memory client: `async with Client(mcp_root_server)`.
Tool parameters use pydantic `Field(default=...)`, and only FastMCP's wrapper resolves those defaults. Calling a tool method directly with an argument omitted leaves it as a raw `FieldInfo` — which is truthy, so `if email:` silently builds a filter out of the `FieldInfo` repr. Direct calls MUST pass every argument.
- NEVER open a `fastmcp.Client` inside a fixture. FastMCP warns this causes
hard-to-diagnose event-loop issues; open it inline in the test.
- ALWAYS use the `mock_api_client` fixture; NEVER construct a `ProwlerAPIClient`.
Tool instances captured the singleton by reference at import time, so only an in-place patch of `.client` reaches them.
- NEVER clear `SingletonMeta._instances`. It orphans every registered tool on an
instance holding a real `httpx.AsyncClient`. Use `isolated_api_client` if you genuinely need a fresh instance.
- NEVER strip `PROWLER_API_KEY`. Tools are built at import time and a construction
failure is swallowed, so the whole `prowler_*` namespace silently drops to zero tools. It is pinned in `[tool.pytest_env]`.
- For `ProwlerAppAuth`, pass `mode=` / `base_url=` explicitly. Those are resolved in
default arguments, evaluated once at module import, so `monkeypatch.setenv` has no effect on them.
- NEVER assert an exact tool count — every future branch would have to bump it.
- Assert on `result.data` (structured output), not `result.content[0].text`.
- Tests are `test_*.py` (prefix), like the API — not the SDK's `*_test.py` suffix.
- `__init__.py` IS required in every `tests/` subdirectory here (unlike the SDK's
repo-root `tests/`), or same-named modules collide under pytest's import mode.
- Async tests need no marker (`asyncio_mode = "auto"`). Do not use `@pytest.mark.anyio`.
- Use only obviously-fake credentials from `tests.helpers.tokens` (TruffleHog).
- One behaviour per test; keep tests self-contained and order-independent.
---
1. Layout
Mirror the source tree *below the package root* — drop the `prowler_mcp_server/` level, exactly as the SDK maps `prowler/providers/...` to `tests/providers/...`. So `prowler_mcp_server/prowler_app/tools/` is tested in `tests/prowler_app/tools/`.
mcp_server/tests/
├── conftest.py # all shared fixtures
├── helpers/ # jsonapi.py, http.py, assertions.py, tokens.py
├── test_server.py # mounted-server contract
├── test_health.py
├── prowler_app/{models,tools,utils}/
├── prowler_hub/
└── prowler_documentation/---
2. Fixtures
| Fixture | Autouse | What it gives you | |---------|---------|-------------------| | `_pinned_environment` | yes | Deterministic env; blocks a developer's `.env` from leaking | | `_no_real_network` | yes | Any real socket connect raises `RuntimeError` | | `_singleton_registry_guard` | yes | Snapshots/restores `SingletonMeta._instances` | | `mock_router` | no | Route registry + request recorder | | `api_client` | no | The live `ProwlerAPIClient` singleton | | `mock_api_client` | no | **The workhorse** — singleton with a mocked transport | | `isolated_api_client` | no | Evicts the singleton, for construction/identity tests | | `mcp_root_server` | no | The mounted root server (session-scoped) | | `health_client` | no | Starlette `TestClient` for `/health` | | `http_request_headers` | no | Injects headers for HTTP-mode auth | | `hub_router` | no | Mocks the Hub sub-server's two sync clients | | `docs_router` | no | Mocks the docs search engine's two sync clients |
`MockRouter`
mock_router.add("GET", "/api/v1/users", json=jsonapi_collection([...]))
mock_router.add("GET", "/api/v1/tasks/t1", json=task_document("t1", "completed"))
mock_router.request_for("GET", "/api/v1/users") # last request, for header asserts
mock_router.query_params("GET", "/api/v1/users") # decoded query string
mock_router.paths() # everything requested so farRegister a route more than once to return a sequence — the last response repeats. That is how you drive `poll_task_until_complete` (`executing`, `executing`, `completed`). An unregistered request raises, listing what *was* registered.
---
3. Patterns
**Tool test** — see `assets/mcp_tool_test.py`. Register routes, call through the in-memory client, assert on `result.data` *and* on the recorded request. When a tool chooses between endpoints, assert `mock_router.paths()` — a wrong choice is invisible in the response body.
**Model test** — see `assets/mcp_model_test.py`. Build the document with the `jsonapi` helpers, run `from_api_response()`, assert on both the model and `model_dump()`. `MinimalSerializerMixin` makes those differ, and an absent relationship (`None`) must never be conflated with an empty one (`[]`).
**Contract test** — see `assets/mcp_contract_test.py`. Namespacing and description coverage across every registered tool.
The worked example in the repo is `findings`, covered across both layers in `tests/prowler_app/{models,tools}/test_findings.py`. Read those first — they exercise every foundation capability in one feature.
Reading coverage
Coverage has a meaningless high floor. Model modules are almost entirely class-body `Field(...)` declarations that execute at import, and `prowler_app/server.py` imports every model module
Read more
name: prowler-test-mcp
description: >
Testing patterns for the Prowler MCP Server: in-memory FastMCP clients, the
ProwlerAPIClient singleton, JSON:API model builders and mocked httpx transports.
Trigger: When writing tests under mcp_server/tests/ (tools, models, api_client, auth, sub-servers).
license: Apache-2.0
metadata:
author: prowler-cloud
version: "1.0.0"
scope: [root, mcp_server]
auto_invoke:
- "Writing Prowler MCP server tests"
- "Testing MCP tools or models"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, TaskCritical Rules
- ALWAYS drive tools through an in-memory client: `async with Client(mcp_root_server)`.
Tool parameters use pydantic `Field(default=...)`, and only FastMCP's wrapper resolves those defaults. Calling a tool method directly with an argument omitted leaves it as a raw `FieldInfo` — which is truthy, so `if email:` silently builds a filter out of the `FieldInfo` repr. Direct calls MUST pass every argument.
- NEVER open a `fastmcp.Client` inside a fixture. FastMCP warns this causes
hard-to-diagnose event-loop issues; open it inline in the test.
- ALWAYS use the `mock_api_client` fixture; NEVER construct a `ProwlerAPIClient`.
Tool instances captured the singleton by reference at import time, so only an in-place patch of `.client` reaches them.
- NEVER clear `SingletonMeta._instances`. It orphans every registered tool on an
instance holding a real `httpx.AsyncClient`. Use `isolated_api_client` if you genuinely need a fresh instance.
- NEVER strip `PROWLER_API_KEY`. Tools are built at import time and a construction
failure is swallowed, so the whole `prowler_*` namespace silently drops to zero tools. It is pinned in `[tool.pytest_env]`.
- For `ProwlerAppAuth`, pass `mode=` / `base_url=` explicitly. Those are resolved in
default arguments, evaluated once at module import, so `monkeypatch.setenv` has no effect on them.
- NEVER assert an exact tool count — every future branch would have to bump it.
- Assert on `result.data` (structured output), not `result.content[0].text`.
- Tests are `test_*.py` (prefix), like the API — not the SDK's `*_test.py` suffix.
- `__init__.py` IS required in every `tests/` subdirectory here (unlike the SDK's
repo-root `tests/`), or same-named modules collide under pytest's import mode.
- Async tests need no marker (`asyncio_mode = "auto"`). Do not use `@pytest.mark.anyio`.
- Use only obviously-fake credentials from `tests.helpers.tokens` (TruffleHog).
- One behaviour per test; keep tests self-contained and order-independent.
---
1. Layout
Mirror the source tree *below the package root* — drop the `prowler_mcp_server/` level, exactly as the SDK maps `prowler/providers/...` to `tests/providers/...`. So `prowler_mcp_server/prowler_app/tools/` is tested in `tests/prowler_app/tools/`.
mcp_server/tests/
├── conftest.py # all shared fixtures
├── helpers/ # jsonapi.py, http.py, assertions.py, tokens.py
├── test_server.py # mounted-server contract
├── test_health.py
├── prowler_app/{models,tools,utils}/
├── prowler_hub/
└── prowler_documentation/---
2. Fixtures
| Fixture | Autouse | What it gives you | |---------|---------|-------------------| | `_pinned_environment` | yes | Deterministic env; blocks a developer's `.env` from leaking | | `_no_real_network` | yes | Any real socket connect raises `RuntimeError` | | `_singleton_registry_guard` | yes | Snapshots/restores `SingletonMeta._instances` | | `mock_router` | no | Route registry + request recorder | | `api_client` | no | The live `ProwlerAPIClient` singleton | | `mock_api_client` | no | **The workhorse** — singleton with a mocked transport | | `isolated_api_client` | no | Evicts the singleton, for construction/identity tests | | `mcp_root_server` | no | The mounted root server (session-scoped) | | `health_client` | no | Starlette `TestClient` for `/health` | | `http_request_headers` | no | Injects headers for HTTP-mode auth | | `hub_router` | no | Mocks the Hub sub-server's two sync clients | | `docs_router` | no | Mocks the docs search engine's two sync clients |
`MockRouter`
mock_router.add("GET", "/api/v1/users", json=jsonapi_collection([...]))
mock_router.add("GET", "/api/v1/tasks/t1", json=task_document("t1", "completed"))
mock_router.request_for("GET", "/api/v1/users") # last request, for header asserts
mock_router.query_params("GET", "/api/v1/users") # decoded query string
mock_router.paths() # everything requested so farRegister a route more than once to return a sequence — the last response repeats. That is how you drive `poll_task_until_complete` (`executing`, `executing`, `completed`). An unregistered request raises, listing what *was* registered.
---
3. Patterns
**Tool test** — see `assets/mcp_tool_test.py`. Register routes, call through the in-memory client, assert on `result.data` *and* on the recorded request. When a tool chooses between endpoints, assert `mock_router.paths()` — a wrong choice is invisible in the response body.
**Model test** — see `assets/mcp_model_test.py`. Build the document with the `jsonapi` helpers, run `from_api_response()`, assert on both the model and `model_dump()`. `MinimalSerializerMixin` makes those differ, and an absent relationship (`None`) must never be conflated with an empty one (`[]`).
**Contract test** — see `assets/mcp_contract_test.py`. Namespacing and description coverage across every registered tool.
The worked example in the repo is `findings`, covered across both layers in `tests/prowler_app/{models,tools}/test_findings.py`. Read those first — they exercise every foundation capability in one feature.
Reading coverage
Coverage has a meaningless high floor. Model modules are almost entirely class-body `Field(...)` declarations that execute at import, and `prowler_app/server.py` imports every model module
Prowler is the world’s most widely used Open-Source Cloud Security Platform that automates security and compliance across any cloud environment.
Repo: prowler-cloud/prowler
Other skills on prowler.
- /framework-compliance-triage
Make a cloud account compliant with a security or industry framework using Prowler Cloud.
Open skill - /ai-sdk-5
Vercel AI SDK 5 patterns. Trigger: When building AI features with AI SDK v5 (chat, streaming, tools/function calling, UIMessage parts), including migration from v4.
Open skill - /django-drf
Django REST Framework patterns. Trigger: When implementing generic DRF APIs (ViewSets, serializers, routers, permissions, filtersets). For Prowler API specifics (RLS/RBAC/Providers), also use prowler-api.
Open skill - /django-migration-psql
Reviews Django migration files for PostgreSQL best practices specific to Prowler. Trigger: When creating migrations, running makemigrations/pgmakemigrations, reviewing migration PRs, adding indexes or constraints to database tables, modifying existing migration files, or writing
Open skill - /gh-aw
Create and maintain GitHub Agentic Workflows (gh-aw) for Prowler. Trigger: When creating agentic workflows, modifying gh-aw frontmatter, configuring safe-outputs, setting up MCP servers in workflows, importing Copilot Custom Agents, or debugging gh-aw compilation.
Open skill - /jsonapi
Strict JSON:API v1.1 specification compliance. Trigger: When creating or modifying API endpoints, reviewing API responses, or validating JSON:API compliance.
Open skill

