Problem
The current server is wired for one Wazuh manager + one indexer. Multi-cluster
deployments need one MCP process per cluster, and there's no way for the LLM
to target a specific cluster (or all of them) per tool call.
CCS topology reference (Wazuh blog):
https://wazuh.com/blog/managing-multiple-wazuh-clusters-with-cross-cluster-search/
What I built
A working solution that fronts N Wazuh managers + 1 centralized CCS
indexer from a single MCP server:
- Per-cluster
WazuhClient for Manager API calls (CCS doesn't federate it).
- One shared
WazuhIndexerClient pointed at the CCS box; every read query is
scoped to a remote-cluster alias (<alias>:wazuh-alerts-*, or
*:wazuh-alerts-* to fan out across all clusters).
- Every tool gets a
cluster_id argument. Required for manager-read and
write tools; optional (defaults to *) for indexer-read tools.
- New
list_wazuh_clusters MCP tool for runtime discovery.
- New
clusters.json registry file (gitignored, mounted read-only) defining
each cluster's id, alias, manager host/port/credentials, and per-cluster
verify_ssl.
/health reports per-cluster + CCS reachability.
Heads-up: my implementation is CCS-only — it removes the single-cluster
code path and requires clusters.json to start. Dropping it in as-is would
break existing single-cluster deployments. If this is something you'd like to
ship, the right shape is probably to keep the single-cluster fallback when
clusters.json is absent.
Files changed
| File |
Change |
src/wazuh_mcp_server/config.py |
Replaced WazuhConfig with WazuhManagerConfig + validating load_clusters_from_file(). |
src/wazuh_mcp_server/security.py |
Added CLUSTER_ID_PATTERN + validate_cluster_id(). |
src/wazuh_mcp_server/api/wazuh_indexer.py |
Every read method takes cluster_alias; new _scoped() prefixes indices; absorbed 6 analytics methods from WazuhClient. |
src/wazuh_mcp_server/api/wazuh_client.py |
Narrowed to per-cluster Manager-API only; takes injected shared indexer client; hybrid methods pass self.cluster_alias. |
src/wazuh_mcp_server/server.py |
Builds manager_clients dict + shared wazuh_indexer_client; new routing helper resolve_cluster_for_tool(); injects cluster_id into every tool's inputSchema; new list_wazuh_clusters tool; lifespan does parallel init/shutdown; /health rewritten to per-cluster + CCS shape. |
compose.yml / compose.dev.yml |
Mount ./clusters.json:/app/config/clusters.json:ro. |
.dockerignore / .gitignore |
Exclude clusters.json, allowlist clusters.example.json. |
.env.example |
Comment out legacy WAZUH_HOST/USER/PASS/PORT block; relabel WAZUH_INDEXER_* as the CCS indexer; add WAZUH_CLUSTERS_FILE. |
clusters.example.json |
New, committed template for the registry. |
Ask
Is this something you'd be interested in upstreaming? If so, happy to work
with you on a version that keeps backwards compatibility with the existing
single-cluster setup. Reply on this issue and I can share code, walk through
specific pieces, or sketch the fallback path in more detail.
Problem
The current server is wired for one Wazuh manager + one indexer. Multi-cluster
deployments need one MCP process per cluster, and there's no way for the LLM
to target a specific cluster (or all of them) per tool call.
CCS topology reference (Wazuh blog):
https://wazuh.com/blog/managing-multiple-wazuh-clusters-with-cross-cluster-search/
What I built
A working solution that fronts N Wazuh managers + 1 centralized CCS
indexer from a single MCP server:
WazuhClientfor Manager API calls (CCS doesn't federate it).WazuhIndexerClientpointed at the CCS box; every read query isscoped to a remote-cluster alias (
<alias>:wazuh-alerts-*, or*:wazuh-alerts-*to fan out across all clusters).cluster_idargument. Required for manager-read andwrite tools; optional (defaults to
*) for indexer-read tools.list_wazuh_clustersMCP tool for runtime discovery.clusters.jsonregistry file (gitignored, mounted read-only) definingeach cluster's
id,alias, manager host/port/credentials, and per-clusterverify_ssl./healthreports per-cluster + CCS reachability.Heads-up: my implementation is CCS-only — it removes the single-cluster
code path and requires
clusters.jsonto start. Dropping it in as-is wouldbreak existing single-cluster deployments. If this is something you'd like to
ship, the right shape is probably to keep the single-cluster fallback when
clusters.jsonis absent.Files changed
src/wazuh_mcp_server/config.pyWazuhConfigwithWazuhManagerConfig+ validatingload_clusters_from_file().src/wazuh_mcp_server/security.pyCLUSTER_ID_PATTERN+validate_cluster_id().src/wazuh_mcp_server/api/wazuh_indexer.pycluster_alias; new_scoped()prefixes indices; absorbed 6 analytics methods fromWazuhClient.src/wazuh_mcp_server/api/wazuh_client.pyself.cluster_alias.src/wazuh_mcp_server/server.pymanager_clientsdict + sharedwazuh_indexer_client; new routing helperresolve_cluster_for_tool(); injectscluster_idinto every tool'sinputSchema; newlist_wazuh_clusterstool; lifespan does parallel init/shutdown;/healthrewritten to per-cluster + CCS shape.compose.yml/compose.dev.yml./clusters.json:/app/config/clusters.json:ro..dockerignore/.gitignoreclusters.json, allowlistclusters.example.json..env.exampleWAZUH_HOST/USER/PASS/PORTblock; relabelWAZUH_INDEXER_*as the CCS indexer; addWAZUH_CLUSTERS_FILE.clusters.example.jsonAsk
Is this something you'd be interested in upstreaming? If so, happy to work
with you on a version that keeps backwards compatibility with the existing
single-cluster setup. Reply on this issue and I can share code, walk through
specific pieces, or sketch the fallback path in more detail.