//
// Purpose:
//
// This Rust application implements an MCP (Model Context Protocol) server that acts as a
// bridge to a Wazuh instance. It exposes various Wazuh functionalities as tools that can
// be invoked by MCP clients (e.g., AI models, automation scripts).
//
// Architecture:
// The application follows a modular design where the main MCP server delegates to
// domain-specific tool modules, promoting separation of concerns and maintainability.
//
// Structure:
// - `main()`: Entry point of the application. Initializes logging (tracing),
// sets up the `WazuhToolsServer`, and starts the MCP server using stdio transport.
//
// - `WazuhToolsServer`: The core orchestrator struct that implements the `rmcp::ServerHandler` trait
// and the `#[tool(tool_box)]` attribute. It acts as a facade that delegates tool calls to
// specialized domain modules:
// - Holds instances of domain-specific tool modules (AgentTools, AlertTools, RuleTools, etc.)
// - Its methods, decorated with `#[tool(...)]`, define the MCP tool interface and delegate
// to the appropriate domain module for actual implementation
// - Manages the lifecycle and configuration of Wazuh client connections
//
// - Domain-Specific Tool Modules (in `tools/` package):
// - `AlertTools` (`tools/alerts.rs`): Handles alert-related operations via Wazuh Indexer
// - `RuleTools` (`tools/rules.rs`): Manages security rule queries via Wazuh Manager API
// - `VulnerabilityTools` (`tools/vulnerabilities.rs`): Processes vulnerability data via Wazuh Manager API
// - `AgentTools` (`tools/agents.rs`): Handles agent management and system information queries
// - `StatsTools` (`tools/stats.rs`): Provides logging, statistics, and cluster health monitoring
// Each module encapsulates:
// - Domain-specific business logic and data formatting
// - Parameter validation and error handling
// - Client interaction patterns for their respective Wazuh APIs
// - Rich output formatting with structured text and emojis
//
// - Tool Parameter Structs (e.g., `GetAlertSummaryParams`):
// - These structs define the expected input parameters for each tool.
// - They use `serde::Deserialize` for parsing input and `schemars::JsonSchema`
// for generating a schema that MCP clients can use to understand how to call the tools.
// - Located within their respective domain modules for better organization
//
// - `wazuh_client` crate:
// - This external crate is used to interact with both the Wazuh Manager API and the Wazuh Indexer API.
// - `WazuhClientFactory` is used to create specific clients (e.g., `WazuhIndexerClient`, `RulesClient`, `AgentsClient`, `LogsClient`, `ClusterClient`, `VulnerabilityClient`).
// - Clients are wrapped in `Arc<Mutex<>>` for thread-safe access across async operations
//
// Workflow:
// 1. Server starts and listens for MCP requests on stdio
// 2. MCP client sends a `call_tool` request to `WazuhToolsServer`
// 3. `WazuhToolsServer` routes the request to the appropriate domain-specific tool module
// 4. The domain module validates parameters, interacts with the relevant Wazuh client, and formats results
// 5. The result (success with formatted data or error) is packaged into a `CallToolResult`
// and sent back to the MCP client via the main server
//
// Exposed Tools:
// The server exposes a set of tools categorized by the Wazuh component they interact with:
//
// Alert Management (via AlertTools):
// - `get_wazuh_alert_summary`: Retrieves a summary of security alerts from the Wazuh Indexer.
//
// Rule Management (via RuleTools):
// - `get_wazuh_rules_summary`: Fetches security rules defined in the Wazuh Manager.
//
// Vulnerability Management (via VulnerabilityTools):
// - `get_wazuh_vulnerability_summary`: Gets vulnerability scan results for a specific agent from the Wazuh Manager.
// - `get_wazuh_critical_vulnerabilities`: Retrieves critical vulnerabilities for an agent.
//
// Agent Management (via AgentTools):
// - `get_wazuh_agents`: Lists active and inactive agents connected to the Wazuh Manager.
// - `get_wazuh_agent_processes`: Retrieves running processes on a specific agent (via Syscollector).
// - `get_wazuh_agent_ports`: Lists open network ports on a specific agent (via Syscollector).
//
// Statistics and Monitoring (via StatsTools):
// - `search_wazuh_manager_logs`: Searches logs generated by the Wazuh Manager.
// - `get_wazuh_manager_error_logs`: Retrieves error-specific logs from the Wazuh Manager.
// - `get_wazuh_log_collector_stats`: Gets log collection statistics for an agent.
// - `get_wazuh_remoted_stats`: Fetches statistics from the Wazuh Manager's remoted daemon.
// - `get_wazuh_weekly_stats`: Retrieves aggregated weekly statistics from the Wazuh Manager.
// - `get_wazuh_cluster_health`: Checks the health status of the Wazuh Manager cluster.
// - `get_wazuh_cluster_nodes`: Lists nodes participating in the Wazuh Manager cluster.
//
// (Detailed parameters and descriptions for each tool are available via the MCP `get_tools` command or in the server's `get_info` response.)
//
// Configuration:
// The server requires the following environment variables to connect to the Wazuh instance:
// - `WAZUH_API_HOST`: Hostname or IP address of the Wazuh API.
// - `WAZUH_API_PORT`: Port number for the Wazuh API (default: 55000).
// - `WAZUH_API_USERNAME`: Username for Wazuh API authentication.
// - `WAZUH_API_PASSWORD`: Password for Wazuh API authentication.
// - `WAZUH_INDEXER_HOST`: Hostname or IP address of the Wazuh Indexer.
// - `WAZUH_INDEXER_PORT`: Port number for the Wazuh Indexer API (default: 9200).
// - `WAZUH_INDEXER_USERNAME`: Username for Wazuh Indexer authentication.
// - `WAZUH_INDEXER_PASSWORD`: Password for Wazuh Indexer authentication.
// - `WAZUH_VERIFY_SSL`: Set to "true" to enable SSL certificate verification, "false" otherwise (default: false).
// - `WAZUH_TEST_PROTOCOL`: (Optional) Protocol to use for Wazuh API/Indexer connections, e.g., "http" or "https" (default: "https").
// Logging behavior is controlled by the `RUST_LOG` environment variable (e.g., `RUST_LOG=info,mcp_server_wazuh=debug`).
use clap::Parser;
use dotenv::dotenv;
use rmcp::{
model::{CallToolResult, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo},
tool,
transport::stdio,
Error as McpError, ServerHandler, ServiceExt,
};
use std::env;
use std::sync::Arc;
use wazuh_client::WazuhClientFactory;
mod tools;
use tools::agents::{AgentTools, GetAgentPortsParams, GetAgentProcessesParams, GetAgentsParams};
use tools::alerts::{AlertTools, GetAlertSummaryParams};
use tools::rules::{GetRulesSummaryParams, RuleTools};
use tools::stats::{
GetClusterHealthParams, GetClusterNodesParams, GetLogCollectorStatsParams,
GetManagerErrorLogsParams, GetRemotedStatsParams, GetWeeklyStatsParams,
SearchManagerLogsParams, StatsTools,
};
use tools::vulnerabilities::{
GetCriticalVulnerabilitiesParams, GetVulnerabilitiesSummaryParams, VulnerabilityTools,
};
#[derive(Parser, Debug)]
#[command(name = "mcp-server-wazuh")]
#[command(about = "Wazuh SIEM MCP Server")]
struct Args {
// Currently only stdio transport is supported
// Future versions may add HTTP-SSE transport
}
#[derive(Clone)]
struct WazuhToolsServer {
agent_tools: AgentTools,
alert_tools: AlertTools,
rule_tools: RuleTools,
stats_tools: StatsTools,
vulnerability_tools: VulnerabilityTools,
}
#[tool(tool_box)]
impl WazuhToolsServer {
fn new() -> Result<Self, anyhow::Error> {
dotenv().ok();
let api_host = env::var("WAZUH_API_HOST").unwrap_or_else(|_| "localhost".to_string());
let api_port: u16 = env::var("WAZUH_API_PORT")
.unwrap_or_else(|_| "55000".to_string())
.parse()
.unwrap_or(55000);
let api_username = env::var("WAZUH_API_USERNAME").unwrap_or_else(|_| "wazuh".to_string());
let api_password = env::var("WAZUH_API_PASSWORD").unwrap_or_else(|_| "wazuh".to_string());
let indexer_host =
env::var("WAZUH_INDEXER_HOST").unwrap_or_else(|_| "localhost".to_string());
let indexer_port: u16 = env::var("WAZUH_INDEXER_PORT")
.unwrap_or_else(|_| "9200".to_string())
.parse()
.unwrap_or(9200);
let indexer_username =
env::var("WAZUH_INDEXER_USERNAME").unwrap_or_else(|_| "admin".to_string());
let indexer_password =
env::var("WAZUH_INDEXER_PASSWORD").unwrap_or_else(|_| "admin".to_string());
let verify_ssl = env::var("WAZUH_VERIFY_SSL")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false);
let test_protocol = env::var("WAZUH_TEST_PROTOCOL")
.ok()
.or_else(|| Some("https".to_string()));
let mut builder = WazuhClientFactory::builder()
.api_host(api_host)
.api_port(api_port)
.api_credentials(&api_username, &api_password)
.indexer_host(indexer_host)
.indexer_port(indexer_port)
.indexer_credentials(&indexer_username, &indexer_password)
.verify_ssl(verify_ssl);
if let Some(protocol) = test_protocol {
builder = builder.protocol(protocol);
}
let wazuh_factory = builder.build();
let wazuh_indexer_client = wazuh_factory.create_indexer_client();
let wazuh_rules_client = wazuh_factory.create_rules_client();
let wazuh_vulnerability_client = wazuh_factory.create_vulnerability_client();
let wazuh_agents_client = wazuh_factory.create_agents_client();
let wazuh_logs_client = wazuh_factory.create_logs_client();
let wazuh_cluster_client = wazuh_factory.create_cluster_client();
let indexer_client_arc = Arc::new(wazuh_indexer_client);
let rules_client_arc = Arc::new(tokio::sync::Mutex::new(wazuh_rules_client));
let vulnerability_client_arc =
Arc::new(tokio::sync::Mutex::new(wazuh_vulnerability_client));
let agents_client_arc = Arc::new(tokio::sync::Mutex::new(wazuh_agents_client));
let logs_client_arc = Arc::new(tokio::sync::Mutex::new(wazuh_logs_client));
let cluster_client_arc = Arc::new(tokio::sync::Mutex::new(wazuh_cluster_client));
let agent_tools =
AgentTools::new(agents_client_arc.clone(), vulnerability_client_arc.clone());
let alert_tools = AlertTools::new(indexer_client_arc.clone());
let rule_tools = RuleTools::new(rules_client_arc.clone());
let stats_tools = StatsTools::new(logs_client_arc.clone(), cluster_client_arc.clone());
let vulnerability_tools = VulnerabilityTools::new(vulnerability_client_arc.clone());
Ok(Self {
agent_tools,
alert_tools,
rule_tools,
stats_tools,
vulnerability_tools,
})
}
#[tool(
name = "get_wazuh_alert_summary",
description = "Retrieves a summary of Wazuh security alerts. Returns formatted alert information including ID, timestamp, and description."
)]
async fn get_wazuh_alert_summary(
&self,
#[tool(aggr)] params: GetAlertSummaryParams,
) -> Result<CallToolResult, McpError> {
self.alert_tools.get_wazuh_alert_summary(params).await
}
#[tool(
name = "get_wazuh_rules_summary",
description = "Retrieves a summary of Wazuh security rules. Returns formatted rule information including ID, level, description, and groups. Supports filtering by level, group, and filename."
)]
async fn get_wazuh_rules_summary(
&self,
#[tool(aggr)] params: GetRulesSummaryParams,
) -> Result<CallToolResult, McpError> {
self.rule_tools.get_wazuh_rules_summary(params).await
}
#[tool(
name = "get_wazuh_vulnerability_summary",
description = "Retrieves a summary of Wazuh vulnerability detections for a specific agent. Returns formatted vulnerability information including CVE ID, severity, detection time, and agent details. Supports filtering by severity level."
)]
async fn get_wazuh_vulnerability_summary(
&self,
#[tool(aggr)] params: GetVulnerabilitiesSummaryParams,
) -> Result<CallToolResult, McpError> {
self.vulnerability_tools
.get_wazuh_vulnerability_summary(params)
.await
}
#[tool(
name = "get_wazuh_critical_vulnerabilities",
description = "Retrieves critical vulnerabilities for a specific Wazuh agent. Returns formatted vulnerability information including CVE ID, title, description, CVSS scores, and detection details. Only shows vulnerabilities with 'Critical' severity level."
)]
async fn get_wazuh_critical_vulnerabilities(
&self,
#[tool(aggr)] params: GetCriticalVulnerabilitiesParams,
) -> Result<CallToolResult, McpError> {
self.vulnerability_tools
.get_wazuh_critical_vulnerabilities(params)
.await
}
#[tool(
name = "get_wazuh_agents",
description = "Retrieves a list of Wazuh agents with their current status and details. Returns formatted agent information including ID, name, IP, status, OS details, and last activity. Supports filtering by status, name, IP, group, OS platform, and version."
)]
async fn get_wazuh_agents(
&self,
#[tool(aggr)] params: GetAgentsParams,
) -> Result<CallToolResult, McpError> {
self.agent_tools.get_wazuh_agents(params).await
}
#[tool(
name = "get_wazuh_agent_processes",
description = "Retrieves a list of running processes for a specific Wazuh agent. Returns formatted process information including PID, name, state, user, and command. Supports filtering by process name/command."
)]
async fn get_wazuh_agent_processes(
&self,
#[tool(aggr)] params: GetAgentProcessesParams,
) -> Result<CallToolResult, McpError> {
self.agent_tools.get_wazuh_agent_processes(params).await
}
#[tool(
name = "get_wazuh_cluster_health",
description = "Checks the health of the Wazuh cluster. Returns whether the cluster is enabled, running, and if nodes are connected."
)]
async fn get_wazuh_cluster_health(
&self,
#[tool(aggr)] params: GetClusterHealthParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_cluster_health(params).await
}
#[tool(
name = "get_wazuh_cluster_nodes",
description = "Retrieves a list of nodes in the Wazuh cluster. Returns formatted node information including name, type, version, IP, and status. Supports filtering by limit, offset, and node type."
)]
async fn get_wazuh_cluster_nodes(
&self,
#[tool(aggr)] params: GetClusterNodesParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_cluster_nodes(params).await
}
#[tool(
name = "search_wazuh_manager_logs",
description = "Searches Wazuh manager logs. Returns formatted log entries including timestamp, tag, level, and description. Supports filtering by limit, offset, level, tag, and a search term."
)]
async fn search_wazuh_manager_logs(
&self,
#[tool(aggr)] params: SearchManagerLogsParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.search_wazuh_manager_logs(params).await
}
#[tool(
name = "get_wazuh_manager_error_logs",
description = "Retrieves Wazuh manager error logs. Returns formatted log entries including timestamp, tag, level (error), and description."
)]
async fn get_wazuh_manager_error_logs(
&self,
#[tool(aggr)] params: GetManagerErrorLogsParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_manager_error_logs(params).await
}
#[tool(
name = "get_wazuh_log_collector_stats",
description = "Retrieves log collector statistics for a specific Wazuh agent. Returns information about events processed, dropped, bytes, and target log files."
)]
async fn get_wazuh_log_collector_stats(
&self,
#[tool(aggr)] params: GetLogCollectorStatsParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_log_collector_stats(params).await
}
#[tool(
name = "get_wazuh_remoted_stats",
description = "Retrieves statistics from the Wazuh remoted daemon. Returns information about queue size, TCP sessions, event counts, and message traffic."
)]
async fn get_wazuh_remoted_stats(
&self,
#[tool(aggr)] params: GetRemotedStatsParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_remoted_stats(params).await
}
#[tool(
name = "get_wazuh_agent_ports",
description = "Retrieves a list of open network ports for a specific Wazuh agent. Returns formatted port information including local/remote IP and port, protocol, state, and associated process/PID. Supports filtering by protocol and state."
)]
async fn get_wazuh_agent_ports(
&self,
#[tool(aggr)] params: GetAgentPortsParams,
) -> Result<CallToolResult, McpError> {
self.agent_tools.get_wazuh_agent_ports(params).await
}
#[tool(
name = "get_wazuh_weekly_stats",
description = "Retrieves weekly statistics from the Wazuh manager. Returns a JSON object detailing various metrics aggregated over the past week."
)]
async fn get_wazuh_weekly_stats(
&self,
#[tool(aggr)] params: GetWeeklyStatsParams,
) -> Result<CallToolResult, McpError> {
self.stats_tools.get_wazuh_weekly_stats(params).await
}
}
#[tool(tool_box)]
impl ServerHandler for WazuhToolsServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2024_11_05,
capabilities: ServerCapabilities::builder()
.enable_prompts()
.enable_resources()
.enable_tools()
.build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"This server provides tools to interact with a Wazuh SIEM instance for security monitoring and analysis.\n\
Available tools:\n\
- 'get_wazuh_alert_summary': Retrieves a summary of Wazuh security alerts. \
Optionally takes 'limit' parameter to control the number of alerts returned (defaults to 100).\n\
- 'get_wazuh_rules_summary': Retrieves a summary of Wazuh security rules. \
Supports filtering by 'level', 'group', and 'filename' parameters, with 'limit' to control the number of rules returned (defaults to 100).\n\
- 'get_wazuh_vulnerability_summary': Retrieves a summary of Wazuh vulnerability detections for a specific agent. \
Requires an 'agent_id' parameter. This must be provided as a string, representing the numeric ID of the agent (e.g., \"0\", \"1\", \"12\", \"001\", \"012\"). The server will automatically format this string into a three-digit, zero-padded identifier. For instance, an input of \"0\" will be treated as \"000\", \"1\" as \"001\", and \"12\" as \"012\". Supports filtering by 'severity' and 'cve' parameters, with 'limit' to control the number of vulnerabilities returned (defaults to 100).\n\
- 'get_wazuh_critical_vulnerabilities': Retrieves only critical vulnerabilities for a specific agent. \
Requires an 'agent_id' parameter. This must be provided as a string, representing the numeric ID of the agent (e.g., \"0\", \"1\", \"12\", \"001\", \"012\"). The server will automatically format this string into a three-digit, zero-padded identifier. For instance, an input of \"0\" will be treated as \"000\", \"1\" as \"001\", and \"12\" as \"012\". Returns detailed information about vulnerabilities with 'Critical' severity level.\n\
- 'get_wazuh_running_agents': Retrieves a list of Wazuh agents with their current status and details. \
Supports filtering by 'status' (active, disconnected, pending, never_connected), 'name', 'ip', 'group', 'os_platform', and 'version' parameters, with 'limit' to control the number of agents returned (defaults to 100, status defaults to 'active').\n\
- 'get_wazuh_agent_processes': Retrieves a list of running processes for a specific Wazuh agent. \
Requires an 'agent_id' parameter (formatted as described for other agent-specific tools). Supports 'limit' (default 100) and 'search' (to filter by process name or command line) parameters.\n\
- 'get_wazuh_agent_ports': Retrieves a list of open network ports for a specific Wazuh agent. \
Requires an 'agent_id' parameter (formatted as described for other agent-specific tools). Supports 'limit' (default 100), 'protocol' (e.g., \"tcp\", \"udp\"), and 'state' (e.g., \"LISTENING\", \"ESTABLISHED\") parameters to filter the results. Note: State filtering is performed client-side by this server.\n\
The 'state' parameter filters results:
- If 'state' is 'LISTENING' (case-insensitive): Only ports explicitly in the 'LISTENING' state are returned. Ports with other states, no state, or an empty state string are filtered out.
- If 'state' is any other value (e.g., 'ESTABLISHED'): Ports that are *not* in the 'LISTENING' state are returned. This includes ports with other defined states (like 'ESTABLISHED', 'TIME_WAIT', etc.) and ports that have *no state* defined. Ports with an empty state string are always filtered out.
Note: State filtering is performed client-side by this server. \
- 'search_wazuh_manager_logs': Searches Wazuh manager logs. \
Optional parameters: 'limit' (default 100), 'offset' (default 0), 'level' (e.g., \"error\", \"info\"), 'tag' (e.g., \"wazuh-modulesd\"), 'search_term' (for free-text search in log descriptions).\n\
- 'get_wazuh_manager_error_logs': Retrieves Wazuh manager error logs. \
Optional parameter: 'limit' (default 100).\n\
- 'get_wazuh_log_collector_stats': Retrieves log collector statistics for a specific Wazuh agent. \
Requires an 'agent_id' parameter (formatted as described for other agent-specific tools). Returns detailed information for 'global' and 'interval' periods, including start/end times, and for each log file: location, events processed, bytes, and target-specific drop counts.\n\
- 'get_wazuh_remoted_stats': Retrieves statistics from the Wazuh remoted daemon (manager-wide).\n\
- 'get_wazuh_weekly_stats': Retrieves weekly statistics from the Wazuh manager. Returns a JSON object detailing various metrics aggregated over the past week. No parameters required.\n\
- 'get_wazuh_cluster_health': Checks the health of the Wazuh cluster. Returns a textual summary of the cluster's health status (e.g., enabled, running, connected nodes). No parameters required.\n\
- 'get_wazuh_cluster_nodes': Retrieves a list of nodes in the Wazuh cluster. \
Optional parameters: 'limit' (max nodes, API default 500), 'offset' (default 0), 'node_type' (e.g., \"master\", \"worker\")."
.to_string(),
),
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::DEBUG.into()),
)
.with_writer(std::io::stderr)
.init();
tracing::info!("Starting Wazuh MCP Server...");
// Create an instance of our Wazuh tools server
let server = WazuhToolsServer::new().expect("Error initializing Wazuh tools server");
tracing::info!("Using stdio transport");
let service = server.serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;
service.waiting().await?;
Ok(())
}