Skip to content
Security
Skill

/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).

From plugin
prowler
15k39 skills1 MCP

Claude Code

$ npx -y skills add prowler-cloud/prowler --skill prowler-test-mcp --agent claude-code

SKILL.md

prowler-test-mcp.SKILL.md
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.1.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, Task

Critical 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`.
  • Warnings are errors (`filterwarnings = ["error", ...]` in `pyproject.toml`): a new

deprecation warning fails the suite. Fix the cause; do not add a blanket filter.

  • 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 far

Register 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 meanin

Read more
@prowler-cloud
Ships withprowler

Prowler is the world’s most widely used Open-Source Cloud Security Platform that automates security and compliance across any cloud environment.

15k39 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
15.2k
Star history of prowler-cloud/prowler on GitHub, a chart by star-history.com

Other skills on prowler.

ai-sdk-5

ai-sdk-5

Vercel AI SDK patterns for the installed major (ui/package.json: `ai` 6.x, `@ai-sdk/react` 3.x). The skill name is historical;…

django-drf

django-drf

Django REST Framework patterns. Trigger: When implementing generic DRF APIs (ViewSets, serializers, routers, permissions,…

gh-aw

gh-aw

Create and maintain GitHub Agentic Workflows (gh-aw) for Prowler. Trigger: When creating agentic workflows, modifying gh-aw…

jsonapi

jsonapi

Strict JSON:API v1.1 specification compliance. Trigger: When creating or modifying API endpoints, reviewing API responses, or…

More skills from other plugins.

api-security

api-security

Use for authorized security assessment of REST, GraphQL, WebSocket, or SOAP APIs, including discovery, authentication,…

@zhaoxuya520@zhaoxuya520Install Skill
nemoclaw-user-guide

nemoclaw-user-guide

Find official NemoClaw documentation for installation, configuration, operation, or troubleshooting. Also handles requested docs…

nemoclaw10d23k
@nvidia@nvidiaInstall Skill

Questions about prowler-test-mcp.

Which coding agents can use prowler-test-mcp?
prowler-test-mcp installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.