use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerSpec {
pub server_info: ServerInfo,
pub protocol_version: String,
pub capabilities: ServerCapabilities,
pub tools: Vec<ToolSpec>,
pub resources: Vec<ResourceSpec>,
#[serde(default)]
pub resource_templates: Vec<ResourceTemplateSpec>,
pub prompts: Vec<PromptSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completions: Option<EmptyCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingCapability {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmptyCapability {}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PromptsCapability {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResourcesCapability {
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolsCapability {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSpec {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: ToolInputSchema,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_schema: Option<ToolOutputSchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInputSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutputSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceSpec {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceTemplateSpec {
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptSpec {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Annotations {
#[serde(flatten)]
pub fields: HashMap<String, serde_json::Value>,
}
impl ServerSpec {
#[must_use]
pub fn has_capability(&self, capability: &str) -> bool {
match capability {
"logging" => self.capabilities.logging.is_some(),
"completions" => self.capabilities.completions.is_some(),
"prompts" => self.capabilities.prompts.is_some(),
"resources" => self.capabilities.resources.is_some(),
"tools" => self.capabilities.tools.is_some(),
_ => false,
}
}
#[must_use]
pub fn supports_list_changed(&self, capability: &str) -> bool {
match capability {
"prompts" => self
.capabilities
.prompts
.as_ref()
.and_then(|c| c.list_changed)
.unwrap_or(false),
"resources" => self
.capabilities
.resources
.as_ref()
.and_then(|c| c.list_changed)
.unwrap_or(false),
"tools" => self
.capabilities
.tools
.as_ref()
.and_then(|c| c.list_changed)
.unwrap_or(false),
_ => false,
}
}
#[must_use]
pub fn supports_resource_subscriptions(&self) -> bool {
self.capabilities
.resources
.as_ref()
.and_then(|c| c.subscribe)
.unwrap_or(false)
}
#[must_use]
pub fn summary(&self) -> String {
format!(
"{} v{}: {} tools, {} resources, {} prompts",
self.server_info.name,
self.server_info.version,
self.tools.len(),
self.resources.len(),
self.prompts.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_spec_serialization() {
let spec = ServerSpec {
server_info: ServerInfo {
name: "test-server".to_string(),
version: "1.0.0".to_string(),
title: None,
},
protocol_version: "2025-11-25".to_string(),
capabilities: ServerCapabilities::default(),
tools: vec![],
resources: vec![],
resource_templates: vec![],
prompts: vec![],
instructions: None,
};
let json = serde_json::to_string_pretty(&spec).unwrap();
assert!(json.contains("test-server"));
assert!(json.contains("2025-11-25"));
}
#[test]
fn test_capability_checks() {
let spec = ServerSpec {
server_info: ServerInfo {
name: "test-server".to_string(),
version: "1.0.0".to_string(),
title: None,
},
protocol_version: "2025-11-25".to_string(),
capabilities: ServerCapabilities {
tools: Some(ToolsCapability {
list_changed: Some(true),
}),
..Default::default()
},
tools: vec![],
resources: vec![],
resource_templates: vec![],
prompts: vec![],
instructions: None,
};
assert!(spec.has_capability("tools"));
assert!(!spec.has_capability("prompts"));
assert!(spec.supports_list_changed("tools"));
assert!(!spec.supports_list_changed("prompts"));
}
}