use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ProgressNotification {
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl ProgressNotification {
pub fn new(progress_token: ProgressToken, progress: f64, message: Option<String>) -> Self {
Self {
progress_token,
progress,
total: None,
message,
}
}
pub fn with_total(mut self, total: f64) -> Self {
self.total = Some(total);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProgressToken {
String(String),
Number(i64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
pub enum ClientNotification {
#[serde(rename = "notifications/initialized")]
Initialized,
#[serde(rename = "notifications/roots/list_changed")]
RootsListChanged,
#[serde(rename = "notifications/cancelled")]
Cancelled(CancelledNotification),
#[serde(rename = "notifications/progress")]
Progress(ProgressNotification),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct CancelledNotification {
pub request_id: super::RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl CancelledNotification {
pub fn new(request_id: super::RequestId) -> Self {
Self {
request_id,
reason: None,
}
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
pub enum ServerNotification {
#[serde(rename = "notifications/progress")]
Progress(ProgressNotification),
#[serde(rename = "notifications/tools/list_changed")]
ToolsChanged,
#[serde(rename = "notifications/prompts/list_changed")]
PromptsChanged,
#[serde(rename = "notifications/resources/list_changed")]
ResourcesChanged,
#[serde(rename = "notifications/roots/list_changed")]
RootsListChanged,
#[serde(rename = "notifications/resources/updated")]
ResourceUpdated(ResourceUpdatedParams),
#[serde(rename = "notifications/message")]
LogMessage(LogMessageParams),
#[serde(rename = "notifications/tasks/status")]
TaskStatus(super::tasks::TaskStatusNotification),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct ResourceUpdatedParams {
pub uri: String,
}
impl ResourceUpdatedParams {
pub fn new(uri: impl Into<String>) -> Self {
Self { uri: uri.into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct LogMessageParams {
pub level: LoggingLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub logger: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl LogMessageParams {
pub fn new(level: LoggingLevel, message: impl Into<String>) -> Self {
Self {
level,
logger: None,
message: message.into(),
data: None,
}
}
pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
self.logger = Some(logger.into());
self
}
pub fn with_data(mut self, data: Value) -> Self {
self.data = Some(data);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Notification {
Client(ClientNotification),
Server(ServerNotification),
Progress(ProgressNotification),
Cancelled(CancelledNotification),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LoggingLevel {
Debug,
Info,
Notice,
Warning,
Error,
Critical,
Alert,
Emergency,
}
pub type LogLevel = LoggingLevel;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_all_notification_types() {
let progress = ServerNotification::Progress(ProgressNotification::new(
ProgressToken::String("token123".to_string()),
50.0,
Some("Processing...".to_string()),
));
let json = serde_json::to_value(&progress).unwrap();
assert_eq!(json["method"], "notifications/progress");
let tools_changed = ServerNotification::ToolsChanged;
let json = serde_json::to_value(&tools_changed).unwrap();
assert_eq!(json["method"], "notifications/tools/list_changed");
let prompts_changed = ServerNotification::PromptsChanged;
let json = serde_json::to_value(&prompts_changed).unwrap();
assert_eq!(json["method"], "notifications/prompts/list_changed");
let resources_changed = ServerNotification::ResourcesChanged;
let json = serde_json::to_value(&resources_changed).unwrap();
assert_eq!(json["method"], "notifications/resources/list_changed");
let roots_changed = ServerNotification::RootsListChanged;
let json = serde_json::to_value(&roots_changed).unwrap();
assert_eq!(json["method"], "notifications/roots/list_changed");
let resource_updated =
ServerNotification::ResourceUpdated(ResourceUpdatedParams::new("file://test.txt"));
let json = serde_json::to_value(&resource_updated).unwrap();
assert_eq!(json["method"], "notifications/resources/updated");
let log_msg = ServerNotification::LogMessage(
LogMessageParams::new(LoggingLevel::Info, "Test log message")
.with_data(json!({"extra": "data"})),
);
let json = serde_json::to_value(&log_msg).unwrap();
assert_eq!(json["method"], "notifications/message");
}
#[test]
fn test_logging_level_all_8_values() {
assert_eq!(serde_json::to_value(LoggingLevel::Debug).unwrap(), "debug");
assert_eq!(serde_json::to_value(LoggingLevel::Info).unwrap(), "info");
assert_eq!(
serde_json::to_value(LoggingLevel::Notice).unwrap(),
"notice"
);
assert_eq!(
serde_json::to_value(LoggingLevel::Warning).unwrap(),
"warning"
);
assert_eq!(serde_json::to_value(LoggingLevel::Error).unwrap(), "error");
assert_eq!(
serde_json::to_value(LoggingLevel::Critical).unwrap(),
"critical"
);
assert_eq!(serde_json::to_value(LoggingLevel::Alert).unwrap(), "alert");
assert_eq!(
serde_json::to_value(LoggingLevel::Emergency).unwrap(),
"emergency"
);
}
#[test]
fn test_log_level_alias_works() {
let level: LogLevel = LoggingLevel::Warning;
assert_eq!(serde_json::to_value(level).unwrap(), "warning");
}
#[test]
fn test_cancelled_notification() {
use crate::types::RequestId;
let cancelled =
CancelledNotification::new(RequestId::Number(123)).with_reason("User cancelled");
let json = serde_json::to_value(&cancelled).unwrap();
assert_eq!(json["requestId"], 123);
assert_eq!(json["reason"], "User cancelled");
}
#[test]
fn test_task_status_notification() {
use crate::types::tasks::{Task, TaskStatus as TStatus, TaskStatusNotification};
let notif = ServerNotification::TaskStatus(TaskStatusNotification {
task: Task::new("t-789", TStatus::Completed)
.with_timestamps("2025-11-25T00:00:00Z", "2025-11-25T00:05:00Z")
.with_status_message("Done"),
});
let json = serde_json::to_value(¬if).unwrap();
assert_eq!(json["method"], "notifications/tasks/status");
assert_eq!(json["params"]["task"]["taskId"], "t-789");
assert_eq!(json["params"]["task"]["status"], "completed");
}
}