A read-only Wazuh SIEM/XDR control CLI and MCP adapter for alerts, agents, vulnerabilities, rules, and more.
Website: lidless.dev/wazuh-mcp
wazuhctrl is a read-only control CLI for the Wazuh SIEM/XDR platform. The same package ships wazuh-mcp, a Model Context Protocol (MCP) adapter that exposes your Wazuh manager and Wazuh Indexer as MCP tools so Claude, Claude Code, or any MCP-compatible client can investigate alerts, triage agents, and pull vulnerability inventory in plain language. It is read-only by design and security-first: TLS verification is on by default, sensitive fields (agent IPs, full logs, file hashes, command lines) are hidden unless you opt in per call, and attacker-controlled SIEM text is wrapped in untrusted-data markers to blunt prompt injection against the calling model.
Run a shared HTTP process from the built repository:
npm run build
node dist/mcp-bin.js --http
# The existing wazuh-mcp bin also accepts --http.Keep the usual manager and optional indexer environment variables on the server.
Generate a separate HTTP token with openssl rand -hex 32, then set:
| Variable | Default | Purpose |
|---|---|---|
WAZUH_MCP_HTTP_TOKEN |
Required | Independent bearer token, at least 32 characters after trimming whitespace |
WAZUH_MCP_HTTP_HOST |
IPv4 loopback | Specific listener bind address. Wildcards are refused |
WAZUH_MCP_HTTP_PORT |
3120 |
TCP port, or 0 for a free port |
Startup rejects a missing or blank token and reuse of manager or indexer credentials, including passwords and Basic authorization values. Bearer tokens are compared with SHA-256 digests and constant-time comparison and registered with the error sanitizer.
/mcp accepts authenticated Streamable HTTP POST, GET, and DELETE requests.
GET /healthz needs no token and returns only {"status":"ok"}.
The Host header must name the bound host or a loopback alias. A supplied Origin
must be an HTTP or HTTPS loopback origin. Requests have a 1 MiB body limit,
a 30-second request timeout, and a 10-second header timeout. The process accepts
128 sessions and expires sessions idle for 30 minutes on a one-minute timer.
Active POST requests prevent expiry. An open idle SSE channel does not.
SIGTERM and SIGINT close all session transports and the listener.
Each HTTP session owns a separate MCP server. The manager client, cached JWT, and in-flight login promise are shared across sessions. Concurrent initial logins and token refreshes coalesce into one authentication request, and a late 401 for an old token reuses the replacement token. The indexer client holds only upstream configuration. Tools are read-only, with no pending confirmations, per-client approvals, deferred visibility sets, or mutable tool result caches. Sensitive-field opt-ins belong to each call. Session deletion does not clear another session's state or the shared JWT.
Every holder of the HTTP token shares the configured upstream account's read
privileges. Session IDs route protocol state and do not identify separate users.
Keep the loopback binding for local clients. Remote access needs a trusted TLS
proxy or tunnel and network access controls. Preserve an allowed Host header
through a proxy. This listener provides no TLS, OAuth, or browser CORS access.
Omit --http to retain the existing stdio behavior.
Export the HTTP token in the environment that launches Claude Code. Add an HTTP entry to its MCP configuration:
{
"mcpServers": {
"wazuh": {
"type": "http",
"url": "http://localhost:3120/mcp",
"headersHelper": "/path/to/helper WAZUH_MCP_HTTP_TOKEN"
}
}
}The executable helper reads the named variable and prints a JSON object with the Authorization header. For example, save this as the helper and make it executable:
#!/usr/bin/env node
const token = process.env[process.argv[2]]
if (!token?.trim()) process.exit(1)
console.log(JSON.stringify({ Authorization: `Bearer ${token}` }))Export WAZUH_MCP_HTTP_TOKEN in the environment that launches Codex. Its value
must match the server's token, without the Bearer prefix:
[mcp_servers.wazuh]
url = "http://localhost:3120/mcp"
bearer_token_env_var = "WAZUH_MCP_HTTP_TOKEN"An example unit is in docs/systemd/wazuh-mcp-http.service.
Adjust its repository and Node paths, build the repository, and create the
referenced EnvironmentFile with mode 0600. Put the manager credentials,
optional indexer credentials, and independent HTTP token in that file. The unit
runs the built dist/mcp-bin.js --http entry point with NoNewPrivileges=true,
PrivateTmp=true, and UMask=0077.
wazuhctrl and wazuh-mcp turn a Wazuh SIEM/XDR deployment into typed operator surfaces. Point your MCP client at the server, give it your Wazuh manager and (optionally) Wazuh Indexer credentials, and the model can list active and disconnected agents, retrieve and full-text search security alerts, pull vulnerability inventory by CVE or severity, inspect detection rules and decoders, review SCA (Security Configuration Assessment) results, walk system inventory (OS, packages, processes, ports, network, hotfixes), read File Integrity Monitoring and rootcheck findings, fetch manager logs and configuration, and run a connection diagnostic. The CLI starts with status, agent inventory, and diagnostics for shells, cron, and CI. The package ships 28 MCP tools, 3 resources, and 3 guided prompts over stdio or Streamable HTTP. Both surfaces only read from Wazuh: the sole writes they perform are JWT authentication against the manager and _search queries against the indexer.
The quickstart below runs the published npm package with npx, which is the recommended path. To work from source instead:
git clone https://github.com/lidless-labs/wazuh-mcp.git
cd wazuh-mcp
npm install
npm run buildThe quickstart runs the published npm package with npx under two dedicated accounts: a Wazuh API user with the built-in readonly role and a Wazuh Indexer user whose custom role reads only the alert and vulnerability indices. Create both with Least-privilege setup first. Do not point the server at the wazuh-wui API administrator or the indexer admin superuser.
Put the settings in a file that only you can read:
mkdir -p -m 700 ~/.config/wazuh-mcp
touch ~/.config/wazuh-mcp/env
chmod 600 ~/.config/wazuh-mcp/envThen fill it in with your editor:
WAZUH_URL=https://your-wazuh-manager:55000
WAZUH_USERNAME=wazuh-mcp-api
WAZUH_PASSWORD='the-api-user-password'
WAZUH_CA_FILE=/path/to/root-ca.pem
WAZUH_INDEXER_URL=https://your-wazuh-indexer:9200
WAZUH_INDEXER_USERNAME=wazuh-mcp-indexer
WAZUH_INDEXER_PASSWORD='the-indexer-user-password'
WAZUH_INDEXER_CA_FILE=/path/to/root-ca.pemThe wrapper below reads this file as shell code, so keep each password in single quotes. Write a single quote inside a password as '\'', or choose a password without one.
To run without the indexer, leave out all four WAZUH_INDEXER_* lines. The agent, rule, decoder, and version tools still work, and the alert and vulnerability tools return a configuration message instead of failing. If you keep WAZUH_INDEXER_URL, you also need WAZUH_INDEXER_USERNAME and WAZUH_INDEXER_PASSWORD, or the server stops at startup. WAZUH_INDEXER_CA_FILE stays optional. TLS verification explains which CA file to use.
Save this wrapper as ~/.local/bin/wazuh-mcp-env. It loads the file and then runs the command that follows it, so no password appears in client config:
mkdir -p ~/.local/bin
cat > ~/.local/bin/wazuh-mcp-env <<'EOF'
#!/bin/sh
# Load wazuh-mcp settings from a 0600 file, then run the given command.
set -eu
set -a
. "${WAZUH_MCP_ENV_FILE:-$HOME/.config/wazuh-mcp/env}"
set +a
exec "$@"
EOF
chmod 755 ~/.local/bin/wazuh-mcp-envCheck both logins, TLS, and the indexer role from a shell before you wire up a client:
~/.local/bin/wazuh-mcp-env npx -y --package wazuh-mcp wazuhctrl diagnosticsIt prints diagnostics status= and one line per check, and it exits 1 when a check reports an error. It does not check the API user's role, because logging in and reading the API version need no permission. wazuh-mcp 2.0.0 and earlier print nothing and exit 0 here because of a bin-symlink bug (see the CHANGELOG), so no output means the check did not run. In that case, wire up the client and ask it to run diagnose_wazuh_connection, which runs the same checks.
Then register the wrapper as the server command. Most MCP clients start the command without a shell, so use the full path to the wrapper instead of ~:
{
"mcpServers": {
"wazuh": {
"command": "/absolute/path/to/wazuh-mcp-env",
"args": ["npx", "-y", "wazuh-mcp"]
}
}
}Drop that into your MCP client's server config (see Usage for the exact file per client), restart the client, and ask it something like "list the active Wazuh agents" or "search alerts for brute force in the last 24 hours." If your client expands environment variables in its config, you can reference variables from the client's own environment instead of using the wrapper. On macOS and Linux, never paste a literal password into client config. The wrapper does not run on Windows, and Usage covers that case.
Prefer a global install?
npm install -g wazuh-mcpThen use "args": ["wazuh-mcp"] instead of the npx invocation above.
wazuh-mcp only reads, so neither account needs write or administrator rights. The permissions below were checked against Wazuh 4.14.8: the readonly API role covered every manager tool, and the indexer role covered the alert and vulnerability tools plus both index checks in diagnose_wazuh_connection. The commands run once as an administrator, need curl and jq, and read passwords with read -rs (bash or zsh) so they stay out of shell history. The administrator password passed to curl -u and the API token passed with -H can still appear briefly in the process list, so run the commands on a host where no other user can see your processes.
Keep WAZUH_VERIFY_SSL and WAZUH_INDEXER_VERIFY_SSL at their default of true. Instead of turning verification off, point WAZUH_CA_FILE and WAZUH_INDEXER_CA_FILE at the root CA that signed your Wazuh certificates. On the Docker deployment that file is wazuh-docker/single-node/config/wazuh_indexer_ssl_certs/root-ca.pem (multi-node/ for a cluster). The host in each URL must be a name or address listed in that service's certificate. The single-node Docker certificates are issued for wazuh.indexer and wazuh.manager, so either reach the services by those names, for example through /etc/hosts entries, or regenerate the certificates with your host names in config/certs.yml.
On a Linux host, Docker creates that directory as root, and the certificate generator sets it to mode 0500 and the files in it to 0400. Only root can read the files from the host, even if your account has user ID 1000, which owns most of them. Copy the CA to a file you own, and use the copy for both settings and for CA_FILE in the setup commands below:
mkdir -p -m 700 ~/.config/wazuh-mcp
sudo cat wazuh-docker/single-node/config/wazuh_indexer_ssl_certs/root-ca.pem > ~/.config/wazuh-mcp/root-ca.pemOn the Docker deployment the indexer certificate is already signed by that CA. The manager API certificate is not: unless you supply one, the API generates a self-signed certificate for localhost at startup, and that certificate fails verification against root-ca.pem. Replace it with a certificate signed by the root CA, saved as server.crt and server.key in the manager's /var/ossec/api/configuration/ssl/, and restart the manager. The same root-ca.pem then works for WAZUH_CA_FILE. The setup commands below verify TLS the same way, so do this step first.
On single-node Docker, the certificate generator already created a manager certificate signed by the root CA, wazuh.manager.pem and its key wazuh.manager-key.pem, and the manager container mounts them as /etc/ssl/filebeat.pem and /etc/ssl/filebeat.key. The API directory is on a Docker volume inside the container, so copy them there and restart the Wazuh daemons inside the container, from wazuh-docker/single-node:
docker compose exec wazuh.manager sh -c 'cp /etc/ssl/filebeat.pem /var/ossec/api/configuration/ssl/server.crt && cp /etc/ssl/filebeat.key /var/ossec/api/configuration/ssl/server.key'
docker compose exec wazuh.manager /var/ossec/bin/wazuh-control restartThe API makes the wazuh user the owner of both files when it starts, and the copies stay on the volume across restarts.
On a package install, the root CA is /etc/filebeat/certs/root-ca.pem on the server node, and filebeat.pem and filebeat-key.pem in that directory are a server certificate signed by it. That directory is readable only by root, so copy those files with sudo.
Wazuh's default certificates are issued for loopback names only. The indexer node certificate lists IP:127.0.0.1 as its only subject alternative name, and the manager API's default self-signed certificate lists only DNS:localhost. A client that reaches either service over the LAN, for example at https://192.0.2.10:9200, cannot verify the certificate, and the usual workaround is WAZUH_VERIFY_SSL=false or WAZUH_INDEXER_VERIFY_SSL=false, which turns off all verification.
Set WAZUH_TLS_SERVERNAME and WAZUH_INDEXER_TLS_SERVERNAME instead. The connection still goes to the host in the URL, and the certificate chain is still checked against the system roots or the file named by WAZUH_CA_FILE and WAZUH_INDEXER_CA_FILE. Only the identity check changes: the certificate must match the configured name rather than the URL host.
WAZUH_URL=https://192.0.2.10:55000
WAZUH_CA_FILE=/path/to/root-ca.pem
WAZUH_TLS_SERVERNAME=localhost
WAZUH_INDEXER_URL=https://192.0.2.10:9200
WAZUH_INDEXER_CA_FILE=/path/to/root-ca.pem
WAZUH_INDEXER_TLS_SERVERNAME=127.0.0.1When the name is a DNS name it is also sent as the TLS SNI value. An IP address is never sent as SNI, because Node rejects it there. The setting has no effect while verification is disabled; diagnose_wazuh_connection reports a warning if you set one with WAZUH_VERIFY_SSL=false or WAZUH_INDEXER_VERIFY_SSL=false. This trusts anyone who holds a certificate for the configured name that your CA signed, so use the narrowest CA file you can.
The built-in readonly role reads agents, groups, rules, decoders, SCA, FIM, rootcheck, syscollector inventory, and manager and cluster status, configuration, and logs. That covers every manager tool. It also reads CDB lists, CIS-CAT results, MITRE data, and the API configuration, which no tool uses, and it changes nothing. It has no security:* permissions, so it cannot read or change other users, roles, policies, or the security configuration. Like every API user, it can still read its own account and policies at /security/users/me and /security/users/me/policies. New API users start with allow_run_as disabled, which is what this server needs. Sign in once as an existing API administrator (wazuh or wazuh-wui) to create the user:
WAZUH_URL=https://your-wazuh-manager:55000
CA_FILE=/path/to/root-ca.pem
API_ADMIN=wazuh-wui
printf 'Password for %s: ' "$API_ADMIN"; read -rs API_ADMIN_PASSWORD; echo
printf 'Password for the new wazuh-mcp-api user: '; read -rs NEW_PASSWORD; echo
export NEW_PASSWORD
TOKEN=$(curl -sS --cacert "$CA_FILE" -u "$API_ADMIN:$API_ADMIN_PASSWORD" \
-X POST "$WAZUH_URL/security/user/authenticate?raw=true")
USER_ID=$(jq -n '{username: "wazuh-mcp-api", password: env.NEW_PASSWORD}' |
curl -sS --cacert "$CA_FILE" -X POST "$WAZUH_URL/security/users" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data-binary @- |
jq -r '.data.affected_items[0].id')
ROLE_ID=$(curl -sS --cacert "$CA_FILE" "$WAZUH_URL/security/roles" \
-H "Authorization: Bearer $TOKEN" |
jq -r '.data.affected_items[] | select(.name == "readonly") | .id')
echo "user id: $USER_ID, readonly role id: $ROLE_ID"
curl -sS --cacert "$CA_FILE" -X POST \
"$WAZUH_URL/security/users/$USER_ID/roles?role_ids=$ROLE_ID" \
-H "Authorization: Bearer $TOKEN"
unset API_ADMIN_PASSWORD NEW_PASSWORD TOKENBoth IDs must be numbers. If the user ID prints as null, run the POST /security/users request again without the final jq to see the API message. The usual causes are a password that breaks the rules (8 to 64 characters with an uppercase letter, a lowercase letter, a number, and a symbol), a wazuh-mcp-api user left over from an earlier run, or a wrong administrator password. The last request answers with All roles were linked to user wazuh-mcp-api.
The server sends GET / for cluster info, which needs the cluster permission cluster:monitor/main. Its HEAD index checks and _search queries go to wazuh-alerts-* and wazuh-states-vulnerabilities*. The vulnerability indices themselves are named wazuh-states-vulnerabilities- plus a suffix, so the role pattern below covers them. The role is read-only and limited to those two index families. Wazuh's documented read-only recipe (cluster_composite_ops_ro plus read on *) can search, but it gets HTTP 403 on both index checks in diagnose_wazuh_connection and can read every index. These commands use the indexer security REST API as the admin user:
INDEXER_URL=https://your-wazuh-indexer:9200
CA_FILE=/path/to/root-ca.pem
printf 'Password for the indexer admin user: '; read -rs INDEXER_ADMIN_PASSWORD; echo
printf 'Password for the new wazuh-mcp-indexer user: '; read -rs NEW_PASSWORD; echo
export NEW_PASSWORD
curl -sS --cacert "$CA_FILE" -u "admin:$INDEXER_ADMIN_PASSWORD" -X PUT \
"$INDEXER_URL/_plugins/_security/api/roles/wazuh_mcp_read" \
-H "Content-Type: application/json" -d '{
"cluster_permissions": ["cluster:monitor/main"],
"index_permissions": [{
"index_patterns": ["wazuh-alerts-*", "wazuh-states-vulnerabilities-*"],
"allowed_actions": ["read", "indices:admin/get"]
}]
}'
jq -n '{password: env.NEW_PASSWORD}' |
curl -sS --cacert "$CA_FILE" -u "admin:$INDEXER_ADMIN_PASSWORD" -X PUT \
"$INDEXER_URL/_plugins/_security/api/internalusers/wazuh-mcp-indexer" \
-H "Content-Type: application/json" --data-binary @-
curl -sS --cacert "$CA_FILE" -u "admin:$INDEXER_ADMIN_PASSWORD" -X PUT \
"$INDEXER_URL/_plugins/_security/api/rolesmapping/wazuh_mcp_read" \
-H "Content-Type: application/json" -d '{"users": ["wazuh-mcp-indexer"]}'
unset INDEXER_ADMIN_PASSWORD NEW_PASSWORDEach request answers with "status":"CREATED" the first time and "status":"OK" on a rerun. The indexer rejects a weak password, or one too close to the user name, with "status":"error", so check that the internalusers request printed CREATED or OK before you continue. To do the same in the Wazuh dashboard, open Indexer management > Security, create the user under Internal users, create a role with the permissions above under Roles, and add the user on that role's Mapped users tab.
The wazuh_mcp_read role only reads, but it is not the only role the account gets. Default Wazuh indexer installs map every internal user to the built-in own_index role (users: ["*"] in roles_mapping.yml). That role allows all index actions on an index named after the user, so wazuh-mcp-indexer can create, write to, and delete an index called wazuh-mcp-indexer. To list the roles the account holds, run this request. curl prompts for the password, and the roles list shows own_index and wazuh_mcp_read:
curl -sS --cacert "$CA_FILE" -u wazuh-mcp-indexer "$INDEXER_URL/_plugins/_security/authinfo"Removing "*" from the own_index mapping takes that role away from every internal user, not only this one.
Running securityadmin.sh -cd, which Wazuh's Docker password-change steps do, reloads the security configuration from the YAML files and deletes users, roles, and mappings created through the REST API. After such a run, send the three requests above again. Keeping them in the YAML files instead needs more than an edit. On Docker only config/wazuh_indexer/internal_users.yml is mounted from the host, so roles.yml and roles_mapping.yml need bind mounts of their own, or edits to them are lost when the container is recreated. The internal_users.yml entry also takes a hash: value from plugins/opensearch-security/tools/hash.sh, not the password.
With both accounts created, go back to Quickstart and write the env file.
The package ships wazuhctrl for shells, cron, and CI. Compatibility alias wazuhctl points at the same binary, and wazuh-mcp remains the MCP stdio adapter.
wazuhctrl status --json
wazuhctrl agents list --limit 20
wazuhctrl diagnostics
wazuhctrl diagnostics --no-connectivity
wazuhctrl mcpwazuhctrl reads the same environment as the MCP adapter: WAZUH_URL, WAZUH_USERNAME, WAZUH_PASSWORD, optional WAZUH_INDEXER_URL, and the indexer credentials that become required once it is set. The Quickstart wrapper works for it too, for example ~/.local/bin/wazuh-mcp-env wazuhctrl diagnostics after a global install. Agent IP addresses stay hidden unless a command explicitly requests them.
The quickstart mcpServers block at the top works for most clients. The per-client recipes below give you the exact file location or CLI command for each. On macOS and Linux, all of them start the server through the wazuh-mcp-env wrapper from the Quickstart, so credentials stay in the 0600 env file. Replace /absolute/path/to/wazuh-mcp-env with the wrapper's full path.
The wrapper is a POSIX shell script and does not run on Windows. There, start npx -y wazuh-mcp directly and pass the WAZUH_* settings through the client: an "env" object on the server entry in a JSON config (see Claude Desktop), or one --env KEY=value option per setting after the server name and before the -- in claude mcp add and codex mcp add. The settings, passwords included, then sit in the client's config file, so keep that file readable only by your account.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"wazuh": {
"command": "/absolute/path/to/wazuh-mcp-env",
"args": ["npx", "-y", "wazuh-mcp"]
}
}
}On Windows, set "command": "npx" and "args": ["-y", "wazuh-mcp"], put the WAZUH_* settings in an "env" object on the same entry, and keep claude_desktop_config.json readable only by your account. Setting them in your Windows user environment does not work, because Claude Desktop passes a stdio server only a short list of system variables such as PATH and APPDATA.
claude mcp add wazuh -- /absolute/path/to/wazuh-mcp-env npx -y wazuh-mcpAdd --scope user to make it available from any directory instead of only the current project.
Codex CLI registers MCP servers via codex mcp add:
codex mcp add wazuh -- /absolute/path/to/wazuh-mcp-env npx -y wazuh-mcpCodex writes the entry to ~/.codex/config.toml under [mcp_servers.wazuh]. Verify with codex mcp list.
With the npm package:
openclaw mcp set wazuh '{
"command": "/absolute/path/to/wazuh-mcp-env",
"args": ["npx", "-y", "wazuh-mcp"]
}'Or, when running from a source checkout, point args at the built dist/mcp-bin.js:
openclaw mcp set wazuh '{
"command": "/absolute/path/to/wazuh-mcp-env",
"args": ["node", "/absolute/path/to/wazuh-mcp/dist/mcp-bin.js"]
}'Then restart the gateway so the new server is picked up:
systemctl --user restart openclaw-gateway
openclaw mcp list # confirm "wazuh" is registeredHermes Agent reads MCP config from ~/.hermes/config.yaml under the mcp_servers key. Add an entry:
mcp_servers:
wazuh:
command: "/absolute/path/to/wazuh-mcp-env"
args: ["npx", "-y", "wazuh-mcp"]Then reload MCP from inside a Hermes session with /reload-mcp.
~/.local/bin/wazuh-mcp-env npx -y wazuh-mcpnpm run dev # Watch mode with tsx
npm run lint # Type checking
npm test # Run testsAll 28 tools are read-only, and each one declares the MCP tool annotations readOnlyHint: true, idempotentHint: true, and openWorldHint: true. The tools query only your own Wazuh deployment, but most of what they return originates on monitored endpoints (see Untrusted SIEM Content), so clients should treat the output as untrusted.
| Tool | Description |
|---|---|
list_agents |
List all agents with optional status filtering (active, disconnected, never_connected, pending) |
get_agent |
Get detailed info for a specific agent by ID |
get_agent_stats |
Get CPU, memory, and disk statistics for an agent |
| Tool | Description |
|---|---|
get_alerts |
Retrieve recent alerts with filtering by time range, level, agent, rule, and text search |
get_alert |
Retrieve a single alert by ID |
search_alerts |
Full-text search across alerts with optional time range filtering |
| Tool | Description |
|---|---|
list_vulnerabilities |
List vulnerability inventory with optional CVE, agent, severity, and package filters |
search_vulnerabilities |
Search vulnerability inventory by CVE, package, agent, or description |
| Tool | Description |
|---|---|
list_rules |
List detection rules with level and group filtering |
get_rule |
Get full rule details including compliance mappings |
search_rules |
Search rules by description text |
| Tool | Description |
|---|---|
get_sca_policies |
List SCA policies and scores for an agent (CIS benchmarks, etc.) |
get_sca_checks |
Get individual check results with remediation steps and compliance mappings |
| Tool | Description |
|---|---|
get_agent_os |
Get OS information (name, version, architecture, hostname) |
get_agent_packages |
List installed software packages with versions |
get_agent_processes |
List running processes with PIDs and command lines |
get_agent_ports |
List open network ports with associated processes |
get_agent_network |
List network interfaces and IP addresses |
get_agent_hotfixes |
List installed Windows hotfixes/patches |
| Tool | Description |
|---|---|
get_fim_files |
Get File Integrity Monitoring results (files, registry keys, hashes) |
get_rootcheck |
Get rootkit detection scan findings |
| Tool | Description |
|---|---|
get_manager_logs |
Get Wazuh manager logs filtered by level and module |
get_manager_config |
Get active manager configuration by section with secret-like values redacted by default |
| Tool | Description |
|---|---|
list_groups |
List all agent groups |
get_group_agents |
List agents in a specific group |
| Tool | Description |
|---|---|
list_decoders |
List log decoders with optional name filtering |
get_wazuh_version |
Get Wazuh manager version and API info |
diagnose_wazuh_connection |
Check sanitized configuration, URL/TLS settings, manager auth/version, and indexer readiness |
Set the following environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
WAZUH_URL |
Yes | - | Wazuh API URL (e.g., https://192.0.2.2:55000). Must be https:// (or http:// with WAZUH_ALLOW_INSECURE_HTTP=true), and must not contain embedded credentials, a query string, or a fragment. A path prefix for a reverse proxy is allowed. |
WAZUH_USERNAME |
Yes | - | API username. Use a dedicated user with the built-in readonly role (see Least-privilege setup). |
WAZUH_PASSWORD |
Yes | - | API password |
WAZUH_VERIFY_SSL |
No | true |
Verifies SSL certificates by default. Set to false (also accepts 0/no/off) to disable verification for trusted self-signed lab environments only. |
WAZUH_CA_FILE |
No | - | Path to a PEM CA bundle used to verify the manager's TLS certificate (private CA or self-signed). Read at startup. The server exits with an error if the file cannot be read. Prefer this over WAZUH_VERIFY_SSL=false. See TLS verification for the Docker deployment's root CA and the manager's default self-signed certificate. |
WAZUH_TLS_SERVERNAME |
No | - | Name to check the manager certificate against instead of the host in WAZUH_URL: an IPv4 address, IPv6 address, or DNS name, with no scheme, port, or path (253 characters at most). The certificate chain is still verified. Ignored when WAZUH_VERIFY_SSL is false, and diagnose_wazuh_connection reports that. See TLS verification. |
WAZUH_ALLOW_INSECURE_HTTP |
No | false |
Allow plain http:// for WAZUH_URL and WAZUH_INDEXER_URL. When unset, http:// URLs are rejected at startup. When enabled and in use, the server prints a startup warning to stderr. Trusted lab networks only. |
WAZUH_TIMEOUT |
No | 30 |
Request timeout in seconds. Must be a positive integer. |
WAZUH_MCP_MAX_RESPONSE_BYTES |
No | 90000 |
Maximum MCP tool response size before returning a truncated preview with metadata. Values below 1024 are raised to 1024. The truncated envelope itself always fits within the cap. The default stays under 100,000 bytes so clients that refuse larger direct results still get the truncated preview. |
WAZUH_MCP_MAX_STDIO_BUFFER_BYTES |
No | 8388608 (8 MiB) |
Maximum stdio read-buffer size in bytes. Must be a positive integer. A single client message exceeding it makes the transport error and close. |
Alternative variable names WAZUH_BASE_URL and WAZUH_USER are also supported.
Wazuh 4.x stores alerts and vulnerability inventory in the Wazuh Indexer (OpenSearch), not the REST API. To enable alert tools (get_alerts, get_alert, search_alerts), vulnerability tools (list_vulnerabilities, search_vulnerabilities), and the wazuh://alerts/recent resource, configure the indexer connection:
| Variable | Required | Default | Description |
|---|---|---|---|
WAZUH_INDEXER_URL |
No | - | Wazuh Indexer URL (e.g., https://192.0.2.2:9200). Same URL rules as WAZUH_URL. |
WAZUH_INDEXER_USERNAME |
Yes, when WAZUH_INDEXER_URL is set |
- | Indexer username. Use a dedicated indexer user with a read-only role (see Least-privilege setup, including the own_index note). The server fails fast at startup if WAZUH_INDEXER_URL is set without it. There is no admin default (2.0.0 and earlier fell back to admin). |
WAZUH_INDEXER_PASSWORD |
Yes, when WAZUH_INDEXER_URL is set |
- | Indexer password. The server fails fast at startup if WAZUH_INDEXER_URL is set without it. |
WAZUH_INDEXER_VERIFY_SSL |
No | true |
Verifies SSL certificates by default. Set to false (also accepts 0/no/off) to disable verification for trusted self-signed lab environments only. |
WAZUH_INDEXER_CA_FILE |
No | - | Path to a PEM CA bundle used to verify the indexer's TLS certificate, for example the deployment's root-ca.pem. Read at startup. The server exits with an error if the file cannot be read. |
WAZUH_INDEXER_TLS_SERVERNAME |
No | - | Name to check the indexer certificate against instead of the host in WAZUH_INDEXER_URL. Same format and rules as WAZUH_TLS_SERVERNAME. Ignored when WAZUH_INDEXER_VERIFY_SSL is false. |
WAZUH_INDEXER_TIMEOUT |
No | 30 |
Indexer request timeout in seconds. Must be a positive integer. |
If WAZUH_INDEXER_URL is not set, alert and vulnerability tools will return a helpful configuration message. All other tools (agents, rules, decoders, version) work without the indexer.
Indexer searches count at most 10000 matching hits and carry a 30-second server-side timeout. When the real match count is higher, pagination.total_is_lower_bound is true and total is 10000. Alert and vulnerability tools accept offset up to 9999 and reject requests where offset + limit exceeds 10000 (OpenSearch's default max_result_window); narrow the query with filters or a time range instead of paging deeper.
SSL certificate verification is enabled by default (secure by default). When either SSL verification setting is explicitly set to false, the server prints a startup warning to stderr. TLS verification is disabled only for that configured Wazuh client.
Several tools return minimized output by default to avoid exposing raw logs, IPs, command lines, hashes, or raw event payloads unless requested:
| Tool | Hidden by default | Opt-in field |
|---|---|---|
list_agents, get_agent, get_group_agents |
Agent IP details | include_ip: true |
get_alerts, search_alerts |
full_log |
include_full_log: true |
get_alert |
full_log, raw data |
include_full_log: true, include_raw_data: true |
list_vulnerabilities, search_vulnerabilities |
Vulnerability descriptions | include_description: true |
get_agent_processes |
Process command lines and arguments | include_command: true |
get_fim_files |
MD5 and SHA-256 hashes | include_hashes: true |
get_manager_logs |
Full log descriptions | include_description: true |
get_manager_config |
Secret-like config values | None. Credential-bearing keys are always redacted. |
Alert, log, and inventory fields originate on monitored endpoints: anyone who can write a log line to a monitored host (a failed SSH login with a crafted username, a web request path, a syslog message) or control a process, package, file, or hostname on it controls the text that lands in full_log, alert rule_description, agent_name, location, and decoder, raw event data, rule and decoder text, manager log descriptions, agent names and OS fields, syscollector process/package/port/interface/hotfix fields, FIM paths and owners, rootcheck log text, SCA policy and check text, and vulnerability package and description fields. To blunt prompt injection against the calling agent, the server wraps those values in <untrusted_siem_data>...</untrusted_siem_data> markers, includes an output.untrusted_data_note warning in affected responses, and states in the tool descriptions that the content is attacker-influenced data, never instructions to follow.
Tool inputs are validated before requests are sent to Wazuh. Pagination is bounded, search text is length-limited, sort fields are enumerated per tool, and path-oriented identifiers such as agent IDs, alert IDs, group IDs, and SCA policy IDs reject unsupported characters.
Paginated tool responses include a pagination object with total, limit, offset, and has_more fields while preserving the existing top-level total, limit, and offset fields.
Tool responses are capped by WAZUH_MCP_MAX_RESPONSE_BYTES. Oversized responses return valid JSON with output.response_truncated, byte counts, and a preview instead of flooding the MCP client.
Transient manager GET requests and indexer search/readiness requests retry briefly on 429, 502, 503, 504, and common transient network reset or timeout errors.
- 28 MCP Tools - Agents, alerts, vulnerabilities, rules, decoders, SCA, syscollector, FIM, rootcheck, groups, manager, and diagnostics
- 3 MCP Resources - Pre-built views for agents, recent alerts, and rule summaries
- 3 MCP Prompts - Alert investigation, agent health checks, and security overviews
- Read-only by design - The only writes are JWT auth and indexer
_search; no tool changes Wazuh state - Secure by default - TLS verification on, sensitive fields redacted unless opted in, untrusted SIEM content delimited, every error sanitized before it reaches the client
- JWT Authentication - Automatic token management with refresh on expiry
- Full Compliance Mapping - PCI-DSS, GDPR, HIPAA, NIST 800-53, MITRE ATT&CK
- Pagination - All list endpoints support limit/offset pagination
- Type-Safe - Full TypeScript with strict mode and Zod schema validation
- Node.js 22+
- A running Wazuh manager with API access (default port 55000)
- Wazuh API credentials for a user with the built-in
readonlyrole - (Optional) A Wazuh Indexer (OpenSearch) user with a read-only role for alert and vulnerability queries
| Resource URI | Description |
|---|---|
wazuh://agents |
All registered agents and their status |
wazuh://alerts/recent |
25 most recent security alerts |
wazuh://rules/summary |
Detection rules sorted by severity |
Use list_agents with status "active" to see all connected agents.
Search alerts for "brute force" and investigate the top result,
including the MITRE ATT&CK technique and remediation steps.
Run an agent health check on agent 001 - check its connection status,
resource usage, and any recent critical alerts.
List all rules with level 12 or higher to see critical detection rules
and their compliance framework mappings.
- The Wazuh dashboard is built for humans clicking through Kibana-style views. It is great for a SOC analyst at a screen, but an AI agent cannot drive it, and it does not turn natural-language questions into the right manager and indexer queries. wazuh-mcp gives the model typed tools instead.
- The raw Wazuh REST API + indexer
_searchcan be called directly, but then every agent has to learn JWT auth, the manager-versus-indexer split (alerts and vulnerabilities live in the indexer in Wazuh 4.x), pagination shapes, and which fields are sensitive. wazuh-mcp wraps all of that, validates inputs, caps response size, and sanitizes errors so credentials never leak back to the model. - A general "run any HTTP request" tool would technically reach Wazuh, but it hands the model your credentials, no input validation, no read-only guarantee, and no redaction of IPs, hashes, or full logs. This server is deliberately read-only and minimizes sensitive output by default.
- Writing your own Wazuh MCP shim is reasonable, and the source here is MIT-licensed if you want to fork it. This one already handles auth refresh, the indexer fallback message, untrusted-content delimiting, transient-error retries, and 28 vetted tools.
- Not a write path. No tool modifies Wazuh state. It cannot restart agents, edit rules, acknowledge alerts, or change configuration. The only writes are JWT authentication and indexer
_searchqueries. - Not a replacement for the Wazuh dashboard or SIEM. It is a query surface for AI clients, not an analyst UI, a data store, or an alerting engine.
- Not a hosted service. It runs locally as a stdio MCP server next to your client. Your Wazuh credentials stay on your machine, in the wrapper's env file or your client's config.
- Not a guarantee against prompt injection. It delimits attacker-influenced SIEM content and warns the model, which reduces risk but does not eliminate it. Treat tool output as data, not instructions.
- Not a way to bypass Wazuh access control. It uses the credentials you give it and can see only what that account can see.
- Website: lidless.dev/wazuh-mcp
- npm:
wazuh-mcp - Issues: github.com/lidless-labs/wazuh-mcp/issues
- Changelog: CHANGELOG.md
- Security policy: SECURITY.md
- Contributing: CONTRIBUTING.md
npm test # Run all tests
npm run typecheck # Type-check TypeScript
npm audit --omit=dev # Audit production dependencies
npm run pack:check # Verify package contents
npm run test:watch # Watch modeTests use mocked Wazuh API responses - no live Wazuh instance needed.
wazuh-mcp/
├── src/
│ ├── mcp-bin.ts # MCP server entry point
│ ├── cli.ts # wazuhctrl command entry point
│ ├── mcp-server.ts # shared MCP server factory
│ ├── config.ts # Environment configuration
│ ├── client.ts # Wazuh REST API client (JWT auth)
│ ├── indexer-client.ts # Wazuh Indexer (OpenSearch) client
│ ├── types.ts # TypeScript type definitions
│ ├── resources.ts # MCP resource handlers
│ ├── prompts.ts # MCP prompt templates
│ └── tools/
│ ├── agents.ts # Agent management tools
│ ├── alerts.ts # Alert query tools
│ ├── rules.ts # Rule query tools
│ ├── decoders.ts # Decoder listing tool
│ ├── version.ts # Version info tool
│ ├── sca.ts # Security Configuration Assessment
│ ├── syscollector.ts # System inventory (OS, packages, ports, etc.)
│ ├── syscheck.ts # File Integrity Monitoring
│ ├── rootcheck.ts # Rootkit detection
│ ├── manager.ts # Manager logs and configuration
│ └── groups.ts # Agent group management
├── tests/
│ ├── client.test.ts # API client unit tests
│ └── tools.test.ts # Tool handler unit tests
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
MIT. See LICENSE.
Part of Lidless Labs · the eye does not close
Security / SOC: soc-stack · misp-mcp · suricata-mcp · thehive-mcp · cortex-mcp · mitre-mcp · zeek-mcp · hotwash

