use agent_client_protocol::schema::AuthMethod;
use agent_client_protocol::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
pub use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
pub use rmcp::model::CreateElicitationRequestParams;
use serde::{Deserialize, Serialize};
pub use mcp_utils::status::{McpServerStatus, McpServerStatusEntry};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonRpcNotification)]
#[notification(method = "_aether/context_usage")]
pub struct ContextUsageParams {
pub usage_ratio: Option<f64>,
pub context_limit: Option<u32>,
pub input_tokens: u32,
#[serde(default)]
pub output_tokens: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>,
#[serde(default)]
pub total_input_tokens: u64,
#[serde(default)]
pub total_output_tokens: u64,
#[serde(default)]
pub total_cache_read_tokens: u64,
#[serde(default)]
pub total_cache_creation_tokens: u64,
#[serde(default)]
pub total_reasoning_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, JsonRpcNotification)]
#[notification(method = "_aether/context_cleared")]
pub struct ContextClearedParams {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonRpcNotification)]
#[notification(method = "_aether/auth_methods_updated")]
pub struct AuthMethodsUpdatedParams {
pub auth_methods: Vec<AuthMethod>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonRpcRequest)]
#[request(method = "_aether/elicitation", response = ElicitationResponse)]
pub struct ElicitationParams {
pub server_name: String,
pub request: CreateElicitationRequestParams,
}
pub use rmcp::model::ElicitationAction;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonRpcResponse)]
pub struct ElicitationResponse {
pub action: ElicitationAction,
pub content: Option<serde_json::Value>,
}
pub use mcp_utils::client::UrlElicitationCompleteParams;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonRpcNotification)]
#[notification(method = "_aether/mcp_event")]
pub enum McpNotification {
ServerStatus { servers: Vec<McpServerStatusEntry> },
UrlElicitationComplete(UrlElicitationCompleteParams),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonRpcNotification)]
#[notification(method = "_aether/mcp_request")]
pub enum McpRequest {
Authenticate { session_id: String, server_name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonRpcNotification)]
#[notification(method = "_aether/sub_agent_progress")]
pub struct SubAgentProgressParams {
pub parent_tool_id: String,
pub task_id: String,
pub agent_name: String,
pub event: SubAgentEvent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SubAgentEvent {
ToolCall { request: SubAgentToolRequest },
ToolCallUpdate { update: SubAgentToolCallUpdate },
ToolResult { result: SubAgentToolResult },
ToolError { error: SubAgentToolError },
Done,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentToolRequest {
pub id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentToolCallUpdate {
pub id: String,
pub chunk: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentToolResult {
pub id: String,
pub name: String,
pub result_meta: Option<ToolResultMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentToolError {
pub id: String,
pub name: String,
}
#[cfg(test)]
mod tests {
use agent_client_protocol::JsonRpcMessage;
use agent_client_protocol::schema::AuthMethodAgent;
use super::*;
#[test]
fn wire_method_names_are_prefixed() {
assert_eq!(ContextClearedParams::default().method(), "_aether/context_cleared");
assert!(AuthMethodsUpdatedParams { auth_methods: vec![] }.method() == "_aether/auth_methods_updated");
assert!(McpNotification::ServerStatus { servers: vec![] }.method() == "_aether/mcp_event");
assert!(
McpRequest::Authenticate { session_id: String::new(), server_name: String::new() }.method()
== "_aether/mcp_request"
);
}
#[test]
fn context_usage_params_roundtrip() {
let params = ContextUsageParams {
usage_ratio: Some(0.75),
context_limit: Some(100_000),
input_tokens: 75_000,
output_tokens: 1_200,
cache_read_tokens: Some(40_000),
cache_creation_tokens: Some(2_000),
reasoning_tokens: Some(500),
total_input_tokens: 200_000,
total_output_tokens: 8_000,
total_cache_read_tokens: 90_000,
total_cache_creation_tokens: 5_000,
total_reasoning_tokens: 1_500,
};
let untyped = params.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/context_usage");
let parsed = ContextUsageParams::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, params);
}
#[test]
fn context_usage_params_omits_unset_optional_token_fields() {
let params = ContextUsageParams {
usage_ratio: Some(0.1),
context_limit: Some(1_000),
input_tokens: 100,
output_tokens: 0,
cache_read_tokens: None,
cache_creation_tokens: None,
reasoning_tokens: None,
total_input_tokens: 0,
total_output_tokens: 0,
total_cache_read_tokens: 0,
total_cache_creation_tokens: 0,
total_reasoning_tokens: 0,
};
let raw = serde_json::to_string(¶ms).unwrap();
assert!(!raw.contains("\"cache_read_tokens\""));
assert!(!raw.contains("\"cache_creation_tokens\""));
assert!(!raw.contains("\"reasoning_tokens\""));
}
#[test]
fn context_cleared_params_roundtrip() {
let params = ContextClearedParams::default();
let untyped = params.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/context_cleared");
let parsed = ContextClearedParams::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, params);
}
#[test]
fn auth_methods_updated_roundtrip() {
let params = AuthMethodsUpdatedParams {
auth_methods: vec![
AuthMethod::Agent(AuthMethodAgent::new("anthropic", "Anthropic").description("authenticated")),
AuthMethod::Agent(AuthMethodAgent::new("openrouter", "OpenRouter")),
],
};
let untyped = params.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/auth_methods_updated");
let parsed = AuthMethodsUpdatedParams::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, params);
}
#[test]
fn mcp_request_authenticate_roundtrip() {
let msg = McpRequest::Authenticate {
session_id: "session-0".to_string(),
server_name: "my oauth server".to_string(),
};
let untyped = msg.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/mcp_request");
let parsed = McpRequest::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, msg);
}
#[test]
fn mcp_notification_server_status_roundtrip() {
let msg = McpNotification::ServerStatus {
servers: vec![
McpServerStatusEntry {
name: "github".to_string(),
status: McpServerStatus::Connected { tool_count: 5 },
},
McpServerStatusEntry { name: "linear".to_string(), status: McpServerStatus::NeedsOAuth },
McpServerStatusEntry {
name: "slack".to_string(),
status: McpServerStatus::Failed { error: "connection timeout".to_string() },
},
],
};
let untyped = msg.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/mcp_event");
let parsed = McpNotification::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, msg);
}
#[test]
fn mcp_notification_url_elicitation_complete_roundtrip() {
let msg = McpNotification::UrlElicitationComplete(UrlElicitationCompleteParams {
server_name: "github".to_string(),
elicitation_id: "el-456".to_string(),
});
let untyped = msg.to_untyped_message().expect("serializable");
let parsed = McpNotification::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, msg);
}
#[test]
fn sub_agent_progress_params_roundtrip() {
let params = SubAgentProgressParams {
parent_tool_id: "call_123".to_string(),
task_id: "task_abc".to_string(),
agent_name: "explorer".to_string(),
event: SubAgentEvent::Done,
};
let untyped = params.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/sub_agent_progress");
}
#[test]
fn elicitation_params_roundtrip() {
use rmcp::model::{ElicitationSchema, EnumSchema};
let params = ElicitationParams {
server_name: "github".to_string(),
request: CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Pick a color".to_string(),
requested_schema: ElicitationSchema::builder()
.required_enum_schema(
"color",
EnumSchema::builder(vec!["red".into(), "green".into(), "blue".into()]).untitled().build(),
)
.build()
.unwrap(),
},
};
let untyped = params.to_untyped_message().expect("serializable");
assert_eq!(untyped.method(), "_aether/elicitation");
let parsed = ElicitationParams::parse_message(untyped.method(), untyped.params()).expect("roundtrip");
assert_eq!(parsed, params);
}
#[test]
fn elicitation_params_url_variant_has_mode_field() {
let params = ElicitationParams {
server_name: "github".to_string(),
request: CreateElicitationRequestParams::UrlElicitationParams {
meta: None,
message: "Authorize GitHub".to_string(),
url: "https://github.com/login/oauth".to_string(),
elicitation_id: "el-123".to_string(),
},
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("\"mode\":\"url\""));
assert!(json.contains("\"server_name\":\"github\""));
}
#[test]
fn mcp_server_status_entry_serde_roundtrip() {
let entry = McpServerStatusEntry {
name: "test-server".to_string(),
status: McpServerStatus::Connected { tool_count: 3 },
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: McpServerStatusEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, entry);
}
#[test]
fn deserialize_tool_call_event() {
let json = r#"{"ToolCall":{"request":{"id":"c1","name":"grep","arguments":"{\"pattern\":\"test\"}"},"model_name":"m"}}"#;
let event: SubAgentEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, SubAgentEvent::ToolCall { .. }));
}
#[test]
fn deserialize_tool_call_update_event() {
let json = r#"{"ToolCallUpdate":{"update":{"id":"c1","chunk":"{\"pattern\":\"test\"}"},"model_name":"m"}}"#;
let event: SubAgentEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, SubAgentEvent::ToolCallUpdate { .. }));
}
#[test]
fn deserialize_tool_result_event() {
let json = r#"{"ToolResult":{"result":{"id":"c1","name":"grep","result_meta":{"display":{"title":"Grep","value":"'test' in src (3 matches)"}}}}}"#;
let event: SubAgentEvent = serde_json::from_str(json).unwrap();
match event {
SubAgentEvent::ToolResult { result } => {
let result_meta = result.result_meta.expect("expected result_meta");
assert_eq!(result_meta.display.title, "Grep");
}
other => panic!("Expected ToolResult, got {other:?}"),
}
}
#[test]
fn deserialize_tool_error_event() {
let json = r#"{"ToolError":{"error":{"id":"c1","name":"grep"}}}"#;
let event: SubAgentEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, SubAgentEvent::ToolError { .. }));
}
#[test]
fn deserialize_done_event() {
let event: SubAgentEvent = serde_json::from_str(r#""Done""#).unwrap();
assert!(matches!(event, SubAgentEvent::Done));
}
#[test]
fn deserialize_other_variant() {
let event: SubAgentEvent = serde_json::from_str(r#""Other""#).unwrap();
assert!(matches!(event, SubAgentEvent::Other));
}
#[test]
fn tool_result_meta_map_roundtrip() {
let meta: ToolResultMeta = ToolDisplayMeta::new("Read file", "Cargo.toml, 156 lines").into();
let map = meta.clone().into_map();
let parsed = ToolResultMeta::from_map(&map).expect("should deserialize ToolResultMeta");
assert_eq!(parsed, meta);
}
}