Wazuh-MCP-Server
Production-grade MCP server for Wazuh SIEM — 55 security tools for alert triage, threat hunting, vulnerability management, compliance (PCI DSS, GDPR, HIPAA, NIST CSF, ISO 27001) and active response. Connect Claude or any LLM to your SOC. OAuth 2.1, RBAC, multi-cluster, air-gap ready.
Links
README
From the repo.
Wazuh MCP Server
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
Overview
- 55 tools in 8 toolsets: alerts, agents, vulnerabilities, threat analysis, compliance (PCI-DSS, HIPAA, SOX, GDPR, NIST, ISO 27001:2022), manager/cluster health, and active response with verification and rollback. Also 5 guided prompts, 6 resources and 3 resource templates.
- Read-only by default. The 14 state-changing tools require the
wazuh:writescope, which is never granted implicitly. - MCP transport: Streamable HTTP at
/mcp. Serves protocol revision 2026-07-28 (stateless requests) and theinitializehandshake for 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. The legacy HTTP+SSE endpoint/ssereturns410 Gone. - Authentication: bearer tokens minted from an API key; OAuth 2.0 (authorization code + PKCE) with sign-in by API key or at an OpenID Connect provider (Entra ID, Google Workspace, Okta, Keycloak); or no auth for local development.
- Deployment: Docker Compose or a published multi-arch image; optional Redis for multi-instance sessions; optional multi-cluster routing.
- Local models: a vLLM + Open WebUI stack (
compose.local-llm.yml) and toolset filtering for small models. The only tool that calls a service outside your Wazuh deployment is the optionalsearch_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.
Quick Start
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.
Pre-built image
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.
Connecting Clients
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
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.
Tools
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 |
- Totals: 41 read tools, 14 write tools. Tokens without
wazuh:writedo not see the write tools intools/list. - In multi-cluster mode a 56th tool,
list_wazuh_clusters(system, read), is added and every tool accepts an optionalcluster_id. WAZUH_DISABLED_TOOLShides individual tools. Hidden tools are removed fromtools/listand refused bytools/call; unknown toolset or tool names stop the server at startup.- Every tool carries MCP annotations derived from its scope: read tools are
readOnlyHint: true; containment tools aredestructiveHint: true; rollback tools aredestructiveHint: false; onlysearch_external_contextisopenWorldHint: true. - Input schemas are closed (
additionalProperties: false); undeclared arguments are refused. - Timestamp filters accept ISO 8601 or OpenSearch date math (
now-24h). - Prompts:
security_investigation,threat_hunt,compliance_audit,vulnerability_assessment,iso27001_assessment.
Per-tool parameters: API documentation.
Active response behaviour
- Results report
execution_status: "dispatched": Wazuh confirms the command was delivered to the agent, not that it ran. Confirm the effect with the matchingwazuh_check_*tool. - Blocks are permanent until removed. Wazuh ignores the timeout for API-triggered commands, so a positive
durationis refused. wazuh_firewall_allowandwazuh_host_allowrequire an operator-deployed undo command (WAZUH_AR_FIREWALL_UNDO_COMMAND,WAZUH_AR_HOSTDENY_UNDO_COMMAND); without one they refuse.
Security Model
| 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.
Configuration
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.
Running from source
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.
HTTP Endpoints
| 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 |
Project Layout
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
Documentation
| 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.
Contributing
See CONTRIBUTING.md. Bugs, feature requests and questions go to Issues, and security reports to a private security advisory.
License
Acknowledgments
Thanks to everyone who has contributed code, reviews, bug reports and design feedback. See also ACKNOWLEDGMENTS.md.
Code and pull requests
- @alokemajumder — maintainer; architecture, MCP transport, security hardening, releases
- @gensecai-dev — the 19 action, verification and rollback tools, broken-endpoint fixes, production hardening
- @andrzej-piotrowski-pl — ISO 27001:2022 compliance tools: Annex A control mapping, domain scoring, gap analysis (#74)
- @blackwell-systems — opt-in GCF response encoding for record tools (#102, #104)
- @lucascruzb — period-wide alert aggregation via scroll, the basis of
get_alerts_aggregated(#79) - @kanylbullen — compact output mode for token-efficient responses (#65)
- @mouse-value-add — optional You.com web-search context (#85)
- @DrRSatzteil —
tools/listpagination fix (#70) - @SiM22 — MCP 2025-06-18 support for Windsurf compatibility (#66)
- @aiunmukto —
.env.example(#12), an early CI workflow and the Glama registry listing - @Karibusan — dependency fixes (#38)
- @lwsinclair — MseeP.ai listing (#9)
- @markeclaudio — OpenID Connect sign-in (#123), active-response guard-rails (#124, #125), session-store bounds (#126), Manager TLS verification by default (#127)
- @MilkyWay88 and @taylorwalton — early pull requests on configuration, logging and packaging
Bug 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
- Wazuh — open source security platform
- Model Context Protocol — the protocol this server implements
- vLLM, Ollama and Open WebUI — local model serving and chat, used in the local LLM stack
Contributors
Contributors
| 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
Collected info
- ★ 237 stars
- ⎇ 65 forks
- Language: Python
- Source updated: 9/21/2026
Config for your environment
Replace {MCP_ENDPOINT_URL} with this MCP’s endpoint URL (from its repo or docs above). No API key — you connect directly.
Tool
OS
Config file: ~/.cursor/mcp.json
{
"mcpServers": {
"mcp-server": {
"url": "{MCP_ENDPOINT_URL}"
}
}
}Paste into mcpServers in the config file. Restart Cursor after saving.
If this MCP is also published on mcpchannel.ai, you can subscribe from Browse and use the gateway config there instead.