
Connects MCP clients to Wazuh SIEM through 48 tools covering alert queries, agent monitoring, vulnerability scans, and active response actions. Query alerts in plain English, block IPs, isolate hosts, kill processes, and check compliance status without touching dashboards or writing API calls. Works with cloud LLMs via Claude Desktop or fully local setups using Ollama and Open WebUI for air-gapped environments. Includes proper RBAC, audit logging, and rate limiting since you're essentially giving an AI the keys to trigger security responses across your infrastructure.
A Model Context Protocol (MCP) server for the Wazuh SIEM.
Lets an MCP client — Claude, Open WebUI backed by a local model, or any client that speaks Streamable HTTP — query alerts, agents, vulnerabilities and compliance data, and dispatch active responses, with scope-based access control and audit logging.
Quick Start · Clients · Tools · Security · Configuration · Docs · Changelog · Upgrading
wazuh:write scope, which is never granted implicitly./mcp. Serves protocol revision 2026-07-28 (stateless requests) and the initialize handshake for 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. The legacy HTTP+SSE endpoint /sse returns 410 Gone.compose.local-llm.yml) and toolset filtering for small models. The only tool that calls a service outside your Wazuh deployment is the optional search_external_context (You.com), which can be disabled on its own.Supported Wazuh versions: 4.8.0 through 4.14.7. Alert, vulnerability and alert-backed compliance tools need the Wazuh Indexer. See WAZUH_COMPATIBILITY.md.
Requires Docker with Compose v2 and a Wazuh Manager API user.
git clone https://github.com/gensecaihq/Wazuh-MCP-Server.git
cd Wazuh-MCP-Server
cp .env.example .env
Set the Wazuh connection in .env:
WAZUH_HOST=your-wazuh-manager
WAZUH_USER=your-api-user
WAZUH_PASS=your-api-password
# Needed for alert, vulnerability and alert-backed compliance tools
WAZUH_INDEXER_HOST=your-wazuh-indexer
WAZUH_INDEXER_USER=your-indexer-user
WAZUH_INDEXER_PASS=your-indexer-password
The Manager's TLS certificate is verified. A stock Wazuh install uses a self-signed API certificate that cannot pass verification, so either reissue it for your host and set WAZUH_CA_BUNDLE, or, for a first test, add WAZUH_ALLOW_SELF_SIGNED=true (connects without verification; logged at startup). Details: Manager TLS.
Generate the signing secret and an API key. compose.yml runs the server with ENVIRONMENT=production, which refuses to start without AUTH_SECRET_KEY:
echo "AUTH_SECRET_KEY=$(openssl rand -hex 32)" >> .env
echo "MCP_API_KEY=wazuh_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')" >> .env
Start the server and check it:
docker compose up -d
curl http://localhost:3000/health # liveness
curl http://localhost:3000/ready # checks Manager/Indexer reachability
Exchange the API key for a bearer token (valid for TOKEN_LIFETIME_HOURS, default 24):
curl -s -X POST http://localhost:3000/auth/token -H 'Content-Type: application/json' \
-d "{\"api_key\": \"$(grep ^MCP_API_KEY= .env | cut -d= -f2)\"}"
The key is read-only. To allow active-response tools, add MCP_API_KEY_SCOPES="wazuh:read wazuh:write" to .env, recreate the container with docker compose up -d (restart keeps the old environment), and mint a new token.
Compose publishes the port on 127.0.0.1 only. The server speaks plain HTTP; put a TLS-terminating reverse proxy in front before exposing it (set MCP_BIND to change the host bind address).
python3 deploy.py (or deploy.bat on Windows) performs the same steps, generating AUTH_SECRET_KEY and MCP_API_KEY if they are missing.
Multi-arch images (amd64, arm64) are published to GitHub Container Registry and can be pulled without logging in:
docker pull ghcr.io/gensecaihq/wazuh-mcp-server:latest # tracks main
docker pull ghcr.io/gensecaihq/wazuh-mcp-server:5.0.0 # latest tagged release
latest is built from main and may include changes listed under Unreleased in the changelog. Release images are tagged 5.0.0, 5.0 and v5.0.0 (4.3.0 and earlier have no v-prefixed tag). Upgrading from 4.x: read UPGRADING.md first.
docker run -d --name wazuh-mcp-server --env-file .env -e MCP_HOST=0.0.0.0 -e ENVIRONMENT=production \
-p 127.0.0.1:3000:3000 ghcr.io/gensecaihq/wazuh-mcp-server:latest
MCP_HOST=0.0.0.0 is required inside a container because .env.example sets MCP_HOST=127.0.0.1 for bare-metal installs. -e wins over --env-file, so ENVIRONMENT=production holds even if your .env sets another value.
All clients use the Streamable HTTP endpoint https://<your-host>/mcp.
| Client | Auth mode | How it authenticates |
|---|---|---|
| Claude custom connectors (claude.ai, Claude Desktop) | oauth | OAuth authorization code with PKCE. The server pre-registers a public client, claude-desktop, for Claude's callback URLs. Users sign in on the server's /oauth/authorize page with a wazuh_ API key (the grant is capped at that key's scopes), or at your OpenID Connect provider when OAUTH_IDP_ISSUER is set. |
| Open WebUI, LibreChat, scripts and other MCP clients | bearer (default) | Authorization: Bearer <token> using a token from POST /auth/token. |
In OAuth mode, set OAUTH_ISSUER_URL to the server's public HTTPS URL (otherwise it is derived from each request, which behind a proxy may not be the public URL). Dynamic Client Registration (/oauth/register) is off unless OAUTH_ENABLE_DCR=true, and cannot be combined with OAUTH_IDP_ISSUER.
Guides: Claude Integration · Local LLMs
The server does not call a model; it only executes tools. To keep SIEM data on-premises, pair it with a local model:
cat >> .env <<EOF
VLLM_API_KEY=$(openssl rand -hex 32)
WEBUI_SECRET_KEY=$(openssl rand -hex 32)
EOF
docker compose -f compose.yml -f compose.local-llm.yml up -d
This adds vLLM (default model Qwen3.6-35B-A3B FP8, about 42 GB of VRAM on one NVIDIA GPU; not published on a host port) and Open WebUI on 127.0.0.1:8080. In Open WebUI's admin settings, add an MCP (Streamable HTTP) tool server at http://wazuh-main-server:3000/mcp with a bearer token.
For smaller models, expose fewer tools with WAZUH_TOOLSETS / WAZUH_DISABLED_TOOLS, and check tool selection before rollout with evals/tool_selection.py (25 SOC scenarios, including two prompt-injection cases, against any OpenAI-compatible endpoint; no tools are executed). Model sizing, Ollama and LiteLLM are covered in the Local LLM Guide.
55 tools, grouped into toolsets that can be enabled with WAZUH_TOOLSETS (comma-separated; default all). R = wazuh:read, W = wazuh:write.
| Toolset | Count | Tools |
|---|---|---|
alerts | 5 R | get_wazuh_alerts, get_wazuh_alert_summary, get_alerts_aggregated, analyze_alert_patterns, search_security_events |
agents | 6 R | get_wazuh_agents, get_wazuh_running_agents, check_agent_health, get_agent_processes, get_agent_ports, get_agent_configuration |
vulnerabilities | 3 R | get_wazuh_vulnerabilities, get_wazuh_critical_vulnerabilities, get_wazuh_vulnerability_summary |
analysis | 5 R | analyze_security_threat, check_ioc_reputation, perform_risk_assessment, get_top_security_threats, generate_security_report |
web_search | 1 R | search_external_context — You.com web search; returns a "not enabled" result unless YDC_API_KEY is set |
compliance | 6 R | run_compliance_check (PCI-DSS, HIPAA, SOX, GDPR, NIST, ISO27001), get_iso27001_dashboard, get_iso27001_control_detail, get_iso27001_gap_analysis, get_iso27001_alerts, get_sca_policy_checks |
system | 10 R | get_wazuh_statistics, get_wazuh_weekly_stats, get_wazuh_cluster_health, get_wazuh_cluster_nodes, get_wazuh_rules_summary, get_wazuh_remoted_stats, get_wazuh_log_collector_stats, search_wazuh_manager_logs, get_wazuh_manager_error_logs, validate_wazuh_connection |
response — containment | 9 W | wazuh_block_ip, wazuh_isolate_host, wazuh_kill_process, wazuh_disable_user, wazuh_quarantine_file, wazuh_firewall_drop, wazuh_host_deny, wazuh_active_response, wazuh_restart |
response — rollback | 5 W | wazuh_unisolate_host, wazuh_enable_user, wazuh_restore_file, wazuh_firewall_allow, wazuh_host_allow |
response — verification | 5 R | wazuh_check_blocked_ip, wazuh_check_agent_isolation, wazuh_check_process, wazuh_check_user_status, wazuh_check_file_quarantine |
wazuh:write do not see the write tools in tools/list.list_wazuh_clusters (system, read), is added and every tool accepts an optional cluster_id.WAZUH_DISABLED_TOOLS hides individual tools. Hidden tools are removed from tools/list and refused by tools/call; unknown toolset or tool names stop the server at startup.readOnlyHint: true; containment tools are destructiveHint: true; rollback tools are destructiveHint: false; only search_external_context is openWorldHint: true.additionalProperties: false); undeclared arguments are refused.now-24h).security_investigation, threat_hunt, compliance_audit, vulnerability_assessment, iso27001_assessment.Per-tool parameters: API documentation.
execution_status: "dispatched": Wazuh confirms the command was delivered to the agent, not that it ran. Confirm the effect with the matching wazuh_check_* tool.duration is refused.wazuh_firewall_allow and wazuh_host_allow require an operator-deployed undo command (WAZUH_AR_FIREWALL_UNDO_COMMAND, WAZUH_AR_HOSTDENY_UNDO_COMMAND); without one they refuse.| Control | Behaviour |
|---|---|
| Scopes (RBAC) | Each tool requires wazuh:read or wazuh:write. A token without a scope claim is read-only. MCP_API_KEY is read-only unless MCP_API_KEY_SCOPES includes wazuh:write. With AUTH_MODE=none, write tools are disabled unless AUTHLESS_ALLOW_WRITE=true. |
| Bearer tokens | JWTs signed with AUTH_SECRET_KEY, must carry exp, and are bound to the API key they were minted from: revoking or rotating the key invalidates its tokens. Refresh tokens are not accepted as access tokens. |
| OAuth | Authorization code flow with mandatory S256 PKCE, single-use codes, refresh-token rotation with replay detection, and revocation. Users sign in with a wazuh_ API key (the grant is capped at that key's scopes, and its tokens end when the key is revoked) or at an OpenID Connect provider (ID token signature, issuer, audience, expiry and nonce verified; tenant, domain and user allow-lists; group-to-scope mapping), so scopes, rate limits and audit entries are per user. |
| Action guardrails | IP-blocking tools refuse loopback, the Manager's address (when WAZUH_HOST is an IP) and anything in WAZUH_PROTECTED_IPS; the generic active-response tool does not dispatch IP blocks, quarantine, process kills or account disables. Actions and restarts aimed at agent 000 (the Manager) need WAZUH_ALLOW_MANAGER_AR=true; fleet-wide blocks need WAZUH_ALLOW_FLEET_AR=true; quarantine refuses system and agent directories. In production, write tools require confirm=true (WAZUH_REQUIRE_ACTION_CONFIRMATION). |
| Wazuh TLS | The Manager and Indexer certificates are verified by default, against the system store or WAZUH_CA_BUNDLE. Disabling Manager verification (WAZUH_ALLOW_SELF_SIGNED=true) is logged at startup, as an error in production. |
| Audit log | Every write-tool call that passes the scope and confirmation checks is logged before and after execution (logger wazuh_mcp_server.audit) with the principal, session, arguments and outcome. |
| Redaction | Credentials and tokens are redacted from tool output in every response format, and from server logs. |
| Input validation | Typed validation of agent IDs, IPs, paths and command names; Indexer queries are built as Query DSL, not by string interpolation. |
| Rate limiting | Sliding window, default 100 requests per 60 s (RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW): per principal and client IP on /mcp and /, per client IP on other endpoints except the probe endpoints (/health, /ready, /metrics and their /healthz, /readyz, /live, /livez aliases). Failed authentication is rate limited by client IP. Set TRUSTED_PROXIES when running behind a proxy. |
| Resource bounds | Circuit breaker on Wazuh calls: opens after 5 consecutive failures, retries after 60 s. Oversized tool results are truncated with a note (MAX_TOOL_RESPONSE_CHARS). limit on get_wazuh_alerts and search_security_events is capped at MAX_ALERTS_PER_QUERY (default 1000). The in-memory session store is capped at MAX_SESSIONS (1000) and MAX_SESSIONS_PER_PRINCIPAL (100), and stored client metadata is truncated. |
| Container | Runs as UID 1000; compose.yml sets a read-only root filesystem, cap_drop: ALL and no-new-privileges. The runtime image does not include pip. |
There is no built-in TLS listener; terminate TLS at a reverse proxy or load balancer. Report vulnerabilities as described in SECURITY.md.
All settings are environment variables (usually via .env). The ones most deployments touch:
| Variable | Default | Purpose |
|---|---|---|
WAZUH_HOST, WAZUH_USER, WAZUH_PASS | — | Manager API connection (required) |
WAZUH_PORT | 55000 | Manager API port |
WAZUH_CA_BUNDLE | — | CA PEM used instead of the system store to verify the Manager and Indexer; see Manager TLS |
WAZUH_ALLOW_SELF_SIGNED | false | true connects to the Manager without certificate verification (logged at startup) |
WAZUH_INDEXER_HOST, WAZUH_INDEXER_USER, WAZUH_INDEXER_PASS | — | Indexer connection; an http:// host prefix selects plain HTTP |
WAZUH_INDEXER_PORT | 9200 | Indexer port |
ENVIRONMENT | development | production requires a strong AUTH_SECRET_KEY (unless AUTH_MODE=none) |
AUTH_MODE | bearer | bearer, oauth or none |
AUTH_SECRET_KEY | generated per process outside production | Token signing key; use the same value on every instance |
MCP_API_KEY / API_KEYS | generated per process if unset (printed only in development) | A single wazuh_ key, or a JSON list of hashed keys with per-key scopes |
MCP_API_KEY_SCOPES | wazuh:read | Space-separated scopes for MCP_API_KEY |
OAUTH_ISSUER_URL | derived from the request | Public HTTPS URL of the server, for AUTH_MODE=oauth |
OAUTH_IDP_ISSUER | — | OpenID Connect provider for OAuth sign-in (with OAUTH_IDP_CLIENT_ID); API-key sign-in when unset |
WAZUH_REQUIRE_ACTION_CONFIRMATION | true in production, else false | Write tools require confirm=true |
MCP_HOST, MCP_PORT | 127.0.0.1 (0.0.0.0 in the Docker image), 3000 | Bind address and port |
ALLOWED_ORIGINS | https://claude.ai,http://localhost:3000 | CORS allow-list (exact match) |
WAZUH_TOOLSETS, WAZUH_DISABLED_TOOLS | all enabled | Limit the exposed tools |
REDIS_URL | — | Shared session store for multi-instance deployments |
WAZUH_CLUSTERS_FILE | ./config/clusters.json | Multi-cluster topology; single-cluster mode when absent |
RESPONSE_FORMAT | json | gcf encodes alert, event and vulnerability collections in the compact GCF format (lossless; falls back to JSON if the encoder is unavailable) |
YDC_API_KEY | — | Enables search_external_context |
Complete reference, including OAuth TTLs, rate limits, sessions and active-response settings: Configuration Guide. Multi-cluster setup: Multi-Cluster Guide and config/clusters.json.example.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[redis,gcf]" # extras are optional
set -a; . ./.env; set +a # the server reads the environment, not .env
python -m wazuh_mcp_server
Requires Python 3.11 or later.
| Endpoint | Method | Description |
|---|---|---|
/mcp | POST, GET, DELETE | MCP Streamable HTTP |
/ | POST, GET | Same handler as /mcp |
/sse | GET, POST | Returns 410 Gone; use /mcp |
/health | GET | Liveness; no dependency checks |
/ready | GET | Readiness; 503 when the Manager, Indexer or memory headroom check fails |
/metrics | GET | Prometheus metrics |
/auth/token | POST | Exchange an API key for a bearer JWT |
/.well-known/oauth-authorization-server | GET | OAuth metadata (RFC 8414), AUTH_MODE=oauth only |
/.well-known/oauth-protected-resource | GET | Protected-resource metadata (RFC 9728), AUTH_MODE=oauth only |
/oauth/authorize, /oauth/token, /oauth/revoke, /oauth/register, /oauth/callback | GET/POST | OAuth endpoints, AUTH_MODE=oauth only (/oauth/register requires OAUTH_ENABLE_DCR=true and no OAUTH_IDP_ISSUER; /oauth/callback is the OpenID Connect redirect URI and needs OAUTH_IDP_ISSUER) |
/docs, /redoc, /openapi.json | GET | OpenAPI documentation |
src/wazuh_mcp_server/
├── server.py # FastAPI app, MCP protocol handling, CORS/Origin checks, tool definitions and dispatch
├── toolsets.py # Toolset membership, WAZUH_TOOLSETS resolution, tool annotations
├── auth.py # API keys and bearer JWTs
├── oauth.py # OAuth 2.0 authorization server (PKCE, API-key sign-in)
├── oidc.py # OpenID Connect sign-in at an external identity provider
├── config.py # Environment configuration and startup validation
├── security.py # Rate limiting, request and input validation, log redaction
├── clusters.py # Multi-cluster registry and Cross-Cluster Search routing
├── session_store.py # In-memory and Redis session storage
├── resilience.py # Circuit breakers, retries, graceful shutdown
├── monitoring.py # Prometheus metrics, structured logging
├── gcf_format.py # Optional GCF response encoding
└── api/
├── wazuh_client.py # Wazuh Manager REST API client
└── wazuh_indexer.py # Wazuh Indexer (OpenSearch) client
| Document | Contents |
|---|---|
| Configuration Guide | Every environment variable, auth modes, RBAC |
| Claude Integration | Connecting Claude custom connectors |
| Local LLM Guide | vLLM, Open WebUI, Ollama, LiteLLM, tool-selection eval |
| Multi-Cluster | Named clusters and Cross-Cluster Search |
| Operations | Deployment, monitoring, maintenance |
| Advanced Features | Multi-instance deployment, compact output |
| Troubleshooting | Common problems and fixes |
| API Reference | Per-tool parameters |
| Security Hardening | Hardening guidance |
| MCP Compliance | Protocol conformance notes |
| Wazuh Compatibility | Supported Wazuh versions |
| Upgrading · Changelog · Security Policy | Release notes and policies |
Related project: Wazuh Autopilot builds automated SOC workflows on top of this server.
See CONTRIBUTING.md. Bugs, feature requests and questions go to Issues, and security reports to a private security advisory.
Thanks to everyone who has contributed code, reviews, bug reports and design feedback. See also ACKNOWLEDGMENTS.md.
Code and pull requests
get_alerts_aggregated (#79)tools/list pagination fix (#70).env.example (#12), an early CI workflow and the Glama registry listingBug reports and discussions
@cbassonbgroup,
@cybersentinel-06,
@daod-arshad,
@mamema,
@marcolinux46,
@matveevandrey,
@punkpeye,
@tonyliu9189,
@Uberkarhu,
@bl4ck5w4n07,
@gnix45,
@hackdefendr,
@melmasry1987,
@Vasanth120v,
@wqfh
Built on and works with
| Avatar | Username | Contributions |
|---|---|---|
| @alokemajumder | 💻 Code, 🐛 Issues, 🔀 PRs, 💬 Discussions | |
| @Karibusan | 💻 Code, 🐛 Issues, 🔀 PRs | |
| @gensecai-dev | 💻 Code, 🔀 PRs, 💬 Discussions | |
| @aiunmukto | 💻 Code, 🔀 PRs | |
| @andrzej-piotrowski-pl | 💻 Code, 🔀 PRs | |
| @blackwell-systems | 💻 Code, 🔀 PRs | |
| @kanylbullen | 💻 Code, 🔀 PRs | |
| @lucascruzb | 💻 Code, 🔀 PRs | |
| @lwsinclair | 💻 Code, 🔀 PRs | |
| @mouse-value-add | 💻 Code, 🔀 PRs | |
| @SiM22 | 💻 Code, 🔀 PRs | |
| @DrRSatzteil | 🔀 PRs | |
| @MilkyWay88 | 🔀 PRs | |
| @taylorwalton | 🔀 PRs | |
| @cbassonbgroup | 🐛 Issues | |
| @cybersentinel-06 | 🐛 Issues | |
| @daod-arshad | 🐛 Issues | |
| @mamema | 🐛 Issues | |
| @marcolinux46 | 🐛 Issues | |
| @matveevandrey | 🐛 Issues | |
| @punkpeye | 🐛 Issues | |
| @tonyliu9189 | 🐛 Issues | |
| @Uberkarhu | 🐛 Issues | |
| @bl4ck5w4n07 | 💬 Discussions | |
| @gnix45 | 💬 Discussions | |
| @hackdefendr | 💬 Discussions | |
| @melmasry1987 | 💬 Discussions | |
| @Vasanth120v | 💬 Discussions | |
| @wqfh | 💬 Discussions |
Legend: 💻 Code · 🐛 Issues · 🔀 Pull Requests · 💬 Discussions
Auto-updated by GitHub Actions