wazuh-mcp
wazuh-mcp
An MCP server for querying a remote Wazuh deployment. It talks to both halves of Wazuh — the Manager API (agents, rules, inventory, configuration) and the Wazuh Indexer (alerts, vulnerability state) — and exposes 34 tools plus a composite report generator.
Read-only by default. State-changing tools exist but stay disabled unless you explicitly opt in.
Why both backends
Wazuh splits its data in a way that trips people up: the Manager API knows about agents, rules and inventory, but historical alerts are not there — they live in the Indexer (OpenSearch). Anything resembling "what fired last night" or "top threats this week" needs the Indexer. Configure both.
The 4.8 release also moved vulnerability detection results from the Manager API into the Indexer. This server detects the manager version and routes vulnerability queries to whichever backend actually holds the data, so the same tool works across releases.
Install
uv syncConfigure
Copy .env.example to .env and fill it in, or set the variables in your MCP
client config.
Variable | Required | Default | Purpose |
| for Manager tools | — | e.g. |
| for Manager tools | — | API credentials |
| for alert tools | — | e.g. |
| for alert tools | — | Indexer credentials |
| no | — | Path to your Wazuh root CA (preferred over disabling TLS) |
| no |
| Set |
| no |
| Enables agent restart, scans, active response |
| no |
| Per-request timeout, seconds |
| no |
| Ceiling on any single result set |
| no |
| Alert index pattern |
| no |
| Vulnerability state index |
Either backend can be configured alone. Tools needing a missing backend fail with a message naming the exact variables to set, rather than a generic error.
Least-privilege credentials
Create a dedicated Wazuh API user with a read-only RBAC role instead of reusing
wazuh-wui. If you never intend to enable WAZUH_ALLOW_WRITE, the role needs
only read actions — the server's read-only default and the API's own RBAC then
reinforce each other.
Transports
stdio (default) for a local client — no token, no network surface.
streamable HTTP for remote clients, authenticated with a bearer token:
WAZUH_TRANSPORT=http WAZUH_PUBLIC_URL=https://wazuh.example.com/mcp \
WAZUH_AUTH_TOKENS=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))') \
uv run wazuh-mcpVariable | Default | Purpose |
|
|
|
|
| Bind address; leave on loopback behind a proxy |
|
| Bind port |
|
| URL path to serve on |
| — | Required for |
| — | Required for |
| from | Accepted |
The HTTP transport fails closed: it will not start without a token of at
least 32 characters, or without WAZUH_PUBLIC_URL. Requests arrive as
Authorization: Bearer <token>; anything else gets a 401 carrying the
protected-resource metadata URL. DNS-rebinding protection is on, so the Host
header must appear in the allowlist — a forged one gets 421.
Terminate TLS in front of it. A bearer token over plain HTTP is sent in clear text, and binding to anything other than loopback logs a warning saying so. See deploy/DEPLOY.md for a systemd + nginx deployment, including the point that running on the Wazuh host lets you close ports 55000 and 9200 entirely.
Connect it
Claude Code
claude mcp add wazuh --env WAZUH_API_URL=https://wazuh.example.com:55000 --env WAZUH_API_USER=wazuh-mcp --env WAZUH_API_PASSWORD=secret --env WAZUH_INDEXER_URL=https://wazuh.example.com:9200 --env WAZUH_INDEXER_USER=wazuh-mcp --env WAZUH_INDEXER_PASSWORD=secret -- uv run --directory /Users/lema/Projects/wazuh-mcp wazuh-mcpClaude Desktop / any stdio client
{
"mcpServers": {
"wazuh": {
"command": "uv",
"args": ["run", "--directory", "/Users/lema/Projects/wazuh-mcp", "wazuh-mcp"],
"env": {
"WAZUH_API_URL": "https://wazuh.example.com:55000",
"WAZUH_API_USER": "wazuh-mcp",
"WAZUH_API_PASSWORD": "secret",
"WAZUH_INDEXER_URL": "https://wazuh.example.com:9200",
"WAZUH_INDEXER_USER": "wazuh-mcp",
"WAZUH_INDEXER_PASSWORD": "secret"
}
}
}
}Tools
Health and operations
Tool | What it answers |
| One call: manager version, daemon states, cluster, agent counts, Indexer health. Start here. |
| Node roles, sync state, healthcheck detail |
|
|
| analysisd queue pressure and dropped events, remoted counters, alert volume |
| The running |
| Read-only escape hatch for any Manager API endpoint |
Agents
Tool | What it answers |
| Filter by status, group, OS, version, name, or a raw |
| One endpoint in full: state, OS, hardware, package count |
| Fleet health: status mix, OS spread, outdated, ungrouped |
| Groups and their members |
| The config an agent actually loaded (did the group change land?) |
| Write. Restart agents |
Alerts (Indexer)
Tool | What it answers |
| The main alert query: time window, severity, agent, rule, MITRE, source IP, free text |
| "Top N by X" — one or two grouping levels, done server-side |
| Volume over time with automatic bucket sizing and peak detection |
| Raw OpenSearch DSL, restricted to |
| What data exists and how far back |
Vulnerabilities
Tool | What it answers |
| CVEs by agent, severity, CVE ID, package, CVSS floor |
| Exposure rollup: severity mix, worst hosts, most widespread CVEs |
Detection content
Tool | What it answers |
| Search the ruleset, including by compliance control |
| One rule plus its XML definition |
| Valid rule groups, or every control in a framework |
| How a log source is being parsed |
| ATT&CK techniques, tactics, groups, software, mitigations |
| CDB lookup lists and their contents |
Posture and inventory
Tool | What it answers |
| CIS-style hardening scores per policy, worst first |
| Individual checks with remediation text; defaults to failures |
| File integrity state, hashes, change counts |
| Legacy rootkit / policy-monitoring findings |
| Packages, processes, ports, interfaces, hardware, hotfixes |
| Fleet-wide package search — "who has log4j?" |
| Write. Trigger a FIM scan now |
Reporting
wazuh_generate_report returns structured data and a rendered markdown
report. Types: executive_summary, threat_activity, agent_health,
vulnerability_exposure, compliance (PCI DSS / GDPR / HIPAA / NIST 800-53 /
TSC), file_integrity, authentication.
Active response
wazuh_active_response — Write, destructive. Runs firewall-drop,
disable-account, host-deny and friends on named agents. Marked destructive
in its tool annotations so clients prompt before use.
Conventions
Time — relative (
30m,24h,7d) or ISO-8601. Alert queries default to 24 hours; reports to 7 days.Severity — maps to Wazuh rule levels: critical 15+, high 12–14, medium 7–11, low 4–6, info 0–3.
Agent IDs — zero-padded to three digits automatically;
000is the manager itself.Paging — list results carry
total_matchingand, when more exist,next_offset.
Safety
Read-only by default. Every state-changing tool checks
WAZUH_ALLOW_WRITEbefore issuing any request, and is annotated withread_only_hint=false/destructive_hint=true.Credential endpoints are blocked.
wazuh_api_requestrefuses/agents/{id}/keyand the authentication endpoint, so agent keys and API tokens cannot be pulled into a model's context.Index access is namespaced. Raw Indexer queries are restricted to
wazuh-*prefixes, with path-traversal patterns rejected, so a query cannot reach unrelated indices sharing the cluster.Output is bounded. Long strings are truncated and large containers summarised, so one broad query cannot flood the context window.
Development
uv run pytestuv run ruff check .The suite runs against a mocked Wazuh (respx) and covers the JWT
refresh-on-401 path, envelope handling, .keyword aggregation fallback,
4.8 vulnerability routing, write gating, index guards and report rendering.
Verified end to end against a live Wazuh 4.14.7 deployment. Version-specific behaviour the mocks alone would have missed, now covered by regression tests:
/agents/summary/statusnests counts underconnection/configuration; older releases returned one flat mapping. Both are parsed.wazuh-agentlessd,wazuh-csyslogd,wazuh-maild,wazuh-reportdandwazuh-clusterdare stopped in a healthy default install, so only stopped core daemons are reported as a problem.vulnerability.severityis the literal"-"for unscored CVEs — around a fifth of findings on a real host. It is relabelledUntriagedrather than dropped, so severity breakdowns reconcile with the total.The MITRE
*_idsparameters match internal STIX ids, not ATT&CK numbers. Lookups queryexternal_id/idsoT1595andattack-pattern--<uuid>both work.
Tests construct settings with _env_file=None and strip inherited WAZUH_*
variables, so a populated .env cannot leak into them and quietly satisfy the
assertions that check for missing configuration.