Skip to content

Multi-cluster support via OpenSearch Cross-Cluster Search (CCS) #77

Description

@TimurSubasic

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions